This repository has no description
21 kB
497 lines
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8" />
5 <title>Unmaintained packages</title>
6 <style>
7 :root {
8 --bg: #0d1117;
9 --panel: #161b22;
10 --fg: #c9d1d9;
11 --dim: #8b949e;
12 --line: #30363d;
13 --red: #f85149;
14 --blue: #58a6ff;
15 --green: #3fb950;
16 --yellow: #d29922;
17 }
18 * {
19 box-sizing: border-box;
20 }
21 body {
22 margin: 0;
23 font:
24 13px/1.4 ui-monospace,
25 SFMono-Regular,
26 Menlo,
27 monospace;
28 background: var(--bg);
29 color: var(--fg);
30 display: grid;
31 grid-template-columns: 340px 1fr 380px;
32 grid-template-rows: auto 1fr;
33 height: 100vh;
34 }
35 header {
36 grid-column: 1 / -1;
37 display: flex;
38 gap: 12px;
39 align-items: center;
40 padding: 8px 12px;
41 background: var(--panel);
42 border-bottom: 1px solid var(--line);
43 }
44 header h1 {
45 font-size: 14px;
46 margin: 0 12px 0 0;
47 }
48 input,
49 select,
50 button {
51 font: inherit;
52 background: var(--bg);
53 color: var(--fg);
54 border: 1px solid var(--line);
55 border-radius: 4px;
56 padding: 3px 6px;
57 }
58 button {
59 cursor: pointer;
60 }
61 button:hover {
62 border-color: var(--blue);
63 }
64 #list {
65 overflow: auto;
66 border-right: 1px solid var(--line);
67 }
68 #list .row {
69 display: flex;
70 justify-content: space-between;
71 gap: 8px;
72 padding: 3px 10px;
73 cursor: pointer;
74 white-space: nowrap;
75 }
76 #list .row:hover,
77 #list .row.sel {
78 background: #1f2630;
79 }
80 #list .row .n {
81 overflow: hidden;
82 text-overflow: ellipsis;
83 }
84 #list .row .c {
85 color: var(--dim);
86 }
87 #list .meta {
88 padding: 6px 10px;
89 color: var(--dim);
90 border-bottom: 1px solid var(--line);
91 }
92 .inst {
93 color: var(--yellow);
94 }
95 #graph {
96 position: relative;
97 overflow: hidden;
98 }
99 #graph svg {
100 width: 100%;
101 height: 100%;
102 display: block;
103 }
104 #toolbar {
105 position: absolute;
106 top: 8px;
107 left: 8px;
108 display: flex;
109 gap: 6px;
110 flex-wrap: wrap;
111 }
112 #legend {
113 position: absolute;
114 bottom: 8px;
115 left: 8px;
116 color: var(--dim);
117 background: #161b22cc;
118 padding: 6px 8px;
119 border-radius: 4px;
120 }
121 #legend span {
122 margin-right: 10px;
123 }
124 .dot {
125 display: inline-block;
126 width: 9px;
127 height: 9px;
128 border-radius: 50%;
129 margin-right: 4px;
130 vertical-align: middle;
131 }
132 #details {
133 overflow: auto;
134 border-left: 1px solid var(--line);
135 padding: 10px 12px;
136 }
137 #details h2 {
138 font-size: 14px;
139 margin: 0 0 6px;
140 word-break: break-all;
141 }
142 #details .k {
143 color: var(--dim);
144 }
145 #details ul {
146 margin: 4px 0 10px;
147 padding-left: 16px;
148 }
149 #details li {
150 cursor: pointer;
151 word-break: break-all;
152 }
153 #details li:hover {
154 color: var(--blue);
155 }
156 a {
157 color: var(--blue);
158 }
159 .u {
160 color: var(--red);
161 }
162 text {
163 fill: var(--fg);
164 font-size: 10px;
165 pointer-events: none;
166 paint-order: stroke;
167 stroke: var(--bg);
168 stroke-width: 3px;
169 }
170 .link {
171 stroke: #3d444d;
172 stroke-width: 1.2;
173 }
174 .link.via {
175 stroke: var(--red);
176 stroke-dasharray: 4 3;
177 opacity: 0.7;
178 }
179 .node circle {
180 stroke: var(--bg);
181 stroke-width: 1.5;
182 cursor: pointer;
183 }
184 .node.focus circle {
185 stroke: #fff;
186 stroke-width: 2.5;
187 }
188 .node.inst circle {
189 stroke: var(--yellow);
190 stroke-width: 2.5;
191 }
192 </style>
193 </head>
194 <body>
195 <header>
196 <h1 id="title">Unmaintained packages</h1>
197 <label
198 >machine
199 <select id="machine"></select
200 ></label>
201 <input id="search" placeholder="filter list…" size="24" />
202 <label
203 ><input type="checkbox" id="onlyInst" /> directly installed only</label
204 >
205 <span id="summary" class="k"></span>
206 </header>
207 <div id="list"></div>
208 <div id="graph">
209 <div id="toolbar">
210 <button
211 id="bExpand"
212 title="add packages that use the selected node (or double-click a node)"
213 >
214 expand users
215 </button>
216 <button
217 id="bPath"
218 title="all shortest chains from the system toplevel down to the selected node"
219 >
220 chains to system
221 </button>
222 <button
223 id="bUnm"
224 title="nearest unmaintained packages that (transitively) use the selected node; adds the 30 biggest, the side panel lists all"
225 >
226 unmaintained users
227 </button>
228 <button id="bReset">reset</button>
229 </div>
230 <div id="legend">
231 <span
232 ><i class="dot" style="background: var(--red)"></i>unmaintained</span
233 >
234 <span><i class="dot" style="background: var(--dim)"></i>other</span>
235 <span
236 ><i class="dot" style="background: var(--green)"></i>system-path</span
237 >
238 <span
239 ><i class="dot" style="background: var(--blue)"></i>system
240 toplevel</span
241 >
242 <span
243 ><i class="dot" style="border: 2px solid var(--yellow)"></i>directly
244 installed</span
245 >
246 <span
247 >arrow: user → used · dashed: via maintained packages · node size:
248 #dependents</span
249 >
250 </div>
251 <svg></svg>
252 </div>
253 <div id="details">
254 <p class="k">
255 Pick a package on the left. The graph shows every shortest chain through
256 which the system (blue, top) pulls it in. Click nodes to inspect,
257 double-click to expand who uses them.
258 </p>
259 </div>
260
261 <script>
262 /*D3*/
263 </script>
264 <script>
265 const D = /*DATA*/;
266 const N = D.names.length, M = D.machines.length;
267 const isU = new Uint8Array(N); for (const k in D.udep) isU[+k] = 1;
268 const rootSet = new Set(D.roots);
269 const children = Array.from({ length: N }, () => []);
270 D.parents.forEach((ps, c) => { for (const p of ps) children[p].push(c); });
271 const direct = new Map(Object.entries(D.direct).map(([k, v]) => [+k, v]));
272 const nameCount = new Map(); for (const n of D.names) nameCount.set(n, (nameCount.get(n) || 0) + 1);
273 const label = i => nameCount.get(D.names[i]) > 1 ? `${D.names[i]} (${D.hash[i]})` : D.names[i];
274
275 const $ = s => document.querySelector(s);
276 const msel = $("#machine");
277 msel.innerHTML = `<option value="-1">all (${M})</option>` + D.machines.map((m, i) => `<option value="${i}">${m}</option>`).join("");
278 let mi = -1;
279 const bitOf = i => 1 << i;
280 const inMachine = i => mi < 0 || (D.mask[i] & bitOf(mi));
281 const depOf = i => mi < 0 ? D.dep[i] : (isU[i] ? D.udep[i][mi] : D.dep[i]);
282 const instOn = i => direct.get(i) || 0;
283 const isInst = i => mi < 0 ? instOn(i) !== 0 : (instOn(i) & bitOf(mi)) !== 0;
284 const machinesOf = bits => D.machines.filter((_, k) => bits & bitOf(k));
285 const parentsOf = i => D.parents[i].filter(inMachine);
286 // D.file[i] = [path shown, link or null]; the generator links nixpkgs files to GitHub when it knows the revision
287 const fileLink = i => {
288 const f = D.file[i]; if (!f) return "";
289 return f[1] ? `<a target="_blank" href="${f[1]}">${f[0]}</a>` : f[0];
290 };
291 document.title = $("#title").textContent = D.title;
292
293 // ---------- list ----------
294 let selected = -1;
295 $("#list").addEventListener("click", e => { const r = e.target.closest(".row"); if (r) focusNode(+r.dataset.i); });
296 $("#search").addEventListener("input", renderList);
297 $("#onlyInst").addEventListener("change", renderList);
298 msel.addEventListener("change", () => { mi = +msel.value; renderList(); if (selected >= 0) focusNode(selected); });
299
300 // one list row per package name; variants = all drvs with that name (different deps/stages)
301 const variants = new Map();
302 for (const k in D.udep) { const i = +k, n = D.names[i]; if (!variants.has(n)) variants.set(n, []); variants.get(n).push(i); }
303 function renderList() {
304 const q = $("#search").value.toLowerCase(), oi = $("#onlyInst").checked;
305 const rows = [];
306 for (const [n, vs] of variants) {
307 if (q && !n.toLowerCase().includes(q)) continue;
308 const v = vs.filter(inMachine); if (!v.length) continue;
309 if (oi && !v.some(isInst)) continue;
310 v.sort((a, b) => depOf(b) - depOf(a));
311 rows.push({ n, best: v[0], count: v.length, dep: depOf(v[0]), inst: v.some(isInst) });
312 }
313 rows.sort((a, b) => b.dep - a.dep);
314 $("#summary").textContent = `${rows.length} unmaintained packages` + (mi < 0 ? " across all machines" : ` on ${D.machines[mi]}`);
315 const shown = rows.slice(0, 800);
316 const sel = selected >= 0 ? D.names[selected] : null;
317 $("#list").innerHTML = `<div class="meta">sorted by #dependents (best variant)${rows.length > shown.length ? ` · showing ${shown.length} of ${rows.length}, filter to narrow` : ""}</div>` +
318 shown.map(r => `<div class="row${r.n === sel ? " sel" : ""}" data-i="${r.best}"><span class="n">${r.inst ? '<span class="inst">●</span> ' : ""}${r.n}${r.count > 1 ? ` <span class="k">×${r.count}</span>` : ""}</span><span class="c">${r.dep}</span></div>`).join("");
319 }
320 const svg = d3.select("#graph svg"), g = svg.append("g");
321 svg.append("defs").append("marker").attr("id", "arrow").attr("viewBox", "0 -4 8 8").attr("refX", 8).attr("refY", 0)
322 .attr("markerWidth", 7).attr("markerHeight", 7).attr("orient", "auto").append("path").attr("d", "M0,-4L8,0L0,4").attr("fill", "#3d444d");
323 const zoom = d3.zoom().scaleExtent([0.05, 4]).on("zoom", e => g.attr("transform", e.transform));
324 svg.call(zoom).on("dblclick.zoom", null);
325 const gl = g.append("g"), gn = g.append("g");
326 let vis = new Map(); // node id -> {id, level, x, y}
327 let via = []; // [{source, target, steps}] dashed shortcuts
328 let focusId = -1;
329 const sim = d3.forceSimulation()
330 .force("link", d3.forceLink().id(d => d.id).distance(90).strength(0.3))
331 .force("charge", d3.forceManyBody().strength(-450))
332 .force("collide", d3.forceCollide(d => radius(d.id) + 10))
333 .force("y", d3.forceY(d => -d.level * 140).strength(0.6))
334 .force("x", d3.forceX(0).strength(0.01))
335 .on("tick", tick);
336
337 const radius = i => 4 + 2.2 * Math.log10(Math.max(0, depOf(i)) + 1);
338 const color = i => rootSet.has(i) ? "var(--blue)" : D.names[i] === "system-path" ? "var(--green)" : isU[i] ? "var(--red)" : "var(--dim)";
339
340 function add(i, level) {
341 if (!vis.has(i)) { const near = [...vis.values()].find(n => n.level === level - 1); vis.set(i, { id: i, level, x: (near?.x || 0) + (Math.random() - .5) * 80, y: -level * 110 }); }
342 }
343 function expand(i, limit = 40) {
344 const lv = vis.get(i).level;
345 const ps = parentsOf(i).sort((a, b) => (isU[b] - isU[a]) || (depOf(b) - depOf(a)));
346 ps.slice(0, limit).forEach(p => add(p, lv + 1));
347 return ps.length;
348 }
349 function bfsUp(start, stop) { // breadth-first over users until stop(i) holds on each branch
350 const prev = new Map([[start, -1]]), q = [start], hits = [];
351 for (let h = 0; h < q.length; h++) {
352 const i = q[h];
353 if (i !== start && stop(i)) { hits.push(i); continue; }
354 for (const p of parentsOf(i)) if (!prev.has(p)) { prev.set(p, i); q.push(p); }
355 }
356 return { prev, hits };
357 }
358 // distance (in "uses" steps) from i up to every node that transitively uses it
359 function distUp(i) {
360 const d = new Map([[i, 0]]), q = [i];
361 for (let h = 0; h < q.length; h++) { const v = q[h]; for (const p of parentsOf(v)) if (!d.has(p)) { d.set(p, d.get(v) + 1); q.push(p); } }
362 return d;
363 }
364 // how far the selected machine's system (or the nearest one, for "all") is above i
365 function systemDistance(i, d = distUp(i)) {
366 const reach = (mi < 0 ? D.roots : [D.roots[mi]]).filter(r => d.has(r));
367 if (!reach.length) return null;
368 const L = Math.min(...reach.map(r => d.get(r)));
369 return { L, roots: reach.filter(r => d.get(r) === L) };
370 }
371 // add every node on a shortest chain system -> ... -> i (walk down from the roots along edges that get one step closer to i)
372 function chainsToSystem(i, cap = 150) {
373 const d = distUp(i), s = systemDistance(i, d);
374 if (!s) return null;
375 const on = new Set(s.roots), q = [...s.roots];
376 for (let h = 0; h < q.length && on.size < cap; h++) {
377 const v = q[h];
378 for (const c of children[v]) if (inMachine(c) && d.get(c) === d.get(v) - 1 && !on.has(c)) { on.add(c); q.push(c); }
379 }
380 const base = vis.get(i)?.level ?? 0;
381 for (const v of on) add(v, base + d.get(v));
382 return { ...s, nodes: on.size, capped: on.size >= cap };
383 }
384 function unmaintainedUsers(i, limit = 30) { // adds the `limit` biggest; the details panel lists all
385 const { prev, hits } = bfsUp(i, j => isU[j]);
386 const lv = vis.get(i).level;
387 hits.sort((a, b) => depOf(b) - depOf(a));
388 for (const h of hits.slice(0, limit)) {
389 let steps = 0; for (let j = h; j !== i; j = prev.get(j)) steps++;
390 add(h, lv + 1);
391 if (steps > 1) via.push({ source: h, target: i, steps });
392 }
393 return hits;
394 }
395
396 function draw() {
397 const nodes = [...vis.values()];
398 const links = [];
399 for (const n of nodes) for (const p of D.parents[n.id]) if (vis.has(p) && inMachine(p)) links.push({ source: p, target: n.id });
400 const vlinks = via.filter(v => vis.has(v.source) && vis.has(v.target)).map(v => ({ ...v, via: true }));
401 const all = links.concat(vlinks);
402 const ls = gl.selectAll("line").data(all, d => `${d.source.id ?? d.source}-${d.target.id ?? d.target}-${!!d.via}`);
403 ls.exit().remove();
404 ls.enter().append("line").attr("class", d => d.via ? "link via" : "link").attr("marker-end", "url(#arrow)")
405 .append("title").text(d => d.via ? `uses via ${d.steps - 1} maintained package(s)` : "uses");
406 const ns = gn.selectAll("g.node").data(nodes, d => d.id);
407 ns.exit().remove();
408 const ne = ns.enter().append("g").attr("class", "node")
409 .call(d3.drag().on("start", (e, d) => { if (!e.active) sim.alphaTarget(.3).restart(); d.fx = d.x; d.fy = d.y; })
410 .on("drag", (e, d) => { d.fx = e.x; d.fy = e.y; })
411 .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = d.fy = null; }))
412 .on("click", (e, d) => { e.stopPropagation(); showDetails(d.id); })
413 .on("dblclick", (e, d) => { e.stopPropagation(); expand(d.id); draw(); showDetails(d.id); });
414 ne.append("circle");
415 ne.append("text").attr("dy", -9).attr("text-anchor", "middle");
416 ne.append("title");
417 const nm = ne.merge(ns);
418 nm.attr("class", d => `node${d.id === focusId ? " focus" : ""}${isInst(d.id) ? " inst" : ""}`);
419 nm.select("circle").attr("r", d => radius(d.id)).attr("fill", d => color(d.id));
420 nm.select("text").text(d => label(d.id));
421 nm.select("title").text(d => `${label(d.id)}\n${depOf(d.id)} dependents · ${parentsOf(d.id).length} direct users`);
422 sim.nodes(nodes);
423 sim.force("link").links(all);
424 sim.alpha(0.8).restart();
425 fitSoon();
426 }
427 function tick() {
428 gl.selectAll("line").each(function (d) {
429 const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y, len = Math.hypot(dx, dy) || 1, r = radius(d.target.id) + 2;
430 this.setAttribute("x1", d.source.x); this.setAttribute("y1", d.source.y);
431 this.setAttribute("x2", d.target.x - dx / len * r); this.setAttribute("y2", d.target.y - dy / len * r);
432 });
433 gn.selectAll("g.node").attr("transform", d => `translate(${d.x},${d.y})`);
434 }
435 function fit() {
436 const ns = sim.nodes().filter(n => isFinite(n.x)); if (!ns.length) return;
437 const { width, height } = svg.node().getBoundingClientRect(), pad = 60;
438 const [x0, x1] = d3.extent(ns, n => n.x), [y0, y1] = d3.extent(ns, n => n.y);
439 const k = Math.min(1.5, (width - 2 * pad) / Math.max(1, x1 - x0), (height - 2 * pad) / Math.max(1, y1 - y0));
440 svg.transition().duration(400).call(zoom.transform, d3.zoomIdentity.translate(width / 2 - k * (x0 + x1) / 2, height / 2 - k * (y0 + y1) / 2).scale(k));
441 }
442 let fitTimer; const fitSoon = () => { clearTimeout(fitTimer); fitTimer = setTimeout(fit, 700); };
443
444 function focusNode(i) {
445 selected = focusId = i;
446 vis = new Map(); via = [];
447 gl.selectAll("*").remove(); gn.selectAll("*").remove();
448 add(i, 0);
449 if (!chainsToSystem(i)) expand(i);
450 draw(); showDetails(i); renderList();
451 history.replaceState(null, "", "#" + D.hash[i]);
452 }
453
454 let detailId = -1;
455 function showDetails(i) {
456 detailId = i;
457 const ps = parentsOf(i).sort((a, b) => (isU[b] - isU[a]) || (depOf(b) - depOf(a)));
458 const ms = machinesOf(D.mask[i]), inst = machinesOf(instOn(i));
459 const li = j => `<li data-i="${j}" class="${isU[j] ? "u" : ""}">${label(j)} <span class="k">(${depOf(j)})</span></li>`;
460 $("#details").innerHTML = `
461 <h2 class="${isU[i] ? "u" : ""}">${label(i)}</h2>
462 <div>${isU[i] ? '<span class="u">no maintainer</span>' : '<span class="k">not flagged</span>'}</div>
463 ${D.file[i] ? `<div><span class="k">file</span> ${fileLink(i)}</div>` : ""}
464 <div><span class="k">drv</span> ${D.hash[i]}…-${D.names[i]}.drv</div>
465 <div><span class="k">dependents</span> ${depOf(i)} ${mi < 0 ? "(max over machines)" : "on " + D.machines[mi]}</div>
466 <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>
467 ${isU[i] ? `<div><span class="k">per machine</span> ${D.machines.map((m, k) => D.udep[i][k] >= 0 ? `${m}:${D.udep[i][k]}` : "").filter(Boolean).join(" ")}</div>` : ""}
468 <div><span class="k">in ${ms.length} machine(s)</span> ${ms.join(" ")}</div>
469 <div><span class="k">directly installed on</span> ${inst.length ? `<span class="inst">${inst.join(" ")}</span>` : "—"}</div>
470 <p><button id="dFocus">focus here</button> <button id="dExpand">expand users</button></p>
471 <div class="k">used directly by ${ps.length} (click to add to graph, red = unmaintained):</div>
472 <ul id="dParents">${ps.slice(0, 300).map(li).join("")}${ps.length > 300 ? `<li class="k">… ${ps.length - 300} more</li>` : ""}</ul>
473 ${(variants.get(D.names[i]) || []).length > 1 ? `<div class="k">other variants of ${D.names[i]} (separate derivations, e.g. bootstrap stages):</div><ul id="dVar">${variants.get(D.names[i]).filter(j => j !== i && inMachine(j)).sort((a, b) => depOf(b) - depOf(a)).map(li).join("")}</ul>` : ""}
474 <div id="dUnm"></div>`;
475 $("#dFocus").onclick = () => focusNode(i);
476 $("#dExpand").onclick = () => { if (!vis.has(i)) add(i, 0); expand(i, 1e9); draw(); };
477 $("#dParents").onclick = e => { const l = e.target.closest("li[data-i]"); if (!l) return; const j = +l.dataset.i; if (!vis.has(i)) add(i, 0); add(j, vis.get(i).level + 1); draw(); };
478 if ($("#dVar")) $("#dVar").onclick = e => { const l = e.target.closest("li[data-i]"); if (l) focusNode(+l.dataset.i); };
479 if (isU[i]) {
480 const { hits } = bfsUp(i, j => isU[j]);
481 hits.sort((a, b) => depOf(b) - depOf(a));
482 $("#dUnm").innerHTML = `<div class="k">nearest unmaintained users (${hits.length}):</div><ul>${hits.slice(0, 200).map(li).join("")}</ul>`;
483 $("#dUnm").onclick = e => { const l = e.target.closest("li[data-i]"); if (l) focusNode(+l.dataset.i); };
484 }
485 }
486 const needSel = f => () => { if (detailId < 0) return; if (!vis.has(detailId)) add(detailId, 0); f(detailId); draw(); };
487 $("#bExpand").onclick = needSel(i => expand(i));
488 $("#bPath").onclick = needSel(i => chainsToSystem(i));
489 $("#bUnm").onclick = needSel(unmaintainedUsers);
490 $("#bReset").onclick = () => { if (focusId >= 0) focusNode(focusId); };
491
492 renderList();
493 const h = location.hash.slice(1); const hi = h ? D.hash.indexOf(h) : -1;
494 if (hi >= 0) focusNode(hi);
495 </script>
496 </body>
497</html>