How Snapcraft helps developers map out their application dependencies and efficiently build snaps

by Igor Ljubuncic on 8 February 2022

One of the core concepts of snaps is cross-distro compatibility. Developers can build their snaps once, and they should run well on more than 40 different Linux distros. But how does one take care of all the required runtime dependencies? By providing them inside the snap, as part of the bundle.

In the snap ecosystem, the functionality is satisfied through stage packages, a list of libraries and other runtime components declared for every application included inside the snap. What makes things rather interesting is how this list is created. In this blog post, we want to take you through the journey of dependency mapping, and how Snapcraft, the command-line tool used to build snaps, can assist you in the process.

BYOD – Bring Your Own Dependencies

Snaps are self-contained, isolated archives, designed to run independently of the underlying system. The abstraction is provided by the snapd service, which enables snaps to start and run. From inside the snap, applications do not see the host’s filesystem; instead, they see a layer called base, a set of libraries that provides a minimal functional environment for these applications.

A base is aligned to one of Ubuntu’s LTS releases. For instance, core18 is aligned to Ubuntu 18.04 LTS. This means that a snap build with base: core18 will “think” it runs on top of Ubuntu 18.04, even though the external operating system may be Ubuntu 20.04 or perhaps Fedora 34.

In addition to the base, developers may need to declare additional runtime dependencies, which are not provided in this minimal layer. These will be the stage packages, which need to be declared for the applications. The big question is, how does one know what to add?

    stage-packages:
      - ibus-gtk3
      - fcitx-frontend-gtk3
      - gvfs-libs
      - libasound2

The hunt for the missing dependencies

If you are well familiar with the application you’re snapping, then you most likely know what needs to be included. Even so, you may not necessarily be aware of the intricate dependencies, especially if you compile or include additional toolkits into your program.

In the classic Linux world, an easy way to detect (most) runtime dependencies is to run the ldd tool against a binary, which will then provide a list of the libraries that the binary needs. Usually, these will be located under /usr or /lib on the host system.

As a developer, you can use this output as your baseline. Now, you can reverse trace the name of the packages that provided these libraries. For instance, on Linux distributions that use deb packages and the dpkg management tool, you can use the -S option to trace the upstream packages.

ldd /bin/ls
linux-vdso.so.1 (0x00007ffff7bec000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007ffa556cb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa552da000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007ffa55068000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffa54e64000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffa55b15000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa54c45000)
dpkg -S libdl.so.2
libc6:i386: /lib/i386-linux-gnu/libdl.so.2
libc6:amd64: /lib/x86_64-linux-gnu/libdl.so.2

This way, you can create a rudimentary list of stage packages for your applications. You will need to list the names of packages that provide the libraries, except libraries already provided by the base. Please note that you will need to use the names (and versions) that match the base you specified in your snap. As a crude example, in Ubuntu 18.04, you have libncurses5, while Ubuntu 21.04 offers libncurses6.

If you’re using a non-Ubuntu system for your package discovery, you will need to “translate” the names of packages that your distro reports (say Manjaro or openSUSE) and see what such packages are called in Ubuntu. Alternatively, you could build your own non-Ubuntu base, but this is far from a trivial exercise.

The use of ldd is not comprehensive, though. If your application uses the dlopen() function, you might not necessarily know what’s missing. In that case, you will need to build your snap, let it run, and then examine the runtime failure error. Typically, you will see what libraries cannot be found, and will then need to be mapped to package names, and added to the stage package list.

/snap/kompozer/x1/usr/lib/kompozer/kompozer-bin: error while loading shared libraries: libmozjs.so: cannot open shared object file: No such file or directory

Snapcraft auto-discover

To help you build your snaps more effectively, Snapcraft will try to do a lot of the search and discovery for you. The exact behavior of the tool somewhat depends on which core you use, but in essence, Snapcraft will try to best guess what you need. The following method should get you going:

  • Build a snap without declaring any stage packages. See if there are any build errors, or if Snapcraft has resolved the missing dependencies for you.
  • Add any packages suggested by Snapcraft.
  • Repeat the process.

For example:

This part is missing libraries that cannot be satisfied with any available stage-packages known to snapcraft:

- libIDL-2.so.0
- libpng12.so.0

These dependencies can be satisfied via additional parts or content sharing. Consider validating configured filesets if this dependency was built.

In the build output above, the command-line output of the Snapcraft build process explicitly tells the user (developer) that it could not satisfy two dependencies. This means they most likely do not exist in the Ubuntu archives (for the chosen base), and you will need to provide them yourself. Often, this will need using the dump plugin, and providing additional libraries or deb packages from your own local sources or a different online repository.

Extra speed

To make your development process even more efficient, you may also want to consider using the snap try and snapcraft pack commands, allowing you to quickly make changes to your work project before you assemble it into a final snap artifact. This can be quite useful especially in figuring out any missing runtime dependencies.

Summary

Hopefully, this article demystifies some of the intricacies of Linux dependency search and discovery, and what steps developers need to take to build their snaps quickly and efficiently. Snapcraft will try to make a lot of intelligent guesses, and resolve the list of needed stage packages for you, making the overall experience more pleasant. If you have any questions or suggestions, please join our forum, and let us know.

Photo by Ruthie on Unsplash.

Newsletter Signup

Related posts

Three ways to package your Electron apps as snaps

Software comes in many shapes and forms. One of the popular cross-platform, cross-architecture frameworks for building and distributing applications in Electron, which combines the Chromium rendering engine and the Node.js runtime. This makes Electron-based applications relatively easy to create. If you want to deploy Electron apps in Lin […]

The Future of Snapcraft

System hysteresis, when applied to software, can roughly be defined as an overall lag between desired implementation of code and actual implementation of said code. Ideally, this delay should be minimal, and programmers would be able to make instantaneous changes and improvements to their applications. In reality, things are more complex […]

Craft Parts – Reusable code, Snapcraft style

Throughout the ages, humans have always used simpler tools and materials to create more complex ones. Wood and stone for smelting bronze and iron; iron to create steel; vacuum tubes to create logical gates; logical gates to create advanced arithmetic engines, and so on. Modern software is no different. With Snapcraft in particular, the sn […]