Thursday, August 29, 2019

Snapping ROS2 - that was quick!


Hearing that snapcraft now supports the ROS2 colcon build system, I figured I'd give it go. I'm a good victim, since I'm pretty new to ROS. Without much fuss or bother, I snapped two C++ ROS2 package, a publisher and subscriber, built with the snapcraft colcon plugin (beta in snapcraft 3.2).

Here's the amd64 snap in the store.

It has two commands, one to publish, one to subscribe:

$ kyle-ros2-colcon-pub-sub.publish

$ kyle-ros2-colcon-pub-sub.subscribe


Here's the github repo with a snapcraft.yaml file and the two ROS2 C++ packages.

See sample output below.

Goals

  • Build a ROS2 snap using the snapcraft colcon plugin.
  • Include two ROS2 C++ packages (a publisher and a subscriber).
  • Provide snap commands for each package node.
  • The snap commands should use ROS2's node launch system.
  • The snap should run with strict confinement.
  • The packages's source should follow the normal ROS2 workspace structure: WORKSPACE/src/PACKAGES  to allow me to develop the packages first (without thinking about snapping them) and then easily snap them (without changing the tree structure).
  • Each ROS2 package should be a separate snapcraft part, so I can independently build them.

Getting started


Learning


  • To get started, I read Kyle Fazzari's ROS2 Colcon Snap post
  • I then reviewed the state of ROS2 here.
  • I found that the Crystal Clemmy ROS2 release is current and that it is supported on Ubuntu 18.04 Bionic here
  • I reviewed colcon here.
  • I learned about the new ROS2 launch system here.

Bionic LXD container for development


I created a Bionic LXD container named ros2 to develop in (lxc launch ubuntu:18.04 ros2). Then, lxc start ros2 and lxc shell ros2 (which logs in as root user). When logged as root, I created another user in the container with my name and added the ssh key I use for github. I log in to that use with lxc exec ros2 -- su myusername.

I decided to build in the container directly instead of using the default (for core18 snaps) of multipass, which builds in a dedicated VM. The LXD container is dedicated to building just this ROS2 snap, so I don't mind if debian packages needed to build the snap get installed. So, I always used --destructive-mode flag on snapcraft to not use multipass.

Create and modify the packages


I got working C++ example ROS2 source package code here, crystal branch. 

  • examples/rclcpp/minimal_publisher/
  • examples/rclcpp/minimal_subscriber/

I modified the package.xml files and few other minor items to use my own package and node names. 

(I also added a bit of code to the publisher to publish messages consisting of a random adjective and a random noun, just for fun.)

Source tree directories


The following directory layout is structured as a ROS2 workspace, so I used this for my snap source tree structure:

workspace/
└── src
    ├── kyle_publisher
    │   └── launch
    └── kyle_subscriber
        └── launch

There is a src directory that contains two packages: kyle_publisher and kyle_subscriber.

Each package contains a launch directory that contains a ROS2-style python launch file:
  • kyle_publisher launch file
  • kyle_subscriber launch file
I'll discuss snapping these launch assets below.

Colcon-source-space keyword


After a little experimentation, I realized I could create the two snapcraft parts (one for the publisher, one for the subscriber) with references to he same source: src directory and use the colcon-source-space keyword to point the part at different package directories:

parts:
  publisher:
    plugin: colcon
    source: src/
    colcon-source-space: kyle_publisher
[. . .]
  subscriber:
    plugin: colcon
    source: src/
    colcon-source-space: kyle_subscriber
[. . .]

This allows each package to be a separate part that I could manage (pull, build, stage, prime) independently to save time during development. 

Issue: duplicate files in both parts


My parts approach causes an issue of some duplicated files. That, is there are a set of files that have the same path and the same name that are installed by both parts, and that is not allowed (or even possible).

The snapcraft tool is kind enough to even list the duplicated files when trying to build the snap!

Solution: suppress the files from one part


The solution is to suppress these files from one of the parts. Folks who have snapped a few things know that the snap build system has stages. First each part is pulled (into parts/PART/src/). Then it is built (into parts/PART/build/). And then the install directory is populated parts/PART/install/. Next, the install directories for all parts are merged into the single stage/ directory.  And here is where any duplicated install files clash. 

So, the solution is to suppress one of the set of duplicate files from being put into the stage/directory, which is easily done like so, in the subscriber part. 

Note: This is a yaml array, so the first hyphen indicates an array element. The second hyphen means suppress the following file, so all of these are suppressed from being placed into the stage/ directory 


subscriber:
[ . . . ]
    stage:
      - -usr/lib/python3/
      - -usr/lib/python3.6/
      - -opt/ros/snap/local_setup.bash
      - -opt/ros/snap/local_setup.ps1
      - -opt/ros/snap/local_setup.sh
      - -opt/ros/snap/local_setup.zsh
      - -opt/ros/snap/setup.bash
      - -opt/ros/snap/setup.ps1
      - -opt/ros/snap/setup.sh
      - -opt/ros/snap/setup.zsh


With this in place, the two parts are separately buildable (if you use snapcraft build subscriber, for example), and they combine without a problem into a final snap, with no missing and no duplicate files.

Snap commands and launch files

I added two commands to the snap to launch the publisher and the subscriber from the ROS2 style python launch files in each package's launch directory.

apps:
  publish:
    command: opt/ros/crystal/bin/ros2 launch kyle_publisher launch_publisher.py
    plugs: [network, network-bind]
  subscribe:
    command: opt/ros/crystal/bin/ros2 launch kyle_subscriber launch_subscriber.py
    plugs: [network, network-bind]

Issue: launch files are not installed in snap


I found that the packages' launch directories (and the ROS2 python launch files they contain) were not automatically carried through to the final snap. Specifically, I found that even though each package had a launch/ subdirectory, it was not populated into the part's install directory here, for example for the publisher part:

parts/publisher/install/opt/ros/snap/share/kyle_publisher/

Solution: override each part's build stage 

The solution is to override the snapcraft build stage for each of the parts. First in the override, the normal snapcraft build is executed (with snapcraftctl build) , and then the launch/ directory  is manually copied from the part's src/ directory to its install/ directory, as follows:

publisher:
[. . .]
    override-build: |
      snapcraftctl build
      cp -r ../src/kyle_publisher/launch $SNAPCRAFT_PART_INSTALL/opt/ros/snap/share/kyle_publisher/

This uses the override-build keyword to take control of the snapcraft build for the part. After completing the snapcraft build (with snapcraftctl build), the directory is copied. The only tricky part is knowing that the cp command executes in the part's build directory. Since I need to copy from the part's src directory, I need to know the relative path to it: ../src . Also note the use of the SNAPCRAFT_PART_INSTALL variable to name the part's install directory without needing to know its location. 

That's it

Using the snapcraft colcon plugin couldn't have been much easier: it pretty much worked as I intuitively expected it to. As noted, launch asset handling required manual steps to copy them into the snap. But this was a simple fix that may not be unfamiliar to anyone who uses snapcraft. I also needed to suppress some redundant files, but this resulted from my decision to treat a single ROS2 workspace with two packages as separate snapcraft parts, which may have been naive on my part.

Try out the snap


Here's the amd64 snap in the store.

Install with:

$ snap install kyle-ros2-colcon-pub-sub

Run the publisher like so: 
$ kyle-ros2-colcon-pub-sub.publish 
[INFO] [launch]: process[publisher_member_function-1]: started with pid [2508]
[INFO] [kyle_publisher]: Publishing: 'tiny television'
[INFO] [kyle_publisher]: Publishing: 'sorry sympathy'
[...]

Run the subscriber like so: 
$ kyle-ros2-colcon-pub-sub.subscribe 
[INFO] [launch]: process[subscriber_member_function-1]: started with pid [2218]
[INFO] [minimal_subscriber]: 'tiny television'
[INFO] [minimal_subscriber]: 'sorry sympathy'
[...]