#!/usr/bin/env python3 """Which store paths would an offline nixos-install need that the ISO lacks? Walk the target drv graph: an output that's PRESENT needs nothing; a missing output forces building its drv, which needs all its input sources present and all referenced input outputs (recursively). This is the maintainer tool that converged the offline pin set in flake.nix (extraDependencies): when an offline install builds from source, run this instead of bisecting the VM. Healthy output is a few dozen per-machine config drvs (etc, initrd-*, unit-*, …) and zero [FETCH/NETWORK] leaves; any real package name here needs pinning. Usage: # 1. the present set = everything the ISO carries: live=$(nix build --no-link --print-out-paths \ .#nixosConfigurations.nomarchy-live.config.system.build.toplevel) nix path-info -r "$live" | sort -u > /tmp/present.txt # 2. instantiate an install-shaped toplevel for a FOREIGN # username/hostname/uuids (see docs/TESTING.md) and: gap-analysis.py /tmp/present.txt """ import json import subprocess import sys target_drv, present_file = sys.argv[1], sys.argv[2] present = set(open(present_file).read().split()) graph = json.loads(subprocess.check_output( ["nix", "derivation", "show", "-r", target_drv + "^*"])) must_build = set() missing_srcs = set() visited = set() def outputs_of(drv_name): return [o["path"] for o in graph[drv_name]["outputs"].values()] def need_drv(drv_name): if drv_name in visited: return visited.add(drv_name) must_build.add(drv_name) node = graph[drv_name] for src in node.get("inputSrcs", []): if src not in present: missing_srcs.add(src) for dep_drv, want in node.get("inputDrvs", {}).items(): dep_outputs = graph.get(dep_drv) if dep_outputs is None: continue wanted = want.get("outputs", []) if isinstance(want, dict) else want for out_name in wanted: path = graph[dep_drv]["outputs"][out_name]["path"] if path not in present: need_drv(dep_drv) break # Roots: the target's own outputs are by definition "to build". need_drv(target_drv) print(f"drvs that must be built offline: {len(must_build)}") for d in sorted(must_build): name = d.split("-", 1)[1] if "-" in d else d builder = graph[d].get("builder", "") fetch = " [FETCH/NETWORK]" if graph[d].get("env", {}).get("urls") or graph[d].get("env", {}).get("url") else "" print(f" {name}{fetch}") print(f"missing input sources: {len(missing_srcs)}") for s in sorted(missing_srcs): print(f" SRC {s}")