In this tutorial you will build a snap package for a Python application called liquitctl using Snapcraft, which is the build ecosystem for creating, publishing and maintaining snaps.
The concepts covered in this tutorial are applicable to all snaps, regardless of their complexity. We’ll cover everything from creating the build environment and the configuration file, to troubleshooting missing libraries and which interfaces may be required.
- 1. Snapcraft setup
- 2. Modify the snapcraft.yaml
- 3. Create an app section
- 4. Test the snap
- 5. Update the confinement level
Snapcraft can be installed on various Linux distributions, as well as on macOS and Windows operating systems. For this tutorial, however, we recommend using Ubuntu 22.04 LTS (Jammy Jellyfish) or later.
This tutorial does not require any programming or specific Linux knowledge, but you will need some familiarity with the Linux command line. All the instructions are run as commands from the Terminal application.
You system also needs to have at least 20GB of storage available.
From the terminal, type the following to install Snapcraft:
sudo snap install snapcraft --classic
1.1 Snapcraft build environment
Snapcraft builds snaps within an LXD container environment by default. This keeps a snap build isolated from your system and ensures that any dependencies the snap requires are only provided by the build process.
To install LXD, type the following:
sudo snap install lxd
You also need to add your current user to the
lxd group to give yourself permission to access its resources:
sudo usermod -a -G lxd $USER
Logout and re-open your user session for the new group to become active.
LXD can now be initialised with the ‘lxd init’ command:
lxd init --minimal
See How to install LXD for further installation options and troubleshooting.
1.2 Create a YAML template
Start by creating a new directory to hold the snap data, and then
cd into this directory:
To create a new YAML template for a working snap, run
snapcraft init within this directory:
The YAML template file is called
snapcraft.yaml and it can be found within a new
1.3 Build a template snap
The template file contains enough information to build a snap without any further modifications. This can be accomplished by running the
snapcraft command in the parent directory:
In the background, Snapcraft will create a new LXD container, install into this whatever the template file contains, and build a snap. The output will look similar to the following and the resultant snap can be found in the current directory:
Executed: pull my-part
Executed: build my-part
Executed: stage my-part
Executed: prime my-part
Executed parts lifecycle
Generated snap metadata
Created snap package my-snap-name_0.1_amd64.snap
2. Modify the snapcraft.yaml
snap/snapcraft.yaml file describes the application, its dependencies and how it should be built. It currently contains the following metadata:
name: my-snap-name # you probably want to 'snapcraft register <name>'
base: core22 # 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
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
grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots
# See 'snapcraft plugins'
The above metadata is enough to build a snap, but the snap has no functionality. To create a functional snap, we need expand the
parts: section and add a new section called
2.1 Create a new part
A snap is assembled from one or more parts and each part describes a component required for the snap to function. This component could be a library or an executable, for example, and parts use plugins to construct and organise whatever components are needed.
Our application is built with Python, and Snapcraft includes a Python plugin to automatically handle its dependencies and install requirements.
snap/snapcraft.yaml with your favourite text editor and navigate to the bottom line,
plugin: nil. Replace
python and add the following
This is all that is required for Snapcraft to access, clone locally, and build the upstream source code of the project.
snapcraft again would build the application and create a new snap. However, this new snap would still not function because we have not yet told Snapcraft which executable to expose and run.
2.2 Build the part
A snap is built in several stages, collectively known as the parts lifecycle, as shown in Snapcraft’s build output.
- Pull retrieves whatever is required for each part to be built
- Build constructs each part using each respective plugins
- Stage copies built components into a shared staging area
- Prime moves staged files and directories into their final locations
This is important because you can stop a build at any stage to look inside the build container.
Run the following
snapcraft command to both start a new snap build and run the build up to the prime step. The command will also open a shell within the build environment.
snapcraft prime --shell
If you’ve already built the same snap, run
snapcraft cleanfirst to reset the build environment.
From the build shell prompt inside the container, type
cd $HOME to change to Snapcraft’s build directory, and
ls to see its contents:
environment.sh parts prime project snap stage
These directories hold the data for each build stage, while the
environments.sh file contains the environment variable configuration.
The executable name is
liquidctl, which we can now search for:
$ find . -name liquidctl
The above output shows how the Python plugin has built and installed the executable within the container. The final binary is in
Using the location of the binary, and to permit access, it needs to be declared within an
app: section of the snapcraft.yaml:
If the sub-section name matches the snap name it becomes the default executable for the snap.
This means that when our snap is installed, typing
my-snap-name will run the
bin/liquidctl binary . It’s more usual for a snap name to match the name of the executable.
3.1 Install the snap in developer mode
The snap can now be rebuilt to produce what should be an installable and executable snap package.
snapcraft will produce a snap package called
my-snap-name_0.1_amd64.snap (depending on your system architecture).
Created snap package my-snap-name_0.1_amd64.snap
This snap package can be installed locally with the snap command, invoking both
--dangerous options to permit system access and installation without verification:
sudo snap install ./my-snap-name_0.1_amd64.snap --dangerous --devmode
With the snap installed, the
my-snap-name command can now be run to execute
liquidctl [options] list
liquidctl [options] initialize [all]
To test a snap properly, it needs to be run as intended. The
liquidctl command, for example, accesses USB devices to read and set proprietary sensor, fan and LEDs values.
Even without such devices connected, the
list will attempt to discover any connected devices:
$ my-snap-name list
usb.core.NoBackendError: No backend available
This produces an error, and unless you know the project code, it’s difficult to say from the error whether it’s a problem with the snap, a problem with not having the hardware, or a problem with our test system.
4.1 Missing dependencies
If you’re a Python developer, or reasonably good at searching the internet, it’s relatively straightforward to work out that the
usb.core.NoBackendError issue is caused by a missing
python3-usb package. This can be added through a new
stage-packages section for the part. Stage packages are those packages you wish to be installed alongside the application:
After building and installing the new snap, the error will have gone. If you had any compatible devices, you would now see output similar to the following:
Device #0: Corsair HX750i
Device #1: Corsair Hydro H100i v2
4.2 System access
Running our snap with real hardware will result in an insufficient permissions error and this is because snaps limit system access by default. Interfaces are used to permit access to individual resources through plugs and slots.
Plugs declares which interfaces an app needs to function, such as home to access local files, or network to access the network. In this case, liquidctl needs access to USB devices, which can be satisfied with the raw-usb interface for device input and output, uhid for user access, and hardware-observe to enable the system to see which devices are connected.
These can added with the creation of a new
plugs: sections beneath the command name for the app:
When the snap is installed, the interfaces are can be activated manually with the following commands:
sudo snap connect my-snap-name:uhid
sudo snap connect my-snap-name:raw-usb
sudo snap connect my-snap-name:hardware-observe
The snap can now be run without encountering any further errors or missing functionality.
The final step when building any snap is to change its grade to
stable and its confinement to
strict. Both of these values are at the top of the snapcraft.yaml file and they default to developer-friendly options so that errors only report themselves rather than stop functionality. They’re useful when building a snap but are far less secure when you want to share it.
grade: stable # must be 'stable' to release into candidate/stable channels
confinement: strict # use 'strict' once you have the right plugs and slots
The snap is now fully functional and can be rebuilt and installed. At this point, your own snaps could be published and shared.