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>
This commit is contained in:
74
tools/vm/gap-analysis.py
Executable file
74
tools/vm/gap-analysis.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user