<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://fzakaria.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fzakaria.com/" rel="alternate" type="text/html" /><updated>2026-10-06T09:55:59-07:00</updated><id>https://fzakaria.com/feed.xml</id><title type="html">Farid Zakaria’s Blog</title><subtitle>I&apos;m a software engineer, father and wishful amateur surfer. If you&apos;ve come seeking my political views, you&apos;ve found the wrong &lt;a href=&quot;https://fareedzakaria.com/&quot;&gt;Fareed&lt;/a&gt;.</subtitle><entry><title type="html">Rewind VM: a flaky build you only catch once</title><link href="https://fzakaria.com/2026/10/03/rewind-vm-a-flaky-build-you-only-catch-once" rel="alternate" type="text/html" title="Rewind VM: a flaky build you only catch once" /><published>2026-10-03T19:52:00-07:00</published><updated>2026-10-03T19:52:00-07:00</updated><id>https://fzakaria.com/2026/10/03/rewind-vm-a-flaky-build-you-only-catch-once</id><content type="html" xml:base="https://fzakaria.com/2026/10/03/rewind-vm-a-flaky-build-you-only-catch-once"><![CDATA[<p><em>If a test fails on CI and nobody can reproduce it, did it really fail?</em> 🧘</p>

<p>Nix gives me a build that is a function of its inputs via the extensional model. 
The same derivation, produces the same store path, and if I am lucky, the same bytes.
What Nix does not give me is the same <em>run</em>.<sup id="fnref:run"><a href="#fn:run" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>
A test suite with a race in it may pass on my laptop, fail once on
CI, and when I rebuild it to look, it passes again.</p>

<!--more-->

<p>If you have experienced bugs like this, you know that it can be frustrating.
You are effectively at times trying to find the <em>needle in the haystack</em>.</p>

<p>I wrote recently that <a href="/2026/07/30/the-nix-sandbox-is-a-hidden-input">the Nix sandbox is a hidden input</a> to a derivation. The same is true of the thread schedule. The order the kernel happens to run your processes in decides whether some builds pass, and nothing within the derivation records it.</p>

<p>These are the <em>hidden inputs</em> to a build that can make it flaky.</p>

<p style="--image-width: 22rem"><a href="/assets/images/ackchyually_nix_meme.png"><img src="/assets/images/ackchyually_nix_meme.png" alt="ackchyually meme: a nerd with glasses says &quot;Ackchyually, Nix isn't reproducible. Your tests depend on the thread schedule.&quot;" /></a></p>

<p>Are we left to hoping that we will find the needle in the haystack? Or is there a way to make the run a function of its inputs too?</p>

<p>I built <a href="https://rewindvm.dev">Rewind VM</a> to make the schedule an input too. It
runs a Nix build, a test suite or any Linux command inside a KVM virtual machine
whose every run is a function of its inputs. <u>The same inputs give the same run,
at the same steps, every time</u>. You can replay a failure, scrub through it, read
any file as it was at any point, and fork it under a different thread interleaving. ✨</p>

<p>Confused? Yes it sounds like magic. The best way to explain it is with short demo.</p>

<h2 id="a-textbook-deadlock">A textbook deadlock</h2>

<p>Let’s investigate the <a href="https://en.wikipedia.org/wiki/Dining_philosophers_problem">dining philosophers</a> problem. The problem is that the five philosophers sit at a round table with a fork between each of them. A philosopher needs both forks beside them
to eat. A philosopher may pick up one fork at a time.<sup id="fnref:world"><a href="#fn:world" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<figure>
<svg viewBox="0 0 380 360" role="img" style="display:block;margin-inline:auto;max-width:20rem;width:100%;height:auto;font-family:var(--mono)" aria-label="An animation of five philosophers, P0 to P4, around a table with forks f0 to f4 between them. P0 picks up two forks and eats, then puts them down, then P2 does the same. Then every philosopher picks up the fork on their left, each waits for the fork on their right, and the five waits close a ring: deadlock.">
  <style>
    @keyframes dp-k1{0.000%{opacity:0}6.250%{opacity:1}12.500%{opacity:1}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:1}50.000%{opacity:1}56.250%{opacity:1}62.500%{opacity:1}68.750%{opacity:1}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k1{opacity:1;animation:dp-k1 17.6s step-end infinite}
    @keyframes dp-k2{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:1}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k2{opacity:0;animation:dp-k2 17.6s step-end infinite}
    @keyframes dp-k3{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:1}56.250%{opacity:1}62.500%{opacity:1}68.750%{opacity:1}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k3{opacity:1;animation:dp-k3 17.6s step-end infinite}
    @keyframes dp-k4{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:1}31.250%{opacity:1}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:1}62.500%{opacity:1}68.750%{opacity:1}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k4{opacity:1;animation:dp-k4 17.6s step-end infinite}
    @keyframes dp-k5{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:1}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k5{opacity:0;animation:dp-k5 17.6s step-end infinite}
    @keyframes dp-k6{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:1}68.750%{opacity:1}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k6{opacity:1;animation:dp-k6 17.6s step-end infinite}
    @keyframes dp-k7{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:1}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k7{opacity:1;animation:dp-k7 17.6s step-end infinite}
    @keyframes dp-k8{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k8{opacity:1;animation:dp-k8 17.6s step-end infinite}
    @keyframes dp-k9{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k9{opacity:1;animation:dp-k9 17.6s step-end infinite}
    @keyframes dp-k10{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k10{opacity:1;animation:dp-k10 17.6s step-end infinite}
    @keyframes dp-k11{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k11{opacity:1;animation:dp-k11 17.6s step-end infinite}
    @keyframes dp-k12{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k12{opacity:1;animation:dp-k12 17.6s step-end infinite}
    @keyframes dp-k13{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:1}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k13{opacity:0;animation:dp-k13 17.6s step-end infinite}
    @keyframes dp-k14{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k14{opacity:0;animation:dp-k14 17.6s step-end infinite}
    @keyframes dp-k15{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:1}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k15{opacity:0;animation:dp-k15 17.6s step-end infinite}
    @keyframes dp-k16{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k16{opacity:0;animation:dp-k16 17.6s step-end infinite}
    @keyframes dp-k17{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k17{opacity:0;animation:dp-k17 17.6s step-end infinite}
    @keyframes dp-k18{0.000%{opacity:1}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k18{opacity:0;animation:dp-k18 17.6s step-end infinite}
    @keyframes dp-k19{0.000%{opacity:0}6.250%{opacity:1}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k19{opacity:0;animation:dp-k19 17.6s step-end infinite}
    @keyframes dp-k20{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:1}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k20{opacity:0;animation:dp-k20 17.6s step-end infinite}
    @keyframes dp-k21{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:1}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k21{opacity:0;animation:dp-k21 17.6s step-end infinite}
    @keyframes dp-k22{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:1}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k22{opacity:0;animation:dp-k22 17.6s step-end infinite}
    @keyframes dp-k23{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:1}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k23{opacity:0;animation:dp-k23 17.6s step-end infinite}
    @keyframes dp-k24{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:1}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k24{opacity:0;animation:dp-k24 17.6s step-end infinite}
    @keyframes dp-k25{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:1}50.000%{opacity:1}56.250%{opacity:1}62.500%{opacity:1}68.750%{opacity:1}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k25{opacity:0;animation:dp-k25 17.6s step-end infinite}
    @keyframes dp-k26{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:1}81.250%{opacity:1}87.500%{opacity:0}93.750%{opacity:0}}
    .dp-k26{opacity:0;animation:dp-k26 17.6s step-end infinite}
    @keyframes dp-k27{0.000%{opacity:0}6.250%{opacity:0}12.500%{opacity:0}18.750%{opacity:0}25.000%{opacity:0}31.250%{opacity:0}37.500%{opacity:0}43.750%{opacity:0}50.000%{opacity:0}56.250%{opacity:0}62.500%{opacity:0}68.750%{opacity:0}75.000%{opacity:0}81.250%{opacity:0}87.500%{opacity:1}93.750%{opacity:1}}
    .dp-k27{opacity:1;animation:dp-k27 17.6s step-end infinite}
    @media (prefers-reduced-motion: reduce) { [class^="dp-k"] { animation: none } }
  </style>
  <defs>
    <marker id="dp-wait" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0,0 L10,5 L0,10 z" fill="#b1201d" />
    </marker>
  </defs>
  <circle cx="190" cy="168" r="78" fill="currentColor" opacity="0.06" />
  <g class="dp-k1"><line x1="202.3" y1="79.5" x2="222.7" y2="111.9" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k2"><line x1="177.7" y1="79.5" x2="157.3" y2="111.9" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k3"><line x1="109.6" y1="129.0" x2="146.8" y2="119.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k4"><line x1="128.0" y1="232.4" x2="130.6" y2="194.1" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k5"><line x1="147.9" y1="246.8" x2="183.5" y2="232.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k6"><line x1="232.1" y1="246.8" x2="196.5" y2="232.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k7"><line x1="278.0" y1="152.3" x2="253.4" y2="181.8" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" /></g>
  <g class="dp-k8"><line x1="177.7" y1="79.5" x2="159.4" y2="108.5" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#dp-wait)" /></g>
  <g class="dp-k9"><line x1="102.0" y1="152.3" x2="124.0" y2="178.7" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#dp-wait)" /></g>
  <g class="dp-k10"><line x1="147.9" y1="246.8" x2="179.8" y2="234.1" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#dp-wait)" /></g>
  <g class="dp-k11"><line x1="252.0" y1="232.4" x2="249.7" y2="198.1" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#dp-wait)" /></g>
  <g class="dp-k12"><line x1="270.4" y1="129.0" x2="237.1" y2="120.5" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#dp-wait)" /></g>
  <rect x="220.4" y="111.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="213.5" y="139.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f0</text>
  <rect x="147.6" y="111.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="166.5" y="139.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f1</text>
  <rect x="125.0" y="181.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="152.0" y="184.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f2</text>
  <rect x="184.0" y="224.0" width="12" height="12" rx="2" fill="currentColor" />
  <text x="190.0" y="212.0" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f3</text>
  <rect x="243.0" y="181.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="228.0" y="184.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f4</text>
  <circle cx="190.0" cy="60.0" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="190.0" y="65.0" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P0</text>
  <g class="dp-k13"><text x="190.0" y="27.0" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text></g>
  <circle cx="87.3" cy="134.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="87.3" y="139.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P1</text>
  <g class="dp-k14"><text x="51.1" y="127.9" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text></g>
  <circle cx="126.5" cy="255.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="126.5" y="260.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P2</text>
  <g class="dp-k15"><text x="104.2" y="291.1" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text></g>
  <circle cx="253.5" cy="255.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="253.5" y="260.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P3</text>
  <g class="dp-k16"><text x="275.8" y="291.1" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text></g>
  <circle cx="292.7" cy="134.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="292.7" y="139.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P4</text>
  <g class="dp-k17"><text x="328.9" y="127.9" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text></g>
  <g class="dp-k18"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">five philosophers, five forks</text></g>
  <g class="dp-k19"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P0 picks up f0</text></g>
  <g class="dp-k20"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P0 picks up f1 and eats</text></g>
  <g class="dp-k21"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P0 puts both down</text></g>
  <g class="dp-k22"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P2 picks up f2</text></g>
  <g class="dp-k23"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P2 picks up f3 and eats</text></g>
  <g class="dp-k24"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">P2 puts both down</text></g>
  <g class="dp-k25"><text x="190" y="345" font-size="15" text-anchor="middle" fill="currentColor">everyone reaches for the left fork</text></g>
  <g class="dp-k26"><text x="190" y="345" font-size="15" text-anchor="middle" fill="#b1201d">each waits on a neighbor&#39;s fork</text></g>
  <g class="dp-k27"><text x="190" y="345" font-size="15" text-anchor="middle" fill="#b1201d">nobody can ever eat: deadlock</text></g>
</svg>
<figcaption>The philosophers take turns, until all five pick up their left fork at once.</figcaption>
</figure>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* The fork on the left first, then the fork on the right. */</span>
<span class="kt">int</span> <span class="n">first</span> <span class="o">=</span> <span class="n">left</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">second</span> <span class="o">=</span> <span class="n">right</span><span class="p">;</span>

<span class="cm">/* Eat MEALS times, holding both forks for each meal. */</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">meal</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">meal</span> <span class="o">&lt;</span> <span class="n">MEALS</span><span class="p">;</span> <span class="n">meal</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">forks</span><span class="p">[</span><span class="n">first</span><span class="p">]);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"philosopher %d picks up fork %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="n">first</span><span class="p">);</span>
  <span class="n">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">forks</span><span class="p">[</span><span class="n">second</span><span class="p">]);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"philosopher %d picks up fork %d and eats</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="n">second</span><span class="p">);</span>
  <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">forks</span><span class="p">[</span><span class="n">second</span><span class="p">]);</span>
  <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">forks</span><span class="p">[</span><span class="n">first</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If all five pick up their left fork before any of them reaches for a right
one, every fork is taken and every philosopher waits on a neighbor who is
also waiting. <strong>Deadlock</strong>.<sup id="fnref:timeout"><a href="#fn:timeout" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>On my laptop 14 of 100 rebuilds deadlocked. 💣</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build <span class="nt">--rebuild</span> <span class="nt">-L</span> github:fzakaria/rewindvm#philosophers
<span class="c">...
</span><span class="gp">philosophers&gt;</span><span class="w"> </span>philosopher 3 picks up fork 3
<span class="gp">philosophers&gt;</span><span class="w"> </span>philosopher 0 picks up fork 0
<span class="gp">philosophers&gt;</span><span class="w"> </span>philosopher 4 picks up fork 4
<span class="gp">philosophers&gt;</span><span class="w"> </span>philosopher 2 picks up fork 2
<span class="gp">philosophers&gt;</span><span class="w"> </span>philosopher 1 picks up fork 1
<span class="gp">philosophers&gt;</span><span class="w"> </span>make: <span class="k">***</span> <span class="o">[</span>Makefile:14: check] Error 124
<span class="go">error: Cannot build '/nix/store/fp2dx8yh62kik2nknqmjzrga0d9yd75s-philosophers-0.1.0.drv'.
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">rewind nix</code> builds the same derivation the way the Nix sandbox would, inside
the deterministic VM.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind nix github:fzakaria/rewindvm#philosophers
<span class="go">rewind: packing 62 store paths for philosophers-0.1.0
</span><span class="c">...
</span><span class="go">rewind: run 1b21bf17737bcb37 exited:0 after 4583 steps, 0.126s virtual, 6.146s wall (poweroff)
</span><span class="c"># the hash of the output tree, compared against the host's build
</span><span class="go">/nix/store/g0zkyzprrlhrnw8sf42xl49gjn2lzq62-philosophers-0.1.0 fad5714e2ad61159 (same as the host's build)
</span></code></pre></div></div>

<p>Oh darn, it passed! That means it will pass forever right? Not quite. The VM is deterministic, but the guest kernel’s scheduler is not. It can reschedule threads at different points in the program, and that can change the outcome.</p>

<p>To find the other interleavings, <code class="language-plaintext highlighter-rouge">rewind check</code> runs the
build again under <strong>perturbed schedules</strong>, we effectively ask the guest kernel
to reschedule at different steps which causes a different sequence of events.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind check github:fzakaria/rewindvm#philosophers
<span class="go">schedule   0: exited:0             4583 steps  fad5714e2ad6  run 1b21bf17737bcb37
schedule   1: exited:2             6483 steps    run 83be7294525ddb91
schedule   2: exited:0             5396 steps  fad5714e2ad6  run 6d76063661b1010b
schedule   3: exited:2             7503 steps    run 800c0f86aef3116b
</span><span class="c"># 13 more schedules omitted
...
</span><span class="go">
</span><span class="gp">schedule 1 ends differently;</span><span class="w"> </span>narrowing the steps it perturbs
<span class="go">perturbing only steps 1748..3461 still ends differently

passing: run 1b21bf17737bcb37
failing: run b0739df0eacce572

</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">rewind check</code> runs one VM per core by default and stops after the first batch of schedules where the exit code differs.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
A failing schedule on its own is not that super helpful. Schedule 1 which had deadlocked likely perturbs every step from the start of the build to the end, and most of those perturbations have nothing to do with the deadlock. 
To help with this, <code class="language-plaintext highlighter-rouge">check</code> narrows it: it shrinks the window
of steps the schedule may perturb, first pulling in the end and then the start, reruns the build for each candidate window, and keeps the smallest one that still deadlocks.</p>
</blockquote>

<p>For our deadlock problem, it might be easier to see the last few lines of the log and see that all five philosophers have picked up their left fork and are waiting for the right one.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind log b0739df0 | <span class="nb">tail</span> <span class="nt">-6</span>
<span class="go">philosopher 0 picks up fork 0
philosopher 3 picks up fork 3
philosopher 4 picks up fork 4
philosopher 1 picks up fork 1
philosopher 2 picks up fork 2
make: *** [Makefile:14: check] Error 124
</span></code></pre></div></div>

<p>We can also inspect the events, which are the same as the log but with timestamps and thread IDs.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind events b0739df0 | <span class="nb">grep</span> <span class="nt">-A1</span> <span class="s1">'philosopher 2 picks up fork 2'</span>
<span class="go">      3470   140/143   write(1, "philosopher 2 picks up fork 2\n")
      4243   139/139   SIGALRM code=-2 addr=0x0
</span></code></pre></div></div>

<p>What if I’m not familiar with the VM? Can we look around? Yes!</p>

<p><code class="language-plaintext highlighter-rouge">rewind shell</code> drops you into a shell inside the VM at any step, in a process’s working directory, with the build’s environment, while everything else in the VM stays stopped. We can use <code class="language-plaintext highlighter-rouge">--with nixpkgs#gdb</code> to bring gdb into that shell, and gdb can attach to the stuck process.<sup id="fnref:gdb"><a href="#fn:gdb" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind shell b0739df0 3471 <span class="nt">--pid</span> 140 <span class="nt">--with</span> nixpkgs#gdb
<span class="gp">rewind: a shell at step 3471 of b0739df0eacce572;</span><span class="w"> </span><span class="nb">exit </span>it to leave
<span class="gp">[rewind] /build/philosophers #</span><span class="w"> </span>gdb <span class="nt">-q</span> <span class="nt">-batch</span> <span class="nt">-p</span> 140 <span class="nt">-ex</span> <span class="s1">'thread apply all -q frame function dine'</span> <span class="nt">-ex</span> <span class="s1">'python print([int(gdb.parse_and_eval(f"forks[{i}].__data.__owner")) for i in range(5)])'</span> 2&gt;/dev/null
<span class="c">...
</span><span class="gp">#</span>2  0x0000560d082a0237 <span class="k">in </span>dine <span class="o">(</span><span class="nv">arg</span><span class="o">=</span>&lt;optimized out&gt;<span class="o">)</span> at philosophers.c:27
<span class="gp">27			pthread_mutex_lock(&amp;forks[second]);</span><span class="w">
</span><span class="gp">#</span>2  0x0000560d082a0237 <span class="k">in </span>dine <span class="o">(</span><span class="nv">arg</span><span class="o">=</span>&lt;optimized out&gt;<span class="o">)</span> at philosophers.c:27
<span class="gp">27			pthread_mutex_lock(&amp;forks[second]);</span><span class="w">
</span><span class="gp">#</span>2  0x0000560d082a0237 <span class="k">in </span>dine <span class="o">(</span><span class="nv">arg</span><span class="o">=</span>&lt;optimized out&gt;<span class="o">)</span> at philosophers.c:27
<span class="gp">27			pthread_mutex_lock(&amp;forks[second]);</span><span class="w">
</span><span class="gp">#</span>2  0x0000560d082a0237 <span class="k">in </span>dine <span class="o">(</span><span class="nv">arg</span><span class="o">=</span>&lt;optimized out&gt;<span class="o">)</span> at philosophers.c:27
<span class="gp">27			pthread_mutex_lock(&amp;forks[second]);</span><span class="w">
</span><span class="gp">#</span>2  0x0000560d082a0237 <span class="k">in </span>dine <span class="o">(</span><span class="nv">arg</span><span class="o">=</span>&lt;optimized out&gt;<span class="o">)</span> at philosophers.c:27
<span class="gp">27			pthread_mutex_lock(&amp;forks[second]);</span><span class="w">
</span><span class="go">[141, 142, 143, 144, 145]
</span></code></pre></div></div>

<p>All five philosophers are on line 27, waiting for their second fork.</p>

<figure>
<svg viewBox="0 0 380 345" role="img" style="display:block;margin-inline:auto;max-width:16rem;width:100%;height:auto;font-family:var(--mono)" aria-label="A round table of five philosophers, P0 to P4, with forks f0 to f4 between them. Every philosopher has picked up the left fork first: each holds one fork and waits for the next, and the five waits close a ring.">
  <defs>
    <marker id="left-first-wait" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0,0 L10,5 L0,10 z" fill="#b1201d" />
    </marker>
  </defs>
  <circle cx="190" cy="150" r="78" fill="currentColor" opacity="0.06" />
  <line x1="202.3" y1="61.5" x2="222.7" y2="93.9" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="109.6" y1="111.0" x2="146.8" y2="101.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="128.0" y1="214.4" x2="130.6" y2="176.1" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="232.1" y1="228.8" x2="196.5" y2="214.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="278.0" y1="134.3" x2="253.4" y2="163.8" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="177.7" y1="61.5" x2="159.4" y2="90.5" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#left-first-wait)" />
  <line x1="102.0" y1="134.3" x2="124.0" y2="160.7" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#left-first-wait)" />
  <line x1="147.9" y1="228.8" x2="179.8" y2="216.1" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#left-first-wait)" />
  <line x1="252.0" y1="214.4" x2="249.7" y2="180.1" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#left-first-wait)" />
  <line x1="270.4" y1="111.0" x2="237.1" y2="102.5" stroke="#b1201d" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#left-first-wait)" />
  <rect x="220.4" y="93.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="213.5" y="121.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f0</text>
  <rect x="147.6" y="93.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="166.5" y="121.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f1</text>
  <rect x="125.0" y="163.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="152.0" y="166.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f2</text>
  <rect x="184.0" y="206.0" width="12" height="12" rx="2" fill="currentColor" />
  <text x="190.0" y="194.0" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f3</text>
  <rect x="243.0" y="163.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="228.0" y="166.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f4</text>
  <circle cx="190.0" cy="42.0" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="190.0" y="47.0" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P0</text>
  <circle cx="87.3" cy="116.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="87.3" y="121.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P1</text>
  <circle cx="126.5" cy="237.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="126.5" y="242.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P2</text>
  <circle cx="253.5" cy="237.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="253.5" y="242.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P3</text>
  <circle cx="292.7" cy="116.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="292.7" y="121.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P4</text>
  <text x="190" y="312" fill="currentColor" font-size="16" font-weight="600" text-anchor="middle">left fork first</text>
  <text x="190" y="332" fill="#b1201d" font-size="14" text-anchor="middle">every wait is on a held fork: a ring</text>
</svg>
</figure>

<p><code class="language-plaintext highlighter-rouge">rewind cat</code>, <code class="language-plaintext highlighter-rouge">rewind shell</code> and <code class="language-plaintext highlighter-rouge">rewind gdb</code> each work on a “throwaway” fork
of the run at a step, so nothing they do changes the recording.</p>

<p>You can use <code class="language-plaintext highlighter-rouge">rewind</code> to do a “real” fork: it branches a run at a step under another schedule.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind fork 1b21bf17 1748 <span class="nt">--schedule</span> 1 <span class="nt">--quiet</span>
<span class="go">rewind: run 0c8d8aa42424941d exited:2 after 6213 steps, 10.111s virtual, 16.938s wall (poweroff)
rewind: the fork first differs from its parent at step 1770
</span></code></pre></div></div>

<p>A recording does not have to stay on the machine that made it. <code class="language-plaintext highlighter-rouge">rewind export
--replayable</code> packs a run into a single <code class="language-plaintext highlighter-rouge">.rwd</code> file, with the VM’s kernel, its
input image and the keyframes, so another machine with the same CPU vendor can
replay it.<sup id="fnref:replay"><a href="#fn:replay" class="footnote" rel="footnote" role="doc-noteref">5</a></sup></p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind <span class="nb">export </span>b0739df0 <span class="nt">--replayable</span> <span class="nt">-o</span> deadlock.rwd
<span class="go">rewind: wrote deadlock.rwd (176.4 MB)

</span><span class="gp">#</span><span class="w"> </span>on another machine
<span class="gp">$</span><span class="w"> </span>rewind import deadlock.rwd
<span class="go">b0739df0eacce572  exited:2          4299 steps  philosophers-0.1.0
</span><span class="gp">$</span><span class="w"> </span>rewind replay b0739df0
<span class="go">identical: 1427 events over 4299 steps
</span></code></pre></div></div>

<p>You are no longer beholden to a random flake on CI.</p>

<p>Run the tests under <code class="language-plaintext highlighter-rouge">rewind check</code> on a CI machine with KVM, upload the failing run’s <code class="language-plaintext highlighter-rouge">.rwd</code> as a build artifact, and you can reproduce the bug perfectly on your machine.</p>

<p>How do we fix the deadlock?</p>

<p>We number the forks and always pick up the lower numbered one first. The last Philosopher now reaches for fork 0 before fork 4, so the waits can never cause a deadlock.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-	/* The fork on the left first, then the fork on the right. */
-	int first = left;
-	int second = right;
</span><span class="gi">+	/* The lower numbered fork first, then the other one. */
+	int first = left &lt; right ? left : right;
+	int second = left &lt; right ? right : left;
</span></code></pre></div></div>

<figure>
<svg viewBox="0 0 380 345" role="img" style="display:block;margin-inline:auto;max-width:16rem;width:100%;height:auto;font-family:var(--mono)" aria-label="The same table with every philosopher picking up the lower numbered fork first. P4 waits for f0 while holding nothing, so f4 stays free, P3 picks it up and eats, and nobody waits in a ring.">
  <defs>
    <marker id="lower-first-wait" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
      <path d="M0,0 L10,5 L0,10 z" fill="currentColor" />
    </marker>
  </defs>
  <circle cx="190" cy="150" r="78" fill="currentColor" opacity="0.06" />
  <line x1="202.3" y1="61.5" x2="222.7" y2="93.9" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="109.6" y1="111.0" x2="146.8" y2="101.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="128.0" y1="214.4" x2="130.6" y2="176.1" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="232.1" y1="228.8" x2="196.5" y2="214.6" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="252.0" y1="214.4" x2="249.4" y2="176.1" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" />
  <line x1="177.7" y1="61.5" x2="159.4" y2="90.5" stroke="currentColor" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#lower-first-wait)" />
  <line x1="102.0" y1="134.3" x2="124.0" y2="160.7" stroke="currentColor" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#lower-first-wait)" />
  <line x1="147.9" y1="228.8" x2="179.8" y2="216.1" stroke="currentColor" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#lower-first-wait)" />
  <line x1="270.4" y1="111.0" x2="237.1" y2="102.5" stroke="currentColor" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#lower-first-wait)" />
  <rect x="220.4" y="93.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="213.5" y="121.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f0</text>
  <rect x="147.6" y="93.8" width="12" height="12" rx="2" fill="currentColor" />
  <text x="166.5" y="121.6" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f1</text>
  <rect x="125.0" y="163.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="152.0" y="166.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f2</text>
  <rect x="184.0" y="206.0" width="12" height="12" rx="2" fill="currentColor" />
  <text x="190.0" y="194.0" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f3</text>
  <rect x="243.0" y="163.2" width="12" height="12" rx="2" fill="currentColor" />
  <text x="228.0" y="166.4" font-size="13" text-anchor="middle" fill="currentColor" opacity="0.75">f4</text>
  <circle cx="190.0" cy="42.0" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="190.0" y="47.0" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P0</text>
  <circle cx="87.3" cy="116.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="87.3" y="121.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P1</text>
  <circle cx="126.5" cy="237.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="126.5" y="242.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P2</text>
  <circle cx="253.5" cy="237.4" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="253.5" y="242.4" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P3</text>
  <text x="253.5" y="275.4" font-size="14" font-weight="600" text-anchor="middle" fill="currentColor">eats</text>
  <circle cx="292.7" cy="116.6" r="21" fill="var(--paper, transparent)" stroke="currentColor" stroke-width="1.5" />
  <text x="292.7" y="121.6" font-size="16" font-weight="600" text-anchor="middle" fill="currentColor">P4</text>
  <text x="190" y="312" fill="currentColor" font-size="16" font-weight="600" text-anchor="middle">lower numbered fork first</text>
  <text x="190" y="332" fill="currentColor" font-size="14" text-anchor="middle">P4 holds nothing, so f4 stays free</text>
</svg>
</figure>

<p>We can then run <code class="language-plaintext highlighter-rouge">rewind check --all</code> to test the fix.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>rewind check <span class="nt">--all</span> .#philosophers
<span class="c">...
</span><span class="go">0 of 64 perturbed schedules ended differently
same result under all 65 schedules
</span></code></pre></div></div>

<p>Before the fix, 9 of the same 64 schedules deadlocked.</p>

<p><a href="https://rewindvm.dev">Rewind VM</a> includes some <a href="https://rewindvm.dev/#tutorials">tutorials</a> with more examples of using <code class="language-plaintext highlighter-rouge">rewind</code> to find and fix bugs if you want to explore further.</p>

<h2 id="what-is-rewind-vm">What is Rewind VM?</h2>

<p>The VM has a single vCPU on stock KVM, so guest code runs on the real CPU at
close to native speed.<sup id="fnref:native"><a href="#fn:native" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> What breaks determinism in a normal VM is everything that
reaches the guest from <em>outside</em> its instruction stream: timer interrupts, clocks,
random numbers, I/O completions, and any other event</p>

<p>Rewind removes each of those or replaces it with a value it controls. Keeping account
of every event and the step it happened at, it can replay the same run.</p>

<div class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">step</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">LR</span><span class="p">;</span>
  <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">box</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="nv">rounded</span><span class="p">,</span> <span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">10</span><span class="o">]</span><span class="p">;</span>
  <span class="k">edge</span> <span class="o">[</span><span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.6</span><span class="p">,</span> <span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">guest</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"guest runs\non the real CPU"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">exit</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"guest exits\n(port or MMIO access)"</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="s2">"rounded,bold"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">monitor</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"monitor\nadvance virtual clock\nrecord event\ninject timer interrupt?"</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">guest</span> <span class="o">-&gt;</span> <span class="nv">exit</span><span class="p">;</span>
  <span class="nv">exit</span> <span class="o">-&gt;</span> <span class="nv">monitor</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"step N"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">monitor</span> <span class="o">-&gt;</span> <span class="nv">guest</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"resume"</span><span class="o">]</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you squint, a run is a derivation. Its inputs are the kernel, the
initramfs, a root filesystem (a Nix closure packed into a read-only image),
the command, a seed and a schedule. Change any of them and you get a
different run. Change none of them and you get the same one.</p>

<p>The <code class="language-plaintext highlighter-rouge">rewind</code> command is open source under the <a href="https://opensource.org/licenses/MIT">MIT</a> license.<sup id="fnref:gpl"><a href="#fn:gpl" class="footnote" rel="footnote" role="doc-noteref">7</a></sup></p>

<p>There is also a desktop app that gives a friendlier view of a recorded run. 
You can drag the playhead, view the build log, the process tree and the files at any event.
“Open shell” and “Attach gdb” open a terminal pane on a fork at the
playhead.</p>

<p><a href="/assets/images/rewind-app-deadlock.png"><img src="/assets/images/rewind-app-deadlock.png" alt="The Rewind desktop app on the deadlocked philosophers build at step 3,471: the timeline of build phases, the build log ending with each philosopher picking up one fork, the five philosopher threads still alive, and a card saying where this run parted from the passing one." /></a></p>

<h2 id="footguns">Footguns</h2>

<ul>
  <li><strong>One vCPU.</strong> Threads interleave but never run in parallel, so races that
need two cores at once are out of reach.</li>
  <li><strong>No preemption between system calls.</strong> A thread spinning on a flag without
yielding stalls the VM.</li>
  <li><strong>AMD needs</strong> <code class="language-plaintext highlighter-rouge">sudo rewind pmu enable</code> once per boot for the exact clock.<sup id="fnref:amd"><a href="#fn:amd" class="footnote" rel="footnote" role="doc-noteref">8</a></sup></li>
  <li><strong>A run replays only on the CPU vendor it was made on</strong>, AMD from Zen 2 on.</li>
  <li><strong>No network</strong> besides loopback, and x86_64 Linux hosts with KVM only.</li>
</ul>

<h2 id="rewind-in-the-wild">Rewind in the wild</h2>

<p>I pointed Rewind at some tools I use every day to see what we can find. Each of these is
reported upstream with a fix.</p>

<ul>
  <li><strong>Nix</strong>: <code class="language-plaintext highlighter-rouge">gc-closure.sh</code> dies of <code class="language-plaintext highlighter-rouge">SIGPIPE</code> when <code class="language-plaintext highlighter-rouge">head -n1</code> exits between
two writes. It never failed in 20,000 runs on my laptop and failed on the
first run in Rewind. <a href="https://github.com/NixOS/nix/issues/16546">#16546</a>,
fixed by <a href="https://github.com/NixOS/nix/pull/16547">#16547</a>
(<a href="https://github.com/fzakaria/rewindvm/blob/main/docs/case-studies/nix-gc-closure-sigpipe.md">case study</a>).</li>
  <li><strong>Nix</strong>: several processes creating a new store at once fail with “database
is busy”, as seen on Hydra. <a href="https://github.com/NixOS/nix/issues/15987">#15987</a>,
fixed by <a href="https://github.com/NixOS/nix/pull/16554">#16554</a>.</li>
  <li><strong>nixd</strong>: formatter output over 64 KiB hangs the language server.
<a href="https://github.com/nix-community/nixd/issues/899">#899</a>, fixed by
<a href="https://github.com/nix-community/nixd/pull/900">#900</a>.</li>
  <li><strong>jujutsu</strong>: three tests fail about half the time on tmpfs, because
operations ending in the same millisecond are ordered by a random id.
<a href="https://github.com/jj-vcs/jj/issues/10306">#10306</a>, fixed by
<a href="https://github.com/jj-vcs/jj/pull/10307">#10307</a>.</li>
</ul>

<h2 id="try-it">Try it</h2>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-fsSL</span> https://rewindvm.dev/install | sh
<span class="gp">#</span><span class="w"> </span>or
<span class="gp">$</span><span class="w"> </span>nix run github:fzakaria/rewindvm <span class="nt">--</span> check github:fzakaria/rewindvm#philosophers
</code></pre></div></div>

<p>On NixOS there is a module:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">inputs</span><span class="o">.</span><span class="nv">rewind</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:fzakaria/rewindvm"</span><span class="p">;</span>

<span class="c"># with inputs.rewind.nixosModules.default imported</span>
<span class="nv">programs</span><span class="o">.</span><span class="nv">rewind</span><span class="o">.</span><span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="nv">programs</span><span class="o">.</span><span class="nv">rewind</span><span class="o">.</span><span class="nv">app</span><span class="o">.</span><span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="c"># AMD only: make the branch counter exact at every boot</span>
<span class="nv">programs</span><span class="o">.</span><span class="nv">rewind</span><span class="o">.</span><span class="nv">amdBranchCounterWorkaround</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
</code></pre></div></div>

<p>If you have a test that fails on CI once a week, I would like to hear whether Rewind catches it and helped you debug it.</p>

<p>Don’t just add a <code class="language-plaintext highlighter-rouge">sleep</code> and paper over your concurrecy failures anymore, replay them. 🔁</p>

<p style="--image-width: 22rem"><a href="/assets/images/rewind_bell_curve.png"><img src="/assets/images/rewind_bell_curve.png" alt="bell curve meme: the low and high ends both say &quot;just re-run it&quot;; the middle cries about retries, sleep(1), quarantining the test and filing a ticket" /></a></p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:run">
      <p>A run is the sequence of events that happen in a process to produce a result. A build is a run of a derivation. <a href="#fnref:run" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:world">
      <p>The world is a metaphor for the thread scheduler. Each philosopher is a thread and each fork a mutex. <a href="#fnref:world" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:timeout">
      <p>The program stops making progress and never exits, so the check phase runs it under <code class="language-plaintext highlighter-rouge">timeout 10</code>, which kills it and exits with status 124. <a href="#fnref:timeout" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gdb">
      <p>There is actually native support for gdb in the VM already. You can also use <code class="language-plaintext highlighter-rouge">--with</code> to bring in any other tool you want to use. <a href="#fnref:gdb" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:replay">
      <p>Without <code class="language-plaintext highlighter-rouge">--replayable</code>, <code class="language-plaintext highlighter-rouge">rewind export</code> writes only the run’s trace, its events and output, and leaves out the kernel, the input image and the keyframes. The file is much smaller and enough to read the run with <code class="language-plaintext highlighter-rouge">rewind log</code> and <code class="language-plaintext highlighter-rouge">rewind events</code>. It can’t be replayed or forked, though, so <code class="language-plaintext highlighter-rouge">rewind cat</code>, <code class="language-plaintext highlighter-rouge">rewind shell</code> and <code class="language-plaintext highlighter-rouge">rewind gdb</code> need the full export. <a href="#fnref:replay" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:native">
      <p>GNU hello build from nixpkgs takes 11.7 s in the VM vs. 14.3 s without it on the same laptop. <a href="#fnref:native" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gpl">
      <p>The guest kernel patch is GPL-2.0 alongside Linux. <a href="#fnref:gpl" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:amd">
      <p>The NixOS module can do it automatically for you and without the setting Rewind falls back to a coarser clock. <a href="#fnref:amd" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[If a test fails on CI and nobody can reproduce it, did it really fail? 🧘 Nix gives me a build that is a function of its inputs via the extensional model. The same derivation, produces the same store path, and if I am lucky, the same bytes. What Nix does not give me is the same run.1 A test suite with a race in it may pass on my laptop, fail once on CI, and when I rebuild it to look, it passes again. A run is the sequence of events that happen in a process to produce a result. A build is a run of a derivation. &#8617;]]></summary></entry><entry><title type="html">Hiding my AI slop from my Nix friends</title><link href="https://fzakaria.com/2026/09/28/hiding-my-ai-slop-from-my-nix-friends" rel="alternate" type="text/html" title="Hiding my AI slop from my Nix friends" /><published>2026-09-28T13:21:00-07:00</published><updated>2026-09-28T13:21:00-07:00</updated><id>https://fzakaria.com/2026/09/28/hiding-my-ai-slop-from-my-nix-friends</id><content type="html" xml:base="https://fzakaria.com/2026/09/28/hiding-my-ai-slop-from-my-nix-friends"><![CDATA[<blockquote class="alert alert-warning">
  <p><strong>Warning</strong>
If you are pissed off reading this, you can <a href="/2026/07/18/how-to-piss-off-your-nix-friends">start here</a> and work your way back.</p>
</blockquote>

<p>I am <a href="/2026/05/31/ai-is-a-boon-for-the-anal-retentive">using AI a lot</a>. I am public about my use of it and when required make the necessary disclosures: “Assited-By” trailers or having to say “I used an AI to help write this software” <em>on every</em> <a href="https://discourse.nixos.org/">Discourse</a> post.<sup id="fnref:discourse"><a href="#fn:discourse" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<!--more-->

<p>For those of you in the “Anti-AI” crowd, it might surprise you to learn that those of us who enjoy using it also don’t want to read “slop”. I also don’t want anything I create to <em>read</em> like I used AI a lot. Commit messages, READMEs, code comments. I’m reading it also!</p>

<p>Being a Nix person, my instructions to the agent are of course declarative. My <code class="language-plaintext highlighter-rouge">~/.claude/CLAUDE.md</code> (and Codex’s <code class="language-plaintext highlighter-rouge">AGENTS.md</code>) is generated by home-manager from <a href="https://github.com/fzakaria/nix-home/blob/master/users/fmzakari/agent-settings.nix">agent-settings.nix</a>. It has a whole section titled <em>“Writing style: no LLM tells”</em> that I stitched together from Wikipedia’s <a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing">Signs of AI writing</a> and Simon Willison’s <a href="https://tools.simonwillison.net/llm-cliche-highlighter">LLM cliche highlighter</a>.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">writingStyle</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">  ## Writing style: no LLM tells</span><span class="err">

</span><span class="s2">  ### Banned vocabulary</span><span class="err">

</span><span class="s2">  Do not use these words. They are statistically the loudest signal of</span><span class="err">
</span><span class="s2">  machine-written text: delve, tapestry, meticulous, pivotal, intricate,</span><span class="err">
</span><span class="s2">  ...</span><span class="err">
</span><span class="s2">''</span><span class="p">;</span>
</code></pre></div></div>

<p>The code-style tries to ban many <em>AI-isms</em> as they emerge such as: “it’s not X, it’s Y”, “dvelve”, “byte-order”, minimal em-dashes and so forth.</p>

<p>I even came across <a href="https://fabiensanglard.net/">Fabien Sanglard</a>’s code style rules from <a href="https://news.ycombinator.com/item?id=48884313">this HackerNews thread</a> that I have enjoyed incorporating as well. Why not have my agent write code that mimics engineers I admire!?</p>

<p>Despite this, I still found my agents continuing to use these banned words and phrases. 😒</p>

<h2 id="reproducible-ai-slop-removal">Reproducible AI-slop removal</h2>

<p>I was <em>a little annoyed</em> I had to keep re-prompting my preferred writing style but I often reflect on <a href="http://www.incompleteideas.net/IncIdeas/BitterLesson.html">the bitter lesson</a> and generally try to avoid any additional work while I wait for the model’s capabilities to improve.</p>

<p>I was however surprised when upon one such occasion I realized that the agent itself had itself gotten frustrated at forgetting my writing style and turned it into a flake check.</p>

<p style="--image-width: 20rem"><a href="/assets/images/scooby_doo_code_comment_meme.png"><img src="/assets/images/scooby_doo_code_comment_meme.png" alt="scooby doo code comment meme" /></a></p>

<p>During the creation of <a href="https://omnibin.dev">omnibin.dev</a>, I noticed the agent had created <a href="https://github.com/fzakaria/omnibin/blob/fc56e228eaf26407d976d59f4dcda306ead87345/tools/check-prose.py">tools/check-prose.py</a> to check for banned words and phrases in the source tree. It is a simple Python script that walks the repo and fails on a banned word, phrase, or a single em dash anywhere.</p>

<p>I had begun setting up a distinct pattern of how I like my repositories laid out so it was funny to see it automatically include this check in the CI of my build.</p>

<p>Despite the <em>bitter lesson</em>, I might try leveraging this check in my other repos. What I like about deterministic checks rather than a markdown context-file is that it’s verifyable and reproducible. I’m not left guessing whether the agent is following my style or not. I can run the check myself and see if it passes or fails.</p>

<h2 id="honesty-is-a-mirror">Honesty is a mirror</h2>

<p>Let’s be honest with myself and do some introspection. How much has AI-isms crept into my writing? I applied the same check to my own blog posts.</p>

<table>
  <thead>
    <tr>
      <th>Years</th>
      <th style="text-align: right">Posts</th>
      <th style="text-align: right">Em dashes</th>
      <th style="text-align: right">Banned words</th>
      <th style="text-align: right">Banned phrases</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2020-2024</td>
      <td style="text-align: right">64</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">17</td>
      <td style="text-align: right">13</td>
    </tr>
    <tr>
      <td>2025-2026</td>
      <td style="text-align: right">91</td>
      <td style="text-align: right">21</td>
      <td style="text-align: right">54</td>
      <td style="text-align: right">16</td>
    </tr>
  </tbody>
</table>

<p>Clearly I was not a fan of em dashes in the last five years, then suddenly I found 21 uses in two, which happen to coincide with the proliferation of AI.<sup id="fnref:leverage"><a href="#fn:leverage" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> 🤔</p>

<p>We seem to be all worried about how many em dashes are in our text. <a href="https://en.wikipedia.org/wiki/Goodhart%27s_law">Goodhart’s law</a> teaches us that at that point, that is no longer a good measure of AI slop.</p>

<p>Treat each other kindly.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:discourse">
      <p>Seems like whether or not you use AI, I feel more compelled just to disclose I did to avoid the “you didn’t disclose it” argument. Such is life. <a href="#fnref:discourse" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:leverage">
      <p>The top offender is <code class="language-plaintext highlighter-rouge">leverage</code> at 51 hits. I used it in 2021 and 2022 too. Some tells are just mine. <a href="#fnref:leverage" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Warning If you are pissed off reading this, you can start here and work your way back. I am using AI a lot. I am public about my use of it and when required make the necessary disclosures: “Assited-By” trailers or having to say “I used an AI to help write this software” on every Discourse post.1 Seems like whether or not you use AI, I feel more compelled just to disclose I did to avoid the “you didn’t disclose it” argument. Such is life. &#8617;]]></summary></entry><entry><title type="html">Every package is already installed</title><link href="https://fzakaria.com/2026/09/24/every-package-is-already-installed" rel="alternate" type="text/html" title="Every package is already installed" /><published>2026-09-24T19:14:00-07:00</published><updated>2026-09-24T19:14:00-07:00</updated><id>https://fzakaria.com/2026/09/24/every-package-is-already-installed</id><content type="html" xml:base="https://fzakaria.com/2026/09/24/every-package-is-already-installed"><![CDATA[<blockquote class="alert alert-note">
  <p><strong>tl;dr;</strong> <a href="https://github.com/fzakaria/omnibin">omnibin</a> is a FUSE filesystem that puts <strong>every binary nixpkgs ever shipped</strong> on your <code class="language-plaintext highlighter-rouge">$PATH</code>. Nothing is installed. Nothing needs building. 0 bytes on disk until something actually reads a file. 😈</p>
</blockquote>

<p>It’s 2026, why am I still installing packages individually?<sup id="fnref:dhh"><a href="#fn:dhh" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p style="--image-width: 20rem"><a href="/assets/images/dhh_yelling_about_doing_little.png"><img src="/assets/images/dhh_yelling_about_doing_little.png" alt="dhh yelling about how little he is doing" /></a></p>

<p>Why must I go through the ritual of adding a package to my <code class="language-plaintext highlighter-rouge">configuration.nix</code>, running <code class="language-plaintext highlighter-rouge">nix-shell</code> or succumb to the hellscape of <code class="language-plaintext highlighter-rouge">nix-env -iA</code>.</p>

<!--more-->

<p>Nix gives us the power of having packages installed side-by-side without conflict. Why do I have to pick which ones I want to install?</p>

<p>Why can’t I just have them all?</p>

<p>What if the machine just had all of them?</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix run github:fzakaria/omnibin
<span class="go">omnibin: tree at /run/user/1000/omnibin, cache at /home/you/.cache/omnibin

</span><span class="gp">$</span><span class="w"> </span><span class="nb">ls</span> /omnibin/bin | <span class="nb">wc</span> <span class="nt">-l</span>
<span class="go">51468

</span><span class="gp">$</span><span class="w"> </span>python3 <span class="nt">--version</span>
<span class="go">Python 3.14.6

</span><span class="gp">$</span><span class="w"> </span>python3@3.6.2 <span class="nt">--version</span>
<span class="go">Python 3.6.2
</span></code></pre></div></div>

<p>That is <u>over fifty thousand</u><sup id="fnref:larger"><a href="#fn:larger" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> top-level binaries available on my <code class="language-plaintext highlighter-rouge">$PATH</code>, from 2013 to 2026 built by <a href="https://github.com/NixOS/nixpkgs">Nixpkgs</a>, available on-demand, without installing anything.</p>

<p style="--image-width: 20rem"><a href="/assets/images/oprah_every_version_omnibin.png"><img src="/assets/images/oprah_every_version_omnibin.png" alt="oprah shouting you get every version" /></a></p>

<p>This is the magic 🧙‍♂️ of <a href="https://nixos.org">Nix</a>, but it’s not restricted to Nix.</p>

<p>Everyone seems to still love <a href="https://www.docker.com/">Docker</a> and <a href="https://www.opencontainers.org/">OCI</a>, why am I still picking which base image to use? Why can’t I just have them all?</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># syntax=docker/dockerfile:1</span>
<span class="k">FROM</span><span class="s"> fmzakari/omnibin:latest</span>

<span class="k">COPY</span><span class="s"> &lt;&lt;'SH' /demo.sh</span>
python3@3.6.2 -c 'import sys; print(sys.version.split()[0])'
jq --version
gcc@10.2.0 --version | head -1
SH

<span class="k">CMD</span><span class="s"> ["bash", "/demo.sh"]</span>
</code></pre></div></div>

<p>Is this the ultimate agent harness? It’s a container with everything in it, right from the start. Try it at <a href="https://hub.docker.com/r/fmzakari/omnibin">fmzakari/omnibin</a>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>docker build <span class="nt">-t</span> example <span class="nb">.</span>
<span class="gp">$</span><span class="w"> </span>docker run <span class="nt">--rm</span> <span class="nt">--device</span> /dev/fuse <span class="nt">--cap-add</span> SYS_ADMIN example
<span class="go">3.6.2
jq-1.8.1
gcc (GCC) 10.2.0
</span></code></pre></div></div>

<p>Of course, I cannot forget our NixOS friends. You no longer have to curate your <code class="language-plaintext highlighter-rouge">environment.systemPackages</code> or <code class="language-plaintext highlighter-rouge">home.packages</code>, you can just have them all.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">imports</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">inputs</span><span class="o">.</span><span class="nv">omnibin</span><span class="o">.</span><span class="nv">nixosModules</span><span class="o">.</span><span class="nv">default</span> <span class="p">];</span>
  <span class="nv">services</span><span class="o">.</span><span class="nv">omnibin</span><span class="o">.</span><span class="nv">enable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What is “package management” if every package is already installed?</p>

<h2 id="what-is-this-sorcery">What is this sorcery?</h2>

<p>Turns out that Hydra writes a <code class="language-plaintext highlighter-rouge">.ls</code> file next to every single narinfo on <a href="https://cache.nixos.org">cache.nixos.org</a> that describes the contents of the archive as JSON:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-s</span> <span class="nt">--compressed</span> https://cache.nixos.org/3n4qphl9s728sz8frmpqqrv9b1m87g68.ls | jq
<span class="go">{
  "root": {
    "entries": {
      "bin": {
        "entries": {
          "python3": { "target": "python3.14", "type": "symlink" },
          "python3.14": { "executable": true, "size": 14264, "type": "regular" }
</span></code></pre></div></div>

<p>That metadata turns out to be the perfect index for a <a href="https://www.kernel.org/doc/html/next/filesystems/fuse.html">FUSE filesystem</a> that can lazily fetch the NARs from the cache and unpack them on-demand. 🤓</p>

<p>None of this would mean anything without <a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a>, which already resolves any <code class="language-plaintext highlighter-rouge">(attribute, version)</code> in nixpkgs history to the store path Hydra built for it on <a href="https://cache.nixos.org">cache.nixos.org</a>.</p>

<p>When you combine the two, you get a filesystem that can answer the question “where is <code class="language-plaintext highlighter-rouge">python3@3.6.2</code>” and then fetch it from the cache and unpack it for you, all without ever having to install it.</p>

<p>I crawled all of it the <code class="language-plaintext highlighter-rouge">.ls</code> files in under twelve minutes. 🤯</p>

<p>Once you have that, the filesystem writes itself:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">ls</span> /nix/store/2lb6nn8ivk1alhckv43n7734lqwbw7h9-python3-3.6.2/bin
<span class="go">2to3      idle     pydoc     python   python3.6         python3-config  pyvenv
2to3-3.6  idle3    pydoc3    python3  python3.6-config  python-config   pyvenv-3.6
          idle3.6  pydoc3.6           python3.6m        python3.6m-config
</span></code></pre></div></div>

<p>That is CPython 3.6.2, from 2017. That <code class="language-plaintext highlighter-rouge">ls</code> <em>downloaded nothing</em>, it is answered from the pre-crawled index.</p>

<h2 id="do-not-ls-the-tree">Do not <code class="language-plaintext highlighter-rouge">ls</code> the tree</h2>

<p>Agents are “a thing”. Making them useful is a thing. Making them useful without installing anything is a thing.</p>

<p>If your agent tried to <code class="language-plaintext highlighter-rouge">ls /omnibin/bin</code> and stat every single entry, it would have a really bad time. There are 881,933 binaries in the tree, and it would take a long time to stat them all.</p>

<p style="--image-width: 20rem"><a href="/assets/images/zoolander_omnibin_meme.png"><img src="/assets/images/zoolander_omnibin_meme.png" alt="zoolander meme of saying how hot agents are" /></a></p>

<p>To help the agents out a bit, <code class="language-plaintext highlighter-rouge">ls /omnibin/bin</code> lists only the bare names, one per executable, each resolving to the newest package that provides it.</p>

<p>The versioned forms all resolve, but they are not listed. For example, <code class="language-plaintext highlighter-rouge">python3</code> resolves to the latest Python 3, which is 3.14.6 at the time of writing, but <code class="language-plaintext highlighter-rouge">python3@3.6.2</code> resolves to the 2017 version.</p>

<p>For everything else there is the index, which is sitting right there in the mount:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sqlite3 /omnibin/index.db <span class="se">\</span>
<span class="go">    "SELECT attr, version
    FROM bins
    WHERE name = 'python3'
    ORDER BY version"
</span></code></pre></div></div>

<p>That UX is a little rough, so you can also use the <code class="language-plaintext highlighter-rouge">omnibin</code> CLI to query the index:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>omnibin which python3
<span class="go">/nix/store/gxzhl7aaiid7zp3y47jqqiq7zg5mqpwp-python3-3.14.6/bin/python3

</span><span class="gp">$</span><span class="w"> </span>omnibin which <span class="nt">--all</span> python3 | <span class="nb">wc</span> <span class="nt">-l</span>
<span class="go">610

</span><span class="gp">$</span><span class="w"> </span>omnibin which <span class="nt">--all</span> ffmpeg | <span class="nb">head</span> <span class="nt">-2</span>
<span class="go">ffmpeg@3.1.7  ffmpeg  0.4 MB  /nix/store/0adpc3…-ffmpeg-3.1.7-bin/bin/ffmpeg
ffmpeg@3.2.4  ffmpeg  0.4 MB  /nix/store/nhfgdv…-ffmpeg-3.2.4-bin/bin/ffmpeg
</span></code></pre></div></div>

<p>Lastly, there is a <a href="https://github.com/fzakaria/omnibin/blob/b3acd8773a92b9acac9adc80997fac56ef9bac6b/src/mount-readme.md">/omnibin/README.md</a> whose entire job is to tell whatever is exploring the filesystem to stop exploring the filesystem and query the database instead. 🤖</p>

<h2 id="whats-the-catch">What’s the catch?</h2>

<p>At this point it should be obvious, but you pay for this on startup for the first access.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">time </span>python3@3.6.2 <span class="nt">-c</span> <span class="s1">'import sys; print(sys.version.split()[0])'</span>
<span class="go">3.6.2
real    0m2.690s

</span><span class="gp">$</span><span class="w"> </span><span class="nb">time </span>python3@3.6.2 <span class="nt">-c</span> <span class="s1">'print(6*7)'</span>
<span class="go">42
real    0m0.035s
</span></code></pre></div></div>

<p>The first run took 2.7 seconds to fetch the NARs and unpack them, the second run was instantaneous because the store paths were already present.</p>

<p>Other than that? Not really, which is pretty amazing.</p>

<p>For any long-lived machine, you would expect your <code class="language-plaintext highlighter-rouge">/nix/store</code> to already be warmed up with the packages you need, so the first access penalty is not a big deal.</p>

<p>I remember one of the first things that blew my mind and sold me on Nix, was seeing a demo by <a href="https://github.com/burke">@burke</a> on <a href="https://github.com/nix-community/comma">comma</a>. The capability to test a package, at a single nixpkgs revision, without “installing it”; revolutionary! I believe this to be a spiritual successor and I hope to imbue others with the same sense of wonder and amazement that I felt back then as a beacon of the power of Nix.</p>

<p>The repo is at <a href="https://github.com/fzakaria/omnibin">github.com/fzakaria/omnibin</a>.</p>

<p>Please <code class="language-plaintext highlighter-rouge">ls</code> responsibly.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:dhh">
      <p>Yes, I am a little inspired after watching <a href="https://www.youtube.com/watch?v=vDjW_dRyKXY">DHH’s keynote at RailsConf 2026</a>. I feel the same way about package management. <a href="#fnref:dhh" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:larger">
      <p>There are actually 881,933 binaries in the tree, but <code class="language-plaintext highlighter-rouge">ls /omnibin/bin</code> only lists the latest version of each binary. The versioned forms are still available, but they are not listed. <a href="#fnref:larger" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[tl;dr; omnibin is a FUSE filesystem that puts every binary nixpkgs ever shipped on your $PATH. Nothing is installed. Nothing needs building. 0 bytes on disk until something actually reads a file. 😈 It’s 2026, why am I still installing packages individually?1 Why must I go through the ritual of adding a package to my configuration.nix, running nix-shell or succumb to the hellscape of nix-env -iA. Yes, I am a little inspired after watching DHH’s keynote at RailsConf 2026. I feel the same way about package management. &#8617;]]></summary></entry><entry><title type="html">A build graph that rolls dice</title><link href="https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice" rel="alternate" type="text/html" title="A build graph that rolls dice" /><published>2026-09-20T18:00:00-07:00</published><updated>2026-09-20T18:00:00-07:00</updated><id>https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice</id><content type="html" xml:base="https://fzakaria.com/2026/09/20/a-build-graph-that-rolls-dice"><![CDATA[<p>We are right around the corner from <a href="https://2026.nixcon.org/">NixCon 2026</a>. Another year where I sadly won’t be present.</p>

<p>I looked at the lineup and saw quite a few talks on dynamic derivations, along with some recent other posts on the topic such as <a href="https://blog.obsidian.systems/cargo-dyndrv-a-beginning/">cargo-dyndrv</a> which had me thinking about revisting the topic since my <a href="/2025/03/10/an-early-look-at-nix-dynamic-derivations">earlier</a> <a href="/2025/03/11/nix-dynamic-derivations-a-practical-application">posts</a> on the subject.</p>

<p>I wanted to better understand the implications of dynamic derivations and how they change the build graph. Originally, I was focused on how it could replace the <em>lang2nix</em> style of build graph generation, but I realized that the implications are much broader than that.</p>

<p>The graph does not need to be known up front. It can be defined as the build progresses. 🧐</p>

<p>What do I mean?</p>

<p>Traditionally, Nix requires you to ask it to build anything and it will tell you exactly the steps it will take ahead of time as the “derivations” that comprise the build graph.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">-q</span> <span class="nt">--requisites</span> <span class="se">\</span>
<span class="gp">    $</span><span class="o">(</span>nix-instantiate <span class="s1">'&lt;nixpkgs&gt;'</span> <span class="nt">-A</span> hello<span class="o">)</span> | <span class="nb">grep</span> <span class="nt">-c</span> <span class="s1">'\.drv$'</span>
<span class="go">196
</span></code></pre></div></div>

<p>This is an important property of Nix. It is what makes it possible to reason about builds without running them by understanding what <em>will</em> be built.</p>

<p>Many Nix tools rely on this property via <code class="language-plaintext highlighter-rouge">nix build --dry-run</code>. 
For instance, <a href="https://github.com/Gabriella439/nix-diff">nix-diff</a> tells you why two closures differ <strong>without building either</strong>.</p>

<p>Why does dynamic derivations change this?</p>

<h2 id="applicative-monadic-bind">Applicative, monadic, bind</h2>

<p>In functional languages it’s very easy to get abstract and use <em>fancy</em> words like “applicative” and “monadic” to describe the difference between types of computation. The difference is subtle, but it is profound.</p>

<p>Nix traditionally was an “applicative” build system. Dynamic derivations make it a “monadic” build system. The difference is that in an applicative build system, the entire build graph is known up front, while in a monadic build system, the graph can be defined as the build progresses.<sup id="fnref:carte"><a href="#fn:carte" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>One way to think about the difference is to look at the type signatures of the two operations that define them:</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;*&gt;</span> <span class="n">apply</span> <span class="o">::</span> <span class="n">f</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">f</span> <span class="n">a</span>          <span class="o">-&gt;</span> <span class="n">f</span> <span class="n">b</span>
<span class="o">&gt;&gt;=</span> <span class="n">bind</span>  <span class="o">::</span> <span class="n">m</span> <span class="n">a</span>        <span class="o">-&gt;</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">m</span> <span class="n">b</span><span class="p">)</span>   <span class="o">-&gt;</span> <span class="n">m</span> <span class="n">b</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">apply</code> (applicative) takes <code class="language-plaintext highlighter-rouge">f a</code> a value known ahead of time and returns the result <code class="language-plaintext highlighter-rouge">f b</code>. You can see the entire graph before you run it.</p>

<p><code class="language-plaintext highlighter-rouge">bind</code> (monadic) takes <code class="language-plaintext highlighter-rouge">(a -&gt; m b)</code>, a <em>function</em>, to return <code class="language-plaintext highlighter-rouge">m b</code>. You cannot see the entire graph before you run it.</p>

<p><strong>Applicative</strong> can be thought of writing the shopping list before you leave the house. You read the recipe, you write down every ingredient, you drive to the store once. The list is a function of the recipe and nothing else.</p>

<p><strong>Monadic</strong> is a recipe with a step that says <em>taste it, and if it is too salty, go buy a potato</em>. You cannot write that shopping list up front. Whether the potato is on it depends on the saltiness, and the saltiness does not exist until you have already done some of the cooking.</p>

<p>Nix used to be solely the former, dynamic derivations add the latter.</p>

<h2 id="actually-nix-always-had-bind">Actually, nix always had bind</h2>

<p style="--image-width: 20rem"><a href="/assets/images/always_has_been_bind_meme.png"><img src="/assets/images/always_has_been_bind_meme.png" alt="always has been bind meme" /></a></p>

<p>Okay, I guess I should have said “Nix is now monadic in the scheduler”. Nix has always had bind in the evaluator. We have been doing monadic builds for years. We call it
<a href="/2020/10/20/nix-parallelism-import-from-derivation">import from derivation</a>.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span>
  <span class="nv">inner</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">runCommand</span> <span class="s2">"inner"</span> <span class="p">{}</span> <span class="s2">"sleep 10; echo hi &gt; $out"</span><span class="p">;</span>
<span class="kn">in</span>
  <span class="nv">pkgs</span><span class="o">.</span><span class="nv">runCommand</span> <span class="s2">"outer"</span> <span class="p">{}</span> <span class="s2">"echo </span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">readFile</span> <span class="nv">inner</span><span class="si">}</span><span class="s2"> &gt; $out"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">builtins.readFile</code> on a derivation output is a <strong>bind</strong> in exactly the sense above: what to build next is a function of a value that does not exist yet. The Nix evaluator cannot produce the graph without that value, and the only way to get it is to stop evaluating and run a builder.</p>

<p>Unfortunately, this has a lot of footguns, such as causing
<a href="/2025/03/10/an-early-look-at-nix-dynamic-derivations"><code class="language-plaintext highlighter-rouge">nix-instantiate</code> to take ten seconds</a>,
and why nixpkgs <em>bans the technique outright</em>.</p>

<p>Whereas <em>import from derivation</em> is a bind in the evaluator, dynamic derivations now adds bind in the scheduler. The difference is that the scheduler can run builders in parallel, ship them to remote machines, and it can substitute their results from a cache. The evaluator cannot do any of that.</p>

<p>Dynamic derivations do not add the bind. The bind was always there. What changes is which layer performs it. 🤓</p>

<h2 id="lets-roll-some-dice">Let’s roll some dice</h2>

<p>Many of the examples of dynamic derivations have been focused on build graph generation via <em>lang2nix</em> tooling, but I wanted to explore the implications of dynamic derivations in a more general sense.</p>

<p>In the true <em>monadic</em> sense, the build graph can be defined as the build progresses. The next step in the build graph can depend on the result of a previous step.</p>

<p>Here is a really simple example, <code class="language-plaintext highlighter-rouge">chain.nix</code> &amp; <code class="language-plaintext highlighter-rouge">step.sh</code>, that rolls a die and either stops or continues the chain by adding a new derivation step to the build graph. The depth of the chain is random, and the result of the build is how deep we got.</p>

<p>Only the leftmost box exists when you run <code class="language-plaintext highlighter-rouge">nix-instantiate</code>. Everything to the
right of it is written by a builder, while the build is already underway.</p>

<div class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">LR</span>
  <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">box</span> <span class="n">style</span><span class="p">=</span><span class="nv">rounded</span> <span class="n">fontname</span><span class="p">=</span><span class="s2">"sans-serif"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">10</span> <span class="n">margin</span><span class="p">=</span><span class="s2">"0.16,0.10"</span><span class="o">]</span>
  <span class="k">edge</span> <span class="o">[</span><span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.7</span> <span class="n">fontname</span><span class="p">=</span><span class="s2">"sans-serif"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#6f685b"</span><span class="o">]</span>

  <span class="k">subgraph</span> <span class="nv">cluster_eval</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"in chain.nix"</span>
    <span class="n">fontname</span><span class="p">=</span><span class="s2">"sans-serif"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#6f685b"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#6f685b"</span>
    <span class="nv">step</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"step-N\nroll a d6"</span><span class="o">]</span>
  <span class="p">}</span>

  <span class="k">subgraph</span> <span class="nv">cluster_run</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"written by step.sh, mid-build"</span>
    <span class="n">fontname</span><span class="p">=</span><span class="s2">"sans-serif"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#6f685b"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#6f685b"</span>
    <span class="nv">res</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"dice-result\necho N &gt; $out"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#1a7f37"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#1a7f37"</span><span class="o">]</span>
    <span class="nv">pass</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"passthrough-N\ncp $inner $out"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
    <span class="nv">next</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"step-N+1\ndepth + 1"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
  <span class="p">}</span>

  <span class="nv">more</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"…"</span> <span class="n">shape</span><span class="p">=</span><span class="nv">plaintext</span><span class="o">]</span>

  <span class="nv">step</span> <span class="o">-&gt;</span> <span class="nv">res</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"six"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#1a7f37"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#1a7f37"</span><span class="o">]</span>
  <span class="nv">step</span> <span class="o">-&gt;</span> <span class="nv">pass</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"anything else"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
  <span class="nv">pass</span> <span class="o">-&gt;</span> <span class="nv">next</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"input: ^out^out"</span> <span class="n">style</span><span class="p">=</span><span class="nv">dashed</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
  <span class="nv">next</span> <span class="o">-&gt;</span> <span class="nv">more</span> <span class="o">[</span><span class="n">style</span><span class="p">=</span><span class="nv">dashed</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
<span class="p">}</span>
</code></pre></div></div>

<details>
  <summary>Show chain.nix</summary>

  <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">depth</span> <span class="o">?</span> <span class="mi">1</span>
<span class="p">,</span> <span class="nv">pkgs</span> <span class="o">?</span> <span class="kr">import</span> <span class="o">&lt;</span><span class="nv">nixpkgs</span><span class="o">&gt;</span> <span class="p">{</span> <span class="p">}</span>
<span class="p">,</span> <span class="nv">bash</span> <span class="o">?</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">bash</span>
<span class="p">,</span> <span class="nv">coreutils</span> <span class="o">?</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">coreutils</span>
<span class="p">,</span> <span class="nv">nix</span> <span class="o">?</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">nixVersions</span><span class="o">.</span><span class="nv">latest</span>
<span class="p">,</span> <span class="nv">chain</span> <span class="o">?</span> <span class="sx">./chain.nix</span>
<span class="p">,</span> <span class="nv">step</span> <span class="o">?</span> <span class="sx">./step.sh</span>
<span class="p">}:</span>
<span class="kd">let</span>
  <span class="nv">roller</span> <span class="o">=</span> <span class="kr">derivation</span> <span class="p">{</span>
    <span class="nv">name</span> <span class="o">=</span> <span class="s2">"step-</span><span class="si">${</span><span class="kr">toString</span> <span class="nv">depth</span><span class="si">}</span><span class="s2">.drv"</span><span class="p">;</span>
    <span class="nv">system</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">currentSystem</span><span class="p">;</span>
    <span class="nv">builder</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">bash</span><span class="si">}</span><span class="s2">/bin/bash"</span><span class="p">;</span>
    <span class="nv">args</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"-e"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">step</span><span class="si">}</span><span class="s2">"</span> <span class="p">];</span>

    <span class="nv">DEPTH</span> <span class="o">=</span> <span class="kr">toString</span> <span class="nv">depth</span><span class="p">;</span>
    <span class="nv">BASHPKG</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">bash</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">COREUTILS</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">coreutils</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">NIXPKG</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">nix</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">CHAIN</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">chain</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">STEP</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">step</span><span class="si">}</span><span class="s2">"</span><span class="p">;</span>
    <span class="nv">PATH</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">coreutils</span><span class="si">}</span><span class="s2">/bin:</span><span class="si">${</span><span class="nv">nix</span><span class="si">}</span><span class="s2">/bin"</span><span class="p">;</span>

    <span class="c"># The builder instantiates derivations, so it needs a store to talk to.</span>
    <span class="nv">requiredSystemFeatures</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"recursive-nix"</span> <span class="p">];</span>

    <span class="c"># This derivation's output is a .drv file. That is what makes it dynamic.</span>
    <span class="nv">__contentAddressed</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">outputHashMode</span> <span class="o">=</span> <span class="s2">"text"</span><span class="p">;</span>
    <span class="nv">outputHashAlgo</span> <span class="o">=</span> <span class="s2">"sha256"</span><span class="p">;</span>
  <span class="p">};</span>
<span class="kn">in</span>
<span class="kr">builtins</span><span class="o">.</span><span class="nv">outputOf</span> <span class="nv">roller</span><span class="o">.</span><span class="nv">outPath</span> <span class="s2">"out"</span>
</code></pre></div>  </div>

</details>

<details>
  <summary>Show step.sh</summary>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set</span> <span class="nt">-eu</span>
<span class="nb">export </span><span class="nv">NIX_CONFIG</span><span class="o">=</span><span class="s1">'experimental-features = nix-command ca-derivations dynamic-derivations'</span>

<span class="nv">roll</span><span class="o">=</span><span class="k">$((</span> <span class="si">$(</span><span class="nb">od</span> <span class="nt">-An</span> <span class="nt">-N1</span> <span class="nt">-tu1</span> &lt; /dev/urandom<span class="si">)</span> <span class="o">%</span> <span class="m">6</span> <span class="o">+</span> <span class="m">1</span> <span class="k">))</span>
<span class="nb">echo</span> <span class="s2">"depth </span><span class="nv">$DEPTH</span><span class="s2"> rolled a </span><span class="nv">$roll</span><span class="s2">"</span>

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$roll</span><span class="s2">"</span> <span class="nt">-eq</span> 6 <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
  <span class="c"># Six. Stop. The answer is how deep we got.</span>
  <span class="nb">cat</span> <span class="o">&gt;</span> answer.nix <span class="o">&lt;&lt;</span><span class="no">NIX</span><span class="sh">
let bash = builtins.storePath </span><span class="nv">$BASHPKG</span><span class="sh">; in
derivation {
  name = "dice-result";
  system = builtins.currentSystem;
  builder = "</span><span class="se">\$</span><span class="sh">{bash}/bin/bash";
  args = [ "-c" "echo </span><span class="nv">$DEPTH</span><span class="sh"> &gt; </span><span class="se">\$</span><span class="sh">out" ];
  __contentAddressed = true;
  outputHashMode = "recursive";
  outputHashAlgo = "sha256";
}
</span><span class="no">NIX
</span><span class="k">else</span>
  <span class="c"># Not a six. My answer is whatever the next level answers.</span>
  <span class="nb">cat</span> <span class="o">&gt;</span> answer.nix <span class="o">&lt;&lt;</span><span class="no">NIX</span><span class="sh">
let
  bash = builtins.storePath </span><span class="nv">$BASHPKG</span><span class="sh">;
  coreutils = builtins.storePath </span><span class="nv">$COREUTILS</span><span class="sh">;

  # This is the bind. Asking chain.nix for the next depth neither builds it
  # nor evaluates it here: it yields a placeholder standing for "whatever
  # depth </span><span class="k">$((</span> DEPTH <span class="o">+</span> <span class="m">1</span> <span class="k">))</span><span class="sh"> eventually answers".
  inner = import (builtins.storePath </span><span class="nv">$CHAIN</span><span class="sh">) {
    inherit bash coreutils;
    depth = </span><span class="k">$((</span> DEPTH <span class="o">+</span> <span class="m">1</span> <span class="k">))</span><span class="sh">;
    nix = builtins.storePath </span><span class="nv">$NIXPKG</span><span class="sh">;
    chain = builtins.storePath </span><span class="nv">$CHAIN</span><span class="sh">;
    step = builtins.storePath </span><span class="nv">$STEP</span><span class="sh">;
  };
in
derivation {
  name = "dice-passthrough";
  system = builtins.currentSystem;
  builder = "</span><span class="se">\$</span><span class="sh">{bash}/bin/bash";
  args = [ "-c" "cp </span><span class="se">\$</span><span class="sh">inner </span><span class="se">\$</span><span class="sh">out" ];
  PATH = "</span><span class="se">\$</span><span class="sh">{coreutils}/bin";
  inherit inner;
  __contentAddressed = true;
  outputHashMode = "recursive";
  outputHashAlgo = "sha256";
}
</span><span class="no">NIX
</span><span class="k">fi

</span><span class="nb">cp</span> <span class="s2">"</span><span class="si">$(</span>nix-instantiate answer.nix<span class="si">)</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$out</span><span class="s2">"</span>
</code></pre></div>  </div>

</details>

<p>The general idea of this derivation is:</p>

<ul>
  <li><strong>roll a six</strong> and we write a derivation that echoes the depth. Ordinary, nothing dynamic about it. This terminates the build graph.</li>
  <li><strong>roll anything else</strong> and we write a <em>passthrough</em>: a derivation whose only job is <code class="language-plaintext highlighter-rouge">cp $inner $out</code>, where <code class="language-plaintext highlighter-rouge">inner</code> is <code class="language-plaintext highlighter-rouge">import chain.nix { depth = n + 1; }</code>.</li>
</ul>

<p>Either way the file copied into <code class="language-plaintext highlighter-rouge">$out</code> is a <code class="language-plaintext highlighter-rouge">.drv</code>, so <code class="language-plaintext highlighter-rouge">builtins.outputOf</code> works the same on a chain that stopped and a chain that kept going.<sup id="fnref:depth"><a href="#fn:depth" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>We can see the build graph grow in the denominator as the build progresses.</p>

<p><img src="/assets/images/nix-dyndrv-dice.gif" alt="A terminal running nix build on the dice chain. The progress counter starts at one derivation and climbs past fifty as each level emits the next one." /></p>

<p>If we try to introspect the graph with <code class="language-plaintext highlighter-rouge">--dry-run</code> or <code class="language-plaintext highlighter-rouge">nix-store -q --tree</code>, we get nothing. Since the graph is not known yet, our tools cannot see it.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build <span class="nt">--store</span> /tmp/dice <span class="nt">-f</span> ./default.nix <span class="nt">--dry-run</span>
<span class="go">warning: Ignoring dynamic derivation /nix/store/vnm4l32g…-step-1.drv.drv^out
</span><span class="gp">while querying missing paths;</span><span class="w"> </span>not yet implemented
</code></pre></div></div>
<p>Dynamic derivations introduce a second graph that exists only after the build, and the ecosystem has no
way to introspect it yet.</p>

<p>I ran the build 900 times and logged the length of the chain. Rolling a six-sided die is one of the most classic ways to simulate geometric decay and we can see the results in the histogram below. The mean is 6.18, median 4, longest 46.</p>

<pre title="Distribution of chain lengths over 893 builds against fresh stores: a geometric decay from 165 runs of length one out to a single run of length 46, with the theoretical geometric curve overlaid"><code class="language-plotnine">import pandas as pd
from plotnine import *

# 893 chains against fresh stores. `nix build --store /tmp/dicebench/$i`,
# eight at a time; the result file holds the chain length.
counts = {1: 165, 2: 113, 3: 100, 4: 70, 5: 93, 6: 58, 7: 45, 8: 40, 9: 24,
          10: 27, 11: 29, 12: 16, 13: 20, 14: 14, 15: 18, 16: 9, 17: 8, 18: 7,
          19: 5, 20: 4, 21: 5, 22: 3, 23: 1, 24: 1, 25: 5, 28: 1, 29: 3,
          30: 1, 31: 2, 33: 1, 37: 1, 38: 1, 43: 1, 45: 1, 46: 1}
n = sum(counts.values())

df = pd.DataFrame({"length": list(counts), "runs": list(counts.values())})
# A byte from /dev/urandom mod 6 is very slightly unfair: 42 of 256 bytes
# roll a six, not 42.67, so the mean is 6.10 rather than 6.
p = 42 / 256
geo = pd.DataFrame({"length": range(1, 47)})
geo["runs"] = n * (1 - p) ** (geo.length - 1) * p

plot = (
    ggplot(df, aes("length", "runs"))
    + geom_col(fill="#b1201d", width=0.8)
    + geom_line(geo, aes("length", "runs"), color="#e08a45", size=0.9)
    + labs(x="derivations in the chain", y="runs out of 900")
)
plot.width, plot.height = 7.0, 3.4
</code></pre>

<p><em>The math maths.</em></p>

<h2 id="beyond-lang2nix">Beyond lang2nix</h2>

<p>Every dynamic derivations demo so far, mine included, has been a build system:
<a href="https://github.com/fzakaria/MakeNix">MakeNix</a> for C,
<a href="https://github.com/fzakaria/NpmNix">NpmNix</a> for node,
<a href="https://github.com/obsidiansystems/cargo-dyndrv">cargo-dyndrv</a> for Rust,
<a href="https://github.com/pdtpartners/nix-ninja">nix-ninja</a> for ninja. That is a reasonable place to start but it does not capture the full power of the primitive. What other ideas can we explore?</p>

<p><strong>Mario.</strong> In <a href="/2026/08/05/super-mario-derivations">Super Mario Derivations</a>
the attribute path is the button sequence and I have to supply the press count.
The dynamic version emulates until Mario dies. The stopping condition is data,
discovered mid-build, and the run is however long it turns out to be.</p>

<p><strong>Searching a state space.</strong> Swap the die for a predicate and you have a search
where each frontier node is a derivation. Model checking, puzzle solvers, a
fuzzer that only expands inputs which found new coverage. States you reach twice
collapse onto one store path, and they survive a reboot.</p>

<p><strong>Crawling.</strong> Fetch a page, parse the links, emit one derivation per link. The
frontier is discovered rather than declared, the crawl resumes because the store
remembers every page already fetched, and the dependency graph of the result is
the link graph.</p>

<p>What are the bounds of this primitive? I don’t know.</p>

<p>The size of the graph now is only limited by whether Nix stops emitting successors. There is no depth limit for the store layer, no <code class="language-plaintext highlighter-rouge">max-call-depth</code> equivalent. My longest honest chain in the die-roll was 46 deep, but I had rigged examples that went to 500. How far can it go?</p>

<p>I am so brainwashed to thinking about making a plan before I start for my build systems, that the idea of making it up as I go is a little scary. 😨</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:carte">
      <p>The paper <a href="https://www.microsoft.com/en-us/research/uploads/prod/2018/03/build-systems.pdf">Build Systems à la Carte</a> is the defacto read on this topic. <a href="#fnref:carte" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:depth">
      <p>We actually need a depth parameter to avoid infinite recursion and so that the store-path of the derivations are different since they are content-addressed. <a href="#fnref:depth" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[We are right around the corner from NixCon 2026. Another year where I sadly won’t be present.]]></summary></entry><entry><title type="html">Visualizing Nix closures</title><link href="https://fzakaria.com/2026/09/15/visualizing-nix-closures" rel="alternate" type="text/html" title="Visualizing Nix closures" /><published>2026-09-15T20:00:00-07:00</published><updated>2026-09-15T20:00:00-07:00</updated><id>https://fzakaria.com/2026/09/15/visualizing-nix-closures</id><content type="html" xml:base="https://fzakaria.com/2026/09/15/visualizing-nix-closures"><![CDATA[<blockquote class="alert alert-note">
  <p><strong>tl;dr</strong> <a href="https://seenix.dev/">seenix.dev</a> lays every byte of a Nix closure out on a map, one pixel per byte, and lets you zoom from a whole NixOS system down to the hex of <code class="language-plaintext highlighter-rouge">libc.so.6</code>. Try <a href="https://seenix.dev/?path=/nix/store/xl1h9i29pgq2q5cszjhm5wpfxfbbqwyi-hello-2.12.3">hello</a>, <a href="https://seenix.dev/?path=/nix/store/5l9n8bw1wifj5kdr8gzlrkk1b510dfiv-firefox-155.0.1">firefox</a> or <a href="https://seenix.dev/?path=/nix/store/5ryb0d1a261bgvxqqd436yx8i7j44qlc-nixos-system-nixos-26.11pre1074086.efe6f071ede9&amp;mode=package">a GNOME desktop</a>. Nothing runs on a server.</p>
</blockquote>

<p>With the advent of LLMs I keep tugging at any <em>crazy</em> question I ask myself. I know there is the <em>anti-AI</em> crowd and they will happily proclaim anything pursued in this vein as “slop” but I am feeling fortunate to be able to explore these questions.</p>

<!--more-->

<p>My recent <em>itch</em> was to ask “what does a Nix closure look like?” and to answer it in a way that is <em>interactive</em> and <em>visual</em>. I wanted to see the bytes, not just the store paths. I had come across <a href="https://binvis.io">binvis.io</a> on Hacker News and I found it a compelling way to look at data. I personally never found a need for it, but I found it fascinating none-the-less.<sup id="fnref:cortesi"><a href="#fn:cortesi" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>The timing for this itch was perfect. I noticed a trending thread on <a href="https://x.com/HSVSphere/status/2045193277977100772?s=20">X</a> where a Python binary seemingly includes <code class="language-plaintext highlighter-rouge">ffmpeg</code> and <code class="language-plaintext highlighter-rouge">ruby</code>. 🤷</p>

<p style="--image-width: 25rem"><a href="/assets/images/remarshal_tweet_hsvsphere.png"><img src="/assets/images/remarshal_tweet_hsvsphere.png" alt="Photo of the tweet of HSVSphere" /></a></p>

<p>I built that tool. You can check it out at <a href="https://seenix.dev/">seenix.dev</a>. It is a single-page web app that runs entirely in your browser, with no server. It fetches the narinfos of a closure and lays them out on a map, one pixel per byte, and lets you zoom in to see the bytes themselves.</p>

<p>We can visualize the closure of that binary, <code class="language-plaintext highlighter-rouge">remarshal</code>, and see if it really does include those two packages. Turns out it does not. The closure is 41 store paths and 234 MiB, with no <code class="language-plaintext highlighter-rouge">ffmpeg</code> and no <code class="language-plaintext highlighter-rouge">ruby</code> among them. Turns out those dependencies are build-time and are not included in the final runtime closure.</p>

<p><a href="/assets/images/seenix-remarshal-package.png"><img src="/assets/images/seenix-remarshal-package.png" alt="remarshal 1.3.0's runtime closure in seenix, coloured by package and with remarshal itself pinned: 41 store paths and 234 MiB, with no ffmpeg and no ruby among them" /></a></p>

<p>We can visualize much larger closures. Here is a GNOME desktop: 1,324 store paths and 5.3 GiB, each colour one package.</p>

<p><a href="/assets/images/seenix-gnome-packages.png"><img src="/assets/images/seenix-gnome-packages.png" alt="A NixOS GNOME system closure drawn as a map: a patchwork of coloured regions, one per store path, each one connected blob" /></a></p>

<p>That picture needed zero NAR downloads. It was laid out in 3 ms from the narinfos alone. 🤯</p>

<h1 id="one-byte-one-pixel">One byte, one pixel</h1>

<p>The “trick” I learned to make this visualization possible, is the <a href="https://en.wikipedia.org/wiki/Hilbert_curve">Hilbert curve</a>. A Hilbert curve is a single, unbroken line that folds back and forth such that it completely fills up a flat square. <strong>It is a fractal</strong>.</p>

<p><img src="/assets/images/hilbert_curve.gif" alt="gif of the hilbert curve" /></p>

<p>Every store path in the closure is sorted by name (the root first) and their NARs are concatenated into one long line of bytes. The Hilbert curve folds that line into a square, so byte <em>n</em> is pixel <em>n</em> along the curve.</p>

<p><img src="/assets/images/seenix-hilbert-layout.png" alt="Three store paths, hello, glibc and libidn2, laid end to end along an 8 by 8 Hilbert curve. Each path fills one connected region, and the curve continues dashed through the padding after the last byte." style="--image-width: 24rem" /></p>

<p>The Hilbert curve has two properties that lend itself nicely to visualize binaries and as a result Nix closures:</p>

<p><strong>Bytes that are near each other in a file stay near each other on the map.</strong> A NAR is a single contiguous range of bytes, so a store path is a single contiguous region on the map. A file inside that store path is a smaller contiguous region, and a section inside that file is smaller still and so forth.</p>

<p><strong>Squares are just byte ranges.</strong> Here’s a tiny 4×4 map. Each number is the byte that lands on that pixel:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 0   1  14  15
 3   2  13  12
 4   7   8  11
 5   6   9  10
</code></pre></div></div>

<p>That means we can easily place a store path on the map by knowing its starting byte and its size. That’s what makes the map cheap to draw.<sup id="fnref:padding"><a href="#fn:padding" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>The layout only needs each path’s <code class="language-plaintext highlighter-rouge">NarSize</code>, which every narinfo carries, so the whole map exists before a single NAR is downloaded. Hovering already tells you which store path you are pointing at, its size, its <em>retained size</em> (the bytes that would leave the closure without it) and a “why is this here” chain back to the root.</p>

<h1 id="zoom-and-enhance">Zoom and enhance</h1>

<p>As you zoom in, the NARs on screen are fetched from the cache and the color fills in. Here is <code class="language-plaintext highlighter-rouge">hello</code>’s closure, most of which is glibc:</p>

<p><a href="/assets/images/seenix-hello-bytes.png"><img src="/assets/images/seenix-hello-bytes.png" alt="hello's closure in bytes mode: speckled regions of black, blue and red where machine code lives, and a large solid blue region of text" /></a></p>

<p>Blue is printable ASCII, red is high bytes, green is control bytes and black is <code class="language-plaintext highlighter-rouge">0x00</code>. The speckled top is machine code. The big solid blue area at the bottom is glibc’s locale data, which is plain text.</p>

<p>Keep zooming and every pixel becomes a byte you can read. Hovering names the file inside the NAR, and for ELF files, the section.</p>

<p><a href="/assets/images/seenix-glibc-hex.png"><img src="/assets/images/seenix-glibc-hex.png" alt="A deep zoom where each cell is one byte printed in hex, with a tooltip reading glibc-2.42-84, lib/libc.so.6, .text" /></a></p>

<p>That is <code class="language-plaintext highlighter-rouge">.text</code> of <code class="language-plaintext highlighter-rouge">libc.so.6</code>, in your browser tab, fetched from <a href="https://cache.nixos.org">cache.nixos.org</a>, <strong>without any server</strong>. 😈</p>

<h1 id="why-though">Why though?</h1>

<p>Does everything need a purpose? Sometimes something is fun to make and to use with no real purpose.</p>

<p>For fun, I even added a Save PNG button, and it saves the view at the canvas’s full resolution. The ultimate ricing of your NixOS system: a pixel image of your desktop closure. Can <a href="https://omarchy.org/">Omarchy</a> do that? 😎</p>

<p>Anything you can export works:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix path-info <span class="nt">-r</span> <span class="nt">--json</span> /run/current-system <span class="o">&gt;</span> closure.json
</code></pre></div></div>

<p>Drop the file and see the map. You can provide additional Nix binary caches to fetch NARs from as well.</p>

<p>The source is at <a href="https://github.com/fzakaria/seenix">github.com/fzakaria/seenix</a>. <a href="https://seenix.dev/?path=/nix/store/5ryb0d1a261bgvxqqd436yx8i7j44qlc-nixos-system-nixos-26.11pre1074086.efe6f071ede9&amp;mode=package">Go look at something big.</a></p>

<p>Build without purpose. Have fun.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:cortesi">
      <p>Aldo Cortesi’s <a href="https://corte.si/posts/visualisation/binvis/">writing on visualising binaries</a> is a great resource on this. <a href="#fnref:cortesi" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:padding">
      <p>This is why the world is always a power of four bytes. hello’s closure is 36 MiB, which fills a bit over half of a 64 MiB square, and the rest is drawn as background. <a href="#fnref:padding" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[tl;dr seenix.dev lays every byte of a Nix closure out on a map, one pixel per byte, and lets you zoom from a whole NixOS system down to the hex of libc.so.6. Try hello, firefox or a GNOME desktop. Nothing runs on a server. With the advent of LLMs I keep tugging at any crazy question I ask myself. I know there is the anti-AI crowd and they will happily proclaim anything pursued in this vein as “slop” but I am feeling fortunate to be able to explore these questions.]]></summary></entry><entry><title type="html">Orange Site Vanity</title><link href="https://fzakaria.com/2026/09/14/orange-site-vanity" rel="alternate" type="text/html" title="Orange Site Vanity" /><published>2026-09-14T15:04:00-07:00</published><updated>2026-09-14T15:04:00-07:00</updated><id>https://fzakaria.com/2026/09/14/orange-site-vanity</id><content type="html" xml:base="https://fzakaria.com/2026/09/14/orange-site-vanity"><![CDATA[<blockquote>
  <p>“Curiosity is only vanity. We usually only want to know something so that we can talk about it”
– Blaise Pascal, <em>Pensées</em></p>
</blockquote>

<p>I enjoy writing. Most of the time I write for myself, or that is what I tell myself. The act of writing is me trying to deeply understand something and then recording my thought process.
It has paid dividends already as I have gone back numerous times to reference myself.</p>

<!--more-->

<p>When I am honest with myself though, I deeply enjoy knowing when others read my work as well. Knowing that something I found interesting and insightful landed for someone else too is incredibly satisfying. If I could have helped someone understand something better while having done so for myself, pure joy.</p>

<p>The peak of that vanity seems to be when the <a href="https://news.ycombinator.com/">Hacker News</a> crowd has deemed your content “worthy” to have made it on the <em>front page</em>.</p>

<p>There is a sort of inner satisfaction when someone else messages me to let me know one of my posts has made it onto Mount Olympus. I have for years added Google Analytics tracking to my site to understand engagement but I rarely went any deeper with the metrics to understand it, until now! 🤓</p>

<p>I have put my vanity on public display by collecting metrics pertaining to <a href="/readership">my readership</a>. 🪞</p>

<p><a href="/assets/images/vanity_readership_photo.png"><img src="/assets/images/vanity_readership_photo.png" alt="Summary tiles from my readership page: clicks from Google, total visits, and submission counts for Hacker News, Lobsters and Reddit" /></a></p>

<p>The numbers deflate the myth a little. As of writing, my writing has been submitted to Hacker News 127 times, and 26 of those reached the front page. Those 26 bought me 121 hours up there in total, under five hours each<sup id="fnref:mean"><a href="#fn:mean" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, and exactly one ever touched #1. Mount Olympus turns out to be crowded, and difficult to climb.</p>

<p>Turns out building the vanity site was itself rewarding. I got a better understanding of the metrics I am collecting through Google Analytics &amp; Search Console. I also tied my writings to submissions to <a href="https://reddit.com">Reddit</a>, <a href="https://lobste.rs/">Lobsters</a> &amp; <a href="https://news.ycombinator.com/">Hacker News</a>.</p>

<p>The data is fetched offline and periodically updated via a <a href="https://github.com/fzakaria/fzakaria.com/blob/942ebae88aa63ee3b8e5f76eabd5141b847080f7/.github/workflows/readership.yml">GitHub Actions workflow</a> and included in the site, of course, as a Nix derivation.</p>

<p>Pascal was probably right. I tell myself I write to understand things, and that part is true, but I have now built a daily pipeline whose only job is to tell me who else was listening. Curiosity is only vanity. My curiosity now has a dashboard.</p>

<p>A goal of mine is to have <a href="https://fareedzakaria.com/">Fareed Zakaria</a> mistaken for me instead of the other way around. 😅</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:mean">
      <p>A mean, which I have <a href="/2026/07/27/the-mean-means-nothing">previously argued</a> means nothing. <a href="#fnref:mean" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[“Curiosity is only vanity. We usually only want to know something so that we can talk about it” – Blaise Pascal, Pensées I enjoy writing. Most of the time I write for myself, or that is what I tell myself. The act of writing is me trying to deeply understand something and then recording my thought process. It has paid dividends already as I have gone back numerous times to reference myself.]]></summary></entry><entry><title type="html">A Nix store is three functions</title><link href="https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions" rel="alternate" type="text/html" title="A Nix store is three functions" /><published>2026-09-11T20:00:00-07:00</published><updated>2026-09-11T20:00:00-07:00</updated><id>https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions</id><content type="html" xml:base="https://fzakaria.com/2026/09/11/a-nix-store-is-three-functions"><![CDATA[<p>While building <a href="/2026/09/04/any-nix-package-live-in-your-browser">trynix</a>
I needed somewhere to host a store-path that did not exist on
<a href="https://cache.nixos.org">cache.nixos.org</a>.<sup id="fnref:cachix"><a href="#fn:cachix" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> I wanted to demonstrate that non-Nixpkgs store paths could be booted just as easily.</p>

<p>The only requirement seemed to be a lenient <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">Cross-Origin Resource Sharing (CORS)</a> policy, <code class="language-plaintext highlighter-rouge">access-control-allow-origin: *</code>, because the fetch happens in JavaScript.</p>

<p>Turns out that GitHub Pages sets that header on every file it
serves. 😈 I committed the output of <code class="language-plaintext highlighter-rouge">nix copy --to file://</code> to my <a href="https://github.com/fzakaria/trynix/tree/main/site/examples/cache">Git repository</a> and voilà, I have a <em>free</em> Nix substituter.</p>

<p>I seem to be late to the party on this discovery. <a href="https://github.com/tomberek/github-store">tomberek’s github-store</a> is a cache assembled out of GitHub release assets.<sup id="fnref:nar"><a href="#fn:nar" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nv">B</span><span class="o">=</span>https://github.com/tomberek/github-store/releases/latest/download
<span class="go">
</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-sL</span> <span class="nv">$B</span>/nix-cache-info
<span class="go">StoreDir: /nix/store

</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-sL</span> <span class="nv">$B</span>/1zy01hjzwvvia6h9dq5xar88v77fgh9x.narinfo
<span class="go">StorePath: /nix/store/1zy01hjzwvvia6h9dq5xar88v77fgh9x-glibc-2.38-44
URL: 0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si.nar.xz
Compression: xz
FileHash: sha256:0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si
FileSize: 6514112
NarHash: sha256:1ikxmc8yxzmm9vzzaa313w9yzrrgm0p6cgscy8arq3z32kynpi94
NarSize: 30244048
References: 1zy01hjzwvvia6h9dq5xar88v77fgh9x-glibc-2.38-44 a3n1vq6fxkpk5jv4wmqa1kpd3jzqhml9-libidn2-2.3.4 …
Deriver: 3fd7s6gjwi6rxfqw00bjq9ghnvazvnnn-glibc-2.38-44.drv
Sig: cache.nixos.org-1:YWkvHMXyvOw1iqWblEdvju+OZbGOvwYXyjyRUxuXluFq17xwJFSzHb0HrIuRr9BXYjV6GxfmGGv+ckVNvAOpDQ==

</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-sL</span> <span class="nv">$B</span>/0hkyywarmj4frwvs6p4lz4yl6z5q5halphswlqksh7lbkn4r75si.nar.xz | <span class="nb">wc</span> <span class="nt">-c</span>
<span class="go">6514112
</span></code></pre></div></div>

<p>GitHub Pages and Releases are static file servers. They have no idea what Nix is. If a humble file server can be a Nix binary cache, what else could we use?</p>

<h2 id="the-interface">The interface</h2>

<p>Turns out that in order to be a Nix binary cache, you must implement only three simple functions. The Nix client does not care what medium you use to implement them, although HTTP is the most common and included by default in <a href="https://github.com/NixOS/nix">CppNix</a>.<sup id="fnref:cppnix"><a href="#fn:cppnix" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET nix-cache-info
 →  StoreDir: /nix/store
GET &lt;32-char hash&gt;.narinfo
 →  metadata naming an archive
GET &lt;the narinfo's URL field&gt;
 →  the compressed archive
</code></pre></div></div>

<p>That’s it.</p>

<p>Anything that can answer those three requests can be used as a remote <em>Nix store</em>.<sup id="fnref:store"><a href="#fn:store" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<h2 id="what-about-the-signatures">What about the signatures!?</h2>

<p style="--image-width: 20rem"><a href="/assets/images/simpsons_meme_children_signatures.png"><img src="/assets/images/simpsons_meme_children_signatures.png" alt="simpson meme about signatures" /></a></p>

<p>The reason we can be this careless about transport is that Nix does not trust it. A narinfo’s signature (<code class="language-plaintext highlighter-rouge">Sig</code>) field covers <code class="language-plaintext highlighter-rouge">StorePath</code>, <code class="language-plaintext highlighter-rouge">NarHash</code>, <code class="language-plaintext highlighter-rouge">NarSize</code> and <code class="language-plaintext highlighter-rouge">References</code>. It does not cover <code class="language-plaintext highlighter-rouge">URL</code>, <code class="language-plaintext highlighter-rouge">FileHash</code>, <code class="language-plaintext highlighter-rouge">FileSize</code> or <code class="language-plaintext highlighter-rouge">Compression</code>.</p>

<p>Once the archive is fetched, Nix decompresses it and checks that the <code class="language-plaintext highlighter-rouge">NarHash</code> matches.</p>

<p>This is the <em>special sauce</em> of how packages that were signed by <a href="https://cache.nixos.org">cache.nixos.org</a> can be fetched from any other binary cache as an intermediary, and the signature still validates.</p>

<p>The <code class="language-plaintext highlighter-rouge">URL</code> field does not even have to be on the same host as the narinfo. It can be anywhere on the internet, and it can be a different protocol than HTTP. Nix does not care. The only thing that matters is that the archive fetched from <code class="language-plaintext highlighter-rouge">URL</code> has the same <code class="language-plaintext highlighter-rouge">NarHash</code> as the narinfo.</p>

<h2 id="alternative-stores">Alternative Stores</h2>

<p>For protocols that are not included by default in the Nix client, you can always write an HTTP proxy that translates the three functions to whatever medium you want.</p>

<p>In research for this post, I found a few interesting ones.</p>

<p><strong><a href="https://github.com/EphraimSiegfried/gachix">gachix</a></strong>: puts the archives in git’s object database. Git content-addresses and delta-compresses blobs already, so the store dedupes itself; the author reports roughly 82% smaller than the equivalent plain cache.</p>

<p><strong>DNS</strong>: I wrote a proof-of-concept that puts the narinfo and 4 KiB slices of the archive in TXT records. The narinfo is small enough to fit on one record but the archive needs to be chunked.</p>

<p><strong><a href="https://pastebin.com/">pastebin</a></strong>: a pastebin can hold the narinfo and the archive. The narinfo is small enough to fit on one paste, but the archive needs to be chunked. Many pastebins have an expiry policy which acts as a natural garbage collector.</p>

<p><strong><a href="https://github.com/cmspam/nixcache-oci">nixcache-oci</a></strong>: uses an OCI registry to store Nix archives.</p>

<p><strong><a href="https://github.com/KKarmugil/Infinite_Storage_Glitch">infinite storage glitch</a></strong>: encodes data within a video and uploads it to YouTube.</p>

<h2 id="npm">npm</h2>

<blockquote>
  <p>“Everything is available on npm”
– Some person on the internet</p>
</blockquote>

<p>Unsurprisingly, npm is a <em>great</em> binary cache and it has some interesting properties for release management we can <del>ab</del>use.</p>

<p><code class="language-plaintext highlighter-rouge">nix copy --to file://</code> emits a directory and npm publishes directories: a match made in heaven. 💑</p>

<p>Let’s walk through a small <code class="language-plaintext highlighter-rouge">hello</code> example.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">packages</span><span class="o">.</span><span class="p">${</span><span class="nv">system</span><span class="p">}</span><span class="o">.</span><span class="nv">default</span> <span class="o">=</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">hello</span><span class="o">.</span><span class="nv">overrideAttrs</span> <span class="p">(</span><span class="nv">old</span><span class="p">:</span> <span class="p">{</span>
  <span class="nv">pname</span> <span class="o">=</span> <span class="s2">"hello-npm"</span><span class="p">;</span>
  <span class="c"># The upstream test suite greps for the original greeting.</span>
  <span class="nv">doCheck</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
  <span class="nv">postPatch</span> <span class="o">=</span> <span class="p">(</span><span class="nv">old</span><span class="o">.</span><span class="nv">postPatch</span> <span class="nv">or</span> <span class="s2">""</span><span class="p">)</span> <span class="o">+</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    substituteInPlace src/hello.c \</span><span class="err">
</span><span class="s2">      --replace-fail 'Hello, world!' 'Hello from the npm registry!'</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>

<p>It is dynamically linked against glibc, so the closure is five paths and
roughly 36 MiB:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix path-info <span class="nt">-rSh</span> result
<span class="go">/nix/store/yh8rykx8wakl1ccn8rc351f6r2wbg4cn-libunistring-1.4.2	   2.0 MiB
/nix/store/nga9d6m9iplygw3iqghk2g840nz7b0gy-libidn2-2.3.8     	   2.3 MiB
/nix/store/ssvq1r0xd8f7paf6zqgpfql1a4drwhy2-xgcc-15.3.0-libgcc	 193.0 KiB
/nix/store/n51dhmdbik1kfrsm62j5knavmigwrl1a-glibc-2.42-84     	  36.0 MiB
/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3  	  36.2 MiB
</span></code></pre></div></div>

<p>We copy it to a local cache, signed with our own key, and add the one file
npm needs (<code class="language-plaintext highlighter-rouge">package.json</code>):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix key generate-secret <span class="nt">--key-name</span> hello-npm-1 <span class="o">&gt;</span> cache-key.sec
<span class="gp">$</span><span class="w"> </span>nix key convert-secret-to-public &lt; cache-key.sec <span class="o">&gt;</span> cache-key.pub
<span class="gp">$</span><span class="w"> </span>nix copy <span class="nt">--to</span> <span class="s2">"file://</span><span class="nv">$PWD</span><span class="s2">/cache?secret-key=</span><span class="nv">$PWD</span><span class="s2">/cache-key.sec"</span> .#
<span class="gp">$</span><span class="w"> </span><span class="nb">cd </span>cache <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> log build-trace-v2 <span class="o">&amp;&amp;</span> npm init <span class="nt">--scope</span><span class="o">=</span>@fzakaria <span class="nt">-y</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">npm publish</code> then dutifully packages our complete closure for us:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>npm publish <span class="nt">--access</span> public
<span class="go">npm notice 606B   3ssib4ic89qw7x1gha10s6mdf42fk15v.narinfo
npm notice 767B   n51dhmdbik1kfrsm62j5knavmigwrl1a.narinfo
npm notice 7.3MB  nar/06fsxd8j9ck4ls5b8xj4r4zk2642xzxzmdj6bl93zfwcm4pqzx0z.nar.xz
npm notice 467.5kB nar/1asdj56kfpvbqh8bpg5wns4v0yd1p3ldl6p2swv6nfwfdgvbwklc.nar.xz
npm notice 21B    nix-cache-info
npm notice name: @fzakaria/hello-nix-cache
npm notice version: 1.0.0
npm notice package size: 8.0 MB   total files: 12
</span></code></pre></div></div>

<p><a href="https://www.npmjs.com/package/@fzakaria/hello-nix-cache"><code class="language-plaintext highlighter-rouge">@fzakaria/hello-nix-cache</code></a>
is now a real package on the public npm registry.</p>

<p>It is now a substituter you can point Nix at directly:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix copy <span class="nt">--from</span> https://unpkg.com/@fzakaria/hello-nix-cache@latest/ <span class="se">\</span>
<span class="go">    --to ./npmstore \
    --trusted-public-keys 'hello-npm-1:u4SntZm2u3sob9zBw2OG6yePvJGoRRQUk00LQh4pnKA=' \
    /nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3
copying 5 paths...
copying path '/nix/store/ssvq1r0xd8f7paf6zqgpfql1a4drwhy2-xgcc-15.3.0-libgcc' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...
copying path '/nix/store/n51dhmdbik1kfrsm62j5knavmigwrl1a-glibc-2.42-84' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...
copying path '/nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3' from 'https://unpkg.com/@fzakaria/hello-nix-cache@latest'...

</span><span class="gp">$</span><span class="w"> </span>bwrap <span class="nt">--ro-bind</span> ./npmstore/nix/store /nix/store <span class="nt">--proc</span> /proc <span class="nt">--dev</span> /dev <span class="se">\</span>
<span class="go">    /nix/store/3ssib4ic89qw7x1gha10s6mdf42fk15v-hello-npm-2.12.3/bin/hello
Hello from the npm registry!
</span></code></pre></div></div>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
We have to use <code class="language-plaintext highlighter-rouge">bwrap</code> to run the binary because <code class="language-plaintext highlighter-rouge">./npmstore</code> is a <em>chroot store</em> and all the paths
are still under <code class="language-plaintext highlighter-rouge">/nix/store</code>. If we had <a href="/2026/06/21/nix-needs-relocatable-binaries">relocatable binaries</a> we could run it directly.</p>
</blockquote>

<p>That is Nix fetching the complete closure from npm and running it. 🤯
We can distribute Nix packages to non-Nix users, let the infection spread!</p>

<p>As an added bonus, similar to Nixpkgs and NixOS we can get nice “channel” semantics by using npm’s dist-tags. The <code class="language-plaintext highlighter-rouge">latest</code> tag is mutable and points to the latest version, while each version is immutable and points to a specific store path.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>npm dist-tag add @fzakaria/hello-nix-cache@1.0.5 staging
<span class="gp">$</span><span class="w"> </span>npm dist-tag add @fzakaria/hello-nix-cache@1.0.4 production
</code></pre></div></div>

<p>The major downside of this approach is that npm has no incremental publishing.
Every version is a whole tarball, so fifty closures sharing glibc upload glibc fifty times.</p>

<p>We <em>could</em> fix that by publishing each store path as a separate package, and then having a small index package that points at them. Each store path would then be uploaded exactly once.</p>

<p>I won’t build that though as it’s not in good faith to the npm ecosystem.</p>

<p>What other store implementations can we find?</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:cachix">
      <p>I was also waiting for <a href="https://github.com/domenkozar">@domenkozar</a> to enable CORS on <a href="https://cache.nixos.org">cache.nixos.org</a> so I could use it. <a href="#fnref:cachix" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nar">
      <p>In order to be a Nix binary cache, the <code class="language-plaintext highlighter-rouge">nar/</code> prefix is stripped from the <code class="language-plaintext highlighter-rouge">URL</code> field in the narinfo, because GitHub releases are a flat namespace. <a href="#fnref:nar" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:cppnix">
      <p>You can write a Nix plugin to implement a new protocol if you wanted. <a href="#fnref:cppnix" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:store">
      <p>We will see that they need not all be on the same medium, protocol or domain even! <a href="#fnref:store" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[While building trynix I needed somewhere to host a store-path that did not exist on cache.nixos.org.1 I wanted to demonstrate that non-Nixpkgs store paths could be booted just as easily. I was also waiting for @domenkozar to enable CORS on cache.nixos.org so I could use it. &#8617;]]></summary></entry><entry><title type="html">Review a pull request by booting it</title><link href="https://fzakaria.com/2026/09/09/review-a-pull-request-by-booting-it" rel="alternate" type="text/html" title="Review a pull request by booting it" /><published>2026-09-09T14:00:00-07:00</published><updated>2026-09-09T14:00:00-07:00</updated><id>https://fzakaria.com/2026/09/09/review-a-pull-request-by-booting-it</id><content type="html" xml:base="https://fzakaria.com/2026/09/09/review-a-pull-request-by-booting-it"><![CDATA[<blockquote class="alert alert-note">
  <p><strong>tl;dr;</strong> <a href="https://github.com/marketplace/actions/trynix-preview">trynix-preview</a> is a GitHub action that comments a link on a pull request which lets you boot the PR’s build in the browser using <a href="https://trynix.dev">https://trynix.dev</a>. No servers, just browsers.</p>
</blockquote>

<p>I ended my earlier <a href="/2026/09/04/any-nix-package-live-in-your-browser">trynix post</a> with a list of ideas I think we could accomplish now that we can boot arbitrary <code class="language-plaintext highlighter-rouge">/nix/store</code> paths in the browser. The most obvious one was to let a reviewer boot a pull request’s build in the browser for testing, validation and feedback.</p>

<p>That is now real. 🤯</p>

<p>A demo is worth a 1000 words: here is a pull request (<a href="https://github.com/fzakaria/sqlelf/pull/31">PR#31</a>) against my <a href="https://github.com/fzakaria/sqlelf">sqlelf</a> project, <u>from a fork</u>, with the comment our action left on it:</p>

<p><a href="/assets/images/trynix-action-pr-comment.png"><img src="/assets/images/trynix-action-pr-comment.png" alt="A GitHub pull request comment from github-actions[bot]. It links &quot;Boot this build in your browser&quot;, says a Linux VM boots in the tab and fetches this pull request's build from the cache, notes which commit it was built from, and has a collapsed &quot;Store paths&quot; section." /></a></p>

<p><a href="https://trynix.dev/?path=/nix/store/x0cz8aax3pcn0byrm1vjyidd17aizk6i-sqlelf&amp;cache=https://sqlelf.cachix.org+sqlelf.cachix.org-1:MLnjolA9AsKscTOJKDSA%2BZAcgIK8BwZA574j4%2BCs2bg%3D">Click the link</a> and a Linux machine boots in your tab with that PR’s <code class="language-plaintext highlighter-rouge">sqlelf</code> on <code class="language-plaintext highlighter-rouge">PATH</code>.</p>

<p>You did not clone anything, you did not build anything. No servers, no SSH, no VPN, no Docker, no VM, no cloud. Just a browser and a link. 😈</p>

<h2 id="gimme-gimme-gimme">Gimme. Gimme. Gimme.</h2>

<p>As with any GitHub action, it’s just a few lines to add to your workflow.</p>

<p><em>The caveat is that you must have built and cached the path already, so the action can link to it. The action does not build or cache anything.</em></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Setup Cachix as our Nix cache.</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">cachix/cachix-action@v17</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">sqlelf</span>
    <span class="na">authToken</span><span class="pi">:</span> <span class="s">$</span>
<span class="c1"># We build the pull request's code and push it to the cache, so the action can link to it.</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">nix build .#default</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">fzakaria/trynix@v1</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">cache</span><span class="pi">:</span> <span class="s">https://sqlelf.cachix.org</span>
    <span class="na">publicKey</span><span class="pi">:</span> <span class="s">sqlelf.cachix.org-1:MLnjolA9AsKscTOJKDSA+ZAcgIK8BwZA574j4+Cs2bg=</span>
    <span class="c1"># You can have multiple attrs if you want to boot more than one path.</span>
    <span class="na">attrs</span><span class="pi">:</span> <span class="s">.#default</span>
</code></pre></div></div>

<p>The action publishes and builds nothing. Whatever already fills your cache keeps doing it, and the action’s whole job is to simply provide the store paths via <code class="language-plaintext highlighter-rouge">nix eval</code> and hand the cache’s URL and public key to the browser.</p>

<p>It is not Nix cache provider specific, but I do recommend <a href="https://cachix.org">Cachix</a> because it is free for open source up to 5GiB.<sup id="fnref:signup"><a href="#fn:signup" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>You can checkout my <a href="https://github.com/fzakaria/sqlelf/blob/b6546b0dbcbc6ff4d8f07f820ee563a0dcab6fcd/.github/workflows/trynix.yaml">trynix.yaml</a> workflow for the full example. You have to set <code class="language-plaintext highlighter-rouge">allow-unsafe-pr-checkout: true</code> in the <code class="language-plaintext highlighter-rouge">actions/checkout</code> step because the workflow runs on a fork’s pull request, and that has security implications.<sup id="fnref:private-cache"><a href="#fn:private-cache" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>If that is not your cup of tea, there is a version where a maintainer types <code class="language-plaintext highlighter-rouge">/trynix</code> on the pull request which kicks off the workflow.</p>

<p>In either case, the workflow runs on the default branch and checks out the pull request’s code, so a fork cannot edit the workflow that builds it.</p>

<h2 id="game-over">Game over?</h2>

<p>Did I just upend all CI products by easily letting reviewers boot a PR?</p>

<p>Unfortunately, no. 🥲</p>

<p>The performance for large binaries is pretty bad. Even with many of the improvements I AI-assisted into the engine, large binaries can still take 1-2 minutes to execute.<sup id="fnref:bench"><a href="#fn:bench" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>Nevertheless, this is still a pretty amazing workflow and showcases the power of Nix.</p>

<p>Maybe as we get closer to AGI, our AI overlords will be able to optimize the engine to execute large binaries in a few seconds, but for now, the action is best suited for small to medium-sized binaries.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:signup">
      <p>You should definitely sign up for Cachix but you can test this out without it since the free tier is very generous. <a href="#fnref:signup" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:private-cache">
      <p>I recommend a private segregated cache for pull request builds, so that a fork cannot push to your main cache. <a href="#fnref:private-cache" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:bench">
      <p>I added a benchmark page, <a href="https://trynix.dev/bench/">https://trynix.dev/bench/</a>, to the site with a lot of rich data on boot and run times for various applications. <a href="#fnref:bench" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[tl;dr; trynix-preview is a GitHub action that comments a link on a pull request which lets you boot the PR’s build in the browser using https://trynix.dev. No servers, just browsers.]]></summary></entry><entry><title type="html">Any Nix package, live in your browser</title><link href="https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser" rel="alternate" type="text/html" title="Any Nix package, live in your browser" /><published>2026-09-04T20:00:00-07:00</published><updated>2026-09-04T20:00:00-07:00</updated><id>https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser</id><content type="html" xml:base="https://fzakaria.com/2026/09/04/any-nix-package-live-in-your-browser"><![CDATA[<blockquote class="alert alert-note">
  <p><strong>tl;dr</strong> Try it at <a href="https://trynix.dev/">https://trynix.dev</a>. Click <a href="https://trynix.dev/?pkg=hello">hello</a>, or <a href="https://trynix.dev/?pkg=python3@3.6.2">python 3.6.2 from 2017</a>, or <a href="https://trynix.dev/?pkg=hello@2.10&amp;pkg=hello@2.12.2">two eras of hello at once</a>, or <a href="https://trynix.dev/?path=/nix/store/awmhh7ci4admi71gs6b73awh0lxgrqqn-hello-trynix-2.12.3&amp;cache=https://trynix.dev/examples/cache%20trynix-examples-1:dZOV2uGWvjHo6IC5ZqCCu0dmIRzLm9pyOQJBDnBsXRY=">a package that exists in no public cache</a>. A Linux machine boots in the tab and you get a shell with those Nix packages on <code class="language-plaintext highlighter-rouge">PATH</code>.</p>
</blockquote>

<p>This is my <em>magnum opus</em> of Nix work.</p>

<p>I knew all the ideas I have been creating were building blogs for something greater: <a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a> indexed every version of every package nixpkgs ever shipped, <a href="/2026/09/01/the-holy-grail-of-nixpkgs-version-ranges">grail</a> taught it version ranges and <a href="/2026/08/28/one-flake-to-rule-them-all">omniflake</a> allowed adding over sixteen thousand flakes from a single input.</p>

<p>The crazy insight I had lately was the craziness of the <a href="/2026/08/14/nixpkgs-multiverse-fast-mode">“fast-mode”</a> of the <a href="https://nixmultiverse.com">nixmultiverse.com</a>, which lets you skip evaluation and go straight to the store path.
This lets you leverage the amazingness of Nix without having to deal with
the complexity of evaluation and building. You can just ask for a package and get the exact store path that Hydra built for it, at any version it ever had.</p>

<p>If we have the produced binaries, we can run them. And if we can run them, we can run <em>any</em> of them, in a browser tab, with nothing installed on the host machine.</p>

<p>Welcome to <a href="https://trynix.dev/">trynix</a>, a browser-based Nix package runner. You can browse the complete history of nixpkgs, over 310,083 package versions, and run any of them<sup id="fnref:nogui"><a href="#fn:nogui" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> in a Linux machine that boots in your tab. It is a Nix store in memory, a Linux kernel in WebAssembly, and a <a href="https://github.com/ghostty-org/ghostty">terminal emulator</a> in the page.</p>

<video autoplay="" loop="" muted="" playsinline="" width="800" height="448">
  <source src="/assets/images/trynix-boot.mp4" type="video/mp4" />
  <a href="/assets/images/trynix-boot.mp4">Screencast: searching nixpkgs for pfetch, picking a version, and booting it to a shell in the browser</a>
</video>

<p>This is bonkers! 🤯
We can boot the VM with the store-path closure within seconds. Nothing is pre-installed: search, pick a version, boot, run it.</p>

<p>The craziest part? We are not restricted to the public cache. You can share a store path you built yourself, and anyone can boot it in their browser tab. The only requirement is that the cache is served with <code class="language-plaintext highlighter-rouge">access-control-allow-origin: *</code>, which GitHub Pages does for free<sup id="fnref:github-pages"><a href="#fn:github-pages" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, so does <a href="https://www.cachix.org/">Cachix</a> and obviously
<a href="https://cache.nixos.org/">cache.nixos.org</a> as well.</p>

<blockquote>
  <p>In a unbelievable twist of fate, I had actually requested 5 years ago for <a href="https://cache.nixos.org/">cache.nixos.org</a> to serve <code class="language-plaintext highlighter-rouge">access-control-allow-origin: *</code> via <a href="https://github.com/NixOS/infra/issues/156">issue#156</a> to make it possible to query the cache from an <a href="https://fzakaria.github.io/nix-http-binary-cache-api-spec/">OpenAPI specification</a> I had implemented. Thank you universe. 🙏</p>
</blockquote>

<p><a href="https://trynix.dev/?path=/nix/store/awmhh7ci4admi71gs6b73awh0lxgrqqn-hello-trynix-2.12.3&amp;cache=https://trynix.dev/examples/cache%20trynix-examples-1:dZOV2uGWvjHo6IC5ZqCCu0dmIRzLm9pyOQJBDnBsXRY=">This link boots</a> a VM with a store-path served from Github Pages of a <a href="https://github.com/fzakaria/trynix/blob/f9a6fcd496cf3811fa8cf07d0af265b9eea969c9/examples/hello-trynix/flake.nix">modified GNU hello</a>.
<em>This is a store path that does not exist on <a href="https://cache.nixos.org/">cache.nixos.org</a> and yet it boots in your browser tab.</em></p>

<p><a href="/assets/images/github_pages_modified_hello.png"><img src="/assets/images/github_pages_modified_hello.png" alt="alt text" /></a></p>

<h2 id="making-the-pieces-fit">Making the pieces fit</h2>

<p>Since we can access store-paths from caches that serve <code class="language-plaintext highlighter-rouge">access-control-allow-origin: *</code>, that makes the browser a legitimate Nix client.</p>

<p>The missing piece the browser lacked was somewhere to <em>run</em> the binaries since they store-paths are either x86-64 or aarch64 ELF executables.</p>

<p>Standing on the shoulders of giants, we can run a <a href="https://github.com/ktock/qemu-wasm">Linux kernel in WebAssembly</a>. This means we can boot a real x86_64 kernel inside our browser tab. Give that kernel a filesystem containing a Nix store. All that’s left knowing which store-paths to fetch, which we beautifully solved with <a href="https://nixmultiverse.com/">nixpkgs-multiverse</a>. 🤌</p>

<div class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">trynix</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">LR</span><span class="p">;</span>
  <span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">;</span>
  <span class="k">node</span> <span class="o">[</span><span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">rect</span><span class="p">,</span>
        <span class="n">style</span><span class="p">=</span><span class="s2">"filled,rounded"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#4C78A8"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#DCE6F1"</span><span class="o">]</span><span class="p">;</span>
  <span class="k">edge</span> <span class="o">[</span><span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#666666"</span><span class="p">,</span> <span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.7</span><span class="o">]</span><span class="p">;</span>
  <span class="n">nodesep</span><span class="p">=</span><span class="mf">0.35</span><span class="p">;</span> <span class="n">ranksep</span><span class="p">=</span><span class="mf">0.55</span><span class="p">;</span> <span class="n">pad</span><span class="p">=</span><span class="mf">0.3</span><span class="p">;</span>

  <span class="k">subgraph</span> <span class="nv">cluster_browser</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"your browser tab"</span><span class="p">;</span>
    <span class="n">fontsize</span><span class="p">=</span><span class="mi">10</span><span class="p">;</span>
    <span class="n">color</span><span class="p">=</span><span class="s2">"#bbbbbb"</span><span class="p">;</span>
    <span class="n">style</span><span class="p">=</span><span class="nv">rounded</span><span class="p">;</span>

    <span class="nv">page</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"the page"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">store</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"nix store\nin memory"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#E8F0E3"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#4f8a6b"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">vm</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"qemu-wasm\nx86_64 Linux"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#F3E6DE"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#9e3413"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">term</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"your shell"</span><span class="o">]</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nv">mv</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"nixpkgs-multiverse\nattr + version → store path"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#EFE6F5"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#8a5fa8"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">cache</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"cache.nixos.org\nnarinfo + NARs"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#EFE6F5"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#8a5fa8"</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">page</span> <span class="o">-&gt;</span> <span class="nv">mv</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"which path?"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">page</span> <span class="o">-&gt;</span> <span class="nv">cache</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"closure"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">cache</span> <span class="o">-&gt;</span> <span class="nv">store</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"unpacked"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">store</span> <span class="o">-&gt;</span> <span class="nv">vm</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"9p"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">vm</span> <span class="o">-&gt;</span> <span class="nv">term</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I have to keep reminding myself: there is no server in the above picture, the web-page is purely static files and everything else is a publicly accessible cache. It is a virtual machine that exists only inside your tab. The ultimate embodiment of <a href="https://grahamc.com/blog/erase-your-darlings/">Erase your darlings</a>.</p>

<p>Since this is Nix, we get the simplicity of managing multiple versions of the same package. You can boot <a href="https://trynix.dev/?pkg=hello@2.10&amp;pkg=hello@2.12.2">two versions of hello</a> in one machine, and they will not conflict because each binary names its own dependencies by absolute path (<code class="language-plaintext highlighter-rouge">RUNPATH</code>) down to the loader and libc.</p>

<p>Once the VM is already started, you can add more store-paths to it while it’s running. This is no different than adding more paths to your own <code class="language-plaintext highlighter-rouge">/nix/store</code> on your laptop. No reboot, or <code class="language-plaintext highlighter-rouge">dnf install</code>, or <code class="language-plaintext highlighter-rouge">apt-get install</code>, the site fetches the closure and adds it to the store.</p>

<h2 id="it-has-to-feel-instant">It has to feel instant</h2>

<p>Booting a kernel under emulation is slow, and despite the amazingness of the idea, no one would use it if it took 30 seconds to get a shell.</p>

<p>The site employs some neat tricks to make it feel instant. The site pre-fetches the engine and the VM snapshot in the background, so by the time you click a link, you have already downloaded it.</p>

<p>The site also never boots the VM from scratch. It resumes. A machine is booted once, ahead of time, on a native build of the same QEMU, and paused at the moment before it mounts the store which is then saved to a snapshot.</p>

<p>Subsequent visits to the site have the engine and snapshot already in the browser cache, so the only thing that has to be fetched is the closure of the store-path you asked for. This makes each subsequent visit feel much faster.</p>

<pre><code class="language-plotnine">import pandas as pd
from plotnine import *

# Measured 2026-09-05 against trynix.dev in headless Chromium on a
# Ryzen 7 7840U: three visits per package with a fresh browser profile,
# then a reload with everything already cached. Medians. The clock runs
# from opening the link to a prompt you can type at.
df = pd.DataFrame({
    "package": ["hello", "hello", "ripgrep", "ripgrep", "python3", "python3"],
    "visit": ["first visit", "revisit", "first visit", "revisit",
              "first visit", "revisit"],
    "seconds": [4.2, 1.5, 4.3, 1.7, 7.5, 3.5],
})
df["package"] = pd.Categorical(
    df["package"], categories=["python3", "ripgrep", "hello"], ordered=True
)
df["visit"] = pd.Categorical(
    df["visit"], categories=["first visit", "revisit"], ordered=True
)
df["label_at"] = df["seconds"] + 0.15

plot = (
    ggplot(df, aes("package", "seconds", fill="visit"))
    + geom_col(position=position_dodge(width=0.75), width=0.65)
    + geom_text(
        aes(y="label_at", label="seconds"),
        position=position_dodge(width=0.75),
        ha="left",
        size=8,
    )
    + coord_flip()
    + scale_fill_manual(values=["#4C78A8", "#A8C7E5"], name="")
    + labs(x="", y="seconds to a shell")
    + theme(figure_size=(6.5, 2.8), legend_position="top")
)
</code></pre>

<p>I have to give a lot of credit to LLMs here for helping find a lot of the performance opportunities and bottlenecks. What first started as a “neat idea” turned into an incredibly usable project with their help.</p>

<p>Despite all the performance work, it is still not instant. It is fast enough to be usable, but it is not instant. Execution of a binary is still slow, because it is running under emulation. The first time you run a binary, it is translated from x86_64 to WebAssembly and that takes time. Subsequent runs are faster, because the translation is cached in memory.</p>

<p>Lastly, we have an upper-bound on the size of the closure we can fetch. The whole closure has to fit in tab memory, which is set to a hard limit of ~1.5GiB as of now and WebAssembly has a hard limit of 4GiB as it is a 32-bit address space.</p>

<h2 id="more-than-a-parlor-trick">More than a parlor trick</h2>

<p>The demo is clearly fun and impressive, but is it more than a parlor trick? I have been thinking of <em>endless ideas</em> of ways in which this could be a new way to use and leverage Nix.</p>

<p><strong>Reviewing a pull request by using the software.</strong> If your CI already pushes to a cache, like Cachix, and if you use Nix, then a PR has produced real artifacts by the time a human looks at it. A bot can leave a link that boots exactly those artifacts. The reviewer does not clone, does not build, does not trust a screenshot. They click, and can immediately test out the software. “Does this fix the bug?” stops being a thought experiment.</p>

<p><strong>Agent Artifacts.</strong> Agents can produce Nix store paths as artifacts, and those artifacts can be shared with humans or other agents. A bot can produce a store path, and another bot can boot it in a browser tab and run tests against it.</p>

<p><strong>Bug reports that carry their own environment.</strong> “Works on my machine” is a URL now for reproduction.</p>

<p><strong>Documentation you can run.</strong> A tutorial that names a tool version can link a shell with that exact version on <code class="language-plaintext highlighter-rouge">PATH</code>, pinned forever, with no install step standing between a reader and the first command.</p>

<p><strong>Archaeology.</strong> You can run historic versions of software and explore their behavior. You can run a version of Python from 2017 and see what it does, or a version of <code class="language-plaintext highlighter-rouge">hello</code> from 2005 and see how it differs from today. This was already possible with Nix, but now you can do it in the browser.</p>

<p>The source is at <a href="https://github.com/fzakaria/trynix">github.com/fzakaria/trynix</a>. The Nix cache has quietly served an open CORS header for years, waiting to be <del>abused</del> used. <a href="https://trynix.dev/?pkg=python2@2.7.18">Go boot something old.</a></p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:nogui">
      <p>It is a serial console, so nothing graphical. The machine boots to a shell, and you can run any command-line program in the store. <a href="#fnref:nogui" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:github-pages">
      <p>I was a little surprised to learn that GitHub Pages can work as a binary cache. It is just a static file server, and it serves <code class="language-plaintext highlighter-rouge">access-control-allow-origin: *</code> on every file. That is all that is needed to make a Nix store path available to <a href="https://trynix.dev">trynix</a>. <a href="#fnref:github-pages" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[tl;dr Try it at https://trynix.dev. Click hello, or python 3.6.2 from 2017, or two eras of hello at once, or a package that exists in no public cache. A Linux machine boots in the tab and you get a shell with those Nix packages on PATH.]]></summary></entry><entry><title type="html">Keeping one version of everything</title><link href="https://fzakaria.com/2026/09/02/keeping-one-version-of-everything" rel="alternate" type="text/html" title="Keeping one version of everything" /><published>2026-09-02T12:00:00-07:00</published><updated>2026-09-02T12:00:00-07:00</updated><id>https://fzakaria.com/2026/09/02/keeping-one-version-of-everything</id><content type="html" xml:base="https://fzakaria.com/2026/09/02/keeping-one-version-of-everything"><![CDATA[<p>After <a href="/2026/09/01/the-holy-grail-of-nixpkgs-version-ranges">the last post</a> introduced <em>the holy grail</em> of version ranges for nixpkgs, <a href="https://github.com/arianvp">@arianvp</a> over <a href="https://x.com/ProgrammerDude/status/2095052917426298981">X</a> felt it was a doomed endeavor: “What happens when a plan spans revisions and you accidentally load two versions of the same library? DOOMED.” 💀</p>

<p>He was right to be skeptical. The diamond dependency problem is a truly nasty one. I am however determined to add enough safeguards to make it a <em>safe</em> endeavor. The first step is to make sure that the solver never produces a plan that mixes versions of the same library in one process image, <em>when requested</em>.</p>

<p>The general shape of the diamond problem: you link <code class="language-plaintext highlighter-rouge">libfoo-2</code> and <code class="language-plaintext highlighter-rouge">libbar-1</code>, but <code class="language-plaintext highlighter-rouge">libbar-1</code> links <code class="language-plaintext highlighter-rouge">libfoo-1</code>. Now your process carries both libfoos, and only one symbol can win.</p>

<p><a href="/assets/images/grail-diamond.svg" style="--image-width: 26rem"><img src="/assets/images/grail-diamond.svg" alt="the diamond dependency problem: your app links libfoo-2 and libbar-1, libbar-1 links libfoo-1, one process carries both libfoos" /></a></p>

<p>He provided an example that happened in Nixpkgs itself. <a href="https://fluentbit.io/"><code class="language-plaintext highlighter-rouge">fluent-bit</code></a> vendored one <a href="https://github.com/facebook/zstd"><code class="language-plaintext highlighter-rouge">zstd</code></a> and linked against libsystemd, which <code class="language-plaintext highlighter-rouge">dlopen</code> another <code class="language-plaintext highlighter-rouge">zstd</code> for compressed logging. <code class="language-plaintext highlighter-rouge">zstd</code> is not ABI compatible across versions like <code class="language-plaintext highlighter-rouge">glibc</code>, so the two disagreed on struct layout, and logging corrupted the address space. Two versions of one library in one process is a crash. 💥<sup id="fnref:fluentbit-vendor"><a href="#fn:fluentbit-vendor" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<h2 id="sidestepping-the-problem">Sidestepping the problem</h2>

<p>Nixpkgs generally sidesteps this problem by virtue of its coherency philosophy: everything is built against a single revision so the diamond dependency problem <em>generally</em> does not arise.<sup id="fnref:fluentbit-bug"><a href="#fn:fluentbit-bug" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> The problem arises when you deliberately mix revisions.</p>

<p>Nix itself however can support, and was designed for, multiple revisions across processes. Each binary points to its own libraries through the <code class="language-plaintext highlighter-rouge">RUNPATH</code> at the <code class="language-plaintext highlighter-rouge">/nix/store</code>/, and they never collide.</p>

<p>The problem is only exposed when we are deliberately mixing revisions in one process image. Within a <code class="language-plaintext highlighter-rouge">^</code> coexistence group, <code class="language-plaintext highlighter-rouge">grail</code> already dodges this problem by virtue of its design: it also
forces the attributes to be coherent <em>to a single</em> revision.</p>

<p>If you are using <code class="language-plaintext highlighter-rouge">grail lock</code> to import top-level binaries into your configuration, you are safe <strong>no matter what</strong>. This is what Nix was designed for!</p>

<p>If however you are using <code class="language-plaintext highlighter-rouge">mkDerivation</code> to build packages with attributes you locked, you might be mixing shared libraries across revisions, if you don’t force a coexistence group <code class="language-plaintext highlighter-rouge">^</code>, and might have had a bad time. I have a solution for you below! 🙌</p>

<h2 id="coherence-constraints">Coherence constraints</h2>

<p>The <code class="language-plaintext highlighter-rouge">grail</code> tool generalizes the <code class="language-plaintext highlighter-rouge">glibc</code> constraint from the last post to any library, and it turned out to need no new data at all.</p>

<p>The tool now supports a <code class="language-plaintext highlighter-rouge">--one &lt;attr&gt;</code> flag that demands every chosen revision fall in <em>one era</em> of the given attribute.
For instance, <code class="language-plaintext highlighter-rouge">--one zstd</code> demands every chosen revision picked to solve the query must have shipped the same <code class="language-plaintext highlighter-rouge">zstd</code> version. The solver will refuse to mix revisions that ship different versions of <code class="language-plaintext highlighter-rouge">zstd</code>, and will report exactly what would have mixed.</p>

<p>Here is the same query <code class="language-plaintext highlighter-rouge">python3@3.10.* postgresql@13.*</code> with and without <code class="language-plaintext highlighter-rouge">--one zstd --one openssl</code>. You can see that the solver picks versions of each such that there is only one <code class="language-plaintext highlighter-rouge">zstd</code> and one <code class="language-plaintext highlighter-rouge">openssl</code> version.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># free: python at its freshest, sixteen months after postgresql 13
# died. glibc is RESOLVED, not constrained: the newest spanned era
# serves every input via symbol versioning
</span><span class="gp">$</span><span class="w"> </span>grail solve <span class="s1">'python3@3.10.* postgresql@13.*'</span>
<span class="go">2 revisions
  2022-05-20-dfd82985c273  (2022-05-20, r771)
    postgresql 13.6
  2023-10-19-7c9cc5a6e5d3  (2023-10-19, r1128)
    python3 3.10.12
  glibc: 2.37 serves every input (eras spanned: 2.34, 2.37)

</span><span class="c"># one zstd, one openssl: python retreats until every library agrees —
# and glibc lands on one era for free
</span><span class="gp">$</span><span class="w"> </span>grail solve <span class="s1">'python3@3.10.* postgresql@13.*'</span> <span class="nt">--one</span> zstd <span class="nt">--one</span> openssl
<span class="go">2 revisions
  2022-05-20-dfd82985c273  (2022-05-20, r771)
    postgresql 13.6
  2022-06-26-f2537a505d45  (2022-06-26, r793)
    python3 3.10.4
  zstd: 1.5.2
  openssl: 1.1.1o
  glibc: 2.34
</span></code></pre></div></div>

<p>This causes the chosen <code class="language-plaintext highlighter-rouge">python3</code> to retreat to an earlier revision to satisfy the <code class="language-plaintext highlighter-rouge">zstd</code> and <code class="language-plaintext highlighter-rouge">openssl</code> coherency.</p>

<p><a href="/assets/images/grail-eras.svg"><img src="/assets/images/grail-eras.svg" alt="postgresql 13 and python 3.10 lifetimes over the glibc, zstd and openssl eras; the coherence window ends 2022-06-26, where the plan pins python 3.10.4" /></a></p>

<p>When no coherent plan exists, the solver names exactly what would have mixed:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>grail solve <span class="s1">'ffmpeg@5.* nodejs@16.*'</span> <span class="nt">--one</span> zstd <span class="nt">--one</span> openssl
<span class="go">unsatisfiable: satisfiable only by mixing zstd 1.5.2/1.5.5,
</span><span class="gp">openssl 1.1.1q/3.0.10;</span><span class="w"> </span><span class="nt">--one</span> forbids that
</code></pre></div></div>

<p>You can run these examples directly in the browser at <a href="https://fzakaria.github.io/grail/">fzakaria.github.io/grail</a> which includes some precanned examples and the ability to select <code class="language-plaintext highlighter-rouge">--one</code> attributes.
The plan graph draws each library as its own node, so a coherent plan is visible at a glance: every revision converging on a single node per library.</p>

<h2 id="what-it-promises-and-what-it-cannot">What it promises, and what it cannot</h2>

<p>Precision matters here. <code class="language-plaintext highlighter-rouge">--one zstd</code> guarantees <strong>version-level ABI agreement</strong>: every chosen revision ships the same <code class="language-plaintext highlighter-rouge">zstd</code> version, so no plan can hand you a mix of two different versions.</p>

<p>It does not guarantee that they consolidate to the same commit or even <code class="language-plaintext highlighter-rouge">/nix/store</code> path. Two revisions can ship the same version as different rebuilds if anything in their closure changed</p>

<p>If you want to guarantee one path, you need to use <code class="language-plaintext highlighter-rouge">^</code> to force a single revision.</p>

<p>We can go even further too! Some libraries, notably <code class="language-plaintext highlighter-rouge">glibc</code>, have backwards compatibility guarantees across versions. The solver can reason about this and allow a plan to mix versions of <code class="language-plaintext highlighter-rouge">glibc</code> that are compatible, but not mix incompatible versions. That can make the solver much more flexible which is something I am keen to explore.</p>

<p>Building software always <em>contains dragons</em>, the only difference is our ability to reason through them. I am personally enjoying throwing a SAT solver at the problem and seeing what I can reliably cook up.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fluentbit-vendor">
      <p>Turns out the bug is even more outlandish because <code class="language-plaintext highlighter-rouge">fluent-bit</code> vendored <code class="language-plaintext highlighter-rouge">zstd</code> itself breaking Nix’s ability to reason about the graph at all. <a href="#fnref:fluentbit-vendor" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fluentbit-bug">
      <p>The <a href="https://github.com/fluent/fluent-bit/issues/10139">fluent-bit#10139</a> bug was itself an outlier within Nixpkgs. <a href="#fnref:fluentbit-bug" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[After the last post introduced the holy grail of version ranges for nixpkgs, @arianvp over X felt it was a doomed endeavor: “What happens when a plan spans revisions and you accidentally load two versions of the same library? DOOMED.” 💀]]></summary></entry></feed>