Skip to content

Bundling Java apps

probonopd edited this page Mar 28, 2018 · 8 revisions

Option 1: Using jlink

To be written. Thanks @overheadhunter for the hint.

For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool. To package a JDK 8 or JDK 7 JRE with your application, use the JDK 8 Java Packager.

Source: https://docs.oracle.com/javase/9/tools/javapackager.htm#JSWOR719

Option 2: Bundlung JRE manually

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.

Step 1 - Gather the JRE(s) you need for your project

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.

Step 2 - Write your launcher script

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!

Step 3 - Setup your Desktop File

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.

Step 4 - Setup your AppDir

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

Step 5 - Create the appimage

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.

Addendum 1 - Renaming the java process

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.

Addendum 2 - Packaging for 32bit as well

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.

Addendum 2 - Ant Build Example

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>