Snap Along With Me

Every so often I find myself with an idle hour and decide to use that time to package some new software for Linux. A common activity among nerds, I’m sure ;). This blog post is a write up of what I did, and why, which may be useful to others with time on their hands.

I keep meaning to live stream when I do, but on this occasion I had a bad hair day was also listening to and engaging with a podcast, so it wasn’t practical. In leiu of that, while the information is fresh in my head - the morning after - I thought I’d write down some notes here.

Why do this

My Linux distro of choice is Ubuntu. A significant number of Ubuntu users are on “old” Long Term Support (LTS) releases. Some estimates put it at around 10x or 20x more users are on Ubuntu LTS releases than the interim non-LTS releases. It’s hard to get new software into the repositories of old Ubuntu releases. While it’s possible for developers to create PPAs or repositories for their software, that’s hard for new users to discover. If I make and help publish a snap for something new, that can get in users hands within hours (or even minutes), with updates coming as they’re published. I wrote a (work) blog post titled “Snaps: How we got here” if you want to know more about “why?”.

Selecting a target

There’s always something to package. Out there a new piece of software has been published, or an old program needs preservation. Perhaps someone explicitly asked for help, or there just something I think is cool, that should be easier to install for Linux users.

I use f5bot which notifies me via email when the keyword ‘snapcraft’ is mentioned on Reddit, HN or Lobsters. I’ll also get pinged by a number of friends online when they discover something and think it should be snapped. You know who you are, thanks for keep my “fun software pipeline” full. :) Sometimes I’ll browse the GitHub Trending page to see what’s new in the world. That’s a great way to find new stuff, especially

On this occasion it was a mention in a Telegram channel I saw scroll past.

Ping

Investigation

Some things make sense to snap, others less so. For example, a ’leaf’ application which is relatively new, fast moving, has complex installation instructions (for novices) might be a good candidate. A library which depends on specific kernel modules, and has esoteric hardware requirements might be less interesting, difficult, or flat out impossible.

Indeed some classes of applications are easier to package as a snap than others. Command-line utilities written using ‘modern’ languages like Go and Rust are way eaiser than large graphical applications built using bleeding-edge versions of Qt or GTK. If I only have an hour to spare, I’m likely to go for easy pickings.

From a political point of view, I’m also unlikely to touch an application where the developer has previously expressed that they don’t want their software packaged in such a way. No point wasting time packaging something if a pull request is going to be rejected. Most developers aren’t that way inclined though, and welcome having their software in more hands.

Reddit

In this case the application - t-rec - is new (to Linux), is actively developed, and the developer specifically asked for help in packaging. The fact that it’s made using Rust is a bonus here. All this was gleaned in a few moments by browsing the repo. I’ve not technically committed to this project yet. This is important to note, because I will often look at something, and reject it because it will take to long, or maybe it’s a commercial application, or has some weirdness that means it’ll be a timesink. This wasn’t one of those cases, so I moved on to snapping it.

“But Alan, it’s Rust, anyone can just use curl to install rustup, then get rust, the build dependencies, and build the software from source themselves”.

You might do that, millions of others won’t / can’t / don’t.

Getting started

Fork - clone - branch

Step one! Fork!

The repository is on GitHub, so my first step is to fork the repo, clone it to my laptop and create a branch. All standard GitHub workflow stuff.

Aside: I don’t always do this. Sometimes I’ll start by creating a folder locally, and build the snap by pointing it at the upstream source. If I’m confident I can prep a PR and have it pushed in short-order, I’ll go the fork-clone-branch route. If I think this might take longer, and be something I will have to publish myself (under my own name) then I’ll build locally without forking. Maybe I’ll do a separate tutorial for that sometime.

Create a snapcraft.yaml

snap/snapcraft.yaml is the file that drives the snapcraft tool to build and package the snap.

Inside the project folder, I’ll just snapcraft init which looks like this:

$ snapcraft  init
Created snap/snapcraft.yaml.
Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more information about the snapcraft.yaml format.

The initial yaml looks like this:

name: my-snap-name # you probably want to 'snapcraft register <name>'
base: core18 # the base snap is the execution environment for this snap
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
  This is my-snap's description. You have a paragraph or two to tell the
  most important story about your snap. Keep it under 100 words though,
  we live in tweetspace and your description wants to look good in the snap
  store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

We’re going to almost completely replace this, but it’s a nice starter, as it reminds of the minimum bits needed to build a snap.

Metadata

This is the easiest bit! I just yoink the details from the upstream repository. Typically this is copy/paste territory. The one you need to be careful of however, is the name:. This is the registered name of the application in the store, and shouldn’t already be taken by someone else. I didn’t actually check, but t-rec seemed pretty unlikely to be used already. (I have subsequently checked by running snapcraft register t-rec and it wasn’t taken (but is now :))).

name: t-rec
summary: Terminal Recorder - t-rec
description: |
  Blazingly fast terminal recorder that generates animated gif images for the web written in rust.

We also have adopt-info which I’ll come back to below.

adopt-info: t-rec

Choose a base

The default base is core18. That means when this snap is built it will do so (by default) inside an Ubuntu 18.04 LTS VM (or container). When the user installs the snap they’ll need the core18 snap too (which will be installed automatically) which contains a super minimal (~ 58MiB) Ubuntu 18.04 base system.

Ubuntu 18.04 LTS is supported, but it’s coming up on 3 years old now. So I decided to go for core20 for my base. There’s a few side-effects of this. Firstly, logically perhaps, it will build the snap inside an Ubuntu 20.04 LTS VM or container, not Ubuntu 18.04 LTS. On the end user system, they’ll need the minimal (~ 64MiB) core20 to match - which again, will be automatically installed for them.

A second effect of choosing core20 is that snapcraft (the tool) has had some behavioural changes, and updates to features between core18 and core20 based snaps. There are a few features missing in snapcraft when building a core20 snap as opposed to a core18 based snap. One example is the currently missing “GNOME Extension” - but I don’t need that because this is a command-line utility, not a GTK / GNOME application.

Basically core18 is recommended. I’m only doing this because I know I don’t need anything that’s currently ‘missing’ from snapcraft for core20.

base: core20

Confinement

Snaps have (simply) two confinement models. strict confinement in which you (as the publisher) specify a set of plugs which interface the application to resources on the system such as camera, x11, wayland, audio-playback and so on. classic confinement is (essentially) unconfined, so the application being snapped behaves much like any other binary on the host, unrestricted by apparmor or cgroups as strict snaps are.

We usually urge developers to aim for strict confinement. That’s not going to work for t-rec because the nature of the application requires it to be able to run any arbitrary binary on the system. strict confinement would block this, making the application pretty much useless.

confinement: classic

If this snap works, and the developer accepts the PR, and decides to publish it in the store, then they’re going to have to request classic confinement. You can’t just make a classic snap and publish it in the stable channel of the Snap Store, the review tools on the backend won’t allow it. There’s a process for vetting applications and publishers, which the

Future Alan with a spoiler here: The upstream publisher accepted the PR, pushed the snap to the Snap Store, and has requested classic confinement. Once Canonical sleep off the new year, they’ll/we’ll action that.

Grade

Just set this to stable and we’ll move on. I personally wish this would go away. Just set it and forget it, that’s what I do.

grade: stable

Part(s)

Ok, here’s where we get to the meat and vegetables of the snap. This is where we define how to build the software we’re planning to put inside the snap.

t-rec is a Rust application, which snapcraft knows how to build. So we just specify the rust plugin, location of the source files - . - because we’re sat in the repo.

Recall earlier where I mentioned sometimes I create a folder and build the snap locally without forking. In that case I’d set source: https://github.com/sassman/t-rec-rs.git, and at build time snapcraft would get the source directly from the main branch (or whatever source-tag or source-branch I specify).

parts:
  t-rec:
    plugin: rust
    source: .

You can run snapcraft help rust to learn more about that rust plugin.

The upstream developer has a nice README.md which details the additional requirements needed when building the snap, and things required at runtime. It’s a pretty minimal list, which is neat.

sudo apt-get install libx11-dev imagemagick

Isn’t it handy that developers use Ubuntu to build their software ;). I don’t need to translate package names from some other distro here!

Note that one is a -dev package - which will be required at build-time, the other is a toolkit of image manipulation utilities, which I guess we need at runtime to create the .gif this application is designed to make. So we specify those like this:

    build-packages: 
      - libx11-dev
    stage-packages: 
      - imagemagick

At build time snapcraft will pull in libx11-dev in the build VM (or container) to ensure t-rec builds okay. It will also stuff imagemagick into the final snap, so t-rec can find it.

Set version

When the snap builds, it needs a version number to stamp on the file. A typical snap filename looks like snapname_version_architecture.snap. So mine will end up as t-rec_v0.4.0_amd64.snap, hopefully. There’s a variety of ways to enumerate the version number. I’m going to just use the git tag. We do this in the override-pull: section, which is just a shell script that gets run at build time by snapcraft just prior to the actual build itself. We use snapcraftctl pull to tell snapcraft to just do whatever it does to pull the source for this part specified in source: earlier, then we jump in and set the version for the project. This is consumed by adopt-info up the top of the yaml metadata.

    override-pull: |
      snapcraftctl pull
      snapcraftctl set-version "$(git describe --tags)"

Build overrides

Sometimes developers have weird build systems, or sets of commands that need to be run, which diverge from a typical rust, cmake, or autotools plugin expectations. This is fine, because we can override the build process, just as we can override the pull stage. In this case I’m going to create a symlink because t-rec expects a binary called usr/bin/convert to exist, which (for whatever reason) doesn’t happen when we staged imagemagick earlier. I use an environment variable set inside snapcraft at build-time called SNAPCRAFT_PART_INSTALL which points to the internal directory where the final assets for this part are assembled. So we run the build as per the plugin expectations with snapcraftctl build then we jump in after and ninja our symlink.

    override-build: |
      snapcraftctl build
      cd $SNAPCRAFT_PART_INSTALL/usr/bin
      ln -s ./convert-im6.q16 ./convert

I discovered this after doing a successful build, but the application would error, saying it couldn’t fine usr/bin/convert. I did a quick find /snap/t-rec/current on the installed snap (although less t-rec_v0.4.0_amd64.snap would also have worked) to see what files were delivered. Perhaps convert was in another location. Then I remembered that the convert binary on Debian based systems points to /etc/alternatives/convert which in turn points to /usr/bin/convert-im6.q16:

alan@robot:~$ which convert
/usr/bin/convert
alan@robot:~$ ls -l /usr/bin/convert
lrwxrwxrwx 1 root root 25 Aug 17 14:54 /usr/bin/convert -> /etc/alternatives/convert
alan@robot:~$ ls -l /etc/alternatives/convert
lrwxrwxrwx 1 root root 24 Aug 17 14:54 /etc/alternatives/convert -> /usr/bin/convert-im6.q16

When the imagemagick deb is pulled into the snap, this dance doesn’t happen. So I worked around that with the symlink.

Build it

Iterate

At this point I typically try building the snap - even though the yaml isn’t actually finished yet. More on that below. But for now, let’s try and build the snap.

Note: By default snapcraft will use Multipass. I prefer to use LXD. I already have lxd configured on my desktop, but if I didn’t then snap install lxd and sudo lxd init --auto followed by logging out/in (to add me to the lxd group) usually works for me.

Ok, from the repo directory I run this:

snapcraft --use-lxd --debug --shell-after
  • --use-lxd - will spin up a container using lxd. The OS release will match the base specified earlier, in this case Ubuntu 20.04 LTS.
  • --debug - if the build fails for whatever reason, snapcraft will leave me ‘inside’ a shell in the container (or indeed the VM if using multipass) in which I can poke around to debug why it failed. I can also re-run snapcraft inside the container which saves a little time as it won’t need to start it up again then shell in. The project repo folder is mapped into the container (or VM) so it’s possible to continue editing the yaml from ‘outside’ the environment, and iterate by re-running snapcraft on the ‘inside’. I find this a super quick way to work on a snap.
  • --shell-after - is exactly the same as --debug but it always drops you to a shell inside the container, even if the build was successful. I do this because sometimes I want to poke around and see what built even if it worked fine.

Here’s some of the end of the output of it running the build:

   Compiling crossbeam-deque v0.8.0                
   Compiling rand_chacha v0.2.2
   Compiling rand v0.7.3   
   Compiling tempfile v3.1.0
   Compiling x11rb v0.7.0 
   Compiling jpeg-decoder v0.1.20
   Compiling tiff v0.6.1    
   Compiling image v0.23.12 
    Finished release [optimized] target(s) in 1m 04s
  Installing /root/parts/t-rec/install/bin/t-rec
   Installed package `t-rec v0.4.0 (/root/parts/t-rec/build)` (executable `t-rec`)                                  
warning: be sure to add `/root/parts/t-rec/install/bin` to your PATH to be able to run the installed binaries       
Staging t-rec                 
Priming t-rec              
Snapping |                                                                                                                                                                                                                              
Snapped t-rec_v0.4.0_amd64.snap
snapcraft-t-rec #

Note here I’m left in the container, which is named snapcraft-(snapname). Just CTRL+D to drop back to my main machine.

snapcraft-t-rec # exit                                                                                              
alan@robot:~/Source/popey/t-rec-rs$ 

Nearly there

Ok, so we have a snap which builds, but it won’t actually work! That’s because despite building a t-rec binary, and bundling some binaries which came with imagemagick, we haven’t exposed them to the world outside the snap. We do that with an apps stanza. We only really need to expose one binary t-rec from the snap to the host.

apps:
  t-rec:
    command: bin/t-rec

I didn’t specify any plugs: for this command because it’s a classic confined snap, so is effectively unconfined. If it were strictly confined I’d consult the documentation to identify the right interfaces to make the application function while confined. Not needed here.

The app name matches the snap name. As such, once installed a binary called t-rec will appear in /snap/bin which will setup the environment and run this rust binary we built. If we wanted to (for some reason) expose the imagemagick usr/bin/convert binary to the outside world we could do this:

  convert:
    command: usr/bin/convert

However, as that command doesn’t match the name of the snap, and may collide with another binary on the host of the same name, it gets namespaced on install. So that would be t-rec.convert. I don’t need to expose that binary, so I didn’t do this.

With the apps: stanza added, we re-run snapcraft as above, and we get a successful build.

Install & Test

Installing locally is done with:

snap install t-rec_v0.4.0_amd64.snap --dangerous --classic
  • --dangerous - is you acknowledging that this snap didn’t go through the Snap Store review process. It could just as easily have been handed to you on a USB key, or maybe I downloaded it from a random URL. I’m saying I trust it, given I built it.
  • --classic - is you acknowledging that this is an unconfined application, and as such has free reign over whatever your user is able to do. Just like binaries you install from a PPA or the Ubuntu repository ;).

I tested the application by opening a new terminal, ran the application for the first time and got this:

t-rec

Narrator: It worked.

Submit PR

Here’s the final yaml I submitted as a pull request to the upstream project.

name: t-rec
base: core20 
adopt-info: t-rec
summary: Terminal Recorder - t-rec
description: |
  Blazingly fast terminal recorder that generates animated gif images for the web written in rust.

grade: stable
confinement: classic

parts:
  t-rec:
    plugin: rust
    source: .
    override-pull: |
      snapcraftctl pull
      snapcraftctl set-version "$(git describe --tags)"
    override-build: |
      snapcraftctl build
      cd $SNAPCRAFT_PART_INSTALL/usr/bin
      ln -s ./convert-im6.q16 ./convert
    build-packages: 
      - libx11-dev
    stage-packages: 
      - imagemagick

apps:
  t-rec:
    command: bin/t-rec

Conclusion

Once the upstream developer has done the necessary dances to get classic confinement approval and publish to the Snap Store, the t-rec snap will be available for anyone running Ubuntu 14.04 through 20.10 and a ton of other distros too, with a simple snap install t-rec. I did see an amusing suggestion on reddit that they should have called it t-recs, which does indeed sound better. If they want to do that, there’s only a few simple changes required in the yaml above.

These rust-based snaps are super fun to make. I cloned the repo at 19:32 last night and pushed the PR at 20:21. Not bad given I was also chatting on a podcast at the same time.

Hope that’s interesting to some of you. Happy snapping. 🥰