<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>aeolus (Posts by ongardie)</title><link>https://yieldsfalsehood.com/aeolus/</link><description></description><atom:link href="https://yieldsfalsehood.com/aeolus/sources/ongardie.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:elliot@yieldsfalsehood.com"&gt;elliot&lt;/a&gt; </copyright><lastBuildDate>Thu, 15 May 2025 02:43:25 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Running a Desktop Virtual Machine</title><link>https://yieldsfalsehood.com/aeolus/posts/running-a-desktop-virtual-machine-6d806ac5/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;This post is about my experience running a Linux desktop virtual machine on a
Linux host. I used the VM interactively, often in fullscreen mode, to develop
software and run web apps (mostly Slack and Google Docs). My experience wasn't
great, as I ran into many challenges.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://ongardie.net/blog/vm/"&gt;previous post on running VMs&lt;/a&gt; discussed running
bare-bones Linux VMs and getting SSH access to them. I usually do that for
testing, where the VMs are light-weight, shortlived, and disposable. It turns
out that long-lived desktop VMs have more challenging requirements and come
with a whole new set of issues.&lt;/p&gt;

&lt;section id="vm-setup"&gt;
&lt;h2&gt;VM setup&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#vm-setup"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Following a &lt;code&gt;virt-install&lt;/code&gt;, I did many of the tasks I normally do for setting
up a Linux host (see my &lt;a href="https://github.com/ongardie/configs"&gt;configs&lt;/a&gt; repo),
including setting the hostname, setting the timezone, configuring APT,
installing packages, and configuring other software.&lt;/p&gt;
&lt;p&gt;I renamed the username and group from &lt;code&gt;debian&lt;/code&gt; to &lt;code&gt;diego&lt;/code&gt;, while logged in as
&lt;code&gt;root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;usermod&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;/home/diego&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;debian
usermod&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'Diego'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-l&lt;span class="hl-w"&gt; &lt;/span&gt;diego&lt;span class="hl-w"&gt; &lt;/span&gt;debian
groupmod&lt;span class="hl-w"&gt; &lt;/span&gt;-n&lt;span class="hl-w"&gt; &lt;/span&gt;diego&lt;span class="hl-w"&gt; &lt;/span&gt;debian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could have created a new user instead, but I guess I like having &lt;code&gt;uid&lt;/code&gt; 1000.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="cpu-ram"&gt;
&lt;h2&gt;CPU and RAM&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#cpu-ram"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For CPU, I typically assigned 4 vCPUs to the VM. Changing this value requires
restarting the VM. Just because the VM has a vCPU doesn't mean it's using it,
so it might be reasonable to set this to half or more of the host CPUs. I think
4 vCPUs is the bare minimum needed for software development with VS Code and
Rust, and I probably should have allocated more.&lt;/p&gt;
&lt;p&gt;For RAM, &lt;code&gt;virt-manager&lt;/code&gt; has a field for the maximum allocation and another for
the current allocation. Changing the maximum allocation requires restarting the
VM, but changing the current allocation up to the maximum can happen at
runtime.&lt;/p&gt;
&lt;p&gt;I created an additional swap disk to relieve memory pressure. Using a separate
disk image was convenient because I didn't have to copy it every time when
moving the VM to a different host. I still copied the swap image the first time
because the VM's &lt;code&gt;/etc/fstab&lt;/code&gt; referred to the swap partition's &lt;code&gt;PARTUUID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I also set
&lt;a href="https://support.mozilla.org/en-US/kb/unload-inactive-tabs-save-system-memory-firefox"&gt;&lt;code&gt;browser.tabs.unloadOnLowMemory&lt;/code&gt;&lt;/a&gt;
to &lt;code&gt;true&lt;/code&gt; in Firefox, which may help with memory pressure.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="display"&gt;
&lt;h2&gt;Display&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#display"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For video settings, I tried a few different options. My main problem was that I
encountered serious rendering glitches with Google Docs in Firefox. I
ultimately used &lt;code&gt;Virtio&lt;/code&gt; with &lt;code&gt;3D Acceleration&lt;/code&gt; on and a display type of &lt;code&gt;Spice Server&lt;/code&gt; with &lt;code&gt;OpenGL&lt;/code&gt; on. However, I set &lt;code&gt;gfx.canvas.accelerated&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in
Firefox to work around the Google Docs rendering glitches.&lt;/p&gt;
&lt;p&gt;I wanted the VM to be able to run in both windowed and fullscreen modes,
which for my widescreen monitor meant &lt;code&gt;1792x1344&lt;/code&gt; and &lt;code&gt;5120x1440&lt;/code&gt; resolutions.&lt;/p&gt;
&lt;p&gt;The default display resolutions were missing &lt;code&gt;5120x1440&lt;/code&gt;. I created
&lt;code&gt;/etc/X11/xorg.conf.d/10-monitor.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xorg.conf"&gt;&lt;span class="hl-se"&gt;Section&lt;/span&gt; &lt;span class="hl-se"&gt;"Monitor"&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Identifier&lt;/span&gt; &lt;span class="hl-no"&gt;"Virtual-1"&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Modeline&lt;/span&gt; &lt;span class="hl-no"&gt;"5120x1440_60.00"  624.50  5120 5496 6048 6976  1440 1443 1453 1493 -hsync +vsync&lt;/span&gt;
	&lt;span class="hl-nb"&gt;Option&lt;/span&gt; &lt;span class="hl-no"&gt;"PreferredMode" "1792x1344"&lt;/span&gt;
&lt;span class="hl-se"&gt;EndSection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got that &lt;code&gt;Modeline&lt;/code&gt; by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;xcvt
&lt;span class="hl-go"&gt;[...]&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;cvt&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;5120&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1440&lt;/span&gt;
&lt;span class="hl-gp"&gt;# &lt;/span&gt;5120x1440&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;59&lt;/span&gt;.96&lt;span class="hl-w"&gt; &lt;/span&gt;Hz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;(&lt;/span&gt;CVT&lt;span class="hl-o"&gt;)&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;hsync:&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;89&lt;/span&gt;.52&lt;span class="hl-w"&gt; &lt;/span&gt;kHz&lt;span class="hl-p"&gt;;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;pclk:&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;624&lt;/span&gt;.50&lt;span class="hl-w"&gt; &lt;/span&gt;MHz
&lt;span class="hl-go"&gt;Modeline "5120x1440_60.00"  624.50  5120 5496 6048 6976  1440 1443 1453 1493 -hsync +vsync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found that the Xfce Display Settings app was causing the VM to enter
&lt;code&gt;5120x1440&lt;/code&gt; mode on login, even though I wanted it to default to &lt;code&gt;1792x1344&lt;/code&gt;. I
think I removed &lt;code&gt;.config/xfce4/xfconf/xfce-perchannel-xml/displays.xml&lt;/code&gt; and
then never opened the Xfce Display Settings again to work around that.&lt;/p&gt;
&lt;p&gt;I created two scripts and two launcher buttons on the Xfce panel to switch
between windowed and fullscreen modes. The &lt;code&gt;video-windowed&lt;/code&gt; script:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="hl-nb"&gt;exec&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xrandr&lt;span class="hl-w"&gt; &lt;/span&gt;--output&lt;span class="hl-w"&gt; &lt;/span&gt;Virtual-1&lt;span class="hl-w"&gt; &lt;/span&gt;--mode&lt;span class="hl-w"&gt; &lt;/span&gt;1792x1344
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the &lt;code&gt;video-fullscreen&lt;/code&gt; script:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="hl-nb"&gt;exec&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xrandr&lt;span class="hl-w"&gt; &lt;/span&gt;--output&lt;span class="hl-w"&gt; &lt;/span&gt;Virtual-1&lt;span class="hl-w"&gt; &lt;/span&gt;--mode&lt;span class="hl-w"&gt; &lt;/span&gt;5120x1440_60.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I ran into an issue with the
&lt;a href="https://notionwm.net/"&gt;Notion window manager&lt;/a&gt; when running the VM in windowed
mode. I normally hold the Meta key and drag the right mouse button to resize
windows. This doesn't work. I could resize a window by dragging the left mouse
button on the window border, but that border so thin that it's hard to hit. As
a workaround, I enabled &lt;code&gt;Shift+Meta&lt;/code&gt; while dragging windows in the VM, in
&lt;code&gt;.notion/cfg_bindings.lua&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-diff"&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;    bdoc("Resize the frame."),
&lt;span class="hl-w"&gt; &lt;/span&gt;    mdrag("Button1@border", "WFrame.p_resize(_)"),
&lt;span class="hl-w"&gt; &lt;/span&gt;    mdrag(META.."Button3", "WFrame.p_resize(_)"),
&lt;span class="hl-gi"&gt;+    mdrag("Shift+"..META.."Button3", "WFrame.p_resize(_)"),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section id="suspend"&gt;
&lt;h2&gt;Suspend&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#suspend"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I wanted to be able to keep this VM "turned on" even while the host computer
was suspended or hibernated.&lt;/p&gt;
&lt;p&gt;Early on, I found that running suspend or hibernate within the VM did not work,
and I disabled it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;mask&lt;span class="hl-w"&gt; &lt;/span&gt;sleep.target&lt;span class="hl-w"&gt; &lt;/span&gt;suspend.target&lt;span class="hl-w"&gt; &lt;/span&gt;hibernate.target&lt;span class="hl-w"&gt; &lt;/span&gt;hybrid-sleep.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Xfce logout dialog stops showing the buttons to take these disabled
actions, which is nice.&lt;/p&gt;
&lt;p&gt;I also found that I couldn't pause and resume the VM in &lt;code&gt;virt-manager&lt;/code&gt;
successfully.&lt;/p&gt;
&lt;p&gt;I could generally suspend the host, and the VM would tolerate that.&lt;/p&gt;
&lt;p&gt;However, in March 2025, the VM kept crashing when the host suspended. I
upgraded the &lt;code&gt;qemu-*&lt;/code&gt; and &lt;code&gt;seabios&lt;/code&gt; packages to the Debian 12 backports
versions (and had to install &lt;code&gt;qemu-system-modules-spice&lt;/code&gt;), which resolved the
suspend issue.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="file-share"&gt;
&lt;h2&gt;Shared filesystem&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#file-share"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I set up a Virtio filesystem to share files between the host and the VM. In
&lt;code&gt;virt-manager&lt;/code&gt;, I used &lt;code&gt;virtio-9p&lt;/code&gt; because &lt;code&gt;virtiofs&lt;/code&gt; wasn't supported when
running libvirt under in user (session) mode:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unable to add device: unsupported configuration: virtiofs is not yet
supported in session mode&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The target path in &lt;code&gt;virt-manager&lt;/code&gt; ends up being the name of the 9p share in the
VM. I added this to &lt;code&gt;/etc/fstab&lt;/code&gt; in the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/diego/host-share /home/diego/host-share 9p trans=virtio,version=9p2000.L,posixacl,msize=104857600 0 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don't remember where I found these options; it may have been the
&lt;a href="https://wiki.qemu.org/Documentation/9psetup"&gt;QEMU wiki&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="migration"&gt;
&lt;h2&gt;Disk image size and VM migration&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#migration"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I occasionally moved this VM from a desktop to a laptop and vice versa. I
didn't set up any fancy online migration; I just shut it down and copied the
disk image.&lt;/p&gt;
&lt;p&gt;Ideally, I'd free up some space and shrink the disk image prior to copying it.
I have &lt;code&gt;discard&lt;/code&gt; enabled in the VM's &lt;code&gt;/etc/fstab&lt;/code&gt;. I found I had to change the
&lt;code&gt;virt-manager&lt;/code&gt; disk's &lt;code&gt;Discard mode&lt;/code&gt; setting from &lt;code&gt;Hypervisor default&lt;/code&gt; to
&lt;code&gt;unmap&lt;/code&gt;. Then I ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;fstrim&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;within the VM, which reported that it trimmed many gigabytes. The &lt;code&gt;qcow2&lt;/code&gt; image
shrank in size accordingly on the host. The manual &lt;code&gt;fstrim&lt;/code&gt; may not be
necessary if you configure &lt;code&gt;virt-manager&lt;/code&gt; from the start.&lt;/p&gt;
&lt;p&gt;I used &lt;code&gt;rsync&lt;/code&gt; to copy the disk image, with the &lt;code&gt;--compress&lt;/code&gt; and &lt;code&gt;--sparse&lt;/code&gt;
flags. I probably should have used the &lt;code&gt;--inplace&lt;/code&gt; flag also (which used to
conflict with &lt;code&gt;--sparse&lt;/code&gt; but doesn't anymore).&lt;/p&gt;
&lt;p&gt;To copy the VM's configuration, I ran this on the original host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;dumpxml&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then imported it on the new host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;define&lt;span class="hl-w"&gt; &lt;/span&gt;--file&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$NAME&lt;/span&gt;.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also needed to change the display Splice device to &lt;code&gt;auto&lt;/code&gt;, but that might be
because I had messed with it on the original host.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="keyboard"&gt;
&lt;h2&gt;Keyboard&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#keyboard"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I set Caps Lock to be Control on some keyboards. I found I had to do this again
within the VM by setting:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nv"&gt;XKBOPTIONS&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s2"&gt;"ctrl:nocaps"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in &lt;code&gt;/etc/default/keyboard&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="webcam"&gt;
&lt;h2&gt;Webcam&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#webcam"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I tried passing through a USB webcam for video conferencing, but this was too
slow to work reliably. I don't have a good solution to this. My workaround was
to run the video conferencing on the host.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Conclusion&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#conclusion"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;That's it. I used this VM for months, so I don't think I'd find new issues
beyond these. The good news is that most of these are either easy to work
around or acceptable limitations, with some patience. My top remaining issues
are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No webcam support,&lt;/li&gt;
&lt;li&gt;No ability to suspend/hibernate/pause the VM, and&lt;/li&gt;
&lt;li&gt;No dynamic CPU allocation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Webcam support is important, especially because video conferencing often needs
to be authenticated, and that account may "belong" inside the VM. I don't know
how to solve it today, other than perhaps passing a PCIe USB card through to
the VM. The last two are "software issues" and might well be solvable with some
more configuration effort.&lt;/p&gt;
&lt;/section&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/running-a-desktop-virtual-machine-6d806ac5/</guid><pubDate>Fri, 09 May 2025 03:50:00 GMT</pubDate></item><item><title>Trying Nushell</title><link>https://yieldsfalsehood.com/aeolus/posts/trying-nushell-5b755f0f/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;I've been trying out &lt;a href="https://www.nushell.sh/"&gt;Nushell&lt;/a&gt; again lately. I
&lt;a href="https://ongardie.net/blog/command-not-found/"&gt;blogged in 2021&lt;/a&gt; about how
&lt;a href="https://code.launchpad.net/~ubuntu-core-dev/command-not-found/ubuntu"&gt;&lt;code&gt;command-not-found&lt;/code&gt;&lt;/a&gt;
is slow to build an index of which commands are provided by which Debian
packages. I created an alternative Posix shell script that performs well
without an index. Now, I ported this script to Nushell. I found it interesting
to compare the differences.&lt;/p&gt;

&lt;section id="comparison"&gt;
&lt;h2&gt;Comparison&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#comparison"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The two versions are
&lt;a href="https://github.com/ongardie/cubicle/tree/main/packages/apt-binary/bin"&gt;here&lt;/a&gt;.
Both rely heavily on regular expressions. The Posix shell version is about 10
lines and 250 characters longer. Part of this is additional logic to use
&lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt; with
&lt;a href="https://github.com/lz4/lz4"&gt;LZ4&lt;/a&gt; when available, yet fall back to some default
that works otherwise:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nv"&gt;PATTERN&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s2"&gt;"^(usr/)?s?bin/&lt;/span&gt;&lt;span class="hl-nv"&gt;$1&lt;/span&gt;&lt;span class="hl-s2"&gt;\s"&lt;/span&gt;
&lt;span class="hl-k"&gt;if&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;rg&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/null&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-v&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/null&lt;span class="hl-p"&gt;;&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-k"&gt;then&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nv"&gt;LINES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;files&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xargs&lt;span class="hl-w"&gt; &lt;/span&gt;-0&lt;span class="hl-w"&gt; &lt;/span&gt;rg&lt;span class="hl-w"&gt; &lt;/span&gt;--no-filename&lt;span class="hl-w"&gt; &lt;/span&gt;--search-zip&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;span class="hl-k"&gt;else&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"Run 'sudo apt install ripgrep lz4' to speed this up"&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-nv"&gt;LINES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;files&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;xargs&lt;span class="hl-w"&gt; &lt;/span&gt;-0&lt;span class="hl-w"&gt; &lt;/span&gt;/usr/lib/apt/apt-helper&lt;span class="hl-w"&gt; &lt;/span&gt;cat-file&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;-P&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;span class="hl-k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The equivalent Nushell code uses
&lt;a href="https://www.nushell.sh/commands/docs/par-each.html"&gt;&lt;code&gt;par-each&lt;/code&gt;&lt;/a&gt; for
parallelism, which is built into Nushell and seems quite handy:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-n"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;packages&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$files&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;par&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-nb"&gt;each&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;{&lt;/span&gt;
&lt;span class="hl-w"&gt;        &lt;/span&gt;&lt;span class="hl-sr"&gt;/usr/&lt;/span&gt;&lt;span class="hl-n"&gt;lib&lt;/span&gt;&lt;span class="hl-sr"&gt;/apt/&lt;/span&gt;&lt;span class="hl-n"&gt;apt&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;helper&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;cat&lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;file&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;parse&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;r&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;(&lt;/span&gt;&lt;span class="hl-s"&gt;'^(?:usr/)?s?bin/'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;+&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$command&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;+&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;'[ \t]+(?&amp;lt;packages&amp;gt;.*)$'&lt;/span&gt;&lt;span class="hl-p"&gt;)&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;get&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;packages&lt;/span&gt;
&lt;span class="hl-w"&gt;            &lt;/span&gt;&lt;span class="hl-c1"&gt;# ...&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;&lt;span class="hl-p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(This website uses &lt;a href="https://pygments.org/"&gt;Pygments&lt;/a&gt; to do syntax highlighting.
I found that it doesn't support Nushell syntax yet, so the above is rendered as
Perl for now. Perl is my go-to when I need syntax highlighting on an
unsupported language, since it accepts almost any syntax.)&lt;/p&gt;
&lt;p&gt;I also appreciate how I was able to replace this sequence of sed commands. It
was opaque enough to require a comment, and the sed command had two bugs I
found in writing this post (missing &lt;code&gt;-E&lt;/code&gt;, which is needed for the &lt;code&gt;+&lt;/code&gt;, and
missing &lt;code&gt;m&lt;/code&gt; flag, which is needed for multiline processing):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-c1"&gt;# This sed expression drops the filename, splits the package list by the comma&lt;/span&gt;
&lt;span class="hl-c1"&gt;# delimiter, and drops the section names.&lt;/span&gt;
&lt;span class="hl-nv"&gt;PACKAGES&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-nv"&gt;$LINES&lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;-E&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'s/^.* +//; s/,/\n/g; s/^.*\///m'&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this Nushell code (following the longer pattern above), which makes its
intent more clear:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-c1"&gt;# ...&lt;/span&gt;
&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;split&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;row&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;','&lt;/span&gt;
&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;str&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;replace&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;-&lt;/span&gt;&lt;span class="hl-n"&gt;r&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;'^.*/'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, another buggy call to sed in Posix shell (this won't work with
newlines):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-nv"&gt;$PACKAGES&lt;/span&gt;&lt;span class="hl-s2"&gt;"&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'s/ /|/g'&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes a more obvious operation in Nushell:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nushell"&gt;&lt;span class="hl-p"&gt;(&lt;/span&gt;&lt;span class="hl-nv"&gt;$packages&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-n"&gt;str&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;join&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;'|'&lt;/span&gt;&lt;span class="hl-p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section id="performance"&gt;
&lt;h2&gt;Performance&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#performance"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I ran a quick benchmark to compare the versions of &lt;code&gt;apt-binary.sh&lt;/code&gt; (with and
without ripgrep) and &lt;code&gt;apt-binary.nu&lt;/code&gt;. I benchmarked with the files in cache, by
repeating each command at least once and discarding the first result, although
it didn't seem to matter much. I used
&lt;a href="https://github.com/nushell/nushell/releases/tag/0.103.0"&gt;Nushell v0.103.0&lt;/a&gt;
from the Github release binary for &lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I ran this in a container where there wasn't much parallelism available, as it
only had Debian Bookworm package lists for one architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;ls&lt;span class="hl-w"&gt; &lt;/span&gt;/var/lib/apt/lists/*Contents*&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;get&lt;span class="hl-w"&gt; &lt;/span&gt;size&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sort
&lt;span class="hl-go"&gt;╭───┬──────────╮&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 0 │  13.6 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 1 │ 105.9 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 2 │ 132.2 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 3 │ 166.8 kB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 4 │   1.5 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 5 │  19.8 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;│ 6 │  57.0 MB │&lt;/span&gt;
&lt;span class="hl-go"&gt;╰───┴──────────╯&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the results for the two versions:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt-get&lt;span class="hl-w"&gt; &lt;/span&gt;remove&lt;span class="hl-w"&gt; &lt;/span&gt;-y&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;ignore
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.sh&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;Run 'sudo apt install ripgrep lz4' to speed this up&lt;/span&gt;
&lt;span class="hl-go"&gt;Sorting... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;Full Text Search... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;cvs/stable 2:1.12.13+real-28+deb12u1 amd64&lt;/span&gt;
&lt;span class="hl-go"&gt;  Concurrent Versions System&lt;/span&gt;

&lt;span class="hl-go"&gt;908ms 116µs 365ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt-get&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;-y&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;ignore
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.sh&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;...&lt;/span&gt;
&lt;span class="hl-go"&gt;804ms 462µs 550ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;apt-binary.nu&lt;span class="hl-w"&gt; &lt;/span&gt;cvs&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;...&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 510ms 564µs 515ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Nushell version takes almost twice as long as the ripgrep version. It seems
to be much slower at doing pipelines with built-in commands, while performing
similarly to &lt;a href="https://en.wikipedia.org/wiki/Almquist_shell#Dash"&gt;dash&lt;/a&gt; for
external pipelines.&lt;/p&gt;
&lt;p&gt;Here's an example with counting bytes, where piping into Nushell's
&lt;a href="https://www.nushell.sh/commands/docs/length.html"&gt;&lt;code&gt;length&lt;/code&gt;&lt;/a&gt; adds a lot of
time:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;&lt;span class="hl-nb"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"/var/lib/apt/lists/deb.debian.org_debian_dists_bookworm_main_Contents-all.lz4"&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;open&lt;span class="hl-w"&gt; &lt;/span&gt;--raw&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;wc&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;57084589&lt;/span&gt;
&lt;span class="hl-go"&gt;4ms 101µs 100ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;open&lt;span class="hl-w"&gt; &lt;/span&gt;--raw&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;57084589&lt;/span&gt;
&lt;span class="hl-go"&gt;756ms 112µs 752ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sh&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;$'lz4 -d ($f) -c | wc -c'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;252ms 204µs 142ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;wc&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;259ms 633µs 621ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;516439434&lt;/span&gt;
&lt;span class="hl-go"&gt;6sec 361ms 656µs 908ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's another example for a basic grep or equivalent, where Nushell's
&lt;a href="https://www.nushell.sh/commands/docs/find.html"&gt;&lt;code&gt;find&lt;/code&gt;&lt;/a&gt; takes much longer:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sh&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;$'lz4 -d ($f) -c | grep ripgrep'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;283ms 70µs 233ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;297ms 517µs 864ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;find&lt;span class="hl-w"&gt; &lt;/span&gt;ripgrep&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/src/rustc-1.78.0/src/tools/rust-analyzer/crates/project-model/test_data/ripgrep-metadata.json devel/rust-web-src&lt;/span&gt;
&lt;span class="hl-go"&gt;805ms 466µs 406ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These may not be quite apples-to-apples comparisons for reasons like Unicode
handling, but it seems like Nushell's internal pipelines could use more
optimization.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="refactoring-issues"&gt;
&lt;h2&gt;Refactoring issues&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#refactoring-issues"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Nushell has some support for
&lt;a href="https://www.nushell.sh/book/testing.html"&gt;testing and assertions&lt;/a&gt;, which I
hoped to use for testing the parsing code. I ran into some problems, however.
It seems like &lt;a href="https://www.nushell.sh/commands/docs/parse.html"&gt;&lt;code&gt;parse&lt;/code&gt;&lt;/a&gt; is
special in being able to take a byte stream and parse lines of it,
but this somehow doesn't work with a function call (a "command" in Nushell
terminology):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;&lt;span class="hl-nb"&gt;let&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"/var/lib/apt/lists/deb.debian.org_debian_dists_bookworm_main_Contents-all.lz4"&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;def&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;[&lt;/span&gt;pattern&lt;span class="hl-o"&gt;]&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$pattern&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^usr/bin/(.*) "&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 142ms 15µs 922ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^usr/bin/(.*) "&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;0&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 344ms 869µs 81ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lines&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parse&lt;span class="hl-w"&gt; &lt;/span&gt;-r&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^usr/bin/(.*) "&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;1sec 269ms 821µs 783ns&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;timeit&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$f&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-c&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;lines&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;parsefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^usr/bin/(.*) "&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;length&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;print&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-go"&gt;8791&lt;/span&gt;
&lt;span class="hl-go"&gt;3sec 169ms 105µs 892ns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It seems like byte streams can't cross into function calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/echo&lt;span class="hl-w"&gt; &lt;/span&gt;hi&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describe
&lt;span class="hl-go"&gt;byte stream&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;def&lt;span class="hl-w"&gt; &lt;/span&gt;describefn&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;[]&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;{&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$in&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describe&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-o"&gt;}&lt;/span&gt;
&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/echo&lt;span class="hl-w"&gt; &lt;/span&gt;hi&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;describefn
&lt;span class="hl-go"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So by trying to refactor my code to test it, I unintentionally changed its
behavior. Getting the old behavior back would require harming performance
drastically or finding a different abstraction boundary.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="closing"&gt;
&lt;h2&gt;Closing thoughts&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#closing"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I'm torn about Nushell. Even with over two decades of using Bash/Dash/Zsh, I
still struggle with it. In writing this post, I found 3 bugs in my Posix shell
script. Nushell feels like a massive improvement. It has nice syntax, type
checking, data structures, convenient argument parsing, and a cohesive library
of built-in commands. Nushell remains succinct enough to feel like a shell
rather than a programming language, with easy escapes into "raw" Unix programs.&lt;/p&gt;
&lt;p&gt;Beyond retraining my brain, I struggle with two things. First, it didn't take
me long to run into the issues above, so Nushell may still need more time/work
to mature. Second, whenever I need interoperability with others, I can count on
them having a Posix shell available, but I can't count on them using Nushell. I
shouldn't let that hold me back from using a better tool on my own computers,
but I can't escape having to write Posix shell scripts going forward, which
means having to remember all the gotchas and tricks. Maybe it'd help if there
was a subset of Nushell that could be compiled to Posix shell for
interoperability.&lt;/p&gt;
&lt;/section&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/trying-nushell-5b755f0f/</guid><pubDate>Mon, 28 Apr 2025 23:03:00 GMT</pubDate></item><item><title>Running Virtual Machines on Linux</title><link>https://yieldsfalsehood.com/aeolus/posts/running-virtual-machines-on-linux-d633831e/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;Virtual machines are useful for running other operating systems within your
computer and for testing and sandboxing system-level software. This post is
about running VMs locally on Linux: how to get a usable disk image and how to
connect to it over SSH. It's not as trivial as it sounds.&lt;/p&gt;
&lt;p&gt;The VM ecosystem has evolved over the last decades.
&lt;a href="https://en.wikipedia.org/wiki/QEMU"&gt;QEMU&lt;/a&gt;/&lt;a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine"&gt;KVM&lt;/a&gt;
is still the easiest way to get started on Linux, but many of the ecosystem's
projects are designed for running cloud services rather than for desktop or
casual server use. This post aims to provide simple instructions for running
VMs without installing large software stacks. It covers running VMs with and
without &lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; and
&lt;a href="https://libvirt.org/"&gt;libvirt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post deals with a few challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Operating systems and distributions traditionally offered installers, which
you'd insert into a computer on removable media, like a CD or USB stick.
Installers can also be used for VMs, but they take a lot of steps to run.
Now, distros typically offer multiple types of disk images, letting users
skip the install process. (These images handle issues that normally come with
cloning disks, like generating new machine IDs and SSH keys.) Where to find
these images or which one to choose can be non-obvious. For VMs, I usually
want barebone images that offer a quick path to SSH access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The virtual disk images are often too small to be practical. For example,
Debian's are only 2 GB in size, so you can't download or install much
software in them before running out of space. Worse yet, lots of software
breaks with confusing errors when the filesystem is out of space. This post
includes instructions for growing the disk images, their root partition, and
their root filesystem. Since QEMU's
&lt;a href="https://en.wikipedia.org/wiki/Qcow"&gt;&lt;code&gt;qcow&lt;/code&gt;&lt;/a&gt; disk image format is sparse,
this won't require much more space on the host's disk.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This post uses
&lt;a href="https://wiki.qemu.org/Documentation/Networking"&gt;QEMU's default user-mode networking stack&lt;/a&gt;,
which creates a local network with NAT for the VM. This allows the VM to make
connections to the Internet and to the host (at &lt;code&gt;10.0.2.2&lt;/code&gt;). Note that the VM
might not be able to ping the host, but TCP and UDP traffic should still
work. However, the host won't be allowed to make connections to the VM. This
post uses QEMU's host port forwarding to allow the host to connect over SSH
to the guest VM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; The VM will be able to access services listening
on localhost on the host, as well as services on the local network, which
could be security concerns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Logging into the distribution-provided disk images can also be a challenge.
This post gets to logging in over SSH and includes both manually installing
an SSH key and using &lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; to
automate this process.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These instructions are Linux, Debian, and &lt;code&gt;amd64&lt;/code&gt;-centric because that's what
I'm most familiar with. There are many alternatives and options at every level
of the stack. This post aims to provide a reasonable starting point, not the
most optimal configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; There's a follow-up post on
&lt;a href="https://ongardie.net/blog/desktop-vm/"&gt;running desktop VMs&lt;/a&gt;.&lt;/p&gt;
&lt;section id="nocloud"&gt;
&lt;h2&gt;&lt;code&gt;nocloud&lt;/code&gt; images&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#nocloud"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section describes how to start a basic interactive VM. To achieve this,
we'll use QEMU/KVM directly and use Debian's &lt;code&gt;nocloud&lt;/code&gt; image. This image allows
&lt;code&gt;root&lt;/code&gt; to log in with no password and does not have &lt;code&gt;cloud-init&lt;/code&gt; installed.
(The next section deals with &lt;code&gt;cloud-init&lt;/code&gt; images.)&lt;/p&gt;
&lt;p&gt;First, ensure the host CPU has
&lt;a href="https://en.wikipedia.org/wiki/X86_virtualization#Central_processing_unit"&gt;virtualization extensions&lt;/a&gt;
enabled. Almost all modern &lt;code&gt;amd64&lt;/code&gt;/&lt;code&gt;x86-64&lt;/code&gt; CPUs support virtualization, but
some systems have this disabled in the UEFI settings. For AMD, the extensions
are called SVM or AMD-V, and should cause an &lt;code&gt;svm&lt;/code&gt; flag to show up in
&lt;code&gt;/proc/cpuinfo&lt;/code&gt; when enabled. For Intel, the extensions are called VT-x and
should cause a &lt;code&gt;vmx&lt;/code&gt; flag to show up in &lt;code&gt;/proc/cpuinfo&lt;/code&gt; when enabled.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-P&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'^flags\b.*\b(svm|vmx)\b'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/proc/cpuinfo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you get no output, reboot into your UEFI settings, enable the setting, and
check again.&lt;/p&gt;
&lt;p&gt;Then, install the packages on the host for KVM/QEMU:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;ovmf&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-system-gui&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-system-x86&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Download the Debian 12 &lt;code&gt;nocloud&lt;/code&gt; VM image, resize the virtual disk, and run the
VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.qcow2
cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-nocloud-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
kvm&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;4G&lt;span class="hl-w"&gt; &lt;/span&gt;-nic&lt;span class="hl-w"&gt; &lt;/span&gt;user,hostfwd&lt;span class="hl-o"&gt;=&lt;/span&gt;tcp::2200-:22&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The options for running KVM/QEMU are vast. Refer to the
&lt;a href="https://dyn.manpages.debian.org/bookworm/qemu-system-x86/qemu-system-x86_64.1.en.html"&gt;man page&lt;/a&gt;
and &lt;a href="https://www.qemu.org/docs/master/system/index.html"&gt;docs&lt;/a&gt; as needed.&lt;/p&gt;
&lt;p&gt;In the graphical window that pops up, log into the VM as &lt;code&gt;root&lt;/code&gt; with no
password.&lt;/p&gt;
&lt;p&gt;Note: If the VM grabs (steals) your mouse and keyboard, there's a magical key
combination to escape, which is
&lt;kbd&gt;&lt;kbd&gt;Ctrl&lt;/kbd&gt;-&lt;kbd&gt;Alt&lt;/kbd&gt;-&lt;kbd&gt;G&lt;/kbd&gt;&lt;/kbd&gt; for me.
Check the window titlebar for a hint if that doesn't work.&lt;/p&gt;
&lt;p&gt;Install an SSH server in the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
apt&lt;span class="hl-w"&gt; &lt;/span&gt;instal&lt;span class="hl-w"&gt; &lt;/span&gt;--yes&lt;span class="hl-w"&gt; &lt;/span&gt;openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you have some authentication options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ideally, authorize an SSH key, for example by downloading it from GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mkdir&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;700&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;.ssh
curl&lt;span class="hl-w"&gt; &lt;/span&gt;https://github.com/⟪USER⟫.keys&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;tee&lt;span class="hl-w"&gt; &lt;/span&gt;-a&lt;span class="hl-w"&gt; &lt;/span&gt;.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Or if that's inconvenient, set a root password:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;passwd
&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'PermitRootLogin yes'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/etc/ssh/sshd_config.d/10rootpassword.conf
systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;restart&lt;span class="hl-w"&gt; &lt;/span&gt;ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Or if you don't care for this VM at all and are confident in the security of
your host's network, allow SSH as root with no password:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cat&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/etc/ssh/sshd_config.d/10insecure.conf&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;lt;&amp;lt;END&lt;/span&gt;
&lt;span class="hl-s"&gt;PermitRootLogin yes&lt;/span&gt;
&lt;span class="hl-s"&gt;PermitEmptyPasswords yes&lt;/span&gt;
&lt;span class="hl-s"&gt;END&lt;/span&gt;
systemctl&lt;span class="hl-w"&gt; &lt;/span&gt;restart&lt;span class="hl-w"&gt; &lt;/span&gt;ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, you can SSH from the host:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;root@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From inside the VM, resize the root partition and filesystem:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;--yes&lt;span class="hl-w"&gt; &lt;/span&gt;cloud-guest-utils
growpart&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/sda&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;1&lt;/span&gt;
resize2fs&lt;span class="hl-w"&gt; &lt;/span&gt;/dev/sda1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;You can shut it down gracefully or kill the &lt;code&gt;kvm&lt;/code&gt; process to stop the VM. Then
you can remove its disk image.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="cloud-init"&gt;
&lt;h2&gt;&lt;code&gt;cloud-init&lt;/code&gt; images&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#cloud-init"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section describes how to run
&lt;a href="https://cloudinit.readthedocs.io/"&gt;&lt;code&gt;cloud-init&lt;/code&gt;&lt;/a&gt; images, which are provided by
many distributions and operating systems. &lt;code&gt;cloud-init&lt;/code&gt; is software inside the
VM that runs at boot to discover configuration provided to it from the outside
world. This requires a little more setup before starting the VM, but then the
VM will be better configured on startup.&lt;/p&gt;
&lt;p&gt;There are various ways to inject the &lt;code&gt;cloud-init&lt;/code&gt; configuration. This post
uses the simplest
&lt;a href="https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html#source-2-drive-with-labeled-filesystem"&gt;NoCloud data source with an extra attached drive&lt;/a&gt;.
The extra drive (like a virtual CD-ROM or thumb drive) is given a volume label
of &lt;code&gt;CIDATA&lt;/code&gt; so &lt;code&gt;cloud-init&lt;/code&gt; inside the VM can discover it during boot and look
for configuration files inside.&lt;/p&gt;
&lt;p&gt;One added benefit of using &lt;code&gt;cloud-init&lt;/code&gt; is that these images typically have
&lt;a href="https://packages.debian.org/bookworm/cloud-initramfs-growroot"&gt;&lt;code&gt;cloud-initramfs-growroot&lt;/code&gt;&lt;/a&gt;,
which means they will automatically grow their root partition and filesystem if
the virtual disk has extra space (at least on the first boot). This saves us
some steps.&lt;/p&gt;
&lt;p&gt;In this section, we'll use Debian's &lt;code&gt;generic&lt;/code&gt; image, which contains
&lt;code&gt;cloud-init&lt;/code&gt;. There is no way to log into this image without using
&lt;code&gt;cloud-init&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;nocloud&lt;/code&gt; terminology is confusing: Debian &lt;code&gt;nocloud&lt;/code&gt; images do not contain
&lt;code&gt;cloud-init&lt;/code&gt;. Debian &lt;code&gt;generic&lt;/code&gt; images contain &lt;code&gt;cloud-init&lt;/code&gt; and can use its
NoCloud data source. No clouds were harmed in the writing of this blog post.&lt;/p&gt;
&lt;p&gt;Debian also offers a &lt;code&gt;genericcloud&lt;/code&gt; image, but I don't recommend it. It's
smaller than the &lt;code&gt;generic&lt;/code&gt; image by omitting a bunch of drivers (about
330 MB vs 420 MB). However, these drivers can be useful, even with
KVM/QEMU:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;QEMU emulates an &lt;code&gt;e1000&lt;/code&gt; driver for the NIC by default, and the
&lt;code&gt;genericcloud&lt;/code&gt; image doesn't have that driver. (You can use a &lt;code&gt;virtio&lt;/code&gt; NIC to
work around this by passing &lt;code&gt;model=virtio-net-pci&lt;/code&gt; as an option to &lt;code&gt;-nic&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;The way that &lt;code&gt;cloud-install&lt;/code&gt; presents its &lt;code&gt;cloud-init&lt;/code&gt; configuration drive
(as used in the next section) also requires drivers, so the &lt;code&gt;genericcloud&lt;/code&gt;
images will fail to find the drive.&lt;/li&gt;
&lt;li&gt;I don't know what other QEMU devices might have missing drivers and cause
headaches in the future.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Download the Debian 12 &lt;code&gt;generic&lt;/code&gt; VM image and resize the virtual disk:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-generic-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, if you run &lt;code&gt;kvm&lt;/code&gt; like before, you'll observe that you can't log
in. This can be frustrating if &lt;code&gt;cloud-init&lt;/code&gt; somehow isn't working.&lt;/p&gt;
&lt;p&gt;Next, set up a &lt;code&gt;cloud-init&lt;/code&gt; configuration to set a password and authorize an
SSH key. This happens in a file called &lt;code&gt;user-data&lt;/code&gt;. The user-data file format
is YAML with an extra &lt;code&gt;#cloud-config&lt;/code&gt; header. This post uses a more JSON-like
format to prevent whitespace errors; as YAML is a superset of JSON, this is
allowed. These options apply to the default user, which varies per distro. The
default username is &lt;code&gt;debian&lt;/code&gt; for Debian images.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;mkdir&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;cidata
tee&lt;span class="hl-w"&gt; &lt;/span&gt;cidata/user-data&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s"&gt;&amp;lt;&amp;lt;END&lt;/span&gt;
&lt;span class="hl-s"&gt;#cloud-config&lt;/span&gt;
&lt;span class="hl-s"&gt;{&lt;/span&gt;
&lt;span class="hl-s"&gt;    "password": "p4ssw0rd",&lt;/span&gt;
&lt;span class="hl-s"&gt;    "chpasswd": {&lt;/span&gt;
&lt;span class="hl-s"&gt;        "expire": false,&lt;/span&gt;
&lt;span class="hl-s"&gt;    },&lt;/span&gt;
&lt;span class="hl-s"&gt;    "ssh_authorized_keys": [&lt;/span&gt;
&lt;span class="hl-s"&gt;        "$(cat ~/.ssh/id_ed25519.pub)",&lt;/span&gt;
&lt;span class="hl-s"&gt;    ],&lt;/span&gt;
&lt;span class="hl-s"&gt;}&lt;/span&gt;
&lt;span class="hl-s"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, create a blank &lt;code&gt;meta-data&lt;/code&gt; file, since that's required:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;cidata/meta-data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://docs.cloud-init.io/en/latest/reference/modules.html#users-and-groups"&gt;&lt;code&gt;cloud-init&lt;/code&gt; documentation&lt;/a&gt; describes the possible fields.&lt;/p&gt;
&lt;p&gt;To get these files to the VM, they'll need to be packaged into an ISO or VFAT
filesystem with the volume label of &lt;code&gt;CIDATA&lt;/code&gt;. Rather than do that manually,
&lt;a href="https://www.qemu.org/docs/master/system/qemu-block-drivers.html#virtual-fat-disk-images"&gt;QEMU can create a VFAT drive from a directory&lt;/a&gt;.
The QEMU invocation is a mouthful, but it's still fairly convenient.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;kvm&lt;span class="hl-w"&gt; &lt;/span&gt;-m&lt;span class="hl-w"&gt; &lt;/span&gt;4G&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;-nic&lt;span class="hl-w"&gt; &lt;/span&gt;user,hostfwd&lt;span class="hl-o"&gt;=&lt;/span&gt;tcp::2200-:22&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;-drive&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;file&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;fat:./cidata,format&lt;span class="hl-o"&gt;=&lt;/span&gt;vvfat,if&lt;span class="hl-o"&gt;=&lt;/span&gt;virtio,label&lt;span class="hl-o"&gt;=&lt;/span&gt;CIDATA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If all goes well, you can log in interactively as user &lt;code&gt;debian&lt;/code&gt; with password
&lt;code&gt;p4ssw0rd&lt;/code&gt; (as set in &lt;code&gt;user-data&lt;/code&gt;), and that user can &lt;code&gt;sudo&lt;/code&gt; without a
password. You can also use SSH with your SSH key:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;debian@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to &lt;code&gt;cloud-init&lt;/code&gt;, the root partition and filesystem have already been
expanded to the available disk image size.&lt;/p&gt;
&lt;p&gt;Upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;Next time you run the VM, you don't need to provide the &lt;code&gt;CIDATA&lt;/code&gt; image, since
its job is done.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="libvirt"&gt;
&lt;h2&gt;libvirt&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#libvirt"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This section runs VMs using &lt;a href="https://libvirt.org/"&gt;libvirt&lt;/a&gt;, which is a way of
managing VMs without lengthy KVM invocations. libvirt isn't necessary, but it's
probably a better approach if you want to manage long-lived VMs with a variety
of operating systems. It also configures KVM with more modern defaults.&lt;/p&gt;
&lt;p&gt;Libvirt is a collection of software, named after the underlying library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/libvirt-clients/virsh.1.en.html"&gt;&lt;code&gt;virsh&lt;/code&gt;&lt;/a&gt;
is its main command-line interface.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/virtinst/virt-install.1.en.html"&gt;&lt;code&gt;virt-install&lt;/code&gt;&lt;/a&gt;
is a command-line tool to create the VMs (as this is difficult to do with
&lt;code&gt;virsh&lt;/code&gt; directly).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/virt-viewer/virt-viewer.1.en.html"&gt;&lt;code&gt;virt-viewer&lt;/code&gt;&lt;/a&gt;
provides a virtual keyboard, video, and mouse.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://virt-manager.org/"&gt;&lt;code&gt;virt-manager&lt;/code&gt;&lt;/a&gt; is a GUI for managing VMs. While
you can use the GUI to do most of this, this post focuses on the command-line
tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Libvirt can be used in a system-wide mode (&lt;code&gt;qemu:///system&lt;/code&gt;) or for your local
user (&lt;code&gt;qemu:///session&lt;/code&gt;). This post uses the user mode. (The system mode may be
useful to bridge your VMs to the host's network. To use the system mode, you'd
need to install &lt;code&gt;libvirt-daemon-system&lt;/code&gt; and add your user to the &lt;code&gt;libvirt&lt;/code&gt;
group.)&lt;/p&gt;
&lt;p&gt;Unfortunately, different libvirt tools have different default modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virsh&lt;/code&gt; defaults to &lt;code&gt;qemu:///session&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-install&lt;/code&gt; defaults to &lt;code&gt;qemu:///system&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-manager&lt;/code&gt; defaults to showing both and connecting to neither.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;virt-viewer&lt;/code&gt; defaults to &lt;code&gt;qemu:///session&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can pass &lt;code&gt;--connect qemu:///session&lt;/code&gt; to any of these tools. You may want to
set up shell aliases for convenience.&lt;/p&gt;
&lt;p&gt;Install the host packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;install&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;gir1.2-spiceclientgtk-3.0&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;libvirt-clients&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;libvirt-daemon&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virtinst&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virt-manager&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;virt-viewer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a VM using &lt;code&gt;virt-install&lt;/code&gt; based on Debian's &lt;code&gt;generic&lt;/code&gt; image. This uses
the image downloaded and the &lt;code&gt;cloud-init&lt;/code&gt; configuration files created in the
previous section.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cp&lt;span class="hl-w"&gt; &lt;/span&gt;-i&lt;span class="hl-w"&gt; &lt;/span&gt;debian-12-generic-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2
qemu-img&lt;span class="hl-w"&gt; &lt;/span&gt;resize&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;32G
virt-install&lt;span class="hl-w"&gt; &lt;/span&gt;--connect&lt;span class="hl-w"&gt; &lt;/span&gt;qemu:///session&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--cloud-init&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'disable=on,meta-data=./cidata/meta-data,user-data=./cidata/user-data'&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--disk&lt;span class="hl-w"&gt; &lt;/span&gt;test.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--import&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--memory&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;4096&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--name&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;test&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;--os-variant&lt;span class="hl-w"&gt; &lt;/span&gt;debian11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses the &lt;code&gt;debian11&lt;/code&gt; OS variant since the &lt;code&gt;osinfo-db&lt;/code&gt; package in Debian 12
does not currently know about Debian 12. The OS variant doesn't appear to
matter much when importing a disk image.&lt;/p&gt;
&lt;p&gt;Use the key combination &lt;kbd&gt;&lt;kbd&gt;Ctrl&lt;/kbd&gt;-&lt;kbd&gt;]&lt;/kbd&gt;&lt;/kbd&gt; to exit the
&lt;code&gt;virsh&lt;/code&gt; console. You can pass &lt;code style="white-space: nowrap;"&gt;--autoconsole
none&lt;/code&gt; to &lt;code&gt;virt-install&lt;/code&gt; if you don't want to be dropped into the console.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;virt-install&lt;/code&gt; supports &lt;code&gt;cloud-init&lt;/code&gt;, we didn't need to have QEMU present
a &lt;code&gt;CIDATA&lt;/code&gt; drive. Actually, we don't even need the YAML files for simple
settings. The following is usually sufficient:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;--cloud-init&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"disable=on,clouduser-ssh-key=&lt;/span&gt;&lt;span class="hl-nv"&gt;$HOME&lt;/span&gt;&lt;span class="hl-s2"&gt;/.ssh/id_ed25519.pub"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Forward a host port to SSH to the VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;qemu-monitor-command&lt;span class="hl-w"&gt; &lt;/span&gt;--hmp&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nb"&gt;test&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'hostfwd_add tcp::2200-:22'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since libvirt doesn't currently support forwarding host ports, you'll need to
run that &lt;code&gt;hostfwd_add&lt;/code&gt; command every time you run the VM. Note that &lt;code&gt;test&lt;/code&gt; in
the command is the name of the VM. This workaround is thanks to
&lt;a href="https://blog.adamspiers.org/2012/01/23/port-redirection-from-kvm-host-to-guest/"&gt;Adam Spiers' blog post from 2012&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;ssh&lt;span class="hl-w"&gt; &lt;/span&gt;debian@localhost&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-m"&gt;2200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upgrade stale packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;update
sudo&lt;span class="hl-w"&gt; &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;upgrade&lt;span class="hl-w"&gt; &lt;/span&gt;--yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your VM is ready to use.&lt;/p&gt;
&lt;p&gt;These are some useful libvirt commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;List VM statuses:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;list&lt;span class="hl-w"&gt; &lt;/span&gt;--all
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boot an existing VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;start&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View a VM's text console:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;console&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View a VM's graphical console:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virt-viewer&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shut down a VM gracefully:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;shutdown&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Shut down a VM forcefully:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;destroy&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Delete a VM and its disks:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;virsh&lt;span class="hl-w"&gt; &lt;/span&gt;undefine&lt;span class="hl-w"&gt; &lt;/span&gt;⟪NAME⟫&lt;span class="hl-w"&gt; &lt;/span&gt;--remove-all-storage
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And recall that &lt;code&gt;virt-manager&lt;/code&gt; is a useful GUI that also provides all this
&lt;code&gt;virsh&lt;/code&gt; and &lt;code&gt;virt-viewer&lt;/code&gt; functionality and more.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="someday"&gt;
&lt;h2&gt;Someday/maybe&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#someday"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/virt-manager/virt-manager/issues/143"&gt;&lt;code&gt;virt-manager&lt;/code&gt; issue #143&lt;/a&gt;
would allow using &lt;code&gt;cloud-init&lt;/code&gt; when creating new VMs from the GUI.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/libvirt/libvirt/-/issues/285"&gt;libvirt issue #285&lt;/a&gt; would
support forwarding host ports.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://manpages.debian.org/bookworm/libguestfs-tools/virt-customize.1.en.html"&gt;&lt;code&gt;virt-customize&lt;/code&gt;&lt;/a&gt;
appears to directly modify VM disk image files and may be an alternative to
&lt;code&gt;cloud-init&lt;/code&gt;. I haven't tried it.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://libvirt.org/ssh-proxy.html"&gt;libvirt SSH proxy&lt;/a&gt; should allow
SSH to VMs over &lt;a href="https://wiki.qemu.org/Features/VirtioVsock"&gt;VSOCK&lt;/a&gt; instead
of TCP. This would remove the need for port forwarding for SSH. However, it's
not packaged for Debian or widely supported in VM images yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Update 2025-05-08:&lt;/strong&gt; There's a follow-up post on
&lt;a href="https://ongardie.net/blog/desktop-vm/"&gt;running desktop VMs&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="images"&gt;
&lt;h2&gt;Other distributions and operating systems&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#images"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many operating systems offer cloud images and &lt;code&gt;cloud-init&lt;/code&gt; support. This
section documents a few that I've tried.&lt;/p&gt;
&lt;h4&gt;Debian&lt;/h4&gt;
&lt;p&gt;Debian images for Debian 12 (Bookworm) were covered above. For other releases,
see &lt;a href="https://cloud.debian.org/images/cloud/"&gt;https://cloud.debian.org/images/cloud/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The default user is &lt;code&gt;debian&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant debian11&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;Fedora&lt;/h4&gt;
&lt;p&gt;These images work with and require &lt;code&gt;cloud-init&lt;/code&gt;. For other releases, see
&lt;a href="https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/"&gt;https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/&lt;/a&gt;. For more
info, see &lt;a href="https://fedoraproject.org/cloud/download"&gt;https://fedoraproject.org/cloud/download&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Fedora 41:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-LO&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default user is &lt;code&gt;fedora&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant fedora37&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;FreeBSD&lt;/h4&gt;
&lt;p&gt;These images allow logging in interactively as &lt;code&gt;root&lt;/code&gt; with no password, and
they grow their root filesystem automatically. Despite its name, even the
&lt;code&gt;BASIC-CLOUDINIT&lt;/code&gt; images do not appear to run &lt;code&gt;cloud-init&lt;/code&gt; yet. For other
releases, see &lt;a href="https://download.freebsd.org/releases/VM-IMAGES/"&gt;https://download.freebsd.org/releases/VM-IMAGES/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;FreeBSD 14.1 &lt;code&gt;BASIC-CLOUDINIT&lt;/code&gt; with ZFS variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2.xz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;xz&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FreeBSD 14.1 standard with ZFS variant:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-zfs.qcow2.xz&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;xz&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="hl-w"&gt; &lt;/span&gt;FreeBSD-14.1-RELEASE-amd64-zfs.qcow2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant freebsd13.1&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;h4&gt;Ubuntu&lt;/h4&gt;
&lt;p&gt;These images work with and require &lt;code&gt;cloud-init&lt;/code&gt; (which is a Canonical project).
For other releases, see &lt;a href="https://cloud-images.ubuntu.com/"&gt;https://cloud-images.ubuntu.com/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ubuntu 24.04 LTS (Noble):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;-o&lt;span class="hl-w"&gt; &lt;/span&gt;noble-server-cloudimg-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ubuntu 24.10 (Oracular):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;curl&lt;span class="hl-w"&gt; &lt;/span&gt;-L&lt;span class="hl-w"&gt; &lt;/span&gt;-o&lt;span class="hl-w"&gt; &lt;/span&gt;oracular-server-cloudimg-amd64.qcow2&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-se"&gt;\&lt;/span&gt;
&lt;span class="hl-w"&gt;    &lt;/span&gt;https://cloud-images.ubuntu.com/oracular/current/oracular-server-cloudimg-amd64.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default user is &lt;code&gt;ubuntu&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;--os-variant ubuntu22.10&lt;/code&gt; as the closest version known to Debian 12.&lt;/p&gt;
&lt;/section&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/running-virtual-machines-on-linux-d633831e/</guid><pubDate>Mon, 02 Dec 2024 01:33:00 GMT</pubDate></item><item><title>Website Updates (2024)</title><link>https://yieldsfalsehood.com/aeolus/posts/website-updates-2024-9b870b0d/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;I did a bunch of more updates to this website, including adding a dark mode.
Most of the other changes are either invisible or barely noticeable. That's OK.
My &lt;em&gt;several&lt;/em&gt; visitors will appreciate them. Or at least I'll appreciate them.&lt;/p&gt;

&lt;section id="dark-mode"&gt;
&lt;h2&gt;Dark mode and syntax highlighting&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#dark-mode"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added a dark mode using CSS, with
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt;
(widely availabe as of 2020). I switched syntax highlighting (using
&lt;a href="https://pygments.org/"&gt;Pygments&lt;/a&gt;) from inline styles to CSS classes, which use
different theme colors for light and dark modes. This change will cause the RSS
feed to not have syntax highlighting (since RSS readers shouldn't interpret CSS
classes), whereas before some RSS readers may have allowed the style elements.
I think RSS feeds aren't supposed to be styled, so that's probably OK.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="rss-feed"&gt;
&lt;h2&gt;RSS feed&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#rss-feed"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I reduced the size of the RSS feed by only including summaries for old posts,
rather than the full content. Also, if I write too many more posts, the RSS
feed will stop including the oldest posts. I really prefer when RSS feeds
include the full content with each post, but I think that's less useful for
very old posts.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="markdown"&gt;
&lt;h2&gt;Markdown renderer&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#markdown"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many of these pages are generated from Markdown. I added support for it in 2014
using &lt;a href="https://python-markdown.github.io/"&gt;Python-Markdown&lt;/a&gt;, which was probably
the obvious choice. Since then, some folks standardized Markdown as
&lt;a href="https://commonmark.org/"&gt;CommonMark&lt;/a&gt;, which is used widely in VS Code and
GitHub (GitHub Flavored Markdown extends CommonMark), among others. The
differences between Python-Markdown and CommonMark are small but annoying, so
I've switched to &lt;a href="https://markdown-it-py.readthedocs.io/"&gt;markdown-it-py&lt;/a&gt;.
Since this website is generated as static pages, it was relatively easy to diff
the HTML and RSS across this change for manual review.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="mako-conflicts"&gt;
&lt;h2&gt;Mako parsing conflicts&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#mako-conflicts"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This website contains both HTML and Markdown pages and templates. The Markdown
pages are processed through the
&lt;a href="https://www.makotemplates.org/"&gt;Mako templating engine&lt;/a&gt; and then the
Markdown renderer. The HTML pages are just processed through Mako. Most of what
I use Mako for is basic variable substitution, loops, and if statements. It
also allows running full Python code inside templates, which I like. (I write
the templates, so I can live with myself abusing them. I wouldn't want this in
a larger team project.)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.makotemplates.org/en/latest/syntax.html"&gt;Mako's syntax&lt;/a&gt; is
usually not in conflict with Markdown or HTML, but I've found three exceptions
where there are ambiguities:&lt;/p&gt;
&lt;section id="variable-ambiguity"&gt;
&lt;h3&gt;&lt;code&gt;${variable}&lt;/code&gt;&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#variable-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako interprets the occasional &lt;code&gt;${variable}&lt;/code&gt; in a shell
script (or &lt;em&gt;this&lt;/em&gt; paragraph) as a Mako variable substitution. Sometimes I want
one behavior (Mako) and sometimes I want the other (literal). Fortunately, when
this happens, it's unlikely that the variable exists in Mako's scope, so it
usually causes a build error.&lt;/p&gt;
&lt;p&gt;I haven't found an easy way to escape &lt;code&gt;${variable}&lt;/code&gt; locally.
The best approach is to use
&lt;a href="https://docs.makotemplates.org/en/latest/syntax.html#text"&gt;&lt;code&gt;&amp;lt;%text&amp;gt;&lt;/code&gt;&lt;/a&gt;
to opt out of Mako for an entire section. Otherwise, using &lt;code&gt;&amp;amp;dollar;&lt;/code&gt; is an
option in HTML, but not within a &lt;code&gt;`code block`&lt;/code&gt; in Markdown
(&lt;a href="https://spec.commonmark.org/0.31.2/#example-35"&gt;see CommonMark example&lt;/a&gt;).
Another approach is to define a Mako variable called &lt;code&gt;dollar&lt;/code&gt; with a value of
&lt;code&gt;$&lt;/code&gt;, then write &lt;code&gt;${dollar}{variable}&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="backslash-ambiguity"&gt;
&lt;h3&gt;Trailing backslash&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#backslash-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako consumes a trailing backslash at the end of a line and merges the line
below it. Many programming languages use this syntax too. It's easy to forget
about this and get poor rendering of code blocks. Similar to the dollar issue,
&lt;code&gt;&amp;lt;%text&amp;gt;&lt;/code&gt; is a good way to disable it, or you can use &lt;code&gt;&amp;amp;#92;&lt;/code&gt;
in HTML but not Markdown, or you can define a backslash variable.&lt;/p&gt;
&lt;p&gt;I found that I was only using the trailing backslash feature of Mako in one
place, so I wanted to turn it off and have a default that won't keep biting me.
Unfortunately, substituting &lt;code&gt;\\&lt;/code&gt; for &lt;code&gt;\&lt;/code&gt; at the end of the line doesn't help,
as consuming the second backslash is
&lt;a href="https://github.com/sqlalchemy/mako/blob/0348fe3/mako/lexer.py#L381"&gt;baked into Mako's lexer&lt;/a&gt;.
Instead, I added a workaround that replaces a trailing &lt;code&gt;\&lt;/code&gt; with a unique string
before Mako runs and then replaces that string back to a &lt;code&gt;\&lt;/code&gt; after Mako runs. I
think that should in most contexts, including code and non-code and
Mako-enabled and Mako-disabled regions. This does completely prevent using a
trailing backslash in Mako and Mako Python blocks
(&lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt;), but those are usually unnecessary. If I forget
and try to use a trailing backslash in Mako, it's likely to cause a build
error, which is the behavior I want.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="hash-hash-ambiguity"&gt;
&lt;h3&gt;Leading hashes&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#hash-hash-ambiguity"&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mako interprets &lt;code&gt;##&lt;/code&gt; at the start of a line as a comment, which Markdown uses
for &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;. I found out about this one almost immediately after starting to use
Markdown on this site. I don't need that style of Mako comments, so I wanted to
disable them.&lt;/p&gt;
&lt;p&gt;I've had a workaround in place for a while that pre-processed the input and
injected &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; tags before Mako ran. That worked well for me, but in theory it
would break &lt;code&gt;##&lt;/code&gt; if used in code blocks or in Mako Python blocks
(&lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I've updated my workaround to replace a leading &lt;code&gt;##&lt;/code&gt; with &lt;code&gt;#&lt;/code&gt; followed by a
unique string before Mako runs, and then replace it back after Mako runs. I
think that'll work in all contexts.&lt;/p&gt;
&lt;/section&gt;
&lt;/section&gt;
&lt;section id="metadata"&gt;
&lt;h2&gt;Social sharing metadata&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#metadata"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added some meta tags for social media. The
&lt;a href="https://ogp.me/"&gt;Open Graph Protocol&lt;/a&gt;
metadata is used by Meta,
&lt;a href="https://www.linkedin.com/help/linkedin/answer/a521928/making-your-website-shareable-on-linkedin?lang=en"&gt;LinkedIn&lt;/a&gt;,
and others, but that site is
&lt;a href="https://github.com/facebookarchive/open-graph-protocol"&gt;archived on GitHub&lt;/a&gt;.
&lt;a href="https://developers.facebook.com/docs/sharing/webmasters"&gt;Facebook's page&lt;/a&gt; is
another resource. Twitter has
&lt;a href="https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started"&gt;its own metadata&lt;/a&gt;
but will largely use Open Graph metadata if available.
&lt;a href="https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/"&gt;&lt;code&gt;fediverse:creator&lt;/code&gt;&lt;/a&gt;
is used on Mastodon.&lt;/p&gt;
&lt;p&gt;Note: there's a
&lt;a href="https://stackoverflow.com/questions/22350105/whats-the-difference-between-meta-name-and-meta-property"&gt;technical distinction&lt;/a&gt;
between:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;meta&lt;/span&gt; &lt;span class="hl-na"&gt;name&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"..."&lt;/span&gt; &lt;span class="hl-na"&gt;content&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"..."&lt;/span&gt;&lt;span class="hl-p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;meta&lt;/span&gt; &lt;span class="hl-na"&gt;property&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"..."&lt;/span&gt; &lt;span class="hl-na"&gt;content&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"..."&lt;/span&gt;&lt;span class="hl-p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where some fields want one or the other.&lt;/p&gt;
&lt;p&gt;This metadata required some code generator changes. Similar to the page title,
the metadata is set above where the main page content is rendered. That
information has to be "pushed up" to be available outside the main content.
Also, the pages had to be made aware of their URLs for &lt;code&gt;og:url&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The OpenGraph Protocol requires &lt;code&gt;og:image&lt;/code&gt; to be set, yet most of my pages
don't have an image. I created a larger version of the
&lt;a href="https://en.wikipedia.org/wiki/Favicon"&gt;favicon&lt;/a&gt; as a default.&lt;/p&gt;
&lt;p&gt;Here's an example of a link to this blog post on Mastodon:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Mastodon share example" src="https://ongardie.net/var/blog/website-updates-2024/mastodon-share.png"&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;section id="favicon"&gt;
&lt;h2&gt;Favicon&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#favicon"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Since I redrew the &lt;a href="https://en.wikipedia.org/wiki/Favicon"&gt;favicon&lt;/a&gt; as an SVG
for the social sharing image, I also added the SVG for browsers that support
it. I created a dark mode variant, but I don't think mainstream browsers
support that yet (see the links from
&lt;a href="https://github.com/whatwg/html/issues/6408"&gt;this issue&lt;/a&gt;). Firefox seems to
ignore the &lt;code&gt;prefers-color-scheme&lt;/code&gt; and take the last SVG. Other browsers may
default to the first SVG, so I have sandwiched the dark favicon declaration in
between two light ones:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"icon"&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"any"&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"(prefers-color-scheme: light)"&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"favicon-light.svg"&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"icon"&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"any"&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"(prefers-color-scheme: dark)"&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"favicon-dark.svg"&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"icon"&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"any"&lt;/span&gt;
    &lt;span class="hl-na"&gt;media&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"(prefers-color-scheme: light)"&lt;/span&gt; &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"favicon-light.svg"&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="hl-p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="hl-nt"&gt;link&lt;/span&gt; &lt;span class="hl-na"&gt;rel&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"icon"&lt;/span&gt; &lt;span class="hl-na"&gt;type&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"image/png"&lt;/span&gt; &lt;span class="hl-na"&gt;sizes&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"16x16"&lt;/span&gt;
    &lt;span class="hl-na"&gt;href&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-s"&gt;"favicon.png"&lt;/span&gt; &lt;span class="hl-p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully dark mode favicons will start working in more browsers over the next
few years.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="heading-links"&gt;
&lt;h2&gt;Heading links&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#html-css"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I added links to many headings to make sharing URLs that jump to a place on the
page easier. On mobile, you can see an example &lt;code&gt;#&lt;/code&gt; link above this paragraph.
On desktop, hover over the heading to see it.&lt;/p&gt;
&lt;p&gt;I chose not to make the entire heading a link because sometimes headings do
legitimately link to things, and that could get confusing. I like that the &lt;code&gt;#&lt;/code&gt;
is hidden until hovering on desktop, but hovering is not really an available
gesture on touch-screens.&lt;/p&gt;
&lt;p&gt;I didn't want the RSS feed to include these links because they might not work,
so the &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags have no content in the HTML, and CSS fills in the &lt;code&gt;#&lt;/code&gt;. Since
RSS feeds don't use the CSS, they will just get an invisible empty link.&lt;/p&gt;
&lt;p&gt;I added these links manually to a bunch of headings. I didn't see a good way to
automate that, since the value of the &lt;code&gt;id&lt;/code&gt; attribute is worth customizing and
the placement would not be obvious to an algorithm. I ended up adding a bunch
of
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"&gt;&lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements&lt;/a&gt;
(2015) for this.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="html-css"&gt;
&lt;h2&gt;HTML and CSS updates&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#html-css"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I updated and modernized some HTML and CSS, including use of these newer
features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/var"&gt;&lt;code&gt;var()&lt;/code&gt; and custom properties&lt;/a&gt;
(2017) are useful for defining theme colors for light/dark modes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:has"&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/a&gt; (2023) was
useful in styling just the &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; elements that contain &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:is"&gt;&lt;code&gt;:is()&lt;/code&gt;&lt;/a&gt; (2021) helped
save some duplication for styles related to &lt;code&gt;h1&lt;/code&gt;-&lt;code&gt;h6&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:not"&gt;&lt;code&gt;:not()&lt;/code&gt;&lt;/a&gt; (2021)
helped keep some styles self-contained.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing"&gt;&lt;code&gt;box-sizing: border-box&lt;/code&gt;&lt;/a&gt;
(2015) is old news but I started this website in 2007.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For testing dark mode and the styling updates, it was convenient to have the
blog index page render the entire contents of the blog on one page that I could
scroll through quickly.&lt;/p&gt;
&lt;p&gt;I tried to keep the website usable with older phones. For example, I assume iOS
users have Safari 15 but not necessarily Safari 16 or newer yet. These are some
CSS features that look nice but that I've avoided for now until they're more
widely available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope"&gt;&lt;code&gt;@scope&lt;/code&gt;&lt;/a&gt; (not
available yet in Firefox) would be helpful to create a namespace for
Pygments' classes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ongardie.net/blog/rss.xml"&gt;nesting&lt;/a&gt; (2023) and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector"&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; nesting selector&lt;/a&gt; (2023) would improve readability significantly.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media"&gt;Range comparisons in &lt;code&gt;@media&lt;/code&gt; queries&lt;/a&gt;
(2023) would be a little easier to read.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark"&gt;&lt;code&gt;light-dark&lt;/code&gt;&lt;/a&gt;
(2024) might avoid some variable definitions for dark mode.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/website-updates-2024-9b870b0d/</guid><pubDate>Sun, 24 Nov 2024 03:03:00 GMT</pubDate></item><item><title>Command Not Found</title><link>https://yieldsfalsehood.com/aeolus/posts/command-not-found-3ad84357/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;On Debian/Ubuntu, &lt;code&gt;command-not-found&lt;/code&gt; tells you what package to install when
you try to run a program you don't have. I find this helpful, but it takes a
long time to maintain its index for lookups. This post tells the meandering
story of how I first optimized &lt;code&gt;command-not-found&lt;/code&gt;, then replaced it with a
script that doesn't use an index at all.&lt;/p&gt;

&lt;section id="update-command-not-found-is-slow"&gt;
&lt;h2&gt;update-command-not-found is slow&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#update-command-not-found-is-slow"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I noticed recently that &lt;code&gt;apt update&lt;/code&gt; stalls on my computer after fetching new
data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get:139 http://ftp.us.debian.org/debian unstable/non-free amd64 Contents (deb) T-2021-01-15-0800.20-F-2021-01-14-0800.15.pdiff [2,707 B]
Fetched 1,504 kB in 39s (38.5 kB/s)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...stall for about 15 seconds...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Reading package lists... Done
Building dependency tree
Reading state information... Done
10 packages can be upgraded. Run 'apt list --upgradable' to see them.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found the culprit to be &lt;code&gt;update-command-not-found&lt;/code&gt; from the
&lt;a href="https://packages.debian.org/command-not-found"&gt;command-not-found&lt;/a&gt; package.
That package provides a useful error message when you attempt to run a command
you don't have installed. It searches through the APT cache for Debian (or
Ubuntu or whatever) packages that would install an executable with the same
name. Here's an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;python2

&lt;span class="hl-go"&gt;Command 'python2' not found, but can be installed with:&lt;/span&gt;

&lt;span class="hl-go"&gt;sudo apt install python2-minimal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this search efficient, &lt;code&gt;update-command-not-found&lt;/code&gt; builds a lookup table
when &lt;code&gt;apt update&lt;/code&gt; runs. This table is stored in in
&lt;code&gt;/var/lib/command-not-found/&lt;/code&gt;. Unfortunately, building this lookup table and
maintaining it is slow, at least for me.&lt;/p&gt;
&lt;p&gt;The code is written in Python. It's maintained
&lt;a href="https://code.launchpad.net/~ubuntu-core-dev/command-not-found/ubuntu"&gt;upstream by Ubuntu&lt;/a&gt;
but is &lt;a href="https://salsa.debian.org/jak/command-not-found"&gt;modified by Debian&lt;/a&gt; (in
a reversal from their typical roles). The relevant code for me is all from
Debian's changes that read through package Contents files. The changes are kept
in a series of patches inside the &lt;code&gt;debian/patches/&lt;/code&gt; directory
(using &lt;a href="https://www.debian.org/doc/manuals/maint-guide/modify.en.html"&gt;quilt&lt;/a&gt;),
and the relevant patch is
&lt;a href="https://salsa.debian.org/jak/command-not-found/-/blob/e94d7236/debian/patches/0003-cnf-update-db-Add-support-for-Contents-files.patch"&gt;0003-cnf-update-db-Add-support-for-Contents-files.patch&lt;/a&gt;.
My computer seems to spend all its time in the method
&lt;code&gt;_parse_single_contents_file&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Upon profiling &lt;code&gt;update-command-not-found&lt;/code&gt; with
&lt;a href="https://github.com/joerick/pyinstrument"&gt;pyinstrument&lt;/a&gt; and plenty of
trial-and- error, I was able to speed it up by about 40% (again, on my
computer) with a minor change. The patch itself is quite small:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-diff"&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;    def _parse_single_contents_file(self, con, f, fp):
&lt;span class="hl-w"&gt; &lt;/span&gt;        # read header
&lt;span class="hl-w"&gt; &lt;/span&gt;        suite=None      # FIXME
&lt;span class="hl-gi"&gt;+        pattern = re.compile(b'usr/sbin|usr/bin|sbin|bin')&lt;/span&gt;

&lt;span class="hl-w"&gt; &lt;/span&gt;        for l in fp:
&lt;span class="hl-gd"&gt;-            l = l.decode("utf-8")&lt;/span&gt;
&lt;span class="hl-gd"&gt;-            if not (l.startswith('usr/sbin') or l.startswith('usr/bin') or&lt;/span&gt;
&lt;span class="hl-gd"&gt;-                    l.startswith('bin') or l.startswith('sbin')):&lt;/span&gt;
&lt;span class="hl-gi"&gt;+            if not pattern.match(l):&lt;/span&gt;
&lt;span class="hl-w"&gt; &lt;/span&gt;                continue
&lt;span class="hl-gi"&gt;+            l = l.decode("utf-8")&lt;/span&gt;
&lt;span class="hl-w"&gt; &lt;/span&gt;            try:
&lt;span class="hl-w"&gt; &lt;/span&gt;                command, pkgnames = l.split(None, 1)
&lt;span class="hl-w"&gt; &lt;/span&gt;            except ValueError:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each line &lt;code&gt;l&lt;/code&gt; in the file stream &lt;code&gt;fp&lt;/code&gt; comes from a Contents file, which looks
like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usr/bin/cvs                       vcs/cvs
usr/bin/parallel                  utils/moreutils,utils/parallel
usr/share/cvs/contrib/README      vcs/cvs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contents files are kept in &lt;code&gt;/var/lib/apt/lists/&lt;/code&gt; and have names like
&lt;code&gt;ftp.us.debian.org_debian_dists_unstable_non-free_Contents-amd64.lz4&lt;/code&gt;. They can
be decompressed with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;lz4&lt;span class="hl-w"&gt; &lt;/span&gt;-d&lt;span class="hl-w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="hl-w"&gt; &lt;/span&gt;FILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;/usr/lib/apt/apt-helper&lt;span class="hl-w"&gt; &lt;/span&gt;cat-file&lt;span class="hl-w"&gt; &lt;/span&gt;FILE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; (2021-03-29): You might not have the contents files on your system.
If you don't, install the &lt;code&gt;apt-file&lt;/code&gt; package and then run &lt;code&gt;apt update&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before, the code would check if each line started with four separate strings. I
optimized that to use a pre-compiled regular expression, which is more
efficient. I also deferred decoding the line from an array of bytes into a
Unicode string. Many lines don't have the ASCII prefixes we're looking for, so
we don't need to spend time decoding those.&lt;/p&gt;
&lt;p&gt;I submitted this change to Debian in
&lt;a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=980076"&gt;bug #980076&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That improvement got the updates down from about 16 seconds to about 10 seconds
for me. I looked for more opportunities to speed up &lt;code&gt;update-command-not-found&lt;/code&gt;,
but nothing major jumped out at me. One approach would be to parallelize the
work so multiple files can be searched at once. Another would be to rewrite the
tool in a faster language; there's lots of discussion about this in
&lt;a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=881692"&gt;Debian bug #881692&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="skip-the-index"&gt;
&lt;h2&gt;Skip the index&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#skip-the-index"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Then I started wondering whether the index that &lt;code&gt;update-command-not-found&lt;/code&gt;
builds is even worth building. If I'm typing a command in my terminal and it
doesn't exist, I'm OK with waiting a second or so to be told what packages to
install. Can we search the Contents files quickly enough to make this
interactive without an index?&lt;/p&gt;
&lt;p&gt;There's a package called &lt;code&gt;apt-file&lt;/code&gt; that &lt;code&gt;command-not-found&lt;/code&gt; depends on and
does just this. It's written in Perl. This is equivalent to what
&lt;code&gt;update-command-not-found&lt;/code&gt; does:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;apt-file&lt;span class="hl-w"&gt; &lt;/span&gt;search&lt;span class="hl-w"&gt; &lt;/span&gt;--regex&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^(usr/)?s?bin/&lt;/span&gt;&lt;span class="hl-nv"&gt;$PACKAGE&lt;/span&gt;$&lt;span class="hl-s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's slower than I'd like (times shown are with warm caches):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/time&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;apt-file&lt;span class="hl-w"&gt; &lt;/span&gt;search&lt;span class="hl-w"&gt; &lt;/span&gt;--regex&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^(usr/)?s?bin/cvs&lt;/span&gt;$&lt;span class="hl-s2"&gt;"&lt;/span&gt;
&lt;span class="hl-go"&gt;cvs: /usr/bin/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;real 3.35&lt;/span&gt;
&lt;span class="hl-go"&gt;user 4.44&lt;/span&gt;
&lt;span class="hl-go"&gt;sys 1.08&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://manpages.debian.org/buster/apt-file/apt-file.1.en.html"&gt;man page&lt;/a&gt;
warns users that the &lt;code&gt;--regexp&lt;/code&gt; option is slow. That turns out to be true. This
next invocation is equivalent but significantly faster:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/time&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;apt-file&lt;span class="hl-w"&gt; &lt;/span&gt;search&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"bin/cvs"&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;grep&lt;span class="hl-w"&gt; &lt;/span&gt;-P&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;": /(usr/)?s?bin/cvs&lt;/span&gt;$&lt;span class="hl-s2"&gt;"&lt;/span&gt;
&lt;span class="hl-go"&gt;cvs: /usr/bin/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;real 1.22&lt;/span&gt;
&lt;span class="hl-go"&gt;user 1.57&lt;/span&gt;
&lt;span class="hl-go"&gt;sys 0.52&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly, that's fast enough, and I probably should have stopped there.
Spoiler: I didn't.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="parallel-search"&gt;
&lt;h2&gt;Parallel search&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#parallel-search"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Searching through multiple Contents files is trivially parallelizable, or at
least it should be. I don't know Perl, so I didn't want to make major changes
to &lt;code&gt;apt-file&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What I wanted to do was a parallel execution of &lt;code&gt;apt-helper cat-file&lt;/code&gt; piped
into &lt;code&gt;grep&lt;/code&gt;. In shell scripts, you can do this using tools like
&lt;a href="https://manpages.debian.org/testing/parallel/parallel.1.en.html"&gt;GNU parallel&lt;/a&gt;
or the conflicting program from
&lt;a href="https://manpages.debian.org/testing/moreutils/parallel.1.en.html"&gt;moreutils&lt;/a&gt;.
I came across this 2012
&lt;a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=597050#75"&gt;rant on GNU parallel&lt;/a&gt;
by Joey Hess, the developer of
&lt;a href="https://joeyh.name/code/moreutils/"&gt;moreutils&lt;/a&gt;, and that's enough reason for
me to avoid it. The whole mess of not knowing which parallel might be installed
at &lt;code&gt;/usr/bin/parallel&lt;/code&gt;, if any, was also off-putting.&lt;/p&gt;
&lt;p&gt;Instead, I switched to using &lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt;.
ripgrep (&lt;code&gt;rg&lt;/code&gt;) is parallel by default. It can decode compressed files with the
&lt;code&gt;-z&lt;/code&gt; flag if &lt;code&gt;lz4&lt;/code&gt; and similar programs are available.&lt;/p&gt;
&lt;p&gt;This initial test was promising:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;/bin/time&lt;span class="hl-w"&gt; &lt;/span&gt;-p&lt;span class="hl-w"&gt; &lt;/span&gt;rg&lt;span class="hl-w"&gt; &lt;/span&gt;-z&lt;span class="hl-w"&gt; &lt;/span&gt;--no-filename&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^(usr/)?s?bin/cvs\s"&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;/var/lib/apt/lists/*Contents*&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;cat
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;usr/bin/cvs                   vcs/cvs&lt;/span&gt;
&lt;span class="hl-go"&gt;real 0.39&lt;/span&gt;
&lt;span class="hl-go"&gt;user 1.42&lt;/span&gt;
&lt;span class="hl-go"&gt;sys 2.12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pipe into &lt;code&gt;cat&lt;/code&gt; is there to disable ripgrep's "nice" output for interactive
terminals.&lt;/p&gt;
&lt;p&gt;So, this is definitely fast enough for interactive use. Even after I &lt;a href="https://linux-mm.org/Drop_Caches"&gt;drop my
caches&lt;/a&gt;, the same command only takes about
0.46 seconds on this machine.&lt;/p&gt;
&lt;p&gt;My actual script, which you can find at the end of this post, has some more
bells and whistles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It falls back to a regular &lt;code&gt;grep&lt;/code&gt; when &lt;code&gt;rg&lt;/code&gt; or &lt;code&gt;lz4&lt;/code&gt; are unavailable.&lt;/li&gt;
&lt;li&gt;It doesn't scan through the small files in &lt;code&gt;/var/lib/apt/lists/&lt;/code&gt; with
extension &lt;code&gt;.diff_Index&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;section id="extracting-package-names"&gt;
&lt;h2&gt;Extracting the package names&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#extracting-package-names"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The next challenge was formatting the data. If you remember from the earlier
example, some of the lines in the Contents files have a comma-separated list of
packages. I don't know of a great way to deal with that in shell script. I
ended up with this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;-E&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'s/^.* +//; s/,/\n/g; s/^.*\///m'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; (2025-04-27): Fixed two bugs in the above (missing &lt;code&gt;-E&lt;/code&gt;, which is
needed for the &lt;code&gt;+&lt;/code&gt;, and missing &lt;code&gt;m&lt;/code&gt; flag, which is needed for multiline
processing).&lt;/p&gt;
&lt;p&gt;The input looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;usr/bin/parallel             utils/moreutils,utils/parallel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first regular expression strips out the path, leaving:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;utils/moreutils,utils/parallel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second regular expression breaks it into lines by comma, leaving:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;utils/moreutils
utils/parallel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The third regular expression strips off the section names, leaving:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;moreutils
parallel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, that gets piped through &lt;code&gt;sort -u&lt;/code&gt; to remove the duplicates from
multiple architectures or distributions.&lt;/p&gt;
&lt;/section&gt;
&lt;section id="printing-info"&gt;
&lt;h2&gt;Printing relevant information&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#printing-info"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I could just print the package names and call it a day, but it's helpful to
print more information. The best way I know to do this is with &lt;code&gt;apt search&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-console"&gt;&lt;span class="hl-gp"&gt;$ &lt;/span&gt;apt&lt;span class="hl-w"&gt; &lt;/span&gt;search&lt;span class="hl-w"&gt; &lt;/span&gt;--names-only&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'^(moreutils|parallel)$'&lt;/span&gt;
&lt;span class="hl-go"&gt;Sorting... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;Full Text Search... Done&lt;/span&gt;
&lt;span class="hl-go"&gt;moreutils/stable 0.62-1 amd64&lt;/span&gt;
&lt;span class="hl-go"&gt;  additional Unix utilities&lt;/span&gt;

&lt;span class="hl-go"&gt;parallel/stable,stable,testing,testing,unstable,unstable,now 20161222-1.1 all&lt;/span&gt;
&lt;span class="hl-go"&gt;  build and execute command lines from standard input in parallel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sadly, this takes about 1 second, but I think it's valuable enough to be worth
the delay.&lt;/p&gt;
&lt;p&gt;You're not really supposed to use &lt;code&gt;apt&lt;/code&gt; in a script like this because its
output may change. I'm not really bothered by that, though, since my script is
intended for human consumption. The
&lt;a href="https://manpages.debian.org/buster/apt/apt.8.en.html"&gt;apt man page&lt;/a&gt;
says to use
&lt;a href="https://manpages.debian.org/buster/apt/apt-cache.8.en.html"&gt;&lt;code&gt;apt-cache&lt;/code&gt;&lt;/a&gt;
instead. However, I don't see a way to get &lt;code&gt;apt-cache&lt;/code&gt; to format the results in
a similar way.&lt;/p&gt;
&lt;p&gt;Building that regular expression from the list of packages is also not obvious
in a shell script. I ended up with this ugly thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;&lt;span class="hl-nv"&gt;PACKAGES_DISJUNCTION&lt;/span&gt;&lt;span class="hl-o"&gt;=&lt;/span&gt;&lt;span class="hl-k"&gt;$(&lt;/span&gt;&lt;span class="hl-nb"&gt;echo&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-nv"&gt;$PACKAGES&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-p"&gt;|&lt;/span&gt;&lt;span class="hl-w"&gt; &lt;/span&gt;sed&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s1"&gt;'s/ /|/g'&lt;/span&gt;&lt;span class="hl-k"&gt;)&lt;/span&gt;
apt&lt;span class="hl-w"&gt; &lt;/span&gt;search&lt;span class="hl-w"&gt; &lt;/span&gt;--names-only&lt;span class="hl-w"&gt; &lt;/span&gt;&lt;span class="hl-s2"&gt;"^(&lt;/span&gt;&lt;span class="hl-nv"&gt;$PACKAGES_DISJUNCTION&lt;/span&gt;&lt;span class="hl-s2"&gt;)&lt;/span&gt;$&lt;span class="hl-s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section id="conclusion"&gt;
&lt;h2&gt;Putting it all together&lt;a class="section-link" href="https://ongardie.net/blog/rss.xml#conclusion"&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I've assembled all this into a script called
&lt;a href="https://github.com/ongardie/cubicle/blob/main/packages/apt-binary/bin/apt-binary"&gt;&lt;code&gt;apt-binary&lt;/code&gt;&lt;/a&gt;,
along with how to enable it for bash and zsh.&lt;/p&gt;
&lt;p&gt;If you're removing &lt;code&gt;command-not-found&lt;/code&gt; from your system, use
&lt;code&gt;apt purge command-not-found&lt;/code&gt; to get rid of its index, too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; (2021-03-29): Note that you'll want to keep the &lt;code&gt;apt-file&lt;/code&gt; package
installed, since that package registers hooks with apt to download the package
contents files.&lt;/p&gt;
&lt;p&gt;I hope this was useful or that you learned a trick or two. I found it to be a
frustrating exercise. I'm a professional software engineer that's comfortable
with several programming languages and different models for parallel
programming, yet for "simple" scripts like this, I'm sort of forced into
ancient UNIX tooling. In this environment, simple parallelism problems and
simple string manipulation can actually be pretty difficult. I'd normally use
Python for a language that's universally available with no setup, but it's not
well-suited to parallel or high-performance code. Go or Rust would have been
better choices, but they might not be set up everywhere. I think I settled on
an OK compromise here with &lt;code&gt;ripgrep&lt;/code&gt; falling back to &lt;code&gt;grep&lt;/code&gt; and a bunch of
regular expressions; I just feel like this should have been easier.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; (2025-04-28): New &lt;a href="https://ongardie.net/blog/nushell/"&gt;post&lt;/a&gt; describing a
Nushell port of this script.&lt;/p&gt;
&lt;/section&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/command-not-found-3ad84357/</guid><pubDate>Sat, 16 Jan 2021 01:12:00 GMT</pubDate></item><item><title>Website Updates (2020)</title><link>https://yieldsfalsehood.com/aeolus/posts/website-updates-2020-1da6a46c/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;The code for this website has been running without any major updates since 2009.
Back then, I wrote it as a Python 2 program using FastCGI, served originally by
&lt;a href="https://www.lighttpd.net/"&gt;lighttpd&lt;/a&gt; and more recently by
&lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I recently overhauled the code to run as a static site generator. This makes it
easier to run locally, feels better from a security perspective, and actually
simplifies the code in a few ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When serving individual requests, you need to figure out what page the
request is asking for. With generating an entire site, you just loop through
all the possible pages.&lt;/li&gt;
&lt;li&gt;When serving individual requests, you need to load in only the relevant data
for those requests. When generating an entire site, you can just load the
input data once at startup.&lt;/li&gt;
&lt;li&gt;When serving individual requests, you need to recover gracefully from errors.
When generating an entire site, you can just let any exceptions propogate to
crashing the program and have the user fix the problem and rerun.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing I gave up in switching to a static site generator was the page trail.
Before, the history of where you've been on this site was tracked with a session
cookie and displayed just under the title bar. I somehow felt that was an
important feature in the early 2000s, but that sort of navigation is better
suited to browser history today.&lt;/p&gt;
&lt;p&gt;On a related note, I used to host the code for this website with a local
&lt;a href="https://git.zx2c4.com/cgit/about/"&gt;cgit&lt;/a&gt; instance. I've turned that off and
moved the code to a &lt;a href="https://github.com/ongardie/website-gen"&gt;GitHub repo&lt;/a&gt;
instead. If you're curious, you can look through the history of that repo to see
what's changed.&lt;/p&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/website-updates-2020-1da6a46c/</guid><pubDate>Fri, 07 Aug 2020 23:30:00 GMT</pubDate></item><item><title>LogCabin 1.1</title><link>https://yieldsfalsehood.com/aeolus/posts/logcabin-1-1-109f22c6/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;Announcing the release of LogCabin 1.1! This is the second stable release of the
&lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt; coordination service, which
includes a C++ implementation of the
&lt;a href="https://raft.github.io"&gt;Raft consensus algorithm&lt;/a&gt;.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-1.1/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/logcabin-1-1-109f22c6/</guid><pubDate>Sun, 26 Jul 2015 18:27:00 GMT</pubDate></item><item><title>LogCabin 1.0</title><link>https://yieldsfalsehood.com/aeolus/posts/logcabin-1-0-39cb6d99/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;LogCabin 1.0 is out! This is the first stable release of the
&lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt; coordination service, which
includes a C++ implementation of the
&lt;a href="https://raft.github.io"&gt;Raft consensus algorithm&lt;/a&gt;. If you're new
to these, I recently spoke about Raft and a little about LogCabin at the
&lt;a href="https://www.meetup.com/Sourcegraph-Tech-Talks/events/221199291/"&gt;Sourcegraph Hacker
Meetup&lt;/a&gt; in
San Francisco; watch the &lt;a href="https://www.youtube.com/watch?v=2dfSOFqOhOU"&gt;video&lt;/a&gt; for a visual
walk-through of how Raft works.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-1.0/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/logcabin-1-0-39cb6d99/</guid><pubDate>Fri, 01 May 2015 23:57:00 GMT</pubDate></item><item><title>LogCabin.appendEntry(6, "Preparing for 1.0")</title><link>https://yieldsfalsehood.com/aeolus/posts/logcabin-appendentry-6-preparing-for-1-0-564785fc/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;This is the sixth in a series of blog posts detailing the ongoing development
of &lt;a href="https://github.com/logcabin/logcabin"&gt;LogCabin&lt;/a&gt;. This entry describes
progress towards the upcoming 1.0.0 release of LogCabin, a useful new
command-line client to access LogCabin, and several other improvements.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/logcabin-2015-04-03/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/logcabin-appendentry-6-preparing-for-1-0-564785fc/</guid><pubDate>Fri, 03 Apr 2015 22:06:00 GMT</pubDate></item><item><title>LevelDB with Transactions on Node.js</title><link>https://yieldsfalsehood.com/aeolus/posts/leveldb-with-transactions-on-node-js-08f0a013/</link><dc:creator>ongardie</dc:creator><description>&lt;p&gt;This post surveys the libraries available in Node.js for LevelDB, shows that
support for transactions is missing, and lays out a path to get there. In
short, we need the Node bindings for LevelDB to expose snapshots, then we can
build transactions with snapshot isolation on top without much trouble.&lt;/p&gt;

                &lt;p&gt;&lt;a href="https://ongardie.net/blog/node-leveldb-transactions/"&gt;Continue reading full article&lt;/a&gt;&lt;/p&gt;</description><guid>https://yieldsfalsehood.com/aeolus/posts/leveldb-with-transactions-on-node-js-08f0a013/</guid><pubDate>Wed, 11 Mar 2015 00:17:00 GMT</pubDate></item></channel></rss>