Craft a snap

In this tutorial, we’ll build a snap package for a Python app called pyfiglet. The concepts we’ll cover transfer to both simple and complex snaps. We’ll cover everything from creating the build environment and the configuration file, to troubleshooting missing libraries and finding which interfaces to open.

It should take 20 minutes to complete.

You won’t need to come prepared with deep knowledge of software packaging, but familiarity with Linux paradigms and terminal operations is required.

Once you complete this tutorial, you’ll have experience hand-crafting snaps that serves as the basis for further work with creating snaps.

Lesson plan

Put simply, this tutorial is a run-through of the process of constructing a snap. We show you how to:

  • Start a snap project from scratch

  • State the project’s essential information

  • Define the project source files and main program

  • Package the snap

  • Modify the build process to enhance the snap’s contents

  • Share root files with the snap

What we’ll work with

The object of this tutorial is to package pyfiglet as a personal test snap. It’s a lightweight app for displaying text as ASCII art, and is simple to build and test.

The snap will be named ukuzama-pyfiglet, after a fictional user. Throughout this course, replace ukuzama with your own username.

What you’ll need

For this tutorial, you’ll need:

  • An x64 system running Ubuntu 22.04 or Ubuntu 24.04

  • A local user with super user privileges

  • 20GB of free storage

Install Snapcraft and LXD

Before you install

If you have a Docker installation, you might run into conflicts with LXD over the course of this tutorial. As a remedy, you can let Snapcraft build with Multipass instead. To do so, start a fresh terminal session and run:

SNAPCRAFT_BUILD_ENVIRONMENT=multipass

Then, proceed to Begin the project.

Snapcraft is itself available as a snap. Let’s begin by installing it. In a terminal, run:

snap install snapcraft --classic

Next, let’s add LXD to your system. It functions as the build provider and containerizes the build environment.

snap install --channel 5.21/stable lxd

You also need to add your user account to the lxd group so you can access the tool’s resources:

sudo usermod -a -G lxd $USER

Log out and back in to your account for the new group to become active. Then, check that you’re a member of the group by running:

groups $USER

Look for lxd in the output.

Finally, initialize LXD with a lightweight, default configuration:

lxd init --minimal

Begin the project

Every snap project resides in its own directory. Start by creating one in a development space on your system:

mkdir ukuzama-pyfiglet
cd ukuzama-pyfiglet

A snap is defined in a declarative snapcraft.yaml file, called the project file. By constructing the file key by key, we’ll be building the snap’s particulars. Snapcraft has an init command that spawns a template project file. Let’s start with that:

snapcraft init

From here onward, we’ll be working primarily in the project file. Open snap/snapcraft.yaml in a text editor.

Define the package information

The first section inside a snap project file is typically its package information, sometimes informally referred to as its metadata. This information tells both humans and machines about the snap, such as its purpose, authors, license, and so on. The comments in the template describe how to use these keys.

Replace the top section with:

snapcraft.yaml
name: ukuzama-pyfiglet
base: core24
version: "0.1"
summary: pyfiglet is a program for making large letters out of ordinary text
description: |
  pyfiglet is a full port of FIGlet (http://www.figlet.org/) into pure python.
  It takes ASCII text and renders it in ASCII art fonts (like the title above,
  which is the 'block' font).

  This snap is not endorsed by the pyfiglet project.

As this is a personal snap, we prepended the project name with a user name. Replace ukuzama with your own user name. You might encounter other snaps in the Snap Store with naming that follows this pattern – it’s the recommended format, to keep personal copies of snaps distinct from their originals.

We didn’t alter the base key, since we want our project to be built on top of the latest Ubuntu LTS release.

Since we’re packaging a project authored by someone else, we ought to respect their intentions and thinking in the description keys. So, we reused them. The summary is a short description with a hard limit of 79 characters, but nothing like that is present in the pyfiglet README. Instead, we sourced one from the upstream project at figlet.org. The fuller description key, which has no length limit, is taken from pyfiglet. We added a disclaimer about endorsement at the end.

Define the target platforms

As we’re building on an x64 system, and we’re only running basic tests for this tutorial, we should constrain our snap to only build on the current CPU architecture, AMD64. At a later point, when you’re able to test on other platforms, you could widen the coverage in this configuration.

Add the platform key after the project information:

snapcraft.yaml
platforms:
  amd64:

With this declaration, Snapcraft will only build the snap on AMD64 machines, for AMD64 machines. Take care to preserve the colon (:) in amd64:.

Define the main part

A part is either a piece of software that we want to build from source or a clump of files. In either case, the purpose of a part is to bundle files from a source into a snap. Usually they’re version-controlled components of the project itself, but you can aggregate them from a variety of locations.

For the parts key, add an entry for our main part, the pyfiglet source code:

snapcraft.yaml
parts:
  pyfiglet:
    plugin: python
    source-type: git
    source: https://github.com/snapcraft-docs/pyfiglet

Parts have three important keys worth discussing.

We set plugin to python because pyfiglet is a Python project and we want to build it from source.

We set source-type to git because the project is stored as a Git repository.

We set source to the remote location of the project. Some software projects take responsibility for their own snaps, and store their own snapcraft.yaml file in the source code. With pyfiglet, we’re merely handling the packaging on the project’s behalf, meaning our project file is downstream of and dependent on it. By pointing to a remote URL, Snapcraft will download the source before it packs the snap.

Pack the snap

We have what we need for a basic snap build. Let’s see what happens when we pack the snap:

snapcraft pack

After a few seconds, the final result is:

Packed ukuzama-pyfiglet_0.1_amd64.snap

That means the snap completed successfully. You’ve already built your first snap!

But we’re not done with it or the pack command yet. You’ll find that every time we iterate on the snap, we repack it.

Inspect the result

Let’s take a look inside the snap to see what happened. At any point in crafting a snap, we can run an interactive shell inside the build container to inspect what Snapcraft has done with the files.

There are many steps and actions that go into packing a snap, so for now let’s focus on its final contents before it’s compressed into a .snap file. Let’s repack the file but halt Snapcraft before it finishes:

snapcraft pack --shell
cd ~/prime

Adding --shell popped us into an interactive shell inside the build container. In here, we can look around, and even touch files, like we were putting the snap together by hand.

The prime directory contains the state of the final files before they’re packed. If we take a look at what’s inside, we’ll see:

/root/prime├── bin   ├── Activate.ps1   ├── activate   ├── activate.csh   ├── activate.fish   ├── pip   ├── pip3   ├── pip3.12   ├── pyfiglet   ├── python -> python3   ├── python3 -> /usr/bin/python3.12   ├── python3.12 -> python3   └── wheel├── include   └── python3.12├── lib   └── python3.12       └── site-packages                      <many dependencies>├── lib64 -> lib├── meta   ├── gui   └── snap.yaml└── pyvenv.cfg

For a Python project, there’s nothing in here that’s out of the ordinary. We see the constituent pieces, comprising the binary executables for the command itself, the dependency packages, and some utility files from the build and from Snapcraft. This is the state of the project as if we had built it ourselves with standard Python tooling.

If we were running into any file pathing problems with the snap, the interactive shell would be the ideal way to investigate.

Everything looks good, so let’s exit the build container:

exit

Define the app

If we were to install the snap we just built, it wouldn’t do anything. That’s because we need to define the snap’s apps – its programs that run as processes and services on the host.

In the project file, the apps key decides all of the snap’s apps. Pyfiglet has one main program – the /bin/pyfiglet file we saw earlier.

Add the following after the parts section:

snapcraft.yaml
apps:
  ukuzama-pyfiglet:
    command: bin/pyfiglet

As this is the main app – in other words, the command we want to run when the user calls the snap by name – it should match the snap name.

The core of an app entry is its command key, which is the shell command that the snap calls on the host. It’s a path to an executable inside the snap, and can contain arguments. It isn’t strictly tied to any binary built by the snap. It could instead be, for example, a combination of POSIX-compatible commands.

Test the snap

Let’s repack the snap and try running it. First, run snapcraft pack again.

Then, install the snap locally:

snap install ukuzama-pyfiglet_0.1_amd64.snap --devmode --dangerous

Normally, snapd prevents us from installing snaps that aren’t vetted or confined. But, if we tell it we’re comfortable with installing a snap with full system access and that doesn’t declare itself as stable – a potentially dangerous decision – it will install. While we’re crafting and especially debugging snaps, it’s easiest to install them with these flags.

At long last, let’s try running our snap.

ukuzama-pyfiglet hello, world!

You should see the successful result:

_          _ _                             _     _ _| |__   ___| | | ___    __      _____  _ __| | __| | || '_ \ / _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | || | | |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_||_| |_|\___|_|_|\___( )   \_/\_/ \___/|_|  |_|\__,_(_)                    |/

Pyfiglet can draw with different typeface styles, too. It’s a fun little command.

crafter@home:~$ ukuzama-pyfiglet -f smscript ciao, mondo!
_  o  _,   _              _         _|   _  |/   | / |  / \_   /|/|/|  / \_/|/|  / |  / \_|\__/|/\/|_/\_/o    | | |_/\_/  | |_/\/|_/\_/ o              /

Clean the build container

Before we continue, we should perform some pre-emptive housekeeping.

As we progress through a build, the contents of the build container can become dirty, and eventually cause conflicts or break the build. It’s a good idea to periodically flush the container for the next build:

snapcraft clean

Override the main part’s build

If you inspect the source files in the pyfiglet source code, you’ll notice that the font files are split into two directories, with the second directory containing all fonts with unaccounted licenses. The project has a separate Make recipe for combining these two directories. We committed to the project’s Python build, so we can’t access this second set of fonts. Since we’re making this snap for personal testing purposes, let’s see if we can preserve all the fonts in the snap.

If we poke around further in the source code, it becomes clear that to copy all the unaccounted font files we must copy them into the build’s fonts directory. This means we’ll need to intrude on the regular build process of the pyfiglet part.

Think back to when we entered the build container. While inside, we could have created, copied, or moved any files as we saw fit. With a build override, we can make manual adjustments of that sort to the build, but through the project file.

Add the following override-build key to the pyfiglet part:

snapcraft.yaml
parts:
  pyfiglet:
    plugin: python
    source-type: git
    source: https://github.com/snapcraft-docs/pyfiglet
    override-build: |
      mkdir pyfiglet/fonts
      cp pyfiglet/fonts-contrib/* pyfiglet/fonts
      cp pyfiglet/fonts-standard/* pyfiglet/fonts
      craftctl default

The key does what its name suggests. It replaces the regular build step of the part’s lifecycle, running whatever shell commands we provide to special effect. In this case, the build pre-empts the project’s setup.py script by creating the font directory and copying both font sets into it at the same time. Then, by concluding with craftctl default, we instruct the part to proceed with the build step like normal, in our snap’s case by running setup.py and autotools.

As a result, all the fonts are now copied into the snap. Let’s give it a try. Repack the snap, reinstall it, and then try it with one of the new fonts:

crafter@home:~$ ukuzama-pyfiglet -f thin bonjour le monde
|                  o                   |                                ||---.,---.,---.    .,---..   .,---.    |    ,---.    ,-.-.,---.,---.,---|,---.|   ||   ||   |    ||   ||   ||        |    |---'    | | ||   ||   ||   ||---'`---'`---'`   '    |`---'`---'`        `---'`---'    ` ' '`---'`   '`---'`---'               `---'

Connect the interfaces

Pyfiglet adds some new functionality to FIGlet, such as a feature for installing new fonts to the user’s ~/.local/share/pyfiglet/fonts directory. But, by default, snaps block access to system resources like USB devices, the network, and home. Interfaces permit access to individual resources on the host, be they software, data, or hardware.

To enable writing to the home directory, we must connect two interfaces:

  • The home interface, which provides base access to the home folder

  • The personal-files interface, which provides access to hidden files in the home folder

Interfaces are established on apps in your snap by the plug key.

First, let’s connect our ukuzama-pyfiglet app to the home interface:

snapcraft.yaml
apps:
  ukuzama-pyfiglet:
    command: bin/pyfiglet
    plugs:
      - home

Next, the personal-files interface. For better confinement, personal-files can only target specific directories for reading and writing, so we must be explicit and configure which paths to link. Configurable interfaces must be declared at the root of the project file, with custom aliases. The alias must be something that users and admins can intuit. For personal-files, the convention is to start with dot- and follow with a short description of our intent.

With all of that in mind, let’s create an entry for personal-files after our apps, granting it write access to ~/.local/share/pyfiglet/fonts:

snapcraft.yaml
plugs:
  dot-pyfiglet-fonts:
    interface: personal-files
    write:
      - $HOME/.local/share/pyfiglet/fonts

Then, add it to the plugs of the ukuzama-pyfiglet app:

snapcraft.yaml
apps:
  ukuzama-pyfiglet:
    command: bin/pyfiglet
    plugs:
      - home
      - dot-pyfiglet-fonts

If we repack and reinstall the snap, we can install a new font for pyfiglet to use.

However, before we repack, let’s go back to two keys we skipped at the beginning.

Secure the snap

Now that we’re handling interfaces – usually the last step in the crafting process – the grade and confinement keys are relevant.

These keys account for the security and stability of the snap. The grade key is a self-attestation of how risky the snap is. When set to devel, snapd and snap stores won’t treat it as ready for production. The confinement key determines whether the snap needs less confinement to function, where it has fewer guardrails and greater access to the system.

We want our snap to be as safe and secure as possible, so let’s change these values to:

snapcraft.yaml
grade: stable
confinement: strict

Now, when we build the snap, the snap’s access to the host is inverted – all sensitive system resources are blocked unless facilitated by an interface. And when we publish the snap, users will be able to install it without the --devmode flag.

Test the Interfaces

Now that the snap is confined, we can test the interfaces realistically.

Build and reinstall the snap, but this time, install it like a production-ready snap:

snap install ukuzama-pyfiglet_0.1_amd64.snap --dangerous

Note

We must continue passing the --dangerous argument during installation because it’s not live in the Snap Store, and therefore not attestable.

Next, let’s gather a font that wasn’t included with pyfiglet and install it.

Download Small Braille from the figlet-fonts project and install it with:

ukuzama-pyfiglet -L smbraille.tlf

And give it a try:

crafter@home:~$ ukuzama-pyfiglet -f smbraille hamba kahle
⣇⡀ ⢀⣀ ⣀⣀  ⣇⡀ ⢀⣀   ⡇⡠ ⢀⣀ ⣇⡀ ⢀⡀⠇⠸ ⠣⠼ ⠇⠇⠇ ⠧⠜ ⠣⠼   ⠏⠢ ⠣⠼ ⠇⠸ ⠣⠭

Review the project file

Here’s the complete code for the ukuzama-pyfiglet project. Yours should look similar to it.

snapcraft.yaml of ukuzama-pyfiglet
name: ukuzama-pyfiglet
base: core24
version: "0.1"
summary: pyfiglet is a program for making large letters out of ordinary text
description: |
  pyfiglet is a full port of FIGlet (http://www.figlet.org/) into pure python.
  It takes ASCII text and renders it in ASCII art fonts (like the title above,
  which is the 'block' font).

  This snap is not endorsed by the pyfiglet project.

platforms:
  amd64:

grade: stable
confinement: strict

parts:
  pyfiglet:
    plugin: python
    source-type: git
    source: https://github.com/snapcraft-docs/pyfiglet
    override-build: |
      mkdir pyfiglet/fonts
      cp pyfiglet/fonts-contrib/* pyfiglet/fonts
      cp pyfiglet/fonts-standard/* pyfiglet/fonts
      craftctl default

apps:
  ukuzama-pyfiglet:
    command: bin/pyfiglet
    plugs:
      - home
      - dot-pyfiglet-fonts

plugs:
  dot-pyfiglet-fonts:
    interface: personal-files
    write:
      - $HOME/.local/share/pyfiglet/fonts

Conclusion and next steps

And you’re done! You have a snap of pyfiglet that works on your system.

It would be a good time to start planning for your first public snap. Ask yourself, what software would be interesting to package? What apps would benefit the most from the security and ease of a snap? Any reason or justification is valid. Snaps can be tools, productivity software, games, or any traditional Linux package.

Take a look on the public Snap Store to see if the apps you use the most have public snaps. If a result comes up empty, that would be good candidate for your first public snap.

When you’re ready to begin crafting in earnest, you should create an account and then register your snap.