···1919import os
2020import re
2121import subprocess
2222+import tempfile
2223import sys
2324from collections import defaultdict
2425from pathlib import Path
···4647 changelog = str (first (m.changelog or null));
4748 license = builtins.filter (x: x != null) (map lic licenses);
4849 vulnerabilities = builtins.filter builtins.isString (m.knownVulnerabilities or [ ]);
5050+ maintainers = builtins.filter builtins.isString
5151+ (map (x: if builtins.isAttrs x then x.github or x.name or null else null) (m.maintainers or [ ]));
5252+ teams = builtins.filter builtins.isString
5353+ (map (t: if builtins.isAttrs t then t.shortName or t.name or null else null) (m.teams or [ ]));
5454+ position = str (m.position or null);
4955 };
5056"""
5157···7682 startSet = [ { key = top.drvPath; d = top; } ];
7783 operator = n: map (d: { key = d.drvPath; inherit d; }) (depsOf n.d);
7884 };
7979- unmaintained = builtins.filter
8080- (n: (n.d.meta.maintainers or [ ]) == [ ] && (n.d.meta.teams or [ ]) == [ ]) closure;
8585+ # generated files (units, etc entries, ...) carry no package meta; leave them out
8686+ packages = builtins.filter (n: n.d ? meta.description || (n.d.meta.maintainers or [ ]) != [ ]) closure;
8187in
8288{
8389 drv = top.drvPath;
8490 # warnings point into the source nixpkgs was evaluated from, which pkgs.path may only be a copy of
8591 nixpkgs = sys.pkgs.hello.meta.position;
8692 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);
9393+ meta = builtins.listToAttrs (map (n: { name = n.d.name; value = metaOf n.d; }) packages);
8894}
8995"""
90969191-# For packages the walk could not reach: try likely attribute paths, accept one whose name matches.
9797+# For packages the walk could not reach: try likely attribute paths. Prefer an attribute with exactly
9898+# this name; otherwise accept the same package in another version (meta is per package, not version).
9299LOOKUP_EXPR = """
93100let
94101 flake = builtins.getFlake %(flake)s;
···101108 find = it:
102109 let
103110 p = builtins.parseDrvName it.name;
104104- py = builtins.match "python3[.0-9]*-(.*)" p.name;
111111+ py = builtins.match "python3[.]?([0-9]*)-(.*)" p.name;
112112+ perl = builtins.match "perl[.0-9]*-(.*)" p.name;
105113 candidates =
106114 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;
115115+ ++ [ [ p.name ] [ "${p.name}-unwrapped" ] [ "haskellPackages" p.name ] ]
116116+ ++ lib.optionals (py != null) [
117117+ [ "python3${builtins.head py}Packages" (lib.last py) ]
118118+ [ "python3Packages" (lib.last py) ]
119119+ ]
120120+ ++ lib.optional (perl != null) [ "perlPackages" (builtins.replaceStrings [ "-" ] [ "" ] (builtins.head perl)) ];
121121+ found = map (c: { path = c; name = let v = get c; in if lib.isDerivation v then nameOf v else null; }) candidates;
122122+ exact = builtins.filter (c: c.name == it.name) found;
123123+ samePackage = builtins.filter (c: c.name != null && (builtins.parseDrvName c.name).name == p.name) found;
124124+ hit = if exact != [ ] then builtins.head exact else if samePackage != [ ] then builtins.head samePackage else null;
110125 in
111111- if hits == [ ] then null
112112- else metaOf (get (builtins.head hits)) // { attr = lib.concatStringsSep "." (builtins.head hits); };
126126+ if hit == null then null
127127+ else metaOf (get hit.path) // {
128128+ attr = lib.concatStringsSep "." hit.path;
129129+ otherVersion = if hit.name == it.name then null else hit.name;
130130+ };
113131in
114132lib.filterAttrs (_: v: v != null) (builtins.listToAttrs (map (it: { name = it.name; value = find it; }) wanted))
115133"""
···161179 return result
162180163181164164-def lookup_meta(flake, machine, kind, warned):
165165- """Meta for the packages the closure walk missed, via attribute lookup on one machine's pkgs."""
182182+def lookup_meta(flake, machine, kind, names):
183183+ """Meta for packages the closure walk missed, via attribute lookup on one machine's pkgs.
184184+185185+ `names` maps package name -> warning position ("" if none), used to find by-name attributes.
186186+ """
166187 wanted = []
167167- for name, path in warned.items():
168168- m = re.search(r"/pkgs/by-name/[^/]+/([^/]+)/package\.nix:\d+$", path)
188188+ for name, position in names.items():
189189+ m = re.search(r"/pkgs/by-name/[^/]+/([^/]+)/package\.nix:\d+$", position)
169190 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)
191191+ # tens of thousands of names do not fit on a command line
192192+ with tempfile.NamedTemporaryFile("w", suffix=".json") as f:
193193+ json.dump(wanted, f)
194194+ f.flush()
195195+ expr = LOOKUP_EXPR % {
196196+ "flake": nix_str(flake),
197197+ "kind": kind,
198198+ "machine": nix_str(machine),
199199+ "meta_fn": META_FN,
200200+ "wanted": f"(builtins.readFile {nix_str(f.name)})",
201201+ }
202202+ proc = nix_eval_json(expr)
178203 if proc.returncode != 0:
179204 lines = proc.stderr.strip().splitlines()
180205 log(f"[meta] lookup failed, continuing without: {lines[-1] if lines else '?'}")
···281306 "udep": {str(i): [dep[i].get(k, -1) for k in range(len(mach))] for i in unmaintained},
282307 "file": {str(i): file_entry(warned[names[i]], sources) for i in unmaintained},
283308 "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},
309309+ "meta": {},
285310 }
286286- return data, len({names[i] for i in unmaintained})
311311+ # meta for every package in the graph, with its definition linked like the unmaintained ones
312312+ for n in set(names):
313313+ m = meta.get(n)
314314+ if m is None:
315315+ continue
316316+ m = dict(m)
317317+ position = m.pop("position", None)
318318+ if position:
319319+ m["file"] = file_entry(position, sources)
320320+ data["meta"][n] = m
321321+ unmaintained_names = {names[i] for i in unmaintained}
322322+ with_meta = sum(1 for n in unmaintained_names if n in data["meta"])
323323+ log(
324324+ f"[meta] {with_meta}/{len(unmaintained_names)} unmaintained and "
325325+ f"{len(data['meta']) - with_meta} other packages with meta"
326326+ )
327327+ return data, len(unmaintained_names)
287328288329289330def main():
···333374 warned = {name: path for r in results.values() for name, path in r["warned"]}
334375 meta = {}
335376 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")
377377+ for n, v in r.pop("meta").items():
378378+ meta.setdefault(n, v)
342379343380 with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool:
344381 graphs = dict(zip(results, pool.map(lambda m: derivation_graph(results[m]["drv"]), results)))
382382+383383+ # everything in the graph the walk did not reach; names that are no attribute just miss cheaply
384384+ missing = {n: warned.get(n, "") for g in graphs.values() for n, _ in g.values() if n not in meta}
385385+ if missing:
386386+ first = next(m for m, _ in machines if m in results)
387387+ meta.update(lookup_meta(flake, first, dict(machines)[first], missing))
345388346389 title = args.title or f"Unmaintained packages in {Path(flake).name if os.path.isabs(flake) else flake}"
347390 data, count = build_data(title, results, graphs, meta)