Building a C snap by example
by Igor Ljubuncic on 19 December 2019
Quite often, getting started with new technologies is a chicken and an egg problem. You want to fall back and rely upon work done by others, so you can learn from it, and yet, as a technological pioneer, you will be facing first-of-kind issues that won’t have readily available answers. We are fully aware of this conundrum, and would like to make the process of creating snaps more accurate and enjoyable.
People often require tangible examples, in addition to sound documentation, to translate theory into practice. Following up on a Rust example from last week, we’d like to walk you through the snapping process of a C language application. We will do this with a rather non-trivial example of Dosbox-x, which should help you get familiar with the process, and hopefully encourage you to snap your own code.
Metadata
We declare the application name, summary and a multi-line description.
name: dosbox-x
summary: DOSBox-X fork of the DOSBox project
description: |
DOSBox-X is a x86 emulator with Tandy/Hercules/CGA/EGA/VGA/SVGA
graphics sound and DOS. It's been designed to run old DOS games
under platforms that don't support it.
Optionally, you can also use the adopt-info key to extract application metadata from a file. This is a convenient way of using up-to-date external data in your snapcraft.yaml. Please note that we don’t have the version set above. We will explain that shortly.
Base
As the documentation explains, a base snap is a special kind of snap that provides a runtime environment with a minimal set of libraries that are common to most applications. The use of core18 (synonymous with Ubuntu 18.04 LTS) is recommended, and it will be applicable in most scenarios.
base: core18
Architecture
This is an optional stanza, which defined the target architectures. If you do not have the necessary toolchain to build for all required platforms, you can use the remote build feature or the online Build Service to create your snaps.
architectures:
- build-on: i386
- build-on: amd64
Confinement
By design, snaps are confined and limited in what they can do. This is an important feature that distinguishes snaps from software distributed using the traditional repository methods. The confinement allows for a high level of isolation and security, and prevents snaps from being affected by underlying system changes, affecting one another or affecting the system.
The confinement level describes what degree of access the application will have once installed on the user’s system. Confinement levels can be treated as filters that define what type of system resources the application can access outside the snap.
confinement: strict
Grade
This is an optional key that defines the quality of the snap. If you’re early in the development phase, you can use devel, and switch to stable when you feel confident your application can be published into the candidate or stable channels.
grade: stable
Application
This stanza defines the application(s) inside the snap. There can be more than one application. Each application must have a name and the command to run. Some applications will also require additional declarations, like environment and plugs.
apps:
dosbox-x:
command: desktop-launch $SNAP/dosbox-x
environment:
LD_LIBRARY_PATH: "$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio"
plugs: [network, network-bind, unity7, opengl, home, pulseaudio, desktop, desktop-legacy, removable-media]
In our example, we need to modify the application runtime environment. We override the library path for the application to search for the pulseaudio binary inside the snap. We use several specific environment variables for this.
Moreover, we also define a number of plugs – part of the interfaces mechanism, which allows strictly confined snaps to access and utilize system resources. Without having declared the plugs, Dosbox-x would not have been able to access the home directory, network, audio, or any other resource.
Our list provides the application with access to: network connection, ability to bind ports (depends on user permissions), Unity desktop, OpenGL, home directory, PulseAudio, desktop, and removable media (like USB devices).
Parts
This section defines all the components used to build the Dosbox-x application. In this particular case, we will be compiling from sources. For other applications, the parts section can be much simpler, and may not even require any compilation. Let’s do it step by step.
parts:
dosbox-x:
plugin: autotools
after: [desktop-glib-only,ffmpeg]
source-type: git
source: https://github.com/joncampbell123/dosbox-x.git
We declare a part called dosbox-x. We will build it using the autotools plugin. This part will only be built after two other parts are complete – desktop-glib-only and ffmpeg. The source for our compilation comes from a Git repository.
Build override
The override-build scriplet allows us to customize the build process. It means the default compilation options in the selected plugin (in this case, autotools) are not sufficient for us, and we need to make modifications.
Similarly, you can also use override-pull to modify the collection of sources, or make changes before the build process starts. The overrides are written as Dash scripts. Once you have made your modifications, you can optional call the default build command snapcraftctl build.
Now, let’s see what we have in our snapcraft.yaml:
override-build: |
# There is no pattern in the release tags for DosBox-X
# This should resolve to a version or datestamp or both.
last_committed_tag="$(git describe --tags --abbrev=0)"
last_committed_tag_ver="$(echo ${last_committed_tag} | sed -e 's/dosbox-x-//' -e 's/wip-//' -e 's/windows-//' -e 's/v//')"
last_released_tag="$(snap info dosbox-x | awk '$1 == "beta:" { print $2 }')"
# If the latest tag from the upstream project has not been
# released to beta, build that tag instead of master.
if [ "${last_committed_tag_ver}" != "${last_released_tag}" ]; then
git fetch
git checkout "${last_committed_tag}"
fi
./build
cp src/dosbox-x $SNAPCRAFT_PART_INSTALL
snapcraftctl set-version $(head -n 1 CHANGELOG | awk -F ' ' '{print $1}' | tr -d '\r')
In our example, the main purpose of the override process is to determine the exact version of the application, grab the relevant code and then build it. At the end of the section, we set the version of our snap (which is why it wasn’t declared at the beginning of the yaml).
Build packages
You may require additional libraries or tools to compile your code. This optional list will include any dependencies you need to build the part(s). The package names need to correspond to the software available in the relevant repository archives (e.g. Ubuntu 18.04), as they will be fetched by the apt package manager inside the snap’s build environment.
build-packages:
- g++
- make
- libsdl1.2-dev
- libpng-dev
- libsdl-net1.2-dev
...
Stage packages
Similarly, you may require additional, optional runtime components that are not available in your selected base. Since version 3.7, snapcraft will auto-detect missing runtime dependencies, and propose a fix for you, allowing for faster development.
stage-packages:
- libnuma1
- libogg0
- libopus0
- libsoxr0
...
Other parts
Now that we understand the bulk of the logic, the build process of the other parts in the snapcraft.yaml should be easier.
desktop-glib-only:
plugin: make
source: https://github.com/ubuntu/snapcraft-desktop-helpers.git
source-subdir: glib-only
build-packages:
- libglib2.0-dev
stage-packages:
- libglib2.0-bin
For the desktop-glib-only part, we use the make plugin, and we specify a single build and stage package each. In general, the desktop helpers simplify the snapping process for desktop applications, by initializing desktop-specific functionality (themes, fonts, etc.).
nv-codec-headers:
plugin: make
source: https://github.com/FFmpeg/nv-codec-headers/releases/download/n9.0.18.1/nv-codec-headers-9.0.18.1.tar.gz
override-build: |
make install PREFIX=/usr
build-packages:
- pkg-config
The nv-codec-headers part is a FFmpeg version of headers required to interface with Nvidia codec APIs. This may not be applicable in every scenario, but it will be included in the snap, and allow users with Nvidia graphics card to be able to make full use of Dosbox-x functionality (including recording of in-game videos, for instance). Similarly to what we’ve seen before, we use the make plugin (with the relevant build overrides and build packages).
fdk-aac:
plugin: autotools
source: https://github.com/mstorsjo/fdk-aac/archive/v2.0.0.tar.gz
build-packages:
- g++
configflags:
- --prefix=/usr
- --disable-static
prime:
- usr/lib
- -usr/lib/pkgconfig
This next part compiles a standalone library of the Fraunhofer FDK AAC code. We use the autotools plugins, we need the g++ build package, and we have several configflag overrides. We also make use of the prime key.
Prime
In the snap creation process, the prime step is used to “tidy up” the built and staged components before they are assembled into the final snap image. It allows you to add or remove specific folders and files from a built part, so that your snap only contain the relevant data.
Components listed with the minus sign prefix (in addition to the actual YAML syntax list) will be excluded, while those without it will be included. Specifically in our example, we do want the entire usr/lib folder from the build fdk-aac part, minus the pkgconfig folder. This can save space, and make the created snap smaller.
Ffmpeg
The last part (and the last component of our snapcraft.yaml file) is ffmpeg. We need it included in the Dosbox-x snap so that the application can capture and convert in-game videos. This is a necessary step, because the snap is strictly confined, and it cannot access and execute “random” binaries on the system. To that end, we’re compiling ffmpeg so it’s included in the snap.
ffmpeg:
plugin: autotools
source: https://github.com/FFmpeg/FFmpeg.git
override-pull: |
snapcraftctl pull
last_committed_tag="$(git tag -l | grep -v v | sort -rV | head -n1)"
last_committed_tag_ver="$(echo ${last_committed_tag} | sed 's/n//')"
last_released_tag="$(snap info ffmpeg | awk '$1 == "beta:" { print $2 }')"
# If the latest tag from the upstream project has not been released to
# beta, build that tag instead of master.
if [ "${last_committed_tag_ver}" != "${last_released_tag}" ]; then
git fetch
git checkout "${last_committed_tag}"
fi
snapcraftctl set-version "$last_committed_tag_ver"
build-packages:
- libass-dev
- libbz2-dev
...
We use the autotools plugin, once more. Here, we also override the pull process, similar to what we did with the build override scriptlet. We set the ffmpeg version based on our version checking logic. Ffmpeg also had a long list of build and stage packages.
And by that, we’re done building our snap!
Summary
We hope you find exercise useful, and it will help you get started with your own snap. Of course, you still require a reasonable understanding of the application you’re building and its requirements. Equally, it is important for snapcraft to offer a simple, friendly build environment so you can focus on your code.
We intend to follow up with several more guides on how to create snaps with other programming languages and additional use of plugins. If you have any questions or requests, or you’d like to comment on this article, please join our forum for a discussion.
Photo by Maarten van den Heuvel on Unsplash.