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>
75 lines
2.6 KiB
Python
Executable File
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}")
|