<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Michael Stapelbergs Website: posts tagged debian</title>
  <link href="https://michael.stapelberg.ch/posts/tags/debian/feed.xml" rel="self"/>
  <link href="https://michael.stapelberg.ch/posts/tags/debian/"/>


  <id>https://michael.stapelberg.ch/posts/tags/debian/</id>
  <generator>Hugo -- gohugo.io</generator>
  <entry>
    <title type="html"><![CDATA[Debian Code Search: OpenAPI now available]]></title>
    <link href="https://michael.stapelberg.ch/posts/2021-03-06-debian-code-search-openapi/"/>
    <id>https://michael.stapelberg.ch/posts/2021-03-06-debian-code-search-openapi/</id>
    <published>2021-03-06T11:15:11+01:00</published>
    <content type="html"><![CDATA[<p><a href="https://codesearch.debian.net/">Debian Code Search</a> now offers an OpenAPI-based API!</p>
<p>Various developers have created ad-hoc client libraries based on how the web
interface works.</p>
<p>The goal of offering an OpenAPI-based API is to provide developers with
automatically generated client libraries for a large number of programming
languages, that target a stable interface independent of the web interface’s
implementation details.</p>
<h2 id="getting-started">Getting started</h2>
<ol>
<li>
<p>Visit <a href="https://codesearch.debian.net/apikeys/">https://codesearch.debian.net/apikeys/</a> to download your personal API
key. Login via <a href="https://salsa.debian.org/">Debian’s GitLab instance
salsa.debian.org</a>; register there if you have no
account yet.</p>
</li>
<li>
<p>Find the Debian Code Search client library for your programming language. If
none exists yet, <a href="https://editor.swagger.io/?url=https://codesearch.debian.net/openapi2.yaml">auto-generate a client library on editor.swagger.io: click
“Generate
Client”</a>.</p>
</li>
<li>
<p>Search all code in Debian from your own analysis tool, migration tracking
dashboard, etc.</p>
</li>
</ol>
<h2 id="curl-example">curl example</h2>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>curl <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>  -H <span style="color:#4070a0">&#34;x-dcs-apikey: </span><span style="color:#007020;font-weight:bold">$(</span>cat dcs-apikey-stapelberg.txt<span style="color:#007020;font-weight:bold">)</span><span style="color:#4070a0">&#34;</span> <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>  -X GET <span style="color:#4070a0;font-weight:bold">\
</span></span></span><span style="display:flex;"><span><span style="color:#4070a0;font-weight:bold"></span>  <span style="color:#4070a0">&#34;https://codesearch.debian.net/api/v1/search?query=i3Font&amp;match_mode=regexp&#34;</span> 
</span></span></code></pre></div><h2 id="web-browser-example">Web browser example</h2>
<p>You can try out the API in your web browser in the <a href="https://codesearch.debian.net/apikeys/#openapi-doc-browser">OpenAPI
documentation</a>.</p>
<h2 id="code-example-go">Code example (Go)</h2>
<p>Here’s an example program that demonstrates how to set up an auto-generated Go
client for the Debian Code Search OpenAPI, run a query, and aggregate the results:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#f0f0f0;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#007020;font-weight:bold">func</span><span style="color:#bbb"> </span><span style="color:#06287e">burndown</span>()<span style="color:#bbb"> </span><span style="color:#902000">error</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>cfg<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>openapiclient.<span style="color:#06287e">NewConfiguration</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>cfg.<span style="color:#06287e">AddDefaultHeader</span>(<span style="color:#4070a0">&#34;x-dcs-apikey&#34;</span>,<span style="color:#bbb"> </span>apiKey)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>client<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>openapiclient.<span style="color:#06287e">NewAPIClient</span>(cfg)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>ctx<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>context.<span style="color:#06287e">Background</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Search through the full Debian Code Search corpus, blocking until all</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// results are available:</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>results,<span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>client.SearchApi.<span style="color:#06287e">Search</span>(ctx,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;fmt.Sprint(err)&#34;</span>,<span style="color:#bbb"> </span><span style="color:#666">&amp;</span>openapiclient.SearchApiSearchOpts{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// Literal searches are faster and do not require escaping special</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// characters, regular expression searches are more powerful.</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>MatchMode:<span style="color:#bbb"> </span>optional.<span style="color:#06287e">NewString</span>(<span style="color:#4070a0">&#34;literal&#34;</span>),<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>})<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#60a0b0;font-style:italic">// Print to stdout a CSV file with the path and number of occurrences:</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wr<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>csv.<span style="color:#06287e">NewWriter</span>(os.Stdout)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>header<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>[]<span style="color:#902000">string</span>{<span style="color:#4070a0">&#34;path&#34;</span>,<span style="color:#bbb"> </span><span style="color:#4070a0">&#34;number of occurrences&#34;</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>wr.<span style="color:#06287e">Write</span>(header);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>occurrences<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020">make</span>(<span style="color:#007020;font-weight:bold">map</span>[<span style="color:#902000">string</span>]<span style="color:#902000">int</span>)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>result<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">range</span><span style="color:#bbb"> </span>results<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>occurrences[result.Path]<span style="color:#666">++</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">for</span><span style="color:#bbb"> </span>_,<span style="color:#bbb"> </span>result<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">range</span><span style="color:#bbb"> </span>results<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>o,<span style="color:#bbb"> </span>ok<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>occurrences[result.Path]<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>!ok<span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">continue</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#60a0b0;font-style:italic">// Print one CSV record per path:</span><span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020">delete</span>(occurrences,<span style="color:#bbb"> </span>result.Path)<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>record<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>[]<span style="color:#902000">string</span>{result.Path,<span style="color:#bbb"> </span>strconv.<span style="color:#06287e">Itoa</span>(o)}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span><span style="color:#007020;font-weight:bold">if</span><span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">:=</span><span style="color:#bbb"> </span>wr.<span style="color:#06287e">Write</span>(record);<span style="color:#bbb"> </span>err<span style="color:#bbb"> </span><span style="color:#666">!=</span><span style="color:#bbb"> </span><span style="color:#007020;font-weight:bold">nil</span><span style="color:#bbb"> </span>{<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">			</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>err<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">		</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>}<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span>wr.<span style="color:#06287e">Flush</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb">	</span><span style="color:#007020;font-weight:bold">return</span><span style="color:#bbb"> </span>wr.<span style="color:#06287e">Error</span>()<span style="color:#bbb">
</span></span></span><span style="display:flex;"><span><span style="color:#bbb"></span>}<span style="color:#bbb">
</span></span></span></code></pre></div><p>The full example can be found under
<a href="https://github.com/Debian/dcs/blob/3d6a18f010e915f77b4833189286100308c539cb/_example/burndown.go"><code>burndown.go</code></a>.</p>
<h2 id="feedback">Feedback?</h2>
<p>File a <a href="https://github.com/Debian/dcs/issues">GitHub issue on
<code>github.com/Debian/dcs</code></a> please!</p>
<h2 id="migration-status">Migration status</h2>
<p>I’m aware of the following <a href="https://codesearch.debian.net/thirdparty">third-party projects using Debian Code
Search</a>:</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Migration status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://salsa.debian.org/debian/codesearch-cli">Debian Code Search CLI tool</a></td>
          <td><a href="https://salsa.debian.org/debian/codesearch-cli/-/merge_requests/1">Updated to OpenAPI</a></td>
      </tr>
      <tr>
          <td><a href="https://salsa.debian.org/aviau/identify-incomplete-xs-go-import-path">identify-incomplete-xs-go-import-path</a></td>
          <td><a href="https://salsa.debian.org/aviau/identify-incomplete-xs-go-import-path/-/merge_requests/1">Update pending</a></td>
      </tr>
      <tr>
          <td><a href="https://gitlab.gnome.org/nbenitez/gnome-codesearch">gnome-codesearch</a></td>
          <td>makes no API queries</td>
      </tr>
  </tbody>
</table>
<p>If you find any others, please point them to this post in case they are not
using Debian Code Search’s OpenAPI yet.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[a new distri linux (fast package management) release]]></title>
    <link href="https://michael.stapelberg.ch/posts/2020-05-16-distri-release/"/>
    <id>https://michael.stapelberg.ch/posts/2020-05-16-distri-release/</id>
    <published>2020-05-16T09:13:00+02:00</published>
    <content type="html"><![CDATA[<p>I just <a href="https://distr1.org/release-notes/supersilverhaze/">released a new version of distri</a>.</p>
<p>The focus of this release lies on:</p>
<ul>
<li>
<p>a better developer experience, allowing users to debug any installed package
without extra setup steps</p>
</li>
<li>
<p>performance improvements in all areas (starting programs, building distri
packages, generating distri images)</p>
</li>
<li>
<p>better tooling for keeping track of upstream versions</p>
</li>
</ul>
<p>See the <a href="https://distr1.org/release-notes/supersilverhaze/">release notes</a> for
more details.</p>
<p>The <a href="https://distr1.org/">distri research linux distribution</a> project <a href="/posts/2019-08-17-introducing-distri/">was started in
2019</a> to research whether a few
architectural changes could enable drastically faster package management.</p>
<p>While the package managers in common Linux distributions (e.g. apt, dnf, …) <a href="/posts/2019-08-17-linux-package-managers-are-slow/">top
out at data rates of only a few
MB/s</a>, distri effortlessly
saturates 1 Gbit, 10 Gbit and even 40 Gbit connections, resulting in fast
installation and update speeds.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Hermetic packages (in distri)]]></title>
    <link href="https://michael.stapelberg.ch/posts/2020-05-09-distri-hermetic-packages/"/>
    <id>https://michael.stapelberg.ch/posts/2020-05-09-distri-hermetic-packages/</id>
    <published>2020-05-09T18:48:00+02:00</published>
    <content type="html"><![CDATA[<p>In <a href="https://distr1.org/">distri</a>, packages (e.g. <code>emacs</code>) are hermetic. By
hermetic, I mean that the dependencies a package uses (e.g. <code>libusb</code>) don’t
change, even when newer versions are installed.</p>
<p>For example, if package <code>libusb-amd64-1.0.22-7</code> is available at build time, the
package will always use that same version, even after the newer
<code>libusb-amd64-1.0.23-8</code> will be installed into the package store.</p>
<p>Another way of saying the same thing is: <em>packages in distri are always
co-installable</em>.</p>
<p>This makes the package store more robust: additions to it will not break the
system. On a technical level, the package store is implemented as a directory
containing distri SquashFS images and metadata files, into which packages are
installed in an atomic way.</p>
<h2 id="out-of-scope-plugins-are-not-hermetic-by-design">Out of scope: plugins are not hermetic by design</h2>
<p>One exception where hermeticity is not desired are plugin mechanisms: optionally
loading out-of-tree code at runtime obviously is not hermetic.</p>
<p>As an example, consider <a href="https://www.gnu.org/software/libc/manual/html_node/Name-Service-Switch.html">glibc’s Name Service Switch
(NSS)</a>
mechanism. Page <a href="https://www.gnu.org/software/libc/manual/html_node/Adding-another-Service-to-NSS.html#Adding-another-Service-to-NSS">29.4.1 Adding another Service to
NSS</a>
describes how glibc searches <code>$prefix/lib</code> for shared libraries at runtime.</p>
<p>Debian <a href="https://packages.debian.org/search?suite=buster&amp;arch=amd64&amp;mode=filename&amp;searchon=contents&amp;keywords=libnss_%20.so.2">ships about a dozen NSS
libraries</a>
for a variety of purposes, and enterprise setups might add their own into the
mix.</p>
<p>systemd (as of v245) accounts for 4 NSS libraries,
e.g. <a href="https://www.freedesktop.org/software/systemd/man/nss-systemd.html">nss-systemd</a>
for user/group name resolution for users allocated through <a href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html">systemd’s
<code>DynamicUser=</code></a>
option.</p>
<p>Having packages be as hermetic as possible remains a worthwhile goal despite any
exceptions: I will gladly use a 99% hermetic system over a 0% hermetic system
any day.</p>
<p>Side note: Xorg’s driver model (which can be characterized as a plugin
mechanism) does not fall under this category because of its tight API/ABI
coupling! For this case, where drivers are only guaranteed to work with
precisely the Xorg version for which they were compiled, distri uses per-package
exchange directories.</p>
<h2 id="implementation-of-hermetic-packages-in-distri">Implementation of hermetic packages in distri</h2>
<p>On a technical level, the requirement is: all paths used by the program must
always result in the same contents. This is implemented in distri via the
read-only package store mounted at <code>/ro</code>, e.g. files underneath
<code>/ro/emacs-amd64-26.3-15</code> never change.</p>
<p>To change all paths used by a program, in practice, three strategies cover most
paths:</p>
<h3 id="elf-interpreter-and-dynamic-libraries">ELF interpreter and dynamic libraries</h3>
<p>Programs on Linux use the <a href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format">ELF file
format</a>, which
contains two kinds of references:</p>
<p>First, <strong>the ELF interpreter</strong> (<code>PT_INTERP</code> segment), which is used to start the
program. For dynamically linked programs on 64-bit systems, this is typically
<a href="https://manpages.debian.org/testing/manpages/ld.so.8.en.html"><code>ld.so(8)</code></a>.</p>
<p>Many distributions use system-global paths such as
<code>/lib64/ld-linux-x86-64.so.2</code>, but distri compiles programs with
<code>-Wl,--dynamic-linker=/ro/glibc-amd64-2.31-4/out/lib/ld-linux-x86-64.so.2</code> so
that the full path ends up in the binary.</p>
<p>The ELF interpreter is shown by <code>file(1)</code>, but you can also use <code>readelf -a $BINARY | grep 'program interpreter'</code> to display it.</p>
<p>And secondly, <a href="https://en.wikipedia.org/wiki/Rpath"><strong>the rpath</strong>, a run-time search
path</a> for dynamic libraries. Instead of
storing full references to all dynamic libraries, we set the rpath so that
<code>ld.so(8)</code> will find the correct dynamic libraries.</p>
<p>Originally, we used to just set a long rpath, containing one entry for each
dynamic library dependency. However, we have since <a href="https://github.com/distr1/distri/commit/19f342071283f4d78353bdbac8d6849809927f93">switched to using a single
<code>lib</code> subdirectory per
package</a>
as its rpath, and placing symlinks with full path references into that <code>lib</code>
directory, e.g. using <code>-Wl,-rpath=/ro/grep-amd64-3.4-4/lib</code>. This is better for
performance, as <code>ld.so</code> uses a per-directory cache.</p>
<p>Note that program load times are significantly influenced by how quickly you can
locate the dynamic libraries. distri uses a FUSE file system to load programs
from, so <a href="https://github.com/distr1/distri/commit/b6a0e43368d54d5ed0e03af687158dc3e2106e38">getting proper <code>-ENOENT</code> caching into
place</a>
drastically sped up program load times.</p>
<p>Instead of compiling software with the <code>-Wl,--dynamic-linker</code> and <code>-Wl,-rpath</code>
flags, one can also modify these fields after the fact using <code>patchelf(1)</code>. For
closed-source programs, this is the only possibility.</p>
<p>The rpath can be inspected by using e.g. <code>readelf -a $BINARY | grep RPATH</code>.</p>
<h3 id="environment-variable-setup-wrapper-programs">Environment variable setup wrapper programs</h3>
<p>Many programs are influenced by environment variables: to start another program,
said program is often found by checking each directory in the <code>PATH</code> environment
variable.</p>
<p>Such search paths are prevalent in scripting languages, too, to find
modules. Python has <code>PYTHONPATH</code>, Perl has <code>PERL5LIB</code>, and so on.</p>
<p>To set up these search path environment variables at run time, distri employs an
indirection. Instead of e.g. <code>teensy-loader-cli</code>, you run a small wrapper
program that calls precisely one <code>execve</code> system call with the desired
environment variables.</p>
<p>Initially, I used shell scripts as wrapper programs because they are easily
inspectable. This turned out to be too slow, so I switched to <a href="https://github.com/distr1/distri/blob/3ee4437f88605174fd82144381cfa726fc683ccb/internal/build/build.go#L1085-L1112">compiled
programs</a>. I’m
linking them statically for fast startup, and I’m linking them against <a href="https://musl.libc.org/">musl
libc</a> for significantly smaller file sizes than glibc
(per-executable overhead adds up quickly in a distribution!).</p>
<p>Note that the wrapper programs prepend to the <code>PATH</code> environment variable, they
don’t replace it in its entirely. This is important so that users have a way to
extend the <code>PATH</code> (and other variables) if they so choose. This doesn’t hurt
hermeticity because it is only relevant for programs that were not present at
build time, i.e. plugin mechanisms which, by design, cannot be hermetic.</p>
<h3 id="shebang-interpreter-patching">Shebang interpreter patching</h3>
<p>The <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">Shebang</a> of scripts contains
a path, too, and hence needs to be changed.</p>
<p><a href="https://github.com/distr1/distri/issues/67">We don’t do this in distri yet</a>
(the number of packaged scripts is small), but we should.</p>
<h3 id="performance-requirements">Performance requirements</h3>
<p>The performance improvements in the previous sections are not just good to have,
but practically required when many processes are involved: without them, you’ll
encounter second-long delays in <a href="https://magit.vc/">magit</a> which spawns many git
processes under the covers, or in
<a href="https://en.wikipedia.org/wiki/Dracut_(software)">dracut</a>, which spawns one
<code>cp(1)</code> process per file.</p>
<h2 id="downside-rebuild-of-packages-required-to-pick-up-changes">Downside: rebuild of packages required to pick up changes</h2>
<p>Linux distributions such as Debian consider it an advantage to roll out security
fixes to the entire system by updating a single shared library package
(e.g. <code>openssl</code>).</p>
<p>The flip side of that coin is that changes to a single critical package can
break the entire system.</p>
<p>With hermetic packages, all reverse dependencies must be rebuilt when a
library’s changes should be picked up by the whole system. E.g., when <code>openssl</code>
changes, <code>curl</code> must be rebuilt to pick up the new version of <code>openssl</code>.</p>
<p>This approach trades off using more bandwidth and more disk space (temporarily)
against reducing the blast radius of any individual package update.</p>
<h2 id="downside-long-env-variables-are-cumbersome-to-deal-with">Downside: long env variables are cumbersome to deal with</h2>
<p>This can be partially mitigated by <a href="https://github.com/distr1/distri/commit/6ac53cac4a5027622ae8622be2a208778dd54e74">removing empty directories at build
time</a>,
which will result in shorter variables.</p>
<p>In general, there is no getting around this. One little trick is to use <code>tr : '\n'</code>, e.g.:</p>
<pre tabindex="0"><code>distri0# echo $PATH
/usr/bin:/bin:/usr/sbin:/sbin:/ro/openssh-amd64-8.2p1-11/out/bin

distri0# echo $PATH | tr : &#39;\n&#39;
/usr/bin
/bin
/usr/sbin
/sbin
/ro/openssh-amd64-8.2p1-11/out/bin
</code></pre><h2 id="edge-cases">Edge cases</h2>
<p>The implementation outlined above works well in hundreds of packages, and only a
small handful exhibited problems of any kind. Here are some issues I encountered:</p>
<h3 id="issue-accidental-abi-breakage-in-plugin-mechanisms">Issue: accidental ABI breakage in plugin mechanisms</h3>
<p>NSS libraries built against glibc 2.28 and newer <a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=928769">cannot be loaded by glibc
2.27</a>. In all
likelihood, such changes do not happen too often, but it does illustrate that
glibc’s <a href="https://www.gnu.org/software/libc/manual/html_node/Adding-another-Service-to-NSS.html#Adding-another-Service-to-NSS">published interface
spec</a>
is not sufficient for forwards and backwards compatibility.</p>
<p>In distri, we could likely use a per-package exchange directory for glibc’s NSS
mechanism to prevent the above problem from happening in the future.</p>
<h3 id="issue-wrapper-bypass-when-a-program-re-executes-itself">Issue: wrapper bypass when a program re-executes itself</h3>
<p>Some programs try to arrange for themselves to be re-executed outside of their
current process tree. For example, consider building a program with the <code>meson</code>
build system:</p>
<ol>
<li>
<p>When <code>meson</code> first configures the build, it generates <code>ninja</code> files (think
Makefiles) which contain command lines that run the <code>meson --internal</code>
helper.</p>
</li>
<li>
<p>Once <code>meson</code> returns, <code>ninja</code> is called as a separate process, so it will not
have the environment which the <code>meson</code> wrapper sets up. <code>ninja</code> then runs the
previously persisted <code>meson</code> command line. Since the command line uses the
full path to <code>meson</code> (not to its wrapper), it bypasses the wrapper.</p>
</li>
</ol>
<p>Luckily, not many programs try to arrange for other process trees to run
them. Here is a table summarizing how affected programs might try to arrange for
re-execution, whether the technique results in a wrapper bypass, and what we do
about it in distri:</p>
<table>
  <thead>
      <tr>
          <th>technique to execute itself</th>
          <th>uses wrapper</th>
          <th>mitigation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>run-time: find own basename in <code>PATH</code></td>
          <td>yes</td>
          <td>wrapper program</td>
      </tr>
      <tr>
          <td>compile-time: embed expected path</td>
          <td>no; bypass!</td>
          <td>configure or patch</td>
      </tr>
      <tr>
          <td>run-time: <code>argv[0]</code> or <code>/proc/self/exe</code></td>
          <td>no; bypass!</td>
          <td><a href="https://github.com/distr1/distri/commit/f45ee9ac1121da284f2943c80e2c30afa24ca80d">patch</a></td>
      </tr>
  </tbody>
</table>
<p>One might think that setting <code>argv[0]</code> to the wrapper location seems like a way
to side-step this problem. We tried doing this in distri, but <a href="https://github.com/distr1/distri/commit/b517cb33ed827d358b00737434c7a09dd75583b7">had to
revert</a>
and <a href="https://github.com/distr1/distri/commit/9fd34936d4415f9963202bbb9ee454c970874b18">go the other
way</a>.</p>
<h3 id="misc-smaller-issues">Misc smaller issues</h3>
<ul>
<li>Login shells are <a href="https://unix.stackexchange.com/a/46856/181634">started by convention with a <code>-</code> character prepended to
<code>argv[0]</code></a>, so <a href="https://github.com/distr1/distri/commit/3c3a9d6ef4fc76edca6fb8351a716b18b83ff3af">shells like
bash or zsh cannot use wrapper
programs</a>.</li>
<li><a href="https://github.com/distr1/distri/commit/cefded2b2ce39407cc2d75936ec6cb018d533846">LDFLAGS leaked to
pkgconfig</a>
(<a href="https://github.com/distr1/distri/commit/434b7298ad7ef8d4ae229df84dd2353badf48fa1">upstream
reports</a>)</li>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1635036">mozjs tries to run autoconf with the shell directly, but should use
autoconf’s wrapper</a></li>
</ul>
<h2 id="appendix-could-other-distributions-adopt-hermetic-packages">Appendix: Could other distributions adopt hermetic packages?</h2>
<p>At a very high level, adopting hermetic packages will require two steps:</p>
<ol>
<li>
<p>Using fully qualified paths whose contents don’t change
(e.g. <code>/ro/emacs-amd64-26.3-15</code>) generally requires rebuilding programs,
e.g. with <code>--prefix</code> set.</p>
</li>
<li>
<p>Once you use fully qualified paths you need to make the packages able to
exchange data. distri solves this with exchange directories, implemented in the
<code>/ro</code> file system which is backed by a FUSE daemon.</p>
</li>
</ol>
<p>The first step is pretty simple, whereas the second step is where I expect
controversy around any suggested mechanism.</p>
<h2 id="appendix-demo-in-distri">Appendix: demo (in distri)</h2>
<p>This appendix contains commands and their outputs, run on upcoming distri
version <code>supersilverhaze</code>, but verified to work on older versions, too.</p>
<p>Large outputs have been collapsed and can be expanded by clicking on the output.</p>
<p>The <code>/bin</code> directory contains symlinks for the union of all package’s <code>bin</code> subdirectories:</p>
<details class="output" open><summary><code>distri0# readlink -f /bin/teensy_loader_cli</code></summary><pre><code>/ro/teensy-loader-cli-amd64-2.1+g20180927-7/bin/teensy_loader_cli</code></pre></details>
<p>The wrapper program in the <code>bin</code> subdirectory is small:</p>
<details class="output" open><summary><code>distri0# ls -lh $(readlink -f /bin/teensy_loader_cli)</code></summary><pre><code>-rwxr-xr-x 1 root root 46K Apr 21 21:56 /ro/teensy-loader-cli-amd64-2.1+g20180927-7/bin/teensy_loader_cli</code></pre></details>
<p>Wrapper programs execute quickly:</p>
<details class="output"><summary><code>distri0# strace -fvy /bin/teensy_loader_cli |& head | cat -n</code></summary><pre><code>     1  execve("/bin/teensy_loader_cli", ["/bin/teensy_loader_cli"], ["USER=root", "LOGNAME=root", "HOME=/root", "PATH=/ro/bash-amd64-5.0-4/bin:/r"..., "SHELL=/bin/zsh", "TERM=screen.xterm-256color", "XDG_SESSION_ID=c1", "XDG_RUNTIME_DIR=/run/user/0", "DBUS_SESSION_BUS_ADDRESS=unix:pa"..., "XDG_SESSION_TYPE=tty", "XDG_SESSION_CLASS=user", "SSH_CLIENT=10.0.2.2 42556 22", "SSH_CONNECTION=10.0.2.2 42556 10"..., "SSH_TTY=/dev/pts/0", "SHLVL=1", "PWD=/root", "OLDPWD=/root", "_=/usr/bin/strace", "LD_LIBRARY_PATH=/ro/bash-amd64-5"..., "PERL5LIB=/ro/bash-amd64-5.0-4/ou"..., "PYTHONPATH=/ro/bash-amd64-5.b0-4/"...]) = 0
     2  arch_prctl(ARCH_SET_FS, 0x40c878)       = 0
     3  set_tid_address(0x40ca9c)               = 715
     4  brk(NULL)                               = 0x15b9000
     5  brk(0x15ba000)                          = 0x15ba000
     6  brk(0x15bb000)                          = 0x15bb000
     7  brk(0x15bd000)                          = 0x15bd000
     8  brk(0x15bf000)                          = 0x15bf000
     9  brk(0x15c1000)                          = 0x15c1000
    10  execve("/ro/teensy-loader-cli-amd64-2.1+g20180927-7/out/bin/teensy_loader_cli", ["/ro/teensy-loader-cli-amd64-2.1+"...], ["USER=root", "LOGNAME=root", "HOME=/root", "PATH=/ro/bash-amd64-5.0-4/bin:/r"..., "SHELL=/bin/zsh", "TERM=screen.xterm-256color", "XDG_SESSION_ID=c1", "XDG_RUNTIME_DIR=/run/user/0", "DBUS_SESSION_BUS_ADDRESS=unix:pa"..., "XDG_SESSION_TYPE=tty", "XDG_SESSION_CLASS=user", "SSH_CLIENT=10.0.2.2 42556 22", "SSH_CONNECTION=10.0.2.2 42556 10"..., "SSH_TTY=/dev/pts/0", "SHLVL=1", "PWD=/root", "OLDPWD=/root", "_=/usr/bin/strace", "LD_LIBRARY_PATH=/ro/bash-amd64-5"..., "PERL5LIB=/ro/bash-amd64-5.0-4/ou"..., "PYTHONPATH=/ro/bash-amd64-5.0-4/"...]) = 0</code></pre></details>
<p>Confirm which ELF interpreter is set for a binary using <code>readelf(1)</code>:</p>
<details class="output" open><summary><code>distri0# readelf -a /ro/teensy-loader-cli-amd64-2.1+g20180927-7/out/bin/teensy_loader_cli | grep 'program interpreter'</code></summary><pre><code>[Requesting program interpreter: /ro/glibc-amd64-2.31-4/out/lib/ld-linux-x86-64.so.2]</code></pre></details>
<p>Confirm the rpath is set to the package’s lib subdirectory using <code>readelf(1)</code>:</p>
<details class="output" open><summary><code>distri0# readelf -a /ro/teensy-loader-cli-amd64-2.1+g20180927-7/out/bin/teensy_loader_cli | grep RPATH</code></summary><pre><code> 0x000000000000000f (RPATH)              Library rpath: [/ro/teensy-loader-cli-amd64-2.1+g20180927-7/lib]</code></pre></details>
<p>…and verify the lib subdirectory has the expected symlinks and target versions:</p>
<details class="output"><summary><code>distri0# find /ro/teensy-loader-cli-amd64-*/lib -type f -printf '%P -> %l\n'</code><pre>libc.so.6 -> /ro/glibc-amd64-2.31-4/out/lib/libc-2.31.so</pre></summary><pre><code>libpthread.so.0 -> /ro/glibc-amd64-2.31-4/out/lib/libpthread-2.31.so
librt.so.1 -> /ro/glibc-amd64-2.31-4/out/lib/librt-2.31.so
libudev.so.1 -> /ro/libudev-amd64-245-11/out/lib/libudev.so.1.6.17
libusb-0.1.so.4 -> /ro/libusb-compat-amd64-0.1.5-7/out/lib/libusb-0.1.so.4.4.4
libusb-1.0.so.0 -> /ro/libusb-amd64-1.0.23-8/out/lib/libusb-1.0.so.0.2.0</code></pre></details>
<p>To verify the correct libraries are actually loaded, you can set the <code>LD_DEBUG</code>
environment variable for <code>ld.so(8)</code>:</p>
<details class="output"><summary><code>distri0# LD_DEBUG=libs teensy_loader_cli</code></summary><pre><code>[…]
       678:     find library=libc.so.6 [0]; searching
       678:      search path=/ro/teensy-loader-cli-amd64-2.1+g20180927-7/lib            (RPATH from file /ro/teensy-loader-cli-amd64-2.1+g20180927-7/out/bin/teensy_loader_cli)
       678:       trying file=/ro/teensy-loader-cli-amd64-2.1+g20180927-7/lib/libc.so.6
       678:
[…]</code></pre></details>
<p>NSS libraries that distri ships:</p>
<details class="output"><summary><code>find /lib/ -name "libnss_*.so.2" -type f -printf '%P -> %l\n'</code><pre>libnss_myhostname.so.2 -> ../systemd-amd64-245-11/out/lib/libnss_myhostname.so.2</pre></summary><pre><code>libnss_mymachines.so.2 -> ../systemd-amd64-245-11/out/lib/libnss_mymachines.so.2
libnss_resolve.so.2 -> ../systemd-amd64-245-11/out/lib/libnss_resolve.so.2
libnss_systemd.so.2 -> ../systemd-amd64-245-11/out/lib/libnss_systemd.so.2
libnss_compat.so.2 -> ../glibc-amd64-2.31-4/out/lib/libnss_compat.so.2
libnss_db.so.2 -> ../glibc-amd64-2.31-4/out/lib/libnss_db.so.2
libnss_dns.so.2 -> ../glibc-amd64-2.31-4/out/lib/libnss_dns.so.2
libnss_files.so.2 -> ../glibc-amd64-2.31-4/out/lib/libnss_files.so.2
libnss_hesiod.so.2 -> ../glibc-amd64-2.31-4/out/lib/libnss_hesiod.so.2</code></pre></details>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[distri: 20x faster initramfs (initrd) from scratch]]></title>
    <link href="https://michael.stapelberg.ch/posts/2020-01-21-initramfs-from-scratch-golang/"/>
    <id>https://michael.stapelberg.ch/posts/2020-01-21-initramfs-from-scratch-golang/</id>
    <published>2020-01-21T17:50:00+01:00</published>
    <content type="html"><![CDATA[<p>In case you are not yet familiar with why an initramfs (or initrd, or initial
ramdisk) is typically used when starting Linux, let me quote the <a href="https://en.wikipedia.org/wiki/Initial_ramdisk">wikipedia
definition</a>:</p>
<p>“[…] initrd is a scheme for loading a temporary root file system into memory,
which may be used as part of the Linux startup process […] to make preparations
before the real root file system can be mounted.”</p>
<p>Many Linux distributions do not compile all file system drivers into the kernel,
but instead load them on-demand from an initramfs, which saves memory.</p>
<p>Another common scenario, in which an initramfs is required, is full-disk
encryption: the disk must be unlocked from userspace, but since userspace is
encrypted, an initramfs is used.</p>
<h2 id="motivation">Motivation</h2>
<p>Thus far, building a <a href="https://distr1.org/">distri</a> disk image was quite slow:</p>
<p>This is on an AMD Ryzen 3900X 12-core processor (2019):</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
80.29s user 13.56s system 186% cpu 50.419 total # 19s image, 31s initrd
</code></pre><p>Of these 50 seconds,
<a href="https://en.wikipedia.org/wiki/Dracut_(software)"><code>dracut</code></a>’s initramfs
generation accounts for 31 seconds (62%)!</p>
<p>Initramfs generation time drops to 8.7 seconds once <code>dracut</code> no longer needs to
use the single-threaded <a href="https://manpages.debian.org/gzip.1"><code>gzip(1)</code></a>
, but the
multi-threaded replacement <a href="https://manpages.debian.org/pigz.1"><code>pigz(1)</code></a>
:</p>
<p>This brings the total time to build a distri disk image down to:</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
76.85s user 13.23s system 327% cpu 27.509 total # 19s image, 8.7s initrd
</code></pre><p>Clearly, when you use <code>dracut</code> on any modern computer, you should make pigz
available. <code>dracut</code> should fail to compile unless one explicitly opts into the
known-slower gzip. For more thoughts on optional dependencies, see <a href="/posts/2019-05-23-optional-dependencies/">“Optional
dependencies don’t work”</a>.</p>
<p>But why does it take 8.7 seconds still? Can we go faster?</p>
<p>The answer is <strong>Yes</strong>! I recently built a distri-specific initramfs I’m calling
<code>minitrd</code>. I wrote both big parts from scratch:</p>
<ol>
<li>the initramfs generator program (<a href="https://github.com/distr1/distri/blob/master/cmd/distri/initrd.go"><code>distri initrd</code></a>)</li>
<li>a custom Go userland (<a href="https://github.com/distr1/distri/blob/master/cmd/minitrd/minitrd.go"><code>cmd/minitrd</code></a>), running as <code>/init</code> in the initramfs.</li>
</ol>
<p><code>minitrd</code> generates the initramfs image in ≈400ms, bringing the total time down
to:</p>
<pre tabindex="0"><code>distri % time make cryptimage serial=1
50.09s user 8.80s system 314% cpu 18.739 total # 18s image, 400ms initrd
</code></pre><p>(The remaining time is spent in preparing the file system, then installing and
configuring the distri system, i.e. preparing a disk image you can <a href="https://distr1.org/#run-distri-on-real-hardware">run on real
hardware</a>.)</p>
<p>How can <code>minitrd</code> be 20 times faster than <code>dracut</code>?</p>
<p><code>dracut</code> is mainly written in shell, with a C helper program. It drives the
generation process by spawning lots of external dependencies (e.g. <code>ldd</code> or the
<code>dracut-install</code> helper program). I assume that the combination of using an
interpreted language (shell) that spawns lots of processes and precludes a
concurrent architecture is to blame for the poor performance.</p>
<p><code>minitrd</code> is written in Go, with speed as a goal. It leverages concurrency and
uses no external dependencies; everything happens within a single process (but
with enough threads to saturate modern hardware).</p>
<p>Measuring early boot time using qemu, I measured the <code>dracut</code>-generated
initramfs taking 588ms to display the full disk encryption passphrase prompt,
whereas <code>minitrd</code> took only 195ms.</p>
<p>The rest of this article dives deeper into how <code>minitrd</code> works.</p>
<h2 id="what-does-an-initramfs-do">What does an initramfs do?</h2>
<p>Ultimately, the job of an initramfs is to make the root file system available
and continue booting the system from there. Depending on the system setup, this
involves the following 5 steps:</p>
<h3 id="1-load-kernel-modules-to-access-the-block-devices-with-the-root-file-system">1. Load kernel modules to access the block devices with the root file system</h3>
<p>Depending on the system, the block devices with the root file system might
already be present when the initramfs runs, or some kernel modules might need to
be loaded first. On my Dell XPS 9360 laptop, the NVMe system disk is already
present when the initramfs starts, whereas in qemu, we need to load the
<code>virtio_pci</code> module, followed by the <code>virtio_scsi</code> module.</p>
<p>How will our userland program know which kernel modules to load? Linux kernel
modules declare patterns for their supported hardware as an alias, e.g.:</p>
<pre tabindex="0"><code>initrd# grep virtio_pci lib/modules/5.4.6/modules.alias
alias pci:v00001AF4d*sv*sd*bc*sc*i* virtio_pci
</code></pre><p>Devices in <code>sysfs</code> have a <code>modalias</code> file whose content can be matched against
these declarations to identify the module to load:</p>
<pre tabindex="0"><code>initrd# cat /sys/devices/pci0000:00/*/modalias
pci:v00001AF4d00001005sv00001AF4sd00000004bc00scFFi00
pci:v00001AF4d00001004sv00001AF4sd00000008bc01sc00i00
[…]
</code></pre><p>Hence, for the initial round of module loading, it is sufficient to locate all
<code>modalias</code> files within <code>sysfs</code> and load the responsible modules.</p>
<p>Loading a kernel module can result in new devices appearing. When that happens,
the kernel sends a
<a href="https://stackoverflow.com/questions/22803469/uevent-sent-from-kernel-to-user-space-udev">uevent</a>,
which the uevent consumer in userspace receives via a netlink socket. Typically,
this consumer is <a href="https://manpages.debian.org/udev.7"><code>udev(7)</code></a>
, but in our case, it’s
<code>minitrd</code>.</p>
<p>For each uevent messages that comes with a <code>MODALIAS</code> variable, <code>minitrd</code> will
load the relevant kernel module(s).</p>
<p>When loading a kernel module, its dependencies need to be loaded
first. Dependency information is stored in the <code>modules.dep</code> file in a
<code>Makefile</code>-like syntax:</p>
<pre tabindex="0"><code>initrd# grep virtio_pci lib/modules/5.4.6/modules.dep
kernel/drivers/virtio/virtio_pci.ko: kernel/drivers/virtio/virtio_ring.ko kernel/drivers/virtio/virtio.ko
</code></pre><p>To load a module, we can open its file and then call the Linux-specific <a href="https://manpages.debian.org/finit_module.2"><code>finit_module(2)</code></a>
 system call. Some modules are expected to
return an error code, e.g. <code>ENODEV</code> or <code>ENOENT</code> when some hardware device is not
actually present.</p>
<p>Side note: next to the textual versions, there are also binary versions of the
<code>modules.alias</code> and <code>modules.dep</code> files. Presumably, those can be queried more
quickly, but for simplicitly, I have not (yet?) implemented support in
<code>minitrd</code>.</p>
<h3 id="2-console-settings-font-keyboard-layout">2. Console settings: font, keyboard layout</h3>
<p>Setting a legible font is necessary for hi-dpi displays. On my Dell XPS 9360
(3200 x 1800 QHD+ display), the following works well:</p>
<pre tabindex="0"><code>initrd# setfont latarcyrheb-sun32
</code></pre><p>Setting the user’s keyboard layout is necessary for entering the LUKS full-disk
encryption passphrase in their preferred keyboard layout. I use the <a href="https://www.neo-layout.org">NEO
layout</a>:</p>
<pre tabindex="0"><code>initrd# loadkeys neo
</code></pre><h3 id="3-block-device-identification">3. Block device identification</h3>
<p>In the Linux kernel, block device enumeration order is not necessarily the same
on each boot. Even if it was deterministic, device order could still be changed
when users modify their computer’s device topology (e.g. connect a new disk to a
formerly unused port).</p>
<p>Hence, it is good style to refer to disks and their partitions with stable
identifiers. This also applies to boot loader configuration, and so most
distributions will set a kernel parameter such as
<code>root=UUID=1fa04de7-30a9-4183-93e9-1b0061567121</code>.</p>
<p>Identifying the block device or partition with the specified <code>UUID</code> is the
initramfs’s job.</p>
<p>Depending on what the device contains, the UUID comes from a different
place. For example, <code>ext4</code> file systems have a UUID field in their file system
superblock, whereas LUKS volumes have a UUID in their LUKS header.</p>
<p>Canonically, probing a device to extract the UUID is done by <code>libblkid</code> from the
<code>util-linux</code> package, but the logic can easily be <a href="https://github.com/distr1/distri/blob/master/cmd/minitrd/blkid.go">re-implemented in other
languages</a>
and changes rarely. <code>minitrd</code> comes with its own implementation to avoid
<a href="https://golang.org/cmd/cgo/">cgo</a> or running the <a href="https://manpages.debian.org/blkid.8"><code>blkid(8)</code></a>
 program.</p>
<h3 id="4-luks-full-disk-encryption-unlocking-only-on-encrypted-systems">4. LUKS full-disk encryption unlocking (only on encrypted systems)</h3>
<p>Unlocking a
<a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup">LUKS</a>-encrypted volume
is done in userspace. The kernel handles the crypto, but reading the metadata,
obtaining the passphrase (or e.g. key material from a file) and setting up the
device mapper table entries are done in user space.</p>
<pre tabindex="0"><code>initrd# modprobe algif_skcipher
initrd# cryptsetup luksOpen /dev/sda4 cryptroot1
</code></pre><p>After the user entered their passphrase, the root file system can be mounted:</p>
<pre tabindex="0"><code>initrd# mount /dev/dm-0 /mnt
</code></pre><h3 id="5-continuing-the-boot-process-switch_root">5. Continuing the boot process (switch_root)</h3>
<p>Now that everything is set up, we need to pass execution to the init program on
the root file system with a careful sequence of <a href="https://manpages.debian.org/chdir.2"><code>chdir(2)</code></a>
, <a href="https://manpages.debian.org/mount.2"><code>mount(2)</code></a>
, <a href="https://manpages.debian.org/chroot.2"><code>chroot(2)</code></a>
, <a href="https://manpages.debian.org/chdir.2"><code>chdir(2)</code></a>
 and <a href="https://manpages.debian.org/execve.2"><code>execve(2)</code></a>
 system calls that is explained in <a href="https://github.com/mirror/busybox/blob/9ec836c033fc6e55e80f3309b3e05acdf09bb297/util-linux/switch_root.c#L297">this busybox switch_root
comment</a>.</p>
<pre tabindex="0"><code>initrd# mount -t devtmpfs dev /mnt/dev
initrd# exec switch_root -c /dev/console /mnt /init
</code></pre><p>To conserve RAM, the files in the temporary file system to which the initramfs
archive is extracted are typically deleted.</p>
<h2 id="how-is-an-initramfs-generated">How is an initramfs generated?</h2>
<p>An initramfs “image” (more accurately: archive) is a compressed
<a href="https://en.wikipedia.org/wiki/Cpio">cpio</a> archive. Typically, gzip compression
is used, but the kernel supports a bunch of different algorithms and
distributions such as <a href="https://www.phoronix.com/scan.php?page=news_item&amp;px=LZ4-Initramfs-Ubuntu-Go-Ahead">Ubuntu are switching to lz4</a>.</p>
<p>Generators typically prepare a temporary directory and feed it to the <a href="https://manpages.debian.org/cpio.1"><code>cpio(1)</code></a>
 program. In <code>minitrd</code>, we read the files into memory
and generate the cpio archive using the
<a href="https://github.com/cavaliercoder/go-cpio">go-cpio</a> package. We use the
<a href="https://github.com/klauspost/pgzip">pgzip</a> package for parallel gzip
compression.</p>
<p>The following files need to go into the cpio archive:</p>
<h3 id="minitrd-go-userland">minitrd Go userland</h3>
<p>The <code>minitrd</code> binary is copied into the cpio archive as <code>/init</code> and will be run
by the kernel after extracting the archive.</p>
<p>Like the rest of distri, <code>minitrd</code> is built statically without cgo, which means
it can be copied as-is into the cpio archive.</p>
<h3 id="linux-kernel-modules">Linux kernel modules</h3>
<p>Aside from the <code>modules.alias</code> and <code>modules.dep</code> metadata files, the kernel
modules themselves reside in e.g. <code>/lib/modules/5.4.6/kernel</code> and need to be
copied into the cpio archive.</p>
<p>Copying all modules results in a ≈80 MiB archive, so it is common to only copy
modules that are relevant to the initramfs’s features. This reduces archive size
to ≈24 MiB.</p>
<p>The filtering relies on hard-coded patterns and module names. For example, disk
encryption related modules are all kernel modules underneath <code>kernel/crypto</code>,
plus <code>kernel/drivers/md/dm-crypt.ko</code>.</p>
<p>When generating a host-only initramfs (works on precisely the computer that
generated it), some initramfs generators look at the currently loaded modules
and just copy those.</p>
<h3 id="console-fonts-and-keymaps">Console Fonts and Keymaps</h3>
<p>The <code>kbd</code> package’s <a href="https://manpages.debian.org/setfont.8"><code>setfont(8)</code></a>
 and <a href="https://manpages.debian.org/loadkeys.1"><code>loadkeys(1)</code></a>
 programs load console fonts and keymaps from
<code>/usr/share/consolefonts</code> and <code>/usr/share/keymaps</code>, respectively.</p>
<p>Hence, these directories need to be copied into the cpio archive. Depending on
whether the initramfs should be generic (work on many computers) or host-only
(works on precisely the computer/settings that generated it), the entire
directories are copied, or only the required font/keymap.</p>
<h3 id="cryptsetup-setfont-loadkeys">cryptsetup, setfont, loadkeys</h3>
<p>These programs are (currently) required because <code>minitrd</code> does not implement
their functionality.</p>
<p>As they are dynamically linked, not only the programs themselves need to be
copied, but also the ELF dynamic linking loader (path stored in the <code>.interp</code>
ELF section) and any ELF library dependencies.</p>
<p>For example, <code>cryptsetup</code> in distri declares the ELF interpreter
<code>/ro/glibc-amd64-2.27-3/out/lib/ld-linux-x86-64.so.2</code> and declares dependencies
on shared libraries <code>libcryptsetup.so.12</code>, <code>libblkid.so.1</code> and others. Luckily,
in distri, packages contain a <code>lib</code> subdirectory containing symbolic links to
the resolved shared library paths (hermetic packaging), so it is sufficient to
mirror the lib directory into the cpio archive, recursing into shared library
dependencies of shared libraries.</p>
<p><code>cryptsetup</code> also requires the GCC runtime library <code>libgcc_s.so.1</code> to be present
at runtime, and will abort with an error message about not being able to call
<a href="https://manpages.debian.org/pthread_cancel.3"><code>pthread_cancel(3)</code></a>
 if it is unavailable.</p>
<h3 id="time-zone-data">time zone data</h3>
<p>To print log messages in the correct time zone, we copy <code>/etc/localtime</code> from
the host into the cpio archive.</p>
<h2 id="minitrd-outside-of-distri">minitrd outside of distri?</h2>
<p>I currently have no desire to make <code>minitrd</code> available outside of
<a href="https://distr1.org/">distri</a>. While the technical challenges (such as extending
the generator to not rely on distri’s hermetic packages) are surmountable, I
don’t want to support people’s initramfs remotely.</p>
<p>Also, I think that people’s efforts should in general be spent on rallying
behind <code>dracut</code> and making it work faster, thereby benefiting all Linux
distributions that use dracut (increasingly more). With <code>minitrd</code>, I have
demonstrated that significant speed-ups are achievable.</p>
<h2 id="conclusion">Conclusion</h2>
<p>It was interesting to dive into how an initramfs really works. I had been
working with the concept for many years, from small tasks such as “debug why the
encrypted root file system is not unlocked” to more complicated tasks such as
“set up a root file system on DRBD for a high-availability setup”. But even with
that sort of experience, I didn’t know all the details, until I was forced to
implement every little thing.</p>
<p>As I suspected going into this exercise, <code>dracut</code> is much slower than it needs
to be. Re-implementing its generation stage in a modern language instead of
shell helps a lot.</p>
<p>Of course, my <code>minitrd</code> does a bit less than <code>dracut</code>, but not drastically
so. The overall architecture is the same.</p>
<p>I hope my effort helps with two things:</p>
<ol>
<li>
<p>As a teaching implementation: instead of wading through the various
components that make up a modern initramfs (udev, systemd, various shell
scripts, …), people can learn about how an initramfs works in a single place.</p>
</li>
<li>
<p>I hope the significant time difference motivates people to improve <code>dracut</code>.</p>
</li>
</ol>
<h2 id="appendix-qemu-development-environment">Appendix: qemu development environment</h2>
<p>Before writing any Go code, I did some manual prototyping. Learning how other
people prototype is often immensely useful to me, so I’m sharing my notes here.</p>
<p>First, I copied all kernel modules and a statically built busybox binary:</p>
<pre tabindex="0"><code>% mkdir -p lib/modules/5.4.6
% cp -Lr /ro/lib/modules/5.4.6/* lib/modules/5.4.6/
% cp ~/busybox-1.22.0-amd64/busybox sh
</code></pre><p>To generate an initramfs from the current directory, I used:</p>
<pre tabindex="0"><code>% find . | cpio -o -H newc | pigz &gt; /tmp/initrd
</code></pre><p>In distri’s <code>Makefile</code>, I append these flags to the <code>QEMU</code> invocation:</p>
<pre tabindex="0"><code>-kernel /tmp/kernel \
-initrd /tmp/initrd \
-append &#34;root=/dev/mapper/cryptroot1 rdinit=/sh ro console=ttyS0,115200 rd.luks=1 rd.luks.uuid=63051f8a-54b9-4996-b94f-3cf105af2900 rd.luks.name=63051f8a-54b9-4996-b94f-3cf105af2900=cryptroot1 rd.vconsole.keymap=neo rd.vconsole.font=latarcyrheb-sun32 init=/init systemd.setenv=PATH=/bin rw vga=836&#34;
</code></pre><p>The <code>vga=</code> mode parameter is required for loading font <code>latarcyrheb-sun32</code>.</p>
<p>Once in the <code>busybox</code> shell, I manually prepared the required mount points and
kernel modules:</p>
<pre tabindex="0"><code>ln -s sh mount
ln -s sh lsmod
mkdir /proc /sys /run /mnt
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
modprobe virtio_pci
modprobe virtio_scsi
</code></pre><p>As a next step, I copied <code>cryptsetup</code> and dependencies into the initramfs directory:</p>
<pre tabindex="0"><code>% for f in /ro/cryptsetup-amd64-2.0.4-6/lib/*; do full=$(readlink -f $f); rel=$(echo $full | sed &#39;s,^/,,g&#39;); mkdir -p $(dirname $rel); install $full $rel; done
% ln -s ld-2.27.so ro/glibc-amd64-2.27-3/out/lib/ld-linux-x86-64.so.2
% cp /ro/glibc-amd64-2.27-3/out/lib/ld-2.27.so ro/glibc-amd64-2.27-3/out/lib/ld-2.27.so
% cp -r /ro/cryptsetup-amd64-2.0.4-6/lib ro/cryptsetup-amd64-2.0.4-6/
% mkdir -p ro/gcc-libs-amd64-8.2.0-3/out/lib64/
% cp /ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1 ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1
% ln -s /ro/gcc-libs-amd64-8.2.0-3/out/lib64/libgcc_s.so.1 ro/cryptsetup-amd64-2.0.4-6/lib
% cp -r /ro/lvm2-amd64-2.03.00-6/lib ro/lvm2-amd64-2.03.00-6/
</code></pre><p>In <code>busybox</code>, I used the following commands to unlock the root file system:</p>
<pre tabindex="0"><code>modprobe algif_skcipher
./cryptsetup luksOpen /dev/sda4 cryptroot1
mount /dev/dm-0 /mnt
</code></pre>]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Debian Code Search: positional index, TurboPFor-compressed]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-09-29-dcs-positional-turbopfor-index/"/>
    <id>https://michael.stapelberg.ch/posts/2019-09-29-dcs-positional-turbopfor-index/</id>
    <published>2019-09-29T00:00:00+00:00</published>
    <content type="html"><![CDATA[<style type="text/css">
.bar {
  display: inline-block;
  padding: 0.25em;
  text-align: center;
  vertical-align: middle;
}

.barcon {
  width: 40em;
  display: flex;
}
</style>
<p>See the <a href="#conclusion">Conclusion</a> for a summary if you’re impatient :-)</p>
<h3 id="motivation">Motivation</h3>
<p>Over the last few months, I have been developing a new index format for Debian
Code Search. This required a lot of careful refactoring, re-implementation,
debug tool creation and debugging.</p>
<p>Multiple factors motivated my work on a new index format:</p>
<ol>
<li>
<p>The existing index format has a 2G size limit, into which we have bumped a
few times, requiring manual intervention to keep the system running.</p>
</li>
<li>
<p>Debugging the existing system required creating ad-hoc debugging tools, which
made debugging sessions unnecessarily lengthy and painful.</p>
</li>
<li>
<p>I wanted to check whether <a href="https://github.com/Debian/dcs/issues/85">switching to a different integer compression
format</a> would improve performance
(it does not).</p>
</li>
<li>
<p>I wanted to check whether storing positions with the posting lists would
improve performance of identifier queries (= queries which are not using any
regular expression features), which make up 78.2% of all Debian Code Search
queries (it does).</p>
</li>
</ol>
<p>I figured building a new index from scratch was the easiest approach, compared
to refactoring the existing index to increase the size limit (point ①).</p>
<p>I also figured it would be a good idea to develop the debugging tool in lock
step with the index format so that I can be sure the tool works and is useful
(point ②).</p>
<h3 id="integer-compression-turbopfor">Integer compression: TurboPFor</h3>
<p>As a quick refresher, search engines typically store document IDs (representing
source code files, in our case) in an ordered list (“posting list”). It usually
makes sense to apply at least a rudimentary level of compression: our existing
system used variable integer encoding.</p>
<p><a href="https://github.com/powturbo/TurboPFor">TurboPFor</a>, the self-proclaimed “Fastest
Integer Compression” library, combines an advanced on-disk format with a
carefully tuned SIMD implementation to reach better speeds (in micro benchmarks)
at less disk usage than <a href="https://github.com/google/codesearch/blob/4fe90b597ae534f90238f82c7b5b1bb6d6d52dff/index/write.go#L561">Russ Cox’s varint implementation in
<code>github.com/google/codesearch</code></a>.</p>
<p>If you are curious about its inner workings, check out my “<a href="/posts/2019-02-05-turbopfor-analysis/">TurboPFor: an
analysis</a>”.</p>
<p>Applied on the Debian Code Search index, TurboPFor indeed compresses integers better:</p>
<h4 id="disk-space">Disk space</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">8.9G</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 61%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">5.5G</span>
TurboPFor index
<p>Switching to TurboPFor (via cgo) for storing and reading the index results in a
slight speed-up of a <code>dcs replay</code> benchmark, which is more pronounced the more
i/o is required.</p>
<h4 id="query-speed-regexp-cold-page-cache">Query speed (regexp, cold page cache)</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">18s</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 77.7%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">14s</span>
TurboPFor index (cgo)
<h4 id="query-speed-regexp-warm-page-cache">Query speed (regexp, warm page cache)</h4>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">15s</span>
codesearch varint index
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 93.3%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">14s</span>
TurboPFor index (cgo)
<p>Overall, TurboPFor is an all-around improvement in efficiency, albeit with a
high cost in implementation complexity.</p>
<h3 id="positional-index-trade-more-disk-for-faster-queries">Positional index: trade more disk for faster queries</h3>
<p>This section builds on the previous section: all figures come from the TurboPFor
index, which can optionally support positions.</p>
<p>Conceptually, we’re going from:</p>
<pre tabindex="0"><code>type docid uint32
type index map[trigram][]docid
</code></pre><p>…to:</p>
<pre tabindex="0"><code>type occurrence struct {
    doc docid
    pos uint32 // byte offset in doc
}
type index map[trigram][]occurrence
</code></pre><p>The resulting index consumes more disk space, but can be queried faster:</p>
<ol>
<li>
<p>We can do fewer queries: instead of reading all the posting lists for all
the trigrams, we can read the posting lists for the query’s first and last
trigram only.
<br>
This is one of the tricks described in the paper
“<a href="https://cedric.cnam.fr/fichiers/art_3216.pdf">AS-Index: A
Structure For String Search Using n-grams and Algebraic Signatures</a>”
(PDF), and goes a long way without incurring the complexity, computational
cost and additional disk usage of calculating algebraic signatures.</p>
</li>
<li>
<p>Verifying the delta between the last and first position matches the length
of the query term significantly reduces the number of files to read (lower
false positive rate).</p>
</li>
<li>
<p>The matching phase is quicker: instead of locating the query term in the
file, we only need to compare a few bytes at a known offset for equality.</p>
</li>
<li>
<p>More data is read sequentially (from the index), which is faster.</p>
</li>
</ol>
<h4 id="disk-space-1">Disk space</h4>
<p>A positional index consumes significantly more disk space, but not so much as
to pose a challenge: a Hetzner EX61-NVME dedicated server (≈ 64 €/month)
provides 1 TB worth of fast NVMe flash storage.</p>
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 5.2%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">&nbsp;6.5G</span>
non-positional
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 100%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">123G</span>
positional
<div style="display: inline-block">
	<div class="barcon">
		<div class="bar" style="width: 75.6%; background-color: blue; color: white">
			&nbsp;
		</div>
	</div>
</div>
<span style="margin-right: 2em">&nbsp;&nbsp;93G</span>
positional (posrel)
<p>The idea behind the positional index (posrel) is to not store a <code>(doc,pos)</code>
tuple on disk, but to store positions, accompanied by a stream of doc/pos
relationship bits: 1 means this position belongs to the next document, 0 means
this position belongs to the current document.</p>
<p>This is an easy way of saving some space without modifying the TurboPFor
on-disk format: the posrel technique reduces the index size to about ¾.</p>
<p>With the increase in size, the Linux page cache hit ratio will be lower for
the positional index, i.e. more data will need to be fetched from disk for
querying the index.</p>
<p>As long as the disk can deliver data as fast as you can decompress posting
lists, this only translates into one disk seek’s worth of additional
latency. This is the case with modern NVMe disks that deliver thousands of MB/s,
e.g. the Samsung 960 Pro (used in Hetzner’s aforementioned EX61-NVME server).</p>
<p>The values were measured by running <code>dcs du -h /srv/dcs/shard*/full</code>
without and with the <code>-pos</code> argument.</p>
<h4 id="bytes-read">Bytes read</h4>
<p>A positional index requires fewer queries: reading only the first and last
trigram’s posting lists and positions is sufficient to achieve a lower (!) false
positive rate than evaluating <strong>all</strong> trigram’s posting lists in a
non-positional index.</p>
<p>As a consequence, fewer files need to be read, resulting in fewer bytes required
to read from disk overall.</p>
<p>As an additional bonus, in a positional index, more data is read sequentially
(index), which is faster than random i/o, regardless of the underlying disk.</p>
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 1.2em); background-color: blue; color: white">
  1.2G
</div>
<div class="bar" style="width: calc(2 * 19.8em); background-color: green; color: white">
  19.8G
</div>
</div>
</div>
<span style="margin-right: 2em">21.0G</span>
regexp queries
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 4.2em); background-color: blue; color: white">
  4.2G (index)
</div>
<div class="bar" style="width: calc(2 * 10.8em); background-color: green; color: white">
  10.8G (files)
</div>
</div>
</div>
<span style="margin-right: 2em">15.0G</span>
identifier queries
<p>The values were measured by running <code>iostat -d 25</code> just before running
<a href="https://codesearch.debian.net/research/2019-08-03-dcs-new-index/"><code>bench.zsh</code></a>
on an otherwise idle system.</p>
<h4 id="query-speed">Query speed</h4>
<p>Even though the positional index is larger and requires more data to be read at
query time (see above), thanks to the C TurboPFor library, the 2 queries on a
positional index are roughly as fast as the n queries on a non-positional index
(≈4s instead of ≈3s).</p>
<p>This is more than made up for by the combined i/o matching stage, which shrinks
from ≈18.5s (7.1s i/o + 11.4s matching) to ≈1.3s.</p>
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 3.3em); background-color: blue; color: white">
  3.3s (index)
</div>
<div class="bar" style="width: calc(2 * 7.1em); background-color: green; color: white">
  7.1s (i/o)
</div>
<div class="bar" style="width: calc(2 * 11.4em); background-color: purple; color: white">
  11.4s (matching)
</div>
</div>
</div>
<span style="margin-right: 2em">21.8s</span>
regexp queries
<div style="display: inline-block">
<div class="barcon">
<div class="bar" style="width: calc(2 * 3.92em); background-color: blue; color: white">
  3.92s (index)
</div>
<div class="bar" style="width: calc(2 * 1.3em); background-color: green; color: white">
  ≈1.3s
</div>
</div>
</div>
<span style="margin-right: 2em">5.22s</span>
identifier queries
<p>Note that identifier query i/o was sped up not just by needing to read fewer
bytes, but also by only having to verify bytes at a known offset instead of
needing to locate the identifier within the file.</p>
<h3 id="conclusion">Conclusion</h3>
<p>The new index format is overall slightly more efficient. This disk space
efficiency allows us to introduce a positional index section for the first
time.</p>
<p>Most Debian Code Search queries are positional queries (78.2%) and will be
answered much quicker by leveraging the positions.</p>
<p>Bottomline, it is beneficial to use a positional index on disk over a
non-positional index in RAM.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[distri: a Linux distribution to research fast package management]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-08-17-introducing-distri/"/>
    <id>https://michael.stapelberg.ch/posts/2019-08-17-introducing-distri/</id>
    <published>2019-08-17T18:36:00+02:00</published>
    <content type="html"><![CDATA[<p>Over the last year or so I have worked on a research linux distribution in my
spare time. It’s not a distribution for researchers (like <a href="https://en.wikipedia.org/wiki/Scientific_Linux">Scientific
Linux</a>), but my personal
playground project to research linux distribution development, i.e. try out
fresh ideas.</p>
<p>This article focuses on the package format and its advantages, but there is
more to distri, which I will <a href="#more-to-come">cover in upcoming blog posts</a>.</p>
<h3 id="motivation">Motivation</h3>
<p>I was a Debian Developer for the 7 years from 2012 to 2019, but using the
distribution often left me frustrated, ultimately <a href="/posts/2019-03-10-debian-winding-down/">resulting in me winding down
my Debian work</a>.</p>
<p>Frequently, I was noticing a large gap between the actual speed of an operation
(e.g. doing an update) and the possible speed based on back of the envelope
calculations. I wrote more about this in my blog post <a href="/posts/2019-08-17-linux-package-managers-are-slow/">“Package managers are
slow”</a>.</p>
<p>To me, this observation means that either there is potential to optimize the
package manager itself (e.g. <code>apt</code>), or what the system does is just too
complex. While I remember seeing some low-hanging fruit¹, through my work on
distri, I wanted to explore whether all the complexity we currently have in
Linux distributions such as Debian or Fedora is inherent to the problem space.</p>
<p>I have completed enough of the experiment to conclude that the complexity is not
inherent: I can build a Linux distribution for general-enough purposes which is
much less complex than existing ones.</p>
<p>① Those were low-hanging fruit from a user perspective. I’m not saying that
fixing them is easy in the technical sense; I know too little about <code>apt</code>’s code
base to make such a statement.</p>
<h3 id="key-idea-packages-are-images-not-archives">Key idea: packages are images, not archives</h3>
<p>One key idea is to switch from using archives to using <strong>images</strong> for package
contents. Common package managers such as <a href="https://manpages.debian.org/dpkg.1"><code>dpkg(1)</code></a>

use <a href="https://manpages.debian.org/tar.1"><code>tar(1)</code></a>
 archives with various compression
algorithms.</p>
<p>distri uses <a href="https://en.wikipedia.org/wiki/SquashFS">SquashFS images</a>, a
comparatively simple file system image format that I happen to be familiar with
from my work on the <a href="https://gokrazy.org">gokrazy Raspberry Pi 3 Go platform</a>.</p>
<p>This idea is not novel: <a href="https://en.wikipedia.org/wiki/AppImage">AppImage</a> and
<a href="https://en.wikipedia.org/wiki/Snappy_(package_manager)">snappy</a> also use
images, but only for individual, self-contained applications. distri however
uses images for distribution packages with dependencies. In particular, there is
no duplication of shared libraries in distri.</p>
<p>A nice side effect of using read-only image files is that applications are
immutable and can hence not be broken by accidental (or malicious!)
modification.</p>
<h3 id="key-idea-separate-hierarchies">Key idea: separate hierarchies</h3>
<p>Package contents are made available under a fully-qualified path. E.g., all
files provided by package <code>zsh-amd64-5.6.2-3</code> are available under
<code>/ro/zsh-amd64-5.6.2-3</code>. The mountpoint <code>/ro</code> stands for read-only, which is
short yet descriptive.</p>
<p>Perhaps surprisingly, building software with custom <code>prefix</code> values of
e.g. <code>/ro/zsh-amd64-5.6.2-3</code> is widely supported, thanks to:</p>
<ol>
<li>
<p>Linux distributions, which build software with <code>prefix</code> set to <code>/usr</code>,
whereas FreeBSD (and the autotools default), which build with <code>prefix</code> set to
<code>/usr/local</code>.</p>
</li>
<li>
<p>Enthusiast users in corporate or research environments, who install software
into their home directories.</p>
</li>
</ol>
<p>Because using a custom <code>prefix</code> is a common scenario, upstream awareness for
<code>prefix</code>-correctness is generally high, and the rarely required patch will be
quickly accepted.</p>
<h3 id="key-idea-exchange-directories">Key idea: exchange directories</h3>
<p>Software packages often exchange data by placing or locating files in well-known
directories. Here are just a few examples:</p>
<ul>
<li><a href="https://manpages.debian.org/gcc.1"><code>gcc(1)</code></a>
 locates the <a href="https://manpages.debian.org/libusb.3"><code>libusb(3)</code></a>
 headers via <code>/usr/include</code></li>
<li><a href="https://manpages.debian.org/man.1"><code>man(1)</code></a>
 locates the <a href="https://manpages.debian.org/nginx.1"><code>nginx(1)</code></a>
 manpage via <code>/usr/share/man</code>.</li>
<li><a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
 locates executable programs via <code>PATH</code> components such as <code>/bin</code></li>
</ul>
<p>In distri, these locations are called <strong>exchange directories</strong> and are provided
via FUSE in <code>/ro</code>.</p>
<p>Exchange directories come in two different flavors:</p>
<ol>
<li>
<p>global. The exchange directory, e.g. <code>/ro/share</code>, provides the union of the
<code>share</code> sub directory of all packages in the package store.
<br>
Global exchange directories are largely used for compatibility, <a href="#fhs-compat">see
below</a>.</p>
</li>
<li>
<p>per-package. Useful for tight coupling: e.g. <a href="https://manpages.debian.org/irssi.1"><code>irssi(1)</code></a>
 does not provide any ABI guarantees, so plugins such as <code>irssi-robustirc</code>
can declare that they want
e.g. <code>/ro/irssi-amd64-1.1.1-1/out/lib/irssi/modules</code> to be a per-package
exchange directory and contain files from their <code>lib/irssi/modules</code>.</p>
</li>
</ol>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> Only a few exchange directories are also available in the package build
environment (as opposed to run-time).</div>
  </div>
</aside>

<h4 id="search-paths-sometimes-need-to-be-fixed">Search paths sometimes need to be fixed</h4>
<p>Programs which use exchange directories sometimes use search paths to access
multiple exchange directories. In fact, the examples above were taken from <a href="https://manpages.debian.org/gcc.1"><code>gcc(1)</code></a>
’s <code>INCLUDEPATH</code>, <a href="https://manpages.debian.org/man.1"><code>man(1)</code></a>
’s <code>MANPATH</code> and <a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
’s <code>PATH</code>. These are
prominent ones, but more examples are easy to find: <a href="https://manpages.debian.org/zsh.1"><code>zsh(1)</code></a>
 loads completion functions from its <code>FPATH</code>.</p>
<p>Some search path values are derived from <code>--datadir=/ro/share</code> and require no
further attention, but others might derive from
e.g. <code>--prefix=/ro/zsh-amd64-5.6.2-3/out</code> and need to be pointed to an exchange
directory via a specific command line flag.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> To create the illusion of a writable search path at package
build-time, <code>$DESTDIR/ro/share</code> and <code>$DESTDIR/ro/lib</code> are
diverted to <code>$DESTDIR/$PREFIX/share</code> and
<code>$DESTDIR/$PREFIX/lib</code>, respectively.</div>
  </div>
</aside>

<h4 id="fhs-compat">FHS compatibility</h4>
<p>Global exchange directories are used to make distri provide enough of the
<a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">Filesystem Hierarchy Standard
(FHS)</a> that
third-party software largely just works. This includes a C development
environment.</p>
<p>I successfully ran a few programs from their binary packages such as Google
Chrome, Spotify, or Microsoft’s Visual Studio Code.</p>
<h3 id="fast-package-manager">Fast package manager</h3>
<p>I previously wrote about how <a href="/posts/2019-08-17-linux-package-managers-are-slow/">Linux distribution package managers are too slow</a>.</p>
<p>distri’s package manager is extremely fast. Its main bottleneck is typically the network link, even at high speed links (I tested with a 100 Gbps link).</p>
<p>Its speed comes largely from an architecture which allows the package manager to
do less work. Specifically:</p>
<ol>
<li>
<p>Package images can be added atomically to the package store, so we can safely
skip <a href="https://manpages.debian.org/fsync.2"><code>fsync(2)</code></a>
. Corruption will be cleaned up
automatically, and durability is not important: if an interactive
installation is interrupted, the user can just repeat it, as it will be fresh
on their mind.</p>
</li>
<li>
<p>Because all packages are co-installable thanks to separate hierarchies, there
are no conflicts at the package store level, and no dependency resolution (an
optimization problem requiring <a href="https://research.swtch.com/version-sat">SAT
solving</a>) is required at all.
<br>
In exchange directories, we resolve conflicts by selecting the package with the
highest monotonically increasing distri revision number.</p>
</li>
<li>
<p>distri proves that we can build a useful Linux distribution <a href="/posts/2019-07-20-hooks-and-triggers/">entirely without
hooks and triggers</a>. Not having to
serialize hook execution allows us to download packages into the package
store with maximum concurrency.</p>
</li>
<li>
<p>Because we are using images instead of archives, we do not need to unpack
anything. This means installing a package is really just writing its package
image and metadata to the package store. Sequential writes are typically the
fastest kind of storage usage pattern.</p>
</li>
</ol>
<p>Fast installation also make other use-cases more bearable, such as creating disk
images, be it for testing them in <a href="https://manpages.debian.org/qemu.1"><code>qemu(1)</code></a>
, booting
them on real hardware from a USB drive, or for cloud providers such as Google
Cloud.</p>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Note:</strong> To saturate links above 1 Gbps, transfer packages without compression.</div>
  </div>
</aside>

<h3 id="fast-package-builder">Fast package builder</h3>
<p>Contrary to how distribution package builders are usually implemented, the
distri package builder does not actually install any packages into the build
environment.</p>
<p>Instead, distri makes available a filtered view of the package store (only
declared dependencies are available) at <code>/ro</code> in the build environment.</p>
<p>This means that even for large dependency trees, setting up a build environment
happens in a fraction of a second! Such a low latency really makes a difference
in how comfortable it is to iterate on distribution packages.</p>
<h3 id="package-stores">Package stores</h3>
<p>In distri, package images are installed from a remote <strong>package store</strong> into the
local system package store <code>/roimg</code>, which backs the <code>/ro</code> mount.</p>
<p>A package store is implemented as a directory of package images and their
associated metadata files.</p>
<p>You can easily make available a package store by using <code>distri export</code>.</p>
<p>To provide a mirror for your local network, you can periodically <code>distri update</code>
from the package store you want to mirror, and then <code>distri export</code> your local
copy. Special tooling (e.g. <code>debmirror</code> in Debian) is not required because
<code>distri install</code> is atomic (and <code>update</code> uses <code>install</code>).</p>
<p>Producing derivatives is easy: just add your own packages to a copy of the
package store.</p>
<p>The package store is intentionally kept simple to manage and distribute. Its
files could be exchanged via peer-to-peer file systems, or synchronized from an
offline medium.</p>
<h3 id="distris-first-release">distri’s first release</h3>
<p>distri works well enough to demonstrate the ideas explained above. I have
branched this state into <a href="https://github.com/distr1/distri/tree/jackherer">branch
<code>jackherer</code></a>, distri’s first
release code name. This way, I can keep experimenting in the distri repository
without breaking your installation.</p>
<p>From the branch contents, our autobuilder creates:</p>
<ol>
<li><a href="https://repo.distr1.org/distri/jackherer/img/">disk images</a>, which…</li>
</ol>
<ul>
<li>can be <a href="https://github.com/distr1/distri#run-distri-on-real-hardware">tested on real hardware</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-qemu">tested in qemu</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-virtualbox">tested in virtualbox</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-in-docker">tested in docker</a></li>
<li>can be <a href="https://github.com/distr1/distri#run-distri-on-google-cloud">tested on Google Cloud</a></li>
</ul>
<ol>
<li>
<p>a <a href="https://repo.distr1.org/distri/jackherer/pkg/">package repository</a>. Installations can pick up new packages with
<code>distri update</code>.</p>
</li>
<li>
<p><a href="https://repo.distr1.org/distri/jackherer/docs/">documentation for the release</a>.</p>
</li>
</ol>
<ul>
<li>Definitely check out the <a href="https://distr1.org/things-to-try/">“Cool things to
try”</a> README section.</li>
</ul>
<p>The project website can be found at <a href="https://distr1.org">https://distr1.org</a>. The website is just the
README for now, but we can improve that later.</p>
<p>The repository can be found at <a href="https://github.com/distr1/distri">https://github.com/distr1/distri</a></p>
<h3 id="project-outlook">Project outlook</h3>
<p>Right now, distri is mainly a vehicle for my spare-time Linux distribution
research. <strong>I don’t recommend anyone use distri for anything but research,</strong> and
there are no medium-term plans of that changing. At the very least, please
contact me before basing anything serious on distri so that we can talk about
limitations and expectations.</p>
<p>I expect the distri project to live for as long as I have blog posts to publish,
and we’ll see what happens afterwards. Note that this is a hobby for me: I will
continue to explore, at my own pace, parts that I find interesting.</p>
<p>My hope is that established distributions might get a useful idea or two from
distri.</p>
<h3 id="more-to-come">There’s more to come: subscribe to the distri feed</h3>
<p>I don’t want to make this post too long, but there is much more!</p>
<p>Please subscribe to the following URL in your feed reader to get all posts about
distri:</p>
<p><a href="https://michael.stapelberg.ch/posts/tags/distri/feed.xml">https://michael.stapelberg.ch/posts/tags/distri/feed.xml</a></p>
<p>Next in my queue are articles about hermetic packages and good package
maintainer experience (including declarative packaging).</p>
<h3 id="feedback-or-questions">Feedback or questions?</h3>
<p>I’d love to discuss these ideas in case you’re interested!</p>
<p>Please send feedback to the <a href="https://www.freelists.org/list/distri">distri mailing
list</a> so that everyone can participate!</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Linux package managers are slow]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-08-17-linux-package-managers-are-slow/"/>
    <id>https://michael.stapelberg.ch/posts/2019-08-17-linux-package-managers-are-slow/</id>
    <published>2019-08-17T18:27:00+02:00</published>
    <content type="html"><![CDATA[<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><p><strong>Pending feedback:</strong> <a href="http://allanmcrae.com/2020/10/distri-comparing-apples-and-oranges/">Allan McRae pointed
out</a> that I
should be more precise with my terminology: strictly speaking, <em>distributions</em>
are slow, and package managers are only part of the puzzle.</p>
<p>I’ll try to be clearer in future revisions/posts.</p>
</div>
  </div>
</aside>

<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><strong>Pending feedback:</strong> For a more accurate picture, <a href="https://twitter.com/mueslix/status/1311581199723368448">it would be good to take the
network out of the
picture</a>, or at least
measure and report network speed separately. Ideas/tips for an easy way very
welcome!</div>
  </div>
</aside>

<p>I measured how long the most popular Linux distribution’s package manager take
to install small and large packages (the
<a href="https://manpages.debian.org/ack.1p"><code>ack(1p)</code></a> source code search Perl script
and <a href="https://en.wikipedia.org/wiki/QEMU">qemu</a>, respectively).</p>
<p>Where required, my measurements include metadata updates such as transferring an
up-to-date package list. For me, requiring a metadata update is the more common
case, particularly on live systems or within Docker containers.</p>
<p>All measurements were taken on an <code>Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz</code>
running Docker 20.10.8 on Linux 5.13.10, backed by a Corsair Force MP600 NVMe
drive boasting many hundreds of MB/s write performance. The machine is located
in Zürich and connected to the Internet with a 1 Gigabit fiber connection, so
the expected top download speed is ≈115 MB/s.</p>
<p>See <a href="#appendix-d">Appendix D</a> for details on the measurement method and command
outputs.</p>
<h3 id="measurements">Measurements</h3>
<p>Keep in mind that these are one-time measurements. They should be indicative of
actual performance, but your experience may vary.</p>
<h4 id="ack-small-perl-program">ack (small Perl program)</h4>
<table>
  <thead>
      <tr>
          <th>distribution</th>
          <th>package manager</th>
          <th>data</th>
          <th>wall-clock time</th>
          <th>rate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Fedora</td>
          <td>dnf</td>
          <td>84 MB</td>
          <td>25s</td>
          <td>3.4 MB/s</td>
      </tr>
      <tr>
          <td>NixOS</td>
          <td>Nix</td>
          <td>15 MB</td>
          <td>7s</td>
          <td>2.3 MB/s</td>
      </tr>
      <tr>
          <td>Debian</td>
          <td>apt</td>
          <td>16 MB</td>
          <td>3s</td>
          <td>4.9 MB/s</td>
      </tr>
      <tr>
          <td>Arch Linux</td>
          <td>pacman</td>
          <td>25 MB</td>
          <td>1s</td>
          <td>18.4 MB/s</td>
      </tr>
      <tr>
          <td>Alpine</td>
          <td>apk</td>
          <td>10 MB</td>
          <td>1s</td>
          <td>11.9 MB/s</td>
      </tr>
  </tbody>
</table>
<h4 id="qemu-large-c-program">qemu (large C program)</h4>
<table>
  <thead>
      <tr>
          <th>distribution</th>
          <th>package manager</th>
          <th>data</th>
          <th>wall-clock time</th>
          <th>rate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Fedora</td>
          <td>dnf</td>
          <td>350 MB</td>
          <td>56s</td>
          <td>6.25 MB/s</td>
      </tr>
      <tr>
          <td>Debian</td>
          <td>apt</td>
          <td>256 MB</td>
          <td>39s</td>
          <td>6.5 MB/s</td>
      </tr>
      <tr>
          <td>NixOS</td>
          <td>Nix</td>
          <td>251 MB</td>
          <td>36s</td>
          <td>6.8 MB/s</td>
      </tr>
      <tr>
          <td>Arch Linux</td>
          <td>pacman</td>
          <td>128 MB</td>
          <td>10s</td>
          <td>12.1 MB/s</td>
      </tr>
      <tr>
          <td>Alpine</td>
          <td>apk</td>
          <td>34 MB</td>
          <td>1.8s</td>
          <td>18.6 MB/s</td>
      </tr>
  </tbody>
</table>
<p>(Looking for older measurements? See <a href="#appendix-b">Appendix B (2019)</a> or <a href="#appendix-c">Appendix C (2020)</a>).</p>
<p>The difference between the slowest and fastest package managers is 30x!</p>
<p>How can Alpine’s apk and Arch Linux’s pacman be an order of magnitude faster
than the rest? They are doing a lot less than the others, and more efficiently,
too.</p>
<h4 id="pain-point-too-much-metadata">Pain point: too much metadata</h4>
<p>For example, Fedora transfers a lot more data than others because its main
package list is 60 MB (compressed!) alone. Compare that with Alpine’s 734 KB
<code>APKINDEX.tar.gz</code>.</p>
<p>Of course the extra metadata which Fedora provides helps some use case,
otherwise they hopefully would have removed it altogether. The amount of
metadata seems excessive for the use case of installing a single package, which
I consider the main use-case of an interactive package manager.</p>
<p>I expect any modern Linux distribution to <strong>only transfer absolutely required
data</strong> to complete my task.</p>
<h4 id="pain-point-no-concurrency">Pain point: no concurrency</h4>
<p>Because they need to sequence executing arbitrary package maintainer-provided
code (hooks and triggers), all tested package managers need to install packages
sequentially (one after the other) instead of concurrently (all at the same
time).</p>
<p>In my blog post <a href="/posts/2019-07-20-hooks-and-triggers/">“Can we do without hooks and
triggers?”</a>, I outline that hooks and
triggers are not strictly necessary to build a working Linux distribution.</p>
<h3 id="thought-experiment-further-speed-ups">Thought experiment: further speed-ups</h3>
<p>Strictly speaking, the only required feature of a package manager is to make
available the package contents so that the package can be used: a program can be
started, a kernel module can be loaded, etc.</p>
<p>By only implementing what’s needed for this feature, and nothing more, a package
manager could likely beat <code>apk</code>’s performance. It could, for example:</p>
<ul>
<li>skip archive extraction by mounting file system images (like AppImage or snappy)</li>
<li>use compression which is light on CPU, as networks are fast (like <code>apk</code>)</li>
<li>skip fsync when it is safe to do so, i.e.:
<ul>
<li>package installations don’t modify system state</li>
<li>atomic package installation (e.g. an append-only package store)</li>
<li>automatically clean up the package store after crashes</li>
</ul>
</li>
</ul>
<h3 id="current-landscape">Current landscape</h3>
<p>Here’s a table outlining how the various package managers listed on Wikipedia’s
<a href="https://en.wikipedia.org/wiki/List_of_software_package_management_systems#Linux">list of software package management
systems</a>
fare:</p>
<table>
  <thead>
      <tr>
          <th>name</th>
          <th>scope</th>
          <th>package file format</th>
          <th>hooks/triggers</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AppImage</td>
          <td>apps</td>
          <td>image: ISO9660, SquashFS</td>
          <td>no</td>
      </tr>
      <tr>
          <td><a href="https://snapcraft.io/">snappy</a></td>
          <td>apps</td>
          <td>image: SquashFS</td>
          <td>yes: <a href="https://docs.snapcraft.io/build-snaps/hooks">hooks</a></td>
      </tr>
      <tr>
          <td>FlatPak</td>
          <td>apps</td>
          <td>archive: <a href="https://ostree.readthedocs.io/en/latest/">OSTree</a></td>
          <td>no</td>
      </tr>
      <tr>
          <td>0install</td>
          <td>apps</td>
          <td>archive: tar.bz2</td>
          <td>no</td>
      </tr>
      <tr>
          <td>nix, guix</td>
          <td>distro</td>
          <td>archive: nar.{bz2,xz}</td>
          <td><a href="https://github.com/NixOS/nixos/blob/master/modules/system/activation/activation-script.nix">activation script</a></td>
      </tr>
      <tr>
          <td>dpkg</td>
          <td>distro</td>
          <td>archive: tar.{gz,xz,bz2} in ar(1)</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>rpm</td>
          <td>distro</td>
          <td>archive: cpio.{bz2,lz,xz}</td>
          <td><a href="https://fedoraproject.org/wiki/Packaging:Scriptlets">scriptlets</a></td>
      </tr>
      <tr>
          <td>pacman</td>
          <td>distro</td>
          <td>archive: tar.xz</td>
          <td><a href="https://wiki.archlinux.org/index.php/PKGBUILD#install">install</a></td>
      </tr>
      <tr>
          <td>slackware</td>
          <td>distro</td>
          <td>archive: tar.{gz,xz}</td>
          <td>yes: doinst.sh</td>
      </tr>
      <tr>
          <td>apk</td>
          <td>distro</td>
          <td>archive: tar.gz</td>
          <td>yes: .post-install</td>
      </tr>
      <tr>
          <td>Entropy</td>
          <td>distro</td>
          <td>archive: tar.bz2</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>ipkg, opkg</td>
          <td>distro</td>
          <td>archive: tar{,.gz}</td>
          <td>yes</td>
      </tr>
  </tbody>
</table>
<h3 id="conclusion">Conclusion</h3>
<p>As per the <a href="#current-landscape">current landscape</a>, there is no
distribution-scoped package manager which uses images and leaves out hooks and
triggers, not even in smaller Linux distributions.</p>
<p>I think that space is really interesting, as it uses a minimal design to achieve
significant real-world speed-ups.</p>
<p>I have explored this idea in much more detail, and am happy to talk more about
it in my post <a href="/posts/2019-08-17-introducing-distri/">distri: a Linux distribution to research fast package
management</a>.</p>
<h3 id="appendix-a-related-work">Appendix A: related work</h3>
<p>There are a couple of recent developments going into the same direction:</p>
<ul>
<li><a href="http://0pointer.net/blog/revisiting-how-we-put-together-linux-systems.html">“Revisiting How We Put Together Linux Systems”</a> describes mounting app bundles</li>
<li><a href="https://android.googlesource.com/platform/system/apex/+/refs/heads/master/docs/README.md">Android Q uses ext4 loopback images</a></li>
<li>The Haiku Operating System’s package manager <a href="https://en.wikipedia.org/wiki/Haiku_Depot">Haiku
Depot</a> uses images</li>
</ul>
<h3 id="appendix-d">Appendix D: measurement details (2021)</h3>
<h4 id="ack">ack</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes almost 25 seconds to fetch and unpack 84 MB.
</summary>
<pre tabindex="0"><code>% docker run --security-opt=seccomp:unconfined -t -i fedora /bin/bash
[root@62d3cae2e2f9 /]# time dnf install -y ack
Fedora 35 - x86_64                         25 MB/s |  61 MB
Fedora 35 openh264 (From Cisco) - x86_64  3.5 kB/s | 2.5 kB
Fedora Modular 35 - x86_64                5.0 MB/s | 2.6 MB
Fedora 35 - x86_64 - Updates              6.0 MB/s | 9.3 MB
Fedora Modular 35 - x86_64 - Updates      4.1 MB/s | 3.3 MB
Dependencies resolved.
[…]
real	0m24.882s
user	0m17.377s
sys	0m0.835s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes a little under 7s to fetch and unpack 15 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
39e9186422ba:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -iA nixpkgs.ack&#39;
unpacking channels...
created 1 symlinks in user environment
installing &#39;perl5.34.0-ack-3.5.0&#39;
these paths will be fetched (15.78 MiB download, 86.82 MiB unpacked):
  /nix/store/11xpmmwy95396nkhih3qc3814lqhqb8f-libunistring-0.9.10
  /nix/store/1h18nl3gisw89znbzbmnxhd7jk20xlff-perl5.34.0-File-Next-1.18
  /nix/store/1mpxs3109cjrbhmi3q1vmvc0djz102pl-libidn2-2.3.2
  /nix/store/jr35z7n8jbv9q89my50vhyndqd3y541i-attr-2.5.1
  /nix/store/krc4xirbvjnff8m62snqdbayg46z5l5b-acl-2.3.1
  /nix/store/mij848h2x5wiqkwhg027byvmf9x3gx7y-glibc-2.33-50
  /nix/store/wq38iqzdh40dzfsndb927kh7y5bqh457-perl5.34.0-ack-3.5.0-man
  /nix/store/xyn0240zrpprnspg3n0fi8c8aw5bq0mr-coreutils-8.32
  /nix/store/y8r9ymbz59yjm1bwr3fdvd23jvcb2bzj-perl5.34.0-ack-3.5.0
  /nix/store/ypr273yvmr07n5n1w1gbcqnhpw7lbbvz-perl-5.34.0
copying path &#39;/nix/store/wq38iqzdh40dzfsndb927kh7y5bqh457-perl5.34.0-ack-3.5.0-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/11xpmmwy95396nkhih3qc3814lqhqb8f-libunistring-0.9.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1h18nl3gisw89znbzbmnxhd7jk20xlff-perl5.34.0-File-Next-1.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/1mpxs3109cjrbhmi3q1vmvc0djz102pl-libidn2-2.3.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/mij848h2x5wiqkwhg027byvmf9x3gx7y-glibc-2.33-50&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/jr35z7n8jbv9q89my50vhyndqd3y541i-attr-2.5.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/krc4xirbvjnff8m62snqdbayg46z5l5b-acl-2.3.1&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xyn0240zrpprnspg3n0fi8c8aw5bq0mr-coreutils-8.32&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ypr273yvmr07n5n1w1gbcqnhpw7lbbvz-perl-5.34.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y8r9ymbz59yjm1bwr3fdvd23jvcb2bzj-perl5.34.0-ack-3.5.0&#39; from &#39;https://cache.nixos.org&#39;...
building &#39;/nix/store/pwlxhy7kry56z6593rh397fc49x5avlw-user-environment.drv&#39;...
created 49 symlinks in user environment
real	0m 6.82s
user	0m 3.47s
sys	0m 2.11s
</code></pre></details>
<details>
<summary>
Debian’s apt takes about 3 seconds to fetch and unpack 16 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@40a3899b1f2f:/# time (apt update &amp;&amp; apt install -y ack-grep)
Get:1 http://deb.debian.org/debian sid InRelease [165 kB]
Get:2 http://deb.debian.org/debian sid/main amd64 Packages [8800 kB]
Fetched 8965 kB in 1s (9495 kB/s)
[…]
The following NEW packages will be installed:
  ack libfile-next-perl libgdbm-compat4 libgdbm6 libperl5.32 netbase perl perl-modules-5.32
0 upgraded, 8 newly installed, 0 to remove and 24 not upgraded.
Need to get 7479 kB of archives.
After this operation, 47.7 MB of additional disk space will be used.
[…]
real	0m3.260s
user	0m2.463s
sys	0m0.352s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes a little over 1s to fetch and unpack 25 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux:base
[root@9f6672688a64 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm ack)
:: Synchronizing package databases...
 core           138.8 KiB  1542 KiB/s
 extra         1569.8 KiB  26.9 MiB/s
 community        5.8 MiB  92.2 MiB/s
resolving dependencies...
looking for conflicting packages...

Packages (5) db-5.3.28-5  gdbm-1.22-1  perl-5.34.0-2  perl-file-next-1.18-3  ack-3.5.0-2

Total Download Size:   16.77 MiB
Total Installed Size:  66.21 MiB
[…]
real	0m1.403s
user	0m0.484s
sys	0m0.211s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes a little under 1 second to fetch and unpack 10 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
# time apk add ack
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
(1/4) Installing libbz2 (1.0.8-r1)
(2/4) Installing perl (5.32.1-r0)
(3/4) Installing perl-file-next (1.18-r2)
(4/4) Installing ack (3.5.0-r1)
Executing busybox-1.33.1-r3.trigger
OK: 43 MiB in 18 packages
real	0m 0.76s
user	0m 0.27s
sys	0m 0.09s
</code></pre></details>
<h4 id="qemu">qemu</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes about 1 minute to fetch and unpack 350 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i fedora /bin/bash
[root@6a52ecfc3afa /]# time dnf install -y qemu
Fedora 35 - x86_64                           15 MB/s |  61 MB
Fedora 35 openh264 (From Cisco) - x86_64    3.0 kB/s | 2.5 kB
Fedora Modular 35 - x86_64                  5.2 MB/s | 2.6 MB
Fedora 35 - x86_64 - Updates                6.6 MB/s | 9.3 MB
Fedora Modular 35 - x86_64 - Updates        2.2 MB/s | 3.3 MB
Dependencies resolved.
[…]

Total download size: 274 M
Downloading Packages:
[…]

real	0m56.031s
user	0m31.275s
sys	0m3.868s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes almost 36s to fetch and unpack 230 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
83971cf79f7e:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -iA nixpkgs.qemu&#39;
unpacking channels...
created 1 symlinks in user environment
installing &#39;qemu-6.1.0&#39;
these paths will be fetched (230.72 MiB download, 1424.84 MiB unpacked):
[…]
real	0m 36.55s
user	0m 19.83s
sys	0m 3.34s
</code></pre></details>
<details>
<summary>
Debian’s apt takes almost 39 seconds to fetch and unpack 256 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@b7cc25a927ab:/# time (apt update &amp;&amp; apt install -y qemu-system-x86)
Get:1 http://deb.debian.org/debian sid InRelease [146 kB]
Get:2 http://deb.debian.org/debian sid/main amd64 Packages [8400 kB]
Fetched 8965 kB in 1s (9048 kB/s)
[…]
Fetched 247 MB in 4s (64.9 MB/s)
[…]
real	0m38.875s
user	0m21.282s
sys	0m5.298s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes about 10s to fetch and unpack 128 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux:base
[root@58c78bda08e8 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm qemu)
:: Synchronizing package databases...
 core          138.7 KiB  1541 KiB/s
 extra        1569.8 KiB  35.7 MiB/s
 community       5.8 MiB  92.2 MiB/s
[…]
Total Download Size:   118.97 MiB
Total Installed Size:  586.68 MiB
[…]
real	0m10.542s
user	0m3.092s
sys	0m1.569s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes only about 1.8 seconds to fetch and unpack 34 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
/ # time apk add qemu-system-x86_64
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
[…]
OK: 281 MiB in 66 packages
real	0m 1.83s
user	0m 0.77s
sys	0m 0.24s
</code></pre></details>
<h3 id="appendix-c">Appendix C: measurement details (2020)</h3>
<h4 id="ack-1">ack</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes almost 33 seconds to fetch and unpack 114 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i fedora /bin/bash
[root@62d3cae2e2f9 /]# time dnf install -y ack
Fedora 32 openh264 (From Cisco) - x86_64     1.9 kB/s | 2.5 kB     00:01
Fedora Modular 32 - x86_64                   6.8 MB/s | 4.9 MB     00:00
Fedora Modular 32 - x86_64 - Updates         5.6 MB/s | 3.7 MB     00:00
Fedora 32 - x86_64 - Updates                 9.9 MB/s |  23 MB     00:02
Fedora 32 - x86_64                            39 MB/s |  70 MB     00:01
[…]
real	0m32.898s
user	0m25.121s
sys	0m1.408s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes a little over 5s to fetch and unpack 15 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
39e9186422ba:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -iA nixpkgs.ack&#39;
unpacking channels...
created 1 symlinks in user environment
installing &#39;perl5.32.0-ack-3.3.1&#39;
these paths will be fetched (15.55 MiB download, 85.51 MiB unpacked):
  /nix/store/34l8jdg76kmwl1nbbq84r2gka0kw6rc8-perl5.32.0-ack-3.3.1-man
  /nix/store/9df65igwjmf2wbw0gbrrgair6piqjgmi-glibc-2.31
  /nix/store/9fd4pjaxpjyyxvvmxy43y392l7yvcwy1-perl5.32.0-File-Next-1.18
  /nix/store/czc3c1apx55s37qx4vadqhn3fhikchxi-libunistring-0.9.10
  /nix/store/dj6n505iqrk7srn96a27jfp3i0zgwa1l-acl-2.2.53
  /nix/store/ifayp0kvijq0n4x0bv51iqrb0yzyz77g-perl-5.32.0
  /nix/store/w9wc0d31p4z93cbgxijws03j5s2c4gyf-coreutils-8.31
  /nix/store/xim9l8hym4iga6d4azam4m0k0p1nw2rm-libidn2-2.3.0
  /nix/store/y7i47qjmf10i1ngpnsavv88zjagypycd-attr-2.4.48
  /nix/store/z45mp61h51ksxz28gds5110rf3wmqpdc-perl5.32.0-ack-3.3.1
copying path &#39;/nix/store/34l8jdg76kmwl1nbbq84r2gka0kw6rc8-perl5.32.0-ack-3.3.1-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/czc3c1apx55s37qx4vadqhn3fhikchxi-libunistring-0.9.10&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9fd4pjaxpjyyxvvmxy43y392l7yvcwy1-perl5.32.0-File-Next-1.18&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/xim9l8hym4iga6d4azam4m0k0p1nw2rm-libidn2-2.3.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/9df65igwjmf2wbw0gbrrgair6piqjgmi-glibc-2.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/y7i47qjmf10i1ngpnsavv88zjagypycd-attr-2.4.48&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/dj6n505iqrk7srn96a27jfp3i0zgwa1l-acl-2.2.53&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/w9wc0d31p4z93cbgxijws03j5s2c4gyf-coreutils-8.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/ifayp0kvijq0n4x0bv51iqrb0yzyz77g-perl-5.32.0&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/z45mp61h51ksxz28gds5110rf3wmqpdc-perl5.32.0-ack-3.3.1&#39; from &#39;https://cache.nixos.org&#39;...
building &#39;/nix/store/m0rl62grplq7w7k3zqhlcz2hs99y332l-user-environment.drv&#39;...
created 49 symlinks in user environment
real	0m 5.60s
user	0m 3.21s
sys	0m 1.66s
</code></pre></details>
<details>
<summary>
Debian’s apt takes almost 10 seconds to fetch and unpack 16 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@1996bb94a2d1:/# time (apt update &amp;&amp; apt install -y ack-grep)
Get:1 http://deb.debian.org/debian sid InRelease [146 kB]
Get:2 http://deb.debian.org/debian sid/main amd64 Packages [8400 kB]
Fetched 8546 kB in 1s (8088 kB/s)
[…]
The following NEW packages will be installed:
  ack libfile-next-perl libgdbm-compat4 libgdbm6 libperl5.30 netbase perl perl-modules-5.30
0 upgraded, 8 newly installed, 0 to remove and 23 not upgraded.
Need to get 7341 kB of archives.
After this operation, 46.7 MB of additional disk space will be used.
[…]
real	0m9.544s
user	0m2.839s
sys	0m0.775s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes a little under 3s to fetch and unpack 6.5 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux/base
[root@9f6672688a64 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm ack)
:: Synchronizing package databases...
 core            130.8 KiB  1090 KiB/s 00:00
 extra          1655.8 KiB  3.48 MiB/s 00:00
 community         5.2 MiB  6.11 MiB/s 00:01
resolving dependencies...
looking for conflicting packages...

Packages (2) perl-file-next-1.18-2  ack-3.4.0-1

Total Download Size:   0.07 MiB
Total Installed Size:  0.19 MiB
[…]
real	0m2.936s
user	0m0.375s
sys	0m0.160s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes a little over 1 second to fetch and unpack 10 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/4) Installing libbz2 (1.0.8-r1)
(2/4) Installing perl (5.30.3-r0)
(3/4) Installing perl-file-next (1.18-r0)
(4/4) Installing ack (3.3.1-r0)
Executing busybox-1.31.1-r16.trigger
OK: 43 MiB in 18 packages
real	0m 1.24s
user	0m 0.40s
sys	0m 0.15s
</code></pre></details>
<h4 id="qemu-1">qemu</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes over 4 minutes to fetch and unpack 226 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i fedora /bin/bash
[root@6a52ecfc3afa /]# time dnf install -y qemu
Fedora 32 openh264 (From Cisco) - x86_64     3.1 kB/s | 2.5 kB     00:00
Fedora Modular 32 - x86_64                   6.3 MB/s | 4.9 MB     00:00
Fedora Modular 32 - x86_64 - Updates         6.0 MB/s | 3.7 MB     00:00
Fedora 32 - x86_64 - Updates                 334 kB/s |  23 MB     01:10
Fedora 32 - x86_64                            33 MB/s |  70 MB     00:02
[…]

Total download size: 181 M
Downloading Packages:
[…]

real	4m37.652s
user	0m38.239s
sys	0m6.321s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes almost 34s to fetch and unpack 180 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
83971cf79f7e:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -iA nixpkgs.qemu&#39;
unpacking channels...
created 1 symlinks in user environment
installing &#39;qemu-5.1.0&#39;
these paths will be fetched (180.70 MiB download, 1146.92 MiB unpacked):
[…]
real	0m 33.64s
user	0m 16.96s
sys	0m 3.05s
</code></pre></details>
<details>
<summary>
Debian’s apt takes over 95 seconds to fetch and unpack 224 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@b7cc25a927ab:/# time (apt update &amp;&amp; apt install -y qemu-system-x86)
Get:1 http://deb.debian.org/debian sid InRelease [146 kB]
Get:2 http://deb.debian.org/debian sid/main amd64 Packages [8400 kB]
Fetched 8546 kB in 1s (5998 kB/s)
[…]
Fetched 216 MB in 43s (5006 kB/s)
[…]
real	1m25.375s
user	0m29.163s
sys	0m12.835s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes almost 44s to fetch and unpack 142 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux/base
[root@58c78bda08e8 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm qemu)
:: Synchronizing package databases...
 core          130.8 KiB  1055 KiB/s 00:00
 extra        1655.8 KiB  3.70 MiB/s 00:00
 community       5.2 MiB  7.89 MiB/s 00:01
[…]
Total Download Size:   135.46 MiB
Total Installed Size:  661.05 MiB
[…]
real	0m43.901s
user	0m4.980s
sys	0m2.615s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes only about 2.4 seconds to fetch and unpack 26 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
/ # time apk add qemu-system-x86_64
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
[…]
OK: 78 MiB in 95 packages
real	0m 2.43s
user	0m 0.46s
sys	0m 0.09s
</code></pre></details>
<h3 id="appendix-b">Appendix B: measurement details (2019)</h3>
<h4 id="ack-2">ack</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes almost 30 seconds to fetch and unpack 107 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i fedora /bin/bash
[root@722e6df10258 /]# time dnf install -y ack
Fedora Modular 30 - x86_64            4.4 MB/s | 2.7 MB     00:00
Fedora Modular 30 - x86_64 - Updates  3.7 MB/s | 2.4 MB     00:00
Fedora 30 - x86_64 - Updates           17 MB/s |  19 MB     00:01
Fedora 30 - x86_64                     31 MB/s |  70 MB     00:02
[…]
Install  44 Packages

Total download size: 13 M
Installed size: 42 M
[…]
real	0m29.498s
user	0m22.954s
sys	0m1.085s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes 14s to fetch and unpack 15 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
39e9186422ba:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -i perl5.28.2-ack-2.28&#39;
unpacking channels...
created 2 symlinks in user environment
installing &#39;perl5.28.2-ack-2.28&#39;
these paths will be fetched (14.91 MiB download, 80.83 MiB unpacked):
  /nix/store/57iv2vch31v8plcjrk97lcw1zbwb2n9r-perl-5.28.2
  /nix/store/89gi8cbp8l5sf0m8pgynp2mh1c6pk1gk-attr-2.4.48
  /nix/store/gkrpl3k6s43fkg71n0269yq3p1f0al88-perl5.28.2-ack-2.28-man
  /nix/store/iykxb0bmfjmi7s53kfg6pjbfpd8jmza6-glibc-2.27
  /nix/store/k8lhqzpaaymshchz8ky3z4653h4kln9d-coreutils-8.31
  /nix/store/svgkibi7105pm151prywndsgvmc4qvzs-acl-2.2.53
  /nix/store/x4knf14z1p0ci72gl314i7vza93iy7yc-perl5.28.2-File-Next-1.16
  /nix/store/zfj7ria2kwqzqj9dh91kj9kwsynxdfk0-perl5.28.2-ack-2.28
copying path &#39;/nix/store/gkrpl3k6s43fkg71n0269yq3p1f0al88-perl5.28.2-ack-2.28-man&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/iykxb0bmfjmi7s53kfg6pjbfpd8jmza6-glibc-2.27&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/x4knf14z1p0ci72gl314i7vza93iy7yc-perl5.28.2-File-Next-1.16&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/89gi8cbp8l5sf0m8pgynp2mh1c6pk1gk-attr-2.4.48&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/svgkibi7105pm151prywndsgvmc4qvzs-acl-2.2.53&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/k8lhqzpaaymshchz8ky3z4653h4kln9d-coreutils-8.31&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/57iv2vch31v8plcjrk97lcw1zbwb2n9r-perl-5.28.2&#39; from &#39;https://cache.nixos.org&#39;...
copying path &#39;/nix/store/zfj7ria2kwqzqj9dh91kj9kwsynxdfk0-perl5.28.2-ack-2.28&#39; from &#39;https://cache.nixos.org&#39;...
building &#39;/nix/store/q3243sjg91x1m8ipl0sj5gjzpnbgxrqw-user-environment.drv&#39;...
created 56 symlinks in user environment
real	0m 14.02s
user	0m 8.83s
sys	0m 2.69s
</code></pre></details>
<details>
<summary>
Debian’s apt takes almost 10 seconds to fetch and unpack 16 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@b7cc25a927ab:/# time (apt update &amp;&amp; apt install -y ack-grep)
Get:1 http://cdn-fastly.deb.debian.org/debian sid InRelease [233 kB]
Get:2 http://cdn-fastly.deb.debian.org/debian sid/main amd64 Packages [8270 kB]
Fetched 8502 kB in 2s (4764 kB/s)
[…]
The following NEW packages will be installed:
  ack ack-grep libfile-next-perl libgdbm-compat4 libgdbm5 libperl5.26 netbase perl perl-modules-5.26
The following packages will be upgraded:
  perl-base
1 upgraded, 9 newly installed, 0 to remove and 60 not upgraded.
Need to get 8238 kB of archives.
After this operation, 42.3 MB of additional disk space will be used.
[…]
real	0m9.096s
user	0m2.616s
sys	0m0.441s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes a little over 3s to fetch and unpack 6.5 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux/base
[root@9604e4ae2367 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm ack)
:: Synchronizing package databases...
 core            132.2 KiB  1033K/s 00:00
 extra          1629.6 KiB  2.95M/s 00:01
 community         4.9 MiB  5.75M/s 00:01
[…]
Total Download Size:   0.07 MiB
Total Installed Size:  0.19 MiB
[…]
real	0m3.354s
user	0m0.224s
sys	0m0.049s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes only about 1 second to fetch and unpack 10 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
/ # time apk add ack
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
(1/4) Installing perl-file-next (1.16-r0)
(2/4) Installing libbz2 (1.0.6-r7)
(3/4) Installing perl (5.28.2-r1)
(4/4) Installing ack (3.0.0-r0)
Executing busybox-1.30.1-r2.trigger
OK: 44 MiB in 18 packages
real	0m 0.96s
user	0m 0.25s
sys	0m 0.07s
</code></pre></details>
<h4 id="qemu-2">qemu</h4>
<p>You can expand each of these:</p>
<details>
<summary>
Fedora’s dnf takes over a minute to fetch and unpack 266 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i fedora /bin/bash
[root@722e6df10258 /]# time dnf install -y qemu
Fedora Modular 30 - x86_64            3.1 MB/s | 2.7 MB     00:00
Fedora Modular 30 - x86_64 - Updates  2.7 MB/s | 2.4 MB     00:00
Fedora 30 - x86_64 - Updates           20 MB/s |  19 MB     00:00
Fedora 30 - x86_64                     31 MB/s |  70 MB     00:02
[…]
Install  262 Packages
Upgrade    4 Packages

Total download size: 172 M
[…]
real	1m7.877s
user	0m44.237s
sys	0m3.258s
</code></pre></details>
<details>
<summary>
NixOS’s Nix takes 38s to fetch and unpack 262 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i nixos/nix
39e9186422ba:/# time sh -c &#39;nix-channel --update &amp;&amp; nix-env -i qemu-4.0.0&#39;
unpacking channels...
created 2 symlinks in user environment
installing &#39;qemu-4.0.0&#39;
these paths will be fetched (262.18 MiB download, 1364.54 MiB unpacked):
[…]
real	0m 38.49s
user	0m 26.52s
sys	0m 4.43s
</code></pre></details>
<details>
<summary>
Debian’s apt takes 51 seconds to fetch and unpack 159 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i debian:sid
root@b7cc25a927ab:/# time (apt update &amp;&amp; apt install -y qemu-system-x86)
Get:1 http://cdn-fastly.deb.debian.org/debian sid InRelease [149 kB]
Get:2 http://cdn-fastly.deb.debian.org/debian sid/main amd64 Packages [8426 kB]
Fetched 8574 kB in 1s (6716 kB/s)
[…]
Fetched 151 MB in 2s (64.6 MB/s)
[…]
real	0m51.583s
user	0m15.671s
sys	0m3.732s
</code></pre></details>
<details>
<summary>
Arch Linux’s pacman takes 1m2s to fetch and unpack 124 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i archlinux/base
[root@9604e4ae2367 /]# time (pacman -Sy &amp;&amp; pacman -S --noconfirm qemu)
:: Synchronizing package databases...
 core       132.2 KiB   751K/s 00:00
 extra     1629.6 KiB  3.04M/s 00:01
 community    4.9 MiB  6.16M/s 00:01
[…]
Total Download Size:   123.20 MiB
Total Installed Size:  587.84 MiB
[…]
real	1m2.475s
user	0m9.272s
sys	0m2.458s
</code></pre></details>
<details>
<summary>
Alpine’s apk takes only about 2.4 seconds to fetch and unpack 26 MB.
</summary>
<pre tabindex="0"><code>% docker run -t -i alpine
/ # time apk add qemu-system-x86_64
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.10/community/x86_64/APKINDEX.tar.gz
[…]
OK: 78 MiB in 95 packages
real	0m 2.43s
user	0m 0.46s
sys	0m 0.09s
</code></pre></details>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Linux distributions: Can we do without hooks and triggers?]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-07-20-hooks-and-triggers/"/>
    <id>https://michael.stapelberg.ch/posts/2019-07-20-hooks-and-triggers/</id>
    <published>2019-07-20T00:00:00+00:00</published>
    <content type="html"><![CDATA[<p>Hooks are an extension feature provided by all package managers that are used in
larger Linux distributions. For example, Debian uses apt, which has various
<a href="https://www.debian.org/doc/debian-policy/ap-flowcharts.html">maintainer
scripts</a>. Fedora
uses rpm, which has
<a href="https://fedoraproject.org/wiki/Packaging:Scriptlets">scriptlets</a>. Different
package managers use different names for the concept, but all of them offer
package maintainers the ability to run arbitrary code during package
installation and upgrades. Example hook use cases include adding daemon user
accounts to your system (e.g. <code>postgres</code>), or generating/updating cache files.</p>
<p>Triggers are a kind of hook which run when <em>other</em> packages are installed. For
example, on Debian, the <a href="https://manpages.debian.org/man.1"><code>man(1)</code></a> package
comes with a trigger which regenerates the search database index whenever any
package installs a manpage. When, for example, the
<a href="https://manpages.debian.org/nginx.8"><code>nginx(8)</code></a> package is installed, a
trigger provided by the <a href="https://manpages.debian.org/man.1"><code>man(1)</code></a> package
runs.</p>
<p>Over the past few decades, Open Source software has become more and more
uniform: instead of each piece of software defining its own rules, a small
number of build systems are now widely adopted.</p>
<p>Hence, I think it makes sense to revisit whether offering extension via hooks
and triggers is a net win or net loss.</p>
<h3 id="hooks-preclude-concurrent-package-installation">Hooks preclude concurrent package installation</h3>
<p>Package managers commonly can make very little assumptions about what hooks do,
what preconditions they require, and which conflicts might be caused by running
multiple package’s hooks concurrently.</p>
<p>Hence, package managers cannot concurrently install packages. At least the
hook/trigger part of the installation needs to happen in sequence.</p>
<p>While it seems technically feasible to retrofit package manager hooks with
concurrency primitives such as locks for mutual exclusion between different hook
processes, the required overhaul of all hooks¹ seems like such a daunting task
that it might be better to just get rid of the hooks instead. Only deleting code
frees you from the burden of maintenance, automated testing and debugging.</p>
<p>① In Debian, there are 8620 non-generated maintainer scripts, as reported by
<code>find shard*/src/*/debian -regex &quot;.*\(pre\|post\)\(inst\|rm\)$&quot;</code> on a Debian
Code Search instance.</p>
<h3 id="triggers-slow-down-installingupdating-other-packages">Triggers slow down installing/updating other packages</h3>
<p>Personally, I never use the
<a href="https://manpages.debian.org/apropos.1"><code>apropos(1)</code></a> command, so I don’t
appreciate the <a href="https://manpages.debian.org/man.1"><code>man(1)</code></a> package’s trigger
which updates the database used by
<a href="https://manpages.debian.org/apropos.1"><code>apropos(1)</code></a>. The process takes a long
time and, because hooks and triggers must be executed serially (see previous
section), blocks my installation or update.</p>
<p>When I tell people this, they are often surprised to learn about the existance
of the <a href="https://manpages.debian.org/apropos.1"><code>apropos(1)</code></a> command. I suggest
adopting an opt-in model.</p>
<h3 id="unnecessary-work-if-programs-are-not-used-between-updates">Unnecessary work if programs are not used between updates</h3>
<p>Hooks run when packages are installed. If a package’s contents are not used
between two updates, running the hook in the first update could have been
skipped. Running the hook lazily when the package contents are used reduces
unnecessary work.</p>
<p>As a welcome side-effect, lazy hook evaluation automatically makes the hook work
in operating system images, such as live USB thumb drives or SD card images for
the Raspberry Pi. Such images must not ship the same crypto keys (e.g. OpenSSH
host keys) to all machines, but instead generate a different key on each
machine.</p>
<p>Why do users keep packages installed they don’t use? It’s extra work to remember
and clean up those packages after use. Plus, users might not realize or value
that having fewer packages installed has benefits such as faster updates.</p>
<p>I can also imagine that there are people for whom the cost of re-installing
packages incentivizes them to just keep packages installed—you never know when
you might need the program again…</p>
<h3 id="implemented-in-an-interpreted-language">Implemented in an interpreted language</h3>
<p>While working on hermetic packages (more on that in another blog post), where
the contained programs are started with modified environment variables
(e.g. <code>PATH</code>) via a wrapper bash script, I noticed that the overhead of those
wrapper bash scripts quickly becomes significant. For example, when using the
excellent <a href="https://magit.vc/">magit</a> interface for Git in Emacs, I encountered
second-long delays² when using hermetic packages compared to standard
packages. Re-implementing wrappers in a compiled language provided a significant
speed-up.</p>
<p>Similarly, getting rid of an extension point which mandates using shell scripts
allows us to build an efficient and fast implementation of a predefined set of
primitives, where you can reason about their effects and interactions.</p>
<p>② magit needs to run git a few times for displaying the full status, so small
overhead quickly adds up.</p>
<h3 id="incentivizing-more-upstream-standardization">Incentivizing more upstream standardization</h3>
<p>Hooks are an escape hatch for distribution maintainers to express anything which
their packaging system cannot express.</p>
<p>Distributions should only rely on well-established interfaces such as autoconf’s
classic <code>./configure &amp;&amp; make &amp;&amp; make install</code> (including commonly used flags) to
build a distribution package. Integrating upstream software into a distribution
should not require custom hooks. For example, instead of requiring a hook which
updates a cache of schema files, the library used to interact with those files
should transparently (re-)generate the cache or fall back to a slower code path.</p>
<p>Distribution maintainers are hard to come by, so we should value their time. In
particular, there is a 1:n relationship of packages to distribution package
maintainers (software is typically available in multiple Linux distributions),
so it makes sense to spend the work in the 1 and have the n benefit.</p>
<h3 id="can-we-do-without-them">Can we do without them?</h3>
<p>If we want to get rid of hooks, we need another mechanism to achieve what we
currently achieve with hooks.</p>
<p>If the hook is not specific to the package, it can be moved to the package
manager. The desired system state should either be derived from the package
contents (e.g. required system users can be discovered from systemd service
files) or declaratively specified in the package build instructions—more on that
in another blog post. This turns hooks (arbitrary code) into configuration,
which allows the package manager to collapse and sequence the required state
changes. E.g., when 5 packages are installed which each need a new system user,
the package manager could update <code>/etc/passwd</code> just once.</p>
<p>If the hook is specific to the package, it should be moved into the package
contents. This typically means moving the functionality into the program start
(or the systemd service file if we are talking about a daemon). If (while?)
upstream is not convinced, you can either wrap the program or patch it. Note
that this case is relatively rare: I have worked with hundreds of packages and
the only package-specific functionality I came across was automatically
generating host keys before starting OpenSSH’s
<a href="https://manpages.debian.org/sshd.8"><code>sshd(8)</code></a>³.</p>
<p>There is one exception where moving the hook doesn’t work: packages which modify
state outside of the system, such as bootloaders or kernel images.</p>
<p>③ Even that can be moved out of a package-specific hook, <a href="https://src.fedoraproject.org/rpms/openssh/blob/30922f629cc135e3233e263d5e3eb346f9251c4e/f/sshd-keygen%40.service">as Fedora
demonstrates</a>.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Global state modifications performed as part of package installation today use
hooks, an overly expressive extension mechanism.</p>
<p>Instead, all modifications should be driven by configuration. This is feasible
because there are only a few different kinds of desired state
modifications. This makes it possible for package managers to optimize package
installation.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Optional dependencies don’t work]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-05-23-optional-dependencies/"/>
    <id>https://michael.stapelberg.ch/posts/2019-05-23-optional-dependencies/</id>
    <published>2019-05-23T00:00:00+00:00</published>
    <content type="html"><![CDATA[<p>In the i3 projects, we have always tried hard to avoid optional
dependencies. There are a number of reasons behind it, and as I have recently
encountered some of the downsides of optional dependencies firsthand, I
summarized my thoughts in this article.</p>
<h3 id="what-is-a-compile-time-optional-dependency">What is a (compile-time) optional dependency?</h3>
<p>When building software from source, most programming languages and build systems
support conditional compilation: different parts of the source code are compiled
based on certain conditions.</p>
<p>An optional dependency is conditional compilation hooked up directly to a knob
(e.g. command line flag, configuration file, …), with the effect that the
software can now be built without an otherwise required dependency.</p>
<p>Let’s walk through a few issues with optional dependencies.</p>
<h3 id="inconsistent-experience-in-different-environments">Inconsistent experience in different environments</h3>
<p>Software is usually not built by end users, but by packagers, at least when we
are talking about Open Source.</p>
<p>Hence, end users don’t see the knob for the optional dependency, they are just
presented with the fait accompli: their version of the software behaves
differently than other versions of the same software.</p>
<p>Depending on the kind of software, this situation can be made obvious to the
user: for example, if the optional dependency is needed to print documents, the
program can produce an appropriate error message when the user tries to print a
document.</p>
<p>Sometimes, this isn’t possible: when i3 introduced an optional dependency on
cairo and pangocairo, the behavior itself (rendering window titles) worked in
all configurations, but non-ASCII characters might break depending on whether i3
was compiled with cairo.</p>
<p>For users, it is frustrating to only discover in conversation that a program has
a feature that the user is interested in, but it’s not available on their
computer. For support, this situation can be hard to detect, and even harder to
resolve to the user’s satisfaction.</p>
<h3 id="packaging-is-more-complicated">Packaging is more complicated</h3>
<p>Unfortunately, many build systems don’t stop the build when optional
dependencies are not present. Instead, you sometimes end up with a broken build,
or, even worse: with a successful build that does not work correctly at runtime.</p>
<p>This means that packagers need to closely examine the build output to know which
dependencies to make available. In the best case, there is a summary of
available and enabled options, clearly outlining what this build will
contain. In the worst case, you need to infer the features from the checks that
are done, or work your way through the <code>--help</code> output.</p>
<p>The better alternative is to configure your build system such that it stops when
<em>any</em> dependency was not found, and thereby have packagers acknowledge each
optional dependency by explicitly disabling the option.</p>
<h3 id="untested-code-paths-bit-rot">Untested code paths bit rot</h3>
<p>Code paths which are not used will inevitably bit rot. If you have optional
dependencies, you need to test both the code path without the dependency and the
code path with the dependency. It doesn’t matter whether the tests are automated
or manual, the test matrix must cover both paths.</p>
<p>Interestingly enough, this principle seems to apply to all kinds of software
projects (but it slows down as change slows down): one might think that
important Open Source building blocks should have enough users to cover all
sorts of configurations.</p>
<p>However, consider this example: building cairo without libxrender results in all
GTK application windows, menus, etc. being displayed as empty grey
surfaces. Cairo does not fail to build without libxrender, but the code path
clearly is broken without libxrender.</p>
<h3 id="can-we-do-without-them">Can we do without them?</h3>
<p>I’m not saying optional dependencies should <em>never</em> be used. In fact, for
bootstrapping, disabling dependencies can save a lot of work and can sometimes
allow breaking circular dependencies. For example, in an early bootstrapping
stage, binutils can be compiled with <code>--disable-nls</code> to disable
internationalization.</p>
<p>However, optional dependencies are broken so often that I conclude they are
overused. Read on and see for yourself whether you would rather commit to best
practices or not introduce an optional dependency.</p>
<h3 id="best-practices">Best practices</h3>
<p>If you do decide to make dependencies optional, please:</p>
<ol>
<li>Set up automated testing for <strong>all</strong> code path combinations.</li>
<li>Fail the build until packagers explicitly pass a <code>--disable</code> flag.</li>
<li>Tell users their version is missing a dependency at runtime, e.g. in <code>--version</code>.</li>
</ol>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Winding down my Debian involvement]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-03-10-debian-winding-down/"/>
    <id>https://michael.stapelberg.ch/posts/2019-03-10-debian-winding-down/</id>
    <published>2019-03-10T00:00:00+00:00</published>
    <content type="html"><![CDATA[<p>This post is hard to write, both in the emotional sense but also in the “I would
have written a shorter letter, but I didn’t have the time” sense. Hence, please
assume the best of intentions when reading it—it is not my intention to make
anyone feel bad about their contributions, but rather to provide some insight
into why my frustration level ultimately exceeded the threshold.</p>
<p>Debian has been in my life for well over 10 years at this point.</p>
<p>A few weeks ago, I have visited some old friends at the Zürich Debian meetup
after a multi-year period of absence. On my bike ride home, it occurred to me
that the topics of our discussions had remarkable overlap with my last visit. We
had a discussion about the merits of systemd, which took a detour to respect in
open source communities, returned to processes in Debian and eventually
culminated in democracies and their theoretical/practical failings. Admittedly,
that last one might be a Swiss thing.</p>
<p>I say this not to knock on the Debian meetup, but because it prompted me to
reflect on what feelings Debian is invoking lately and whether it’s still a good
fit for me.</p>
<p>So I’m finally making a decision that I should have made a long time ago: I am
winding down my involvement in Debian to a minimum.</p>
<h2 id="what-does-this-mean">What does this mean?</h2>
<p>Over the coming weeks, I will:</p>
<ul>
<li>transition packages to be team-maintained where it makes sense</li>
<li>remove myself from the <code>Uploaders</code> field on packages with other maintainers</li>
<li>orphan packages where I am the sole maintainer</li>
</ul>
<p>I will try to keep up best-effort maintenance of the
<a href="https://manpages.debian.org/">manpages.debian.org</a> service and the
<a href="https://codesearch.debian.net/">codesearch.debian.net</a> service, but any help
would be much appreciated.</p>
<p>For all intents and purposes, please treat me as permanently on vacation. I will
try to be around for administrative issues (e.g. permission transfers) and
questions addressed directly to me, permitted they are easy enough to answer.</p>
<h2 id="why">Why?</h2>
<p>When I joined Debian, I was still studying, i.e. I had luxurious amounts of
spare time. Now, over 5 years of full time work later, my day job taught me a
lot, both about what works in large software engineering projects and how I
personally like my computer systems. I am very conscious of how I spend the
little spare time that I have these days.</p>
<p>The following sections each deal with what I consider a major pain point, in no
particular order. Some of them influence each other—for example, if changes
worked better, we could have a chance at transitioning packages to be more
easily machine readable.</p>
<h3 id="change-process-in-debian">Change process in Debian</h3>
<p>The last few years, my current team at work conducted various smaller and larger
refactorings across the entire code base (touching thousands of projects), so we
have learnt a lot of valuable lessons about how to effectively do these
changes. It irks me that Debian works almost the opposite way in every regard. I
appreciate that every organization is different, but I think a lot of my points
do actually apply to Debian.</p>
<p>In Debian, packages are nudged in the right direction by a document called the
<a href="https://www.debian.org/doc/debian-policy/">Debian Policy</a>, or its programmatic
embodiment, lintian.</p>
<p>While it is great to have a lint tool (for quick, local/offline feedback), it is
even better to not require a lint tool at all. The team conducting the change
(e.g. the C++ team introduces a new hardening flag for all packages) should be
able to do their work transparent to me.</p>
<p>Instead, currently, all packages become lint-unclean, all maintainers need to
read up on what the new thing is, how it might break, whether/how it affects
them, manually run some tests, and finally decide to opt in. This causes a lot
of overhead and manually executed mechanical changes across packages.</p>
<p>Notably, the <strong>cost of each change</strong> is distributed onto the package maintainers in
the Debian model. At work, we have found that the opposite works better: if the
team behind the change is put in power to do the change for as many users as
possible, they can be significantly more efficient at it, which reduces the
total cost and time a lot. Of course, exceptions (e.g. a large project abusing a
language feature) should still be taken care of by the respective owners, but
the important bit is that the default should be the other way around.</p>
<p>Debian is <strong>lacking tooling for large changes</strong>: it is hard to programmatically
deal with packages and repositories (see the section below). The closest to
“sending out a change for review” is to open a bug report with an attached
patch. I thought the workflow for accepting a change from a bug report was too
complicated and started <a href="/posts/2016-07-17-mergebot/">mergebot</a>, but only Guido
ever signaled interest in the project.</p>
<p>Culturally, reviews and reactions are slow. There are no deadlines. I literally
sometimes get emails notifying me that a patch I sent out a few years ago (!!)
is now merged. This turns projects from a small number of weeks into many years,
which is a huge demotivator for me.</p>
<p>Interestingly enough, you can see artifacts of the slow online activity manifest
itself in the offline culture as well: I don’t want to be discussing systemd’s
merits 10 years after I first heard about it.</p>
<p>Lastly, changes can easily be slowed down significantly by holdouts who refuse
to collaborate. My canonical example for this is rsync, whose maintainer refused
my patches to make the package use debhelper purely out of personal preference.</p>
<p>Granting so much personal freedom to individual maintainers prevents us as a
project from raising the abstraction level for building Debian packages, which
in turn makes tooling harder.</p>
<p>How would things look like in a better world?</p>
<ol>
<li>As a project, we should strive towards more unification. Uniformity still
does not rule out experimentation, it just changes the trade-off from easier
experimentation and harder automation to harder experimentation and easier
automation.</li>
<li>Our culture needs to shift from “this package is my domain, how dare you
touch it” to a shared sense of ownership, where anyone in the project can
easily contribute (reviewed) changes without necessarily even involving
individual maintainers.</li>
</ol>
<p>To learn more about how successful large changes can look like, I recommend <a href="https://www.youtube.com/watch?v=TrC6ROeV4GI">my
colleague Hyrum Wright’s talk “Large-Scale Changes at Google: Lessons Learned
From 5 Yrs of Mass Migrations”</a>.</p>
<h3 id="fragmented-workflow-and-infrastructure">Fragmented workflow and infrastructure</h3>
<p>Debian generally seems to prefer decentralized approaches over centralized
ones. For example, individual packages are maintained in separate repositories
(as opposed to in one repository), each repository can use any SCM (git and svn
are common ones) or no SCM at all, and each repository can be hosted on a
different site. Of course, what you do in such a repository also varies subtly
from team to team, and even within teams.</p>
<p>In practice, non-standard hosting options are used rarely enough to not justify
their cost, but frequently enough to be a huge pain when trying to automate
changes to packages. Instead of using GitLab’s API to create a merge request,
you have to design an entirely different, more complex system, which deals with
intermittently (or permanently!) unreachable repositories and abstracts away
differences in patch delivery (bug reports, merge requests, pull requests,
email, …).</p>
<p>Wildly diverging workflows is not just a temporary problem either. I
participated in long discussions about different git workflows during DebConf
13, and gather that there were similar discussions in the meantime.</p>
<p>Personally, I cannot keep enough details of the different workflows in my
head. Every time I touch a package that works differently than mine, it
frustrates me immensely to re-learn aspects of my day-to-day.</p>
<p>After noticing workflow fragmentation in the Go packaging team (which I
started), I tried fixing this with the <a href="https://go-team.pages.debian.net/workflow-changes.html">workflow changes
proposal</a>, but did not
succeed in implementing it. The lack of effective automation and slow pace of
changes in the surrounding tooling despite my willingness to contribute time and
energy killed any motivation I had.</p>
<h3 id="old-infrastructure-package-uploads">Old infrastructure: package uploads</h3>
<p>When you want to make a package available in Debian, you upload GPG-signed files
via anonymous FTP. There are several batch jobs (the queue daemon, <code>unchecked</code>,
<code>dinstall</code>, possibly others) which run on fixed schedules (e.g. <code>dinstall</code> runs
at 01:52 UTC, 07:52 UTC, 13:52 UTC and 19:52 UTC).</p>
<p>Depending on timing, I estimated that you might wait for over 7 hours (!!)
before your package is actually installable.</p>
<p>What’s worse for me is that feedback to your upload is asynchronous. I like to
do one thing, be done with it, move to the next thing. The current setup
requires a many-minute wait and costly task switch for no good technical
reason. You might think a few minutes aren’t a big deal, but when all the time I
can spend on Debian per day is measured in minutes, this makes a huge difference
in perceived productivity and fun.</p>
<p>The last communication I can find about speeding up this process is <a href="https://lists.debian.org/debian-project/2008/12/msg00014.html">ganneff’s
post</a> from 2008.</p>
<p>How would things look like in a better world?</p>
<ol>
<li>Anonymous FTP would be replaced by a web service which ingests my package and
returns an authoritative accept or reject decision in its response.</li>
<li>For accepted packages, there would be a status page displaying the build
status and when the package will be available via the mirror network.</li>
<li>Packages should be available within a few minutes after the build completed.</li>
</ol>
<h3 id="old-infrastructure-bug-tracker">Old infrastructure: bug tracker</h3>
<p>I dread interacting with the Debian bug
tracker. <a href="https://en.wikipedia.org/wiki/Debbugs">debbugs</a> is a piece of software
(from 1994) which is only used by Debian and the GNU project these days.</p>
<p>Debbugs processes emails, which is to say it is asynchronous and cumbersome to
deal with. Despite running on the fastest machines we have available in Debian
(or so I was told when the subject last came up), its web interface loads very
slowly.</p>
<p>Notably, the web interface at bugs.debian.org is read-only. Setting up a working
email setup for
<a href="https://manpages.debian.org/stretch/reportbug/reportbug.1.en.html"><code>reportbug(1)</code></a>
or manually dealing with attachments is a rather big hurdle.</p>
<p>For reasons I don’t understand, every interaction with debbugs results in <a href="https://twitter.com/zekjur/status/1027995569770442752">many
different email <em>threads</em></a>.</p>
<p>Aside from the technical implementation, I also can never remember the different
ways that Debian uses pseudo-packages for bugs and processes. I need them rarely
enough to establish a mental model of how they are set up, or working memory of
how they are used, but frequently enough to be annoyed by this.</p>
<p>How would things look like in a better world?</p>
<ol>
<li>Debian would switch from a custom bug tracker to a (any) well-established
one.</li>
<li>Debian would offer automation around processes. It is great to have a
paper-trail and artifacts of the process in the form of a bug report, but the
primary interface should be more convenient (e.g. a web form).</li>
</ol>
<h3 id="old-infrastructure-mailing-list-archives">Old infrastructure: mailing list archives</h3>
<p>It baffles me that in 2019, we still don’t have a conveniently browsable
threaded archive of mailing list discussions. Email and threading is more widely
used in Debian than anywhere else, so this is somewhat
ironic. <a href="https://en.wikipedia.org/wiki/Gmane">Gmane</a> used to paper over this
issue, but Gmane’s availability over the last few years has been spotty, to say
the least (it is down as I write this).</p>
<p>I tried to contribute a threaded list archive, but our listmasters didn’t seem
to care or want to support the project.</p>
<h3 id="debian-is-hard-to-machine-read">Debian is hard to machine-read</h3>
<p>While it is obviously possible to deal with Debian packages programmatically,
the experience is far from pleasant. Everything seems slow and cumbersome. I
have picked just 3 quick examples to illustrate my point.</p>
<p><a href="https://github.com/Debian/debiman/">debiman</a> needs <a href="https://github.com/Debian/debiman/issues/12">help from
piuparts</a> in analyzing the
alternatives mechanism of each package to display the manpages of
e.g. <a href="https://manpages.debian.org/stretch/postgresql-client-9.6/psql.1.en.html"><code>psql(1)</code></a>. This
is because maintainer scripts modify the alternatives database by calling shell
scripts. Without actually installing a package, you cannot know which changes it
does to the alternatives database.</p>
<p><a href="https://github.com/Debian/pk4">pk4</a> needs to maintain its own cache to look up
package metadata based on the package name. Other tools parse the apt database
from scratch on every invocation. A proper database format, or at least a binary
interchange format, would go a long way.</p>
<p><a href="https://github.com/Debian/dcs/">Debian Code Search</a> wants to ingest new
packages as quickly as possible. There used to be a
<a href="https://github.com/fedora-infra/fedmsg">fedmsg</a> instance for Debian, but it no
longer seems to exist. It is unclear where to get notifications from for new
packages, and where best to fetch those packages.</p>
<h3 id="complicated-build-stack">Complicated build stack</h3>
<p>See my <a href="/posts/2016-11-25-build-tools/">“Debian package build tools”</a> post. It
really bugs me that the sprawl of tools is not seen as a problem by others.</p>
<h3 id="developer-experience-pretty-painful">Developer experience pretty painful</h3>
<p>Most of the points discussed so far deal with the experience in <em>developing
Debian</em>, but as I recently described in my post <a href="/posts/2019-02-15-debian-debugging-devex/">“Debugging experience in
Debian”</a>, the experience when
<em>developing using Debian</em> leaves a lot to be desired, too.</p>
<h3 id="i-have-more-ideas">I have more ideas</h3>
<p>At this point, the article is getting pretty long, and hopefully you got a rough
idea of my motivation.</p>
<p>While I described a number of specific shortcomings above, the final nail in the
coffin is actually the lack of a positive outlook. I have more ideas that seem
really compelling to me, but, based on how my previous projects have been going,
I don’t think I can make any of these ideas happen within the Debian project.</p>
<p>I intend to publish a few more posts about specific ideas for improving
operating systems here. Stay tuned.</p>
<p>Lastly, I hope this post inspires someone, ideally a group of people, to improve
the developer experience within Debian.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Debugging experience in Debian]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-02-15-debian-debugging-devex/"/>
    <id>https://michael.stapelberg.ch/posts/2019-02-15-debian-debugging-devex/</id>
    <published>2019-02-15T00:00:00+00:00</published>
    <content type="html"><![CDATA[<p>Recently, a user reported that <a href="https://bugs.debian.org/918925">they don’t see window titles in i3 when running
i3 on a Raspberry Pi with Debian</a>.</p>
<p>I copied the latest <a href="https://wiki.debian.org/RaspberryPi3">Raspberry Pi Debian
image</a> onto an SD card, booted it, and was
able to reproduce the issue.</p>
<p>Conceptually, at this point, I should be able to install and start <code>gdb</code>, set a
break point and step through the code.</p>
<h3 id="enabling-debug-symbols-in-debian">Enabling debug symbols in Debian</h3>
<aside class="admonition note">
  <div class="note-container">
    <div class="note-icon" style="width: 20px; height: 20px">
      <svg id="exclamation-icon" width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
    <path d="M0,0L24,0L24,24L0,24L0,0Z" style="fill:none;"/>
    <g transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM13,17L11,17L11,15L13,15L13,17ZM13,13L11,13L11,7L13,7L13,13Z" style="fill-rule:nonzero;"/>
    </g>
</svg>

    </div>
    <div class="admonition-content"><p><strong>Update (February 2021):</strong> Debian <a href="https://lists.debian.org/debian-devel-announce/2021/02/msg00003.html">has
announced</a>
a <a href="https://wiki.debian.org/Debuginfod"><code>debuginfod</code> service</a>. This makes
enabling and finding debug symbols much easier: <code>export DEBUGINFOD_URLS=&quot;https://debuginfod.debian.net&quot; </code> is all that’s necessary, and
hopefully that step will become unnecessary next, too.</p>
<p>Unfortunately, (this instance of?) debuginfod does not cover sources, yet.</p>
</div>
  </div>
</aside>

<p>Debian, by default, strips debug symbols when building packages to conserve disk
space and network bandwidth. The motivation is very reasonable: most users will
never need the debug symbols.</p>
<p>Unfortunately, obtaining debug symbols when you do need them is unreasonably hard.</p>
<p>We begin by configuring an additional apt repository which contains
automatically generated debug packages:</p>
<pre tabindex="0"><code>raspi# cat &gt;&gt;/etc/apt/sources.list.d/debug.list &lt;&lt;&#39;EOT&#39;
deb http://deb.debian.org/debian-debug buster-debug main contrib non-free
EOT
raspi# apt update
</code></pre><p>Notably, not all Debian packages have debug packages. As <a href="https://wiki.debian.org/DebugPackage">the DebugPackage
Debian Wiki page</a> explains,
<code>debhelper/9.20151219</code> started generating debug packages (ending in <code>-dbgsym</code>)
automatically. Packages which have not been updated might come with their own
debug packages (ending in <code>-dbg</code>) or might not preserve debug symbols at all!</p>
<p>Now that we <strong>can</strong> install debug packages, how do we know <strong>which ones</strong> we need?</p>
<h3 id="finding-debug-symbol-packages-in-debian">Finding debug symbol packages in Debian</h3>
<p>For debugging i3, we obviously need at least the <code>i3-dbgsym</code> package, but i3
uses a number of other libraries through whose code we may need to step.</p>
<p>The <code>debian-goodies</code> package ships a tool called
<a href="https://manpages.debian.org/testing/debian-goodies/find-dbgsym-packages.1.en.html">find-dbgsym-packages</a>
which prints the required packages to debug an executable, core dump or running
process:</p>
<pre tabindex="0"><code>raspi# apt install debian-goodies
raspi# apt install $(find-dbgsym-packages $(which i3))
</code></pre><p>Now we should have symbol names and line number information available in
<code>gdb</code>. But for effectively stepping through the program, access to the source
code is required.</p>
<h3 id="obtaining-source-code-in-debian">Obtaining source code in Debian</h3>
<p>Naively, one would assume that <code>apt source</code> should be sufficient for obtaining
the source code of any Debian package. However, <code>apt source</code> defaults to the
package candidate version, not the version you have installed on your
system.</p>
<p>I have addressed this issue with the
<a href="https://manpages.debian.org/testing/pk4/pk4.1.en.html"><code>pk4</code></a> tool, which
defaults to the installed version.</p>
<p>Before we can extract any sources, we need to configure yet another apt
repository:</p>
<pre tabindex="0"><code>raspi# cat &gt;&gt;/etc/apt/sources.list.d/source.list &lt;&lt;&#39;EOT&#39;
deb-src http://deb.debian.org/debian buster main contrib non-free
EOT
raspi# apt update
</code></pre><p>Regardless of whether you use <code>apt source</code> or <code>pk4</code>, one remaining problem is
the directory mismatch: the debug symbols contain a certain path, and that path
is typically not where you extracted your sources to. While debugging, you will
need to tell <code>gdb</code> about the location of the sources. This is tricky when you
debug a call across different source packages:</p>
<pre tabindex="0"><code>(gdb) pwd
Working directory /usr/src/i3.
(gdb) list main
229     * the main loop. */
230     ev_unref(main_loop);
231   }
232 }
233
234 int main(int argc, char *argv[]) {
235  /* Keep a symbol pointing to the I3_VERSION string constant so that
236   * we have it in gdb backtraces. */
237  static const char *_i3_version __attribute__((used)) = I3_VERSION;
238  char *override_configpath = NULL;
(gdb) list xcb_connect
484	../../src/xcb_util.c: No such file or directory.
</code></pre><p>See <a href="https://sourceware.org/gdb/onlinedocs/gdb/Source-Path.html">Specifying Source
Directories</a> in the
gdb manual for the <code>dir</code> command which allows you to add multiple directories to
the source path. This is pretty tedious, though, and does not work for all
programs.</p>
<h3 id="positive-example-fedora">Positive example: Fedora</h3>
<p>While Fedora conceptually shares all the same steps, the experience on Fedora is
so much better: when you run <code>gdb /usr/bin/i3</code>, it will tell you what the next
step is:</p>
<pre tabindex="0"><code># gdb /usr/bin/i3
[…]
Reading symbols from /usr/bin/i3...(no debugging symbols found)...done.
Missing separate debuginfos, use: dnf debuginfo-install i3-4.16-1.fc28.x86_64
</code></pre><p>Watch what happens when we run the suggested command:</p>
<pre tabindex="0"><code># dnf debuginfo-install i3-4.16-1.fc28.x86_64
enabling updates-debuginfo repository
enabling fedora-debuginfo repository
[…]
Installed:
  i3-debuginfo.x86_64 4.16-1.fc28
  i3-debugsource.x86_64 4.16-1.fc28
Complete!
</code></pre><p>A single command understood our intent, enabled the required repositories and
installed the required packages, both for debug symbols and source code (stored
in e.g. <code>/usr/src/debug/i3-4.16-1.fc28.x86_64</code>). Unfortunately, <code>gdb</code> doesn’t
seem to locate the sources, which seems like a bug to me.</p>
<p>One downside of Fedora’s approach is that <code>gdb</code> will only print all required
dependencies once you actually run the program, so you may need to run multiple
<code>dnf</code> commands.</p>
<h3 id="in-an-ideal-world">In an ideal world</h3>
<p>Ideally, none of the manual steps described above would be necessary. It seems
absurd to me that so much knowledge is required to efficiently debug programs in
Debian. Case in point: I only learnt about <code>find-dbgsym-packages</code> a few days ago
when talking to one of its contributors.</p>
<p>Installing <code>gdb</code> should be all that a user needs to do. Debug symbols and
sources can be transparently provided through a lazy-loading FUSE file
system. If our build/packaging infrastructure assured predictable paths and
automated debug symbol extraction, we could have transparent, quick and reliable
debugging of all programs within Debian.</p>
<p>NixOS’s dwarffs is an implementation of this idea:
<a href="https://github.com/edolstra/dwarffs">https://github.com/edolstra/dwarffs</a></p>
<h3 id="conclusion">Conclusion</h3>
<p>While I agree with the removal of debug symbols as a general optimization, I
think every Linux distribution should strive to provide an entirely transparent
debugging experience: you should not even have to know that debug symbols are
not present by default. Debian really falls short in this regard.</p>
<p>Getting Debian to a fully transparent debugging experience requires a lot of
technical work and a lot of social convincing. In my experience,
programmatically working with the Debian archive and packages is tricky, and
ensuring that <em>all</em> packages in a Debian release have debug packages (let alone
predictable paths) seems entirely unachievable due to the fragmentation of
packaging infrastructure and holdouts blocking any progress.</p>
<p>My go-to example is <a href="https://sources.debian.org/src/rsync/3.1.3-5/debian/rules/">rsync’s
debian/rules</a>, which
intentionally (!) still has not adopted debhelper. It is not a surprise that
there are no debug symbols for <code>rsync</code> in Debian.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[TurboPFor: an analysis]]></title>
    <link href="https://michael.stapelberg.ch/posts/2019-02-05-turbopfor-analysis/"/>
    <id>https://michael.stapelberg.ch/posts/2019-02-05-turbopfor-analysis/</id>
    <published>2019-02-05T09:00:00+01:00</published>
    <content type="html"><![CDATA[<h3 id="motivation">Motivation</h3>
<p>I have recently been looking into speeding up Debian Code Search. As a quick
reminder, search engines answer queries by consulting an inverted index: a map
from term to documents containing that term (called a “posting list”). See <a href="https://codesearch.debian.net/research/bsc-thesis.pdf">the
Debian Code Search Bachelor
Thesis</a> (PDF) for a lot
more details.</p>
<p>Currently, Debian Code Search does not store positional information in its
index, i.e. the index can only reveal <em>that</em> a certain trigram is present in a
document, not <em>where</em> or <em>how often</em>.</p>
<p>From analyzing Debian Code Search queries, I knew that identifier queries (70%)
massively outnumber regular expression queries (30%). When processing identifier
queries, storing positional information in the index enables a significant
optimization: instead of identifying the possibly-matching documents and having
to read them all, we can determine matches from querying the index alone, no
document reads required.</p>
<p>This moves the bottleneck: having to read all possibly-matching documents
requires a lot of expensive random I/O, whereas having to decode long posting
lists requires a lot of cheap sequential I/O.</p>
<p>Of course, storing positions comes with a downside: the index is larger, and a
larger index takes more time to decode when querying.</p>
<p>Hence, I have been looking at various posting list compression/decoding
techniques, to figure out whether we could switch to a technique which would
retain (or improve upon!) current performance despite much longer posting lists
and produce a small enough index to fit on our current hardware.</p>
<h3 id="literature">Literature</h3>
<p>I started looking into this space because of Daniel Lemire’s <a href="https://lemire.me/blog/2017/09/27/stream-vbyte-breaking-new-speed-records-for-integer-compression/">Stream
VByte</a>
post. As usual, Daniel’s work is well presented, easily digestible and
accompanied by not just one, but multiple implementations.</p>
<p>I also looked for scientific papers to learn about the state of the art and
classes of different approaches in general. The best I could find is
<a href="https://dl.acm.org/citation.cfm?doid=2682862.2682870">Compression, SIMD, and Postings
Lists</a>. If you don’t have
access to the paper, I hear that
<a href="https://en.wikipedia.org/wiki/Sci-Hub">Sci-Hub</a> is helpful.</p>
<p>The paper is from 2014, and doesn’t include all algorithms. If you know of a
better paper, please let me know and I’ll include it here.</p>
<p>Eventually, I stumbled upon an algorithm/implementation called TurboPFor, which
the rest of the article tries to shine some light on.</p>
<h3 id="turbopfor">TurboPFor</h3>
<p>If you’re wondering: PFor stands for Patched Frame Of Reference and describes a
family of algorithms. The principle is explained e.g. in <a href="https://arxiv.org/pdf/1401.6399.pdf">SIMD Compression and
the Intersection of Sorted Integers (PDF)</a>.</p>
<p>The <a href="https://github.com/powturbo/TurboPFor">TurboPFor project’s README file</a>
claims that TurboPFor256 compresses with a rate of 5.04 bits per integer, and
can decode with 9400 MB/s on a single thread of an Intel i7-6700 CPU.</p>
<p>For Debian Code Search, we use unsigned integers of 32 bit (uint32), which
TurboPFor will compress into as few bits as required.</p>
<p>Dividing Debian Code Search’s file sizes by the total number of integers, I get
similar values, at least for the docid index section:</p>
<ul>
<li>5.49 bits per integer for the docid index section</li>
<li>11.09 bits per integer for the positions index section</li>
</ul>
<p>I can confirm the order of magnitude of the decoding speed, too. My benchmark
calls TurboPFor from Go via cgo, which introduces some overhead. To exclude disk
speed as a factor, data comes from the page cache. The benchmark sequentially
decodes all posting lists in the specified index, using as many threads as the
machine has cores¹:</p>
<ul>
<li>≈1400 MB/s on a  1.1 GiB docid index section</li>
<li>≈4126 MB/s on a 15.0 GiB position index section</li>
</ul>
<p>I think the numbers differ because the position index section contains larger
integers (requiring more bits). I repeated both benchmarks, capped to 1 GiB, and
decoding speeds still differed, so it is not just the size of the index.</p>
<p>Compared to Streaming VByte, a TurboPFor256 index comes in at just over half the
size, while still reaching 83% of Streaming VByte’s decoding speed. This seems
like a good trade-off for my use-case, so I decided to have a closer look at how
TurboPFor works.</p>
<p>① See <a href="https://github.com/stapelberg/goturbopfor/blob/d7954fb81e66080941891dccc27407d8496f65d9/cmd/gp4-verify/verify.go">cmd/gp4-verify/verify.go</a> run on an Intel i9-9900K.</p>
<h3 id="methodology">Methodology</h3>
<p>To confirm my understanding of the details of the format, I implemented a
pure-Go TurboPFor256 decoder. Note that it is intentionally <em>not optimized</em> as
its main goal is to use simple code to teach the TurboPFor256 on-disk format.</p>
<p>If you’re looking to use TurboPFor from Go, I recommend using cgo. cgo’s
function call overhead is about 51ns <a href="https://go-review.googlesource.com/c/go/+/30080">as of Go
1.8</a>, which will easily be
offset by TurboPFor’s carefully optimized, vectorized (SSE/AVX) code.</p>
<p>With that caveat out of the way, you can find my teaching implementation at
<a href="https://github.com/stapelberg/goturbopfor">https://github.com/stapelberg/goturbopfor</a></p>
<p>I verified that it produces the same results as TurboPFor’s <code>p4ndec256v32</code>
function for all posting lists in the Debian Code Search index.</p>
<h3 id="on-disk-format">On-disk format</h3>
<p>Note that TurboPFor does not fully define an on-disk format on its own. When
encoding, it turns a list of integers into a byte stream:</p>
<pre tabindex="0"><code>size_t p4nenc256v32(uint32_t *in, size_t n, unsigned char *out);
</code></pre><p>When decoding, it decodes the byte stream into an array of integers, but needs
to know the number of integers in advance:</p>
<pre tabindex="0"><code>size_t p4ndec256v32(unsigned char *in, size_t n, uint32_t *out);
</code></pre><p>Hence, you’ll need to keep track of the number of integers and length of the
generated byte streams separately. When I talk about on-disk format, I’m
referring to the byte stream which TurboPFor returns.</p>
<p>The TurboPFor256 format uses blocks of 256 integers each, followed by a trailing
block — if required — which can contain fewer than 256 integers:</p>
<img src="/turbopfor/ondisk.svgo.svg">
<p>SIMD bitpacking is used for all blocks but the trailing block (which uses
regular bitpacking). This is not merely an implementation detail for decoding:
the on-disk structure is different for blocks which can be SIMD-decoded.</p>
<p>Each block starts with a 2 bit header, specifying the type of the block:</p>
<ul>
<li>11: <a href="#block-constant">constant</a></li>
<li>00: <a href="#block-bitpack">bitpacking</a></li>
<li>10: <a href="#block-bitpackex">bitpacking with exceptions (bitmap)</a></li>
<li>01: <a href="#block-bitpackvb">bitpacking with exceptions (variable byte)</a></li>
</ul>
<p>Each block type is explained in more detail in the following sections.</p>
<p>Note that none of the block types store the number of elements: you will always
need to know how many integers you need to decode. Also, you need to know in
advance how many bytes you need to feed to TurboPFor, so you will need some sort
of container format.</p>
<p>Further, TurboPFor automatically choses the best block type for each block.</p>
<h4 id="block-constant">Constant block</h4>
<p>A constant block (all integers of the block have the same value) consists of a
single value of a specified bit width ≤ 32. This value will be stored in each
output element for the block. E.g., after calling <code>decode(input, 3, output)</code>
with <code>input</code> being the constant block depicted below, output is <code>{0xB8912636, 0xB8912636, 0xB8912636}</code>.</p>
<img src="/turbopfor/block-constant.svgo.svg">
<p>The example shows the maximum number of bytes (5). Smaller integers will use
fewer bytes: e.g. an integer which can be represented in 3 bits will only use 2
bytes.</p>
<h4 id="block-bitpack">Bitpacking block</h4>
<p>A bitpacking block specifies a bit width ≤ 32, followed by a stream of
bits. Each value starts at the Least Significant Bit (LSB), i.e. the 3-bit
values 0 (<code>000b</code>) and 5 (<code>101b</code>) are encoded as <code>101000b</code>.</p>
<img src="/turbopfor/block-bitpack.svgo.svg">
<h4 id="block-bitpackex">Bitpacking with exceptions (bitmap) block</h4>
<p>The constant and bitpacking block types work well for integers which don’t
exceed a certain width, e.g. for a series of integers of width ≤ 5 bits.</p>
<p>For a series of integers where only a few values exceed an otherwise common
width (say, two values require 7 bits, the rest requires 5 bits), it makes sense
to cut the integers into two parts: value and exception.</p>
<p>In the example below, decoding the third integer <code>out2</code> (<code>000b</code>) requires
combination with exception <code>ex0</code> (<code>10110b</code>), resulting in <code>10110000b</code>.</p>
<p>The number of exceptions can be determined by summing the 1 bits in the bitmap
using the <a href="https://en.wikipedia.org/wiki/Hamming_weight">popcount instruction</a>.</p>
<img src="/turbopfor/block-bitpackex.svgo.svg">
<h4 id="block-bitpackvb">Bitpacking with exceptions (variable byte)</h4>
<p>When the exceptions are not uniform enough, it makes sense to switch from
bitpacking to a variable byte encoding:</p>
<img src="/turbopfor/block-bitpackvb.svgo.svg">
<h3 id="decoding-variable-byte">Decoding: variable byte</h3>
<p>The variable byte encoding used by the TurboPFor format is similar to the one
<a href="https://sqlite.org/src4/doc/trunk/www/varint.wiki">used by SQLite</a>, which is
described, alongside other common variable byte encodings, at
<a href="https://web.archive.org/web/20201119135834/https://github.com/stoklund/varint">github.com/stoklund/varint</a>.</p>
<p>Instead of using individual bits for dispatching, this format classifies the
first byte (<code>b[0]</code>) into ranges:</p>
<ul>
<li>[0—176]: the value is <code>b[0]</code></li>
<li>[177—240]: a 14 bit value is in <code>b[0]</code> (6 high bits) and <code>b[1]</code> (8 low bits)</li>
<li>[241—248]: a 19 bit value is in <code>b[0]</code> (3 high bits), <code>b[1]</code> and <code>b[2]</code> (16 low bits)</li>
<li>[249—255]: a 32 bit value is in <code>b[1]</code>, <code>b[2]</code>, <code>b[3]</code> and possibly <code>b[4]</code></li>
</ul>
<p>Here is the space usage of different values:</p>
<ul>
<li>[0—176] are stored in 1 byte (as-is)</li>
<li>[177—16560] are stored in 2 bytes, with the highest 6 bits added to 177</li>
<li>[16561—540848] are stored in 3 bytes, with the highest 3 bits added to 241</li>
<li>[540849—16777215] are stored in 4 bytes, with 0 added to 249</li>
<li>[16777216—4294967295] are stored in 5 bytes, with 1 added to 249</li>
</ul>
<p>An overflow marker will be used to signal that encoding the
values would be less space-efficient than simply copying them
(e.g. if all values require 5 bytes).</p>
<p>This format is very space-efficient: it packs 0-176 into a single byte, as
opposed to 0-128 (most others). At the same time, it can be decoded very
quickly, as only the first byte needs to be compared to decode a value (similar
to PrefixVarint).</p>
<h3 id="decoding-bitpacking">Decoding: bitpacking</h3>
<h4 id="regular-bitpacking">Regular bitpacking</h4>
<p>In regular (non-SIMD) bitpacking, integers are stored on disk one after the
other, padded to a full byte, as a byte is the smallest addressable unit when
reading data from disk. For example, if you bitpack only one 3 bit int, you will
end up with 5 bits of padding.</p>
<img src="/turbopfor/bitpacking.svgo.svg">
<h4 id="simd-bitpacking-256v32">SIMD bitpacking (256v32)</h4>
<p>SIMD bitpacking works like regular bitpacking, but processes 8 uint32
little-endian values at the same time, leveraging the <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">AVX instruction
set</a>. The following
illustration shows the order in which 3-bit integers are decoded from disk:</p>
<img src="/turbopfor/bitpacking256v32.svgo.svg">
<h3 id="in-practice">In Practice</h3>
<p>For a Debian Code Search index, 85% of posting lists are short enough to only
consist of a trailing block, i.e. no SIMD instructions can be used for decoding.</p>
<p>The distribution of block types looks as follows:</p>
<ul>
<li>72% bitpacking with exceptions (bitmap)</li>
<li>19% bitpacking with exceptions (variable byte)</li>
<li>5% constant</li>
<li>4% bitpacking</li>
</ul>
<p>Constant blocks are mostly used for posting lists with just one entry.</p>
<h3 id="conclusion">Conclusion</h3>
<p>The TurboPFor on-disk format is very flexible: with its 4 different kinds of
blocks, chances are high that a very efficient encoding will be used for most
integer series.</p>
<p>Of course, the flip side of covering so many cases is complexity: the format and
implementation take quite a bit of time to understand — hopefully this article
helps a little! For environments where the C TurboPFor implementation cannot be
used, smaller algorithms might be simpler to implement.</p>
<p>That said, if you can use the TurboPFor implementation, you will benefit from a
highly optimized SIMD code base, which will most likely be an improvement over
what you’re currently using.</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[Looking for a new Raspberry Pi image maintainer]]></title>
    <link href="https://michael.stapelberg.ch/posts/2018-06-03-raspi3-looking-for-maintainer/"/>
    <id>https://michael.stapelberg.ch/posts/2018-06-03-raspi3-looking-for-maintainer/</id>
    <published>2018-06-03T08:43:00+02:00</published>
    <content type="html"><![CDATA[<p>
  <strong>This is taken care of: Gunnar Wolf has taken on maintenance of the Raspberry Pi image. Thank you!</strong>
</p>

<p>
  (Cross-posting this message I sent to pkg-raspi-maintainers for broader visibility.)
</p>

<p>
  I started building Raspberry Pi images because I thought there should be an easy, official way to install Debian on the Raspberry Pi.
</p>

<p>
  I still believe that, but I’m not actually using Debian on any of my Raspberry Pis anymore¹, so my personal motivation to do any work on the images is gone.
</p>

<p>
  On top of that, I realize that my commitments exceed my spare time capacity, so I need to get rid of responsibilities.
</p>

<p>
  Therefore, <strong>I’m looking for someone to take up maintainership of the Raspberry Pi images</strong>. Numerous people have reached out to me with thank you notes and questions, so I think the user interest is there. Also, I’ll be happy to answer any questions that you might have and that I can easily answer. Please reply here (or in private) if you’re interested.
</p>

<p>
  If I can’t find someone within the next 7 days, I’ll put up an announcement message in the raspi3-image-spec README, wiki page, and my blog posts, stating that the image is unmaintained and looking for a new maintainer.
</p>

<p>
  Thanks for your understanding,
</p>

<p>
  ① just in case you’re curious, I’m now running cross-compiled Go programs directly under a Linux kernel and minimal userland, see <a href="https://gokrazy.org/">https://gokrazy.org/</a>
</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[sbuild-debian-developer-setup(1)]]></title>
    <link href="https://michael.stapelberg.ch/posts/2018-03-19-sbuild-debian-developer-setup/"/>
    <id>https://michael.stapelberg.ch/posts/2018-03-19-sbuild-debian-developer-setup/</id>
    <published>2018-03-19T08:00:00+01:00</published>
    <content type="html"><![CDATA[<p>
  I have heard a number of times that sbuild is too hard to get started with,
  and hence people don’t use it.
</p>

<p>
  To reduce hurdles from using/contributing to Debian, I wanted to make sbuild
  easier to set up.
</p>

<p>
  sbuild ≥ 0.74.0 provides a Debian package
  called <a href="https://packages.debian.org/sid/sbuild-debian-developer-setup">sbuild-debian-developer-setup</a>. Once
  installed, run
  the <a href="https://manpages.debian.org/unstable/sbuild/sbuild-debian-developer-setup.1">sbuild-debian-developer-setup(1)</a>
  command to create a chroot suitable for building packages for Debian unstable.
</p>

<p>
  On a system without any sbuild/schroot bits installed, a transcript of the
  full setup looks like this:
</p>

<pre>
% sudo apt install -t unstable sbuild-debian-developer-setup
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  libsbuild-perl sbuild schroot
Suggested packages:
  deborphan btrfs-tools aufs-tools | unionfs-fuse qemu-user-static
Recommended packages:
  exim4 | mail-transport-agent autopkgtest
The following NEW packages will be installed:
  libsbuild-perl sbuild sbuild-debian-developer-setup schroot
0 upgraded, 4 newly installed, 0 to remove and 1454 not upgraded.
Need to get 1.106 kB of archives.
After this operation, 3.556 kB of additional disk space will be used.
Do you want to continue? [Y/n]
Get:1 http://localhost:3142/deb.debian.org/debian unstable/main amd64 libsbuild-perl all 0.74.0-1 [129 kB]
Get:2 http://localhost:3142/deb.debian.org/debian unstable/main amd64 sbuild all 0.74.0-1 [142 kB]
Get:3 http://localhost:3142/deb.debian.org/debian testing/main amd64 schroot amd64 1.6.10-4 [772 kB]
Get:4 http://localhost:3142/deb.debian.org/debian unstable/main amd64 sbuild-debian-developer-setup all 0.74.0-1 [62,6 kB]
Fetched 1.106 kB in 0s (5.036 kB/s)
Selecting previously unselected package libsbuild-perl.
(Reading database ... 276684 files and directories currently installed.)
Preparing to unpack .../libsbuild-perl_0.74.0-1_all.deb ...
Unpacking libsbuild-perl (0.74.0-1) ...
Selecting previously unselected package sbuild.
Preparing to unpack .../sbuild_0.74.0-1_all.deb ...
Unpacking sbuild (0.74.0-1) ...
Selecting previously unselected package schroot.
Preparing to unpack .../schroot_1.6.10-4_amd64.deb ...
Unpacking schroot (1.6.10-4) ...
Selecting previously unselected package sbuild-debian-developer-setup.
Preparing to unpack .../sbuild-debian-developer-setup_0.74.0-1_all.deb ...
Unpacking sbuild-debian-developer-setup (0.74.0-1) ...
Processing triggers for systemd (236-1) ...
Setting up schroot (1.6.10-4) ...
Created symlink /etc/systemd/system/multi-user.target.wants/schroot.service → /lib/systemd/system/schroot.service.
Setting up libsbuild-perl (0.74.0-1) ...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up sbuild (0.74.0-1) ...
Setting up sbuild-debian-developer-setup (0.74.0-1) ...
Processing triggers for systemd (236-1) ...

% sudo sbuild-debian-developer-setup
The user `michael' is already a member of `sbuild'.
I: SUITE: unstable
I: TARGET: /srv/chroot/unstable-amd64-sbuild
I: MIRROR: http://localhost:3142/deb.debian.org/debian
I: Running debootstrap --arch=amd64 --variant=buildd --verbose --include=fakeroot,build-essential,eatmydata --components=main --resolve-deps unstable /srv/chroot/unstable-amd64-sbuild http://localhost:3142/deb.debian.org/debian
I: Retrieving InRelease 
I: Checking Release signature
I: Valid Release signature (key id 126C0D24BD8A2942CC7DF8AC7638D0442B90D010)
I: Retrieving Packages 
I: Validating Packages 
I: Found packages in base already in required: apt 
I: Resolving dependencies of required packages...
[…]
I: Successfully set up unstable chroot.
I: Run "sbuild-adduser" to add new sbuild users.
ln -s /usr/share/doc/sbuild/examples/sbuild-update-all /etc/cron.daily/sbuild-debian-developer-setup-update-all
Now run `newgrp sbuild', or log out and log in again.

% newgrp sbuild

% sbuild -d unstable hello
sbuild (Debian sbuild) 0.74.0 (14 Mar 2018) on x1

+==============================================================================+
| hello (amd64)                                Mon, 19 Mar 2018 07:46:14 +0000 |
+==============================================================================+

Package: hello
Distribution: unstable
Machine Architecture: amd64
Host Architecture: amd64
Build Architecture: amd64
Build Type: binary
[…]
</pre>

<p>
  I hope you’ll find this useful.
</p>
]]></content>
  </entry>
  <entry>
    <title type="html"><![CDATA[dput usability changes]]></title>
    <link href="https://michael.stapelberg.ch/posts/2018-03-10-dput-usability/"/>
    <id>https://michael.stapelberg.ch/posts/2018-03-10-dput-usability/</id>
    <published>2018-03-10T10:00:00+01:00</published>
    <content type="html"><![CDATA[<p>
  dput-ng ≥ 1.16 contains two usability changes which make uploading easier:
</p>

<ol>
  <li>
    When no arguments are specified, dput-ng auto-selects the most recent .changes file (with confirmation).
  </li>
  <li>
    Instead of erroring out when detecting an unsigned .changes file, <a href="https://manpages.debian.org/stretch/devscripts/debsign.1">debsign(1)</a> is invoked to sign the .changes file before proceeding.
  </li>
</ol>

<p>
  With these changes, after building a package, you just need to
  type <code>dput</code> (in the correct directory of course) to sign and upload
  it.
</p>
]]></content>
  </entry>
</feed>
