···66derivation changes). The warnings name the unmaintained packages; the graph
77comes from `nix derivation show -r` on the system derivation, so build
88dependencies are included.
99+1010+Package meta (description, homepage, ...) is collected in the same evaluation
1111+by walking the derivation values the system is built from. Packages only
1212+referenced through strings (e.g. "${pkg}/bin/foo") are not reachable that way;
1313+for those the tool looks up the attribute by name in one extra evaluation.
914"""
10151116import argparse
···1823from collections import defaultdict
1924from pathlib import Path
20252626+# second group is "<file>:<line>", the package's meta.position
2127WARNING = re.compile(
2222- r"evaluation warning: Package '([^']+)' in (/nix/store/[^:\s]+):\d+ "
2828+ r"evaluation warning: Package '([^']+)' in (/nix/store/[^:\s]+:\d+) "
2329 r"has the following problem: maintainerless"
2430)
2531MAX_MACHINES = 31 # the page stores machine membership in 32-bit masks
3232+3333+# Nix function: the cheap, JSON-safe subset of a derivation's meta
3434+META_FN = """
3535+ metaOf = d:
3636+ let
3737+ m = d.meta or { };
3838+ str = v: if builtins.isString v then v else null;
3939+ first = v: if builtins.isList v then (if v == [ ] then null else builtins.head v) else v;
4040+ lic = x: if builtins.isAttrs x then x.spdxId or x.shortName or x.fullName or null else str x;
4141+ licenses = let l = m.license or [ ]; in if builtins.isList l then l else [ l ];
4242+ in
4343+ {
4444+ description = str (m.description or null);
4545+ homepage = str (first (m.homepage or null));
4646+ changelog = str (first (m.changelog or null));
4747+ license = builtins.filter (x: x != null) (map lic licenses);
4848+ vulnerabilities = builtins.filter builtins.isString (m.knownVulnerabilities or [ ]);
4949+ };
5050+"""
26512752EVAL_EXPR = """
2853let
···3459 ];
3560 };
3661 top = if %(kind_str)s == "nixos" then sys.config.system.build.toplevel else sys.system;
6262+ %(meta_fn)s
6363+ isDrv = v: builtins.isAttrs v && (v.type or null) == "derivation";
6464+ # derivation values a derivation was given as attributes: directly, in lists, or one attrset deep.
6565+ # All of these were already forced to compute drvPath, so walking them is nearly free.
6666+ depsOf = d:
6767+ let
6868+ pick = v:
6969+ if isDrv v then [ v ]
7070+ else if builtins.isList v then builtins.concatMap pick v
7171+ else if builtins.isAttrs v then builtins.filter isDrv (builtins.attrValues v)
7272+ else [ ];
7373+ in
7474+ builtins.concatMap pick (builtins.attrValues (d.drvAttrs or { }));
7575+ closure = builtins.genericClosure {
7676+ startSet = [ { key = top.drvPath; d = top; } ];
7777+ operator = n: map (d: { key = d.drvPath; inherit d; }) (depsOf n.d);
7878+ };
7979+ unmaintained = builtins.filter
8080+ (n: (n.d.meta.maintainers or [ ]) == [ ] && (n.d.meta.teams or [ ]) == [ ]) closure;
3781in
3882{
3983 drv = top.drvPath;
4084 # warnings point into the source nixpkgs was evaluated from, which pkgs.path may only be a copy of
4185 nixpkgs = sys.pkgs.hello.meta.position;
4286 rev = sys.config.system.nixos.revision or sys.config.system.nixpkgsRevision or "";
8787+ meta = builtins.listToAttrs (map (n: { name = n.d.name; value = metaOf n.d; }) unmaintained);
4388}
8989+"""
9090+9191+# For packages the walk could not reach: try likely attribute paths, accept one whose name matches.
9292+LOOKUP_EXPR = """
9393+let
9494+ flake = builtins.getFlake %(flake)s;
9595+ pkgs = flake.%(kind)sConfigurations.${%(machine)s}.pkgs;
9696+ lib = pkgs.lib;
9797+ %(meta_fn)s
9898+ wanted = builtins.fromJSON %(wanted)s;
9999+ get = path: let r = builtins.tryEval (lib.attrByPath path null pkgs); in if r.success then r.value else null;
100100+ nameOf = v: let r = builtins.tryEval (v.name or null); in if r.success then r.value else null;
101101+ find = it:
102102+ let
103103+ p = builtins.parseDrvName it.name;
104104+ py = builtins.match "python3[.0-9]*-(.*)" p.name;
105105+ candidates =
106106+ lib.optional (it.byname != null) [ it.byname ]
107107+ ++ [ [ p.name ] [ "haskellPackages" p.name ] ]
108108+ ++ lib.optional (py != null) [ "python3Packages" (builtins.head py) ];
109109+ hits = builtins.filter (c: let v = get c; in lib.isDerivation v && nameOf v == it.name) candidates;
110110+ in
111111+ if hits == [ ] then null
112112+ else metaOf (get (builtins.head hits)) // { attr = lib.concatStringsSep "." (builtins.head hits); };
113113+in
114114+lib.filterAttrs (_: v: v != null) (builtins.listToAttrs (map (it: { name = it.name; value = find it; }) wanted))
44115"""
4511646117···79150 "kind": kind,
80151 "kind_str": nix_str(kind),
81152 "machine": nix_str(machine),
153153+ "meta_fn": META_FN,
82154 }
83155 proc = nix_eval_json(expr)
84156 if proc.returncode != 0:
···87159 result = json.loads(proc.stdout)
88160 result["warned"] = sorted(set(WARNING.findall(proc.stderr)))
89161 return result
162162+163163+164164+def lookup_meta(flake, machine, kind, warned):
165165+ """Meta for the packages the closure walk missed, via attribute lookup on one machine's pkgs."""
166166+ wanted = []
167167+ for name, path in warned.items():
168168+ m = re.search(r"/pkgs/by-name/[^/]+/([^/]+)/package\.nix:\d+$", path)
169169+ wanted.append({"name": name, "byname": m.group(1) if m else None})
170170+ expr = LOOKUP_EXPR % {
171171+ "flake": nix_str(flake),
172172+ "kind": kind,
173173+ "machine": nix_str(machine),
174174+ "meta_fn": META_FN,
175175+ "wanted": nix_str(json.dumps(wanted)),
176176+ }
177177+ proc = nix_eval_json(expr)
178178+ if proc.returncode != 0:
179179+ lines = proc.stderr.strip().splitlines()
180180+ log(f"[meta] lookup failed, continuing without: {lines[-1] if lines else '?'}")
181181+ return {}
182182+ return json.loads(proc.stdout)
901839118492185def derivation_graph(drv):
···124217 return [bin(a).count("1") for a in anc]
125218126219127127-def file_entry(path, sources):
128128- """[path to show, GitHub link or None] for the file a warning points at."""
220220+def file_entry(position, sources):
221221+ """[file:line to show, GitHub link to that line or None] for a warning's meta.position."""
222222+ path, _, line = position.rpartition(":")
129223 for src, rev in sources.items():
130224 if path.startswith(src + "/"):
131225 rel = path[len(src) + 1 :]
132226 link = re.fullmatch(r"[0-9a-f]{7,40}", rev or "")
133133- return [rel, f"https://github.com/NixOS/nixpkgs/blob/{rev}/{rel}" if link else None]
227227+ url = f"https://github.com/NixOS/nixpkgs/blob/{rev}/{rel}#L{line}" if link else None
228228+ return [f"{rel}:{line}", url]
134229 m = re.match(r"/nix/store/[^/]+/(.*)", path)
135135- return [(m.group(1) if m else path) + " (not nixpkgs)", None]
230230+ return [f"{m.group(1) if m else path}:{line} (not nixpkgs)", None]
136231137232138138-def build_data(title, results, graphs):
233233+def build_data(title, results, graphs, meta):
139234 mach = sorted(results)
140235 # "<nixpkgs>/pkgs/by-name/he/hello/package.nix:42" -> "<nixpkgs>"
141236 sources = {r["nixpkgs"].rsplit("/pkgs/", 1)[0]: r["rev"] for r in results.values()}
···186281 "udep": {str(i): [dep[i].get(k, -1) for k in range(len(mach))] for i in unmaintained},
187282 "file": {str(i): file_entry(warned[names[i]], sources) for i in unmaintained},
188283 "parents": [sorted(p) for p in parents],
284284+ "meta": {n: meta[n] for n in {names[i] for i in unmaintained} if n in meta},
189285 }
190286 return data, len({names[i] for i in unmaintained})
191287···234330 if not results:
235331 sys.exit("every evaluation failed")
236332333333+ warned = {name: path for r in results.values() for name, path in r["warned"]}
334334+ meta = {}
335335+ for r in results.values():
336336+ meta.update((n, v) for n, v in r.pop("meta").items() if n in warned)
337337+ missing = {n: p for n, p in warned.items() if n not in meta}
338338+ if missing:
339339+ first = next(m for m, _ in machines if m in results)
340340+ meta.update(lookup_meta(flake, first, dict(machines)[first], missing))
341341+ log(f"[meta] {len(meta)}/{len(warned)} packages with meta")
342342+237343 with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool:
238344 graphs = dict(zip(results, pool.map(lambda m: derivation_graph(results[m]["drv"]), results)))
239345240346 title = args.title or f"Unmaintained packages in {Path(flake).name if os.path.isabs(flake) else flake}"
241241- data, count = build_data(title, results, graphs)
347347+ data, count = build_data(title, results, graphs, meta)
242348243349 template = Path(os.environ["UNMAINTAINED_GRAPH_TEMPLATE"]).read_text()
244350 d3 = Path(os.environ["UNMAINTAINED_GRAPH_D3"]).read_text()