Building a Java snap by example
by Alan Pope on 6 February 2020
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