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.

Newsletter Signup

Related posts

Discover cool apps with snap find

Software discovery and installation broadly comes in two flavors – via graphical user interface or on the command line. If you’re using a Linux distribution with a friendly software frontend offering integrated snap support, e.g. KDE Discover or GNOME Software, you can enjoy the experience without having to resort to using a terminal wind […]

How to build ROS 2 Eloquent Snaps

The end of 2019 brings about the latest ROS 2 release – Eloquent Elusor. Despite an ever growing set of features and some changes throughout the ecosystem, packaging with snaps is as easy as always. Let’s go through a quick example! Prerequisites You’ll need two tools: “snapcraft,” the program that builds snaps, and “multipass,” the […]

Building a Rust snap by Example

There’s plenty of official documentation which details how to create snaps. We live in the copy/paste generation though. So lets walk through a real-world example you can use as a template, or selectively yoink sections from. This is the first in a series of posts which break down how to build snaps based on published […]