Speed up your ROS snap builds
A while back I wrote a post about distributing a ROS system among multiple snaps. If you want to enable some sort of add-on story, you need to have multiple snaps, and that remains the way to do it today with ROS. That approach works, but I'll be the first to admit that it's not terribly elegant, and we're working on making it better. However, in my experience talking to various ROS users creating snaps, they oftentimes end up using multiple snaps not for an add-on or app store story, but for build speed. If you're one of those people, keep reading, because there's a better way to do this.
How can distributing one's ROS system among multiple snaps increase speed? Because if you have a large number of dependencies that don't change very often (e.g. upstream ROS components) you can package them all in their own snap. Then you can use its staging area when building the snap containing your own sweet, sweet code. This bypasses time spent fetching, unpacking, or building dependencies.
That's great, and I'm happy that pattern has solved issues for folks. However, selecting it is also selecting the extra responsibility and maintenance burden that comes along with it. For example, handling the atomic update and rollback of a single snap is simple, but it quickly gets complex when you're dealing with multiple snaps that need to be in lockstep. Basically, if you don't need to distribute multiple snaps tied together with content sharing, I recommend avoiding it. Embrace bundling dependencies into one isolated snap that represents your product. Thankfully, there's a way to do both: get the speed benefits of having multiple snaps, while only needing to distribute one.
If you've built a snap in the past, you're probably familiar with the stage-packages option available to parts. It's essentially a list of Debian packages for the snapcraft CLI to download, unpack, and distribute along with the rest of the part (i.e. bundle into the snap). As of version 3.2 of the snapcraft CLI, the stage-snaps option is also available, which offers the same functionality for snaps. Version 3.4 of the snapcraft CLI adds the functionality required to save as much time as possible when building ROS snaps using stage-snaps. Note: v3.4 is not yet released as of this writing-- you'll need to use the edge channel of snapcraft to get the functionality documented here.
What this means is that, instead of needing to maintain a "producer" snap (which is a runtime dependency), as well as keep hold of its staging area for building a "consumer", you can simply build a snap of all your dependencies and then bundle it straight into the "consumer" and only ship it as your end-product.
This is best explained with an example.
Similar to the post about distributing a ROS system among multiple snaps, we're going to create two snaps: one to contain common dependencies, and another to be our actual product. The word "base" has been overloaded by the snap project, so we'll call our dependency snap our "foundation" and the product snap our "app" snap.
Create a snap/snapcraft.yaml with the following contents:
summary: ROS foundational snap
Contains roscore and basic ROS utilities. Meant for use as a stage-snap in
You'll notice I called mine ros-foundation-kyrofa to avoid any name clashes. It's pretty useless on its own: it contains no apps, so it's not like end-users can interact with it. It's really just a blob of stuff. What stuff? roslib, roscore, std_msgs, etc. In this case it doesn't contain any of my own Catkin packages, but it certainly could-- got some libraries that don't change very often? Put them here.
Run snapcraft on that, and after a little time you'll have your "foundation" snap ready for use. Let's get it into the stable channel of the store:
$ snapcraft register ros-foundation-kyrofa # You only need to do this once
$ snapcraft push ros-foundation-kyrofa_0.1_amd64.snap --release=stable
The starting point for our "app" snap is the standard ROS talker/listener snap you've probably seen a thousand times. We'll take it from the current source tree of the snapcraft CLI and tweak it a bit.
At this point, if you were distributing both foundation and app snaps, you'd go through the dance of tarring up the foundation staging area, adding a new part to the app snap, etc. We don't need to do that in this case-- our tweak is far simpler. Make the snap/snapcraft.yaml look something like this:
summary: ROS app snap
Contains ROS talker/listener, including foundational ROS stuff from stage-snaps.
command: roslaunch listener talk_and_listen.launch
Other than the addition of the base option, the most important parts are the two lines at the bottom: the addition of the stage-snaps option, and the specification not to include roscore (because it's already provided by our "foundation" snap). In order to see other effects of this new pattern, run snapcraft pull, which is responsible for downloading all dependencies of the snap:
$ snapcraft pull
<snip... installing build packages>
<snip... fetching roslib>
Downloading snap 'ros-foundation-kyrofa'
<snip... fetching catkin, compilers, and rosdep>
Initializing rosdep database...
Updating rosdep database...
Determining system dependencies for Catkin packages...
That's it. Normally, after "determining system dependencies," you'd see it move on to actually fetching those dependencies. However, again as of v3.4, the snapcraft CLI is smart enough to realize that those dependencies are already contained within the "foundation" snap that we included in stage-snaps, so it doesn't bother to fetch them again.
Go ahead and run snapcraft now to build the "app" snap the rest of the way, and then you can test it out without needing the "foundation" snap at runtime (since it's bundled within the "app" snap now):
$ sudo snap install ros-app-kyrofa_0.1_amd64.snap --dangerous
ros-app-kyrofa 0.1 installed
auto-starting new master
[ INFO] [1554841533.046517090]: Hello world 0
[ INFO] [1554841533.146675333]: Hello world 1
[ INFO] [1554841533.246651599]: Hello world 2
[INFO] [1554841533.246916]: I heard Hello world 2
[ INFO] [1554841533.346644894]: Hello world 3
[INFO] [1554841533.347463]: I heard Hello world 3
As you can see, it works just as well as before, but now there's no content sharing involved: just one snap. One snap that could be installed by anyone and work the exact same way.
There are, of course, some limitations. For example. when building the "app" snap, you'll notice that the snapcraft CLI fetches roslib, even though it's already contained within the "foundation." This happens because the snapcraft CLI doesn't define the order in which it handles stage-snaps and stage-packages. This does waste some time (at least on the first pull; subsequent pulls should have it cached), and will need to be a future optimization.
Another limitation is that, oftentimes, dependencies are Debian packages, and there's no simple way to break a Debian package's dependency chain. For example, say your Catkin package depends upon A. If the stage-snap already contains A, great, the snapcraft CLI won't fetch it again. However, let's say your Catkin package depends upon B, which then depends upon A. In that case, unless the stage-snap also contains B, the snapcraft CLI will fetch both A and B, since the B Debian package depends upon the A Debian package, and that's just the way the APT API works. There are some things we can try here, but again, it's a future optimization.
If you're one of the people trying to ship multiple snaps just because you want your app snaps to build faster, take a step back. Perhaps you could benefit from just using stage-snaps and only shipping a single snap to your users or devices. It will simplify your life, and you'll still get the speed savings you were craving.
As always, feel free to ask any questions here or in the snapcraft forum.