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