This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.unmaintained-graph: show meta for all packages in the graph

+82 -35
+9 -5
tools/unmaintained-graph/graph.html
··· 287 287 const isInst = i => mi < 0 ? instOn(i) !== 0 : (instOn(i) & bitOf(mi)) !== 0; 288 288 const machinesOf = bits => D.machines.filter((_, k) => bits & bitOf(k)); 289 289 const parentsOf = i => D.parents[i].filter(inMachine); 290 - // D.file[i] = [path shown, link or null]; the generator links nixpkgs files to GitHub when it knows the revision 290 + // [path shown, link or null]: from the warning for unmaintained packages, else from the package's meta.position 291 291 const fileLink = i => { 292 - const f = D.file[i]; if (!f) return ""; 292 + const f = D.file[i] || metaOf(D.names[i])?.file; if (!f) return ""; 293 293 return f[1] ? `<a target="_blank" href="${f[1]}">${f[0]}</a>` : f[0]; 294 + }; 295 + const maintainedBy = n => { 296 + const m = metaOf(n); if (!m) return ""; 297 + return [...(m.maintainers || []), ...(m.teams || []).map(t => `team ${t}`)].map(esc).join(", "); 294 298 }; 295 299 document.title = $("#title").textContent = D.title; 296 300 ··· 305 309 m.homepage && `<div><span class="k">homepage</span> ${urlHtml(m.homepage)}</div>`, 306 310 m.license?.length && `<div><span class="k">license</span> ${esc(m.license.join(", "))}</div>`, 307 311 m.changelog && `<div><span class="k">changelog</span> ${urlHtml(m.changelog)}</div>`, 308 - m.attr && `<div><span class="k">attribute</span> pkgs.${esc(m.attr)}</div>`, 312 + m.attr && `<div><span class="k">attribute</span> pkgs.${esc(m.attr)}${m.otherVersion ? ` <span class="k">(meta taken from ${esc(m.otherVersion)})</span>` : ""}</div>`, 309 313 m.vulnerabilities?.length && `<div class="u">known vulnerabilities:<ul>${m.vulnerabilities.map(v => `<li>${esc(v)}</li>`).join("")}</ul></div>`, 310 314 ].filter(Boolean).join(""); 311 315 } ··· 479 483 const li = j => `<li data-i="${j}" class="${isU[j] ? "u" : ""}">${label(j)} <span class="k">(${depOf(j)})</span></li>`; 480 484 $("#details").innerHTML = ` 481 485 <h2 class="${isU[i] ? "u" : ""}">${label(i)}</h2> 482 - <div>${isU[i] ? '<span class="u">no maintainer</span>' : '<span class="k">not flagged</span>'}</div> 486 + <div>${isU[i] ? '<span class="u">no maintainer</span>' : maintainedBy(D.names[i]) ? `<span class="k">maintainers</span> ${maintainedBy(D.names[i])}` : metaOf(D.names[i]) ? '<span class="k">no maintainer listed (not checked, e.g. fixed-output)</span>' : '<span class="k">no package meta (generated file, source, …)</span>'}</div> 483 487 ${metaHtml(D.names[i])} 484 - ${D.file[i] ? `<div><span class="k">file</span> ${fileLink(i)}</div>` : ""} 488 + ${fileLink(i) ? `<div><span class="k">file</span> ${fileLink(i)}</div>` : ""} 485 489 <div><span class="k">drv</span> ${D.hash[i]}…-${D.names[i]}.drv</div> 486 490 <div><span class="k">dependents</span> ${depOf(i)} ${mi < 0 ? "(max over machines)" : "on " + D.machines[mi]}</div> 487 491 <div><span class="k">distance to system</span> ${(s => s ? `${s.L} step(s)${mi < 0 ? ` (nearest: ${s.roots.map(r => D.machines[D.roots.indexOf(r)]).join(" ")})` : ""}` : "not reachable")(systemDistance(i))}</div>
+73 -30
tools/unmaintained-graph/unmaintained_graph.py
··· 19 19 import os 20 20 import re 21 21 import subprocess 22 + import tempfile 22 23 import sys 23 24 from collections import defaultdict 24 25 from pathlib import Path ··· 46 47 changelog = str (first (m.changelog or null)); 47 48 license = builtins.filter (x: x != null) (map lic licenses); 48 49 vulnerabilities = builtins.filter builtins.isString (m.knownVulnerabilities or [ ]); 50 + maintainers = builtins.filter builtins.isString 51 + (map (x: if builtins.isAttrs x then x.github or x.name or null else null) (m.maintainers or [ ])); 52 + teams = builtins.filter builtins.isString 53 + (map (t: if builtins.isAttrs t then t.shortName or t.name or null else null) (m.teams or [ ])); 54 + position = str (m.position or null); 49 55 }; 50 56 """ 51 57 ··· 76 82 startSet = [ { key = top.drvPath; d = top; } ]; 77 83 operator = n: map (d: { key = d.drvPath; inherit d; }) (depsOf n.d); 78 84 }; 79 - unmaintained = builtins.filter 80 - (n: (n.d.meta.maintainers or [ ]) == [ ] && (n.d.meta.teams or [ ]) == [ ]) closure; 85 + # generated files (units, etc entries, ...) carry no package meta; leave them out 86 + packages = builtins.filter (n: n.d ? meta.description || (n.d.meta.maintainers or [ ]) != [ ]) closure; 81 87 in 82 88 { 83 89 drv = top.drvPath; 84 90 # warnings point into the source nixpkgs was evaluated from, which pkgs.path may only be a copy of 85 91 nixpkgs = sys.pkgs.hello.meta.position; 86 92 rev = sys.config.system.nixos.revision or sys.config.system.nixpkgsRevision or ""; 87 - meta = builtins.listToAttrs (map (n: { name = n.d.name; value = metaOf n.d; }) unmaintained); 93 + meta = builtins.listToAttrs (map (n: { name = n.d.name; value = metaOf n.d; }) packages); 88 94 } 89 95 """ 90 96 91 - # For packages the walk could not reach: try likely attribute paths, accept one whose name matches. 97 + # For packages the walk could not reach: try likely attribute paths. Prefer an attribute with exactly 98 + # this name; otherwise accept the same package in another version (meta is per package, not version). 92 99 LOOKUP_EXPR = """ 93 100 let 94 101 flake = builtins.getFlake %(flake)s; ··· 101 108 find = it: 102 109 let 103 110 p = builtins.parseDrvName it.name; 104 - py = builtins.match "python3[.0-9]*-(.*)" p.name; 111 + py = builtins.match "python3[.]?([0-9]*)-(.*)" p.name; 112 + perl = builtins.match "perl[.0-9]*-(.*)" p.name; 105 113 candidates = 106 114 lib.optional (it.byname != null) [ it.byname ] 107 - ++ [ [ p.name ] [ "haskellPackages" p.name ] ] 108 - ++ lib.optional (py != null) [ "python3Packages" (builtins.head py) ]; 109 - hits = builtins.filter (c: let v = get c; in lib.isDerivation v && nameOf v == it.name) candidates; 115 + ++ [ [ p.name ] [ "${p.name}-unwrapped" ] [ "haskellPackages" p.name ] ] 116 + ++ lib.optionals (py != null) [ 117 + [ "python3${builtins.head py}Packages" (lib.last py) ] 118 + [ "python3Packages" (lib.last py) ] 119 + ] 120 + ++ lib.optional (perl != null) [ "perlPackages" (builtins.replaceStrings [ "-" ] [ "" ] (builtins.head perl)) ]; 121 + found = map (c: { path = c; name = let v = get c; in if lib.isDerivation v then nameOf v else null; }) candidates; 122 + exact = builtins.filter (c: c.name == it.name) found; 123 + samePackage = builtins.filter (c: c.name != null && (builtins.parseDrvName c.name).name == p.name) found; 124 + hit = if exact != [ ] then builtins.head exact else if samePackage != [ ] then builtins.head samePackage else null; 110 125 in 111 - if hits == [ ] then null 112 - else metaOf (get (builtins.head hits)) // { attr = lib.concatStringsSep "." (builtins.head hits); }; 126 + if hit == null then null 127 + else metaOf (get hit.path) // { 128 + attr = lib.concatStringsSep "." hit.path; 129 + otherVersion = if hit.name == it.name then null else hit.name; 130 + }; 113 131 in 114 132 lib.filterAttrs (_: v: v != null) (builtins.listToAttrs (map (it: { name = it.name; value = find it; }) wanted)) 115 133 """ ··· 161 179 return result 162 180 163 181 164 - def lookup_meta(flake, machine, kind, warned): 165 - """Meta for the packages the closure walk missed, via attribute lookup on one machine's pkgs.""" 182 + def lookup_meta(flake, machine, kind, names): 183 + """Meta for packages the closure walk missed, via attribute lookup on one machine's pkgs. 184 + 185 + `names` maps package name -> warning position ("" if none), used to find by-name attributes. 186 + """ 166 187 wanted = [] 167 - for name, path in warned.items(): 168 - m = re.search(r"/pkgs/by-name/[^/]+/([^/]+)/package\.nix:\d+$", path) 188 + for name, position in names.items(): 189 + m = re.search(r"/pkgs/by-name/[^/]+/([^/]+)/package\.nix:\d+$", position) 169 190 wanted.append({"name": name, "byname": m.group(1) if m else None}) 170 - expr = LOOKUP_EXPR % { 171 - "flake": nix_str(flake), 172 - "kind": kind, 173 - "machine": nix_str(machine), 174 - "meta_fn": META_FN, 175 - "wanted": nix_str(json.dumps(wanted)), 176 - } 177 - proc = nix_eval_json(expr) 191 + # tens of thousands of names do not fit on a command line 192 + with tempfile.NamedTemporaryFile("w", suffix=".json") as f: 193 + json.dump(wanted, f) 194 + f.flush() 195 + expr = LOOKUP_EXPR % { 196 + "flake": nix_str(flake), 197 + "kind": kind, 198 + "machine": nix_str(machine), 199 + "meta_fn": META_FN, 200 + "wanted": f"(builtins.readFile {nix_str(f.name)})", 201 + } 202 + proc = nix_eval_json(expr) 178 203 if proc.returncode != 0: 179 204 lines = proc.stderr.strip().splitlines() 180 205 log(f"[meta] lookup failed, continuing without: {lines[-1] if lines else '?'}") ··· 281 306 "udep": {str(i): [dep[i].get(k, -1) for k in range(len(mach))] for i in unmaintained}, 282 307 "file": {str(i): file_entry(warned[names[i]], sources) for i in unmaintained}, 283 308 "parents": [sorted(p) for p in parents], 284 - "meta": {n: meta[n] for n in {names[i] for i in unmaintained} if n in meta}, 309 + "meta": {}, 285 310 } 286 - return data, len({names[i] for i in unmaintained}) 311 + # meta for every package in the graph, with its definition linked like the unmaintained ones 312 + for n in set(names): 313 + m = meta.get(n) 314 + if m is None: 315 + continue 316 + m = dict(m) 317 + position = m.pop("position", None) 318 + if position: 319 + m["file"] = file_entry(position, sources) 320 + data["meta"][n] = m 321 + unmaintained_names = {names[i] for i in unmaintained} 322 + with_meta = sum(1 for n in unmaintained_names if n in data["meta"]) 323 + log( 324 + f"[meta] {with_meta}/{len(unmaintained_names)} unmaintained and " 325 + f"{len(data['meta']) - with_meta} other packages with meta" 326 + ) 327 + return data, len(unmaintained_names) 287 328 288 329 289 330 def main(): ··· 333 374 warned = {name: path for r in results.values() for name, path in r["warned"]} 334 375 meta = {} 335 376 for r in results.values(): 336 - meta.update((n, v) for n, v in r.pop("meta").items() if n in warned) 337 - missing = {n: p for n, p in warned.items() if n not in meta} 338 - if missing: 339 - first = next(m for m, _ in machines if m in results) 340 - meta.update(lookup_meta(flake, first, dict(machines)[first], missing)) 341 - log(f"[meta] {len(meta)}/{len(warned)} packages with meta") 377 + for n, v in r.pop("meta").items(): 378 + meta.setdefault(n, v) 342 379 343 380 with concurrent.futures.ThreadPoolExecutor(args.jobs) as pool: 344 381 graphs = dict(zip(results, pool.map(lambda m: derivation_graph(results[m]["drv"]), results))) 382 + 383 + # everything in the graph the walk did not reach; names that are no attribute just miss cheaply 384 + missing = {n: warned.get(n, "") for g in graphs.values() for n, _ in g.values() if n not in meta} 385 + if missing: 386 + first = next(m for m, _ in machines if m in results) 387 + meta.update(lookup_meta(flake, first, dict(machines)[first], missing)) 345 388 346 389 title = args.title or f"Unmaintained packages in {Path(flake).name if os.path.isabs(flake) else flake}" 347 390 data, count = build_data(title, results, graphs, meta)