Part 1: Packaging our first ROS application as a snap

Packaging our first ROS application as a snap

Robotics developers know app development inside-out, but deploying a robotics application can be challenging. It’s not uncommon to compile the code on robots, copy/paste compiled packages and end up with unknown versions of software. Even worse, one can experience the infamous “It works on my machine” syndrome.

Robotics software should benefit from a controlled and stable environment, with the same portability and reliability as any other software application. Achieving this reliability should be simple, even if our software relies on hundreds of dependencies.

What we will learn

This developer guide will take us through the process of packaging our first ROS application as a snap.

Snaps offer a solution to build and distribute containerised robotics applications or any software. It is the de facto distribution tool for companies deploying software on Ubuntu, including Microsoft, Google, Spotify and more. As such, we will be able to leverage the same tooling and global infrastructure for our application.

Throughout this developer guide, we will cover the basics of snap creation for ROS and ROS 2 applications. By introducing the main concepts behind snaps, we will see how to confine our robotics application and make it installable on dozens of Linux distributions.

Requirements

We will need an up and running Ubuntu (or similar) operating system (20.04 being the minimum version as it is still under maintenance). The installation could be native or in a VM. If using a container, we must be sure that we can install and run systemd, snapd and snaps.

In addition, we will need basic knowledge about ROS or ROS 2 as well as some basic understanding of the Linux environment (Ubuntu).

This developer guide has been tailored for robotics developers looking for a solution to deploy their robotics software and applications. No previous experience with snaps is necessary.

What is a snap?

Snaps are the perfect solution for software deployment in embedded Linux devices.

Snaps are containers that bundle an application and all its dependencies, offering roboticists:

  • A containerised solution: snaps bundle all our dependencies and assets in one package, making our application installable on dozens of Linux distributions and across distro versions. We won’t even have to install anything else on our robots’ operating system, no dependencies, not even ROS if we are using it.
  • Strict confinement: snaps are designed to be secure and isolated from the underlying system and other applications, with dedicated interfaces to access specific resources of the host machine, or of other snaps.
  • CI/CD integration: the creation of snaps can be integrated into our CI pipeline, making the updates effortless.
  • OTA and delta updates: snaps can update automatically and transactionally, making sure the device is never broken.
  • Multi-architecture: snaps come with a multi-architecture feature, allowing us to build our snap package for multiple architectures.
  • Managing updates: snaps can be updated automatically, or we can control the update options (update hours, update holds, update history). It also comes with multiple release channels for role-based access controls and application versioning.
  • Reduce boot time: We can configure a snap application as a daemon, so it starts automatically at boot.

What can snaps do for our robotics applications?

Snaps are meant to deploy software that has been developed and tested.

Snaps offer a solution to deploy and distribute our software. It’s an alternative package manager (like APT). With snaps, we can manage updates and keep track of the version installed on our robot without ever breaking our installation. Sharing and deploying our application to all our users or all our devices is made easy. With snaps, we can seamlessly run an application on our distro and access the host machine and its resources securely.

As such, a snap is a solution to deploy our robotics applications.

What can’t snaps do for our robotics applications?

Snaps are not meant for testing and debugging.

Snaps don’t embed our source code. As such, developers can’t use snaps to test some fresh code. It’s not a distribution mechanism for early debugging sessions. On its own, snaps are not meant for cloud web services deployments or web services applications; i.e. Docker. Snaps are used in environments where secure access to the host machine and resources is crucial.

Snaps were designed for embedded Linux applications, with optimisations for ROS packaging.


Install snapd, snapcraft and LXD

Snapd

Snapd is a daemon required to download, install and run snaps. Snapd also includes the snap command, used to communicate with snapd.

Installing snapd is straightforward in most Linux distributions. Snapd comes pre-installed on most Ubuntu flavours.
In most cases, snapd can be installed with:

sudo apt update
sudo apt install snapd

For different distributions, we can refer to the documentation to install snapd.

Snapd is also available through a snap. We can get a more recent version of snapd with:

sudo snap install snapd

After installing snapd you should be able to type in a terminal:

snap --version

A quick look at snap help will show everything that we can do with the snap command

snap help

Commonly used commands can be classified as follows:

         Basics: find, info, install, remove, list
        ...more: refresh, revert, switch, disable, enable, create-cohort
        History: changes, tasks, abort, watch
        Daemons: services, start, stop, restart, logs
    Permissions: connections, interface, connect, disconnect
  Configuration: get, set, unset, wait
    App Aliases: alias, aliases, unalias, prefer
        Account: login, logout, whoami
      Snapshots: saved, save, check-snapshot, restore, forget
         Device: model, reboot, recovery
      ... Other: warnings, okay, known, ack, version
    Development: download, pack, run, try

Snapcraft

While snapd is used to download and run snaps, snapcraft is the tool we need to build snaps.

If we installed snapcraft as a .deb package previously, we will have to uninstall it, the Debian package is no longer updated. To do so, just run: sudo apt remove snapcraft --purge

To install snapcraft simply run:

sudo snap install snapcraft --classic

The --classic flag refers to the confinement. We will address the confinement topic later in this guide.

Snapcraft is not only a tool to build snaps, but more generally a developer tool, meaning that we will use it to build, upload and share our snaps.

LXD

LXD is the container technology used by snaps to isolate our snap build. LXD is not only dedicated to snaps. It’s a next generation system container and virtual machine manager.

LXD can be installed with a snap and must be configured to be used. To install LXD simply run:

sudo snap install lxd

Now that LXD is installed, we must configure it. We are going to use a default profile to do so:

sudo lxd init --auto

We can make sure everything went well by listing the profiles and making sure the default profile is listed:

$ lxc profile list

+---------+---------------------+---------+

| NAME    | DESCRIPTION         | USED BY |

+---------+---------------------+---------+

| default | Default LXD profile | 0       |

+---------+---------------------+---------+

First ROS 2 snap

Our first snap will be a basic ROS 2 Humble talker-listener. We are going to use ros2_demos: demo_nodes_cpp. It contains a talker publishing a message and a listener subscribing to it. Both nodes can be launched with the help of the talker_listener.launch.py.

Understanding the snapcraft.yaml file

First clone the package from GitHub:

git clone https://github.com/ubuntu-robotics/ros2-humble-talker-listener-snap.git

The repository contains a snap folder with a snapcraft.yaml file.
Snaps are defined in a single YAML file placed in our project. Let’s explore our snapcraft.yaml and break it down in the next section:

Metadata

The snapcraft.yaml file starts with a small amount of human-readable metadata, which usually can be lifted from the GitHub description or project README.md. This data is used in the presentation of our app in the Snap Store (for example, see PlotJuggler front page).

name: ros2-talker-listener
version: '0.1'
summary: ROS 2 Talker/Listener Example
description: |
    This example launches a ROS 2 talker and listener.

The name must be unique in the Snap Store in case we later want to publish it. Valid snap names consist of lower-case alphanumeric characters and hyphens. They cannot be all numbers, and they cannot start or end with a hyphen.

This is a declarative version of the packaged software and is not linked to the version of the snap software itself. It’s also possible to write a script to calculate the version, or to take a tag or commit from a git repository.

The summary cannot exceed 79 characters.

Base

Next in the YAML file we will find the base keyword. This defines a special kind of snap that provides a run-time environment with a minimal set of libraries that are common to most applications. They’re transparent to users, but they need to be considered, and specified, when building a snap. As snap developers, we should consider it, but our users will be able to install the final snap independently of their OS version.

base: core22

core22 is the current standard base for snap building and is based on Ubuntu 22.04 LTS. It is therefore the base for ROS 2 Humble snaps.

Security model

Following, we will see the confinement keyword. Snaps are containerised to ensure predictable application behaviour and to provide greater security. We will review this topic later in this guide.

To get started, we won’t confine this application. Unconfined applications are specified with devmode.

confinement: devmode

Parts

Parts follow next. They define how to build our app and can be anything: programs, libraries, or other assets needed to create and run our application. Their source can be local directories, remote git repositories, or tarballs. Multiple parts can be defined within the same snapcraft.yaml in order to build dependencies or even an additional application.

In this example, we have a single part: ‘ros-demos’.

parts:
  ros-demos:
    plugin: colcon
    source: https://github.com/ros2/demos.git
    source-branch: humble
    source-subdir: demo_nodes_cpp
    stage-packages: [ros-humble-ros2launch]

Snapcraft relies on well known and well established ROS tools such as, in this example, colcon. Plugins allow us to identify such tools.

The packages we’re building must have install rules, or else snapcraft won’t know which components to place into the snap. We should make sure we install binaries, libraries, header files, launch files, etc. Here, we selected the humble branch of ros2-demos github repository as source-branch.
Since ros2-demos contains multiple packages, we select demo_nodes_cpp with the source-subdir entry.

We notice that ros-humble-ros2launch is listed as a stage-packages. Stage packages are packages required to run the part. Usually this exec dependency is missing from the package.xml hence we must specify it. The rest of the dependencies are going to be automatically downloaded with rosdep based on the package.xml.

Apps

After parts we find the apps keyword. These are the commands and services exposed to end users.

apps:
  ros2-talker-listener:
    command: opt/ros/humble/bin/ros2 launch demo_nodes_cpp talker_listener.launch.py
    extensions: [ros2-humble]

The entry under apps is the app name that should be exposed to the end users. In our case the app name is ros2-talker-listener.

In snap, an application is usually prefixed by the snap name so that the application my-app from the snap my-snap can be executed by calling my-snap.my-app.
However, if both the snap and the app are called the same, as is the case in our ROS 2 example, the execution command collapses to avoid the tediousness of writing twice the same words.

As a result, the command ros2-talker-listener.ros2-talker-listener simply becomes ros2-talker-listener. We will see this when we run the snap.

Multiple apps can be defined within the same snapcraft.yaml. Our snap will then expose multiple commands.
Then, after the app name, we find the command entry. This specifies the path to the binary to be run, along with arguments. This is resolved relative to the root of our snap contents (hence there is no ‘/’ before opt).

Finally, the ros2-humble extension will set our ROS 2 humble build and runtime environment. This way we don’t have to source manually our ROS 2 environment.

Building, installing and running a snap

Now that our snapcraft.yaml is ready, we will describe how to build our package. In this section, we will also cover how to install and run the created snap.

Building

The file snapcraft.yaml is expected to be found in the snap/ directory. So the snapcraft command is expected to be run at the root of our repository where we can find the snap/ directory. The snapcraft command is going to look for snap/snapcraft.yaml and start building our snap.

Snapcraft is building the snap in steps:

  1. pull: downloads or otherwise retrieves the components needed to build the part.
  2. build: constructs the part from the previously pulled components. The plugin of a part specifies how it is constructed.
  3. stage: copies the built components into the staging area. This is the first time all the different parts that make up the snap are actually placed in the same directory.
  4. prime: copies the staged components into the priming area, to their final locations for the resulting snap. This is very similar to the stage step, but files go into the priming area instead of the staging area. The prime step exists because the staging area might still contain files that are required for the build but not for the snap.

To build our snap, we will run:

snapcraft

This will take some time, but once it’s done we will see:

Created snap package ros2-talker-listener_0.1_amd64.snap

This .snap file is our packaged application.

Installing

If we could install snapd on our current distribution, it means that we can install our freshly built snap. We don’t need to be running Ubuntu 22.04 to run this ROS 2 Humble snap. We don’t even need to install ROS on our host system to install and run the snap.

Snaps bundle all their dependencies as well as their “core” which make them host-agnostic.

Since our snap is currently not confined, we will install it with the flag --devmode.

sudo snap install ros2-talker-listener_0.1_amd64.snap --devmode

Running the snap

Now let’s run the snap that we just installed.

We can start the snap by running:

ros2-talker-listener

We will see the talker listener starting to exchange messages. We can then ctrl-c it to stop it.
Note that we built, installed and ran this ROS application without even installing ROS 2 on our host.

In this example, our snap has only one app, but snap can contain as many applications (commands) as we need. We can easily get info on our installed snap with the snap info command:

$ snap info ros2-talker-listener

name:      ros2-talker-listener
summary:   ROS 2 Talker/Listener Example
publisher: –
license:   unset
description: |
  This example launches a ROS 2 talker and listener.
Commands:
  - ros2-talker-listener
refresh-date: today at 10:54 CEST
installed:    0.1 (x1) 64MB devmode

We can see all kinds of metadata as well as the commands available from the snap.

Confining our first snap application

Our application was installed in `devmode`. This means that our snap can access every resource from our host system (files, devices, etc.). For security, snaps are meant to be run and distributed as strictly confined applications.

In this section we will explore the confinement types, grades and interfaces available. Then, we will strictly confine our application.

Confinement types

So far, in our snapcraft.yaml, we only declared:

confinement: devmode

Let’s have a closer look at the types of confinement:

  • Devmode

    A special mode for snap creators and developers. A devmode snap runs as a strictly confined snap with full access to system resources, and produces debug output to identify unspecified interfaces. Installation requires the --devmode command line argument.

  • Classic

    Allows access to our system’s resources in much the same way traditional packages do. To safeguard against abuse, publishing a classic snap requires manual approval, and installation requires the --classic command line argument. The typical applications allowed with classic confinement are IDEs (vscode, qtcreator).

  • Strict

    Used by the majority of snaps. Strictly confined snaps run in complete isolation, up to a minimal access level that’s deemed always safe. Consequently, strictly confined snaps can not access our files, network, processes or any other system resource without requesting specific access via an interface.

In this case, our application should be confined as strict since we want to be able to share it securely. Everything it needs to access can be declared through interfaces. Let’s make that change:

confinement: strict

Grade

By adding the grade keyword, we can declare the quality of our snap. By defining the grade, we can make sure that a development version never goes into a stable channel.

There are only two grades possible:

  • Devel

    A devel snap indicates that this is a development version, and it is not meant to be released on either a stable or candidate channel.

  • Stable

    The default one. Meant for production grade snaps, so it can later be released to every user.

For this example, let’s add the grade keyword and select stable:

grade: stable

Interfaces

Interfaces enable resources from one snap to be shared with another or with the system. An interface consists of a connection between a slot and a plug. The slot is the provider of the interface while the plug is the consumer, and a slot can support multiple plug connections.

The list of available interfaces is available on the online documentation. We can find interfaces to access the home directory, the CANBus, the network etc.

The interfaces to use are declared in the snapcraft.yaml for each application. In the online documentation, we will see that some interfaces are listed as “auto-connect=yes” and some are not. The auto-connectable interface will connect at the installation of the snap, while the others will have to be connected manually. This resembles the security validation of an app requesting permissions to the user to access some resources.

Which interfaces a snap requires, and provides, is very much dependent on the type of snap and its own requirements.

For our ROS 2 snap, we will need two auto-connect interfaces: network to enable network access and network-bind to let our snap operate as a network service.

plugs: [network, network-bind]

Confining and rebuilding our snap

By changing the confinement level and adding the grade and plugs, in our snapcraft.yaml. We will confine our application.

We can find the updated code from the confined branch.

To switch to the confined branch:

git switch confined

Our snapcraft.yaml file should have these modifications:

-confinement: devmode
+confinement: strict
+grade: stable
apps:
  ros2-talker-listener:
  command: opt/ros/humble/bin/ros2 launch talker-listener talker_listener.launch.py
+ plugs: [network, network-bind]

Once our changes are done, let’s rebuild our snap:

snapcraft

This time our snap is confined, so we don’t need the --devmode flag any more. Yet, we will need the --dangerous flag, since our snap hasn’t been signed by an official store.

To install our snap:

sudo snap install ros2-talker-listener_0.1_amd64.snap --dangerous

Let’s check the connections of our freshly confined snap. Run:

$ snap connections ros2-talker-listener

Interface    Plug                              Slot          Notes
network     ros2-talker-listener:network       :network      -
network-bind ros2-talker-listener:network-bind :network-bind -

In the above output, we can see that our snap is connected to the network and network-bind slot.

If everything looks good, let’s run our confined snap:

ros2-talker-listener

We will now face this log:

[talker-1] 2022-07-13 15:47:10.570 [RTPS_TRANSPORT_SHM Error] Failed to create segment cbbe40933e75c60a: Permission denied -> Function compute_per_allocation_extra_size
[listener-2] 2022-07-13 15:47:10.654 [RTPS_TRANSPORT_SHM Error] Failed to create segment 59f8e836a0800439: Permission denied -> Function compute_per_allocation_extra_size
[talker-1] 2022-07-13 15:47:10.657 [RTPS_MSG_OUT Error] Permission denied -> Function init
[listener-2] 2022-07-13 15:47:10.657 [RTPS_MSG_OUT Error] Permission denied -> Function init
[talker-1] [INFO] [1657720031.730359135] [talker]: Publishing: 'Hello World: 1'
[listener-2] [INFO] [1657720031.730705569] [listener]: I heard: [Hello World: 1]
[talker-1] [INFO] [1657720032.730352803] [talker]: Publishing: 'Hello World: 2'
[listener-2] [INFO] [1657720032.730571444] [listener]: I heard: [Hello World: 2]

The error that we see is related to the shared memory transport not being able to create its file. This is expected. We will cover how to manage shared memory in our snaps later. For now, even with the shared memory failing, ROS 2 falls back to network transport (UDP) and messages are properly sent and received.

Apart from our shared memory error message, our snap is now running strictly confined with only access to our network.

Run our snap as a daemon

One of the advantages of using snaps is that they can turn our application into a service (or a daemon) in an incredibly easy way. Once we have turned our application into a service, it can automatically start at boot and end when the machine is shut down. We can also start and stop on demand through socket activation.

A daemon can take different forms, where the first two daemons are the most used forms:

  • simple: Run for as long as the service is active - this is typically the default option.
  • oneshot: Run once and exit after completion, notifying systemd.
  • forking: The configured command calls fork() as part of its start-up, and the parent process is then expected to exit when start-up is complete.
  • notify: Assumes the command will send a signal to systemd to indicate its running state.

The daemon feature is per command and not per snap. This means that one snap containing multiple commands can have some running as daemon and others running as simple commands.

To turn our talker-publisher into a simple daemon, we will add daemon: simple in our snapcraft.yaml.

apps:
  ros2-talker-listener:
    command: opt/ros/humble/bin/ros2 launch talker-listener talker_listener.launch.py
+   daemon: simple
    plugs: [network, network-bind]
    extensions: [ros2-humble]

That’s it.
To switch to this version of the file run:

git switch daemon

Now let’s rebuild and install our snap. The build should be quicker as this has been done before.

snapcraft
sudo snap install ros2-talker-listener_0.1_amd64.snap --dangerous

Let’s explore what happens now that we have turned our snap application into a daemon.

First, we get some info about our snap:

$ snap info ros2-talker-listener

name: ros2-talker-listener
summary: ROS 2 Talker/Listener Example
publisher: –
license: unset
description: |
  This example launches a ROS 2 talker and listener.
services:
  ros2-talker-listener: simple, enabled, active
refresh-date: today at 16:39 CEST
installed: 0.1 (x9) 64MB -

Our command is now listed as a service and marked enabled and active. This means that our talker-listener is currently running as a daemon.

Active means the talker-listener is now running in the background. And enabled means that it will automatically start at boot and restart in case of failure.

Log our service

We can inspect the log of our running service with the snap tool. To inspect our snap log run:

$ sudo snap logs ros2-talker-listener

ros2-talker-listener.ros2-talker-listener[970635]: [talker-1] [INFO] [1657724117.996643358] [talker]: Publishing: 'Hello World: 957'
...
ros2-talker-listener.ros2-talker-listener[970635]: [listener-2] [INFO] [1657724121.996731460] [listener]: I heard: [Hello World: 961]

We can also add the -f flag if we want to wait for new lines and print them as they come in.

Snap logs are actually available in the systemd journal. Hence, we can log a snap service directly from the journalctl command:

journalctl -fu snap.ros2-talker-listener.ros2-talker-listener.service

(-f for follow and -u to show the log of our specific unit)

Interact with our service

We saw that our service was enabled and active, we can obviously interact with these states.

In case we want to temporarily stop our service, we can by using running:

sudo snap stop ros2-talker-listener

We have stopped our service, so it’s no longer running in the background. But if we reboot, our service will still start automatically. This is because our service is still enabled.

To disable our service, we can do it with:

sudo snap stop --disable ros2-talker-listener

Now our service won’t start again. We can verify the result of our actions by running the snap info command on our snap.

We have seen how to stop/disable our service, but of course we also have the corresponding start/enable command. Please visit the documentation to know more about service-management.

Conclusion

In this developer guide, we went through the creation of a basic ROS 2 snap. But in the process, we learned the basics to create our own ROS snap as well. We have covered the basic concepts of a snap, how to build and run them. We also shared the benefits and good development practices. There are more features and advanced development tips that we have yet to cover. The turtlebot3 snap example shows how we can use snap to make your robot software easily installable.

Visit the robotics documentation to go further. If you have any questions or need help, you can visit and post your question on the ubuntu robotics forum.

1 Like

Under “Grade”, defining a grade of “devel” means that a snap cannot be released on either a stable or candidate channel.

Under “Interfaces”:

“Interfaces enable resources from one snap to be shared with another or with the system.”

That seems like a poorly-worded definition of interfaces.

Thanks, I made the change regarding the “Grade”.
For the interfaces’ definition, it directly comes from the snapcraft documentation.