[Documentation] [TitleIndex] [WordIndex

Note: This tutorial assumes that you have completed the previous tutorials: examining the simple publisher and subscriber, Snapcraft tour.
(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Packaging your ROS project as a snap

Description: This tutorial covers how to package and deploy your ROS project as a snap.

Tutorial Level: INTERMEDIATE

Next Tutorial: Writing a Tutorial

What are snaps?

Snaps are packages that bundle an application and its dependencies. They offer a number of features that address important concerns as one gets closer to shipping a robotic platform:

Creating a snap

This tutorial will demonstrate how to use Snapcraft to create a new snap, and what you can do with it after it has been created.

First, install Snapcraft. You can install it from source (read HACKING.md, or if you're using Ubuntu (or an Ubuntu-based distro), install it from the archives:

$ sudo apt install snapcraft

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
version: '0.1'
summary: ROS Example
description: |
  The ROS publisher/subscriber example packaged as a snap.

grade: stable
confinement: strict

parts:
  workspace:
    plugin: catkin
    rosdistro: kinetic
    catkin-packages: [beginner_tutorials]

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

  talker:
    command: rosrun beginner_tutorials talker
    plugs: [network, network-bind]

  listener:
    command: rosrun beginner_tutorials listener
    plugs: [network, network-bind]

The snapcraft.yaml explained

Let's break that down piece by piece.

name: publisher-subscriber
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.

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
    rosdistro: kinetic
    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 also specify that we're using Kinetic here as opposed to another ROS distribution. Finally, 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.

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

Sharing the snap with the world

Emailing snaps to people doesn't scale, but more importantly, it gives the snap users no upgrade path. If you make your snap available in the Ubuntu Store, whenever you release a new version your users will automatically update. In order to do this, you'll need to create a (free) store account at dashboard.snapcraft.io. Then, using Snapcraft, login with that account:

$ snapcraft login

Remember our previous discussion about how snap names need to be unique. Before you can release your snap in the store, you need to register the snap name to your store account:

$ snapcraft register <my snap name>

Finally, push the snap up to the store and release it into the stable channel:

$ snapcraft push path/to/my.snap --release=stable

Once the upload and automated reviews finish successfully, anyone in the world can install your snap as simply as:

$ sudo snap install <my snap name>

And they can rest secure knowing that when you release an update, they'll get it, no further work required.


2017-10-21 12:25