From 479193b10b6646193070c7afe19bcfb72f41160d Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Wed, 10 Jun 2026 16:48:33 +0100 Subject: [PATCH] fix(install): survive the live ISO offline and its non-interactive bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by running the unattended install in an offline QEMU VM: - compgen is missing from nixpkgs' non-interactive bash (the package shebang) — replaced with plain glob tests in hardware-db and prewipe - disko's eval resolves via NIX_PATH (a dead channel on the ISO) and tried channels.nixos.org — export NIX_PATH to the pinned nixpkgs source and point the flake registry at an empty baked file - lock nomarchy by path (the source the ISO carries) instead of git+https: git inputs clone even when narHash is known, which kills offline installs; `original` keeps the forge URL so a later `nix flake update` re-resolves normally. Works from dirty-tree ISOs too. Co-Authored-By: Claude Fable 5 --- flake.nix | 11 +++++--- pkgs/nomarchy-install/compose-lock.py | 7 ++--- pkgs/nomarchy-install/default.nix | 12 ++++++--- pkgs/nomarchy-install/hardware-db.sh | 6 +++-- pkgs/nomarchy-install/nomarchy-install.sh | 31 +++++++++++++---------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/flake.nix b/flake.nix index 9afec61..c8350d1 100644 --- a/flake.nix +++ b/flake.nix @@ -44,16 +44,19 @@ hardwareModuleNames = pkgs.writeText "nixos-hardware-modules.txt" (nixpkgs.lib.concatStringsSep "\n" (builtins.attrNames nixos-hardware.nixosModules)); + nixpkgsPath = nixpkgs.outPath; flakeUrl = "git+${gitUrl}"; + # Future updates re-resolve from the forge (original); the install + # itself resolves from the ISO store (locked = path) — fully + # offline, even from a dirty-tree ISO. originalNode = { type = "git"; url = gitUrl; }; lockedNode = { - type = "git"; - url = gitUrl; - rev = self.rev or null; # null on a dirty tree → installer needs network + type = "path"; + path = self.outPath; narHash = self.narHash; lastModified = self.lastModified or 0; - revCount = self.revCount or 0; }; + rev = self.rev or null; }; in { diff --git a/pkgs/nomarchy-install/compose-lock.py b/pkgs/nomarchy-install/compose-lock.py index 71f6199..a97f6b5 100644 --- a/pkgs/nomarchy-install/compose-lock.py +++ b/pkgs/nomarchy-install/compose-lock.py @@ -27,8 +27,8 @@ def main() -> None: src_lock, out, locked_json, original_json = sys.argv[1:5] locked = json.loads(locked_json) - if not locked.get("rev"): - sys.exit("compose-lock: no rev in locked metadata (ISO built from a dirty tree?)") + if not locked.get("narHash"): + sys.exit("compose-lock: no narHash in locked metadata") with open(src_lock) as f: upstream = json.load(f) @@ -50,7 +50,8 @@ def main() -> None: with open(out, "w") as f: json.dump({"nodes": nodes, "root": "root", "version": 7}, f, indent=2) f.write("\n") - print(f"compose-lock: wrote {out} (nomarchy @ {locked['rev'][:12]})") + pin = locked.get("rev", locked["narHash"])[:12] + print(f"compose-lock: wrote {out} (nomarchy @ {pin})") if __name__ == "__main__": diff --git a/pkgs/nomarchy-install/default.nix b/pkgs/nomarchy-install/default.nix index 876c95c..7ad21f5 100644 --- a/pkgs/nomarchy-install/default.nix +++ b/pkgs/nomarchy-install/default.nix @@ -18,10 +18,13 @@ , templateDir # templates/downstream (home.nix, theme-state.json) , nomarchyLock # the distro's flake.lock (for offline lock composition) , hardwareModuleNames # newline-separated nixos-hardware module names +, nixpkgsPath # pinned nixpkgs source (NIX_PATH for disko's eval) , flakeUrl # flake-style URL written into the generated flake.nix , lockedNode # attrset: the flake.lock "locked" node for nomarchy - # (rev = null when the ISO was built from a dirty tree) -, originalNode # attrset: the flake.lock "original" node + # (path-type: resolves from the ISO store, offline) +, originalNode # attrset: the flake.lock "original" node (the forge, + # so `nix flake update` later re-resolves normally) +, rev ? null # informational: rev the ISO was built from }: stdenvNoCC.mkDerivation { @@ -44,6 +47,8 @@ stdenvNoCC.mkDerivation { install -Dm644 compose-lock.py "$share/compose-lock.py" install -Dm644 ${nomarchyLock} "$share/flake.lock" install -Dm644 ${hardwareModuleNames} "$share/hardware-modules.txt" + # Empty flake registry: no network lookups for indirect refs. + echo '{"flakes":[],"version":2}' > "$share/registry.json" mkdir -p "$share/template" cp ${templateDir}/home.nix ${templateDir}/theme-state.json "$share/template/" @@ -55,8 +60,9 @@ stdenvNoCC.mkDerivation { util-linux gptfdisk parted cryptsetup lvm2 pciutils btrfs-progs ]} \ --set NOMARCHY_INSTALL_SHARE "$share" \ + --set NOMARCHY_NIXPKGS ${nixpkgsPath} \ --set NOMARCHY_FLAKE_URL ${lib.escapeShellArg flakeUrl} \ - --set NOMARCHY_REV ${lib.escapeShellArg (toString (lockedNode.rev or ""))} \ + --set NOMARCHY_REV ${lib.escapeShellArg (toString rev)} \ --set NOMARCHY_LOCKED_JSON ${lib.escapeShellArg (builtins.toJSON lockedNode)} \ --set NOMARCHY_ORIGINAL_JSON ${lib.escapeShellArg (builtins.toJSON originalNode)} diff --git a/pkgs/nomarchy-install/hardware-db.sh b/pkgs/nomarchy-install/hardware-db.sh index d34d01f..acc89c4 100644 --- a/pkgs/nomarchy-install/hardware-db.sh +++ b/pkgs/nomarchy-install/hardware-db.sh @@ -140,8 +140,10 @@ nomarchy_detect_hw() { (( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; } fi - # Chassis - if compgen -G "/sys/class/power_supply/BAT*" >/dev/null; then + # Chassis (glob test, not compgen — nixpkgs' non-interactive bash + # is built without the completion builtins) + local bats=(/sys/class/power_supply/BAT*) + if [[ -e "${bats[0]}" ]]; then echo "MODULE common-pc-laptop" echo "DETAIL chassis: laptop (battery present)" else diff --git a/pkgs/nomarchy-install/nomarchy-install.sh b/pkgs/nomarchy-install/nomarchy-install.sh index 98ccde7..58da6f2 100644 --- a/pkgs/nomarchy-install/nomarchy-install.sh +++ b/pkgs/nomarchy-install/nomarchy-install.sh @@ -62,6 +62,12 @@ command -v nixos-install >/dev/null \ grep -q nomarchy-live /etc/hostname 2>/dev/null \ || warn "This doesn't look like the Nomarchy live ISO; proceeding anyway." +# Offline-proof nix: disko's eval resolves (a dead channel on +# the live ISO) and flake commands may consult the global registry — +# point both at what the ISO already carries. +export NIX_PATH="nixpkgs=$NOMARCHY_NIXPKGS" +export NIX_CONFIG="flake-registry = $SHARE/registry.json" + # ─── Disk selection ───────────────────────────────────────────────────── section "Target disk" @@ -201,7 +207,7 @@ gum style --border normal --padding "0 2" \ "Timezone: $TIMEZONE" \ "Hardware: ${HW_PROFILES[*]:-none}" \ "Snapshots: snapper timeline on /" \ - "Offline: $([[ -n "${NOMARCHY_REV:-}" ]] && echo "yes (pinned ${NOMARCHY_REV:0:12})" || echo "no (will fetch nomarchy over the network)")" + "Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— pinned into the ISO, no network needed" if [[ "$UNATTENDED" != "1" ]]; then typed=$(gum input --placeholder "type the disk name ($(basename "$TARGET_DISK")) to confirm the wipe") @@ -230,11 +236,11 @@ prewipe() { cryptsetup close "$name" done < <(dmsetup ls --target crypt 2>/dev/null) fi - if compgen -G "${drive}?*" >/dev/null; then - for part in "${drive}"?*; do - [[ -b "$part" ]] && wipefs -af "$part" >/dev/null - done - fi + for part in "${drive}"?*; do + # unmatched glob stays literal; -b filters it out + [[ -b "$part" ]] || continue + wipefs -af "$part" >/dev/null + done wipefs -af "$drive" >/dev/null sgdisk --zap-all "$drive" >/dev/null # 16 MiB covers LUKS2 headers and the first BTRFS superblock — @@ -363,13 +369,12 @@ $AUTOLOGIN_CONFIG$RESUME_CONFIG } EOF -# The flake.lock: offline when the ISO knows what it was built from, -# otherwise resolve over the network. -if [[ -n "${NOMARCHY_REV:-}" ]]; then - python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \ - "$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON" -else - warn "ISO built from a dirty tree — resolving nomarchy from its forge (needs network)." +# The flake.lock: composed offline — nomarchy is path-locked to the very +# source the ISO carries (original stays the forge URL, so a later +# `nix flake update` on the installed machine re-resolves normally). +if ! python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \ + "$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"; then + warn "Offline lock composition failed — resolving over the network." (cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock) fi