How to make your first snap
by Rhys Davies on 8 April 2021
Snaps are a way to package your software so it is easy to install on Linux. If you’re a snap developer already or you’re a part of the Linux community, and you care about how software is deployed, and you’re well versed in how software is packaged, and are tuned into the discussions around packaging formats, then you know about snaps and this article isn’t for you. If you’re anyone who is not all of those things, welcome. Let me tell you how I packaged my first snap to make it easier for people to install on Linux.
Why bother?
Before we get into this, let’s touch briefly on why you might want to bother packaging your application as a snap. If you don’t care about why, skip ahead. Linux distributions change almost as fast as applications are developed. Whether you’re running the latest Fedora release or a years-old Ubuntu release, you should be able to have your favourite applications at your fingertips, and should be able to try the latest and greatest software on release day. Likewise, your user’s choice of Linux distribution should not be a blocker to getting your software in the hands of as many people as you want. Software packaging and distribution can be complex and tiresome.
This is the biggest problem that snaps address. Snaps can be easily installed on any Linux distribution that uses systemd – which is most of them – and developers can integrate packaging snaps with their own CI/CD pipelines. You can take advantage of the snapcraft extensions for specific kinds of middleware, and know that most any language your application is written in is supported in snaps. Once an application is ‘snapped’ it can be available in minutes, across almost all Linux distributions, with a built-in way to keep them up to date, maintained, and promoted to users.
Whether you want to snap your own application or you want to contribute to the ecosystem of applications already out there, snapping the application is worth it, under certain circumstances…
Where to begin?
My experience with software development is very limited so I didn’t start by snapping my own application. Instead, I went looking for an app I liked that hadn’t already been snapped – and snapped it. I started my search on GitHub Trending. This is a good list of applications and projects that are obviously active, so might take contributions, but you really could choose anything. Of course, it does help if the application is your own or you’re an avid user, that way you likely give more of a sh*t.
What makes sense to snap?
One of the most common misconceptions around snaps is that Canonical is trying to replace other ways of packaging software, even when the experience is worse. This simply isn’t true. There are lots of places/instances where it makes no sense to package software as a snap. Snaps are not a replacement for debs, and they are not a competitor to docker containers, despite using containerisation technologies. They are designed to solve different sets of problems that aren’t solved elsewhere. These solutions, and differences, are documented elsewhere, but here are some tips to avoid snapping something that doesn’t need to be a snap:
1. Check the Ubuntu archive – If a Linux application is relatively unknown or unpopular, but is interesting to you, you should make a snap of it. If it’s already in the Ubuntu archive as a deb and has lots of users and is happy, there’s no need to snap it.
Side note: If you’re using Ubuntu you can check whether the app is already in the archive by attempting to run it in the CLI without having it installed. If it’s in the archive, you’ll be prompted to install it as an apt package.
2. Libraries (libs) don’t snap well. Snaps are a way of packaging applications, they shouldn’t be used to package libs unless for a special case.
3. Modern languages – My first snap is an application written in Rust. There are numerous other examples of snaps of applications written in other languages, too. Modern languages don’t come with the same baggage or preconceptions about packaging because the developers are less concerned about packaging. This means, if you’re snapping an app in a modern language you’re more likely to get an appreciative developer on the other end.
However, when I started looking it took me a while to find something that I 1. Understood, given my inexperience 2. Wasn’t already a snap or 3. Fit the criteria. So instead I searched for popular Linux command line tools. (I looked for command line tools because they say they’re easier to snap than a GUI app.) After some poking around I found ‘Googler’, a tool that lets you search Google from the command line. I found a deb and an rpm package in the upstream GitHub repo, but no snap. A good start.
Before I got stuck in, as one last check, I searched ‘googler snap’ (kind of meta huh), to see if the upstream had anything against snaps. They didn’t. But I did find that it was already a snap -_- just not upstream. So I added the project to a doc to remind me it might be worth getting in touch to tell them about the snap in the future, and my search continued.
This happened a few times with a few different apps. Either the snap already existed, or on a couple of occasions, the upstream was less than friendly towards snaps, so I didn’t bother. Snaps are pretty mature by this point and so it’s not surprising that there is a lot of stuff out there already snapped. Or that they have their detractors. But neither are good reasons to stop developers benefitting from snaps when they can. Eventually, I found viu. An image and GIF viewing application. Woo.The end
Can it be built?
Before I got too excited I did some quick checks to see if it can actually be built, easily. Assuming you’ve met all the criteria we talked about earlier, you can almost certainly make it a snap. But given my level of understanding I had a quick run through the snapcraft docs to make sure I wasn’t getting into something horribly complicated. The docs are full of other terms and much deeper technical ‘stuff’ but it was easy to see with a side by side with the viu repo that the complicated stuff wasn’t relevant to me.
Getting started
First, I forked everything in the GitHub repository for the application I wanted to snap. Then I cloned that repository onto my computer, and created a branch. This is standard GitHub workflow stuff but if you’ve not done it before, fear not, GitHub has great documentation too. I did this so that I could work on a snap of the application locally, do my testing and mucking around and then, when ready, I could submit a pull request to the upstream project. Of course, if you’re snapping your own application you can do whatever you like.
Snapcraft YAML
Once cloned and branched I opened a terminal with the VSCode (visual studio code) snap and `cd`’d into the new local version of the repo to make a new directory:
$ mkdir viu-snap
They say good practice is to name the directory ‘application-name-snap’ so it’s easy to find and when you end up making heaps and heaps of snaps, (I know you will) you’re used to the syntax.
Then I went into the folder and ran snapcraft init to create a template in the snap/snapcraft.yaml:
$ cd viu-snap
$ snapcraft init
If you were to go into that new snapcraft.yaml file you’ll see it looks something 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
This is all template stuff that you edit away to make a snap. If you’re looking for more details than I give here, there is lots more information about different parts of the file in the snapcraft documentation and in various blog posts. Let’s start with the metadata (the stuff at the top).
Metadata
If you’re snapping an application that is not yours, it’s best to replace the metadata with data from wherever you found the application, in mycase, the upstream git repo. name
is the published name of your snap so it’s best to make sure it’s descriptive if the application is yours, and correct if it’s not. Typically you want this to be more than three characters. I ran into issues with viu
as you’ll see later.
Choose a base
The default base
is core18
. That means when we build the snap it is done inside an Ubuntu 18.04 LTS VM. When users install the snap they need the core18
snap which installs for them automatically. For viu
I used the base of core20 so it was built in an Ubuntu 20.04 LTS VM. I didn’t give this too much thought; you can always change to another base later if it causes problems.
Next, I added adopt-info
to specify that the version should come from the viu
part (we’ll get to parts in a moment), so I replaced ‘version’
with adopt-info
. The summary and description are both copied from the upstream git repo. At this point my YAML looked like this:
name: viu # you probably want to 'snapcraft register <name>'
base: core20 # the base snap is the execution environment for this snap
adopt-info: viu
summary: Simple terminal image viewer # 79 char long summary
description: |
A small command-line application to view images from the terminal written
in Rust. It is basically the front-end of viuer. It uses either iTerm or
Kitty graphics protocol, if supported. If not, lower half blocks (▄ or
\u2584) are displayed instead.
Confinement and architectures
The next line in the YAML is grade
, this doesn’t affect the stability of the snap itself or interact much once it’s published, it’s so you can signal to yourself and to users that no matter what channel your snap is in whether it’s stable or not. I just set this to stable and carried on.
I wanted this to be strictly confined, so I set that next in the snapcraft.yaml. When things went wrong later I changed this for debugging purposes but it turned out that confinement wasn’t the issue. There are two options here, strict
confinement, where you specify plugs
, and classic confinement, where the application being snapped behaves pretty much the same as any other application on the host.
Then we can add what architectures we want to build for, this is a new stanza that I added after the description:
grade: stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
architectures:
- build-on: amd64
- build-on: arm64
- build-on: armhf
- build-on: ppc64el
Part(s)
They say the parts
of a snap are the most important parts [sic]. This is where we define how to build the software we’re putting inside the snap. viu
is a Rust application that is a supported language in snapcraft, so I specified that plugin.
Then I needed to tell snapcraft the location of the source code, i.e the original GitHub repo that I forked earlier. I think it’s better to specify that one rather than your local version, even for testing, in case you mess something up in the local version. And finally, I needed to list the libraries and dependencies needed to build the application.
viu
only has one part, the viu
project itself, but I still needed to specify a version. When snaps build they need a version number to apply to the file so the name becomes: snapname_version_arch.snap. I did this by adding an override-pull:
section which specifies a script to run at build time. This is what I was left with:
parts:
viu:
plugin: rust
source: https://github.com/atanunq/viu.git
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags)"
The script here uses snapcraftctl pull
to tell snapcraft to pull from the source and then I used snapcraftctl set-version
to call back to adopt-info
earlier and set that to “$(git describe --tags)”
, which takes the version specified at the source. It took me a few read-throughs to understand this too. I could have just added a ‘version: something
’ line in the metadata, but this seemed better.
An unconfined snap or a snap with ‘classic’
confinement would just search for these dependencies on my host system or pull them down from the internet. Because I defined my snap as strictly confined, it can’t do that, and so I needed to list all of the packages it needs so snapcraft knows what specifically to bundle into the snap.
In the YAML we specify these as build-packages:
and stage-packages:
. This was the hardest part of the whole process. I had to go into the projects repo and poke around to see if there’s anything special going on and find the packages they use. Some developers lay all of this stuff out in their README.md files, and some don’t. viu
did not.
Digging for packages
When I was hunting around the internet for the packages I needed I asked for a lot of help. I was told that when developing applications developers make a lot of choices about dependencies and packages and paths in the file system. Unless you developed the application yourself, you obviously weren’t privy to those kinds of decisions. So I had to decide to take the time to understand the application more deeply or just work the puzzle. Assuming its not all laid out in the README.md, here are the tips I accumulated to find the right packages:
- Check if it’s a deb – If the application is in the archive and you want to make it a snap for one of the reasons we talked about earlier then you have a good start. Investigate the deb, see what packages it pulls in and investigate if they’re in Ubuntu (they might have a different name).
- Check the AUR (Arch User Repository) – If the application you want to snap is in Arch Linux then you might just have all the info you need. Search
arch <app name>
in google and if it’s in Arch it’ll be on top of the search, go in there, go to source files on the RHS, click the PKGBUILD if there is one, and you’ll be able to deduce the dependencies and things to include as stage packages.
- Check for travis.yml – If the application’s CI process uses travis then it has a travis.yml file which is a YAML file that contains the programming language used, the build and test environments and the packages/dependencies needed for building.
These files might contain more or less than what you need but are good places to start. In the case of viu
I used the travis.yml
file which specified all the build packages I would need.
For stage packages, I again poked around the repo. Imagemagick is listed in the install instructions so I stuck that in. And then I checked the AUR and in there it showed imagemagick and another dependency, “libxslt
” so I stuck that in stage-packages
too.
Note: I don’t know exactly what each of the build or stage packages actually do, I can make a guess but unless I run into issues I don’t necessarily need to know. I’m hoping I’ll pick that up as I do more of these. What I’m saying is don’t worry if you don’t know all of the nuances of packages. Of course if you’re snapping your own application you likely do already know, well done you.
And we’re left with this. Woo:
parts:
viu:
plugin: rust
source: https://github.com/atanunq/viu.git
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags)"
build-packages:
- build-essential
- libcanberra-dev
stage-packages:
- imagemagick
- libxslt
At this point this might not be correct, I might have picked the wrong packages or missed some, but I’ll find out in the next step. If we have missed something then the search continues, but if I have some things correct it becomes easier to find what’s missing.
Apps and interfaces
At this point, the snap has everything it needs to complete a build, so you could try that, but it probably won’t work. I pointed snapcraft at the binaries and the right packages but because I made this a strictly confined (strict
) snap it doesn’t have access to the outside world. This is so no one can get in and interfere with the application. I allowed the snap to talk to the outside world with an apps
stanza where we expose specific ‘parts’ of the snap to the host.
First I specified the application, then the location of the binary inside that application, and gave it all the interfaces it needs to work. Interfaces in the YAML are called plugs, they are designed such that they ‘plug’ into your snap to safely interact with the outside world. More info can be found in the snapcraft documentation of course. Once I do this a few times I imagine there are a few that are used more than others and I’ll be able to make a good guess at what the app needs without needing the docs. Finally, we are left with this:
name: viu # you probably want to 'snapcraft register <name>'
base: core20 # the base snap is the execution environment for this snap
adopt-info: viu
summary: Simple terminal image viewer # 79 char long summary
description: |
A small command-line application to view images from the terminal written
in Rust. It is basically the front-end of viuer. It uses either iTerm or
Kitty graphics protocol, if supported. If not, lower half blocks (▄ or
\u2584) are displayed instead.
grade: stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
architectures:
- build-on: amd64
- build-on: arm64
- build-on: armhf
- build-on: ppc64el
apps:
viu:
command: bin/viu
plugs:
- network
- home
- removable-media
- alsa
- pulseaudio
parts:
viu:
plugin: rust
source: https://github.com/atanunq/viu.git
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags)"
build-packages:
- build-essential
- libcanberra-dev
stage-packages:
- imagemagick
- - libxslt
What fun. Now, don’t get too excited, get a little bit excited, but not too much, if you get this far, you may or may not be done. But you might be. This might just work and the rest is easy. Deep breaths.
Build and test
To build and test make sure you’re in the right directory – /application-name-snap
, otherwise things don’t work and you get confused and have to ask stupid questions to busy people before you realise your mistake. Once there, run:
snapcraft --debug --shell-after
--debug
means that if the build failssnapcraft
would leave me ‘inside’ the shell of the VM that’s doing the building. That way if it failed I could poke around and do some debugging. This also means if I wanted to try and build the snap again I could just runsnapcraft
again without the extra options to save (a lot of) time since it wouldn’t need to start up and shell in all over again.- Note: The folder you’re working in is mapped to the Multipass VM so it’s possible to continue editing the YAML outside of the VM if you see issues and then re-running
snapcraft
picks up any saved changes.
- Note: The folder you’re working in is mapped to the Multipass VM so it’s possible to continue editing the YAML outside of the VM if you see issues and then re-running
--shell-after
does the same thing except it leaves me inside the shell after its done building if it succeeds in case I want to poke around anyway.
Snapcraft
uses Multipass by default to spin up the VM in which to build your snap. There are other options, for example there is also support for LXD
. So far I’ve only used Multipass because it’s already there and it’s easy but if you’re more familiar with these things or want to try something else, there you go. It’s worth noting that with Multipass, the VM stays up for a while before it shuts down, but it doesn’t die, this caused me confusion. You can run multipass list
to check. LXD doesn’t have the same problem because it spins up and down much faster. It only gets thrown away when you do snapcraft clean.
If the build is successful, snapcraft outputs something like this, but with more fluff:
Launching a VM.
Launched: snapcraft-viu-image-viewer
…
Preparing to …
Unpacking …
Setting up …
Reading package lists... Done
Building dependency tree
Suggested packages:
Recommended packages:
The following NEW packages will be installed:
Pulling viu
Building viu
Rust is installed now. Great!
Staging viu
+ snapcraftctl stage
Priming viu
+ snapcraftctl prime
Snapping |
Snapped viu-image-viewer_v1.3.0-12-g4160c8b_amd64.snap
snapcraft-viu-image-viewer #
You can see I’m still in the shell here, so since I didn’t want to do any rooting around I ran CTRL+D and got out. Because the VM doesn’t go away right away if I were to run snapcraft --debug --shell-after
again it wouldn’t take as long to run.
If you try this and are unsuccessful, it means there’s an issue. Duh. Hopefully, you get a nice helpful error message or maybe there’s an obvious error in the YAML file. If the issue isn’t obvious compare your YAML syntax to mine at the end of this article and open that good old documentation. If you still can’t find the answer, don’t panic, the snapcraft forum is always alive with folks who can help.
Install and test
Once the build finishes successfully we see a file that looks something like this in the working directory:
viu_v1.3.10-gOdba818_amd64.snap
That’s the snap, ready and raring to go. To test it I first needed to install it locally:
snap install viu_v1.3.10-gOdba818_amd64.snap --dangerous
--dangerous
signals that this snap has NOT gone through the typical Snap Store review but I’m okay with installing it because I built it.
This command can be run from anywhere. If for any reason I hadn’t made my snap strictly confined, maybe I was debugging or it’s just not suitable for strict confinement, I could add the --classic
flag. This signals that what I’m installing is an unconfined application which has free reign over everything else on the system. (There are a number of other flags you can find by running snap install --help
).
Once installed I could test it. For viu
, there was a caveat. I needed to have the Kitty or ITerm emulators installed for it to work properly. But here is an example of it working in the Kitty terminal:
rhys@rhys-desktop:~$ viu pikapika.gif
This is where, if the application is not yours, you should exercise some due diligence. Test it properly, poke it, kick the tyres, jump up and down on it with combat boots. Just make sure that it does all the things it’s supposed to do. If you run into errors, take some time to figure them out and correct the YAML. Because once that’s done and you’re confident the thing works, you can register and publish it!
Snap registration and publication
Before publishing it we need to register the snap name with the store. This is a process to make sure there’s no repeats and so that if there are problems with copyrights etc it’s easily fixable. To register a name I ran:
snapcraft register viu
When I ran this for viu
it spat out an issue. The snap store has reserved or at least does not allow three letter named snaps. This is to encourage applications to be more descriptive but also to avoid conflicts in other snap related commands. If the upstream were to want the viu
snap name we could make a case and create the viu
three letter alias, but as I am not the upstream I simply made the snap name ’viu-image-viewer’
instead. Not ideal, but if I was the upstream or the application developer this wouldn’t have been necessary. Shrug.
Once there are no issues and the name is registered I uploaded the snap to the store. The way it’s recommended to do this is to first run the super nifty remote-build
feature in the working directory. If it’s your first time doing this you have to sign in or create an account on launchpad where all the building happens. I had made one of these before for other things so I just moved onto the building bit.
This can take some time, especially if there’s a queue:
$ snapcraft remote-build
All data sent to remote builders will be publicly available. Are you sure you want to continue? [y/N]: y
snapcraft remote-build is experimental and is subject to change - use with caution.
Building snap package for amd64, arm64, armhf, ppc64el, and s390x. This may take some time to finish.
Build status as of 2021-02-03 11:52:34.926743:
arch=amd64 state=Uploading build
arch=arm64 state=Currently building
arch=armhf state=Currently building
arch=s390x state=Currently building
arch=ppc64el state=Currently building
remote-build
does what it says on the tin and, after a little while, (it varies depending on how busy the system is) I was left with a list of snaps. You can check the que or the resulting snaps by going to your launchpad account and checking.
For example, I went to this URL: `https://launchpad.net/~rhys-davies/+snaps` and could see stuff working away. The reason remote build is great is because it does the building in the cloud and just delivers you the binary. So I don’t have to slow my machine down doing all the building myself. And then you end up with a list of snaps:
viu-image-viewer_v1.3.0-10-g0dba818_amd64.snap
viu-image-viewer_v1.3.0-10-g0dba818_arm64.snap
viu-image-viewer_v1.3.0-10-g0dba818_armhf.snap
viu-image-viewer_v1.3.0-12-g4160c8b_s390x.snap
viu-image-viewer_v1.3.0-12-g4160c8b_ppc64el.snap
These could then be uploaded with this little loop Alan shared in his blog post:
$ for f in viu_v1.1.0_*.snap; do snapcraft upload $f --release=candidate; done
This is a for loop that looks at each file in the directory that starts with viu_v1.1.0_
and ends with .snap
, runs snapcraft upload
on it and puts it into the ‘candidate’ release channel. The release channels are different places to publish snaps to. The idea is that you put the snaps that are ready for general use in the stable channel, for user testing in the edge channel, and for dedicated testing in the candidate channel. The last line specifies that snapcraft
is using the review-tools
command to do some checks on the snap before it gets uploaded. This is a separate snap I installed with:
snap install review-tools
The Snap Store would run these checks when I uploaded the snaps anyway but if the store found a problem it would take some time to let me know and I’d have to go through the process all over again. It’s easier to find out before things get uploaded just by having this installed.
Assuming there are no issues, each snap gets uploaded to the candidate channel. Since the remote builder feature gave me a snap for each architecture I specified earlier I could then go and test that the app works on any other platforms or distributions that I’m interested in. Ask your friends, ask around on the forum, maybe you can ask the upstream? And once you’re happy with that too you can promote the snap to the stable channel for the whole world to see.
The Snap Store
Before making your snap stable you should do some due diligence to test the thing properly, and there’s a couple of housekeeping notes they are recommended, too.
1. Linking everything up to a GitHub repo and connecting it to the build process. This way each time the application is updated in GitHub a new release is built and pushed to the edge channel in the store. To do this, I logged into my snapcraft developer account, selected the viu
snap and went to the ‘Builds’ tab and clicked the big green button:
From here I was able to select the repo I forked earlier and link it up to the snapcraft build system. With this set up I could review and in turn promote these pushes in my developer account. At any time I can now click the ‘Trigger new build’ button and force a new build. The first time you do it looks something like this:
2. Take the time to give your snap a nice landing page. You can do this in your developer account by going to the ‘Listing’ and ‘Publicise’ tabs. Here are some examples of snaps with good landing pages, that’s what you’re aiming for. If you have the time. It’s completely not mandatory. Good looking snaps are also more likely to get “featured” on the front page of snapcraft.io. A snap that is featured in the snap store grows users significantly.
You can fill out all the basics, add some images and videos of the snap working or link the app up to your GitHub or social accounts for users to have a look at too. There are lots of things you can do here that will make your snap more successful and more likely to attract more users. But there’s a whole other article about that so I won’t go into detail here.
The end of my first snap
That’s it. Time for some cake. If you followed this process for your own application, congratulations, users can now install and run your application on most any Linux distribution with a single command. If you’ve done what I’ve done and snapped someone else’s application then we both have one last thing to do. File an issue and make a pull request with the upstream project to let them know. Hopefully, they’ll be happy and might even take over maintenance of the application and adopt it into their workflow. Of course, they are not obliged to at all.
Everything we’ve covered here feels like a lot, I know it’s a long bloody article, but hopefully, you can see it’s mostly straightforward. The hardest part is debugging but if you have a good understanding of the application or time enough to find help on the forum, even that can be easy. Heck, if I can do it, believe me, you can too. Happy snapping.
This blog was originally posted on Rhys’ personal blog in case you’re interested in seeing the ever so slightly different version over there.
Here’s the final YAML:
name: viu-image-viewer # you probably want to 'snapcraft register <name>'
base: core20 # the base snap is the execution environment for this snap
adopt-info: viu
summary: Simple terminal image viewer # 79 char long summary
description: |
A small command-line application to view images from the terminal written
in Rust. It is basically the front-end of viuer. It uses either iTerm or
Kitty graphics protocol, if supported. If not, lower half blocks (▄ or
\u2584) are displayed instead.
grade: stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
architectures:
- build-on: amd64
- build-on: arm64
- build-on: armhf
- build-on: ppc64el
apps:
viu:
command: bin/viu
plugs:
- network
- home
- removable-media
- alsa
- pulseaudio
parts:
viu:
plugin: rust
source: https://github.com/atanunq/viu.git
override-pull: |
snapcraftctl pull
snapcraftctl set-version "$(git describe --tags)"
build-packages:
- build-essential
- libcanberra-dev
stage-packages:
- imagemagick