-
-
Notifications
You must be signed in to change notification settings - Fork 563
Bundling Java apps
As of 2019, we can use https://adoptopenjdk.net/ to produce a subset of Java just enough to run our payload application.
The following illustrates how to do this for an application that is provided by upstream without a JRE.
wget -c https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.3%2B7_openj9-0.14.3/OpenJDK11U-jdk_x64_linux_openj9_11.0.3_7_openj9-0.14.3.tar.gz
tar xf OpenJDK11U-jdk_x64_linux_openj9_11.0.3_7_openj9-0.14.3.tar.gz
wget -c https://github.com/bengtmartensson/harctoolboxbundle/releases/download/ci-build/IrScrutinizer-1.4.3-SNAPSHOT-x86_64.AppImage
sudo mount ./IrScrutinizer-1.4.3-SNAPSHOT-x86_64.AppImage /mnt
sudo cp -r /mnt/ AppDir && sudo chown -R $USER AppDir
sudo umount /mnt
#
# Find out which subset of the Java classes need to be bundled
#
./jdk-11.0.3+7/bin/jdeps --list-deps AppDir/usr/share/irscrutinizer/IrScrutinizer-jar-with-dependencies.jar
java.base
java.datatransfer
java.desktop
java.logging
java.xml
# For other applications, it may be necessary to use --module-path, see
# https://medium.com/azulsystems/using-jlink-to-build-java-runtimes-for-non-modular-applications-9568c5e70ef4
#
# Create our custom subset of Java
#
./jdk-11.0.3+7/bin/jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.datatransfer,java.desktop,java.logging,java.xml --output usr
#
# Copy it into the AppDir and re-create the AppImage
#
cp -Rf usr AppDir/
wget -c https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage AppDir/
# Launcher script needs to be adjusted a bit, make sure to run
# java -jar -Xquickstart
# because otherwise Java optimizes for long-running processes,
# which means a slower start (roughly 5 rather than 3 seconds)
# https://www.eclipse.org/openj9/docs/xquickstart/
# From 76 MB to 37 MB
# TBD: Use Shared Classes Cache?
me@host:~$ ./jdk-11.0.3+7/bin/jdeps --list-deps squashfs-root/usr/bin/MediathekView.jar
Warning: split package: javax.transaction.xa jrt:/java.transaction.xa squashfs-root/usr/bin/MediathekView.jar
JDK removed internal API/apple.laf
JDK removed internal API/com.apple.laf
JDK removed internal API/com.sun.java.swing.plaf.windows
JDK removed internal API/sun.awt.windows
JDK removed internal API/sun.swing
JDK removed internal API/sun.swing.plaf.synth
java.base/sun.security.action
java.compiler
java.datatransfer
java.desktop/sun.awt
java.desktop/sun.awt.image
java.desktop/sun.awt.shell
java.desktop/sun.swing
java.instrument
java.logging
java.management
java.naming
java.prefs
java.rmi
java.scripting
java.security.jgss
java.security.sasl
java.sql
java.xml
jdk.jfr
jdk.jsobject
jdk.unsupported
jdk.unsupported.desktop
jdk.xml.dom
me@host:~$ ./jdk-11.0.3+7/bin/jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base/sun.security.action,java.compiler,java.datatransfer,java.desktop/sun.awt,java.desktop/sun.awt.image,java.desktop/sun.awt.shell,java.desktop/sun.swing,java.instrument,java.logging,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.xml,jdk.jfr,jdk.jsobject,jdk.unsupported,jdk.unsupported.desktop,jdk.xml.dom --output usr
Error: Module java.desktop/sun.awt.image not found
What to do?
The preferred way of packaging JavaFX and Java desktop applications in Java 9+ is the javapackager. This guide assumes you already know how to use it (e.g. for .dmg or .exe packaging).
For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool.
Generate Java application image using java packager. Use the -native image
option, which (on Linux) produces the application directory containing both, your application and the JVM. There is also an Ant library to invoke javapackager (example build.xml) as well as a variety of community-based plugins for other build tools such as Maven or Gradle.
Your resulting java application image should look like this:
./libpackager.so # runtime library generated by javapackager
./YourApp # binary generated by javapackager
./app/ # your actual java application
./app/your-app.jar
./app/some-lib.jar
./app/some-resource.xml
./runtime/ # contains the JVM
./runtime/...
ℹ️ This step is only needed for non-jigsaw apps if you whish to reduce the size of your embedded JVM. If your project happens to be jigsaw compatible, the JVM will automatically be stripped down to only the modules needed by your application.
Otherwise you can try to reduce the footprint of your JVM manually. The --limit-modules
flag doesn't seem to work at the moment of writing this. Don't know if it's a bug or a feature. If you know exactly what modules you depend on (you can use the jdeps
tool to analyze module dependencies), you can instead use jlink manually to create a customized version of the JVM.
In this example jlink
is used to fully replace the runtime/
directory of the generated java application image from the previous step.
--strip-native-commands
argument, since you don't want to distribute a JVM that can be started from third party processes. This increases security, reduces the size of your image and fits the strategy of AppImages to isolate applications from each other.
Now just copy the full java application image to YourApplication.AppDir
and create a symlink from AppRun
to YourApp
(make sure to use relative paths!).
You also need a desktop file as well as an application icon at the root of your AppDir. The resulting AppDir should look like this:
YourApplication.AppDir/AppRun # relative symlink to YourApp
YourApplication.AppDir/libpackager.so
YourApplication.AppDir/YourApp
YourApplication.AppDir/YourApp.desktop # desktop file
YourApplication.AppDir/YourApp.png # application icon
YourApplication.AppDir/app/
YourApplication.AppDir/app/your-app.jar
YourApplication.AppDir/app/some-lib.jar
YourApplication.AppDir/app/some-resource.xml
YourApplication.AppDir/runtime/
YourApplication.AppDir/runtime/...
This is the very minimal AppDir containing all necessary files. Though, you might also want to include a usr/share/applications
and usr/share/icons
directory with its respective contents to improve desktop integration. See AppDir for details.
This is an example of step 3.
Side note: The binary created by javapackager will not run, if it resides inside a path that contains bin
, presumably due to some security checks. So other than normal AppImages you must not store it in usr/bin/
.
Now, that your AppDir is ready, you can create the actual AppImage. Download appimagetool and invoke:
appimagetool-x86_64.AppImage YourApplication.AppDir YourApplication.AppImage
Here is what I did for my project. It is not a perfect solution, but it works for me. Please feel free to improve and/or generalize this information, but it should be a good starting point.
I couldn't find a standalone version of the OpenJRE to download, but I used the Open JRE from Ubuntu and it works on every linux distribution I tested, following the testing guidelines given by appimage (old debian, etc.)
The command cp -rL /usr/lib/jvm/java-1.8.0-openjdk-amd64/jre linux-jre
did the trick for me. -r
is for recursive. -L
replaces sym links with the link targets.
It would almost certainly be better to unpack the java runtime .deb file rather than using the method I outlined above, but I spent a little time trying to get that to work and couldn't. I think you may need to merge the contents of more than one deb together to get the full jre. If you get that working, please let me know or edit it in.
If you are going to release a 64 bit and 32 bit version, you need to do this on a 32 bit linux distribution as well.
Anyway, save that JRE for later.
Here's mine. It's simple and direct. We're going to launch this using appimage's built in launcher script, so it doesn't need to do anything fancy:
#!/bin/bash
DIR="$(dirname "$(readlink -f "$0")")"
cd $DIR
./jre/bin/java -jar <any jvm arguments you need> target.jar "$@" &
disown
exit 0
The first thing it does it change the working directory to the directory of this script. My program expects that and you can't really change the working directory in java, so I do it here.
Then I launch java, calling my program, passing the commandline arguments with $@
. Notice the &
at the end of the command. This means "run in the background".
Then we disown
the java process, meaning we can exit this script without terminating the jvm and our program.
Then we exit. Done!
Here's mine:
[Desktop Entry]
Name=Hypnos
Exec=hypnos %F
Icon=hypnos
Type=Application
Categories=Audio;AudioVideo;
Comment=Music Player and Library
MimeType=inode/directory;audio/flac;
Name[en]=Hypnos
Terminal=false
StartupNotify=true
NoDisplay=false
Save this as [program name].desktop and keep it for the next step.
Here's how mine is structured. I'm using my actual file names because it's easier and more clear than typing [program name]
all the time.
Hypnos.AppDir/
usr/
bin/
jre/ <-- our jre folder from step 1
hypnos <-- our launch script from step 2
hypnos.jar <-- your program's jar file
<Whatever other resources your program uses>
<this is your program's main directory>
hypnos.desktop
Hypnos.png
AppRun <-- provided by app image
Use the appimage tool to build your app image with a command that looks like this:
./appimagetool-x86_64.AppImage Hypnos.AppDir Hypnos.AppImage
If everything works, voila! You have an appimage with an embedded JRE. If you're having trouble, join the IRC channel and look for me (JoshuaD) and I'll see what I can do to help you.
If you use the above method, your program will run, but in ps
and in the taskmananger, it will be named java
rather than hypnos
, which is annoying.
To fix this, rename your embedded jre/bin/java
to jre/bin/hypnos
. Yes it's a hack, but no one else is using this JRE and it seems to work perfectly for me. You'll have to update your launch script (step 2) as well.
I looked for a long time for much more clever solutions and had nothing work. I have been very satisfied with this simple solution.
You need to get a 32bit JRE, and then use the appimagetool option --runtime-file runtime-i686
. You can get the updated runtime file from the appimage project.
Here is very simplified version of my ant build file, which may be helpful to you:
<project name="Hypnos Music Player" default="compile" basedir=".">
<property name="src" location="src"/>
<property name="build" location="bin"/>
<property name="stage" location="stage" />
<property name="dist" location="distribution/" />
<property name="temp" location="temp" />
<property name="packaging" location="packaging/" />
<property name="jarFile" location="${stage}/hypnos.jar" />
<property name="appImageTool" location="${packaging}/appimagetool-x86_64.AppImage" />
<buildnumber file="${packaging}/build.num"/>
<path id="class.path">
<fileset dir="${stage}/lib">
<include name="**/*.jar" />
</fileset>
<pathelement location="${jarFile}" />
</path>
<target name="init">
<tstamp />
<mkdir dir="${build}"/>
</target>
<target name="compile" depends="init" description="compile the source">
<javac fork="yes" target="1.8" source ="1.8" includeantruntime="false" srcdir="." destdir="${build}">
<classpath refid="class.path" />
</javac>
</target>
<target name="jar" depends="compile" description="Create a jar.">
<jar destfile="${jarFile}" basedir="${build}">
<manifest>
<attribute name="Main-Class" value="net.joshuad.hypnos.Hypnos" />
<attribute name="Class-Path" value="... items removed for brevity ... " />
</manifest>
</jar>
</target>
<target name="dist-nix-64bit" depends="jar" description="Make an AppImage for 64 bit Linux">
<sequential>
<delete dir="${temp}" />
<mkdir dir="${temp}/" />
<copy todir="${temp}/Hypnos.AppDir" >
<fileset dir="packaging/Hypnos.AppDir" />
</copy>
<copy todir="${temp}/Hypnos.AppDir/usr/bin" >
<fileset dir="stage" >
<exclude name="**/bin/**" />
</fileset>
</copy>
<copy todir="${temp}/Hypnos.AppDir/usr/bin/jre" >
<fileset dir="${packaging}/jres/linux-64bit" />
</copy>
<exec executable="${appImageTool}">
<arg value="${temp}/Hypnos.AppDir" />
<arg value="${dist}/Hypnos-nix-64bit.AppImage" />
</exec>
<delete dir="${temp}" />
</sequential>
</target>
<target name="dist-nix-32bit" depends="jar" description="Make an AppImage for 32 bit Linux">
<sequential>
<delete dir="${temp}" />
<mkdir dir="${temp}/" />
<copy todir="${temp}/Hypnos.AppDir" >
<fileset dir="packaging/Hypnos.AppDir" />
</copy>
<copy todir="${temp}/Hypnos.AppDir/usr/bin" >
<fileset dir="stage" >
<exclude name="**/bin/**" />
</fileset>
</copy>
<copy todir="${temp}/Hypnos.AppDir/usr/bin/jre" >
<fileset dir="${packaging}/jres/linux-32bit" />
</copy>
<exec executable="${appImageTool}">
<arg value="${temp}/Hypnos.AppDir" />
<arg value="--runtime-file" />
<arg value="${packaging}/runtime-i686" />
<arg value="${dist}/Hypnos-nix-32bit.AppImage" />
</exec>
<delete dir="${temp}" />
</sequential>
</target>
</project>