<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog on Jon Seager</title><link>https://jnsgr.uk/tags/blog/</link><description>Recent content in Blog on Jon Seager</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 26 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jnsgr.uk/tags/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>ntpd-rs: it's about time!</title><link>https://jnsgr.uk/2026/03/ntpd-rs-its-about-time/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2026/03/ntpd-rs-its-about-time/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/ntpd-rs-its-about-time/79154" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I am thrilled to announce the next target in our campaign to replace core system utilities with memory-safe Rust rewrites in Ubuntu. In upcoming releases, Ubuntu will be adopting &lt;a href="https://trifectatech.org/projects/ntpd-rs/" target="_blank" rel="noreferrer"&gt;ntpd-rs&lt;/a&gt; as the default time synchronization client and server, eventually replacing &lt;a href="https://chrony-project.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;chrony&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://www.linuxptp.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;linuxptp&lt;/code&gt;&lt;/a&gt; and with any luck, &lt;a href="https://gpsd.io/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;gpsd&lt;/code&gt;&lt;/a&gt; for time syncing use-cases.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://trifectatech.org/projects/ntpd-rs/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ntpd-rs&lt;/code&gt;&lt;/a&gt; is a full-featured implementation of the Network Time Protocol (NTP), written entirely in Rust. Maintained by the Trifecta Tech Foundation as part of &lt;a href="https://github.com/pendulum-project" target="_blank" rel="noreferrer"&gt;Project Pendulum&lt;/a&gt;, &lt;code&gt;ntpd-rs&lt;/code&gt; places a strong focus on security, stability, and memory safety.&lt;/p&gt;
&lt;p&gt;To deliver on this goal, we&amp;rsquo;re building on our partnership with the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt; who are behind &lt;a href="https://trifectatech.org/projects/sudo-rs/" target="_blank" rel="noreferrer"&gt;sudo-rs&lt;/a&gt;, &lt;a href="https://trifectatech.org/projects/zlib-rs/" target="_blank" rel="noreferrer"&gt;zlib-rs&lt;/a&gt; and more. We will be funding the Trifecta Tech Foundation to build new features, enhance security isolation, and ultimately deliver a unified, memory-safe time synchronization utility for the Linux ecosystem. This work meshes well with the Trifecta Tech Foundations goals to improve the security of time synchronization everywhere.&lt;/p&gt;
&lt;h2 id="ntp-nts-and-ptp" class="relative group"&gt;NTP, NTS, and PTP &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ntp-nts-and-ptp" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before diving into the mechanics and reasoning behind the transition, I wanted to give some background on the protocols at play, and the problems we&amp;rsquo;re hoping to solve. Keeping accurate time is a critical system function, not least because it involves constant interaction with the internet and forms the basis for cryptographic verification in protocols such as Transport Layer Security (TLS).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NTP (Network Time Protocol)&lt;/strong&gt; is the foundational protocol that most operating systems implement to accurately determine the current time from a network source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NTS (Network Time Security)&lt;/strong&gt; is to NTP what HTTPS is to HTTP. Historically, the Network Time Protocol was used unencrypted, like many of the early web protocols. NTS introduces cryptographic security to time synchronization, ensuring that bad actors cannot intercept or spoof time data. We already pushed to make NTS the default out-of-the-box in Ubuntu 25.10, which we accomplished by migrating away from &lt;code&gt;ntpd&lt;/code&gt; to &lt;code&gt;chrony&lt;/code&gt; as the default time-syncing implementation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PTP (Precision Time Protocol)&lt;/strong&gt; is used for systems that require sub-microsecond synchronization. While the precision offered by a standard NTP deployment is sufficient for general-purpose computing, PTP is often used for complex, specialized deployments like telecommunications networks, power grids, and automotive applications.&lt;/p&gt;
&lt;h2 id="proven-at-scale" class="relative group"&gt;Proven at Scale &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#proven-at-scale" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Transitioning core utilities in Ubuntu comes with a responsibility to ensure that replacements are of high quality, resilient and offer something to the platform. We may be the first major Linux distribution to adopt ntpd-rs by default, but we aren&amp;rsquo;t the first to recognize the readiness of &lt;code&gt;ntpd-rs&lt;/code&gt; - it has already been &lt;a href="https://letsencrypt.org/2024/06/24/ntpd-rs-deployment" target="_blank" rel="noreferrer"&gt;proven at scale by Let&amp;rsquo;s Encrypt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While Let&amp;rsquo;s Encrypt&amp;rsquo;s core Certificate Authority software has always been written in memory-safe Go, their server operating systems and network infrastructure historically relied on memory-unsafe languages like C and C++, which routinely led to vulnerabilities requiring patching.&lt;/p&gt;
&lt;p&gt;Following extensive development, &lt;code&gt;ntpd-rs&lt;/code&gt; was deployed to Let&amp;rsquo;s Encrypt&amp;rsquo;s staging environment in April 2024, and rolled out to full production by June 2024, marking a major milestone for ntpd-rs.&lt;/p&gt;
&lt;p&gt;The fact that one of the world&amp;rsquo;s most prolific and security-conscious certificate authorities trusts &lt;code&gt;ntpd-rs&lt;/code&gt; to keep time across its fleet should provide us, and our enterprise customers, with tremendous confidence in its resilience and suitability.&lt;/p&gt;
&lt;h2 id="a-single-memory-safe-utility-for-ntp-and-ptp" class="relative group"&gt;A Single, Memory-Safe Utility for NTP and PTP &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#a-single-memory-safe-utility-for-ntp-and-ptp" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We want to provide a single utility for configuring both NTP/NTS and Precision Time Protocol (PTP) on Linux. The Trifecta Tech Foundation is concurrently developing &lt;a href="https://trifectatech.org/projects/statime/" target="_blank" rel="noreferrer"&gt;Statime&lt;/a&gt;, a memory-safe PTP implementation that delivers synchronization performance on par with &lt;code&gt;linuxptp&lt;/code&gt;, but with the goal of being easier to configure and use.&lt;/p&gt;
&lt;p&gt;The goal is to integrate Statime&amp;rsquo;s PTP capabilities directly into &lt;code&gt;ntpd-rs&lt;/code&gt;, improving the user experience by bringing all time synchronization concerns into one utility with common configuration and usage patterns, obviating the need for complex manual configuration (and troubleshooting) that users of &lt;code&gt;linuxptp&lt;/code&gt; may be familiar with.&lt;/p&gt;
&lt;h2 id="timelines-and-goals" class="relative group"&gt;Timelines and Goals &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#timelines-and-goals" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As with our transition to &lt;code&gt;sudo-rs&lt;/code&gt; and &lt;code&gt;uutils coreutils&lt;/code&gt;, leading the mainstream adoption of foundational system utilities comes with responsibility. We want to ensure that &lt;code&gt;ntpd-rs&lt;/code&gt; matches the security isolation and performance standards our users expect from &lt;code&gt;chrony&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Canonical is funding the Trifecta Tech Foundation&amp;rsquo;s development efforts toward these goals over the coming cycles. This work will take place between July 2026 and January 2027 in several major milestones. Our current timeline and targeted goals are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 26.10:&lt;/strong&gt; If all goes well, we aim to land the latest version of &lt;code&gt;ntpd-rs&lt;/code&gt; in the archive, making it available to test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 27.04:&lt;/strong&gt; By 27.04, &lt;code&gt;ntpd-rs&lt;/code&gt; should have integrated &lt;code&gt;statime&lt;/code&gt;, and we will ship the unified client/server binary for NTP, NTS and PTP in Ubuntu by default, with the aim of providing a smooth migration path for those who already manage complex &lt;code&gt;chrony&lt;/code&gt; configs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get us there, the Trifecta Tech Foundation will be working on the following items:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature Parity &amp;amp; Hardware Support:&lt;/strong&gt; Adding &lt;code&gt;gpsd&lt;/code&gt; IP socket support, multi-threading support for NTP servers, and support for multi-homed servers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security &amp;amp; Isolation:&lt;/strong&gt; &lt;code&gt;chrony&lt;/code&gt; is isolated via AppArmor and seccomp. We&amp;rsquo;ll be working on robust AppArmor and seccomp profiles for &lt;code&gt;ntpd-rs&lt;/code&gt; to ensure we don&amp;rsquo;t buy memory safety at the cost of system-level privilege boundaries. We are also ensuring &lt;code&gt;rustls&lt;/code&gt; can use &lt;code&gt;openssl&lt;/code&gt; as a crypto provider to satisfy strict corporate cryptography policies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PTP &amp;amp; Automotive Profiles:&lt;/strong&gt; Adding support for gPTP, which will allow us to support complex deployments like the Automotive profile directly from &lt;code&gt;nptd-rs&lt;/code&gt; (via Statime). Additionally, experimental support for the proposed Client-Server PTP protocol (CSPTP, IEEE 1588.1) will be added.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benchmarking &amp;amp; Testing:&lt;/strong&gt; Comprehensive benchmarking of long-term memory, CPU usage, and synchronization performance against &lt;code&gt;chrony&lt;/code&gt; to give our cloud partners and enterprise users complete confidence in the transition.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-experience:&lt;/strong&gt; Logging improvements and enhancements to configuration that help users configure the time synchronisation target to optimise network usage, as well as improvements to the ntp-cli&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="about-the-trifecta-tech-foundation" class="relative group"&gt;About the Trifecta Tech Foundation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#about-the-trifecta-tech-foundation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Trifecta Tech Foundation is a non-profit and a Public Benefit Organisation (501(c)(3) equivalent) that creates open-source building blocks for critical infrastructure software. Their initiatives on data compression, time synchronization, and privilege boundary, impact the digital security of millions of people. If you&amp;rsquo;d like to support their work, please contact them via &lt;a href="https://trifectatech.org/support" target="_blank" rel="noreferrer"&gt;https://trifectatech.org/support&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I am really excited to deepen our already productive relationship with the Trifecta Tech Foundation to make these transitions viable for the wider ecosystem. We&amp;rsquo;ll be working hard on testing and integration to ensure seamless migration paths, and heavily document the changes ahead of the 26.10 and 27.04 releases.&lt;/p&gt;
&lt;p&gt;Stay tuned!&lt;/p&gt;</description></item><item><title>Brewlog: Coffee &amp; Agents</title><link>https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/</link><pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I’m really into speciality coffee. In July 2025 I started tracking my coffee habits with &lt;a href="https://roast.guide/" target="_blank" rel="noreferrer"&gt;Roastguide&lt;/a&gt;, a delightfully designed iOS app for people with a similar obsession to mine. It does a good job of tracking brews, roasters and bags, but over time I found myself wanting the data in a system that I controlled. I wanted to be able to query it, back it up, and adjust some of the flows to work better for how I brew and consume coffee.&lt;/p&gt;
&lt;p&gt;Despite my past scepticism of the previous generation of coding tools, recent developments have made them hard to ignore, and I wanted an excuse to build something substantial from scratch to get some experience.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/jnsgruk/brewlog" target="_blank" rel="noreferrer"&gt;b{rew}log&lt;/a&gt;, which is a self-hosted coffee logging platform. It tracks roasters, roasts, bags, brews, equipment, and cafe visits. It&amp;rsquo;s live at &lt;a href="https://coffee.jnsgr.uk" target="_blank" rel="noreferrer"&gt;coffee.jnsgr.uk&lt;/a&gt; if you want to poke around and witness the depths of my strange filter coffee obsession!&lt;/p&gt;
&lt;p&gt;This was by far the most complex project I&amp;rsquo;d built with &lt;a href="https://claude.com/product/claude-code" target="_blank" rel="noreferrer"&gt;Claude Code&lt;/a&gt;, or agentic coding tools in general. I&amp;rsquo;m responsible for the technology choices, architecture, and visual design, but I wrote almost none of the code myself.&lt;/p&gt;
&lt;p&gt;This post will cover the core features of Brewlog and how I designed the app, and finish with some observations and tips about agentic programming.&lt;/p&gt;
&lt;h2 id="core-features" class="relative group"&gt;Core Features &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#core-features" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Brewlog tracks roasters, roasts, bags, brews, equipment, and cafe visits. The landing page you see below summarises details of my last few brews, currently open bags of coffee and how much remains in each and some basic stats. When logged in, it provides quick controls to repeat a brew, or brew a particular coffee:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_445dd8a54a9a4ea8.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_7bd2a139c538b332.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_28dfa3482ef59c87.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_ceb4eac36acb5f30.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1682"
class="mx-auto my-0 rounded-md"
alt="brewlog landing page"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_a6bf0a68f91b6a7d.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_f4a29c4400db4876.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_a6bf0a68f91b6a7d.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01_hu_6d2fed53d5402f30.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/01.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Brews are coffees I’ve made myself. Each brew is logged against a specific bag and captures the recipe (dose, water, time, temperature), the equipment used, and tasting notes. Over time this will build up a history of what I&amp;rsquo;ve brewed, how I brewed it and how much I use the brewing gear I&amp;rsquo;ve accumulated over time. The image below shows the detail page for a specific brew:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_50bbb0b2b6963101.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_35a1e01fcdddafaf.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_29bb4eaed781336a.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_e31901b84c0d9203.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1340"
class="mx-auto my-0 rounded-md"
alt="detail of a specific brew in the application"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_efb40addffa8ed5e.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_2e550bb129e56e74.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_efb40addffa8ed5e.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04_hu_305a0558504abcb0.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/04.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Finally, brewlog tracks cafes I&amp;rsquo;ve visited, and &amp;ldquo;cups&amp;rdquo;, which are coffees I&amp;rsquo;ve enjoyed, but not brewed myself.&lt;/p&gt;
&lt;p&gt;The most fun part is the extensive stats page, which shows an interactive map of where my coffees are grown, roasted and drunk, as well as common flavour notes and brew times.&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_808a259ff87d6ed8.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_e2884e1516b66776.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_ac2b5c228a3d9507.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_de544f83159c15c5.webp 1130w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1130"
height="1682"
class="mx-auto my-0 rounded-md"
alt="coffee stats - map view and consumption numbers"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_12440ddec0cd6378.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_342f11bff0c05800.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_12440ddec0cd6378.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02_hu_2ce5ee59505317cb.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/02.png 1130w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="llm-powered-bag-scanning" class="relative group"&gt;LLM-Powered Bag Scanning &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#llm-powered-bag-scanning" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Roastguide has a nice feature that allows you to take a picture of a bag of coffee and it&amp;rsquo;ll fetch the details. If my understanding is correct, their implementation relies on a database of roasters and coffees that the creators maintain. My app wouldn&amp;rsquo;t have access to that database, nor did I want to spend too much time building ingestion pipelines to store lots of data about all the possible coffees/roasters.&lt;/p&gt;
&lt;p&gt;In brewlog, I implemented this feature using LLM extraction: I take a photo and send it to a multi-modal model hosted by &lt;a href="https://openrouter.ai/" target="_blank" rel="noreferrer"&gt;OpenRouter&lt;/a&gt;. The &lt;a href="https://github.com/jnsgruk/brewlog/blob/edffb1679db3f17a8ed8e47735e8e1aff137e117/src/infrastructure/ai.rs#L11-L28" target="_blank" rel="noreferrer"&gt;prompt&lt;/a&gt; instructs the model to extract text from the image, perform a web search, and return a JSON object containing details of the coffee or roaster. OpenRouter enables me to switch models easily without worrying about API differences.&lt;/p&gt;
&lt;p&gt;In practice this works surprisingly well. Most speciality coffee bags are covered in useful information, and vision models are good at reading it. The main failure modes I see are mixing up roast names from the web and occasionally inventing tasting notes, but the process includes a review step which makes those cheap to correct.&lt;/p&gt;
&lt;p&gt;The cost is almost negligible per scan. I&amp;rsquo;ve been using &lt;a href="https://openrouter.ai/google/gemini-2.5-flash" target="_blank" rel="noreferrer"&gt;&lt;code&gt;google/gemini-2.5-flash&lt;/code&gt;&lt;/a&gt; for the LLM extraction feature, which results in a cost of around $0.01 - $0.02 per scan.&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_6001b8e6d92a665d.webp 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_8b28c8a2ed6b8f2.webp 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_3f53612e16cc5157.webp 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_e04a32d09fe6bcd4.webp 1203w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1203"
height="969"
class="mx-auto my-0 rounded-md"
alt="adding a roaster with the option of fetching details using an llm with web search tool"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_c42d5bcb268c69d3.png" srcset="https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_b8fcf892c9d1ebc0.png 330w,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_c42d5bcb268c69d3.png 660w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03_hu_9b633d69b01861f.png 1024w
,https://jnsgr.uk/2026/03/brewlog-coffee-and-agents/03.png 1203w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="design-decisions" class="relative group"&gt;Design Decisions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#design-decisions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="backend" class="relative group"&gt;Backend &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#backend" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Brewlog is built with Rust and &lt;a href="https://github.com/tokio-rs/axum" target="_blank" rel="noreferrer"&gt;Axum&lt;/a&gt; for the backend. I&amp;rsquo;ve been &lt;a href="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/" target="_blank" rel="noreferrer"&gt;learning Rust&lt;/a&gt; for a while now, and wanted to push further with a more substantial project. Templates are handled by &lt;a href="https://github.com/djc/askama" target="_blank" rel="noreferrer"&gt;Askama&lt;/a&gt;, whose compile‑time templates worked well with Claude because template errors surface as Rust compiler errors, which are picked up and fixed by the agent automatically.&lt;/p&gt;
&lt;p&gt;The database is &lt;a href="https://www.sqlite.org/" target="_blank" rel="noreferrer"&gt;SQLite&lt;/a&gt;. A single file, easy to back up, easy to move around. For a single-user application like this, SQLite is more than sufficient and removes the need for a separate database server. &lt;a href="https://github.com/jnsgruk/brewlog/tree/main/migrations" target="_blank" rel="noreferrer"&gt;Migrations&lt;/a&gt; are embedded in the binary and &lt;a href="https://github.com/jnsgruk/brewlog/blob/edffb1679db3f17a8ed8e47735e8e1aff137e117/src/infrastructure/database.rs#L38" target="_blank" rel="noreferrer"&gt;run automatically&lt;/a&gt; on application startup.&lt;/p&gt;
&lt;h3 id="frontend" class="relative group"&gt;Frontend &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#frontend" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I wanted the app to feel modern, but keep much of the rendering server-side. I didn&amp;rsquo;t want to be serving a large client-side Javascript single-page application. I&amp;rsquo;ve been curious about the likes of &lt;a href="https://htmx.org/" target="_blank" rel="noreferrer"&gt;HTMX&lt;/a&gt;, and the observations made in a post from last year on &lt;a href="https://www.lorenstew.art/blog/eta-htmx-lit-stack" target="_blank" rel="noreferrer"&gt;building apps with Eta, HTMX and Lit&lt;/a&gt; really resonated with me.&lt;/p&gt;
&lt;p&gt;More recently I discovered &lt;a href="https://data-star.dev/" target="_blank" rel="noreferrer"&gt;Datastar&lt;/a&gt;, which has similar goals to &lt;a href="https://htmx.org/" target="_blank" rel="noreferrer"&gt;HTMX&lt;/a&gt;, but it&amp;rsquo;s newer and results in cleaner, simpler templates. Where HTMX swaps HTML fragments, Datastar adds a reactive signal system and can patch both HTML and JSON data from the server.&lt;/p&gt;
&lt;p&gt;This was a bit of a test for Claude Code, since Datastar was new enough that it likely didn&amp;rsquo;t feature in the training data for the model, but its ability to read and digest the documentation was quite startling.&lt;/p&gt;
&lt;p&gt;While occasionally the agent regressed to vanilla &lt;code&gt;fetch&lt;/code&gt; calls, or manual Javascript DOM manipulation, I treated that as a hole in my instructions, not the model. Each time it happened I &lt;a href="https://github.com/jnsgruk/brewlog/blob/main/CLAUDE.md#gotchas" target="_blank" rel="noreferrer"&gt;updated&lt;/a&gt; CLAUDE.md to reinforce the Datastar patterns and prevent the agent making the same mistake again.&lt;/p&gt;
&lt;h3 id="single-user-passkeys-only" class="relative group"&gt;Single User, Passkeys Only &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#single-user-passkeys-only" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Brewlog is deliberately single-user. I&amp;rsquo;m happy for people to self-host their own instance, but I don&amp;rsquo;t have much interest in providing a service more widely. The codebase is structured such that making it multi-user wouldn&amp;rsquo;t be a particularly complex task, but I&amp;rsquo;m not convinced I&amp;rsquo;ll ever do it.&lt;/p&gt;
&lt;p&gt;Perhaps unusually, authentication via username and password is not supported. Authentication is only supported using passkeys via &lt;a href="https://webauthn.io/" target="_blank" rel="noreferrer"&gt;WebAuthn&lt;/a&gt; or with an API token.&lt;/p&gt;
&lt;p&gt;Passkeys are phishing-resistant and significantly more difficult to brute-force both from an academic and a practical standpoint, which removes an entire class of abuse vector for my service.&lt;/p&gt;
&lt;h3 id="comprehensive-cli-client" class="relative group"&gt;Comprehensive CLI Client &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#comprehensive-cli-client" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;One of the many things I&amp;rsquo;ve learned from &lt;a href="https://github.com/niemeyer" target="_blank" rel="noreferrer"&gt;@niemeyer&lt;/a&gt; during my time at Canonical is to ensure that when new API endpoints are added to such applications, a corresponding CLI command or flag lands at the same time.&lt;/p&gt;
&lt;p&gt;He also illustrated to me the power of shipping a single binary that is both a server, and a client to itself. The brewlog binary runs the server that hosts the API and web UI, but also serves as a first-class API client for the app.&lt;/p&gt;
&lt;p&gt;If you’re building an API‑backed app, I highly recommend this pattern. It keeps the surface area small and gives you (and an agent&amp;hellip;) a first‑class, &lt;a href="https://github.com/jnsgruk/brewlog/blob/main/scripts/bootstrap-db.sh" target="_blank" rel="noreferrer"&gt;scriptable&lt;/a&gt; way to exercise the API and troubleshoot problems in data transformation.&lt;/p&gt;
&lt;h3 id="domain-driven-design" class="relative group"&gt;Domain-Driven Design &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#domain-driven-design" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The codebase follows a domain-driven design approach with four layers: domain (business logic only, no external dependencies), infrastructure (database, HTTP clients, third-party APIs), application (HTTP server, routes, middleware, services), and presentation (CLI commands and web view models):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─────────────────────────────────────────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Presentation │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌─────────────────────────┐ ┌──────────────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ CLI │ │ Web │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ roasters, roasts, bags │ │ views, templates │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ brews, cups, gear ... │ │ roasters, roasts, bags │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └─────────────────────────┘ └──────────────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Application │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Routes │ │ Services │ │ Server / State │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ /api/* │ │ BagService │ │ Axum router │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ /app/* │ │ BrewService │ │ AppState (DI) │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ │ │ RoastService .. │ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └──────────────┘ └──────────────────┘ └─────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─────────────────────────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Domain (pure Rust — no framework dependencies) │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌──────────────────────┐ ┌─────────────────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Entities &amp;amp; Values │ │ Repository Traits │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ coffee/ roasters, │ │ trait RoasterRepository │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ roasts, bags, │ │ trait RoastRepository │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ brews, cups, gear │ │ trait BagRepository ... │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ auth/ users, │ │ │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ sessions, tokens │ │ Errors, IDs, Listing, │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ analytics/ │ │ Formatting │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └──────────────────────┘ └─────────────────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├──────────────────────────────────┬──────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Infrastructure │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ┌────────────────────────────┐ │ ┌───────────────────┐ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ Repositories (SQLite) │ │ │ External Clients │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlRoasterRepository │ │ │ OpenRouter (AI) │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlRoastRepository │ │ │ Foursquare │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ SqlBagRepository ... │ │ │ WebAuthn │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ │ (implement domain traits) │ │ │ Backup │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └────────────────────────────┘ │ └───────────────────┘ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└──────────────────────────────────┴──────────────────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In this model, dependencies flow inward. The domain layer knows nothing about Axum, SQLite, or any other implementation detail. Repository traits are defined in the domain and implemented in the infrastructure layer. This creates flexibility in the future if, for example, I want to move to PostgreSQL rather than SQLite, or if I want to change how I store images, etc.&lt;/p&gt;
&lt;p&gt;It also simplifies testing. The domain-driven design approach encourages loose coupling and practices like dependency injection, which usually lead to simpler integration tests. For example, I could ask the agent for focused tests against pure domain logic without pulling in Axum or SQLite.&lt;/p&gt;
&lt;h2 id="agentic-coding-patterns" class="relative group"&gt;Agentic Coding Patterns &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#agentic-coding-patterns" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;A big motivation for this project was to learn some new technology and skills, and it certainly did that, but it also reinforced some patterns I&amp;rsquo;ve been using for a while now.&lt;/p&gt;
&lt;h3 id="pre-commit-hooks-as-a-contract" class="relative group"&gt;Pre-commit hooks as a contract &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#pre-commit-hooks-as-a-contract" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve never been too fond of &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" target="_blank" rel="noreferrer"&gt;pre-commit hooks&lt;/a&gt;. I&amp;rsquo;ve always been disciplined in using formatters and linters in projects, but never enjoyed having them set up to run automatically. This changed when I started working with agents.&lt;/p&gt;
&lt;p&gt;I treat &lt;code&gt;pre‑commit&lt;/code&gt; as a contract between me and the agent. The agent is instructed to always fix linting/testing failures before returning to me for input or declaring success. In particular, I&amp;rsquo;ve been using &lt;a href="https://prek.j178.dev/" target="_blank" rel="noreferrer"&gt;prek&lt;/a&gt;, which is a Rust rewrite of &lt;a href="https://pre-commit.com/" target="_blank" rel="noreferrer"&gt;pre-commit&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="stricter-lints-for-agents" class="relative group"&gt;Stricter lints for agents &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#stricter-lints-for-agents" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;With agents, I&amp;rsquo;m also more inclined toward stricter, more pedantic lints to enforce properties like maximum line count in a function or file. When I&amp;rsquo;m writing code myself I make these changes instinctively. It&amp;rsquo;s obvious when a function becomes cumbersome when you&amp;rsquo;re editing by hand, but I noticed early on that an agent seems much more comfortable with 100+ line functions than I am.&lt;/p&gt;
&lt;p&gt;If you care about such constraints (function length, etc.), encode them as lints rather than hoping the agent internalises your preferences.&lt;/p&gt;
&lt;p&gt;As an aside, I&amp;rsquo;d never used &lt;a href="https://flake.parts" target="_blank" rel="noreferrer"&gt;flake-parts&lt;/a&gt; before, but its ability to automatically configure pre-commit hooks and formatting tools like &lt;a href="https://treefmt.com/latest/" target="_blank" rel="noreferrer"&gt;treefmt&lt;/a&gt; in a Nix dev shell is really slick.&lt;/p&gt;
&lt;h3 id="self-updating-instructions" class="relative group"&gt;Self-updating instructions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#self-updating-instructions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I hadn’t previously realised how good these tools are at writing their own instructions. I had been confused in the past at the length/complexity of &lt;code&gt;AGENTS.md&lt;/code&gt;/&lt;code&gt;CLAUDE.md&lt;/code&gt; files I&amp;rsquo;d seen, not realising that when you work with an agent to solve a problem, or remedy something it did, you can then prompt it to summarise what just happened in the &lt;code&gt;CLAUDE.md&lt;/code&gt; file to prevent it from happening again.&lt;/p&gt;
&lt;h3 id="plan-mode" class="relative group"&gt;Plan Mode &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#plan-mode" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;To begin with I made extensive use of &amp;ldquo;Plan Mode&amp;rdquo;. The premise is that the agent can explore the codebase, read docs online and work with you to plan new features, but it cannot make changes to the code or your system.&lt;/p&gt;
&lt;p&gt;This had been working well for me, but there were a couple of occasions where it seemed to get stuck in a &amp;ldquo;doom loop&amp;rdquo; trying to plan its way out of a complex failure.&lt;/p&gt;
&lt;p&gt;On one particular occasion, this lasted for over an hour. I then switched to &amp;ldquo;Agent Mode&amp;rdquo;, gave it a follow-up prompt that instructed it to inspect a running application on Kubernetes directly using &lt;code&gt;kubectl&lt;/code&gt;, and it was able to diagnose and solve the same problem in around 5 minutes. The problem here was that some &amp;ldquo;tools&amp;rdquo; are explicitly not permitted in &amp;ldquo;Plan Mode&amp;rdquo;, but depending on what you&amp;rsquo;re trying to achieve that can sometimes be a significant hindrance.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been favouring a different approach where I remain in &amp;ldquo;Agent Mode&amp;rdquo;, and prompt the agent that I&amp;rsquo;d like to work on a plan in a shared document (e.g. &lt;code&gt;./plans/new-feature.md&lt;/code&gt;), and that it may only edit that file until I say otherwise. I then work in a loop with the Agent where it writes a plan, I leave comments in-line in the file, and then it takes that into account - and repeat until I&amp;rsquo;m happy. Before asking the agent to implement the plan, I start a new session and ask it to implement the plan with fresh context. So far I&amp;rsquo;ve found this to be really effective.&lt;/p&gt;
&lt;h3 id="worktrees-and-parallel-agents" class="relative group"&gt;Worktrees and Parallel Agents &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#worktrees-and-parallel-agents" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Pretty soon after starting work on more complicated features, I became interested in running parallel agents. Sometimes an agent can take a long time to work through a well-developed, and in that time I often felt I could be working my way through other items on my roadmap.&lt;/p&gt;
&lt;p&gt;I initially tried to just run them concurrently on the same branch, but predictably they ended up fighting and making a bit of a mess. I&amp;rsquo;ve long been a fan of git &lt;a href="https://git-scm.com/docs/git-worktree" target="_blank" rel="noreferrer"&gt;worktrees&lt;/a&gt;, and they&amp;rsquo;re a perfect fit here - enabling me to run concurrent agents on separate instances of the codebase.&lt;/p&gt;
&lt;p&gt;Even with worktrees, I still tend to work on orthogonal concerns so I don&amp;rsquo;t end up with big merge conflicts, but being able to work on a frontend feature, a backend feature and the CI pipeline all at once is a nice upgrade.&lt;/p&gt;
&lt;h3 id="visual-prompting" class="relative group"&gt;Visual prompting &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#visual-prompting" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;When working on frontend components, I built a nice workflow around screenshotting the UI, marking it up with &lt;a href="https://flameshot.org/" target="_blank" rel="noreferrer"&gt;Flameshot&lt;/a&gt;, then pasting the image back into Claude Code with the next prompt.&lt;/p&gt;
&lt;p&gt;I found this to be a really effective way to drive iteration on web components. It felt closer to pair‑designing with a human: I’d circle spacing issues or misaligned elements, and the agent would propose CSS tweaks in response to what it saw.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I had a blast building Brewlog, and I&amp;rsquo;ve been using it every day for about a month now. It&amp;rsquo;s different from how I&amp;rsquo;ve approached projects in the past, in a way that&amp;rsquo;s both liberating and terrifying in equal measures.&lt;/p&gt;
&lt;p&gt;While some of the code in this project lacks the same attention to detail I might have applied, I likely would never have got around to building it without an agent.&lt;/p&gt;
&lt;p&gt;I was surprised what the process taught me. Every day I hacked on Brewlog with Claude Code, I learned something. Whether that was something about Tailwind CSS, or an architectural pattern.&lt;/p&gt;
&lt;p&gt;I think the key to useful output from these tools is that you remain the driver. You&amp;rsquo;re still responsible what you ship, which means giving the generated code the appropriate amount of review.&lt;/p&gt;
&lt;p&gt;Brewlog is live at &lt;a href="https://coffee.jnsgr.uk" target="_blank" rel="noreferrer"&gt;coffee.jnsgr.uk&lt;/a&gt; and runs comfortably on a free-tier instance on &lt;a href="https://fly.io/" target="_blank" rel="noreferrer"&gt;fly.io&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you’re into speciality coffee and like owning your data, give it a try and let me know how you get on.&lt;/p&gt;
&lt;p&gt;Finally, I&amp;rsquo;d like to thank the authors of Roastguide for the amazing work they&amp;rsquo;ve done on their app, the attention they pay to their community on Discord, and the fact that they were willing to get me a dump of my data I could use to populate the first deployment of Brewlog!&lt;/p&gt;</description></item><item><title>An update on upki</title><link>https://jnsgr.uk/2026/02/upki-update/</link><pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2026/02/upki-update/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/an-update-on-upki/77063" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Last year, I &lt;a href="https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/" target="_blank" rel="noreferrer"&gt;announced&lt;/a&gt; that Canonical had begun supporting the development of &lt;a href="https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/" target="_blank" rel="noreferrer"&gt;upki&lt;/a&gt;, a project that will bring browser-grade Public Key Infrastructure (PKI) to Linux. Since then, development has been moving at pace thanks to the tireless work of &lt;a href="https://dirkjan.ochtman.nl/" target="_blank" rel="noreferrer"&gt;Dirkjan&lt;/a&gt; and &lt;a href="https://jbp.io/" target="_blank" rel="noreferrer"&gt;Joe&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this post, I’ll explore the progress we’ve made, how you can try an early version, and where we’re going next.&lt;/p&gt;
&lt;h3 id="architecture--progress" class="relative group"&gt;Architecture &amp;amp; Progress &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#architecture--progress" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;As a reminder, upki’s primary goal is to provide a reliable, privacy-preserving, and efficient certificate revocation mechanism for Linux system utilities, package managers, and language runtimes. The solution is built around &lt;a href="https://blog.mozilla.org/security/2020/01/09/crlite-part-1-all-web-pki-revocations-compressed/" target="_blank" rel="noreferrer"&gt;CRLite&lt;/a&gt;, an efficient data format that compresses and distributes certificate revocation information at scale.&lt;/p&gt;
&lt;p&gt;The upki &lt;a href="https://github.com/rustls/upki" target="_blank" rel="noreferrer"&gt;repository&lt;/a&gt; is structured as a Cargo workspace containing five crates, each serving a distinct role:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;upki&lt;/code&gt;&lt;/strong&gt;: the core library and CLI tool. This crate contains the revocation query engine, the client-side sync logic for fetching filter updates, and the command-line interface. The revocation interface was originally embedded in the CLI, but has since been promoted into the library so that other Rust projects can use it directly as a dependency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;upki-mirror&lt;/code&gt;&lt;/strong&gt;: the server-side mirroring tool. This binary fetches and validates CRLite filters from Mozilla&amp;rsquo;s infrastructure such that they can be served using a standard web server like &lt;code&gt;nginx&lt;/code&gt; or &lt;code&gt;apache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;upki-ffi&lt;/code&gt;&lt;/strong&gt;: the C Foreign Function Interface. Built as a &lt;code&gt;cdylib&lt;/code&gt;, this crate uses &lt;a href="https://github.com/mozilla/cbindgen" target="_blank" rel="noreferrer"&gt;&lt;code&gt;cbindgen&lt;/code&gt;&lt;/a&gt; to auto-generate a &lt;code&gt;upki.h&lt;/code&gt; header file, exposing the revocation query API to C, C++, Go and any other language with C FFI support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rustls-upki&lt;/code&gt;&lt;/strong&gt;: an integration crate that wires upki&amp;rsquo;s revocation engine into &lt;a href="https://github.com/rustls/rustls" target="_blank" rel="noreferrer"&gt;rustls&lt;/a&gt;, enabling any Rust application using rustls to perform CRLite-backed revocation checks transparently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;revoke-test&lt;/code&gt;&lt;/strong&gt;: testing infrastructure for validating revocation queries against known-revoked certificates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The team recently released &lt;a href="https://github.com/rustls/upki/releases/tag/upki-0.1.0" target="_blank" rel="noreferrer"&gt;v0.1.0&lt;/a&gt;, which should help us to gather more feedback on the work we&amp;rsquo;ve done so far.&lt;/p&gt;
&lt;h3 id="how-to-try-it" class="relative group"&gt;How to try it &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-to-try-it" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;d like to try the code in its current form, you&amp;rsquo;ll need to have a version of the Rust toolchain installed. The easiest way to do this on Ubuntu is &lt;a href="https://documentation.ubuntu.com/ubuntu-for-developers/howto/rust-setup/#installing-the-latest-rust-toolchain-using-rustup" target="_blank" rel="noreferrer"&gt;using the &lt;code&gt;rustup&lt;/code&gt; snap&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ensure you have a C compiler in your PATH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y build-essential curl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install the rustup snap and get the stable toolchain&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install --classic rustup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rustup install stable
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install upki&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install upki
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.cargo/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fetch revocation data. This will be done in the background&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# when installed through the distro in the future&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;upki fetch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That should be all you need to install the development version of &lt;code&gt;upki&lt;/code&gt;, and you can now use it to run a revocation check by piping certificate output from &lt;code&gt;curl&lt;/code&gt; into &lt;code&gt;upki&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sw &lt;span class="s1"&gt;&amp;#39;%{certs}&amp;#39;&lt;/span&gt; https://google.com &lt;span class="p"&gt;|&lt;/span&gt; upki revocation check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NotRevoked
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Early versions of docs for the &lt;a href="https://docs.rs/upki-ffi/latest/upki/" target="_blank" rel="noreferrer"&gt;C FFI crate&lt;/a&gt; and &lt;a href="https://docs.rs/upki/latest/upki/" target="_blank" rel="noreferrer"&gt;Rust crate documentation&lt;/a&gt; are available, but if you&amp;rsquo;d like to explore, build the project from source, or contribute, the &lt;a href="https://github.com/rustls/upki" target="_blank" rel="noreferrer"&gt;repository&lt;/a&gt; is the best place to start. For an example of the C FFI interface in action you can take a look at the &lt;a href="https://github.com/rustls/upki-go-demo" target="_blank" rel="noreferrer"&gt;upki-go-demo&lt;/a&gt; Dirkjan published.&lt;/p&gt;
&lt;h3 id="next-steps" class="relative group"&gt;Next Steps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#next-steps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Now the foundational pieces are in place, our focus is shifting to external consumption, performance, and integration with the wider Linux ecosystem. In the coming days there should be an early &lt;code&gt;0.1.0&lt;/code&gt; binary release.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll also be doing some performance benchmarking on the initial fetch and of the revocation checks themselves. Currently, each revocation check reads several CRLite filter files into memory. There may be quick wins to improve this, but we’ll benchmark first and see if it warrants optimisation at this time.&lt;/p&gt;
&lt;p&gt;We also need to deploy some production infrastructure for serving the CRLite filters. If you follow the steps above, you&amp;rsquo;ll be fetching from a pre-production web server hosted at &lt;a href="https://upki.rustls.dev" target="_blank" rel="noreferrer"&gt;https://upki.rustls.dev&lt;/a&gt;. We&amp;rsquo;ve built a &lt;a href="https://github.com/jnsgruk/upki-mirror-k8s-operator" target="_blank" rel="noreferrer"&gt;Juju charm&lt;/a&gt; for operating the CRLite mirror on Kubernetes. This charm packages the &lt;code&gt;upki-mirror&lt;/code&gt; binary in a &lt;a href="https://ubuntu.com/blog/combining-distroless-and-ubuntu-chiselled-containers" target="_blank" rel="noreferrer"&gt;chiselled Rock&lt;/a&gt;, and will be deployed into Canonical&amp;rsquo;s datacentres to serve CRLite data at &lt;a href="https://crlite.ubuntu.com/" target="_blank" rel="noreferrer"&gt;crlite.ubuntu.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Our Ubuntu Foundations team is also working on packaging the various upki components for inclusion in the Ubuntu archive, which will enable you to &lt;code&gt;apt install upki&lt;/code&gt; in the future, and also enable us to package and enable it by default in Ubuntu 26.10 and beyond.&lt;/p&gt;
&lt;h3 id="further-down-the-road" class="relative group"&gt;Further Down the Road &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#further-down-the-road" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;While the work above covers what&amp;rsquo;s immediately in front of us, there is scope to expand upki&amp;rsquo;s capabilities further. Two areas of interest are Certificate Transparency enforcement, and support for Merkle Tree Certificates.&lt;/p&gt;
&lt;h4 id="certificate-transparency-enforcement" class="relative group"&gt;Certificate Transparency Enforcement &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#certificate-transparency-enforcement" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;While upki&amp;rsquo;s initial focus is on revocation checking, the project also aims to eventually support &lt;a href="https://certificate.transparency.dev/" target="_blank" rel="noreferrer"&gt;Certificate Transparency&lt;/a&gt; (CT) enforcement. CT is a more modern security measure that relies upon a set of publicly auditable, append-only logs that record every TLS certificate issued by a Certificate Authority (CA). This prevents CAs from issuing fraudulent or erroneous certificates without a means for that fraudulent activity to be discovered - a problem that has &lt;a href="https://blog.cloudflare.com/unauthorized-issuance-of-certificates-for-1-1-1-1/" target="_blank" rel="noreferrer"&gt;bitten organisations&lt;/a&gt; in the past.&lt;/p&gt;
&lt;p&gt;CT Enforcement would enable clients to refuse to establish a connection unless the server provides cryptographic proof that its certificate has been correctly logged. Browsers like Chrome and Firefox already enforce this, but the rest of the Linux ecosystem would need a tool such as upki to enable such functionality.&lt;/p&gt;
&lt;h4 id="intermediate-preloading" class="relative group"&gt;Intermediate Preloading &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#intermediate-preloading" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;A correctly configured TLS server should not only send its own certificate, but also the intermediate certificates needed to chain back to a trusted root. In practice, many servers omit the intermediate certificates, and because browsers have quietly worked around this for years, the misconfiguration often goes unnoticed.&lt;/p&gt;
&lt;p&gt;Firefox has been &lt;a href="https://blog.mozilla.org/security/2020/11/13/preloading-intermediate-ca-certificates-into-firefox/" target="_blank" rel="noreferrer"&gt;preloading all intermediates&lt;/a&gt; disclosed to the &lt;a href="https://www.ccadb.org/" target="_blank" rel="noreferrer"&gt;Common CA Database&lt;/a&gt; (CCADB) since Firefox 75, while Chrome and Edge will silently fetch missing intermediates using the Authority Information Access (AIA) extension in the server&amp;rsquo;s certificate. The result is that a broken certificate chain that works perfectly in every browser will produce an opaque &lt;code&gt;UNKNOWN_ISSUER&lt;/code&gt; error when accessed by Linux utilities like &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Because upki already maintains a regularly synced local data store, it&amp;rsquo;s well positioned to ship the known set of intermediates alongside the CRLite filters. This wouldn&amp;rsquo;t provide a security improvement so much as a usability improvement. It would also bring non-browser clients up to parity with browsers with respect to connection reliability. There is an additional privacy benefit too: rather than fetching a missing intermediate from the issuing CA (which discloses browsing activity to the CA), the intermediate is already present locally.&lt;/p&gt;
&lt;h4 id="merkle-tree-certificates" class="relative group"&gt;Merkle Tree Certificates &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#merkle-tree-certificates" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;Looking even further ahead, upki could support the next generation of web PKI by including support for &lt;a href="https://datatracker.ietf.org/doc/draft-davidben-tls-merkle-tree-certs/" target="_blank" rel="noreferrer"&gt;Merkle Tree Certificates (MTCs)&lt;/a&gt;. This is an area of active development in the IETF, with Cloudflare and Chrome recently &lt;a href="https://blog.cloudflare.com/bootstrap-mtc/" target="_blank" rel="noreferrer"&gt;announcing an experimental deployment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The motivation for MTCs comes largely from the transition to &lt;a href="https://openquantumsafe.org/post-quantum-crypto.html" target="_blank" rel="noreferrer"&gt;Post-Quantum (PQ) cryptography&lt;/a&gt;. PQ signatures are significantly larger than their non-PQ counterparts. The signatures for &lt;a href="https://openquantumsafe.org/liboqs/algorithms/sig/ml-dsa.html" target="_blank" rel="noreferrer"&gt;ML-DSA-44&lt;/a&gt; are 2,420 bytes compared to 64 bytes for ECDSA-P256. A typical TLS handshake today involves multiple signatures and public keys across the certificate chain and CT proofs, which means a simple swap to PQ algorithms would add tens of kilobytes of overhead per connection and likely a noticeable increase in connection latency.&lt;/p&gt;
&lt;p&gt;MTCs address this by rethinking how certificates are validated. Rather than transmitting a full certificate chain with multiple signatures, a Certificate Authority can batch certificates into a Merkle Tree and sign only the tree&amp;rsquo;s root hash. The client then receives just a single signature, a public key, and a compact Merkle tree inclusion proof that demonstrates the certificate&amp;rsquo;s presence in the batch. The signed tree heads can be distributed to clients out-of-band, meaning the per-handshake overhead is drastically reduced.&lt;/p&gt;
&lt;p&gt;Because upki already maintains a local data store that is regularly synced, it could cache tree head data alongside CRLite filters, thereby enabling the inclusion proofs sent during TLS handshakes to be even smaller. Rather than proving inclusion all the way from the leaf to the root, the server could send a &amp;ldquo;truncated&amp;rdquo; proof that starts partway up the tree, with the client computing the remainder from data it already has locally. There is a &lt;a href="https://datatracker.ietf.org/doc/draft-davidben-tls-merkle-tree-certs/" target="_blank" rel="noreferrer"&gt;TLS extension&lt;/a&gt; being developed to negotiate this.&lt;/p&gt;
&lt;p&gt;The implementation of MTCs for TLS is still highly experimental. MTCs are not yet deployed in any browser, but upki will lay the groundwork for Linux system utilities to benefit from this evolution as the technology is adopted.&lt;/p&gt;
&lt;h3 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;In the few weeks since we announced upki, the core revocation engine has been established and is now functional, the CRLite mirroring tool is working and a production deployment in Canonical&amp;rsquo;s datacentres is ongoing. We&amp;rsquo;re now preparing for an alpha release and remain on track for an opt-in preview for Ubuntu 26.04 LTS.&lt;/p&gt;
&lt;p&gt;Beyond revocation, we&amp;rsquo;re keeping a close eye on the evolving PKI landscape and particularly CT enforcement and Merkle Tree Certificates.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to extend my thanks again to &lt;a href="https://dirkjan.ochtman.nl/" target="_blank" rel="noreferrer"&gt;Dirkjan&lt;/a&gt; and &lt;a href="https://jbp.io/" target="_blank" rel="noreferrer"&gt;Joe&lt;/a&gt; for their continued collaboration on this work, and the utmost professionalism they&amp;rsquo;ve demonstrated throughout.&lt;/p&gt;</description></item><item><title>Developing with AI on Ubuntu</title><link>https://jnsgr.uk/2026/01/developing-with-ai-on-ubuntu/</link><pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2026/01/developing-with-ai-on-ubuntu/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/developing-with-ai-on-ubuntu/75299" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI-assisted tooling is becoming more and more common in the workflows of engineers at all experience levels. As I see it, our challenge is one of consideration, enablement and constraint. We must enable those who opt-in to safely and responsibly harness the power of these tools, while respecting those who do not wish to have their platform defined or overwhelmed by this class of software.&lt;/p&gt;
&lt;p&gt;The use of AI is a divisive topic among the tech community. I find myself a little in both camps, somewhere between sceptic and advocate. While I&amp;rsquo;m quick to acknowledge the negative impacts that the use of LLMs &lt;em&gt;can have&lt;/em&gt; on open source projects, I&amp;rsquo;m also surrounded by examples where it has been used responsibly to great effect.&lt;/p&gt;
&lt;p&gt;Examples of this include &lt;a href="https://filippo.io" target="_blank" rel="noreferrer"&gt;Filippo&lt;/a&gt;&amp;rsquo;s article &lt;a href="https://words.filippo.io/claude-debugging/" target="_blank" rel="noreferrer"&gt;debugging low-level cryptography with Claude Code&lt;/a&gt;, &lt;a href="https://mitchellh.com" target="_blank" rel="noreferrer"&gt;Mitchell&lt;/a&gt;&amp;rsquo;s article on &lt;a href="https://mitchellh.com/writing/non-trivial-vibing" target="_blank" rel="noreferrer"&gt;Vibing a Non-Trivial Ghostty Feature&lt;/a&gt;, and &lt;a href="https://github.com/crawshaw" target="_blank" rel="noreferrer"&gt;David&lt;/a&gt;&amp;rsquo;s article &lt;a href="https://crawshaw.io/blog/programming-with-agents" target="_blank" rel="noreferrer"&gt;How I Program with Agents&lt;/a&gt;. These articles come from engineers with proven expertise in careful, precise software engineering, yet they share an important sentiment: AI-assisted tools can be a remarkable force-multiplier when used &lt;em&gt;in conjunction&lt;/em&gt; with their lived experience, but care must still be taken to avoid poor outcomes.&lt;/p&gt;
&lt;p&gt;The aim of this post is not to convince you to use AI in your work, but rather to introduce the elements of Ubuntu that make it a first-class platform for safe, efficient experimentation and development. My goals for AI and Ubuntu are currently focused on enabling those who want to develop responsibly with AI tools, without negatively impacting the experience of those who&amp;rsquo;d prefer not to opt-in.&lt;/p&gt;
&lt;h3 id="hardware--drivers" class="relative group"&gt;Hardware &amp;amp; Drivers &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#hardware--drivers" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;AI-specific silicon is moving just as fast as AI software tooling, and without constant work to integrate drivers and userspace tools into Ubuntu, it would be impossible to efficiently utilise this specialised hardware.&lt;/p&gt;
&lt;p&gt;Last year we announced that we will ship both &lt;a href="https://canonical.com/blog/canonical-announces-it-will-support-and-distribute-nvidia-cuda-in-ubuntu" target="_blank" rel="noreferrer"&gt;NVIDIA&amp;rsquo;s CUDA&lt;/a&gt; and &lt;a href="https://canonical.com/blog/canonical-amd-rocm-ai-ml-hpc-libraries" target="_blank" rel="noreferrer"&gt;AMD&amp;rsquo;s ROCm&lt;/a&gt; in the Ubuntu archive for Ubuntu 26.04 LTS, in addition to our previous work on &lt;a href="https://snapcraft.io/publisher/openvino" target="_blank" rel="noreferrer"&gt;OpenVINO&lt;/a&gt;. This will make installing the latest drivers and toolkits easier and more secure, with no third-party software repositories. Distributing this software as part of Ubuntu enables us to be proactive in the delivery of security updates and the demonstration of provenance.&lt;/p&gt;
&lt;p&gt;Our work is not limited to AMD and NVIDIA; we recently &lt;a href="https://canonical.com/blog/ubuntu-ga-for-qualcomm-dragonwing" target="_blank" rel="noreferrer"&gt;announced&lt;/a&gt; support for Qualcomm&amp;rsquo;s &lt;a href="https://www.qualcomm.com/dragonwing" target="_blank" rel="noreferrer"&gt;Dragonwing&lt;/a&gt; platforms and others. You can read more about our silicon partner projects &lt;a href="https://canonical.com/partners/silicon" target="_blank" rel="noreferrer"&gt;on our website&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="inference-snaps" class="relative group"&gt;Inference Snaps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#inference-snaps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;At the &lt;a href="https://ubuntu.com/summit" target="_blank" rel="noreferrer"&gt;Ubuntu Summit 25.10&lt;/a&gt;, we &lt;a href="https://canonical.com/blog/canonical-releases-inference-snaps" target="_blank" rel="noreferrer"&gt;released&lt;/a&gt; &amp;ldquo;Inference Snaps&amp;rdquo; into the wild, which provide a hassle-free mechanism for obtaining the “famous model” you want to work with, but automatically receive a version of that model which is optimised for the silicon in your machine. This removes the need to spend hours on &lt;a href="https://huggingface.co/" target="_blank" rel="noreferrer"&gt;HuggingFace&lt;/a&gt; identifying the correct model to download that matches with your hardware, and obviates the need for in-depth understanding of model quantisation and tuning when getting started.&lt;/p&gt;
&lt;p&gt;Each of our inference snaps provide a consistent experience: you need only learn the basics once, but can apply those skills to different models as they emerge, whether you&amp;rsquo;re on a laptop or a server.&lt;/p&gt;
&lt;p&gt;At the time of writing, we&amp;rsquo;ve published &lt;code&gt;beta&lt;/code&gt; quality snaps for &lt;a href="https://snapcraft.io/qwen-vl" target="_blank" rel="noreferrer"&gt;qwen-vl&lt;/a&gt;, &lt;a href="https://snapcraft.io/deepseek-r1" target="_blank" rel="noreferrer"&gt;deepseek-r1&lt;/a&gt; and &lt;a href="https://snapcraft.io/gemma3" target="_blank" rel="noreferrer"&gt;gemma3&lt;/a&gt;. You can find a current list of snaps &lt;a href="https://documentation.ubuntu.com/inference-snaps/reference/snaps/" target="_blank" rel="noreferrer"&gt;in the documentation&lt;/a&gt;, along with the silicon-optimised variants.&lt;/p&gt;
&lt;h3 id="sandboxing-agents" class="relative group"&gt;Sandboxing Agents &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sandboxing-agents" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;While many start their journey in a web browser chatting to &lt;a href="https://chat.com" target="_blank" rel="noreferrer"&gt;ChatGPT&lt;/a&gt;, &lt;a href="https://claude.ai" target="_blank" rel="noreferrer"&gt;Claude&lt;/a&gt;, &lt;a href="https://gemini.google.com/app" target="_blank" rel="noreferrer"&gt;Gemini&lt;/a&gt;, &lt;a href="https://perplexity.ai" target="_blank" rel="noreferrer"&gt;Perplexity&lt;/a&gt; or one of the myriad of alternatives, many developers will find &amp;ldquo;agentic&amp;rdquo; tools such as &lt;a href="https://github.com/features/copilot" target="_blank" rel="noreferrer"&gt;Copilot&lt;/a&gt;, &lt;a href="https://openai.com/codex/" target="_blank" rel="noreferrer"&gt;Codex&lt;/a&gt;, &lt;a href="https://claude.com/product/claude-code" target="_blank" rel="noreferrer"&gt;Claude Code&lt;/a&gt; or &lt;a href="https://ampcode.com/" target="_blank" rel="noreferrer"&gt;Amp&lt;/a&gt; quite attractive. In my experience, agents are a clear level-up in an LLM&amp;rsquo;s capability for developers, but they can still make poor decisions and are generally safer to run in sandboxed environment at the time of writing.&lt;/p&gt;
&lt;p&gt;Where a traditional chat-based AI tool responds reactively to user prompts within a single conversation, an agent operates (semi-)autonomously to pursue goals. It perceives its environment, plans, makes decisions and can call out to external tools and services to achieve those goals. If you grant permission, an agent can read and understand your code, implement features, troubleshoot bugs, optimise performance and many other tasks. The catch is that they often need &lt;em&gt;access to your system&lt;/em&gt; - whether that be to modify files or run commands.&lt;/p&gt;
&lt;p&gt;Issues such as accidental file deletion, or the inclusion of a spurious (and potentially compromised) dependency are an inevitable failure mode of the current generation of agents due to how they&amp;rsquo;re trained (see the &lt;a href="https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/" target="_blank" rel="noreferrer"&gt;Reddit post&lt;/a&gt; about Claude Code deleting a user&amp;rsquo;s home directory).&lt;/p&gt;
&lt;h4 id="my-agent-sandboxes-itself" class="relative group"&gt;My agent sandboxes itself! &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#my-agent-sandboxes-itself" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;Some of you will be reading this wondering why additional sandboxing is required, since many of the popular agents &lt;a href="https://code.claude.com/docs/en/sandboxing" target="_blank" rel="noreferrer"&gt;advertise their own sandboxing&lt;/a&gt;. The fact that some agents include some measures to protect the user&amp;rsquo;s machine is of course a good thing. The touted benefits include filesystem isolation by restricting the agent to a specific directory, or prompting for approval before modifying files. Some agents also include network sandboxing to restrict network access to a list of approved domains, or by using a custom proxy to impose rules on outbound traffic.&lt;/p&gt;
&lt;p&gt;On Linux, these agent-imposed sandboxes are often implemented with &lt;a href="https://github.com/containers/bubblewrap" target="_blank" rel="noreferrer"&gt;bubblewrap&lt;/a&gt;, which is &amp;ldquo;a tool for constructing sandbox environments&amp;rdquo;, but note that the upstream project&amp;rsquo;s README includes &lt;a href="https://github.com/containers/bubblewrap#sandbox-security" target="_blank" rel="noreferrer"&gt;a section&lt;/a&gt; which states that it is &lt;em&gt;not&lt;/em&gt; a &amp;ldquo;ready-made sandbox with a specific security policy&amp;rdquo;. &lt;code&gt;bubblewrap&lt;/code&gt; is a relatively low-level tool that must be given its configuration, which in this case is provided &lt;em&gt;by the agent&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The limitation upon these tools is the shared kernel - a severe kernel exploit could enable an agent to escape from its sandbox. Of course, such vulnerabilities are rare, but note that even if the sandboxing technologies do their job, agents often run in the context of the user&amp;rsquo;s session, meaning they inherit environment variables which could contain sensitive information. They&amp;rsquo;re also agent specific: Claude Code&amp;rsquo;s sandboxing won&amp;rsquo;t help you if you&amp;rsquo;re using &lt;a href="https://cursor.com/" target="_blank" rel="noreferrer"&gt;Cursor&lt;/a&gt; or &lt;a href="https://antigravity.google/" target="_blank" rel="noreferrer"&gt;Antigravity&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Depending on your threat model and the project you&amp;rsquo;re working on, you may deem the built-in sandboxing of coding agents to be sufficient, but there are other options available to Ubuntu users that provide either different, or additional protection&amp;hellip;&lt;/p&gt;
&lt;h4 id="sandbox-with-lxd-containers" class="relative group"&gt;Sandbox with LXD containers &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sandbox-with-lxd-containers" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;Canonical&amp;rsquo;s &lt;a href="https://canonical.com/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt; works out-of-the-box on Ubuntu, and is a great way to sandbox an agent into a disposable environment where the blast radius is limited should the agent make a mistake. My personal workflow is to create an Ubuntu container (or VM) with my project directory mounted. This way, I can edit my code directly on my filesystem with my preferred (already configured) editor, but have the agent run inside the container.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Initialise the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc init ubuntu:noble dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Mount my project directory into the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc config device add -q dev datadir disk &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/my-project&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/ubuntu/project
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc start dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell inside the container as the &amp;#39;ubuntu&amp;#39; user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc &lt;span class="nb"&gt;exec&lt;/span&gt; dev -- sudo -u ubuntu -i bash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run a command in the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc &lt;span class="nb"&gt;exec&lt;/span&gt; dev -- sudo -u ubuntu -i bash -c &lt;span class="s2"&gt;&amp;#34;cd project; claude&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can learn more about LXD in the official &lt;a href="https://documentation.ubuntu.com/lxd/stable-5.21/" target="_blank" rel="noreferrer"&gt;documentation&lt;/a&gt; and &lt;a href="https://documentation.ubuntu.com/lxd/stable-5.21/tutorial/first_steps/#first-steps" target="_blank" rel="noreferrer"&gt;tutorial&lt;/a&gt;, as well as specific instructions on &lt;a href="https://ubuntu.com/tutorials/gpu-data-processing-inside-lxd#1-overview" target="_blank" rel="noreferrer"&gt;enabling GPU data processing in containers/VMs&lt;/a&gt;. I&amp;rsquo;ve written &lt;a href="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/" target="_blank" rel="noreferrer"&gt;previously&lt;/a&gt; about my use of LXD in development.&lt;/p&gt;
&lt;p&gt;With LXD, you can choose between running your sandbox as a container or a VM, depending on your project&amp;rsquo;s needs. If I&amp;rsquo;m working on a project that requires Kubernetes or similar, I use a VM, but for lighter projects I use system containers, preferring their lower overhead.&lt;/p&gt;
&lt;h4 id="sandbox-with-lxd-vms" class="relative group"&gt;Sandbox with LXD VMs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sandbox-with-lxd-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;LXD is best known for its ability to run &amp;ldquo;system containers&amp;rdquo;, which are somewhat analogous to Docker/OCI containers, but rather than being focused on a single application (and dependencies), a system container essentially runs an entire Ubuntu user-space (including &lt;code&gt;systemd&lt;/code&gt;, etc.). Like OCI containers, however, system containers share the kernel with the host.&lt;/p&gt;
&lt;p&gt;In some situations, you may seek more isolation from your host machine by running tools inside a virtual machine with their own kernel. LXD makes this simple - you can follow the same commands as above, but add &lt;code&gt;--vm&lt;/code&gt; to the &lt;code&gt;init&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Initialise the virtual machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc init --vm ubuntu:noble dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can also configure the virtual machine&amp;rsquo;s CPU, memory and disk requirements. A simple example is below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc init --vm ubuntu:noble dev &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c limits.cpu&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c limits.memory&lt;span class="o"&gt;=&lt;/span&gt;8GiB &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d root,size&lt;span class="o"&gt;=&lt;/span&gt;100GiB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can find more details on instance configuration in the &lt;a href="https://documentation.ubuntu.com/lxd/stable-5.21/howto/instances_configure/" target="_blank" rel="noreferrer"&gt;LXD documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="sandbox-with-multipass" class="relative group"&gt;Sandbox with Multipass &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sandbox-with-multipass" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;&lt;a href="https://multipass.run/" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt; provides on-demand access to Ubuntu VMs from any workstation - whether that workstation is running Linux, macOS or Windows. It is designed to replicate, in a lightweight way, the experience of provisioning a simple Ubuntu VM on a cloud.&lt;/p&gt;
&lt;p&gt;Multipass&amp;rsquo; scope is more limited than LXD, but for many users it provides a simple on-ramp for development with Ubuntu. Where it lacks advanced features like GPU passthrough, it boasts a simplified CLI and a first-class &lt;a href="https://documentation.ubuntu.com/multipass/latest/reference/gui-client/" target="_blank" rel="noreferrer"&gt;GUI client&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To get started similarly to the LXD example above, try the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install Multipass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install multipass
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch an instance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass launch noble -n dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Mount your project directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass mount ~/my-project dev:/home/ubuntu/project
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell in the instance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass shell dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run a command in the instance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass &lt;span class="nb"&gt;exec&lt;/span&gt; dev -- claude
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can find more details on how to configure and manage instances &lt;a href="https://documentation.ubuntu.com/multipass/latest/" target="_blank" rel="noreferrer"&gt;in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="sandbox-with-wsl" class="relative group"&gt;Sandbox with WSL &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sandbox-with-wsl" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;If you&amp;rsquo;re on Windows, &lt;a href="https://documentation.ubuntu.com/wsl/stable/tutorials/develop-with-ubuntu-wsl/" target="_blank" rel="noreferrer"&gt;development with WSL&lt;/a&gt; includes first-class &lt;a href="https://documentation.ubuntu.com/wsl/stable/howto/gpu-cuda/" target="_blank" rel="noreferrer"&gt;support for GPU acceleration&lt;/a&gt;, and is even supported for use with the &lt;a href="https://ubuntu.com/blog/accelerate-ai-development-with-ubuntu-and-nvidia-ai-workbench" target="_blank" rel="noreferrer"&gt;NVIDIA AI Workbench&lt;/a&gt;, &lt;a href="https://docs.nvidia.com/nim/wsl2/latest/getting-started.html" target="_blank" rel="noreferrer"&gt;NVIDIA NIM&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/windows/ai/directml/gpu-cuda-in-wsl" target="_blank" rel="noreferrer"&gt;CUDA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ubuntu is the default Linux distribution for WSL, and you can find more information about how to set up and use Ubuntu on WSL in &lt;a href="https://documentation.ubuntu.com/wsl/stable/" target="_blank" rel="noreferrer"&gt;our documentation&lt;/a&gt;. WSL benefits from all the same technologies as a &amp;ldquo;regular&amp;rdquo; Ubuntu install, including the ability to use Snaps, Docker and LXD.&lt;/p&gt;
&lt;p&gt;For the enterprise developer, we recently announced &lt;a href="https://canonical.com/blog/canonical-announces-ubuntu-pro-for-wsl" target="_blank" rel="noreferrer"&gt;Ubuntu Pro for WSL&lt;/a&gt;, as well as the ability to manage WSL instances &lt;a href="https://documentation.ubuntu.com/landscape/how-to-guides/wsl-integration/manage-wsl-instances/" target="_blank" rel="noreferrer"&gt;using Landscape&lt;/a&gt;, making it easier to get access to first-class developer tooling with Ubuntu on your corporate machine.&lt;/p&gt;
&lt;h3 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;While opinion remains divided on the value and impact of current AI tooling, its presence in modern development workflows and its demands on underlying compute infrastructure are difficult to ignore.&lt;/p&gt;
&lt;p&gt;Developers who wish to experiment need reliable access to modern hardware, predictable tooling, and strong isolation boundaries. Ubuntu’s role is not to dictate how these tools are used, but to provide a stable and dependable platform on which they can be explored and deployed safely, without compromising security, provenance, or the day-to-day experience of those who choose to opt out.&lt;/p&gt;
&lt;p&gt;In addition to powering development workflows, Ubuntu makes for a dependable production operating system for your workloads. We&amp;rsquo;re building &lt;a href="https://documentation.ubuntu.com/canonical-kubernetes/latest/" target="_blank" rel="noreferrer"&gt;Canonical Kubernetes&lt;/a&gt; with first-class GPU support, &lt;a href="https://canonical.com/mlops/kubeflow" target="_blank" rel="noreferrer"&gt;Kubeflow&lt;/a&gt; and &lt;a href="https://canonical.com/mlops/mlflow" target="_blank" rel="noreferrer"&gt;MLFlow&lt;/a&gt; for model training and serving and a suite of applications like &lt;a href="https://canonical.com/data/postgresql" target="_blank" rel="noreferrer"&gt;PostgreSQL&lt;/a&gt;, &lt;a href="https://canonical.com/data/mysql" target="_blank" rel="noreferrer"&gt;MySQL&lt;/a&gt;, &lt;a href="https://canonical.com/data/opensearch" target="_blank" rel="noreferrer"&gt;Opensearch&lt;/a&gt;, as well as other data-centric tools such as &lt;a href="https://canonical.com/data/kafka" target="_blank" rel="noreferrer"&gt;Kafka&lt;/a&gt; and &lt;a href="https://canonical.com/data/spark" target="_blank" rel="noreferrer"&gt;Spark&lt;/a&gt; that can be deployed with full &lt;a href="https://ubuntu.com/pro" target="_blank" rel="noreferrer"&gt;Ubuntu Pro&lt;/a&gt; support. Let me know if you&amp;rsquo;d find value in a follow-up post on those topics!&lt;/p&gt;</description></item><item><title>Addressing Linux's Missing PKI Infrastructure</title><link>https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/</link><pubDate>Mon, 08 Dec 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/addressing-linuxs-missing-pki-infrastructure/73314" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Earlier this year, &lt;a href="https://lwn.net/" target="_blank" rel="noreferrer"&gt;LWN&lt;/a&gt; featured an excellent article titled &amp;ldquo;&lt;a href="https://lwn.net/Articles/1033809/" target="_blank" rel="noreferrer"&gt;Linux&amp;rsquo;s missing CRL infrastructure&lt;/a&gt;&amp;rdquo;. The article highlighted a number of key issues surrounding traditional Public Key Infrastructure (PKI), but critically noted how even the available measures are effectively ignored by the majority of system-level software on Linux.&lt;/p&gt;
&lt;p&gt;One of the motivators for the discussion is that the Online Certificate Status Protocol (OCSP) will cease to be supported by Let&amp;rsquo;s Encrypt. The remaining alternative is to use Certificate Revocation Lists (CRLs), yet there is little or no support for managing (or even querying) these lists in most Linux system utilities.&lt;/p&gt;
&lt;p&gt;To solve this, I&amp;rsquo;m happy to share that in partnership with &lt;a href="https://github.com/rustls/rustls" target="_blank" rel="noreferrer"&gt;rustls&lt;/a&gt; maintainers &lt;a href="https://dirkjan.ochtman.nl/" target="_blank" rel="noreferrer"&gt;Dirkjan Ochtman&lt;/a&gt; and &lt;a href="https://jbp.io/" target="_blank" rel="noreferrer"&gt;Joe Birr-Pixton&lt;/a&gt;, we&amp;rsquo;re starting the development of upki: a universal PKI tool. This project initially aims to close the revocation gap through the combination of a new system utility and eventual library support for common TLS/SSL libraries such as &lt;a href="https://openssl-library.org/" target="_blank" rel="noreferrer"&gt;OpenSSL&lt;/a&gt;, &lt;a href="https://gnutls.org/" target="_blank" rel="noreferrer"&gt;GnuTLS&lt;/a&gt; and &lt;a href="https://github.com/rustls/rustls" target="_blank" rel="noreferrer"&gt;rustls&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-problem" class="relative group"&gt;The Problem &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-problem" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Online Certificate Authorities responsible for issuing TLS certificates have long had mechanisms for revoking known bad certificates. What constitutes a known bad certificate varies, but generally it means a certificate was issued either in error, or by a malicious actor of some form. There have been two primary mechanisms for this revocation: &lt;a href="https://datatracker.ietf.org/doc/html/rfc5280" target="_blank" rel="noreferrer"&gt;Certificate Revocation Lists&lt;/a&gt; (CRLs) and the &lt;a href="https://datatracker.ietf.org/doc/html/rfc6960" target="_blank" rel="noreferrer"&gt;Online Certificate Status Protocol&lt;/a&gt; (OCSP).&lt;/p&gt;
&lt;p&gt;In July 2024, &lt;a href="https://letsencrypt.org/" target="_blank" rel="noreferrer"&gt;Let’s Encrypt&lt;/a&gt; &lt;a href="https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls.html" target="_blank" rel="noreferrer"&gt;announced&lt;/a&gt; the deprecation of support for the Online Certificate Status Protocol (OCSP). This wasn&amp;rsquo;t entirely unexpected - the protocol has suffered from privacy defects which leak the browsing habits of users to Certificate Authorities. Various implementations have also suffered reliability issues that forced most implementers to adopt &amp;ldquo;soft-fail&amp;rdquo; policies, rendering the checks largely ineffective.&lt;/p&gt;
&lt;p&gt;The deprecation of OCSP leaves us with CRLs. Both Windows and macOS rely on operating system components to centralise the fetching and parsing of CRLs, but Linux has traditionally delegated this responsibility to individual applications. This is done most effectively in browsers such as Mozilla Firefox, Google Chrome and Chromium, but this has been achieved with bespoke infrastructure.&lt;/p&gt;
&lt;p&gt;However, Linux itself has fallen short by not providing consistent revocation checking infrastructure for the rest of userspace - tools such as curl, system package managers and language runtimes lack a unified mechanism to process this data.&lt;/p&gt;
&lt;p&gt;The ideal solution to this problem, which is slowly &lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45.html" target="_blank" rel="noreferrer"&gt;becoming more prevalent&lt;/a&gt;, is to issue short-lived credentials with an expiration of 10 days or less, somewhat removing the need for complicated revocation infrastructure, but reducing certificate lifetimes is happening slowly and requires significant automation.&lt;/p&gt;
&lt;h2 id="crlite" class="relative group"&gt;CRLite &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#crlite" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;There are several key challenges with CRLs in practice - the size of the list has grown dramatically as the web has scaled, and one must collate CRLs from all relevant certificate authorities in order to be useful. CRLite was originally proposed by researchers at IEEE S&amp;amp;P and subsequently adopted in Mozilla Firefox. It offers a pragmatic solution to the problem of distributing large CRL datasets to client machines.&lt;/p&gt;
&lt;p&gt;In a recent &lt;a href="https://hacks.mozilla.org/2025/08/crlite-fast-private-and-comprehensive-certificate-revocation-checking-in-firefox/" target="_blank" rel="noreferrer"&gt;blog post&lt;/a&gt;, Mozilla outlined how their CRLite implementation meant that on average users &amp;ldquo;downloaded 300kB of revocation data per day, a 4MB snapshot every 45 days and a sequence of &amp;ldquo;delta-updates&amp;rdquo; in-between&amp;rdquo;, which amounts to CRLite being 1000x more bandwidth-efficient than daily CRL downloads.&lt;/p&gt;
&lt;p&gt;At its core, CRLite is a data structure compressing the full set of web-PKI revocations into a compact, efficiently queryable form. You can find more information about CRLite&amp;rsquo;s design and implementation on &lt;a href="https://blog.mozilla.org/security/tag/crlite/" target="_blank" rel="noreferrer"&gt;Mozilla&amp;rsquo;s Security Blog&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="introducing-upki" class="relative group"&gt;Introducing upki &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introducing-upki" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Following our work on &lt;a href="https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/" target="_blank" rel="noreferrer"&gt;oxidizing Ubuntu&lt;/a&gt;, &lt;a href="https://dirkjan.ochtman.nl/" target="_blank" rel="noreferrer"&gt;Dirkjan&lt;/a&gt; reached out to me with a proposal to introduce a system-level utility backed by CRLite to non-browser users.&lt;/p&gt;
&lt;p&gt;upki will be an open source project, initially packaged for Ubuntu but available to all Linux distributions, and likely portable to other Unix-like operating systems. Written in Rust, upki supports three roles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Server-side mirroring tool&lt;/strong&gt;: responsible for downloading and mirroring the CRLite filters provided by Mozilla, enabling us to operate independent CDN infrastructure for CRLite users, and serving them to clients. This will insulate upki from changes in the Mozilla backend, and enable standing up an independent data source if required. The server-side tool will manifest as a service that periodically checks the Mozilla Firefox CRLite filters, downloads and validates the files, and serves them.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client-side sync tool&lt;/strong&gt;: run regularly by a systemd-timer, network-up events or similar, this tool ensures the contents of the CDN are reflected in the on-disk filter cache. This will be extremely low on bandwidth and CPU usage assuming everything is up to date.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client-side query tool&lt;/strong&gt;: a CLI interface for querying revocation data. This will be useful for monitoring and deployment workflows, as well as for users without a good C FFI.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The latter two roles are served by a single Rust binary that runs in different modes depending on how it is invoked. The server-side tool will be a separate binary, since its use will be much less widespread. Under the hood, all of this will be powered by Rust library crates that can be integrated in other projects via crates.io.&lt;/p&gt;
&lt;p&gt;For the initial release, Canonical will stand up the backend infrastructure required to mirror and serve the CRLite data for upki users, though the backend will be configurable. This prevents unbounded load on Mozilla’s infrastructure and ensures long-term stability even if Firefox’s internal formats evolve.&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_443927a2cc8ea5be.webp 330w,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_f1c7127e41b7d6cc.webp 660w
,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_705ea1ebe4137e28.webp 1024w
,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_be2b5fbb2881c88a.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1720"
height="1670"
class="mx-auto my-0 rounded-md"
alt="architecture diagram for upki"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_b16265b7d66a056c.png" srcset="https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_77c0dd2534a34637.png 330w,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_b16265b7d66a056c.png 660w
,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_21c7d0a4f341695e.png 1024w
,https://jnsgr.uk/2025/12/addressing-linuxs-missing-pki-infra/01_hu_34470e9d78fe7948.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="ecosystem-compatibility" class="relative group"&gt;Ecosystem Compatibility &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ecosystem-compatibility" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;So far we&amp;rsquo;ve covered the introduction of a new Rust binary (and crate) for supporting the fetching, serving and querying of CRL data, but that doesn&amp;rsquo;t provide much service to the existing ecosystem of Linux applications and libraries in the problem statement.&lt;/p&gt;
&lt;p&gt;The upki project will also provide a shared object library for a stable ABI that allows C and C-FFI programs to make revocation queries, using the contents of the on-disk filter cache.&lt;/p&gt;
&lt;p&gt;Once &lt;code&gt;upki&lt;/code&gt; is released and available, work can begin on integrating existing crypto libraries such as OpenSSL, GNUtls and rustls. This will be performed through the shared object library by means of an optional callback mechanism these libraries can use to check the revocation lists before establishing a connection to a given server with a certificate.&lt;/p&gt;
&lt;h2 id="timeline" class="relative group"&gt;Timeline &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#timeline" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;While we&amp;rsquo;ve been discussing this project for a couple of months, ironing out the details of funding and design, work will soon begin on the initial implementation of upki.&lt;/p&gt;
&lt;p&gt;Our aim is to make upki available as an opt-in preview for the release of Ubuntu 26.04 LTS, meaning we&amp;rsquo;ll need to complete the implementation of the server/client functionality, and bootstrap the mirroring/serving infrastructure at Canonical before April 2026.&lt;/p&gt;
&lt;p&gt;In the following Ubuntu release cycle, the run up to Ubuntu 26.10, we&amp;rsquo;ll aim to ship the tool by default on Ubuntu systems, and begin work on integration with the likes of NSS, OpenSSL, GNUtls and rustls.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Linux has a clear gap in its handling of revocation data for PKIs. Over the coming months we&amp;rsquo;re hoping to address that gap by developing upki not just for Ubuntu, but for the entire ecosystem. Thanks to Mozilla&amp;rsquo;s work on CRLite, and the expertise of Dirkjan and Joe, we&amp;rsquo;re confident that we&amp;rsquo;ll deliver a resilient and efficient solution that should make a meaningful contribution to systems security across the web.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d like to do more reading on the subject, I&amp;rsquo;d recommend the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LWN.net:&lt;/strong&gt; &lt;a href="https://lwn.net/Articles/1033809/" target="_blank" rel="noreferrer"&gt;Linux&amp;rsquo;s missing CRL infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mozilla Security Blog:&lt;/strong&gt; &lt;a href="https://blog.mozilla.org/security/2020/01/09/crlite-part-1-all-web-pki-revocations-compressed/" target="_blank" rel="noreferrer"&gt;CRLite Part 1: All Web PKI Revocations Compressed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mozilla Security Blog:&lt;/strong&gt; &lt;a href="https://blog.mozilla.org/security/2020/01/09/crlite-part-2-end-to-end-design/" target="_blank" rel="noreferrer"&gt;CRLite Part 2: End-to-End Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let’s Encrypt:&lt;/strong&gt; &lt;a href="https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls.html" target="_blank" rel="noreferrer"&gt;Replacing OCSP with CRLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IEEE Symposium on Security &amp;amp; Privacy:&lt;/strong&gt; &lt;a href="https://www.google.com/search?q=https://ieeexplore.ieee.org/document/7958572" target="_blank" rel="noreferrer"&gt;CRLite: A Scalable System for Pushing All TLS Revocations to All Browsers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Ubuntu Summit 25.10: Personal Highlights</title><link>https://jnsgr.uk/2025/11/ubuntu-summit-25/</link><pubDate>Sun, 02 Nov 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/11/ubuntu-summit-25/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/ubuntu-summit-25-10-personal-highlights/71509" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I recently had the privilege of attending the &lt;a href="https://ubuntu.com/summit" target="_blank" rel="noreferrer"&gt;Ubuntu Summit 25.10&lt;/a&gt;. Ubuntu Summits have a relatively long history. Some years ago Canonical ran the ‘Ubuntu Developer Summits (UDS)’, but recently the events were brought back and reimagined as the ‘Ubuntu Summit’.&lt;/p&gt;
&lt;p&gt;For the most recent Summit, we tried out a new format. We invited a select few folks to come and give talks at our London office, with a small in-person crowd. In addition, the event was livestreamed, and we encouraged people to host &amp;ldquo;watching parties&amp;rdquo; across the world as part of &lt;a href="https://ubuntu.com/community/docs/locos?next=%2Fg1m%2F" target="_blank" rel="noreferrer"&gt;Ubuntu Local Communities (LoCos)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While Ubuntu may feature in the name, the event does not require talks to be centred on Ubuntu, and in fact is aiming to draw contributions from our partners and from right across the open source community, whether or not the content is relevant to Ubuntu or Canonical - it&amp;rsquo;s designed to be a showcase for the very best of open source, and this year I felt that the talks were of a particularly high calibre.&lt;/p&gt;
&lt;p&gt;In this post I&amp;rsquo;ll highlight some of my favourite talks, in no particular order! If any of these catch your interest, you can see &lt;a href="https://discourse.ubuntu.com/t/ubuntu-summit-25-10-timetable/65271" target="_blank" rel="noreferrer"&gt;when they were aired&lt;/a&gt; and catch-up on the &lt;a href="https://www.youtube.com/live/bEEamxJ60aI" target="_blank" rel="noreferrer"&gt;Day 1&lt;/a&gt; and &lt;a href="https://www.youtube.com/live/WvNgMEumSoA" target="_blank" rel="noreferrer"&gt;Day 2&lt;/a&gt; streams.&lt;/p&gt;
&lt;h2 id="doom-in-space" class="relative group"&gt;DOOM in Space &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#doom-in-space" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_65dfe195c8d0716f.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_9322be6f635856bb.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_9172933e609edc31.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_8b2cd4b205915e58.webp 1280w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1280"
height="720"
class="mx-auto my-0 rounded-md"
alt="opening slide for doom in space talk"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_2091739036a71cb.png" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_b9533f51793b46ca.png 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_2091739036a71cb.png 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/04_hu_9ae56e6f039ce272.png 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/04.png 1280w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What a way to kick off the Summit! &lt;a href="https://discourse.ubuntu.com/t/doom-in-space/67019" target="_blank" rel="noreferrer"&gt;DOOM in Space&lt;/a&gt; was a talk given by &lt;a href="https://olafurw.com/aboutme/" target="_blank" rel="noreferrer"&gt;Ólafur Waage&lt;/a&gt;, who introduced himself as a &amp;ldquo;professional keyboard typist&amp;rdquo;!&lt;/p&gt;
&lt;p&gt;The talk was immediately after Mark Shuttleworth&amp;rsquo;s opening remarks, and covered his journey in getting DOOM to run on the European Space Agency&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/OPS-SAT" target="_blank" rel="noreferrer"&gt;OPS-SAT&lt;/a&gt; satellite. DOOM has famously been ported to &lt;a href="https://en.wikipedia.org/wiki/List_of_Doom_ports" target="_blank" rel="noreferrer"&gt;many devices&lt;/a&gt;, though some were only questionably &amp;ldquo;running&amp;rdquo; the game.&lt;/p&gt;
&lt;p&gt;Ólafur covered how he became involved in the project, and the unique approach they needed to take to guarantee success, since they would only get a very limited amount of time in order to conduct their &amp;ldquo;experiment&amp;rdquo; on the satellite.&lt;/p&gt;
&lt;p&gt;Of particular note was the work done to integrate imagery from the OPS-SAT&amp;rsquo;s onboard camera into the game, which involved some clever reassigning of colors in the game&amp;rsquo;s original palette to more faithfully represent the imagery taken from the camera in-game.&lt;/p&gt;
&lt;h2 id="infrastructure-wide-profiling-of-nvidia-cuda" class="relative group"&gt;Infrastructure-Wide Profiling of Nvidia CUDA &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#infrastructure-wide-profiling-of-nvidia-cuda" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="03.jpeg"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_c19c7f7a82ce28ec.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_3289cc8a3c6ace68.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_63ea02edaca8b2f8.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_47b446e14afc4dc7.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1600"
height="900"
class="mx-auto my-0 rounded-md"
alt="opening slide for profiling talk"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_a857ebebb22203c0.jpeg" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_ee47e997a3027b8c.jpeg 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_a857ebebb22203c0.jpeg 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_a29a7d039dd13cc1.jpeg 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/03_hu_cb8dc774efe7bf9f.jpeg 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://discourse.ubuntu.com/t/infrastructure-wide-profiling-of-nvidia-cuda/67248" target="_blank" rel="noreferrer"&gt;This talk&lt;/a&gt; was given by &lt;a href="https://github.com/brancz" target="_blank" rel="noreferrer"&gt;Frederic Branczyk&lt;/a&gt;, CEO and Founder of &lt;a href="https://polarsignals.com" target="_blank" rel="noreferrer"&gt;Polar Signals&lt;/a&gt;. Canonical has partnered with Polar Signals a couple of times in recent years. They were part of our journey to &lt;a href="https://ubuntu.com/blog/ubuntu-performance-engineering-with-frame-pointers-by-default" target="_blank" rel="noreferrer"&gt;enabling frame pointers by default&lt;/a&gt; on Ubuntu, and many of our teams have been using their zero-instrumentation &lt;a href="https://github.com/parca-dev/parca-agent" target="_blank" rel="noreferrer"&gt;eBPF profiler&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;While CPU profiling has been commonplace for developers for many years, giving the ability to analyse CPU and memory-bound workloads, profiling GPU workloads has been less prominent, and is particularly difficult in production.&lt;/p&gt;
&lt;p&gt;Polar Signals advocate for &amp;ldquo;continuous profiling&amp;rdquo;, which means running a profiler at all times, on all nodes, in production. The benefit of this is that when an issue occurs, you don&amp;rsquo;t have to set up a profiler and try to reproduce the issue - you already have the data. It also negates the uncertainty of the impact a profiler might have on the code during reproduction. This would have been difficult with traditional profiling tools, but with technologies like &lt;a href="https://ebpf.io/" target="_blank" rel="noreferrer"&gt;eBPF&lt;/a&gt;, the overhead of the profiler is incredibly low compared to the potential performance gains from acting on the data it produces.&lt;/p&gt;
&lt;p&gt;In this talk, Frederic outlined the work they have done bringing infrastructure-wide profiling of CUDA workloads into Polar Signals Cloud. Their approach combines the &lt;a href="https://docs.nvidia.com/cupti/" target="_blank" rel="noreferrer"&gt;CUPTI profiling API&lt;/a&gt; with &lt;a href="https://docs.ebpf.io/linux/concepts/usdt/" target="_blank" rel="noreferrer"&gt;USDT&lt;/a&gt; probes and eBPF into a pipeline, relying upon the ability to inject a small library into CUDA workloads using the &lt;code&gt;CUDA_INJECTION64_PATH&lt;/code&gt; without modification.&lt;/p&gt;
&lt;p&gt;You can see more details &lt;a href="https://www.polarsignals.com/blog/posts/2025/10/22/gpu-profiling" target="_blank" rel="noreferrer"&gt;on their website&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="inference-snaps" class="relative group"&gt;Inference Snaps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#inference-snaps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_1d018afaaf936f4c.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_633333fcdd4ed333.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_9d1bbcb6c1e5044a.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_ca0ce363eac0f419.webp 1280w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1280"
height="720"
class="mx-auto my-0 rounded-md"
alt="opening slide for inference snaps talk"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_d3e5811b82e6933.png" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_ebbc2e9e26d4db3.png 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_d3e5811b82e6933.png 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/02_hu_711ebc18bd1dda97.png 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/02.png 1280w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This talk served as the first &lt;a href="https://canonical.com/blog/canonical-releases-inference-snaps" target="_blank" rel="noreferrer"&gt;public announcement&lt;/a&gt; of Inference Snaps from Canonical, which represents a few months of working combining many of the new technologies behind Snaps.&lt;/p&gt;
&lt;p&gt;As Large Language Models continue to gain pace along with the rest of the AI community, silicon manufacturers are increasingly including dedicated hardware in commodity CPUs and GPUs, as well as shipping dedicated accelerators for some workloads.&lt;/p&gt;
&lt;p&gt;AI models often need to be tuned in some way in order to work optimally - for example &lt;a href="https://huggingface.co/docs/optimum/en/concept_guides/quantization" target="_blank" rel="noreferrer"&gt;quantisation&lt;/a&gt; which aims to reduce the computational memory costs of running inference on a given model.&lt;/p&gt;
&lt;p&gt;Inference snaps provide a hassle-free mechanism for users to obtain the &amp;ldquo;famous model&amp;rdquo; they want to work with, but automatically receive a version of that model which is optimised for the silicon in their machine, removing the need to spend hours on HuggingFace trying to identify the correct model to download that matches with their hardware.&lt;/p&gt;
&lt;p&gt;Using our extensive partner network, we&amp;rsquo;ll continue to work with multiple silicon vendors to ensure that models are available for the latest hardware as it drops, and provide a consistent experience to Ubuntu users that wish to work with AI.&lt;/p&gt;
&lt;p&gt;Find out more in the &lt;a href="https://canonical.com/blog/canonical-releases-inference-snaps" target="_blank" rel="noreferrer"&gt;announcement&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="nøughty-linux-ubuntus-stability-meets-nixpkgs-freshness" class="relative group"&gt;Nøughty Linux: Ubuntu’s Stability Meets Nixpkgs’ Freshness &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#n%c3%b8ughty-linux-ubuntus-stability-meets-nixpkgs-freshness" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_a99f9dc0efe25bc0.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_dc0c16f39603b598.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_40086b0b15441a95.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_d2c99ab7d8057151.webp 1280w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1280"
height="720"
class="mx-auto my-0 rounded-md"
alt="opening slide for noughty linux talk"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_61b22107d4c49f5e.png" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_448ff89cbcd9feba.png 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_61b22107d4c49f5e.png 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/05_hu_fc9324d6dc71ff1c.png 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/05.png 1280w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://discourse.ubuntu.com/t/noughty-linux-ubuntus-stability-meets-nixpkgs-freshness/69962" target="_blank" rel="noreferrer"&gt;talk&lt;/a&gt; was a bit of a guilty pleasure for me! Delivered by &lt;a href="https://wimpysworld.com/" target="_blank" rel="noreferrer"&gt;Martin Wimpress (wimpy)&lt;/a&gt;, the audience were shown how they could take a stock Ubuntu Server deployment, and use a collection of scripts to layer a cutting-edge GUI stack on top using &lt;a href="https://github.com/NixOS/nixpkgs" target="_blank" rel="noreferrer"&gt;Nixpkgs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Wimpy outlined his motivation as wanting to rely upon the stable kernel and hardware support offered by Ubuntu, but wanting to be more experimental with his desktop environment and utilities - preferring a tiling window management experience.&lt;/p&gt;
&lt;p&gt;Having spent some years on NixOS, Wimpy was recently required to run a security &amp;ldquo;agent&amp;rdquo; for work, which was very difficult to enable on NixOS, but worked out of the box on Ubuntu. Recognising the need to make the switch, he was reluctant to move away from the workflow he&amp;rsquo;d built so much muscle-memory around - and so &lt;a href="https://noughtylinux.org/" target="_blank" rel="noreferrer"&gt;Nøughty Linux&lt;/a&gt; was born!&lt;/p&gt;
&lt;p&gt;Nøughty Linux is not a Linux distribution, rather a set of configurations for an Ubuntu Server machine. It utilises &lt;a href="https://github.com/soupglasses/nix-system-graphics" target="_blank" rel="noreferrer"&gt;&lt;code&gt;nix-system-graphics&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/numtide/system-manager" target="_blank" rel="noreferrer"&gt;&lt;code&gt;system-manager&lt;/code&gt;&lt;/a&gt; and is actually &lt;em&gt;very&lt;/em&gt; similar to a configuration I ran in my own &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;nixos-config&lt;/a&gt; repository for my laptop for a while - though Wimpy has chased down significantly more of the papercuts than I did!&lt;/p&gt;
&lt;h2 id="are-we-stuck-with-the-same-desktop-ux-forever" class="relative group"&gt;Are we stuck with the same Desktop UX forever? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#are-we-stuck-with-the-same-desktop-ux-forever" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_9509dcaa896a87c.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_cd096a2b6be3a7cf.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_e7aaa31012bfd7fe.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_8caa86dfbc373634.webp 1280w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1280"
height="720"
class="mx-auto my-0 rounded-md"
alt="opening slide for desktop ux talk"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_652830ca100ce6c5.png" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_942ca798315d8db4.png 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_652830ca100ce6c5.png 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/06_hu_63850a6cf7ed9e47.png 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/06.png 1280w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jenson.org/" target="_blank" rel="noreferrer"&gt;Scott Jenson&lt;/a&gt; delivered an incredibly engaging &lt;a href="https://discourse.ubuntu.com/t/are-we-stuck-with-the-same-desktop-ux-forever/67253" target="_blank" rel="noreferrer"&gt;talk&lt;/a&gt; in which he posited that desktop user experience has somewhat stagnated, and worse that many of the patterns we&amp;rsquo;ve become used to on the desktop are antiquated and unergonomic.&lt;/p&gt;
&lt;p&gt;The crux of the talk was to focus on user &lt;em&gt;experience&lt;/em&gt;, rather than user &lt;em&gt;interfaces&lt;/em&gt; - challenging developers to think about how people learn, and how desktops could benefit more from design affordances by rethinking some critical elements such as window management or text editing.&lt;/p&gt;
&lt;p&gt;Using his years of experience at Apple, Symbian and Google, Scott delivered one of the most engaging conference talks I&amp;rsquo;ve seen, and I thoroughly recommend watching it on our YouTube channel!&lt;/p&gt;
&lt;h2 id="honorable-mentions" class="relative group"&gt;Honorable Mentions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#honorable-mentions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In addition to the talks above, it was a delight to meet &lt;a href="https://cs.ru.nl/~M.Schoolderman/" target="_blank" rel="noreferrer"&gt;Mark Schoolderman&lt;/a&gt; from the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt; in-person, who led the work on &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;sudo-rs&lt;/code&gt;&lt;/a&gt; as part of our &amp;ldquo;Oxidising Ubuntu&amp;rdquo; story, and interesting to hear about the value the project derived from Ubuntu&amp;rsquo;s &lt;a href="https://documentation.ubuntu.com/project/MIR/main-inclusion-review/" target="_blank" rel="noreferrer"&gt;Main Inclusion Review&lt;/a&gt; process as part of landing &lt;code&gt;sudo-rs&lt;/code&gt; in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Equally, I was delighted that &lt;a href="https://github.com/kaplun" target="_blank" rel="noreferrer"&gt;Samuele Kaplun&lt;/a&gt; from &lt;a href="https://proton.me/" target="_blank" rel="noreferrer"&gt;Proton&lt;/a&gt; could join us to talk about the work we&amp;rsquo;ve been doing together on bringing first-class Snap packages for &lt;a href="https://proton.me/mail" target="_blank" rel="noreferrer"&gt;Proton Mail&lt;/a&gt;, &lt;a href="https://protonvpn.com/?ref=pme_lp_b2c_proton_submenu" target="_blank" rel="noreferrer"&gt;Proton VPN&lt;/a&gt;, &lt;a href="https://proton.me/pass" target="_blank" rel="noreferrer"&gt;Proton Pass&lt;/a&gt; and &lt;a href="https://proton.me/authenticator" target="_blank" rel="noreferrer"&gt;Proton Authenticator&lt;/a&gt; to the &lt;a href="https://snapcraft.io/publisher/proton-ag" target="_blank" rel="noreferrer"&gt;Snap store&lt;/a&gt;, and their reasons for choosing Snaps, adventures with &lt;a href="https://snapcraft.io/docs/snap-confinement" target="_blank" rel="noreferrer"&gt;confinement&lt;/a&gt;, and more.&lt;/p&gt;
&lt;p&gt;I was delighted to see &lt;a href="https://www.craigloewen.com/" target="_blank" rel="noreferrer"&gt;Craig Loewen&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/clintrutkas/" target="_blank" rel="noreferrer"&gt;Clint Rutkas&lt;/a&gt; present on their &lt;a href="https://discourse.ubuntu.com/t/engineering-wsl-in-the-open-a-deep-dive-into-open-sourcing-wsl-at-microsoft/67022" target="_blank" rel="noreferrer"&gt;journey&lt;/a&gt; open sourcing the &lt;a href="https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux" target="_blank" rel="noreferrer"&gt;Windows Subsystem For Linux (WSL)&lt;/a&gt;, which represents a growing proportion of Ubuntu users, and a key bridge to open source development for many.&lt;/p&gt;
&lt;p&gt;Finally, thank you to &lt;a href="https://github.com/utkarsh2102" target="_blank" rel="noreferrer"&gt;Utkarsh&lt;/a&gt; for this wonderful slide as part of his talk on Ubuntu Snapshot Releases:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.jpg"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_33528e98771d0bee.webp 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_3200c3c97ea3fdc8.webp 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_bdcdf80fde7402d2.webp 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_dee895df12e2c378.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1445"
height="813"
class="mx-auto my-0 rounded-md"
alt="a slide depicting my profile picture, but with laser eyes and the title &amp;ldquo;violence&amp;rdquo;"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_25071a7fcfa9dc2a.jpg" srcset="https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_aa0d0f0d8dcb5517.jpg 330w,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_25071a7fcfa9dc2a.jpg 660w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_f6f1d1cda3bd05f2.jpg 1024w
,https://jnsgr.uk/2025/11/ubuntu-summit-25/01_hu_41b986c0c48d6e4d.jpg 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="conclusion" class="relative group"&gt;Conclusion &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#conclusion" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Overall, I found the Ubuntu Summit 25.10 a really enjoyable event, with talks that were uniformly high in quality, charisma and creativity. I&amp;rsquo;m pleased that Canonical has broadened the Summit&amp;rsquo;s reach and I hope it continues to serve as a platform to showcase the very best open source innovation.&lt;/p&gt;
&lt;p&gt;Until next time!&lt;/p&gt;</description></item><item><title>Ubuntu Engineering in 2025: A Retrospective</title><link>https://jnsgr.uk/2025/10/ubuntu-25/</link><pubDate>Thu, 09 Oct 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/10/ubuntu-25/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/ubuntu-25-10-a-retrospective/69127" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="ubuntu-2510-a-retrospective" class="relative group"&gt;Ubuntu 25.10: A Retrospective &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ubuntu-2510-a-retrospective" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In February this year, I published &lt;a href="https://discourse.ubuntu.com/t/engineering-ubuntu-for-the-next-20-years/55000" target="_blank" rel="noreferrer"&gt;Engineering Ubuntu For The Next 20 Years&lt;/a&gt;, which was something of a manifesto I pledged to enact in the design, build and release of Ubuntu. This week, we released Ubuntu 25.10 Questing Quokka, which was the first full engineering cycle under this new manifesto, and it seems like a good time to reflect on what we achieved in each category, as well as highlight some of the more impactful changes that have just landed in Ubuntu.&lt;/p&gt;
&lt;p&gt;In that first article, I outline four themes for Ubuntu Engineering at Canonical to focus on: Communication, Automation, Process and Modernisation.&lt;/p&gt;
&lt;h3 id="communication" class="relative group"&gt;Communication &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#communication" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;A notable improvement throughout this engineering cycle has been the frequency with which the teams at Canonical have written about their work, often in some detail. Many of these posts can be found under the &lt;a href="https://discourse.ubuntu.com/tag/blog" target="_blank" rel="noreferrer"&gt;blog tag&lt;/a&gt;, which had never been used until around six months ago, and now sees a couple of new posts per week outlining the work people are doing toward these themes.&lt;/p&gt;
&lt;p&gt;I stated that I consider documentation a key part of our communication strategy, and this last six months has seen some of the most substantial changes to Ubuntu documentation in many years. The &lt;a href="https://documentation.ubuntu.com/project/" target="_blank" rel="noreferrer"&gt;Ubuntu Project Docs&lt;/a&gt; was a project started in May 2025, and is quickly becoming the single documentation hub that a current or potential Ubuntu contributor needs to understand how, why and when to do their job. Similarly, the &lt;a href="https://documentation.ubuntu.com/ubuntu-for-developers/" target="_blank" rel="noreferrer"&gt;Ubuntu for Developers&lt;/a&gt; was created to illuminate a path for developers across numerous languages on Ubuntu.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important for us to celebrate such efforts, but also to remember that this is only the start! In order for these efforts to remain useful, both our internal teams and our community must continue to engage with these efforts - adding, refining and pruning content as necessary. As the sun-setting of wiki.ubuntu.com approaches, it&amp;rsquo;s imperative that these new documentation sites continue to get the attention they need.&lt;/p&gt;
&lt;p&gt;Lots of the changes we&amp;rsquo;ve made in the last cycle have attracted attention from online blogs, news outlets, youtubers, etc. Part of the challenge with such changes is &amp;ldquo;owning the narrative&amp;rdquo; and ensuring that legitimate concerns are heard (and taken into account), but also that there are appropriate responses to uncertainty, without getting drawn into unproductive discussions.&lt;/p&gt;
&lt;p&gt;Finally, the transition to &lt;a href="https://ubuntu.com/community/docs/communications/matrix" target="_blank" rel="noreferrer"&gt;Matrix&lt;/a&gt; as the default synchronous communication means for the project has, in my opinion, made it easier than ever to get in touch with our community of experts - whether it be for support, or to start a journey for contribution to Ubuntu.&lt;/p&gt;
&lt;h3 id="automation" class="relative group"&gt;Automation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The largest item we took on here was in pursuit of the &lt;a href="https://discourse.ubuntu.com/t/61876" target="_blank" rel="noreferrer"&gt;monthly snapshot releases&lt;/a&gt;. This went much better than I expected, and to some extent covers off the &amp;ldquo;Process&amp;rdquo; theme as well as &amp;ldquo;Automation&amp;rdquo;, but through a combination of studying our process and whittling it down as lean as we could, and beginning to automate more of the process, the team were able to release four snapshot releases before the 25.10 Beta.&lt;/p&gt;
&lt;p&gt;The scale of the automation efforts was relatively limited this cycle, but the automation of release testing has really accelerated in the past few months. The vast majority of the &lt;a href="https://github.com/canonical/ubuntu-gui-testing/tree/main/tests" target="_blank" rel="noreferrer"&gt;test cases&lt;/a&gt; that qualify an Ubuntu Desktop ISO for release are now fully automated, and the &lt;a href="https://github.com/canonical/yarf" target="_blank" rel="noreferrer"&gt;same framework&lt;/a&gt; that makes this possible was also used to develop a suite of tests for &lt;a href="https://discourse.ubuntu.com/t/tpm-fde-progress-for-ubuntu-25-10/65146" target="_blank" rel="noreferrer"&gt;TPM FDE&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Work was also done on our &lt;a href="https://discourse.ubuntu.com/t/crafting-your-software/64809" target="_blank" rel="noreferrer"&gt;craft tools&lt;/a&gt; to better the experience with the &lt;code&gt;test&lt;/code&gt; sub-command of build tools like &lt;code&gt;snapcraft&lt;/code&gt;, &lt;code&gt;rockcraft&lt;/code&gt; and &lt;code&gt;charmcraft&lt;/code&gt; - all of which will have a trickle-down effect on the upcoming &lt;code&gt;debcraft&lt;/code&gt;, and make it trivial to include many new kinds of tests in our packaging workflows.&lt;/p&gt;
&lt;p&gt;Behind the scenes, every team in Ubuntu Engineering at Canonical has been writing charms that make the underlying infrastructure behind Ubuntu more portable, resilient and scalable. This includes services like &lt;a href="https://manpages.ubuntu.com/" target="_blank" rel="noreferrer"&gt;Ubuntu Manpages&lt;/a&gt;, &lt;a href="https://autopkgtest.ubuntu.com/" target="_blank" rel="noreferrer"&gt;autopkgtest&lt;/a&gt;, &lt;a href="https://errors.ubuntu.com/" target="_blank" rel="noreferrer"&gt;error-tracker&lt;/a&gt;, and a staging deployment of &lt;a href="https://temporal.io" target="_blank" rel="noreferrer"&gt;Temporal&lt;/a&gt; to enable the next phase of our release automation.&lt;/p&gt;
&lt;h3 id="process" class="relative group"&gt;Process &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#process" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This item was probably where the least concrete progress was made, though I probably could have predicted that. Many of the processes in the Ubuntu project serve to ensure that we ship resilient software, and don&amp;rsquo;t break users - so changing them in a hurry is not generally a good idea.&lt;/p&gt;
&lt;p&gt;That said, there was some good progress on the &lt;a href="https://documentation.ubuntu.com/project/MIR/main-inclusion-review/#mir-process-overview" target="_blank" rel="noreferrer"&gt;Main Inclusion Review&lt;/a&gt; (MIR) process, whose team documentation was moved into the &lt;a href="https://documentation.ubuntu.com/project" target="_blank" rel="noreferrer"&gt;Ubuntu Project Docs&lt;/a&gt; after a thorough review, and the &lt;a href="https://documentation.ubuntu.com/project/how-ubuntu-is-made/processes/stable-release-updates/" target="_blank" rel="noreferrer"&gt;Stable Release Updates&lt;/a&gt; (SRU) team are in the process of the same transition. Moving and re-reviewing the documentation is essentially the first step of the process improvement I was seeking: understanding where we are!&lt;/p&gt;
&lt;p&gt;Internally, we&amp;rsquo;ve been piloting a new process for onboarding &lt;a href="https://documentation.ubuntu.com/project/who-makes-ubuntu/developers/dmb-index/#the-uploader-s-journey" target="_blank" rel="noreferrer"&gt;Ubuntu Developers&lt;/a&gt; that sees engineers start by working toward gaining upload rights for a single package, but has a complete curriculum that can take them through to Core Developer status. Details of this should be released in the coming months, outlining a clear and well-trodden journey for new contributors. Much of this material already existed, but the team have worked on polishing it, and making it clearer how the process work from end to end.&lt;/p&gt;
&lt;p&gt;The next step for each of these processes is measurement. We&amp;rsquo;ve begun instrumenting these processes to understand where the most time is spent so we can use that information to guide improvements and streamline processes in future cycles, and even set &lt;a href="https://en.wikipedia.org/wiki/Service-level_objective" target="_blank" rel="noreferrer"&gt;Service Level Objectives&lt;/a&gt; (SLOs) against those timelines.&lt;/p&gt;
&lt;h3 id="modernisation" class="relative group"&gt;Modernisation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#modernisation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Much of what I’ve already described could be considered modernisation, but from a technical standpoint the most obvious candidate here was the &amp;ldquo;&lt;a href="https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995" target="_blank" rel="noreferrer"&gt;Oxidising Ubuntu&lt;/a&gt;&amp;rdquo; effort, which has seen us replace numerous core utilities in Ubuntu 25.10 with modern Rust rewrites.&lt;/p&gt;
&lt;p&gt;We began this effort in close collaboration with the &lt;a href="https://uutils.github.io/" target="_blank" rel="noreferrer"&gt;uutils&lt;/a&gt; project and the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt;. The former is the maintainer of a Rust &lt;code&gt;coreutils&lt;/code&gt; rewrite, and the latter the maintainer of &lt;code&gt;sudo-rs&lt;/code&gt;, which we &lt;a href="https://discourse.ubuntu.com/t/adopting-sudo-rs-by-default-in-ubuntu-25-10/60583" target="_blank" rel="noreferrer"&gt;made the default&lt;/a&gt; in 25.10. The technical impact of these changes in defaults will only truly be known once Ubuntu 25.10 is &amp;ldquo;out there&amp;rdquo;, but I&amp;rsquo;m pleased with how we approached the shift. In both cases, we contacted the upstreams in good time to ascertain their view on their projects&amp;rsquo; readiness, then agreed funding to ensure they had the financial support they needed to land changes in support of Ubuntu, and then worked closely with them throughout the cycle to solve various performance and implementation issues we discovered along the way.&lt;/p&gt;
&lt;p&gt;As it stands today, &lt;code&gt;sudo-rs&lt;/code&gt; is the default &lt;code&gt;sudo&lt;/code&gt; implementation on Ubuntu 25.10, and uutils&amp;rsquo; &lt;code&gt;coreutils&lt;/code&gt; has &lt;em&gt;mostly&lt;/em&gt; replaced the GNU implementation, with a &lt;a href="https://git.launchpad.net/ubuntu/&amp;#43;source/coreutils-from/tree/debian/coreutils-from-uutils.links" target="_blank" rel="noreferrer"&gt;few exceptions&lt;/a&gt;, many of which will be resolved by releases in the coming weeks. These diversions back to the existing implementations demonstrate that stability and resilience are more important than &amp;ldquo;hype&amp;rdquo; in our approach: I expect us to have completed the migration during the next cycle, but not before the tools are ready.&lt;/p&gt;
&lt;p&gt;Following the &lt;a href="https://discourse.ubuntu.com/t/spec-switch-to-dracut/54776" target="_blank" rel="noreferrer"&gt;&amp;ldquo;Switch to Dracut&amp;rdquo; specification&lt;/a&gt;, Ubuntu Desktop 25.10 will use &lt;a href="https://dracut-ng.github.io/dracut-ng/" target="_blank" rel="noreferrer"&gt;Dracut&lt;/a&gt; as its default initrd infrastructure (replacing initramfs-tools). Dracut will use systemd in the initrd and supports new features like Bluetooth and NVMe over Fabric (NVM-oF) support. Ubuntu Server installations will continue using &lt;code&gt;initramfs-tools&lt;/code&gt; until &lt;a href="https://bugs.launchpad.net/ubuntu/&amp;#43;source/dracut/&amp;#43;bug/2125790" target="_blank" rel="noreferrer"&gt;remaining hooks are ported&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For each of these changes (&lt;code&gt;coreutils&lt;/code&gt;, &lt;code&gt;sudo-rs&lt;/code&gt; and &lt;code&gt;dracut&lt;/code&gt;) the previous implementations will remain supported for now, with well-documented instructions on the reversion of each change for those who run into unavoidable issues - though we expect this to be a very small number of cases.&lt;/p&gt;
&lt;h2 id="whats-next" class="relative group"&gt;What&amp;rsquo;s Next? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#whats-next" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Well&amp;hellip; more of the same! We intend to carry on with the increased cadence of written updates, so keep an eye out for those.&lt;/p&gt;
&lt;p&gt;We have some exciting announcements to make over the coming weeks, including support for more modern micro-architectural variants (like &lt;code&gt;amd64v3&lt;/code&gt;), better system-wide handling of revoked TLS certificates, updates on our Debcraft package for a more modern packaging experience and an effort to update many of our tools from &amp;ldquo;behind the scenes&amp;rdquo; using a combination of Rust and Go.&lt;/p&gt;
&lt;p&gt;My final words are to thank all of those who have driven these efforts. I&amp;rsquo;ll omit the long list of names, but there have been countless examples of people stepping up substantially to deliver these efforts - without whom we&amp;rsquo;d have made a lot less progress.&lt;/p&gt;
&lt;p&gt;Well done, and let&amp;rsquo;s make Resolute Raccoon an LTS to remember - for all the &lt;em&gt;right&lt;/em&gt; reasons!&lt;/p&gt;</description></item><item><title>The Immutable Linux Paradox</title><link>https://jnsgr.uk/2025/09/immutable-linux-paradox/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/09/immutable-linux-paradox/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/the-immutable-linux-paradox/66456" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Immutable Linux distributions have been around since the early 2000s, but adoption has significantly accelerated in the last five years. Mainstream operating systems (OSes) such as &lt;a href="https://www.apple.com/macos" target="_blank" rel="noreferrer"&gt;macOS&lt;/a&gt;, &lt;a href="https://www.android.com/intl/en_uk/" target="_blank" rel="noreferrer"&gt;Android&lt;/a&gt;, &lt;a href="https://chromeos.google/intl/en_uk/" target="_blank" rel="noreferrer"&gt;ChromeOS&lt;/a&gt; and &lt;a href="https://www.apple.com/ios" target="_blank" rel="noreferrer"&gt;iOS&lt;/a&gt; have all embraced similar principles, reflecting a growing trend toward resilience, longevity, and maintainability as core ideals of OS development.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ubuntu.com/core" target="_blank" rel="noreferrer"&gt;Ubuntu Core&lt;/a&gt; has been at the forefront of this movement for IoT, appliances and edge deployments, with work ongoing to release a &amp;ldquo;Core Desktop&amp;rdquo; experience. Other projects such as &lt;a href="https://nixos.org/" target="_blank" rel="noreferrer"&gt;NixOS&lt;/a&gt;, &lt;a href="https://fedoraproject.org/atomic-desktops/silverblue/" target="_blank" rel="noreferrer"&gt;Fedora Silverblue&lt;/a&gt; and &lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_image_mode_for_rhel_to_build_deploy_and_manage_operating_systems/introducing-image-mode-for-rhel_using-image-mode-for-rhel-to-build-deploy-and-manage-operating-systems" target="_blank" rel="noreferrer"&gt;Red Hat image mode&lt;/a&gt; are gaining adoption, alongside more specialised immutable distributions such as &lt;a href="https://store.steampowered.com/steamos" target="_blank" rel="noreferrer"&gt;SteamOS&lt;/a&gt; and &lt;a href="https://www.talos.dev/" target="_blank" rel="noreferrer"&gt;Talos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post explores how different Linux distributions achieve immutability, the trade-offs, and why you should give it a try!&lt;/p&gt;
&lt;h2 id="what-is-an-immutable-linux-distribution" class="relative group"&gt;What is an immutable Linux distribution? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-is-an-immutable-linux-distribution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The key principle of an immutable OS is that the core system is unchangeable at runtime.&lt;/p&gt;
&lt;p&gt;Every OS installation has at least one filesystem that stores system software, user software, and user data. Immutable OSes must cleanly separate &amp;ldquo;system&amp;rdquo; and &amp;ldquo;user&amp;rdquo; software and data, such that regular user interactions cannot compromise the integrity of the OS.&lt;/p&gt;
&lt;p&gt;Immutable deployments are often separated into three layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Base OS&lt;/strong&gt; - immutable core, updated only through controlled mechanisms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applications&lt;/strong&gt; - user applications, often delivered in containerised formats such as &lt;a href="https://snapcraft.io/docs" target="_blank" rel="noreferrer"&gt;Snap&lt;/a&gt;, &lt;a href="https://flatpak.org/" target="_blank" rel="noreferrer"&gt;Flatpak&lt;/a&gt;, &lt;a href="https://appimage.org/" target="_blank" rel="noreferrer"&gt;AppImage&lt;/a&gt;, &lt;a href="https://github.com/Containerpak/cpak" target="_blank" rel="noreferrer"&gt;cpak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User data&lt;/strong&gt; - writable and persistent, independent of OS updates or rollbacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Immutable systems use atomic, transactional updates meaning updates are applied as unitary, indivisible operations that either wholly succeed, or fail completely and trigger an automated roll-back to a previous known-good state.&lt;/p&gt;
&lt;h2 id="why-immutability" class="relative group"&gt;Why immutability? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#why-immutability" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The major benefit of an immutable OS is &lt;em&gt;resilience&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Immutable OSes make it easier to reproduce systems with a given configuration, which is particularly useful in scale-out use-cases such as cloud or IoT.&lt;/p&gt;
&lt;p&gt;Traditional package managers often maintain a database of installed packages, consisting of those included in the base OS, and those explicitly installed by the user, and their dependencies. The package manager &lt;em&gt;doesn&amp;rsquo;t&lt;/em&gt; have a clear notion of which packages make up the &amp;ldquo;core system&amp;rdquo;, and which are &amp;ldquo;optional&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;This can cause &amp;ldquo;configuration drift&amp;rdquo;, which occurs over time - a package could be explicitly installed by a user, used for a while and then removed, but without removing its dependencies. This leaves the system in a different, and somewhat undefined, state than it was in prior to the package being installed.&lt;/p&gt;
&lt;p&gt;Often the traditional notion of OS security is improved with immutable OS concepts too. In most implementations, the core OS files are mounted read-only such that users &lt;em&gt;cannot&lt;/em&gt; make changes - which also raises the bar for malicious modifications. When combined with technologies such as secure boot and confinement, immutable OSes can dramatically reduce the attack surface of a machine.&lt;/p&gt;
&lt;p&gt;Finally, convenience! Immutable OSes often include recovery or rollback features, which enable users to &amp;ldquo;undo&amp;rdquo; a bad system change, reverting to a previous known-good revision.&lt;/p&gt;
&lt;h2 id="the-immutability-paradox" class="relative group"&gt;The immutability paradox &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-immutability-paradox" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In reality, no general-purpose operating system is fully immutable.&lt;/p&gt;
&lt;p&gt;There is always persistent, user-writable storage - because without this there would be a huge limitation on usefulness! Similarly, how can a system be truly immutable, yet still support software updates?&lt;/p&gt;
&lt;p&gt;The terms &amp;ldquo;immutable&amp;rdquo; and &amp;ldquo;stateless&amp;rdquo; are often conflated - when in reality neither are excellent terms for describing what has become widely known as &amp;ldquo;immutable OSes&amp;rdquo;. This was explored in some depth in &lt;a href="https://blog.verbum.org/2020/08/22/immutable-%E2%86%92-reprovisionable-anti-hysteresis/" target="_blank" rel="noreferrer"&gt;this blog post&lt;/a&gt; which proposes terms such as &amp;ldquo;image based&amp;rdquo; and &amp;ldquo;fully managed&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;By definition, changes to configuration, the installation of applications and the use of temporary runtime storage are all violations of immutability, and thus immutability concepts must be applied in some sort of layering system.&lt;/p&gt;
&lt;p&gt;Striking the balance between &amp;rsquo;true&amp;rsquo; immutability and user experience is one of the hardest challenges in immutable OS design. A system that is too rigid can be difficult to manage and use, appearing inflexible to end users.&lt;/p&gt;
&lt;p&gt;A common pattern is to run an immutable desktop OS and use virtualisation or containerisation technologies (e.g. &lt;a href="https://canonical.com/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt;, &lt;a href="https://podman.io/" target="_blank" rel="noreferrer"&gt;Podman&lt;/a&gt;, &lt;a href="https://containertoolbx.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;toolbx&lt;/code&gt;&lt;/a&gt; &lt;a href="https://distrobox.it/" target="_blank" rel="noreferrer"&gt;Distrobox&lt;/a&gt;) to create mutable environments in which to work on projects. This results in a very stable workstation that benefits from immutability, with the flexibility of a traditional mutable OS where it&amp;rsquo;s needed.&lt;/p&gt;
&lt;h2 id="approaches-to-immutability" class="relative group"&gt;Approaches to immutability &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#approaches-to-immutability" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Different distributions solve the immutability challenge in different ways. In this section we&amp;rsquo;ll explore the four different approaches of &lt;code&gt;ostree&lt;/code&gt; based distributions, &lt;code&gt;bootc&lt;/code&gt; based distributions, NixOS and Ubuntu Core.&lt;/p&gt;
&lt;h3 id="fedora-silverblue--coreos--endlessos-ostree" class="relative group"&gt;Fedora Silverblue / CoreOS / EndlessOS (&lt;code&gt;ostree&lt;/code&gt;) &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#fedora-silverblue--coreos--endlessos-ostree" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href="https://fedoraproject.org/atomic-desktops/silverblue/" target="_blank" rel="noreferrer"&gt;Fedora Silverblue&lt;/a&gt; and &lt;a href="https://fedoraproject.org/coreos/" target="_blank" rel="noreferrer"&gt;Fedora CoreOS&lt;/a&gt; are also popular choices for those exploring immutable OSes. The two share a lot of underlying technology with Silverblue targeting desktop use cases, and CoreOS targeting server deployments.&lt;/p&gt;
&lt;p&gt;Both are based on &lt;a href="https://ostreedev.github.io/ostree/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ostree&lt;/code&gt;&lt;/a&gt;, which provides:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tools that combine a &amp;lsquo;git-like&amp;rsquo; model for committing and downloading bootable filesystem trees, along with a layer for deploying them and managing the bootloader configuration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Silverblue and CoreOS actually rely on &lt;a href="https://coreos.github.io/rpm-ostree/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rpm-ostree&lt;/code&gt;&lt;/a&gt; , a &amp;ldquo;hybrid image/package manager&amp;rdquo; which combines RPM packaging technology with &lt;code&gt;ostree&lt;/code&gt; to manage deployments.&lt;/p&gt;
&lt;p&gt;The update mechanism involves switching the filesystem to track a different remote &amp;ldquo;ref&amp;rdquo;, which is analogous to a git &lt;a href="https://git-scm.com/book/ms/v2/Git-Internals-Git-References" target="_blank" rel="noreferrer"&gt;ref&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.endlessos.org/" target="_blank" rel="noreferrer"&gt;EndlessOS&lt;/a&gt; is based on &lt;a href="https://www.debian.org/" target="_blank" rel="noreferrer"&gt;Debian&lt;/a&gt;, but uses &lt;code&gt;ostree&lt;/code&gt; to achieve immutability. EndlessOS is a desktop experience designed more for the &amp;ldquo;average user&amp;rdquo; and focuses on providing a reliable system that works well in low-bandwidth or offline situations.&lt;/p&gt;
&lt;p&gt;Users often use Flatpak to install graphical user applications atop the immutable base, or a user-space package manager such as &lt;a href="https://brew.sh/" target="_blank" rel="noreferrer"&gt;brew&lt;/a&gt; for other utilities.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ostree&lt;/code&gt; based distributions also support &amp;ldquo;&lt;a href="https://docs.fedoraproject.org/en-US/fedora-silverblue/getting-started/#package-layering" target="_blank" rel="noreferrer"&gt;package layering&lt;/a&gt;&amp;rdquo; which enables adding packages to the base system without fetching a whole new filesystem ref, but does require the system to be rebooted before the package is persistently available. The documentation notes that this approach is to be used &amp;ldquo;sparingly&amp;rdquo;, and that users should prefer using Flatpak or &lt;a href="https://containertoolbx.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;toolbx&lt;/code&gt;&lt;/a&gt; to access additional packages.&lt;/p&gt;
&lt;h3 id="rhel-image-mode-bootc" class="relative group"&gt;RHEL &amp;ldquo;Image Mode&amp;rdquo; (&lt;code&gt;bootc&lt;/code&gt;) &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#rhel-image-mode-bootc" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;bootc&lt;/code&gt; based distributions use an alternate approach, packaging the base system into OCI containers (commonly referred to as Docker containers). Atomicity and transactionality are achieved by using container images to deliver the entire core system, and rebooting into a new revision.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_image_mode_for_rhel_to_build_deploy_and_manage_operating_systems/introducing-image-mode-for-rhel_using-image-mode-for-rhel-to-build-deploy-and-manage-operating-systems" target="_blank" rel="noreferrer"&gt;RHEL Image Mode&lt;/a&gt; uses &lt;a href="https://bootc-dev.github.io/bootc/intro.html" target="_blank" rel="noreferrer"&gt;&lt;code&gt;bootc&lt;/code&gt;&lt;/a&gt;. This technology capitalises on the success of OCI containers as a transport and delivery mechanism for software by packing an entire OS base image into a single container, including the kernel image.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;bootc&lt;/code&gt; project builds on &lt;a href="https://ostreedev.github.io/ostree/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ostree&lt;/code&gt;&lt;/a&gt; , but where &lt;code&gt;ostree&lt;/code&gt; never delivered an opinionated &amp;ldquo;install mechanism&amp;rdquo;, &lt;code&gt;bootc&lt;/code&gt; does. The contents of a &lt;code&gt;bootc&lt;/code&gt; image is an &lt;code&gt;ostree&lt;/code&gt; filesystem.&lt;/p&gt;
&lt;p&gt;Installing new system packages generally means building a new base image, downloading that image and rebooting into it with a command such as &lt;code&gt;bootc switch &amp;lt;image reference&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Users often use Flatpak to install graphical user applications atop the immutable base, or a user-space package manager such as &lt;a href="https://brew.sh/" target="_blank" rel="noreferrer"&gt;brew&lt;/a&gt; for other utilities.&lt;/p&gt;
&lt;h3 id="nixos" class="relative group"&gt;NixOS &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#nixos" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The Nix project first appeared in 2003. &lt;a href="https://nixos.org/" target="_blank" rel="noreferrer"&gt;NixOS&lt;/a&gt; is built on top of the Nix package manager, using it to manage both packages &lt;em&gt;and&lt;/em&gt; system configuration.&lt;/p&gt;
&lt;p&gt;NixOS defines the entire system through a declarative configuration, with changes applied via “generations” that can be rolled back. Changes to the system are applied by &amp;ldquo;rebuilding&amp;rdquo; the system configuration, which produces a new &amp;ldquo;generation&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Nix packages, and therefore NixOS, eschews the traditional &lt;a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard" target="_blank" rel="noreferrer"&gt;Unix FHS&lt;/a&gt; in favour of the Nix &amp;ldquo;store&amp;rdquo; and a collection of symlinks and wrappers managed by Nix. Only the Nix package manager can write to the store.&lt;/p&gt;
&lt;p&gt;The Nix store also (mostly) enables the building and switching of generations without a reboot. Updates are atomic: new generations must build completely before they can be activated. The &lt;a href="https://github.com/nix-community/home-manager" target="_blank" rel="noreferrer"&gt;&lt;code&gt;home-manager&lt;/code&gt;&lt;/a&gt; project extends these concepts to the user environment and dotfile management.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/nix-community/impermanence" target="_blank" rel="noreferrer"&gt;&lt;code&gt;impermanence&lt;/code&gt;&lt;/a&gt; project requires that every persistent directory is explicitly labelled, or else it&amp;rsquo;s deleted on every reboot, forcing the base OS to be rebuilt from the Nix store and system configuration - essentially &amp;ldquo;enforcing&amp;rdquo; core system immutability between reboots. This was inspired by blog posts &amp;ldquo;&lt;a href="https://grahamc.com/blog/erase-your-darlings/" target="_blank" rel="noreferrer"&gt;Erase Your Darlings&lt;/a&gt;&amp;rdquo; and &amp;ldquo;&lt;a href="https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/" target="_blank" rel="noreferrer"&gt;NixOS tmpfs as root&lt;/a&gt;&amp;rdquo;, which are worth a read, too!&lt;/p&gt;
&lt;h3 id="ubuntu-core" class="relative group"&gt;Ubuntu Core &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ubuntu-core" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Ubuntu Core achieves immutability by packaging every component (kernel, base system, applications) as Snaps.&lt;/p&gt;
&lt;p&gt;Snap &lt;a href="https://snapcraft.io/docs/snap-confinement" target="_blank" rel="noreferrer"&gt;confinement&lt;/a&gt; enforces isolation, and &lt;code&gt;snapd&lt;/code&gt; manages transactional updates and rollbacks. The system is designed for reliability, fleet management, and modular upgrades, making it well-suited for IoT and soon, desktop use.&lt;/p&gt;
&lt;p&gt;The key &lt;a href="https://documentation.ubuntu.com/core/explanation/core-elements/inside-ubuntu-core/" target="_blank" rel="noreferrer"&gt;components&lt;/a&gt; of an Ubuntu Core deployment are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gadget snap&lt;/strong&gt;: provides boot assets, including board specific binaries and data (bootloader, device tree, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel snap&lt;/strong&gt;: kernel image and associated modules, along with initial ramdisk for system initialisation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base snap&lt;/strong&gt;: execution environment in which applications run - includes &amp;ldquo;base&amp;rdquo; Ubuntu LTS packages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System snaps&lt;/strong&gt;: packages critical to system function such as &lt;a href="https://documentation.ubuntu.com/core/explanation/system-snaps/network-manager/" target="_blank" rel="noreferrer"&gt;Network-Manager&lt;/a&gt;, &lt;a href="https://documentation.ubuntu.com/core/explanation/system-snaps/bluetooth/" target="_blank" rel="noreferrer"&gt;bluez&lt;/a&gt;, pulseaudio, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application snaps&lt;/strong&gt;: define the functionality of the system, &lt;a href="https://snapcraft.io/docs/snap-confinement" target="_blank" rel="noreferrer"&gt;confined to a sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapd&lt;/strong&gt;: manages updates, rollbacks and snapshotting/restoring of user data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a Core Desktop installation, the desktop environment (GNOME, Plasma, etc.), display manager, login manager would all be delivered as &amp;ldquo;system snaps&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Snap &lt;a href="https://snapcraft.io/docs/snap-confinement" target="_blank" rel="noreferrer"&gt;confinement&lt;/a&gt; ensures packages cannot incorrectly interact with the underlying system or user data without explicit approval. In an Ubuntu Core deployment, this notion is extended to every component of the OS, offering a straightforward yet powerful way to manage risk for each system component.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Immutable Linux distributions approach the immutability paradox differently. We explored four different approaches here, and you can learn more about other approaches taken by the likes of &lt;a href="https://microos.opensuse.org/" target="_blank" rel="noreferrer"&gt;SUSE MicroOS&lt;/a&gt; (filesystem based immutability) and &lt;a href="https://vanillaos.org/" target="_blank" rel="noreferrer"&gt;Vanilla OS&lt;/a&gt; (uses &lt;a href="https://github.com/Vanilla-OS/ABRoot" target="_blank" rel="noreferrer"&gt;ABRoot&lt;/a&gt;) in this &lt;a href="https://dataswamp.org/~solene/2023-07-12-intro-to-immutable-os.html" target="_blank" rel="noreferrer"&gt;excellent blog post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ubuntu Core focuses on transactional packaging and a clean separation of system &amp;amp; user data. &lt;code&gt;bootc&lt;/code&gt;-based systems take a full image-based approach, while NixOS offers extreme flexibility through declarative configuration, but at the cost of complexity.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve yet to try an immutable Linux distribution I&amp;rsquo;d recommend giving it a go. Whether you prioritise simplicity, security or declarative control there&amp;rsquo;s almost certainly an immutable Linux distribution that fits your needs.&lt;/p&gt;</description></item><item><title>Crafting Your Software</title><link>https://jnsgr.uk/2025/07/crafting-your-software/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/07/crafting-your-software/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/crafting-your-software/64809" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Packaging software is notoriously tricky. Every language, framework, and build system has its quirks, and the variety of artifact types — from Debian packages to OCI images and cloud images — only adds to the complexity.&lt;/p&gt;
&lt;p&gt;Over the past decade, Canonical has been refining a family of tools called “crafts” to tame this complexity and make building, testing, and releasing software across ecosystems much simpler.&lt;/p&gt;
&lt;p&gt;The journey began on 23rd June 2015 when the first commit was made to &lt;a href="https://github.com/canonical/snapcraft" target="_blank" rel="noreferrer"&gt;Snapcraft&lt;/a&gt;, the tool used to build Snap packages. For years, Snapcraft was &lt;em&gt;the only&lt;/em&gt; craft in our portfolio, but in the last five years, we’ve generalized much of what we learned about building, testing, and releasing software into a number of &amp;ldquo;crafts&amp;rdquo; for building different artifact types.&lt;/p&gt;
&lt;p&gt;Last month, I &lt;a href="https://jnsgr.uk/2025/06/introducing-debcrafters/" target="_blank" rel="noreferrer"&gt;outlined&lt;/a&gt; Canonical&amp;rsquo;s plan to build &lt;code&gt;debcraft&lt;/code&gt; as a next-generation way to build Debian packages. In this post I&amp;rsquo;ll talk about what exactly &lt;em&gt;makes&lt;/em&gt; a craft, and why you should bother learning to use them.&lt;/p&gt;
&lt;h2 id="software-build-lifecycle" class="relative group"&gt;Software build lifecycle &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#software-build-lifecycle" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;At the heart of all our crafts is &lt;a href="https://canonical-craft-parts.readthedocs-hosted.com/latest/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;craft-parts&lt;/code&gt;&lt;/a&gt;, which according to the &lt;a href="https://canonical-craft-parts.readthedocs-hosted.com/latest/" target="_blank" rel="noreferrer"&gt;documentation&lt;/a&gt; &amp;ldquo;provides a mechanism to obtain data from different sources, process it in various ways, and prepare a filesystem sub-tree suitable for packaging&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Put simply, &lt;code&gt;craft-parts&lt;/code&gt; gives developers consistent tools to fetch, build, and prepare software from any ecosystem for packaging into various formats.&lt;/p&gt;
&lt;h3 id="lifecycle-stages" class="relative group"&gt;Lifecycle stages &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lifecycle-stages" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Every part has a minimum of four lifecycle stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PULL&lt;/code&gt;: source code or binary artifacts, along with dependencies are pulled from various sources&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BUILD&lt;/code&gt;: software is built automatically by a &lt;code&gt;plugin&lt;/code&gt;, or a set of custom steps defined by the developer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;STAGE&lt;/code&gt;: select outputs from the &lt;code&gt;BUILD&lt;/code&gt; phase are copied to a unified staging area for all parts&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PRIME&lt;/code&gt;: files from the staging area are copied to the priming area for use in the final artifact.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;STAGE&lt;/code&gt; and &lt;code&gt;PRIME&lt;/code&gt; steps are similar, except that &lt;code&gt;PRIME&lt;/code&gt; only happens after &lt;em&gt;all&lt;/em&gt; parts of the build are staged. Additionally, &lt;code&gt;STAGE&lt;/code&gt; provides the opportunity for parts to build/supply dependencies for other parts, but that might not be required in the final artifact.&lt;/p&gt;
&lt;h3 id="lifecycle-in-the-cli" class="relative group"&gt;Lifecycle in the CLI &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lifecycle-in-the-cli" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The lifecycle stages aren’t just in the build recipe, they’re also first-class citizens in each craft’s CLI, thanks to the &lt;a href="https://github.com/canonical/craft-cli" target="_blank" rel="noreferrer"&gt;craft-cli&lt;/a&gt; library. This ensures a consistent command-line experience across all craft tools.&lt;/p&gt;
&lt;p&gt;Take the following examples:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the full process including PULL, BUILD, STAGE, PRIME and then pack the final artifact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapcraft pack
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;charmcraft pack
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rockcraft pack
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the process up to the end of the STAGE step&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rockcraft stage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the process up to the PRIME step&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;charmcraft prime
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This design feature supports a smoother iterative development and debugging workflow for building and testing software artifacts.&lt;/p&gt;
&lt;h3 id="part-definition" class="relative group"&gt;Part definition &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#part-definition" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;parts&lt;/code&gt; of a build vary in complexity - some require two-three trivial lines, others require detailed specification of dependencies, build flags, environment variables and steps. The best way to understand the flexibility of this system is by looking at some examples.&lt;/p&gt;
&lt;p&gt;First, consider this (annotated) example from my &lt;a href="https://github.com/jnsgruk/icloudpd-snap/blob/beb2c7d2539547dfff5d4fd99687573d75597633/snap/snapcraft.yaml" target="_blank" rel="noreferrer"&gt;icloudpd snap&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;icloudpd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Use the &amp;#39;python&amp;#39; plugin to build the&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# software. This takes care of identifying&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Python package dependencies, building the wheel&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# and ensuring the project&amp;#39;s dependencies are staged&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# appropriately.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;python&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Fetch the project from Github, using the tag the matches&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# the version of the project.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://github.com/icloud-photos-downloader/icloud_photos_downloader&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source-tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v$SNAPCRAFT_PROJECT_VERSION&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This spec is everything required to fetch, build and stage the important bits required to run the software - in this case a Python wheel and its dependencies.&lt;/p&gt;
&lt;p&gt;Some projects might require more set up, perhaps an additional package is required or a specific version of a dependency is needed. Let&amp;rsquo;s take a look at a slightly more complex example taken from my &lt;a href="https://github.com/jnsgruk/zinc-k8s-operator/blob/5516be2c50e52b33742c674f266c8dfca55e6edf/rockcraft.yaml#L90C3-L100C20" target="_blank" rel="noreferrer"&gt;zinc-k8s-operator&lt;/a&gt; project:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kube-log-runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Use the &amp;#39;go&amp;#39; plugin to build the software.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Fetch the source code from Git at the &amp;#39;v0.17.0&amp;#39; tag.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://github.com/kubernetes/release&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source-tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v0.17.8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Change to the specified sub-directory for the build.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;source-subdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;images/build/go-runner&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Install the following snaps in the build environment.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-snaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;go/1.20/stable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Set the following environment variables in the build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# environment.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;GOOS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;linux&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This instructs &lt;code&gt;rockcraft&lt;/code&gt; to fetch a Git repository at a particular tag, change into the sub-directory &lt;code&gt;images/build/go-runner&lt;/code&gt;, then build the software using the &lt;code&gt;go&lt;/code&gt; plugin. It also specifies that the build required the &lt;code&gt;go&lt;/code&gt; snap from the &lt;code&gt;1.20/stable&lt;/code&gt; track, and sets some environment variables. That&amp;rsquo;s a lot of result for not much YAML. The end result of this is a single binary that&amp;rsquo;s &amp;ldquo;staged&amp;rdquo; and ready to be placed (in this case) into a &lt;a href="https://documentation.ubuntu.com/rockcraft/en/latest/explanation/rocks/" target="_blank" rel="noreferrer"&gt;Rock&lt;/a&gt; (Canonical&amp;rsquo;s name for OCI images).&lt;/p&gt;
&lt;p&gt;And the best part: this exact definition can be used in a &lt;code&gt;rockcraft.yaml&lt;/code&gt; when building a Rock, a &lt;code&gt;snapcraft.yaml&lt;/code&gt; when building a Snap, a &lt;code&gt;charmcraft.yaml&lt;/code&gt; when building a Charm, etc.&lt;/p&gt;
&lt;p&gt;The plugin system is extensive: at the time of writing there are &lt;a href="https://canonical-craft-parts.readthedocs-hosted.com/latest/reference/plugins/" target="_blank" rel="noreferrer"&gt;22 supported plugins&lt;/a&gt;, including &lt;code&gt;go&lt;/code&gt;, &lt;code&gt;maven&lt;/code&gt;, &lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;meson&lt;/code&gt; and more. If your build system of choice isn&amp;rsquo;t supported you can specify manual steps, giving you as much flexibility as you need:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;wasi-sdk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# There is no appropriate plugin for this part, so set&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# it to &amp;#39;nil&amp;#39; and we&amp;#39;ll specify our own build process&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# using &amp;#39;override-build&amp;#39;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# In this recipe, a previous part named &amp;#39;clang&amp;#39; is&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# required to build before attempting to build this&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# part.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;clang&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Specify any `apt` packages required in the build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# environment.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;wget&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Set some environment variables for the build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# environment.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;WASI_BRANCH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;15&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;WASI_RELEASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;15.0&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Define how to pull the software manually.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;override-pull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ROOT=https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$WASI_BRANCH
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; wget $ROOT/wasi-sysroot-$WASI_RELEASE.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; wget $ROOT/libclang_rt.builtins-wasm32-wasi-$WASI_RELEASE.tar.gz&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Define how to &amp;#39;build&amp;#39; the software manually&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;override-build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; craftctl default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; tar -C $CRAFT_STAGE -xf wasi-sysroot-$WASI_RELEASE.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; tar -C $CRAFT_STAGE/usr/lib/clang/* -xf libclang_rt.builtins-wasm32-wasi-$WASI_RELEASE.tar.gz&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Don&amp;#39;t prime anything for inclusion in the&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# final artifact; this part is only used for&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# another part&amp;#39;s build process.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;override-prime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Here, multiple stages of the lifecycle are overridden using &lt;code&gt;override-build&lt;/code&gt;, &lt;code&gt;override-pull&lt;/code&gt; and &lt;code&gt;override-stage&lt;/code&gt;, and we see &lt;code&gt;craftctl default&lt;/code&gt; for the first time, which instructs snapcraft to do whatever it would have done prior being overridden, but allows the developer to provide additional steps either before or after the default actions.&lt;/p&gt;
&lt;h2 id="isolated-build-environments" class="relative group"&gt;Isolated build environments &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#isolated-build-environments" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Even once a recipe for building software is defined, preparing machines to build software can be painful. Different major versions of the same OS might have varying package availability, your team might run completely different operating systems, and you might have limited image availability in your CI environment.&lt;/p&gt;
&lt;p&gt;The crafts solve this with build &amp;ldquo;backends&amp;rdquo;. Currently the crafts can use &lt;a href="https://canonical.com/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt; or &lt;a href="https://canonical.com/multipass" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt; to create isolated build environments, which makes it work nicely on Linux, macOS and Windows. This functionality is handled automatically by the crafts through the &lt;a href="https://canonical-craft-providers.readthedocs-hosted.com/en/latest/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;craft-providers&lt;/code&gt;&lt;/a&gt; library. The &lt;code&gt;craft-providers&lt;/code&gt; library provides uniform interfaces for creating build environments, configuring base images and executing builds.&lt;/p&gt;
&lt;p&gt;This means if you can run &lt;code&gt;snapcraft pack&lt;/code&gt; on your machine, your teammates can also run the same command without worrying about installing the right dependencies or polluting their machines with software and temporary files that might result from the build.&lt;/p&gt;
&lt;p&gt;One of my favourite features of this setup is the ability to drop into a shell inside the build environment automatically on a few different conditions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Drop into a shell if any part of the build fails.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapcraft pack --debug
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Drop into a shell after the build stage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rockcraft build --shell-after
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Drop to a shell in lieu of the prime stage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapcraft prime --shell
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This makes troubleshooting a failing build much simpler, while allowing the developer to maintain a clean separation between the build environment and their local machine. Should the build environment ever become polluted, or otherwise difficult to work with, you can always start from a clean slate with &lt;code&gt;snapcraft|rockcraft|charmcraft clean&lt;/code&gt;. Each build machine is constructed using a cached &lt;code&gt;build-base&lt;/code&gt;, which contains all the baseline packages required by the craft - so recreating the build environment for a specific package only requires that base to be cloned and augmented with project specific concerns - making the process faster.&lt;/p&gt;
&lt;h2 id="saving-space" class="relative group"&gt;Saving space &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#saving-space" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;When packaging any kind of software, a common concern is the size of the artifact. This might be because you&amp;rsquo;re building an OCI-image that is pulled thousands of times a day as part of a major SaaS deployment, or maybe it&amp;rsquo;s a Snap for an embedded device running &lt;a href="https://ubuntu.com/core" target="_blank" rel="noreferrer"&gt;Ubuntu Core&lt;/a&gt; with a limited flash. In the container world, &amp;ldquo;&lt;a href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noreferrer"&gt;distroless&lt;/a&gt;&amp;rdquo; became a popular way to solve this problem - essentially popularising the practice of shipping the barest minimum in a container image, eschewing much of the traditional Unix FHS.&lt;/p&gt;
&lt;p&gt;The parts mechanism has provided a way of &amp;ldquo;filtering&amp;rdquo; what is staged or primed into a final artifact from the start, which already gave developers autonomy to choose exactly what went into their builds.&lt;/p&gt;
&lt;p&gt;In addition to this, Canonical built &amp;ldquo;&lt;a href="https://documentation.ubuntu.com/chisel/en/latest/tutorial/getting-started/" target="_blank" rel="noreferrer"&gt;chisel&lt;/a&gt;&amp;rdquo;, which extends the distroless concept beyond containers to any kind of artifact. With &lt;code&gt;chisel&lt;/code&gt;, developers can slice out just the binaries, libraries, and configuration files they need from the Ubuntu Archive, enabling ultra-small packages without losing the robustness of Ubuntu’s ecosystem.&lt;/p&gt;
&lt;p&gt;We later launched &lt;a href="https://ubuntu.com/blog/chiseled-ubuntu-containers-openjre" target="_blank" rel="noreferrer"&gt;Chiseled JRE&lt;/a&gt; containers, and there are numerous other Rocks that utilise &lt;code&gt;chisel&lt;/code&gt; to provide a balance between shipping &lt;em&gt;tiny&lt;/em&gt; container images, while benefiting from the huge selection and quality of software in the Ubuntu Archive.&lt;/p&gt;
&lt;p&gt;Because the crafts are all built on a common platform, they now all have the ability to use &amp;ldquo;slices&amp;rdquo; from &lt;a href="https://github.com/canonical/chisel-releases" target="_blank" rel="noreferrer"&gt;chisel-releases&lt;/a&gt;, which enables a greater range of use-cases where artifact size is a primary concern. Slices are community maintained, and specified in simple to understand YAML files. You can see the list of available slices for the most recent Ubuntu release (25.04 Plucky Puffin) &lt;a href="https://github.com/canonical/chisel-releases/tree/ubuntu-25.04/slices" target="_blank" rel="noreferrer"&gt;on GitHub&lt;/a&gt;, and further documentation on slices and how they&amp;rsquo;re used in the &lt;a href="https://documentation.ubuntu.com/chisel/en/latest/explanation/mode-of-operation/" target="_blank" rel="noreferrer"&gt;Chisel docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="multi-architecture-builds" class="relative group"&gt;Multi-architecture builds &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#multi-architecture-builds" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ubuntu supports six major architectures at the time of writing (&lt;code&gt;amd64&lt;/code&gt;, &lt;code&gt;arm64&lt;/code&gt;, &lt;code&gt;armhf&lt;/code&gt;, &lt;code&gt;ppc64le&lt;/code&gt;, &lt;code&gt;s390x&lt;/code&gt;, &lt;code&gt;riscv64&lt;/code&gt;), and all of our crafts have first-class support for each of them. This functionality is provided primarily by the &lt;a href="https://github.com/canonical/craft-platforms" target="_blank" rel="noreferrer"&gt;craft-platforms&lt;/a&gt; library, and supported by the &lt;a href="https://github.com/canonical/craft-grammar" target="_blank" rel="noreferrer"&gt;craft-grammar&lt;/a&gt; library, which enables more complex definitions where builds may have different steps or requirements for different architectures.&lt;/p&gt;
&lt;p&gt;At a high-level, each artifact defines which architectures or platforms it is built &lt;em&gt;for&lt;/em&gt;, and which it is built &lt;em&gt;on&lt;/em&gt;. These are often, but not always, the same. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;amd64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is shorthand for &amp;ldquo;build the project on &lt;code&gt;amd64&lt;/code&gt; for &lt;code&gt;amd64&lt;/code&gt;&amp;rdquo;, but in a different example taken from a &lt;code&gt;charmcraft.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;amd64]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;all]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In this case the software is built on &lt;code&gt;amd64&lt;/code&gt;, but can run on any of the supported architectures - this can happen with all-Python wheels, &lt;code&gt;bash&lt;/code&gt; scripts and other interpreted languages which don&amp;rsquo;t link platform-specific libraries.&lt;/p&gt;
&lt;p&gt;In some build processes, the process or dependencies might differ per-architecture, which is where &lt;code&gt;craft-grammar&lt;/code&gt; comes in, enabling expressions such as (taken from &lt;a href="https://github.com/canonical/mesa-core22/blob/86060bf66e70d0f5d421fe818d61cdc0f18f9b31/snap/snapcraft.yaml#L265C3-L280C46" target="_blank" rel="noreferrer"&gt;GitHub&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;fit-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build-packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;wget&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;libjson-c-dev:${CRAFT_ARCH_BUILD_FOR}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;libcryptsetup-dev:${CRAFT_ARCH_BUILD_FOR}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Only use the following build packages when building for armhf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;to armhf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;binutils-arm-linux-gnueabi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;gcc-arm-linux-gnueabihf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;pkgconf:armhf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# When building for arm64, use a different set&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;to arm64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Dependencies for building *for* arm64 *on* amd64!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;on amd64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;gcc-aarch64-linux-gnu&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;pkgconf:arm64&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;on arm64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;gcc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Being able to define how to build on different architectures is only half of the battle, though. It&amp;rsquo;s one thing to define &lt;em&gt;how&lt;/em&gt; to build software on an &lt;code&gt;s390x&lt;/code&gt; machine but few developers have mainframes handy to actually &lt;em&gt;run&lt;/em&gt; the build! This is where the crafts&amp;rsquo; &lt;code&gt;remote-build&lt;/code&gt; capability comes in. The &lt;code&gt;remote-build&lt;/code&gt; command sends builds to Canonical&amp;rsquo;s build farm, which has native support for all of Ubuntu&amp;rsquo;s supported architectures. This is built into all of our crafts, and is triggered with &lt;code&gt;snapcraft remote-build&lt;/code&gt;, &lt;code&gt;rockcraft remote-build&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;Remote builds are a lifeline for publishers and communities who need to reach a larger audience, but can&amp;rsquo;t necessarily get their own build farm together. One example of this is &lt;a href="https://snapcrafters.org/" target="_blank" rel="noreferrer"&gt;Snapcrafters&lt;/a&gt;, a community-driven organisation that packages popular software as Snaps, who use &lt;code&gt;remote-build&lt;/code&gt; to drive multi-architecture builds from &lt;a href="https://github.com/snapcrafters/ci" target="_blank" rel="noreferrer"&gt;GitHub Actions&lt;/a&gt; as part of their publishing workflow (as seen &lt;a href="https://github.com/snapcrafters/helm/actions/runs/16166314558" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/snapcrafters/terraform/actions/runs/15607983328" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt; for example).&lt;/p&gt;
&lt;h2 id="unified-testing-framework" class="relative group"&gt;Unified testing framework &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#unified-testing-framework" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Testing is often the missing piece in build tools: developers are forced to rely on separate CI systems or ad-hoc scripts to verify their artifacts. To close this gap, we’re introducing a unified &lt;code&gt;test&lt;/code&gt; sub-command in the crafts.&lt;/p&gt;
&lt;p&gt;We recently added the &lt;code&gt;test&lt;/code&gt; sub-command to our crafts as an experimental (for now!) feature. Under the hood, &lt;code&gt;craft test&lt;/code&gt; will introduce a new lifecycle stage (&lt;code&gt;TEST&lt;/code&gt;). The enables packagers of any artifact type to specify how that artifact should be tested using a common framework across artifact types.&lt;/p&gt;
&lt;p&gt;Craft&amp;rsquo;s testing capability is powered by &lt;a href="https://github.com/canonical/spread" target="_blank" rel="noreferrer"&gt;spread&lt;/a&gt;, a convenient full-system task distribution system. Spread was built to simplify the massive number of integration tests run for the &lt;a href="https://github.com/canonical/snapd" target="_blank" rel="noreferrer"&gt;snapd&lt;/a&gt; project. It enables developers to specify tests in a simple language, and distribute them concurrently to any infrastructure they have available.&lt;/p&gt;
&lt;p&gt;This enables a developer to define tests and test infrastructure, and make it trivial to run the same tests locally, or remotely on cloud infrastructure. This can really speed up the development process - preventing developers from needing to wait on CI runners to spin up and test their code while iterating, they can run the very same integration tests locally using &lt;code&gt;craft test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are lots of fine details to &lt;code&gt;spread&lt;/code&gt;, and the team is working on artifact-specific abstractions for the crafts that will make testing &lt;em&gt;delightful&lt;/em&gt;. Imagine maintaining the Snap for a GUI application, and being able to enact the following workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pull the repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/some-gui-app/snap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; snap
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Make some changes, perhaps fix a bug&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vim snap/snapcraft.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Build the snap, and run the integration tests.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# These tests might include spinning up a headless&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# graphical VM, which actually installs and runs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# the snap, and interacts with it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapcraft &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;By integrating a common testing tool into the build tooling, the Starcraft team will be able to curate unique testing experiences for each kind of artifact. A snap might need a headless graphical VM, where an OCI-image simply requires a container runtime, but the &lt;code&gt;spread&lt;/code&gt; underpinnings allow a common test-definition language for each.&lt;/p&gt;
&lt;p&gt;There are a couple of examples of this in the wild already:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install charmcraft&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install --classic charmcraft
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Clone the repo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/jnsgruk/zinc-k8s-operator
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; zinc-k8s-operator
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# List the available tests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;charmcraft &lt;span class="nb"&gt;test&lt;/span&gt; --list lxd:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the integration testing suite, spinning up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# a small VM, inside which is a full Kubernetes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# instance, with a Juju controller bootstrapped.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# From here the charm will be deployed and tested to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ensure it&amp;#39;s integrations with the observability&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# stack and ingress charms are functioning correctly.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;charmcraft &lt;span class="nb"&gt;test&lt;/span&gt; -v lxd:ubuntu-24.04:tests/spread/observability-relations:juju_3_6
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The test above is powered by this &lt;a href="https://github.com/jnsgruk/zinc-k8s-operator/blob/main/spread.yaml" target="_blank" rel="noreferrer"&gt;spread.yaml&lt;/a&gt;, and this &lt;a href="https://github.com/jnsgruk/zinc-k8s-operator/blob/5516be2c50e52b33742c674f266c8dfca55e6edf/tests/spread/observability-relations/task.yaml" target="_blank" rel="noreferrer"&gt;test definition&lt;/a&gt;. With a little bit of &lt;a href="https://github.com/jnsgruk/zinc-k8s-operator/blob/5516be2c50e52b33742c674f266c8dfca55e6edf/.github/workflows/build-and-test.yaml#L80-L129" target="_blank" rel="noreferrer"&gt;work&lt;/a&gt;, it&amp;rsquo;s also possible to integrate &lt;code&gt;spread&lt;/code&gt; with GitHub matrix actions, giving you one GitHub job per &lt;code&gt;spread&lt;/code&gt; test - as seen &lt;a href="https://github.com/jnsgruk/zinc-k8s-operator/actions/runs/15638336939" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can see a similar example in our &lt;a href="https://github.com/canonical/postgresql-snap/tree/7e6ee6d3148c20309cc7067dc40520e208f862e5/spread/tests" target="_blank" rel="noreferrer"&gt;PostgreSQL Snap test suite&lt;/a&gt;, and we&amp;rsquo;ll be adding more and more of this kind of test across our Rock, Snap, Charm, Image and Deb portfolio.&lt;/p&gt;
&lt;p&gt;There is work to do, but I&amp;rsquo;m really excited about bringing a common testing framework to the crafts which should make the testing of all kinds of artifacts more consistent and easier to integrate across teams and systems.&lt;/p&gt;
&lt;h2 id="crafting-the-crafts" class="relative group"&gt;Crafting the crafts &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#crafting-the-crafts" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As the portfolio expanded from &lt;code&gt;snapcraft&lt;/code&gt;, to &lt;code&gt;charmcraft&lt;/code&gt;, to &lt;code&gt;rockcraft&lt;/code&gt; and is now expanding further to &lt;code&gt;debcraft&lt;/code&gt; and &lt;code&gt;imagecraft&lt;/code&gt; it was clear that we&amp;rsquo;d need a way to make it easy to build crafts for different artifacts, while being rigorous about consistency across the tools. A couple of years ago, the team built the &lt;a href="https://github.com/canonical/craft-application" target="_blank" rel="noreferrer"&gt;craft-application&lt;/a&gt; base library, which now forms the foundation of all our crafts.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;craft-application&lt;/code&gt; library combines many of the existing libraries that were in use across the crafts (listed below), providing a consistent base upon which artifact-specific logic can be built. The allows craft developers to spend less time implementing CLI details, &lt;code&gt;parts&lt;/code&gt; lifecycles and store interactions, and more time on curating a great experience for the maintainers of their artifact type.&lt;/p&gt;
&lt;p&gt;For the curious, &lt;code&gt;craft-application&lt;/code&gt; builds upon the following libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-archives" target="_blank" rel="noreferrer"&gt;craft-archives&lt;/a&gt;: manages interactions with &lt;code&gt;apt&lt;/code&gt; package repositories&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-cli" target="_blank" rel="noreferrer"&gt;craft-cli&lt;/a&gt;: CLI client builder that follows the Canonical&amp;rsquo;s CLI guidelines&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-parts" target="_blank" rel="noreferrer"&gt;craft-parts&lt;/a&gt;: obtain, process, and organize data sources into deployment-ready filesystems.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-grammar" target="_blank" rel="noreferrer"&gt;craft-grammar&lt;/a&gt;: advanced description grammar for parts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-providers" target="_blank" rel="noreferrer"&gt;craft-providers&lt;/a&gt;: interface for instantiating and executing builds for a variety of target environments&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-platforms" target="_blank" rel="noreferrer"&gt;craft-platforms&lt;/a&gt;: manage target platforms and architectures for craft applications&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-store" target="_blank" rel="noreferrer"&gt;craft-store&lt;/a&gt;: manage interactions with Canonical&amp;rsquo;s software stores&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/canonical/craft-artifacts" target="_blank" rel="noreferrer"&gt;craft-artifacts&lt;/a&gt;: pack artifacts for craft applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="examples-and-docs" class="relative group"&gt;Examples and docs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#examples-and-docs" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before I leave you, I wanted to reference a few &lt;code&gt;*craft.yaml&lt;/code&gt; examples, and link to the documentation for each of the crafts, where you&amp;rsquo;ll find the canonical (little c!) truth on each tool.&lt;/p&gt;
&lt;p&gt;You can find documentation for the crafts below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://documentation.ubuntu.com/snapcraft/stable/" target="_blank" rel="noreferrer"&gt;Snapcraft docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://canonical-charmcraft.readthedocs-hosted.com/stable/" target="_blank" rel="noreferrer"&gt;Charmcraft docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://documentation.ubuntu.com/rockcraft/en/stable/" target="_blank" rel="noreferrer"&gt;Rockcraft docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://canonical-robotics.readthedocs-hosted.com/en/latest/tutorials/" target="_blank" rel="noreferrer"&gt;Robotics / Snapcraft tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And some example recipes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Snap: &lt;code&gt;icloudpd&lt;/code&gt; - &lt;a href="https://github.com/jnsgruk/icloudpd-snap/blob/main/snap/snapcraft.yaml" target="_blank" rel="noreferrer"&gt;snapcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Snap: &lt;code&gt;parca-agent&lt;/code&gt; - &lt;a href="https://github.com/parca-dev/parca-agent/blob/main/snap/snapcraft.yaml" target="_blank" rel="noreferrer"&gt;snapcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Snap: &lt;code&gt;signal-desktop&lt;/code&gt; - &lt;a href="https://github.com/snapcrafters/signal-desktop/blob/candidate/snap/snapcraft.yaml" target="_blank" rel="noreferrer"&gt;snapcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Charm: &lt;code&gt;ubuntu-manpages-operator&lt;/code&gt; - &lt;a href="https://github.com/canonical/ubuntu-manpages-operator/blob/main/charmcraft.yaml" target="_blank" rel="noreferrer"&gt;charmcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rock: &lt;code&gt;grafana&lt;/code&gt; - &lt;a href="https://github.com/canonical/grafana-rock/blob/main/11.4.0/rockcraft.yaml" target="_blank" rel="noreferrer"&gt;rockcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rock: &lt;code&gt;temporal-server&lt;/code&gt; - &lt;a href="https://github.com/canonical/temporal-rocks/blob/main/temporal-server/1.23.1/rockcraft.yaml" target="_blank" rel="noreferrer"&gt;rockcraft.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The craft ecosystem provides developers with a rigorous, consistent and pleasant experience for building many kinds of artifacts. At the moment, we support Snaps, Rocks and Charms but we&amp;rsquo;re actively developing crafts for Debian packages, cloud images and more.The basic build process, &lt;code&gt;parts&lt;/code&gt; ecosystem and foundations of the crafts are &amp;ldquo;battle tested&amp;rdquo; at this point, and I&amp;rsquo;m excited to see how the experimental &lt;code&gt;craft test&lt;/code&gt; commands shape up across the crafts.&lt;/p&gt;
&lt;p&gt;One of the killer features for the crafts is the ability to reuse part definitions across different artifacts - which makes the pay off for learning the &lt;code&gt;parts&lt;/code&gt; language very high - it&amp;rsquo;s a skill you&amp;rsquo;ll be able to use to build Snaps, Rocks, Charms, VM Images and soon Debs!&lt;/p&gt;
&lt;p&gt;If I look at ecosystems like Debian, where tooling like &lt;code&gt;autopkgtest&lt;/code&gt; is the standard, I think &lt;code&gt;debcraft test&lt;/code&gt; will offer an intuitive entrypoint and encourage more testing, and the same is true of Snaps, both graphical and command-line.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all for now!&lt;/p&gt;</description></item><item><title>Introducing Debcrafters</title><link>https://jnsgr.uk/2025/06/introducing-debcrafters/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/06/introducing-debcrafters/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/63674" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Earlier this year, Canonical&amp;rsquo;s Ubuntu Engineering organisation gained a new team, seeded with some of our most prolific contributors to Ubuntu. Debcrafters is a new team dedicated to the maintenance of the Ubuntu Archive.&lt;/p&gt;
&lt;p&gt;The team&amp;rsquo;s primary goal is to maintain the health of the Ubuntu Archive, but its unique construction aims to attract a broad range of Linux distribution expertise; contributors to distributions like Debian, Arch Linux, NixOS and others are encouraged to join the team, and will even get paid to contribute one day per week to those projects to foster learning and idea sharing&lt;/p&gt;
&lt;h3 id="bootstrapping-the-team" class="relative group"&gt;Bootstrapping the team &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#bootstrapping-the-team" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The Debcrafters team is a global team. We have a squad in the Americas, a squad in EMEA and will have a squad in APAC. At present, we&amp;rsquo;ve staffed the AMER and EMEA teams with existing Canonical employees from our Foundations, Desktop, Server and Public Cloud teams. Each team currently has a manager, and four engineers.&lt;/p&gt;
&lt;p&gt;The team comprises Debian Developers, Stable Release Updates (SRU) team members and archive administrators, and began working together for the first time at our recent Engineering Sprint in Frankfurt held in early May 2025.&lt;/p&gt;
&lt;h3 id="mission" class="relative group"&gt;Mission &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#mission" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The Debcrafters&amp;rsquo; primary mission is to maintain the health of the Ubuntu Archive.&lt;/p&gt;
&lt;p&gt;This team will take the lead on syncing &amp;amp; merging packages from Debian, reviewing proposed migration issues, upstreaming Ubuntu deltas, and take ownership of major transitions such as upgrades to &lt;code&gt;glibc&lt;/code&gt; and past examples such as the &lt;code&gt;t64&lt;/code&gt; and &lt;code&gt;python3&lt;/code&gt; transitions.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;ll manage the scheduling, triggering and reporting on archive test rebuilds which we conduct when making major changes to critical packages. We did this when we enabled frame pointers by default, and when we switched &lt;code&gt;coreutils&lt;/code&gt; to the &lt;code&gt;uutils&lt;/code&gt; implementation in Ubuntu 25.10.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;ll be responsible for the evolution and maintenance of the &lt;code&gt;autopkgtest&lt;/code&gt; infrastructure for Ubuntu, as well as taking an instrumental role in introducing more distro-scale integration tests.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;ll work on improving the reporting and dashboarding of the Ubuntu Archive, its contributors and status, as well as taking a broader interest in shaping the tools we use to build and shape Ubuntu.&lt;/p&gt;
&lt;p&gt;What sets this team apart from the likes of Desktop, Server and Foundations is the range of packages they will work on. Members of the Debcrafters team will move thousands of packages every cycle - many of which they will not be intimately familiar with, but will use their growing distro maintenance and packaging skills to perform maintenance where there is no other clear or present owner.&lt;/p&gt;
&lt;h3 id="tools--processes" class="relative group"&gt;Tools &amp;amp; processes &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tools--processes" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;One of the key goals in my first &lt;a href="https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/" target="_blank" rel="noreferrer"&gt;post&lt;/a&gt; was to modernise the contribution experience for Ubuntu Developers by focusing on tools and processes.&lt;/p&gt;
&lt;p&gt;The Debian project recently adopted &lt;a href="https://wiki.debian.org/tag2upload" target="_blank" rel="noreferrer"&gt;tag2upload&lt;/a&gt;, which allows Debian Developers to use &lt;a href="https://packages.debian.org/search?keywords=git-debpush" target="_blank" rel="noreferrer"&gt;git-debpush&lt;/a&gt; to push a signed &lt;code&gt;git&lt;/code&gt; tag when uploading packages. While we’re not following that exact path, we share many of the same goals and intentions.&lt;/p&gt;
&lt;p&gt;For some time Ubuntu Developers have been able to use &lt;a href="https://canonical-git-ubuntu.readthedocs-hosted.com/en/latest/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;git-ubuntu&lt;/code&gt;&lt;/a&gt; as part of their development workflow, which aims to provide &amp;ldquo;unified git-based workflows for the development of Ubuntu source packages&amp;rdquo;. This project brought us closer to our desired experience, but still needs work to achieve our complete vision. I&amp;rsquo;d like to put more emphasis on the experience we provide for &lt;em&gt;testing&lt;/em&gt; packages, as well as signing, uploading and releasing packages.&lt;/p&gt;
&lt;p&gt;In the coming weeks our Starcraft team (responsible for &lt;a href="https://github.com/canonical/snapcraft" target="_blank" rel="noreferrer"&gt;Snapcraft&lt;/a&gt;, &lt;a href="https://github.com/canonical/rockcraft" target="_blank" rel="noreferrer"&gt;Rockcraft&lt;/a&gt;, &lt;a href="https://github.com/canonical/charmcraft" target="_blank" rel="noreferrer"&gt;Charmcraft&lt;/a&gt;) will begin prototyping &lt;code&gt;debcraft&lt;/code&gt;, which will (in time) become the de facto method for creating, testing and uploading packages to the Ubuntu archive.&lt;/p&gt;
&lt;p&gt;The first prototype of &lt;code&gt;debcraft&lt;/code&gt; will focus on unifying the current workflow adopted by most Ubuntu Developers at Canonical. It will wrap existing tools (such as &lt;code&gt;git-ubuntu&lt;/code&gt;, &lt;code&gt;lintian&lt;/code&gt;, &lt;code&gt;autopkgtest&lt;/code&gt;) to provide familiar, streamlined commands such as &lt;code&gt;debcraft pack&lt;/code&gt;, &lt;code&gt;debcraft lint&lt;/code&gt; and &lt;code&gt;debcraft test&lt;/code&gt;. Uploading packages, and a more native &amp;ldquo;craft&amp;rdquo; experience for constructing packages will come later.&lt;/p&gt;
&lt;p&gt;Details will make their way into the new &lt;a href="https://canonical-ubuntu-project.readthedocs-hosted.com/" target="_blank" rel="noreferrer"&gt;Ubuntu Project Docs&lt;/a&gt; throughout the course of the 25.10 Questing Quokka cycle, including the newly renovated &amp;ldquo;Ubuntu Packaging Guide&amp;rdquo;, which will aim to provide a &amp;ldquo;one ring to rule them all&amp;rdquo; approach to documenting how to package software for Ubuntu.&lt;/p&gt;
&lt;h3 id="attracting-contributors" class="relative group"&gt;Attracting contributors &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#attracting-contributors" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;While the team has been seeded with seasoned Ubuntu contributors, one of the primary goals of the team is to grow the contributor base across generations.&lt;/p&gt;
&lt;p&gt;One of the sub-teams is currently leading the roll out of a new contributor journey that will soon be publicly available. This process lays out the journey from complete beginner to &amp;ldquo;Core Dev&amp;rdquo;, stopping off at &amp;ldquo;Package Maintainer&amp;rdquo;, &amp;ldquo;Package Set Maintainer&amp;rdquo;, &amp;ldquo;&lt;a href="https://canonical-ubuntu-project.readthedocs-hosted.com/reference/glossary/#term-MOTU" target="_blank" rel="noreferrer"&gt;MOTU&lt;/a&gt;&amp;rdquo;, etc. along the way. The process also aims to help candidates prepare for Developer Membership Board interviews.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re a junior engineer just graduating from University, or you&amp;rsquo;re a seasoned Linux contributor elsewhere in the Linux ecosystem, the Debcrafters team is an excellent place to learn software packaging skills and contribute to the world&amp;rsquo;s most deployed Linux distribution.&lt;/p&gt;
&lt;h3 id="contribution-beyond-ubuntu" class="relative group"&gt;Contribution beyond Ubuntu &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#contribution-beyond-ubuntu" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The Debcrafters&amp;rsquo; primary commitment is to Ubuntu, but we recognise the enormous value in collaborating with other distributions. Many of the hard lessons I&amp;rsquo;ve personally learned resulted from contributing to NixOS and building Snaps. Packaging is a complex and ever-changing discipline, and other distributions are facing many of the complex problems we are - often with different or novel approaches to solving them.&lt;/p&gt;
&lt;p&gt;In recognition of this, we&amp;rsquo;re actively seeking maintainers from other distributions - be that Debian, Arch, NixOS, Guix, Fedora, Universal Blue or any other - packaging and distribution engineering skills are often common across distributions, and we believe that Ubuntu can benefit from broader perspectives, while contributing back to the wider ecosystem of distributions in the process.&lt;/p&gt;
&lt;p&gt;The Debcrafters must spend the majority of their work time on Ubuntu, but they will be encouraged to spend a day per week contributing to other distributions to gain understanding, and bring fresh perspectives to Ubuntu (and the reverse, hopefully!). This will be structured as a &lt;em&gt;literal&lt;/em&gt; day per week, agreed with the team management - for example &amp;ldquo;I work on NixOS on Tuesdays&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Canonical has launched a new team, the Debcrafters, who are dedicated to maintaining the very core of Ubuntu: the archive. This team has a global footprint, and deep expertise in software packaging drawn from across the Linux ecosystem. They&amp;rsquo;ll lead transitions, improve tooling improvements and strengthen our distribution testing infrastructure.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re an experienced Debian Developer, a maintainer from another Linux distribution or a new engineer starting your career in open source, Debcrafters offers a unique opportunity to learn, grow, and contribute to the world’s most widely deployed Linux distribution.&lt;/p&gt;</description></item><item><title>From NixOS to Ubuntu</title><link>https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/</link><pubDate>Tue, 24 Jun 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/</guid><description>&lt;p&gt;Following my appointment as VP Engineering for Ubuntu, I moved all of my machines from NixOS to Ubuntu. Being responsible for decisions that affect millions of Ubuntu users comes with, in my opinion, the obligation to &lt;em&gt;use&lt;/em&gt; the product and live with those decisions myself.&lt;/p&gt;
&lt;p&gt;Following years of running Arch Linux and NixOS, I imagined this would be uncomfortable, but was pleasantly surprised. In this post, I&amp;rsquo;ll outline my setup and a new philosophy for how I configure my machines.&lt;/p&gt;
&lt;p&gt;Last year, I wrote &lt;a href="https://jnsgr.uk/2024/07/how-i-computer-in-2024/" target="_blank" rel="noreferrer"&gt;in detail&lt;/a&gt; about my setup. Consider this post a &amp;ldquo;diff&amp;rdquo; on what&amp;rsquo;s changed since.&lt;/p&gt;
&lt;h2 id="recap-why-nixos" class="relative group"&gt;Recap: Why NixOS? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#recap-why-nixos" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;NixOS affords a seemingly endless selection of applications, desktop environments and when combined with &lt;a href="https://github.com/nix-community/home-manager" target="_blank" rel="noreferrer"&gt;Home Manager&lt;/a&gt; it provides a consistent way to manage the configuration of almost all aspects of a system.&lt;/p&gt;
&lt;p&gt;People have argued that this is overkill, and results in needlessly complex configurations that produce difficult to read error messages, and make a system more difficult to troubleshoot.&lt;/p&gt;
&lt;p&gt;While I had some troubles adopting NixOS, I consistently felt that the positives outweighed the negatives: I loved being able to overlay individual packages and tweak fundamentals of the system in a machine specific way. I also liked being able to trivially reuse configuration for app and hardware configuration across my machines in a single &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;flake&lt;/a&gt;, which remains (to my amazement) one of my most popular GitHub repositories.&lt;/p&gt;
&lt;p&gt;As I planned my move to Ubuntu, I decided to change the way I used my computer to avoid fighting my machine, and any feeling that I could be missing out on the &lt;del&gt;complexity&lt;/del&gt; flexibility I&amp;rsquo;d become so used to.&lt;/p&gt;
&lt;h2 id="adopting-jfui" class="relative group"&gt;Adopting &amp;ldquo;JFUI&amp;rdquo; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#adopting-jfui" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The guiding principle for my new way of thinking is &lt;strong&gt;JFUI: Just F*cking Use It&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;The primary motivation behind JFUI is to pick applications for which the defaults are close enough to my preferences, then use them with as little (or no) configuration as possible.&lt;/p&gt;
&lt;p&gt;By employing this principle, I should spend less of my time updating my configuration files as things change, and spend less time obsessing over every last theme detail for each application on my machine.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve spent years collecting &lt;a href="https://github.com/jnsgruk/dotfiles" target="_blank" rel="noreferrer"&gt;repositories full of dotfiles&lt;/a&gt;, which contains the ~2500 lines of configuration and scripts I used prior to moving to NixOS. As of today, the most recent commit in &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;my Nix flake&lt;/a&gt;, &lt;code&gt;cloc&lt;/code&gt; reports:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;---------------------------------------------------
Language files blank comment code
---------------------------------------------------
Nix 113 401 232 3666
Bourne Again Shell 3 25 30 95
Bourne Shell 1 17 0 83
Markdown 1 19 1 81
YAML 4 8 3 51
diff 1 1 7 6
---------------------------------------------------
SUM: 123 471 273 3982
---------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that seems like &lt;em&gt;a lot&lt;/em&gt; to me! Yet this number omits my Visual Studio Code configuration (a further 200 lines of JSON), various browser configurations, etc.&lt;/p&gt;
&lt;p&gt;So I challenged myself to set up my Ubuntu machines with as little configuration as possible and to choose apps with better defaults.&lt;/p&gt;
&lt;h2 id="desktop-environment" class="relative group"&gt;Desktop Environment &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#desktop-environment" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;d been a long time since I&amp;rsquo;d used a computer regularly without a tiling window manager. I switched to &lt;a href="https://swaywm.org/" target="_blank" rel="noreferrer"&gt;sway&lt;/a&gt; in 2019, and to &lt;a href="https://hypr.land/" target="_blank" rel="noreferrer"&gt;Hyprland&lt;/a&gt; in 2023.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve built a lot of muscle memory by using a consistent keymap across both environments. I &lt;a href="https://jnsgr.uk/2024/07/how-i-computer-in-2024/" target="_blank" rel="noreferrer"&gt;use&lt;/a&gt; a 57&amp;quot; ultrawide monitor, and the idea of using it &lt;em&gt;without tiling&lt;/em&gt; seemed like total anarchy to me.&lt;/p&gt;
&lt;p&gt;But it didn&amp;rsquo;t take me long to adapt to using GNOME again. Despite abstaining from it for years, I&amp;rsquo;ve always appreciated the visual design of GNOME and often used their apps as part of my tiling experience (&lt;a href="https://apps.gnome.org/en-GB/Nautilus/" target="_blank" rel="noreferrer"&gt;Files&lt;/a&gt;, &lt;a href="https://apps.gnome.org/en-GB/Papers/" target="_blank" rel="noreferrer"&gt;Papers&lt;/a&gt;, &lt;a href="https://apps.gnome.org/en-GB/Loupe/" target="_blank" rel="noreferrer"&gt;Loupe&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Features such as the keyring, light/dark mode switching worked for me under Sway/Hyprland, but it always required complex, fragile configuration. The out of the box experience for configuring WiFi networks and Bluetooth devices feels modern, and like a part of the OS, rather than a kit of parts. In my latter few months of running Hyprland, I struggled more with consistent theming and stability as Hyprland evolved separately from the themes and apps I liked.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t attribute any blame for this; it&amp;rsquo;s a natural side effect of combining lots of independent and often complex parts from across the Linux desktop ecosystem, and I was consciously running pre 1.0 software knowing there would be issues because I enjoyed the overall experience.&lt;/p&gt;
&lt;h3 id="gnome-extensions" class="relative group"&gt;GNOME Extensions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#gnome-extensions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This is the area where I violated JFUI the most, though some of it is temporary as I get further away from my existing workflow. I disable the Ubuntu dock, desktop icons, app indicators and Ubuntu tiling assistant. While the tiling assistant was a great step up on what came before it, it fell short of what I needed to manage windows on my large display.&lt;/p&gt;
&lt;p&gt;I took inspiration from &lt;a href="https://omakub.org/" target="_blank" rel="noreferrer"&gt;Omakub&lt;/a&gt;, which is where I discovered &lt;a href="https://extensions.gnome.org/extension/4548/tactile/" target="_blank" rel="noreferrer"&gt;Tactile&lt;/a&gt; - a GNOME extension for tiling windows to a custom on-screen grid using the keyboard. I&amp;rsquo;ve found this to be invaluable, and easily the best window management experience for GNOME. I&amp;rsquo;ve customised the grid to the following ratios (using &lt;code&gt;gsettings&lt;/code&gt; in a script):&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_563a2ca534804b07.webp 330w,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_a88883170768e81a.webp 660w
,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_f9d64651fdeef908.webp 959w
,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_f9d64651fdeef908.webp 959w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="959"
height="522"
class="mx-auto my-0 rounded-md"
alt="a screenshot of the tactile gnome extension configuration window"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_d63430834ff566c4.png" srcset="https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_ebdaf981cf8217b8.png 330w,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01_hu_d63430834ff566c4.png 660w
,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01.png 959w
,https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/01.png 959w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve grown to like &lt;a href="https://extensions.gnome.org/extension/5090/space-bar/" target="_blank" rel="noreferrer"&gt;Space Bar&lt;/a&gt; - it provides desktop workspaces akin to those in Sway/Hyprland which enabled me to use the muscle memory I&amp;rsquo;d developed over years. As I&amp;rsquo;ve progressed, I think I &lt;em&gt;could&lt;/em&gt; live without Space Bar and use the native workspace features in GNOME, so I&amp;rsquo;ll experiment with that soon.&lt;/p&gt;
&lt;p&gt;The final two are somewhat simpler: &lt;a href="https://extensions.gnome.org/extension/6242/emoji-copy/" target="_blank" rel="noreferrer"&gt;Emoji Copy&lt;/a&gt; (mapped to &lt;code&gt;Super + E&lt;/code&gt;) and &lt;a href="https://extensions.gnome.org/extension/4839/clipboard-history/" target="_blank" rel="noreferrer"&gt;Clipboard History&lt;/a&gt; (mapped to &lt;code&gt;Super + V&lt;/code&gt;), functions that were previously enabled by &lt;a href="https://github.com/SimplyCEO/wofi" target="_blank" rel="noreferrer"&gt;&lt;code&gt;wofi&lt;/code&gt;&lt;/a&gt; with a couple of plugins and lots of configuration.&lt;/p&gt;
&lt;h2 id="editors" class="relative group"&gt;Editors &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#editors" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I spend a lot of time in an editor. As I wrote &lt;a href="https://jnsgr.uk/uses" target="_blank" rel="noreferrer"&gt;last year&lt;/a&gt;, I&amp;rsquo;ve been using &lt;code&gt;neovim&lt;/code&gt; and Visual Studio Code for some years. I never &amp;ldquo;managed&amp;rdquo; Visual Studio Code with NixOS because I always found their settings sync to be quite sufficient.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/jnsgruk/nixos-config/blob/aad045010b1ac61d271858dd5f4c2fa8dcb6e5d4/home/common/shell/vim.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;nvim&lt;/code&gt; config&lt;/a&gt; wasn&amp;rsquo;t too complicated - though it is made to look simpler because Home Manager abstracts away the details of managing plugins. I hadn&amp;rsquo;t taken the time to set up language server support, code completion or other creature comforts that one might expect from an editor in 2025.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d dabbled before with &lt;a href="https://helix-editor.com/" target="_blank" rel="noreferrer"&gt;Helix&lt;/a&gt;, a modal command-line editor not dissimilar from &lt;code&gt;vim&lt;/code&gt;. Helix comes with a lot more out of the box: it supports the Language Server Protocol (LSP), has many built in colour schemes, supports fuzzy finding files/buffers, project wide search and more. The keymap took some practice, but my entire configuration amounts to 8 lines including some blank lines and provides many more modern features.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;catppuccin_macchiato&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cursorline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;text-width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;rulers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lsp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;display-inlay-hints&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;There is a separate file with some LSP configuration, but only a few more lines. Once I got this set up, I spent about 8 weeks using &lt;em&gt;just&lt;/em&gt; Helix to make sure that the keymap and operating model were sufficiently burned in to my mind. I&amp;rsquo;ve been really impressed - Helix is fast, the default features are great and I don&amp;rsquo;t anticipate returning to &lt;code&gt;vim&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On the desktop side, I&amp;rsquo;d was intrigued by &lt;a href="https://zed.dev/" target="_blank" rel="noreferrer"&gt;Zed&lt;/a&gt;. I don&amp;rsquo;t always feel the need for a desktop editor, but there are projects that I prefer working on in a more graphical environment. I last tried Zed about 18 months ago and found it a little too sparse on features, but things have really evolved since then and there is an encouraging rate of change on the project. I&amp;rsquo;ve been using it most days for the last 4-5 months, and while it still lacks some of the polish (and features) of Visual Studio Code, it&amp;rsquo;s significantly lighter on resources, and I&amp;rsquo;m much happier with the defaults (details below)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ui_font_size&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;buffer_font_size&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;theme&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;system&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;light&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Catppuccin Latte&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;dark&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Catppuccin Macchiato&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;buffer_font_family&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;MesloLGMDZ Nerd Font Mono&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ui_font_family&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.SystemUIFont&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;base_keymap&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SublimeText&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="terminal" class="relative group"&gt;Terminal &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#terminal" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I switched to &lt;code&gt;zsh&lt;/code&gt; around 2013 and have used it every day since: starting with the infamous &lt;a href="https://ohmyz.sh/" target="_blank" rel="noreferrer"&gt;Oh My Zsh&lt;/a&gt;, &lt;a href="https://github.com/Powerlevel9k/powerlevel9k" target="_blank" rel="noreferrer"&gt;powerlevel9k&lt;/a&gt;, and subsequently &lt;a href="https://github.com/romkatv/powerlevel10k" target="_blank" rel="noreferrer"&gt;powerlevel10k&lt;/a&gt;, and finally settling on &lt;a href="https://starship.rs/" target="_blank" rel="noreferrer"&gt;Starship&lt;/a&gt; with a couple of &lt;code&gt;zsh&lt;/code&gt; plugins such as &lt;a href="https://github.com/zsh-users/zsh-autosuggestions" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zsh-autosuggestions&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/zsh-users/zsh-syntax-highlighting" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;zsh&lt;/code&gt; config had become quite complex - an area in which NixOS/Home Manager really helped by providing (mostly)neat abstractions for plugins and &lt;code&gt;starship&lt;/code&gt; integration, but the equivalent configuration on Ubuntu would have been 100s of lines of &lt;code&gt;zsh&lt;/code&gt;, notwithstanding the need to load plugins in the right order, etc.&lt;/p&gt;
&lt;p&gt;Most of the configuration I was doing with &lt;code&gt;zsh&lt;/code&gt; was to imitate &lt;a href="https://fishshell.com/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;fish&lt;/code&gt;&lt;/a&gt;. I&amp;rsquo;d tried &lt;code&gt;fish&lt;/code&gt; in the past, but reverted when I couldn&amp;rsquo;t use &lt;code&gt;sudo !!&lt;/code&gt;, or &lt;code&gt;/some/command $!&lt;/code&gt; and other tricks. I&amp;rsquo;d been following &lt;code&gt;fish&lt;/code&gt;&amp;rsquo;s rewrite and decided to give it another go, and have stuck with it since. It fits my JFUI mantra perfectly, leaving me with just a few lines of config for essentially identical functionality:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; fish_greeting &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fish_add_path ~/.nix-profile/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fish_add_path ~/.local/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fish_add_path ~/.cargo/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fish_add_path ~/go/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fish_add_path ~/scripts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; status is-interactive
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; starship init fish &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fzf --fish &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; atuin init fish --disable-up-arrow &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -gx EDITOR hx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -gx SUDO_EDITOR hx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I still occasionally fall foul of typing &lt;code&gt;sudo !!&lt;/code&gt;, but overall I&amp;rsquo;ve found &lt;code&gt;fish&lt;/code&gt; to be an excellent interactive shell replacement. In particular, the native tab completion support is leagues ahead of anything I ever managed to configure with &lt;code&gt;zsh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I moved from Alacritty to &lt;a href="https://ghostty.org/" target="_blank" rel="noreferrer"&gt;Ghostty&lt;/a&gt;, which has been absolutely excellent. It&amp;rsquo;s wicked fast, it has my favourite colour scheme built in, and I just love the way &lt;a href="https://github.com/mitchellh" target="_blank" rel="noreferrer"&gt;@mitchellh&lt;/a&gt; has set the project up for success in the long term. It also fits in nicely with my minimally configured applications, with excellent defaults out of the box.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;catppuccin-macchiato&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;font-family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;MesloLGMDZ Nerd Font Mono&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;font-size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;14&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;window-padding-x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;window-padding-y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;window-decoration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Finally: &lt;a href="https://github.com/tmux/tmux" target="_blank" rel="noreferrer"&gt;tmux&lt;/a&gt;, which I had been happily using for many years, but slowly collecting configuration for. I decided to give &lt;a href="https://zellij.dev/" target="_blank" rel="noreferrer"&gt;Zellij&lt;/a&gt; a try, and haven&amp;rsquo;t looked back. It also has a hellishly complicated configuration in my case 😉:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kdl" data-lang="kdl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;catppuccin-macchiato&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;default_layout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;compact&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;show_startup_tips&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="software-availability" class="relative group"&gt;Software Availability &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#software-availability" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I always felt incredibly spoiled by the vast availability of software for NixOS, and even more so that the contribution model was so simple that I was able to augment to collection myself.&lt;/p&gt;
&lt;p&gt;On Ubuntu, I use Snaps wherever possible and fall back to archive and installing software with &lt;code&gt;apt&lt;/code&gt; where it makes sense. If neither of those have what I need, I use the Nix package manager, which works well on Ubuntu. While some of the &amp;ldquo;additional&amp;rdquo; software might be installable using &lt;code&gt;go install&lt;/code&gt;, or &lt;code&gt;cargo install&lt;/code&gt;, or other package managers like &lt;code&gt;brew&lt;/code&gt;, &lt;code&gt;flakpak&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, etc., I wanted to keep things as simple as possible, so if it&amp;rsquo;s not available from Ubuntu-native sources I get it from &lt;code&gt;nixpkgs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve included what I&amp;rsquo;m actually getting from the Snap store (63 snaps), and from &lt;code&gt;nixpkgs&lt;/code&gt; (21 packages) below.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Installed snaps&lt;/summary&gt;
&lt;pre&gt;
❯ snap list | tail -n+2
agendrr 0.1.1 3 latest/stable jnsgruk* -
astral-uv 0.7.13 627 latest/stable lengau classic
audacity 3.7.1 1208 latest/candidate snapcrafters* -
bare 1.0 5 latest/stable canonical** base
bitwarden 2025.5.1 140 latest/stable bitwarden** -
charmcraft 3.4.6 6672 latest/stable canonical** classic
code 18e3a1ec 197 latest/stable vscode** classic
core 16-2.61.4-20250508 17212 latest/stable canonical** core
core18 20250523 2887 latest/stable canonical** base
core20 20250526 2599 latest/stable canonical** base
core22 20250528 2010 latest/stable canonical** base
core24 20250526 1006 latest/stable canonical** base
desktop-security-center 0+git.f7ad73a 59 1/stable/… canonical** -
discord 0.0.98 243 latest/candidate snapcrafters* -
docker 28.1.1+1 3265 latest/stable canonical** -
dotrun 1.4.8 85 latest/stable canonicalwebteam -
ffmpeg-2404 7.1.1 75 latest/stable snapcrafters* -
firefox 139.0.4-1 6316 latest/stable/… mozilla** -
firmware-updater 0+git.22198be 167 1/stable/… canonical** -
ghstat 0.4.1 91 latest/stable jnsgruk* -
ght 1.11.7 110 latest/edge tbmb -
gimp 3.0.4 525 latest/stable snapcrafters* -
gnome-3-28-1804 3.28.0-19-g98f9e67.98f9e67 198 latest/stable canonical** -
gnome-3-34-1804 0+git.3556cb3 93 latest/stable canonical** -
gnome-42-2204 0+git.38ea591 202 latest/stable/… canonical** -
gnome-46-2404 0+git.d9f8bf6-sdk0+git.c8a281c 90 latest/stable canonical** -
go 1.24.4 10907 latest/stable canonical** classic
gopls 0.19.0 1089 latest/stable alexmurray* classic
goreleaser 2.10.2 1060 latest/stable caarlos0 classic
gtk-common-themes 0.1-81-g442e511 1535 latest/stable/… canonical** -
helix 25.01.1 91 latest/stable lauren-brock classic
icloudpd 1.28.1 12 latest/stable jnsgruk* -
jhack 0.4.4.0.13 461 latest/stable ppasotti -
jq 1.5+dfsg-1 6 latest/stable mvo* -
juju 3.6.7 31266 3/stable canonical** -
kubectl 1.33.2 3609 latest/stable canonical** classic
lxd 5.21.3-c5ae129 33110 5.21/stable canonical** -
mattermost-desktop 5.12.1 789 latest/stable snapcrafters* -
mesa-2404 24.2.8-snap183 887 latest/stable canonical** -
multipass 1.15.1 14535 latest/stable canonical** -
node 22.16.0 10226 22/stable iojs** classic
obsidian 1.8.10 47 latest/stable obsidianmd classic
pinta 3.0.1 56 latest/stable james-carroll* -
prompting-client 0+git.d542a5d 104 1/stable/… canonical** -
rambox 2.4.1 44 latest/stable ramboxapp** -
rockcraft 1.12.0 3367 latest/stable canonical** classic
ruff 0.11.13 1377 latest/stable lengau -
rustup 1.27.1 1471 latest/stable canonical** classic
shellcheck v0.10.0 1725 latest/stable koalaman -
shfmt 3.5.1 33 latest/stable ankushpathak -
signal-desktop 7.58.0 799 latest/candidate snapcrafters* -
snap-store 0+git.90575829 1270 2/stable/… canonical** -
snapcraft 8.9.4 15082 latest/stable canonical** classic
snapd 2.68.5 24718 latest/stable canonical** snapd
snapd-desktop-integration 0.9 253 latest/stable/… canonical** -
sublime-merge 2102 95 latest/stable snapcrafters* classic
thonny 4.1.7 239 latest/stable sameersharma2006 -
thunderbird 128.11.1esr-1 737 latest/stable canonical** -
todoist 9.17.0 1340 latest/stable doist** -
typescript-language-server 4.3.4 211 latest/stable alexmurray* -
yazi shipped 293 latest/stable sxyazi classic
yq v4.44.5 2634 latest/stable mikefarah -
zellij 0.42.2 41 latest/stable dominz88 classic
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;Installed Nix packages&lt;/summary&gt;
&lt;pre&gt;
❯ nix profile list | grep -Po "Name:[ ]*\K.+\$"
atuin
bash-language-server
cargo-udeps
deadnix
fzf
gh
gofumpt
nil
nixd
nixfmt-rfc-style
prettier
pyright
python-lsp-server
spread
starship
statix
taplo
terraform-ls
typos-lsp
vscode-langservers-extracted
yaml-language-server
&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;I continue to find Nix development shells useful, and even &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/5f9f2fe2ced2416e8a1fb3116d88d1b51c9fdbc7/flake.nix#L99-L122" target="_blank" rel="noreferrer"&gt;use one&lt;/a&gt; to develop this site.&lt;/p&gt;
&lt;h2 id="configuration-management" class="relative group"&gt;Configuration Management &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#configuration-management" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In the past I&amp;rsquo;ve used all kinds of scripts, Ansible playbooks and dotfile managers to solve this problem, and its a problem that was solved very elegantly by NixOS/Home Manager. I experimented with Home Manager on Ubuntu to manage dotfiles and configuration, but found the experience had more rough edges than I would like, and didn&amp;rsquo;t really adhere to my JFUI principles.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve settled on a directory full of idempotent, single-purpose scripts which get executed by a wrapper named &lt;code&gt;provision&lt;/code&gt;. This is somewhat inspired by &lt;a href="https://omakub.org/" target="_blank" rel="noreferrer"&gt;Omakub&lt;/a&gt;, but without the menus and configuration they supply to allow their users some customisation. The scripts install packages, write configuration with tools like &lt;code&gt;gsettings&lt;/code&gt; and symlink configuration into place where needed:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;❯ ls
app-1password dev-python tool-agendrr
app-chrome dev-rust tool-atuin
app-flameshot font-meslo tool-gearlever
app-ghostty hw-yubikey tool-gh
app-obsidian kara-audioengine tool-git
app-pinta kara-backup tool-helix
app-rambox kara-data-disk tool-junction
app-signal kara-hiring-automation tool-lxd
app-sublime-merge kara-hiring-reports tool-multipass
app-thunderbird provision tool-podman
app-todoist system-flatpak tool-spread
configs system-fs tool-starship
dev-charms system-gnome tool-syncthing
dev-containers system-gnome-extensions tool-tailscale
dev-go system-nix tool-zed
dev-node system-shell tool-zellij
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An example of a specific script, such as &lt;code&gt;tool-zellij&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -e
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt; dirname &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install --classic zellij
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.config/zellij&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ln -sf &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/configs/zellij/config.kdl&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.config/zellij/config.kdl&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The &lt;code&gt;provision&lt;/code&gt; script is similarly simple (and naive):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -ex
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt; dirname &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt-get upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;categories&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;system hw dev tool app font &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;hostname&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; c in &lt;span class="nv"&gt;$categories&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; x in &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;-*&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is &lt;em&gt;just about&lt;/em&gt; satisfactory. I&amp;rsquo;ve got into the habit of only ever installing software by creating the relevant script and configuration. If I don&amp;rsquo;t need it to persist, the work gets done in an ephemeral LXD container/VM then thrown away.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m working on a better solution for this named &lt;code&gt;miso&lt;/code&gt;, short for &amp;ldquo;Make It So&amp;rdquo;. This a homegrown, multi-host configuration management tool for my machines that draws inspiration from &lt;code&gt;cloud-init&lt;/code&gt;, &lt;code&gt;home-manager&lt;/code&gt;, &lt;code&gt;terraform&lt;/code&gt; and a few others. I&amp;rsquo;ll write about that in a future post when the code is a little more complete, but there is an example of an early configuration format I&amp;rsquo;m targeting available as a &lt;a href="https://gist.github.com/jnsgruk/64b7418183bd3abfbe68e878907608e3" target="_blank" rel="noreferrer"&gt;gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Everything in the linked config file is currently implemented and working with a decent suite of integration tests - but I&amp;rsquo;ve got a lot of tidying to do with error handling and such before I release it.&lt;/p&gt;
&lt;h2 id="so-what-about-nix" class="relative group"&gt;So What About Nix? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#so-what-about-nix" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I thoroughly enjoyed my adventures with Nix, and I consider having learned how to package software with Nix and use the available tooling to manage servers, desktops and development shells to have been incredibly worthwhile.&lt;/p&gt;
&lt;p&gt;Even &lt;em&gt;if&lt;/em&gt; I were to never touch Nix again, the general packaging and distribution engineering skills I learned have been invaluable, and I&amp;rsquo;m grateful to everyone who helped me on that journey through Matrix chats, Pull Requests and Mastodon interactions.&lt;/p&gt;
&lt;p&gt;I remain active on the ~35 packages in &lt;a href="https://github.com/NixOS/nixpkgs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;nixpkgs&lt;/code&gt;&lt;/a&gt; for which I&amp;rsquo;m the maintainer. I continue to use Nix for development shells, CI and for certain packages on my Ubuntu machines. I have archived my &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;flake&lt;/a&gt; for now because it&amp;rsquo;s not being maintained, but I&amp;rsquo;ve left it there in case there are any patterns that might be useful for others.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ubuntu has been very stable on my desktop, server and both of my laptops. I&amp;rsquo;ve enjoyed the level of integration and polish that comes with no effort in the desktop environment, and managing less configuration has been a freeing experience - even if some of my apps no longer have matching themes 😱.&lt;/p&gt;
&lt;p&gt;Provisioning and configuration management are less structured and more cumbersome in Ubuntu, but that has driven me to build my own tool which was good fun, and gave me a nice challenge to solve through my journey learning Rust! I hope to learn from the project in a way that helps inform the development of Ubuntu itself in future releases.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re into numbers, here are the stats for my renewed &amp;ldquo;config&amp;rdquo; directory, which contains all of the text-based configuration &amp;amp; scripts for my machines:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;----------------------------------------------------------
Language files blank comment code
----------------------------------------------------------
Bourne Again Shell 47 154 81 413
TOML 3 13 1 53
Fish Shell 1 13 3 38
JSON 1 0 0 35
----------------------------------------------------------
SUM: 52 180 85 539
----------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From 3982 lines, to 539 lines, and much of that could be reduced if it wasn&amp;rsquo;t for the slightly repetitive nature of maintaining separate, idempotent scripts. Not bad.&lt;/p&gt;
&lt;p&gt;As much as I feared the transition, my journey back to Ubuntu has been very enjoyable. I&amp;rsquo;m not &amp;ldquo;quitting Nix&amp;rdquo; or &amp;ldquo;over it&amp;rdquo;, but at least for now I&amp;rsquo;m enjoying a less complex existence with my personal computers.&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;</description></item><item><title>Supercharging Ubuntu Releases: Monthly Snapshots &amp; Automation</title><link>https://jnsgr.uk/2025/05/supercharging-ubuntu-releases/</link><pubDate>Thu, 29 May 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/05/supercharging-ubuntu-releases/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/61876" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Ubuntu has shipped on a predictable, six-month cadence for two decades. Twenty years ago, the idea of releasing an entire distribution every six months was considered forward looking, bold and even difficult. Things have changed since then: software engineering has evolved as a practice, and the advent of both rolling-release distributions like Arch Linux, and more recently image-based immutable distributions such as Universal Blue have meant that other projects with similar goals have adopted vastly different release models with some desirable properties.&lt;/p&gt;
&lt;p&gt;My goal over the coming months is to build a release process that takes advantage of modern release engineering practices, while retaining the resilience and stability of our six-monthly releases. We&amp;rsquo;ll introduce significantly more automated testing, and ensure that the release process is transparent, repeatable and executable in a much shorter and well-known timeframe with little to no human intervention.&lt;/p&gt;
&lt;p&gt;This journey will also create space for better system-wide testing, earlier detection of regressions, and a more productive collaboration with our community.&lt;/p&gt;
&lt;h2 id="monthly-snapshot-releases" class="relative group"&gt;Monthly Snapshot Releases &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#monthly-snapshot-releases" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Starting in May 2025, we&amp;rsquo;re introducing monthly snapshot releases for Ubuntu.&lt;/p&gt;
&lt;p&gt;Ubuntu is not &amp;ldquo;moving to monthly releases&amp;rdquo; or adopting a rolling release model; we&amp;rsquo;re committed to our six-monthly releases with a Long Term Support (LTS) release every two years. That doesn&amp;rsquo;t mean that our release process should be exempt from the same scrutiny that the rest of our engineering processes are subject to.&lt;/p&gt;
&lt;p&gt;Today the Ubuntu Release process is the product of twenty years of evolution: it safeguards Ubuntu releases with a wealth of checks and balances, but is a largely manual process requiring significant human involvement.&lt;/p&gt;
&lt;p&gt;The Ubuntu Release Team is a crowd of seasoned Ubuntu veterans who have been steadily releasing Ubuntu for many years. Many of this team are community members, some are or have been employed by Canonical in the past. More recently we have established the Canonical Ubuntu Release Management Team - a relatively new team at Canonical who&amp;rsquo;ll be collaborating with the Ubuntu Release Team to develop the new process.&lt;/p&gt;
&lt;p&gt;To aid the Canonical team in their understanding of the existing processes, and the immovable requirements that sit beneath it, we&amp;rsquo;re introducing monthly snapshot releases for Ubuntu. These will not be fully-fledged releases of Ubuntu, but rather curated, testable milestones from our development stream. For the 25.10 (Questing Quokka) cycle, you can expect the following &lt;a href="https://discourse.ubuntu.com/t/questing-quokka-release-schedule/36462" target="_blank" rel="noreferrer"&gt;release schedule&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;May 29, 2025&lt;/strong&gt;: Questing Quokka - Snapshot 1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;June 26, 2025&lt;/strong&gt;: Questing Quokka - Snapshot 2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;July 31, 2025&lt;/strong&gt;: Questing Quokka - Snapshot 3&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;August 28, 2025&lt;/strong&gt;: Questing Quokka - Snapshot 4&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;September 18, 2025&lt;/strong&gt;: Questing Quokka - Beta&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;October 9, 2025&lt;/strong&gt;: Questing Quokka - Final Release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn&amp;rsquo;t mean you&amp;rsquo;ll start seeing Ubuntu versions off the six-month cadence. There will be no Ubuntu 25.07 or 25.08, etc. The monthly snapshots are exactly that: a snapshot of the development of Ubuntu 25.10. Snapshots are not meant for production use, but will help the release team move away from deep institutional knowledge, and toward clean well-documented automated workflows that are transparent, repeatable and testable.&lt;/p&gt;
&lt;p&gt;With our current model, failure modes are not detected until they&amp;rsquo;re urgent and blocking an imminent release. The team conducts rigorous retrospectives on each release, but in my opinion it&amp;rsquo;s hard to meaningfully evolve such a process when it&amp;rsquo;s only exercised every six months. The monthly snapshots will create opportunities for us to test, understand and improve the process.&lt;/p&gt;
&lt;h2 id="embracing-automation" class="relative group"&gt;Embracing Automation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#embracing-automation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;One of the most valuable outcomes of this journey will be the opportunity to automate more of the process, freeing up time for the team to focus on more strategic tasks. Releasing a distribution is a complex process requiring coordination across architectures, images, mirrors, websites, testing infrastructure and even partner agreements. This also makes it hard to place a traditional CI tool at the heart of the process. As much as I like Github Actions, I think we&amp;rsquo;d quickly get lost trying to release Ubuntu with such a system, notwithstanding the fact that we&amp;rsquo;d lose control of the underlying infrastructure that releases Ubuntu.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been exploring the world of Durable Execution, which according to &lt;a href="https://restate.dev/what-is-durable-execution/" target="_blank" rel="noreferrer"&gt;restate.dev&lt;/a&gt; is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;the practice of making code execution persistent, so that services recover automatically from crashes and restore the results of already completed operations and code blocks without re-executing them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At Canonical, we&amp;rsquo;ve adopted &lt;a href="https://temporal.io/" target="_blank" rel="noreferrer"&gt;Temporal&lt;/a&gt; in a few of our products and in many of our business processes. Temporal is a durable execution product that enables developers to solve complex distributed problems, but without being deep distributed systems experts. It&amp;rsquo;s a framework for composing tasks into workflows, with first-class primitives for dealing with failures, retries, exponential back-off and other concepts that enable the build of long-running complex workflows.&lt;/p&gt;
&lt;p&gt;Having spent some time with Temporal myself, and watched other teams adopt it, I think it&amp;rsquo;s a great fit for engineering our next-generation release process. I want our engineers to focus on the logic of the release process, not the infrastructure behind it, and Temporal should enable them to do just that. The Temporal &lt;a href="https://temporal.io/" target="_blank" rel="noreferrer"&gt;homepage&lt;/a&gt; sums it up nicely:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write code as if failure doesn’t exist&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Temporal &lt;a href="https://docs.temporal.io/evaluate/understanding-temporal#workflow" target="_blank" rel="noreferrer"&gt;workflows&lt;/a&gt; and &lt;a href="https://docs.temporal.io/evaluate/understanding-temporal#activities" target="_blank" rel="noreferrer"&gt;activities&lt;/a&gt; can be written in many languages - and particularly in Python and Go. My expectation is that Go will prove to be an excellent fit for our process: it&amp;rsquo;s a fast and productive language that specialises in concurrency and asynchronous network operations, and has a powerful standard library containing much of the functionality we&amp;rsquo;ll need to build our new release process.&lt;/p&gt;
&lt;p&gt;To take an overly simplistic view of how I expect this to go: we&amp;rsquo;ll take our existing release checklist, write a Go function for each step with some &lt;a href="https://docs.temporal.io/develop/go/testing-suite" target="_blank" rel="noreferrer"&gt;tests&lt;/a&gt;, and compose them together into one or more Temporal workflows that represent the full release process. This will take time, but this approach will enable us to incrementally demonstrate progress toward a fully-automated process over the coming cycles.&lt;/p&gt;
&lt;p&gt;By making this move, not only will we make the process quicker, but also more &lt;a href="https://docs.temporal.io/develop/go/observability" target="_blank" rel="noreferrer"&gt;observable&lt;/a&gt;, &lt;a href="https://docs.temporal.io/develop/go/testing-suite" target="_blank" rel="noreferrer"&gt;testable&lt;/a&gt;, reliable and easier to understand for everyone, not just the release team.&lt;/p&gt;
&lt;h2 id="improving-test-coverage" class="relative group"&gt;Improving Test Coverage &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#improving-test-coverage" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;One area I&amp;rsquo;d like to improve as a side-effect of this work is more full-system integration testing. Packages in the Ubuntu archive generally enjoy good coverage through a suite of &lt;a href="https://autopkgtest.ubuntu.com/" target="_blank" rel="noreferrer"&gt;autopkgtest&lt;/a&gt; tests, and there are numerous other places where integration tests are run on Ubuntu. With our traditional six-monthly cadence, full end-to-end testing of ISOs and the installer typically ramps up close to release time when changes are fewer (and riskier) and time is short.&lt;/p&gt;
&lt;p&gt;With the introduction of monthly snapshots, we can integrate installer testing, full-disk encryption testing, graphical application testing and more as a regular, automated part of the release pipeline - not just as part of the development pipeline of each individual package. This means we should catch regressions earlier and surface more edge cases to be resolved before release.&lt;/p&gt;
&lt;p&gt;One of the most important parts of increasing our testing culture is to make it clear where and how to contribute tests to Ubuntu. The easier we make it to write and contribute tests, the more tests we&amp;rsquo;re likely to add to the suite. We&amp;rsquo;re doing some work on this in parallel which will likely turn into a blog post of its own in the coming months.&lt;/p&gt;
&lt;p&gt;In our current process, we have a heroic group of volunteers who kindly spend hours on our behalf testing the various flavours - exercising all the possible install paths and validating that what is about to be published is fit for purpose. I&amp;rsquo;d like to ensure that our volunteers&amp;rsquo; time is spent as productively and rewardingly as possible, and I think we can automate much of this testing and allow them to focus on the more complex and nuanced aspects of each release, and raise the quality of Ubuntu across all the flavours.&lt;/p&gt;
&lt;h2 id="whats-next" class="relative group"&gt;What&amp;rsquo;s Next? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#whats-next" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We’re starting by modeling the current release process as it is. Once we&amp;rsquo;ve validated our assumptions about the current process, we’ll layer in improvements by reducing manual gates, parallelising independent steps, introducing more testing, and exercising the process each month to test (and measure) any improvements we&amp;rsquo;ve made.&lt;/p&gt;
&lt;p&gt;My ultimate goal is a release system that’s incredibly &amp;ldquo;boring&amp;rdquo;: transparent, predictable, observable, and easy to reason about (even when things go wrong).&lt;/p&gt;
&lt;p&gt;The new, fully-automated process will likely take several months to complete. When we think we&amp;rsquo;re done, we&amp;rsquo;ll do a release that runs both processes in parallel to ensure we get the outcome we expect before finally sunsetting the old process.&lt;/p&gt;
&lt;p&gt;We’ll be building this work in the open (and &lt;a href="https://canonical.com/careers" target="_blank" rel="noreferrer"&gt;hiring&lt;/a&gt;!) so if you’ve used Temporal in similar contexts, or are curious about contributing to this effort, we’d love to hear from you.&lt;/p&gt;
&lt;p&gt;Until next time!&lt;/p&gt;</description></item><item><title>Adopting sudo-rs By Default in Ubuntu 25.10</title><link>https://jnsgr.uk/2025/05/adopting-sudo-rs-by-default-in-ubuntu/</link><pubDate>Tue, 06 May 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/05/adopting-sudo-rs-by-default-in-ubuntu/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/60583" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Following on from &lt;a href="https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/" target="_blank" rel="noreferrer"&gt;Carefully But Purposefully Oxidising Ubuntu&lt;/a&gt;, Ubuntu will be the first major Linux distribution to adopt &lt;code&gt;sudo-rs&lt;/code&gt; as the default implementation of &lt;code&gt;sudo&lt;/code&gt;, in partnership with the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The change will be effective from the release of Ubuntu 25.10. You can see the Trifecta Tech Foundation&amp;rsquo;s announcement &lt;a href="https://trifectatech.org/blog/memory-safe-sudo-to-become-the-default-in-ubuntu/" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-is-sudo-rs" class="relative group"&gt;What is &lt;code&gt;sudo-rs&lt;/code&gt;? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#what-is-sudo-rs" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;sudo-rs&lt;/code&gt; is a reimplementation of the traditional &lt;code&gt;sudo&lt;/code&gt; tool, written in Rust. It’s being developed by the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation (TTF)&lt;/a&gt;, a nonprofit focused on building secure, open source infrastructure components. The project is part of the Trifecta Tech Foundation&amp;rsquo;s &lt;a href="https://trifectatech.org/initiatives/privilege-boundary/" target="_blank" rel="noreferrer"&gt;Privilege Boundary initiative&lt;/a&gt;, which aims to handle privilege escalation with memory-safe alternatives.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo&lt;/code&gt; command has long served as the defacto means of privilege escalation on Linux. As described in the &lt;a href="https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/" target="_blank" rel="noreferrer"&gt;original post&lt;/a&gt;, Rust provides strong guarantees against certain classes of memory-safety issues, which is pivotal for components at the privilege boundary.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; team is collaborating with &lt;a href="https://www.millert.dev/" target="_blank" rel="noreferrer"&gt;Todd Miller&lt;/a&gt;, who’s maintained the original &lt;code&gt;sudo&lt;/code&gt; for over thirty years. &lt;code&gt;sudo-rs&lt;/code&gt; should not be considered a fork in the road, but rather a handshake across generations of secure systems. Throughout the development of &lt;code&gt;sudo-rs&lt;/code&gt;, the TTF team have also made contributions to enhance the original &lt;code&gt;sudo&lt;/code&gt; implementation.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; project is designed to be a drop in replacement for the original tool. For the vast majority of users, the upgrade should be completely transparent to their workflow. That said, &lt;code&gt;sudo-rs&lt;/code&gt; is a not a &amp;ldquo;blind&amp;rdquo; reimplementation. The developers are taking a &amp;ldquo;less is more&amp;rdquo; approach. This means that some features of the original &lt;code&gt;sudo&lt;/code&gt; may not be reimplemented if they serve only niche, or more recently considered &amp;ldquo;outdated&amp;rdquo; practices.&lt;/p&gt;
&lt;p&gt;Erik Jonkers, Chair of the Trifecta Tech Foundation explains:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While no piece of software - in any language - is flawless, we believe the transition to Rust in systems programming is a vital step forward, it is very exciting to see Ubuntu committing to &lt;code&gt;sudo-rs&lt;/code&gt; and taking the lead in moving the needle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="sponsoring-mainstream-adoption" class="relative group"&gt;Sponsoring Mainstream Adoption &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#sponsoring-mainstream-adoption" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Leading the mainstream adoption of a replacement to such a universally understood tool comes with responsibility. Before committing to ship &lt;code&gt;sudo-rs&lt;/code&gt; in Ubuntu 26.04 LTS, we&amp;rsquo;ll test the transition in Ubuntu 25.10. We&amp;rsquo;re also sponsoring the development of some specific items, which has manifested as &lt;a href="https://trifectatech.org/initiatives/workplans/sudo-rs/#current-work" target="_blank" rel="noreferrer"&gt;Milestone 5&lt;/a&gt; in the upstream project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coarse-grained shell escape prevention (NOEXEC) on Linux (See &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1073" target="_blank" rel="noreferrer"&gt;PR #1073&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The ability to control AppArmor profiles (First &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1067" target="_blank" rel="noreferrer"&gt;PR #1067&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;sudoedit&lt;/code&gt; implementation&lt;/li&gt;
&lt;li&gt;Support for Linux Kernels older than version 5.9&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final item may seem out of place, but because Ubuntu 20.04 LTS is still in support, without this work there could be situations where &lt;code&gt;sudo&lt;/code&gt; fails to function if, for example, a 26.04 LTS OCI container was run on a 20.04 LTS host!&lt;/p&gt;
&lt;p&gt;The team have also already &lt;a href="https://github.com/trifectatechfoundation/sudo-rs/pull/1079" target="_blank" rel="noreferrer"&gt;begun work&lt;/a&gt; on ensuring that the test-suite is as compatible as possible with Ubuntu, to ensure any issues are caught early.&lt;/p&gt;
&lt;p&gt;This isn’t just about shipping a new binary. It’s about setting a direction. We&amp;rsquo;re not abandoning C, or even rewriting all the utilities ourselves, but by choosing to replace one of the most security-critical tools in the system with a memory-safe alternative, we&amp;rsquo;re making a statement: resilience and sustainability are not optional in the future of open infrastructure.&lt;/p&gt;
&lt;h2 id="progress-on-coreutils" class="relative group"&gt;Progress on &lt;code&gt;coreutils&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#progress-on-coreutils" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Since the initial announcement, we&amp;rsquo;ve been working hard to more clearly define a plan for the migration to uutils &lt;code&gt;coreutils&lt;/code&gt; in 25.10 and beyond. Similarly to our engagement with the Trifecta Tech Foundation, we&amp;rsquo;re also sponsoring the uutils project to ensure that some key gaps are closed before we ship 25.10. The sponsorship will primarily cover the development of SELinux support for common commands such as &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;The first step toward developing SELinux support was to &lt;a href="https://github.com/uutils/coreutils/pull/7440/files" target="_blank" rel="noreferrer"&gt;add support for automated testing in Github Actions&lt;/a&gt;, since then the maintainers have begun work on the actual implementation.&lt;/p&gt;
&lt;p&gt;The other feature we&amp;rsquo;re sponsoring is internationalisation support. At present, some of the utility implementations (such as &lt;code&gt;sort&lt;/code&gt;) have an incomplete understanding of locales, and therefore may yield unexpected results. We expect that these two features should land in time for us to ship in 25.10, and we&amp;rsquo;ll continue to work with the uutils project throughout the 26.04 LTS cycle to close any remaining gaps we identify in the interim release.&lt;/p&gt;
&lt;p&gt;One of the major concerns outlined in Julian&amp;rsquo;s post is about binary size. We&amp;rsquo;ve got a few tricks we can play here to get the size down, and there is already some conversation started &lt;a href="https://salsa.debian.org/rust-team/debcargo-conf/-/merge_requests/895" target="_blank" rel="noreferrer"&gt;upstream in Debian&lt;/a&gt; on how that might be achieved. There are also security implications, such as AppArmor’s lack of support for multi-call binaries. We’re currently working with the respective upstreams to discuss addressing this systematically, through in the interim we may need to build small wrapper binaries to enable compatibility with existing AppArmor profiles from the start.&lt;/p&gt;
&lt;h2 id="migration-mechanics" class="relative group"&gt;Migration Mechanics &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#migration-mechanics" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Julian Klode &lt;a href="https://discourse.ubuntu.com/t/migration-to-rust-coreutils-in-25-10/59708" target="_blank" rel="noreferrer"&gt;posted recently&lt;/a&gt; on the Ubuntu Discourse outlining the packaging plan that will enable us both to migrate transparently to uutils &lt;code&gt;coreutils&lt;/code&gt;, but also provide a convenient means for users to opt-out and switch back to GNU &lt;code&gt;coreutils&lt;/code&gt; if they wish, or if they identify a gap in the new implementation. I expect this will be rare, but we want to make sure it&amp;rsquo;s as easy as possible to revert, and will be documenting this in detail before release.&lt;/p&gt;
&lt;p&gt;Replacing coreutils isn&amp;rsquo;t as simple as swapping binaries. As an &lt;code&gt;Essential&lt;/code&gt; package, its replacement must work immediately upon unpacking without relying on maintainer scripts, and without conflicting files across packages. To solve this, we’re introducing new &lt;code&gt;coreutils-from-uutils&lt;/code&gt; and &lt;code&gt;coreutils-from-gnu&lt;/code&gt; packages, as well as &lt;code&gt;coreutils-from&lt;/code&gt; itself. For all the gory details, see the &lt;a href="https://discourse.ubuntu.com/t/migration-to-rust-coreutils-in-25-10/59708" target="_blank" rel="noreferrer"&gt;Discourse post&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The packaging work required to switch to &lt;code&gt;sudo-rs&lt;/code&gt; is somewhat less complicated than with &lt;code&gt;coreutils&lt;/code&gt;. The package is already available in Ubuntu (which you can still test on Ubuntu 24.04, 24.10 and 25.04 with &lt;a href="https://github.com/jnsgruk/oxidizr" target="_blank" rel="noreferrer"&gt;oxidizr&lt;/a&gt;!), but unlike &lt;code&gt;coreutils&lt;/code&gt;, &lt;code&gt;sudo&lt;/code&gt; is not an &lt;code&gt;Essential&lt;/code&gt; package, so we&amp;rsquo;ll be able to make use of the Debian &lt;a href="https://wiki.debian.org/DebianAlternatives" target="_blank" rel="noreferrer"&gt;alternatives&lt;/a&gt; system for the transition.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Things are progressing nicely. We’ve established strong, productive relationships and are sponsoring work upstream to make these transitions viable.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve got a strategy for migrating the default implementation of &lt;code&gt;coreutils&lt;/code&gt; and &lt;code&gt;sudo&lt;/code&gt; in Ubuntu 25.10 which will enable a seamless revert in cases where that is desired. While &lt;code&gt;sudo-rs&lt;/code&gt; will be the default in 25.10, the original &lt;code&gt;sudo&lt;/code&gt; will remain available for users who need it, and we’ll be gathering feedback to ensure a smooth transition before the 26.04 LTS.&lt;/p&gt;
&lt;p&gt;Additionally, we&amp;rsquo;ve begun investigating the feasibility of providing &lt;a href="https://sequoia-pgp.org/" target="_blank" rel="noreferrer"&gt;SequoiaPGP&lt;/a&gt; and using it in APT instead of GnuPG. SequoiaPGP is a new OpenPGP library with a focus on safety and correctness, written in Rust. The GnuPG maintainers have recently forked the OpenPGP standard and are no longer compliant with it. Sequoia provides a modern alternative to GnuPG with strict behavior, and is already used in various other systems. More details to follow!&lt;/p&gt;
&lt;p&gt;Stay tuned!&lt;/p&gt;</description></item><item><title>Revitalising Ubuntu Project Documentation</title><link>https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/58694" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Back in February I &lt;a href="https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/" target="_blank" rel="noreferrer"&gt;wrote some thoughts&lt;/a&gt; on Ubuntu&amp;rsquo;s documentation and its role within the community. For a mostly-online software community, documentation is one of our most critical forms of communication.&lt;/p&gt;
&lt;p&gt;In the last two years there has been lots of focus on the technical aspects of our documentation (how-to guides, tutorials, etc.), but I&amp;rsquo;d like to focus more on what I&amp;rsquo;m calling the &amp;ldquo;Ubuntu Project Documentation&amp;rdquo; over the coming months.&lt;/p&gt;
&lt;p&gt;Documentation isn&amp;rsquo;t only about technical how-to guides and tutorials, nor is it only about troubleshooting or satisfying particular use-cases. Our documentation can set the tone for the project, give a means for the community to state an intent, and guide both current and future contributors in their daily work.&lt;/p&gt;
&lt;p&gt;Ubuntu has a lot of documentation, most of which has grown organically over the last 20 years, but it&amp;rsquo;s not always easy to find or understand. Our documentation should illuminate and inspire a path to contribution. It should provide direction and clarity on complex issues, reference on technology and past decisions, and precision in the execution of process.&lt;/p&gt;
&lt;p&gt;Our project documentation should detail what makes Ubuntu happen. How are decisions made? What are the teams contributing to Ubuntu? How are those teams appointed? What are their responsibilities? If you&amp;rsquo;re on the Main Inclusion Review (MIR) team and you&amp;rsquo;re assigned a package to review, what steps should you take? How does package sponsorship work, and who should you contact if you&amp;rsquo;re stuck? How are the Access Control Lists (ACLs) updated for packages and package sets, and who can make those changes? What does the journey look like from first time package bug-fixer to Ubuntu Core Developer?&lt;/p&gt;
&lt;p&gt;These are all examples of questions that we, the collective conscious of Ubuntu, know the answers to, yet it is still difficult to find up-to-date answers to these questions, often requiring input from some of our busiest and most knowledgeable contributors to settle discussions and answer basic queries.&lt;/p&gt;
&lt;p&gt;If a potential contributor identifies a bug in a package, there should be one authoritative source of information on where the package source can be located, how it can be pulled, built and tested, and how to work with a sponsor to land changes. Such a process is satisfying for contributors, making it more likely they&amp;rsquo;ll stay engaged, and therefore benefits the distribution&amp;rsquo;s longevity and sustainability.&lt;/p&gt;
&lt;p&gt;Answering questions and mentoring people will remain a central part of our community&amp;rsquo;s role, but many of the first questions asked could be serviced by better documentation.&lt;/p&gt;
&lt;h2 id="the-challenge" class="relative group"&gt;The Challenge &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-challenge" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Much of the required content already exists. The venerable &lt;a href="https://wiki.ubuntu.com/" target="_blank" rel="noreferrer"&gt;Ubuntu Wiki&lt;/a&gt; was the go-to destination for such documentation, but has become outdated both technologically and in the content it serves. This degradation gained pace as we diversified the number of destinations that documentation could live: the Wiki, Discourse, Github, Launchpad, etc.&lt;/p&gt;
&lt;p&gt;The Ubuntu Community team have made significant efforts over the past months to centralise the documentation for &lt;a href="https://ubuntu.com/community/membership" target="_blank" rel="noreferrer"&gt;membership&lt;/a&gt;, our &lt;a href="https://ubuntu.com/community/ethos/code-of-conduct" target="_blank" rel="noreferrer"&gt;code of conduct&lt;/a&gt; and project &lt;a href="https://ubuntu.com/community/governance" target="_blank" rel="noreferrer"&gt;governance&lt;/a&gt;. I also called out the renewed &lt;a href="https://documentation.ubuntu.com/sru/en/latest/" target="_blank" rel="noreferrer"&gt;Stable Release Update (SRU)&lt;/a&gt; documentation in my first post for having made its first steps toward a new and improved structure.&lt;/p&gt;
&lt;p&gt;These examples prove we have all the skills we need to write &lt;em&gt;excellent&lt;/em&gt; documentation. Throughout the 25.10 cycle, I intend to put some focus on this, consolidating as much of our content into modern formats as possible, thereby making it as accessible as possible.&lt;/p&gt;
&lt;p&gt;In doing this work, I hope to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Illustrate contributor journeys across disciplines&lt;/li&gt;
&lt;li&gt;Create resilience in the project by reducing the &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Bus_factor" target="_blank" rel="noreferrer"&gt;bus factor&lt;/a&gt;&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Increase the accessibility and ergonomics of our documentation&lt;/li&gt;
&lt;li&gt;Enable more efficient, asynchronous collaboration on a wide range of tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="end-goal" class="relative group"&gt;End Goal &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#end-goal" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;To quote the &lt;a href="https://canonical.com/documentation" target="_blank" rel="noreferrer"&gt;Canonical.com&lt;/a&gt; page on Documentation Practice:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;we have embarked on a comprehensive, long-term project to transform documentation. Our aim is to create and maintain documentation product and practice that will represent a standard of excellence. We want documentation to be the best it possibly can be.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At the heart of this mission &lt;a href="https://diataxis.fr/" target="_blank" rel="noreferrer"&gt;Diátaxis&lt;/a&gt;: a way of thinking about documentation. Diátaxis &amp;ldquo;prescribes approaches to content, architecture and form that emerge from a systematic approach to understanding the needs of documentation users&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll have seen Diátaxis in use across many of our product documentation pages: the &lt;a href="https://documentation.ubuntu.com/juju/3.6/" target="_blank" rel="noreferrer"&gt;Juju docs&lt;/a&gt;, the &lt;a href="https://maas.io/docs" target="_blank" rel="noreferrer"&gt;MAAS docs&lt;/a&gt;, the &lt;a href="https://documentation.ubuntu.com/pebble/" target="_blank" rel="noreferrer"&gt;Pebble docs&lt;/a&gt;, the &lt;a href="https://documentation.ubuntu.com/rockcraft/en/latest/" target="_blank" rel="noreferrer"&gt;Rockcraft docs&lt;/a&gt; and many more.&lt;/p&gt;
&lt;p&gt;Most of those existing sites are specific - they document a particular &lt;em&gt;product&lt;/em&gt; or &lt;em&gt;ecosystem&lt;/em&gt; which neatly scopes the documentation structure, but the Diátaxis framework can also be used to bring structure, precision and clarity to the documentation of the Ubuntu project as a whole.&lt;/p&gt;
&lt;p&gt;Earlier this month I surveyed the various documentation sites in use by Canonical and the Ubuntu Community, and settled on three common themes around which we will structure our renewed project documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Governance&lt;/strong&gt;: in which membership, code of conduct, team structures, communication practices, delegation, mission, software licensing and 3rd-party software guidelines will be documented.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Develop Ubuntu&lt;/strong&gt;: documentation for current and aspiring Ubuntu developers, including how to package software for Ubuntu, how to merge packages from Debian, how to sponsor packages, how to use &lt;code&gt;git-ubuntu&lt;/code&gt; and conduct &amp;ldquo;&lt;a href="https://wiki.ubuntu.com/PlusOneMaintenanceTeam" target="_blank" rel="noreferrer"&gt;+1 Maintenance&lt;/a&gt;&amp;rdquo;, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Archive Administration&lt;/strong&gt;: the nuts and bolts of managing Ubuntu&amp;rsquo;s prolific software repositories: how to manage seeds, configure phased updates, conduct an MIR, run an SRU process, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These categories were not immediately obvious, and they&amp;rsquo;re not necessarily mutually exclusive, but they fell out quite naturally when trying to logically organise our existing content.&lt;/p&gt;
&lt;p&gt;During the process, I came up with this rough sketch:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_33eb0915229d8456.webp 330w,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_7a7a7ec846696071.webp 660w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_201b198e9ec6a925.webp 1024w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_f16f633e9202adc.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1456"
height="1445"
class="mx-auto my-0 rounded-md"
alt="an outline of how our Ubuntu Project documentation might be structured"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_b6a78828bc273f24.png" srcset="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_66850f0a42a7fa52.png 330w,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_b6a78828bc273f24.png 660w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_6fc297bdd4b822b0.png 1024w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/02_hu_5724714e3f85011e.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This illustrates how multiple categories of documentation from different corners of Ubuntu might come together in a single landing page. To give an idea of how we might further break down existing content by type, then category:&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_461980ebada284ec.webp 330w,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_ea902b706ac7a8c7.webp 660w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_983d860a85a74b80.webp 800w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_983d860a85a74b80.webp 800w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="800"
height="910"
class="mx-auto my-0 rounded-md"
alt="an outline of how our Ubuntu Project documentation TOC might be structured"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_ce266968f87a5ceb.png" srcset="https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_774bfd2e138d3eb3.png 330w,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03_hu_ce266968f87a5ceb.png 660w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03.png 800w
,https://jnsgr.uk/2025/04/revitalising-ubuntu-project-documentation/03.png 800w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This may not be the final structure, but it&amp;rsquo;s indicative of how we can use Diátaxis to break down large documentation premises into smaller, more digestible and more ergonomic pieces.&lt;/p&gt;
&lt;h2 id="the-plan" class="relative group"&gt;The Plan &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-plan" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;During the Ubuntu 25.10 cycle, we&amp;rsquo;ll be dedicating two of our &lt;a href="https://canonical.com/documentation/work-and-careers" target="_blank" rel="noreferrer"&gt;Technical Authors&lt;/a&gt; to make this happen. One of these authors has been largely responsible for overhauling the &lt;a href="https://documentation.ubuntu.com/server/" target="_blank" rel="noreferrer"&gt;Ubuntu Server docs&lt;/a&gt;, but both are very familiar with Diátaxis and the tooling we using to deliver documentation.&lt;/p&gt;
&lt;p&gt;Throughout this process, we&amp;rsquo;ll likely come across outdated, poorly reviewed or incorrect documentation, but as we work through the process of consolidating, we can note where this has happened and get it on our backlog to fix.&lt;/p&gt;
&lt;p&gt;Perhaps we&amp;rsquo;ll find items which lend themselves to inclusion in the &lt;a href="https://canonical.com/documentation/open-documentation-academy" target="_blank" rel="noreferrer"&gt;Canonical Open Documentation Academy&lt;/a&gt;, or maybe we&amp;rsquo;ll need to reach out to some of our less active community members for clarification, but once the structure is in place we&amp;rsquo;ll at least have a place to collaborate.&lt;/p&gt;
&lt;p&gt;Once the transition is complete, there will be an authoritative source for project documentation that is easy to navigate, easy to contribute to and with a well-defined review process that encourages progress over gatekeeping.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Documentation is the backbone of a thriving open-source community, guiding contributors, setting expectations, and ensuring long-term sustainability.&lt;/p&gt;
&lt;p&gt;While Ubuntu has extensive documentation, much of it is scattered, outdated, or difficult to navigate. By leveraging the Diátaxis framework, we aim to bring structure, clarity, and accessibility to Ubuntu Project Documentation. Our focus will be on governance, development, and archive administration, ensuring that key processes and responsibilities are well-documented and easy to follow.&lt;/p&gt;
&lt;p&gt;With dedicated technical authors and community collaboration, the Ubuntu 25.10 cycle will mark a significant step toward making our documentation searchable, structured, and sustainable.&lt;/p&gt;
&lt;p&gt;I hope this effort will empower contributors, reduce reliance on institutional knowledge, and create a more resilient project for the next generation of Ubuntu developers and users.&lt;/p&gt;</description></item><item><title>Carefully But Purposefully Oxidising Ubuntu</title><link>https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/03/carefully-but-purposefully-oxidising-ubuntu/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Last month I published &lt;a href="https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/" target="_blank" rel="noreferrer"&gt;Engineering Ubuntu For The Next 20 Years&lt;/a&gt;, which outlines four key themes for how I intend to evolve Ubuntu in the coming years. In this post, I&amp;rsquo;ll focus on &amp;ldquo;Modernisation&amp;rdquo;. There are many areas we could look to modernise in Ubuntu: we could focus on the graphical shell experience, the virtualisation stack, core system utilities, default shell utilities, etc.&lt;/p&gt;
&lt;p&gt;Over the years, projects like GNU Coreutils have been instrumental in shaping the Unix-like experience that Ubuntu and other Linux distributions ship to millions of users. According to the GNU &lt;a href="https://www.gnu.org/software/coreutils/" target="_blank" rel="noreferrer"&gt;website&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The GNU Core Utilities are the basic file, shell and text manipulation utilities of the GNU operating system. These are the core utilities which are expected to exist on every operating system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This package provides utilities which have become synonymous with Linux to many - the likes of &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, and &lt;code&gt;mv&lt;/code&gt;. In recent years, there has been an &lt;a href="https://uutils.github.io/" target="_blank" rel="noreferrer"&gt;effort&lt;/a&gt; to reimplement this suite of tools in Rust, with the goal of reaching 100% compatibility with the existing tools. Similar projects, like &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;sudo-rs&lt;/a&gt;, aim to replace key security-critical utilities with more modern, memory-safe alternatives.&lt;/p&gt;
&lt;p&gt;Starting with Ubuntu 25.10, my goal is to adopt some of these modern implementations as the default. My immediate goal is to make uutils&amp;rsquo; coreutils implementation the default in Ubuntu 25.10, and subsequently in our next Long Term Support (LTS) release, Ubuntu 26.04 LTS, if the conditions are right.&lt;/p&gt;
&lt;h2 id="but-why" class="relative group"&gt;But… why? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#but-why" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Performance is a frequently cited rationale for &amp;ldquo;Rewrite it in Rust&amp;rdquo; projects. While performance is high on my list of priorities, it&amp;rsquo;s not the primary driver behind this change. These utilities are at the heart of the distribution - and it&amp;rsquo;s the enhanced resilience and safety that is more easily achieved with Rust ports that are most attractive to me.&lt;/p&gt;
&lt;p&gt;The Rust language, its type system and its borrow checker (and its community!) work together to encourage developers to write safe, sound, resilient software. With added safety comes an increase in security guarantees, and with an increase in security comes an increase in overall resilience of the system - and where better to start than with the foundational tools that build the distribution?&lt;/p&gt;
&lt;p&gt;I recently read an &lt;a href="https://smallcultfollowing.com/babysteps/blog/2025/03/10/rust-2025-intro/" target="_blank" rel="noreferrer"&gt;article&lt;/a&gt; about targeting foundational software with Rust in 2025. Among other things, the article asserts that &amp;ldquo;foundational software needs performance, reliability — and productivity&amp;rdquo;. If foundational software fails, so do all of the other layers built on top. If foundational packages have performance bottlenecks, they become a floor on the performance achievable by the layers above.&lt;/p&gt;
&lt;p&gt;Ubuntu powers millions of devices around the world, from servers in your data centre, to safety critical systems in autonomous systems, so it behooves us to be absolutely certain we&amp;rsquo;re shipping the most resilient and trustworthy software we can.&lt;/p&gt;
&lt;p&gt;There are lots of ways to achieve this: we can provide &lt;a href="https://canonical.com/blog/12-year-lts-for-kubernetes" target="_blank" rel="noreferrer"&gt;long term support for projects like Kubernetes&lt;/a&gt;, we can &lt;a href="https://canonical.com/blog/canonicals-commitment-to-quality-management" target="_blank" rel="noreferrer"&gt;assure the code we write&lt;/a&gt;, and we can &lt;a href="https://canonical.com/blog/canonical-achieves-iso-21434-certification" target="_blank" rel="noreferrer"&gt;strive to achieve compliance with safety-centric standards&lt;/a&gt;, but another is by shipping software with the values of safety, soundness, correctness and resilience at their core.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not to throw shade on the existing implementations, of course. Many of these tools have been stable for many years, quietly improving performance and fixing bugs. A lovely side benefit of working on newer implementations, is that it &lt;a href="https://ferrous-systems.com/blog/testing-sudo-rs/" target="_blank" rel="noreferrer"&gt;sometimes facilitates&lt;/a&gt; improvements in the original upstream projects, too!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve written about my desire to increase the number of Ubuntu contributors, and I think projects like this will help. Rust may present a steeper learning curve than C in some ways, but by providing such a strong framework around the use of memory it also lowers the chances that a contributor accidentally commits potentially unsafe code.&lt;/p&gt;
&lt;h2 id="introducing-oxidizr" class="relative group"&gt;Introducing &lt;code&gt;oxidizr&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introducing-oxidizr" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I did my homework before writing this post. I wanted to see how easy it was for me to live with these newer implementations and get a sense of their readiness for prime-time within the distribution. I also wanted a means of toggling between implementations so that I could easily switch back should I run into incompatibilities - and so &lt;a href="https://github.com/jnsgruk/oxidizr" target="_blank" rel="noreferrer"&gt;&lt;code&gt;oxidizr&lt;/code&gt;&lt;/a&gt; was born!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;oxidizr&lt;/code&gt; is a command-line utility for managing system experiments that replace traditional Unix utilities with modern Rust-based alternatives on Ubuntu systems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;oxidizr&lt;/code&gt; utility enables you to quickly swap in and out newer implementations of certain packages with &lt;em&gt;relatively&lt;/em&gt; low risk. It has the notion of &lt;em&gt;Experiments&lt;/em&gt;, where each experiment is a package that already exists in the archive that can be swapped in as an alternative to the default.&lt;/p&gt;
&lt;p&gt;Version &lt;a href="https://github.com/jnsgruk/oxidizr/releases/tag/v1.0.0" target="_blank" rel="noreferrer"&gt;1.0.0&lt;/a&gt; supports the following experiments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/coreutils" target="_blank" rel="noreferrer"&gt;uutils coreutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/findutils" target="_blank" rel="noreferrer"&gt;uutils findutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/uutils/diffutils" target="_blank" rel="noreferrer"&gt;uutils diffutils&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;sudo-rs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-does-it-work" class="relative group"&gt;How does it work? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-does-it-work" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Each experiment is subtly different since the paths of the utilities being replaced vary, but the process for enabling an experiment is generally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the alternative package (e.g. &lt;code&gt;apt install rust-coreutils&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For each binary shipped in the new package:
&lt;ul&gt;
&lt;li&gt;Lookup the default path for that utility (e.g &lt;code&gt;which date&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Back up that file (e.g. &lt;code&gt;cp /usr/bin/date /usr/bin/.date.oxidizr.bak&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Symlink the new implementation in place (e.g. &lt;code&gt;ln -s /usr/bin/coreutils /usr/bin/date&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is also the facility to &amp;ldquo;disable&amp;rdquo; an experiment, which does the reverse of the sequence above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For each binary shipped in the new package:
&lt;ul&gt;
&lt;li&gt;Lookup the default path for the utility (e.g &lt;code&gt;which date&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Check for and restore any backed up versions (e.g &lt;code&gt;cp /usr/bin/.date.oxidizr.bak /usr/bin/date&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Uninstall the package (e.g. &lt;code&gt;apt remove rust-coreutils&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thereby returning the system back to its original state! The tool is covered by a suite of integration tests which illustrate this behaviour which you can find &lt;a href="https://github.com/jnsgruk/oxidizr/tree/ca955677b4f5549e5d7f06726f5c5cf1846fe448/tests" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="get-started" class="relative group"&gt;Get started &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#get-started" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;blockquote&gt;
&lt;p&gt;⚠️ WARNING ⚠️: &lt;code&gt;oxidizr&lt;/code&gt; is an experimental tool to play with alternatives to foundational system utilities. It may cause a loss of data, or prevent your system from booting, so use with caution!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are a couple of ways to get &lt;code&gt;oxidizr&lt;/code&gt; on your system. If you already use &lt;code&gt;cargo&lt;/code&gt;, you can do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install --git https://github.com/jnsgruk/oxidizr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Otherwise, you can download and install binary releases from &lt;a href="https://github.com/jnsgruk/oxidizr/releases" target="_blank" rel="noreferrer"&gt;Github&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Download version 1.0.0 and extract to /usr/bin/oxidizr&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sL &lt;span class="s2"&gt;&amp;#34;https://github.com/jnsgruk/oxidizr/releases/download/v1.0.0/oxidizr_Linux_&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -m&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tar -xvzf - -C /usr/bin oxidizr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Once installed you can invoke &lt;code&gt;oxidizr&lt;/code&gt; to selectively enable/disable experiments. The default set of experiments in &lt;code&gt;v1.0.0&lt;/code&gt; is &lt;code&gt;rust-coreutils&lt;/code&gt; and &lt;code&gt;sudo-rs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable default experiments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Disable default experiments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr disable
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable just coreutils&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt; --experiments coreutils
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable all experiments without prompting with debug logging enabled&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr &lt;span class="nb"&gt;enable&lt;/span&gt; --all --yes -v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Disable all experiments without prompting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo oxidizr disable --all --yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The tool should work on all versions of Ubuntu after 24.04 LTS - though the &lt;code&gt;diffutils&lt;/code&gt; experiment is only available from Ubuntu 24.10 onward.&lt;/p&gt;
&lt;p&gt;The tool itself is stable and well covered with unit and integration tests, but nonetheless I&amp;rsquo;d urge you to start with a test virtual machine or a machine that &lt;em&gt;isn&amp;rsquo;t&lt;/em&gt; your production workstation or server! I&amp;rsquo;ve been running the &lt;code&gt;coreutils&lt;/code&gt; and &lt;code&gt;sudo-rs&lt;/code&gt; experiments for around 2 weeks now on my Ubuntu 24.10 machines and haven&amp;rsquo;t had many issues (more on that below…).&lt;/p&gt;
&lt;h2 id="how-to-help" class="relative group"&gt;How to Help &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#how-to-help" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re interested in helping out on this mission, then I&amp;rsquo;d encourage you to play with the packages, either by installing them yourself or using &lt;code&gt;oxidizr&lt;/code&gt;. Reply to the Discourse post with your experiences, file bugs and perhaps even dedicate some time to the relevant upstream projects to help with resolving bugs, implementing features or improving documentation, depending on your skill set.&lt;/p&gt;
&lt;p&gt;You can also join us to discuss on our &lt;a href="https://ubuntu.com/community/communications/matrix/onboarding" target="_blank" rel="noreferrer"&gt;Matrix instance&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="next-steps" class="relative group"&gt;Next Steps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#next-steps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Earlier this week, I met with &lt;a href="https://github.com/sylvestre" target="_blank" rel="noreferrer"&gt;@sylvestre&lt;/a&gt; to discuss my proposal to make uutils coreutils the default in Ubuntu 25.10. I was pleased to hear that he feels the project is ready for that level of exposure, so now we just need to work out the specifics. The Ubuntu Foundations team is already working up a plan for next cycle.&lt;/p&gt;
&lt;p&gt;There will certainly be a few rough edges we&amp;rsquo;ll need to work out. In my testing, for example, the only incompatibility I&amp;rsquo;ve come across is that the &lt;code&gt;update-initramfs&lt;/code&gt; script for Ubuntu uses &lt;code&gt;cp -Z&lt;/code&gt; to preserve &lt;code&gt;selinux&lt;/code&gt; labels when copying files. The &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt; commands from uutils &lt;a href="https://github.com/uutils/coreutils/issues/2404" target="_blank" rel="noreferrer"&gt;don&amp;rsquo;t yet support&lt;/a&gt; the &lt;code&gt;-Z&lt;/code&gt; flag, but I think we&amp;rsquo;ve worked out a way to unblock that work going forward, both in the upstream and in the next release of Ubuntu.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to do some more digging on &lt;a href="https://github.com/trifectatechfoundation/sudo-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;sudo-rs&lt;/code&gt;&lt;/a&gt; over the coming weeks, with a view to assessing a similar transition.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m really excited to see so much investment in the foundational utilities behind Linux. The uutils project seems to be picking up speed after their recent &lt;a href="https://fosdem.org/2025/schedule/event/fosdem-2025-6196-rewriting-the-future-of-the-linux-essential-packages-in-rust-/" target="_blank" rel="noreferrer"&gt;appearance at FOSDEM 2025&lt;/a&gt;, with efforts ongoing to rework &lt;a href="https://github.com/uutils/procps" target="_blank" rel="noreferrer"&gt;procps&lt;/a&gt;, &lt;a href="https://github.com/uutils/util-linux" target="_blank" rel="noreferrer"&gt;util-linux&lt;/a&gt; and more.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sudo-rs&lt;/code&gt; project is now maintained by the &lt;a href="https://trifectatech.org/" target="_blank" rel="noreferrer"&gt;Trifecta Tech Foundation&lt;/a&gt;, who are focused on &amp;ldquo;open infrastructure software in the public interest&amp;rdquo; . Their &lt;a href="https://github.com/trifectatechfoundation/zlib-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zlib-rs&lt;/code&gt;&lt;/a&gt; recently released v0.4.2, which appears to now be &lt;a href="https://trifectatech.org/blog/zlib-rs-is-faster-than-c/" target="_blank" rel="noreferrer"&gt;the fastest API-compatible zlib implementation&lt;/a&gt;. They&amp;rsquo;re also behind the &lt;a href="https://github.com/pendulum-project" target="_blank" rel="noreferrer"&gt;Pendulum Project&lt;/a&gt; and &lt;a href="https://github.com/pendulum-project/ntpd-rs" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ntpd-rs&lt;/code&gt;&lt;/a&gt; for memory-safe time synchronisation.&lt;/p&gt;
&lt;p&gt;With Ubuntu, we&amp;rsquo;re in a position to drive awareness and adoption of these modern equivalents by making them either trivially available, or the default implementation for the world&amp;rsquo;s most deployed Linux distribution.&lt;/p&gt;
&lt;p&gt;We will need to do so carefully, and be willing to scale back on the ambition where appropriate to avoid diluting the promise of stability and reliability that the Ubuntu LTS releases have become known for, but I&amp;rsquo;m confident that we can make progress on these topics over the coming months.&lt;/p&gt;</description></item><item><title>Own Your Calendar</title><link>https://jnsgr.uk/2025/03/own-your-calendar/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/03/own-your-calendar/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Over the past few years, I&amp;rsquo;ve come to a series of realisations about how I can get the most from my calendar, or rather how to get the most from my time at work, while preventing work from spilling out into my personal life too much.&lt;/p&gt;
&lt;p&gt;Staying on top of my calendar has helped me manage my time better. It helps me get more done, and set reasonable work-life boundaries. Calendar management helps my colleagues understand how I spend my time, what I&amp;rsquo;m working on, and when it might be best to book my time.&lt;/p&gt;
&lt;h2 id="bound-your-day" class="relative group"&gt;Bound Your Day &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#bound-your-day" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The first step toward taking control of your calendar is to bound your day. In more conventional workplaces where everyone commutes into an office, working patterns are frequently more predictable. With a remote, globally distributed workforce things are harder to predict. Your 9-5 might be in the middle of the night for others, and either you or your colleagues might have flexible working arrangements that make their work hours less obvious.&lt;/p&gt;
&lt;p&gt;One way to help colleagues book time with you smoothly is by clearly indicating when your day starts, when it ends, and when you&amp;rsquo;ll break for lunch. Your calendar tool might do a good job of this already.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an indicative example of how I set up my calendar:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/03/own-your-calendar/01_hu_c22420f58e86b327.webp 330w,https://jnsgr.uk/2025/03/own-your-calendar/01_hu_ca3b43a0edfcb222.webp 660w
,https://jnsgr.uk/2025/03/own-your-calendar/01_hu_1ca38b2df4caa3db.webp 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/01_hu_5369a19e3ae6d3d0.webp 1155w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1155"
height="942"
class="mx-auto my-0 rounded-md"
alt="A baseline empty calendar showing start and end of day, as well as a lunch break."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/03/own-your-calendar/01_hu_eb19094a42a2aa55.png" srcset="https://jnsgr.uk/2025/03/own-your-calendar/01_hu_f0c0ce05a9b949f6.png 330w,https://jnsgr.uk/2025/03/own-your-calendar/01_hu_eb19094a42a2aa55.png 660w
,https://jnsgr.uk/2025/03/own-your-calendar/01_hu_464c632f2bb8989f.png 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/01.png 1155w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Some of you are raising your eyebrow over taking an &lt;em&gt;hour for lunch&lt;/em&gt; every day, but hear me out. This time is not just used for the critical act of feeding yourself, but gives you the chance to get some fresh air, rest your eyes and give your brain a chance to focus on something else for a while. How many times have you solved a complicated problem in the shower? This is the same thing.&lt;/p&gt;
&lt;p&gt;Indicating to your colleagues through status messages that you&amp;rsquo;re on a break on Slack/Mattermost/Teams/whatever can also be helpful.&lt;/p&gt;
&lt;p&gt;There may be occasions when people need to get hold of you &lt;em&gt;right now&lt;/em&gt;, and you should let them know how to do that. For me, the &lt;a href="https://en.wikipedia.org/wiki/Bat_phone" target="_blank" rel="noreferrer"&gt;bat phone&lt;/a&gt; of choice is &lt;a href="https://signal.org/" target="_blank" rel="noreferrer"&gt;Signal&lt;/a&gt;. No matter how much I silence my work email notifications, or use Do Not Disturb on Mattermost, Signal is the only app that never gets silenced because it&amp;rsquo;s what I use to communicate with those closest to me - so my colleagues know to use that if they need to, and that frees me up to concentrate on the day and not get too distracted by a stream of instant messages and emails.&lt;/p&gt;
&lt;h2 id="make-space-for-focus" class="relative group"&gt;Make Space For Focus &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#make-space-for-focus" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;There are two kinds of focus time I need in my day: one to triage emails and messages, respond to &amp;ldquo;new tasks&amp;rdquo; and another to progress &amp;ldquo;planned work&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Like many others, I get a lot of email and instant messages. Because of the global nature of my company, these tend to keep coming overnight. I like to set aside 30 minutes every morning assigned to &amp;ldquo;Catch Up&amp;rdquo;. In that time, I respond to emails and messages I&amp;rsquo;ve received overnight, review my schedule for the rest of the day, and set up &lt;a href="https://obsidian.md" target="_blank" rel="noreferrer"&gt;Obsidian&lt;/a&gt; for note-taking (see &lt;a href="https://jnsgr.uk/2024/07/how-i-computer-in-2024/#productivity-apps" target="_blank" rel="noreferrer"&gt;How I Computer in 2024&lt;/a&gt;). As a side-effect of this, I&amp;rsquo;m always up to date by the time I start my first meeting each day.&lt;/p&gt;
&lt;p&gt;For planned work, I set aside at least one hour per day. This is often neglected, leading to people becoming overwhelmed by meetings: group meetings, team 1:1s, daily/weekly/fortnightly/monthly rituals, leadership meetings, etc. Before long, there&amp;rsquo;s no slack in their day to actually &lt;em&gt;think&lt;/em&gt; about their work.&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/03/own-your-calendar/02_hu_8bfb5d3db7dbc906.webp 330w,https://jnsgr.uk/2025/03/own-your-calendar/02_hu_ed26453af1f10233.webp 660w
,https://jnsgr.uk/2025/03/own-your-calendar/02_hu_90ffd221078d281b.webp 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/02_hu_c0fc93c1477aa032.webp 1155w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1155"
height="942"
class="mx-auto my-0 rounded-md"
alt="Daily focus time slots placed into the calendar, yet to be labelled."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/03/own-your-calendar/02_hu_cf638e4373a2109c.png" srcset="https://jnsgr.uk/2025/03/own-your-calendar/02_hu_c9326ac996778f90.png 330w,https://jnsgr.uk/2025/03/own-your-calendar/02_hu_cf638e4373a2109c.png 660w
,https://jnsgr.uk/2025/03/own-your-calendar/02_hu_9e8b4592ea31b3e8.png 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/02.png 1155w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An hour a day is a minimum, but at least by blocking it out you guarantee that minimum. Depending on your role and responsibilities, these slots may occupy more of less of your week.&lt;/p&gt;
&lt;p&gt;These occur at the same times each week in my calendar where possible, making it easier for my colleagues to plan meetings when they need to. I label these slots with what I&amp;rsquo;ll focus on in that time. In my calendar these are recurring events named &amp;ldquo;Focus Time&amp;rdquo;, then every Monday during my &amp;ldquo;Catch Up&amp;rdquo; slot, I figure out what&amp;rsquo;s important to get done in that week, and label each slot with what I plan to work on. You might label a slot with the Jira ticket you&amp;rsquo;ll tackle, the specific Professional Development activity you&amp;rsquo;re planning, the document you&amp;rsquo;ll review or maybe it&amp;rsquo;s a Pair Programming session with a colleague.&lt;/p&gt;
&lt;p&gt;In my experience, this practice helps my colleagues understand what I&amp;rsquo;m working on, but on busy days it also helps me avoid wasting time deciding what to work on when I become free between meetings - my calendar tells me!&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;ve conducted my Monday &amp;ldquo;Catch Up&amp;rdquo; each week, the focus blocks in my calendar look more like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/03/own-your-calendar/03_hu_38624cdbf5366180.webp 330w,https://jnsgr.uk/2025/03/own-your-calendar/03_hu_e716a12daacc79fd.webp 660w
,https://jnsgr.uk/2025/03/own-your-calendar/03_hu_d82e95ba66483c0c.webp 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/03_hu_656aecac4894bdcc.webp 1155w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1155"
height="942"
class="mx-auto my-0 rounded-md"
alt="Daily focus time slots now labelled with planned tasks for the week."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/03/own-your-calendar/03_hu_f8d1021ae4b35f6a.png" srcset="https://jnsgr.uk/2025/03/own-your-calendar/03_hu_e091a88c6d42e433.png 330w,https://jnsgr.uk/2025/03/own-your-calendar/03_hu_f8d1021ae4b35f6a.png 660w
,https://jnsgr.uk/2025/03/own-your-calendar/03_hu_97048a630b14de2e.png 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/03.png 1155w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the above example, I&amp;rsquo;ve extended the Thursday slot to get something specific done. I avoid planning contiguous focus blocks for more than 2 or 3 hours. Much beyond that, and most people will begin to lose focus, become less effective and more frustrated, ultimately getting less done. I find I&amp;rsquo;m better taking a break to do something else, then coming back to the task later with fresh eyes.&lt;/p&gt;
&lt;h2 id="plan-regular-meetings" class="relative group"&gt;Plan Regular Meetings &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#plan-regular-meetings" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I try to be deliberate about the meetings I attend. In most roles there are meetings which are an inescapable reality. These might include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Team 1:1s&lt;/li&gt;
&lt;li&gt;Leadership Syncs&lt;/li&gt;
&lt;li&gt;Project Reviews&lt;/li&gt;
&lt;li&gt;Planning Meetings&lt;/li&gt;
&lt;li&gt;Mentoring/Coaching&lt;/li&gt;
&lt;li&gt;Demos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These will vary in length and cadence. Try to schedule your most demanding meetings at the time of day when you&amp;rsquo;re at your best; for me this translates into scheduling most of my team 1:1s in the morning (timezones permitting!).&lt;/p&gt;
&lt;p&gt;If you have regular fortnightly meetings, try to ensure that there is something scheduled on the &amp;ldquo;off weeks&amp;rdquo; at the same time. This will prevent accidentally scheduling a weekly meeting in a slot where you&amp;rsquo;re already committed fortnightly - a surprisingly easy mistake.&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/03/own-your-calendar/04_hu_35d08a70d315e207.webp 330w,https://jnsgr.uk/2025/03/own-your-calendar/04_hu_55b8aa7b98328602.webp 660w
,https://jnsgr.uk/2025/03/own-your-calendar/04_hu_4ff6c3f1a4a873d4.webp 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/04_hu_ad01e6f618aaa04a.webp 1155w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1155"
height="942"
class="mx-auto my-0 rounded-md"
alt="Regular meetings planned into the schedule."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/03/own-your-calendar/04_hu_f452fafbdf770192.png" srcset="https://jnsgr.uk/2025/03/own-your-calendar/04_hu_f8a2beb76ec541b0.png 330w,https://jnsgr.uk/2025/03/own-your-calendar/04_hu_f452fafbdf770192.png 660w
,https://jnsgr.uk/2025/03/own-your-calendar/04_hu_21847060a74a9d27.png 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/04.png 1155w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I find it helpful to label meetings with their cadence. This may not help day-to-day, but can help in reviewing how you spend your time (more on that later).&lt;/p&gt;
&lt;p&gt;In my current role, I attend a business review every six weeks. In these weeks, Tuesday and Thursday afternoons are consumed by business review activity. As a result, I make sure there are no recurring meetings in those times to avoid a scramble to re-arrange them all every six weeks. In weeks where I don&amp;rsquo;t have Business Reviews, these are great spots to use for focus time, interviews, and other ad-hoc meetings.&lt;/p&gt;
&lt;h2 id="maintain-blank-space" class="relative group"&gt;Maintain Blank Space &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#maintain-blank-space" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This might be the most important point in the whole post: ensure there is blank space in your calendar &lt;em&gt;every day&lt;/em&gt; outside of your regular planned events.&lt;/p&gt;
&lt;p&gt;This can be hard to achieve, and may not manifest in you &lt;em&gt;actually having&lt;/em&gt; blank space by the time each day starts, but if your work week is already 100% booked with regular events and planned work, how will you respond to unplanned events? Unexpected customer meetings? Without any blank space in your calendar, you&amp;rsquo;re destined to spend an unhealthy amount of time worrying about or rearranging your calendar, and struggle to get things done.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m able to be more present in meetings, more able to ignore notifications and distractions when I know there will be time in the day to get around to them. If there is never any space for these tasks, I&amp;rsquo;m easily distracted by staying on top of message notifications, emails, etc.&lt;/p&gt;
&lt;p&gt;A good test of this is to look forward 2-3 weeks from now in your calendar. What does it look like? If there is no blank space in your calendar, then start reviewing regular commitments and get it back under control.&lt;/p&gt;
&lt;p&gt;I recently re-read &lt;a href="https://uk.bookshop.org/p/books/it-doesn-t-have-to-be-crazy-at-work-jason-fried/1364337?ean=9780008323448" target="_blank" rel="noreferrer"&gt;It Doesn&amp;rsquo;t Have To Be Crazy At Work&lt;/a&gt;, having read it first some years ago. While there are some minor points that don&amp;rsquo;t quite resonate with me, the general principle that we should stop glorifying packed schedules and competing with our colleagues to be the busiest or the most overworked is absolutely spot on.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re in a leadership position, this is not just for your benefit but for everyone around you too. People look to leadership for their example; organisations and teams mimic the habits of their leaders over time. If you&amp;rsquo;re in work from 6am to 9pm every day and your schedule is constantly back-to-back, there&amp;rsquo;s a good chance others will copy, glorify, and expect those behaviours of others (yes, even if you tell them they don&amp;rsquo;t have to, and you don&amp;rsquo;t expect it of them, and&amp;hellip;).&lt;/p&gt;
&lt;h2 id="bookable-placeholders" class="relative group"&gt;Bookable Placeholders &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#bookable-placeholders" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;You may have recurring tasks each week that are less predictable. This could be interviews, customer calls, or anything where you expect to perform a certain number each week, but can&amp;rsquo;t always predict the exact timings in advance.&lt;/p&gt;
&lt;p&gt;My approach is to use blank space for this, but an alternate approach is to add placeholders in your calendar that indicate when you would prefer for those events to be scheduled. This can save time and round-trips via email/instant message, and reduce the number of times you&amp;rsquo;re booked for something at a time that doesn&amp;rsquo;t suit you.&lt;/p&gt;
&lt;p&gt;Most calendaring tools will allow you to mark time like this in your calendar without it showing you as &amp;ldquo;Busy&amp;rdquo;. This will take some experimentation while you understand your average weekly commitment and when those events are most likely to occur, but might look something like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/03/own-your-calendar/05_hu_b80865ac2ac6fbee.webp 330w,https://jnsgr.uk/2025/03/own-your-calendar/05_hu_f0f75d395b23663f.webp 660w
,https://jnsgr.uk/2025/03/own-your-calendar/05_hu_ac06990f5bc25ce2.webp 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/05_hu_fbf7dfedc0761d4a.webp 1155w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1155"
height="942"
class="mx-auto my-0 rounded-md"
alt="Placeholders for regular but unplanned events such as interviews can help your colleagues schedule your time more appropriately."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/03/own-your-calendar/05_hu_ab6282813a9412ce.png" srcset="https://jnsgr.uk/2025/03/own-your-calendar/05_hu_1437191c3e3ed88e.png 330w,https://jnsgr.uk/2025/03/own-your-calendar/05_hu_ab6282813a9412ce.png 660w
,https://jnsgr.uk/2025/03/own-your-calendar/05_hu_1bee912efbd4bd04.png 1024w
,https://jnsgr.uk/2025/03/own-your-calendar/05.png 1155w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="review-regularly" class="relative group"&gt;Review Regularly &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#review-regularly" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Even if you do all of the above, your calendar will fill up over time. You&amp;rsquo;ll get invited to the next important monthly review meeting, perhaps collect an extra report or two, take responsibility for a major project, or be asked to mentor a colleague. There are countless ways in which your blank space can get eaten up, and when your calendar becomes cluttered, it&amp;rsquo;s likely to create a higher mental load which distracts you from the work you really need to get done.&lt;/p&gt;
&lt;p&gt;Pick a cadence on which you review all of your regular engagements for necessity, length and frequency. At Canonical we have company roadmap sprints every 3 months, and I&amp;rsquo;ve found that to be a useful cadence (and reminder) to review my calendar. After each sprint, I spend one of my focus blocks staring at my calendar trying to work out which planned meetings are still useful and effective, and which I&amp;rsquo;m going to either stop attending or reduce the frequency of.&lt;/p&gt;
&lt;p&gt;A couple of examples where I&amp;rsquo;ll &amp;ldquo;trim the fat&amp;rdquo;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Group meetings that have grown too large over time, and become less effective.&lt;/li&gt;
&lt;li&gt;Mentoring or coaching engagements where significant progress has been made, and the frequency can be reduced. Perhaps the relationship with the person you&amp;rsquo;re mentoring is good enough that you can revert to ad-hoc scheduling when they need assistance.&lt;/li&gt;
&lt;li&gt;Placeholders that are going unused week-to-week.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="remote-work-and-flexibility" class="relative group"&gt;Remote Work and Flexibility &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#remote-work-and-flexibility" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;One of the advantages of remote work is the flexibility it affords employees, but it&amp;rsquo;s important to be respectful of that privilege. The foundations of most effective workplaces are trust and respect. A structured and well-planned calendar doesn&amp;rsquo;t mean reducing flexibility - in many cases a structured calendar can enable more flexibility.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re fortunate enough to work remotely, then you should take advantage of that, but it&amp;rsquo;s also important to stay accountable and make it predictable where you can. If you go to the gym every Wednesday at 10am, then put that in your calendar and make it clear on your calendar where that time is made up. That way, your colleagues can plan around it.&lt;/p&gt;
&lt;p&gt;I like mountain biking and I find the winter (in the UK) pretty miserable. I go mountain biking on a Monday afternoon from 1300-1600, but then I work on a Monday from 1900-2200. This actually fits in with my role - a number of my reports are in the US or Australia, and returning to work on a Monday evening means I can do our 1:1s in their timezone without asking them to work late/early.&lt;/p&gt;
&lt;p&gt;For me the benefit is actually seeing daylight for a decent length of time in the middle of the day. If the weather is poor and I don&amp;rsquo;t feel like biking, I do something else on Monday afternoons, but I still keep that commitment to my US/APAC colleagues in the evening. This consistency makes it easy to plan around, but still gives me plenty of chances to go biking.&lt;/p&gt;
&lt;h2 id="tips--tricks" class="relative group"&gt;Tips &amp;amp; Tricks &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tips--tricks" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;There are some other tricks that can help you get the most from your calendar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Buffer Time&lt;/strong&gt;: Give yourself a few minutes between meetings. Whether it be to get up and stretch your legs, grab a drink or whatever. I configure Google Calendar to default to 25 minute and 50 minute meetings, rather than 30 minutes and 60 minutes. On the days I&amp;rsquo;m disciplined enough to stick to that, it gives me a few valuable minutes between meetings to pee!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colour Coding/Emojis&lt;/strong&gt;: I used to dismiss this, but have come to really appreciate it. Distinctively marking items in your calendar can help you subconsciously prepare for what&amp;rsquo;s coming, as well as help you see where you spend the majority of your time. I use dark blue regular meetings, yellow &amp;ldquo;People&amp;rdquo; meetings, purple for focus blocks, green for external meetings and orange for business/commerical review calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task Management&lt;/strong&gt;: When I&amp;rsquo;m asked to do something which will take me more than 5 minutes, I generally put that task in my calendar. If I&amp;rsquo;m asked to review a pull request and I think it&amp;rsquo;ll take me 30 minutes, I create a 30 minute event in some of the blank space named &amp;ldquo;Review PR #113&amp;rdquo;. This ensures I get the time I need, and the requester gets to see when I&amp;rsquo;ve planned the work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This post summarises a collection of lessons I&amp;rsquo;ve learned over time. Each of these have incrementally improved my time management at work. I feel more productive and less stressed, which means I find it easier to switch off from work in the evening and at weekend.&lt;/p&gt;
&lt;p&gt;My final note is on flexibility. Most of this article describes principles - ideas you should &lt;em&gt;try&lt;/em&gt; to implement. In reality, collaboration and calendar management across teams and timezones is hard. In my experience applying these principles helps account for the &amp;ldquo;messier&amp;rdquo; weeks in my work schedule and helps me keep order, but nonetheless you won&amp;rsquo;t always have total control and you might need to be flexible to accommodate some of your colleagues.&lt;/p&gt;
&lt;p&gt;Until next time!&lt;/p&gt;
&lt;p&gt;(And thanks to my wife Laura for helping me refine and edit this post!)&lt;/p&gt;</description></item><item><title>Engineering Ubuntu For The Next 20 Years</title><link>https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/</link><pubDate>Tue, 11 Feb 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/02/engineering-ubuntu-for-the-next-20-years/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article was originally posted &lt;a href="https://discourse.ubuntu.com/t/engineering-ubuntu-for-the-next-20-years/55000" target="_blank" rel="noreferrer"&gt;on the Ubuntu Discourse&lt;/a&gt;, and is reposted here. I welcome comments and further discussion in that thread.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been a VP Engineering at Canonical for 3 years now, building &lt;a href="https://juju.is" target="_blank" rel="noreferrer"&gt;Juju&lt;/a&gt; and our catalog of &lt;a href="https://charmhub.io/" target="_blank" rel="noreferrer"&gt;charms&lt;/a&gt;. In the last week of January, I was appointed the VP Engineering for Ubuntu at Canonical, where I will now oversee the Ubuntu Foundations, Server and Desktop teams.&lt;/p&gt;
&lt;p&gt;Over the past 20 years, Ubuntu has become synonymous with &amp;ldquo;Linux&amp;rdquo; to many people. I fondly remember receiving my first Ubuntu CD in the post, shortly after my own Linux journey began in 2003 with booting Knoppix on a school computer. Throughout my career, Linux and open source have been prominent features that I&amp;rsquo;m very proud of. In the past few years I&amp;rsquo;ve made contributions to Ubuntu, Arch Linux, and more recently NixOS.&lt;/p&gt;
&lt;p&gt;Ubuntu&amp;rsquo;s recent 20 year milestone is a timely reminder to pause and reflect on what made Ubuntu so exciting, so successful and so captivating to the Linux community. In 2004, the idea of releasing an operating system every six months was laughed off by many, but has now become the norm. Ubuntu builds upon Debian, aiming to bring the latest and very best open source had to offer to the masses. In the past 10 years, we&amp;rsquo;ve seen huge shifts in the way software is delivered - the success of large-scale cloud based operations necessitated a shift towards more automated testing, releasing and monitoring, and as the open source community around these projects grew, we had to evolve our ways of thinking, designing and communicating about software.&lt;/p&gt;
&lt;h2 id="four-key-themes" class="relative group"&gt;&lt;strong&gt;Four Key Themes&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#four-key-themes" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As I step into this new role, I&amp;rsquo;ve reflected on how we can steer the engineering efforts behind Ubuntu. I&amp;rsquo;ve anchored this vision around four themes: Communication, Automation, Process and Modernisation.&lt;/p&gt;
&lt;h3 id="communication" class="relative group"&gt;&lt;strong&gt;Communication&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#communication" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Communication is a central component of a distributed workforce - whether that workforce is employed by Canonical, members of our community or contributors from our partners. Ubuntu has relied for many years on mailing lists and IRC. These platforms enabled global teams to collaborate for years, and have been invaluable to the community. In 2025 we&amp;rsquo;re fortunate to have a wealth of communications platforms at our disposal, but we must use these tools strategically to avoid fragmentation.&lt;/p&gt;
&lt;p&gt;On Jan 29 2025, the Ubuntu developer mailing list &lt;a href="https://lists.ubuntu.com/archives/ubuntu-devel-announce/2025-January/001365.html" target="_blank" rel="noreferrer"&gt;announced&lt;/a&gt; that the primary means of communication for Ubuntu developers will be the Ubuntu Community Matrix server. Matrix provides a rich, modern communications medium that is familiar to the next generation of engineers and tinkerers, who will be central to the continued progression of Ubuntu and open source. We&amp;rsquo;re in good company on Matrix, with many other Linux distributions and projects maintaining a presence on the platform. The recent &lt;a href="https://fridge.ubuntu.com/2024/12/08/ubuntu-forums-migration/" target="_blank" rel="noreferrer"&gt;migration&lt;/a&gt; of Ubuntu Forums to the Ubuntu Discourse, further consolidates the range of platforms we use to connect with one another.&lt;/p&gt;
&lt;p&gt;To effect much of the change I&amp;rsquo;m describing in this post, we will need community support. I&amp;rsquo;ll be encouraging the leads of our internal teams in Ubuntu Foundations, Server and Desktop to be more forthcoming and regular with public updates that will serve two purposes: to share our intentions, progress and dreams for Ubuntu, but also to collaborate on refining our vision, ensuring we deliver a platform that is not just exciting, but &lt;em&gt;relevant&lt;/em&gt; for many years to come.&lt;/p&gt;
&lt;p&gt;Documentation is a critical form of communication. Our documentation enables our current users, but also illuminates the path for new contributors. Such documentation does exist, but much of it is fragmented across different platforms, duplicated and/or contradictory or simply difficult to find. As a company, and as a community, we must focus on ensuring both existing and potential contributors have access to the information they need on conventions, tools and processes. A good example of where this has already happened is the &lt;a href="https://documentation.ubuntu.com/sru" target="_blank" rel="noreferrer"&gt;SRU documentation&lt;/a&gt; which was recently rebuilt in line with our documentation &lt;a href="https://canonical.com/documentation" target="_blank" rel="noreferrer"&gt;practices&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="automation" class="relative group"&gt;&lt;strong&gt;Automation&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Delivering a Linux distribution is a monumental task. With tens of thousands of packages across multiple architectures, the workload can be overwhelming - leaving little room for innovation until the foundational work is done. We&amp;rsquo;re fortunate to benefit from the diligent work done by the Debian community, yet there is a huge amount of work that goes into each Ubuntu release. One of our primary tasks as a distribution is package maintenance. While some may see this as menial or repetitive, it remains critical to the future of Ubuntu, and is a valuable specialist skill in its own right.&lt;/p&gt;
&lt;p&gt;Software packaging is a complex and constantly evolving topic. Ubuntu relies heavily on a blend of Debian packages, and our own Snap packaging format. Debian packaging was revolutionary - responsible for huge advancements in the way we thought about delivering software, but as things have moved on some of those tools and practices are beginning to show their age.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to focus on enriching our build process with modern ideals and processes for automating the version bumps, testing, performance benchmarking and releasing of packages in the archive. High complexity tasks are error-prone and, without sufficient automation, risk becoming overly dependent on a few skilled individuals. We have the same challenge with Snaps, but they benefit from significantly more modern tooling as a consequence of the observations made about Debian packaging over many years.&lt;/p&gt;
&lt;p&gt;The goal of this theme is not just to automate as much as possible (thereby increasing our collective capacity), but also to simplify processes where we can. Much of Ubuntu&amp;rsquo;s build process *is* automated, but those systems are disparate and often opaque to all but our most experienced contributors.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been inspired by how the NixOS community manages packaging. Every single package for the distro is represented as text files, in a &lt;a href="https://github.com/NixOS/nixpkgs" target="_blank" rel="noreferrer"&gt;single Git repository&lt;/a&gt;, with a universally observable continuous integration and integration testing pipeline (&lt;a href="https://wiki.nixos.org/wiki/Hydra" target="_blank" rel="noreferrer"&gt;Hydra&lt;/a&gt;) that performs version bumps and simple maintenance tasks semi-autonomously. While this model carries its own challenges, there is something alluring about the transparency and accessibility of the systems that assemble, test and deliver software to their users.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://universal-blue.org/" target="_blank" rel="noreferrer"&gt;Universal Blue&lt;/a&gt;, and by extension &lt;a href="https://projectbluefin.io/" target="_blank" rel="noreferrer"&gt;Project Bluefin&lt;/a&gt;, are recent additions to the Linux ecosystem that benefited from thinking hard about the tooling they use to build their distribution. They&amp;rsquo;ve centered their process around tools with which their cloud-native audience are already familiar.&lt;/p&gt;
&lt;p&gt;My suggestion is not to imitate these projects, rather that the open source community is at its strongest when we collaborate and learn from one another. I think we can take inspiration from those surrounding us, and use that to inform our plans for Ubuntu&amp;rsquo;s future.&lt;/p&gt;
&lt;h3 id="process" class="relative group"&gt;&lt;strong&gt;Process&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#process" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Process is closely tied to automation, but is frequently viewed negatively in software engineering, carrying connotations of bureaucracy and slowdowns. In my experience, a well-designed process empowers people to enact changes with confidence.&lt;/p&gt;
&lt;p&gt;Ubuntu is built by all of us, in many countries and across all timezones. Concise, well-defined, lightweight processes promote autonomy and reduce uncertainty - enabling people to unblock themselves. Ubuntu is no stranger to process: the &lt;a href="https://canonical-ubuntu-packaging-guide.readthedocs-hosted.com/en/latest/explanation/main-inclusion-review/" target="_blank" rel="noreferrer"&gt;Main Inclusion Review (MIR)&lt;/a&gt;, the aforementioned &lt;a href="https://canonical-sru-docs.readthedocs-hosted.com/en/latest/" target="_blank" rel="noreferrer"&gt;Stable Release Updates (SRU)&lt;/a&gt; process, the &lt;a href="https://forum.snapcraft.io/t/process-for-aliases-auto-connections-and-tracks/455" target="_blank" rel="noreferrer"&gt;process&lt;/a&gt; for Snap store requests and many more have contributed to the success of Ubuntu, setting clear guardrails for contributors and ensuring we work to common standards.&lt;/p&gt;
&lt;p&gt;My goal over the coming months is to work with you, the people behind Ubuntu, to identify which of these processes still serve us, and which need revising to simplify our work while maintaining our dedication to stability. I&amp;rsquo;ll consolidate the definitions of these processes, make them searchable, peer-reviewable, and more discoverable. Examples of where this has worked well are the &lt;a href="https://github.com/golang/proposal" target="_blank" rel="noreferrer"&gt;Go proposal&lt;/a&gt; process, and the &lt;a href="https://eips.ethereum.org/" target="_blank" rel="noreferrer"&gt;Ethereum Improvement Proposal&lt;/a&gt; process - both of which make it trivial to create, track and discuss proposals across the breadth of their respective projects.&lt;/p&gt;
&lt;p&gt;If you submit an MIR, or work on an SRU, it should be trivial to understand the status of that request, and to communicate with the team executing that process where needed. If you&amp;rsquo;re interested in joining our community, it should be simple to get a sense of what is changing across the project, and where you might be able to help.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to tackle these problems and make these processes as transparent as possible.&lt;/p&gt;
&lt;h3 id="modernisation" class="relative group"&gt;&lt;strong&gt;Modernisation&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#modernisation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The world of computing has evolved dramatically in the last 20 years, and I’m proud that Ubuntu has continually adapted and thrived. In Linux alone there have been huge changes to what is considered &amp;ldquo;normal&amp;rdquo; for a Linux machine. Whether it be the introduction of `systemd`, the advent of languages with a focus on memory safety, the huge growth in virtualisation and containerisation technology, or even the introduction of Rust into the Linux kernel itself - the foundations of our distribution must be constantly assessed against the needs of our users.&lt;/p&gt;
&lt;p&gt;I was proud to see the &lt;a href="https://discourse.ubuntu.com/t/kernel-version-selection-for-ubuntu-releases/47007?u=d0od" target="_blank" rel="noreferrer"&gt;announcement&lt;/a&gt; last year that the Ubuntu Kernel team committed to shipping the very latest kernels in new versions of Ubuntu, wherever they possibly can. Even if that means shipping a kernel that&amp;rsquo;s in the release candidate phase, the team will stand by that kernel and continue to support it through the Ubuntu release&amp;rsquo;s life. While this could appear cavalier at a glance, what it represents is a willingness to rise to the challenge of shipping the very best of open source to our users. I&amp;rsquo;d like to see more of this. Ubuntu is a flagship Linux distribution and a starting point for many; we must ensure that our users are presented with the very best our community has to offer - even if that means a bit more hustle in the early days of a given release. This is of particular importance for our Long Term Support releases, which are relied upon by governments, financial institutions, educational establishments, nonprofits and many others for years after the initial release date.&lt;/p&gt;
&lt;p&gt;We should look deeply at the tools we ship with Ubuntu by default - selecting for tools that have resilience, performance and maintainability at their core. There are countless examples in the open source community of tools being re-engineered, and re-imagined using tools and practices that have only relatively recently become available. Some of my personal favourites include command-line utilities such as &lt;a href="https://github.com/eza-community/eza" target="_blank" rel="noreferrer"&gt;eza&lt;/a&gt;, &lt;a href="https://github.com/sharkdp/bat" target="_blank" rel="noreferrer"&gt;bat&lt;/a&gt;, and &lt;a href="https://helix-editor.com/" target="_blank" rel="noreferrer"&gt;helix&lt;/a&gt;, the new &lt;a href="https://ghostty.org/" target="_blank" rel="noreferrer"&gt;ghostty&lt;/a&gt; terminal emulator, and more foundational projects such as the &lt;a href="https://uutils.github.io/" target="_blank" rel="noreferrer"&gt;uutils&lt;/a&gt; rewrite of &lt;a href="https://github.com/uutils/coreutils" target="_blank" rel="noreferrer"&gt;coreutils in Rust&lt;/a&gt;. Each of these projects are at varying levels of maturity, but have demonstrated a vision for a more modern Unix-like experience that emphasises resilience, performance and usability.&lt;/p&gt;
&lt;p&gt;Another example of this is our work on &lt;a href="https://ubuntu.com/blog/tpm-backed-full-disk-encryption-is-coming-to-ubuntu" target="_blank" rel="noreferrer"&gt;TPM-backed full disk encryption&lt;/a&gt;, a project which promises encryption of our users&amp;rsquo; data with no degradation to their user experience. This feature relies upon cryptographic hardware and techniques that have only recently become available to us, but enable us to deliver the potent combination of security &lt;em&gt;and&lt;/em&gt; usability to our users.&lt;/p&gt;
&lt;h2 id="delivering-features" class="relative group"&gt;&lt;strong&gt;Delivering Features&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#delivering-features" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;What I&amp;rsquo;ve shared so far is a high-level overview, and many of the points under the four themes will take time to implement, with most appearing as a series of gradual improvements. You might be wondering whether we&amp;rsquo;ll focus on the latest trends and features, or prioritise that bug you reported.&lt;/p&gt;
&lt;p&gt;While focusing on the latest trends or a single breakthrough feature can yield short-term progress, embracing these principles will create the space for sustained, impactful innovation.&lt;/p&gt;
&lt;p&gt;That said, I’ve also been working on a list of incremental features and improvements that we can deliver in the coming months to enhance the Ubuntu experience. You’ll hear more from me and the team leads regularly as we share updates and progress.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;&lt;strong&gt;Summary&lt;/strong&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I’m incredibly excited to embark on this journey, and consider it a privilege to serve in this role. Together with the Ubuntu community, Canonical engineers, and our partners, we will build an open-source platform that enables the next 20 years of innovation in computing.&lt;/p&gt;
&lt;p&gt;If you have ideas for the future of Ubuntu, or something in this post has resonated with you and you want to be involved either as a community member, or perhaps a future employee of Canonical, I&amp;rsquo;d love to hear from you.&lt;/p&gt;</description></item><item><title>Packaging the Multipass Flutter GUI for NixOS</title><link>https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/</link><pubDate>Thu, 16 Jan 2025 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The very first application I ever packaged in &lt;a href="https://github.com/NixOS/nixpkgs" target="_blank" rel="noreferrer"&gt;nixpkgs&lt;/a&gt; was &lt;a href="https://multipass.run" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt;. Multipass is, according to the website:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;a CLI to launch and manage VMs on Windows, Mac and Linux that simulates a cloud environment with support for cloud-init.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wrote in &lt;a href="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/" target="_blank" rel="noreferrer"&gt;some detail&lt;/a&gt; about how I use Multipass and LXD on my workstation to create and manage VMs for testing and development. Last year Multipass shipped a new GUI written from the ground up in Flutter. It provides a clean, modern way to launch, manage and interact with VMs. When the GUI first shipped, I briefly attempted to get it to build with Nix, but some combination of my lack of knowledge, and the maturity of the Flutter tooling in &lt;code&gt;nixpkgs&lt;/code&gt; at the time meant I never finished it.&lt;/p&gt;
&lt;p&gt;As version &lt;a href="https://github.com/canonical/multipass/releases/tag/v1.15.0" target="_blank" rel="noreferrer"&gt;&lt;code&gt;1.15.0&lt;/code&gt;&lt;/a&gt; was in its release candidate stage, I decided to have another go!&lt;/p&gt;
&lt;p&gt;This post will break down the process into steps, but if you&amp;rsquo;d like the tl;dr, take a look at the &lt;a href="https://github.com/NixOS/nixpkgs/pull/363626" target="_blank" rel="noreferrer"&gt;pull request&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_e1bb4efedfd29fe4.webp 330w,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_1db33041b332f771.webp 660w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_8f34185708da23c2.webp 1024w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_85584ba5f57aa0a.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1500"
height="1018"
class="mx-auto my-0 rounded-md"
alt="A screenshot showing the instances overview page in Multipass"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_c74fe6f8f3af867b.png" srcset="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_4bf35557a15148ae.png 330w,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_c74fe6f8f3af867b.png 660w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_8d2fa521ec867841.png 1024w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/02_hu_f1ec9a5cbfc9375e.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="housekeeping" class="relative group"&gt;Housekeeping &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#housekeeping" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before getting started on packaging the GUI, I did some housekeeping on the Multipass package. First order of business was to simply &lt;a href="https://github.com/NixOS/nixpkgs/pull/363626/commits/ee43b0f9fd7d15f5869ffbe0c014e4d1983d3058" target="_blank" rel="noreferrer"&gt;bump the version&lt;/a&gt; to &lt;code&gt;1.15.0&lt;/code&gt; and ensure the package still built, which it did.&lt;/p&gt;
&lt;p&gt;Last year, &lt;a href="https://github.com/NixOS/rfcs/pull/140" target="_blank" rel="noreferrer"&gt;RFC 140&lt;/a&gt; was introduced to simplify the directory structure of &lt;code&gt;nixpkgs&lt;/code&gt;, introducing a new &lt;code&gt;pkgs/by-name&lt;/code&gt; directory which will (eventually) render &lt;a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/all-packages.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pkgs/top-level/all-packages.nix&lt;/code&gt;&lt;/a&gt; useless. This new structure will make finding package definitions much easier, as they&amp;rsquo;ll all be arranged alphabetically by the first two characters of their name. Using the Multipass example, rather than &lt;code&gt;pkgs/tools/virtualization/multipass/default.nix&lt;/code&gt;, the new preferred path would be &lt;code&gt;pkgs/by-name/mu/multipass/package.nix&lt;/code&gt;. There was a &lt;a href="https://media.ccc.de/v/nixcon-2023-35713-not-all-packages-anymore-nix" target="_blank" rel="noreferrer"&gt;talk&lt;/a&gt; at NixCon 2023 which summarised this nicely if you&amp;rsquo;d like more information.&lt;/p&gt;
&lt;p&gt;As I was going to be carrying out some hefty work on Multipass by introducing the GUI, I took the opportunity to move to the new scheme as part of the changes.&lt;/p&gt;
&lt;h2 id="restructuring" class="relative group"&gt;Restructuring &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#restructuring" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The Multipass package carries a few patches, and is already reasonably involved, so I decided that the best route forward was to create separate files for &lt;code&gt;multipassd&lt;/code&gt;/&lt;code&gt;multipass&lt;/code&gt; (the server &amp;amp; command line client binaries, written in C++, and already packaged) and &lt;code&gt;multipass.gui&lt;/code&gt; (the new Flutter application). Thus the new structure for the package looks like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Derivation files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── gui.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── multipassd.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── package.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Lock file for Fluter app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── pubspec.lock.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Update script for daemon/cli &amp;amp; GUI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── update.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Patches&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── cmake_no_fetch.patch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── cmake_warning.patch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── lxd_socket_path.patch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── test_unreachable_call.patch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── vcpkg_no_install.patch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="using-symlinkjoin" class="relative group"&gt;Using &lt;code&gt;symlinkJoin&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#using-symlinkjoin" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This package now takes a more unusual shape. Because the GUI is essentially a entirely separate application, written with a different toolkit in a different language, but I wanted the two to be installed as part of the overall &lt;code&gt;multipass&lt;/code&gt; package, I decided to use two completely separate derivations and use &lt;a href="https://nixos.org/manual/nixpkgs/stable/#trivial-builder-symlinkJoin" target="_blank" rel="noreferrer"&gt;&lt;code&gt;symlinkJoin&lt;/code&gt;&lt;/a&gt; to bundle the two together. According to the manual, &lt;code&gt;symlinkJoin&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;can be used to put many derivations into the same directory structure. It works by creating a new derivation and adding symlinks to each of the paths listed. It expects two arguments, name, and paths. name (or alternatively pname and version) is the name used in the Nix store path for the created derivation. paths is a list of paths that will be symlinked. These paths can be to Nix store derivations or any other subdirectory contained within.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is particularly useful for the Multipass package, because the Multipass GUI dynamically-links against libraries that are built as part of the &lt;code&gt;multipassd&lt;/code&gt; derivation, and linking the two derivations in this way ensures that all those files are located correctly at runtime without any extra plumbing.
This allowed me to keep the top-level &lt;a href="https://github.com/NixOS/nixpkgs/blob/113da45159c3ba1bedc9663e27d3174435db76bf/pkgs/by-name/mu/multipass/package.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;package.nix&lt;/code&gt;&lt;/a&gt; relatively simple (annotated below):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ... snip ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multipass&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1.15.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Fetch the source just once, to be used in both the multipassd and GUI derivations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;multipass_src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetchFromGitHub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;canonical&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multipass&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;refs/tags/v&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-RoOCh1winDs7BZwyduZziHj6EMe3sGMYonkA757UrIU=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fetchSubmodules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Shared metadata between derivations.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;commonMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;homepage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://multipass.run&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;license&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licenses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gpl3Plus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;maintainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maintainers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;jnsgruk&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;platforms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64-linux&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Build the multipass server &amp;amp; client binaries from the shared source&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# and common metadata.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;multipassd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callPackage&lt;/span&gt; &lt;span class="sr"&gt;./multipassd.nix&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;commonMeta&lt;/span&gt; &lt;span class="n"&gt;multipass_src&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Build the multipass GUI using shared source and common metadata.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;multipass-gui&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callPackage&lt;/span&gt; &lt;span class="sr"&gt;./gui.nix&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;commonMeta&lt;/span&gt; &lt;span class="n"&gt;multipass_src&lt;/span&gt; &lt;span class="n"&gt;multipassd&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create the top-level `multipass` package using `symlinkJoin`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;symlinkJoin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Join both of the newly created derivations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;multipassd&lt;/span&gt; &lt;span class="n"&gt;multipass-gui&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Ensure tests are run, and define a (new) update script.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;passthru&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optionalAttrs&lt;/span&gt; &lt;span class="n"&gt;stdenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isLinux&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nixosTests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;multipass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;updateScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;./update.sh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commonMeta&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Ubuntu VMs on demand for any workstation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="building-the-gui" class="relative group"&gt;Building the GUI &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#building-the-gui" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Normally the server, client and GUI binaries are all built as part of a single invocation of &lt;code&gt;cmake&lt;/code&gt;. We can&amp;rsquo;t do that here, so the &lt;a href="https://github.com/NixOS/nixpkgs/blob/859919e56a721a3a158242baa891e101673ce8ed/pkgs/by-name/mu/multipass/multipassd.nix#L110-L111" target="_blank" rel="noreferrer"&gt;early code&lt;/a&gt; I put into the &lt;code&gt;multipass&lt;/code&gt; package when I failed to package the GUI before still stood - essentially stopping &lt;code&gt;cmake&lt;/code&gt; from trying to build the Flutter GUI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# We&amp;#39;ll build the flutter application separately using buildFlutterApplication&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmakeFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-DMULTIPASS_ENABLE_FLUTTER_GUI=false&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Next, I created &lt;a href="https://github.com/NixOS/nixpkgs/blob/859919e56a721a3a158242baa891e101673ce8ed/pkgs/by-name/mu/multipass/gui.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;gui.nix&lt;/code&gt;&lt;/a&gt; which uses the &lt;a href="https://nixos.org/manual/nixpkgs/stable/#ssec-dart-flutter" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pkgs.buildFlutterApplication&lt;/code&gt;&lt;/a&gt; helper. The derivation is annotated below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Passed in from the package.nix shown above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;commonMeta&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;multipass_src&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;multipassd&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... snip ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;flutter327&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildFlutterApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multipass-gui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multipass_src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Ensure we build in the correct directory for the GUI code.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sourceRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;multipass_src&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/src/client/gui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# To make the builds as deterministic as possible, nixpkgs reads the pubspec.lock&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# file for the Flutter application and downloads/caches the correct versions of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# flutter packages to be used in the build. It does this with `pub2nix`.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Here I use a JSON representation of the pubspec.lock file committed into nixpkgs.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pubspecLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;importJSON&lt;/span&gt; &lt;span class="sr"&gt;./pubspec.lock.json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Some of the Flutter dependencies used are pulled from Github (branches) directly, so this&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ensures that the versions/commits are pinned for future builds.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;gitHashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dartssh2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-2pypKwurziwGLZYuGaxlS2lzN3UvJp3bRTvvYYxEqRI=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hotkey_manager_linux&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-aO0h94YZvgV/ggVupNw8GjyZsnXrq3qTHRDtuhNv3oI=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system_info2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-fly7E2vG+bQ/+QGzXk+DYba73RZccltdW2LpZGDKX60=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tray_menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-riiAiBEms+9ARog8i+MR1fto1Yqx+gwbBWyNbNq6VTM=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;window_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-71PqQzf+qY23hTJvcm0Oye8tng3Asr42E2vfF1nBmVA=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;xterm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-h8vIonTPUVnNqZPk/A4ZV7EYCMyM0rrErL9ZOMe4ZBE=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... snip ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# The Multipass GUI relies on protobuf for API communication; ensure the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Dart protobuf definitions are compiled before we build.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;preBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir -p lib/generated
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Generate the Dart gRPC code for the Multipass GUI.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; protoc \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --plugin=protoc-gen-dart=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getExe&lt;/span&gt; &lt;span class="n"&gt;protoc-gen-dart&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --dart_out=grpc:lib/generated \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -I ../../rpc \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; ../../rpc/multipass.proto \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; google/protobuf/timestamp.proto
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Libraries built by multipassd are linked dynamically at runtime; multipassd must&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# be installed for the GUI to function.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;runtimeDependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;multipassd&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Housekeeping to ensure the icons/desktop files are correctly installed.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postFixup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mv $out/bin/multipass_gui $out/bin/multipass.gui
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; install -Dm444 $out/app/multipass-gui/data/flutter_assets/assets/icon.png \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; $out/share/icons/hicolor/256x256/apps/multipass.gui.png
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp $out/share/applications/multipass.gui.autostart.desktop \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; $out/share/applications/multipass.gui.desktop
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... snip ..&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="update-script" class="relative group"&gt;Update Script &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#update-script" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;That was enough to ensure that the GUI worked correctly. My concern now lied with the maintenance of the package. The process for updating the derivation now involved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bumping the version string&lt;/li&gt;
&lt;li&gt;Updating the source hashes&lt;/li&gt;
&lt;li&gt;Updating the &lt;code&gt;pubspec.lock.json&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Updating the &lt;code&gt;gitHashes&lt;/code&gt; of the Flutter dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This seemed a little convoluted and error prone to handle manually each time, so I decided to implement an update script for the package to take care of future version bumps. Thankfully &lt;code&gt;nixpkgs&lt;/code&gt; has good provision for this, allowing package maintainers to specify &lt;code&gt;passthru.updateScript&lt;/code&gt; to a derivation, which I did &lt;a href="https://github.com/NixOS/nixpkgs/blob/c0b4b9614390535f61f103fff1e48255a588fa4b/pkgs/by-name/mu/multipass/package.nix#L55" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t post the &lt;a href="https://github.com/NixOS/nixpkgs/blob/c0b4b9614390535f61f103fff1e48255a588fa4b/pkgs/by-name/mu/multipass/update.sh" target="_blank" rel="noreferrer"&gt;full update script&lt;/a&gt; here, as it&amp;rsquo;s lots of unsightly &lt;code&gt;bash&lt;/code&gt;! Essentially the script takes care of determining the latest version released on Github by using &lt;code&gt;curl&lt;/code&gt;, then &lt;a href="https://github.com/NixOS/nixpkgs/blob/c0b4b9614390535f61f103fff1e48255a588fa4b/pkgs/by-name/mu/multipass/update.sh#L11-L18" target="_blank" rel="noreferrer"&gt;updates the &lt;code&gt;pubspec.lock.json&lt;/code&gt;&lt;/a&gt; using a combination of &lt;code&gt;curl&lt;/code&gt;/&lt;code&gt;yj&lt;/code&gt;, then finally works through updating the source hash for the Multipass repo, gRPC fork it uses, and each of the git hashes for the Flutter dependencies.&lt;/p&gt;
&lt;p&gt;This can be invoked from within the &lt;code&gt;nixpkgs&lt;/code&gt; repo with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix-shell maintainers/scripts/update.nix --argstr package multipass
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Overall, this turned out cleaner than I expected, though it took me longer to figure out than this post might suggest. My hope is that this will a useful reference to anybody trying to package Flutter applications with Nix, and if you&amp;rsquo;re a user of the &lt;code&gt;multipass&lt;/code&gt; package, check out the shiny new GUI that&amp;rsquo;s available to you! The GUI is available from &lt;code&gt;1.15.0&lt;/code&gt; onwards, which at the time of writing means you&amp;rsquo;ll need to install the package from &lt;code&gt;nixos-unstable&lt;/code&gt; or similar (sadly I didn&amp;rsquo;t land the change in time for NixOS 24.11).&lt;/p&gt;
&lt;p&gt;In my opinion, the Multipass team have done a lovely job on the GUI - and for many users this will provide a much improved onboarding experience. Nice work 😎&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_9aefc9dd3e3a21cc.webp 330w,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_a99bdf4a4285ca48.webp 660w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_6495f16eee0b90ee.webp 1024w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_9ea399f52ef167e6.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1500"
height="1018"
class="mx-auto my-0 rounded-md"
alt="Screenshot showing a terminal visible on one of the configured instances in Multipass"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_2b1c9bf3b471e7ae.png" srcset="https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_86bf70f6672c300e.png 330w,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_2b1c9bf3b471e7ae.png 660w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_3d7d93c80092107b.png 1024w
,https://jnsgr.uk/2025/01/packaging-multipass-flutter-app-for-nix/03_hu_910547f8943596e7.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Experimenting with Rust, Nix, K6 and Parca</title><link>https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Over the past couple of weeks, I&amp;rsquo;ve been teaching myself &lt;a href="https://www.rust-lang.org/" target="_blank" rel="noreferrer"&gt;Rust&lt;/a&gt;. I don&amp;rsquo;t have a pressing need to write much Rust right now, but I&amp;rsquo;m intrigued by the promises of memory safety, and have been increasingly impressed at the quality of some of the software that the community produces. I also think that the concepts popularised by Rust, such as the &lt;a href="https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html" target="_blank" rel="noreferrer"&gt;borrow checker&lt;/a&gt;, will stick around in computing for many years to come and I&amp;rsquo;d like to have more hands-on experience with that.&lt;/p&gt;
&lt;p&gt;The Rust language also encourages the ideas of safety and soundness - sound code is (approximately) code that can&amp;rsquo;t cause memory corruption or exhibit undefined behaviour. You can read more in this excellent post &lt;a href="https://jacko.io/safety_and_soundness.html" target="_blank" rel="noreferrer"&gt;Safety and Soundness in Rust&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This blog post started out life as a post about Rust and my experience learning it, but I got interested in the performance of the server implementation I came up with and the post evolved into a post more about profiling and load testing!&lt;/p&gt;
&lt;h2 id="learning-rust" class="relative group"&gt;Learning Rust &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#learning-rust" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s been a little while since I dug into a new programming language, so I thought I&amp;rsquo;d mention how I went about it. In general I&amp;rsquo;m someone who learns best &amp;ldquo;by doing&amp;rdquo;. I normally try to read through some of the basic concepts, then jump to a project that will enable me to exercise them quite quickly. Fortunately, Rust has an &lt;em&gt;excellent&lt;/em&gt; official guide in the form of the &lt;a href="https://doc.rust-lang.org/book/" target="_blank" rel="noreferrer"&gt;Rust Book&lt;/a&gt;, which covers everything from the obligatory &lt;code&gt;Hello, World!&lt;/code&gt;, to concurrency, memory safety, publishing packages on &lt;a href="https://crates.io" target="_blank" rel="noreferrer"&gt;crates.io&lt;/a&gt; and more. I thoroughly recommend it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also picked up a copy of &lt;a href="https://rust-for-rustaceans.com/" target="_blank" rel="noreferrer"&gt;Rust for Rustaceans&lt;/a&gt; which was recommended by a couple of different colleagues - I intend to work through this next.&lt;/p&gt;
&lt;p&gt;So, after working through the Rust Book over the course of about a week in my spare time, I needed a project!&lt;/p&gt;
&lt;h2 id="rewriting-gosherve-in-rust" class="relative group"&gt;Rewriting &lt;code&gt;gosherve&lt;/code&gt; in Rust &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#rewriting-gosherve-in-rust" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In my &lt;a href="https://jnsgr.uk/2024/01/building-a-blog-with-go-nix-hugo/#serving-the-blog" target="_blank" rel="noreferrer"&gt;first post&lt;/a&gt; on this blog I talked about a small Go project I wrote several years ago named &lt;a href="https://github.com/jnsgruk/gosherve" target="_blank" rel="noreferrer"&gt;&lt;code&gt;gosherve&lt;/code&gt;&lt;/a&gt;. This was one of my first Go projects - a simple web server that can serve some static assets, and a set of short-links/redirects which are specified in a Github Gist. It&amp;rsquo;s been happily running my website for several years, and it felt like a small, but ambitious enough project for my first adventure into Rust - particuarly as over the years &lt;code&gt;gosherve&lt;/code&gt; has grown Prometheus &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/metrics.go" target="_blank" rel="noreferrer"&gt;metrics&lt;/a&gt; and the ability to serve assets from &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/main.go#L24" target="_blank" rel="noreferrer"&gt;an embedded filesystem&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As I anticipated, naming the new project was the hardest part. I landed on &lt;strong&gt;servy&lt;/strong&gt;, at least for now. I&amp;rsquo;m reasonably happy with &lt;a href="https://github.com/jnsgruk/servy" target="_blank" rel="noreferrer"&gt;the code&lt;/a&gt; - at the time of writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can serve redirects&lt;/li&gt;
&lt;li&gt;It can serve embedded web assets&lt;/li&gt;
&lt;li&gt;It provides a metrics server on a separate port&lt;/li&gt;
&lt;li&gt;It has reasonable unit/integration test coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the things I love about Go is the built-in ability to embed static assets into a binary through the &lt;code&gt;//go:embed&lt;/code&gt; directive, which gives you a pointer to an embedded filesystem. I was able to achieve a similar effect in Rust with &lt;a href="https://docs.rs/axum-embed/latest/axum_embed/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;axum-embed&lt;/code&gt;&lt;/a&gt;, which in turn builds upon &lt;a href="https://docs.rs/crate/rust-embed/latest" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rust-embed&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I used Nix to create different variants of the build (i.e. a &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/servy.nix" target="_blank" rel="noreferrer"&gt;&amp;ldquo;vanilla&amp;rdquo;&lt;/a&gt; build, and one that serves my website by creating a &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk-content.nix" target="_blank" rel="noreferrer"&gt;derivation&lt;/a&gt; just for the web content, and another for the &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;jnsgruk&lt;/code&gt; binary&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You can run a copy of this website (frozen in time just before I wrote this post!) by running &lt;code&gt;nix run github:jnsgruk/servy#jnsgruk&lt;/code&gt;, or build a container for it by running &lt;code&gt;nix build github:jnsgruk/servy#jnsgruk-container&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I was expecting the Rust binary to be quite a lot smaller. I&amp;rsquo;m not sure why. The old (Go) binary weighs in at &lt;strong&gt;68MB&lt;/strong&gt;, where the new Rust binary comes in at &lt;strong&gt;67MB&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So I was right! I guess?! The result here is relatively uninteresting - a good chunk of both binaries is just the static assets (images!) that make up this site. At the time of writing the &lt;a href="https://github.com/jnsgruk/servy/blob/71a337317defc779c6c55d486e20d104c5d478f2/nix/jnsgruk-content.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;jnsgruk-content&lt;/code&gt;&lt;/a&gt; derivation evaluates at around 57MB - meaning there is 10MB and 9MB respectively for Go and Rust added by the &lt;em&gt;actual server code&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="basic-load-testing-with-k6" class="relative group"&gt;Basic Load Testing with &lt;code&gt;k6&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#basic-load-testing-with-k6" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been looking for an excuse to play with &lt;a href="https://k6.io/" target="_blank" rel="noreferrer"&gt;k6&lt;/a&gt; for a while. According to the &lt;a href="https://grafana.com/docs/k6/latest/" target="_blank" rel="noreferrer"&gt;documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Grafana k6 is an open-source, developer-friendly, and extensible load testing tool. k6 allows you to prevent performance issues and proactively improve reliability.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In very simple terms, to use &lt;code&gt;k6&lt;/code&gt; you define a script (in Javascript) that outlines a set of requests to make, their success criteria and (optionally) the strategy for ramping up load on the server. It has &lt;em&gt;many&lt;/em&gt; more features than I used for this project, but I was impressed with how simple it was to get started. I began by running the following (grabbing &lt;code&gt;k6&lt;/code&gt; from &lt;code&gt;nixpkgs&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start a shell with k6 available&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix shell github:NixOS/nixpkgs/nixos-unstable#k6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Init a new k6 script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ k6 new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Initialized a new k6 &lt;span class="nb"&gt;test&lt;/span&gt; script in script.js. You can now execute it by running &lt;span class="sb"&gt;`&lt;/span&gt;k6 run script.js&lt;span class="sb"&gt;`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Now I just needed to work out what I wanted to test! I wanted a relatively simple test that requested a mix of web assets and redirects for a sustained period, to see how much throughput I could achieve with each of the server implementations. The template came with some sensible setup for the number of VUs (virtual users) and duration of the test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;k6&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;k6/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A number specifying the number of VUs to run concurrently.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;vus&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// A string specifying the total duration of the test run.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;30s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;VUs are an emulation of a user interacting with your service; each of them is an agent which will execute the test script. Each time the script is executed (by a VU), that&amp;rsquo;s known as an &amp;ldquo;iteration&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I wanted to test a variety of requests, so I first generated a list of valid URLs for my server. I used &lt;a href="https://github.com/edoardottt/cariddi" target="_blank" rel="noreferrer"&gt;&lt;code&gt;cariddi&lt;/code&gt;&lt;/a&gt; which is a web crawler written in Go, combined with &lt;code&gt;jq&lt;/code&gt; to create a list of valid paths in a JSON file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start a shell with k6 available&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix shell github:NixOS/nixpkgs/nixos-unstable#cariddi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run cariddi and generate the paths.json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ &lt;span class="nb"&gt;echo&lt;/span&gt; http://localhost:8080 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cariddi -- -plain &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -d&lt;span class="s2"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; -f4- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; jq -r -nR &lt;span class="s1"&gt;&amp;#39;[inputs | select(length&amp;gt;0)]&amp;#39;&lt;/span&gt; &amp;gt; paths.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Similarly I generated a list of valid redirects:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ curl -s &lt;span class="s2"&gt;&amp;#34;https://gist.githubusercontent.com/jnsgruk/b590f114af1b041eeeab3e7f6e9851b7/raw&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; cut -d&lt;span class="s2"&gt;&amp;#34; &amp;#34;&lt;/span&gt; -f1 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; jq -r -nR &lt;span class="s1"&gt;&amp;#39;[inputs | select(length&amp;gt;0)]&amp;#39;&lt;/span&gt; &amp;gt; redirects.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Now in my &lt;code&gt;k6&lt;/code&gt; script, I was able to read those files to create a list of URLs to &lt;a href="https://grafana.com/docs/k6/latest/javascript-api/k6-http/batch/" target="_blank" rel="noreferrer"&gt;batch&lt;/a&gt; &lt;code&gt;GET&lt;/code&gt; during the load test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Allow the host to be overridden by the &amp;#39;K6_HOST&amp;#39; env var.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;__ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;K6_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://localhost:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Read the list of paths/redirects.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`./paths.json`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`./redirects.json`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Batch requests to redirects and static assets.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirects&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// For each response, ensure we get a 200/301/308/404
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// response - we shouldn&amp;#39;t see anything else.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m using a pretty naive &lt;a href="https://grafana.com/docs/k6/latest/using-k6/checks/" target="_blank" rel="noreferrer"&gt;check&lt;/a&gt; function here, though they can be expanded to check for other conditions in the response body, the size of the response, etc.&lt;/p&gt;
&lt;p&gt;You can see my final test script &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/script.js" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I ran &lt;code&gt;k6&lt;/code&gt; and let it rip! This initial run was against &lt;code&gt;servy&lt;/code&gt;, running on my workstation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ k6 run script.js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /&lt;span class="se"&gt;\ &lt;/span&gt; Grafana /‾‾/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /&lt;span class="se"&gt;\ &lt;/span&gt; / &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt; __ / /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / &lt;span class="se"&gt;\/&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;/ / / ‾‾&lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;‾&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / __________ &lt;span class="se"&gt;\ &lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;_&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\_\ &lt;/span&gt; &lt;span class="se"&gt;\_&lt;/span&gt;____/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; execution: &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; script: script.js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; output: -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; scenarios: &lt;span class="o"&gt;(&lt;/span&gt;100.00%&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; scenario, &lt;span class="m"&gt;10&lt;/span&gt; max VUs, 1m0s max duration &lt;span class="o"&gt;(&lt;/span&gt;incl. graceful stop&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * default: &lt;span class="m"&gt;10&lt;/span&gt; looping VUs &lt;span class="k"&gt;for&lt;/span&gt; 30s &lt;span class="o"&gt;(&lt;/span&gt;gracefulStop: 30s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO&lt;span class="o"&gt;[&lt;/span&gt;0030&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;k6-reporter v2.3.0&lt;span class="o"&gt;]&lt;/span&gt; Generating HTML summary report &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;console
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✓ status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; checks.........................: 100.00% ✓ &lt;span class="m"&gt;1331889&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data_received..................: &lt;span class="m"&gt;148&lt;/span&gt; GB 4.9 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; data_sent......................: &lt;span class="m"&gt;163&lt;/span&gt; MB 5.4 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.41µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;390ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.72µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.3ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;3.6µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;4.91µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;462.21µs p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;501.25µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.08µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;139.77µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.41ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;291.59µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;419.65µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;501.25µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.08µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;139.77µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.41ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;291.59µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;419.65µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;1331889&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;379.39µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.13µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;29.07µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.23ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;108.31µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;177.05µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.45µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;960ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.48µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.36ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;6.83µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;8.79µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;117.4µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33.65µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;98.19µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;23.03ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;176.34µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;225.09µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; http_reqs......................: &lt;span class="m"&gt;1331889&lt;/span&gt; 44333.219428/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;38.08ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.92ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;45.46ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80.35ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;47.68ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;48.53ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; iterations.....................: &lt;span class="m"&gt;7881&lt;/span&gt; 262.326742/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;running &lt;span class="o"&gt;(&lt;/span&gt;0m30.0s&lt;span class="o"&gt;)&lt;/span&gt;, 00/10 VUs, &lt;span class="m"&gt;7881&lt;/span&gt; &lt;span class="nb"&gt;complete&lt;/span&gt; and &lt;span class="m"&gt;0&lt;/span&gt; interrupted iterations
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;default ✓ &lt;span class="o"&gt;[======================================]&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; VUs 30s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Nice! Between the 10 VUs we configured, &lt;code&gt;k6&lt;/code&gt; managed nearly 8000 iterations, and my new server responded with a total of 148GB of data!&lt;/p&gt;
&lt;h2 id="nix-ified-load-testing" class="relative group"&gt;Nix-ified Load Testing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#nix-ified-load-testing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Now I&amp;rsquo;d figured out the basics of &lt;code&gt;k6&lt;/code&gt;, I wanted to create some infrastructure that would allow me to run load tests in &lt;em&gt;consistent environments&lt;/em&gt; against both old and new implementations of my server.&lt;/p&gt;
&lt;p&gt;A couple of ideas came to mind here - the first of which was the NixOS &lt;a href="https://nixcademy.com/posts/nixos-integration-tests/" target="_blank" rel="noreferrer"&gt;integration test driver&lt;/a&gt;. I absolutely love this feature of Nix, and the scripts for interacting with the driver are nice and simple to maintain. One slight irritation in this case is that the test machines don&amp;rsquo;t have access to the internet - which is where the redirects map is fetched from in my precompiled server binaries. It&amp;rsquo;s certainly possible to get clever with a fake redirects server and some DNS shennanigans in the test machines, but I opted instead to build simple &lt;code&gt;nixosConfiguration&lt;/code&gt;s which could be started as VMs, similar to the approach taken in a &lt;a href="https://jnsgr.uk/2024/02/nixos-vms-in-github-actions/" target="_blank" rel="noreferrer"&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wanted to automate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The creation of a VM with both the Go and Rust versions of my server&lt;/li&gt;
&lt;li&gt;The generation of the &lt;code&gt;paths.json&lt;/code&gt; and &lt;code&gt;redirects.json&lt;/code&gt; files I created above&lt;/li&gt;
&lt;li&gt;Running the &lt;code&gt;k6&lt;/code&gt; load test&lt;/li&gt;
&lt;li&gt;Fetching the results from the load test&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="defining-test-vms" class="relative group"&gt;Defining Test VMs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#defining-test-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I created a new repository with a &lt;code&gt;flake.nix&lt;/code&gt; which took &lt;code&gt;nixpkgs&lt;/code&gt;, my &lt;code&gt;jnsgr.uk&lt;/code&gt; repo and my &lt;code&gt;servy&lt;/code&gt; repo as inputs, pinned to the latest revisions at the time of writing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;web server benchmarking flake&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:nixos/nixpkgs/nixpkgs-unstable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:jnsgruk/jnsgr.uk/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:jnsgruk/servy/71a337317defc779c6c55d486e20d104c5d478f2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Next up I needed a virtual machine definition, which I first defined as a &lt;code&gt;nixosConfiguration&lt;/code&gt; in the flake&amp;rsquo;s outputs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... inputs ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;forAllSystems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;genAttrs&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64-linux&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aarch64-linux&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# A minimal NixOS virtual machine which used for testing craft applications.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixosConfigurations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forAllSystems&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;benchvm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;specialArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnsgruk-rust&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="sr"&gt;./vm.nix&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The actual machine configuration lives in &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix" target="_blank" rel="noreferrer"&gt;vm.nix&lt;/a&gt;. This is a standard NixOS configuration, with the addition of some elements that define the specs of the virtual machine used to boot it (cores, memory, disk, etc.). In this case the machine is configured with just a &lt;code&gt;root&lt;/code&gt; user, and the password &lt;code&gt;password&lt;/code&gt;. This is not an ideal setup from a security standpoint, but these VMs were only run on my workstation (behind NAT) for a short period of time and the plain text password eased the automation I&amp;rsquo;m about to describe. A more robust approach would have been to put my SSH public keys into the authorized keys definition for the user, but then you folks wouldn&amp;rsquo;t have been able to play along as easily!&lt;/p&gt;
&lt;p&gt;In this particular configuration, the VM is set up like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;virtualisation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;forwardPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;guest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;memorySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;diskSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With that in place, we can now boot the VM (in the background):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix run .#nixosConfigurations.benchvm.config.system.build.vm -- --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ ssh -p &lt;span class="m"&gt;2222&lt;/span&gt; root@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The authenticity of host &lt;span class="s1"&gt;&amp;#39;[localhost]:2222 ([127.0.0.1]:2222)&amp;#39;&lt;/span&gt; can&lt;span class="s1"&gt;&amp;#39;t be established.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;ED25519 key fingerprint is SHA256:icnH3EQAzmjdfCkyPWFljQWaVSCaXdP2M+ekKXd0NlY.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;This key is not known by any other names.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;Warning: Permanently added &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;localhost&lt;span class="o"&gt;]&lt;/span&gt;:2222&lt;span class="err"&gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;ED25519&lt;span class="o"&gt;)&lt;/span&gt; to the list of known hosts.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;root@localhost&lt;span class="o"&gt;)&lt;/span&gt; Password:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;root@benchvm:~&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="wrapping-k6" class="relative group"&gt;Wrapping &lt;code&gt;k6&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#wrapping-k6" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Now on to automating the test itself. When configuring the VM, I made sure to include a &lt;code&gt;systemd&lt;/code&gt; &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix#L34-L56" target="_blank" rel="noreferrer"&gt;unit&lt;/a&gt; for each of the server implementations, so next I wanted to write a simple script that would execute the load test for each implementation and fetch the results.&lt;/p&gt;
&lt;p&gt;Before that, I modified the &lt;code&gt;k6&lt;/code&gt; script to not only output a report to &lt;code&gt;stdout&lt;/code&gt;, but also to a text file, a JSON file and a rendered HTML file (I didn&amp;rsquo;t know at the time which I&amp;rsquo;d prefer, and I wanted to survey the options!). I achieved this by defining the &lt;a href="https://grafana.com/docs/k6/latest/results-output/end-of-test/custom-summary/#about-handlesummary" target="_blank" rel="noreferrer"&gt;&lt;code&gt;handleSummary&lt;/code&gt;&lt;/a&gt; function &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/script.js#L31-L38" target="_blank" rel="noreferrer"&gt;in the &lt;code&gt;k6&lt;/code&gt; script&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.json&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.html&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;htmlReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;summary.txt&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableColors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;textSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enableColors&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;\n\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;First I scripted the generation of the paths and the execution of the test script, and packaged the combination up as a Nix package for installation into the test machine. This is just a &lt;code&gt;bash&lt;/code&gt; &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/k6test" target="_blank" rel="noreferrer"&gt;script&lt;/a&gt; which ensures only the specified implementation is running (with &lt;code&gt;systemd&lt;/code&gt;), generates the list of URLs to test and then runs &lt;code&gt;k6&lt;/code&gt; against the server. That script is packaged as a &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L70-L94" target="_blank" rel="noreferrer"&gt;Nix package&lt;/a&gt; called &lt;code&gt;k6test&lt;/code&gt; which contains both my &lt;code&gt;bash&lt;/code&gt; script and the &lt;code&gt;k6&lt;/code&gt; test script; the Nix package is then &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/vm.nix#L58" target="_blank" rel="noreferrer"&gt;added to the VM configuration&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="automating-test-execution" class="relative group"&gt;Automating Test Execution &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automating-test-execution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I borrowed the &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/51025f3c4ea463644935dae8434f82a606a56742/test/vm-exec" target="_blank" rel="noreferrer"&gt;&lt;code&gt;vm-exec&lt;/code&gt;&lt;/a&gt; script from a past project, renaming it &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/benchvm-exec" target="_blank" rel="noreferrer"&gt;&lt;code&gt;benchvm-exec&lt;/code&gt;&lt;/a&gt;, and created a new script called &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/scripts/benchvm-test" target="_blank" rel="noreferrer"&gt;&lt;code&gt;benchvm-test&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\e[92m[&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;\e[0m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Set some SSH options to ignore host key errors and make logging quieter.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This is a bad idea in general, but here is used to faciliate comms with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# a brand new VM each time.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;-P &lt;span class="m"&gt;2222&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;UserKnownHostsFile=/dev/null&amp;#34;&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;StrictHostKeyChecking=no&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Running k6test against &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;benchvm-exec k6test &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Collecting results files&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.json &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.html &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sshpass -ppassword scp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; root@localhost:summary.txt &lt;span class="s2"&gt;&amp;#34;summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info &lt;span class="s2"&gt;&amp;#34;Results available in summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.txt, summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json and summary-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This was the final piece of the puzzle for now; after building &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L60-L67" target="_blank" rel="noreferrer"&gt;and packaging&lt;/a&gt; this script and &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L50" target="_blank" rel="noreferrer"&gt;defining the test VM as a package&lt;/a&gt; in the flake, I&amp;rsquo;d enabled the following workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Build &amp;amp; run the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ run-benchvm-vm --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test the gosherve based server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test jnsgruk-go
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test the Rust based server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test jnsgruk-rust
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# All done, power down the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-exec poweroff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Excellent! Now on to some actual testing!&lt;/p&gt;
&lt;h2 id="initial-load-test-results" class="relative group"&gt;Initial Load Test Results &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#initial-load-test-results" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Having seen the results for &lt;code&gt;servy&lt;/code&gt; earlier, I started up the existing &lt;code&gt;gosherve&lt;/code&gt; based server and ran the same test, only to discover quite a delta in the results. Where &lt;code&gt;servy&lt;/code&gt; managed 4.9 GB/s throughout the test, &lt;code&gt;gosherve&lt;/code&gt; only achieved 697 MB/s (sending 21GB in total). The full results are in the details box below, but overall it managed much lower performance across the board on the measurements that &lt;code&gt;k6&lt;/code&gt; makes.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Initial &lt;code&gt;gosherve&lt;/code&gt; load-test results&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 189449 ✗ 0
data_received..................: 21 GB 697 MB/s
data_sent......................: 26 MB 848 kB/s
http_req_blocked...............: avg=2.02µs min=400ns med=1.42µs max=19.76ms p(90)=2.29µs p(95)=2.77µs
http_req_connecting............: avg=203ns min=0s med=0s max=19.52ms p(90)=0s p(95)=0s
http_req_duration..............: avg=9.3ms min=29.49µs med=9.36ms max=152.39ms p(90)=13.16ms p(95)=14.76ms
{ expected_response:true }...: avg=9.3ms min=29.49µs med=9.36ms max=152.39ms p(90)=13.16ms p(95)=14.76ms
http_req_failed................: 0.00% ✓ 0 ✗ 189449
http_req_receiving.............: avg=106.56µs min=4.88µs med=48.7µs max=6.01ms p(90)=215.06µs p(95)=381.38µs
http_req_sending...............: avg=4.81µs min=950ns med=3.97µs max=19.71ms p(90)=5.93µs p(95)=6.98µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=9.19ms min=22.85µs med=9.26ms max=152.2ms p(90)=12.96ms p(95)=14.51ms
http_reqs......................: 189449 6281.256789/s
iteration_duration.............: avg=268.14ms min=176.88ms med=250.61ms max=433.23ms p(90)=334.14ms p(95)=342.1ms
iterations.....................: 1121 37.1672/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;I was quite surprised by this: my expectation was that the Go HTTP server would outperform what I&amp;rsquo;d put together in Rust. I decided to look a little deeper and see if I could figure out why, or at least why the delta was so big.&lt;/p&gt;
&lt;h2 id="profiling-gosherve-with-parca" class="relative group"&gt;Profiling &lt;code&gt;gosherve&lt;/code&gt; with Parca &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#profiling-gosherve-with-parca" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For last couple of years, I&amp;rsquo;ve been playing with &lt;a href="https://parca.dev" target="_blank" rel="noreferrer"&gt;Parca&lt;/a&gt;, which is a continuous profiling tool written in Go by the folks at Polar Signals. I wrote about it back in 2022 &lt;a href="https://discourse.charmhub.io/t/continuous-profiling-for-juju-parca-on-machines-and-kubernetes/6815" target="_blank" rel="noreferrer"&gt;on Charmhub&lt;/a&gt;, demonstrating how Parca could be used for profiling applications deployed with Juju.&lt;/p&gt;
&lt;h3 id="instrumenting-gosherve" class="relative group"&gt;Instrumenting &lt;code&gt;gosherve&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#instrumenting-gosherve" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;There are two components to the open source Parca offering - the server backend, and a zero-instrumentation, eBPF-based &lt;a href="https://github.com/parca-dev/parca-agent" target="_blank" rel="noreferrer"&gt;agent&lt;/a&gt; which can be used for on-CPU profiling of any workload. Because &lt;code&gt;gosherve&lt;/code&gt; is Go based, I didn&amp;rsquo;t need the agent so long as I enabled the &lt;code&gt;pprof&lt;/code&gt; endpoint in my server, which is trivial for almost any Go application. I took &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/main.go" target="_blank" rel="noreferrer"&gt;&lt;code&gt;main.go&lt;/code&gt;&lt;/a&gt; and applied the following changes to ensure that I could hit the &lt;code&gt;pprof&lt;/code&gt; endpoints on port &lt;code&gt;6060&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;diff --git a/main.go b/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;index 964ceb2..374c440 100644
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;--- a/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+++ b/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -10,6 +10,9 @@ import (
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;log/slog&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;os&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;#34;net/http&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ _ &amp;#34;net/http/pprof&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;github.com/jnsgruk/gosherve/pkg/logging&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;github.com/jnsgruk/gosherve/pkg/server&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -29,6 +32,11 @@ func main() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flag.Parse()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; logging.SetupLogger(*logLevel)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ go func() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ http.ListenAndServe(&amp;#34;:6060&amp;#34;, nil)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ slog.Info(&amp;#34;pprof server started on port 6060&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ }()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Create an fs.FS from the embedded filesystem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fsys, err := fs.Sub(publicFS, &amp;#34;public&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if err != nil {
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Next I fired up Parca (having &lt;a href="https://github.com/NixOS/nixpkgs/pull/359635" target="_blank" rel="noreferrer"&gt;first packaged it&lt;/a&gt; for NixOS 😉) and &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/parca/parca.yaml#L11" target="_blank" rel="noreferrer"&gt;configured it&lt;/a&gt; to scrape the new &lt;code&gt;jnsgruk&lt;/code&gt; binary while I ran the same test against it, which resulted in the following profile (which you can &lt;a href="https://pprof.me/20cdda5097bf379ffd282679e29ee32b" target="_blank" rel="noreferrer"&gt;explore yourself on pprof.me&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_b7f370c9787a6a87.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_2e044e16f34c8a1.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_e14618bcf0a96fe4.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_96ae66342724a104.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1702"
height="1799"
class="mx-auto my-0 rounded-md"
alt="pre-optimisation icicle graph representing the cpu profile of gosherve during a load test"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_8faa6e00b9e7decd.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_76aacc9b7f044124.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_8faa6e00b9e7decd.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_cbe2acf47a023708.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/01_hu_a0e7f911c48d6eb7.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking at this, we can see that around a third of the time was spent in garbage collection. This seemed high; it was more prominent on this run than others but it was persistently a high portion of the CPU time. Something to loop back to! However, inside the route handler itself there are two things that stood out to me:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_b4b345b2f13dbb92.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_dcc48e6ec09cb8c5.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_80b24a836ba222c6.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_266d93fd1f999e77.webp 1193w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1193"
height="979"
class="mx-auto my-0 rounded-md"
alt="zoomed in cpu profile showing activity in the route handler"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_7c530c6ab68fe57c.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_f2a831e1bc1b1a5e.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_7c530c6ab68fe57c.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02_hu_8bbbb86555338ad5.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/02.png 1193w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="initial-optimisations" class="relative group"&gt;Initial optimisations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#initial-optimisations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;In the red box, you can see about a third of the time spent in the route handler was in creating SHA1 sums, which is happening &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/route_handler.go#L70" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt; to calculate the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag" target="_blank" rel="noreferrer"&gt;ETag&lt;/a&gt; for content before serving it. This could probably be optimised - either by changing how the ETag is calculated (i.e. not hashing the whole file contents), or by caching ETags in memory, particularly given that in the case of my website, &lt;code&gt;gosherve&lt;/code&gt; only serves assets which are embedded (read only) in the binary, so the ETag will never change for a given file for a particular compiled binary.&lt;/p&gt;
&lt;p&gt;In the blue box, we can see that another third of the time spent is on refreshing the redirects. This seemed strange; the redirects don&amp;rsquo;t change throughout the life of the test, and we&amp;rsquo;re only requesting known redirects from our &lt;code&gt;redirects.json&lt;/code&gt; file. This seemed much more likely to be a culprit in the poor load test results, because unlike calculating a SHA1 sum (which is almost entirely CPU-bound), this function is making an outbound (TLS) connection to Github and potentially blocking on IO before parsing the response, rechecking the redirects map for a match, etc.&lt;/p&gt;
&lt;p&gt;As I looked at the code, I noticed an obvious (and somewhat embarrassing!) mistake I&amp;rsquo;d made when implementing &lt;code&gt;gosherve&lt;/code&gt;. Because the server tries to parse redirects &lt;em&gt;before&lt;/em&gt; looking for files with a matching path, every request for a file causes the redirects map to be updated from its upstream source 🤦. Looking back at the new &lt;code&gt;servy&lt;/code&gt; implementation, I hadn&amp;rsquo;t made the same mistake - perhaps this was the reason the Rust version was &lt;em&gt;so much faster&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;I made a &lt;a href="https://github.com/jnsgruk/gosherve/commit/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea" target="_blank" rel="noreferrer"&gt;small change&lt;/a&gt; to &lt;code&gt;gosherve&lt;/code&gt; and re-ran the test, which resulted in &lt;a href="https://pprof.me/e216e90fb0176ffbf11e7fac23b99682" target="_blank" rel="noreferrer"&gt;this profile&lt;/a&gt; and quite a substantial bump in request performance!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checks.........................: 100.00% ✓ &lt;span class="m"&gt;913783&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_received..................: &lt;span class="m"&gt;101&lt;/span&gt; GB 3.4 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_sent......................: &lt;span class="m"&gt;123&lt;/span&gt; MB 4.1 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.21µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;390ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.12µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14.12ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.19µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;5.7µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;70ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14.09ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.8ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.12µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;899.13µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;31.31ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.86ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.45ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.8ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.12µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;899.13µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;31.31ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.86ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.45ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;913783&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;261.88µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.85µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;63.59µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.8ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;620.15µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;1.31ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.21µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.05µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5.21µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8.73ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;9.87µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;13.07µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.53ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30.81µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;656.71µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30.76ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;4.38ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;5.91ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_reqs......................: &lt;span class="m"&gt;913783&lt;/span&gt; 30435.133157/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;55.5ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;28.86ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;55.18ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;77.07ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;61.02ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;62.98ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iterations.....................: &lt;span class="m"&gt;5407&lt;/span&gt; 180.089545/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The overall throughput increased by five times, with a decrease in latency across the board. Still around a third less overall throughput than the Rust server, but a huge improvement nonetheless.&lt;/p&gt;
&lt;p&gt;I subsequently experimented with (naively) removing the ETag calculation to see whether or not it was worth implementing some caching - but it actually resulted in very little difference in throughput and CPU utilisation. Besides, while the calculation takes some CPU time, in a real-life deployment it reduces the overall load on the server by ensuring that &lt;code&gt;http.ServeContent&lt;/code&gt; only sends actual content when there is a new version, relying more heavily on the user&amp;rsquo;s browser cache. My &lt;code&gt;k6&lt;/code&gt; tests (deliberately) weren&amp;rsquo;t sending the right headers (such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match" target="_blank" rel="noreferrer"&gt;&lt;code&gt;If-None-Match&lt;/code&gt;&lt;/a&gt;) to benefit from the behaviour one might expect from a browser, and it was just requesting the same files over and over again with the equivalent of a cold/empty cache.&lt;/p&gt;
&lt;h3 id="reducing-allocations" class="relative group"&gt;Reducing Allocations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#reducing-allocations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Now back to all those allocations, and the time spent in garbage collection as a result&amp;hellip; &lt;code&gt;gosherve&lt;/code&gt; was spending about a third of it&amp;rsquo;s time in the &lt;code&gt;io.ReadFile&lt;/code&gt; function, and given the amount of time spent in garbage collection, any optimisation made to the number of allocations on the heap would likely yield another big performance increase.&lt;/p&gt;
&lt;p&gt;The problem lied in the &lt;a href="https://github.com/jnsgruk/gosherve/blob/4ea0fdb6ca3bc18b2557c06b8c11460b2f7f76ea/pkg/server/route_handler.go#L63-L67" target="_blank" rel="noreferrer"&gt;&lt;code&gt;routeHandler&lt;/code&gt;&lt;/a&gt;, where I was reading the entire contents of each file that was being served into memory. &lt;code&gt;fs.ReadFile&lt;/code&gt; makes an allocation the size of the file it&amp;rsquo;s reading, meaning the contents of the file end up on the heap. In a situation like our load test - this means the entire contents of my website ended up on the heap, and the Go runtime was busily garbage collecting to clean up.&lt;/p&gt;
&lt;p&gt;I looked around at alternative ways to implement the same functionality in a more efficient manner. In Go 1.22, &lt;a href="https://pkg.go.dev/net/http@master#ServeFileFS" target="_blank" rel="noreferrer"&gt;&lt;code&gt;http.ServeFileFS&lt;/code&gt;&lt;/a&gt; was introduced as a counterpart to &lt;a href="https://pkg.go.dev/net/http@master#ServeFile" target="_blank" rel="noreferrer"&gt;&lt;code&gt;http.ServeFile&lt;/code&gt;&lt;/a&gt;. &lt;code&gt;http.ServeFileFS&lt;/code&gt; operates on an &lt;code&gt;fs.FS&lt;/code&gt; rather than the host filesystem. Under the hood, rather than reading the whole file into memory &lt;code&gt;ServeFileFS&lt;/code&gt; is using &lt;code&gt;io.Copy&lt;/code&gt; and thus &lt;a href="https://cs.opensource.google/go/go/&amp;#43;/refs/tags/go1.23.3:src/io/io.go;l=407" target="_blank" rel="noreferrer"&gt;&lt;code&gt;io.copyBuffer&lt;/code&gt;&lt;/a&gt; which allocates a single buffer that fits on the stack - fewer allocations means less memory usage, and less time spent occupying cores with garbage collection!&lt;/p&gt;
&lt;p&gt;The change to &lt;code&gt;gosherve&lt;/code&gt; was pretty &lt;a href="https://github.com/jnsgruk/gosherve/commit/c5647476a8c6bcdf703ef685809ad06007774b6f" target="_blank" rel="noreferrer"&gt;simple&lt;/a&gt; - you can see a slightly shortened diff below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;diff --git a/pkg/server/route_handler.go b/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;index 2417e45..e0c9d68 100644
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;--- a/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+++ b/pkg/server/route_handler.go
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -1,7 +1,6 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // snip!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- // Try reading the file and return early if that fails
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- b, err := fs.ReadFile(*s.webroot, filepath)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- if err != nil {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- return false
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;-
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; w.Header().Set(&amp;#34;Cache-Control&amp;#34;, &amp;#34;public, max-age=31536000, must-revalidate&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- w.Header().Set(&amp;#34;ETag&amp;#34;, fmt.Sprintf(`&amp;#34;%d-%x&amp;#34;`, len(b), sha1.Sum(b)))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ w.Header().Set(&amp;#34;ETag&amp;#34;, fmt.Sprintf(`&amp;#34;%s-%d-%x&amp;#34;`, fi.Name(), fi.Size(), sha1.Sum([]byte(fi.ModTime().String()))))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- http.ServeContent(w, r, filepath, time.Now(), bytes.NewReader(b))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ http.ServeFileFS(w, r, *s.webroot, filepath)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s.metrics.responseStatus.WithLabelValues(strconv.Itoa(http.StatusOK)).Inc()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; l.Info(&amp;#34;served file&amp;#34;, slog.Group(&amp;#34;response&amp;#34;, &amp;#34;status_code&amp;#34;, http.StatusOK, &amp;#34;file&amp;#34;, filepath))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This change also necessitated a change to ETag calculation, which now only hashes the filename, size and modified time. There is a slight oddity in that files from an embedded filesystem have their timestamps all fixed (to avoid changes in static files messing with build reproducibility), but since the files cannot change during a binary&amp;rsquo;s lifetime, the ETag calculated from the modified timestamp is still just as valid.&lt;/p&gt;
&lt;p&gt;I quickly made a new build of &lt;code&gt;jnsgruk&lt;/code&gt; using a &lt;a href="https://go.dev/ref/mod#go-mod-file-replace" target="_blank" rel="noreferrer"&gt;locally referenced&lt;/a&gt; copy of &lt;code&gt;gosherve&lt;/code&gt; and ran &lt;code&gt;k6&lt;/code&gt;/&lt;code&gt;parca&lt;/code&gt; to check the outcome. You can explore the profile yourself &lt;a href="https://pprof.me/d28a5d25a34c4ed742cf397e72b43ed3" target="_blank" rel="noreferrer"&gt;on pprof.me&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_3dad8a8a81768f62.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_babadf94790b33ba.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_8c779802f200a61b.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_d6176e1511be897b.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1574"
height="1068"
class="mx-auto my-0 rounded-md"
alt="zoomed in cpu profile showing activity in the route handler post optimisation, without all the extra allocations"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_233c4dbdf63307de.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_ecff087adc597bc6.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_233c4dbdf63307de.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_734ef44999d7406d.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/04_hu_7cd9bc9067fe5458.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here you can clearly see that the runtime is spending &lt;em&gt;much&lt;/em&gt; less time in garbage collection, and the call to &lt;code&gt;io.Copy&lt;/code&gt;/&lt;code&gt;io.copyBuffer&lt;/code&gt; without the calls to &lt;code&gt;fs.ReadFile&lt;/code&gt;. It&amp;rsquo;s now also much harder to spot the ETag generation - likely because we&amp;rsquo;re now only hashing a small string, rather than the whole file contents.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;k6&lt;/code&gt; results came back as follows 🚀:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checks.........................: 100.00% ✓ &lt;span class="m"&gt;1525300&lt;/span&gt; ✗ &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_received..................: &lt;span class="m"&gt;170&lt;/span&gt; GB 5.7 GB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data_sent......................: &lt;span class="m"&gt;207&lt;/span&gt; MB 6.9 MB/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_blocked...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;410ns &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.36µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;18.47ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;5.02µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;6.77µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_connecting............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;40ns &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;17.98ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_duration..............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;999.6µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.17µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;645.03µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24.22ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;2.34ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;3.09ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; expected_response:true &lt;span class="o"&gt;}&lt;/span&gt;...: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;999.6µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;42.17µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;645.03µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24.22ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;2.34ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;3.09ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_failed................: 0.00% ✓ &lt;span class="m"&gt;0&lt;/span&gt; ✗ &lt;span class="m"&gt;1525300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_receiving.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;245.43µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.64µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;75.29µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;23.48ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;626.08µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;1.18ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_sending...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9.69µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5.49µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;19.54ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;10.86µs p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;14.46µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_tls_handshaking.......: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;0s p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;0s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_req_waiting...............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;744.47µs &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;28.09µs &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;395.81µs &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20.62ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;1.89ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;2.58ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;http_reqs......................: &lt;span class="m"&gt;1525300&lt;/span&gt; 50823.211595/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iteration_duration.............: &lt;span class="nv"&gt;avg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;34.41ms &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;11.91ms &lt;span class="nv"&gt;med&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;34.44ms &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;67.75ms p&lt;span class="o"&gt;(&lt;/span&gt;90&lt;span class="o"&gt;)=&lt;/span&gt;37.6ms p&lt;span class="o"&gt;(&lt;/span&gt;95&lt;span class="o"&gt;)=&lt;/span&gt;38.61ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iterations.....................: &lt;span class="m"&gt;8716&lt;/span&gt; 290.418352/s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus............................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vus_max........................: &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The Go server is now in the lead! There isn&amp;rsquo;t a huge margin here between the Rust server and the Go server, but this is about the sort of difference I was expecting based on the reading I&amp;rsquo;d done before doing the testing.&lt;/p&gt;
&lt;h2 id="profiling-servy" class="relative group"&gt;Profiling &lt;code&gt;servy&lt;/code&gt;? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#profiling-servy" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I &lt;em&gt;did&lt;/em&gt; profile &lt;code&gt;servy&lt;/code&gt; using &lt;code&gt;parca-agent&lt;/code&gt;, but I&amp;rsquo;ve yet to look at the results in detail. The profile is a lot more complex (see below), partially as a result of how much work is going on under the hood when standing up an &lt;code&gt;axum&lt;/code&gt; server with &lt;code&gt;hyper&lt;/code&gt;, &lt;code&gt;tower&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt;. I&amp;rsquo;m going to spend some more time with this profile over the coming weeks, but you can explore it for yourself &lt;a href="https://pprof.me/e6e8ad8c98d4c1406363312a9cec09b1/?color_by=filename" target="_blank" rel="noreferrer"&gt;on pprof.me&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_aaa7fb6101f64508.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_b6b418958567b5cd.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_4c2117bf11075aff.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_f6be7bd277688eed.webp 1297w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1297"
height="1936"
class="mx-auto my-0 rounded-md"
alt="crazy cpu profile for servy with lots of densely packed information on screen"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_69070aebbb6abb3d.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_9950977b539af752.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_69070aebbb6abb3d.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05_hu_393a8846648a767.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/05.png 1297w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you do play with the profile on &lt;code&gt;pprof.me&lt;/code&gt;, try filtering by function name and entering &lt;code&gt;servy&lt;/code&gt;, and you&amp;rsquo;ll see the parts of the code I wrote highlighted on the profile to explore:&lt;/p&gt;
&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_dd08b26f6f7a1751.webp 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_323dfdfd62326307.webp 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_1e109ba191282b1b.webp 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_29b75dc9a5be1e09.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1387"
height="943"
class="mx-auto my-0 rounded-md"
alt="filtering by function name on pprof.me"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_e987abf4a0772495.png" srcset="https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_6d5aa0cf511d0dd1.png 330w,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_e987abf4a0772495.png 660w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_cb45d31250cb9e89.png 1024w
,https://jnsgr.uk/2024/12/experiments-with-rust-nix-k6-parca/06_hu_873da1b8aa292b52.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By the time you read this, the &lt;a href="https://github.com/NixOS/nixpkgs/pull/360132" target="_blank" rel="noreferrer"&gt;&lt;code&gt;parca-agent&lt;/code&gt; should be upstream&lt;/a&gt; into &lt;code&gt;nixpkgs&lt;/code&gt;, but in the mean time you can pull the package I included in the &lt;code&gt;server-bench&lt;/code&gt; repo and use that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo parca-agent -- &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --remote-store-address&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;grpc.polarsignals.com:443&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --remote-store-bearer-token&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --node&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or, on Ubuntu (see polarsignals.com/docs/setup-collection-snaps)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo snap install --classic parca-agent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In my Nix example above, I&amp;rsquo;m sending profiles to &lt;a href="https://www.polarsignals.com/" target="_blank" rel="noreferrer"&gt;Polar Signals Cloud&lt;/a&gt;, but you can also set the &lt;code&gt;remote-store-address&lt;/code&gt; to the address of a locally running &lt;code&gt;parca&lt;/code&gt; instance no problem.&lt;/p&gt;
&lt;p&gt;Depending on the language you&amp;rsquo;re profiling, you&amp;rsquo;ll either need to include debug info in the binary you&amp;rsquo;re profiling (which is what I did), or upload your source/debug info manually. There are more instructions for that &lt;a href="https://www.polarsignals.com/docs/rust" target="_blank" rel="noreferrer"&gt;in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="final-results--comparisons" class="relative group"&gt;Final Results &amp;amp; Comparisons &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#final-results--comparisons" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I wanted to gather some test results from all three targets across a number of different machine configurations; my website runs on a teeny-tiny &lt;a href="https://fly.io" target="_blank" rel="noreferrer"&gt;Fly.io&lt;/a&gt; instance with 1 vCPU and 256MB RAM, so I was interested to see how performance changed on machines with varying configurations. I&amp;rsquo;ve included a summary in the table below which highlights just the &lt;code&gt;data_received&lt;/code&gt; measurement from each test, and you can see the full reports from &lt;code&gt;k6&lt;/code&gt; by expanding the details box:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Expand for full `k6` reports&lt;/summary&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 23322 ✗ 0
data_received..................: 2.6 GB 86 MB/s
data_sent......................: 3.1 MB 105 kB/s
http_req_blocked...............: avg=2.41µs min=400ns med=1.28µs max=1.04ms p(90)=1.87µs p(95)=2.27µs
http_req_connecting............: avg=50ns min=0s med=0s max=508.13µs p(90)=0s p(95)=0s
http_req_duration..............: avg=7.57ms min=49.54µs med=7.71ms max=143.13ms p(90)=9.42ms p(95)=10.65ms
{ expected_response:true }...: avg=7.57ms min=49.54µs med=7.71ms max=143.13ms p(90)=9.42ms p(95)=10.65ms
http_req_failed................: 0.00% ✓ 0 ✗ 23322
http_req_receiving.............: avg=98.03µs min=4.46µs med=57.25µs max=7.69ms p(90)=177.19µs p(95)=250.6µs
http_req_sending...............: avg=5.7µs min=970ns med=3.1µs max=480.07µs p(90)=4.79µs p(95)=9.26µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=7.47ms min=37.26µs med=7.62ms max=142.9ms p(90)=9.24ms p(95)=10.33ms
http_reqs......................: 23322 776.881114/s
iteration_duration.............: avg=217.52ms min=187.06ms med=198.66ms max=359.52ms p(90)=276.89ms p(95)=280.19ms
iterations.....................: 138 4.59693/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 158691 ✗ 0
data_received..................: 18 GB 587 MB/s
data_sent......................: 21 MB 714 kB/s
http_req_blocked...............: avg=1.89µs min=379ns med=1.08µs max=1.15ms p(90)=1.62µs p(95)=1.98µs
http_req_connecting............: avg=11ns min=0s med=0s max=655.05µs p(90)=0s p(95)=0s
http_req_duration..............: avg=1.04ms min=48.25µs med=537.57µs max=31.33ms p(90)=2.63ms p(95)=3.66ms
{ expected_response:true }...: avg=1.04ms min=48.25µs med=537.57µs max=31.33ms p(90)=2.63ms p(95)=3.66ms
http_req_failed................: 0.00% ✓ 0 ✗ 158691
http_req_receiving.............: avg=91.12µs min=3.66µs med=20.8µs max=23.05ms p(90)=156.29µs p(95)=287.17µs
http_req_sending...............: avg=6.47µs min=900ns med=2.57µs max=2.22ms p(90)=4.06µs p(95)=6.21µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=949.02µs min=35.88µs med=466.14µs max=31.3ms p(90)=2.46ms p(95)=3.47ms
http_reqs......................: 158691 5287.057691/s
iteration_duration.............: avg=31.95ms min=21.16ms med=30.39ms max=107.46ms p(90)=36.19ms p(95)=41.18ms
iterations.....................: 939 31.284365/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;1 vCPU/256MB RAM/1 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 148213 ✗ 0
data_received..................: 16 GB 547 MB/s
data_sent......................: 18 MB 605 kB/s
http_req_blocked...............: avg=1.5µs min=350ns med=971ns max=1.11ms p(90)=1.48µs p(95)=1.82µs
http_req_connecting............: avg=11ns min=0s med=0s max=451.97µs p(90)=0s p(95)=0s
http_req_duration..............: avg=928.79µs min=69.84µs med=671.38µs max=55.59ms p(90)=1.57ms p(95)=2.18ms
{ expected_response:true }...: avg=928.79µs min=69.84µs med=671.38µs max=55.59ms p(90)=1.57ms p(95)=2.18ms
http_req_failed................: 0.00% ✓ 0 ✗ 148213
http_req_receiving.............: avg=136.98µs min=3.76µs med=17.14µs max=46.31ms p(90)=103.7µs p(95)=192.52µs
http_req_sending...............: avg=5.33µs min=890ns med=2.35µs max=2.99ms p(90)=3.69µs p(95)=5.05µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=786.47µs min=55.89µs med=629.47µs max=17.92ms p(90)=1.43ms p(95)=1.96ms
http_reqs......................: 148213 4940.080272/s
iteration_duration.............: avg=34.19ms min=16.57ms med=25.68ms max=109.45ms p(90)=55.08ms p(95)=57.83ms
iterations.....................: 877 29.231244/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 116272 ✗ 0
data_received..................: 13 GB 426 MB/s
data_sent......................: 16 MB 518 kB/s
http_req_blocked...............: avg=2.25µs min=380ns med=1.06µs max=4.3ms p(90)=1.69µs p(95)=2.05µs
http_req_connecting............: avg=475ns min=0s med=0s max=4.27ms p(90)=0s p(95)=0s
http_req_duration..............: avg=15.37ms min=29.8µs med=14.28ms max=145.82ms p(90)=24.79ms p(95)=29.78ms
{ expected_response:true }...: avg=15.37ms min=29.8µs med=14.28ms max=145.82ms p(90)=24.79ms p(95)=29.78ms
http_req_failed................: 0.00% ✓ 0 ✗ 116272
http_req_receiving.............: avg=174.41µs min=4.15µs med=28.84µs max=21.95ms p(90)=241.72µs p(95)=703.97µs
http_req_sending...............: avg=7.22µs min=890ns med=2.98µs max=5.58ms p(90)=5.8µs p(95)=9.15µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=15.19ms min=22.98µs med=14.11ms max=144.5ms p(90)=24.48ms p(95)=29.37ms
http_reqs......................: 116272 3837.442876/s
iteration_duration.............: avg=439.41ms min=323.27ms med=430.04ms max=567.74ms p(90)=505.87ms p(95)=516.96ms
iterations.....................: 688 22.706763/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 362167 ✗ 0
data_received..................: 40 GB 1.3 GB/s
data_sent......................: 49 MB 1.6 MB/s
http_req_blocked...............: avg=2.23µs min=360ns med=770ns max=12.31ms p(90)=1.34µs p(95)=1.66µs
http_req_connecting............: avg=117ns min=0s med=0s max=1.84ms p(90)=0s p(95)=0s
http_req_duration..............: avg=4.7ms min=32.38µs med=3.94ms max=47.81ms p(90)=9.04ms p(95)=11.63ms
{ expected_response:true }...: avg=4.7ms min=32.38µs med=3.94ms max=47.81ms p(90)=9.04ms p(95)=11.63ms
http_req_failed................: 0.00% ✓ 0 ✗ 362167
http_req_receiving.............: avg=196.28µs min=3.38µs med=15.42µs max=25.62ms p(90)=142.41µs p(95)=896.21µs
http_req_sending...............: avg=8.44µs min=890ns med=2.19µs max=18.13ms p(90)=3.63µs p(95)=6.34µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=4.5ms min=25.57µs med=3.77ms max=45.46ms p(90)=8.74ms p(95)=11.13ms
http_reqs......................: 362167 12040.737924/s
iteration_duration.............: avg=140.21ms min=80.54ms med=139.82ms max=221.91ms p(90)=153.25ms p(95)=157.95ms
iterations.....................: 2143 71.24697/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;2 vCPU/4GB RAM/10 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 464412 ✗ 0
data_received..................: 51 GB 1.7 GB/s
data_sent......................: 57 MB 1.9 MB/s
http_req_blocked...............: avg=1.99µs min=360ns med=720ns max=14.38ms p(90)=1.13µs p(95)=1.42µs
http_req_connecting............: avg=92ns min=0s med=0s max=6.71ms p(90)=0s p(95)=0s
http_req_duration..............: avg=3.67ms min=44.91µs med=3.07ms max=60.17ms p(90)=5.9ms p(95)=7.98ms
{ expected_response:true }...: avg=3.67ms min=44.91µs med=3.07ms max=60.17ms p(90)=5.9ms p(95)=7.98ms
http_req_failed................: 0.00% ✓ 0 ✗ 464412
http_req_receiving.............: avg=206.58µs min=3.41µs med=14.06µs max=56.96ms p(90)=56.97µs p(95)=143.43µs
http_req_sending...............: avg=7.12µs min=870ns med=1.62µs max=14.84ms p(90)=3.01µs p(95)=5.78µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=3.45ms min=35.61µs med=3.01ms max=24.64ms p(90)=5.7ms p(95)=7.54ms
http_reqs......................: 464412 15448.582291/s
iteration_duration.............: avg=109.3ms min=63.59ms med=108.88ms max=161.77ms p(90)=120.71ms p(95)=124.65ms
iterations.....................: 2748 91.41173/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt; (old)&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 119990 ✗ 0
data_received..................: 13 GB 439 MB/s
data_sent......................: 16 MB 534 kB/s
http_req_blocked...............: avg=2.05µs min=440ns med=1.55µs max=972.62µs p(90)=2.32µs p(95)=2.74µs
http_req_connecting............: avg=38ns min=0s med=0s max=275.54µs p(90)=0s p(95)=0s
http_req_duration..............: avg=14.82ms min=35.49µs med=15.16ms max=148.26ms p(90)=21.12ms p(95)=23.89ms
{ expected_response:true }...: avg=14.82ms min=35.49µs med=15.16ms max=148.26ms p(90)=21.12ms p(95)=23.89ms
http_req_failed................: 0.00% ✓ 0 ✗ 119990
http_req_receiving.............: avg=104.7µs min=5.07µs med=43.71µs max=8.96ms p(90)=228.22µs p(95)=396.05µs
http_req_sending...............: avg=4.52µs min=940ns med=3.99µs max=944.81µs p(90)=6.28µs p(95)=7.86µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=14.71ms min=23.97µs med=15.05ms max=148.25ms p(90)=20.95ms p(95)=23.7ms
http_reqs......................: 119990 3957.203279/s
iteration_duration.............: avg=426.57ms min=343.33ms med=422.38ms max=526.97ms p(90)=491ms p(95)=502.47ms
iterations.....................: 710 23.415404/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;gosherve&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 1297244 ✗ 0
data_received..................: 144 GB 4.8 GB/s
data_sent......................: 175 MB 5.8 MB/s
http_req_blocked...............: avg=2.83µs min=419ns med=1.63µs max=7.97ms p(90)=2.95µs p(95)=3.78µs
http_req_connecting............: avg=2ns min=0s med=0s max=142.69µs p(90)=0s p(95)=0s
http_req_duration..............: avg=1.22ms min=31.96µs med=697.97µs max=36.9ms p(90)=3.16ms p(95)=4.07ms
{ expected_response:true }...: avg=1.22ms min=31.96µs med=697.97µs max=36.9ms p(90)=3.16ms p(95)=4.07ms
http_req_failed................: 0.00% ✓ 0 ✗ 1297244
http_req_receiving.............: avg=234.98µs min=4.21µs med=39.77µs max=35.6ms p(90)=640.71µs p(95)=1.24ms
http_req_sending...............: avg=7.19µs min=930ns med=3.91µs max=30.11ms p(90)=6.84µs p(95)=9.22µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=980.01µs min=19.74µs med=465.75µs max=16.64ms p(90)=2.75ms p(95)=3.68ms
http_reqs......................: 1297244 43221.02723/s
iteration_duration.............: avg=39.08ms min=23.12ms med=38.84ms max=76ms p(90)=43.87ms p(95)=45.71ms
iterations.....................: 7676 255.745723/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;16 vCPU/16GB RAM/10 VUs - &lt;code&gt;servy&lt;/code&gt;&lt;/summary&gt;
&lt;pre&gt;
checks.........................: 100.00% ✓ 1407770 ✗ 0
data_received..................: 156 GB 5.2 GB/s
data_sent......................: 172 MB 5.7 MB/s
http_req_blocked...............: avg=2.28µs min=420ns med=1.64µs max=3.96ms p(90)=3µs p(95)=3.94µs
http_req_connecting............: avg=2ns min=0s med=0s max=292.26µs p(90)=0s p(95)=0s
http_req_duration..............: avg=512.52µs min=40.21µs med=177.39µs max=45.65ms p(90)=444.41µs p(95)=623.84µs
{ expected_response:true }...: avg=512.52µs min=40.21µs med=177.39µs max=45.65ms p(90)=444.41µs p(95)=623.84µs
http_req_failed................: 0.00% ✓ 0 ✗ 1407770
http_req_receiving.............: avg=324.43µs min=4.02µs med=26.56µs max=45.31ms p(90)=88.04µs p(95)=147.94µs
http_req_sending...............: avg=4.33µs min=969ns med=3.35µs max=4.11ms p(90)=5.86µs p(95)=7.31µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=183.74µs min=31.45µs med=132.09µs max=8.96ms p(90)=344.35µs p(95)=465.04µs
http_reqs......................: 1407770 46854.747614/s
iteration_duration.............: avg=36.03ms min=5.19ms med=45.31ms max=77.29ms p(90)=48.06ms p(95)=48.95ms
iterations.....................: 8330 277.247027/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
&lt;/pre&gt;
&lt;/details&gt;
&lt;/details&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;gosherve (old)&lt;/code&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;gosherve&lt;/code&gt;&lt;/th&gt;
&lt;th style="text-align: left"&gt;&lt;code&gt;servy&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;1 vCPU/256MB RAM/1 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;86MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;587MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;547MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;2 vCPU/4GB RAM/10 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;426MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;1.3GB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;1.7GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;16 vCPU/16GB RAM/10 VUs&lt;/td&gt;
&lt;td style="text-align: left"&gt;439MB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;4.8GB/s&lt;/td&gt;
&lt;td style="text-align: left"&gt;&lt;strong&gt;5.2GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Interestingly, it seems that the &lt;code&gt;servy&lt;/code&gt; variant actually outperformed &lt;code&gt;gosherve&lt;/code&gt; again in these VM-based tests, where it didn&amp;rsquo;t when run directly on my machine. In any case, the gap is now &lt;em&gt;much&lt;/em&gt; smaller between the two, and &lt;code&gt;gosherve&lt;/code&gt; still seems to maintain an edge on very small machines.&lt;/p&gt;
&lt;p&gt;Note I had to drop the VUs to &lt;code&gt;1&lt;/code&gt; on the smallest iteration, or I was seeing all three of the variants getting OOM-killed!&lt;/p&gt;
&lt;p&gt;I also added a &lt;a href="https://github.com/jnsgruk/server-bench/blob/6db6c179649f44bb2f0fcc6a2441d8fc38b24f03/flake.nix#L6" target="_blank" rel="noreferrer"&gt;new flake input&lt;/a&gt; in the &lt;code&gt;server-bench&lt;/code&gt; repo that adds the &lt;code&gt;gosherve&lt;/code&gt; based repo again, but pinned to the pre-optimised version.&lt;/p&gt;
&lt;p&gt;As such, if you want to try reproducing any of these results and measure old vs. new vs. &lt;code&gt;servy&lt;/code&gt;, you just need to do the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start the devShell for the server-bench project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ nix develop github:jnsgruk/server-bench
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Build &amp;amp; run the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ run-benchvm-vm --daemonize --display none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Edit the core/memory count as required&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ vim vm.nix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Start, and load test a server, choosing your variety&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-test &amp;lt;jnsgruk-go-old&lt;span class="p"&gt;|&lt;/span&gt;jnsgruk-go&lt;span class="p"&gt;|&lt;/span&gt;jnsgruk-rust&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Power down the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ benchvm-exec poweroff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The results will be gathered in a collection of &lt;code&gt;summary-*.&amp;lt;txt|html|json&amp;gt;&lt;/code&gt; files for you to inspect.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This was supposed to be a blog about learning Rust, but I&amp;rsquo;m pleased with where it ended up! This was by no means a particularly in-depth dive into measuring the performance of web servers, focusing almost entirely on the request throughput. There are other factors that could be taken into consideration - such as the resulting memory pressure on the machine, CPU usage, etc. I may come back to this in another post with more comprehensive measurement of both.&lt;/p&gt;
&lt;p&gt;Hopefully this post illustrated the power of load testing and profiling tools - even when only used at quite a surface level, and demonstrated how Nix can be used to create robust structures around existing tools to help with such workflows.&lt;/p&gt;
&lt;p&gt;I owe a thank you to &lt;a href="https://github.com/brancz" target="_blank" rel="noreferrer"&gt;Frederic&lt;/a&gt; from Polar Signals, who gave me a bunch of helpful tips along the way while I was using Parca and profiling &lt;code&gt;gosherve&lt;/code&gt;. After my initial improvement in the request handler, Frederic helped me track down and remove the use of &lt;code&gt;fs.ReadFile&lt;/code&gt; and further improve the performance of &lt;code&gt;gosherve&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still very early in my Rust development, so if you&amp;rsquo;ve spotted something horrible in &lt;code&gt;servy&lt;/code&gt;, or you&amp;rsquo;ve got experience with &lt;code&gt;k6&lt;/code&gt; and &lt;code&gt;parca&lt;/code&gt;, then reach out and let me know! I&amp;rsquo;d love to see the creative ways people use these tools.&lt;/p&gt;
&lt;p&gt;The performance gap between &lt;code&gt;servy&lt;/code&gt; and &lt;code&gt;gosherve&lt;/code&gt; is now quite narrow. Close enough, in fact, that they seem to win over one another depending on different conditions in the test environment, and never by a particularly high margin. I haven&amp;rsquo;t migrated this site over yet, but perhaps I will in the near future. Besides, Fly.io are &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/3240a0104c01ae672a6f5f7b0529ad08bcbc8af2/fly.toml#L27-L30" target="_blank" rel="noreferrer"&gt;already kindly limiting the damage&lt;/a&gt; that can be done by too many requests for me!&lt;/p&gt;
&lt;p&gt;See you next time!&lt;/p&gt;</description></item><item><title>Hot Tub Monitoring with Home Assistant and ESPHome</title><link>https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/</link><pubDate>Tue, 12 Nov 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This year we put a wood-fired hot tub in our back garden. It&amp;rsquo;s a total indulgence and a real treat. I originally purchased the tub for my wife, but I must admit to having grown quite fond of it myself!&lt;/p&gt;
&lt;p&gt;The tub itself is made from Canadian Redwood Cedar. The photo below was taken shortly after it was assembled, so there is still a little bit of leakage which stopped a few days later once the wood had expanded. The 30kW wood burner is the most effective way to heat the tub (which holds around 1700l of water), but there is also a small electric heater in line with the pump &amp;amp; filter which are stashed behind the tub.&lt;/p&gt;
&lt;p&gt;There is something wonderfully simple and low-tech about the whole arrangement, but naturally I wanted to get an understanding of the energy usage, and get a reading on the temperature so that I could more accurately set it up for when we wanted to use it. Part of the reasoning for understanding the energy usage was to ensure that the pump and UV filter only fire up when the house is generating enough solar energy to cover it.&lt;/p&gt;
&lt;p&gt;A picture paints a thousand words, so you can see what I&amp;rsquo;m talking about below:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.jpg"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_9c750dd42fdbf08a.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_14979630695e492c.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_af7fc98f51dd5ff9.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_bc97ab272a62837c.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1600"
height="1600"
class="mx-auto my-0 rounded-md"
alt="photograph of a wooden hot tub with a wood burner next to it"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_39cc3e120645411.jpg" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_afd11ad5d01bebd6.jpg 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_39cc3e120645411.jpg 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_12596a4d6ff5d124.jpg 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/01_hu_a20db4fd754ffe4.jpg 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="energy-usage-monitoring" class="relative group"&gt;Energy Usage Monitoring &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#energy-usage-monitoring" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Starting with the easy part: monitoring the energy usage for each of the components. What you can&amp;rsquo;t see in the picture above is the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;a href="https://vulcanpools.co.uk/product/splasher/" target="_blank" rel="noreferrer"&gt;Elecro Vulcan 3kw heater&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://evolutionaqua.com/evouv" target="_blank" rel="noreferrer"&gt;evoUV 15w&lt;/a&gt; UV clarifier&lt;/li&gt;
&lt;li&gt;A &lt;a href="http://www.crystalclearpond.co.uk/product_info.php/products_id/2860" target="_blank" rel="noreferrer"&gt;Crystal Enterprises CC2513&lt;/a&gt; pump&lt;/li&gt;
&lt;li&gt;A &lt;a href="http://www.cheshireluxurypools.co.uk/product_info.php/products_id/2873" target="_blank" rel="noreferrer"&gt;Crystal Enterprises CC3030&lt;/a&gt; filter unit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The UV filter, heater and sand filter are all essentially &amp;ldquo;in line&amp;rdquo; - when the pump is switched on, water is pulled from the bottom of the tub, through the UV clarifier, sand filter and heater, and then back into the tub (not necessarily in that order!). The heater has a thermostat in it so that it can cut in/out depending on the desired temperature as the water passes through.&lt;/p&gt;
&lt;p&gt;In reality, the 3kw heater would take 12-15 hours to heat the tub from scratch, which is why we use the burner that usually takes around 1.5-2 hours. The electric heater &lt;em&gt;is&lt;/em&gt; good for maintaining the temperature once the initial heating has been done.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve had quite a lot of success with &lt;a href="https://www.tp-link.com/uk/home-networking/smart-plug/tapo-p110/" target="_blank" rel="noreferrer"&gt;TP-Link Tapo P110&lt;/a&gt; smart plugs. They&amp;rsquo;re very cheap, and there is already a competent Home Assistant integration for them. The only downside is that they require a proprietary app for initial setup. After having bought a few of these, I learned about &lt;a href="https://templates.blakadder.com/index.html" target="_blank" rel="noreferrer"&gt;Tasmota&lt;/a&gt;, and will probably buy smart plugs compatible with this or similar open source firmwares in the future.&lt;/p&gt;
&lt;p&gt;My friends from the &lt;a href="https://selfhosted.show" target="_blank" rel="noreferrer"&gt;Self Hosted Podcast&lt;/a&gt; seem to recommend buying pre-flashed smart devices from &lt;a href="https://cloudfree.shop" target="_blank" rel="noreferrer"&gt;cloudfree.shop&lt;/a&gt;, which I&amp;rsquo;ll certainly consider next time.&lt;/p&gt;
&lt;p&gt;All of the above said - the TP-Link plugs work great for this use-case. I&amp;rsquo;ve got one for the pump and one for the heater. The pump is configured to run for 2 hours a day during peak daylight hours, and the heater can be toggled as and when I need it. The result, when added to Home Assistant is some neat energy monitoring and the ability to toggle things on and off easily when I need:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_7dbe639b216ccdb3.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_29913e39997f8e3b.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_49514efc3fe598ba.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_6f079d903dd5ed65.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1402"
height="1003"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing energy usage from the tub&amp;rsquo;s pump and heater over a 3 day period"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_a6df70eed0afa415.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_fae68af3650d9d.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_a6df70eed0afa415.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_4a961cf7e78ab48b.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/02_hu_70f2f1ded757218.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In an alternative view that summarises power usage for my home, there is a neat section covering the usage from individual devices (all connected to smart plugs):&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_a38dcee712d88b88.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_de9cf2e4c4998116.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_d5ddee38ee36c759.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_45ba595b1a6c1e4.webp 1084w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1084"
height="190"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing energy usage for multiple devices on a given day"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_c7b62c4f9afb72e2.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_be295f188117b6e8.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_c7b62c4f9afb72e2.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03_hu_f4b965214c6ee965.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/03.png 1084w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you can see - the heater is incredibly power-hungry, and as a result we hardly use it. In the two hours it ran on the day shown above, it consumed almost twice the energy that my main workstation used &lt;em&gt;all day&lt;/em&gt;. We now constrain it&amp;rsquo;s use to a couple of hours per day in the summer months when solar generation is at its peak - otherwise we just heat the tub with the wood burner as we want.&lt;/p&gt;
&lt;h2 id="temperature-sensor-initial-solution" class="relative group"&gt;Temperature Sensor: Initial Solution &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#temperature-sensor-initial-solution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;When the tub originally arrived, I looked around for an internet-connected pool thermometer, and was quite surprised at the lack of options. I had assumed this was something relatively common. I ended up ordering an &lt;a href="https://inkbird.com/collections/25-off-pool-thermometers/products/wireless-pool-thermometer-set-ibs-p02r" target="_blank" rel="noreferrer"&gt;Inkbird IBS-P02R&lt;/a&gt; kit.&lt;/p&gt;
&lt;p&gt;The kit comprises a small floating thermometer, and a display unit that communicates wirelessly with the thermometer. I picked this particular model because the display unit can be connected to Wi-Fi and then checked on through an app. I had (naively&amp;hellip;) assumed that I&amp;rsquo;d be able to get access through some sort of API, but that turned out not to be the case.&lt;/p&gt;
&lt;p&gt;Overall, this solution worked &lt;em&gt;okay&lt;/em&gt;. The temperature is reported in 5/10/15 minute intervals according to the configuration, and I had no issues with range/connectivity even though the tub is some distance from my house (and the house is heavily insulated with triple glazing!).&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://apps.apple.com/us/app/inkbird/id1589369968" target="_blank" rel="noreferrer"&gt;Inkbird app for iOS&lt;/a&gt; leaves a lot to be desired, though. I found the user experience pretty frustrating. The web service was often slow to respond, and it required an annoying number of clicks to get to the information I wanted.&lt;/p&gt;
&lt;p&gt;While researching the different ways I could get access to the information, I re-discovered the &lt;a href="https://inkbird.com/products/bluetooth-pool-thermometer-ibs-p01b" target="_blank" rel="noreferrer"&gt;Inkbird IBS-P01B&lt;/a&gt; which is a very similar thermometer to the one I&amp;rsquo;d bought, but communicates over bluetooth. I&amp;rsquo;d initially ruled this out since my server is well out of bluetooth range, but I&amp;rsquo;d also been looking for an excuse to play with an ESP32-based microcontroller&amp;hellip;&lt;/p&gt;
&lt;h2 id="temperature-sensor-homemade-solution" class="relative group"&gt;Temperature Sensor: Homemade Solution &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#temperature-sensor-homemade-solution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;After a little bit of research I ended up ordering the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1x &lt;a href="https://inkbird.com/products/bluetooth-pool-thermometer-ibs-p01b" target="_blank" rel="noreferrer"&gt;Inkbird IBS-P01B&lt;/a&gt; thermometer&lt;/li&gt;
&lt;li&gt;1x &lt;a href="https://www.amazon.co.uk/gp/product/B0D7ZGT9PM/" target="_blank" rel="noreferrer"&gt;ESP32-WROOM-32U&lt;/a&gt; development board&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The former was ordered from AliExpress, and the latter from Amazon. I chose the ESP32-WROOM-32U specifically because it has an external antenna. Given the eventual placement of the ESP32 and the location of the hot tub, I wanted to maximise the chances of establishing a good bluetooth connection.&lt;/p&gt;
&lt;p&gt;This was to be my first foray into ESP32/Arduino type development. I&amp;rsquo;d read (and heard&amp;hellip;) lots about &lt;a href="https://esphome.io/index.html" target="_blank" rel="noreferrer"&gt;ESPHome&lt;/a&gt;, and given my goal was to integrate with Home Assistant, this felt like the right route.&lt;/p&gt;
&lt;h3 id="connecting-the-esp32" class="relative group"&gt;Connecting the ESP32 &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#connecting-the-esp32" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The first step was to flash ESPHome onto the device. The development board I bought comes with a &lt;a href="https://www.sparkfun.com/datasheets/IC/cp2102.pdf" target="_blank" rel="noreferrer"&gt;CP2012&lt;/a&gt; USB to UART bridge on the board, so connecting the device to my machine was as simple as plugging it in with a MicroUSB cable.&lt;/p&gt;
&lt;p&gt;Before the device can be flashed, it needs to be put into programming mode. In my case, the board has a handy &lt;code&gt;BOOT&lt;/code&gt; button. The process for entering programming mode was therefore (starting with the device unplugged and powered down):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Press and hold the &lt;code&gt;BOOT&lt;/code&gt; button&lt;/li&gt;
&lt;li&gt;Connect the ESP32 over USB&lt;/li&gt;
&lt;li&gt;Wait a few seconds&lt;/li&gt;
&lt;li&gt;Release the &lt;code&gt;BOOT&lt;/code&gt; button&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other development boards may not have a button, but all it really does is bridge &lt;code&gt;GPIO0&lt;/code&gt; and &lt;code&gt;GND&lt;/code&gt; on the board which can be done with a wire, too. There are good docs on the connection process on the ESPHome &lt;a href="https://esphome.io/guides/physical_device_connection" target="_blank" rel="noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="esphome-support" class="relative group"&gt;ESPHome Support &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#esphome-support" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;ESPHome doesn&amp;rsquo;t natively support the IBS-P01B, but after &lt;a href="https://community.home-assistant.io/t/inkbird-ibs-p01b-temp-readings/324402/5" target="_blank" rel="noreferrer"&gt;some reading&lt;/a&gt; it seemed that the underlying messaging format/protocol is similar enough to the &lt;a href="https://inkbird.com/products/bluetooth-thermometer-ibs-th1" target="_blank" rel="noreferrer"&gt;IBS-TH1&lt;/a&gt; and &lt;a href="https://inkbird.com/products/hygrometer-ibs-th2" target="_blank" rel="noreferrer"&gt;IBS-TH2&lt;/a&gt; that the same &lt;a href="https://esphome.io/components/sensor/inkbird_ibsth1_mini.html" target="_blank" rel="noreferrer"&gt;configuration&lt;/a&gt; can be used.&lt;/p&gt;
&lt;p&gt;I also came across this &lt;a href="https://blog.rpanachi.com/monitoring-swimming-pool-temperature-cheap-sensor-esphome" target="_blank" rel="noreferrer"&gt;blog post&lt;/a&gt; where the author had manually created an ESPHome configuration that appeared to work, and with the exact same hardware.&lt;/p&gt;
&lt;p&gt;Nonetheless - to my mind, simpler is better most of the time, so I set about creating a simple configuration using the supported ESPHome platform.&lt;/p&gt;
&lt;h3 id="esphome-dashboard" class="relative group"&gt;ESPHome Dashboard &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#esphome-dashboard" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;There are a few different ways to get started with ESPHome. You can &amp;ldquo;install&amp;rdquo; &lt;a href="https://esphome.io/guides/getting_started_command_line#installation" target="_blank" rel="noreferrer"&gt;using Docker&lt;/a&gt;, which seems to be the preferred method (passing through &lt;code&gt;/dev/ttyUSBx&lt;/code&gt; to the container for USB connectivity), or &lt;a href="https://esphome.io/guides/installing_esphome" target="_blank" rel="noreferrer"&gt;manually&lt;/a&gt; using Python packages.&lt;/p&gt;
&lt;p&gt;I tried to follow the &lt;a href="https://esphome.io/guides/getting_started_hassio" target="_blank" rel="noreferrer"&gt;instructions&lt;/a&gt; to get started with Home Assistant, but it seems this is not compatible with &amp;ldquo;unsupervised&amp;rdquo; Home Assistant servers like mine.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;esphome&lt;/code&gt; package is readily available in &lt;code&gt;nixpkgs&lt;/code&gt;, so I was able to get started and fire up the dashboard like so:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;❯ nix run unstable#esphome -- dashboard .
2024-11-12 14:47:19,072 INFO Starting dashboard web server on http://0.0.0.0:6052 and configuration dir ....
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Browsing to &lt;code&gt;http://localhost:6052&lt;/code&gt; yielded me the following page:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_11e1a75854fbb59c.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_65f070cc9b71de14.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_39ea7c3989bad6f.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_484f0b64c7f4bb09.webp 1104w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1104"
height="892"
class="mx-auto my-0 rounded-md"
alt="a screenshot of the ESPHome dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_d37d737ae5f8bab8.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_ae75eb0844fa5268.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_d37d737ae5f8bab8.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04_hu_bb24b08ac0f74764.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/04.png 1104w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="creating-a-firmware" class="relative group"&gt;Creating a Firmware &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#creating-a-firmware" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I proceeded to create a new device, and entered some basic information about my Wifi network:&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_87048ccf5be57e5f.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_f768b4bd2f6b2c11.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_a93e91e0fbf0c71.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_849be840a5755bd7.webp 1104w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1104"
height="892"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing the new device setup flow for ESPHome"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_7f7cd87d72eec855.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_9df322858e3c327a.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_7f7cd87d72eec855.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05_hu_2aa2e98ddcdc5319.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/05.png 1104w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is where things got interesting (for me, at least!). It turns out that the ESPHome dashboard makes use of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API" target="_blank" rel="noreferrer"&gt;Web Serial API&lt;/a&gt; to program the ESP32 chip over USB from the browser. Given that my board was already connected and in programming mode, this was pretty simple!&lt;/p&gt;
&lt;p&gt;The first time I tried this, I got a permission error. After looking through the messages in my kernel&amp;rsquo;s ring buffer with &lt;code&gt;journalctl -k&lt;/code&gt;, and looking at the permissions on the &lt;code&gt;/dev/ttyUSB0&lt;/code&gt; device, it seemed likely that the issue was the &lt;code&gt;root&lt;/code&gt;:&lt;code&gt;root&lt;/code&gt; ownership with limited permissions. I solved this by running the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown root:users /dev/ttyUSB0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod g+rwx /dev/ttyUSB0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This changed the group of the serial device to the same group as my user, then gave the group read, write and execute permissions. I retried in the browser and the device was flashed with the new firmware, connecting it to my network! Once the device was online, it appeared as such in the ESPHome dashboard, and I cooked up the following configuration by studing the docs on the &lt;a href="https://esphome.io/components/sensor/inkbird_ibsth1_mini.html" target="_blank" rel="noreferrer"&gt;&lt;code&gt;inkbird_ibsth1_mini&lt;/code&gt;&lt;/a&gt; sensor component:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;substitutions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esphome-web-caf3b8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;friendly_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Tub Monitor&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;esphome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${name}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;friendly_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${friendly_name}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;min_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2024.6.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name_add_mac_suffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esphome.web&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;esp32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;board&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esp32dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;arduino&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;web_server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;improv_serial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esphome&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;wifi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Pulled in from a separate secrets.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ssid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret wifi_ssid&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret wifi_password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;esp32_ble_tracker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;scan_parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;continuous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1min&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;inkbird_ibsth1_mini&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Bluetooth MAC address of my IBS-P01B&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mac_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Tub Temperature&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;battery_level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Tub Monitor Battery&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A few points on the above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The device setup wizard created a &lt;code&gt;secrets.yaml&lt;/code&gt; which can hold secrets to be referenced with the &lt;code&gt;!secret &amp;lt;secret name&amp;gt;&lt;/code&gt; syntax. This is the same format as &lt;a href="https://www.home-assistant.io/docs/configuration/secrets/" target="_blank" rel="noreferrer"&gt;used by Home Assistant&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Specifying &lt;code&gt;web_server:&lt;/code&gt; enables a small embedded web server that can be used to check status (see below!).&lt;/li&gt;
&lt;li&gt;I had to specify the MAC address of the pool thermometer, which I collected using the &lt;a href="https://apps.apple.com/us/app/bluetooth-inspector/id1509085044" target="_blank" rel="noreferrer"&gt;Bluetooth Inspector&lt;/a&gt; app for iOS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I clicked &lt;code&gt;Install&lt;/code&gt; in the top right of the window, and the firmware was compiled and flashed to the device! This time I was able to flash over Wifi since the device was now connected to my network as a result of the onboarding process:&lt;/p&gt;
&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_59db1e8b219fd69.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_f078b419b821f7f5.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_90bbd9ea3e00fb4a.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_3f542038070607a9.webp 1061w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1061"
height="877"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing the firmware install process in the esphome dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_2f1e71c9cb2f7d4.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_cc8ba32ff3c9f96d.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_2f1e71c9cb2f7d4.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06_hu_738713feff6aa040.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/06.png 1061w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After selecting &amp;ldquo;Wirelessly&amp;rdquo;, and waiting for the firmware to be flashed, the logs shortly started flowing in:&lt;/p&gt;
&lt;p&gt;&lt;a href="07.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_bd91ecb74c852f07.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_404437f4e869e9e6.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_df393da50a8f0c1d.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_36fa8abb8f6c58e6.webp 1061w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1061"
height="877"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing the logs streaming from the esphome device"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_d1e36230b8177fb1.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_8e050354e259eaea.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_d1e36230b8177fb1.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07_hu_5aa417b0265ce723.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/07.png 1061w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To double check things were working, I then browsed to the IP address of the ESPHome device, and was greeted with a simple page that shows the current state of the device:&lt;/p&gt;
&lt;p&gt;&lt;a href="08.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_526d495136e4bf43.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_6728cca70d234058.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_be168fb535edcef7.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_ce231d2032e324d9.webp 1117w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1117"
height="914"
class="mx-auto my-0 rounded-md"
alt="a screenshot showing the logs streaming from the esphome device directly"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_478fd4933a2aa510.png" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_6698dac749aadabd.png 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_478fd4933a2aa510.png 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08_hu_b59566bc4a9d3abf.png 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/08.png 1117w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Adding to Home Assistant was as simple as adding a new integration of type ESPHome and specifying the IP address of the device!&lt;/p&gt;
&lt;h3 id="3d-printing-an-enclosure" class="relative group"&gt;3D Printing an Enclosure &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#3d-printing-an-enclosure" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The final part of my build was to create a small enclosure for the device that would house the antenna correctly. I found a nice &lt;a href="https://www.printables.com/model/762529-esp32-wroom-32u-casing" target="_blank" rel="noreferrer"&gt;model&lt;/a&gt; on Printables, which printed pretty fast on my BambuLab X1 Carbon printer.&lt;/p&gt;
&lt;p&gt;The photo below shows the device in place on the window sill, with the hot tub just visible in the background:&lt;/p&gt;
&lt;p&gt;&lt;a href="09.jpg"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_6628e1c3257274c2.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_2b1f8c5e580e76e4.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_d3fa6cf8dc68e919.webp 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_99aaacfe3f73752f.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1600"
height="1600"
class="mx-auto my-0 rounded-md"
alt="a photo of the ESPHome device in its 3D printed case, with the hot tub visible through the window in the background"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_d23c0c9ee32fc3a2.jpg" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_4fa3fb29ffc4f336.jpg 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_d23c0c9ee32fc3a2.jpg 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_fc2d700e3da7ee43.jpg 1024w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/09_hu_e32b393a835e37e5.jpg 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="home-assistant-dashboard" class="relative group"&gt;Home Assistant Dashboard &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#home-assistant-dashboard" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;With all this in place, all that remained was tying the information into my Home Assistant dashboard:&lt;/p&gt;
&lt;p&gt;&lt;a href="10.jpg"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_f046761096960933.webp 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_ff5bda847b7e0c0d.webp 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_fe6fd645e34e38f.webp 738w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_fe6fd645e34e38f.webp 738w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="738"
height="1600"
class="mx-auto my-0 rounded-md"
alt="a screenshot of the Home Assistant iOS app displaying my newly created dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_4ddbf6c26174b872.jpg" srcset="https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_cb94fcae018047ed.jpg 330w,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10_hu_4ddbf6c26174b872.jpg 660w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10.jpg 738w
,https://jnsgr.uk/2024/11/hot-tub-monitoring-with-esphome/10.jpg 738w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The above shows the hot tub temperature, as well as the battery level of the IBS-P01 sensor. Beneath, I&amp;rsquo;ve added basic controls and measurements available through the Tapo Home Assistant integration, which allows me to quickly toggle the pump and heater, and see their daily energy usage at a glance.&lt;/p&gt;
&lt;p&gt;If you look closely, you&amp;rsquo;ll see the room temperatures resulting from the Roth underfloor heating integration I &lt;a href="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/" target="_blank" rel="noreferrer"&gt;wrote about&lt;/a&gt; previously.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;re super pleased with our tub - it was built by Andy from &lt;a href="https://rustictubs.com/" target="_blank" rel="noreferrer"&gt;Rustic Tubs&lt;/a&gt;. If you&amp;rsquo;re considering one and you&amp;rsquo;re in the UK, you could do a lot worse. It&amp;rsquo;s beautifully crafted, and Andy was really helpful and communicative throughout the process.&lt;/p&gt;
&lt;p&gt;This was my first foray into ESP32-based projects, and I was pleasantly surprised with how seamless the process was. The ESPHome docs were clear, and the no-hassle flashing through the browser was a nice way to get started.&lt;/p&gt;
&lt;p&gt;I hope this post is useful to people who are just getting started, and feel free to reach out if I&amp;rsquo;ve missed something!&lt;/p&gt;</description></item><item><title>Writing a Home Assistant Core Integration: Part 2</title><link>https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/</link><pubDate>Wed, 16 Oct 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In my &lt;a href="https://jnsgr.uk/2024/09/pytouchlinesl/" target="_blank" rel="noreferrer"&gt;last post&lt;/a&gt; I described the first steps toward creating a new &lt;a href="https://www.home-assistant.io/" target="_blank" rel="noreferrer"&gt;Home Assistant&lt;/a&gt; integration for the underfloor heating system in my house. In that post I outlined in detail how I set about creating a Python client for the API provided by the underfloor heating controller vendor.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;rsquo;ll describe the development setup, project structure and contribution process for building and landing a Home Assistant Core integration. I don&amp;rsquo;t consider myself an expert here, but I&amp;rsquo;ve documented my journey here in the hope that my experience might be useful to potential future contributors.&lt;/p&gt;
&lt;p&gt;The finished integration can be seen in the Home Assistant &lt;a href="https://www.home-assistant.io/integrations/touchline_sl/" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt; under the name &lt;code&gt;touchline_sl&lt;/code&gt;, and the code can be found &lt;a href="https://github.com/home-assistant/core/tree/dev/homeassistant/components/touchline_sl" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="development-setup" class="relative group"&gt;Development Setup &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#development-setup" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The Home Assistant documentation &lt;a href="https://developers.home-assistant.io/docs/development_environment/" target="_blank" rel="noreferrer"&gt;recommends&lt;/a&gt; the use of Visual Studio Code with a &lt;a href="https://containers.dev/" target="_blank" rel="noreferrer"&gt;devcontainer&lt;/a&gt;. This was a very quick way to get a working environment up and running, especially given that I already use Visual Studio Code for most of my programming, so I was immediately familiar.&lt;/p&gt;
&lt;p&gt;The repository provides some &lt;a href="https://code.visualstudio.com/Docs/editor/tasks" target="_blank" rel="noreferrer"&gt;tasks&lt;/a&gt; to help get started, including a task named &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/.vscode/tasks.json#L5" target="_blank" rel="noreferrer"&gt;&lt;code&gt;Run Home Assistant Core&lt;/code&gt;&lt;/a&gt;, which takes care of setting up the runtime environment, installing dependencies and starting the server. Neat!&lt;/p&gt;
&lt;p&gt;There are also a set of &lt;a href="https://pre-commit.com/" target="_blank" rel="noreferrer"&gt;pre-commit&lt;/a&gt; hooks set up to ensure you don&amp;rsquo;t make any common mistakes, accidentally violate the formatting/static-typing rules for the project, forget to update &lt;code&gt;requirements.txt&lt;/code&gt; files, etc.&lt;/p&gt;
&lt;p&gt;This turned out to be a really nice way to get started, and if you&amp;rsquo;re new to Home Assistant Core development, I&amp;rsquo;d recommend giving this &amp;ldquo;batteries-included&amp;rdquo; approach a go. If it&amp;rsquo;s not for you, the project provides &lt;a href="https://developers.home-assistant.io/docs/development_environment#manual-environment" target="_blank" rel="noreferrer"&gt;manual setup instructions&lt;/a&gt; too.&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_e619f013707ac582.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_238af1d3fe1ebaf1.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_7c9ddf97d3985a97.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_6f025739013db0a9.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1882"
height="1418"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant core running inside visual studio code"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_c01fb9f0989b2a8b.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_699eeee9ddc86df9.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_c01fb9f0989b2a8b.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_4a270193b779133.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/01_hu_5da2db046e9fcedb.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="integration-basics" class="relative group"&gt;Integration Basics &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#integration-basics" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;According to the &lt;a href="https://developers.home-assistant.io/docs/architecture_components" target="_blank" rel="noreferrer"&gt;documentation&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[An] integration is responsible for a specific domain within Home Assistant. Integrations can listen for or trigger events, offer actions, and maintain states.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Where a domain is&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;a short name consisting of characters and underscores. This domain has to be unique and cannot be changed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Err, right&amp;hellip; while this &lt;em&gt;is&lt;/em&gt; an accurate statement, it&amp;rsquo;s perhaps not the most enlightening for the budding new integration developer! At their core, integrations are Python modules that take information about a given system (like an underfloor heating system, or a smart plug, or a light bulb) and represent information about them in a format compatible with one of Home Assistant&amp;rsquo;s archetypes for &lt;a href="https://developers.home-assistant.io/docs/device_registry_index" target="_blank" rel="noreferrer"&gt;devices&lt;/a&gt;/&lt;a href="https://developers.home-assistant.io/docs/core/entity/sensor" target="_blank" rel="noreferrer"&gt;sensors&lt;/a&gt;/&lt;a href="https://developers.home-assistant.io/docs/creating_platform_index" target="_blank" rel="noreferrer"&gt;platforms&lt;/a&gt;/&lt;a href="https://developers.home-assistant.io/docs/core/entity" target="_blank" rel="noreferrer"&gt;entities&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this case, we&amp;rsquo;ll be representing &lt;a href="https://developers.home-assistant.io/docs/core/entity/climate/" target="_blank" rel="noreferrer"&gt;Climate Entities&lt;/a&gt;, which have the sort of properties you might expect - &lt;code&gt;target_temperature&lt;/code&gt;, &lt;code&gt;current_humidity&lt;/code&gt;, &lt;code&gt;current_temperature&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;To use Home Assistant&amp;rsquo;s terms, in my setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The platform is the underfloor heating system&lt;/li&gt;
&lt;li&gt;The platform consists of some devices, in this case the physical thermostats in each room of my house&lt;/li&gt;
&lt;li&gt;The devices each represent one or more climate entities (humidity, temperature, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="file-structure" class="relative group"&gt;File Structure &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#file-structure" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The &lt;a href="https://github.com/home-assistant/core/tree/dev/homeassistant/components/touchline_sl" target="_blank" rel="noreferrer"&gt;basic file structure&lt;/a&gt; can be laid down with some &lt;a href="https://developers.home-assistant.io/docs/creating_component_index" target="_blank" rel="noreferrer"&gt;scaffold tooling&lt;/a&gt;, but even in its finished state, my integration doesn&amp;rsquo;t have many files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── __init__.py &lt;span class="c1"&gt;# the component file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── climate.py &lt;span class="c1"&gt;# ties info from the api into home assistant terms&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── config_flow.py &lt;span class="c1"&gt;# defines the fields/flow for integration config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── const.py &lt;span class="c1"&gt;# constants used across the integration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── coordinator.py &lt;span class="c1"&gt;# data update coordinator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── manifest.json &lt;span class="c1"&gt;# defines project dependencies and metadata&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── strings.json &lt;span class="c1"&gt;# defines strings displayed in various ui elements&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── translations &lt;span class="c1"&gt;# a directory containing one file per language&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── en.json &lt;span class="c1"&gt;# english translation of strings.json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And to give an idea of the scale of the project in its completed form:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;----------------------------------------------------------------------
Language files blank comment code
----------------------------------------------------------------------
Python 5 71 27 215
JSON 3 0 0 82
----------------------------------------------------------------------
SUM: 8 71 27 297
----------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="manifestjson" class="relative group"&gt;&lt;code&gt;manifest.json&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#manifestjson" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Starting with the most simple first! The &lt;code&gt;manifest.json&lt;/code&gt; describes the integration: what its name is, where its documentation is found, who owns the code and the libraries it depends on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;domain&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;touchline_sl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Roth Touchline SL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;codeowners&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;@jnsgruk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;config_flow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;documentation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://www.home-assistant.io/integrations/touchline_sl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;integration_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hub&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;iot_class&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cloud_polling&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;requirements&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pytouchlinesl==0.1.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Here I selected &lt;code&gt;cloud_polling&lt;/code&gt; as the &lt;code&gt;iot_class&lt;/code&gt;, because my integration reaches out periodically to the API, polling for new information. You&amp;rsquo;ll note also that the integration requires my &lt;a href="https://pypi.org/project/pytouchlinesl/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pytouchlinesl&lt;/code&gt;&lt;/a&gt; library in order to run.&lt;/p&gt;
&lt;h3 id="__init__py" class="relative group"&gt;&lt;code&gt;__init__.py&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#__init__py" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Typically for integrations, this file defines how to setup the integration, and how to unload it. You can see the full source of my implementation &lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I define an &lt;code&gt;async_setup_entry()&lt;/code&gt; method which takes care of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py#L28-L30" target="_blank" rel="noreferrer"&gt;Establishing a coordinator per TouchlineSL module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Performing the &lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py#L32-L37" target="_blank" rel="noreferrer"&gt;initial hydration of data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py#L42-L51" target="_blank" rel="noreferrer"&gt;Registering each TouchlineSL module as a device&lt;/a&gt; in Home Assistant&amp;rsquo;s &lt;a href="https://developers.home-assistant.io/docs/device_registry_index?_highlight=device" target="_blank" rel="noreferrer"&gt;Device Registry&lt;/a&gt;, which is where Home Assistant &amp;ldquo;keeps track of devices&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a shortened, annotated version of the method below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_setup_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HomeAssistant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TouchlineSLConfigEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Set up Roth Touchline SL from a config entry.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TouchlineSL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;coordinators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TouchlineSLModuleCoordinator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TouchlineSLModuleCoordinator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Create a new Device for each coorodinator to represent each module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coordinators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;device_registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_get_or_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config_entry_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;identifiers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runtime_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coordinators&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config_entries&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_forward_entry_setups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Before returning &lt;code&gt;async_setup_entry()&lt;/code&gt; &lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py#L54" target="_blank" rel="noreferrer"&gt;invokes&lt;/a&gt; &lt;code&gt;async_forward_entry_setups()&lt;/code&gt;. This ensures that &lt;code&gt;async_setup_entry()&lt;/code&gt; in &lt;code&gt;climate.py&lt;/code&gt; is called to ensure each of the climate entities is registered.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;PLATFORMS&lt;/code&gt; variable is a list of platform types that the integration supports, &lt;a href="https://github.com/home-assistant/core/blob/82e9792b4d44c653cfc38c495e8e6907d08878cd/homeassistant/components/touchline_sl/__init__.py#L17" target="_blank" rel="noreferrer"&gt;in this case&lt;/a&gt; a single-item list containing just &lt;code&gt;Platform.CLIMATE&lt;/code&gt;, which is how &lt;code&gt;async_forward_entry_setups&lt;/code&gt; knows to invoke the &lt;code&gt;async_setup_entry()&lt;/code&gt; method in &lt;code&gt;climate.py&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;homeassistant.const&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Platform&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIMATE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;To initialise the integration, the user must authenticate with the Roth API so that module details are fetched before constructing the coordinator. Thus, before this code is executed, the user must go through the config flow&amp;hellip;&lt;/p&gt;
&lt;h3 id="config_flowpy" class="relative group"&gt;&lt;code&gt;config_flow.py&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#config_flowpy" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Despite the final implementation being quite simple, this is probably one of the areas I found most challenging to get right. There are some &lt;a href="https://developers.home-assistant.io/docs/config_entries_config_flow_handler/" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt; but they only scratch the surface, and implementations in other integrations seem to vary quite dramatically (mostly depending on when they were written).&lt;/p&gt;
&lt;p&gt;I went through a few iterations of this config flow, mostly because I had originally implemented the ability to select a specific module from the user&amp;rsquo;s account. The review process guided me toward simply logging into the account, then enrolling each of the modules associated with the account - since users can always disable entities they don&amp;rsquo;t wish to manage in Home Assistant.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;config_flow.py&lt;/code&gt; defines which input fields need to be presented to the user, and then passes on the relevant information needed to set up the integration. In my implementation, the code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompts the user for their username and password&lt;/li&gt;
&lt;li&gt;Authenticates with the service and fetches the user&amp;rsquo;s unique ID&lt;/li&gt;
&lt;li&gt;Registers that unique ID, aborting if the specified account has already been used&lt;/li&gt;
&lt;li&gt;Creates a config entry in Home Assistant that stores the user&amp;rsquo;s credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The config flow also has some basic error handling that can distinguish the difference between poor credentials, networking issues, etc. The full implementation can be seen &lt;a href="https://github.com/home-assistant/core/blob/4964470e9c2c168f5004188bf77417764fc4977c/homeassistant/components/touchline_sl/config_flow.py" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;, but the important parts are highlighted in the following snippet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TouchlineSLConfigFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConfigFlow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DOMAIN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_step_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ConfigFlowResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TouchlineSL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONF_USERNAME&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONF_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Use the credentials to fetch unique user id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RothAPIError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Set unique ID, abort setup if already used&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;unique_account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_set_unique_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique_account_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_abort_if_unique_id_configured&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Create a config entry containing the user&amp;#39;s credentials&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_create_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONF_USERNAME&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_show_form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;step_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STEP_USER_DATA_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;What caught me out was how the fields are given titles/descriptions. These attributes are all configured in the &lt;a href="https://github.com/home-assistant/core/blob/4964470e9c2c168f5004188bf77417764fc4977c/homeassistant/components/touchline_sl/strings.json" target="_blank" rel="noreferrer"&gt;&lt;code&gt;strings.json&lt;/code&gt;&lt;/a&gt;, where the &lt;code&gt;config&lt;/code&gt; map contains keys for each of the config &amp;ldquo;steps&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The code above defines a step named &lt;code&gt;user&lt;/code&gt;, since the method name is &lt;code&gt;async_step_user&lt;/code&gt;. The step&amp;rsquo;s name, description and input fields are defined in the &lt;a href="https://github.com/home-assistant/core/blob/4964470e9c2c168f5004188bf77417764fc4977c/homeassistant/components/touchline_sl/strings.json" target="_blank" rel="noreferrer"&gt;&lt;code&gt;strings.json&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;flow_title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Touchline SL Setup Flow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cannot_connect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;[%key:common::config_flow::error::cannot_connect%]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Login to Touchline SL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Your credentials for the Roth Touchline SL mobile app/web service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;username&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;[%key:common::config_flow::data::username%]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;[%key:common::config_flow::data::password%]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;abort&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;already_configured&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;[%key:common::config_flow::abort::already_configured_device%]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The values displayed to the user are pulled from the translation files at runtime depending on their language configuration. In my integration, the corresponding &lt;code&gt;translations/en.json&lt;/code&gt; contains the following fields that map to those defined above:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;flow_title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Touchline SL Setup Flow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cannot_connect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Failed to connect&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;step&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;username&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Username&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Your credentials for the Roth Touchline SL mobile app/web service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Login to Touchline SL&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;abort&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;already_configured&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Device is already configured&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In hindsight, this is covered in the &lt;a href="https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#defining-your-config-flow" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt;, but it definitely didn&amp;rsquo;t click with me when I was going through it, so it seemed worth calling out! The net result of this setup is a configuration dialog that looks like so:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_4f2d422ffb337a21.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_f998b279c6c1af1c.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_bb79608ae17a2b0e.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_8007fd9399bd035.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant config flow for the touchline_sl integration"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_3a60cc2a169aec83.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_3fc1213c7ca284a5.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_3a60cc2a169aec83.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04_hu_7bbc9038da878edc.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/04.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="coordinatorpy" class="relative group"&gt;&lt;code&gt;coordinator.py&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#coordinatorpy" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;This was an addition I made during the review process (more on that later), and appears to be the preferred way to implement the &lt;a href="https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities" target="_blank" rel="noreferrer"&gt;fetching of data&lt;/a&gt; from upstream APIs. By implementing a &lt;code&gt;DataUpdateCoordinator&lt;/code&gt;, Home Assistant can ensure that a single coordinated poll happens across all entities managed by an integration. If an integration manages many entities for which it needs to fetch/update details, the coordinator helps ensure that the API is called only as often as is needed.&lt;/p&gt;
&lt;p&gt;The coordinator class is very simple: it defines a single method &lt;code&gt;_async_update_data&lt;/code&gt; which returns the data for the device its coordinating. As the developer, you can specify the type of the data returned by the coordinator. I chose to represent this as a Python &lt;code&gt;dataclass&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TouchlineSLModuleData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Zone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GlobalScheduleModel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The coordinator is initialised with some basic information such as a name and an update interval:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TouchlineSLModuleCoordinator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DataUpdateCoordinator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TouchlineSLModuleData&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HomeAssistant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_LOGGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Touchline SL (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;update_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The &lt;code&gt;_async_update_data&lt;/code&gt; method then queries the API, and returns data in the newly defined format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_async_update_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TouchlineSLModuleData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RothAPIError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Return the data using our dataclass from above&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TouchlineSLModuleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can see the full implementation &lt;a href="https://github.com/home-assistant/core/blob/4964470e9c2c168f5004188bf77417764fc4977c/homeassistant/components/touchline_sl/coordinator.py" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="climatepy" class="relative group"&gt;&lt;code&gt;climate.py&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#climatepy" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;And finally, on to the business logic of tying fields from the upstream API into the relevant attributes in Home Assistant!&lt;/p&gt;
&lt;p&gt;The first task handled by this file is registering each of the climate entities by iterating over each zone, in each coordinator&amp;rsquo;s module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_setup_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HomeAssistant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TouchlineSLConfigEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;async_add_entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AddEntitiesCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Set up the Touchline devices.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;coordinators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runtime_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;async_add_entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;TouchlineSLZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coordinators&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;zone_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zones&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Home Assistant entities have well-defined APIs - the &lt;a href="https://developers.home-assistant.io/docs/core/entity/climate/" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt; for climate entities show the supported attributes and their data types. I used a combination of the docs, and the &lt;a href="https://github.com/home-assistant/core/blob/3cbadb1bd23fa1174055aad75fe4d469b0a743bb/homeassistant/components/climate/__init__.py" target="_blank" rel="noreferrer"&gt;source code&lt;/a&gt; to establish how to implement &lt;a href="https://github.com/home-assistant/core/blob/3cbadb1bd23fa1174055aad75fe4d469b0a743bb/homeassistant/components/touchline_sl/climate.py" target="_blank" rel="noreferrer"&gt;my &lt;code&gt;ClimateEntity&lt;/code&gt;&lt;/a&gt;, which boiled down to the following interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TouchlineSLZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CoordinatorEntity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TouchlineSLModuleCoordinator&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ClimateEntity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Construct a Touchline SL climate zone.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TouchlineSLModuleCoordinator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Handle updated data from the coordinator.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@callback&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_handle_coordinator_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Return the device object from the coordinator data.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Zone&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Report if the device is available.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;#Set new target temperature.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_set_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Assign the zone to a particular global schedule.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_set_preset_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preset_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Populate attributes with data from the coordinator.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Arguably the most important part here is &lt;code&gt;set_attr()&lt;/code&gt;. which takes care of mapping fields from the objects provided by my &lt;code&gt;pytouchlinesl&lt;/code&gt; library to attributes on the climate entity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Populate attributes with data from the coordinator.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedule_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_current_temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_target_temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_temperature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_current_humidity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;humidity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_preset_modes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;schedule_names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CONSTANT_TEMPERATURE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;constantTemp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_preset_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONSTANT_TEMPERATURE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;globalSchedule&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_attr_preset_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The full implementation is &lt;a href="https://github.com/home-assistant/core/blob/3cbadb1bd23fa1174055aad75fe4d469b0a743bb/homeassistant/components/touchline_sl/climate.py" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="testing" class="relative group"&gt;Testing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Testing is (understandably) a little complicated. The &lt;a href="https://developers.home-assistant.io/docs/development_testing#writing-tests-for-integrations" target="_blank" rel="noreferrer"&gt;requirements&lt;/a&gt; for landing code for an integration stipulate that you must include unit tests for any config/options flows. In my case, this meant the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implementing a mock TouchlineSL &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/tests/components/touchline_sl/conftest.py#L32" target="_blank" rel="noreferrer"&gt;client fixture&lt;/a&gt; to ensure that the unit tests don&amp;rsquo;t try to reach out to the real Roth API&lt;/li&gt;
&lt;li&gt;Implementing a mock &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/tests/components/touchline_sl/conftest.py#L51" target="_blank" rel="noreferrer"&gt;config entry fixture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A unit test for a &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/tests/components/touchline_sl/test_config_flow.py#L24" target="_blank" rel="noreferrer"&gt;successful config flow execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A parametrised unit test for &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/tests/components/touchline_sl/test_config_flow.py#L54" target="_blank" rel="noreferrer"&gt;unsuccessful config flow&lt;/a&gt; due to possible exceptions when hitting the API&lt;/li&gt;
&lt;li&gt;A unit test to ensure that multiple config flows &lt;a href="https://github.com/home-assistant/core/blob/72f1c358d97dd387e8d7d8e537cfb0554b274124/tests/components/touchline_sl/test_config_flow.py#L92" target="_blank" rel="noreferrer"&gt;resulting in the same user ID fail&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To me this feels like the bare minimum, and unfortunately doesn&amp;rsquo;t really provide any confidence that the integration actually functions correctly. I&amp;rsquo;m hoping to improve this in the future, but for now further testing has been manual.&lt;/p&gt;
&lt;h2 id="docs--brand" class="relative group"&gt;Docs &amp;amp; Brand &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#docs--brand" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As I was creating the Pull Request to contribute my integration, I was prompted by the template to link to further PRs that added the documentation and brand assets for my integration. I used the existing &lt;code&gt;touchline&lt;/code&gt; docs as a template and modified them for my integration. The docs &lt;a href="https://github.com/home-assistant/home-assistant.io/pull/34441" target="_blank" rel="noreferrer"&gt;pull request&lt;/a&gt; added a single file &lt;code&gt;touchline_sl.markdown&lt;/code&gt; containing 29 lines, which results in some nicely &lt;a href="https://www.home-assistant.io/integrations/touchline_sl/" target="_blank" rel="noreferrer"&gt;rendered docs&lt;/a&gt; on the Home Assistant website:&lt;/p&gt;
&lt;p&gt;&lt;a href="08.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_ce9fdf84fc77fd48.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_3c2fe9033e79aa2a.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_f25fb2483f7e8700.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_5a045c267bf2ed99.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1386"
height="1261"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant touchline_sl docs"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_23986d6fbe90ca5e.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_4df001117675cfa1.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_23986d6fbe90ca5e.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_ae4eb57a770d62ba.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/08_hu_b70628168cb45e33.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There were already branding assets for Roth as part of the &lt;code&gt;touchlinesl&lt;/code&gt; integration that was previously merged. Based on some review feedback, I &lt;a href="https://github.com/home-assistant/brands/pull/5797" target="_blank" rel="noreferrer"&gt;created&lt;/a&gt; a new &lt;a href="https://developers.home-assistant.io/docs/creating_integration_brand" target="_blank" rel="noreferrer"&gt;Integration Brand&lt;/a&gt; named &lt;code&gt;roth&lt;/code&gt;, with which both the &lt;code&gt;touchline&lt;/code&gt; and &lt;code&gt;touchline_sl&lt;/code&gt; integrations are associated. This has the nice effect of grouping them when setting up a new integration:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_bef6947b69f45695.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_223ad87034ccda07.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_a365c4c9d039aee6.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_ce97175f2f0ac901.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant adding a new integration showing the roth brand"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_b2bc3d2b17261d10.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_d87bae56bb52a1f7.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_b2bc3d2b17261d10.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02_hu_261d6b711f2f21b7.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/02.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;
&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_bfa08fb04d519199.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_379cc19ef67cad3.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_4a424b8a9e05ba9d.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_7e992f6d742b0207.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant adding a new integration showing the roth integrations"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_9ba384f08ca00f00.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_cd8bcd4c6734df91.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_9ba384f08ca00f00.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03_hu_c1e291bcba50ae33.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/03.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="contribution-process" class="relative group"&gt;Contribution Process &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#contribution-process" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I submitted my first efforts for review in &lt;a href="https://github.com/home-assistant/core/pull/124557" target="_blank" rel="noreferrer"&gt;home-assistant/core#124557&lt;/a&gt;. The checklist guided me nicely through what needed to be done, and overall the process went pretty smoothly, and pretty quickly (despite the 117 comments!). The process took a little under two days in total. I was also fortunate with my timing, since my code landed the day before the next beta release was cut, so it shipped relatively quickly.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to offer my sincere thanks to &lt;a href="https://github.com/joostlek" target="_blank" rel="noreferrer"&gt;@joostlek&lt;/a&gt; who not only reviewed my code extremely quickly, but took the time to explain things to me both in the Github PR, but also by proactively reaching out to me on the Home Assistant Discord, which I really appreciated.&lt;/p&gt;
&lt;p&gt;I got &lt;em&gt;a lot&lt;/em&gt; of feedback, which I expected. This was my first attempt at writing code for Home Assistant, and it&amp;rsquo;s a pretty large and well established project. I do think the project could do with some better developer documentation, which would dramatically reduce the burden of effort on reviewers and give contributors a better chance of &amp;ldquo;getting it right&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;One might argue I could have done &lt;em&gt;more&lt;/em&gt; reading and &lt;em&gt;more&lt;/em&gt; research - though I spent a good amount of time reading both the documentation and the source code of other integrations. I&amp;rsquo;m not sure I&amp;rsquo;d ever have reached the conclusions that &lt;a href="https://github.com/joostlek" target="_blank" rel="noreferrer"&gt;@joostlek&lt;/a&gt; kindly nudged me toward.&lt;/p&gt;
&lt;p&gt;Overall, my implementation was more brief, more simple and more efficient as a result of the review process, and based on my experience I&amp;rsquo;d advocate for having a go if you&amp;rsquo;ve been on the fence! Often submitting code to such a project can be daunting, but as with my experience when contributing to &lt;code&gt;nixpkgs&lt;/code&gt;, if you go in with an open mind you&amp;rsquo;re sure to learn something from the process.&lt;/p&gt;
&lt;h2 id="results" class="relative group"&gt;Results &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#results" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m really pleased with the results. However obtuse the developer experience felt at times, there is no denying that what I was able to get from my 297 lines of implementation code is quite staggering. I was super impressed that just by following the conventions I was able to get such intuitive controls, a full graphable history of temperatures and such a wide variety of ways to display the information in my various dashboards.&lt;/p&gt;
&lt;p&gt;Once set up, you&amp;rsquo;re able to get a view of all the different zones imported by the module (the names of each zone are pulled from the upstream API if the zones are named, and each can be associated with a given area in Home Assistant):&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_861a6c6dd1a00d10.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_fc450367fc3c76d6.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_888ad44219ff5b2b.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_ff1a2df0fd420005.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant showing touchline_sl climate entities"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_7735981d2e08a9db.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_c4d867158df2bb03.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_7735981d2e08a9db.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05_hu_a4c4b3e407756f51.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/05.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The default dashboard displays thermostat controls for each zone. These allow you to see the current and target temprature, as well as adjust the target temperature if you need to:&lt;/p&gt;
&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_3ffdb6fdb94daf2d.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_da36117191be85b8.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_f26fc2be6a0e867c.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_579fe18ff3263f60.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant showing touchline_sl climate dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_31e3b4342ce7e24.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_be9bda065305d294.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_31e3b4342ce7e24.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06_hu_b115b2fdbc50db0e.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/06.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Expanding the thermostat cards gives you a more detailed view, showing the mode and the &amp;ldquo;Preset&amp;rdquo;. My implementation maps &amp;ldquo;Presets&amp;rdquo; to &amp;ldquo;Global Schedules&amp;rdquo; configured in the Roth module:&lt;/p&gt;
&lt;p&gt;&lt;a href="07.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_359de8d442cc3850.webp 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_50aca5be90c3eec6.webp 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_c94a20fe1d242fc3.webp 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_3fbade3901d3e122.webp 1120w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1120"
height="987"
class="mx-auto my-0 rounded-md"
alt="screenshot of home assistant showing touchline_sl thermostat controls"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_1601445451adc6e8.png" srcset="https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_b2b6930c24eb65c.png 330w,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_1601445451adc6e8.png 660w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07_hu_4f949bdf74411a5a.png 1024w
,https://jnsgr.uk/2024/10/writing-a-home-assistant-integration/07.png 1120w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;And that&amp;rsquo;s a wrap! I learned a bunch writing this integration, and the resulting user experience is quite a lot better than I get with the default Roth application.&lt;/p&gt;
&lt;p&gt;My next mission is to use the information from these climate entities to automate opening the Velux windows in the roof when things get warm in the summer, but I&amp;rsquo;ve got a lot to learn about Home Assistant in the mean time!&lt;/p&gt;</description></item><item><title>Writing a Home Assistant Core Integration: Part 1</title><link>https://jnsgr.uk/2024/09/pytouchlinesl/</link><pubDate>Wed, 11 Sep 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/09/pytouchlinesl/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Back in March, my family and I moved into a new home. It&amp;rsquo;s a modern construction which came with solar panels (and associated inverter/battery storage), and uses an &lt;a href="https://en.wikipedia.org/wiki/Air_source_heat_pump" target="_blank" rel="noreferrer"&gt;air source heat pump&lt;/a&gt; to heat the house with underfloor heating. Being a new renovation, nearly all of the appliances and components in the house have a form of internet connectivity (some more useful than others!).&lt;/p&gt;
&lt;p&gt;Since day 1, I&amp;rsquo;ve been hoping to consolidate all of the various applications, data feeds and functions into one single place. I&amp;rsquo;ve been a long-time listener to the &lt;a href="https://selfhosted.show/" target="_blank" rel="noreferrer"&gt;Self Hosted&lt;/a&gt; podcast, which often extols the virtues of &lt;a href="https://www.home-assistant.io/" target="_blank" rel="noreferrer"&gt;Home Assistant&lt;/a&gt;. I&amp;rsquo;ve got no prior experience with Home Assistant, but for the last three months I&amp;rsquo;ve been running it on my home server, with a collection of custom integrations and hacks that enable me to control the underfloor heating and solar inverter.&lt;/p&gt;
&lt;p&gt;The underfloor heating controller is a &lt;a href="https://www.roth-uk.com/products/control-systems/roth-touchliner-sl-wireless-system" target="_blank" rel="noreferrer"&gt;Roth Touchline SL&lt;/a&gt; system. In my set up, there is a single &amp;ldquo;module&amp;rdquo; which represents my house, and a number of &amp;ldquo;zones&amp;rdquo; which represent different rooms.&lt;/p&gt;
&lt;p&gt;There was unfortunately no integration for this system in Home Assistant - &lt;a href="https://www.home-assistant.io/integrations/touchline/" target="_blank" rel="noreferrer"&gt;there is one&lt;/a&gt; for the previous generation &amp;ldquo;Roth Touchline&amp;rdquo;, but this appears to function over the LAN, whereas the Touchline SL system is controlled over the internet using their API.&lt;/p&gt;
&lt;p&gt;After reading the source code of a few other climate integrations in Home Assistant, it became clear to me that the first step was to create a Python client for the API which could be used in the integration.&lt;/p&gt;
&lt;p&gt;This post will cover the design, implementation and limitations of the library I wrote: &lt;a href="https://pypi.org/project/pytouchlinesl/" target="_blank" rel="noreferrer"&gt;pytouchlinesl&lt;/a&gt;. If you came here to read about writing code &lt;em&gt;for Home Assistant&lt;/em&gt;, you&amp;rsquo;ll have to wait for the next post! 😉&lt;/p&gt;
&lt;h2 id="designing-the-library" class="relative group"&gt;Designing the library &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#designing-the-library" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="upstream-api" class="relative group"&gt;Upstream API &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#upstream-api" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Usually, one would interact with a Roth Touchline SL system through their mobile apps, or through their &lt;a href="https://roth-touchlinesl.com/" target="_blank" rel="noreferrer"&gt;online portal&lt;/a&gt;. The mobile app seems to be quite a lightweight wrapper around the web application, and I&amp;rsquo;ve not been able to detect any difference in functionality.&lt;/p&gt;
&lt;p&gt;A bit of searching uncovered that Roth also provide an API for the Touchline SL system, and an &lt;a href="https://api-documentation.roth-touchlinesl.com/" target="_blank" rel="noreferrer"&gt;OpenAPI spec&lt;/a&gt;. This made the process significantly easier, though there are some discrepancies in what is documented compared with how the API &lt;em&gt;actually&lt;/em&gt; behaves. It feels to me like the API may have evolved, and the documentation has remained static - or perhaps it was always inaccurate? Either way, I spent quite a bit of time manually &lt;a href="https://en.wikipedia.org/wiki/Fuzzing" target="_blank" rel="noreferrer"&gt;fuzzing&lt;/a&gt; the API to work out the correct set of parameters for some endpoints.&lt;/p&gt;
&lt;p&gt;I also studied the web application using the Chrome Dev Tools. Of all the endpoints documented, it seemed like I&amp;rsquo;d only need the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /authentication&lt;/code&gt;: authenticates with the API, taking a username and password, and returning a token&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/{user_id}/modules&lt;/code&gt;: returns a list of modules associated with the user&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/{user_id}/modules/{module_udid}&lt;/code&gt;: returns details of a specific module (zones, schedules, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a slight awkwardness here, in that the last endpoint returns &lt;em&gt;all&lt;/em&gt; of the data - for all zones, all schedules, etc. This feels inefficient, but I couldn&amp;rsquo;t find a way of getting information about a specific zone, or a specific schedule. The web application seems to rely upon polling the (undocumented) endpoint &lt;code&gt;GET /users/{user_id}/modules/{module_id}/update/data/parents/[]/alarm_ids/[]/last_update/{timestamp}&lt;/code&gt;, which delivers deltas in the data since a given timestamp. This is useful in the context of the app because it can request the full dataset once, then request only changes from that point onwards, keeping the app state up to date without requesting the whole dataset.&lt;/p&gt;
&lt;p&gt;Making changes to the configuration of zones and their temperatures is also fragmented. In essence, one can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set a zone to a constant temperature: &lt;code&gt;POST /users/{user_id}/modules/{module_udid}/zones&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Place a zone on a global schedule: &lt;code&gt;POST /users/{user_id}/modules/{module_udid}/zones/{zone_id}/global_schedule&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Place a zone on a local schedule: &lt;code&gt;POST /users/{user_id}/modules/{module_udid}/zones/{zone_id}/local_schedule&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is self-explanatory, enabling a zone to be set to a constant temperature (&lt;code&gt;19.0C&lt;/code&gt;, for example). Touchline SL modules also support &amp;ldquo;schedules&amp;rdquo; which contain time periods for the specified zones to reach certain temperatures. In the case of a &amp;ldquo;Global Schedule&amp;rdquo;, multiple zones can be assigned, while a &amp;ldquo;Local Schedule&amp;rdquo; is specific to a single zone. The awkwardness in the API here is that to &amp;ldquo;add&amp;rdquo; a zone to a global schedule, you must re-specify the entire schedule, and specify all of the zones that should be on the schedule&amp;hellip;&lt;/p&gt;
&lt;h3 id="basic-requirements" class="relative group"&gt;Basic Requirements &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#basic-requirements" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;In order to fulfil the basic functionality of my (future) Home Assistant integration, I limited the requirements of the first version to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authenticate with the API using a username and password&lt;/li&gt;
&lt;li&gt;List modules associated with the account&lt;/li&gt;
&lt;li&gt;Get a specific module&lt;/li&gt;
&lt;li&gt;Get a specific zone&lt;/li&gt;
&lt;li&gt;Get a list of global schedules&lt;/li&gt;
&lt;li&gt;Get a specific global schedule&lt;/li&gt;
&lt;li&gt;Assign a constant temperature to a zone&lt;/li&gt;
&lt;li&gt;Assign a zone to a specific global schedule&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don&amp;rsquo;t use local schedules in my system, so I&amp;rsquo;ve omitted them for now, though updating the library to support them would be trivial.&lt;/p&gt;
&lt;h3 id="outline-designexperience" class="relative group"&gt;Outline design/experience &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#outline-designexperience" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;With those requirements in mind, I came up with a rough sketch of how I&amp;rsquo;d like the library to behave:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tsl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TouchlineSL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;foo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bar&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deadbeef&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;lounge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Lounge&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;kitchen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Properties should be available such as:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# lounge.current_temperature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# kitchen.humidity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;kitchen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;living_spaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedule_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Living Spaces&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;lounge&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;living_spaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And from that came a reasonable outline of the API for the library:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TouchlineSL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Construct a class that represents a Touchline SL account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a list of modules associated with the account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a specific module, by ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Module&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a list of zones from the module, optionally including disabled zones&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_off&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Zone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a specific zone, by ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a specific zone, by name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zone_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zone_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Zone&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a list of global schedules&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Schedule&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a specific schedule, by ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Schedule&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get a specific schedule, by name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Schedule&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Get the schedule the zone is assigned to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Schedule&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Set the zone to a constant temperature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Assign the zone to a specific schedule&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schedule_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Note that after reading the code from other &lt;code&gt;climate&lt;/code&gt; integrations in Home Assistant, it became clear to me that they favour the use of &lt;code&gt;async&lt;/code&gt; libraries, and thus my library was designed to use &lt;code&gt;asyncio&lt;/code&gt; from the start.&lt;/p&gt;
&lt;h2 id="python-toolslibraries" class="relative group"&gt;Python tools/libraries &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#python-toolslibraries" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;There are a couple of things I&amp;rsquo;ve found tiresome about Python over the years, but things do seem to be looking up. I&amp;rsquo;ve always found the package management and distribution to be awkward, and I can&amp;rsquo;t be the only one if the number of projects looking to target that problem is anything to go by (e.g. &lt;a href="https://python-poetry.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;poetry&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://rye.astral.sh/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rye&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pdm-project.org/en/latest/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pdm&lt;/code&gt;&lt;/a&gt;, etc.).&lt;/p&gt;
&lt;p&gt;Part of this seems to come from fractures in the community itself - there (still!) appears to be disagreements surrounding PEPs such as &lt;a href="https://peps.python.org/pep-0621/" target="_blank" rel="noreferrer"&gt;PEP-621&lt;/a&gt; which introduced &lt;code&gt;pyproject.toml&lt;/code&gt; as a way of managing project metadata and dependencies, with the maintainers of some high-profile and widely adopted libraries refusing to adopt it.&lt;/p&gt;
&lt;p&gt;That said, there are a couple of things I&amp;rsquo;ve been meaning to try in anger, and this project was a good opportunity to do so:&lt;/p&gt;
&lt;h3 id="uv" class="relative group"&gt;&lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#uv" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Developed by &lt;a href="https://astral.sh" target="_blank" rel="noreferrer"&gt;Astral&lt;/a&gt;, &lt;code&gt;uv&lt;/code&gt; is the &amp;ldquo;new shiny&amp;rdquo; at the time of writing, and I can understand why. Pitched as &amp;ldquo;Cargo, but for Python&amp;rdquo;, it aims to solve a myriad of problems in the Python ecosystem. &lt;code&gt;uv&lt;/code&gt; can handle the download/install of multiple Python versions, the creation of virtual environments, running Python tools in a one-off fashion (like &lt;code&gt;pipx&lt;/code&gt;), locking dependencies deeply in a project (by hash) and still maintains a &lt;code&gt;pip&lt;/code&gt; compatible command-line experience with &lt;code&gt;uv pip&lt;/code&gt;. To add to all of that, it&amp;rsquo;s &lt;em&gt;ridiculously&lt;/em&gt; fast; on a couple of occasions I&amp;rsquo;ve actually found myself wondering if it &lt;em&gt;did anything&lt;/em&gt; when installing dependencies for large projects, because it&amp;rsquo;s so much faster than I&amp;rsquo;m used to.&lt;/p&gt;
&lt;h3 id="pydantic" class="relative group"&gt;&lt;a href="https://docs.pydantic.dev/latest/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pydantic&lt;/code&gt;&lt;/a&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#pydantic" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Pydantic is a data validation library for Python. It&amp;rsquo;s entirely driven by Python&amp;rsquo;s type-hints which means that you get nice integration with language servers. Pydantic allows you define data models in native Python, but emit standard JSON Schema docs for models. It&amp;rsquo;s integrated quite widely across the Python ecosystem, and to me feels like it bridges the gap between what I hoped type annotations would do for Python, and what they actually do in reality!&lt;/p&gt;
&lt;h3 id="ruff" class="relative group"&gt;&lt;a href="https://github.com/astral-sh/ruff" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ruff&lt;/code&gt;&lt;/a&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ruff" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve been using this one for at least the last year for all my Python linting and formatting needs, but I still feel it deserves a call out. There have been a couple of small changes to command line API and such along the way, but overall I&amp;rsquo;ve found it to be a dramatic improvement over my last setup - which comprised of &lt;a href="https://github.com/psf/black" target="_blank" rel="noreferrer"&gt;&lt;code&gt;black&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pycqa.github.io/isort/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;isort&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pypi.org/project/flake8/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;flake8&lt;/code&gt;&lt;/a&gt; and a pile of plugins. I had no particular beef with &lt;code&gt;black&lt;/code&gt;, but I find &lt;code&gt;flake8&lt;/code&gt;&amp;rsquo;s lack of &lt;code&gt;pyproject.toml&lt;/code&gt; support irritating, and grew tired of plugins failing as &lt;code&gt;flake8&lt;/code&gt; released new versions.&lt;/p&gt;
&lt;p&gt;In my experience, &lt;code&gt;ruff&lt;/code&gt; is stupid fast, and because it ships &lt;code&gt;flake8&lt;/code&gt;-compatible rules for all of the plugins I was using as one bundle, they never break. It&amp;rsquo;s also nice to just have &lt;em&gt;one tool&lt;/em&gt; to use everywhere. If you&amp;rsquo;re interested, you can see how I configure &lt;code&gt;ruff&lt;/code&gt; in the &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pyproject.toml#L34" target="_blank" rel="noreferrer"&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="implementation" class="relative group"&gt;Implementation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#implementation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;With a basic design in mind, and tooling ready to go, I set about building the library itself. It was now time to reconcile my intended design with the realities of the provisions made by the upstream API.&lt;/p&gt;
&lt;p&gt;I mentioned previously that the only useful endpoint for getting information about zones/schedules would in fact return &lt;em&gt;all&lt;/em&gt; of the data for a given module. Too many calls to this endpoint would likely result in poor performance, so I wanted to introduce some basic caching along the way.&lt;/p&gt;
&lt;h3 id="client-implementation" class="relative group"&gt;Client implementation &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#client-implementation" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;For the underlying API client implementation, I opted for the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/client/base.py" target="_blank" rel="noreferrer"&gt;&lt;code&gt;BaseClient&lt;/code&gt;&lt;/a&gt;: a class which inherits from Python&amp;rsquo;s &lt;a href="https://docs.python.org/3/library/abc.html#abc.ABC" target="_blank" rel="noreferrer"&gt;&lt;code&gt;abc.ABC&lt;/code&gt;&lt;/a&gt;. This enables the creation of multiple client implementations by defining of the set of methods/properties that any client interacting with the Roth API should define. This decision is primarily to support testing through dependency injection, rather than mocking with patches (more details on that later).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/client/client.py" target="_blank" rel="noreferrer"&gt;&lt;code&gt;RothAPI&lt;/code&gt;&lt;/a&gt;: a concrete implementation of the &lt;code&gt;BaseClient&lt;/code&gt; abstract class. It is here that I built the actual implementation of the client which handles authentication, &lt;code&gt;GET&lt;/code&gt;ing and &lt;code&gt;POST&lt;/code&gt;ing data, caching, and marshalling API responses into the correct types (defined with Pydantic).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also included in the &lt;a href="https://github.com/jnsgruk/pytouchlinesl/tree/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/client" target="_blank" rel="noreferrer"&gt;&lt;code&gt;client&lt;/code&gt;&lt;/a&gt; package is the &lt;a href="https://github.com/jnsgruk/pytouchlinesl/tree/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/client/models" target="_blank" rel="noreferrer"&gt;&lt;code&gt;models&lt;/code&gt;&lt;/a&gt; package. The &lt;code&gt;models&lt;/code&gt; package contains (mostly) auto-generated Pydantic models based on real-life responses I got from the API. This is really a function of laziness, but was a convenient way to get type annotated models for the responses I was receiving from the API. Each time I hit a new endpoint, I took the JSON result and did a quick conversion with &lt;a href="https://jsontopydantic.com/" target="_blank" rel="noreferrer"&gt;https://jsontopydantic.com/&lt;/a&gt;, before manually adjusting names and updating some fields with &lt;code&gt;Literals&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="caching" class="relative group"&gt;Caching &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#caching" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I mentioned earlier that I wanted to implement some basic caching. While I am aware of various plugins for &lt;code&gt;aiohttp&lt;/code&gt; (and other request libraries) that could handle this for me, my requirements were quite simple, so I chose to just build it into the library. In this case, caching is implemented on the &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py" target="_blank" rel="noreferrer"&gt;&lt;code&gt;Module&lt;/code&gt;&lt;/a&gt; class. This is because the large blob of data that is requested to populate details about a module and its zones/schedules is requested &lt;em&gt;per module&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The caching works like this:&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py" target="_blank" rel="noreferrer"&gt;&lt;code&gt;Module&lt;/code&gt;&lt;/a&gt; class has &amp;ldquo;private&amp;rdquo; attributes named &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L62" target="_blank" rel="noreferrer"&gt;&lt;code&gt;_raw_data&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L64" target="_blank" rel="noreferrer"&gt;&lt;code&gt;_last_fetched&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;module_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AccountModuleModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cache_validity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Raw data about the zones, schedules, tiles in the module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModuleModel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Unix timestamp representing the last time the _raw_data was fetched&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_fetched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache_validity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_validity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;There is only one method on this class that calls the underlying client, and that&amp;rsquo;s another &amp;ldquo;private&amp;rdquo; method named &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L70" target="_blank" rel="noreferrer"&gt;&lt;code&gt;_data&lt;/code&gt;&lt;/a&gt;. This method takes an optional &lt;code&gt;refresh&lt;/code&gt; keyword argument, which forces the &lt;code&gt;_raw_data&lt;/code&gt; attribute to be updated, but by default will only fetch data if the cached data has expired (after the number of seconds specified in &lt;code&gt;self._cache_validity&lt;/code&gt;). If &lt;code&gt;refresh&lt;/code&gt; is false, and the cache hasn&amp;rsquo;t expired, it simply returns the stored raw data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ModuleModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Get the raw representation of the module from the upstream API.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; If the data has never been fetched from upstream, or the data is older
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; than the cache validity period, then the data is refreshed using the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; upstream API.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; refresh: (Optional): Force the data to be refreshed using the API.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_fetched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache_validity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_fetched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Each of the public methods (&lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L92" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zones()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L112" target="_blank" rel="noreferrer"&gt;&lt;code&gt;zone()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/module.py#L145" target="_blank" rel="noreferrer"&gt;&lt;code&gt;schedule()&lt;/code&gt;&lt;/a&gt;, etc.) access the raw data through the &lt;code&gt;_data()&lt;/code&gt; method, and pass through the refresh flag which is exposed. This means that any developer consuming this library can chose to live with the caching, or override it and force a refresh like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tsl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TouchlineSL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;foo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bar&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;deadbeef&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Request a zone, accepting cached data (default)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kitchen&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or force the data to be refreshed using the upstream API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zone_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kitchen&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="testing-ci--publishing" class="relative group"&gt;Testing, CI &amp;amp; Publishing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing-ci--publishing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="testing" class="relative group"&gt;Testing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#testing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;In the previous section, I described how I&amp;rsquo;d set up an abstract base class for the API client, then created an implementation of that for the actual Roth API. One of the main reasons I like this pattern for API clients is that it simplifies testing and reduces the need for monkey patching or traditional mocking (yes, I know the fake API client is sort of a mock&amp;hellip;).&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;TouchlineSL&lt;/code&gt; can &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/pytouchlinesl/touchlinesl.py#L33" target="_blank" rel="noreferrer"&gt;optionally be constructed&lt;/a&gt; with any client that implements &lt;code&gt;BaseClient&lt;/code&gt;&amp;rsquo;s abstract base class, it&amp;rsquo;s trivial to implement a fake API backend that returns fixture data to be used in testing. In this case, the fixtures are &lt;a href="https://github.com/jnsgruk/pytouchlinesl/tree/a0e02f19f95edc01093f45e85705dbff44da949a/tests/sample-data" target="_blank" rel="noreferrer"&gt;stored as JSON files&lt;/a&gt; in the repository, and contain real life responses received from the API.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/tests/fake_client.py" target="_blank" rel="noreferrer"&gt;&lt;code&gt;FakeRothAPI&lt;/code&gt;&lt;/a&gt; returns the sample data for each of the methods defined in the abstract class. The following is a partial extract from the fake client code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pytouchlinesl.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseClient&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pytouchlinesl.client.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AccountModuleModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModuleModel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;data_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sample-data&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeRothAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseClient&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123456789&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deadbeef&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_user_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_token&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AccountModuleModel&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;modules.json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AccountModuleModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;From there, I defined a &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/tests/conftest.py" target="_blank" rel="noreferrer"&gt;number of test fixtures&lt;/a&gt; using &lt;code&gt;pytest&lt;/code&gt;&amp;rsquo;s &lt;code&gt;@pytest.fixture&lt;/code&gt; decorator, which provide an initialised &lt;code&gt;TouchlineSL&lt;/code&gt; instance, backed by the fake client, a &lt;code&gt;Module&lt;/code&gt; instance, and a &lt;code&gt;Zone&lt;/code&gt; instance.&lt;/p&gt;
&lt;p&gt;From there, the &lt;a href="https://github.com/jnsgruk/pytouchlinesl/tree/a0e02f19f95edc01093f45e85705dbff44da949a/tests" target="_blank" rel="noreferrer"&gt;tests&lt;/a&gt; are fairly simple. Beyond injecting the fake client, there is no mocking required, which in my opinion keeps the test code much easier to read and understand. It also means I could focus more of my energy on validating the logic I was testing, rather than worrying about how patching might interact with the rest of the code. I&amp;rsquo;ve felt bad about mocking for years, and I think the clearest articulation I&amp;rsquo;ve seen is &lt;a href="https://hynek.me/articles/what-to-mock-in-5-mins/" target="_blank" rel="noreferrer"&gt;&amp;ldquo;Don&amp;rsquo;t Mock What You Don&amp;rsquo;t Own&amp;rdquo; in 5 Minutes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One aspect of the test suite I don&amp;rsquo;t love is the use of &lt;code&gt;time.sleep&lt;/code&gt; in &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/tests/test_module.py#L42" target="_blank" rel="noreferrer"&gt;certain tests&lt;/a&gt;. This is because my caching implementation relies on reading a timestamp to decide on whether it should refresh data. In general I steer away from sleeps in tests, as they&amp;rsquo;re often used to mask an underlying non-determinism, but in this case it felt a reasonable trade-off, given that I&amp;rsquo;m testing a time-based functionality.&lt;/p&gt;
&lt;h3 id="ci" class="relative group"&gt;CI &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ci" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I wanted to ensure that any pull requests were tested, and that they conform to the project&amp;rsquo;s formatting/linting rules. Since the project is hosted on Github, I used Github Actions for this. The pipeline for this project is pretty simple, it lints and formats the code with &lt;code&gt;ruff&lt;/code&gt; failing if any files were changed by the formatter or any of the linting rules were violated.&lt;/p&gt;
&lt;p&gt;Finally, &lt;code&gt;uv&lt;/code&gt; &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/.github/workflows/_test.yaml#L54-L56" target="_blank" rel="noreferrer"&gt;is used&lt;/a&gt; to run &lt;code&gt;pytest&lt;/code&gt; across a &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/.github/workflows/_test.yaml#L30-L35" target="_blank" rel="noreferrer"&gt;matrix of supported Python versions&lt;/a&gt;. I could have used &lt;code&gt;uv&lt;/code&gt; to handle the download and install of different Python versions too, but the &lt;code&gt;setup-python&lt;/code&gt; actions has served me perfectly well in the past.&lt;/p&gt;
&lt;h3 id="publishing" class="relative group"&gt;Publishing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#publishing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Publishing is taken care of in CI. I don&amp;rsquo;t like having to remember the magic incantation for building, authenticating and publishing locally. I want the process to be as consistent and as transparent as possible for the people consuming the project.&lt;/p&gt;
&lt;p&gt;What was new to me this time was publishing to PyPI with a &amp;ldquo;Trusted Publisher&amp;rdquo; setup. To quote their &lt;a href="https://docs.pypi.org/trusted-publishers/" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Trusted publishing&amp;rdquo; is our term for using the OpenID Connect (OIDC) standard to exchange short-lived identity tokens between a trusted third-party service and PyPI. This method can be used in automated environments and eliminates the need to use manually generated API tokens to authenticate with PyPI when publishing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rather than setting up Github Actions to hold an API token in a Secret, there is some automation that links a Github project to a PyPI project. The project doesn&amp;rsquo;t even have to be previously published on PyPI to get started, and &lt;a href="https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/" target="_blank" rel="noreferrer"&gt;new projects can be configured&lt;/a&gt; as &amp;ldquo;Pending&amp;rdquo;, then published to for the first time from your CI system of choice 🚀.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://github.com/jnsgruk/pytouchlinesl/blob/a0e02f19f95edc01093f45e85705dbff44da949a/.github/workflows/publish.yaml" target="_blank" rel="noreferrer"&gt;configured Github Actions&lt;/a&gt; to trigger the release of &lt;code&gt;pytouchlinesl&lt;/code&gt; on new tags being pushed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build and Publish to PyPI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;tests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Trusted publisher setup for PyPI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pypi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://pypi.org/p/pytouchlinesl&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Checkout the code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install `uv`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; curl -LsSf https://astral.sh/uv/install.sh | sh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build the package&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; uvx --from build pyproject-build --installer uv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publish to PyPi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pypa/gh-action-pypi-publish@v1.10.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="contributing-to-nixpkgs" class="relative group"&gt;Contributing to &lt;code&gt;nixpkgs&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#contributing-to-nixpkgs" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Finally, my Home Assistant server runs NixOS, so in order for the integration to be packaged easily in the future, I created a small PR to include &lt;code&gt;pytouchlinesl&lt;/code&gt; in &lt;code&gt;nixpkgs&lt;/code&gt;. The &lt;a href="https://github.com/NixOS/nixpkgs/pull/336794" target="_blank" rel="noreferrer"&gt;original PR&lt;/a&gt; went through pretty quickly - thanks to &lt;a href="https://github.com/drupol" target="_blank" rel="noreferrer"&gt;@drupol&lt;/a&gt; for the fast review!&lt;/p&gt;
&lt;p&gt;Since then I&amp;rsquo;ve made a couple of minor version bumps to the library as I discovered small issues when building the integration, but the &lt;a href="https://github.com/NixOS/nixpkgs/blob/1355a0cbfeac61d785b7183c0caaec1f97361b43/pkgs/development/python-modules/pytouchlinesl/default.nix" target="_blank" rel="noreferrer"&gt;final derivation&lt;/a&gt; is quite compact (see below). The Python build tooling in Nix is quite mature at this point, and I gained a fair bit of experience using it when packaging &lt;code&gt;snapcraft&lt;/code&gt; and &lt;code&gt;charmcraft&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;buildPythonPackage&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytouchlinesl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.1.5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pyproject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pythonOlder&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.10&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetchFromGitHub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jnsgruk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytouchlinesl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;refs/tags/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-kdLMuxA1Ig85mH7s9rlmVjEsItXxRlDA1JTFasnJogg=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;build-system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;setuptools&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nativeCheckInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pytestCheckHook&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pytest-asyncio&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pythonImportsCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytouchlinesl&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;And that concludes the first part of this series! Hopefully you found this useful - I&amp;rsquo;ve learned a lot from people over the years by understanding how they approach problems, so I thought I&amp;rsquo;d post my methodology here in case it helps anyone refine their process.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m certainly no Python expert, so if you&amp;rsquo;ve spotted a mistake or you think I&amp;rsquo;m wrong, get in touch.&lt;/p&gt;
&lt;p&gt;In the next post, I&amp;rsquo;ll cover writing the Home Assistant integration, and contributing it to Home Assistant.&lt;/p&gt;</description></item><item><title>Libations: Tailscale on the Rocks</title><link>https://jnsgr.uk/2024/08/tailscale-on-the-rocks/</link><pubDate>Wed, 21 Aug 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/08/tailscale-on-the-rocks/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m a long-time, self-professed connoisseur of cocktails. I&amp;rsquo;ve always enjoyed making (and drinking!) the classics, but I also like experimenting with new base spirits, techniques, bitters etc.&lt;/p&gt;
&lt;p&gt;Over the years, I&amp;rsquo;ve collected recipes from a variety of sources. Some originated from books (such as &lt;a href="https://www.amazon.co.uk/dp/160774970X" target="_blank" rel="noreferrer"&gt;Cocktail Codex&lt;/a&gt; and &lt;a href="https://www.amazon.co.uk/dp/1770857753" target="_blank" rel="noreferrer"&gt;Cocktails Made Easy&lt;/a&gt;), others from websites (&lt;a href="https://www.diffordsguide.com/" target="_blank" rel="noreferrer"&gt;Difford&amp;rsquo;s Guide&lt;/a&gt;), and most importantly those that I&amp;rsquo;ve either guessed from things I&amp;rsquo;ve drunk elsewhere (like my favourite bar in Bristol, &lt;a href="https://milkthistlebristol.com/" target="_blank" rel="noreferrer"&gt;The Milk Thistle&lt;/a&gt;), or modifications to recipes from the referenced sources.&lt;/p&gt;
&lt;p&gt;I wanted somewhere to store all these variations: somewhere that I could search easily from my iPhone (which is nearly always close to me when I&amp;rsquo;m making drinks). I wanted each recipe to fit in its entirety on my iPhone screen without the need to scroll.&lt;/p&gt;
&lt;p&gt;Around the time I started thinking about this problem, I also learned about &lt;a href="https://tailscale.com/kb/1244/tsnet" target="_blank" rel="noreferrer"&gt;&lt;code&gt;tsnet&lt;/code&gt;&lt;/a&gt; and was desperate for an excuse to try it out - and thus &lt;a href="https://github.com/jnsgruk/libations" target="_blank" rel="noreferrer"&gt;Libations&lt;/a&gt; was born as the product of two things I love: &lt;a href="https://tailscale.com/" target="_blank" rel="noreferrer"&gt;Tailscale&lt;/a&gt; and cocktails!&lt;/p&gt;
&lt;h2 id="tswhat" class="relative group"&gt;tswhat? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tswhat" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Some time ago, Tailscale released a Go library named &lt;a href="https://tailscale.com/kb/1244/tsnet" target="_blank" rel="noreferrer"&gt;&lt;code&gt;tsnet&lt;/code&gt;&lt;/a&gt;. To quote the website:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;tsnet is a library that lets you embed Tailscale inside of a Go program&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this case, the embedded Tailscale works slightly different to how &lt;code&gt;tailscaled&lt;/code&gt; works (by default, anyway&amp;hellip;). Rather than using the universal TUN/TAP driver in the Linux kernel, &lt;code&gt;tsnet&lt;/code&gt; instead uses a userspace TCP/IP networking stack, which enables the process embedding it to make direct connections to other devices on your &lt;a href="https://tailscale.com/kb/1136/tailnet" target="_blank" rel="noreferrer"&gt;tailnet&lt;/a&gt; as if it were &amp;ldquo;just another machine&amp;rdquo;. This makes it easy to embed, and drops the requirement for the process to be privileged enough to access &lt;code&gt;/dev/tun&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One of the things I like about how &lt;code&gt;tsnet&lt;/code&gt; presents applications as devices on the tailnet, is that you can employ &lt;a href="https://tailscale.com/kb/1018/acls" target="_blank" rel="noreferrer"&gt;ACLs&lt;/a&gt; to control who and what on your tailnet can access &lt;em&gt;the application&lt;/em&gt;, rather than &lt;em&gt;the device&lt;/em&gt;. I&amp;rsquo;ve solved this problem before by putting applications in &lt;a href="https://github.com/jnsgruk/nixos-config/blob/main/host/common/services/servarr/lib.nix" target="_blank" rel="noreferrer"&gt;their own &lt;code&gt;systemd-nspawn&lt;/code&gt; container&lt;/a&gt; and joining those containers to my tailnet. Another nice option is &lt;a href="https://github.com/boinkor-net/tsnsrv" target="_blank" rel="noreferrer"&gt;&lt;code&gt;tsnsrv&lt;/code&gt;&lt;/a&gt; which essentially acts as a Tailscale-aware proxy for individual applications, but in this case I wanted to bake it into the application - which I would &lt;em&gt;only&lt;/em&gt; access over my tailnet.&lt;/p&gt;
&lt;p&gt;Getting started with &lt;code&gt;tsnet&lt;/code&gt; couldn&amp;rsquo;t be easier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir tsnet-app&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; tsnet-app
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go mod init tsnet-app
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go get tailscale.com/tsnet
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That will get you set up with a basic Go project, with the &lt;code&gt;tsnet&lt;/code&gt; library available. Create a new &lt;code&gt;main.go&lt;/code&gt; file with the following contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;log&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tailscale.com/tsnet&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Create a new tsnet server instance&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tsnet-test&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Have the tsnet server listen on :8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;:8080&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Define a very simple handler with a simple Hello, World style message&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Hello from %s, tailnet!&amp;lt;/h1&amp;gt;\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Hostname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Start an HTTP server on the tsnet listener&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is about the most minimal example I could contrive. The code creates a simple instance of &lt;code&gt;tsnet.Server&lt;/code&gt; with the hostname &lt;code&gt;tsnet-app&lt;/code&gt;, listens on port &lt;code&gt;8080&lt;/code&gt; and serves up a simple &lt;code&gt;Hello, World!&lt;/code&gt; style message. On running the application you&amp;rsquo;ll see the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ go run .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024/08/13 15:03:37 tsnet running state path /home/jon/.config/tsnet-tsnet-app/tailscaled.state
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024/08/13 15:03:37 tsnet starting with hostname &lt;span class="s2"&gt;&amp;#34;tsnet-test&amp;#34;&lt;/span&gt;, varRoot &lt;span class="s2"&gt;&amp;#34;/home/jon/.config/tsnet-tsnet-app&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024/08/13 15:03:38 LocalBackend state is NeedsLogin&lt;span class="p"&gt;;&lt;/span&gt; running StartLoginInteractive...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2024/08/13 15:03:43 To start this tsnet server, restart with TS_AUTHKEY set, or go to: https://login.tailscale.com/a/deadbeeffeebdaed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Clicking the link will open a page in your browser that runs you through Tailscale&amp;rsquo;s authentication flow, after which you should be able to &lt;code&gt;curl&lt;/code&gt; the page directly from any of your devices (assuming you&amp;rsquo;re not doing anything complicated with ACLs that might prevent it)!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ tailscale status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.93.165.28 kara jnsgruk@ linux -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;100.106.82.10 tsnet-test jnsgruk@ linux -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ curl http://tsnet-test:8080
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Hello from tsnet-test, tailnet!&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The library has a pretty small API surface, all of which is documented on &lt;a href="https://pkg.go.dev/tailscale.com/tsnet" target="_blank" rel="noreferrer"&gt;pkg.go.dev&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="libations" class="relative group"&gt;Libations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#libations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For my cocktail app, I wanted to employ a similar, albeit simplified, set of techniques that I &lt;a href="https://jnsgr.uk/2024/01/building-a-blog-with-go-nix-hugo/" target="_blank" rel="noreferrer"&gt;use to build this blog&lt;/a&gt;. I love Go&amp;rsquo;s ability to embed static files that can be served as web assets.&lt;/p&gt;
&lt;h3 id="recipe-schema" class="relative group"&gt;Recipe Schema &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#recipe-schema" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I wanted to represent recipes in a format that could be updated by hand if necessary, and easily parsed into a web frontend. I decided to use JSON for this, representing the recipes as a list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;New York Sour&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;base&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Bourbon&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;glass&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;12oz Lowball&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;method&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Dry Shake&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Shake&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ice&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Cubed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ingredients&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Lemon Juice&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;measure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;20&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ml&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Sugar&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;measure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;20&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ml&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Red Wine&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;measure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;10&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ml&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bourbon&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;measure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;40&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ml&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Egg White&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;measure&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;20&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ml&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;garnish&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Lemon Sail&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;notes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Use claret or malbec&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This schema is able to capture all the relevant details from the different formats I&amp;rsquo;ve seen over the years. It would take some time to format my favourites into this schema, but that was always going to be the case.&lt;/p&gt;
&lt;p&gt;I was fortunate enough to get access to the recipe collection from a well regarded cocktail bar in the UK. Unfortunately it was given to me in a hard-to-parse PDF, which resulted in many hours of playing with OCR tools and manual data cleaning - but enabled me to bootstrap the app with around 450 high quality recipes. I didn&amp;rsquo;t include their recipes in the libations &lt;a href="https://github.com/jnsgruk/libations" target="_blank" rel="noreferrer"&gt;repository&lt;/a&gt;, but I did include some of my own favourite concoctions in a &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/static/sample.json" target="_blank" rel="noreferrer"&gt;sample recipe file&lt;/a&gt;. My &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/static/sample.json#L2-L20" target="_blank" rel="noreferrer"&gt;Mezcal Margarita&lt;/a&gt; gets pretty good reviews 😉.&lt;/p&gt;
&lt;h3 id="server" class="relative group"&gt;Server &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#server" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The server implementation needed to fulfil a few requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parse a specific recipes file, optionally passed via the command line&lt;/li&gt;
&lt;li&gt;Have an embedded filesystem to contain static assets and templates&lt;/li&gt;
&lt;li&gt;Be able to render HTML templates with the given recipes&lt;/li&gt;
&lt;li&gt;Listen on either a tailnet (via &lt;code&gt;tsnet&lt;/code&gt;), or locally (for testing convenience)&lt;/li&gt;
&lt;li&gt;When listening on the tailnet, listen on HTTPS, redirecting HTTP traffic accordingly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wanted to keep dependencies to a minimum to make things easier to maintain over time. The &lt;code&gt;tsnet&lt;/code&gt; library pulls in a few indirect dependencies, but everything else Libations uses is in the Go standard library.&lt;/p&gt;
&lt;p&gt;The recipes JSON schema is very simple, and is modelled with a couple of Go structs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Ingredient represents the name and quantity of a given ingredient in a recipe.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Ingredient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Measure&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Unit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Drink represents all of the details for a given drink.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Drink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Glass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Ice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Ingredients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;Ingredient&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Garnish&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Notes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/main.go#L73-L101" target="_blank" rel="noreferrer"&gt;&lt;code&gt;parseRecipes&lt;/code&gt;&lt;/a&gt; function checks whether or not the user passed the path to a specific recipe file, or whether it should default to parsing the sample recipes file. Once it&amp;rsquo;s determined the right file to parse, and validated its existence, it unmarshals the JSON using the Go standard library.&lt;/p&gt;
&lt;p&gt;Users have the option of passing the &lt;code&gt;-local&lt;/code&gt; flag when starting Libations, which bypasses &lt;code&gt;tsnet&lt;/code&gt; completely and starts a local HTTP listener on the specific port. This makes for easier testing when iterating through changes to the web UI and other elements:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;addr&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;:8080&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;the address to listen on in the case of a local listener&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;start on local addr; don&amp;#39;t attach to a tailnet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Listener&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;localListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;tailscaleListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;tsnetLogs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;failed to create listener&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Setting up the &lt;code&gt;tsnet&lt;/code&gt; server and listener is only &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/main.go#L141-L183" target="_blank" rel="noreferrer"&gt;mildly more complicated&lt;/a&gt; &amp;ndash; but mostly due to my requirement that all HTTP traffic is redirected to HTTPS, using the &lt;a href="https://letsencrypt.org/" target="_blank" rel="noreferrer"&gt;LetsEncrypt&lt;/a&gt; certificates that Tailscale &lt;a href="https://tailscale.com/kb/1153/enabling-https" target="_blank" rel="noreferrer"&gt;provides automatically&lt;/a&gt;. The redirects are handled by a separate Goroutine in this case:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Start a standard HTTP server in the background to redirect HTTP -&amp;gt; HTTPS.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;httpLn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsnetServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;tcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;:80&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unable to start HTTP listener, redirects from http-&amp;gt;https will not work&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;started HTTP listener with tsnet at %s:80&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;httpLn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;newURL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://%s%s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RequestURI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;newURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusMovedPermanently&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unable to start http server, redirects from http-&amp;gt;https will not work&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With the correct listener selected, I create an &lt;code&gt;http.ServeMux&lt;/code&gt; to &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/main.go#L103-L124" target="_blank" rel="noreferrer"&gt;handle routing to static assets and rendering templates&lt;/a&gt;, and pass that mux to the &lt;code&gt;http.Serve&lt;/code&gt; method from the Go standard library - and that&amp;rsquo;s it! At the time of writing the Go code totals 235 lines - not bad!&lt;/p&gt;
&lt;h3 id="web-interface" class="relative group"&gt;Web Interface &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#web-interface" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The web interface was designed primarily for mobile devices, and I&amp;rsquo;ve not yet done the work to make it excellent for larger-screened devices - though it&amp;rsquo;s certainly bearable. It&amp;rsquo;s also read-only at the moment: you can browse all the recipes, and there is a simple full-text search which can narrow the list of recipes down by searching for an ingredient, method, glass type, notes, etc.&lt;/p&gt;
&lt;p&gt;As mentioned in &lt;a href="https://jnsgr.uk/2024/05/tracking-software-across-teams/" target="_blank" rel="noreferrer"&gt;an earlier post&lt;/a&gt;, I&amp;rsquo;m a big fan of the &lt;a href="https://vanillaframework.io/" target="_blank" rel="noreferrer"&gt;Vanilla Framework&lt;/a&gt;, which is a &amp;ldquo;simple, extensible CSS framework&amp;rdquo; from &lt;a href="https://canonical.com" target="_blank" rel="noreferrer"&gt;Canonical&lt;/a&gt;, and is used for all of Canonical&amp;rsquo;s apps and websites. Given that I had some prior experience using it, I decided to use it again here. I started using my tried and tested recipe of Vanilla + &lt;a href="https://gohugo.io" target="_blank" rel="noreferrer"&gt;Hugo&lt;/a&gt;, but later &lt;a href="https://github.com/jnsgruk/libations/commit/d2783cf1adebd2432e832b27b335d2037d485da2" target="_blank" rel="noreferrer"&gt;reverted&lt;/a&gt; to using simple HTML templates with Go&amp;rsquo;s &lt;a href="https://pkg.go.dev/html/template" target="_blank" rel="noreferrer"&gt;&lt;code&gt;html/template&lt;/code&gt;&lt;/a&gt; package.&lt;/p&gt;
&lt;p&gt;The result is a set of &lt;a href="https://github.com/jnsgruk/libations/tree/main/templates" target="_blank" rel="noreferrer"&gt;templates&lt;/a&gt;, which iterate over the recipe data from the JSON file, and output nicely styled HTML elements:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_af4ff8d1f418a0b4.webp 330w,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_7fbdd88302603ff9.webp 660w
,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_f065a86f491e1893.webp 1024w
,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_63ba8145e090a22f.webp 1170w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1170"
height="2532"
class="mx-auto my-0 rounded-md"
alt="screenshot of the libations app displaying the recipe for a mezcal margarita"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_4fe842ef4d44499f.png" srcset="https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_3ed6744b3577b08d.png 330w,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_4fe842ef4d44499f.png 660w
,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01_hu_ea0bc09c4c0790ec.png 1024w
,https://jnsgr.uk/2024/08/tailscale-on-the-rocks/01.png 1170w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is nothing fancy going on here - it&amp;rsquo;s nearly all stock Vanilla Framework. I do specify some &lt;a href="https://github.com/jnsgruk/libations/blob/main/static/css/overrides.css" target="_blank" rel="noreferrer"&gt;overrides&lt;/a&gt; to make the colours a bit less Ubuntu-ish, but that&amp;rsquo;s it!&lt;/p&gt;
&lt;p&gt;One detail I&amp;rsquo;m pleased with is the dynamic drink icons. The icons indicate the type of glass the particular drink should be served in. This is a simple trick: for each drink the HTML template renders a &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/templates/glass-icon.html" target="_blank" rel="noreferrer"&gt;&lt;code&gt;glass-icon&lt;/code&gt;&lt;/a&gt; partial, which reads the glass type specified in the recipe, and renders the &lt;a href="https://github.com/jnsgruk/libations/tree/dc1e50c60ba992a01dfab82d7550ca76a2655efd/templates/icons" target="_blank" rel="noreferrer"&gt;appropriate SVG file&lt;/a&gt; which is then &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/static/css/overrides.css#L42" target="_blank" rel="noreferrer"&gt;coloured with CSS&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="packaging-for-nixos" class="relative group"&gt;Packaging for NixOS &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#packaging-for-nixos" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;There were two main tasks in this category: creating the Nix package itself, and writing a simple NixOS module that would make it simple for me to run it on my NixOS server.&lt;/p&gt;
&lt;p&gt;The project uses a &lt;a href="https://nixos.wiki/wiki/Flakes" target="_blank" rel="noreferrer"&gt;Flake&lt;/a&gt; to provide the package, overlay and module. The standard library in Nix has good tooling for Go applications now, meaning the &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/nix/libations.nix" target="_blank" rel="noreferrer"&gt;derivation&lt;/a&gt; is short:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildGo122Module&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lastModifiedDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substring&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="n"&gt;lastModifiedDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;buildGo122Module&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;libations&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanSource&lt;/span&gt; &lt;span class="sr"&gt;../.&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-AWvaHyJL7Cm+zCY/vTuTAsgLbVy6WUNfmaGbyQOzMMQ=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I haven&amp;rsquo;t cut any versioned releases of Libations at the time of writing - I&amp;rsquo;m using the last modified date of the flake to version the binaries.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/jnsgruk/libations/blob/dc1e50c60ba992a01dfab82d7550ca76a2655efd/nix/module.nix" target="_blank" rel="noreferrer"&gt;module&lt;/a&gt; starts the application with &lt;code&gt;systemd&lt;/code&gt;, and optionally provides it with a recipes file. There are four options defined at the time of writing: &lt;code&gt;services.libations.{enable,recipesFile,tailscaleKeyFile,package}&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mkEnableOption&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Enables the libations service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;recipesFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nullOr&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/libations/recipes.json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; A file containing drinks recipes per the Libations file format.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; See https://github.com/jnsgruk/libations.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tailscaleKeyFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nullOr&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/run/agenix/libations-tsauthkey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; A file containing a key for Libations to join a Tailscale network.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; See https://tailscale.com/kb/1085/auth-keys/.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mkPackageOption&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;libations&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The &lt;code&gt;tailscaleKeyFile&lt;/code&gt; option enables the service to automatically join a tailnet using an API key, rather than prompting the user to click a link and authorise manually.&lt;/p&gt;
&lt;p&gt;These options are translated into a simple &lt;code&gt;systemd&lt;/code&gt; unit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Libations cocktail recipe viewer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wantedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multi-user.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;network.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;XDG_CONFIG_HOME&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/libations/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DynamicUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ExecStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/libations -recipes-file &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recipesFile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Restart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;always&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;EnvironmentFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tailscaleKeyFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StateDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;libations&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StateDirectoryMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0750&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The &lt;code&gt;XDG_CONFIG_HOME&lt;/code&gt; variable is set so that &lt;code&gt;tsnet&lt;/code&gt; stores it&amp;rsquo;s state in &lt;code&gt;/var/lib/libations&lt;/code&gt;, rather than trying to store it in the home directory of the &lt;a href="https://0pointer.net/blog/dynamic-users-with-systemd.html" target="_blank" rel="noreferrer"&gt;dynamically created user&lt;/a&gt; for the &lt;code&gt;systemd&lt;/code&gt; unit.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://github.com/ryantm/agenix" target="_blank" rel="noreferrer"&gt;agenix&lt;/a&gt; on my NixOS machines to manage encrypted secrets, and for this project I used it to encrypt both the initial Tailscale &lt;a href="https://tailscale.com/kb/1085/auth-keys" target="_blank" rel="noreferrer"&gt;auth key&lt;/a&gt;, and my super-secret recipe collection! The configuration to provide the secrets and configure the server to run libations is available &lt;a href="https://github.com/jnsgruk/nixos-config/blob/0f4df26871c1cacc5d9c24cdd46e495c808f6639/host/common/services/libations.nix" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;, but looks like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;libations-auth-key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/thor-libations-tskey.age&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;400&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;libations-recipes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/secrets/thor-libations-recipes.age&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;444&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;recipesFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libations-recipes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tailscaleKeyFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libations-auth-key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;As a result, the application is now available at &lt;code&gt;https://libations&lt;/code&gt;, with a valid LetsEncrypt certificate, on all of my machines! 🎉&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This was a really fun project. It felt like a fun way to explore &lt;code&gt;tsnet&lt;/code&gt;, and resulted in something that I&amp;rsquo;ve used a lot over the past year. I don&amp;rsquo;t have many plans to adjust things in the near future - though I do find myself wanting a nice interface to add new recipes from time to time.&lt;/p&gt;
&lt;p&gt;And now for the twist: three weeks ago I gave up drinking alcohol (likely for good), so now I&amp;rsquo;m on a mission to find some non-alcoholic recipes to make life a little tastier! I suspect this will be hard work - and I&amp;rsquo;ll certainly miss some of my favourites, but my wife and I have already found some compelling alternatives.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve got a favourite recipe (alcoholic or not) and you liked the article, then perhaps open a PR and add it to the &lt;a href="https://github.com/jnsgruk/libations/blob/main/static/sample.json" target="_blank" rel="noreferrer"&gt;sample recipes file&lt;/a&gt;!&lt;/p&gt;</description></item><item><title>How I Computer in 2024</title><link>https://jnsgr.uk/2024/07/how-i-computer-in-2024/</link><pubDate>Wed, 31 Jul 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/07/how-i-computer-in-2024/</guid><description>&lt;blockquote&gt;
&lt;p&gt;Since writing this post, I&amp;rsquo;ve posted &lt;a href="https://jnsgr.uk/2025/06/from-nixos-to-ubuntu/" target="_blank" rel="noreferrer"&gt;an update&lt;/a&gt; about moving from NixOS to Ubuntu with more up to date content on my current setup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m always fascinated to see how people use their computers - which applications they choose, how they set up their desktop environments and even how their screens are laid out on their desk. I&amp;rsquo;ve learned some great tricks from friends and colleagues over the years, so I thought I&amp;rsquo;d write up how I use my machines in 2024.&lt;/p&gt;
&lt;p&gt;The setup I&amp;rsquo;m using today has been quite static for a couple of years, with only minor adjustments. Each time I change something significant, I leave it for at least a couple of months to try and build muscle memory and see if I&amp;rsquo;m going to make the adjustment permanent.&lt;/p&gt;
&lt;h2 id="hardware" class="relative group"&gt;Hardware &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#hardware" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="desktop" class="relative group"&gt;Desktop &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#desktop" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;My main machine is a custom built desktop machine. It&amp;rsquo;s in a sombre looking, all black &lt;a href="https://www.bequiet.com/en/case/1501" target="_blank" rel="noreferrer"&gt;beQuiet Silent Base 600&lt;/a&gt; case. I&amp;rsquo;ve never been into RGB lights - I&amp;rsquo;m much more into good thermals and &lt;em&gt;silent&lt;/em&gt; operation. The full spec is as follows:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU&lt;/strong&gt;: &lt;a href="https://www.amd.com/en/products/processors/desktops/ryzen/7000-series/amd-ryzen-9-7950x.html" target="_blank" rel="noreferrer"&gt;AMD Ryzen 9 7950X&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;GPU&lt;/strong&gt;: &lt;a href="https://www.xfxforce.com/shop/xfx-speedster-merc310-7900xt" target="_blank" rel="noreferrer"&gt;AMD Radeon RX 7900XT&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;RAM&lt;/strong&gt;: &lt;a href="https://www.gskill.com/product/165/390/1665020865/F5-6000J3040G32GX2-TZ5NR" target="_blank" rel="noreferrer"&gt;G.SKILL Trident Z5 Neo RGB 64GB DDR5-6000&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;PSU&lt;/strong&gt;: &lt;a href="https://www.corsair.com/uk/en/p/psu/cp-9020259-uk/hx1000i-fully-modular-ultra-low-noise-platinum-atx-1000-watt-pc-power-supply-cp-9020259-uk" target="_blank" rel="noreferrer"&gt;Corsair HX1000i&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Disk&lt;/strong&gt;: &lt;a href="https://www.westerndigital.com/products/internal-drives/wd-black-sn850x-nvme-ssd?sku=WDS100T2X0E" target="_blank" rel="noreferrer"&gt;1TB SN850X&lt;/a&gt; + &lt;a href="https://www.westerndigital.com/products/internal-drives/wd-black-sn850x-nvme-ssd?sku=WDS200T2X0E" target="_blank" rel="noreferrer"&gt;2TB SN850X&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Board&lt;/strong&gt;: &lt;a href="https://www.msi.com/Motherboard/MPG-X670E-CARBON-WIFI" target="_blank" rel="noreferrer"&gt;MSI MPG X670E CARBON WIFI&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Cooler&lt;/strong&gt;: &lt;a href="https://www.bequiet.com/en/cpucooler/4466" target="_blank" rel="noreferrer"&gt;beQuiet Dark Rock Pro 5&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Case&lt;/strong&gt;: &lt;a href="https://www.bequiet.com/en/case/1501" target="_blank" rel="noreferrer"&gt;beQuiet Silent Base 600&lt;/a&gt; with 3x &lt;a href="https://www.bequiet.com/en/casefans/3703" target="_blank" rel="noreferrer"&gt;beQuiet Silent Wings 4 PWM&lt;/a&gt; fans&lt;br/&gt;
&lt;strong&gt;Keyboard&lt;/strong&gt;: &lt;a href="https://www.durgod.com/product/k320-space-gray/" target="_blank" rel="noreferrer"&gt;DURGOD Taurus K320 TKL&lt;/a&gt; with Cherry MX Brown switches&lt;br/&gt;
&lt;strong&gt;Mouse&lt;/strong&gt;: &lt;a href="https://www.razer.com/ap-en/gaming-mice/razer-deathadder-v2-pro" target="_blank" rel="noreferrer"&gt;Razer Deathadder V2 Pro&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Monitor&lt;/strong&gt;: &lt;a href="https://www.samsung.com/uk/monitors/gaming/odyssey-neo-g9-g95nc-57-inch-240hz-curved-dual-uhd-ls57cg952nuxxu/" target="_blank" rel="noreferrer"&gt;57&amp;quot; Samsung G95NC Odessey Neo G9&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Camera&lt;/strong&gt;: &lt;a href="https://www.sony.co.uk/interchangeable-lens-cameras/products/ilme-fx3-body---kit" target="_blank" rel="noreferrer"&gt;Sony ILME-FX3&lt;/a&gt; / &lt;a href="https://www.sony.co.uk/electronics/camera-lenses/sel2870" target="_blank" rel="noreferrer"&gt;FE 28-70mm F3.5-5.6&lt;/a&gt; / &lt;a href="https://www.elgato.com/uk/en/p/cam-link-4k" target="_blank" rel="noreferrer"&gt;Elgato Cam Link 4K&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Speakers&lt;/strong&gt;: &lt;a href="https://audioengineeu.com/products/audioengine-a2-wireless-bluetooth-computer-speakers-60w-bluetooth-speaker-system-for-home-studio-gaming" target="_blank" rel="noreferrer"&gt;Audioengine A2+&lt;/a&gt;&lt;br/&gt;
&lt;strong&gt;Mic&lt;/strong&gt;: &lt;a href="https://rode.com/en/microphones/on-camera/videomic-go-ii" target="_blank" rel="noreferrer"&gt;RODE VideoMic GO II&lt;/a&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;On my desk, you&amp;rsquo;ll find a &lt;a href="https://www.samsung.com/uk/monitors/gaming/odyssey-neo-g9-g95nc-57-inch-240hz-curved-dual-uhd-ls57cg952nuxxu/" target="_blank" rel="noreferrer"&gt;57&amp;quot; Samsung G95NC Odessey Neo G9&lt;/a&gt; monitor mounted on a &lt;a href="https://www.amazon.co.uk/gp/product/B0B73XXDP5/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1" target="_blank" rel="noreferrer"&gt;gas spring arm&lt;/a&gt;, which is the newest addition to my setup. For 5 years, I&amp;rsquo;d been running a pair of 27&amp;quot; &lt;a href="https://www.lg.com/us/monitors/lg-27un850-w-4k-uhd-led-monitor" target="_blank" rel="noreferrer"&gt;LG 27&amp;quot; UN850 4K&lt;/a&gt; monitors mounted on a dual monitor arm and had toyed with the idea of moving to an ultra-wide for a while. The Samsung display is the first I have found that doesn&amp;rsquo;t compromise on resolution - it&amp;rsquo;s the same resolution as my two LG monitors combined, but on a single panel. I must admit that I&amp;rsquo;m quite surprised how much of a productivity booster it is &lt;em&gt;not&lt;/em&gt; having the split down the middle.&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_d086f5b7eb8785bb.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_afa6804c56494bde.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_82aee2c8b3250c73.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_cffccf31f5645789.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1600"
height="900"
class="mx-auto my-0 rounded-md"
alt="a photograph of my desk including a huge ultra-wide monitor"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_5d49891b975005ae.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_d6a0a234c7ef3c2d.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_5d49891b975005ae.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_bc0ce5a0d6ba62ed.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/01_hu_bcb6679a926b0a7c.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;At the time of writing, I work for &lt;a href="https://canonical.com" target="_blank" rel="noreferrer"&gt;Canonical&lt;/a&gt; which is an all remote company. The combination of the company itself and my role as VP Engineering means I spend a good portion of my day on video calls. In my opinion, investing in a solid AV setup is a service to your colleagues, particularly where your role involves managing people. I&amp;rsquo;m currently running a &lt;a href="https://www.sony.co.uk/interchangeable-lens-cameras/products/ilme-fx3-body---kit" target="_blank" rel="noreferrer"&gt;Sony ILME-FX3&lt;/a&gt; with the standard &lt;a href="https://www.sony.co.uk/electronics/camera-lenses/sel2870" target="_blank" rel="noreferrer"&gt;FE 28-70mm F3.5-5.6&lt;/a&gt; lens, hooked up to an &lt;a href="https://www.elgato.com/uk/en/p/cam-link-4k" target="_blank" rel="noreferrer"&gt;Elgato Cam Link 4K&lt;/a&gt;. For audio, I use a &lt;a href="https://rode.com/en/microphones/on-camera/videomic-go-ii" target="_blank" rel="noreferrer"&gt;RODE VideoMic GO II&lt;/a&gt; and a pair of &lt;a href="https://audioengineeu.com/products/audioengine-a2-wireless-bluetooth-computer-speakers-60w-bluetooth-speaker-system-for-home-studio-gaming" target="_blank" rel="noreferrer"&gt;Audioengine A2+&lt;/a&gt; speakers.&lt;/p&gt;
&lt;p&gt;In the world of Linux desktops, I&amp;rsquo;ve found audio devices that present their own USB interface to be much less hassle. I completely disable the motherboard&amp;rsquo;s onboard sound, as well as the HDMI/DisplayPort sound outputs on my machine and leave just the USB audio interfaces from my mic and speakers enabled.&lt;/p&gt;
&lt;h3 id="server" class="relative group"&gt;Server &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#server" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I use the term &amp;ldquo;server&amp;rdquo; loosely… my homelab has gone through many iterations over the years, from all &amp;ldquo;on-prem&amp;rdquo;, to a mix of cloud services and devices, and back again.&lt;/p&gt;
&lt;p&gt;My current setup is very modest, partly because my workstation is such a monster, meaning I can easily spin up multiple VMs/containers there when I want to experiment and not really impact the performance of the machine for more routine tasks.&lt;/p&gt;
&lt;p&gt;Most of my services run on a single &lt;a href="https://ark.intel.com/content/www/us/en/ark/products/89187/intel-nuc-kit-nuc6i7kyk.html" target="_blank" rel="noreferrer"&gt;Intel NUC6i7KYK&lt;/a&gt;. This machine has an Intel i7-6770HQ CPU, 16GB RAM and a 512GB Samsung 970 Pro NVMe drive internally. It&amp;rsquo;s connected to a Samsung 840 EVO 4TB SATA drive by USB. I&amp;rsquo;m not much of a data-hoarder so I don&amp;rsquo;t require too much storage.&lt;/p&gt;
&lt;p&gt;This machine is getting a bit tired and I&amp;rsquo;m thinking about replacing it with something a little more modern later this year.&lt;/p&gt;
&lt;h3 id="laptop" class="relative group"&gt;Laptop &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#laptop" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;If I&amp;rsquo;m not at my desk, then I&amp;rsquo;m using my &lt;a href="https://www.lenovo.com/gb/en/p/laptops/thinkpad/thinkpadz/thinkpad-z13-%2813-inch-amd%29/len101t0036?srsltid=AfmBOor-8ic5yZrW3rlDXTTRwK8r05y-gjCpJK04fA4qtote0u2HZ7I6" target="_blank" rel="noreferrer"&gt;Lenovo Z13 Gen 1&lt;/a&gt;. I specified this machine with the AMD Ryzen 7 Pro 6860Z, 32GB RAM and a Hi-DPI display.&lt;/p&gt;
&lt;p&gt;I can&amp;rsquo;t rate this machine highly enough. The build quality is a cut above even Lenovo&amp;rsquo;s normal standard - it feels very premium and much more in the style of Apple&amp;rsquo;s uni-body aluminium laptops. It&amp;rsquo;s got plenty of power, and the battery lasts most of the day under moderate usage.&lt;/p&gt;
&lt;p&gt;I tend towards ultralight machines when I travel because I can always use my desktop machine remotely if I need more grunt (more on that later…), and I&amp;rsquo;m certainly not interested in trying to make dual integrated/discrete GPUs work properly.&lt;/p&gt;
&lt;h3 id="phone" class="relative group"&gt;Phone &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#phone" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I carry an &lt;a href="https://www.apple.com/uk/iphone-15-pro/" target="_blank" rel="noreferrer"&gt;Apple iPhone 15 Pro&lt;/a&gt;. I&amp;rsquo;ve been an iPhone user since around 2011 and likely won&amp;rsquo;t change any time soon. My family all use iPhones (and therefore FaceTime) and I like the particular trade-off of convenience/privacy that&amp;rsquo;s provided by Apple - however flawed that might be in absolute terms. The phone works great with my Airpods, the camera is better than I am at taking photos, and the battery life seems pretty good too.&lt;/p&gt;
&lt;p&gt;I wrap the phone in a &lt;a href="https://uk.mous.co/products/limitless-5-0-magsafe-compatible-phone-case-aramid_fibre" target="_blank" rel="noreferrer"&gt;Mous Limitless 5.0 Aramid Fibre&lt;/a&gt; case to avoid too many oops moments!&lt;/p&gt;
&lt;p&gt;I find it difficult to get too excited about phones these days, I see them more as a commodity.&lt;/p&gt;
&lt;h2 id="connectivity--security" class="relative group"&gt;Connectivity &amp;amp; Security &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#connectivity--security" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In 2021 I started using &lt;a href="https://tailscale.com/" target="_blank" rel="noreferrer"&gt;Tailscale&lt;/a&gt; in place of my hand-rolled Wireguard setup, and I haven&amp;rsquo;t looked back. It has to be one of my favourite pieces of technology ever. It runs on all of my things - desktops, laptops, servers, phones, tablets, etc.&lt;/p&gt;
&lt;p&gt;I also recently took advantage of their &lt;a href="https://tailscale.com/kb/1258/mullvad-exit-nodes" target="_blank" rel="noreferrer"&gt;partnership with Mullvad&lt;/a&gt;. I&amp;rsquo;ve used &lt;a href="https://mullvad.net/en" target="_blank" rel="noreferrer"&gt;Mullvad&lt;/a&gt; as my default VPN provider when using untrusted networks for a few years - but using it through Tailscale means I can still access my tailnet while my internet traffic egresses through Mullvad without any extra configuration.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://nextdns.io/" target="_blank" rel="noreferrer"&gt;NextDNS&lt;/a&gt; as an alternative to running a &lt;a href="https://pi-hole.net/" target="_blank" rel="noreferrer"&gt;Pi-Hole&lt;/a&gt; or similar. Tailscale have a nice &lt;a href="https://tailscale.com/kb/1218/nextdns" target="_blank" rel="noreferrer"&gt;integration&lt;/a&gt; which means that all the devices on my tailnet automatically get &lt;a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noreferrer"&gt;DNS-over-HTTPS&lt;/a&gt; without any additional configuration, as well as DNS-level ad-blocking.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got a couple of shared nodes in my tailnet - including one that my family can use as an exit node when they travel. As a result, I make quite extensive use of Tailscale &lt;a href="https://tailscale.com/kb/1018/acls" target="_blank" rel="noreferrer"&gt;ACLs&lt;/a&gt; to ensure people can only access what I want them to.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been a &lt;a href="https://1password.com/" target="_blank" rel="noreferrer"&gt;1Password&lt;/a&gt; user for more than a decade now and I think their products are fantastic. Their Linux app sets the bar for modern cross-platform applications in my view. I recently started using their secrets capability at the CLI - the ability to store a &lt;code&gt;.env&lt;/code&gt; file with a bunch of benign secret references, and have the actual &lt;a href="https://developer.1password.com/docs/cli/secrets-scripts" target="_blank" rel="noreferrer"&gt;secrets injected into the environment&lt;/a&gt; or a &lt;a href="https://developer.1password.com/docs/cli/secrets-config-files" target="_blank" rel="noreferrer"&gt;config file&lt;/a&gt; is very handy.&lt;/p&gt;
&lt;p&gt;I also have a small collection of &lt;a href="https://www.yubico.com/products/yubikey-5-overview/" target="_blank" rel="noreferrer"&gt;Yubikeys&lt;/a&gt; with different connectors. One lives on my desk attached to my desktop, another lives in my pocket or otherwise on my person, and another is in a safe. They&amp;rsquo;re all NFC enabled so they work nicely with my mobile devices. I configure my Yubikeys with ed25519 &lt;a href="https://developers.yubico.com/SSH/Securing_git_with_SSH_and_FIDO2.html" target="_blank" rel="noreferrer"&gt;resident keys&lt;/a&gt; for SSH, along with storing my GPG key (which rarely gets used these days&amp;hellip;).&lt;/p&gt;
&lt;p&gt;One of my favourite things about the Yubikey is their ability to store TOTP codes. It&amp;rsquo;s a bit of a pain when I onboard a new account having to add the new secret to each key, but the upside is I don&amp;rsquo;t have to work out how to update/transfer them all each time I get a new phone! It&amp;rsquo;s also handy on the desktop to be able to run &lt;code&gt;ykman oath accounts code &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="productivity-apps" class="relative group"&gt;Productivity Apps &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#productivity-apps" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;A lot of my work is done in a browser. Canonical uses &lt;a href="https://workspace.google.com/intl/en_uk/" target="_blank" rel="noreferrer"&gt;Google Workspace&lt;/a&gt; for emails, documents, slides, etc., so my default mode since joining has been to use Google Chrome for work things, and Firefox for personal things. I know that Firefox has &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/" target="_blank" rel="noreferrer"&gt;Account Containers&lt;/a&gt; and other features that would help segregate the two, but I&amp;rsquo;ve found keeping my work and personal concerns in completely separate browsers to be useful.&lt;/p&gt;
&lt;p&gt;After having run a &lt;a href="https://nextcloud.com/" target="_blank" rel="noreferrer"&gt;Nextcloud&lt;/a&gt; server for several years on a Droplet (&lt;a href="https://github.com/jnsgruk/nextcloud-docker-compose" target="_blank" rel="noreferrer"&gt;using &lt;code&gt;docker-compose&lt;/code&gt;&lt;/a&gt;), I ultimately realised that I was &lt;em&gt;only&lt;/em&gt; using the file syncing capability, and wasn&amp;rsquo;t moving much data even then. I switched to using &lt;a href="https://syncthing.net/" target="_blank" rel="noreferrer"&gt;Syncthing&lt;/a&gt; to avoid the overhead of running a server instance, which works particularly well when combined with Tailscale.&lt;/p&gt;
&lt;p&gt;All of my notes, both work and personal, live in &lt;a href="https://obsidian.md/" target="_blank" rel="noreferrer"&gt;Obsidian&lt;/a&gt;. When I first discovered Obsidian I fell for the classic trick of installing &lt;strong&gt;all the extensions&lt;/strong&gt;, and have since paired that back. I went very deep with &lt;a href="https://blacksmithgu.github.io/obsidian-dataview/" target="_blank" rel="noreferrer"&gt;Dataview&lt;/a&gt;, using it to collate actions from across my vault into various categories (meeting agendas, personal, reviews, etc.), but I found that as my vault grew the performance suffered quite a lot. A few months ago, I removed dataview, did some painful refactoring of my notes (lots of &lt;code&gt;sed&lt;/code&gt;erry and &lt;code&gt;grep&lt;/code&gt;pery!) and reverted to using Obsidian&amp;rsquo;s &lt;a href="https://help.obsidian.md/Plugins/Search#Embed%20search%20results%20in%20a%20note" target="_blank" rel="noreferrer"&gt;embedded search queries&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried to get into &lt;a href="https://zettelkasten.de/introduction/" target="_blank" rel="noreferrer"&gt;Zettelkasten&lt;/a&gt; but found the maintenance a little&amp;hellip; boring? I&amp;rsquo;ve ended up with a simple structure that I find really helps me in my day-to-day at work. Each day gets its own &amp;ldquo;Daily Note&amp;rdquo; which includes my agenda, linking to ongoing notes with the people or regular meetings I&amp;rsquo;m in. The daily notes are also a place for me to collate loose notes which might be searched later:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_36c456e44671248d.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_4261ec2f7b9c319d.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_351459cda373a59b.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_92167cc4f39c1704.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1360"
height="1077"
class="mx-auto my-0 rounded-md"
alt="obsidian.md screenshot showing my daily note template"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_917fffad5c2c26a4.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_b24f528c7326236b.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_917fffad5c2c26a4.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_2ec4c76d5f5911b5.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/02_hu_c0443de8f91f42cb.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The agenda and the links are automatically generated using a small Go application I wrote - this application scrapes my Google Calendar, and according to some rules and the knowledge it has of my vault, generates the Markdown for the agenda and copies it to the clipboard. Each day, I sit down and type &lt;code&gt;agenda&lt;/code&gt; at the command line, then paste into Obsidian. The notes for each person contain a running log of my notes with that person or group by date.&lt;/p&gt;
&lt;p&gt;I use a few Obsidian plugins to help here - including &lt;a href="https://github.com/SilentVoid13/Templater" target="_blank" rel="noreferrer"&gt;Templater&lt;/a&gt;, &lt;a href="https://github.com/chhoumann/quickadd" target="_blank" rel="noreferrer"&gt;QuickAdd&lt;/a&gt;, &lt;a href="https://github.com/scambier/obsidian-omnisearch" target="_blank" rel="noreferrer"&gt;Omnisearch&lt;/a&gt; and &lt;a href="https://github.com/platers/obsidian-linter" target="_blank" rel="noreferrer"&gt;Linter&lt;/a&gt;. The first two are particularly handy for quickly inserting common meeting agendas, sets of interview questions, playbooks, etc.&lt;/p&gt;
&lt;p&gt;I moved away from tracking tasks in Obsidian, and started using &lt;a href="https://todoist.com/" target="_blank" rel="noreferrer"&gt;Todoist&lt;/a&gt; late last year. Todoist is great - I like to keep running lists of tasks per person, so that when I next meet them in a 1:1 or otherwise, I have a quick reference of all the things I&amp;rsquo;m meant to speak with them about - and I can achieve that very easily with Todoist labels. The Obsidian integration means I can integrate the agenda with the meeting note for a specific person:&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_f2eb061e2f274815.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_db8c5e18d368880.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_f18872dc4431ff00.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_241836378de466ba.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="2499"
height="1094"
class="mx-auto my-0 rounded-md"
alt="obsidian and todoist side-by-side showing the integration"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_67f3f065d3f6c0b8.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_375a7f20cc31d276.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_67f3f065d3f6c0b8.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_ce5423ecfd08d45d.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/03_hu_aa06cb2a37009c4.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="development" class="relative group"&gt;Development &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#development" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been a long-time user of &lt;a href="https://alacritty.org/" target="_blank" rel="noreferrer"&gt;Alacritty&lt;/a&gt; as a terminal emulator. I mostly use &lt;a href="https://code.visualstudio.com/" target="_blank" rel="noreferrer"&gt;Visual Studio Code&lt;/a&gt; on the desktop - I like the community support for plugins, themes, etc. I&amp;rsquo;m also pretty handy in vim - I still have quite a snazzy &lt;a href="https://neovim.io/" target="_blank" rel="noreferrer"&gt;Neovim&lt;/a&gt; setup which I use whenever I&amp;rsquo;m at the terminal. You can see my &lt;a href="https://github.com/jnsgruk/nixos-config/blob/main/home/common/shell/vim.nix" target="_blank" rel="noreferrer"&gt;neovim config&lt;/a&gt; on Github - I don&amp;rsquo;t go too wild on plugins, but I&amp;rsquo;ve come to like &lt;a href="https://github.com/itchyny/lightline.vim" target="_blank" rel="noreferrer"&gt;lightline&lt;/a&gt;, &lt;a href="https://github.com/nvim-telescope/telescope.nvim" target="_blank" rel="noreferrer"&gt;telescope&lt;/a&gt;, and &lt;a href="https://github.com/nvim-tree/nvim-tree.lua" target="_blank" rel="noreferrer"&gt;nvim-tree-lua&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_4e296dc1dc51fef2.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_64bf18bb5448a8c6.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_7d188397ca7c7573.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_420c274aec36aeeb.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="2035"
height="1231"
class="mx-auto my-0 rounded-md"
alt="the alacritty terminal emulator showing a tmux session with neovim loaded"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_c06d0df4afb272aa.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_7d8d9746d1bfc261.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_c06d0df4afb272aa.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_4ac5677691b9e4ad.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/04_hu_a01d9d9a94198a79.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I mostly drive &lt;code&gt;git&lt;/code&gt; from the command line, but I&amp;rsquo;ve recently taken to using &lt;a href="https://www.sublimemerge.com/" target="_blank" rel="noreferrer"&gt;Sublime Merge&lt;/a&gt; for complicated rebases, or where I want to stage lots of small hunks in files. I was a dedicated user of &lt;a href="https://www.sublimetext.com/" target="_blank" rel="noreferrer"&gt;Sublime Text&lt;/a&gt; for some years, but felt like it lagged behind Visual Studio Code on features after a while - despite being somewhat addicted to how lightning fast Sublime Text felt in comparison.&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_30fc1daf73e4eb23.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_79d9ee819eca0cd3.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_7bfe86df5dd0a20.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_ef0c6e3633b7e20f.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="3128"
height="1335"
class="mx-auto my-0 rounded-md"
alt="visual studio code and sublime merge side-by-side"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_1d1bb9067b09700c.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_7b1df3ee0ff0caf8.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_1d1bb9067b09700c.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_cd2a454a51a75eba.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/05_hu_1f3f2d4146c51859.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="os--desktop" class="relative group"&gt;OS / Desktop &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#os--desktop" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;ve read my blog before, it&amp;rsquo;ll be no surprise to you that I&amp;rsquo;m all-in on NixOS for all the things. I started that journey around 2 years ago and haven&amp;rsquo;t looked back. My journey on the Linux desktop has been quite varied over the years: my first ever Linux desktop experience was with &lt;a href="https://www.knopper.net/knoppix/index-en.html" target="_blank" rel="noreferrer"&gt;Knoppix&lt;/a&gt; back in 2003. I then spent a few years dabbling with the various releases of Ubuntu before starting to use Linux on the desktop full-time in around 2014. From there I spent years on Arch Linux swapping between Plasma and GNOME about every 12 months.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve become a fairly dedicated tiling window manager user, though I&amp;rsquo;ll admit that I bounced off it a few times before it stuck. When I made the switch to &lt;a href="https://swaywm.org/" target="_blank" rel="noreferrer"&gt;Sway&lt;/a&gt; in 2021, something clicked and I&amp;rsquo;ve not gone back from tiling since. I stuck with Sway in various configurations for quite a while, before moving to an almost identical looking setup based on &lt;a href="https://hyprland.org/" target="_blank" rel="noreferrer"&gt;Hyprland&lt;/a&gt; around 15 months ago. Hyprland seems nice - it&amp;rsquo;s mostly stable and I like the eye-candy.&lt;/p&gt;
&lt;p&gt;Absolutely everything is themed with &lt;a href="https://github.com/catppuccin/catppuccin" target="_blank" rel="noreferrer"&gt;Catppuccin Macchiato&lt;/a&gt;. Not only do I love the theme, but I love how pervasive it is across all the apps/tools I use - and I&amp;rsquo;m a sucker for consistency!&lt;/p&gt;
&lt;p&gt;You can see all the gory details of my Hyprland, waybar, rofi, mako, etc. &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="07.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_7d32d2b40e44587b.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_ec7076dcc777b9b8.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_3d18fb46f7d6b570.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_71565ad53b95470e.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="7680"
height="2160"
class="mx-auto my-0 rounded-md"
alt="screenshot of a very busy hyprland desktop with editors, browsers, etc."
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_dd42ae4b8c136bc4.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_d936ca64c3e8317e.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_dd42ae4b8c136bc4.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_ed045c2b6baf7da4.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/07_hu_d5e154cb50b150a7.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="server--homelab" class="relative group"&gt;Server / Homelab &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#server--homelab" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;My server machine also runs NixOS, with a collection of media services and utilities. Where possible all of the services are run as &amp;ldquo;native&amp;rdquo; NixOS modules, with some running inside &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html" target="_blank" rel="noreferrer"&gt;systemd-nspawn&lt;/a&gt; containers using the built-in language for &lt;a href="https://nixos.wiki/wiki/NixOS_Containers" target="_blank" rel="noreferrer"&gt;NixOS Containers&lt;/a&gt;. If I&amp;rsquo;m experimenting with a new service, I sometimes run them in Docker to start with, especially if there isn&amp;rsquo;t already a NixOS module and I want to decide whether or not to invest the time in writing one!&lt;/p&gt;
&lt;p&gt;I run &lt;a href="https://caddyserver.com/" target="_blank" rel="noreferrer"&gt;Caddy&lt;/a&gt; as a reverse proxy (&lt;a href="https://github.com/jnsgruk/nixos-config/commit/dffac1dc0635f377865bcdfc2349387d41fc965d" target="_blank" rel="noreferrer"&gt;recently switched&lt;/a&gt; from Traefik). It can &lt;a href="https://tailscale.com/kb/1190/caddy-certificates" target="_blank" rel="noreferrer"&gt;talk directly to the Tailscale daemon&lt;/a&gt; to issue LetsEncrypt certs for devices on your tailnet. This Caddy instance acts as a reverse proxy onto all the services running on the server, along with some other services on my home LAN, all over TLS. I tend to access each of these services through &lt;a href="https://gethomepage.dev/latest/" target="_blank" rel="noreferrer"&gt;Homepage&lt;/a&gt; (which I previously &lt;a href="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/" target="_blank" rel="noreferrer"&gt;blogged about&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;a href="08.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_afa1eb2de0032a2e.webp 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_c0ffd64a7716a7dc.webp 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_4bcba8ba359a9d06.webp 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_2e7b132c3c891c9f.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1430"
height="985"
class="mx-auto my-0 rounded-md"
alt="my personal dashboard using gethomepage.dev"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_200e83fbbc7dac8.png" srcset="https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_57dbc59186fd3f04.png 330w,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_200e83fbbc7dac8.png 660w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_fb506dc413afb23.png 1024w
,https://jnsgr.uk/2024/07/how-i-computer-in-2024/08_hu_fd7b04bbbe5adaa7.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Each night, the contents of my iCloud Photos library is dumped using &lt;a href="https://github.com/icloud-photos-downloader/icloud_photos_downloader" target="_blank" rel="noreferrer"&gt;icloud-photos-downloader&lt;/a&gt; so that I have a local (and backed up) copy of my photos should anything untoward ever happen to my iCloud account.&lt;/p&gt;
&lt;p&gt;It also runs a &lt;a href="https://www.home-assistant.io/" target="_blank" rel="noreferrer"&gt;Home Assistant&lt;/a&gt; instance which runs my (in-progress!) custom integration for the underfloor heating and solar inverter in my house. I haven&amp;rsquo;t yet spent enough time with Home Assistant, but I plan to get it better set up over the coming months. I recently moved to a house with lots of &amp;ldquo;smart&amp;rdquo; devices, and I&amp;rsquo;d like to bring control of all the various devices into a single application. Once my experimenting is done, I&amp;rsquo;ll probably move the Home Assistant deployment to a dedicated low-power device.&lt;/p&gt;
&lt;p&gt;This machine&amp;rsquo;s data is backed up nightly to &lt;a href="https://www.borgbase.com/" target="_blank" rel="noreferrer"&gt;Borgbase&lt;/a&gt;. As mentioned above, I use &lt;a href="https://syncthing.net/" target="_blank" rel="noreferrer"&gt;Syncthing&lt;/a&gt; to move files around, and I configure this server to act as a &amp;ldquo;receive only&amp;rdquo; target for all the directories that I sync. This means that my data is always in at least three places: on my desktop or laptop, on my server, and backed up to Borgbase. Sometimes I&amp;rsquo;ll ad-hoc access files that aren&amp;rsquo;t synced to a given machine using &lt;a href="https://www.files.gallery/" target="_blank" rel="noreferrer"&gt;Files&lt;/a&gt;, which is a nice looking, single PHP-file gallery for your files. I keep meaning to replace this with something that &lt;em&gt;isn&amp;rsquo;t PHP&lt;/em&gt;, but I&amp;rsquo;ve yet to find a more compelling blend of simplicity and compelling user experience.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I don&amp;rsquo;t know how many other people are interested in how other people use their computers - but I hope you enjoyed the article. Feel free to reach out if you think I could be doing something better, or if you think you&amp;rsquo;ve got a killer app I might enjoy using!&lt;/p&gt;</description></item><item><title>Workstation VMs with LXD &amp; Multipass</title><link>https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/</link><pubDate>Tue, 25 Jun 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Over the years, I&amp;rsquo;ve used countless tools for creating virtual machines - often just for short periods of time when testing new software, trying out a new desktop environment, or creating a more isolated development environment. I&amp;rsquo;ve gone from just using the venerable &lt;a href="https://www.qemu.org/" target="_blank" rel="noreferrer"&gt;qemu&lt;/a&gt; at the command line, to full-blown desktop applications like &lt;a href="https://www.virtualbox.org/" target="_blank" rel="noreferrer"&gt;Virtualbox&lt;/a&gt;, to using &lt;a href="https://virt-manager.org/" target="_blank" rel="noreferrer"&gt;virt-manager&lt;/a&gt; with &lt;a href="https://libvirt.org/" target="_blank" rel="noreferrer"&gt;libvirt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I joined Canonical back in March 2021, I&amp;rsquo;d hardly used &lt;a href="https://canonical.com/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt;, and I hadn&amp;rsquo;t ever used &lt;a href="https://multipass.run" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt;. Since then, they&amp;rsquo;ve both become indispensable parts of my workflow, so I thought I&amp;rsquo;d share why I like them, and how I use each of them in my day to day work.&lt;/p&gt;
&lt;p&gt;I work for Canonical, and am therefore invested in the success of their products, but at the time of writing I&amp;rsquo;m not responsible for either LXD or Multipass, and this post represents my honest opinions as a user of the products, and nothing more.&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_ac2017cf2c80d4cb.webp 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_443f9e818d7af594.webp 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_b3508bb5f485f9b9.webp 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_fd05a169716c075c.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1482"
height="1228"
class="mx-auto my-0 rounded-md"
alt="lxd ui showing multiple vms and containers"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_d1f4c9075b739493.png" srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_dc6b3729927da789.png 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_d1f4c9075b739493.png 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_f89640c0ea781c0f.png 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/01_hu_9dbb5a7a3c150bd8.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="installation--distribution" class="relative group"&gt;Installation / Distribution &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#installation--distribution" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Both &lt;a href="https://snapcraft.io/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt; and &lt;a href="https://snapcraft.io/multipass" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt; are available as &lt;a href="https://snapcraft.io" target="_blank" rel="noreferrer"&gt;snap packages&lt;/a&gt;, and that&amp;rsquo;s the most supported and recommended route for installation. LXD is available in the repos of a few other Linux distributions (including &lt;a href="https://search.nixos.org/options?channel=24.05&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages&amp;amp;query=virtualisation.lxd." target="_blank" rel="noreferrer"&gt;NixOS&lt;/a&gt;, &lt;a href="https://wiki.archlinux.org/title/LXD" target="_blank" rel="noreferrer"&gt;Arch Linux&lt;/a&gt;), but the snap package also works great on Arch, Fedora, etc. I personally ran Multipass and LXD as &lt;a href="https://wiki.archlinux.org/title/Snap" target="_blank" rel="noreferrer"&gt;snaps on Arch Linux&lt;/a&gt; for a couple of years without issue.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d like to follow along with the commands in this post, you can get setup like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install lxd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo lxd init --minimal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If you&amp;#39;d like to use LXD/LXC commands without sudo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# run the following command and logout/login:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# sudo usermod -aG lxd $USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install multipass
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Early on in my journey with NixOS, I &lt;a href="https://github.com/NixOS/nixpkgs/pull/214193" target="_blank" rel="noreferrer"&gt;packaged&lt;/a&gt; Multipass for Nix. I still maintain (and use!) the NixOS module. This was my first ever contribution to NixOS &amp;ndash; a fairly colourful review process to say the least&amp;hellip;&lt;/p&gt;
&lt;p&gt;The result is that you can use something like the following in your configuration, and have multipass be available to you after a &lt;code&gt;nixos-rebuild switch&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;virtualisation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multipass&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;LXD has been maintained in NixOS for many years now - and around this time last year I &lt;a href="https://github.com/NixOS/nixpkgs/pull/241314" target="_blank" rel="noreferrer"&gt;added support&lt;/a&gt; for the LXD UI. The screenshots you see throughout this post are all from LXD UI running on a NixOS machine using the following configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;virtualisation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lxd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zfsSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;networking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;trustedInterfaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;lxdbr0&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="ubuntu-on-demand-with-multipass" class="relative group"&gt;Ubuntu on-demand with Multipass &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#ubuntu-on-demand-with-multipass" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href="https://multipass.run/" target="_blank" rel="noreferrer"&gt;Multipass&lt;/a&gt; is designed to provide simple on-demand access to Ubuntu VMs from any workstation - whether that workstation is running Linux, macOS or Windows. It is designed to replicate, in a lightweight way, the experience of provisioning a simple Ubuntu VM on a cloud.&lt;/p&gt;
&lt;p&gt;Multipass makes use of whichever the most appropriate hypervisor is on a given platform. On Linux, it can use QEMU, LXD or libvirt as backends, on Windows it can use Hyper-V or Virtualbox, and on macOS it can use QEMU or Virtualbox. Multipass refers to these backends as &lt;a href="https://multipass.run/docs/driver" target="_blank" rel="noreferrer"&gt;drivers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Multipass&amp;rsquo; scope is relatively limited, but in my opinion that&amp;rsquo;s what makes it so delightful to use. Once installed, the basic operation of Multipass couldn&amp;rsquo;t be simpler:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ multipass shell
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Launched: primary
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Mounted &lt;span class="s1"&gt;&amp;#39;/home/jon&amp;#39;&lt;/span&gt; into &lt;span class="s1"&gt;&amp;#39;primary:Home&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Welcome to Ubuntu 24.04 LTS &lt;span class="o"&gt;(&lt;/span&gt;GNU/Linux 6.8.0-35-generic x86_64&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * Documentation: https://help.ubuntu.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * Management: https://landscape.canonical.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * Support: https://ubuntu.com/pro
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; System information as of Tue Jun &lt;span class="m"&gt;25&lt;/span&gt; 11:17:55 BST &lt;span class="m"&gt;2024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; System load: 0.4 Processes: &lt;span class="m"&gt;132&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Usage of /: 38.9% of 3.80GB Users logged in: &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Memory usage: 31% IPv4 address &lt;span class="k"&gt;for&lt;/span&gt; ens3: 10.93.253.20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Swap usage: 0%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Expanded Security Maintenance &lt;span class="k"&gt;for&lt;/span&gt; Applications is not enabled.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;3&lt;/span&gt; updates can be applied immediately.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;1&lt;/span&gt; of these updates is a standard security update.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To see these additional updates run: apt list --upgradable
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enable ESM Apps to receive additional future security updates.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;See https://ubuntu.com/esm or run: sudo pro status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ubuntu@primary:~$
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This one command will take care of creating the &lt;code&gt;primary&lt;/code&gt; instance if it doesn&amp;rsquo;t already exist, start the instance and drop you into a &lt;code&gt;bash&lt;/code&gt; shell - normally in under a minute.&lt;/p&gt;
&lt;p&gt;Multipass has a neat trick: it bundles a reverse SSHFS server that enables easy mounting of the host&amp;rsquo;s home directory into the VM. This happens by default for the &lt;code&gt;primary&lt;/code&gt; instance. As a result the instance I created above has my home directory mounted at &lt;code&gt;/home/ubuntu/Home&lt;/code&gt; - making it trivial to jump between editing code/files on my host and in the VM. I find this really useful - I can edit files on my workstation in my own editor, using my Yubikey to sign and push commits without having to worry about complicated provisioning or passthrough to the VM, and any files resulting from a build process on my workstation are instantly available in the VM for testing.&lt;/p&gt;
&lt;p&gt;Multipass instances can be customised a little. You won&amp;rsquo;t find complicated features like PCI-passthrough, but basic parameters can be tweaked. The commands I usually run for setting up a development machine when I&amp;rsquo;m working on Juju/Charms are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create a machine named &amp;#39;dev&amp;#39; with 16 cores, 40GiB RAM and 100GiB disk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass launch noble -n dev -c &lt;span class="m"&gt;16&lt;/span&gt; -m 40G -d 100G
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Mount my home directory into the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass mount /home/jon dev:/home/ubuntu/Home
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell in the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass shell dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Once you&amp;rsquo;re done with an instance, you can remove it like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass remove dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass purge
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Multipass does have some more interesting features, though most of my usage is represented above. One feature that might be of more interest for MacOS or Windows users is &lt;a href="https://multipass.run/docs/using-aliases" target="_blank" rel="noreferrer"&gt;aliases&lt;/a&gt;. This feature enables you to alias local commands to their counterparts in a Multipass VM, meaning for example that every time you run &lt;code&gt;docker&lt;/code&gt; on your Mac, the command is actually executed inside the Multipass VM:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example of mapping the local `mdocker` command -&amp;gt; `docker` in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# the multipass VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;multipass &lt;span class="nb"&gt;alias&lt;/span&gt; dev:docker mdocker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Multipass will launch the latest Ubuntu LTS by default, but there are a number of other images available - including some &amp;ldquo;appliance&amp;rdquo; images for applications like Nextcloud, Mosquitto, etc.&lt;/p&gt;
&lt;p&gt;There is also the concept of &lt;a href="https://multipass.run/docs/blueprint" target="_blank" rel="noreferrer"&gt;Blueprints&lt;/a&gt; which are essentially recipes for virtual machines with a given purpose. These are curated partly by the Multipass team, and partly by the community. A blueprint enables the author to specify cores, memory, disk, cloud-init data, aliases, health checks and more. The recipes themselves are maintained &lt;a href="https://github.com/canonical/multipass-blueprints/tree/main/v1" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;, and you can see the list of available images/blueprints using &lt;code&gt;multipass find&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ multipass find
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Image Aliases Version Description
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;core core16 &lt;span class="m"&gt;20200818&lt;/span&gt; Ubuntu Core &lt;span class="m"&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;core18 &lt;span class="m"&gt;20211124&lt;/span&gt; Ubuntu Core &lt;span class="m"&gt;18&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;core20 &lt;span class="m"&gt;20230119&lt;/span&gt; Ubuntu Core &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;core22 &lt;span class="m"&gt;20230717&lt;/span&gt; Ubuntu Core &lt;span class="m"&gt;22&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;20.04 focal &lt;span class="m"&gt;20240612&lt;/span&gt; Ubuntu 20.04 LTS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;22.04 jammy &lt;span class="m"&gt;20240614&lt;/span&gt; Ubuntu 22.04 LTS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;23.10 mantic &lt;span class="m"&gt;20240619&lt;/span&gt; Ubuntu 23.10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;24.04 noble,lts &lt;span class="m"&gt;20240622&lt;/span&gt; Ubuntu 24.04 LTS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;daily:24.10 oracular,devel &lt;span class="m"&gt;20240622&lt;/span&gt; Ubuntu 24.10
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;appliance:adguard-home &lt;span class="m"&gt;20200812&lt;/span&gt; Ubuntu AdGuard Home Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;appliance:mosquitto &lt;span class="m"&gt;20200812&lt;/span&gt; Ubuntu Mosquitto Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;appliance:nextcloud &lt;span class="m"&gt;20200812&lt;/span&gt; Ubuntu Nextcloud Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;appliance:openhab &lt;span class="m"&gt;20200812&lt;/span&gt; Ubuntu openHAB Home Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;appliance:plexmediaserver &lt;span class="m"&gt;20200812&lt;/span&gt; Ubuntu Plex Media Server Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Blueprint Aliases Version Description
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;anbox-cloud-appliance latest Anbox Cloud Appliance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;charm-dev latest A development and testing environment &lt;span class="k"&gt;for&lt;/span&gt; charmers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker 0.4 A Docker environment with Portainer and related tools
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;jellyfin latest Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;minikube latest minikube is &lt;span class="nb"&gt;local&lt;/span&gt; Kubernetes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ros-noetic 0.1 A development and testing environment &lt;span class="k"&gt;for&lt;/span&gt; ROS Noetic.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ros2-humble 0.1 A development and testing environment &lt;span class="k"&gt;for&lt;/span&gt; ROS &lt;span class="m"&gt;2&lt;/span&gt; Humble.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The team also recently introduced the ability to &lt;a href="https://multipass.run/docs/snapshot" target="_blank" rel="noreferrer"&gt;snapshot&lt;/a&gt; virtual machines, though I must confess I&amp;rsquo;ve not tried it out in anger yet.&lt;/p&gt;
&lt;h2 id="lxd-for-vms" class="relative group"&gt;LXD… for VMs? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lxd-for-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For many people, LXD is a container manager - and indeed for many years it could &amp;ldquo;only&amp;rdquo; manage containers. LXD was built for running &amp;ldquo;system containers&amp;rdquo;, as opposed to &amp;ldquo;application containers&amp;rdquo; like Docker/Podman (or Kubernetes). Running a container with LXD is more similar to to running a container with &lt;code&gt;systemd-nspawn&lt;/code&gt;, but with the added bonus that it can &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/clustering/" target="_blank" rel="noreferrer"&gt;cluster&lt;/a&gt; across machines, &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/authentication/" target="_blank" rel="noreferrer"&gt;authenticate against different identity backends&lt;/a&gt;, and manage more sophisticated &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/explanation/storage/" target="_blank" rel="noreferrer"&gt;storage&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Because LXD manages system containers, each container gets its own &lt;code&gt;systemd&lt;/code&gt;, and behaves more like a &amp;rsquo;lightweight VM&amp;rsquo; sharing the host&amp;rsquo;s kernel. This turns out to be a very interesting property for people who want to get some of the benefits of containerisation (i.e. higher workload density, easier snapshotting, migration, etc.) with more legacy applications that might struggle to run effectively in application containers.&lt;/p&gt;
&lt;p&gt;But this post is about virtual machines. Since the 4.0 LTS release, LXD has also supported running VMs with &lt;code&gt;qemu&lt;/code&gt;. The API for launching a container is identical to launching a virtual machine. Better still, Canonical provides images for lots of different Linux distributions, and even desktop variants of some images - meaning you can quickly get up and running with a wide range of distributions, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch a Ubuntu 24.04 LTS VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch ubuntu:noble ubuntu --vm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell inside the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc &lt;span class="nb"&gt;exec&lt;/span&gt; ubuntu bash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch a Fedora 40 VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch images:fedora/40 fedora --vm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell inside the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc &lt;span class="nb"&gt;exec&lt;/span&gt; fedora bash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch an Arch Linux VM (doesn&amp;#39;t support secure boot yet)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch images:archlinux arch --vm -c security.secureboot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a shell inside the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc &lt;span class="nb"&gt;exec&lt;/span&gt; arch bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can get a full list of virtual machine images like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc image ls images: --format&lt;span class="o"&gt;=&lt;/span&gt;compact &lt;span class="p"&gt;|&lt;/span&gt; grep VIRTUAL-MACHINE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="lxd-desktop-vms" class="relative group"&gt;LXD Desktop VMs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lxd-desktop-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Another neat trick for LXD is desktop virtual machines. These are launched with curated images that drop you into a minimal desktop environment that&amp;rsquo;s configured to automatically login. This has to be one of my favourite features of LXD!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch a Ubuntu 24.04 LTS desktop VM and get a console&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch images:ubuntu/24.04/desktop ubuntu --vm --console&lt;span class="o"&gt;=&lt;/span&gt;vga
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_8603299554659120.webp 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_cd8f409b5cd6a090.webp 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_2b99d6ccc9b2e8cf.webp 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_3046a3ace3f05d97.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1329"
height="1053"
class="mx-auto my-0 rounded-md"
alt="gnome desktop from ubuntu 24.04 lts running in spice viewer"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_96b6de961228d3be.png" srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_2030a0ace072ea60.png 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_96b6de961228d3be.png 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_a851dd3d61af648c.png 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/02_hu_4082323e1227c560.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The guest is pre-configured to work correctly with SPICE, so that means clipboard integration, automatic resizing with the viewer window, USB redirection, etc. The same also works for other distros, as before:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch an Arch desktop VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch images:archlinux/desktop-gnome arch --vm &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c limits.cpu&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c limits.memory&lt;span class="o"&gt;=&lt;/span&gt;16GiB &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -c security.secureboot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Get a console using a separate command (if preferred!)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc console --type&lt;span class="o"&gt;=&lt;/span&gt;vga arch
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="lxd-ui-" class="relative group"&gt;LXD UI 😍 &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lxd-ui-" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Back in June 2023, Canonical announced early access to the LXD graphical user interface &lt;a href="https://ubuntu.com/blog/lxd_ui" target="_blank" rel="noreferrer"&gt;on their blog&lt;/a&gt;. The LXD UI is now generally available and enabled by default from LXD 5.21 onwards - though you can find instructions for enabling it on earlier versions in the &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/howto/access_ui/" target="_blank" rel="noreferrer"&gt;docs&lt;/a&gt;. The summary is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc config &lt;span class="nb"&gt;set&lt;/span&gt; core.https_address :8443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap &lt;span class="nb"&gt;set&lt;/span&gt; lxd ui.enable&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reload snap.lxd.daemon
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In my opinion, the LXD UI is one of the best, if not &lt;em&gt;the best&lt;/em&gt; way to interact with a hypervisor yet. Being a full-stack web application, it gains independence from different GUI toolkits on Linux and, provided the cluster is remote, can be accessed the same way from Windows, Mac and Linux.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve used other hypervisors with web UIs, particularly Proxmox, and I&amp;rsquo;ve found the experience with LXD UI to be very smooth, even from the early days. The UI can walk you through the creation and management of VMs, containers, storage and networking. The UI can also give you a nice concise summary of each instance (below is the summary of the VM created using the command in the last section):&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_4108535946a3ab6c.webp 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_c7c9035457f7a91f.webp 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_9eb32f970711e1e.webp 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_c342df0b23541960.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1622"
height="1273"
class="mx-auto my-0 rounded-md"
alt="lxd ui showing a virtual machine instance summary"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_93da003b5711f596.png" srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_49034627176ef198.png 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_93da003b5711f596.png 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_39241ec8979da25f.png 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/03_hu_5d19c34fb51d36dd.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One of my favourite features is the web-based SPICE console for desktop VMs, which combined with the management features makes it trivial to stand up a desktop VM and start testing:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_5c51dcd053016602.webp 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_86621a60933e6ff1.webp 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_f027f814146e4f12.webp 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_fd0a904398b17f9a.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1622"
height="1273"
class="mx-auto my-0 rounded-md"
alt="lxd ui showing a web-based spice console with a gnome desktop running on arch linux inside"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_58e6d8ef56bc8dd1.png" srcset="https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_135013a8349dbd57.png 330w,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_58e6d8ef56bc8dd1.png 660w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_8d768e9c0a20a841.png 1024w
,https://jnsgr.uk/2024/06/desktop-vms-lxd-multipass/04_hu_d72fa6b816d14c2a.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="why-both" class="relative group"&gt;Why both? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#why-both" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;By now you&amp;rsquo;ve probably realised that LXD can do everything Multipass can do, and give much more flexibility - and that&amp;rsquo;s true. LXD is a full-featured hypervisor which supports much more sophisticated networking, &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/reference/devices/#devices" target="_blank" rel="noreferrer"&gt;PCI-passthrough&lt;/a&gt;, clustering, integration with enterprise identity providers, observability through Prometheus &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/metrics/" target="_blank" rel="noreferrer"&gt;metrics&lt;/a&gt; and &lt;a href="https://documentation.ubuntu.com/lxd/en/latest/howto/logs_loki/" target="_blank" rel="noreferrer"&gt;Loki log-forwarding&lt;/a&gt;, etc.&lt;/p&gt;
&lt;p&gt;Multipass is small, lean and very easy to configure. If I just want a quick command-line only Ubuntu VM to play with, I still find &lt;code&gt;multipass shell&lt;/code&gt; to be most convenient - especially with the automatic home directory mounting.&lt;/p&gt;
&lt;p&gt;When I want to work with desktop VMs, interact with non-Ubuntu distributions, or work more closely with hardware, then I use LXD. I was already a bit of a closet LXD fan, having previously described it as a bit of a &amp;ldquo;secret weapon&amp;rdquo; for Canonical, but since the introduction of the LXD UI, I&amp;rsquo;m a fully paid up member of the LXD fan club 😉&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As I mentioned in the opening paragraphs - both LXD and Multipass have become central to a lot of my technical workflows. The reason I packaged Multipass for NixOS, was that I wanted to dive into daily-driving NixOS, but not without Multipass! In my opinion, the LXD UI is one of the most polished experiences for managing containers and VMs on Linux, and I&amp;rsquo;m really excited for what that team cooks up next.&lt;/p&gt;</description></item><item><title>Tracking Releases &amp; CI Across Software Teams and Forges</title><link>https://jnsgr.uk/2024/05/tracking-software-across-teams/</link><pubDate>Wed, 22 May 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/05/tracking-software-across-teams/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In my day job at Canonical, I lead the teams developing &lt;a href="https://juju.is" target="_blank" rel="noreferrer"&gt;Juju&lt;/a&gt;, and a whole host of &lt;a href="https://charmhub.io" target="_blank" rel="noreferrer"&gt;charms&lt;/a&gt;. Charms are software packages used for deploying applications on any infrastructure you have available. The packages are portable, meaning you can use our PostgreSQL operator on AWS, on Azure, on Openstack, on Google Cloud, etc. We&amp;rsquo;re building up quite the portfolio of popular open source applications across data engineering, observability, identity, telco, MLOps and more. I won&amp;rsquo;t go into detail about Juju or charms in this post, but I likely will in a future post.&lt;/p&gt;
&lt;p&gt;The important thing for this post is that I look after &amp;gt;10 software teams, who use two different software forges (Github and Launchpad), and all push artifacts into both the &lt;a href="https://snapcraft.io" target="_blank" rel="noreferrer"&gt;Snap Store&lt;/a&gt; and the &lt;a href="https://charmhub.io" target="_blank" rel="noreferrer"&gt;Charmhub&lt;/a&gt;. I wanted a way to keep track of their releases, and provide a tool that my managers could use to do the same. Put simply, I wanted a unified view of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The latest Github releases&lt;/li&gt;
&lt;li&gt;The latest Launchpad tags&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snapcraft.io/docs/channels" target="_blank" rel="noreferrer"&gt;Channels&lt;/a&gt; in the &lt;a href="https://snapcraft.io" target="_blank" rel="noreferrer"&gt;Snap Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snapcraft.io/docs/channels" target="_blank" rel="noreferrer"&gt;Channels&lt;/a&gt; in &lt;a href="https://charmhub.io" target="_blank" rel="noreferrer"&gt;Charmhub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CI status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In particular, I wanted an easy way to be able to tie releases/commits in a forge to specific revisions in our various stores. This would enable us to troubleshoot more easily when issues are reported in customer environments by making it easier to jump to the code for the specific revision they&amp;rsquo;re running.&lt;/p&gt;
&lt;p&gt;At the time I started working on this, the effort at Canonical to build the portfolio of charms was really ramping up - and the only way of tracking which team owned which charm (between product teams, our IS department and our datacentre team) was a giant spreadsheet which was perpetually out of date (obviously!). The one thing that could be relied upon was which team owned a repo on Github or Launchpad - so my hope was to also reliably answer the question &amp;ldquo;which team owns the &lt;code&gt;foo&lt;/code&gt; charm?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You can see the result of this effort at &lt;a href="https://releases.juju.is" target="_blank" rel="noreferrer"&gt;https://releases.juju.is&lt;/a&gt;, and a sneak peek below:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_8ec233b885900655.webp 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_5063b8f1fab85f9c.webp 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_9ac040c8220b2ee0.webp 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_5d3375656c16ccf6.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1433"
height="1208"
class="mx-auto my-0 rounded-md"
alt="preview of releases.juju.is"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_642e9dc9a1155c8.png" srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_616246e11e81436.png 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_642e9dc9a1155c8.png 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_ad0d13e729726686.png 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/01_hu_d6b0b1a7471e3135.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="prior-art" class="relative group"&gt;Prior Art &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#prior-art" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I wasn&amp;rsquo;t the first to have these problems. The idea to start tracking information in this way came from the &lt;a href="https://releases.elementary.io/" target="_blank" rel="noreferrer"&gt;elementaryOS releases tracker&lt;/a&gt;. This was a tool they built for keeping track of their various repositories, and in particular those which had seen many commits since the last release. The code for the site is available &lt;a href="https://github.com/elementary/releases" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The elementaryOS tracker uses a &lt;a href="https://github.com/elementary/releases/blob/main/release.py" target="_blank" rel="noreferrer"&gt;Python script&lt;/a&gt; to scrape the Github API, which outputs a &lt;a href="https://github.com/elementary/releases/blob/main/_data/repos.json" target="_blank" rel="noreferrer"&gt;JSON representation&lt;/a&gt; of the state of their repositories. This is then parsed during the build of a &lt;a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll" target="_blank" rel="noreferrer"&gt;Jekyll site&lt;/a&gt;, which is published on Github Pages using &lt;a href="https://github.com/elementary/releases/blob/main/.github/workflows/build.yml" target="_blank" rel="noreferrer"&gt;a Github Workflow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This actually got me very close, and in fact my first attempt was a trivial fork of this project with a few slight modifications to the Python script. However, their tool wasn&amp;rsquo;t designed to be used across multiple distinct teams, and there are some limitations such as not rendering Markdown in the release notes.&lt;/p&gt;
&lt;p&gt;I also wanted to make some stylistic changes. I&amp;rsquo;m not particularly familiar with &lt;a href="https://jekyllrb.com/" target="_blank" rel="noreferrer"&gt;Jekyll&lt;/a&gt; and have generally used &lt;a href="https://gohugo.io" target="_blank" rel="noreferrer"&gt;Hugo&lt;/a&gt; for such tasks. I did maintain a Jekyll version for some months, but my lack of (recent) familiarity with the Ruby &amp;amp; gems ecosystem was making updates and maintenance more tedious than I liked.&lt;/p&gt;
&lt;p&gt;Another tool I came across when I was looking to solve this was &lt;a href="https://git.sr.ht/~amolith/willow" target="_blank" rel="noreferrer"&gt;willow&lt;/a&gt; by &lt;a href="https://secluded.site/" target="_blank" rel="noreferrer"&gt;Amolith&lt;/a&gt;. According to the README:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Willow helps developers, sysadmins, and homelabbers keep up with software releases across arbitrary forge platforms, including full-featured forges like GitHub, GitLab, or Forgejo as well as more minimal options like cgit or stagit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is super close - but it doesn&amp;rsquo;t have the snap/charm tracking I sought, and probably won&amp;rsquo;t do given the scope of the project.&lt;/p&gt;
&lt;h2 id="releasegen" class="relative group"&gt;&lt;code&gt;releasegen&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#releasegen" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As I started to modify the Python script used in the elementaryOS release tracker, it started to get quite large, and I became a little uncomfortable with the state of it. At the time I was reintroducing myself into Go after a couple of years out, and decided that I would essentially &amp;ldquo;start again&amp;rdquo; and write a tool for my release tracker from scratch.&lt;/p&gt;
&lt;p&gt;What I came up with is &lt;a href="https://github.com/jnsgruk/releasegen" target="_blank" rel="noreferrer"&gt;&lt;code&gt;releasegen&lt;/code&gt;&lt;/a&gt;. A terribly boring and unimaginative name that I originally intended to change, but never got around to! Nevertheless, &lt;code&gt;releasegen&lt;/code&gt; solved a number of problems for me even in its first release. The first important change was support for separating releases across multiple Github organisations and teams, through the use of a simple config format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Frontend&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-corp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;frontend-bots&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ignores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;some-old-project&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Backend&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-corp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;backend-engineers&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Packaging&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;launchpad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;project-groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;acme-corp-debs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can see the config file used to generated &lt;code&gt;releases.juju.is&lt;/code&gt; &lt;a href="https://github.com/canonical/charm-eng-releases/blob/main/releasegen.yaml" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The output format is heavily inspired by that of the original Python script, but has been enriched with a few more fields over time.&lt;/p&gt;
&lt;p&gt;This tool is really quite simple: it takes a config file which points it at a combination of Github Orgs/Teams and Launchpad project groups, and outputs a big JSON file containing details of releases and associated packages. You can see an example of the output &lt;a href="https://github.com/canonical/charm-eng-releases/blob/main/data/repos.json" target="_blank" rel="noreferrer"&gt;in the &lt;code&gt;charm-eng-releases&lt;/code&gt; repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One of the main reasons I chose Go in the first place was because of it&amp;rsquo;s great support for concurrency. Data from Github is gathered using the &lt;a href="https://github.com/google/go-github" target="_blank" rel="noreferrer"&gt;&lt;code&gt;google/go-github&lt;/code&gt;&lt;/a&gt; package, which provides an interface to the Github API. When I first started adding repos to the original Python version, the run time quickly grew to 10+ minutes. My intention was to spin up a goroutine per repository in &lt;code&gt;releasegen&lt;/code&gt;, but I quickly ran into secondary rate-limits. It turns out that the Github API prohibits you from making &lt;a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#about-secondary-rate-limits" target="_blank" rel="noreferrer"&gt;too many concurrent requests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For Launchpad, things are more complicated. There is &lt;a href="https://help.launchpad.net/API/Hacking" target="_blank" rel="noreferrer"&gt;an API&lt;/a&gt; but it lacks methods for grabbing tags, specific commits etc. I also wanted to avoid cloning all of the repos for which information is gathered. The Launchpad code browsing UI is based on &lt;a href="https://git.zx2c4.com/cgit/about/" target="_blank" rel="noreferrer"&gt;cgit&lt;/a&gt;, and hasn&amp;rsquo;t fundamentally changed in a &lt;strong&gt;long&lt;/strong&gt; time. For now, releasegen relies upon scraping the Launchpad web pages (using &lt;a href="https://github.com/PuerkitoBio/goquery" target="_blank" rel="noreferrer"&gt;goquery&lt;/a&gt;) to get the information it needs. This is not ideal, but has been functioning better than you might expect for around 18 months. There&amp;rsquo;s also no limit on the requests that can be made to the web frontend of Launchpad - so processing is able to be done concurrently across repos in this case.&lt;/p&gt;
&lt;p&gt;In more recent times, &lt;code&gt;releasegen&lt;/code&gt; grew the ability to read badges out of project READMEs, and use those badges to link repos to a particular store (either the Snap store or the Charmhub). Support for reading badges and parsing Github CI badges was &lt;a href="https://github.com/jnsgruk/releasegen/pull/1" target="_blank" rel="noreferrer"&gt;kindly contributed by one of my colleagues&lt;/a&gt;. I have subsequently generalised that initial implementation and added support for the Snap store too.&lt;/p&gt;
&lt;h2 id="parsing-releasegen-with-hugo" class="relative group"&gt;Parsing &lt;code&gt;releasegen&lt;/code&gt; with Hugo &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#parsing-releasegen-with-hugo" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Pleasingly, Hugo has &lt;a href="https://gohugo.io/templates/data-templates/#the-data-directory" target="_blank" rel="noreferrer"&gt;the ability&lt;/a&gt; to read data from YAML, JSON, XML, or TOML files, which can then be rendered throughout the site.&lt;/p&gt;
&lt;p&gt;Given that this tool was to be used for tracking releases from Canonical teams, I created a basic &lt;a href="https://gohugo.io/methods/page/layout/" target="_blank" rel="noreferrer"&gt;layout&lt;/a&gt; using the excellent &lt;a href="https://vanillaframework.io/" target="_blank" rel="noreferrer"&gt;Vanilla Framework&lt;/a&gt;. A layout in Hugo is nothing more than a collection of HTML/CSS/JS into which data can be rendered. The entry point for that in my project is this &lt;a href="https://github.com/canonical/charm-eng-releases/blob/main/layouts/index.html" target="_blank" rel="noreferrer"&gt;index.html&lt;/a&gt;. Here I layout the basic structure of the page, and use a number of &lt;a href="https://gohugo.io/templates/partials/" target="_blank" rel="noreferrer"&gt;partials&lt;/a&gt; to render common components across the site. There are &lt;a href="https://github.com/canonical/charm-eng-releases/blob/85fb452086419a89ccba40dcbb2e811803e4e8e2/layouts/partials/head.html#L23-L90" target="_blank" rel="noreferrer"&gt;very few adjustments&lt;/a&gt; to the standard Vanilla Framework style, and a couple of &lt;a href="https://github.com/canonical/charm-eng-releases/tree/85fb452086419a89ccba40dcbb2e811803e4e8e2/layouts/partials/js" target="_blank" rel="noreferrer"&gt;small Javascript files&lt;/a&gt; to provide tabs, modals, expanding table rows, table sorting and suchlike.&lt;/p&gt;
&lt;p&gt;Aside from the big screenshot at the start of this post, I want to highlight a couple of details in the UI. Firstly, if there is an associated artifact for a given repository (a Snap or a Charm), the row can be expanded to show details of that artifact:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_c41b3e0d03f6a18e.webp 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_f1dd123fbb2f9d2.webp 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_46498fc581f6e61a.webp 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_d38212cfcbcbd772.webp 1259w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1259"
height="263"
class="mx-auto my-0 rounded-md"
alt="expanding table row - snap"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_85b93f4980a0ebdf.png" srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_29f757e8f7822a6b.png 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_85b93f4980a0ebdf.png 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/02_hu_3fcae7f9641442ab.png 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/02.png 1259w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_b0329c594fcd0618.webp 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_8de96637cfd3f247.webp 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_1ce173416e17e4a4.webp 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_9da207ef9ac534da.webp 1242w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1242"
height="314"
class="mx-auto my-0 rounded-md"
alt="expanding table row - charm"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_3183affabd62bda0.png" srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_4688fb9ace7ae291.png 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_3183affabd62bda0.png 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/03_hu_7aa1a7ac2b613206.png 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/03.png 1242w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Each individual release can also be expanded using the &amp;ldquo;eye&amp;rdquo; icon, at which point a modal will be displayed containing the release notes, complete with links:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_9d68e77cc86da46a.webp 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_1b546c99549996f5.webp 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_d5ff8000a49e06ba.webp 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_dd7e38a22fb676a4.webp 1252w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1252"
height="938"
class="mx-auto my-0 rounded-md"
alt="release modal"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_fb4c258dc8add747.png" srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_2dd050dc49333aa5.png 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_fb4c258dc8add747.png 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/04_hu_7e2efabe0fe56726.png 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/04.png 1252w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is also some visual distinction between those repos on Github, and those on Launchpad:&lt;/p&gt;
&lt;p&gt;&lt;a href="05.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/05_hu_be2e5eadb6dfdad.webp"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="593"
height="257"
class="mx-auto my-0 rounded-md"
alt="forge icons"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/05.png"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="publishing-with-github-actions" class="relative group"&gt;Publishing with Github Actions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#publishing-with-github-actions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The site is hosted on Github Pages, and if you&amp;rsquo;ve been following along, you&amp;rsquo;ll notice that the site would need to be &amp;ldquo;regenerated&amp;rdquo; for the information to remain fresh - Hugo is ultimately a &lt;em&gt;static site generator&lt;/em&gt;. This problem is solved in my case with Github Actions. I created a workflow which is triggered every hour to dump the latest report using &lt;code&gt;releasegen&lt;/code&gt;, regenerate the Hugo site, and commit the outcome to branch of the repo which is used to serve the page. The workflow itself is pretty simple, and can be seen &lt;a href="https://github.com/canonical/charm-eng-releases/blob/main/.github/workflows/build.yml" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="06.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_cf3ea7b874b1af79.webp 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_49839baec41b63fb.webp 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_d1ee8d9fb0d4f989.webp 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_5444ded37d4e6bcb.webp 1118w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1118"
height="960"
class="mx-auto my-0 rounded-md"
alt="github actions deployment workflow"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_f28192cc3679aa46.png" srcset="https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_a730a09504d5cd8d.png 330w,https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_f28192cc3679aa46.png 660w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/06_hu_4d0986e56324c504.png 1024w
,https://jnsgr.uk/2024/05/tracking-software-across-teams/06.png 1118w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This tool scratched an itch for me. It&amp;rsquo;s a little flawed in places: it doesn&amp;rsquo;t update in real time, &lt;code&gt;releasegen&lt;/code&gt; could do with some &lt;del&gt;more&lt;/del&gt; tests, and I&amp;rsquo;m not super proud of how it parses data about Launchpad projects. That said, it&amp;rsquo;s been fairly reliable over the past 18 months, and it does present information in a pretty consistent way (though I&amp;rsquo;m no designer!). I personally think it&amp;rsquo;s a good example of how things can be automated and simplified by combining existing tools in a short space of time.&lt;/p&gt;
&lt;p&gt;I have an idea about how to generalise the processing of data, which would both remove my reliance on the Github API, and also unify the approach for gathering information about Git repos across forges (making it easier to support the likes of Codeberg and Sourcehut). I read an interesting blog recently about &lt;a href="https://mediocregopher.com/posts/git-proxy" target="_blank" rel="noreferrer"&gt;Serving a Website from a Git Repo Without Cloning It&lt;/a&gt; which implies that a lot of the information I require such as commits, tags, etc. could be gleaned directly from the git endpoint over HTTP, but I haven&amp;rsquo;t yet looked in detail.&lt;/p&gt;
&lt;p&gt;It &lt;em&gt;did&lt;/em&gt; solve the problem of keeping a big spreadsheet up to date as a means of tracking project ownership, and it&amp;rsquo;s certainly proved a useful tool for me to understand the relationship between commits, Github Releases and released revisions in our stores across teams. Some of my managers/seniors have found it really valuable, others not so much - this is generally reflected by how complete the information is for each team&amp;rsquo;s repos. Over the past few months, I&amp;rsquo;ve had a couple of teams reach out to me and ask to be added despite them not being part of my org, because they see it as a useful tool, so there&amp;rsquo;s that!&lt;/p&gt;
&lt;p&gt;This tooling was also adopted by the &lt;a href="https://snapcrafters.org" target="_blank" rel="noreferrer"&gt;Snapcrafters&lt;/a&gt; who run their own version of the &lt;a href="https://snapcrafters.org/snap-packages/" target="_blank" rel="noreferrer"&gt;dashboard&lt;/a&gt;. There are some subtle differences here - the UI in this case also displays each snap&amp;rsquo;s base (e.g. &lt;code&gt;core18&lt;/code&gt;, &lt;code&gt;core22&lt;/code&gt;, etc.). You can see the source code for that &lt;a href="https://github.com/snapcrafters/snapcrafters.org" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all for now! If you&amp;rsquo;ve built something similar or you think I&amp;rsquo;ve missed a trick, let me know!&lt;/p&gt;</description></item><item><title>Secure Boot &amp; TPM-backed Full Disk Encryption on NixOS</title><link>https://jnsgr.uk/2024/04/nixos-secure-boot-tpm-fde/</link><pubDate>Sat, 20 Apr 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/04/nixos-secure-boot-tpm-fde/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Since I wrote this post, I have been made aware of a particular situation where, at the time I write this (2025-01-17), the steps described in this article will result in a setup that is still (in many cases) vulnerable to an attack where the attacker has physical access to the machine. This may be acceptable in your threat model, but I&amp;rsquo;d encourage you to read the &lt;a href="https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/" target="_blank" rel="noreferrer"&gt;excellent article&lt;/a&gt; to gain a full understanding of the issue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For the last decade (whoa&amp;hellip;) or so, I&amp;rsquo;ve defaulted to using LUKS-encrypted drives for my machines. In general, I configure an unencrypted boot/EFI partition, then place either an ext4 or btrfs filesystem inside a LUKS container which is used for the root partition.&lt;/p&gt;
&lt;p&gt;Some of my machines also have extra disks: my desktop has a 1TB NVMe drive for root, and a 2TB NVMe &amp;ldquo;data&amp;rdquo; drive mounted in my home directory under &lt;code&gt;/home/jon/data&lt;/code&gt;. I don&amp;rsquo;t like having to type two different encryption passphrases at boot, so I usually have the extra disk automatically unlocked by putting the key in a file on the root drive, and placing an entry in &lt;a href="https://www.man7.org/linux/man-pages/man5/crypttab.5.html" target="_blank" rel="noreferrer"&gt;&lt;code&gt;/etc/crypttab&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This setup works fine for desktop machines, but it&amp;rsquo;s cumbersome on headless machines because unattended reboots require the disk passphrase to be entered at boot. Even then, all of my computers are exclusively used by me, and this setup means I have to enter two passwords on every boot to get to a working desktop environment (one for the disk, and one for the login manager).&lt;/p&gt;
&lt;p&gt;I solved this recently with a combination of Secure Boot and a Trusted Platform Module (TPM), so let&amp;rsquo;s look at those first with a brief and high-level overview of each.&lt;/p&gt;
&lt;h2 id="whats-a-tpm" class="relative group"&gt;What&amp;rsquo;s a TPM? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#whats-a-tpm" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Most machines that have been manufactured in the last decade, and certainly in the last 5 years, contain a cryptographic coprocessor conforming to the Trusted Platform Module (TPM) &lt;a href="https://trustedcomputinggroup.org/resource/tpm-library-specification/" target="_blank" rel="noreferrer"&gt;spec&lt;/a&gt;. The TPM is a dedicated microcontroller primarily used for verifying the integrity of a machine.&lt;/p&gt;
&lt;p&gt;TPMs can be used for storing cryptographic key material and performing basic cryptographic operations. The general premise is that keys can be loaded into the TPM, which enables the TPM to perform cryptographic operations using that key (signing, encrypting, etc.), but the key cannot be recovered or read from the TPM unless certain conditions are met. TPMs also provide other facilities such as secure random number generation, which in turn enables them to securely generate cryptographic keys.&lt;/p&gt;
&lt;p&gt;Verifying system integrity essentially boils down to being able to ensure the machine hasn&amp;rsquo;t been tampered with between boots, and that the boot process itself hasn&amp;rsquo;t been compromised. A given firmware or operating system can take hardware &amp;ldquo;measurements&amp;rdquo; and store those measurements in dedicated slots called Platform Configuration Registers (PCRs). The measurements pertain to the underlying hardware and configuration of the machine. The TPM itself never performs the actual verification of the PCRs, and in fact has no knowledge of whether a measurement is inherently &amp;ldquo;good&amp;rdquo; or &amp;ldquo;bad&amp;rdquo;, but it can provide signed attestations of their values, which are then judged by the application requesting the attestation according to some policy.&lt;/p&gt;
&lt;p&gt;Each PCR contains a hash representing a particular hardware measurement, which can be read at any time, but cannot be overwritten. Rather than allowing a traditional write operation, PCRs are updated through an &amp;ldquo;extend&amp;rdquo; operation which depends on the previous hash value, creating a chain of trust not dissimilar from how a blockchain is formed. This means that a given measurement can never be fully removed from the TPM.&lt;/p&gt;
&lt;p&gt;Once the measurements are stored in the PCRs, there are various times and purposes for which the firmware or an operating system might read them - one example is for &lt;a href="https://www.gradient.tech/faq-items/what-are-platform-configuration-registers-pcrs/" target="_blank" rel="noreferrer"&gt;remote attestation&lt;/a&gt; during login to a system. In this scenario the attestation can be used to verify that the machine hasn&amp;rsquo;t been tampered with (perhaps in an &lt;a href="https://en.wikipedia.org/wiki/Evil_maid_attack" target="_blank" rel="noreferrer"&gt;Evil Maid&lt;/a&gt; attack). One could also store a Certificate Authority signing key in a TPM, and have the Certificate Authority software interface with the TPM to sign certificates using the &lt;a href="https://en.wikipedia.org/wiki/PKCS_11" target="_blank" rel="noreferrer"&gt;PKCS#11 standard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My use-case is to enabling the TPM to provide the passphrase to unlock a LUKS-encrypted disk, which is what I&amp;rsquo;ll focus on in this post.&lt;/p&gt;
&lt;h2 id="secure-boot" class="relative group"&gt;Secure Boot &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#secure-boot" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Secure Boot is the mechanism by which the code executed by a machine&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/UEFI" target="_blank" rel="noreferrer"&gt;Unified Extensible Firmware Interface (UEFI)&lt;/a&gt; can be verified as trusted. In the vast majority of cases, the first thing executed by the UEFI is a bootloader.&lt;/p&gt;
&lt;p&gt;When Secure Boot is enabled, each binary executed by the UEFI must contain a checksum and a signature - which the UEFI verifies before launching the code. In the case that either the checksum or signature do not match, the UEFI will refuse the execute the code, and the boot process will halt.&lt;/p&gt;
&lt;p&gt;Many OEM machines ship with Microsoft Windows installed, and thus ship with the necessary keys to validate signatures created with Microsoft&amp;rsquo;s certificate authority. Linux systems are able to utilise these keys through &lt;a href="https://github.com/rhboot/shim" target="_blank" rel="noreferrer"&gt;&lt;code&gt;shim&lt;/code&gt;&lt;/a&gt; - a small and easily verifiable piece of software which is signed by Microsoft. &lt;code&gt;shim&lt;/code&gt; sits between the UEFI and the bootloader in the boot process, obviating the need for every Linux bootloader to be signed by Microsoft on every release. The shim is designed to &lt;em&gt;extend trust&lt;/em&gt; from the keys trusted by the computer&amp;rsquo;s firmware to a new set of keys controlled by the operating system.&lt;/p&gt;
&lt;p&gt;But what does Secure Boot get us in reality? By signing the kernel, and in some cases a single UEFI PE binary known as a &lt;a href="https://wiki.archlinux.org/title/Unified_kernel_image" target="_blank" rel="noreferrer"&gt;Unified Kernel Image (UKI)&lt;/a&gt; (which contains the bootloader, the kernel, the command-line used to boot the kernel, and &lt;a href="https://uapi-group.org/specifications/specs/unified_kernel_image/#uki-components" target="_blank" rel="noreferrer"&gt;other resources&lt;/a&gt;), one can be reasonably sure that the boot process hasn&amp;rsquo;t been tampered with.&lt;/p&gt;
&lt;p&gt;This process thwarts a number of common physical attack vectors, such as &lt;a href="https://linuxconfig.org/recover-reset-forgotten-linux-root-password" target="_blank" rel="noreferrer"&gt;manipulating the kernel command line to bypass the machine&amp;rsquo;s login&lt;/a&gt; and drop straight to a root shell - and combined with disk encryption can prevent offline data transfer from the machine. It also defends against malware which compromises the operating system&amp;rsquo;s boot process such that it can start before the OS and obfuscate it&amp;rsquo;s presence.&lt;/p&gt;
&lt;h2 id="threat-modelling" class="relative group"&gt;Threat Modelling &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#threat-modelling" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;As with any security measure, Secure Boot is not a silver bullet. You should always consider your own personal threat model, and the sorts of attacks you&amp;rsquo;re looking to defend against.&lt;/p&gt;
&lt;p&gt;For example, Secure Boot can help prevent the sort of malware infections described in the previous section, but if an attacker gets physical access to your machine, and the UEFI isn&amp;rsquo;t adequately protected, they could simply disable secure boot and carry on unhindered.&lt;/p&gt;
&lt;p&gt;I mitigate this by password protecting the UEFI. This isn&amp;rsquo;t perfect, but is likely sufficient protection for my threat model which is more about protecting chancers and petty thieves from gaining access to my information, than from determined attackers who gain physical access to my property.&lt;/p&gt;
&lt;p&gt;Storing keys in a TPM is &lt;em&gt;theoretically&lt;/em&gt; safe, in that each TPM has a unique seed which cannot be retrieved, and enables the TPM to deterministically generate keys between reboots. It&amp;rsquo;s &lt;em&gt;very difficult&lt;/em&gt; to retrieve the seed, and thus &lt;em&gt;very difficult&lt;/em&gt; to duplicate a TPM, but not impossible. Even then, the Linux kernel&amp;rsquo;s communication with the TPM on-the-wire is unencrypted, and the same can be said for many other subsystems which use the TPM. A &lt;a href="https://hackaday.com/2024/02/06/beating-bitlocker-in-43-seconds/" target="_blank" rel="noreferrer"&gt;recent example&lt;/a&gt; of this vulnerability was demonstrated by sniffing a Bitlocker key off the &lt;a href="https://en.wikipedia.org/wiki/Low_Pin_Count" target="_blank" rel="noreferrer"&gt;LPC bus&lt;/a&gt; in a Lenovo X1 Carbon laptop (using a Raspberry Pi Pico, no less). In many modern machines this is mitigated by the TPM being on-CPU, but the point still stands.&lt;/p&gt;
&lt;p&gt;I choose to enroll Microsoft&amp;rsquo;s platform keys, which in theory degrades the security of my device in the case that Microsoft&amp;rsquo;s signing key is compromised, though all of my machines are compatible with &lt;code&gt;fwupd&lt;/code&gt; and can receive updates to the database through that mechanism if required (and in fact have done in the &lt;a href="https://uefi.org/revocationlistfile/archive" target="_blank" rel="noreferrer"&gt;past 18 months&lt;/a&gt;). This could be further mitigated by using custom keys and certificates for the full chain, but this is more overhead for daily operations and updates. It&amp;rsquo;s also worth considering whether you have the resources to fully secure your own chain - especially by comparison to Microsoft who spend tens of millions of dollars per year on security. If an attacker wants your information, and are able to compromise Microsoft&amp;rsquo;s CA, your own CA may not be such a hurdle.&lt;/p&gt;
&lt;p&gt;Security measures are always a trade-off between Confidentiality, Availability and Integrity (CIA). In general, the more rigidly secure boot is implemented and configured, the more you&amp;rsquo;re protecting confidentiality and integrity. The choices I&amp;rsquo;ve made are slightly more in favour of availability, but nonetheless raise the bar for any attacker significantly.&lt;/p&gt;
&lt;h2 id="enabling-secure-boot-on-nixos" class="relative group"&gt;Enabling Secure Boot on NixOS &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enabling-secure-boot-on-nixos" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Now for the fun part! The process for enabling Secure Boot on NixOS has simplified in recent months owing to the creation of &lt;a href="https://github.com/nix-community/lanzaboote" target="_blank" rel="noreferrer"&gt;&lt;code&gt;lanzaboote&lt;/code&gt;&lt;/a&gt; - a project which takes of preparing and signing Unified Kernel Images containing a custom stub, the bootloader, the Linux kernel, the kernel&amp;rsquo;s &lt;code&gt;initrd&lt;/code&gt; and the kernel command line. &lt;code&gt;lanzaboote&lt;/code&gt; also takes care of installing the UKI on the &lt;a href="https://en.wikipedia.org/wiki/EFI_system_partition" target="_blank" rel="noreferrer"&gt;ESP partition&lt;/a&gt; so the UEFI can execute it at boot.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;lanzaboote&lt;/code&gt; stub differs slightly from &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-stub.html" target="_blank" rel="noreferrer"&gt;&lt;code&gt;systemd-stub&lt;/code&gt;&lt;/a&gt;, in that it doesn&amp;rsquo;t require the kernel and initrd to be part of the UKI. This is important for a generation-based operating system like NixOS because bundling the kernel and initrd into a new UKI for every generation would consume a lot of disk space, and quickly exhaust the ESP on most machines. In &lt;code&gt;lanzaboote&lt;/code&gt;&amp;rsquo;s implementation, the kernel and initrd are stored separately on the ESP, and the chain of trust is preserved by validating the signature of the kernel, and embedding a cryptographic hash of the initrd into the signed UKI.&lt;/p&gt;
&lt;p&gt;The project takes advantage of systems that have &lt;a href="https://github.com/NixOS/rfcs/blob/master/rfcs/0125-bootspec.md" target="_blank" rel="noreferrer"&gt;bootspec&lt;/a&gt; enabled, which is a relatively recent NixOS RFC that ensures configured machines maintain a file containing a set of memoised facts about a system&amp;rsquo;s closure. Bootspec aims to &amp;ldquo;provide more uniform feature support&amp;rdquo; to bootloaders in the NixOS ecosystem and &amp;ldquo;enable NixOS users to implement custom bootloader tools and policy&amp;rdquo; - of which &lt;code&gt;lanzaboote&lt;/code&gt; is one.&lt;/p&gt;
&lt;h3 id="generating-secure-boot-keys" class="relative group"&gt;Generating Secure Boot Keys &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#generating-secure-boot-keys" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The first step is to generate some keys for the secure boot process. This can be achieved using the &lt;a href="https://search.nixos.org/packages?channel=unstable&amp;amp;show=sbctl&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages&amp;amp;query=sbctl" target="_blank" rel="noreferrer"&gt;&lt;code&gt;sbctl&lt;/code&gt; package&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo nix run nixpkgs#sbctl create-keys
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Created Owner UUID 6ac34cc3-a23d-9745-ef33-a03f523d20a3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating secure boot keys...✓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Secure boot keys created!
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This should only take a few seconds at maximum, and will result in a set of keys being populated in &lt;code&gt;/etc/secureboot&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ tree /etc/secureboot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/etc/secureboot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── files.db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── GUID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── keys
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── db.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── db.pem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── dbx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── dbx.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── dbx.pem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── KEK
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── KEK.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── KEK.pem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── PK
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── PK.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── PK.pem
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="enable-bootspec" class="relative group"&gt;Enable Bootspec &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enable-bootspec" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Ensure that &lt;code&gt;bootspec&lt;/code&gt; is enabled in your Nix configuration. You can see this in my flake for my desktop machine &lt;a href="https://github.com/jnsgruk/nixos-config/blob/e436e046f19c76fcea0ac2570e7a747153c02ad5/host/kara/boot.nix#L5" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bootspec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="enable-lanzaboote" class="relative group"&gt;Enable &lt;code&gt;lanzaboote&lt;/code&gt; &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enable-lanzaboote" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I use a &lt;a href="https://nixos.wiki/wiki/Flakes" target="_blank" rel="noreferrer"&gt;flake&lt;/a&gt; to configure all of my machines, so I&amp;rsquo;m able to get access to &lt;code&gt;lanzaboote&lt;/code&gt; by adding the upstream flake as &lt;a href="https://github.com/jnsgruk/nixos-config/blob/e436e046f19c76fcea0ac2570e7a747153c02ad5/flake.nix#L25-L26" target="_blank" rel="noreferrer"&gt;an input&lt;/a&gt; to my own &lt;a href="https://github.com/jnsgruk/nixos-config" target="_blank" rel="noreferrer"&gt;nixos-config flake&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lanzaboote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:nix-community/lanzaboote&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Once you&amp;rsquo;ve added &lt;code&gt;lanzaboote&lt;/code&gt; as a dependency, you&amp;rsquo;ll need to import the &lt;code&gt;lanzaboote&lt;/code&gt; module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;lanzaboote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosModules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lanzaboote&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In my flake, I use a custom &lt;a href="https://github.com/jnsgruk/nixos-config/blob/e436e046f19c76fcea0ac2570e7a747153c02ad5/lib/helpers.nix#L39" target="_blank" rel="noreferrer"&gt;helper function&lt;/a&gt; to build NixOS configurations, so the module is &lt;a href="https://github.com/jnsgruk/nixos-config/blob/e436e046f19c76fcea0ac2570e7a747153c02ad5/lib/helpers.nix#L59" target="_blank" rel="noreferrer"&gt;passed directly&lt;/a&gt; to &lt;code&gt;lib.nixosSystem&lt;/code&gt; through the &lt;code&gt;modules&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;lanzaboote&lt;/code&gt; module replaces the &lt;code&gt;systemd-boot&lt;/code&gt; module, and as such you must explicitly &lt;em&gt;disable&lt;/em&gt; &lt;code&gt;systemd-boot&lt;/code&gt; when enabling &lt;code&gt;lanzaboote&lt;/code&gt;. Additionally, if you wish to use the TPM for disk unlock (described in the next section), you must use the systemd initrd hooks (or something like &lt;a href="https://github.com/latchset/clevis/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;clevis&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;boot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;initrd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemd-boot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkForce&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lanzaboote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pkiBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/etc/secureboot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is represented in my config &lt;a href="https://github.com/jnsgruk/nixos-config/blob/e436e046f19c76fcea0ac2570e7a747153c02ad5/host/kara/boot.nix#L6-L10" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once enabled, rebuild your system (in my case with &lt;code&gt;sudo nixos-rebuild switch --flake /home/jon/nixos-config&lt;/code&gt;) and verify that your machine is ready for Secure Boot. Don&amp;rsquo;t panic about the kernel images being reported as not signed, this is expected:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo nix run unstable#sbctl verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Verifying file database and EFI images in /boot...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ /boot/EFI/BOOT/BOOTX64.EFI is signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ /boot/EFI/Linux/nixos-generation-414-376jna572gsb23snqs67t7s4bwxzb3epblmdnzweghuepopml2va.efi is signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ /boot/EFI/Linux/nixos-generation-415-iqulgohymbdppgtxzho6ou3fcuxjbxhumpzm4vojmipwy3sbmuna.efi is signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ /boot/EFI/Linux/nixos-generation-416-kxnzioafnduwwck3oypo7rqwtoat745czp2bpehoufp4yqiawypa.efi is signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✗ /boot/EFI/nixos/kernel-6.8.2-242idodyvf36cpl6s5dskjy6mo4tjhszuwa3hye7qcjyuo5vnehq.efi is not signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✗ /boot/EFI/nixos/kernel-6.8.5-zqulrwsucm6okcyns6v2jhh6fregk3bvsdth3yloqfymfbgnh64a.efi is not signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✗ /boot/EFI/nixos/kernel-6.8.7-6mmixkr6ewywm5swgbi5ethbpgnyia4borzmkevcjx7n7t3mtida.efi is not signed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ /boot/EFI/systemd/systemd-bootx64.efi is signed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="prepare-the-uefi" class="relative group"&gt;Prepare the UEFI &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#prepare-the-uefi" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Reboot your machine and enter the UEFI interface. This part of the process will vary from machine to machine depending on the UEFI implementation, but you&amp;rsquo;re looking to enable Secure Boot, and clear the preloaded Secure Boot keys. This may be referred to as &amp;ldquo;Setup Mode&amp;rdquo;, or erasing the &amp;ldquo;Platform Keys&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;While you&amp;rsquo;re here, I&amp;rsquo;d also advise setting a UEFI password before rebooting back into NixOS.&lt;/p&gt;
&lt;h3 id="enroll-secure-boot-keys" class="relative group"&gt;Enroll Secure Boot Keys &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#enroll-secure-boot-keys" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The final stage in the process is to enroll your newly generated Secure Boot keys from step 1 into the UEFI. This is again achieved with &lt;code&gt;sbctl&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo nix run nixpkgs#sbctl enroll-keys -- --microsoft
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enrolling keys to EFI variables...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;With vendor keys from microsoft...✓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enrolled keys to the EFI variables!
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I chose to use the &lt;code&gt;--microsoft&lt;/code&gt; option to also enroll the UEFI vendor certificates from Microsoft. Some systems contain firmware that is signed and validated when Secure Boot is enabled, and omitting the Microsoft keys could prevent your device from booting - omit this option with caution!&lt;/p&gt;
&lt;h3 id="verify-secure-boot" class="relative group"&gt;Verify Secure Boot &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#verify-secure-boot" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;Once you&amp;rsquo;ve enrolled the keys, reboot the machine back into NixOS and use &lt;code&gt;bootctl&lt;/code&gt; to confirm that Secure Boot is in fact enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ bootctl status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;System:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Firmware: UEFI 2.80 &lt;span class="o"&gt;(&lt;/span&gt;American Megatrends 5.26&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Firmware Arch: x64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Secure Boot: enabled &lt;span class="o"&gt;(&lt;/span&gt;user&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TPM2 Support: yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Boot into FW: supported
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="tpm-unlock-of-root-partition" class="relative group"&gt;TPM Unlock of Root Partition &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#tpm-unlock-of-root-partition" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Now that we&amp;rsquo;re (reasonably) confident that no one can tamper with the boot process, we can progress to allowing the machine to auto-unlock the encrypted disk using a key stored in the TPM.&lt;/p&gt;
&lt;p&gt;This is actually more common than you might think - Windows has enabled this behaviour by default for some time with Bitlocker disk encryption, and Canonical is also &lt;a href="https://ubuntu.com/blog/tpm-backed-full-disk-encryption-is-coming-to-ubuntu" target="_blank" rel="noreferrer"&gt;working on&lt;/a&gt; bringing TPM-backed full disk encryption to Ubuntu.&lt;/p&gt;
&lt;p&gt;This is probably the easiest step of them all! A simple invocation of &lt;code&gt;systemd-cryptenroll&lt;/code&gt; is all that&amp;rsquo;s required. The arguments below instruct the machine that PCRs 0, 2, 7 and 12 should be measured and verified before the TPM is allowed to unlock the disk.&lt;/p&gt;
&lt;p&gt;According to the &lt;a href="https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/" target="_blank" rel="noreferrer"&gt;Linux TPM PCR Regsitry&lt;/a&gt;, this means the following are measured before the LUKS key is presented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PCR 0: Core system firmware executable code&lt;/li&gt;
&lt;li&gt;PCR 2: Extended or pluggable executable code&lt;/li&gt;
&lt;li&gt;PCR 7: SecureBoot state&lt;/li&gt;
&lt;li&gt;PCR 12: Kernel command line, system credentials and system configuration images&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❯ sudo systemd-cryptenroll --tpm2-device&lt;span class="o"&gt;=&lt;/span&gt;auto --tpm2-pcrs&lt;span class="o"&gt;=&lt;/span&gt;0+2+7+12 --wipe-slot&lt;span class="o"&gt;=&lt;/span&gt;tpm2 /dev/nvme0n1p2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s it! The next time you reboot, your disk should be automatically unlocked by the TPM, and your machine should boot straight to your display manager, or the TTY login if no display manager is configured.&lt;/p&gt;
&lt;h2 id="useful-resources" class="relative group"&gt;Useful Resources &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#useful-resources" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;None of the knowledge in this post is novel, but rather the culmination of some knowledge acquired over the past few years, and some more targeted reading more recently. In the process, I learned a bunch from the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nix-community/lanzaboote" target="_blank" rel="noreferrer"&gt;Lanzaboote on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot" target="_blank" rel="noreferrer"&gt;ArchWiki on UEFI Secure Boot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discourse.nixos.org/t/full-disk-encryption-tpm2/29454" target="_blank" rel="noreferrer"&gt;NixOS Discourse Post on TPM Unlock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NixOS/rfcs/blob/master/rfcs/0125-bootspec.md" target="_blank" rel="noreferrer"&gt;Bootspec RFC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fosdem.org/2024/schedule/event/fosdem-2024-1985-ukis-tpms-immutable-initrds-and-full-disk-encryption-what-distributions-should-keep-in-mind-when-hopping-onto-the-system-integrity-train/" target="_blank" rel="noreferrer"&gt;Lennart Poettering&amp;rsquo;s UKI Talk at FOSDEM 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fosdem.org/2024/schedule/event/fosdem-2024-3141-linux-kernel-tpm-security-and-trusted-key-updates/" target="_blank" rel="noreferrer"&gt;James Bottomley&amp;rsquo;s TPM Talk at FOSDEM 2024&lt;/a&gt;
&lt;a href="https://ericchiang.github.io/post/tpm-keys/" target="_blank" rel="noreferrer"&gt;The Trusted Platform Module Key Hierarchy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;About 5 years ago, I was sporting a fully secure-boot enabled Dell XPS 13 running Arch Linux. Back then, the process was complicated, manual, and required a lot of maintenance between upgrades. For me, it was more pain than gain, but an interesting learning experience nonetheless.&lt;/p&gt;
&lt;p&gt;When I sat down earlier this year to enable Secure Boot on NixOS, I&amp;rsquo;d set aside a few hours. I was astounded that 10 minutes later I was finished. I wrote this post as a memo to my future self, but also to illustrate how simple it can be to enable Secure Boot and TPM disk unlock in 2024.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t claim to be an expert on the inner workings of TPMs, nor Secure Boot. The things I can say for certain are that TPMs are complex, that there are improvements that could be made to Linux&amp;rsquo;s interactions with the TPM, and that a determined and well-resourced attacker is likely going to succeed one way or another.&lt;/p&gt;
&lt;p&gt;If you spot an inaccuracy in this post, reach out and let me know on Mastodon, on Telegram, by email, or however you prefer!&lt;/p&gt;
&lt;p&gt;Until next time!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Update 2024/04/29: Thanks to &lt;a href="https://github.com/pimeys/" target="_blank" rel="noreferrer"&gt;@pimeys&lt;/a&gt; for pointing out that one must enable the systemd initrd hooks &lt;code&gt;systemd-cryptenroll&lt;/code&gt; to function correctly, and also that PCR 12 must be measured to prevent the LUKS key from being released if the kernel command line has been modified.&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>Simplifying Test &amp; Release of Snapped GUI Apps</title><link>https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For the past few months, I&amp;rsquo;ve been getting steadily more involved in &lt;a href="https://github.com/snapcrafters" target="_blank" rel="noreferrer"&gt;Snapcrafters&lt;/a&gt;. The Snapcrafters are a community dedicated to the creation and maintenance of ~100 snap packages. They&amp;rsquo;re currently maintaining snaps for applications like &lt;a href="https://snapcraft.io/signal-desktop" target="_blank" rel="noreferrer"&gt;Signal Desktop&lt;/a&gt;, &lt;a href="https://snapcraft.io/discord" target="_blank" rel="noreferrer"&gt;Discord&lt;/a&gt;, &lt;a href="https://snapcraft.io/gimp" target="_blank" rel="noreferrer"&gt;Gimp&lt;/a&gt;, &lt;a href="https://snapcraft.io/terraform" target="_blank" rel="noreferrer"&gt;Terraform&lt;/a&gt; and &lt;a href="https://snapcraft.io/search?q=publisher%3Asnapcrafters" target="_blank" rel="noreferrer"&gt;many more&lt;/a&gt;, some with &lt;em&gt;hundreds of thousands of weekly active users&lt;/em&gt;. As with any community organisation, maintainer participation can ebb and flow over time as people find themselves with competing priorities.&lt;/p&gt;
&lt;p&gt;One of my personal goals for participation in the Snapcrafters org was to help them build more automated, sustainable processes for bumping versions of snaps as the upstreams move forward, and find a more robust way to test GUI applications before they&amp;rsquo;re released to the masses.&lt;/p&gt;
&lt;p&gt;The snap store comes with a surprisingly rich delivery mechanism consisting of &lt;a href="https://snapcraft.io/docs/channels" target="_blank" rel="noreferrer"&gt;tracks, risks and branches&lt;/a&gt;. This means that (among other things) changes to applications can be tested by way of an incremental roll out by those willing to help out - by subscribing to the &lt;code&gt;edge&lt;/code&gt; or &lt;code&gt;candidate&lt;/code&gt; channels.&lt;/p&gt;
&lt;h2 id="the-problem" class="relative group"&gt;The Problem &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-problem" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;While bumping the versions of the applications in snap packages &lt;a href="https://github.com/snapcrafters/signal-desktop/blob/325c06602d6bbb976afbabe48e16c688f1d70c94/.github/workflows/sync-version-with-upstream.yml" target="_blank" rel="noreferrer"&gt;can be done trivially&lt;/a&gt;, testing that the new application can launch on the Linux desktop and function correctly is more difficult - especially given the &amp;ldquo;headless&amp;rdquo; nature of CI systems, and the inherent complexity of some of the applications involved.&lt;/p&gt;
&lt;p&gt;Electron has, in my opinion, been a huge net win for the Linux desktop. Performance and early Wayland compatibility aside, the selection of mainstream applications available to the Linux desktop user is certainly much greater as a result. One downside is that each app is essentially a browser with lots of complex moving parts which can be difficult to maintain for packagers over time - and particularly so for a team of volunteers who may not be experts in the applications they help to maintain.&lt;/p&gt;
&lt;p&gt;In a &lt;a href="https://jnsgr.uk/2024/02/nixos-vms-in-github-actions/" target="_blank" rel="noreferrer"&gt;previous post&lt;/a&gt; I wrote about how KVM-acceleration is now available on Github Actions runners. While it&amp;rsquo;s certainly possible to just pull in various pieces of the Linux desktop using &lt;code&gt;apt&lt;/code&gt; directly on a Github Actions runner, the resulting configuration normally involves convoluted setup with VNC or similar. Such setups are usually fragile, and can be more difficult to reproduce locally - making it slower to debug any issues that do arise.&lt;/p&gt;
&lt;h2 id="lxd-desktop-vms" class="relative group"&gt;LXD Desktop VMs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#lxd-desktop-vms" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before working at Canonical, I must confess to having paid &lt;em&gt;very little&lt;/em&gt; attention to &lt;a href="https://canonical.com/lxd" target="_blank" rel="noreferrer"&gt;LXD&lt;/a&gt;. Since joining, it&amp;rsquo;s become one of my most used tools in my daily workflow. I had some early experience with LXD a few years ago when it essentially &amp;ldquo;just&amp;rdquo; did Ubuntu containers, but it&amp;rsquo;s evolved into a very competent hypervisor in its own right, providing both container and virtual machine images for numerous different Linux distributions. In more recent history, desktop virtual machines were introduced which gives a very fast way to boot into a desktop across multiple distributions.&lt;/p&gt;
&lt;p&gt;To boot into a Ubuntu 22.04 LTS desktop virtual machine, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc launch images:ubuntu/22.04/desktop ubuntu --vm --console&lt;span class="o"&gt;=&lt;/span&gt;vga
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;As a side note, last year Canonical &lt;a href="https://ubuntu.com/blog/lxd_ui" target="_blank" rel="noreferrer"&gt;announced&lt;/a&gt; the LXD UI, which is a beautiful web UI for managing clusters of LXD servers, and includes a graphical web console for virtual machines - which is incredibly useful for testing software across versions and desktops.&lt;/p&gt;
&lt;h2 id="wrapping-lxd" class="relative group"&gt;Wrapping LXD &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#wrapping-lxd" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;After playing with LXD a bunch locally, I confirmed that between &lt;code&gt;lxc exec&lt;/code&gt;, &lt;code&gt;lxc file pull&lt;/code&gt; I had all I need. The commands were all relatively simple, but I wanted to ensure that the Github Actions I wrote were as maintainable as possible, so I decided to write a small wrapper for LXD, which ended up being named &lt;code&gt;ghvmctl&lt;/code&gt; (Github Virtual Machine Control) because naming is…. hard!&lt;/p&gt;
&lt;p&gt;There is nothing special about &lt;code&gt;ghvmctl&lt;/code&gt;, it is just a &lt;code&gt;bash&lt;/code&gt; script. Perhaps one day I&amp;rsquo;ll implement it in something a little more… rigorous? That said, it&amp;rsquo;s wrapping relatively few shell commands, with very few variables, and it&amp;rsquo;s been solid for several months now. The sum of &lt;code&gt;ghvmctl&lt;/code&gt;&amp;rsquo;s capabilities can be summarised as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Launch Ubuntu desktop VMs&lt;/li&gt;
&lt;li&gt;Dismiss any initial setup wizards&lt;/li&gt;
&lt;li&gt;Ensure that &lt;code&gt;gnome-screenshot&lt;/code&gt; is installed&lt;/li&gt;
&lt;li&gt;Provide a simple way to install and run snaps&lt;/li&gt;
&lt;li&gt;Provide a simple way to screenshot the whole screen, and the active window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The script is mostly contained in a &lt;a href="https://github.com/snapcrafters/ghvmctl/blob/1ac4a99dd1c6f78226b60eda205cd06c6ac20dfa/src/ghvmctl" target="_blank" rel="noreferrer"&gt;single file&lt;/a&gt;, apart from &lt;a href="https://github.com/snapcrafters/ghvmctl/blob/1ac4a99dd1c6f78226b60eda205cd06c6ac20dfa/src/ghvmctl-runner" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ghvmctl-runner&lt;/code&gt;&lt;/a&gt; which is pushed automatically into any VMs started by &lt;code&gt;ghvmctl&lt;/code&gt;, and provides a way for applications to be run with all the appropriate environment variables such that graphical applications can run when the VM is being controlled headlessly (such as &lt;code&gt;DISPLAY&lt;/code&gt;, &lt;code&gt;WAYLAND_DISPLAY&lt;/code&gt;, &lt;code&gt;XDG_SESSION_TYPE&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;p&gt;There are very few dependencies for the script, but I decided to &lt;a href="https://github.com/snapcrafters/ghvmctl/blob/1ac4a99dd1c6f78226b60eda205cd06c6ac20dfa/snap/snapcraft.yaml" target="_blank" rel="noreferrer"&gt;package it as a snap&lt;/a&gt; to simplify installing it on Github runners. The snap is simple, containing just the two scripts mentioned above, and the LXC client. This also means you can install and use the tool locally should you wish to experiment with it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install ghvmctl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap install ghvmctl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Allow ghvmctl to access the LXD socket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snap connect ghvmctl:lxd lxd:lxd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Launch a VM and prepare for testing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ghvmctl prepare
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install a snap from the store&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ghvmctl snap-install signal-desktop
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the snap on the desktop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ghvmctl snap-run signal-desktop
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Wait a few seconds for the app to start...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Take screenshots and pull them back to $HOME/ghvmctl-screenshots&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ghvmctl screenshot-full
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ghvmctl screenshot-window
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;To simplify the installation and setup of &lt;code&gt;ghvmctl&lt;/code&gt;, there is also a &lt;a href="https://github.com/snapcrafters/ci/tree/8eb0566a765cd0196d7223734dd4cc0f3eb4521f/setup-ghvmctl" target="_blank" rel="noreferrer"&gt;Github Action&lt;/a&gt; which takes care of enabling KVM on the runner, initialising LXD, installing &lt;code&gt;ghvmctl&lt;/code&gt; and ensuring it has access to the LXD socket.&lt;/p&gt;
&lt;h2 id="building-an-integrated-workflow" class="relative group"&gt;Building An Integrated Workflow &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#building-an-integrated-workflow" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;By now, the Snapcrafters have quite a &lt;a href="https://github.com/snapcrafters/ci" target="_blank" rel="noreferrer"&gt;sophisticated collection&lt;/a&gt; of Github Actions which are used for managing the release lifecycle of snaps, but for me it was this piece that tied it all together for GUI applications. The actions can be summarised as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parsing &lt;code&gt;snapcraft.yaml&lt;/code&gt; files for details such as name, architectures, version&lt;/li&gt;
&lt;li&gt;Building snaps locally on Github Actions runners when Pull Requests are made&lt;/li&gt;
&lt;li&gt;Building snaps across architectures on the Launchpad build farm when changes are merged&lt;/li&gt;
&lt;li&gt;Create a &amp;ldquo;Call for Testing&amp;rdquo; Github Issue with details of the &lt;code&gt;candidate&lt;/code&gt; revisions&lt;/li&gt;
&lt;li&gt;Follow up on the issue with screenshots in a comment&lt;/li&gt;
&lt;li&gt;Promote the snap to &lt;code&gt;stable&lt;/code&gt; when a maintainer issues the command&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these things are particularly hard in their own right, but they amount to some complicated juggling of Github Actions artefacts and tokens for various repositories and external services.&lt;/p&gt;
&lt;h2 id="building-a-screenshot-action" class="relative group"&gt;Building A Screenshot Action &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#building-a-screenshot-action" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The Github Action &lt;a href="https://github.com/snapcrafters/ci/tree/8eb0566a765cd0196d7223734dd4cc0f3eb4521f/get-screenshots" target="_blank" rel="noreferrer"&gt;responsible for collecting screenshots&lt;/a&gt; is used across multiple snaps to give maintainers a bit more confidence when releasing changes into the &lt;code&gt;stable&lt;/code&gt; channels for their snaps.&lt;/p&gt;
&lt;p&gt;The action makes use of &lt;code&gt;ghvmctl&lt;/code&gt; to launch VMs, install the &lt;code&gt;candidate&lt;/code&gt; snaps and collect screenshots of them. This turned out to be simple with the introduction of &lt;code&gt;ghvmctl&lt;/code&gt; - the complicated part turned out to be where to store the screenshots such that they could be published in a comment! Github provides image/file hosting for comments on issue &lt;em&gt;when the comments are made through the web UI&lt;/em&gt;. As far as I can tell, there is no way to submit a comment with an embedded picture from the Github API (let me know!), and I wasn&amp;rsquo;t keen to rely on a third party service such as Imgur.&lt;/p&gt;
&lt;p&gt;The solution we settled on was to create a &lt;a href="https://github.com/snapcrafters/ci-screenshots" target="_blank" rel="noreferrer"&gt;&lt;code&gt;ci-screenshots&lt;/code&gt;&lt;/a&gt;, which could be published to by the workflows of each snap repository. We will, over time, clear out older screenshots.&lt;/p&gt;
&lt;h2 id="end-result" class="relative group"&gt;End Result &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#end-result" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;An example of the end result can be seen in &lt;a href="https://github.com/snapcrafters/signal-desktop/issues/267" target="_blank" rel="noreferrer"&gt;this Github Issue&lt;/a&gt;. Let&amp;rsquo;s break down what happened.&lt;/p&gt;
&lt;p&gt;First, once a change was merged into the Signal Desktop snap repository, and the resulting snap was built for each of its target architectures:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_fafbb694af97bb75.webp 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_1ccda47c0fbc34a.webp 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_2580f508fb236d67.webp 1024w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_89c072b70232a46e.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="2238"
height="694"
class="mx-auto my-0 rounded-md"
alt="complete ci workflow on github actions"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_37a169e3d17a36e1.png" srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_dd4a9f7b594b9d4f.png 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_37a169e3d17a36e1.png 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_de66138f407ef2a9.png 1024w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/01_hu_6f5bce1fb8f383af.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As part of this process an issue was automatically created containing information about the new &lt;code&gt;candidate&lt;/code&gt; versions:&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_d11ec7d0d05027c9.webp 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_9ec42ca1fc2f112e.webp 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_21bad717e4953f76.webp 1024w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_2a1e4b2bd386a874.webp 1249w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1249"
height="1260"
class="mx-auto my-0 rounded-md"
alt="example call for testing post for Signal Desktop"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_80067b83b7fe31e7.png" srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_44490b647e67e3b3.png 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_80067b83b7fe31e7.png 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02_hu_6330c88e415662a4.png 1024w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/02.png 1249w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A couple of minutes later, the bot followed up with a comment containing screenshots of the application running on the desktop:&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_17944afe0d371816.webp 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_44dd85c8159616ed.webp 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_b658abbdec44e80c.webp 946w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_b658abbdec44e80c.webp 946w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="946"
height="1458"
class="mx-auto my-0 rounded-md"
alt="Github Issue comment containing screenshots from an automated test run"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_72ccc750b515132e.png" srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_94f7e00cd1c90f0c.png 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03_hu_72ccc750b515132e.png 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03.png 946w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/03.png 946w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once I was content that the snap was working correctly, and I&amp;rsquo;d tested the revision out locally, I then issued a command to promote the snap into the &lt;code&gt;stable&lt;/code&gt; channel, where it was slowly rolled out across the user base:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_9c93800eb9b1f694.webp 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_7482b899ecead064.webp 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_a34852695c1eb548.webp 939w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_a34852695c1eb548.webp 939w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="939"
height="391"
class="mx-auto my-0 rounded-md"
alt="Github Issue comment showing revision promotion workflow"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_ed2dc7c734c9f31d.png" srcset="https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_62f7b81e9d05693c.png 330w,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04_hu_ed2dc7c734c9f31d.png 660w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04.png 939w
,https://jnsgr.uk/2024/03/simplifying-snap-gui-testing/04.png 939w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can see examples of this working across multiple snaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/snapcrafters/gimp/issues/260" target="_blank" rel="noreferrer"&gt;gimp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/snapcrafters/discord/issues/184" target="_blank" rel="noreferrer"&gt;discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/snapcrafters/sublime-text/issues/59" target="_blank" rel="noreferrer"&gt;sublime-text&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/snapcrafters/sublime-merge/issues/31" target="_blank" rel="noreferrer"&gt;sublime-merge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/snapcrafters/mattermost-desktop/issues/100" target="_blank" rel="noreferrer"&gt;mattermost-desktop&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;re still in the process of refining and rolling out this process, but I hope that it will help reduce burden on maintainers over time, and result in fresher and more reliable desktop snaps in the &lt;a href="https://snapcraft.io" target="_blank" rel="noreferrer"&gt;Snap Store&lt;/a&gt;. If you&amp;rsquo;d like to get involved in Snapcrafters, reach out to me, post on the &lt;a href="https://forum.snapcraft.io/t/snapcrafters-reboot/24625" target="_blank" rel="noreferrer"&gt;Snapcraft Discourse&lt;/a&gt; or join the &lt;a href="https://matrix.to/#/#snapcrafters:matrix.org" target="_blank" rel="noreferrer"&gt;Matrix room&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>A homelab dashboard for NixOS</title><link>https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/</link><pubDate>Tue, 05 Mar 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I run a very small homelab that provides some basic services to my home network. I&amp;rsquo;m not much of a data hoarder, but my lab consists of some redundant storage in a &lt;a href="https://www.raidz-calculator.com/raidz-types-reference.aspx" target="_blank" rel="noreferrer"&gt;&lt;code&gt;raidz2&lt;/code&gt;&lt;/a&gt; ZFS pool, and I use the homelab as a receive-only target for &lt;a href="https://syncthing.net/" target="_blank" rel="noreferrer"&gt;Syncthing&lt;/a&gt;, and as the point from which backups of my critical data are made using &lt;a href="https://www.borgbackup.org/" target="_blank" rel="noreferrer"&gt;Borg&lt;/a&gt; &amp;amp; &lt;a href="https://www.borgbase.com/" target="_blank" rel="noreferrer"&gt;Borgbase&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It also runs a few other small services - all of which are exclusively available over Tailscale to my other devices. I wanted a small dashboard solution that could give me links to each of those services with a nice simple URL.&lt;/p&gt;
&lt;p&gt;There are certainly plenty of options; this seems to be a highly crowded space in the open source homelab world. I settled on the rather ambiguously named &lt;a href="https://gethomepage.dev" target="_blank" rel="noreferrer"&gt;homepage&lt;/a&gt;. At the time of writing, my dashboard looks like so, though there are people who have been far more creative with the appearance!&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_f87c0e5969f67ecf.webp 330w,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_15232444802d3e4.webp 660w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_3f5486c9654f579e.webp 1024w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_bfdedcb42790b0bf.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="2649"
height="1519"
class="mx-auto my-0 rounded-md"
alt="my dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_9f5ada7c491d9d98.png" srcset="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_caff88d829867e50.png 330w,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_9f5ada7c491d9d98.png 660w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_3dd9fc39189de2c5.png 1024w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/01_hu_4daae9c3741e3788.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Naturally, I wanted to run this on NixOS, so in July 2023 I landed one of my early contributions to the project in the form of PR &lt;a href="https://github.com/NixOS/nixpkgs/pull/243094" target="_blank" rel="noreferrer"&gt;#243094&lt;/a&gt; which added the package (named &lt;code&gt;homepage-dashboard&lt;/code&gt;), a basic NixOS module and a basic test.&lt;/p&gt;
&lt;h2 id="homepages-configuration" class="relative group"&gt;Homepage&amp;rsquo;s Configuration &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#homepages-configuration" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Homepage is configured using a &lt;a href="https://github.com/gethomepage/homepage/tree/6b961abc4e73b924f780b1cb32481640213fd477/src/skeleton" target="_blank" rel="noreferrer"&gt;set of YAML files&lt;/a&gt; named &lt;code&gt;services.yaml&lt;/code&gt;, &lt;code&gt;bookmarks.yaml&lt;/code&gt;, &lt;code&gt;widgets.yaml&lt;/code&gt;, etc. When I originally started writing the module, it would look for those config files in a hard-coded location (&lt;code&gt;/config&lt;/code&gt;), and if the files were missing it would copy a &lt;a href="https://github.com/gethomepage/homepage/tree/6b961abc4e73b924f780b1cb32481640213fd477/src/skeleton" target="_blank" rel="noreferrer"&gt;skeleton config&lt;/a&gt; into place with some defaults to get you going.&lt;/p&gt;
&lt;p&gt;The hard-coded location results from the fact that the upstream primarily support deploying Homepage using Docker. They expect the config directory to be bind-mounted into the container from the host. As part of the initial packaging effort, I contributed a &lt;a href="https://github.com/gethomepage/homepage/pull/1673" target="_blank" rel="noreferrer"&gt;patch&lt;/a&gt; upstream to allow customising this location by setting the &lt;code&gt;HOMEPAGE_CONFIG_DIR&lt;/code&gt; environment variable, which I then set in the systemd unit configuration &lt;a href="https://github.com/NixOS/nixpkgs/pull/243094/files#diff-e4532095c5cc6e132e60ca8a1fa0589898576aa2073624135c1086cbc7e78a7cR38" target="_blank" rel="noreferrer"&gt;in the NixOS module&lt;/a&gt; to &lt;code&gt;/var/lib/homepage-dashboard&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This has been working fine for a few months, but it&amp;rsquo;s been bugging me that my dashboard configuration is not part of the declarative system configuration. Once the initial skeleton had been copied in place, you were left to edit the files manually (and back them up) if you wanted to make changes. Moreover, &lt;code&gt;homepage&lt;/code&gt; defaults to creating a &lt;code&gt;logs&lt;/code&gt; directory as a subdirectory of the &lt;code&gt;config&lt;/code&gt; directory. This makes some sense in a container environment, but given that homepage also logs to stdout (which is collected by the systemd journal on NixOS), it&amp;rsquo;s really just unnecessary duplication.&lt;/p&gt;
&lt;h2 id="evolving-the-module-design" class="relative group"&gt;Evolving The Module Design &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#evolving-the-module-design" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Before writing the actual implementation of the module, I decided to first sketch out what I wanted my NixOS configuration to look like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homepage-dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# These options were already present in my configuration.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unstable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homepage-dashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# The following options were what I planned to add.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/settings/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/bookmarks/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bookmarks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/services/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/service-widgets/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;widgets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/kubernetes/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;kubernetes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/docker/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;docker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# https://gethomepage.dev/latest/configs/custom-css-js/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;customJS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;customCSS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Each of the new sections would then map neatly to the &lt;a href="https://gethomepage.dev/latest/configs/" target="_blank" rel="noreferrer"&gt;different configuration section&lt;/a&gt; in the upstream documentation. Based on my learning from &lt;a href="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/" target="_blank" rel="noreferrer"&gt;the Scrutiny module&lt;/a&gt;, I wanted to utilise the same &lt;a href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md" target="_blank" rel="noreferrer"&gt;RFC42&lt;/a&gt; approach which would obviate the need for the module to specify every possible supported configuration option, resulting in a large, difficult to maintain module which could quickly fall behind the upstream project.&lt;/p&gt;
&lt;p&gt;Homepage supports a large number of &lt;a href="https://gethomepage.dev/latest/widgets/" target="_blank" rel="noreferrer"&gt;widgets&lt;/a&gt; (see below) which are able to scrape information from the API of various devices and services. These often require an API key or token of some kind, and having those in plaintext as part of the machine configuration is undesirable from a security perspective - even if your services are all on a private network like mine. Luckily I found out (tucked away &lt;a href="https://gethomepage.dev/latest/installation/docker/#using-environment-secrets" target="_blank" rel="noreferrer"&gt;in the docs&lt;/a&gt;) that Homepage can inject secret values into the configuration using environment variables.&lt;/p&gt;
&lt;p&gt;Given the template value of &lt;code&gt;{{HOMEPAGE_VAR_FOOBAR}}&lt;/code&gt; as part of the configuration, Homepage will automatically substitute the value of the variable &lt;code&gt;HOMEPAGE_VAR_FOOBAR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="./02.png"&gt;
&lt;figure&gt;&lt;img src="./02.png" alt="homepage widgets" class="mx-auto my-0 rounded-md" /&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I decided to provide a single configuration option named &lt;code&gt;environmentFile&lt;/code&gt; so that users can supply the path to an environment file containing all of their variables. This file can be omitted from Git repositories and configurations, or included in encrypted form. I achieve this by including the file encrypted using &lt;a href="https://github.com/ryantm/agenix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;agenix&lt;/code&gt;&lt;/a&gt; which integrates &lt;a href="https://github.com/FiloSottile" target="_blank" rel="noreferrer"&gt;@FiloSottile&lt;/a&gt;&amp;rsquo;s wonderful &lt;a href="https://age-encryption.org/" target="_blank" rel="noreferrer"&gt;&lt;code&gt;age&lt;/code&gt;&lt;/a&gt; into NixOS. You can see how that&amp;rsquo;s supplied as &lt;a href="https://github.com/jnsgruk/nixos-config/blob/ab46f2b45aea11634c85c2c2024eac1c4f5601e0/host/common/services/homepage/thor.nix#L2-L9" target="_blank" rel="noreferrer"&gt;part of my nixos-config&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="backwards-compatibility" class="relative group"&gt;Backwards Compatibility &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#backwards-compatibility" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;According to my &lt;a href="https://github.com/search?q=language%3Anix&amp;#43;homepage-dashboard&amp;#43;NOT&amp;#43;is%3Afork&amp;#43;NOT&amp;#43;repo%3ANixOS%2Fnixpkgs&amp;amp;type=code" target="_blank" rel="noreferrer"&gt;relatively naive Github search&lt;/a&gt; I estimated that there are not &lt;em&gt;that many&lt;/em&gt; users of the module - likely in the tens, rather than the hundreds or thousands. That said, I think its important not to break those users. There&amp;rsquo;s no reason to expect that a &lt;code&gt;nix flake update&lt;/code&gt; should break your system.&lt;/p&gt;
&lt;p&gt;The way I chose to handle this in the module was to check if any of the &lt;em&gt;new config options&lt;/em&gt; are set. If they&amp;rsquo;re not, the module behaves as before, but displays a deprecation warning:&lt;/p&gt;
&lt;p&gt;&lt;a href="./03.png"&gt;
&lt;figure&gt;&lt;img src="./03.png" alt="deprecation warning for existing users" class="mx-auto my-0 rounded-md" /&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The implementation of this check is relatively crude, but it works, and it will only be around until the release of NixOS 24.05 (in May 24):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If homepage-dashboard is enabled, but none of the configuration values have been updated,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# then default to &amp;#34;unmanaged&amp;#34; configuration which is manually updated in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# var/lib/homepage-dashboard. This is to maintain backwards compatibility, and should be&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# deprecated in a future release.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;managedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookmarks&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customCSS&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customJS&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kubernetes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;widgets&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;configDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;managedConfig&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/etc/homepage-dashboard&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/homepage-dashboard&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;using unmanaged configuration for homepage-dashboard is deprecated and will be removed&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; in 24.05. please see the NixOS documentation for `services.homepage-dashboard&amp;#39; and add&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; your bookmarks, services, widgets, and other configuration using the options provided.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Display the deprecation warning if the configuration isn&amp;#39;t managed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;managedConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="solving-log-duplication" class="relative group"&gt;Solving Log Duplication &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#solving-log-duplication" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I mentioned in a previous section that Homepage logs to both stdout and a logs directory by default. While the log file path &lt;a href="https://gethomepage.dev/latest/configs/settings/#log-path" target="_blank" rel="noreferrer"&gt;can be customised&lt;/a&gt;, it&amp;rsquo;s not currently possible to disable the file logging completely. It&amp;rsquo;s not desirable to have the log file in this context, because all of the logs are collected by systemd anyway.&lt;/p&gt;
&lt;p&gt;Looking at the upstream implementation, the logger is instantiated and configured in a single &lt;a href="https://github.com/gethomepage/homepage/blob/6b961abc4e73b924f780b1cb32481640213fd477/src/utils/logger.js#L41-L68" target="_blank" rel="noreferrer"&gt;&lt;code&gt;logger.js&lt;/code&gt;&lt;/a&gt; file. Homepage has a policy that they won&amp;rsquo;t accept feature contributions (even if you do the implementation) unless the feature gets at least 10 upvotes. I filed a &lt;a href="https://github.com/gethomepage/homepage/discussions/3067" target="_blank" rel="noreferrer"&gt;feature request&lt;/a&gt;, but it&amp;rsquo;s yet to get enough votes to be accepted.&lt;/p&gt;
&lt;p&gt;In the mean time I wrote &lt;a href="https://github.com/gethomepage/homepage/commit/3be28a2c8b68f2404e4083e7f32eebbccdc4d293" target="_blank" rel="noreferrer"&gt;a short patch&lt;/a&gt; on a branch in my personal fork which makes Homepage adhere to an environment variable named &lt;code&gt;LOG_TARGETS&lt;/code&gt;. The possible values are &lt;code&gt;both&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt; or &lt;code&gt;stdout&lt;/code&gt; with a default value of &lt;code&gt;both&lt;/code&gt; to respect the existing behaviour and remain backward compatible. The patch is now &lt;a href="https://github.com/NixOS/nixpkgs/blob/6dc8cbe3cc1520315d85c3e4490b50a73c7c7381/pkgs/servers/homepage-dashboard/default.nix#L42-L54" target="_blank" rel="noreferrer"&gt;applied&lt;/a&gt; in the Nix package as part of nixpkgs, and the module &lt;a href="https://github.com/NixOS/nixpkgs/blob/6dc8cbe3cc1520315d85c3e4490b50a73c7c7381/nixos/modules/services/misc/homepage-dashboard.nix#L223-L227" target="_blank" rel="noreferrer"&gt;configures the systemd unit&lt;/a&gt; by setting the &lt;code&gt;LOG_TARGETS&lt;/code&gt; variable to &lt;code&gt;stdout&lt;/code&gt; in cases where the configuration is managed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HOMEPAGE_CONFIG_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configDir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;toString&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listenPort&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;LOG_TARGETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;managedConfig&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;stdout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: My feature request got upvoted pretty quickly, and as a result my &lt;a href="https://github.com/gethomepage/homepage/pull/3075" target="_blank" rel="noreferrer"&gt;pull request&lt;/a&gt; was merged. This means that following the next release of Homepage, I&amp;rsquo;ll be able to drop the patch from the Nix package. Thanks to all those who upvoted it!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="bolstering-the-test-suite" class="relative group"&gt;Bolstering The Test Suite &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#bolstering-the-test-suite" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;When I originally implemented the tests for the module, they simply enabled the service and ensure that it responded on the specified port. I wanted to include some logic in the test that ensured the ability to detect when managed configuration should be used, and when the module should respect an existing implementation.&lt;/p&gt;
&lt;p&gt;The NixOS test suite supports specifying multiple machines as part of a given test, so extending the previous implementation wasn&amp;rsquo;t particularly cumbersome. See below for the (annotated) implementation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="sr"&gt;./make-test-python.nix&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;homepage-dashboard&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maintainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maintainers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;jnsgruk&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Create a machine that uses the legacy module format,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# where configuration is unmanaged by nix, and relies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# upon YAML files.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unmanaged_conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homepage-dashboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Create another machine that sets some simple&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# configuration using the new module system. This&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# doesn&amp;#39;t need to be exhaustive, just enough to trigger&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# the condition that makes the module use managed config.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;managed_conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;homepage-dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;custom&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;testScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure the services are started on unmanaged machine,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # and that the service responds to HTTP requests on the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # expected port.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; unmanaged_conf.wait_for_unit(&amp;#34;homepage-dashboard.service&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; unmanaged_conf.wait_for_open_port(8082)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; unmanaged_conf.succeed(&amp;#34;curl --fail http://localhost:8082/&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure that /etc/homepage-dashboard doesn&amp;#39;t exist, and boilerplate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # configs are copied into place in `/var/lib/homepage-dashboard`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # This validates the existing behaviour.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; unmanaged_conf.fail(&amp;#34;test -d /etc/homepage-dashboard&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; unmanaged_conf.succeed(&amp;#34;test -f /var/lib/private/homepage-dashboard/settings.yaml&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure the services are started on managed machine,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # and that the service responds to HTTP requests on the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # expected port.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; managed_conf.wait_for_unit(&amp;#34;homepage-dashboard.service&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; managed_conf.wait_for_open_port(8082)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; managed_conf.succeed(&amp;#34;curl --fail http://localhost:8082/&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure /etc/homepage-dashboard is created and unmanaged
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # conf location isn&amp;#39;t present
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; managed_conf.succeed(&amp;#34;test -d /etc/homepage-dashboard&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; managed_conf.fail(&amp;#34;test -f /var/lib/private/homepage-dashboard/settings.yaml&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is by no means exhaustive, and I can certainly imagine increasing the coverage here at a later date, but it does at least give some confidence when working on the module that the two basic modes of operation are functioning correctly.&lt;/p&gt;
&lt;h2 id="migrating-existing-configurations" class="relative group"&gt;Migrating Existing Configurations &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#migrating-existing-configurations" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;If you have been using the module in its past form, you may be wondering what the easiest way to migrate to the new format is&amp;hellip;&lt;/p&gt;
&lt;p&gt;I made the shift using &lt;a href="https://github.com/euank/yaml2nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;yaml2nix&lt;/code&gt;&lt;/a&gt; to convert my existing YAML configurations to Nix expressions, and then formatted the output using &lt;a href="https://github.com/nix-community/nixpkgs-fmt" target="_blank" rel="noreferrer"&gt;&lt;code&gt;nixpkgs-fmt&lt;/code&gt;&lt;/a&gt;. For example, given the following &lt;code&gt;settings.yaml&lt;/code&gt; (which came from my homelab before I moved over):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# For configuration options and examples, please see:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# https://gethomepage.dev/en/configs/settings&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sgrs dashboard&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;favicon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://jnsgr.uk/favicon.ico&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;headerStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;clean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;row&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;row&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;machines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;row&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;You can do the following to get a Nix expression that can be assigned to &lt;code&gt;services.homepage-dashboard.settings&lt;/code&gt; in your machine configuration, converting the YAML to a Nix expression:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; ~/temp
❯ nix run nixpkgs#yaml2nix settings.yaml
{ title = &amp;#34;sgrs dashboard&amp;#34;; favicon = &amp;#34;https://jnsgr.uk/favicon.ico&amp;#34;; headerStyle = &amp;#34;clean&amp;#34;; layout = { media = { style = &amp;#34;row&amp;#34;; columns = 3; }; infra = { style = &amp;#34;row&amp;#34;; columns = 4; }; machines = { style = &amp;#34;row&amp;#34;; columns = 4; }; }; }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that output, you can insert a few line breaks and rely on &lt;code&gt;nixpkgs-fmt&lt;/code&gt; to get everything lined up properly. You can see my complete dashboard configuration in Nix format as part of my &lt;a href="https://github.com/jnsgruk/nixos-config/blob/ab46f2b45aea11634c85c2c2024eac1c4f5601e0/host/common/services/homepage/thor.nix" target="_blank" rel="noreferrer"&gt;nixos-config&lt;/a&gt; repository.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;The PR was merged earlier today, and will now need to trickle through the branches on its way to &lt;code&gt;nixos-unstable&lt;/code&gt;. At the time of writing, it hasn&amp;rsquo;t quite made it there:&lt;/p&gt;
&lt;p&gt;&lt;a href="04.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_b649056a2c609f2f.webp 330w,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_b482bddbde06b2f8.webp 660w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_a785e177a11783fd.webp 946w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_a785e177a11783fd.webp 946w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="946"
height="703"
class="mx-auto my-0 rounded-md"
alt="PR progress"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_680b0c216025bcb2.png" srcset="https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_7f0c36005196670c.png 330w,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04_hu_680b0c216025bcb2.png 660w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04.png 946w
,https://jnsgr.uk/2024/03/a-homelab-dashboard-for-nixos/04.png 946w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can track for yourself on the &lt;a href="https://nixpk.gs/pr-tracker.html?pr=291554" target="_blank" rel="noreferrer"&gt;nixpkgs tracker&lt;/a&gt;, but the time delay should give you a chance to migrate your configuration!&lt;/p&gt;
&lt;p&gt;See Github for the full &lt;a href="https://github.com/NixOS/nixpkgs/blob/6dc8cbe3cc1520315d85c3e4490b50a73c7c7381/nixos/modules/services/misc/homepage-dashboard.nix" target="_blank" rel="noreferrer"&gt;module&lt;/a&gt; implementation, &lt;a href="https://github.com/NixOS/nixpkgs/blob/6dc8cbe3cc1520315d85c3e4490b50a73c7c7381/pkgs/servers/homepage-dashboard/default.nix#L42-L54" target="_blank" rel="noreferrer"&gt;package&lt;/a&gt; and &lt;a href="https://github.com/NixOS/nixpkgs/blob/6dc8cbe3cc1520315d85c3e4490b50a73c7c7381/nixos/tests/homepage-dashboard.nix" target="_blank" rel="noreferrer"&gt;tests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let me know if you&amp;rsquo;re using the module, or if you run into any issues! If you&amp;rsquo;re a fan of Homepage, then consider helping out with the project or &lt;a href="https://github.com/sponsors/gethomepage" target="_blank" rel="noreferrer"&gt;sponsoring them on Github&lt;/a&gt;, and once again thank you to those who helped review and shape the module as part of this upgrade!&lt;/p&gt;</description></item><item><title>Contributing Scrutiny to nixpkgs</title><link>https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/</link><pubDate>Mon, 19 Feb 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m quite overwhelmed by the number of people who read, and engaged with my &lt;a href="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/" target="_blank" rel="noreferrer"&gt;last post&lt;/a&gt; on packaging &lt;a href="https://github.com/AnalogJ/scrutiny" target="_blank" rel="noreferrer"&gt;Scrutiny&lt;/a&gt; for NixOS. I hadn&amp;rsquo;t intended on posting to this blog more than maybe once per month, but I decided to continue documenting the journey of landing Scrutiny upstream in &lt;a href="https://github.com/NixOS/nixpkgs" target="_blank" rel="noreferrer"&gt;nixpkgs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One of the things that really struck me about NixOS early on was how simple the contribution process is. A single Github repository contains all the packages, all the modules, and all the tests.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve found the community incredibly helpful and respectful in all of my contributions - often with 2-3 people quickly providing reviews to help shape the contribution. I&amp;rsquo;ve heard complaints that reviews can be quite pedantic, and while I agree to some extent, the reviews nearly always come with concrete suggestions which are a great way to learn. Notwithstanding the fact that you are contributing to an operating system package that could land on many thousands of machines - it&amp;rsquo;s important to be careful!&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been deliberating about whether or not to submit your package, module or otherwise, I&amp;rsquo;d encourage you to go for it. I&amp;rsquo;ve always found it rewarding and it&amp;rsquo;s taught me a lot about Nix and NixOS.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/NixOS/nixpkgs/pull/214193" target="_blank" rel="noreferrer"&gt;first contribution&lt;/a&gt; was adding a package, module and test for &lt;a href="https://multipass.run" target="_blank" rel="noreferrer"&gt;multipass&lt;/a&gt; - as you can see I had plenty to learn, but people were generous with their time and it resulted in a contribution that I use daily to get my work done on NixOS.&lt;/p&gt;
&lt;p&gt;For those losing sleep over the poorly disk I showed in my last post, you&amp;rsquo;ll be relieved to know that it&amp;rsquo;s now been replaced! Incidentally this was the first time I&amp;rsquo;ve replaced a disk in a ZFS array and I was super impressed with how easy the process was!&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_70ac169804d3c594.webp 330w,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_899a5137cf5cefc9.webp 660w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_2de91695989da5d.webp 1024w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_cf43947bd8725c4c.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1626"
height="1743"
class="mx-auto my-0 rounded-md"
alt="healthy disks"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_75fa28d8d8175e48.png" srcset="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_e5c4f8060124c79f.png 330w,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_75fa28d8d8175e48.png 660w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_13320c9cf5b8871d.png 1024w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/02_hu_de11ff0354721e3f.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="improving-my-work" class="relative group"&gt;Improving My Work &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#improving-my-work" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Over the weekend following my first post about Scrutiny, a couple of people identified some small issues and made pull requests to resolve them - thank you to those people! You can see their work &lt;a href="https://github.com/jnsgruk/nixos-config/pull/14" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;, &lt;a href="https://github.com/jnsgruk/nixos-config/pull/17" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/jnsgruk/nixos-config/pull/15" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;. Someone also &lt;a href="https://toot.community/@bouk/111951919753119887" target="_blank" rel="noreferrer"&gt;pointed out&lt;/a&gt; to me that string-interpolating YAML wasn&amp;rsquo;t the most ideal solution to rendering the configuration file, and the I could have used &lt;a href="https://nixos.org/manual/nix/stable/language/builtins.html#builtins-toJSON" target="_blank" rel="noreferrer"&gt;&lt;code&gt;builtins.toJSON&lt;/code&gt;&lt;/a&gt;, taking advantage of the fact that JSON is also valid YAML.&lt;/p&gt;
&lt;p&gt;I also wanted to make sure that there was a way for people to use the module with an existing InfluxDB deployment. My original implementation assumed that it could just add &lt;code&gt;services.influxdb2.enable = true&lt;/code&gt; to the system configuration and use the default credentials to connect. I added &lt;a href="https://github.com/jnsgruk/nixos-config/blob/34d91414594d1e492b5a6dfe43514da01c594e83/modules/nixos/scrutiny.nix#L72-L126" target="_blank" rel="noreferrer"&gt;some options&lt;/a&gt; to the module for configuring a different InfluxDB instance if required. These were later modified slightly to use &lt;code&gt;lib.types.nullOr lib.types.str&lt;/code&gt; following a helpful &lt;a href="https://github.com/NixOS/nixpkgs/pull/289934#discussion_r1495476364" target="_blank" rel="noreferrer"&gt;review comment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next up was refactoring the helper function I wrote for generating the configuration file. As mentioned above, Nix comes with a handy &lt;a href="https://nixos.org/manual/nix/stable/language/builtins.html#builtins-toJSON" target="_blank" rel="noreferrer"&gt;&lt;code&gt;builtins.toJSON&lt;/code&gt;&lt;/a&gt; function which takes a native Nix expression (an &lt;a href="https://nixos.org/guides/nix-pills/basics-of-language#id1366" target="_blank" rel="noreferrer"&gt;attribute set&lt;/a&gt; in this case) and renders it to JSON. This obviates the need to string-interpolate YAML, which is error prone at the best of times. The renewed function looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mkScrutinyCfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeTextFile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toJSON&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;web&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/scrutiny/scrutiny.db&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frontend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/share/scrutiny&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;influxdb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;influxdb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insecure_skip_verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;influxdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tlsSkipVerify&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I also noticed that I had hard-coded the path to the binaries in the rendered systemd units. You might recall that each Nix package contains &lt;code&gt;meta&lt;/code&gt; block, which adds information about the package such as the license, maintainer, etc. For each of the packages, &lt;a href="https://github.com/jnsgruk/nixos-config/commit/13b9a0e1d640266fe6b6b6eafad579152198d384" target="_blank" rel="noreferrer"&gt;I added&lt;/a&gt; a &lt;code&gt;mainProgram&lt;/code&gt; attribute which enables the use of something like &lt;code&gt;lib.getExe pkgs.scrutiny&lt;/code&gt; rather than &lt;code&gt;${pkgs.scrutiny}/bin/scrutiny&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mainProgram&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Finally, I wanted to simplify the packaging of the Go programs slightly. In the previous post I used &lt;code&gt;buildPhase&lt;/code&gt; to override the default build behaviour of the &lt;code&gt;buildGoModule&lt;/code&gt; function. I later discovered the &lt;code&gt;subPackages&lt;/code&gt; attribute, which enabled me to achieve the same effect without overriding the process manually. You can see the commit with all the changes &lt;a href="https://github.com/jnsgruk/nixos-config/commit/34d91414594d1e492b5a6dfe43514da01c594e83" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;, but below is a (slightly modified) preview of the simplified collector derivation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildGoModule&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# (source, name, version, vendor hash omitted)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;collector/cmd/collector-metrics/collector-metrics.go&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;makeWrapper&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CGO_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ldflags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-extldflags=-static&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;netgo&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;static&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir -p $out/bin
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp $GOPATH/bin/collector-metrics $out/bin/scrutiny-collector-metrics
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; wrapProgram $out/bin/scrutiny-collector-metrics \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --prefix PATH : &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;makeBinPath&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartmontools&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# (meta block omitted)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="contributing-to-nixpkgs" class="relative group"&gt;Contributing To nixpkgs &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#contributing-to-nixpkgs" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;With these changes made, it was time to open a pull request. The NixOS community provide some &lt;a href="https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md" target="_blank" rel="noreferrer"&gt;contributing guidelines&lt;/a&gt; which are worth a read before you start.&lt;/p&gt;
&lt;p&gt;There was a structural transition in the &lt;code&gt;pkgs&lt;/code&gt; directory lately, so this was my first contribution under that new structure. There was a &lt;a href="https://media.ccc.de/v/nixcon-2023-35713-not-all-packages-anymore-nix" target="_blank" rel="noreferrer"&gt;detailed talk&lt;/a&gt; about that transition at Nixcon if you&amp;rsquo;d like more information.&lt;/p&gt;
&lt;p&gt;I needed to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create the &lt;code&gt;scrutiny&lt;/code&gt; and &lt;code&gt;scrutiny-collector&lt;/code&gt; packages&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;scrutiny&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;Add the tests&lt;/li&gt;
&lt;li&gt;Update the release notes for the next release of NixOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which translated into the following stack of commits on &lt;a href="https://github.com/NixOS/nixpkgs/pull/289934" target="_blank" rel="noreferrer"&gt;PR #289934&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_b749c65d6f14ba42.webp 330w,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_5e38c31b2e35f6db.webp 660w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_14fb9615314a8426.webp 1014w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_14fb9615314a8426.webp 1014w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1014"
height="450"
class="mx-auto my-0 rounded-md"
alt="commits on nixpkgs pr"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_3eb9022749dfe697.png" srcset="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_c7cfe6e1b0689203.png 330w,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01_hu_3eb9022749dfe697.png 660w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01.png 1014w
,https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/01.png 1014w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There were a few other minor changes, such as how input packages (like &lt;code&gt;smartmontools&lt;/code&gt; and &lt;code&gt;makeWrapper&lt;/code&gt;) are passed to the packages. In the nixpkgs repository, packages are passed directly to the derivations, rather than &lt;code&gt;pkgs&lt;/code&gt; being passed as a top-level argument and packages being referred to by &lt;code&gt;pkgs.smartmontools&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;I also learned something new about module configuration. In the first iteration I had set the default value of &lt;code&gt;services.scrutiny.collector.enable&lt;/code&gt; to the value of &lt;code&gt;services.scrutiny.enable&lt;/code&gt;, meaning that the collector would be enabled automatically when Scrutiny was enabled if no other configuration was specified. It transpires that this is not permitted in nixpkgs, and resulted in &lt;a href="https://github.com/NixOS/nixpkgs/actions/runs/7957910596/job/21721675050?pr=289934" target="_blank" rel="noreferrer"&gt;this CI failure&lt;/a&gt;. I updated the module with a subtle API change, meaning that consumers of the module will now need to do the following to get the same behaviour.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="review-feedback" class="relative group"&gt;Review Feedback &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#review-feedback" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;From the first review I learned about the &lt;a href="https://github.com/NixOS/nixpkgs/blob/c6aa0f73ec0204b16a7245c2b39679b5e11bd307/lib/types.nix#L632-L649" target="_blank" rel="noreferrer"&gt;&lt;code&gt;lib.types.nullOr&lt;/code&gt;&lt;/a&gt; meta-type which enables the specification of optionally &lt;code&gt;null&lt;/code&gt; configuration options in a NixOS module.&lt;/p&gt;
&lt;p&gt;What followed was &lt;a href="https://github.com/NixOS/nixpkgs/pull/289934#pullrequestreview-1890026984" target="_blank" rel="noreferrer"&gt;a suggestion&lt;/a&gt; to embrace a different configuration format according to &lt;a href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md" target="_blank" rel="noreferrer"&gt;RFC42&lt;/a&gt;: a proposal for structured module configuration which is more rigorously type checked, and more flexible as new options are added to the underlying workload.&lt;/p&gt;
&lt;p&gt;Many NixOS modules solve this problem today with options like &lt;code&gt;extraConfig&lt;/code&gt; or &lt;code&gt;configLines&lt;/code&gt;, which append strings to the end of any options-generated configuration file. What RFC42 proposes is a hybrid approach whereby certain options are well-defined (and thus feature &lt;a href="https://search.nixos.org/options?channel=unstable&amp;amp;from=0&amp;amp;size=50&amp;amp;sort=relevance&amp;amp;type=packages&amp;amp;query=services.scrutiny." target="_blank" rel="noreferrer"&gt;in the documentation&lt;/a&gt;), but additional options can be injected as needed. The has the nice side-effect that modules do not need to support dozens of options which can become a burden to maintain and test over time, but consumers can still make use of new features and options that are introduced to the underlying workload.&lt;/p&gt;
&lt;p&gt;You can see the full resulting options definition &lt;a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/monitoring/scrutiny.nix" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;, and small annotated excerpt below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkEnableOption&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Enables the scrutiny web application.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; Scrutiny settings to be rendered into the configuration file.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; See https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This is the key to RFC42 - defining `settings` as type `submodule`.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submodule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This ensures that attrsets, lists, etc. are rendered as JSON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;freeformType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# The following are examples of &amp;#34;well-defined&amp;#34; options with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# formal types and metadata.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Port for web application to listen on.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.0.0.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Interface address for web application to bind to.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This enables the following user configurations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# These options are well-defined in the module with types,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# examples, descriptions and are rendered in the docs.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;100.64.12.34&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# These are not, but will be rendered still as JSON and enable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# people to make use of options supported by Scrutiny, but&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# not directly by the module.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;notify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;discord://token@channel&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I was impressed by the simplicity of the approach, and subsequently adopted the same for the collector, which now has the ability to take configuration through the &lt;a href="https://github.com/NixOS/nixpkgs/blob/7713853c8624abf65e020ee7f07c081ac7dbf07b/nixos/modules/services/monitoring/scrutiny.nix#L127-L153" target="_blank" rel="noreferrer"&gt;&lt;code&gt;services.scrutiny.collector.settings&lt;/code&gt;&lt;/a&gt; attribute.&lt;/p&gt;
&lt;p&gt;One aspect of Scrutiny&amp;rsquo;s design that helped is the ability to take configuration either through from file, or through environment variables, or both! I chose to supply some fixed options &lt;a href="https://github.com/NixOS/nixpkgs/blob/7713853c8624abf65e020ee7f07c081ac7dbf07b/nixos/modules/services/monitoring/scrutiny.nix#L181-L185" target="_blank" rel="noreferrer"&gt;as environment variables&lt;/a&gt; to the systemd services, and allow the rest of the configuration to be set using the generated file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/JulienMalka" target="_blank" rel="noreferrer"&gt;@JulienMalka&lt;/a&gt; reviewed next, suggesting changes to ensure that various &lt;code&gt;preInstall&lt;/code&gt; and &lt;code&gt;postInstall&lt;/code&gt; hooks were being called where I was overriding &lt;code&gt;installPhase&lt;/code&gt;, though what followed from &lt;a href="https://github.com/katexochen" target="_blank" rel="noreferrer"&gt;@katexochen&lt;/a&gt;&amp;rsquo;s review was a simplification that reduced this need slightly.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/katexochen" target="_blank" rel="noreferrer"&gt;@katexochen&lt;/a&gt; suggested a couple of nice changes, among which was the ability to drop explicitly mentioning the &lt;code&gt;netgo&lt;/code&gt; tag in the Go packages, since &lt;code&gt;CGO_ENABLED&lt;/code&gt; is disabled already, this implies using the Go internal version of many libraries. They also &lt;a href="https://github.com/NixOS/nixpkgs/pull/289934#discussion_r1497170464" target="_blank" rel="noreferrer"&gt;suggested&lt;/a&gt; moving the install of the frontend pieces to &lt;code&gt;postInstall&lt;/code&gt;, rather than overriding the standard &lt;code&gt;installPhase&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And finally &lt;a href="https://github.com/SuperSandro2000" target="_blank" rel="noreferrer"&gt;@SuperSandro2000&lt;/a&gt; reviewed with some naming changes to simplify the package definitions and reduce repetition. I also noticed that I was unnecessarily declaring the &lt;code&gt;frontend&lt;/code&gt; definition using the &lt;a href="https://nixos.org/manual/nix/stable/language/constructs.html#recursive-sets" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rec&lt;/code&gt; keyword&lt;/a&gt;. Once these were fixed the PR was merged!&lt;/p&gt;
&lt;h2 id="contribution-tips" class="relative group"&gt;Contribution Tips &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#contribution-tips" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m still relatively new to this - you can see that my &lt;a href="https://github.com/NixOS/nixpkgs/commits?author=jnsgruk" target="_blank" rel="noreferrer"&gt;list of contributions&lt;/a&gt; is not extensive, but I&amp;rsquo;ve been through the process of adding, updating and refining packages and modules a few times.&lt;/p&gt;
&lt;p&gt;Take this advice for what it is, but I hope you find it helpful:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read the &lt;a href="https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md" target="_blank" rel="noreferrer"&gt;contribution guidelines&lt;/a&gt;&lt;/strong&gt;: This is probably the best start. Try to follow the guidance as best you can, and take a look at some other &lt;a href="https://github.com/NixOS/nixpkgs/pulls?q=is%3Apr&amp;#43;sort%3Aupdated-desc&amp;#43;is%3Aclosed" target="_blank" rel="noreferrer"&gt;recently closed Pull Requests&lt;/a&gt; to see if you can pick up on any patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be humble&lt;/strong&gt;: Everyone is new when they start. You might find some reviews a little terse. Rest assured that this is only because the maintainers are incredibly busy. Assume good intentions, remain polite and respectful. Nixpkgs is one of the &lt;a href="https://github.com/NixOS/nixpkgs/pulse" target="_blank" rel="noreferrer"&gt;busiest Github repositories&lt;/a&gt; I&amp;rsquo;ve seen, and it&amp;rsquo;s largely maintained by volunteers who often have limited time to give for a huge number of contributions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be patient&lt;/strong&gt;: Don&amp;rsquo;t expect a review immediately. If you&amp;rsquo;re looking for a preliminary review, feel free to ping me &lt;a href="https://jnsgr.uk/mastodon" target="_blank" rel="noreferrer"&gt;on Mastodon&lt;/a&gt; and I&amp;rsquo;ll try to offer any guidance I can.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the Discourse&lt;/strong&gt;: Because of the volume of PRs, the community run ongoing &amp;ldquo;PRs Ready for Review&amp;rdquo; threads &lt;a href="https://discourse.nixos.org/c/dev/14" target="_blank" rel="noreferrer"&gt;on Discourse&lt;/a&gt; which you can use to attract reviewers if your PR has been stale a while. It&amp;rsquo;s also a great place to &lt;a href="https://discourse.nixos.org/c/learn/9" target="_blank" rel="noreferrer"&gt;ask for help&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once merged, commits trickle through the various branches of the repository. You can track the progress of your PR in that process once it&amp;rsquo;s merged using the &lt;a href="https://nixpk.gs/pr-tracker.html" target="_blank" rel="noreferrer"&gt;PR Tracker&lt;/a&gt;. You can see an example of this by &lt;a href="https://nixpk.gs/pr-tracker.html?pr=289934" target="_blank" rel="noreferrer"&gt;tracking the Scrutiny PR&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="03.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/03_hu_9994c2067a558e26.webp"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="537"
height="472"
class="mx-auto my-0 rounded-md"
alt="nixpkgs pr tracker"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/03.png"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="summary--thanks" class="relative group"&gt;Summary &amp;amp; Thanks &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary--thanks" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;My Scrutiny packages, module and tests landed in nixpkgs approximately 4 days after I submitted the pull request. As ever, I received a bunch of helpful reviews from the community, and Scrutiny is now easier than ever to consume on NixOS.&lt;/p&gt;
&lt;p&gt;You can see the finished components here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/sc/scrutiny/package.nix" target="_blank" rel="noreferrer"&gt;scrutiny package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/sc/scrutiny-collector/package.nix" target="_blank" rel="noreferrer"&gt;scrutiny-collector package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/monitoring/scrutiny.nix" target="_blank" rel="noreferrer"&gt;nixos module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/scrutiny.nix" target="_blank" rel="noreferrer"&gt;nixos test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you were previously using my flake to consume Scrutiny, you&amp;rsquo;ll now see a deprecation warning. To migrate over, import the module from &lt;code&gt;unstable&lt;/code&gt; in your flake and ensure that you explicitly enable the collector:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Before I wrap up, I&amp;rsquo;d like to thank all those people who took the time to review the PR, and especially &lt;a href="https://github.com/JulienMalka" target="_blank" rel="noreferrer"&gt;@JulienMalka&lt;/a&gt; and &lt;a href="https://github.com/RaitoBezarius" target="_blank" rel="noreferrer"&gt;@RaitoBezarius&lt;/a&gt; who have helped me through countless contributions over the past months and been incredibly responsive to my questions.&lt;/p&gt;
&lt;p&gt;Give it a go and let me know how you get on!&lt;/p&gt;</description></item><item><title>Packaging Scrutiny for NixOS</title><link>https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Since writing this post, I&amp;rsquo;ve contributed Scrutiny upstream to nixpkgs/NixOS. You can see the write up of that effort &lt;a href="https://jnsgr.uk/2024/02/contributing-scrutiny-to-nixpkgs/" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In a recent (well, recent-ish) &lt;a href="https://selfhosted.show/28" target="_blank" rel="noreferrer"&gt;episode of the Self Hosted Show&lt;/a&gt;, there was some talk of a hard drive monitoring tool called Scrutiny. Scrutiny is a hard drive monitoring tool that exposes S.M.A.R.T data in a nice, clean dashboard. It gathers that S.M.A.R.T data using the venerable &lt;a href="https://www.smartmontools.org/" target="_blank" rel="noreferrer"&gt;smartd&lt;/a&gt;, which is a Linux daemon that monitors S.M.A.R.T data from a huge number of ATA, IDE, SCSI-3 drives. The code is available &lt;a href="https://github.com/AnalogJ/scrutiny" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The aim of running such monitoring is to detect and replace failing hard drives before they cause an outage, or any data loss. Depending on the firmware of a drive, there are potentially hundreds of S.M.A.R.T attributes that can be collected, so it can be hard to understand which to pay attention to.&lt;/p&gt;
&lt;p&gt;As good as &lt;code&gt;smartd&lt;/code&gt; is, it has some shortcomings such as not recording historical data for each attribute, and relying upon value thresholds that are set by the manufacturers of the drive. Scrutiny aims to provide historical tracking of attribute readings, and thresholds based on real-world data from &lt;a href="https://www.backblaze.com/" target="_blank" rel="noreferrer"&gt;Backblaze&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This all looked very compelling, but it hasn&amp;rsquo;t been packaged for NixOS as far as I can tell. This post is quite &amp;ldquo;code heavy&amp;rdquo; and is intended as a bit of a deep-dive/walkthrough for people who are new to packaging in Nix. I&amp;rsquo;m certainly no expert here, and if you spot something I&amp;rsquo;ve done wrong - I&amp;rsquo;d love to hear about it!&lt;/p&gt;
&lt;p&gt;Below is a screenshot of Scrutiny&amp;rsquo;s dashboard:&lt;/p&gt;
&lt;p&gt;&lt;a href="01.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_761403fb3ee2ed1d.webp 330w,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_30aa19e407398844.webp 660w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_dbc5b78f93540287.webp 1024w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_34e1f8e4d7504664.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1706"
height="1781"
class="mx-auto my-0 rounded-md"
alt="scrutiny dashboard"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_40ab3ebca42d9fa7.png" srcset="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_30082aed7d3bd8b1.png 330w,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_40ab3ebca42d9fa7.png 660w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_ab5545e3b7717abe.png 1024w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/01_hu_293c20cbd316ee1d.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="architecture" class="relative group"&gt;Architecture &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#architecture" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In order to get Scrutiny up and running, there were a few components that all needed packaging separately.&lt;/p&gt;
&lt;p&gt;Scrutiny itself is made up of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A web application &lt;a href="https://github.com/AnalogJ/scrutiny/tree/master/webapp/backend" target="_blank" rel="noreferrer"&gt;backend&lt;/a&gt; - written in Go&lt;/li&gt;
&lt;li&gt;A web application &lt;a href="https://github.com/AnalogJ/scrutiny/tree/master/webapp/frontend" target="_blank" rel="noreferrer"&gt;frontend&lt;/a&gt; - written in NodeJs/Angular&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://github.com/AnalogJ/scrutiny/tree/master/collector" target="_blank" rel="noreferrer"&gt;collector&lt;/a&gt; service - written in Go&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The web application is the dashboard that you&amp;rsquo;ll see the pretty screenshots of. The collector service is designed to be run on an interval to collect information from &lt;code&gt;smartd&lt;/code&gt;, and send it to the web application&amp;rsquo;s API.&lt;/p&gt;
&lt;p&gt;Scrutiny relies upon &lt;a href="https://www.influxdata.com/products/influxdb/" target="_blank" rel="noreferrer"&gt;InfluxDB&lt;/a&gt; to store data about smart attributes in a timeseries.&lt;/p&gt;
&lt;p&gt;Thinking about how to structure things, I decided that I would build two separate Nix packages: one for the dashboard and UI, and another for the collector. It seems one could run the collector and dashboard on different machines, so this seemed like a logical split.&lt;/p&gt;
&lt;h2 id="packaging-for-nixos" class="relative group"&gt;Packaging for NixOS &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#packaging-for-nixos" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 id="packaging-the-dashboard" class="relative group"&gt;Packaging the Dashboard &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#packaging-the-dashboard" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;I decided to start with the web application. Because of how the project is laid out, this means combining two separate derivations based built from the same source code: one for the UI and one for the backend. I began by creating &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/pkgs/scrutiny/common.nix" target="_blank" rel="noreferrer"&gt;a file&lt;/a&gt; that would carry common attributes for each of the derivations, such as the version, repository information, hash, etc.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# common.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.7.2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Fetch the source code from Github, referring to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# the version required by Git tag.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchFromGitHub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AnalogJ&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-UYKi+WTsasUaE6irzMAHr66k7wXyec8FXc8AWjEk0qs=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I then started packaging the frontend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Import some common attributes such&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# as name, version, src repo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="sr"&gt;./common.nix&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Create a package for the Javascript frontend&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;frontend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildNpmPackage&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-webapp-frontend&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webapp/frontend&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This hash is generated at build time, and uniquely identifies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# the cache of NodeJS dependencies used for the build.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;npmDepsHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-M8P41LPg7oJ/C9abDuNM5Mn+OO0zK56CKi2BwLxv8oQ=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Override the build phase to match the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# upstream build process.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook preBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir dist
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; npm run build:prod --offline -- --output-path=dist
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook postBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Copy the output of the compiled javascript bundle&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# and site assets to the package output.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook preInstall
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir $out
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp -r dist/* $out
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook postInstall
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This is a relatively simple derivation, mostly thanks to the magic of the &lt;a href="https://nixos.org/manual/nixpkgs/stable/#javascript-tool-specific" target="_blank" rel="noreferrer"&gt;&lt;code&gt;buildNpmPackage&lt;/code&gt;&lt;/a&gt; function. This helper function takes care of creating an offline cache containing all of the NodeJS dependencies required to build the frontend. Nix builds are always done in an offline environment to help ensure reproducibility, so source code and dependencies are fetched (and hashed) early in the process before any software is actually built.&lt;/p&gt;
&lt;p&gt;I chose to override the build phase to match the process used by the upstream &lt;a href="https://github.com/AnalogJ/scrutiny/blob/a3dfce3561bcddcd8b70e4e7f483e22594c8af4d/Makefile#L103-L106" target="_blank" rel="noreferrer"&gt;Makefile&lt;/a&gt;. The result of this derivation is a package named &lt;code&gt;scrutiny-webapp-frontend&lt;/code&gt;, which contains just the built output from the &lt;code&gt;npm run build:prod&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;Next up was the dashboard backend. Another pleasingly simple derivation thanks to some help from &lt;code&gt;buildGoModule&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildGoModule&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# The vendor hash is used for multiple packages, and&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# thus centralised and imported here.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-webapp-backend&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Use the source block defined in &amp;#39;common.nix&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CGO_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Override the build phase to ensure the correct&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# binary is built. This project ships both Go applications&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# and the NodeJS frontend in the same repository, so some&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# specificity is required.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook preBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; go build \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -o scrutiny-web \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -ldflags=&amp;#34;-extldflags=-static&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -tags &amp;#34;static netgo&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; ./webapp/backend/cmd/scrutiny
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook postBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Add the built binary to the package output, as well&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# as the build output from the frontend.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir -p $out/bin $out/share/scrutiny
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp scrutiny-web $out/bin/scrutiny-web
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp -r &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;frontend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;/* $out/share/scrutiny
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This one is a little more interesting. There are some commonalities, such as setting the &lt;code&gt;vendorHash&lt;/code&gt; to ensure the correct Go dependencies are used (imported from &lt;code&gt;common.nix&lt;/code&gt; in this case), and overriding the build to match the upstream process and ensure the right binary is built.&lt;/p&gt;
&lt;p&gt;Where things differ is in the install phase, where the contents of the &lt;code&gt;scrutiny-webapp-frontend&lt;/code&gt; derivation is copied into the output of this derivation - which will ultimately result in a single Nix package (named &lt;code&gt;scrutiny-webapp-backend&lt;/code&gt;) which will contain both the frontend and backend components of the application. This is &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/pkgs/default.nix#L8-L9" target="_blank" rel="noreferrer"&gt;ultimately exposed&lt;/a&gt; in an overlay as a package simply named &lt;code&gt;scrutiny&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can see the finished product (with additional package metadata) in &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/pkgs/scrutiny/app.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;app.nix&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/pkgs/scrutiny/common.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;common.nix&lt;/code&gt;&lt;/a&gt; on Github.&lt;/p&gt;
&lt;h3 id="packaging-the-collector" class="relative group"&gt;Packaging the Collector &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#packaging-the-collector" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;The collector is just a single, statically compiled Go binary, and as such the derivation very much resembles that of the web application backend above:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Again import common attributes from &amp;#39;common.nix&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="sr"&gt;./common.nix&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildGoModule&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-collector&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# We&amp;#39;ll create a wrapper script for the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# collector, which makeWrapper helps with.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;makeWrapper&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CGO_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Override the build phase to ensure the correct&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# binary is built.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook preBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; go build \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -o scrutiny-collector-metrics \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -ldflags=&amp;#34;-extldflags=-static&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; -tags &amp;#34;static netgo&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; ./collector/cmd/collector-metrics
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; runHook postBuild
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Install both the binary, and a generated wrapper script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# which ensures that &amp;#39;smartctl&amp;#39; is in the PATH of the collector.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;installPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir -p $out/bin
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; cp scrutiny-collector-metrics $out/bin/scrutiny-collector-metrics
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; wrapProgram $out/bin/scrutiny-collector-metrics \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --prefix PATH : &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;makeBinPath&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartmontools&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Of interest here is the &lt;code&gt;installPhase&lt;/code&gt;. The collector works by invoking &lt;code&gt;smartctl&lt;/code&gt; to scrape information from &lt;code&gt;smartd&lt;/code&gt;. Scrutiny itself expects that tool to be readily available in it&amp;rsquo;s &lt;code&gt;PATH&lt;/code&gt;, and to accomplish that I used the &lt;code&gt;makeWrapper&lt;/code&gt; package to create a wrapper script that ensures &lt;code&gt;scrutiny-collector-metrics&lt;/code&gt; is executed with the &lt;code&gt;PATH&lt;/code&gt; correctly set such that &lt;code&gt;smartctl&lt;/code&gt; can be found.&lt;/p&gt;
&lt;p&gt;The final derivation for the collector can be seen in &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/pkgs/scrutiny/collector.nix" target="_blank" rel="noreferrer"&gt;&lt;code&gt;collector.nix&lt;/code&gt;&lt;/a&gt; on Github.&lt;/p&gt;
&lt;h2 id="writing-a-nixos-module" class="relative group"&gt;Writing a NixOS Module &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#writing-a-nixos-module" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;While I now had functioning packages for all of Scrutiny&amp;rsquo;s components, for them to function correctly on a machine there are a few things that need to be in place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The dashboard package must be installed and started&lt;/li&gt;
&lt;li&gt;The collector collector package must be installed and started&lt;/li&gt;
&lt;li&gt;InfluxDB must be installed and started&lt;/li&gt;
&lt;li&gt;Scrutiny dashboard must be configured to speak to the host&amp;rsquo;s InfluxDB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smartd&lt;/code&gt; must be installed and running&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This sort of challenge is exactly what the &lt;a href="https://nixos.wiki/wiki/NixOS_modules" target="_blank" rel="noreferrer"&gt;NixOS modules system&lt;/a&gt; aims to solve. Modules take a set of configuration attributes, and convert them into rendered system configuration in the form of &lt;code&gt;systemd&lt;/code&gt; units, configuration files and more.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s tempting to try to support all possible configurations when writing a module, but I generally prefer to start small, and support only the configuration I need for my use-case. This keeps things easy to test (and leaves some fun for the future!). In this case, the module would be first installed on a server which hosts a set of services behind the &lt;a href="https://traefik.io/traefik/" target="_blank" rel="noreferrer"&gt;Traefik&lt;/a&gt; reverse proxy. To work out what configuration I wanted to provide, I looked at the upstream&amp;rsquo;s &lt;a href="https://github.com/AnalogJ/scrutiny/blob/a3dfce3561bcddcd8b70e4e7f483e22594c8af4d/example.scrutiny.yaml" target="_blank" rel="noreferrer"&gt;example config file&lt;/a&gt;. The important things that stood out to me for consideration were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The location of the web UI files to serve&lt;/li&gt;
&lt;li&gt;The host/port of the InfluxDB instance&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;base path&amp;rdquo; - Scrutiny will be exposed at &lt;code&gt;https://&amp;lt;tailscale node&amp;gt;/scrutiny&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As mentioned before - each NixOS module consists of some options, and some rendered config. The options block for my Scrutiny web app looks like the below snippet. I&amp;rsquo;ve omitted comments this time, as I think the language is quite descriptive here:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkEnableOption&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Enables the scrutiny web application&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Use the `scrutiny` package by default.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkPackageOptionMD&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Port for web application to listen on&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.0.0.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Interface address for web application to bind to&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; If Scrutiny will be behind a path prefixed reverse proxy, you can override this
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; value to serve Scrutiny on a subpath.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; Do not include the leading &amp;#39;/&amp;#39;.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;openFirewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Open the default ports in the firewall for Scrutiny.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;INFO&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;DEBUG&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;INFO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Log level for Scrutiny.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This provides some basic configuration for the attributes I care about - noticeably missing is any advanced configuration for InfluxDB (such as &lt;code&gt;org&lt;/code&gt;, &lt;code&gt;bucket&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;), and and any ability to configure notifications which Scrutiny supports through the excellent &lt;a href="https://github.com/containrrr/shoutrrr" target="_blank" rel="noreferrer"&gt;shoutrrr&lt;/a&gt; library. These things will come later.&lt;/p&gt;
&lt;p&gt;I needed a convenient way to convert this Nix-native configuration format into the right format for Scrutiny - I wrote a small Nix function to help with that, which takes configuration elements from the options defined above, and writes them into a small YAML file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mkScrutinyCfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeTextFile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny.yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; version: 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; web:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; listen:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; port: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;toString&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; host: &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; basepath: &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basepath&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; database:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; location: &amp;#34;/var/lib/scrutiny/scrutiny.db&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; src:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; frontend:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; path: &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;/share/scrutiny&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; log:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; level: &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Note that this hard-codes some key elements, such as the path of the web application assets which are shipped as part of the &lt;code&gt;scrutiny&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at the part of the module which turns these options into a rendered system configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If scrutiny is enabled, also enable InfluxDB with default settings&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;influxdb2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Open the relevant ports in the system firewall if configured&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;networking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firewall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openFirewall&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;allowedTCPPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If Scrutiny is enabled, create a systemd unit to start it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;systemd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Hard Drive S.M.A.R.T Monitoring, Historical Trends &amp;amp; Real World Failure Thresholds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wantedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multi-user.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;network.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Don&amp;#39;t run the application a root - create a dynamic user as it starts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;DynamicUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Start the application with a config rendered by the helper function&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ExecStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/scrutiny-web start --config &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;mkScrutinyCfg&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Restart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;always&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StateDirectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;StateDirectoryMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0750&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s enough to get the dashboard started, but it doesn&amp;rsquo;t take care of starting the collector. For that, I added a couple more configuration options to the module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;collectorCfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;collector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Enables the scrutiny collector&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Use the `scrutiny-collector` package by default.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkPackageOptionMD&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny-collector&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;toString&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basepath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Scrutiny app API endpoint for sending metrics to.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkOption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;15m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mdDoc&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; Interval on which to collect information about disks.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; Examples: 15m, 10s, 2h.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;With that configuration in place, I needed to adjust the rendered configuration to include starting the collector. The collector is designed to run on an interval to post metrics to the dashboard. The upstream achieves this with &lt;code&gt;cron&lt;/code&gt;, but I decided to use systemd timers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If the collector is enabled, enable smartd so that the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# collector can scrape metrics from it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collectorCfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;extraOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-A /var/log/smartd/&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--interval=600&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;systemd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Setup a systemd unit to start the collector&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny-collector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;collectorCfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Scrutiny Collector Service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;COLLECTOR_API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;collectorCfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oneshot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ExecStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/scrutiny-collector-metrics run&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Set up a systemd timer to trigger the collector at an&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# interval (default 15m).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scrutiny-collector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkIf&lt;/span&gt; &lt;span class="n"&gt;collectorCfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timerConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;OnBootSec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;5m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;OnUnitActiveSec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;collectorCfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny-collector.service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s it! You can see the final module all stitched together &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/modules/nixos/scrutiny.nix" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="automated-testing" class="relative group"&gt;Automated Testing &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#automated-testing" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;To me, a super exciting part of the NixOS ecosystem is its automated testing framework, which is used to great effect for testing &lt;a href="https://github.com/NixOS/nixpkgs/tree/master/nixos/tests" target="_blank" rel="noreferrer"&gt;hundreds of packages&lt;/a&gt; end-to-end before they&amp;rsquo;re released to the various NixOS channels. The NixOS testing framework provides a set of helpers for spawning fresh NixOS virtual machines, and ensuring that applications can start, services have the right side effects, etc.&lt;/p&gt;
&lt;p&gt;I wanted to write a simple test to validate that Scrutiny will continue to work as I update my flake. In my view, the test needed to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure that the packages could be built&lt;/li&gt;
&lt;li&gt;Ensure that when &lt;code&gt;services.scrutiny.enable = true&lt;/code&gt; is set, the services start&lt;/li&gt;
&lt;li&gt;Ensure that the dashboard app renders the UI&lt;/li&gt;
&lt;li&gt;Ensure that the metrics collector can speak to the dashboard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of these are relatively trivial - one piece that&amp;rsquo;s a little harder is testing that the UI renders correctly. The application is rendered using Javascript client-side, so a simple &lt;code&gt;curl&lt;/code&gt; won&amp;rsquo;t get us the results we&amp;rsquo;re expecting. I had previously used &lt;code&gt;selenium&lt;/code&gt; for this purpose when I &lt;a href="https://github.com/NixOS/nixpkgs/blob/1225df86908f6f5b23553e9d77da4df4bfdd58ef/nixos/tests/lxd/ui.nix#L4" target="_blank" rel="noreferrer"&gt;submitted a test for the LXD UI&lt;/a&gt;, so I chose to use that approach again.&lt;/p&gt;
&lt;p&gt;The test code can be seen below, or on &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/modules/nixos/tests/scrutiny.nix" target="_blank" rel="noreferrer"&gt;Github&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;span class="lnt"&gt;46
&lt;/span&gt;&lt;span class="lnt"&gt;47
&lt;/span&gt;&lt;span class="lnt"&gt;48
&lt;/span&gt;&lt;span class="lnt"&gt;49
&lt;/span&gt;&lt;span class="lnt"&gt;50
&lt;/span&gt;&lt;span class="lnt"&gt;51
&lt;/span&gt;&lt;span class="lnt"&gt;52
&lt;/span&gt;&lt;span class="lnt"&gt;53
&lt;/span&gt;&lt;span class="lnt"&gt;54
&lt;/span&gt;&lt;span class="lnt"&gt;55
&lt;/span&gt;&lt;span class="lnt"&gt;56
&lt;/span&gt;&lt;span class="lnt"&gt;57
&lt;/span&gt;&lt;span class="lnt"&gt;58
&lt;/span&gt;&lt;span class="lnt"&gt;59
&lt;/span&gt;&lt;span class="lnt"&gt;60
&lt;/span&gt;&lt;span class="lnt"&gt;61
&lt;/span&gt;&lt;span class="lnt"&gt;62
&lt;/span&gt;&lt;span class="lnt"&gt;63
&lt;/span&gt;&lt;span class="lnt"&gt;64
&lt;/span&gt;&lt;span class="lnt"&gt;65
&lt;/span&gt;&lt;span class="lnt"&gt;66
&lt;/span&gt;&lt;span class="lnt"&gt;67
&lt;/span&gt;&lt;span class="lnt"&gt;68
&lt;/span&gt;&lt;span class="lnt"&gt;69
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scrutiny&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Configure a NixOS virtual machine for the test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Ensure that my NixOS module is imported&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosModules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Add the package overlay from my flake so that `pkgs.scrutiny` resolves.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Configure the VM to use my module and enable it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;let&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# A selenium script that fetches the dashboard using geckodriver&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;seleniumScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writePython3Bin&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;selenium-script&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;libraries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;python3Packages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; from selenium import webdriver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; from selenium.webdriver.common.by import By
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; from selenium.webdriver.firefox.options import Options
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; from selenium.webdriver.support.ui import WebDriverWait
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; options = Options()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; options.add_argument(&amp;#34;--headless&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; service = webdriver.FirefoxService(executable_path=&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getExe&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geckodriver&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;) # noqa: E501
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; driver = webdriver.Firefox(options=options, service=service)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; driver.implicitly_wait(10)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; driver.get(&amp;#34;http://localhost:8080/web/dashboard&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; wait = WebDriverWait(driver, 60)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Look for some elements that should be rendered by the Javascript bundle in the UI
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; assert len(driver.find_elements(By.CLASS_NAME, &amp;#34;mat-button-wrapper&amp;#34;)) &amp;gt; 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; assert len(driver.find_elements(By.CLASS_NAME, &amp;#34;top-bar&amp;#34;)) &amp;gt; 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; driver.close()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="n"&gt;firefox-unwrapped&lt;/span&gt; &lt;span class="n"&gt;geckodriver&lt;/span&gt; &lt;span class="n"&gt;seleniumScript&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This is the test code that will check if our service is running correctly:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;testScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; start_all()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Wait for InfluxDB to be available
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; machine.wait_for_unit(&amp;#34;influxdb2&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; machine.wait_for_open_port(8086)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Wait for Scrutiny to be available
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; machine.wait_for_unit(&amp;#34;scrutiny&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; machine.wait_for_open_port(8080)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure the API responds as we expect
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; output = machine.succeed(&amp;#34;curl localhost:8080/api/health&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; assert output == &amp;#39;{&amp;#34;success&amp;#34;:true}&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Start the collector service to send some metrics
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; collect = machine.succeed(&amp;#34;systemctl start scrutiny-collector.service&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; # Ensure the application is actually rendered by the Javascript
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; machine.succeed(&amp;#34;PYTHONUNBUFFERED=1 selenium-script&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This relatively short code snippet will take care of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Building a dedicated virtual machine according to the &lt;code&gt;machine&lt;/code&gt; spec&lt;/li&gt;
&lt;li&gt;Starting the VM&lt;/li&gt;
&lt;li&gt;Running the tests inside the VM (specified in `testScript)&lt;/li&gt;
&lt;li&gt;Collecting the results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ended up adding this check in a Github Actions &lt;a href="https://github.com/jnsgruk/nixos-config/blob/1ae03d09e2fba5b53dfe56087d523fd7493e776e/.github/workflows/flake-check.yaml#L20-L33" target="_blank" rel="noreferrer"&gt;workflow&lt;/a&gt; so that it&amp;rsquo;s run each time I make a change to my flake.&lt;/p&gt;
&lt;h2 id="try-it" class="relative group"&gt;Try It! &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#try-it" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;You can give this a go too! If you&amp;rsquo;ve got a NixOS machine, or perhaps a virtual machine you&amp;rsquo;ve been experimenting with, then you need only add the following to your flake:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs-unstable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:nixos/nixpkgs/nixos-unstable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;github:jnsgruk/nixos-config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;follows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nixpkgs-unstable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And in your NixOS machine configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Where &amp;#39;inputs&amp;#39; is the inputs attribute of your flake&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nixosModules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrutiny&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The next time you rebuild your system, you should be able to browse to &lt;a href="http://localhost:8080" target="_blank" rel="noreferrer"&gt;http://localhost:8080&lt;/a&gt; and see some hard drive metrics!&lt;/p&gt;
&lt;h2 id="whats-next" class="relative group"&gt;What&amp;rsquo;s Next? &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#whats-next" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;This was a pretty quick exercise - I&amp;rsquo;d estimate at about 3 hours in total. No doubt I will find some bugs, but already in my mind are the following future improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add some more tests that exercise more of the config options&lt;/li&gt;
&lt;li&gt;Enable notifications configuration in the module&lt;/li&gt;
&lt;li&gt;Submit the packages/module to the upstream or to nixpkgs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also need to go and investigate this rather sad looking hard disk&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a href="02.png"&gt;
&lt;figure&gt;
&lt;picture
class="mx-auto my-0 rounded-md"
&gt;
&lt;source
srcset="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_7e7bc02d3ccb4bb2.webp 330w,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_d0092e32200d8a2c.webp 660w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_d109e9a1c90eb6dd.webp 1024w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_23e74f22b4389663.webp 1320w
"
sizes="100vw"
type="image/webp"
/&gt;
&lt;img
width="1658"
height="1516"
class="mx-auto my-0 rounded-md"
alt="sad hard disk is sad"
loading="lazy" decoding="async"
src="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_be9bb51dccfe0447.png" srcset="https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_329daaccd12cdf4b.png 330w,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_be9bb51dccfe0447.png 660w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_3706b402a4290099.png 1024w
,https://jnsgr.uk/2024/02/packaging-scrutiny-for-nixos/02_hu_6a8cdfc315c4ae8f.png 1320w
"
sizes="100vw"
/&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all for now! Thanks for reading if you got this far!&lt;/p&gt;</description></item><item><title>Integration testing with NixOS in Github Actions</title><link>https://jnsgr.uk/2024/02/nixos-vms-in-github-actions/</link><pubDate>Sat, 10 Feb 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/02/nixos-vms-in-github-actions/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;While in my own time I&amp;rsquo;ve tended toward NixOS over the past 18 months, in my day-to-day work for &lt;a href="https://canonical.com" target="_blank" rel="noreferrer"&gt;Canonical&lt;/a&gt; I&amp;rsquo;m required to interact with a fair few of our products - and particularly build tools.&lt;/p&gt;
&lt;p&gt;I frequently need to use some combination of &lt;a href="https://snapcraft.io/docs" target="_blank" rel="noreferrer"&gt;Snaps&lt;/a&gt;, &lt;a href="https://juju.is/docs/juju/charmed-operator" target="_blank" rel="noreferrer"&gt;Charms&lt;/a&gt; and &lt;a href="https://ubuntu.com/server/docs/rock-images/introduction" target="_blank" rel="noreferrer"&gt;Rocks&lt;/a&gt;. Each of these have their own &amp;ldquo;craft&amp;rdquo; build tools (&lt;a href="https://github.com/snapcore/snapcraft" target="_blank" rel="noreferrer"&gt;&lt;code&gt;snapcraft&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/canonical/charmcraft" target="_blank" rel="noreferrer"&gt;&lt;code&gt;charmcraft&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/canonical/rockcraft" target="_blank" rel="noreferrer"&gt;&lt;code&gt;rockcraft&lt;/code&gt;&lt;/a&gt;), which are distributed exclusively as Snap packages and thus a little tricky to consume from NixOS.&lt;/p&gt;
&lt;h2 id="the-problem" class="relative group"&gt;The Problem &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#the-problem" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Packaging the tools for Nix was a little repetitive, but not particularly difficult. They&amp;rsquo;re all built with Python, and share a common set of libraries. Testing that the packages were working correctly (i.e. could actually build software) &lt;em&gt;on NixOS&lt;/em&gt; using the &lt;em&gt;NixOS version of LXD&lt;/em&gt; in Github Actions proved more difficult.&lt;/p&gt;
&lt;p&gt;Github Actions defaults to Ubuntu as the operating system for its runners - an entirely sensible choice, but not one that was going to help me test packages could work together on NixOS.&lt;/p&gt;
&lt;p&gt;I could have hosted my own Github Actions runners to solve the problem, but I didn&amp;rsquo;t want to maintain such a deployment.&lt;/p&gt;
&lt;p&gt;For a while I relied on just testing each of the crafts locally before pushing, and the CI simply installed the Nix package manager on the runners (using the &lt;em&gt;excellent&lt;/em&gt; &lt;a href="https://github.com/DeterminateSystems/nix-installer" target="_blank" rel="noreferrer"&gt;Nix installer from Determinate Systems&lt;/a&gt;) and ensured that the build could succeed, but this left a lot to be desired - particularly when I accidentally (and somewhat inevitably) broke one of the packages.&lt;/p&gt;
&lt;h2 id="kvm-for-github-actions" class="relative group"&gt;KVM for Github Actions &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#kvm-for-github-actions" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;Some time later I came across &lt;a href="https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/" target="_blank" rel="noreferrer"&gt;this post&lt;/a&gt; on the Github Blog, stating the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Starting on February 23, 2023, Actions users [&amp;hellip;] will be able to make use of hardware acceleration [&amp;hellip;].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What follows is an example of a relatively simple addition to a Github Workflow to enable KVM on Github Actions runners:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Enable KVM group perms&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; echo &amp;#39;KERNEL==&amp;#34;kvm&amp;#34;, GROUP=&amp;#34;kvm&amp;#34;, MODE=&amp;#34;0666&amp;#34;, OPTIONS+=&amp;#34;static_node=kvm&amp;#34;&amp;#39; | sudo tee /etc/udev/rules.d/99-kvm4all.rules
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sudo udevadm control --reload-rules
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; sudo udevadm trigger --name-match=kvm&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Given the ability to relatively easily create NixOS VMs from a machine configuration, this should enable me to run a NixOS VM inside my Github Actions runners, and use that VM to run end to end tests of my craft packages.&lt;/p&gt;
&lt;p&gt;After some quick tests, I confirmed that the above snippet worked just fine on the freely available runners that are assigned to public projects. After &lt;a href="https://hachyderm.io/@jnsgruk/111449289662026017" target="_blank" rel="noreferrer"&gt;tooting excitedly&lt;/a&gt; about this, it was also picked up by the folks at Determinate Systems who &lt;a href="https://octodon.social/@grahamc/111450168028125913" target="_blank" rel="noreferrer"&gt;promptly added support&lt;/a&gt; for this in their Nix install Github Action - enabling the feature by default.&lt;/p&gt;
&lt;h2 id="building-vms-with-nix" class="relative group"&gt;Building VMs with Nix &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#building-vms-with-nix" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;A really nice feature of NixOS that I discovered relatively late, is that given a NixOS machine configuration &lt;a href="https://gist.github.com/FlakM/0535b8aa7efec56906c5ab5e32580adf" target="_blank" rel="noreferrer"&gt;it&amp;rsquo;s trivial to build a virtual machine&lt;/a&gt; image for that configuration. This has the nice property that one can actually boot a VM-equivalent of any previously defined machines. You could, for example, boot a VM-equivalent of my laptop with the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/nixos-config#nixosConfigurations.freyja.config.system.build.vm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In order to test my craft tools, I needed a relatively simple NixOS VM that had LXD enabled, and my craft tools installed. My test VM &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/f63f315ee2832a112e0777b8af575297c8c9e62d/test/vm.nix" target="_blank" rel="noreferrer"&gt;configuration&lt;/a&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;span class="lnt"&gt;42
&lt;/span&gt;&lt;span class="lnt"&gt;43
&lt;/span&gt;&lt;span class="lnt"&gt;44
&lt;/span&gt;&lt;span class="lnt"&gt;45
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;modulesPath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flake&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# A nice helper that handles creating the VM launch script, which in turn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ensures the disk image is created as required, and QEMU is launched&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# with sensible parameters.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;imports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;modulesPath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/virtualisation/qemu-vm.nix&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Define the version of NixOS and the architecture.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stateVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;23.11&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64-linux&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This overlay is provided by the crafts-flake, and ensures that&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#39;pkgs.snapcraft&amp;#39;, &amp;#39;pkgs.charmcraft&amp;#39;, &amp;#39;pkgs.rockcraft&amp;#39; all resolve to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# the packages in the flake.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;flake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overlay&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# These values are tuned such that the VM performs on Github Actions runners.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;virtualisation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;forwardPorts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;guest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;memorySize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;diskSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10240&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Configure the root user without password and enable SSH.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# This VM will only ever be used in short-lived testing environments with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# no inbound networking permitted, so there is minimal (if any) risk.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# If you put this VM on the internet, you can keep the pieces! :)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;networking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firewall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openssh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openssh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermitRootLogin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;yes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extraUsers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Ensure that LXD is installed, and started on boot.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;virtualisation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lxd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Include the `craft-test` script, ensuring the craft apps are installed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# and included in its PATH.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeShellApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;craft-test&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;runtimeInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;unixtools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xxd&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;snapcraft&lt;/span&gt; &lt;span class="n"&gt;charmcraft&lt;/span&gt; &lt;span class="n"&gt;rockcraft&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;builtins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readFile&lt;/span&gt; &lt;span class="sr"&gt;./craft-test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Anybody can build and launch this VM trivially:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/crafts-flake#testVM
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="writing-a-github-workflow" class="relative group"&gt;Writing a Github workflow &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#writing-a-github-workflow" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;All the building blocks are in place! I wanted to keep the actual workflow definition for the tests as clean and understandable as possible, so I put together the &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/f63f315ee2832a112e0777b8af575297c8c9e62d/test/craft-test" target="_blank" rel="noreferrer"&gt;&lt;code&gt;craft-test&lt;/code&gt;&lt;/a&gt; script as a small helper which automates the building of real artefacts. An example invocation might be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bash craft-test snapcraft
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;On each invocation, the script creates temporary directory, clones some representative build files for the selected craft tool, and launches the craft. The repos it uses for the representative packages are hard-coded for each craft for now.&lt;/p&gt;
&lt;p&gt;I wrote one more &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/f63f315ee2832a112e0777b8af575297c8c9e62d/test/vm-exec" target="_blank" rel="noreferrer"&gt;small helper script&lt;/a&gt; to simplify connecting to the VM with the required parameters. It&amp;rsquo;s a wrapper around &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;sshpass&lt;/code&gt; that&amp;rsquo;s hard-coded with the credentials of the test VM (don&amp;rsquo;t @ me!), and executes commands over SSH in the test VM. Using this script, one can &lt;code&gt;bash vm-exec -- craft-test snapcraft&lt;/code&gt; and the &lt;code&gt;craft-test&lt;/code&gt; script will be executed over SSH in the VM.&lt;/p&gt;
&lt;p&gt;With all that said and done, the resulting &lt;a href="https://github.com/jnsgruk/crafts-flake/blob/f63f315ee2832a112e0777b8af575297c8c9e62d/.github/workflows/test.yaml" target="_blank" rel="noreferrer"&gt;workflow&lt;/a&gt; is pleasingly simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;charmcraft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;rockcraft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;snapcraft&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Checkout flake&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install nix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DeterminateSystems/nix-installer-action@v9&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build and run the test VM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nix run .#testVm -- -daemonize -display none&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Test ${{ matrix.package }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nix run .#testVmExec -- craft-test ${{ matrix.package }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A separate job is run for each of the crafts, and a real artefact is built in each, giving reasonable confidence that the consumers of my flake will be successful when building snaps, rocks and charms natively on NixOS. A successful run can be seen &lt;a href="https://github.com/jnsgruk/crafts-flake/actions/runs/7772604925" target="_blank" rel="noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;In this article we&amp;rsquo;ve covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enabling KVM on Github Runners&lt;/li&gt;
&lt;li&gt;Building NixOS VMs using Flakes&lt;/li&gt;
&lt;li&gt;Booting NixOS VMs in Github Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;d like to build snaps, rocks or charms and you&amp;rsquo;re running NixOS, you can run the tools individually from my flake:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run charmcraft&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/crafts-flake#charmcraft
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run rockcraft&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/crafts-flake#rockcraft
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run snapcraft&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/crafts-flake#snapcraft
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Or you can check out the &lt;a href="https://github.com/jnsgruk/crafts-flake" target="_blank" rel="noreferrer"&gt;README&lt;/a&gt; for instructions on how to integrate into your Nix config using overlays!&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all for now! 🤓&lt;/p&gt;</description></item><item><title>Building a blog with Go, Nix and Hugo</title><link>https://jnsgr.uk/2024/01/building-a-blog-with-go-nix-hugo/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><guid>https://jnsgr.uk/2024/01/building-a-blog-with-go-nix-hugo/</guid><description>&lt;h2 id="introduction" class="relative group"&gt;Introduction &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#introduction" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been procrastinating about blogging for about a decade. Long-form writing is a format I enjoy
consuming, and I&amp;rsquo;ve learned a huge amount from the various blogs I&amp;rsquo;ve subscribed to over the years.
Yet, there have always been a couple of nagging reasons preventing me from starting my own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why would anyone want to read &lt;em&gt;my&lt;/em&gt; blog?&lt;/li&gt;
&lt;li&gt;How would I come up with ideas for content?&lt;/li&gt;
&lt;li&gt;Where would I find the time to write the blog?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Perhaps what&amp;rsquo;s changed recently is a new found enjoyment in some side projects, and the realisation
that I might just write about things &lt;em&gt;for the love of it&lt;/em&gt;, whether or not its directly useful to
anyone else. Of course I&amp;rsquo;d love people find the content useful, engage, etc., but that isn&amp;rsquo;t my
primary motivation.&lt;/p&gt;
&lt;p&gt;The second two points are closely linked, but I ultimately decided they didn&amp;rsquo;t matter. So I present
this blog as a self-indulgence, and something that I&amp;rsquo;ll update when I&amp;rsquo;m excited about writing, and
not feel bad about the rest of the time! 😉&lt;/p&gt;
&lt;p&gt;Being a Software Engineer, I quickly established that it was important to spend time
over-engineering my blog before sitting down and writing any content, and this first post
illustrates that journey &lt;span style="color: #999"&gt;&amp;lt;/sarcasm&amp;gt;&lt;/span&gt;.&lt;/p&gt;
&lt;h2 id="rendering-the-blog" class="relative group"&gt;Rendering the blog &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#rendering-the-blog" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been fond of &lt;a href="https://gohugo.io" target="_blank" rel="noreferrer"&gt;Hugo&lt;/a&gt; for years now (I even named my son Hugo 😉). I&amp;rsquo;ve used it in a few
projects, and I find it to be largely easy to understand and well maintained. My previous site was
built with Hugo, using a theme named &lt;a href="https://jpanther.github.io/congo/" target="_blank" rel="noreferrer"&gt;congo&lt;/a&gt; which I&amp;rsquo;d been underutilising by only creating a
&amp;ldquo;business card&amp;rdquo; style page. I decided to stick with this setup, and just use more of the layouts
provided by the theme.&lt;/p&gt;
&lt;p&gt;In many ways the Hugo site is the most &amp;ldquo;boring&amp;rdquo; part of the site. The source code is all available
in the &lt;a href="https://github.com/jnsgruk/jnsgr.uk/tree/main/site" target="_blank" rel="noreferrer"&gt;&lt;code&gt;site&lt;/code&gt; directory&lt;/a&gt; of the Github repo, but I won&amp;rsquo;t talk much about the site itself in this
blog, as there isn&amp;rsquo;t much more to say!&lt;/p&gt;
&lt;h2 id="serving-the-blog" class="relative group"&gt;Serving the blog &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#serving-the-blog" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;4 years ago, I made the &lt;a href="https://github.com/jnsgruk/gosherve/commit/1df0d8804c57a836b905b4ff2528be995d16631f" target="_blank" rel="noreferrer"&gt;first commit&lt;/a&gt; to a project named &lt;code&gt;gosherve&lt;/code&gt;. This was one of my first
adventures into Go, and I was left with a small, but functional web server that could serve files
from a directory, and serve redirects specified in a publicly accessible text file.&lt;/p&gt;
&lt;p&gt;I chose to host the redirect definitions &lt;a href="https://gist.github.com/jnsgruk/b590f114af1b041eeeab3e7f6e9851b7" target="_blank" rel="noreferrer"&gt;in a Github Gist&lt;/a&gt;. When I want to share a link
frequently, or place one somewhere visible like a slide, I update the Gist with a new alias, and
&lt;code&gt;jnsgr.uk/&amp;lt;alias&amp;gt;&lt;/code&gt; comes online as a handy short link the first time someone requests it.&lt;/p&gt;
&lt;p&gt;Last year I decided to use &lt;code&gt;gosherve&lt;/code&gt; as a tool for learning more about &lt;a href="https://pkg.go.dev/golang.org/x/exp/slog" target="_blank" rel="noreferrer"&gt;&lt;code&gt;slog&lt;/code&gt;&lt;/a&gt; and the Go
&lt;a href="https://github.com/prometheus/client_golang" target="_blank" rel="noreferrer"&gt;Prometheus client&lt;/a&gt;. I did some refactoring that tidied up the logging, and introduced basic
metrics for the number of times each redirect was accessed, how many redirects were defined and the
total number of redirects served.&lt;/p&gt;
&lt;p&gt;For my new blog, I wanted to keep the short URLs I&amp;rsquo;d defined, and I wanted to embed the static site
into the binary to make deployment as simple as possible. I made two small changes to &lt;code&gt;gosherve&lt;/code&gt; to
enable this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jnsgruk/gosherve/commit/9d5e77c67031a944d5193ad37308d08ac82b13e4" target="_blank" rel="noreferrer"&gt;refactor: move packages from internal -&amp;gt; pkg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jnsgruk/gosherve/commit/3f81dd97cdd7c60bf4028443aa7fd743c451425f" target="_blank" rel="noreferrer"&gt;refactor: use fs.FS as webroot rather than path (string)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first change enables the &lt;code&gt;server&lt;/code&gt; and &lt;code&gt;logging&lt;/code&gt; components of &lt;code&gt;gosherve&lt;/code&gt; to be imported as
libraries, and the second enables &lt;code&gt;gosherve&lt;/code&gt; to serve files from a filesystem (and critically, an
embedded filesystem).&lt;/p&gt;
&lt;h2 id="embedding-the-blog" class="relative group"&gt;Embedding the blog &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#embedding-the-blog" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;One of the things I love about Go is how rich the standard library is, and how it can simplify the
creation of small, but powerful applications. The code for my website&amp;rsquo;s server as I write this is
below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:generate hugo --minify -s site -d ../public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;embed&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;github.com/jnsgruk/gosherve/pkg/logging&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;github.com/jnsgruk/gosherve/pkg/server&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dev&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;log-level&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;info&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;log level of the application&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redirectsURL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://gist.githubusercontent.com/jnsgruk/b590f114af1b041eeeab3e7f6e9851b7/raw&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;//go:embed public&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publicFS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fsys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicFS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;public&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;fsys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redirectsURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RefreshRedirects&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;slog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unable to fetch redirect map&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve omitted some comments, imports and logging for brevity here, but the complete file (at 55
lines) can be found &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/6112321824f7b36e7ecb0414b3d7a6c04f13dc4b/main.go" target="_blank" rel="noreferrer"&gt;on Github&lt;/a&gt; for the curious.&lt;/p&gt;
&lt;p&gt;There key elements here are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;//go:generate hugo --minify -s site -d ../public&lt;/code&gt;: this makes sure &lt;code&gt;go generate&lt;/code&gt; invokes Hugo
to build the site and place the output in the &lt;code&gt;public&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;//go:embed public&lt;/code&gt;: embeds the &lt;code&gt;public&lt;/code&gt; directory into the binary as an embedded filesystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="building-the-blog" class="relative group"&gt;Building the blog &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#building-the-blog" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;For the last 18 months, I&amp;rsquo;ve been enjoying &lt;a href="https://github.com/NixOS/nix" target="_blank" rel="noreferrer"&gt;Nix&lt;/a&gt; and &lt;a href="https://nixos.org" target="_blank" rel="noreferrer"&gt;NixOS&lt;/a&gt; for my personal machines, so I wanted
to use my newly acquired knowledge to package and build my website using Nix.&lt;/p&gt;
&lt;p&gt;There is lots of ongoing discussion in the Nix community about &lt;a href="https://nixos.wiki/wiki/Flakes" target="_blank" rel="noreferrer"&gt;Flakes&lt;/a&gt;, which are an experimental
technology aimed at simplifying usability and improving reproducibility of Nix installations. There
are lots of other facets to the discussion which I&amp;rsquo;ll likely touch upon in future posts, but for
now I&amp;rsquo;ll just say that I like Flakes, and they were the obvious choice for packaging this site.&lt;/p&gt;
&lt;p&gt;Packaging a Go application for Nix is relatively simple thanks to helpers like &lt;code&gt;buildGoModule&lt;/code&gt;. I
had to make some minor modifications to accommodate the &lt;code&gt;go generate&lt;/code&gt; step to build the Hugo site,
and patch out some elements of the Hugo site that relied upon access to the local Git tree, but the
resulting derivation remains relatively easy to digest (see &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/6112321824f7b36e7ecb0414b3d7a6c04f13dc4b/flake.nix" target="_blank" rel="noreferrer"&gt;flake.nix&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;buildGoModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jnsgruk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanSource&lt;/span&gt; &lt;span class="sr"&gt;./.&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;vendorHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-4f04IS76JtH+I4Xpu6gF8JQSO3TM7p56mCs8BwyPo8U=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;cacert&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nativeBuildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;hugo&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Nix doesn&amp;#39;t play well with Hugo&amp;#39;s &amp;#34;GitInfo&amp;#34; module, so disable it and inject&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# the revision from the flake.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postPatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; substituteInPlace ./site/layouts/shortcodes/gitinfo.html \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --replace &amp;#34;{{ .Page.GitInfo.Hash }}&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;rev&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; substituteInPlace ./site/config/_default/config.yaml \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; --replace &amp;#34;enableGitInfo: true&amp;#34; &amp;#34;enableGitInfo: false&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Generate the Hugo site before building the Go application which embeds the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# built site.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;preBuild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; go generate ./...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ldflags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-X main.commit=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;rev&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Rename the main executable in the output directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postInstall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mv $out/bin/jnsgr.uk $out/bin/jnsgruk
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mainProgram&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jnsgruk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This defines a Nix package named &lt;code&gt;jnsgruk&lt;/code&gt;, containing a single binary at &lt;code&gt;bin/jnsgruk&lt;/code&gt;. This
binary can be run anywhere to get a working version of this site. You can even try at home with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix run github:jnsgruk/jnsgr.uk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="deploying-the-blog" class="relative group"&gt;Deploying the blog &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#deploying-the-blog" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been hosting my site on &lt;a href="https://fly.io" target="_blank" rel="noreferrer"&gt;Fly.io&lt;/a&gt; without issue for a couple of years. They have a nice feature
that allows you to &lt;a href="https://fly.io/docs/languages-and-frameworks/dockerfile/" target="_blank" rel="noreferrer"&gt;Deploy via Dockerfile&lt;/a&gt;, where their command-line utility &lt;code&gt;flyctl&lt;/code&gt; will send off
a local &lt;code&gt;Dockerfile&lt;/code&gt; to be built on their infrastructure and then launched, and that had been
working great in &lt;a href="https://github.com/jnsgruk/jnsgr.uk/tree/98eed123f5bc111eff481c9a485c158783310478" target="_blank" rel="noreferrer"&gt;previous versions of my site&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wanted to be able to build and run the exact same bits on my own machines as were hosted by Fly,
so I opted to build an OCI image with Nix, then upload that to &lt;a href="https://fly.io" target="_blank" rel="noreferrer"&gt;Fly.io&lt;/a&gt;&amp;rsquo;s registry as part of the
deployment. Adding a container image to the flake as an additional output was simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dockerTools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildImage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jnsgruk/jnsgr.uk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;now&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;copyToRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildEnv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;image-root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt; &lt;span class="n"&gt;cacert&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pathsToLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/bin&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/etc/ssl/certs&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Entrypoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getExe&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jnsgruk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Expose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="mi"&gt;8801&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;10000:10000&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;All that remained was to wire up Github Actions to build and deploy the site each time I make a new
commit. Because the build tooling setup is all handled by Nix, the resulting &lt;a href="https://github.com/jnsgruk/jnsgr.uk/blob/6112321824f7b36e7ecb0414b3d7a6c04f13dc4b/.github/workflows/publish.yaml" target="_blank" rel="noreferrer"&gt;Github workflow&lt;/a&gt; is
quite brief:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;span class="lnt"&gt;34
&lt;/span&gt;&lt;span class="lnt"&gt;35
&lt;/span&gt;&lt;span class="lnt"&gt;36
&lt;/span&gt;&lt;span class="lnt"&gt;37
&lt;/span&gt;&lt;span class="lnt"&gt;38
&lt;/span&gt;&lt;span class="lnt"&gt;39
&lt;/span&gt;&lt;span class="lnt"&gt;40
&lt;/span&gt;&lt;span class="lnt"&gt;41
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Fly Deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy app&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Checkout&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install nix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DeterminateSystems/nix-installer-action@v9&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Login to GitHub Container Registry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;docker/login-action@v3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ github.actor }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nix build -L .#jnsgruk-container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Upload container to ghcr.io&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker load &amp;lt; result
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker tag &amp;#34;jnsgruk/jnsgr.uk:$(git rev-parse --short HEAD)&amp;#34; &amp;#34;ghcr.io/jnsgruk/jnsgr.uk:$(git rev-parse --short HEAD)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; docker push &amp;#34;ghcr.io/jnsgruk/jnsgr.uk:$(git rev-parse --short HEAD)&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy site&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nix run nixpkgs#flyctl -- deploy -i &amp;#34;ghcr.io/jnsgruk/jnsgr.uk:$(git rev-parse --short HEAD)&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;FLY_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.FLY_API_TOKEN }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s the end! You&amp;rsquo;re reading this article as a result of the above workflow succeeding.&lt;/p&gt;
&lt;h2 id="summary" class="relative group"&gt;Summary &lt;span class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100"&gt;&lt;a class="group-hover:text-primary-300 dark:group-hover:text-neutral-700" style="text-decoration-line: none !important;" href="#summary" aria-label="Anchor"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve never had a blog before, but I&amp;rsquo;m looking forward to documenting some of my adventures in
Linux, Software Engineering, Technical Leadership and more over the coming year. Thanks for
reading!&lt;/p&gt;</description></item></channel></rss>