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.
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
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.
Start by creating a new directory to hold the snap data, and then cd
into this directory:
mkdir mysnap
cd mysnap
To create a new YAML template for a working snap, run snapcraft init
within this directory:
snapcraft init
The YAML template file is called snapcraft.yaml
and it can be found within a new snap
sub-directory.
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:
snapcraft
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:
Launching instance...
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
The 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
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
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 app:
.
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.
Open snap/snapcraft.yaml
with your favourite text editor and navigate to the bottom line, plugin: nil
. Replace nil
with python
and add the following source-type
and source
lines:
plugin: python
source-type: git
source: https://github.com/liquidctl/liquidctl
This is all that is required for Snapcraft to access, clone locally, and build the upstream source code of the project.
Running 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.
A snap is built in several stages, collectively known as the parts lifecycle, as shown in Snapcraft’s build output.
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 clean
first 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
./project/squashfs-root/bin/liquidctl
./parts/my-part/build/build/lib/liquidctl
./parts/my-part/build/liquidctl
./parts/my-part/src/liquidctl
./parts/my-part/install/bin/liquidctl
./parts/my-part/install/lib/python3.10/site-packages/liquidctl
./stage/bin/liquidctl
./stage/lib/python3.10/site-packages/liquidctl
The above output shows how the Python plugin has built and installed the executable within the container. The final binary is in ./stage/bin
.
Type exit
to quit the build environment shell.
Using the location of the binary, and to permit access, it needs to be declared within an app:
section of the snapcraft.yaml:
apps:
my-snap-name:
command: bin/liquidctl
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.
The snap can now be rebuilt to produce what should be an installable and executable snap package.
Running 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 --devmode
and --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
:
$ my-snap-name
Usage:
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.
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:
stage-packages:
- python3-usb
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
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:
apps:
my-snap-name:
command: bin/liquidctl
plugs:
- raw-usb
- uhid
- hardware-observe
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.
Last updated 5 months ago.