Files
Nomarchy/tools/vm/gap-analysis.py
Bernardo Magri a50e9793ea feat(tools): commit the VM test harness + one-command installer regression
Promotes the throwaway harness that verified the installer into tools/:
- tools/vm/qmp.py        QMP keystroke injection / typing / quit
- tools/vm/vncshot.py    GL-safe screenshots via VNC readback (QMP
                         screendump shows "no surface" with virtio-vga-gl)
- tools/vm/gap-analysis.py  drv-graph diff that converged the offline
                         pin set; run it when offline installs build
                         from source
- tools/test-install.sh  the full offline regression: build ISO, boot
                         offline, unattended LUKS+swap install via a
                         config disk (typed long commands drop keys),
                         wait for poweroff, boot the installed disk,
                         screenshot the first boot for visual verdict

docs/TESTING.md §4 now points at the script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:05:13 +01:00

75 lines
2.6 KiB
Python
Executable File

#!/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 <target.drv> /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}")