1. Overview

What you’ll learn

In this tutorial we will create a snap of a Wayland-native application to act as the graphical user interface for an IoT or kiosk device. For the introduction to this tutorial series and the Mir display server please visit here.

We will walk through the process for a simple application, solving common problems along the way.

The combination of Snap, the “mir-kiosk” Wayland server and Ubuntu Core ensures the reliability and security for any graphical embedded device application.

What you’ll need

  • An Ubuntu desktop running any current release of Ubuntu or an Ubuntu Virtual Machine on another OS.
  • A ‘Target Device’ from one of the following:
    • A device running Ubuntu Core 18.
      This guide shows you how to set up a supported device. If there’s no supported image that fits your needs you can create your own core image.
    • Using a VM You don’t have to have a physical “Target Device”, you can follow the tutorial with Ubuntu Core in a VM. Install the ubuntu-core-vm snap: sudo snap install --beta ubuntu-core-vm --devmode For the first run, create a VM running the latest Core 18 image: sudo ubuntu-core-vm init From then on, you can spin it up with: sudo ubuntu-core-vm You should see a new window with Ubuntu Core running inside. Setting up Ubuntu Core on this VM is the same as for any other device or VM. See, for example, https://developer.ubuntu.com/core/get-started/kvm.
    • Using Ubuntu Classic You don’t have to use Ubuntu Core, you can use also a “Target Device” with Ubuntu Classic. Read this guide to understand how to run kiosk snaps on your desktop, as the particular details won’t be repeated here.

2. Using Wayland

We use Wayland as the primary display interface. We will use Mir to manage the display and support connections from Wayland clients. Snapd will confine the applications and enable Wayland protocol interactions through Mir, securely.

wayland-snap-architecture

Toolkits with Wayland support

  • GTK3/4
  • Qt5
  • SDL2

This is not an exhaustive list. There may be other toolkits that can work with Wayland but we know these work with Mir.

Native support for Wayland is the simplest case, as the application can talk to Mir directly.

If your application does not use the above toolkits, or fails to run with Wayland, fret not! In this tutorial we will describe how to snap X11-based applications.


3. Preparation

Install Mir

On your Ubuntu desktop, install Mir:

sudo apt-add-repository --update ppa:mir-team/release
sudo apt install mir-demos mir-graphics-drivers-desktop

Snapcraft setup

To build snaps, you need to install snapcraft:

sudo snap install snapcraft --classic

and install Multipass:

sudo snap install multipass --classic --beta

4. Test your application supports Wayland

Try to run your application like this:

miral-app -kiosk -launcher '/path/to/my_application'

This command runs a Mir server, and executes your application in a Wayland environment.

You should see a window pop up - Mir is running in this window (Mir-on-X) - and if your application supports Wayland, your application should appear inside this window.

If your application fails to start, or appears outside the Mir window, it may require X11 after all. To snap it, follow this guide instead.


5. Introducing glmark2-wayland

Let’s begin with a trivial example: glmark2-wayland - it is a test application that uses OpenGL and Wayland. This is a useful snap for verifying that the graphics stack of your hardware is correctly set up.

Install glmark2-wayland

Following the principle of detecting problems as early as possible we first prove our example works with Mir kiosk before snapping it.

Install glmark2-wayland from the “deb” archive:

sudo apt install glmark2-wayland

Verify glmark2-wayland runs on Mir

miral-app -kiosk -launcher 'glmark2-wayland --fullscreen'

Inside the Mir-on-X window, you should see various graphical animations, and statistics printed to your console. All should be fine before proceeding.


6. First Pass Snapping: Test on Desktop

For our first pass we will snap glmark2-wayland and run it in DevMode (i.e. unconfined) on our Ubuntu desktop.

This guide assumes you are familiar with creating snaps. If not, please read here first.

Create the snap directory by forking https://github.com/snapcrafters/fork-and-rename-me.

git clone https://github.com/snapcrafters/fork-and-rename-me.git glmark2-example

Inside the glmark2-example directory edit the “snap/snapcraft.yaml” file, and let’s try the following (SPOILER, won’t work immediately, read on for troubleshooting):

name: glmark2-example
version: '0.1'
summary: GLMark2 IoT example kiosk
description: GLMark2 IoT example kiosk, using Wayland
base: core18
confinement: devmode
grade: devel

apps:
  glmark2-example:
    command: "usr/bin/glmark2-wayland --fullscreen"

plugs:
  opengl:
  wayland:

parts:
  glmark2-wayland:
    plugin: nil
    stage-packages:
      - glmark2-wayland

  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - libwayland-egl1-mesa
      - libglu1-mesa

Building the snap

Create the snap by returning to the “glmark2-example” directory and running

snapcraft

You should be left with a “glmark2-example_0.1_amd64.snap” file.

Testing the snap

Run Miral-kiosk to launch a Wayland server, then install this snap and run it

miral-kiosk&
sudo snap install --dangerous ./glmark2-example_0.1_amd64.snap --devmode
snap run glmark2-example

(“dangerous” needed as local snap file being installed, and “devmode” required as we need to debug it)

Oh no! It fails with these errors:

Error: Failed to open models directory '/usr/share/glmark2/models'
Error: Failed to open models directory '/usr/share/glmark2/textures'
Error: main: Could not initialize canvas
[1]    16532 segmentation fault (core dumped)  snap run glmark2-example

We need to solve these.

Leave miral-kiosk running while we work through the issues.


7. Common Problem 1: Files are not where they’re expected to be!

One important thing to remember about snaps is that all its files are located in a subdirectory $SNAP which maps to /snap/<snap_name>/<version>. To prove this, try the following:

snap run --shell glmark2-example

This lands us inside a shell which exists inside the same snap environment the glmark2-wayland application will have. A quick “ls” tells us our theory is correct:

user@in-snap:~$ ls /usr/share/glmark2
ls: cannot access '/usr/share/glmark2': No such file or directory

If binaries have paths to resources hard-coded in, then in a snap environment it will fail to locate those resources.

Here glmark2-wayland is looking for files within /usr/share/glmark2, which in actuality are located in $SNAP/usr/share/glmark2:

user@in-snap:~$ ls $SNAP/usr/share/glmark2
models    shaders  textures

This is extremely common when snapping applications, therefore there are a few approaches to solving this:

1. Your application may read an environment variable

Your application may read an environment variable that specifies where it should look for those resources. In that case, adjust your YAML file to add it with something like this:

apps:
  glmark2-example:
  command: "usr/bin/glmark2-wayland --fullscreen"
  environment:
    RESOURCES_PATH: $SNAP/usr/share/glmark2

In our case, glmark2-wayland has the path /usr/share/glmark2 hard-coded in, so this is not going to work for us.

2. Changing the application

Sometimes you can edit/recompile the application to add the environment variable mentioned above. If it is your own code you are snapping, this is a good approach.

We could do this, but glmark2-wayland is not our own code and this adds an unnecessary maintenance overhead.

3. Using layouts for hard-coded paths

This snapd feature bind-mounts directories inside the snap into any location, so that the binaries’ hard-coded paths are correct. To use, append this to the YAML file:

layout:
  /usr/share/glmark2:
    bind: $SNAP/usr/share/glmark2

8. First Pass Snapping (resumed)

For this guide we are going to use “layouts” frequently whenever paths are hard-coded into binaries. So adding the snippet above, our YAML becomes

name: glmark2-example
version: '0.1'
summary: GLMark2 IoT example kiosk
description: GLMark2 IoT example kiosk, using Wayland
base: core18
confinement: devmode
grade: devel

apps:
  glmark2-example:
    command: "usr/bin/glmark2-wayland --fullscreen"

plugs:
  opengl:
  wayland:

parts:
  glmark2-wayland:
    plugin: nil
    stage-packages:
      - glmark2-wayland

  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - libwayland-egl1-mesa
      - libglu1-mesa

layout:
  /usr/share/glmark2:
    bind: $SNAP/usr/share/glmark2

Build this, install and try running the snap again. Unfortunately it still is not working, but we have got rid of the initial error messages:

Error: main: Could not initialize canvas
[1] 16532 segmentation fault (core dumped)  snap run glmark2-example

If you snap run --shell into the snap environment, you’ll see that /usr/share/glmark2 now contains the resources glmark2 needs - so one problem solved!


9. Common Problem 2: Unable to connect to Wayland server

Error: main: Could not initialize canvas

This error message is not too helpful, but an important thing to check is if the Wayland socket can be found. If not, glmark2-wayland cannot create a surface/canvas.

The convention is that the Wayland socket directory is specified by the $XDG_RUNTIME_DIR environment variable. The default name for the socket is wayland-0 but it can be different (specified by $WAYLAND_DISPLAY).

Generally, $XDG_RUNTIME_DIR is set to /run/user/$UID where $UID is the user id.

  • Ubuntu Desktop: miral-shell creates the Wayland socket at /run/user/$UID/wayland-0, where $UID is your user id.
  • Ubuntu Core: Mir-kiosk runs as root on Core, thus the Mir-kiosk snap’s Wayland socket is located at /run/user/0/wayland-0

However inside each snap’s environment, $XDG_RUNTIME_DIR is a custom subdirectory. To see this, this command gives a shell inside the same snap environment your application lives:

snap run --shell glmark2-example
user@in-snap:~# echo $XDG_RUNTIME_DIR
/run/user/1000/snap.glmark2-example

This reveals a problem with the Wayland socket not being in the right location for the application snap.

The solution we use is to symlink the Wayland socket into the in-snap $XDG_RUNTIME_DIR (and to be safe, ensure the directory exists before adding the link):

user@in-snap:~# mkdir -p $XDG_RUNTIME_DIR
user@in-snap:~# ln -s $XDG_RUNTIME_DIR/../wayland-0 $XDG_RUNTIME_DIR/

We could override the value of $XDG_RUNTIME_DIR in the app snap to be /run/user/0. The reason we do not is for avoiding later problems with strict confinement. In strict mode on Ubuntu Core, AppArmor will deny access to anything in that directory aside from the Wayland socket:

root@in-snap-strict:~# ls /run/user/0
ls: cannot open directory '/run/user/0': Permission denied
root@in-snap-strict:~# ls /run/user/0/wayland-0
/run/user/0/wayland-0

sudo journalctl -r” will show the AppArmor denial:

AVC apparmor="DENIED" operation="open" profile="snap.glmark2-example.glmark2-example" name="/run/user/0/" pid=24664 comm="ls" requested_mask="r" denied_mask="r" fsuid=0 ouid=0)

Let’s test it:

user@in-snap:~# $SNAP/usr/bin/glmark2-wayland
/snap/glmark2-example/x1/usr/bin/glmark2-wayland: error while loading shared libraries: libjpeg.so.8: cannot open shared object file: No such file or directory

Yikes! What’s happened?

Once again: files are not where they’re expected to be! Don’t forget, we’re in the snap environment still. All the libraries glmark2-wayland needs are not in the usual places - we need to tell it where.

Run these commands to set the linker paths $LD_LIBRARY_PATH and executable paths $PATH to commonly required locations in the snap environment (snapd does this before running your snap):

root@in-snap:~# export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH"
root@in-snap:~# export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${SNAP}/usr/lib/:${SNAP}/usr/lib/x86_64-linux-gnu/"

Note: you should add any additional library paths that the binary requires.

Snapcraft creates a script in $SNAP that contains the command specified to run. Have a look at

root@in-snap:~# cat $SNAP/command-glmark2-example.wrapper
#!/bin/sh
exec "$SNAP/usr/bin/glmark2-wayland" "--fullscreen" "$@"

It is this script which is called when you do “snap run …”. As we are still in the snap environment, we get the same result by running it:

root@in-snap:~# $SNAP/command-glmark2-example.wrapper

Oh, still fails with a bunch of errors like

Error: eglGetDisplay() failed with error: 0x3000
Error: eglGetDisplay() failed with error: 0x3000
Error: main: Could not initialize canvas

Courage! This is progress, we’re almost done in fact. There’s just one more thing to fix…


10. Common Problem 3: GL drivers are not where they usually are

This is another typical problem for snapping graphics applications: the GL drivers it needs are bundled inside the snap, and the application needs to be configured to use these drivers.

Is this “files are not where they’re expected to be” yet again? Yes, but here we are lucky as there’s environment variables we can use to specify the correct location for the GL driver files (you could use layouts too, but this is more efficient).

Simply use:

root@in-snap:~# export __EGL_VENDOR_LIBRARY_DIRS="$SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d"
root@in-snap:~# export LIBGL_DRIVERS_PATH="/snap/glmark2-example/x8/usr/lib/x86_64-linux-gnu/dri"
root@in-snap:~# export LIBVA_DRIVERS_PATH="/snap/glmark2-example/x8/usr/lib/x86_64-linux-gnu/dri"

Now try once more:

root@in-snap:~# $SNAP/command-glmark2-example.wrapper

It works! Woo! Finally! You deserve a nice cup of tea for that. Now we know what to fix, exit the snap environment with Ctrl+D.

On a Desktop Environment that supports Wayland you may find that glmark connects to your Wayland-based desktop shell and not Mir.

This is easy to work around: use a different Wayland socket name to avoid confusion:

miral-kiosk --wayland-socket-name mir-kiosk&
export WAYLAND_DISPLAY=mir-kiosk
snap run glmark2-example

Referring to “x86_64-linux-gnu” means the snap will only function on amd64 machines.

We will revisit this later to ensure the snap can be compiled to function on other architectures.


11. First Pass Snapping: The Final Bits

We need to set up the symlink of the Wayland socket into the $XDG_RUNTIME_DIR directory, and set the libgl environment variable before executing the binary inside the snap. The easiest option is using mir-kiosk-snap-launch a utility maintained by the Mir team to make this easy. Add the following to the .yaml in the parts section:

  mir-kiosk-snap-launch:
    plugin: dump
    source: https://github.com/MirServer/mir-kiosk-snap-launch.git
    override-build:  $SNAPCRAFT_PART_BUILD/build-with-plugs.sh opengl wayland

…and change command: in the snapcraft.yaml file to use the wayland-launch it supplies. Like this:

…
    command: wayland-launch $SNAP/usr/bin/glmark2-wayland --fullscreen
…

We can ask snapcraft to set the environment variables to fix graphics by adding an environment section to the .yaml:

…
environment:
  LD_LIBRARY_PATH: ${LD_LIBRARY_PATH}:${SNAP}/usr/lib/:${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/
  PATH: $SNAP/bin/:$SNAP/usr/bin/:${PATH}
  # Prep EGL
  __EGL_VENDOR_LIBRARY_DIRS: $SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
  LIBGL_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  LIBVA_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
…

We can also enable strict confinement and see if everything works. Due to the care we took above, it does. Your own application may require more tweaking to function fully confined however.

The working YAML file:

name: glmark2-example
version: '0.1'
summary: GLMark2 IoT example kiosk
description: GLMark2 IoT example kiosk, using Wayland
base: core18
confinement: strict
grade: devel

apps:
  glmark2-example:
    command: wayland-launch $SNAP/usr/bin/glmark2-wayland --fullscreen

plugs:
  opengl:
  wayland:

environment:
  LD_LIBRARY_PATH: ${LD_LIBRARY_PATH}:${SNAP}/usr/lib/:${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/
  PATH: $SNAP/bin/:$SNAP/usr/bin/:${PATH}
  # Prep EGL
  __EGL_VENDOR_LIBRARY_DIRS: $SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
  LIBGL_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  LIBVA_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri

parts:
  glmark2-wayland:
    plugin: nil
    stage-packages:
      - glmark2-wayland

  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - libwayland-egl1-mesa
      - libglu1-mesa

  mir-kiosk-snap-launch:
    plugin: dump
    source: https://github.com/MirServer/mir-kiosk-snap-launch.git
    override-build:  $SNAPCRAFT_PART_BUILD/build-with-plugs.sh opengl wayland

layout:
  /usr/share/glmark2:
    bind: $SNAP/usr/share/glmark2

Rebuild the snap and install it

snapcraft
sudo snap install --dangerous ./glmark2-example_0.1_amd64.snap --devmode
snap run glmark2-example

Inside the Mir-on-X window, you should see the same graphical animations you saw earlier, and statistics printed to your console. The difference is that this time they are coming from your fully snapped application!


12. First Pass Snapping: Notes

Why did we step through that process instead of simply presenting the working .yaml?

Because snapping applications can reveal lots of hard-coded paths and assumptions that applications make, which snap confinement will break. It is good to understand the steps needed to debug and solve these problems.

There can be many, many environment variables and support files that need to be set up inside snaps, for applications to run correctly. Thankfully much of this work has already been done and automated in the snapcraft-desktop-helpers project, which we will be using in a follow-up tutorial.

We now have a snap working with a desktop Wayland server

But our goal is to have it working as a confined snap, with mir-kiosk on your chosen device.


13. Second Pass Snapping: Your Device

The goal here is to have our graphical snap running full-screen on your device.

Before we proceed, you need to have Ubuntu Core 18 already running on your device.

Device Setup

Open another terminal and ssh login to your device and from this login install the “mir-kiosk” snap.

snap install mir-kiosk

It auto-starts, so now you should have a black screen with a white mouse cursor.

“mir-kiosk” provides the graphical environment needed for running a graphical snap.

Differences between running on Desktop and Ubuntu Core

The main difference is a current technical limitation with snaps:

  • Graphical snaps need to run as root

You need not worry about this being a security issue however, the snap security model is designed to make this safe.

Another difference is that we want the graphical app to start automatically (and restart if it crashes).

We can address these differences with a simple change to the snapcraft.yaml file: add a “daemon” command to apps:

…
apps:
…
  daemon:
    command: run-daemon wayland-launch $SNAP/usr/bin/glmark2-wayland --fullscreen
    daemon: simple
    restart-condition: always
…

The run-daemon utility (like wayland-launch) comes from mir-kiosk-snap-launch. It ensures that the daemon runs on Ubuntu Core and (by default) not on Classic systems.

The explicit restart-condition ensures glmark2 is restarted, even when it quits successfully.

Note this results in the console output from the application being sent to the journal. To view its output while running, do sudo snap logs -f glmark2-example.

That’s all the changes we need to make! We just need to snap it up for the target device.

The final full YAML file:

name: glmark2-example
version: '0.1'
summary: GLMark2 IoT example kiosk
description: GLMark2 IoT example kiosk, using Wayland
base: core18
confinement: strict
grade: devel

apps:
  glmark2-example:
    command: wayland-launch $SNAP/usr/bin/glmark2-wayland --fullscreen

  daemon:
    command: run-daemon wayland-launch $SNAP/usr/bin/glmark2-wayland --fullscreen
    daemon: simple
    restart-condition: always

plugs:
  opengl:
  wayland:

environment:
  LD_LIBRARY_PATH: ${LD_LIBRARY_PATH}:${SNAP}/usr/lib/:${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/
  PATH: $SNAP/bin/:$SNAP/usr/bin/:${PATH}
  # Prep EGL
  __EGL_VENDOR_LIBRARY_DIRS: $SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
  LIBGL_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  LIBVA_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri

parts:
  glmark2-wayland:
    plugin: nil
    stage-packages:
      - glmark2-wayland

  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - libwayland-egl1-mesa
      - libglu1-mesa

  mir-kiosk-snap-launch:
    plugin: dump
    source: https://github.com/MirServer/mir-kiosk-snap-launch.git
    override-build:  $SNAPCRAFT_PART_BUILD/build-with-plugs.sh opengl wayland

layout:
  /usr/share/glmark2:
    bind: $SNAP/usr/share/glmark2

14. Building for different architectures

If your target device has the same CPU architecture as your PC, you can just build it with the usual:

snapcraft

However you need to build the snap for a different CPU architecture, it is not quite as simple. However we provide a build service to help with this.

Building snaps using the Snapcraft build service

One day, perhaps, snapcraft will fully support cross building with the --target-arch option. But getting that to work is beyond the scope of this tutorial. Instead we’ll make use of Snapcraft builders to build the snap for all architectures (including the one your device provides).

Create a GitHub repository for your snap and push your changes to the snap project there:

git init
git commit -a
git remote remove origin
git remote add origin https://github.com/<username>/<repo>.git
git push -u origin master

Now visit Snapcraft Build and follow the “Set up in Minutes” link. You can configure Snapcraft Build to watch your GitHub repository, and it will auto-build Snaps supporting multiple architectures: amd64, i386, armhf and amd64, ppc64el and s390x. This is a free shared build service so there can be delays before your build is started.

Visiting the link

https://build.snapcraft.io/user/<username>/<repo>

will show you your build status, allow you to find where the built Snaps are, and trigger a new build.

To download the built Snap, you need to click on the build number of the architecture you need, and the first line of the build log will be a URL pointing at Launchpad (of this form):

https://launchpad.net/~build.snapcraft.io/+snap/91eff219d76d5721eb24ffb6a83032b6/+build/593821

Visiting this URL, you can find the built Snap under the “Built files” section.

To test your snap on your target device, find the build for your device architecture and download it. Or you can grab its URL and download it directly to your device:

wget https://launchpad.net/~build.snapcraft.io/+snap/91eff219d76d5721eb24ffb6a83032b6/+build/593821/+files/glmark2-wayland_0.1_armhf.snap

15. Deploy the snap on the device

Push the snap to your device:

scp glmark2-example_0.1_amd64.snap <user>@<ip-address>:~

using your device’s SSH username & IP address details.

We now have the .snap file on the device in its home directory. We need to install the snap, configure it to talk Wayland to mir-kiosk and run the application. In your ssh session to your device:

snap install --dangerous ./glmark2-example_0.1_amd64.snap

On your device, you should see the same graphical animations you saw earlier (and statistics logged to the journal). It will continue to run until you run “snap stop glmark2-example”.

Your device is now a kiosk! Rebooting will restart mir-kiosk and glmark2-example automatically.

Should you wish to share this snap, the next step would be to push your snap to the Snap Store.


16. Congratulations

Congratulations, you have created a snap of a Wayland kiosk application and deployed it to an Ubuntu Core device.

Note

For more details on mir-kiosk-snap-launch visit: Kiosk snaps made easy