Create a new snap

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.

Requirements

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.

1. Snapcraft setup

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:

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.

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:

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

2. Modify the snapcraft.yaml

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:.

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.

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.

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.

  1. Pull retrieves whatever is required for each part to be built
  2. Build constructs each part using each respective plugins
  3. Stage copies built components into a shared staging area
  4. 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

:information_source: 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.

3. Create an app section

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.

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.

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]
[...]

4. Test the snap

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:

    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

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:

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.

5. Update confinement level

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.

This option -k is not supported on zsh or bash on either Linux or macOS. Not sure if this is a typo, or, otherwise what this is trying to do. I guess it should be removed?

Thanks so much for flagging this. I think it was intended to be -p so that no error is output if the directory already exists. But it’s probably safer to just remove the argument completely (which I’ve done).

I have no recollection that I have written this post before, am I having amnesia or something is wrong with the post’s ownership?

I tried to check the earliest revision of the post content, however it still doesn’t seem to be in my writing style…

You are absolutely right. Over time, your original post has been replaced, and I recently overhauled the whole document (because the Build your first snap was a good starting point in the documentation).

We’ve been doing this generally across the whole set of docs (updating and replacing documentation people have contributed over time), but I can see this may cause issues. You can always see the edit history in the tiny notepad/number icon: imagewhich I think you’ve already used.

If you’d prefer, I can revert this post and put the content elsewhere and in future I’ll try to leave a comment when something is significantly updated.