Files
2026-09-22 21:44:35 +02:00

100 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Validate current repository files without changing the Git index."""
from collections import deque
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent.parent
def main():
os.chdir(ROOT)
run_dir = Path(tempfile.mkdtemp(prefix="nixos-validate-", dir="/tmp"))
print(f"Logs: {run_dir}", flush=True)
def run(label, command):
log = run_dir / f"{label}.log"
print(f"{label} ...", flush=True)
with log.open("wb") as output:
result = subprocess.run(command, stdout=output, stderr=subprocess.STDOUT)
if result.returncode:
print(f"{label}: FAILED ({result.returncode}); {log}", flush=True)
with log.open(errors="replace") as output:
excerpt = "".join(deque(output, maxlen=40))
print(excerpt[-12000:])
raise SystemExit(result.returncode)
print(f"{label}: OK", flush=True)
# Git supplies selection only: contents always come from the working tree.
paths = subprocess.check_output(
["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"]
).split(b"\0")
files = sorted({os.fsdecode(path) for path in paths if path})
files = [name for name in files if os.path.lexists(ROOT / name)]
with tempfile.TemporaryDirectory(prefix="nixos-source-", dir="/tmp") as temp:
source = Path(temp) / "source"
def snapshot():
if source.exists():
shutil.rmtree(source)
source.mkdir()
for name in files:
original = ROOT / name
destination = source / name
destination.parent.mkdir(parents=True, exist_ok=True)
if original.is_symlink():
destination.symlink_to(os.readlink(original))
elif original.is_file():
shutil.copy2(original, destination)
else:
raise RuntimeError(f"Unsupported source entry: {name}")
snapshot()
flake = f"path:{source}"
nix = ["nix", "--quiet", "--accept-flake-config"]
locked = ["--no-update-lock-file"]
# The repository currently exposes only x86_64-linux outputs.
nix_files = [str(ROOT / name) for name in files if name.endswith(".nix")]
run("format", nix + ["run", *locked, f"{flake}#formatter.x86_64-linux", "--", *nix_files])
snapshot()
# Upstream devenv evaluates generated Nix (including cabal2nix). Realize
# those evaluation dependencies before flake check enters read-only mode.
run("prepare-devenv", nix + ["eval", *locked, "--raw", f"{flake}#packages.x86_64-linux.devenv.drvPath"])
run("check", nix + ["flake", "check", "--no-build", *locked, flake])
hosts_file = run_dir / "hosts.json"
# Keep machine-readable output separate from Nix diagnostics.
with hosts_file.open("wb") as output, (run_dir / "hosts.log").open("wb") as errors:
result = subprocess.run(
nix + ["eval", *locked, "--json", f"{flake}#nixosConfigurations", "--apply", "builtins.attrNames"],
stdout=output, stderr=errors,
)
if result.returncode:
print(f"Host discovery failed; see {run_dir / 'hosts.log'}")
raise SystemExit(result.returncode)
available = json.loads(hosts_file.read_text())
hosts = list(dict.fromkeys(sys.argv[1:])) or available
unknown = set(hosts) - set(available)
if unknown:
raise RuntimeError(f"Unknown hosts: {', '.join(sorted(unknown))}")
for host in hosts:
run(f"build-{host}", nix + ["build", *locked, "--no-link", f"{flake}#nixosConfigurations.{host}.config.system.build.toplevel"])
if not sys.argv[1:]:
run("build-workbench", nix + ["build", *locked, "--no-link", f"{flake}#devShells.x86_64-linux.workbench"])
print(f"Validation passed: {', '.join(hosts)}. Logs: {run_dir}", flush=True)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit("Validation interrupted; completed stages do not establish a full pass.")
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
sys.exit(str(error))