<?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-08-25T21:42:34-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">Stamping build info in constant memory</title><link href="https://fzakaria.com/2026/08/25/stamping-build-info-in-constant-memory" rel="alternate" type="text/html" title="Stamping build info in constant memory" /><published>2026-08-25T18:00:00-07:00</published><updated>2026-08-25T18:00:00-07:00</updated><id>https://fzakaria.com/2026/08/25/stamping-build-info-in-constant-memory</id><content type="html" xml:base="https://fzakaria.com/2026/08/25/stamping-build-info-in-constant-memory"><![CDATA[<p>This is a fun little trick I came across at <code class="language-plaintext highlighter-rouge">$DAYJOB</code>. I did not invent it, but I thought
it was interesting enough to understand better and share.</p>

<p>At <code class="language-plaintext highlighter-rouge">$WORK</code> we build with <a href="https://buck2.build/">buck2</a> and we stamp our executables
with build information: build-id, timestamp, author, the usual suspects using <code class="language-plaintext highlighter-rouge">llvm-objcopy</code> as a step <strong>after</strong> the link.</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">cat </span>buildinfo.json
<span class="go">{"revision":"9f3c1ad","built_at":"2026-08-25T12:00:00Z",
 "builder":"buck2","host":"nyx"}

</span><span class="c"># attach it as a section
</span><span class="gp">$</span><span class="w"> </span>llvm-objcopy <span class="nt">--add-section</span> .buildinfo<span class="o">=</span>buildinfo.json app app.stamped
<span class="go">
</span><span class="c"># read it back out
</span><span class="gp">$</span><span class="w"> </span>llvm-objcopy <span class="nt">--dump-section</span> .buildinfo<span class="o">=</span>- app.stamped /dev/null
<span class="go">{"revision":"9f3c1ad","built_at":"2026-08-25T12:00:00Z",
 "builder":"buck2","host":"nyx"}
</span></code></pre></div></div>

<p>The reason it is a separate step is caching. If the build info was generated at link time then everytime we link the binary it would produce different bytes causing it to not be
bit-reproducible. When something is bit-reproducible, it is safe to cache it, and the build system can apply early cut-off optimizations.</p>

<p>That works, until the binaries get big. We noticed that <code class="language-plaintext highlighter-rouge">llvm-objcopy</code>’s memory use
scales with the size of the file it is stamping. Stamping is exactly the kind of
step that runs massively parallel at the end of a build so this can cause a lot of memory pressure.</p>

<p>Why is the stamping step reading the binary at all? 🤔</p>

<h1 id="the-memory-problem">The memory problem</h1>

<p>Let’s measure the claim that the memory use of <code class="language-plaintext highlighter-rouge">llvm-objcopy</code> scales with the size of the file. We will attach a JSON build info blob to an increasingly large synthetic executable and measure the peak RSS of the stamping step.</p>

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

# ru_maxrss of a single --add-section, via os.wait4. NixOS,
# llvm-objcopy 20.1.8, 88-byte JSON payload.
sizes = [4, 16, 64, 256, 512, 1024]
df = pd.DataFrame({
    "mib": sizes,
    "rss": [52.7, 76.9, 172.9, 556.8, 1068.6, 2092.6],
})

# Linear axes on purpose. The claim being made is about the slope, and a log
# plot flatters a straight line into looking like a law rather than a cost.
plot = (
    ggplot(df, aes("mib", "rss"))
    + geom_line(size=1.0, color="#b1201d")
    + geom_point(size=2.6, color="#b1201d")
    + scale_x_continuous(breaks=[0, 256, 512, 768, 1024])
    + scale_y_continuous(breaks=[0, 500, 1000, 1500, 2000])
    + labs(x="executable size (MiB)", y="peak RSS (MiB)")
)
plot.width, plot.height = 7.0, 3.2
</code></pre>

<p>The graph confirms the claim. The memory use of <code class="language-plaintext highlighter-rouge">llvm-objcopy</code> scales linearly with the size of the file being stamped. Surprisingly, the slope is <strong>two</strong>. The peak RSS is roughly twice the size of the file being stamped irrespective of the size of the build info being attached.<sup id="fnref:gnu"><a href="#fn:gnu" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>I am helping to shepherd a <a href="https://github.com/llvm/llvm-project/pull/217706">PR open against LLVM</a> to stream the ELF output rather than materialize it, which roughly halves the peak. That is a definite improvement but the problem remains that in order to add
a tiny section to a large binary, the whole binary has to be read into memory. The memory use is still linear in the size of the file.</p>

<h1 id="a-section-is-three-things">A section is three things</h1>

<p>The problem is not poor implementation on the part of <code class="language-plaintext highlighter-rouge">llvm-objcopy</code>. Adding a section to
an ELF touches three separate things:</p>

<ol>
  <li>the section’s <strong>bytes</strong>, somewhere in the file</li>
  <li>a 64-byte <strong>entry</strong> in the section header table describing where those bytes are</li>
  <li>the section’s <strong>name</strong>, which is not in the entry itself but rather the entry holds a  <code class="language-plaintext highlighter-rouge">sh_name</code> offset into <code class="language-plaintext highlighter-rouge">.shstrtab</code>, so the name has to be appended to that string table</li>
</ol>

<svg viewBox="0 0 760 268" role="img" style="display:block;margin-inline:auto;max-width:100%;height:auto;font-family:var(--mono)" aria-label="A single Elf64_Shdr entry drawn as a list of fields, with two arrows leaving it. The sh_name field, highlighted in red, holds 0x78 and is an offset into the .shstrtab byte table drawn at the top right, where the .buildinfo cell sits at that offset. The sh_offset and sh_size fields, highlighted in blue, point together at the section contents drawn at the bottom right. The entry itself holds no name and no bytes; it only refers to them.">

  <text x="20" y="30" fill="currentColor" font-size="13" font-weight="600">Elf64_Shdr — one 64-byte entry</text>

  <rect x="20" y="42" width="250" height="182" rx="4" fill="#8a8580" fill-opacity="0.10" stroke="#8a8580" stroke-width="1" />
  <rect x="20" y="42" width="250" height="26" rx="4" fill="#b1201d" fill-opacity="0.18" />
  <rect x="20" y="146" width="250" height="52" fill="#4c72b0" fill-opacity="0.15" />

  <g font-size="11.5">
    <g fill="currentColor">
      <text x="34" y="59">sh_name</text>
      <text x="34" y="85">sh_type</text>
      <text x="34" y="111">sh_flags</text>
      <text x="34" y="137">sh_addr</text>
      <text x="34" y="163">sh_offset</text>
      <text x="34" y="189">sh_size</text>
      <text x="34" y="215">sh_link …</text>
    </g>
    <g text-anchor="end">
      <text x="256" y="59" fill="#b1201d" font-weight="600">0x78</text>
      <text x="256" y="85" fill="currentColor" opacity="0.7">PROGBITS</text>
      <text x="256" y="111" fill="currentColor" opacity="0.7">0</text>
      <text x="256" y="137" fill="currentColor" opacity="0.7">0x0</text>
      <text x="256" y="163" fill="#4c72b0" font-weight="600">0x401021</text>
      <text x="256" y="189" fill="#4c72b0" font-weight="600">0x58</text>
      <text x="256" y="215" fill="currentColor" opacity="0.7">0</text>
    </g>
  </g>

  <!-- sh_name is an index into the string table, not a name -->
  <text x="370" y="30" fill="currentColor" font-size="13" font-weight="600">.shstrtab</text>
  <g stroke="#b1201d" stroke-width="1.5" fill="none">
    <path d="M274 55 L458 55" />
    <path d="M466 55 l-9 -4 l0 8 z" fill="#b1201d" stroke="none" />
  </g>
  <text x="366" y="46" fill="#b1201d" font-size="10" text-anchor="middle">0x78 bytes in</text>

  <g stroke="#8a8580" stroke-width="1" fill="#8a8580" fill-opacity="0.10">
    <rect x="470" y="42" width="90" height="34" />
    <rect x="680" y="42" width="60" height="34" />
  </g>
  <rect x="560" y="42" width="120" height="34" fill="#b1201d" fill-opacity="0.22" stroke="#b1201d" stroke-width="1" />
  <g fill="currentColor" font-size="10.5" text-anchor="middle">
    <text x="515" y="63">.interp\0</text>
    <text x="620" y="63">.buildinfo\0</text>
    <text x="710" y="63">.rela…</text>
  </g>
  <g stroke="#8a8580" stroke-width="1">
    <line x1="470" y1="80" x2="470" y2="86" />
    <line x1="560" y1="80" x2="560" y2="86" />
    <line x1="680" y1="80" x2="680" y2="86" />
  </g>
  <g fill="currentColor" font-size="9.5" text-anchor="middle" opacity="0.75">
    <text x="470" y="98">0x70</text>
    <text x="560" y="98">0x78</text>
    <text x="680" y="98">0x83</text>
  </g>

  <!-- sh_offset and sh_size locate the bytes, wherever they are -->
  <text x="470" y="146" fill="currentColor" font-size="13" font-weight="600">the section&#39;s bytes</text>
  <path d="M270 146 l8 0 l0 52 l-8 0" stroke="#4c72b0" stroke-width="1.5" fill="none" />
  <g stroke="#4c72b0" stroke-width="1.5" fill="none">
    <path d="M278 172 L458 172" />
    <path d="M466 172 l-9 -4 l0 8 z" fill="#4c72b0" stroke="none" />
  </g>
  <rect x="470" y="156" width="270" height="34" rx="3" fill="#4c72b0" fill-opacity="0.18" stroke="#4c72b0" stroke-width="1" />
  <text x="605" y="177" fill="currentColor" font-size="10.5" text-anchor="middle">88 bytes of JSON, at 0x401021</text>

  <text x="20" y="252" fill="currentColor" font-size="10.5" opacity="0.8">The entry holds neither the name nor the bytes. It only refers to them — and only one of those two references can be repointed in place.</text>
</svg>

<p>In order to account for the new section, the section header table has to grow by one entry, and <code class="language-plaintext highlighter-rouge">.shstrtab</code> has to grow by the length of the new name.</p>

<p>The current model for <code class="language-plaintext highlighter-rouge">llvm-objcopy</code> is to read the whole file into memory, add the new section, and write the whole file back out. That is why the memory use scales with the size of the file.</p>

<h1 id="pay-the-byte-at-link-time">Pay the byte at link time</h1>

<p>How can we avoid having to rebuild the whole file just to add a tiny section? The trick is to pay the cost at link time rather than at stamp time.</p>

<p>We can have the linker emit a placeholder section with the right name and a single byte of content. The section header table entry is already there, and the name is already in <code class="language-plaintext highlighter-rouge">.shstrtab</code>. The post-link stamping step can then append the payload to the end of the file and update the section header entry to point to it. 💡</p>

<p>We make linker emit the section during the normal build. It does not need to hold
anything; it just needs to exist so that it owns a name and a header.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* A placeholder the linker will emit a section header for.
   It holds one byte and is deliberately not SHF_ALLOC,
   so it is metadata rather than image. */</span>
<span class="n">__asm__</span><span class="p">(</span><span class="s">".section .buildinfo,</span><span class="se">\"\"</span><span class="s">,@progbits</span><span class="se">\n</span><span class="s">"</span>
        <span class="s">".byte 0</span><span class="se">\n</span><span class="s">"</span>
        <span class="s">".previous"</span><span class="p">);</span>
</code></pre></div></div>

<p>Our “stamp” step is now incredibly simple. It does not need to read the file at all, it just needs to write the new payload and update the section header entry.</p>

<ol>
  <li>append the payload to the end of the file</li>
  <li>write the new <code class="language-plaintext highlighter-rouge">sh_offset</code> and <code class="language-plaintext highlighter-rouge">sh_size</code> into the placeholder’s section header entry</li>
</ol>

<p>Nothing that already exists moves. <code class="language-plaintext highlighter-rouge">e_shoff</code> does not move, the section header table
does not move, no other <code class="language-plaintext highlighter-rouge">sh_offset</code> changes. The edit is <strong>sixteen bytes</strong>, at a file offset you can compute from the ELF header, plus a <code class="language-plaintext highlighter-rouge">cat</code>.</p>

<svg viewBox="0 0 760 316" role="img" style="display:block;margin-inline:auto;max-width:100%;height:auto;font-family:var(--mono)" aria-label="Two file layouts compared. In the objcopy layout the whole file is drawn in red, because every byte is read into memory and written back out even though almost none of it changes. In the reserve-and-append layout the same file is drawn untouched in grey, with a one-byte placeholder section already present, a single highlighted entry inside the section header table, and a new payload block appended past the end of the file; an arrow runs from that entry to the payload.">

  <!-- panel one: objcopy has to rebuild the file -->
  <text x="20" y="20" fill="currentColor" font-size="14" font-weight="600">objcopy --add-section</text>
  <text x="740" y="20" fill="#b1201d" font-size="14" font-weight="600" text-anchor="end">every byte copied</text>

  <g stroke="#b1201d" stroke-width="1" fill="#b1201d" fill-opacity="0.16">
    <rect x="20" y="58" width="48" height="34" rx="3" />
    <rect x="72" y="58" width="54" height="34" rx="3" />
    <rect x="130" y="58" width="310" height="34" rx="3" />
    <rect x="458" y="58" width="80" height="34" rx="3" />
    <rect x="542" y="58" width="150" height="34" rx="3" />
  </g>
  <g fill="currentColor" font-size="11" text-anchor="middle">
    <text x="44" y="80">ehdr</text>
    <text x="99" y="80">phdrs</text>
    <text x="285" y="80">.text .rodata …</text>
    <text x="498" y="80">.shstrtab</text>
    <text x="617" y="80">section headers</text>
  </g>
  <path d="M20 104 l0 8 l672 0 l0 -8" stroke="#b1201d" stroke-width="1.5" fill="none" />
  <text x="356" y="128" fill="#b1201d" font-size="11" text-anchor="middle">read into a model, serialized again — 1,238 bytes of it actually differ</text>

  <!-- panel two: the section already exists, so only its entry changes -->
  <text x="20" y="188" fill="currentColor" font-size="14" font-weight="600">reserve one byte, then append</text>
  <text x="740" y="188" fill="#b1201d" font-size="14" font-weight="600" text-anchor="end">16 bytes + a tail</text>

  <g stroke="#8a8580" stroke-width="1" fill="#8a8580" fill-opacity="0.12">
    <rect x="20" y="222" width="48" height="34" rx="3" />
    <rect x="72" y="222" width="54" height="34" rx="3" />
    <rect x="130" y="222" width="310" height="34" rx="3" />
    <rect x="458" y="222" width="80" height="34" rx="3" />
    <rect x="542" y="222" width="150" height="34" rx="3" />
  </g>
  <rect x="444" y="222" width="10" height="34" rx="2" fill="#8a8580" fill-opacity="0.35" />
  <rect x="650" y="224" width="14" height="30" rx="2" fill="#b1201d" />
  <rect x="700" y="222" width="40" height="34" rx="3" fill="#b1201d" fill-opacity="0.2" stroke="#b1201d" stroke-width="1" />
  <g fill="currentColor" font-size="11" text-anchor="middle">
    <text x="44" y="244">ehdr</text>
    <text x="99" y="244">phdrs</text>
    <text x="285" y="244">.text .rodata …</text>
    <text x="498" y="244">.shstrtab</text>
    <text x="596" y="244">section headers</text>
    <text x="720" y="244">payload</text>
  </g>

  <line x1="449" y1="256" x2="449" y2="272" stroke="#8a8580" stroke-width="1.5" />
  <text x="449" y="288" fill="currentColor" font-size="11" text-anchor="middle" opacity="0.75">the reserved byte</text>

  <path d="M657 258 l0 20 l63 0 l0 -18" stroke="#b1201d" stroke-width="1.5" fill="none" />
  <path d="M720 256 l-5 9 l10 0 z" fill="#b1201d" />
  <text x="700" y="300" fill="#b1201d" font-size="11" text-anchor="middle">sh_offset, sh_size</text>
</svg>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
Why 1 byte?
Turns out that <code class="language-plaintext highlighter-rouge">llvm-objcopy</code> and GNU <code class="language-plaintext highlighter-rouge">objcopy</code> disagree on whether an empty section is a valid ELF. The one byte is a cheap way to make both linkers happy.</p>
</blockquote>

<p>The payload lands <em>after</em> the section header table, which looks alarming the first time
you see it but is completely legal. Nothing in ELF says section contents must precede the
section header table. The kernel also never looks at section headers also, it loads
<code class="language-plaintext highlighter-rouge">PT_LOAD</code> segments out of the program headers, which we do not touch.</p>

<h1 id="benchmark">Benchmark</h1>

<p>I wrote a small C version to benchmark it in contrast, please be mindful that this
graph is log-log.</p>

<details>
  <summary>elfstamp.c</summary>

  <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* elfstamp -- point a pre-reserved ELF section at data appended to the file.
 *
 * usage: elfstamp &lt;elf&gt; &lt;section-name&gt; &lt;payload-file&gt;
 *
 * The section must already have a header in the file; this never adds one.
 * Nothing that already exists is moved, so the only things ever held in
 * memory are one section header, the section-name string table and a fixed
 * copy buffer -- regardless of how large the ELF is. */</span>
<span class="cp">#define _GNU_SOURCE
#include</span> <span class="cpf">&lt;elf.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;errno.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stddef.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
</span>
<span class="cm">/* Fixed staging buffer for the append; the whole point is that this number
   does not depend on the size of the executable being stamped. */</span>
<span class="cp">#define COPY_CHUNK (64 * 1024)
</span>
<span class="cm">/* Offsets of the two fields inside an Elf64_Shdr that this tool rewrites. */</span>
<span class="cp">#define SHDR_OFF_SH_OFFSET offsetof(Elf64_Shdr, sh_offset)
#define SHDR_OFF_SH_SIZE offsetof(Elf64_Shdr, sh_size)
</span>
<span class="cm">/* Payload placement alignment. Nothing requires more than this for a
   non-allocated note, and it keeps the arithmetic obvious. */</span>
<span class="cp">#define PAYLOAD_ALIGN 8
</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">die</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">what</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"elfstamp: %s: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">what</span><span class="p">,</span> <span class="n">strerror</span><span class="p">(</span><span class="n">errno</span><span class="p">));</span>
  <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">read_exact</span><span class="p">(</span><span class="kt">int</span> <span class="n">fd</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">,</span> <span class="kt">off_t</span> <span class="n">off</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pread</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">off</span><span class="p">)</span> <span class="o">!=</span> <span class="p">(</span><span class="kt">ssize_t</span><span class="p">)</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="s">"short read"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"usage: %s &lt;elf&gt; &lt;section&gt; &lt;payload&gt;</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">elf_path</span> <span class="o">=</span> <span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="o">*</span><span class="n">want</span> <span class="o">=</span> <span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="o">*</span><span class="n">payload_path</span> <span class="o">=</span> <span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>

  <span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="n">elf_path</span><span class="p">,</span> <span class="n">O_RDWR</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="n">elf_path</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="cm">/* The header tells us where the section header table is; that table is the
     only index we need, and we walk it one entry at a time. */</span>
  <span class="n">Elf64_Ehdr</span> <span class="n">eh</span><span class="p">;</span>
  <span class="n">read_exact</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">eh</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">eh</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">memcmp</span><span class="p">(</span><span class="n">eh</span><span class="p">.</span><span class="n">e_ident</span><span class="p">,</span> <span class="n">ELFMAG</span><span class="p">,</span> <span class="n">SELFMAG</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">||</span>
      <span class="n">eh</span><span class="p">.</span><span class="n">e_ident</span><span class="p">[</span><span class="n">EI_CLASS</span><span class="p">]</span> <span class="o">!=</span> <span class="n">ELFCLASS64</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"elfstamp: not a 64-bit ELF</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="cm">/* Section names live in their own string table; read just that section. */</span>
  <span class="n">Elf64_Shdr</span> <span class="n">sh</span><span class="p">;</span>
  <span class="n">read_exact</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sh</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">sh</span><span class="p">,</span> <span class="n">eh</span><span class="p">.</span><span class="n">e_shoff</span> <span class="o">+</span> <span class="p">(</span><span class="kt">off_t</span><span class="p">)</span><span class="n">eh</span><span class="p">.</span><span class="n">e_shstrndx</span> <span class="o">*</span> <span class="n">eh</span><span class="p">.</span><span class="n">e_shentsize</span><span class="p">);</span>
  <span class="kt">char</span> <span class="o">*</span><span class="n">shstr</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">sh</span><span class="p">.</span><span class="n">sh_size</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">shstr</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="s">"malloc"</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">read_exact</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">shstr</span><span class="p">,</span> <span class="n">sh</span><span class="p">.</span><span class="n">sh_size</span><span class="p">,</span> <span class="n">sh</span><span class="p">.</span><span class="n">sh_offset</span><span class="p">);</span>

  <span class="cm">/* Find the placeholder section header the linker already emitted. */</span>
  <span class="kt">off_t</span> <span class="n">target</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">eh</span><span class="p">.</span><span class="n">e_shnum</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">off_t</span> <span class="n">at</span> <span class="o">=</span> <span class="n">eh</span><span class="p">.</span><span class="n">e_shoff</span> <span class="o">+</span> <span class="p">(</span><span class="kt">off_t</span><span class="p">)</span><span class="n">i</span> <span class="o">*</span> <span class="n">eh</span><span class="p">.</span><span class="n">e_shentsize</span><span class="p">;</span>
    <span class="n">read_exact</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sh</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">sh</span><span class="p">,</span> <span class="n">at</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">strcmp</span><span class="p">(</span><span class="n">shstr</span> <span class="o">+</span> <span class="n">sh</span><span class="p">.</span><span class="n">sh_name</span><span class="p">,</span> <span class="n">want</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">target</span> <span class="o">=</span> <span class="n">at</span><span class="p">;</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="n">free</span><span class="p">(</span><span class="n">shstr</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">target</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"elfstamp: no section named '%s'</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">want</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">sh</span><span class="p">.</span><span class="n">sh_flags</span> <span class="o">&amp;</span> <span class="n">SHF_ALLOC</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"elfstamp: '%s' is SHF_ALLOC; it is mapped and cannot move</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">want</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="cm">/* Append the payload past everything, aligned. Nothing already in the file
     is read or rewritten, so this is a pure O(payload) copy. */</span>
  <span class="kt">off_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">lseek</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">SEEK_END</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">end</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="s">"lseek"</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kt">off_t</span> <span class="n">where</span> <span class="o">=</span> <span class="p">(</span><span class="n">end</span> <span class="o">+</span> <span class="n">PAYLOAD_ALIGN</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="o">~</span><span class="p">(</span><span class="kt">off_t</span><span class="p">)(</span><span class="n">PAYLOAD_ALIGN</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">ftruncate</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">where</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="s">"ftruncate"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kt">int</span> <span class="n">pfd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="n">payload_path</span><span class="p">,</span> <span class="n">O_RDONLY</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pfd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="n">payload_path</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="n">COPY_CHUNK</span><span class="p">];</span>
  <span class="kt">uint64_t</span> <span class="n">written</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
    <span class="kt">ssize_t</span> <span class="n">n</span> <span class="o">=</span> <span class="n">read</span><span class="p">(</span><span class="n">pfd</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">buf</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">die</span><span class="p">(</span><span class="s">"read payload"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">pwrite</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">where</span> <span class="o">+</span> <span class="n">written</span><span class="p">)</span> <span class="o">!=</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">die</span><span class="p">(</span><span class="s">"write payload"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">written</span> <span class="o">+=</span> <span class="n">n</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="n">close</span><span class="p">(</span><span class="n">pfd</span><span class="p">);</span>

  <span class="cm">/* Repoint the section header: sixteen bytes, in place. */</span>
  <span class="kt">uint64_t</span> <span class="n">off64</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">where</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">pwrite</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">off64</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">off64</span><span class="p">,</span> <span class="n">target</span> <span class="o">+</span> <span class="n">SHDR_OFF_SH_OFFSET</span><span class="p">)</span> <span class="o">!=</span> <span class="k">sizeof</span> <span class="n">off64</span> <span class="o">||</span>
      <span class="n">pwrite</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">written</span><span class="p">,</span> <span class="k">sizeof</span> <span class="n">written</span><span class="p">,</span> <span class="n">target</span> <span class="o">+</span> <span class="n">SHDR_OFF_SH_SIZE</span><span class="p">)</span> <span class="o">!=</span> <span class="k">sizeof</span> <span class="n">written</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">die</span><span class="p">(</span><span class="s">"patch section header"</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"%s: .%s -&gt; %llu bytes at 0x%llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">elf_path</span><span class="p">,</span> <span class="n">want</span><span class="p">,</span>
         <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span><span class="n">written</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span><span class="n">where</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>

</details>

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

# ru_maxrss of a single stamping command, via os.wait4. NixOS,
# llvm-objcopy 20.1.8, 88-byte JSON payload.
sizes = [4, 16, 64, 256, 512, 1024]
df = pd.DataFrame({
    "mib": sizes * 2,
    "tool": ["llvm-objcopy"] * 6 + ["append + 16 bytes"] * 6,
    "rss": [52.7, 76.9, 172.9, 556.8, 1068.6, 2092.6,
            9.5, 9.5, 9.5, 9.5, 9.5, 9.5],
})
df["tool"] = pd.Categorical(
    df["tool"], categories=["llvm-objcopy", "append + 16 bytes"], ordered=True)

# Log-log, because the interesting thing is the slope: two lines at 45 degrees
# and one that is flat.
plot = (
    ggplot(df, aes("mib", "rss", color="tool"))
    + geom_line(size=1.0)
    + geom_point(size=2.6)
    + scale_x_log10(breaks=sizes, labels=[str(s) for s in sizes])
    + scale_y_log10(breaks=[10, 30, 100, 300, 1000, 3000],
                    labels=lambda bs: [f"{b:g}" for b in bs])
    + scale_color_manual(values={"llvm-objcopy": "#b1201d",
                                 "append + 16 bytes": "#4c72b0"})
    + labs(x="executable size (MiB)", y="peak RSS (MiB)", color="")
    + theme(legend_position="top")
)
plot.width, plot.height = 7.0, 3.4
</code></pre>

<p>Our trick works! The “append + 16 bytes” approach is constant memory.
Not only is the peak RSS constant, but the wall time is also constant and much faster
by avoiding the read and write of the whole file.</p>

<h1 id="one-trick-pony">One trick pony</h1>

<p>It is often easy to reach for general-purpose tools like <code class="language-plaintext highlighter-rouge">objcopy</code> as they are a swiss-army knife for manipulating object files. What I like about this trick though is that there are meaningful improvements to be made by writing special purpose tools and that does not mean we have to accrue large maintenance costs. In this case it was a tiny 200-line C program.</p>

<p>The economics of these tools is also changing with the rise of LLMs in our workflow. While many are concerned about the influx of generated code, I remain optimistic that we
we can use them to find such opportunities.</p>

<p>Don’t be afraid to write a small tool to solve a specific problem.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:gnu">
      <p>For those thinking this is an LLVM specific issue, GNU <code class="language-plaintext highlighter-rouge">objcopy</code> exhibits
    the same behavior. <a href="#fnref:gnu" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[This is a fun little trick I came across at $DAYJOB. I did not invent it, but I thought it was interesting enough to understand better and share.]]></summary></entry><entry><title type="html">Actually Queryable Executables</title><link href="https://fzakaria.com/2026/08/24/actually-queryable-executables" rel="alternate" type="text/html" title="Actually Queryable Executables" /><published>2026-08-24T17:00:00-07:00</published><updated>2026-08-24T17:00:00-07:00</updated><id>https://fzakaria.com/2026/08/24/actually-queryable-executables</id><content type="html" xml:base="https://fzakaria.com/2026/08/24/actually-queryable-executables"><![CDATA[<p>I was pleasantly surprised and happy to see that my article ‘<a href="/2026/08/23/your-executable-is-a-sqlite-database">Your executable is a SQLite database</a>’ resonated with people. It is a format I have been thinking about for a while, and the idea seems to have
struck a chord with others.</p>

<p><img src="/assets/images/sqlite_is_life.png" alt="meme of Danny from Ted Lasso saying sqlite is life" /></p>

<p>A quick recap: <strong>SELF</strong>, a format where the program is a SQLite database. We can use <a href="https://docs.kernel.org/admin-guide/binfmt-misc.html">binfmt_misc</a> to trigger a custom
interpreter that maps the rows in the <code class="language-plaintext highlighter-rouge">segments</code> table and jumps to the entry point,
and a whole class of binary tooling collapses into SQL.</p>

<p>What keeps surprising me is how having the file format be a SQLite database keeps collapsing everything into SQL. One idea that was immediately evident to myself and others through comments: If the executable is a database, and a database is something you can write to, can the <em>running program</em> use it to also store its state? 🤔</p>

<p>Yes! 🤯
We can collapse not only a complete distribution but all the state for every application into a single file, alleviating the need for <code class="language-plaintext highlighter-rouge">/var/</code> or <code class="language-plaintext highlighter-rouge">/tmp/</code> or <code class="language-plaintext highlighter-rouge">/home/</code> or any other filesystem. The program can store its own state in the same file it is running from, and it can do so transactionally.</p>

<p><strong><a href="https://github.com/fzakaria/selfdb/tree/main/examples/server">self-httpd</a></strong> is a proof-of-concept webserver that does exactly that. It is a single file program executed from a database. The file contains the program, the website, the routes and all the visitor logs. All state is updated <strong>in the same SQLite</strong> file as the program itself.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Our server is a single file, and it is a SQLite database
</span><span class="gp">$</span><span class="w"> </span>file server
<span class="go">server: SQLite 3.x database, application id 1397050438, ...

</span><span class="gp">$</span><span class="w"> </span>./server <span class="nt">--journal</span> wal 8080
<span class="go">self-httpd: serving 3 routes out of /srv/self/server
self-httpd: listening on http://0.0.0.0:8080 with 4 workers

</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-s</span> localhost:8080 | <span class="nb">head</span> <span class="nt">-1</span>
<span class="go">&lt;!doctype html&gt;

</span><span class="c"># nobody has pressed the button on that page yet
</span><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s1">'SELECT count(*) FROM presses'</span>
<span class="go">0

</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-s</span> <span class="nt">-X</span> POST <span class="nt">-d</span> press localhost:8080/api/press
<span class="go">{"presses":1,"button":"press"}

</span><span class="c"># the application data is inside the same database
</span><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s1">'SELECT id, at, button FROM presses'</span>
<span class="go">1|2026-08-25 03:11:28|press

</span><span class="c"># so was the GET that fetched the page in the first place
</span><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s1">'SELECT count(*) AS n, path
</span><span class="go">                  FROM visits GROUP BY path'
1|/
1|/api/press
</span></code></pre></div></div>

<p>This web-server is live at <strong><a href="https://selfdb.exe.xyz">https://selfdb.exe.xyz</a></strong>.<sup id="fnref:exe"><a href="#fn:exe" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> It is one file, a SQLite database, and it is also the server. It is the website, it is the program, and it is the visitor log and state.</p>

<p><img src="/assets/images/selfdb-exe-xyz.png" alt="Screenshot of selfdb.exe.xyz. The heading reads &quot;This page is a row in the
executable that served it.&quot; Below it a console block shows `file server`
reporting a SQLite database with application id 1397050438, and `xxd` showing
the bytes &quot;....SELF&quot; at offset 68. Under the heading &quot;What is in it, right
now&quot; is a grid of live counters read out of the file while it answered the
request: 13 segments, 179 symbols, 105 relocations, 2 needed libraries, 3
routes, 12 tables, 103 visits recorded, 24 presses
recorded." /></p>

<h1 id="everything-is-my-demon-muse">Everything is my demon muse</h1>

<p>I have a lot of admiration for the work of <a href="https://justine.lol/">Justine Tunney</a>,
whose prior art <a href="https://redbean.dev">redbean</a>: a webserver in a
single file, built as an <a href="https://github.com/jart/cosmopolitan">Actually Portable Executable</a> with a self-extracting ZIP archive, inspired the idea.</p>

<p>SELF is many ways is less brilliant. It relies on simpler tools to achieve something
very similar but I’m amazed how much collapses into a single domain: SQL.</p>

<p>Whereas, redbean needs to include an archive format (ZIP), the database
itself is the container. Redbean provides Lua hooks to manipulate the responses,
whereas the equivalent in SELF is a new row in a <code class="language-plaintext highlighter-rouge">handlers</code> table.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">handlers</span> <span class="k">VALUES</span>
  <span class="p">(</span><span class="s1">'/api/busiest'</span><span class="p">,</span> <span class="s1">'SELECT path, count(*)
                    FROM visits GROUP BY path
                    ORDER BY 2 DESC LIMIT 5'</span><span class="p">);</span>
</code></pre></div></div>

<p>If redbean is an <strong>Actually Portable Executable</strong>, this is an <strong>Actually Queryable Executable</strong>. One of them runs anywhere, the other one you can <code class="language-plaintext highlighter-rouge">SELECT</code> from.</p>

<h1 id="all-you-need-is-argv0">All you need is <code class="language-plaintext highlighter-rouge">argv[0]</code></h1>

<p>How does the process get access to itself? 🤔</p>

<p>For now, you cannot use <code class="language-plaintext highlighter-rouge">/proc/self/exe</code>.<sup id="fnref:transparent"><a href="#fn:transparent" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> When <code class="language-plaintext highlighter-rouge">binfmt_misc</code> matches, the kernel does
not <code class="language-plaintext highlighter-rouge">execve</code> your file at all , it execs the <em>interpreter</em>,  and hands it the
path:</p>

<p><code class="language-plaintext highlighter-rouge">self-exec</code> passes <code class="language-plaintext highlighter-rouge">argv + 1</code> through to the program, so the program’s
<code class="language-plaintext highlighter-rouge">argv[0]</code> is the path to the executable itself. The interpreter also releases its SQLite connection before jumping to the entry point, so the program can open its own file and query it.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">sqlite3</span> <span class="o">*</span><span class="n">db</span><span class="p">;</span>
	<span class="cm">/* the file the kernel just executed */</span>
	<span class="n">sqlite3_open</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">db</span><span class="p">);</span>
	<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is pretty unrestricted and <em>magical</em>. You can read your own segment table or a new table next to it. The writes persist across invocations. ✨</p>

<h1 id="self-httpd">self-httpd</h1>

<p>The web-server for our example is three tables: <code class="language-plaintext highlighter-rouge">routes</code>, <code class="language-plaintext highlighter-rouge">visits</code> and <code class="language-plaintext highlighter-rouge">presses</code>.
We will record every visitor and every button press.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- the content, added to the executable</span>
<span class="c1">-- after it is compiled and linked</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">routes</span>  <span class="p">(</span><span class="n">path</span> <span class="nb">TEXT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
                      <span class="n">mime</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">body</span> <span class="nb">BLOB</span><span class="p">);</span>
<span class="c1">-- what the site collects, written back </span>
<span class="c1">-- into the executable while it runs</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">visits</span>  <span class="p">(</span><span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="k">at</span> <span class="nb">TEXT</span><span class="p">,</span>
                      <span class="n">ua</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">path</span> <span class="nb">TEXT</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">presses</span> <span class="p">(</span><span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
                      <span class="k">at</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">button</span> <span class="nb">TEXT</span><span class="p">);</span>
</code></pre></div></div>

<p>Building the application feels very unremarkable and familiar. We execute DDL to
create the application schema and <code class="language-plaintext highlighter-rouge">INSERT</code> the website.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># an ordinary ELF for now
</span><span class="gp">$</span><span class="w"> </span>cc <span class="nt">-O2</span> server.c <span class="nt">-o</span> server.elf <span class="si">$(</span>pkg-config <span class="nt">--libs</span> sqlite3<span class="si">)</span>
<span class="c"># the same program, as rows
</span><span class="gp">$</span><span class="w"> </span>elf2self server.elf server
<span class="gp">$</span><span class="w"> </span>sqlite3 server &lt; site/schema.sql
<span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s2">"INSERT INTO routes VALUES
</span><span class="go">                    ('/index.html', 'text/html',
                     readfile('site/index.html'))"
</span></code></pre></div></div>

<p>The asset pipeline looks like a “normal webserver” until you realize it’s querying itself
with SQL for the content. Oh, and “itself” is a SQLite database.</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">11</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.75</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="o">]</span>

  <span class="nv">req</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"GET /"</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">plaintext</span><span class="o">]</span>
  <span class="nv">krn</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"execve()\nbinfmt_misc"</span><span class="o">]</span>
  <span class="nv">se</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"self-exec"</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">note</span><span class="o">]</span>
  <span class="nv">proc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"running\nserver"</span><span class="o">]</span>
  <span class="nv">rsp</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"200 OK"</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">plaintext</span><span class="o">]</span>

  <span class="k">subgraph</span> <span class="nv">cluster_file</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"server — the same file!"</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">11</span>
    <span class="n">style</span><span class="p">=</span><span class="nv">rounded</span>
    <span class="nv">seg</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"segments\n(the program)"</span><span class="o">]</span>
    <span class="nv">rt</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"routes\n(the website)"</span><span class="o">]</span>
    <span class="nv">vis</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"visits\n(the log)"</span><span class="o">]</span>
  <span class="p">}</span>

  <span class="nv">krn</span> <span class="o">-&gt;</span> <span class="nv">se</span>
  <span class="nv">se</span> <span class="o">-&gt;</span> <span class="nv">seg</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"SELECT content"</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span>
  <span class="nv">se</span> <span class="o">-&gt;</span> <span class="nv">proc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"map, jump"</span><span class="o">]</span>
  <span class="nv">req</span> <span class="o">-&gt;</span> <span class="nv">proc</span>
  <span class="nv">proc</span> <span class="o">-&gt;</span> <span class="nv">rt</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"SELECT body"</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span>
  <span class="nv">proc</span> <span class="o">-&gt;</span> <span class="nv">vis</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"INSERT"</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span>
  <span class="nv">proc</span> <span class="o">-&gt;</span> <span class="nv">rsp</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The page at <a href="https://selfdb.exe.xyz">https://selfdb.exe.xyz</a> shows a lot of fun additional information besides
the visitor log and button presses. I included segments, symbols and relocations. Those are not baked in at built time, they are queried from itself while running.</p>

<h1 id="editing-a-live-site-is-a-transaction">Editing a live site is a transaction</h1>

<p>Once you have the capability to do ACID transactions, interesting things become possible.
The webserver can edit its own content while it is running, and the edits are transactional. The <code class="language-plaintext highlighter-rouge">UPDATE</code> is committed to the same file as the program, and a <code class="language-plaintext highlighter-rouge">ROLLBACK</code> undoes it.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># change the running site. no restart, no reload, no deploy
</span><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s2">"UPDATE routes SET body = readfile('new.html')
</span><span class="go">                  WHERE path = '/index.html'"
</span><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-s</span> localhost:8080
<span class="go">&lt;!doctype html&gt;&lt;h1&gt;edited in place&lt;/h1&gt;
</span></code></pre></div></div>

<p>Since the file format is SQLite we can also take advantage of the cornicopea of tooling
that exists. <code class="language-plaintext highlighter-rouge">sqldiff</code> will tell you exactly what a “deploy did”, this can let us audit and identify changes between two versions of the same program.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sqldiff <span class="nt">--summary</span> yesterday.server server
<span class="go">routes:      1 changes, 0 inserts, 0 deletes, 2 unchanged
segments:    0 changes, 0 inserts, 0 deletes, 13 unchanged
symbols:     0 changes, 0 inserts, 0 deletes, 174 unchanged
relocations: 0 changes, 0 inserts, 0 deletes, 99 unchanged
</span></code></pre></div></div>

<p>What about full-text search? <a href="https://www.sqlite.org/fts5.html">FTS5</a> is a <code class="language-plaintext highlighter-rouge">CREATE VIRTUAL TABLE</code> away, so a webserver can index its own pages, inside itself, and still be a webserver afterwards:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s2">"CREATE VIRTUAL TABLE search USING fts5(path, body);
</span><span class="go">                  INSERT INTO search SELECT path, body FROM routes
                    WHERE mime LIKE 'text/%'"

</span><span class="gp">$</span><span class="w"> </span>sqlite3 server <span class="s2">"SELECT path, snippet(search, 1, '[', ']', '...', 6)
</span><span class="go">                  FROM search WHERE search MATCH 'transaction'"
/index.html|...Editing is a [transaction].&lt;/h2&gt;

</span><span class="c"># still runs. it just knows about itself now
</span><span class="gp">$</span><span class="w"> </span>./server 8080
</code></pre></div></div>

<p>None of that is machinery I wrote. It is machinery SQLite already has, that a
program inherits for free by being a database.</p>

<p>All the rage <em>was</em> static site generators, but the future is an <strong>actually queryable executable</strong>.</p>

<h1 id="deploying-is-scp-of-one-file">Deploying is <code class="language-plaintext highlighter-rouge">scp</code> of one file</h1>

<p>I am really enjoying the simplicity that seems to be popular and heralded by
products like <a href="https://exe.dev/">exe.dev</a>. People often yearn to go back to the
“good old days” of <code class="language-plaintext highlighter-rouge">scp</code> and <code class="language-plaintext highlighter-rouge">ssh</code> to deploy a single file, and SELF is a format that makes that possible again, but better! Rather than just shipping an archive of PHP, we ship the whole system or application closure down to the <code class="language-plaintext highlighter-rouge">libc</code>.</p>

<p>How would we make a deployment if the data and code is intertwined?</p>

<p>We can think of a redeploy as a data migration, and the migration
is two <code class="language-plaintext highlighter-rouge">INSERT ... SELECT</code>, because the program and its data are the same
file!</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- the running deployment</span>
<span class="n">ATTACH</span> <span class="s1">'/srv/self/server'</span> <span class="k">AS</span> <span class="k">old</span><span class="p">;</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">visits</span>  <span class="p">(</span><span class="k">at</span><span class="p">,</span> <span class="n">ua</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>
  <span class="k">SELECT</span> <span class="k">at</span><span class="p">,</span> <span class="n">ua</span><span class="p">,</span> <span class="n">path</span> <span class="k">FROM</span> <span class="k">old</span><span class="p">.</span><span class="n">visits</span><span class="p">;</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">presses</span> <span class="p">(</span><span class="k">at</span><span class="p">,</span> <span class="n">button</span><span class="p">)</span>
  <span class="k">SELECT</span> <span class="k">at</span><span class="p">,</span> <span class="n">button</span> <span class="k">FROM</span> <span class="k">old</span><span class="p">.</span><span class="n">presses</span><span class="p">;</span>
</code></pre></div></div>

<p>Swap the file, restart, and the visitor log survives the new build.
You can even do this for the program itself in reverse. The <code class="language-plaintext highlighter-rouge">segments</code> table is just like any other table. 😈</p>

<h1 id="go-press-the-button">Go press the button</h1>

<p><a href="https://selfdb.exe.xyz">https://selfdb.exe.xyz</a> has a button on it. Pressing it is an <code class="language-plaintext highlighter-rouge">INSERT</code> into
the executable that served you the page</p>

<p>The code is at <a href="https://github.com/fzakaria/selfdb">fzakaria/selfdb</a> if you
are curious. It is probably a bit <em>half-baked</em>, and definitely AI assisted, but that’s OK with me. I wanted to explore this idea and see if it was feasible and what might be possible.</p>

<p>I think I only scratched the surface of some of the fun possibilities. I am curious to see what others might do with it, and I would love to see a few more examples of “actually queryable executables” in the wild.<sup id="fnref:mark"><a href="#fn:mark" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>Turns out that when we re-envision what we considered to be simply a <em>byte layout</em>
specification was actually better off being a <em>database</em>, a lot of machinery we have been using for decades simply stops being necessary. The program is the database, and the database is the program.</p>

<blockquote>
  <p>“Never, ever underestimate the importance of having fun”</p>

  <p>– Randy Pausch</p>
</blockquote>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:exe">
      <p>If the site is not working for you, sorry. I deployed it on their smallest tier.
    I included a screenshot of the site just in case for posterity! <a href="#fnref:exe" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:transparent">
      <p>Funny enough, the VFS Linux maintainer recently landed support for 
            transparent <code class="language-plaintext highlighter-rouge">binfmt_misc</code> in the kernel, which would make <code class="language-plaintext highlighter-rouge">/proc/self/exe</code> point to the original file. I wrote <a href="/2026/07/20/linux-kernel-will-support-origin-sort-of">about it here</a>. <a href="#fnref:transparent" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:mark">
      <p>One idea a friend suggested was discovery over multicase DNS to spread
     program updates via transactions. <a href="#fnref:mark" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I was pleasantly surprised and happy to see that my article ‘Your executable is a SQLite database’ resonated with people. It is a format I have been thinking about for a while, and the idea seems to have struck a chord with others.]]></summary></entry><entry><title type="html">Your executable is a SQLite database</title><link href="https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database" rel="alternate" type="text/html" title="Your executable is a SQLite database" /><published>2026-08-23T07:30:00-07:00</published><updated>2026-08-23T07:30:00-07:00</updated><id>https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database</id><content type="html" xml:base="https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database"><![CDATA[<p>I have been probably obsessed with two things in the last few years: Nix as a tool to explore
innovative ideas that require the capability to rebuild the world and replacing ELF with SQLite as an executable format. You might have noticed that these two ideas are well suited to each other.</p>

<p>I explored the idea during my PhD thesis but found feedback from others unmotivating. 
Radical ideas are hard to sell, as you are working against the inertia of the established
solution.</p>

<p><img src="/assets/images/get-better-material-sqlite.png" alt="Four-panel comic. A crow at a microphone says &quot;Nix is great&quot;; the audience
boos and shouts &quot;get better material&quot;; the crow looks stricken; the last panel
shows its remaining cue cards, which read &quot;SQLite can be an object file format&quot;." style="--image-width: 26rem" /></p>

<p>One of the end results of that exploration was <a href="/2023/03/19/sqlelf-and-20-years-of-nix">sqlelf</a>,
a tool that lets you explore an ELF file declaratively using SQL.<sup id="fnref:sqlelf"><a href="#fn:sqlelf" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>
<code class="language-plaintext highlighter-rouge">SELECT name FROM elf_symbols</code> instead of fiddling with <code class="language-plaintext highlighter-rouge">readelf</code> and <code class="language-plaintext highlighter-rouge">grep</code>.
It was remarkably simple by leveraging <em>virtual tables</em> over the ELF: however I found it
to be a refreshing improvement to explore the ELF file format. I knew however that
there is still something much bigger to be done.</p>

<p>I never let the idea go and with the recent improvements with LLMs, I find it compelling to revisit these ideas to explore further. Specifically, can we replace ELF with SQLite as an executable format? 🤔</p>

<p>Not “a database that describes an executable”, but the actual file you <code class="language-plaintext highlighter-rouge">chmod +x</code>
and run.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>file hello
<span class="go">hello: SQLite 3.x database, application id 0x53454c46, user version 1

</span><span class="gp">$</span><span class="w"> </span>./hello
<span class="go">Hello, world!

</span><span class="gp">$</span><span class="w"> </span>sqlite3 hello <span class="s1">'SELECT soname FROM ldd'</span>
<span class="go">libc.so.6
</span></code></pre></div></div>

<p>I developed a pretty fleshed out prototype. It is called <strong>SELF</strong>, the <em>Structured Executable &amp; Linkable Format</em>, because I am unoriginal. It is on <a href="https://github.com/fzakaria/selfdb">GitHub</a> if you are interested. I’m surprised about all the interesting things that fall out of this idea.</p>

<h1 id="elf-is-a-database-that-refuses-to-admit-it">ELF is a database that refuses to admit it</h1>

<p>Working through my PhD, I realized something that bugged me. ELF is <em>already</em> a database. It just implements many database primitives by hand, along with a surprising number of
data structures for performance, like a bloom filter for symbol lookup.</p>

<table>
  <thead>
    <tr>
      <th>ELF mechanism</th>
      <th>The database primitive it reinvents</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.strtab</code> / <code class="language-plaintext highlighter-rouge">.dynstr</code></td>
      <td>string interning</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.hash</code> / <code class="language-plaintext highlighter-rouge">.gnu.hash</code></td>
      <td>an index (<code class="language-plaintext highlighter-rouge">CREATE INDEX</code>)</td>
    </tr>
    <tr>
      <td>section header table</td>
      <td><code class="language-plaintext highlighter-rouge">sqlite_schema</code>, a table of tables</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">st_name</code> → offset into <code class="language-plaintext highlighter-rouge">.strtab</code></td>
      <td>a foreign key, done by hand</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sh_offset</code> / <code class="language-plaintext highlighter-rouge">sh_size</code></td>
      <td>the record layout of a b-tree page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">.gnu.version_r</code></td>
      <td>a column</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">objcopy --strip-debug</code></td>
      <td><code class="language-plaintext highlighter-rouge">DELETE</code> + <code class="language-plaintext highlighter-rouge">VACUUM</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ldconfig</code> cache, <code class="language-plaintext highlighter-rouge">debuginfod</code></td>
      <td>out-of-band indexes over the above</td>
    </tr>
  </tbody>
</table>

<p>If you ever have to analyze or parse ELF, the kernel, <code class="language-plaintext highlighter-rouge">ld.so</code>, binutils, LIEF, goblin, <code class="language-plaintext highlighter-rouge">readelf</code>, you are re-implementing the same parser over and over again. Every producer re-implements the same serializer.</p>

<p>The format itself is incredibly terse, designed for a world where disk space and network
bandwidth was at an extreme premium. Modifying the format is hard, you often have to zero out
sections and add new ones since it is packed so tightly. There is also no self-describing schema. ELF itself is a very generic format that supports sections of data that by convention
are interpreted in specific ways but the format does not enforce it.</p>

<p>SQLite is the counter-example. They are a self-describing
format that is extremely stable. It is designed to be extended to support new features without breaking existing consumers and supporting a wide range of queries performantly.</p>

<p>If we were to replace ELF with SQLite, what would fall out and can all of the necessary information be represented in a SQLite database? The answer is yes, and it is surprisingly simple.</p>

<h1 id="what-falls-away">What falls away</h1>

<p>A SELF file needs two tables to run: <code class="language-plaintext highlighter-rouge">self_meta</code> is the ELF header as key/value
pairs and <code class="language-plaintext highlighter-rouge">segments</code> is the load image, one row per program header with the bytes
in a <code class="language-plaintext highlighter-rouge">BLOB</code>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">segments</span> <span class="p">(</span>
  <span class="c1">-- original phdr index</span>
  <span class="n">id</span>      <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="c1">-- 'load' | 'tls' | 'stack' | 'relro'</span>
  <span class="k">type</span>    <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="c1">-- original file offset</span>
  <span class="k">offset</span>  <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">vaddr</span>   <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">filesz</span>  <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">memsz</span>   <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">r</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="n">w</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="n">x</span> <span class="nb">INTEGER</span><span class="p">,</span>
  <span class="n">align</span>   <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="mi">4096</span><span class="p">,</span>
  <span class="c1">-- the segment bytes; NULL for pure BSS</span>
  <span class="n">content</span> <span class="nb">BLOB</span>
<span class="p">);</span>
</code></pre></div></div>

<p>A single table for the symbol table replaces many of the ELF sections and the <code class="language-plaintext highlighter-rouge">.gnu.hash</code> index. It is a single table with a single index:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">symbols</span> <span class="p">(</span>
  <span class="n">id</span>      <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">name</span>    <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="c1">-- 'GLIBC_2.2.5'</span>
  <span class="k">version</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">value</span>   <span class="nb">INTEGER</span><span class="p">,</span>
  <span class="k">size</span>    <span class="nb">INTEGER</span><span class="p">,</span>
  <span class="c1">-- 'func' | 'object' | 'tls' | ...</span>
  <span class="k">type</span>    <span class="nb">TEXT</span><span class="p">,</span>
  <span class="c1">-- 'global' | 'weak' | 'local'</span>
  <span class="n">bind</span>    <span class="nb">TEXT</span><span class="p">,</span>
  <span class="k">defined</span>  <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">exported</span> <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span>
<span class="p">);</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_symbols_name</span> <span class="k">ON</span> <span class="n">symbols</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="k">version</span><span class="p">);</span>
</code></pre></div></div>

<p>Our capability to include an index is equivalent to <code class="language-plaintext highlighter-rouge">.gnu.hash</code> and <code class="language-plaintext highlighter-rouge">.hash</code> in ELF, but it is a proper b-tree index maintained by SQLite instead of a hand-rolled bloom filter.<sup id="fnref:gnuhash"><a href="#fn:gnuhash" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>Surprisingly a lot more falls out as well: <code class="language-plaintext highlighter-rouge">.dynstr</code> is gone, because <code class="language-plaintext highlighter-rouge">name</code> is <code class="language-plaintext highlighter-rouge">TEXT</code> and SQLite already interns strings, symbol versioning is a column, not the <code class="language-plaintext highlighter-rouge">.gnu.version_r</code> / <code class="language-plaintext highlighter-rouge">.gnu.version_d</code> contraption and there is no need for a <code class="language-plaintext highlighter-rouge">strings</code> table.</p>

<p>Other tables exist as well for metadata which exist for tooling: <code class="language-plaintext highlighter-rouge">sections</code>,
<code class="language-plaintext highlighter-rouge">notes</code>, <code class="language-plaintext highlighter-rouge">dynamic_entries</code>. Delete them and the program still
runs, which means <code class="language-plaintext highlighter-rouge">strip(1)</code> is a transaction:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ldd(1)
</span><span class="gp">$</span><span class="w"> </span>sqlite3 hello <span class="s1">'SELECT soname FROM ldd'</span> 
<span class="go">libc.so.6

</span><span class="c"># nm -D --undefined
</span><span class="gp">$</span><span class="w"> </span>sqlite3 hello <span class="s1">'SELECT name,version FROM imports LIMIT 3'</span>
<span class="go">__libc_start_main|GLIBC_2.34
_ITM_deregisterTMCloneTable|
puts|GLIBC_2.2.5

</span><span class="c"># readelf -l
</span><span class="gp">$</span><span class="w"> </span>sqlite3 hello <span class="se">\</span>
<span class="go">    "SELECT type,vaddr,memsz,r,w,x FROM segments WHERE type='load'"
load|0|1744|1|0|0
load|4096|361|1|0|1
load|8192|312|1|0|0
load|15768|640|1|1|0

</span><span class="c"># strip(1)
</span><span class="gp">$</span><span class="w"> </span>sqlite3 hello <span class="s1">'DELETE FROM sections; DELETE FROM notes; VACUUM;'</span>
<span class="c"># 57344 -&gt; 49152 bytes
</span><span class="go">
</span><span class="c"># still runs,  the optional tables were optional
</span><span class="gp">$</span><span class="w"> </span>./hello
<span class="go">Hello, world!
</span></code></pre></div></div>

<p>All the tools that operate on ELF files for reading, reduce to queries over the database.
Any tool that modifies an ELF file, like <code class="language-plaintext highlighter-rouge">strip</code>, can operate on the database within a transaction rather than performing fragile offset surgery: <code class="language-plaintext highlighter-rouge">strip</code> is a <code class="language-plaintext highlighter-rouge">DELETE</code> and <code class="language-plaintext highlighter-rouge">VACUUM</code>. <code class="language-plaintext highlighter-rouge">patchelf</code> is an <code class="language-plaintext highlighter-rouge">UPDATE</code>.</p>

<p>Any information missing from the schema can be easily exposed via a view. For example, <code class="language-plaintext highlighter-rouge">ldd</code> is a query over the <code class="language-plaintext highlighter-rouge">needed</code> table, which is a join of the <code class="language-plaintext highlighter-rouge">symbols</code> table with the <code class="language-plaintext highlighter-rouge">segments</code> table to find the sonames of the libraries needed by the program.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">VIEW</span> <span class="n">exports</span> <span class="k">AS</span> <span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="k">version</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="k">size</span> <span class="k">FROM</span> <span class="n">symbols</span> <span class="k">WHERE</span> <span class="n">exported</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">VIEW</span> <span class="n">imports</span> <span class="k">AS</span> <span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="k">version</span> <span class="k">FROM</span> <span class="n">symbols</span> <span class="k">WHERE</span> <span class="k">defined</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">VIEW</span> <span class="n">ldd</span>     <span class="k">AS</span> <span class="k">SELECT</span> <span class="n">ord</span><span class="p">,</span> <span class="n">soname</span> <span class="k">FROM</span> <span class="n">needed</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">ord</span><span class="p">;</span>
</code></pre></div></div>

<h1 id="how-does-it-work">How does it work?</h1>

<p>SQLite reserves a 4-byte
<a href="https://sqlite.org/pragma.html#pragma_application_id"><code class="language-plaintext highlighter-rouge">application_id</code></a> at byte
offset 68 of its header, for exactly this purpose. We stamp it <code class="language-plaintext highlighter-rouge">SELF</code>, so an
ordinary SQLite database never matches:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>xxd <span class="nt">-s</span> 64 <span class="nt">-l</span> 8 hello
<span class="go">00000040: 0000 0001 5345 4c46                      ....SELF
</span></code></pre></div></div>

<p>We can now leverage <a href="https://docs.kernel.org/admin-guide/binfmt-misc.html">binfmt_misc</a>, the subsystem that allows you to invoke any binary as if it were native. We need only to register
the magic to trigger on and an interpreter that will invoke our new file format.</p>

<p>On NixOS the registration is a few lines matching the SQLite magic at offset 0
<em>and</em> <code class="language-plaintext highlighter-rouge">SELF</code> at 68:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">boot</span><span class="o">.</span><span class="nv">binfmt</span><span class="o">.</span><span class="nv">registrations</span><span class="o">.</span><span class="nv">self</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">recognitionType</span> <span class="o">=</span> <span class="s2">"magic"</span><span class="p">;</span>
  <span class="nv">offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="c"># bytes 0-15, 68-71</span>
  <span class="nv">magicOrExtension</span> <span class="o">=</span> <span class="s2">"SQLite format 3</span><span class="se">\\</span><span class="s2">x00"</span> <span class="o">+</span> <span class="o">...</span> <span class="o">+</span> <span class="s2">"SELF"</span><span class="p">;</span>
  <span class="c"># ignore the middle</span>
  <span class="nv">mask</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\\</span><span class="s2">xff..</span><span class="se">\\</span><span class="s2">x00..</span><span class="se">\\</span><span class="s2">xff"</span><span class="p">;</span>
  <span class="nv">interpreter</span> <span class="o">=</span> <span class="s2">"</span><span class="si">${</span><span class="nv">self-exec</span><span class="si">}</span><span class="s2">/bin/self-exec"</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>For now, I have a small tool <code class="language-plaintext highlighter-rouge">elf2self</code> that converts an ELF file into a SELF file. It is a simple <code class="language-plaintext highlighter-rouge">postFixup</code> hook you can opt into per package on NixOS. The tool reads the ELF, extracts the program headers and symbol table, and writes them into the SQLite database. We could look at extending <code class="language-plaintext highlighter-rouge">gcc</code> or <code class="language-plaintext highlighter-rouge">ld</code> to emit SELF directly, but for now this is a simple way to explore the idea.</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">14</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.8</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">12</span><span class="o">]</span>

  <span class="nv">elf</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"hello\n(ELF)"</span><span class="o">]</span>
  <span class="nv">conv</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"elf2self"</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">note</span><span class="o">]</span>
  <span class="nv">self</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"hello\n(SQLite db)"</span><span class="o">]</span>
  <span class="nv">krn</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"execve()\nbinfmt_misc"</span><span class="o">]</span>
  <span class="nv">interp</span><span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"self-exec\n(interpreter)"</span><span class="o">]</span>
  <span class="nv">run</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"running\nprocess"</span><span class="o">]</span>

  <span class="nv">elf</span> <span class="o">-&gt;</span> <span class="nv">conv</span> <span class="o">-&gt;</span> <span class="nv">self</span>
  <span class="nv">self</span> <span class="o">-&gt;</span> <span class="nv">krn</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"magic SELF@68"</span><span class="o">]</span>
  <span class="nv">krn</span> <span class="o">-&gt;</span> <span class="nv">interp</span> <span class="o">-&gt;</span> <span class="nv">run</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">self-exec</code> is the interpreter. It is a small C program linked against <code class="language-plaintext highlighter-rouge">libsqlite3</code>.
Its implementation is remarkably similar to that of <code class="language-plaintext highlighter-rouge">ld.so</code> but it fetches the program headers and symbol table from the database instead of reading them from the ELF file.
It maps the loadable segments into memory, relocates them, and jumps to the entry point.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
<code class="language-plaintext highlighter-rouge">self-exec</code> has to stay an ELF file. An interpreter that also matches the
registration recurses straight into <code class="language-plaintext highlighter-rouge">-ELOOP</code>.</p>
</blockquote>

<h1 id="dynamic-linking">Dynamic linking</h1>

<p>Running a static program was quick and easy but <em>boring</em> and unimaginative. The interesting part is dynamic linking, which is where the database shines.</p>

<p>I explored two different ways to do dynamic linking. The first is to keep <code class="language-plaintext highlighter-rouge">ld.so</code> and just replace the lookup with a SQL query via <code class="language-plaintext highlighter-rouge">glibc</code> <a href="https://man7.org/linux/man-pages/man7/rtld-audit.7.html">rtld-audit</a> interface, to quickly iterate on the design. The second is to replace <code class="language-plaintext highlighter-rouge">ld.so</code> entirely with a new dynamic linker that does the entire lookup and binding in SQL.</p>

<p>glibc’s rtld-audit interface lets an audit library intercept every shared object lookup (<code class="language-plaintext highlighter-rouge">la_objsearch</code>) before any filesystem search happens, <code class="language-plaintext highlighter-rouge">dlopen</code> included. The audit library can then answer the question “which library satisfies this symbol?” with a SQL query instead of walking the <code class="language-plaintext highlighter-rouge">RUNPATH</code> and <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code>. Stock <code class="language-plaintext highlighter-rouge">ld.so</code> maps and relocates it, so the full gamut of glibc features work: lazy PLT, IFUNCs, TLS and symbol versioning, while library storage are rows and library lookups are queries.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># no ELF library anywhere on disk
</span><span class="gp">$</span><span class="w"> </span><span class="nb">rm </span>libgreet.so.1
<span class="gp">$</span><span class="w"> </span>./app
<span class="go">./app: error while loading shared libraries: 
       libgreet.so.1: cannot open ...

</span><span class="gp">$</span><span class="w"> </span>self scan <span class="nt">--db</span> system.db <span class="nb">.</span>
<span class="gp">$</span><span class="w"> </span><span class="nv">SELF_SYSTEM_DB</span><span class="o">=</span>system.db <span class="nv">LD_AUDIT</span><span class="o">=</span>libself-audit.so ./app
<span class="go">Hello, world, from a SQLite library!
</span></code></pre></div></div>

<p>I was curious what a fully SQL dynamic linker would look like, so I prototyped one. It is called <code class="language-plaintext highlighter-rouge">self-ld</code> and it is a small C program that implements the dynamic linker entirely in SQL. It is a proof-of-concept, but it works. It maps every object’s segments, publishes their exports, and for each relocation patches the GOT and jumps to the start.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">value</span> <span class="o">+</span> <span class="n">o</span><span class="p">.</span><span class="n">load_bias</span>
<span class="k">FROM</span>   <span class="n">relocations</span> <span class="n">r</span>
<span class="k">JOIN</span>   <span class="n">symbols</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">r</span><span class="p">.</span><span class="n">symbol</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">id</span>
<span class="k">JOIN</span>   <span class="n">objects</span> <span class="n">o</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="k">object</span> <span class="o">=</span> <span class="n">o</span><span class="p">.</span><span class="n">id</span>
<span class="k">WHERE</span>  <span class="n">r</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="o">?</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">o</span><span class="p">.</span><span class="n">load_order</span>
<span class="k">LIMIT</span>  <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<h1 id="cost--benchmark">Cost &amp; Benchmark</h1>

<p>The two things that often matter when replacing a well-established format are size and latency. How much bigger is a SELF file than an ELF file, and how much slower is it to run?</p>

<p><strong>Size.</strong> A SELF file carries SQLite’s b-tree overhead and lands at roughly
double the ELF.</p>

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

elf  = [15.5, 273.9, 4642.4, 41061.0]
slf  = [56.0, 684.0, 10028.0, 95940.0]     # SELF, unstripped
order = ["hello", "curl", "git", "gdb"]

# A dumbbell, not bars: on a log axis a bar's baseline is arbitrary and a 2x
# gap looks like nothing. The segment IS the overhead.
gap = pd.DataFrame({"subject": order, "lo": elf, "hi": slf,
                    "ratio": [f"{s / e:.1f}x" for e, s in zip(elf, slf)]})
pts = pd.DataFrame({"subject": order * 2, "kib": elf + slf,
                    "kind": (["ELF"] * 4) + (["SELF"] * 4)})
for frame in (gap, pts):
    frame["subject"] = pd.Categorical(frame["subject"],
                                      categories=order[::-1], ordered=True)

plot = (
    ggplot()
    + geom_segment(gap, aes(y="subject", yend="subject", x="lo", xend="hi"),
                   size=1.1, color=INK, alpha=0.35)
    + geom_point(pts, aes("kib", "subject", color="kind"), size=3.4)
    + geom_text(gap, aes(x="hi", y="subject", label="ratio"), size=9,
                color=INK, ha="left", nudge_x=0.06)
    + scale_x_log10(breaks=[10, 100, 1000, 10000, 100000],
                    labels=lambda bs: [f"{b:g}" if b &lt; 1000 else f"{b / 1000:g}K"
                                       for b in bs])
    + scale_color_manual(values={"ELF": "#4c72b0", "SELF": "#b1201d"})
    + expand_limits(x=300000)
    + labs(x="on-disk size (KiB, log scale)", y="", color="")
    + theme(legend_position="top")
)
plot.width, plot.height = 7.0, 3.2
</code></pre>

<p>Similar to ELF binaries, most of that is recoverable, because the overhead is mostly the optional tables for debugging and tooling. Stripping them and deleting them is a transaction. A stripped <code class="language-plaintext highlighter-rouge">coreutils</code> SELF is 1,794,048 B against the ELF’s 1,768,632 B, that is <strong>within 1%</strong>.</p>

<p>We will see though that there are interesting ways to amortise the overhead even more which I found very unique and interesting.</p>

<p><strong>Latency.</strong> I benchmarked various binaries from a 15 KiB <code class="language-plaintext highlighter-rouge">hello</code> to a 42 MiB <code class="language-plaintext highlighter-rouge">gdb</code>
linking 47 libraries:</p>

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

# bench/big.md -- hyperfine -N, warmup 10, min-runs 60. mean +/- sd, ms.
df = pd.DataFrame({
    "subject": ["hello", "curl", "git", "gdb"] * 2,
    "kind": (["ELF"] * 4) + (["SELF"] * 4),
    "mean": [1.401, 11.069, 3.000, 86.109,
             6.915, 15.170, 20.821, 156.414],
    "sd":   [0.303, 2.669, 0.311, 3.248,
             0.678, 1.014, 3.040, 4.492],
})
df["lo"] = df["mean"] - df["sd"]
df["hi"] = df["mean"] + df["sd"]
df["subject"] = pd.Categorical(df["subject"],
                               categories=["hello", "curl", "git", "gdb"],
                               ordered=True)
df["kind"] = pd.Categorical(df["kind"], categories=["ELF", "SELF"],
                            ordered=True)

# Bars from an arbitrary baseline lie on a log axis, so these are points.
plot = (
    ggplot(df, aes("subject", "mean", color="kind"))
    + geom_errorbar(aes(ymin="lo", ymax="hi"),
                    position=position_dodge(0.45), width=0.25, size=0.7)
    + geom_point(position=position_dodge(0.45), size=3.4)
    + scale_y_log10(breaks=[1, 3, 10, 30, 100, 300],
                    labels=lambda bs: [f"{b:g}" for b in bs])
    + scale_color_manual(values={"ELF": "#4c72b0", "SELF": "#b1201d"})
    + labs(x="", y="exec latency (ms, log scale)", color="")
    + theme(legend_position="top")
)
plot.width, plot.height = 7.0, 3.6
</code></pre>

<p>There is a fixed ~5 ms to open SQLite and start the interpreter, plus a copy proportional
to the image. That copy is worse than it looks, because the b-tree pages are not mapped into memory. Two processes running the same SELF binary do not share text pages the way a normally-<code class="language-plaintext highlighter-rouge">mmap</code>‘d ELF does, because the bytes are copied out of the b-tree rather than mapped.<sup id="fnref:curl"><a href="#fn:curl" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<h1 id="the-system-is-a-closure">The system is a closure</h1>

<p>A SQLite database though need not merely be a single executable. It can be a <em>closure</em>, a single file that contains a program and all of its transitive dependencies. The <code class="language-plaintext highlighter-rouge">ldd</code> output of a program is ambiguous: it only lists the sonames of the libraries it needs, not the specific files that satisfy those needs. Nix improves upon this by explicitly resolving every edge to a specific store path via the use of <code class="language-plaintext highlighter-rouge">RUNPATH</code>.<sup id="fnref:runpath"><a href="#fn:runpath" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<p>We can do the same in SELF by storing the resolved path of each edge in the database:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">objects</span> <span class="p">(</span><span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">path</span> <span class="nb">TEXT</span> <span class="k">UNIQUE</span><span class="p">,</span>
                      <span class="n">soname</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">kind</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">is_root</span> <span class="nb">INTEGER</span><span class="p">);</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">needs</span> <span class="p">(</span>
  <span class="n">object_id</span>     <span class="nb">INTEGER</span> <span class="k">REFERENCES</span> <span class="n">objects</span><span class="p">(</span><span class="n">id</span><span class="p">),</span>
  <span class="n">ord</span>           <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">soname</span>        <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="c1">-- the FK that kills ambiguity</span>
  <span class="n">resolved_path</span> <span class="nb">TEXT</span> <span class="k">REFERENCES</span> <span class="n">objects</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">self closure</code> packs a binary and its transitive dependencies into <strong>one database</strong>
with those edges filled in. Shared library resolution stops being a guess and
becomes a foreign key and <code class="language-plaintext highlighter-rouge">ldd</code> becomes a <code class="language-plaintext highlighter-rouge">JOIN</code> 🤯:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>self closure <span class="s2">"</span><span class="si">$(</span><span class="nb">readlink</span> <span class="nt">-f</span> <span class="si">$(</span><span class="nb">command</span> <span class="nt">-v</span> <span class="nb">ls</span><span class="si">))</span><span class="s2">"</span> coreutils.db
<span class="gp">ls + closure -&gt;</span><span class="w"> </span>coreutils.db
<span class="go">
</span><span class="gp">$</span><span class="w"> </span>sqlite3 <span class="nt">-column</span> coreutils.db <span class="se">\</span>
<span class="go">    "SELECT n.soname, substr(n.resolved_path, 12, 20)
     FROM needs n JOIN objects o ON o.id = n.object_id
     WHERE o.is_root = 1"
libgmp.so.10          rfabfsmwq02sn94mb3qg
libacl.so.1           x0zgiss9hdzcsll3cswg
libattr.so.1          08nfpyc4qhzdkc37nznv
libc.so.6             8kvxvr3pmsypxiypq4g8
</span></code></pre></div></div>

<p>This single database is a closure of the <code class="language-plaintext highlighter-rouge">ls</code> executable and its five libraries:
six objects, segment bytes and all, in one 4.8 MiB file. There is no soname ambiguity inside a closure, because a closure by
construction contains exactly one provider per edge.</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.09"</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="o">]</span>

  <span class="nv">ls</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"ls\n(is_root)"</span><span class="p">,</span> <span class="n">style</span><span class="p">=</span><span class="s2">"rounded,filled"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#f4f4f4"</span><span class="o">]</span>
  <span class="nv">libc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"libc.so.6"</span><span class="o">]</span>
  <span class="nv">gmp</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"libgmp.so.10"</span><span class="o">]</span>
  <span class="nv">acl</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"libacl.so.1"</span><span class="o">]</span>
  <span class="nv">attr</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"libattr.so.1"</span><span class="o">]</span>

  <span class="nv">ls</span> <span class="o">-&gt;</span> <span class="nv">gmp</span>
  <span class="nv">ls</span> <span class="o">-&gt;</span> <span class="nv">acl</span>
  <span class="nv">ls</span> <span class="o">-&gt;</span> <span class="nv">attr</span>
  <span class="nv">ls</span> <span class="o">-&gt;</span> <span class="nv">libc</span>
  <span class="nv">acl</span> <span class="o">-&gt;</span> <span class="nv">attr</span>
  <span class="nv">acl</span> <span class="o">-&gt;</span> <span class="nv">libc</span>
  <span class="nv">attr</span> <span class="o">-&gt;</span> <span class="nv">libc</span>
  <span class="nv">gmp</span> <span class="o">-&gt;</span> <span class="nv">libc</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="how-far-does-this-go-one-file-one-userland">How far does this go? One file, one userland</h1>

<p>I hope you’ve been with me so far, because this is where it gets really interesting.
We can go even further and pack <strong>multiple closures</strong> into a single database.</p>

<p><img src="/assets/images/inception-one-file-one-userland.png" alt="Five-panel Inception meme. Cobb: &quot;your executable is a SQLite database.&quot;
Fischer: &quot;and the libraries it links?&quot; Cobb: &quot;also SQLite, so is the whole
userland, one file.&quot; Fischer: &quot;how far down does this go?&quot; Cobb, winking:
&quot;you are in one right now.&quot;" style="--image-width: 20rem" /></p>

<p>I pointed <code class="language-plaintext highlighter-rouge">self closure</code> at every ELF binary on this system’s <code class="language-plaintext highlighter-rouge">PATH</code>: 723 executables,
which pull in 400 distinct shared libraries. 1,123 objects, 346,386 symbols,
3,808 dependency edges, all as <strong>one SQLite file</strong>.</p>

<p>Turns out when you do that, the database is much smaller than you would expect.</p>

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

# 723 root executables from /run/current-system/sw/bin + their ldd closures:
# 1,123 objects total. The straw-man "every root ships its own closure"
# number (5.53 GiB) is left out on purpose -- it would flatten these three.
df = pd.DataFrame({
    "what": ["the member ELF\nfiles on disk",
             "one SQLite\ndatabase",
             "segment payload\n(program bytes)"],
    "mib": [644.4, 611.9, 576.9],
    "kind": ["ELF", "SELF", "payload"],
})
df["what"] = pd.Categorical(df["what"], categories=df["what"][::-1],
                            ordered=True)
df["label"] = [f"{v:.1f} MiB" for v in df["mib"]]

plot = (
    ggplot(df, aes("what", "mib", fill="kind"))
    + geom_col(width=0.62, show_legend=False)
    + geom_text(aes(label="label"), nudge_y=14, size=9, color=INK)
    + scale_fill_manual(values={"ELF": "#4c72b0", "SELF": "#b1201d",
                                "payload": "#8a8580"})
    + coord_flip()
    + expand_limits(y=720)
    + labs(x="", y="total size (MiB)")
)
plot.width, plot.height = 7.0, 2.6
</code></pre>

<p><strong>611.9 MiB of database against 644.4 MiB of ELF files.</strong> The whole userland,
as one queryable file, is <em>smaller</em> than the files it came from. The b-tree cost that doubled a single <code class="language-plaintext highlighter-rouge">hello</code> amortises to nearly nothing across 1,123 objects and is roughly 6% over
the actual program bytes.</p>

<p>The libraries and closure are shared across the executables very similar to how Nix might share them across multiple closures, if the store-path was the same. If every root shipped its own private closure (i.e. the AppImage model), the same 723 programs would come to
5.53 GiB but the deduplication of libraries and symbols falls out naturally from the database schema.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sqlite3 userland.db <span class="se">\</span>
<span class="go">    'SELECT count(DISTINCT soname), count(*)
     FROM objects WHERE soname IS NOT NULL'
345|399

</span><span class="gp">$</span><span class="w"> </span>sqlite3 <span class="nt">-column</span> userland.db <span class="se">\</span>
<span class="go">    'SELECT soname, count(*) FROM objects
     WHERE soname IS NOT NULL
</span><span class="gp">     GROUP BY soname HAVING count(*) &gt;</span><span class="w"> </span>1
<span class="go">     ORDER BY 2 DESC LIMIT 4'
libsystemd.so.0   3
libpthread.so.0   3
libgcc_s.so.1     3
libc.so.6         3

</span><span class="gp">$</span><span class="w"> </span>sqlite3 userland.db <span class="se">\</span>
<span class="go">    "SELECT count(*)
    FROM needs
    WHERE resolved_path IS NULL AND soname NOT LIKE 'ld-%'"
4
</span></code></pre></div></div>

<p>Many common idioms we use in ELF immediately fall out of the database. For example, <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code> is a row in a table rather than an environment variable. The <code class="language-plaintext highlighter-rouge">preload</code> table is a list of objects to map last, so their exports win. This means that turning <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code> on and off is a transaction.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>./app.self<span class="p">;</span> <span class="nb">echo</span> <span class="nv">$?</span>
<span class="go">13

</span><span class="gp">$</span><span class="w"> </span>sqlite3 system.db <span class="s2">"BEGIN;
</span><span class="gp">    CREATE TABLE preload(ord INTEGER PRIMARY KEY, path TEXT);</span><span class="w">
</span><span class="gp">    INSERT INTO preload VALUES (0, 'libmul.so.1.self');</span><span class="w">
</span><span class="gp">  COMMIT;</span><span class="s2">"</span>
<span class="go">
</span><span class="c"># same binary, no env var, no relink
</span><span class="gp">$</span><span class="w"> </span>./app.self<span class="p">;</span> <span class="nb">echo</span> <span class="nv">$?</span>
<span class="go">42

</span><span class="gp">$</span><span class="w"> </span>sqlite3 system.db <span class="s1">'DELETE FROM preload;'</span>
<span class="gp">$</span><span class="w"> </span>./app.self<span class="p">;</span> <span class="nb">echo</span> <span class="nv">$?</span>
<span class="go">13
</span></code></pre></div></div>

<p>We were able to accomplish an atomic <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code> across a whole userland
in one file, “interpose a tracing <code class="language-plaintext highlighter-rouge">malloc</code> everywhere, then <code class="language-plaintext highlighter-rouge">ROLLBACK</code>” is a
single transaction. 😈</p>

<h1 id="where-it-stands">Where it stands</h1>

<p>The format is done and round-trips between ELF and SELF losslessly. The tooling is done and can query, modify, and pack closures.  Lookup through SQL works on unmodified glibc programs perfectly and the native-SQL loader works enough to explore it as a possibility for ideas.</p>

<p>The whole thing is at <a href="https://github.com/fzakaria/selfdb">fzakaria/selfdb</a>.
<code class="language-plaintext highlighter-rouge">nix run .#self-vm</code> boots a NixOS VM where <code class="language-plaintext highlighter-rouge">hello</code> is a SQLite database. 🙌</p>

<p>Nix lets us explore radical ideas like this. We can rebuild the world down to the Linux kernel if needed. We need not be constrained by the existing decisions and constraints of the past. We can explore new ideas and see what falls out. I hope you find this idea as interesting as I do.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:sqlelf">
      <p>I wrote a paper, <a href="https://arxiv.org/abs/2405.03883">arXiv:2405.03883</a>,
that I failed to get published and a follow-up post on <a href="/2023/09/11/quick-insights-using-sqlelf">querying with it</a>. <a href="#fnref:sqlelf" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gnuhash">
      <p><code class="language-plaintext highlighter-rouge">.gnu.hash</code> is a bloom filter plus bucket chains, laid out so
<code class="language-plaintext highlighter-rouge">ld.so</code> can reject a miss without touching the chain during symbol discovery. <a href="#fnref:gnuhash" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:curl">
      <p>You might notice that <code class="language-plaintext highlighter-rouge">curl</code> (274 KiB, 27 libraries) starts slower than ELF <code class="language-plaintext highlighter-rouge">git</code>
(4.6 MiB, 5 libraries). That is <code class="language-plaintext highlighter-rouge">ld.so</code> doing work proportional to the
number of objects rather than the number of bytes, which I have
<a href="/2024/05/03/speeding-up-elf-relocations-for-store-based-systems">complained about before</a>. <a href="#fnref:curl" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:runpath">
      <p>I have written about <code class="language-plaintext highlighter-rouge">RUNPATH</code> on Nix before such as
<a href="/2022/09/12/making-runpath-redundant-for-nix">making it redundant</a> or
<a href="/2022/03/14/shrinkwrap-taming-dynamic-shared-objects">speeding it up</a>. <a href="#fnref:runpath" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been probably obsessed with two things in the last few years: Nix as a tool to explore innovative ideas that require the capability to rebuild the world and replacing ELF with SQLite as an executable format. You might have noticed that these two ideas are well suited to each other.]]></summary></entry><entry><title type="html">Three ways to smuggle SQLite into Nix</title><link href="https://fzakaria.com/2026/08/19/three-ways-to-smuggle-sqlite-into-nix" rel="alternate" type="text/html" title="Three ways to smuggle SQLite into Nix" /><published>2026-08-19T18:40:00-07:00</published><updated>2026-08-19T18:40:00-07:00</updated><id>https://fzakaria.com/2026/08/19/three-ways-to-smuggle-sqlite-into-nix</id><content type="html" xml:base="https://fzakaria.com/2026/08/19/three-ways-to-smuggle-sqlite-into-nix"><![CDATA[<p>The core of <a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a>, when you strip away the Nix API and the CLI, is an index. It is a map from <code class="language-plaintext highlighter-rouge">(attribute, version)</code> to the revision that shipped it as a JSON file.<sup id="fnref:index"><a href="#fn:index" class="footnote" rel="footnote" role="doc-noteref">1</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="nb">ls</span> <span class="nt">-lh</span> index/
<span class="go">-rw-r--r--. 1 fmzakari fmzakari 7.5M Aug 19 13:57 history.json
-rw-r--r--. 1 fmzakari fmzakari 5.3M Aug 19 13:57 versions.json
</span></code></pre></div></div>

<p>As of <a href="https://github.com/fzakaria/nixpkgs-multiverse/commit/9cc02098e177f784f822c57973ebfc3c02c21bed">9cc0209</a>, <code class="language-plaintext highlighter-rouge">versions.json</code> is 5.3 MiB and <code class="language-plaintext highlighter-rouge">history.json</code> is 7.5MiB covering 305,492 package versions across 31,904 packages and 1,534 revisions.</p>

<p>The Nix API loads the JSON files lazily and are all read via <code class="language-plaintext highlighter-rouge">builtins.fromJSON</code>:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">index</span> <span class="o">=</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">fromJSON</span> <span class="p">(</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">readFile</span> <span class="sx">./index/versions.json</span><span class="p">);</span>
</code></pre></div></div>

<p>I would like to enrich the data with even more information however it comes at a cost: mo’data, mo’problems.</p>

<p>The goal of the project is to minimize the number of Nixpkgs that are downloaded. If we merely swap fetching huge Nixpkgs for huge JSON, it’s not a clear win.</p>

<p>For now we have to be judicious about what we store in the JSON files and think of clever encoding schemes to make the data small and compact.</p>

<p>If we were not constrained to the Nix <code class="language-plaintext highlighter-rouge">builtins</code>, we would leverage established technologies to efficiently encode our dataset that allow multiple query access patterns: databases!</p>

<p>Let’s say we were not restricted to JSON, do we have any other options?</p>

<h2 id="one-lookup-costs-the-whole-file">One lookup costs the whole file</h2>

<p>Why are large JSON files so problematic? <code class="language-plaintext highlighter-rouge">builtins.fromJSON</code> is <em>eager</em>. There is no lazy JSON in Nix, no streaming parse (i.e. “just give me this one key”). The moment you touch the result you have parsed all 5.3 MB and materialised all 305,492 values on the Nix heap.</p>

<p>In the case of the multiverse, asking for one package costs the same as what asking for all of them.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
The lookup itself is not the problem. Nix attribute sets are a sorted array,
so access is a binary search, not a scan.
The cost is entirely in the JSON parse and in allocating the values and downloading a large file.</p>
</blockquote>

<p>If we want to do alternate questions over the index, we have to make sure we keep the answers efficiently stored to better match
the access pattern.</p>

<p>What we want is obvious. We want a way to efficiently encode the data and a declarative way to define queries: we want SQLite!<sup id="fnref:sqlite"><a href="#fn:sqlite" 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>sqlite3 index.db <span class="s2">"SELECT version, rev FROM versions WHERE attr='hello'"</span>
<span class="go">2.10|728
</span><span class="c">...
</span><span class="go">0.01s, 4 MB
</span></code></pre></div></div>

<p>Nix <em>by default</em> cannot do this. Unfortunately there is no <code class="language-plaintext highlighter-rouge">builtins.sqlite</code>, although I think there should be…</p>

<p>Turns out though there are knobs we can touch or sources we can patch to get what we want anyways, albeit each one has a caveat. 😈</p>

<h2 id="one-builtinsexec">One: <code class="language-plaintext highlighter-rouge">builtins.exec</code></h2>

<p>I was surprised I did not know about this <code class="language-plaintext highlighter-rouge">builtin</code>, and it has been around since <a href="https://github.com/NixOS/nix/issues/1300">release 1.11.9</a> in April 2017. It is the ultimate escape hatch for a variety of use-cases when you simply can’t get them done with what’s available.</p>

<p><code class="language-plaintext highlighter-rouge">builtins.exec</code> takes a list of strings, runs the program, and <strong>parses its stdout as a Nix expression</strong>.</p>

<p>It is gated behind a setting that makes it clear it’s unsafe.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--option</span> allow-unsafe-native-code-during-evaluation <span class="nb">true</span> <span class="se">\</span>
<span class="go">    --expr 'builtins.exec [ "/bin/sh" "-c" "echo 42" ]'
42
</span></code></pre></div></div>

<p>For integration, SQLite is perfectly capable of printing the Nix syntax. We never need a serialisation format in between as we make SQLite emit the attrset directly:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span>
  <span class="nv">versionsOf</span> <span class="o">=</span> <span class="nv">attr</span><span class="p">:</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">exec</span> <span class="p">[</span>
    <span class="s2">"</span><span class="si">${</span><span class="nv">sqlite</span><span class="si">}</span><span class="s2">/bin/sqlite3"</span> <span class="s2">"-noheader"</span> <span class="s2">"-separator"</span> <span class="s2">""</span> <span class="s2">"./index.db"</span>
    <span class="s2">''</span><span class="err">
</span><span class="s2">      SELECT '{' || group_concat(</span><span class="err">
</span><span class="s2">               '"' || version || '" = ' ||</span><span class="err">
</span><span class="s2">               COALESCE(CAST(rev AS TEXT), 'null') || ';', ' ')</span><span class="err">
</span><span class="s2">           || '}'</span><span class="err">
</span><span class="s2">      FROM versions WHERE attr = '</span><span class="si">${</span><span class="nv">attr</span><span class="si">}</span><span class="s2">';</span><span class="err">
</span><span class="s2">    ''</span>
  <span class="p">];</span>
<span class="kn">in</span>
  <span class="nv">versionsOf</span> <span class="s2">"hello"</span>
</code></pre></div></div>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--impure</span> <span class="nt">-f</span> query.nix <span class="se">\</span>
<span class="go">           --option allow-unsafe-native-code-during-evaluation true
{
</span><span class="gp">  "2.10" = 728;</span><span class="w"> </span><span class="s2">"2.12"</span> <span class="o">=</span> 822<span class="p">;</span> <span class="s2">"2.12.1"</span> <span class="o">=</span> 1369<span class="p">;</span>
<span class="gp">  "2.12.2" = 1486;</span><span class="w"> </span><span class="s2">"2.12.3"</span> <span class="o">=</span> null<span class="p">;</span> <span class="s2">"2.7"</span> <span class="o">=</span> 0<span class="p">;</span> <span class="s2">"2.8"</span> <span class="o">=</span> 13<span class="p">;</span>
<span class="go">}
</span></code></pre></div></div>

<p>The caveat is that every query is now a <code class="language-plaintext highlighter-rouge">fork</code>, an <code class="language-plaintext highlighter-rouge">exec</code>, a process image of SQLite, and a re-parse of the output through the Nix parser.
If you do not plan to execute many queries that overhead is likely acceptable given the simplicity of the integration.</p>

<h2 id="two-builtinsimportnative">Two: <code class="language-plaintext highlighter-rouge">builtins.importNative</code></h2>

<p>From researching <code class="language-plaintext highlighter-rouge">builtins.exec</code>, I stumbled upon <code class="language-plaintext highlighter-rouge">builtins.importNative</code>. It takes a path to a shared object and a symbol name, <code class="language-plaintext highlighter-rouge">dlopen</code>s it, and calls that symbol. It landed in <strong>1.8</strong>, December 2014.<sup id="fnref:import_native"><a href="#fn:import_native" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>The shared object must implement the following signature:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="s">"C"</span> <span class="k">typedef</span> <span class="nf">void</span> <span class="p">(</span><span class="o">*</span><span class="n">ValueInitializer</span><span class="p">)(</span><span class="n">EvalState</span> <span class="o">&amp;</span> <span class="n">state</span><span class="p">,</span> <span class="n">Value</span> <span class="o">&amp;</span> <span class="n">v</span><span class="p">);</span>
</code></pre></div></div>
<p>We can define a new <em>native</em> function that returns the versions for our input:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="s">"C"</span> <span class="kt">void</span> <span class="nf">nix_sqlite_versions</span><span class="p">(</span><span class="n">EvalState</span> <span class="o">&amp;</span> <span class="n">state</span><span class="p">,</span> <span class="n">Value</span> <span class="o">&amp;</span> <span class="n">v</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">v</span><span class="p">.</span><span class="n">mkPrimOp</span><span class="p">(</span><span class="k">new</span> <span class="n">PrimOp</span><span class="p">{</span>
        <span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s">"nix_sqlite_versions"</span><span class="p">,</span>
        <span class="p">.</span><span class="n">args</span> <span class="o">=</span> <span class="p">{</span><span class="s">"dbPath"</span><span class="p">,</span> <span class="s">"attr"</span><span class="p">},</span>
        <span class="p">.</span><span class="n">arity</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
        <span class="p">.</span><span class="n">impl</span> <span class="o">=</span> <span class="n">versions</span><span class="p">,</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The implementation is ordinary C++ using the Nix API. Below is a snippet
of the implementation, making sure to cache our <code class="language-plaintext highlighter-rouge">sqlite3</code> handles to avoid
the same startup penalty as <code class="language-plaintext highlighter-rouge">builtins.exec</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* The whole point: the database handle outlives a single query, so the
   b-tree pages we touch stay warm for the rest of the evaluation. */</span>
<span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">sqlite3</span> <span class="o">*&gt;</span> <span class="n">handles</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">versions</span><span class="p">(</span><span class="n">EvalState</span> <span class="o">&amp;</span> <span class="n">state</span><span class="p">,</span> <span class="k">const</span> <span class="n">PosIdx</span> <span class="n">pos</span><span class="p">,</span>
              <span class="n">Value</span> <span class="o">**</span> <span class="n">args</span><span class="p">,</span> <span class="n">Value</span> <span class="o">&amp;</span> <span class="n">v</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">path</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">forceStringNoCtx</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">pos</span><span class="p">,</span> <span class="s">"..."</span><span class="p">));</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">attr</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">forceStringNoCtx</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">pos</span><span class="p">,</span> <span class="s">"..."</span><span class="p">));</span>

    <span class="c1">// cached across calls</span>
    <span class="k">auto</span> <span class="o">*</span> <span class="n">db</span> <span class="o">=</span> <span class="n">openOnce</span><span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">pos</span><span class="p">,</span> <span class="n">path</span><span class="p">);</span>

    <span class="n">sqlite3_stmt</span> <span class="o">*</span> <span class="n">stmt</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
    <span class="n">sqlite3_prepare_v2</span><span class="p">(</span><span class="n">db</span><span class="p">,</span>
                       <span class="s">"SELECT version, rev "</span>
                       <span class="s">"FROM versions "</span>
                       <span class="s">"WHERE attr = ?1"</span><span class="p">,</span>
                       <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">stmt</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">);</span>
    <span class="n">sqlite3_bind_text</span><span class="p">(</span><span class="n">stmt</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">attr</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span>
                      <span class="n">attr</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="n">SQLITE_TRANSIENT</span><span class="p">);</span>

    <span class="cm">/* ... collect rows ... */</span>

    <span class="cm">/* Build the attrset directly. No text ever exists. */</span>
    <span class="k">auto</span> <span class="n">bindings</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">buildBindings</span><span class="p">(</span><span class="n">rows</span><span class="p">.</span><span class="n">size</span><span class="p">());</span>
    <span class="k">for</span> <span class="p">(</span><span class="k">auto</span> <span class="o">&amp;</span> <span class="p">[</span><span class="n">version</span><span class="p">,</span> <span class="n">rev</span><span class="p">]</span> <span class="o">:</span> <span class="n">rows</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">auto</span> <span class="o">&amp;</span> <span class="n">slot</span> <span class="o">=</span> <span class="n">bindings</span><span class="p">.</span><span class="n">alloc</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">symbols</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="n">version</span><span class="p">));</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">rev</span><span class="p">)</span> <span class="n">slot</span><span class="p">.</span><span class="n">mkInt</span><span class="p">(</span><span class="o">*</span><span class="n">rev</span><span class="p">);</span> <span class="k">else</span> <span class="n">slot</span><span class="p">.</span><span class="n">mkNull</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="n">v</span><span class="p">.</span><span class="n">mkAttrs</span><span class="p">(</span><span class="n">bindings</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Using it looks like this:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--impure</span> <span class="se">\</span>
<span class="go">    --option allow-unsafe-native-code-during-evaluation true \
    --expr '(builtins.importNative
                  ./libnixsqlite.so "nix_sqlite_versions"
            ) "./index.db" "hello"'
{
</span><span class="gp">  "2.10" = 728;</span><span class="w"> </span><span class="s2">"2.12"</span> <span class="o">=</span> 822<span class="p">;</span> <span class="s2">"2.12.1"</span> <span class="o">=</span> 1369<span class="p">;</span>
<span class="gp">  "2.12.2" = 1486;</span><span class="w"> </span><span class="s2">"2.12.3"</span> <span class="o">=</span> null<span class="p">;</span> <span class="s2">"2.7"</span> <span class="o">=</span> 0<span class="p">;</span> <span class="s2">"2.8"</span> <span class="o">=</span> 13<span class="p">;</span>
<span class="go">}
</span></code></pre></div></div>

<h2 id="three-a-giant-nix-file">Three: a giant Nix file</h2>

<p><em>This section was added after publishing based on an idea from <a href="https://github.com/rickynils">rickynils</a>.</em></p>

<p>Nix is often described as resembling JSON and there is a very easy translation from JSON to Nix.
What if instead of reading JSON we read the same contents but as a <code class="language-plaintext highlighter-rouge">.nix</code> file?</p>

<p>Theoretically it should have no parser boundary, no <code class="language-plaintext highlighter-rouge">fromJSON</code>, and no serialisation format at all.
The index becomes an expression the evaluator already knows how to read.</p>

<p>The idea would be to leverage Nix’s laziness. Nix attribute set values are thunks, so in principle you should be able
to <code class="language-plaintext highlighter-rouge">import</code> a very large expression, touch one attribute, and never pay for instantiating the rest.</p>

<p>Transforming the index is a dozen lines of Python, and produces something very similar to the JSON:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">revisionCount</span> <span class="o">=</span> <span class="mi">1534</span><span class="p">;</span>
  <span class="nv">attrs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"2048-in-terminal"</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"2015-01-15"</span> <span class="o">=</span> <span class="mi">157</span><span class="p">;</span> <span class="s2">"2017-11-29"</span> <span class="o">=</span> <span class="mi">166</span><span class="p">;</span> <span class="p">};</span>
    <span class="s2">"2bwm"</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"0.2"</span> <span class="o">=</span> <span class="mi">166</span><span class="p">;</span> <span class="p">};</span>
    <span class="s2">"389-ds-base"</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"1.3.3.9"</span> <span class="o">=</span> <span class="mi">14</span><span class="p">;</span> <span class="s2">"1.3.5.15"</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span> <span class="s2">"1.3.5.19"</span> <span class="o">=</span> <span class="mi">166</span><span class="p">;</span> <span class="p">};</span>
    <span class="c"># ... 31,901 more</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>6.0 MiB of Nix, against 5.3 MiB of JSON, holding identical data.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--impure</span> <span class="nt">--expr</span> <span class="s1">'(import ./index.nix).attrs.hello'</span>
<span class="go">{
</span><span class="gp">  "2.10" = 728;</span><span class="w"> </span><span class="s2">"2.12"</span> <span class="o">=</span> 822<span class="p">;</span> <span class="s2">"2.12.1"</span> <span class="o">=</span> 1369<span class="p">;</span>
<span class="gp">  "2.12.2" = 1486;</span><span class="w"> </span><span class="s2">"2.12.3"</span> <span class="o">=</span> null<span class="p">;</span> <span class="s2">"2.7"</span> <span class="o">=</span> 0<span class="p">;</span> <span class="s2">"2.8"</span> <span class="o">=</span> 13<span class="p">;</span>
<span class="go">}
</span></code></pre></div></div>

<h2 id="four-builtinswasm">Four: <code class="language-plaintext highlighter-rouge">builtins.wasm</code></h2>

<p>Determinate Systems <a href="https://determinate.systems/blog/builtins-wasm/">shipped another option</a> in March of 2026: <code class="language-plaintext highlighter-rouge">builtins.wasm</code>, which calls a function inside a WebAssembly module.<sup id="fnref:eelco"><a href="#fn:eelco" class="footnote" rel="footnote" role="doc-noteref">4</a></sup> The motivation was similar to wanting to extend Nix surface area but avoid expanding <code class="language-plaintext highlighter-rouge">builtins</code>. Wasm is sandboxed and deterministic, so unlike the two builtins above, the goal is to provide a <em>safe escape-hatch</em>.</p>

<p>WebAssembly is a binary instruction format for a stack-based virtual machine. The claim is that it is well suited for Nix because it has <em>deterministic execution</em>, which is a lot more restrained than a backdoor <code class="language-plaintext highlighter-rouge">builtins.exec</code>.</p>

<h3 id="writing-a-module">Writing a module</h3>

<p>A module needs to export <code class="language-plaintext highlighter-rouge">memory</code>, an initialiser called <code class="language-plaintext highlighter-rouge">nix_wasm_init_v1</code>, and the entry point.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#![no_std]</span>
<span class="nd">#![no_main]</span>
<span class="k">type</span> <span class="n">ValueId</span> <span class="o">=</span> <span class="nb">u32</span><span class="p">;</span>

<span class="nd">#[panic_handler]</span>
<span class="k">fn</span> <span class="nf">panic</span><span class="p">(</span><span class="n">_</span><span class="p">:</span> <span class="o">&amp;</span><span class="nn">core</span><span class="p">::</span><span class="nn">panic</span><span class="p">::</span><span class="n">PanicInfo</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">!</span> <span class="p">{</span>
    <span class="nn">core</span><span class="p">::</span><span class="nn">arch</span><span class="p">::</span><span class="nn">wasm32</span><span class="p">::</span><span class="nf">unreachable</span><span class="p">()</span>
<span class="p">}</span>

<span class="c1">// Host functions supplied by the Nix evaluator.</span>
<span class="nd">#[link(wasm_import_module</span> <span class="nd">=</span> <span class="s">"env"</span><span class="nd">)]</span>
<span class="k">unsafe</span> <span class="k">extern</span> <span class="s">"C"</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">get_int</span><span class="p">(</span><span class="n">v</span><span class="p">:</span> <span class="n">ValueId</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i64</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">make_int</span><span class="p">(</span><span class="n">n</span><span class="p">:</span> <span class="nb">i64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">ValueId</span><span class="p">;</span>
<span class="p">}</span>

<span class="nd">#[unsafe(no_mangle)]</span>
<span class="k">pub</span> <span class="k">extern</span> <span class="s">"C"</span> <span class="k">fn</span> <span class="nf">nix_wasm_init_v1</span><span class="p">()</span> <span class="p">{}</span>

<span class="k">fn</span> <span class="nf">fib</span><span class="p">(</span><span class="n">n</span><span class="p">:</span> <span class="nb">i64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i64</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&lt;=</span> <span class="mi">1</span> <span class="p">{</span> <span class="mi">1</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nf">fib</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="nf">fib</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">2</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>

<span class="nd">#[unsafe(no_mangle)]</span>
<span class="k">pub</span> <span class="k">extern</span> <span class="s">"C"</span> <span class="k">fn</span> <span class="nf">fib_entry</span><span class="p">(</span><span class="n">arg</span><span class="p">:</span> <span class="n">ValueId</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">ValueId</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">make_int</span><span class="p">(</span><span class="nf">fib</span><span class="p">(</span><span class="nf">get_int</span><span class="p">(</span><span class="n">arg</span><span class="p">)))</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nixpkgs already includes the target for cross-compilation, so making one is pretty
straightforward:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">pkgs</span><span class="o">.</span><span class="nv">runCommand</span> <span class="s2">"nix-wasm-rust-fib"</span>
<span class="p">{</span>
  <span class="nv">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">rustc</span> <span class="nv">pkgs</span><span class="o">.</span><span class="nv">lld</span> <span class="p">];</span>
  <span class="nv">src</span> <span class="o">=</span> <span class="sx">./modules.rs</span><span class="p">;</span>
<span class="p">}</span> <span class="s2">''</span><span class="err">
</span><span class="s2">  mkdir -p $out</span><span class="err">
</span><span class="s2">  rustc --target wasm32-unknown-unknown --crate-type cdylib -O \</span><span class="err">
</span><span class="s2">    -o $out/modules.wasm $src</span><span class="err">
</span><span class="s2">''</span>
</code></pre></div></div>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--extra-experimental-features</span> wasm-builtin <span class="se">\</span>
<span class="gp">      --expr 'builtins.wasm { path = ./modules.wasm;</span><span class="w">
</span><span class="gp">                              function = "fib_entry";</span><span class="w"> </span><span class="o">}</span> 30<span class="s1">'
</span><span class="go">1346269

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

<p>You call back into the evaluator through the Nix API functions, so a wasm module builds real Nix values, similar to <code class="language-plaintext highlighter-rouge">builtins.importNative</code> minus the footgun.</p>

<h2 id="can-i-haz-sqlite">Can I haz SQLite?</h2>

<p><a href="https://sqlite.org/wasm/doc/trunk/index.md">SQLite ships an official wasm build</a>, so the pieces seem to be sitting right there and the gears
in my mind began to turn.</p>

<p><img src="/assets/images/haz_cheeseburger.png" alt="photo of a cat asking if he can have sqlite as a meme" /></p>

<p>Initial attempts to try and load a SQLite database with the traditional Nix <code class="language-plaintext highlighter-rouge">builtins</code> were a bit of a failure as Nix strings cannot contain NULL bytes.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--impure</span> <span class="nt">--expr</span> <span class="s1">'builtins.stringLength (builtins.readFile ./index.db)'</span>
<span class="go">error: the contents of the file '/tmp/mvsql/index.db' cannot be represented as a Nix string
</span></code></pre></div></div>

<p>Thankfully, with the help of some additional due-diligence by LLMs, we discovered
that one of the Nix API functions is not in the blog post:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Read the contents of a file into Wasm memory. This is like calling
 * `builtins.readFile`, except that it can handle binary files that
 * cannot be represented as Nix strings.
 */</span>
<span class="kt">uint32_t</span> <span class="n">read_file</span><span class="p">(</span><span class="n">ValueId</span> <span class="n">pathId</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">ptr</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">len</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">read_file</code> is <em>specifically</em> designed for this problem. This function allows a WASM module to pull arbitrary raw-bytes off disk into its memory.</p>

<p>Unfortunately, it’s a little <em>too broad</em> in that it reads <strong>the complete file</strong> which is kind of overkill and what we are trying to avoid from our
initial JSON solution.</p>

<p>In the pursuit of exploration, let’s <em>patch</em> the implementation and augment the API to allow random access and partial read of a file.
Turns out the patch to add is relatively small and straightforward.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Read a range of a file into Wasm memory, starting at `offset`
 * and copying at most `len` bytes.
 * Returns the number of bytes actually copied.
 */</span>
<span class="kt">uint32_t</span> <span class="nf">read_file_range</span><span class="p">(</span><span class="n">ValueId</span> <span class="n">pathId</span><span class="p">,</span> <span class="kt">uint64_t</span> <span class="n">offset</span><span class="p">,</span>
                         <span class="kt">uint32_t</span> <span class="n">ptr</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">len</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">auto</span> <span class="o">&amp;</span> <span class="n">pathValue</span> <span class="o">=</span> <span class="n">getValue</span><span class="p">(</span><span class="n">pathId</span><span class="p">);</span>
    <span class="k">auto</span> <span class="n">path</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="n">realisePath</span><span class="p">(</span><span class="n">noPos</span><span class="p">,</span> <span class="n">pathValue</span><span class="p">);</span>

    <span class="k">auto</span> <span class="n">buf</span> <span class="o">=</span> <span class="n">memory</span><span class="p">().</span><span class="n">subspan</span><span class="p">(</span><span class="n">ptr</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>

    <span class="cm">/* If this is a real file on disk, do a positional read*/</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">auto</span> <span class="n">physical</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="n">getPhysicalPath</span><span class="p">())</span> <span class="p">{</span>
        <span class="n">AutoCloseFD</span> <span class="n">fd</span><span class="p">{</span><span class="n">open</span><span class="p">(</span><span class="n">physical</span><span class="o">-&gt;</span><span class="n">string</span><span class="p">().</span><span class="n">c_str</span><span class="p">(),</span>
                            <span class="n">O_RDONLY</span> <span class="o">|</span> <span class="n">O_CLOEXEC</span><span class="p">)};</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">fd</span><span class="p">)</span>
            <span class="k">throw</span> <span class="nf">SysError</span><span class="p">(</span><span class="s">"opening file '%s'"</span><span class="p">,</span> <span class="n">physical</span><span class="o">-&gt;</span><span class="n">string</span><span class="p">());</span>
        <span class="k">auto</span> <span class="n">n</span> <span class="o">=</span> <span class="n">pread</span><span class="p">(</span><span class="n">fd</span><span class="p">.</span><span class="n">get</span><span class="p">(),</span> <span class="n">buf</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">len</span><span class="p">,</span> <span class="n">offset</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span>
            <span class="k">throw</span> <span class="nf">SysError</span><span class="p">(</span><span class="s">"reading file '%s'"</span><span class="p">,</span> <span class="n">physical</span><span class="o">-&gt;</span><span class="n">string</span><span class="p">());</span>
        <span class="k">return</span> <span class="n">n</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="cm">/* Otherwise fall back to materialising the whole file. */</span>
    <span class="k">auto</span> <span class="n">contents</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="n">readFile</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">offset</span> <span class="o">&gt;=</span> <span class="n">contents</span><span class="p">.</span><span class="n">size</span><span class="p">())</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">auto</span> <span class="n">n</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="o">&lt;</span><span class="kt">size_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">len</span><span class="p">,</span> <span class="n">contents</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">-</span> <span class="n">offset</span><span class="p">);</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">buf</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">contents</span><span class="p">.</span><span class="n">data</span><span class="p">()</span> <span class="o">+</span> <span class="n">offset</span><span class="p">,</span> <span class="n">n</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we have everything we need to hook up SQLite and a custom virtual filesystem (VFS) layer to read from the provided <code class="language-plaintext highlighter-rouge">/nix/store</code> path entry.</p>

<p>We build a WASM target of SQLite and we set <code class="language-plaintext highlighter-rouge">SQLITE_OS_OTHER=1</code>. That flag removes SQLite’s entire VFS layer and requires us to supply one.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">pkgs</span><span class="o">.</span><span class="nv">pkgsCross</span><span class="o">.</span><span class="nv">wasi32</span><span class="o">.</span><span class="nv">stdenv</span><span class="o">.</span><span class="nv">mkDerivation</span> <span class="p">{</span>
  <span class="nv">pname</span> <span class="o">=</span> <span class="s2">"sqlite-nix-wasm"</span><span class="p">;</span>
  <span class="nv">buildPhase</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    $CC -O2 -o sqlite_nix.wasm \</span><span class="err">
</span><span class="s2">      -I</span><span class="si">${</span><span class="nv">amalgamation</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">amalgamation</span><span class="si">}</span><span class="s2">/sqlite3.c sqlite_nix.c \</span><span class="err">
</span><span class="s2">      -DSQLITE_OS_OTHER=1 \</span><span class="err">
</span><span class="s2">      -DSQLITE_THREADSAFE=0 \</span><span class="err">
</span><span class="s2">      -DSQLITE_OMIT_LOAD_EXTENSION \</span><span class="err">
</span><span class="s2">      -DSQLITE_OMIT_WAL \</span><span class="err">
</span><span class="s2">      -Wl,--export-memory</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We provide the build a simple implementation of the <code class="language-plaintext highlighter-rouge">xRead</code> API which is a call-back into the Nix evaluator via
that newly exposed <code class="language-plaintext highlighter-rouge">nix_read_file_range</code> function. Everything else is stubs.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="n">sqlite3_io_methods</span> <span class="n">nixIoMethods</span> <span class="o">=</span> <span class="p">{</span>
  <span class="p">.</span><span class="n">iVersion</span>               <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
  <span class="p">.</span><span class="n">xClose</span>                 <span class="o">=</span> <span class="n">nixClose</span><span class="p">,</span>
  <span class="p">.</span><span class="n">xRead</span>                  <span class="o">=</span> <span class="n">nixRead</span><span class="p">,</span>
  <span class="p">.</span><span class="n">xFileSize</span>              <span class="o">=</span> <span class="n">nixFileSize</span><span class="p">,</span>
  <span class="p">.</span><span class="n">xDeviceCharacteristics</span> <span class="o">=</span> <span class="n">nixDeviceCharacteristics</span><span class="p">,</span>
  <span class="cm">/* ... the rest are stubs ... */</span>
<span class="p">};</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">nixRead</span><span class="p">(</span><span class="n">sqlite3_file</span> <span class="o">*</span><span class="n">f</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span>
                   <span class="kt">int</span> <span class="n">amt</span><span class="p">,</span> <span class="n">sqlite3_int64</span> <span class="n">off</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">NixFile</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span> <span class="p">(</span><span class="n">NixFile</span> <span class="o">*</span><span class="p">)</span> <span class="n">f</span><span class="p">;</span>
  <span class="cm">/* The one line that matters: SQLite's pager asks
     for a page, and we ask the Nix evaluator for
     exactly those bytes. */</span>
  <span class="kt">unsigned</span> <span class="n">got</span> <span class="o">=</span> <span class="n">nix_read_file_range</span><span class="p">(</span><span class="n">p</span><span class="o">-&gt;</span><span class="n">pathId</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span> <span class="n">off</span><span class="p">,</span>
                                     <span class="n">buf</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span><span class="p">)</span> <span class="n">amt</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">got</span> <span class="o">&lt;</span> <span class="p">(</span><span class="kt">unsigned</span><span class="p">)</span> <span class="n">amt</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">memset</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span> <span class="n">buf</span> <span class="o">+</span> <span class="n">got</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span><span class="p">)</span> <span class="n">amt</span> <span class="o">-</span> <span class="n">got</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">SQLITE_IOERR_SHORT_READ</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">SQLITE_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
Unfortunately <code class="language-plaintext highlighter-rouge">builtins.wasm</code> gives every call a <strong>fresh instance</strong>. This is deliberate from the implementation, meaning we pay some startup code each time although not quite as drastic as a <code class="language-plaintext highlighter-rouge">fork</code> &amp; <code class="language-plaintext highlighter-rouge">exec</code></p>
</blockquote>

<p>The <code class="language-plaintext highlighter-rouge">sqlite_nix</code> WASM module takes an attrset of <code class="language-plaintext highlighter-rouge">{ db, sql }</code> and returns one attrset per row.<sup id="fnref:gist"><a href="#fn:gist" class="footnote" rel="footnote" role="doc-noteref">5</a></sup></p>

<p>We can provide it any arbitrary SQL and now query our dataset!</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># query.nix</span>
<span class="kr">builtins</span><span class="o">.</span><span class="nv">wasm</span> <span class="p">{</span> <span class="nv">path</span> <span class="o">=</span> <span class="sx">./sqlite_nix.wasm</span><span class="p">;</span> <span class="p">}</span> <span class="p">{</span>
  <span class="nv">db</span>  <span class="o">=</span> <span class="sx">./index.db</span><span class="p">;</span>
  <span class="nv">sql</span> <span class="o">=</span> <span class="s2">"SELECT version, rev FROM versions</span><span class="err">
</span><span class="s2">         WHERE attr = 'hello' ORDER BY version"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--extra-experimental-features</span> wasm-builtin <span class="nt">-f</span> query.nix
<span class="gp">[ { rev = 728;</span><span class="w"> </span>version <span class="o">=</span> <span class="s2">"2.10"</span><span class="p">;</span> <span class="o">}</span> <span class="o">{</span> rev <span class="o">=</span> 822<span class="p">;</span> version <span class="o">=</span> <span class="s2">"2.12"</span><span class="p">;</span> <span class="o">}</span>
<span class="gp">  { rev = 1369;</span><span class="w"> </span>version <span class="o">=</span> <span class="s2">"2.12.1"</span><span class="p">;</span> <span class="o">}</span> <span class="o">{</span> rev <span class="o">=</span> 1486<span class="p">;</span> version <span class="o">=</span> <span class="s2">"2.12.2"</span><span class="p">;</span> <span class="o">}</span>
<span class="gp">  { rev = null;</span><span class="w"> </span>version <span class="o">=</span> <span class="s2">"2.12.3"</span><span class="p">;</span> <span class="o">}</span> <span class="o">{</span> rev <span class="o">=</span> 0<span class="p">;</span> version <span class="o">=</span> <span class="s2">"2.7"</span><span class="p">;</span> <span class="o">}</span>
<span class="gp">  { rev = 13;</span><span class="w"> </span>version <span class="o">=</span> <span class="s2">"2.8"</span><span class="p">;</span> <span class="o">}</span> <span class="o">]</span>
</code></pre></div></div>

<p>The benefit of SQL is that now we are not limited to the shape of the data in JSON.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># which packages have shipped the most versions?</span>
<span class="nv">sql</span> <span class="o">=</span> <span class="s2">"SELECT attr, COUNT(*) AS versions FROM versions</span><span class="err">
</span><span class="s2">       GROUP BY attr ORDER BY versions DESC LIMIT 3"</span><span class="p">;</span>
<span class="c"># =&gt; [ { attr = "linux"; versions = 548; }</span>
<span class="c">#      { attr = "linux_latest"; versions = 540; }</span>
<span class="c">#      { attr = "freefall"; versions = 534; } ]</span>
</code></pre></div></div>

<p><strong>That is a real full SQLite</strong> with all the bells and whistles: query planner, aggregates and subqueries, b-tree descent through an index, executing inside the Nix evaluator.
All through WebAssembly. 🤯</p>

<p>Every one of those answers is byte-identical to what the <code class="language-plaintext highlighter-rouge">sqlite3</code> CLI gives for the same query.</p>

<h2 id="benchmark">Benchmark</h2>

<p>How do these compare? Here is every approach answering the same question: “which revisions shipped this package?” either against the same 22 MB SQLite build of the index or the
whole-file JSON/Nix equivalent.</p>

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

# Best of five runs each. Random attributes drawn from the real index.
# `nix eval --expr '1+1'` costs 0.03s, the floor every line sits on.
# The first four run on stock Nix 2.34.7; the wasm line needs the patched
# Determinate Nix, whose baseline is the same 0.03s.
rows = [
    ("builtins.fromJSON",     [0.29, 0.30, 0.27, 0.29]),
    ("giant .nix file",       [0.51, 0.57, 0.50, 0.51]),
    ("builtins.exec",         [0.05, 0.08, 0.22, 0.74]),
    ("builtins.importNative", [0.05, 0.04, 0.05, 0.05]),
    ("SQLite in wasm",        [2.79, 2.46, 3.10, 4.37]),
]
queries = [1, 10, 50, 200]
df = pd.DataFrame({
    "queries": queries * len(rows),
    "seconds": [v for _, vs in rows for v in vs],
    "how":     [k for k, vs in rows for _ in vs],
})

plot = (
    ggplot(df, aes("queries", "seconds", color="how"))
    + geom_line(size=1.0)
    + geom_point(size=1.8)
    + scale_x_log10(breaks=queries, labels=[str(q) for q in queries])
    + scale_y_log10()
    + scale_color_manual(values={"builtins.fromJSON":     "#8a8580",
                                 "giant .nix file":       "#3f7f5f",
                                 "builtins.exec":         "#4c72b0",
                                 "builtins.importNative": "#b1201d",
                                 "SQLite in wasm":        "#d1892f"},
                         name="")
    + labs(x="point queries in one evaluation", y="seconds (log scale)")
    + theme(legend_position="top", legend_title=element_blank())
)
plot.width, plot.height = 7.0, 3.8
</code></pre>

<p>As we initially complained, <strong><code class="language-plaintext highlighter-rouge">fromJSON</code> is a flat line in the wrong place.</strong> It is 0.29s whether you ask one question or two hundred, because the 5.3 MB parse happens once and dominates everything after it.</p>

<p><strong>The giant <code class="language-plaintext highlighter-rouge">.nix</code> file is the same flat line, drawn higher.</strong> It is roughly twice the time and 1.7× the memory of the JSON it replaced. Surprisngly, laziness never gets a chance to help: importing the file and touching <em>nothing at all</em> already costs 0.53s. The baseline cost is the parse, and the parse is eager as we well. Turns out parsing Nix expressions is even more expensive than JSON. Nix has run the file through its Bison grammar, build an AST for all 305,492 entries, and add every attribute name into the symbol table. <code class="language-plaintext highlighter-rouge">fromJSON</code> skips the AST entirely and goes straight from bytes to values, which is why the format with a “serialisation boundary” beats the one without.</p>

<p><strong><code class="language-plaintext highlighter-rouge">builtins.exec</code> starts the cheapest and climbs</strong>, roughly 3.8 ms per query of <code class="language-plaintext highlighter-rouge">fork</code> + <code class="language-plaintext highlighter-rouge">exec</code> + Nix-parsing the output. It crosses <code class="language-plaintext highlighter-rouge">fromJSON</code> somewhere around eighty queries.</p>

<p><strong><code class="language-plaintext highlighter-rouge">builtins.importNative</code> is flat and nearly free</strong>, 0.05s across the whole range since we <em>reuse SQLite instantiations</em> across multiple invocations. The database is opened once for the entire evaluation and the pages stay warm.</p>

<p>Unfortunately, <strong>SQLite in wasm is dominated by a fixed cost</strong>, roughly <strong>2.5 s</strong> before the first query, then about <strong>7 ms</strong> each query thereafter.
That 2.5 s is Cranelift compiling 1.1 MB of SQLite. Right now that is a limitation of the WASM implementation however Eelco has mentioned that the generated code could be cached on disk in the future across invocations.</p>

<p>For a lock file pinning thirty packages, <code class="language-plaintext highlighter-rouge">fromJSON</code> still wins outright at the current index size.</p>

<h2 id="what-i-actually-want">What I actually want</h2>

<p>None of these is right for shipping the multiverse index, and I am not going to make <code class="language-plaintext highlighter-rouge">nixpkgs-multiverse</code> depend on <code class="language-plaintext highlighter-rouge">allow-unsafe-native-code-during-evaluation</code>. Asking people to run their evaluator with native code loading enabled so my flake can be faster is not a worthwhile request <em>at the moment</em>.</p>

<p>For now, the index stays JSON and I’m holding back on some of the more loftier ideas I have that require <em>a lot more data</em>.</p>

<p>Although philosophically I only use <a href="https://github.com/nixos/nix">CppNix</a>, I was a little intrigued and impressed with what the ecosystem could unlock with WASM. There are definitely some warts however such as waiting for it to JIT and the developer-experience of maybe having checked-in compiled blobs but there is definitely potential to unlock a variety of problems.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:index">
      <p>There are actually a few other files that drive other features such as the statistics or <a href="/2026/08/14/nixpkgs-multiverse-fast-mode">“fast mode”</a>, but they are all JSON as well. <a href="#fnref:index" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sqlite">
      <p><a href="https://github.com/fzakaria/nixpkgs-multiverse">nixpkgs-multiverse</a> already exports a SQLite database as a package to help others explore this data. <a href="#fnref:sqlite" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:import_native">
      <p>The C++ field was originally called <code class="language-plaintext highlighter-rouge">enableImportNative</code> and was renamed to <code class="language-plaintext highlighter-rouge">enableNativeCode</code> for <code class="language-plaintext highlighter-rouge">exec</code>. <a href="#fnref:import_native" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:eelco">
      <p>Eelco gave a talk about this at <a href="https://www.socallinuxexpo.org/scale/23x/presentations/builtinswasm-nix-meets-webassembly">SCALE 23x</a>. <a href="#fnref:eelco" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:gist">
      <p>The full <code class="language-plaintext highlighter-rouge">sqlite_nix.c</code>, the VFS, the build derivation and the Nix patch are all <a href="https://gist.github.com/fzakaria/8fe754ee7db752a24cb9c55b38492844">in this gist</a>. <a href="#fnref:gist" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[The core of nixpkgs-multiverse, when you strip away the Nix API and the CLI, is an index. It is a map from (attribute, version) to the revision that shipped it as a JSON file.1 There are actually a few other files that drive other features such as the statistics or “fast mode”, but they are all JSON as well. &#8617;]]></summary></entry><entry><title type="html">nixpkgs-multiverse: the fewest nixpkgs</title><link href="https://fzakaria.com/2026/08/17/nixpkgs-multiverse-the-fewest-nixpkgs" rel="alternate" type="text/html" title="nixpkgs-multiverse: the fewest nixpkgs" /><published>2026-08-17T10:15:00-07:00</published><updated>2026-08-17T10:15:00-07:00</updated><id>https://fzakaria.com/2026/08/17/nixpkgs-multiverse-the-fewest-nixpkgs</id><content type="html" xml:base="https://fzakaria.com/2026/08/17/nixpkgs-multiverse-the-fewest-nixpkgs"><![CDATA[<p>If you have not seen my previous posts, I have been working on <a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a>. It is a tool that lets you pin any package to any version it ever shipped, from one flake input.<sup id="fnref:website"><a href="#fn:website" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>You can specify a set of pins by release, commit or version.
By version is particularly useful because it lets you pin packages to a version you might not want to update while still getting the latest of everything else.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">multiverse</span><span class="o">.</span><span class="nv">pins</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nv">ripgrep</span> <span class="o">=</span> <span class="s2">"13.0.0"</span><span class="p">;</span>
  <span class="nv">fd</span> <span class="o">=</span> <span class="s2">"8.7.0"</span><span class="p">;</span>
  <span class="nv">jq</span> <span class="o">=</span> <span class="s2">"1.6"</span><span class="p">;</span>
  <span class="nv">hello</span> <span class="o">=</span> <span class="s2">"2.12.1"</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>In the worst case, each of those resolves on its own, against whichever revision last shipped that version. They are four different revisions, so that configuration is <strong>four</strong> Nixpkgs trees fetched and evaluated.</p>

<p>The cost of the multiverse has never been per package. It is per <em>revision touched</em>. Asking for five packages out of one revision only costs the one revision; asking for five packages out of five revisions and you pay five times.</p>

<p>It would be useful however to minimize the number of Nixpkgs revisions fetched and evaluated. I thought this was a SAT problem (NP-Complete) however the problem turns out to be simpler and solvable in polynomial time, <em>with a small caveat</em>.</p>

<h2 id="pins-are-intervals">Pins are intervals</h2>

<p>A version is not a point in Nixpkgs history, it is a <em>stretch</em>. <code class="language-plaintext highlighter-rouge">ripgrep</code> was at 13.0.0 from June 2021 until November 2023 throughout 541 consecutive channel bumps where <code class="language-plaintext highlighter-rouge">pkgs.ripgrep.version</code> returned exactly that string.</p>

<p>Every pin is a contiguous block on one axis, and a revision <em>serves</em> a pin if it lands inside that pin’s block.</p>

<p><strong>What is the fewest points that touch every block?</strong></p>

<figure>
<svg viewBox="0 0 760 366" role="img" style="display:block;margin-inline:auto;max-width:100%;height:auto;font-family:var(--mono)" aria-label="Six pins drawn as blocks on the revision axis, spaced for legibility rather than to scale. Sorted by where each block ends, the sweep places a revision at the end of the earliest-ending unserved block: jq forces the first, neovim forces the second. Those two blocks never overlap, which is why two revisions is the minimum. hello is served by both and joins the newer.">
  <g stroke="#b1201d" stroke-width="1.5" stroke-dasharray="4 4" opacity="0.85">
    <line x1="342" y1="48" x2="342" y2="258" />
    <line x1="598" y1="48" x2="598" y2="258" />
  </g>
  <g fill="currentColor" font-size="15" text-anchor="middle" font-weight="600">
    <text x="342" y="36">revision 1</text>
    <text x="598" y="36">revision 2</text>
  </g>

  <g fill="currentColor" font-size="15" text-anchor="end">
    <text x="140" y="79">jq 1.6</text>
    <text x="140" y="113">fd 8.7.0</text>
    <text x="140" y="147">ripgrep 13.0.0</text>
    <text x="140" y="181">neovim 0.10.4</text>
    <text x="140" y="215">hello 2.12.1</text>
    <text x="140" y="249">helix 25.01.1</text>
  </g>

  <rect x="150" y="68" width="192" height="12" rx="6" fill="#b1201d" />
  <rect x="214" y="102" width="224" height="12" rx="6" fill="#4c72b0" />
  <rect x="278" y="136" width="256" height="12" rx="6" fill="#4c72b0" />
  <rect x="406" y="170" width="192" height="12" rx="6" fill="#b1201d" />
  <rect x="214" y="204" width="448" height="12" rx="6" fill="#4c72b0" />
  <rect x="502" y="238" width="160" height="12" rx="6" fill="#4c72b0" />

  <g fill="currentColor">
    <circle cx="342" cy="74" r="5" />
    <circle cx="342" cy="108" r="5" />
    <circle cx="342" cy="142" r="5" />
    <circle cx="598" cy="176" r="5" />
    <circle cx="598" cy="210" r="5" />
    <circle cx="598" cy="244" r="5" />
  </g>
  <text x="352" y="276" fill="currentColor" font-size="14" opacity="0.85">hello is served by both, and joins the newer</text>

  <line x1="150" y1="298" x2="726" y2="298" stroke="currentColor" stroke-width="1" opacity="0.35" />
  <g fill="currentColor" font-size="14" opacity="0.85">
    <text x="150" y="317">older revisions</text>
    <text x="726" y="317" text-anchor="end">newer revisions</text>
  </g>

  <g fill="#b1201d">
    <rect x="150" y="333" width="192" height="3" rx="1.5" />
    <rect x="406" y="333" width="192" height="3" rx="1.5" />
  </g>
  <text x="438" y="357" fill="currentColor" font-size="14" opacity="0.85" text-anchor="middle">the two blocks that forced a revision never overlap &#8212; so no plan smaller than 2 exists</text>
</svg>
</figure>

<p>In the example above, six pins require at a minimum two revisions. The dashed lines are the Nixpkgs that actually get fetched, the dots are where each pin ends up, and the red blocks are the two pins that decided it.</p>

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

<p>Turns out the algorithm is relatively simple once we visualize it.
It is the opposite to <strong>interval partitioning</strong> (i.e. fewest meeting rooms), we are doing <strong>activity selection</strong>.</p>

<p>We sort the pins by where their block <strong>ends</strong>. Walk them in that order. If the last revision you placed does not reach the block in front of you, place a new one at that block’s end.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># blocks[i] = (first, last), one pin's stretch of revisions
</span><span class="nc">SWEEP</span><span class="p">(</span><span class="n">blocks</span><span class="p">):</span>
  <span class="n">order</span>  <span class="o">=</span> <span class="n">indices</span> <span class="mf">0.</span><span class="p">.</span><span class="n">n</span><span class="p">,</span> <span class="nb">sorted</span> <span class="n">by</span> <span class="n">blocks</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">last</span> <span class="n">ascending</span>
  <span class="c1"># the revisions we will actually fetch
</span>  <span class="n">chosen</span> <span class="o">=</span> <span class="p">[]</span>

  <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">order</span><span class="p">:</span>
    <span class="p">(</span><span class="n">first</span><span class="p">,</span> <span class="n">last</span><span class="p">)</span> <span class="o">=</span> <span class="n">blocks</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>

    <span class="k">if</span> <span class="n">chosen</span> <span class="ow">and</span> <span class="n">chosen</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">first</span><span class="p">:</span>
      <span class="c1"># a revision we already placed falls inside this pin's block
</span>      <span class="k">continue</span>

    <span class="c1"># unserved, place a revision at the end of its block
</span>    <span class="n">chosen</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">last</span><span class="p">)</span>

  <span class="k">return</span> <span class="n">chosen</span>
</code></pre></div></div>

<p>The algorithm is effectively a sort plus one pass: <code class="language-plaintext highlighter-rouge">O(n log n)</code>. Happily we did not need a solver, z3, or backtracking. It is not approximation either, we get the optimal solution.</p>

<p>How much can this help?</p>

<p>Here is a simulation of the sweep over random pin sets of various sizes, drawn from the real index. The sweep is run three times: once with one revision per pin, once minimised over any era, and once minimised over recent versions.</p>

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

# Simulated over the real index (index/history.json, 1,532 revisions):
# 400 random pin sets at each size, run through the same sweep mvs ships.
#
# "any era" draws versions uniformly from all fourteen years, which is the
# adversarial case. "recent versions" draws from roughly the last three years,
# which is what people actually pin.
df = pd.DataFrame({
    "pins": [2, 3, 5, 8, 12, 16, 20, 30] * 3,
    "revisions": [2, 3, 5, 8, 12, 16, 20, 30]
               + [1.9, 2.7, 4.2, 6.2, 8.6, 10.8, 12.6, 17.1]
               + [1.6, 2.1, 3.1, 4.2, 5.5, 6.7, 7.8, 9.9],
    "how": ["one revision per pin"] * 8
         + ["minimised, any era"] * 8
         + ["minimised, recent versions"] * 8,
})

plot = (
    ggplot(df, aes("pins", "revisions", color="how"))
    + geom_line(size=1.0)
    + geom_point(size=1.8)
    + scale_color_manual(values={"one revision per pin": "#8a8580",
                                 "minimised, any era": "#4c72b0",
                                 "minimised, recent versions": "#b1201d"},
                         name="")
    + labs(x="packages pinned", y="Nixpkgs fetched and evaluated")
    + theme(legend_position="top", legend_title=element_blank())
)
plot.width, plot.height = 7.0, 3.6
</code></pre>

<p>We are able to reduce the number of Nixpkgs revisions fetched from thirty to ten for thirty pins of recent versions. The same thirty package versions, but only ten revisions fetched and evaluated.</p>

<blockquote class="alert alert-tip">
  <p><strong>Tip</strong>
This matters much less if you are on the <a href="/2026/08/14/nixpkgs-multiverse-fast-mode">fast path</a>. A pin the store-path index knows costs no fetch at all as it is immediately substituted from <a href="https://cache.nixos.org/">cache.nixos.org</a>.</p>
</blockquote>

<h2 id="the-receipt">The Receipt</h2>

<p>Every revision the sweep places was placed <em>because</em> of one specific pin, the one it could not reach. Those pins are pairwise disjoint, meaning they never overlap. <strong>k disjoint pins need k distinct revisions.</strong></p>

<p>We expose this information via a “plan” that is viewable from
the <code class="language-plaintext highlighter-rouge">mvs</code> CLI or the Nix API. It is a certificate that the solution is optimal, and it is also useful for debugging.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>mvs solve jq@1.6 fd@8.7.0 ripgrep@13.0.0 <span class="se">\</span>
<span class="go">            hello@2.12.1 neovim@0.10.4 helix@25.01.1
2 revisions · minimal
5 of 6 pins served by the store-path index

ATTR     VERSION  REVISION      DATE        MOVED
jq       1.6      6500b4580c2a  2023-09-25
fd       8.7.0    6500b4580c2a  2023-09-25  24 days (9 revs)
ripgrep  13.0.0   6500b4580c2a  2023-09-25  59 days (20 revs)
hello    2.12.1   698214a32beb  2025-03-25  56 days (27 revs)
neovim   0.10.4   698214a32beb  2025-03-25
helix    25.01.1  698214a32beb  2025-03-25  111 days (47 revs)

  minimal: jq 1.6.x and neovim 0.10.4.x never overlapped
</span></code></pre></div></div>

<p>The sweep might place a particular version earlier than the last revision that shipped it which is highlighted by the <code class="language-plaintext highlighter-rouge">MOVED</code> column.</p>

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

<p>The problem is solvable in polynomial time, but only if every pin is contiguous. If a pin has holes in it, the problem becomes NP-Complete.</p>

<p>In practice though versions do have holes. A package gets dropped from Nixpkgs and comes back at the same version albeit very uncommon.<sup id="fnref:hole"><a href="#fn:hole" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> The sweep above does not know which stretch to use, and it is possible that the wrong choice will force a second revision.</p>

<p>A hole turns one decision into two:</p>

<ol>
  <li><strong>which revisions do we place?</strong></li>
  <li><strong>which stretch of each pin do we aim at?</strong></li>
</ol>

<p>For instance, suppose <code class="language-plaintext highlighter-rouge">foo</code> shipped 1.0, lost it, and got it back later for two stretches. <code class="language-plaintext highlighter-rouge">bar</code> 2.0 was only ever current during the first of them.</p>

<figure>
<svg viewBox="0 0 760 330" role="img" style="display:block;margin-inline:auto;max-width:100%;height:auto;font-family:var(--mono)" aria-label="Two pins. foo 1.0 shipped, was dropped, and came back, so it has two stretches; bar 2.0 overlaps only the earlier one. If foo is held to its newest stretch, nothing overlaps and the plan needs two revisions. If foo may use its earlier stretch, one revision serves both. Which stretch foo should use cannot be decided by looking at foo.">
  <text x="20" y="30" fill="currentColor" font-size="15" font-weight="600">foo must use its newest stretch</text>
  <text x="740" y="30" fill="#b1201d" font-size="15" font-weight="600" text-anchor="end">2 revisions</text>

  <g stroke="#b1201d" stroke-width="1.5" stroke-dasharray="4 4" opacity="0.85">
    <line x1="400" y1="48" x2="400" y2="126" />
    <line x1="660" y1="48" x2="660" y2="126" />
  </g>
  <g fill="currentColor" font-size="15" text-anchor="end">
    <text x="150" y="79">foo 1.0</text>
    <text x="150" y="115">bar 2.0</text>
  </g>
  <rect x="180" y="68" width="140" height="12" rx="6" fill="#4c72b0" fill-opacity=".16" stroke="#4c72b0" stroke-opacity=".55" stroke-width="1" stroke-dasharray="3 3" />
  <rect x="500" y="68" width="160" height="12" rx="6" fill="#4c72b0" />
  <rect x="230" y="104" width="170" height="12" rx="6" fill="#4c72b0" />
  <g fill="#b1201d">
    <circle cx="660" cy="74" r="5" />
    <circle cx="400" cy="110" r="5" />
  </g>

  <line x1="20" y1="160" x2="740" y2="160" stroke="currentColor" stroke-width="1" opacity="0.25" />

  <text x="20" y="196" fill="currentColor" font-size="15" font-weight="600">foo may use its earlier stretch</text>
  <text x="740" y="196" fill="#b1201d" font-size="15" font-weight="600" text-anchor="end">1 revision</text>

  <g stroke="#b1201d" stroke-width="1.5" stroke-dasharray="4 4" opacity="0.85">
    <line x1="320" y1="214" x2="320" y2="292" />
  </g>
  <g fill="currentColor" font-size="15" text-anchor="end">
    <text x="150" y="245">foo 1.0</text>
    <text x="150" y="281">bar 2.0</text>
  </g>
  <rect x="180" y="234" width="140" height="12" rx="6" fill="#4c72b0" />
  <rect x="500" y="234" width="160" height="12" rx="6" fill="#4c72b0" fill-opacity=".16" stroke="#4c72b0" stroke-opacity=".55" stroke-width="1" stroke-dasharray="3 3" />
  <rect x="230" y="270" width="170" height="12" rx="6" fill="#4c72b0" />
  <g fill="#b1201d">
    <circle cx="320" cy="240" r="5" />
    <circle cx="320" cy="276" r="5" />
  </g>

  <text x="380" y="320" fill="currentColor" font-size="14" opacity="0.85" text-anchor="middle">nothing about <tspan font-style="italic">foo</tspan> says which stretch to use, only <tspan font-style="italic">bar</tspan> does</text>
</svg>
</figure>

<p>Depending on which stretch of <code class="language-plaintext highlighter-rouge">foo</code> you choose, the plan is either one or two revisions. The choice of which stretch to use cannot be made by looking at <code class="language-plaintext highlighter-rouge">foo</code> alone, only by looking at <code class="language-plaintext highlighter-rouge">bar</code>.</p>

<p>This turns each decision into two, causing the algorithmic complexity to become exponential in the number of holed pins. The problem is NP-Complete, and it is equivalent to <a href="https://en.wikipedia.org/wiki/Vertex_cover">vertex cover</a>.</p>

<p>This is the part I got wrong. I looked at the problem and saw constraints being satisfied, which I pattern-matched to SAT.</p>

<p>Turns out by not having a second choice, we get to stay in polynomial time. The fix is to not have a second choice. A pin is defined to take the <strong>newest</strong> of its stretches and only that one which simplifies our problem. Our greedy algorithm is now optimal.</p>

<h2 id="minor-footgun">Minor footgun</h2>

<p>Grouping pins pulls some of them <em>backwards</em>. From our example above: <code class="language-plaintext highlighter-rouge">helix 25.01.1</code> is picked earlier than the last revision that shipped it in order to group with <code class="language-plaintext highlighter-rouge">neovim 0.10.4</code>.</p>

<p>What does this mean in practice?</p>

<p>Although a version is a stretch, it is technically not
the same throughout. Dependencies and build inputs can change, so
the closure of a package at one revision is not guaranteed to be
the same as the closure of that same package at another revision,
even if the version string is identical.</p>

<p>You may be missing improvements or fixes to the closure of a package despite the version string being the same.</p>

<h2 id="using-it">Using it</h2>

<p><code class="language-plaintext highlighter-rouge">mvs solve</code> answers the fewest revisions necessary to serve a set of pins.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># the plan, as JSON, with the certificate
</span><span class="gp">$</span><span class="w"> </span>mvs solve <span class="nt">--json</span> python3@3.8 nodejs@14 | jq .why
<span class="go">"one revision serves every pin"

</span><span class="c"># if you need exactly one Nixpkgs, assert it -- the plan already knows
</span><span class="gp">$</span><span class="w"> </span>mvs solve <span class="nt">--json</span> python3@3.8 nodejs@14 | jq <span class="nt">-e</span> <span class="s1">'.revisions == 1'</span>
</code></pre></div></div>

<p>An existing lock file can be optimized in place:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>mvs lock minimize
<span class="go">4 pins · 4 revisions → 1 · minimal

ATTR     VERSION  REVISION      DATE        OLDER BY
fd       8.7.0    6500b4580c2a  2023-09-25  24 days
hello    2.12.1   6500b4580c2a  2023-09-25  603 days
ripgrep  13.0.0   6500b4580c2a  2023-09-25  59 days

  minimal: one revision serves every pin

</span><span class="c"># report and refuse to write, for CI
</span><span class="gp">$</span><span class="w"> </span>mvs lock minimize <span class="nt">--check</span>
</code></pre></div></div>

<p>On the Nix side, the whole set resolves at once:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">mv</span><span class="o">.</span><span class="nv">solvePins</span> <span class="p">{</span> <span class="nv">ripgrep</span> <span class="o">=</span> <span class="s2">"13.0.0"</span><span class="p">;</span> <span class="nv">fd</span> <span class="o">=</span> <span class="s2">"8.7.0"</span><span class="p">;</span> <span class="nv">jq</span> <span class="o">=</span> <span class="s2">"1.6"</span><span class="p">;</span> <span class="p">}</span>
<span class="c"># =&gt; { ripgrep = &lt;drv&gt;; fd = &lt;drv&gt;; jq = &lt;drv&gt;; }</span>
<span class="c"># all three out of 2023-09-25-6500b4580c2a</span>
</code></pre></div></div>

<p>You can customize this behavior through the modules. It is on by default:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">multiverse</span><span class="o">.</span><span class="nv">pins</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">python3</span> <span class="o">=</span> <span class="s2">"3.8.9"</span><span class="p">;</span>
    <span class="nv">nodejs</span> <span class="o">=</span> <span class="s2">"14.17.3"</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="c"># `plan` is computed from the index without</span>
  <span class="c"># fetching anything, so you can</span>
  <span class="c"># demand a single Nixpkgs and fail the</span>
  <span class="c"># build if you cannot have one.</span>
  <span class="nv">assertions</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="nv">assertion</span> <span class="o">=</span> <span class="nv">config</span><span class="o">.</span><span class="nv">multiverse</span><span class="o">.</span><span class="nv">plan</span><span class="o">.</span><span class="nv">revisions</span> <span class="o">==</span> <span class="mi">1</span><span class="p">;</span>
      <span class="nv">message</span> <span class="o">=</span> <span class="nv">config</span><span class="o">.</span><span class="nv">multiverse</span><span class="o">.</span><span class="nv">plan</span><span class="o">.</span><span class="nv">why</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can find the full design and API documentation on the <a href="https://nixmultiverse.com/docs/design#minimising">nixpkgs-multiverse website</a>.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:website">
      <p><a href="https://nixmultiverse.com/">nixpkgs-multiverse</a> sitis a nice website that makes the data browseable. <a href="#fnref:website" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:hole">
      <p>About <strong>1.7%</strong> of all <code class="language-plaintext highlighter-rouge">(attribute, version)</code> pairs in the index have such holes. <a href="#fnref:hole" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[If you have not seen my previous posts, I have been working on nixpkgs-multiverse. It is a tool that lets you pin any package to any version it ever shipped, from one flake input.1 nixpkgs-multiverse sitis a nice website that makes the data browseable. &#8617;]]></summary></entry><entry><title type="html">DEFCON34 wrap-up</title><link href="https://fzakaria.com/2026/08/16/defcon34-wrap-up" rel="alternate" type="text/html" title="DEFCON34 wrap-up" /><published>2026-08-16T09:15:00-07:00</published><updated>2026-08-16T09:15:00-07:00</updated><id>https://fzakaria.com/2026/08/16/defcon34-wrap-up</id><content type="html" xml:base="https://fzakaria.com/2026/08/16/defcon34-wrap-up"><![CDATA[<p>I recently came back from <a href="https://defcon.org/html/defcon-34/dc-34-index.html">DEFCON34</a> and the <a href="https://nix.vegas/">nix.vegas</a> community. The talks I gave are now online if you are interested in watching them. 🙌</p>

<p>Many thanks to all the organizers of DEFCON34 and nix.vegas. This is our, the Nix community and mine specifically, second year at DEFCON34 and it was a blast. To be honest,
I barely interacted with the rest of DEFCON because I was so busy with the Nix community. The talks, the hallway conversations, and the in-chance encounters were all amazing.</p>

<p>One particular story, was that <a href="https://blog.carldong.me/">Carl Dong</a> happen to be walking by the Nix Vegas village as I was giving my talk on <a href="/2026/07/29/guix-by-nix">Guix by Nix</a>. He was a Bitcoin core developer and was one of the contributors responsible for the <a href="https://bitcoinops.org/en/topics/reproducible-builds/">Bitcoin Core reproducible builds</a> project that leverges <a href="https://guix.gnu.org/">Guix</a>.<sup id="fnref:bitcoin"><a href="#fn:bitcoin" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>Kismet.</p>

<h2 id="what-is-nixvegas">What is nix.vegas?</h2>

<p>For those that don’t know: <a href="https://nix.vegas/">nix.vegas</a> is the Nix community that runs within DEF CON in Las Vegas, hosted by the <a href="https://socalnixos.org/">SoCal NixOS User Group</a> and Distractions, Inc. This was its second year: DEF CON 33 ran under the banner <em>“Rebuild the World”</em>, and this year’s theme was <em>“Escape Your Fate”</em>.</p>

<p>The <a href="https://www.youtube.com/playlist?list=PLLa7deZPvzZ4">full playlist is on YouTube</a>.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
If the sound is a bit off or weird, this year DEF CON 
experimented with “silent” talks. Each talk was broadcasted and
attendees had to wear headphones to listen. It was a bit weird
giving talks to a quiet room. 🤷</p>
</blockquote>

<h2 id="relocatable-nix-binaries">Relocatable Nix Binaries</h2>

<iframe src="https://www.youtube.com/embed/jeLJ5ObNKDg" title="Farid Zakaria - Relocatable Nix Binaries" frameborder="0" allowfullscreen=""></iframe>

<p><strong>Summary</strong>: Nix’s absolute <code class="language-plaintext highlighter-rouge">/nix/store</code> paths buy us reproducibility, but costs us the ability to put the store anywhere else. You <em>can</em> change the store prefix today, but it changes the hash of every single derivation in the closure down to <code class="language-plaintext highlighter-rouge">bash</code>, so you get to rebuild the world before you get to run <code class="language-plaintext highlighter-rouge">hello</code>.</p>

<p>How can we circumvent this?</p>

<p>The talk walks through <code class="language-plaintext highlighter-rouge">$ORIGIN</code> in <code class="language-plaintext highlighter-rouge">RUNPATH</code> and upstreaming support in the Linux kernel via a eBPF-based <code class="language-plaintext highlighter-rouge">binfmt_misc</code> solution.</p>

<p>Further reading: <a href="/2026/07/20/linux-kernel-will-support-origin-sort-of">Linux kernel will support $ORIGIN, sort of</a>.</p>

<h2 id="guix-by-nix">Guix by Nix</h2>

<iframe src="https://www.youtube.com/embed/oTsXNxMapj8" title="Farid Zakaria - Guix by Nix: Stealing an entire distro's bootstrap, one .drv at a time" frameborder="0" allowfullscreen=""></iframe>

<p><strong>Summary</strong>: What was meant to be a lightning talk on <a href="https://github.com/fzakaria/guix-transfer">guix-transfer</a> and <a href="https://github.com/fzakaria/guixpkgs">GuixPkgs</a> but went a little over. This is our project on rewriting Guix derivations into Nix derivations so that every Guix package becomes buildable by Nix. This lets us include their source-bootstrapped JDK for instance, which nixpkgs does not have.</p>

<p>Further reading: <a href="/2026/07/29/guix-by-nix">Guix by Nix</a> and <a href="/2026/06/25/guixpkgs-every-guix-package-as-a-nix-flake">GuixPkgs: every Guix package, as a Nix flake</a></p>

<h2 id="how-to-piss-off-your-nix-friends">How to piss off your Nix friends</h2>

<iframe src="https://www.youtube.com/embed/q2-fspvj18U" title="Farid Zakaria - How to piss off your Nix friends" frameborder="0" allowfullscreen=""></iframe>

<p><strong>Summary</strong>: This talk is a bit of a rant, but it is given in good faith with a dose of humor. The core claim is that we optimize Nix and nixpkgs for social comfort and broad appeal, and we pay for it in technical ambition.</p>

<p>Further reading: <a href="/2026/07/18/how-to-piss-off-your-nix-friends">How to piss off your Nix friends</a>.</p>

<p>Looking forward to next year. Three talks in two days was a little ambitious, but I would do it again.</p>

<p>Everything lives on my <a href="/talks">talks page</a> alongside their slides and the rest of my talks.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:bitcoin">
      <p>He was pleasantly surprised and happy to hear that Nix also has reproducible builds that start from <a href="https://savannah.nongnu.org/projects/stage0/">stage0</a>. <a href="#fnref:bitcoin" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I recently came back from DEFCON34 and the nix.vegas community. The talks I gave are now online if you are interested in watching them. 🙌]]></summary></entry><entry><title type="html">nixpkgs-multiverse: fast mode</title><link href="https://fzakaria.com/2026/08/14/nixpkgs-multiverse-fast-mode" rel="alternate" type="text/html" title="nixpkgs-multiverse: fast mode" /><published>2026-08-14T16:20:00-07:00</published><updated>2026-08-14T16:20:00-07:00</updated><id>https://fzakaria.com/2026/08/14/nixpkgs-multiverse-fast-mode</id><content type="html" xml:base="https://fzakaria.com/2026/08/14/nixpkgs-multiverse-fast-mode"><![CDATA[<blockquote>
  <p><em>“The fastest evaluation is the one that never happens.”</em></p>

  <p>– Sun Tzu, <em>The Art of Evaluation</em></p>
</blockquote>

<p><a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a> gives you every version of every package that ever shipped in Nixpkgs from a single flake input.</p>

<blockquote class="alert alert-info">
  <p><strong>Note</strong>
It continues to blow my mind that this is even possible. It feels like it suddenly
unlocks a new dimension of Nixpkgs, and I am still trying to understand what it means.
I think this capability is a fundamental change to the way we think about Nixpkgs, and it is not just a new feature. It is a new way of thinking about the entire ecosystem.</p>
</blockquote>

<p>There was always a penalty at the center of it. Asking for a specific version of <code class="language-plaintext highlighter-rouge">python3</code>, such as <code class="language-plaintext highlighter-rouge">3.8.9</code>, meant fetching the whole ~378 MB Nixpkgs tree from 2021 and evaluating it to determine the <code class="language-plaintext highlighter-rouge">outPath</code>.</p>

<p>What if we could skip that evaluation? What if we could just ask for the path directly, and have Nix fetch it from the cache if it is there?</p>

<p>This is a common idiom if you have ever used <code class="language-plaintext highlighter-rouge">nix-store</code>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># check if the store has the path already
</span><span class="gp">$</span><span class="w"> </span>nix path-info <span class="nt">--store</span> https://cache.nixos.org/ <span class="se">\</span>
<span class="go">      /nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9
/nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9

</span><span class="c"># fetch it if it does
</span><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">--realise</span> /nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9
<span class="go">/nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9
</span></code></pre></div></div>

<p>That requires knowing the store path upfront.</p>

<p><a href="https://github.com/fzakaria/nixpkgs-multiverse">nixpkgs-multiverse</a> now has a <code class="language-plaintext highlighter-rouge">fast</code> attribute that does exactly that: it gives you the store path for every indexed version of every package. This lets you skip the download and evaluation of Nixpkgs and get the store path straight from the cache.</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="s1">'github:fzakaria/nixpkgs-multiverse#fast.versions.python3."3.8.9".out'</span> <span class="se">\</span>
<span class="go">      --print-out-paths
/nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9

</span><span class="gp">$</span><span class="w"> </span>nix shell <span class="s1">'github:fzakaria/nixpkgs-multiverse#fast.versions.python3."3.8.9".out'</span>
<span class="gp">$</span><span class="w"> </span>python3 <span class="nt">--version</span>
<span class="go">Python 3.8.9
</span></code></pre></div></div>

<p>No Nixpkgs is fetched. Nothing is evaluated. No experimental features and no <code class="language-plaintext highlighter-rouge">--impure</code> needed for this to work.</p>

<p>The complete Nix API, except for releases, works with this fast path.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># a specific version, zero-eval</span>
<span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="nv">version</span> <span class="s2">"python3"</span> <span class="s2">"3.8.9"</span>
<span class="c"># newest indexed version, as of the pin</span>
<span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="nv">latest</span><span class="o">.</span><span class="nv">python3</span>
<span class="c"># what was current when the pin was cut</span>
<span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="nv">tip</span><span class="o">.</span><span class="nv">hello</span>
<span class="c"># a whole revision, as fakes</span>
<span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="nv">at</span> <span class="s2">"2022-03-15"</span>
<span class="c"># exact revision keys work too</span>
<span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="s2">"967d40bec14b"</span><span class="o">.</span><span class="nv">python3</span>
</code></pre></div></div>

<p>If you want to learn more <a href="https://nixmultiverse.com/docs/nix-api#the-fast-path">read the docs</a> about the feature.</p>

<h2 id="the-trick-mkfakederivation">The trick: <code class="language-plaintext highlighter-rouge">mkFakeDerivation</code></h2>

<p>Every <code class="language-plaintext highlighter-rouge">nixos-unstable</code> channel bump published a listing of every path Hydra built for it: <code class="language-plaintext highlighter-rouge">store-paths.xz</code>, or a <code class="language-plaintext highlighter-rouge">MANIFEST</code> for back in the pre-2017 era. These files are still available, and they are the source of the multiverse index.</p>

<p>The listing is a map from derivation name to store path. The multiverse index is a map from <code class="language-plaintext highlighter-rouge">(attribute, version)</code> to the revision that shipped it. By joining the two, every historical version gets a concrete address:</p>

<div class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">join</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">TB</span>
  <span class="c1">// JetBrains Mono, and nothing else, because it is the one font the page</span>
  <span class="c1">// itself serves: an HTML-label table is drawn to the width Graphviz</span>
  <span class="c1">// measures here, and any face the browser substitutes instead would push</span>
  <span class="c1">// the text past the cell borders. flake.nix puts the same file fontconfig</span>
  <span class="c1">// finds here in front of the reader as a @font-face.</span>
  <span class="n">fontname</span><span class="p">=</span><span class="s2">"JetBrains Mono"</span>
  <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">plaintext</span> <span class="n">fontname</span><span class="p">=</span><span class="s2">"JetBrains Mono"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">11</span><span class="o">]</span>
  <span class="k">edge</span> <span class="o">[</span><span class="n">fontname</span><span class="p">=</span><span class="s2">"JetBrains Mono"</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">10</span> <span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.7</span><span class="o">]</span>
  <span class="n">nodesep</span><span class="p">=</span><span class="mf">0.6</span> <span class="n">ranksep</span><span class="p">=</span><span class="mf">0.5</span> <span class="n">pad</span><span class="p">=</span><span class="mf">0.2</span>

  <span class="nv">index</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span>
    <span class="nt">&lt;table</span> <span class="na">border=</span><span class="s">"0"</span> <span class="na">cellborder=</span><span class="s">"1"</span> <span class="na">cellspacing=</span><span class="s">"0"</span> <span class="na">cellpadding=</span><span class="s">"6"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td</span> <span class="na">colspan=</span><span class="s">"3"</span><span class="nt">&gt;</span>multiverse index<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>attribute<span class="nt">&lt;/td&gt;&lt;td&gt;</span>version<span class="nt">&lt;/td&gt;&lt;td&gt;</span>revision<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>python3<span class="nt">&lt;/td&gt;&lt;td&gt;</span>3.8.9<span class="nt">&lt;/td&gt;</span>
          <span class="nt">&lt;td&gt;&lt;font</span> <span class="na">color=</span><span class="s">"#b1201d"</span><span class="nt">&gt;</span>967d40bec14b<span class="nt">&lt;/font&gt;&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>python3<span class="nt">&lt;/td&gt;&lt;td&gt;</span>3.9.6<span class="nt">&lt;/td&gt;&lt;td&gt;</span>2846d0dc2eb1<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
    <span class="nt">&lt;/table&gt;</span><span class="p">&gt;</span><span class="o">]</span>

  <span class="nv">listing</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span>
    <span class="nt">&lt;table</span> <span class="na">border=</span><span class="s">"0"</span> <span class="na">cellborder=</span><span class="s">"1"</span> <span class="na">cellspacing=</span><span class="s">"0"</span> <span class="na">cellpadding=</span><span class="s">"6"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td</span> <span class="na">colspan=</span><span class="s">"2"</span><span class="nt">&gt;</span>store-paths.xz @ <span class="nt">&lt;font</span> <span class="na">color=</span><span class="s">"#b1201d"</span><span class="nt">&gt;</span>967d40bec14b<span class="nt">&lt;/font&gt;&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>name<span class="nt">&lt;/td&gt;&lt;td&gt;</span>store path<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>bash-5.1-p8<span class="nt">&lt;/td&gt;&lt;td&gt;</span>/nix/store/1ck5…-bash-5.1-p8<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
      <span class="nt">&lt;tr&gt;&lt;td&gt;</span>python3-3.8.9<span class="nt">&lt;/td&gt;&lt;td&gt;</span>/nix/store/6cfa…-python3-3.8.9<span class="nt">&lt;/td&gt;&lt;/tr&gt;</span>
    <span class="nt">&lt;/table&gt;</span><span class="p">&gt;</span><span class="o">]</span>

  <span class="nv">out</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">margin</span><span class="p">=</span><span class="s2">"0.25,0.14"</span>
       <span class="n">label</span><span class="p">=&lt;</span>fast.versions.python3."3.8.9".out<span class="nt">&lt;br/&gt;</span>/nix/store/6cfa…-python3-3.8.9<span class="p">&gt;</span>
       <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>

  <span class="p">{</span> <span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span> <span class="nv">index</span><span class="p">;</span> <span class="nv">listing</span> <span class="p">}</span>

  <span class="nv">index</span> <span class="o">-&gt;</span> <span class="nv">listing</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">" revision "</span><span class="o">]</span>
  <span class="nv">index</span> <span class="o">-&gt;</span> <span class="nv">out</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">" attribute + version "</span> <span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span>
  <span class="nv">listing</span> <span class="o">-&gt;</span> <span class="nv">out</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">" name → store path "</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Knowing the path is not enough, especially in the Nix language. We need to convince Nix that a string that looks like a store path actually <em>is</em> a store path. <code class="language-plaintext highlighter-rouge">builtins.storePath</code> exists but it is an impure function and requires <code class="language-plaintext highlighter-rouge">--impure</code> to work.</p>

<p>How do we get around this?</p>

<p>We attach “context” to the String. Context is the invisible baggage a String carries in Nix. When you interpolate a derivation into a String, the result remembers where it came from, and that is what makes <code class="language-plaintext highlighter-rouge">nix build</code> realise the dependency instead of writing a dangling path into a script.</p>

<p><code class="language-plaintext highlighter-rouge">builtins.appendContext</code> lets you attach it by hand.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">storePath</span> <span class="o">=</span> <span class="nv">p</span><span class="p">:</span> <span class="kr">builtins</span><span class="o">.</span><span class="nv">appendContext</span> <span class="nv">p</span> <span class="p">{</span> <span class="p">${</span><span class="nv">p</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span> <span class="nv">path</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="p">};</span> <span class="p">};</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">path = true</code> identifies that “this String names a store path that must exist,” which is exactly what <code class="language-plaintext highlighter-rouge">builtins.storePath</code> produces for a path already in your store, except this works for a path that is not in your store yet and is not in this evaluation’s input closure either. Loopole! 👿</p>

<p>We then wrap that in an attrset that resembles like a derivation and the Nix CLI is satisfied:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">type</span> <span class="o">=</span> <span class="s2">"derivation"</span><span class="p">;</span>
  <span class="nv">name</span> <span class="o">=</span> <span class="s2">"python3-3.8.9"</span><span class="p">;</span>
  <span class="nv">pname</span> <span class="o">=</span> <span class="s2">"python3"</span><span class="p">;</span>
  <span class="nv">version</span> <span class="o">=</span> <span class="s2">"3.8.9"</span><span class="p">;</span>
  <span class="nv">system</span> <span class="o">=</span> <span class="s2">"x86_64-linux"</span><span class="p">;</span>
  <span class="nv">outputs</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"out"</span> <span class="p">];</span>
  <span class="nv">out</span> <span class="o">=</span> <span class="nv">storePath</span> <span class="s2">"/nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is <a href="https://github.com/tomberek">tomberek</a>’s <code class="language-plaintext highlighter-rouge">mkFakeDerivation</code> trick from <a href="https://github.com/tomberek/fastpkgs">fastpkgs</a>, and it is an amazing trick to circumvent needing <code class="language-plaintext highlighter-rouge">--impure</code>.</p>

<p>Everything about this remains pure evaluation, and the resulting graph is guaranteed to
be bit-for-bit identical to what Nixpkgs would have produced if it had been evaluated. The only difference is that we skip the evaluation of Nixpkgs itself, and instead use the store path directly.</p>

<p>The eval path <em>derives</em> the address, the fast path <em>remembers</em> it.</p>

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

<p>A “fake” (<code class="language-plaintext highlighter-rouge">mkFakeDerivation</code>) derivation has no <code class="language-plaintext highlighter-rouge">drvPath</code>, because there is no <code class="language-plaintext highlighter-rouge">.drv</code> behind it. Nothing can build it and it can only be substituted. The <code class="language-plaintext highlighter-rouge">nix</code> CLI often wants a <code class="language-plaintext highlighter-rouge">drvPath</code> though when you hand it a derivation attrset, so we must make sure to append the output (i.e. <code class="language-plaintext highlighter-rouge">.out</code>):</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="s1">'github:fzakaria/nixpkgs-multiverse#fast.latest.hello.out'</span>
<span class="gp">$</span><span class="w"> </span>nix build <span class="s1">'github:fzakaria/nixpkgs-multiverse#fast.latest.ffmpeg.lib'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">override</code> and <code class="language-plaintext highlighter-rouge">nix develop</code> need a real derivation. Every fake derivation carries a lazy <code class="language-plaintext highlighter-rouge">.eval</code> that is the real, revision-exact derivation:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">mv</span><span class="o">.</span><span class="nv">fast</span><span class="o">.</span><span class="nv">version</span> <span class="s2">"python3"</span> <span class="s2">"3.8.9"</span><span class="p">)</span><span class="o">.</span><span class="nv">eval</span><span class="o">.</span><span class="nv">override</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
</code></pre></div></div>

<p>In the spirit of trying to keep my index small, <code class="language-plaintext highlighter-rouge">meta</code> is empty, so there is
not additional information about the package. You can still get the <code class="language-plaintext highlighter-rouge">meta</code> from the real derivation by using <code class="language-plaintext highlighter-rouge">.eval</code> as well.</p>

<p>This scheme rests on <a href="https://cache.nixos.org">cache.nixos.org</a> still serving thirteen-year-old paths, which thankfully it does and with the same signing key.</p>

<p>To demonstrate that the cache is offering nearly every path that Nixpkgs ever built, I ran a census of every indexed version of every package and asked the cache if it was still alive.</p>

<p>As of August 14 2026, <strong>all 271,187 of them are alive</strong>.  All of them, down to every NAR payload file. That is 14.8 TB of unpacked software from 2013 onward, one <em>fast</em> command away.<sup id="fnref:gc"><a href="#fn:gc" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>There’s some other data on <a href="https://nixmultiverse.com/">nixmultiverse.com</a> about the census, dependency graphs and additional features. Check it out!</p>

<video autoplay="" loop="" muted="" playsinline="" width="800" height="470">
  <source src="/assets/images/multiverse-universe-slider.mp4" type="video/mp4" />
  <a href="/assets/images/multiverse-universe-slider.mp4">Screencast of the nixmultiverse.com universe slider</a>
</video>

<p>All of this is also available via the <a href="https://nixmultiverse.com/docs/cli">mvs</a> command line tool as well for offline use.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># how big is it, unpacked, downloaded, and in full closure
</span><span class="gp">$</span><span class="w"> </span>mvs size python3@3.8.9
<span class="go">python3 3.8.9 · /nix/store/6cfajs6lsy9b4wxp3jvyyl1g5x2pjmpr-python3-3.8.9
  nar (unpacked)  50.1 MiB
  download        10.6 MiB
  closure         93.8 MiB · 16 paths
  cache           live

</span><span class="c"># who links against it
</span><span class="gp">$</span><span class="w"> </span>mvs rdeps pcre2
<span class="go">pcre2 10.47 · referenced by 255 indexed packages

</span><span class="c"># what is this path in my store, actually
</span><span class="gp">$</span><span class="w"> </span>mvs identify /nix/store/8qi947kixhz1nw83dkwxm6d0wndprqkj-hello-2.12.2
<span class="go">  package  hello 2.12.2

</span><span class="c"># run takes the fast path by default
</span><span class="gp">$</span><span class="w"> </span>mvs run hello@2.12.2
<span class="go">Hello, world!
</span></code></pre></div></div>

<p>I guess now there is a caveat: there is now a trick in the multiverse. It remains mostly an index, some JSON, and a <code class="language-plaintext highlighter-rouge">fetchTree</code> behind a memo table. The clever trick is <a href="https://github.com/tomberek">tomberek</a>’s, and it is three <em>important</em> lines.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:gc">
      <p>The NixOS infrastructure has never garbage collected the binary cache. It is an S3 bucket that only grows, and the bill is paid by the <a href="https://nixos.org/community/">NixOS Foundation</a> and its sponsors. <a href="#fnref:gc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[“The fastest evaluation is the one that never happens.” – Sun Tzu, The Art of Evaluation]]></summary></entry><entry><title type="html">nixpkgs-multiverse is audacitymaxxing</title><link href="https://fzakaria.com/2026/08/11/nixpkgs-multiverse-is-audacitymaxxing" rel="alternate" type="text/html" title="nixpkgs-multiverse is audacitymaxxing" /><published>2026-08-11T08:30:00-07:00</published><updated>2026-08-11T08:30:00-07:00</updated><id>https://fzakaria.com/2026/08/11/nixpkgs-multiverse-is-audacitymaxxing</id><content type="html" xml:base="https://fzakaria.com/2026/08/11/nixpkgs-multiverse-is-audacitymaxxing"><![CDATA[<p><em>Every package manager on earth picks one version for you. Nixpkgs picked one too. It never had to.</em></p>

<p>I shared <a href="/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed">nixpkgs-multiverse</a> recently: one flake input that hands you every version of every package that ever shipped in Nixpkgs.</p>

<p>I love how unbelievable audacious Nix lets me be, <em>audacitymaxxing</em>.</p>

<p>As of this writing, you have access to 31,783 packages and
304,484 distinct package version pairs pulled from 1,537 revisions. 🤯</p>

<p>The fact most distributions only give you one version of each package is not a bug. It is often considered a feature: a single self-consistent set of software that boots and runs together.
It falls directly out of a shared global filesystem, the <a href="https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html">filesystem hierarchy standard</a> (FHS), like <code class="language-plaintext highlighter-rouge">/usr</code>, <code class="language-plaintext highlighter-rouge">/lib</code> and <code class="language-plaintext highlighter-rouge">/etc</code>.</p>

<p>The purpose and existence of Nix is to eschew from that convention and allow multiple versions of the same package to coexist. Nixpkgs is a distribution built on that capability, and yet, it has been doing the same thing as every other distribution: picking one version of everything.</p>

<p><a href="https://github.com/fzakaria/nixpkgs-multiverse">nixpkgs-multiverse</a> only supports, <em>at the moment</em>, top-level attributes that are packages but already the sheer volume of
installable software dwarfs <code class="language-plaintext highlighter-rouge">nixpkgs-unstable</code>.<sup id="fnref:repology"><a href="#fn:repology" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<pre><code class="language-plotnine">import re

import pandas as pd
from plotnine import *

# Total installable package entries per repository, 2026-08-11, from
# repology.org/repositories/packages -- the "Packages / Total" column.
#
# nixpkgs-multiverse is its own index rather than a repology row: 304,484
# (attribute, version) pairs across 1,537 indexed nixpkgs revisions.
ROWS = (
    "nixpkgs-multiverse:304484 nixpkgs unstable:147500 AUR:117218 "
    "Debian Unstable:42762 Ubuntu 26.04:40697 FreeBSD Ports:38574 "
    "Debian 13:38559 GNU Guix:32956 Fedora 43:30089 Alpine 3.22:26318 "
    "openSUSE Tumbleweed:17108 Arch Linux:15449 Homebrew:12984 "
)

df = pd.DataFrame([(m[0], int(m[1]))
                   for m in re.findall(r"([A-Za-z0-9 .+-]+?):(\d+)", ROWS)],
                  columns=["repo", "entries"])
df = df.sort_values("entries").reset_index(drop=True)
df["repo"] = pd.Categorical(df["repo"], categories=df["repo"], ordered=True)

# Only the last row is the multiverse; everything above it is a snapshot.
df["kind"] = ["one snapshot"] * (len(df) - 1) + ["every snapshot"]

plot = (
    ggplot(df, aes("repo", "entries", fill="kind"))
    + geom_col(width=0.65)
    + geom_text(aes(label="entries"), format_string="{:,}", size=7.5,
                ha="left", nudge_y=4000)
    + coord_flip()
    + scale_y_continuous(limits=(0, 350000), expand=(0, 0),
                         labels=lambda ys: [f"{int(y / 1000)}k" for y in ys])
    + scale_fill_manual(values={"one snapshot": "#4c72b0",
                                "every snapshot": "#b1201d"}, name="")
    + labs(x="", y="installable package entries")
    + theme(legend_position="none")
)
plot.width, plot.height = 7.0, 4.0
</code></pre>

<p>Nix’s answer to the FHS was audacious in 2003 and is still audacious now. A package lives at <code class="language-plaintext highlighter-rouge">/nix/store/&lt;hash&gt;-python3-3.12.10</code>, where the hash is derived from every input that went into building it: the <a href="/2025/03/08/demystifying-nix-s-intensional-model">intensional model</a>.</p>

<p>How audacious are we? How about 246 distinct CPython versions, from 2.6.8 forward, all installable side by side, all built and cached, all addressable by version number instead of commit hash.<sup id="fnref:python_repology"><a href="#fn:python_repology" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

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

# Distinct CPython versions per repository, 2026-08-11.
# Repos: repology /api/v1/project/python, counting distinct `version` values.
# multiverse: the union over every `python*` attribute in index/versions.json.
df = pd.DataFrame({
    "repo": ["Debian 13", "Ubuntu 26.04", "Arch", "Alpine 3.22", "GNU Guix",
             "nixpkgs unstable", "Homebrew", "openSUSE Tumbleweed",
             "FreeBSD Ports", "Fedora 43", "AUR", "nixpkgs-multiverse"],
    "n": [1, 1, 1, 1, 4, 5, 6, 6, 8, 14, 21, 246],
})
df = df.sort_values("n").reset_index(drop=True)
df["repo"] = pd.Categorical(df["repo"], categories=df["repo"], ordered=True)
df["kind"] = ["today's snapshot"] * 11 + ["every snapshot"]

# Linear, not log. A log axis would flatter the distros by turning "one" into
# a respectable-looking bar, and the whole point is the ratio.
plot = (
    ggplot(df, aes("repo", "n", fill="kind"))
    + geom_col(width=0.65)
    + geom_text(aes(label="n"), size=7.5, ha="left", nudge_y=4)
    + coord_flip()
    + scale_y_continuous(limits=(0, 270), expand=(0, 0))
    + scale_fill_manual(values={"today's snapshot": "#4c72b0",
                                "every snapshot": "#b1201d"}, name="")
    + labs(x="", y="distinct CPython versions installable")
    + theme(legend_position="none")
)
plot.width, plot.height = 7.0, 3.6
</code></pre>

<p>To re-iterate, these are distinct versions of CPython, including their transitive dependencies. There is no <code class="language-plaintext highlighter-rouge">glibc</code> or <code class="language-plaintext highlighter-rouge">openssl</code> or <code class="language-plaintext highlighter-rouge">zlib</code> that is shared between them.<sup id="fnref:glibc"><a href="#fn:glibc" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> They work just as reliably as when they were first released, and they are all still installable today and can be substituted from the cache.</p>

<p>People want to pin to a version. Upgrading software can be disruptive, and some people have to stay on a particular version but that should not impede the rest of the world from moving forward.</p>

<p>The <a href="https://github.com/fzakaria/nixpkgs-multiverse">nixpkgs-multiverse</a> helped solve one of the oldest <a href="https://devenv.sh/">devenv.sh</a> issues, <a href="https://github.com/cachix/devenv/issues/16">cachix/devenv#16</a>,
the desire to pin a specific package.</p>

<blockquote>
  <p>“It is not really practical to pin a separate version of nixpkgs for every different version of a tool needed in a dev environment. Normally we have at least 20-30 different tools all with a specific pinned version that we would want to specify.” – <a href="https://github.com/cachix/devenv/issues/16#issuecomment-4150126963">itpropro</a></p>
</blockquote>

<p>The issue, “Pinning a specific package”, was opened on 2022-11-10 and is now closed. devenv <a href="https://devenv.sh/pinning/#pinning-an-individual-package-version">now documents</a> the multiverse as the solution. 💪</p>

<p>The audacity of the multiverse is not technical. Nix took care of that. There is no clever trick in here; it’s 5 MB of JSON, about 200 lines of Nix and a <code class="language-plaintext highlighter-rouge">builtins.fetchTree</code> behind a memo table.</p>

<p>The audacity is in the premise.</p>

<h2 id="odds-and-ends">Odds and ends</h2>

<p>Two smaller things landed that I like and which was driven by feedback from the community.</p>

<p><strong>A soak period.</strong> <code class="language-plaintext highlighter-rouge">daysBehind</code> gives you the whole of <code class="language-plaintext highlighter-rouge">nixos-unstable</code> as it stood N days before an anchor, a cooldown window, in the spirit of <a href="https://determinate.systems/blog/nixpkgs-cooldown/#reducing-the-risk-with-cooldowns">Determinate Systems’ cooldowns</a>, except the anchor can be any selector <code class="language-plaintext highlighter-rouge">at</code> takes.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">nix-repl&gt;</span><span class="w"> </span><span class="o">(</span>mv.daysBehind <span class="s2">"tip"</span> 7<span class="o">)</span>.hello.version
<span class="go">"2.12.3"
</span><span class="gp">nix-repl&gt;</span><span class="w"> </span><span class="o">(</span>mv.daysBehind <span class="s2">"tip"</span> 365<span class="o">)</span>.hello.version
<span class="go">"2.12.2"
</span></code></pre></div></div>

<p><strong>Provenance.</strong> Every package set carries where it came from, so a <code class="language-plaintext highlighter-rouge">pkgs</code> you were handed can be interrogated rather than guessed at.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">nix-repl&gt;</span><span class="w"> </span><span class="o">(</span>mv.at <span class="s2">"26.05"</span><span class="o">)</span>.multiverse
<span class="gp">{ build = 7376;</span><span class="w"> </span><span class="nb">date</span> <span class="o">=</span> <span class="s2">"2026-08-09"</span><span class="p">;</span> name <span class="o">=</span> <span class="s2">"nixos-26.05.7376.fcb8fcd6bf2d"</span><span class="p">;</span>
<span class="gp">  release = "26.05";</span><span class="w"> </span>rev <span class="o">=</span> <span class="s2">"fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d"</span><span class="p">;</span> <span class="o">}</span>
<span class="go">
</span><span class="gp">nix-repl&gt;</span><span class="w"> </span><span class="o">(</span>mv.at <span class="s2">"2022-03-15"</span><span class="o">)</span>.multiverse
<span class="gp">{ date = "2022-03-14";</span><span class="w"> </span>label <span class="o">=</span> <span class="s2">"2022-03-14-73ad5f9e147c"</span><span class="p">;</span>
<span class="gp">  rev = "73ad5f9e147c0d2a2061f1d4bd91e05078dc0b58";</span><span class="w"> </span><span class="o">}</span>
</code></pre></div></div>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:repology">
      <p>The data was fetched from the <a href="https://repology.org/repositories/graphs">Repology</a> repository size map. <a href="#fnref:repology" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:python_repology">
      <p>The data for other distributions was fetched from <a href="https://repology.org/project/python/versions">Repology</a>. <a href="#fnref:python_repology" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:glibc">
      <p>Unless they happen to dedupe due to their hash. <a href="#fnref:glibc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Every package manager on earth picks one version for you. Nixpkgs picked one too. It never had to.]]></summary></entry><entry><title type="html">nixpkgs-multiverse: every version that ever existed</title><link href="https://fzakaria.com/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed" rel="alternate" type="text/html" title="nixpkgs-multiverse: every version that ever existed" /><published>2026-08-09T15:23:00-07:00</published><updated>2026-08-09T15:23:00-07:00</updated><id>https://fzakaria.com/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed</id><content type="html" xml:base="https://fzakaria.com/2026/08/09/nixpkgs-multiverse-every-version-that-ever-existed"><![CDATA[<p><em>Enter the Nixpkgs multiverse. All the versions that ever existed, all in one place.</em></p>

<p>I bumped the <code class="language-plaintext highlighter-rouge">nixpkgs</code> release for my NixOS configuration to refresh many of my packages and found that a package I depended on at a particular version is no longer available.</p>

<p>The package was “version bumped forward” in a way that broke some of my tooling. It’s late and I don’t want to fix it, so I just add another <code class="language-plaintext highlighter-rouge">nixpkgs</code> input pinned to the commit that had the version I want. This works, but it is miserable in a way that compounds.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nv">inputs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">nixpkgs-unstable</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-unstable"</span><span class="p">;</span>
    <span class="nv">nixpkgs-25_11</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-25.11"</span><span class="p">;</span>
    <span class="nv">nixpkgs-25_05</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-25.05"</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The need for the most recent package is so common that I had keep an overlay that would inject <code class="language-plaintext highlighter-rouge">unstable</code> as a package set for me to easily pull from.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">unstable-packages</span> <span class="o">=</span> <span class="nv">final</span><span class="p">:</span> <span class="nv">_prev</span><span class="p">:</span> <span class="p">{</span>
  <span class="nv">unstable</span> <span class="o">=</span> <span class="kr">import</span> <span class="nv">inputs</span><span class="o">.</span><span class="nv">nixpkgs-unstable</span> <span class="p">{</span>
    <span class="nv">system</span> <span class="o">=</span> <span class="nv">final</span><span class="o">.</span><span class="nv">stdenv</span><span class="o">.</span><span class="nv">hostPlatform</span><span class="o">.</span><span class="nv">system</span><span class="p">;</span>
    <span class="nv">config</span><span class="o">.</span><span class="nv">allowUnfree</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">overlays</span> <span class="o">=</span> <span class="p">[</span><span class="nv">inputs</span><span class="o">.</span><span class="nv">nix-vscode-extensions</span><span class="o">.</span><span class="nv">overlays</span><span class="o">.</span><span class="nv">default</span><span class="p">];</span>
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div></div>

<p>If I have a need for a particular version of a package and it’s not present in my current <code class="language-plaintext highlighter-rouge">nixpkgs</code>, I am left searching for the commit and pinning it.<sup id="fnref:pin"><a href="#fn:pin" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>Every pin is a whole extra <code class="language-plaintext highlighter-rouge">nixpkgs</code> in the file. Flake inputs are fetched <strong>eagerly</strong> even if not used. A flake with three <code class="language-plaintext highlighter-rouge">nixpkgs</code> inputs whose output references only the <em>first</em> are all materialised.</p>

<p>Nix lets us easily create a closure that reproduces a specific version of a package, but Nixpkgs makes it hard to hold one package still while everything else moves.</p>

<p>Each Nixpkgs input to a flake is a distinct universe. If we can have multiple Nixpkgs as input to achieve fetching a particular package, why not <strong>have every version that ever existed always available</strong>? 🤯</p>

<h2 id="nixpkgs-multiverse">nixpkgs-multiverse</h2>

<p><a href="https://github.com/fzakaria/nixpkgs-multiverse">nixpkgs-multiverse</a> is one
flake input that gives you <strong>all of them</strong> at once.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix run <span class="s1">'github:fzakaria/nixpkgs-multiverse#versions.python3."3.6.2"'</span> <span class="nt">--</span> <span class="nt">--version</span>
<span class="go">Python 3.6.2

</span><span class="gp">$</span><span class="w"> </span>nix run <span class="s1">'github:fzakaria/nixpkgs-multiverse#versions.python3."3.8.9"'</span> <span class="nt">--</span> <span class="nt">--version</span>
<span class="go">Python 3.8.9

</span><span class="c"># We can also get the latest version of a package.
</span><span class="gp">$</span><span class="w"> </span>nix run <span class="s1">'github:fzakaria/nixpkgs-multiverse#latest.python3'</span> <span class="nt">--</span> <span class="nt">--version</span>
<span class="go">Python 3.14.6
</span></code></pre></div></div>

<p>We can query the flake for all the versions of a package that <strong>ever existed in Nixpkgs</strong>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--json</span> <span class="nt">--apply</span> <span class="s1">'f: f "python3"'</span> <span class="se">\</span>
<span class="go">   github:fzakaria/nixpkgs-multiverse#multiverse.x86_64-linux.versionsOf
[
  "3.5.3",
  "3.6.2",
</span><span class="c">  # 53 other versions omitted for brevity
  # ... 
</span><span class="go">  "3.13.13",
  "3.14.6"
]
</span></code></pre></div></div>

<p>If we want a specific complete revision of Nixpkgs we can use the <code class="language-plaintext highlighter-rouge">at</code> function.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span>
  <span class="nv">mv</span> <span class="o">=</span> <span class="nv">multiverse</span><span class="o">.</span><span class="nv">multiverse</span><span class="o">.</span><span class="nv">x86_64-linux</span><span class="p">;</span>
  <span class="c"># newest revision the index knows, as a real Nixpkgs</span>
  <span class="nv">pkgs_tip</span> <span class="o">=</span> <span class="nv">mv</span><span class="o">.</span><span class="nv">tip</span><span class="p">;</span>
  <span class="c"># by release</span>
  <span class="nv">pkgs_24_11</span> <span class="o">=</span> <span class="nv">mv</span><span class="o">.</span><span class="nv">at</span> <span class="s2">"24.11"</span><span class="p">;</span>
  <span class="c"># newest revision on or before that date</span>
  <span class="nv">pkgs_2022_03_15</span> <span class="o">=</span> <span class="nv">mv</span><span class="o">.</span><span class="nv">at</span> <span class="s2">"2022-03-15"</span><span class="p">;</span>
  <span class="c"># by commit</span>
  <span class="nv">pkgs_aae12a743f75</span> <span class="o">=</span> <span class="nv">mv</span><span class="o">.</span><span class="nv">at</span> <span class="s2">"aae12a743f75"</span><span class="p">;</span>
<span class="kn">in</span> <span class="p">{</span>
  <span class="nv">packages</span> <span class="o">=</span> <span class="p">[</span>
      <span class="nv">pkgs_tip</span><span class="o">.</span><span class="nv">python3</span>
      <span class="nv">pkgs_24_11</span><span class="o">.</span><span class="nv">python3</span>
      <span class="nv">pkgs_2022_03_15</span><span class="o">.</span><span class="nv">python3</span>
      <span class="nv">pkgs_aae12a743f75</span><span class="o">.</span><span class="nv">python3</span>
    <span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That is access to all the versions of all the packages that ever existed in Nixpkgs. You can mix them all together in one shell, one package or a build environment.</p>

<p><img src="/assets/images/lotr_multiverse.png" alt="lotr meme about having one flake to rule them all" /></p>

<p>How is it possible to have multiple Python versions? That is the whole point of Nix itself. Every package immaculately describes its dependencies using a hash via the <a href="/2025/03/08/demystifying-nix-s-intensional-model">intensional model</a>.<sup id="fnref:intensional"><a href="#fn:intensional" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<div class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">multiverse</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">TB</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="o">]</span><span class="p">;</span>

  <span class="nv">env</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"buildEnv\n\"three-pythons\""</span><span class="p">,</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="s2">"rounded,bold"</span><span class="o">]</span><span class="p">;</span>

  <span class="k">subgraph</span> <span class="nv">cluster_a</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"revision 2023-06-12"</span><span class="p">;</span>
    <span class="n">style</span><span class="p">=</span><span class="nv">dashed</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="nv">pa</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"python3-3.10.11"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">ba</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"bash-5.2-p15"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">ga</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"glibc-2.37-8"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">oa</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"openssl-3.0.9"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">pa</span> <span class="o">-&gt;</span> <span class="nv">ba</span><span class="p">;</span> <span class="nv">pa</span> <span class="o">-&gt;</span> <span class="nv">ga</span><span class="p">;</span> <span class="nv">pa</span> <span class="o">-&gt;</span> <span class="nv">oa</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">subgraph</span> <span class="nv">cluster_b</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"revision 24.05"</span><span class="p">;</span>
    <span class="n">style</span><span class="p">=</span><span class="nv">dashed</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="nv">pb</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"python3-3.11.9"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">bb</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"bash-5.2p26"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">gb</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"glibc-2.39-52"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">ob</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"openssl-3.0.13"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">pb</span> <span class="o">-&gt;</span> <span class="nv">bb</span><span class="p">;</span> <span class="nv">pb</span> <span class="o">-&gt;</span> <span class="nv">gb</span><span class="p">;</span> <span class="nv">pb</span> <span class="o">-&gt;</span> <span class="nv">ob</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">subgraph</span> <span class="nv">cluster_c</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"revision 25.05"</span><span class="p">;</span>
    <span class="n">style</span><span class="p">=</span><span class="nv">dashed</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="nv">pc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"python3-3.12.10"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">bc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"bash-5.2p37"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">gc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"glibc-2.40-66"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">oc</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"openssl-3.4.1"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">pc</span> <span class="o">-&gt;</span> <span class="nv">bc</span><span class="p">;</span> <span class="nv">pc</span> <span class="o">-&gt;</span> <span class="nv">gc</span><span class="p">;</span> <span class="nv">pc</span> <span class="o">-&gt;</span> <span class="nv">oc</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nv">env</span> <span class="o">-&gt;</span> <span class="nv">pa</span><span class="p">;</span> <span class="nv">env</span> <span class="o">-&gt;</span> <span class="nv">pb</span><span class="p">;</span> <span class="nv">env</span> <span class="o">-&gt;</span> <span class="nv">pc</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nixpkgs already supports multiple versions of a package in a single revision (i.e. <code class="language-plaintext highlighter-rouge">python39</code>, <code class="language-plaintext highlighter-rouge">python312</code>, <code class="language-plaintext highlighter-rouge">gcc12</code>) as separate attributes. We took this to its logical conclusion of making them all available easily.</p>

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

<p>Our <code class="language-plaintext highlighter-rouge">flake.nix</code> deliberately has no inputs: <code class="language-plaintext highlighter-rouge">inputs = { }</code>. Inputs are fetched eagerly, and we have 1,393 of them. We need to fetch them lazily, only when something actually references a revision. To do this, we fetch revisions with <code class="language-plaintext highlighter-rouge">builtins.fetchTree</code>, pinned by <code class="language-plaintext highlighter-rouge">narHash</code>, only when needed.</p>

<p>Two files do all the work: <code class="language-plaintext highlighter-rouge">revisions.json</code> and <code class="language-plaintext highlighter-rouge">versions.json</code>.</p>

<p><code class="language-plaintext highlighter-rouge">revisions.json</code> is one ordered array of every revision from <a href="https://github.com/NixOS/nixpkgs">Nixpkgs</a>, 1,393 as of this writing, from 2017 to 2026.<sup id="fnref:revisions"><a href="#fn:revisions" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"rev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0eeebd64de89…"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-06-12"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nixos-unstable"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"narHash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256-2xT+Jmk3m…"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"rev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"afb2b21ba489…"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-05-23"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"channel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"release"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"release"</span><span class="p">:</span><span class="w"> </span><span class="s2">"25.05"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"narHash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256-rWtXrcIzU5wm…"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>We limit our commits to those that were actually built and cached by Hydra, so we only include commits that were either a release or a <code class="language-plaintext highlighter-rouge">nixos-unstable</code> channel bump.</p>

<p>How do we know which revisions to pick for the <code class="language-plaintext highlighter-rouge">nixos-unstable</code> ones?</p>

<p>We rely on the <a href="https://nix-releases.s3.amazonaws.com/">nix-releases S3 bucket</a> to tell us which commits actually became published builds. The S3 bucket uses the commit hash as the directory name, so we can list the bucket and get a complete list of all revisions that were actually built.</p>

<p><code class="language-plaintext highlighter-rouge">index/versions.json</code> is the map from (attribute, version) to a revision:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"revisionCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">1393</span><span class="p">,</span><span class="w">
  </span><span class="nl">"attrs"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"python3"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"3.8.9"</span><span class="p">:</span><span class="w"> </span><span class="mi">412</span><span class="p">,</span><span class="w"> </span><span class="nl">"3.12.10"</span><span class="p">:</span><span class="w"> </span><span class="mi">1204</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>That integer is an offset into <code class="language-plaintext highlighter-rouge">revisions.json</code>. It is the most recent revision that shipped that version.</p>

<h2 id="sparse-data">Sparse data</h2>

<p>At this many revisions, it turns out that how you encode the data matters a lot.
My first encoding stored every revision a version appeared in.</p>

<p>Although it was simple, it was a disaster in terms of size for these JSON files.
As you might expect, most versions of most packages are unchanged across many revisions.
The size of our <code class="language-plaintext highlighter-rouge">versions.json</code> file was growing linearly with the number of revisions.</p>

<p>By storing only the <em>newest</em> revision that shipped a version, we can keep the file small and still answer the question “which revision had this version”. Here is how it actually grows as revisions get indexed:</p>

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

# Measured while indexing all 1,393 revisions oldest-first, newest-only encoding.
df = pd.DataFrame({
    "revisions": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100,
                  1200, 1300, 1393],
    "megabytes": [0.50, 0.67, 0.84, 1.14, 1.37, 1.63, 1.86, 2.06, 2.32, 2.64,
                  3.17, 3.75, 4.41, 5.18],
})

plot = (
    ggplot(df, aes("revisions", "megabytes"))
    + geom_area(fill="#4c72b0", alpha=0.2)
    + geom_line(color="#4c72b0", size=0.9)
    + geom_point(color="#4c72b0", size=1.6)
    + labs(x="revisions indexed", y="index/versions.json (MB)")
    + scale_x_continuous(labels=lambda xs: [f"{int(x):,}" for x in xs])
)
plot.width, plot.height = 7.0, 3.2
</code></pre>

<p>5.18 MB covering <strong>1,393 revisions</strong> and 289,521 distinct (attribute, version)
pairs.</p>

<h2 id="performance">Performance</h2>

<p>The key design rule for our flake:</p>

<blockquote>
  <p>Cost is per <strong>revision touched</strong>, not per package.</p>
</blockquote>

<p>If we were to add revisions as inputs, evaluating our flake would explode. Each flake in our measurement below has N <code class="language-plaintext highlighter-rouge">nixpkgs</code> inputs and an output that references <strong>only the first one</strong>; the timing is how long before that output evaluates.<sup id="fnref:perf"><a href="#fn:perf" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

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

# Each N uses a different set of revisions so nothing is warm from the last run.
eager = pd.DataFrame({
    "pins": [1, 2, 3, 4, 5],
    "seconds": [6.2, 12.3, 18.8, 23.2, 26.2],
    "approach": "flake inputs (eager)",
})
# The multiverse knows about 1,393 revisions; touching none costs one JSON parse.
lazy = pd.DataFrame({
    "pins": [1, 2, 3, 4, 5],
    "seconds": [0.20, 0.20, 0.20, 0.20, 0.20],
    "approach": "multiverse (lazy)",
})
df = pd.concat([eager, lazy])

plot = (
    ggplot(df, aes("pins", "seconds", color="approach"))
    + geom_line(size=0.9)
    + geom_point(size=1.9)
    + scale_color_manual(values={"flake inputs (eager)": "#b1201d",
                                 "multiverse (lazy)": "#55a868"})
    + labs(x="nixpkgs revisions you have pinned",
           y="seconds before your output evaluates")
)
plot.width, plot.height = 7.0, 3.4
</code></pre>

<p>Five <code class="language-plaintext highlighter-rouge">nixpkgs</code> pins that are not used cost <strong>26 seconds</strong> before the output evaluates.
Each input costs about 5 seconds, and the input is fetched and materialised even if never used. In contrast, the green line is <code class="language-plaintext highlighter-rouge">nixpkgs-multiverse</code> with <strong>1,393 revisions available</strong>, which is a flat 0.20s to parse the JSON. 🤩</p>

<p>Revisions are memoised, so pulling 3 packages out of one revision costs the
same as pulling one.</p>

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

df = pd.DataFrame({
    "what": ["index only\n(0 revisions)", "1 revision", "3 revisions",
             "5 revisions", "3 packages,\nsame revision"],
    "seconds": [0.30, 0.37, 0.62, 1.66, 0.29],
    "kind": ["index", "fetch", "fetch", "fetch", "memoised"],
})
df["what"] = pd.Categorical(df["what"], categories=df["what"], ordered=True)

plot = (
    ggplot(df, aes("what", "seconds", fill="kind"))
    + geom_col(width=0.6)
    + scale_fill_manual(values={"index": "#8c8c8c", "fetch": "#4c72b0",
                                "memoised": "#55a868"})
    + labs(x="", y="cpu seconds")
)
plot.width, plot.height = 7.0, 3.4
</code></pre>

<h2 id="why-i-like-this">Why I like this</h2>

<p>That concept that the <code class="language-plaintext highlighter-rouge">/nix/store</code> can hold many graphs of the same package is core to understanding Nix. The popularity and rise of flakes made it even more apparent that we can mix multiple revisions of Nixpkgs together.</p>

<p>The thing I keep coming back to is that Nixpkgs history <em>already is</em> the
multiverse. Every version that ever existed is already built, already cached,
already reachable. It was just addressed by commit hash instead of by version
number, which is exactly backwards from how anyone thinks about it.</p>

<p>The whole project is 5 MB of JSON and about 200 lines of Nix. It does not
build anything, mirror anything, or host anything. It is a phone book.</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">multiverse</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:fzakaria/nixpkgs-multiverse"</span><span class="p">;</span>
</code></pre></div></div>

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

# Every revision from the first nixos-unstable bump onward, delta-encoded in
# days from it (2017-11-29) so the whole set fits in the fence that draws it.
# The releases older than that are left out; the only python3 versions this
# drops are 3.5.3 in 17.03 and 3.6.2 in 17.09.
GAPS = (
    "0 1 0 1 1 1 1 9 6 7 6 7 1 1 7 3 1 1 1 5 2 0 1 8 4 1 3 1 2 3 8 1 1 1 "
    "19 0 11 3 1 2 18 0 2 5 3 10 0 1 0 1 1 1 2 1 17 11 6 2 0 1 5 2 0 2 20 "
    "2 10 0 1 0 1 4 7 2 2 8 0 8 1 1 3 3 0 6 0 4 2 1 1 7 4 8 11 3 10 16 3 "
    "1 0 1 2 1 1 0 1 0 0 1 13 3 4 10 0 2 4 1 9 6 1 2 1 0 1 0 1 1 6 2 1 1 "
    "1 3 1 3 1 1 3 10 2 0 1 3 12 1 4 4 5 7 1 4 3 2 1 1 1 2 1 5 2 1 0 0 1 "
    "0 3 3 1 2 2 1 0 1 1 4 1 2 4 0 2 3 0 5 8 0 4 1 1 4 1 10 0 1 1 4 0 0 3 "
    "1 2 0 1 1 1 0 2 0 1 0 1 0 0 2 0 7 2 1 0 1 0 2 0 0 4 3 1 7 10 1 0 1 2 "
    "2 13 1 0 1 1 1 1 7 3 8 3 12 3 1 7 1 2 2 0 4 11 1 1 3 1 1 1 2 8 2 1 1 "
    "2 1 2 1 3 0 1 1 1 2 1 1 1 1 1 1 3 1 9 10 2 1 2 1 1 1 1 1 1 2 1 1 0 8 "
    "3 2 1 7 1 10 0 4 2 1 3 4 0 1 4 2 13 3 0 2 2 2 8 2 4 4 8 1 2 1 2 2 8 "
    "1 2 1 6 1 7 1 7 4 2 4 3 1 1 6 1 2 1 18 0 4 4 3 1 3 2 2 11 4 5 2 2 0 "
    "1 6 0 6 5 2 3 0 3 3 1 2 3 4 1 1 3 4 2 14 2 1 1 2 4 2 1 2 2 6 2 4 4 6 "
    "1 1 1 2 3 1 1 3 3 1 2 3 2 4 4 2 3 3 3 1 3 4 3 4 6 2 1 2 2 1 2 2 1 1 "
    "2 1 2 0 2 6 1 1 1 2 0 4 1 1 1 1 1 1 3 1 2 1 3 1 1 2 1 3 2 4 3 5 3 1 "
    "1 2 4 2 1 1 0 3 0 2 2 1 1 2 1 1 0 1 1 3 3 3 5 2 1 2 0 2 2 1 1 1 2 1 "
    "1 1 4 1 2 2 2 1 3 2 1 1 1 1 1 1 1 1 2 1 1 2 4 1 3 2 3 2 1 2 1 1 1 2 "
    "3 3 1 3 6 1 3 1 1 4 3 2 2 2 2 5 2 1 3 1 0 1 1 2 4 1 3 1 1 1 2 2 2 1 "
    "0 2 1 1 1 4 1 3 4 2 1 3 2 5 2 1 1 1 3 2 3 1 1 3 0 1 1 1 2 8 1 8 1 3 "
    "5 2 2 6 1 2 4 3 4 3 2 1 2 1 0 3 1 1 6 1 1 2 6 2 4 1 1 1 2 0 1 2 1 2 "
    "4 1 1 3 1 0 1 3 4 1 3 1 1 1 1 1 2 1 2 1 1 2 1 1 2 3 1 1 1 1 2 1 1 1 "
    "1 2 3 0 1 1 1 2 1 1 1 1 1 1 1 0 1 1 3 0 2 1 1 1 1 1 2 1 1 1 1 2 1 1 "
    "1 2 1 0 1 1 1 1 3 1 8 1 1 1 1 1 1 1 2 3 1 2 1 1 1 1 2 2 1 1 1 1 1 1 "
    "1 2 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 2 0 2 1 1 1 3 1 1 1 1 "
    "1 2 1 0 1 1 1 1 2 3 0 2 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 4 3 1 2 1 2 2 "
    "1 2 3 0 2 1 1 1 2 4 1 3 2 5 1 1 1 1 1 2 1 2 2 1 1 1 1 1 1 1 2 1 1 3 "
    "1 2 1 3 2 1 1 1 1 1 1 2 1 2 1 1 2 3 0 2 0 1 2 2 1 2 2 1 2 1 1 1 1 2 "
    "1 1 1 1 1 3 1 1 1 1 2 1 1 2 2 1 1 8 1 1 1 0 2 0 1 1 1 1 1 3 1 1 2 0 "
    "1 3 0 1 2 1 1 1 1 1 2 2 1 5 1 2 1 1 2 1 1 1 2 1 1 3 1 1 1 2 1 0 1 2 "
    "2 1 1 2 1 1 2 1 2 1 1 3 1 1 1 1 1 3 1 1 1 1 1 1 2 1 2 2 1 0 1 2 1 1 "
    "1 2 2 2 3 3 1 2 2 3 3 2 2 2 3 2 3 2 5 3 5 2 3 4 2 8 2 3 2 2 2 3 3 0 "
    "3 2 5 2 6 2 3 2 3 3 3 4 2 5 2 2 2 2 4 2 2 2 2 3 2 2 2 2 1 2 2 2 2 2 "
    "2 2 1 3 3 3 3 2 3 1 3 2 2 2 2 5 0 3 2 3 2 3 0 3 2 3 1 2 5 1 2 2 2 2 "
    "1 3 2 3 1 0 3 3 2 2 1 2 2 2 3 2 2 2 2 2 2 2 3 3 1 2 5 1 3 2 2 3 2 2 "
    "2 2 1 3 2 3 2 2 2 3 4 3 3 4 3 4 2 4 1 2 3 2 1 2 2 3 3 2 3 2 3 5 2 2 "
    "2 3 2 4 4 3 4 2 3 1 4 6 3 1 1 3 2 2 2 2 2 4 3 7 2 4 2 4 2 2 1 1 2 5 "
    "2 1 2 1 2 3 2 1 2 1 1 2 3 2 1 2 3 3 2 1 2 2 2 1 1 2 4 1 1 3 1 3 3 2 "
    "3 1 2 3 1 2 1 3 6 3 2 1 4 1 2 1 1 1 1 2 0 3 3 2 3 2 3 0 2 2 2 2 3 2 "
    "2 6 4 2 2 3 0 3 3 4 2 2 6 2 3 4 2 1 2 3 2 1 3 3 3 2 5 6 1 2 2 3 3 3 "
    "2 3 3 2 1 2 3 4 4 4 1 2 3 3 4 3 3 3 3 2 3 3 3 1 4 1 3 2 2 3 3 0 2 3 "
    "3 3 4 3 3 4 3 2 3 3 2 1 3 2 3 4 1 2 3 4 3 1 1 0 4 3 2 4 6 4 3 1 1 2 "
    "2 2 2 1 1 1 1 1 1 3 3 4 4 4 4 5 4 4 5 3 5 5 5 6 2 0 7 1 6 4 6 10 3 3 "
    "3 3 3 3 1 3 1 "
)

# Every python3 version in the index, at the newest revision that shipped it:
# the revision `versions.python3."&lt;v&gt;"` resolves to.
PYTHONS = (
    "2017-12-19:3.6.3 2018-04-10:3.6.4 2018-07-10:3.6.5 2018-11-17:3.6.6 "
    "2018-12-14:3.7.1 2019-04-04:3.7.2 2019-07-12:3.7.3 2019-10-20:3.7.4 "
    "2019-12-26:3.7.5 2020-04-20:3.7.6 2020-06-14:3.7.7 2020-08-01:3.8.3 "
    "2020-10-27:3.8.5 2021-01-17:3.8.6 2021-02-24:3.8.7 2021-04-24:3.8.8 "
    "2021-07-18:3.8.9 2021-07-27:3.9.5 2021-12-25:3.9.6 2022-02-09:3.9.9 "
    "2022-04-03:3.9.10 2022-04-17:3.9.11 2022-05-31:3.9.12 "
    "2022-06-18:3.9.13 2022-06-26:3.10.4 2022-08-15:3.10.5 "
    "2022-09-28:3.10.6 2022-10-31:3.10.7 2022-12-16:3.10.8 "
    "2023-02-25:3.10.9 2023-04-25:3.10.10 2023-06-17:3.10.11 "
    "2023-10-19:3.10.12 2023-11-14:3.11.5 2024-01-08:3.11.6 "
    "2024-02-22:3.11.7 2024-04-16:3.11.8 2024-07-03:3.11.9 "
    "2024-08-28:3.12.4 2024-10-09:3.12.5 2024-10-29:3.12.6 "
    "2024-12-20:3.12.7 2025-02-18:3.12.8 2025-05-04:3.12.9 "
    "2025-06-17:3.12.10 2025-07-08:3.13.4 2025-08-19:3.13.5 "
    "2025-09-08:3.13.6 2025-10-19:3.13.7 2025-11-11:3.13.8 "
    "2025-12-28:3.13.9 2026-02-17:3.13.11 2026-05-15:3.13.12 "
    "2026-07-05:3.13.13 2026-07-19:3.14.6 "
)

REVS = "1,391 revisions"
PYS = "55 python3 versions"

dates = pd.to_datetime("2017-11-29") + pd.to_timedelta(
    list(itertools.accumulate(int(g) for g in GAPS.split())), unit="D"
)

pythons = pd.DataFrame(
    [p.split(":") for p in PYTHONS.split()], columns=["date", "version"]
)
pythons["date"] = pd.to_datetime(pythons["date"])
pythons = pythons.sort_values("date").reset_index(drop=True)
pythons["minor"] = pythons["version"].str.rsplit(".", n=1).str[0]

# Every tick the same height: the height is not carrying data, and staggering
# it to make room for 55 labels only turns the chart into a staircase. The
# patch releases are the marks; the series is what gets named.
TICK = 0.55
pythons["y0"], pythons["y1"], pythons["kind"] = 0.0, TICK, PYS

# One label per series, at the oldest version in it. Series starts are months
# apart, so these never crowd each other.
series = pythons.groupby("minor", as_index=False).first()

# The ribbon of every revision, centred inside the ticks it is being read
# against.
revisions = pd.DataFrame(
    {"date": dates, "y0": TICK * 0.3, "y1": TICK * 0.7, "kind": REVS}
)

plot = (
    ggplot(mapping=aes(x="date", xend="date", y="y0", yend="y1", color="kind"))
    + geom_segment(pythons, size=0.9)
    + geom_segment(revisions, size=0.3, alpha=0.45)
    + geom_text(series, aes(x="date", y=TICK + 0.06, label="minor"), size=8,
                color="#55a868", ha="left", va="bottom", inherit_aes=False)
    + scale_color_manual(values={REVS: "#4c72b0", PYS: "#55a868"},
                         breaks=[PYS, REVS], name="")
    + scale_y_continuous(limits=(0, TICK + 0.28), expand=(0, 0))
    + guides(color=guide_legend(override_aes={"size": 1.4, "alpha": 1}))
    + labs(x="", y="")
    + theme(axis_text_y=element_blank(), axis_ticks_major_y=element_blank(),
            panel_grid_major_y=element_blank(),
            panel_grid_minor_y=element_blank(),
            legend_position="bottom", legend_direction="horizontal",
            legend_title=element_blank())
)
plot.width, plot.height = 7.0, 2.8
</code></pre>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pin">
      <p>Thankfully sites like <a href="https://www.nixhub.io/">nixhub.io</a> or <a href="https://lazamar.co.uk/nix-versions/">lazamar’s search</a> make this a little easier. <a href="#fnref:pin" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:intensional">
      <p>The hash is a unique identifier for the exact set of inputs that were used to build it. If you change any input, the hash changes and you get a new package. <a href="#fnref:intensional" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:revisions">
      <p>A NixOS release is not special; it is a commit that happens to carry a <code class="language-plaintext highlighter-rouge">release</code> label. <a href="#fnref:revisions" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:perf">
      <p>Everything is <code class="language-plaintext highlighter-rouge">git+file</code> against a local clone, so there is no network latency. <a href="#fnref:perf" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Enter the Nixpkgs multiverse. All the versions that ever existed, all in one place.]]></summary></entry><entry><title type="html">Super Mario Derivations</title><link href="https://fzakaria.com/2026/08/05/super-mario-derivations" rel="alternate" type="text/html" title="Super Mario Derivations" /><published>2026-08-05T21:20:00-07:00</published><updated>2026-08-05T21:20:00-07:00</updated><id>https://fzakaria.com/2026/08/05/super-mario-derivations</id><content type="html" xml:base="https://fzakaria.com/2026/08/05/super-mario-derivations"><![CDATA[<p>One of the most surprising aspects of the Nix language is that it is <em>lazy</em>, especially if you have never used a lazy language before. This laziness is what makes much of <a href="https://github.com/NixOS/nixpkgs">Nixpkgs</a> possible, and its complexity.</p>

<p>One of the simplest ways to observe the laziness is by understanding that only the attributes you access are evaluated.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--expr</span> <span class="s1">'let pkgs = 
</span><span class="gp">   { hello = "hi";</span><span class="w"> </span><span class="s1">broken = throw "never forced"; }; 
</span><span class="go">   in pkgs.hello'
"hi"
</span></code></pre></div></div>

<p>The more whackier version of this is you can have <em>endless</em> recursion in an attribute set. Nixpkgs is filled with these bottomless attribute sets:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">-f</span> <span class="s1">'&lt;nixpkgs&gt;'</span> <span class="s1">'pkgs.hello'</span> <span class="nt">--raw</span>
<span class="go">/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2

</span><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">-f</span> <span class="s1">'&lt;nixpkgs&gt;'</span> <span class="s1">'pkgs.pkgs.pkgs.hello'</span> <span class="nt">--raw</span>
<span class="go">/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2

</span><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">-f</span> <span class="s1">'&lt;nixpkgs&gt;'</span> <span class="s1">'pkgs.python3Packages.pkgs.hello'</span> <span class="nt">--raw</span>
<span class="go">/nix/store/18bbdvag5v2f3d4y37pdbkzvh7s71cw4-hello-2.12.2
</span></code></pre></div></div>

<p>The same store path every time. <code class="language-plaintext highlighter-rouge">pkgs</code> contains itself, and so does every package set inside it. 🤯</p>

<p>If laziness is what lets a recursive attribute set terminate, then the recursion doesn’t have to bottom out <strong>at all</strong>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--expr</span> <span class="se">\</span>
<span class="gp">    'let countdown = n: { value = n;</span><span class="w"> </span>next <span class="o">=</span> countdown <span class="o">(</span>n + 1<span class="o">)</span><span class="p">;</span> <span class="o">}</span><span class="p">;</span>
<span class="go">     in (countdown 0).next.next.next.value'
3
</span></code></pre></div></div>

<p>That attribute set is infinitely deep. Indexing three levels into it costs exactly three levels of evaluation, and the rest of the infinite tree is never built because nobody asked.</p>

<p>So an attribute path is a walk through a lazily-generated tree. Which made me wonder: what if the attribute path were <em>input to something</em>? 🤔</p>

<p>I decided to take that idea and make the attribute path a sequence of button presses in <a href="https://en.wikipedia.org/wiki/Super_Mario_Bros._3">Super Mario Bros. 3</a>. Each node in the tree is a frame of the game, and each child is a button press that produces a new frame. Game states are recursive by nature.</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="s1">'.#level1.rightb.rightb.rightab.rightb'</span>
<span class="gp">$</span><span class="w"> </span>file <span class="nt">-L</span> result
<span class="go">result: PNG image data, 256 x 240, 8-bit/color RGB, non-interlaced
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">.rightb</code> is right + B, which in Super Mario Bros. 3 is “run right”. <code class="language-plaintext highlighter-rouge">.rightab</code> is run and jump. The output is the frame you’d be looking at if you’d pressed those buttons in that order, on real hardware, in that game.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>Append <code class="language-plaintext highlighter-rouge">.play</code> anywhere along the path and you get the whole run stitched into a recording:</p>

<p><img src="/assets/images/nes-nix-mario.gif" alt="Super Mario Bros. 3 running in an emulator: the title screen, the 1/2-player menu, the World 1 map, then Mario running right and jumping in level 1-1" /></p>

<p>The coolest thing though is that every one of those frames <strong>is a separate derivation in my store</strong>.</p>

<p>The code is at <a href="https://github.com/fzakaria/nes-nix">fzakaria/nes-nix</a>. It is generalized and the ROM is a flake input you point wherever you like for any other game.</p>

<p>The flake computes a derivation based on the attribute path such that each press is its own derivation, and it takes <strong>the previous press’s savestate as an input</strong>. Each derivation <em>never</em> re-emulates its ancestors’ frames.<sup id="fnref:image"><a href="#fn:image" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></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="o">]</span>

  <span class="nv">trunk1</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"level1\n2y1qjbk7…-nes-wait16"</span><span class="o">]</span>
  <span class="nv">trunk2</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">".rightb\ngdbgfpdk…-nes-rightb"</span><span class="o">]</span>

  <span class="nv">run</span>    <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">".rightb\nkpjlw529…-nes-rightb"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="nv">a</span>      <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">".a\nq02kp71k…-nes-a"</span>           <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="nv">righta</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">".righta\nnb87m9ss…-nes-righta"</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="nv">jump</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">".rightab\niv6asl0i…-nes-rightab"</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">trunk1</span> <span class="o">-&gt;</span> <span class="nv">trunk2</span>
  <span class="nv">trunk2</span> <span class="o">-&gt;</span> <span class="nv">run</span>    <span class="o">[</span><span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="nv">trunk2</span> <span class="o">-&gt;</span> <span class="nv">jump</span>   <span class="o">[</span><span class="n">color</span><span class="p">=</span><span class="s2">"#e08a45"</span><span class="o">]</span>
  <span class="nv">run</span>    <span class="o">-&gt;</span> <span class="nv">a</span>      <span class="o">[</span><span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="nv">run</span>    <span class="o">-&gt;</span> <span class="nv">righta</span> <span class="o">[</span><span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The practical consequence is that the store becomes the emulator’s savestate history:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 3 derivations, cold
</span><span class="gp">$</span><span class="w"> </span>nix build <span class="s1">'.#game.start4.wait2.right'</span>
<span class="c"># 1 derivation, prefix reused
</span><span class="gp">$</span><span class="w"> </span>nix build <span class="s1">'.#game.start4.wait2.left'</span>
<span class="c"># 1 derivation, all of it reused
</span><span class="gp">$</span><span class="w"> </span>nix build <span class="s1">'.#game.start4.wait2.right.right'</span>
</code></pre></div></div>

<p>Branching off the middle of a hundred-press run costs one press as does appending to the end of it.</p>

<p>We can look at it the other way. The dependency graph <em>is</em> the input sequence, so we can ask Nix what buttons produced a frame:</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">--query</span> <span class="nt">--tree</span> 
<span class="gp">     $</span><span class="o">(</span>nix <span class="nb">eval</span> <span class="nt">--raw</span> <span class="s1">'.#game.start.wait4.start.drvPath'</span><span class="o">)</span>
<span class="go">/nix/store/32n4ni0zg01b9c9v64x67am37rdmmr9y-nes-start.drv
└───/nix/store/j5vy3385pgs9dzw0y7sdrdmn7xnrxgji-nes-wait4.drv
    └───/nix/store/w4zz5aqj5zxqhnialabdc7p3sy80v6dc-nes-start.drv
        └───/nix/store/k9wfz8w5157d0xdwaw1vvhf019dvw5s0-nes-boot.drv
</span></code></pre></div></div>

<p>So what is <code class="language-plaintext highlighter-rouge">.play</code> actually doing?</p>

<p>Almost nothing. Every frame along the path is already sitting in the store as the output of its own press, so the recording never emulates anything. It is a directory of symlinks to the frames for <code class="language-plaintext highlighter-rouge">ffmpeg</code> to process.</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="s1">'.#level1.rightb.rightb.rightab.play'</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">ls</span> <span class="nt">-l</span> result/frames | <span class="nb">head</span> <span class="nt">-4</span>
<span class="gp">0000.png -&gt;</span><span class="w"> </span>/nix/store/3p2fxwngh…-nes-boot
<span class="gp">0001.png -&gt;</span><span class="w"> </span>/nix/store/4ha88l0dk…-nes-start
<span class="gp">0002.png -&gt;</span><span class="w"> </span>/nix/store/nh4zfsq6x…-nes-wait4
<span class="gp">0003.png -&gt;</span><span class="w"> </span>/nix/store/ghbgn28f1…-nes-start
</code></pre></div></div>

<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.14,0.08"</span><span class="o">]</span>

  <span class="k">subgraph</span> <span class="nv">cluster_play</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"result/frames : the play derivation"</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">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">f0</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"0000.png"</span><span class="o">]</span>
    <span class="nv">f1</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"0001.png"</span><span class="o">]</span>
    <span class="nv">f2</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"0002.png"</span><span class="o">]</span>
    <span class="nv">f3</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"0003.png"</span><span class="o">]</span>
  <span class="p">}</span>

  <span class="k">subgraph</span> <span class="nv">cluster_store</span> <span class="p">{</span>
    <span class="n">label</span><span class="p">=</span><span class="s2">"/nix/store : one derivation per press"</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">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">p0</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"3p2fxwngh…-nes-boot"</span>   <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
    <span class="nv">p1</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"4ha88l0dk…-nes-start"</span>  <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
    <span class="nv">p2</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"nh4zfsq6x…-nes-wait4"</span>  <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
    <span class="nv">p3</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"ghbgn28f1…-nes-start"</span>  <span class="n">color</span><span class="p">=</span><span class="s2">"#b1201d"</span> <span class="n">fontcolor</span><span class="p">=</span><span class="s2">"#b1201d"</span><span class="o">]</span>
  <span class="p">}</span>

  <span class="k">edge</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">"#6f685b"</span> <span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.7</span><span class="o">]</span>
  <span class="nv">f0</span> <span class="o">-&gt;</span> <span class="nv">p0</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"symlink"</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="nv">f1</span> <span class="o">-&gt;</span> <span class="nv">p1</span>
  <span class="nv">f2</span> <span class="o">-&gt;</span> <span class="nv">p2</span>
  <span class="nv">f3</span> <span class="o">-&gt;</span> <span class="nv">p3</span>
<span class="p">}</span>
</code></pre></div></div>

<p>How far can we take this input-sequence game input idea?</p>

<p>Nix <strong>by default</strong> gives out at around 2,400 presses, with:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--raw</span> <span class="s2">".#game.right.right.right…drvPath"</span>
<span class="gp">error: stack overflow;</span><span class="w"> </span>max-call-depth exceeded
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">max-call-depth</code> defaults to 10,000 and evaluating each press costs roughly four nested calls.</p>

<p>It’s a guard against runaway recursion, not a structural limit, and we can raise it to 10 million and get 20,000 presses:</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">ulimit</span> <span class="nt">-s</span> unlimited
<span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--raw</span> <span class="nt">--option</span> max-call-depth 10000000 <span class="se">\</span>
<span class="gp">      ".#</span>game.<span class="si">$(</span>
<span class="go">        python3 -c 'print(".".join(["right"]*20000))')
      .drvPath"
/nix/store/p4nm0a4p4k9bdjqsag1jj0baah9mj6hb-nes-right.drv
</span></code></pre></div></div>

<p>20,000 presses, takes roughly fourteen seconds to evaluate on my laptop. The cost is linear in the number of presses, and it is roughly 0.7ms “per press”.</p>

<pre title="nix eval time against attribute path length: roughly linear from under a second at 100 presses to about 14 seconds at 20,000, with the default max-call-depth wall at 2,400 and the kernel argv limit at 21,845"><code class="language-plotnine">import pandas as pd
from plotnine import *

# `nix eval --raw .#game.&lt;n presses&gt;.drvPath`, warm store, one run each.
df = pd.DataFrame({
    "presses": [100, 250, 500, 1000, 2000, 4000, 8000, 12000, 16000, 20000],
    "seconds": [0.82, 1.32, 1.45, 1.72, 2.35, 3.73, 6.73, 7.90, 9.75, 14.39],
})

plot = (
    ggplot(df, aes("presses", "seconds"))
    + geom_vline(xintercept=2400, linetype="dotted", color=INK, alpha=0.7)
    + geom_vline(xintercept=21845, linetype="dashed", color="#e08a45")
    + annotate("text", x=3000, y=13.2, label="default max-call-depth\nstops you here (~2,400)",
               ha="left", size=7, color=INK)
    + annotate("text", x=21000, y=5.5, label="kernel argv limit\n21,845 presses",
               ha="right", size=7, color="#e08a45")
    + geom_line(color="#b1201d", size=0.9)
    + geom_point(color="#b1201d", size=1.9)
    + labs(x="presses in the attribute path", y="nix eval (seconds)")
    + scale_x_continuous(labels=lambda xs: [f"{int(x):,}" for x in xs])
)
plot.width, plot.height = 7.0, 3.4
</code></pre>

<p>The next bottleneck though is that the kernel gives out at 21,845 presses on my machine. An attribute path is a single <code class="language-plaintext highlighter-rouge">argv</code> element, and Linux caps the size of the argument list in total and individual arguments.</p>

<p>The per-argument limit is 131,072 bytes (<code class="language-plaintext highlighter-rouge">MAX_ARG_STRLEN</code>), and each press is six bytes long (<code class="language-plaintext highlighter-rouge">right.</code>), so 21,845 presses is the maximum that can be passed to <code class="language-plaintext highlighter-rouge">nix eval</code> as a single argument.</p>

<p>The escape hatch is to stop passing the run as an argument. and we can feed in the input-sequence as from a file:</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">--impure</span> <span class="nt">--expr</span> <span class="se">\</span>
<span class="go">    '(builtins.getFlake (toString ./.))
      .packages.x86_64-linux.game.sequenceFile 
        ./runs/world1-1.txt'
</span></code></pre></div></div>

<p>This produces the byte-identical derivation to the equivalent attribute path, so a run kept in a file still shares the same store paths.</p>

<p>All of this was to simply <em>evaluate</em> the Nix expression. Now we have to build it. Although Nix is great at building derivations in parallel, the recursion here is tail-recursive and therefore serial.</p>

<p>I benchmarked the build time of a growing list of button presses and the cost is also linear, as we would expect, with the number of presses. The cost per press is roughly 1.27 seconds with substituters enabled and 0.28 seconds with them disabled. The round-trips cost for checking whether the derivation is in the cache costs noticeably more than emulating the frames does.<sup id="fnref:local"><a href="#fn:local" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<pre title="Build time against number of presses. Total wall clock is linear in both configurations, 1.27 seconds per press with substituters enabled and 0.28 with them disabled; cost per press is flat in both cases with a constant gap of roughly 4.5x"><code class="language-plotnine">import pandas as pd
from plotnine import *

# Each row is a cold chain: forked onto its own branch first, so every press
# genuinely emulates rather than hitting the store.
df = pd.DataFrame({
    "presses": [25, 50, 100, 200] * 2,
    "seconds": [55.22, 72.06, 157.42, 278.06,
                8.96, 15.29, 29.87, 58.71],
    "mode": ["default (queries binary caches)"] * 4 + ["--option substitute false"] * 4,
})
df["per_press"] = df.seconds / df.presses

tall = df.melt(id_vars=["presses", "mode"], value_vars=["seconds", "per_press"])
panels = ["total wall clock (s)", "seconds per press"]
tall["panel"] = pd.Categorical(
    tall.variable.map(dict(zip(["seconds", "per_press"], panels))),
    categories=panels, ordered=True,
)

plot = (
    ggplot(tall, aes("presses", "value", color="mode", shape="mode"))
    + geom_line(size=0.9)
    + geom_point(size=2.0)
    + facet_wrap("panel", scales="free_y")
    + scale_color_manual(values=["#1a7f37", "#b1201d"])
    + expand_limits(y=0)
    + labs(x="presses built", y="", color="", shape="")
    + theme(legend_position="bottom", legend_box_margin=0, subplots_adjust={"wspace": 0.3})
)
plot.width, plot.height = 7.4, 3.6
</code></pre>

<p>We’re used to the attribute path being a <em>name</em>, simply a coordinate into a catalogue of things that exist. Laziness means it’s really a <em>program</em>: a sequence of steps the evaluator walks, generating whatever it needs as it goes.</p>

<p>Nixpkgs happens to use that machinery to describe software, but nothing about it requires that the tree be a catalogue at all. Coupled with the fact that the store turns out to be a decent persistence layer for reproducible state-machines, makes a our “package manager” reasonable to use for playing Mario. 🍄</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>The prefix <code class="language-plaintext highlighter-rouge">.#level1</code> is a precanned sequence of button presses that gets you to the start of level 1-1. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:image">
      <p>A screenshot of the frame is also produced, which is used when we want to stitch a video sequence together. <a href="#fnref:image" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:local">
      <p>We can set <code class="language-plaintext highlighter-rouge">preferLocalBuild</code> or <code class="language-plaintext highlighter-rouge">allowSubstitutes</code> if we want to avoid this cost. <a href="#fnref:local" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[One of the most surprising aspects of the Nix language is that it is lazy, especially if you have never used a lazy language before. This laziness is what makes much of Nixpkgs possible, and its complexity.]]></summary></entry></feed>