Building a Java snap by example

Following up on the previous example of building a rust and C based snaps, I thought we’d take a look at bundling a Java application as a snap. In this example we’ll use an open source game called “Shattered Pixel Dungeon“. It’s a little more complex than some more common snaps, which helps highlight some of the ways we can accommodate tricky-to-snap applications.

You’ll find the full snapcraft.yaml in my git repository.

Metadata

As usual, we can start with the easy part, the metadata. This is the human-readable section, which seeds the information in the store, once published. After being initially set, this can either be changed here in the yaml, or in the Snap Store listing section. If updating here, the snap needs to be rebuilt and the command snapcraft push-metadata used to upload the metadata.

name: shattered-pixel-dungeon
summary: LibGDX port of the awesome Pixel Dungeon
description: |
 Shattered Pixel Dungeon is a Roguelike RPG, with pixel art graphics and lots of variety and replayability. Every game is unique, with four different playable characters, randomized levels and enemies, and over 150 items to collect and use. The game is simple to get into, but has lots of depth. Strategy is required if you want to win!

In the previous blog post, I used adopt-info to programmatically determine which version of the application to build, based on the most recent tag in the git repo. Here, I’ve just specified the version number directly in the yaml. I’ve done this because the application doesn’t change often. When the upstream developer publishes a new release, I will just edit this line which triggers a new build of the latest release, thanks to https://build.snapcraft.io/ .

version: '0.7.5f'

Base

The base determines which minimal core snap will be used at runtime. When the Shattered Pixel Dungeon snap is installed on a system, the core18 base snap will be automatically installed, too. Other (older) bases are available and in the future, newer bases will be made, but today, this is an appropriate base to build with.

Essentially, this determines the version of libc the application is built against. The core18 base snap is built from Ubuntu 18.04 (LTS) and as such, we should build this snap in a VM or container running the same release. Thankfully snapcraft knows how to do this using either multipass or lxd by default.

A byproduct of this will be the version of the Java Virtual Machine we bundle in the snap. We’ll see that further down in the stage-packages section.

base: core18

Architectures

Both the snapcraft build service and Launchpad have the capability to build snaps for 6 architectures. However, not all applications may successfully build or run on all of them. Sometimes the dependencies or build tools aren’t available for every CPU architecture, or perhaps the toolchain is not supported on anything but mainstream architectures.

Unlike the Syzygy snap, in the case of Shattered Pixel Dungeon, I didn’t specify the architectures stanza, which means it will build for all supported architectures, if submitted to the snapcraft build service. So if you’ve got an IBM Mainframe handy, perhaps you can play Shattered Pixel Dungeon on it!

Confinement

Shattered Pixel Dungeon is a self-contained game which doesn’t require much system-level access outside the sandbox it runs in. It does need access to take input from the keyboard and mouse, display things on the screen, and play audio. As such, it can be strictly confined.

confinement: strict

Grade

Grade is an indicator of the quality of this snap. Initially, we might set this to devel while it’s in development. However, in order to be published in the stable or candidate channels in the store, we should set this to stable. Only applications with a stable grade are permitted in the stable and candidate channels.

grade: stable

Parts

Parts are the meat and potatoes of the snap. Here we outline what actually gets built and put inside the snap package. Shattered Pixel Dungeon is a game written in Java, and has binary artifacts hosted externally on GitHub. We can use the nil plugin to allow us total flexibility with how to “build” the part using override-build.

parts:
  shattered-pixel-dungeon:
    plugin: nil

override-build

The $SNAPCRAFT_PROJECT_VERSION inserts the version number from the `version:` line above in order to grab the correct release of the game. The $SNAPCRAFT_PART_INSTALL points to a folder that `snapcraft` will assemble the parts from at build time. Typically, the parts are pulled to project/partname/src, built in project/partname/build and then ‘installed’ into project/partname/install. Using the nil plugin and override-build stanza, we can control exactly which files from the part(s) end up in the snap.

    override-build: |
      wget -O $SNAPCRAFT_PART_INSTALL/ShatteredPD.Desktop.v$SNAPCRAFT_PROJECT_VERSION.jar https://github.com/00-Evan/shattered-pixel-dungeon-gdx/releases/download/v$SNAPCRAFT_PROJECT_VERSION/ShatteredPD.Desktop.v$SNAPCRAFT_PROJECT_VERSION.jar
      mv $SNAPCRAFT_PART_INSTALL/usr/lib/jvm/java-8-openjdk-* $SNAPCRAFT_PART_INSTALL/usr/lib/jvm/java-8-openjdk

Here we download the published upstream jar file which will land directly in the snap, unmodified. Next we rename the folder that the java runtime environment is installed into, removing the architecture. This makes configuring the environment later in the snap much easier.

build-packages

Given the part “build” is essentially just using wget to grab the jar from the upstream git repo and plonk it inside the snap, the only build dependency we have is the wget command itself. This won’t get bundled in the snap, but only used at build time.

    build-packages:
      - wget

stage-packages

Here we specify the Java Runtime Environment via the openjdk-8-jre package, which comes from the Ubuntu 18.04 LTS archive. This is by virtue of having core18 as our base snap.


We also pull in a number of other libraries to enable the game to draw on the screen and play audio. If we didn’t specify these, the game would not work, even if the libraries existed on the host OS. The snap is strictly confined, so it cannot see any external libraries, and we need to list them here, so that they are bundled with the game.

We can discover which packages to add by running snapcraft without specifying any stage packages in this section. Snapcraft will introspect the binaries shipped in the snap, and list a best-guess array of required packages.

    stage-packages:
      - openjdk-8-jre
      - ca-certificates
      - ca-certificates-java
      - libpulse0
      - libpulsedsp
      - libxxf86vm1
      - libgl1-mesa-dri
      - libglu1-mesa
      - libgl1-mesa-glx
      - libgles2-mesa
      - x11-xserver-utils

Additional parts

desktop-gtk2 part

This is a reusable component developed by the Ubuntu Desktop developers. It stands up an environment inside the sandboxed snap at runtime. This is necessary because inside the snap, the application cannot get to some of the desktop features which may (or may not) be installed on the host. There are versions for GTK2, GTK3, Qt4 and Qt5 available at the GitHub url in the source of this part.

These are a great time-saving convenience. This removes the need for each snap packager having to figure out what’s necessary to successfully launch a desktop application within a confined environment. I merely copy/pasted it in, and adjusted the launcher (below) to use it.

  desktop-gtk2:
    build-packages:
      - build-essential
      - libgtk2.0-dev
    make-parameters:
      - FLAVOR=gtk2
    plugin: make
    source: https://github.com/ubuntu/snapcraft-desktop-helpers.git
    source-subdir: gtk
    stage-packages:
      - libxkbcommon0
      - ttf-ubuntu-font-family
      - dmz-cursor-theme
      - light-themes
      - adwaita-icon-theme
      - gnome-themes-standard
      - shared-mime-info
      - libgtk2.0-0
      - libgdk-pixbuf2.0-0
      - libglib2.0-bin
      - libgtk2.0-bin
      - unity-gtk2-module
      - locales-all
      - libappindicator1
      - xdg-user-dirs
      - ibus-gtk
      - libibus-1.0-5

launcher part

Some applications require additional help to launch successfully and operate correctly when contained in a snap. This is especially true for applications written using more established languages and frameworks like Java. For this game, I bundled padsp as a launch tool to direct audio output from the game to pulseaudio.

I also had to create an ‘alias’ called sensible-browser because Java has a somewhat old-school way of launching the web browser on Linux. I place a script early in the path called ‘sensible-browser’ which runs snapctl user-open that works when confined. Finally, I rename an xorg utility xprop because it causes a crash when called in the snap environment, and isn’t actually needed.

We use the organize keyword as part of the dump plugin to both determine where files being dumped end up, and 

  launcher:
    plugin: dump
    source: snap/local
    organize:
      'padsp': 'bin/'
      'sensible-browser': 'bin/'
      'usr/lib/*/gvfs/*.so': 'usr/lib'
      'usr/bin/xprop': 'usr/bin/xprop.disabled'

Application

The apps stanza is where we setup the environment and expose the binary inside the snap to the outside world. This includes ensuring the binary can find the GL drivers required to paint the game window, and extend the library search path to include the pulseaudio libraries for audio playback.

In addition, the game has an internal environment variable, which should point to the game data folder. We use the SNAP runtime environment variable to construct the correct path to the game files configured earlier.

Finally, we specify the required plugs to enable the game to draw on the screen, play audio, access the GPU via opengl, and suppress the screensaver.

apps:
  shattered-pixel-dungeon:
    command: desktop-launch $SNAP/bin/padsp $JAVA_BIN -jar -Duser.home=$SNAP_USER_COMMON $SNAP/ShatteredPD.Desktop.v$SNAP_VERSION.jar "$@"
    environment:
      XDG_DATA_HOME: "$SNAP/usr/share"
      JAVA_HOME: "$SNAP/usr/lib/jvm/java-8-openjdk"
      JAVA_BIN: "$SNAP/usr/lib/jvm/java-8-openjdk/bin/java"
      _JAVA_OPTIONS: "-Dsun.java2d.xrender=true -Dprism.useFontConfig=false -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel -Dswing.crossplatformlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel"
      JAVA_FONTS: "$SNAP/usr/share/fonts/truetype"
     PATH: "$SNAP/bin:$PATH:$SNAP/usr/lib/jvm/java-8-openjdk/jre/bin"
      GVFS_MOUNTABLE_DIR: "$SNAP/usr/share/gvfs/mounts"
      GVFS_MONITOR_DIR: "$SNAP/usr/share/gvfs/remote-volume-monitors"
      LD_LIBRARY_PATH: "$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/pulseaudio"
    plugs:
    - opengl
    - home
    - joystick
    - pulseaudio
   - desktop
   - wayland
    - x11
    - desktop-legacy
    - unity7

Summary

Building and publishing snaps of applications written in Java can be a little tricky. Ensuring the launch command has a correctly configured environment, and that it enables required command-line options may require a little trial and error. Once the environment is setup correctly though, it rarely needs changing, as the Java runtime environment is very stable. As the upstream developer pushes out new releases, we just crank a new build with the existing config, test it and release it.

We welcome new games and applications in the Snap Store. The developers of snapd, snapcraft and the Snap Store hang out over on the snapcraft forum. Join us there if you have any questions or comments about this or need assistance building a new snap.

Photo by Anni Denkova on Unsplash

Posted in: