This repository has no description
0

Configure Feed

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

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>