<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Snapcraft on Jon Seager</title><link>https://jnsgr.uk/tags/snapcraft/</link><description>Recent content in Snapcraft on Jon Seager</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 21 Jul 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://jnsgr.uk/tags/snapcraft/index.xml" rel="self" type="application/rss+xml"/><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>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></channel></rss>