[Documentation] [TitleIndex] [WordIndex

Creating a snap

This tutorial will demonstrate how to use Snapcraft to create a new snap, and then the usage.

First, install Snapcraft. It’s recommended to install it from the Snap Store:

$ sudo snap install --classic snapcraft

(Note that using the apt repositories for snapcraft is not recommended and this tutorial will assume that you installed the snap.)

Snapcraft has built-in support for Catkin: you point it at a workspace, and tell it what packages to include in the snap. In order for it to know which project components to include, you must ensure that your projects have good install rules. Let's do that now. Open up your CMakeLists.txt with rosed beginner_tutorials CMakeLists.txt. If you followed the Python tutorial, add the install target at the end so that it looks like this (unused bits and comments removed):

cmake_minimum_required(VERSION 2.8.3)
project(beginner_tutorials)

## Find catkin and any catkin packages
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg)

## Declare ROS messages and services
add_message_files(FILES Num.msg)
add_service_files(FILES AddTwoInts.srv)

## Generate added messages and services
generate_messages(DEPENDENCIES std_msgs)

## Declare a catkin package
catkin_package()

include_directories(include ${catkin_INCLUDE_DIRS})

## Install scripts
install(PROGRAMS scripts/talker.py scripts/listener.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

If you followed the C++ tutorial, add the install target at the end so that the CMakeLists.txt looks like this:

cmake_minimum_required(VERSION 2.8.3)
project(beginner_tutorials)

## Find catkin and any catkin packages
find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg)

## Declare ROS messages and services
add_message_files(FILES Num.msg)
add_service_files(FILES AddTwoInts.srv)

## Generate added messages and services
generate_messages(DEPENDENCIES std_msgs)

## Declare a catkin package
catkin_package()

## Build talker and listener
include_directories(include ${catkin_INCLUDE_DIRS})

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker beginner_tutorials_generate_messages_cpp)

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener beginner_tutorials_generate_messages_cpp)

## Install talker and listener
install(TARGETS talker listener
  RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

Change to the root of the catkin workspace you used in the "writing a simple publisher and subscriber" tutorial:

# This workspace should contain the beginner_tutorials package.
$ cd ~/catkin_ws

Initialize a new Snapcraft project here:

$ snapcraft init
Created snap/snapcraft.yaml.
Edit the file to your liking or run `snapcraft` to get started

The snapcraft.yaml

Open that snap/snapcraft.yaml file and make it look like this:

name: publisher-subscriber
base: core
version: '0.1'
summary: ROS Example
description: |
  The ROS publisher/subscriber example packaged as a snap.

grade: stable
confinement: strict

parts:
  workspace:
    plugin: catkin
    source: .
    catkin-packages: [beginner_tutorials]

apps:
  roscore:
    command: roscore
    plugs: [network, network-bind]

  talker:
    # Use talker.py if you followed the Python tutorial
    command: rosrun beginner_tutorials talker
    plugs: [network, network-bind]

  listener:
    # Use listener.py if you followed the Python tutorial
    command: rosrun beginner_tutorials listener
    plugs: [network, network-bind]

The snapcraft.yaml explained

Let's break that down piece by piece.

name: publisher-subscriber
base: core
version: '0.1'
summary: ROS Example
description: |
  The ROS publisher/subscriber example packaged as a snap.

This is the basic metadata that all snaps require. These fields are fairly self-explanatory, but note that the name must be globally unique among all snaps. The one that is probably not obvious is base.

A base is a special kind of snap that provides a minimal set of libraries common to most applications. A base snap mounts itself as the root filesystem within your snap so that when your application runs, the base's library paths are searched directly after the paths for your specific snap. In our case, we're using base: core which is a rootfs generated from Ubuntu 16.04 (Xenial). As a result, snapcraft understands what you're wanting to use ROS Kinetic. Another option would be to use base: core18, which is a rootfs generated from Ubuntu 18.04 (Bionic), thereby causing snapcraft to use ROS Melodic.

grade: stable
confinement: strict

grade can be either stable or devel. If it's devel, the store will prevent you from releasing into one of the two stable channels (stable and candidate, specifically)-- think of it as a safety net to prevent accidental releases. If it's stable, you can release it anywhere.

confinement can be strict, devmode, or classic. strict enforces confinement, whereas devmode allows all accesses, even those that would be disallowed under strict confinement (and logs accesses that would otherwise be disallowed for your reference). classic is even less confined than devmode, in that it doesn't even get private namespaces anymore (among other things). There is more extensive documentation on confinement available.

parts:
  workspace:
    plugin: catkin
    source: .
    catkin-packages: [beginner_tutorials]

Snapcraft is responsible for taking many disparate parts and orchestrating them all into one cohesive snap. You tell it the parts that make up your snap, and it takes care of the rest. Here, we tell Snapcraft that we have a single part called workspace. We specify that it builds with Catkin, and we specify the packages in this workspace that we want included in the snap. In our case, we only have one: the beginner_tutorials package we've been working on through the tutorials. Note that this is not required: if you leave catkin-packages off entirely, Snapcraft will build all packages in the workspace.

apps:
  roscore:
    command: roscore
    plugs: [network, network-bind]

  talker:
    # Use talker.py if you followed the Python tutorial
    command: rosrun beginner_tutorials talker
    plugs: [network, network-bind]

  listener:
    # Use listener.py if you followed the Python tutorial
    command: rosrun beginner_tutorials listener
    plugs: [network, network-bind]

Now things get a little interesting. When this snap is built, it will include a complete ROS system: roscpp, rospy, roscore, your ROS workspace, etc. It's a standalone unit, and you're in complete control over how the user interacts with it. You exercise that control via the apps keyword, where you expose specific commands to the user.

Here we simply expose the three components of the publisher/subscriber tutorial: roscore, the talker, and the listener. We could just as easily written a launch file to bring up the entire system in a single app.

We use plugs to specify that each app requires network access (read more about interfaces). Without this, each app would be confined such that they couldn't communicate.

Actually create the snap

That's it: time to build the snap.

$ cd ~/catkin_ws
$ snapcraft

That will take a few minutes. You may be prompted to install multipass, a tool used by snapcraft to ensure build isolation (so it doesn't dirty up your host when building). You'll see Snapcraft fetch rosdep, which is then used to determine the dependencies of the ROS packages in the workspace. It then pulls those down and puts them into the snap along with roscore. Finally, it builds the requested packages in the workspace, and installs them into the snap as well. At the end, you'll have your snap.

Testing the snap

As we discussed previously, this snap is completely standalone: you could email it to someone and they'd be able to install it and run your ROS system, even if they didn't have ROS installed themselves. Test it out yourself:

# We use --dangerous here because the snap doesn't come from an
# authenticated source
$ sudo snap install --dangerous publisher-subscriber_0.1_amd64.snap

Now you can take it for a spin just like you did when examining the simple publisher and subscriber. First, run roscore:

$ publisher-subscriber.roscore

Now run the publisher:

$ publisher-subscriber.talker

And you'll see the familiar output:

[ INFO] [1497643945.491444894]: hello world 5
[ INFO] [1497643945.591430533]: hello world 6
[ INFO] [1497643945.691426519]: hello world 7
[ INFO] [1497643945.791444793]: hello world 8
[ INFO] [1497643945.891433313]: hello world 9

Now let's run the listener:

$ publisher-subscriber.listener

And you'll see the this project works exactly the same as before:

[ INFO] [1497643969.443662208]: I heard: [hello world 41]
[ INFO] [1497643969.543668530]: I heard: [hello world 42]
[ INFO] [1497643969.643621679]: I heard: [hello world 43]
[ INFO] [1497643969.743650720]: I heard: [hello world 44]
[ INFO] [1497643969.843650108]: I heard: [hello world 45]

When you're done, as usual, press Ctrl-C to terminate roscore, as well as the talker and listener.


2024-03-23 12:23