Java deliverables as executable bundle and the launcher trick

In the past I coded some command line tools in Java which are quite useful for me and which I ought to be useful for others1. Providing just fat JAR files implies launching of those Java applications to be more or less cumbersome: Besides requiring a JVM being installed, some annoying command line got to be typed in, needing the exact location of the fat JAR file in question at hand to be passed as argument2. Whilst trying to ease the situation, I stumbled over some tricks and ideas creating deliverables helping to lessen and even eliminate the pains.

Deliverables

There are some approaches I was playing around to get a more satisfying user experience when providing my command line tools’ deliverables3. Here I will focus on the launcher, bundle and installer approaches, which means providing either a launcher, being a shell or operating system specific launcher prepended(!) to your fat JAR file, taking care of seeking a suitable JVM on the local machine which then is invoked for launching (just one self contained file is being delivered) or an executable bundle containing a JVM under the hood, which transparently is installed the first time the executable is launched (for each targeted platform we need a dedicated executable)4 and finally a classic installer, installing the application on your operating system. Besides these approaches, there are more approaches such as the native deliverables5.

Toolchain

For building launcher, bundle as well as the installer deliverables, we require a fat JAR file dropping out of our build process, containing all the resources, third party libraries and dependencies your application requires to run when being invoked via java -jar .... This can be achieved in several ways: As I am using Maven for build automation, I decided to go with the Maven Shade Plugin - I wont’t use any heavy weight frameworks such as Spring Boot5 for my command line tools!

If not mentioned otherwise, shell scripts *.sh proposed to be invoked are intended to be issued in a bash alike shell on GNU Linux or within a GNU userland such as Cygwin or Git BASH on MS Windows. Plain commands may also be issued using CMD on MS Windows.

Java & Maven

First of all an according JDK needs to be installed, either by directly using the downloads from openjdk.org or by using SDKMAN!6. Make sure you have set the JAVA_HOME environment variable correctly and your JDK is on your system’s path7. You may verify your installation by invoking java --version in a terminal:

As we use Java with a version >= 16, make sure your JDK is chosen accordingly.

java 17.0.5 2022-10-18 LTS
Java(TM) SE Runtime Environment (build 17.0.5+9-LTS-191)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.5+9-LTS-191, mixed mode, sharing)

The printed version information should contain a version >= 16 somewhere in the output.

Maven is expected to be installed already8, though when using SDKMAN!6, it is easily installed as of sdk install maven 3.8.6.

Archetypes

Below find a selection of Maven Archetypes of which we will use one in the next section to get our Maven project with some sample Java code up and running:

For a jump start into developing Java driven command line tools, I created some fully pre-configured Maven Archetypes available on Maven Central. Those Maven Archetypes already provide means to directly create native executables, bundles as well as launchers and support out of the box command line argument parsing as well as out of the box property file handling.

Please adjust my.corp with your actual Group-ID and myapp with your actual Artifact-ID:

refcodes-archetype-alt-cli

Use the refcodes-archetype-alt-cli to create a bare metal command line interface (CLI) driven Java application:

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-cli \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

refcodes-archetype-alt-csv

Use the refcodes-archetype-alt-csv archetype to create a bare metal CSV (CLI) driven Java application:

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-csv \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

refcodes-archetype-alt-c2

Use the refcodes-archetype-alt-c2 archetype to create a bare metal command & control (CLI) driven Java application:

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-c2 \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

refcodes-archetype-alt-eventbus

Use the refcodes-archetype-alt-eventbus archetype to create a bare metal event driven driven Java service:

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-eventbus \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

refcodes-archetype-alt-filter

Use the refcodes-archetype-alt-filter archetype to create a bare metal command line interfaceCLI) driven Pipes and Filters Java application (the pipes are provided by the shell, your application will be the filter to interact with other UNIX filters via pipes):

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-filter \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

refcodes-archetype-alt-rest

Use the refcodes-archetype-alt-rest to create a bare metal REST driven Java application within just one source code file:

mvn archetype:generate [...]
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-rest \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

Project

To examine the launcher, bundle as well as the installer approaches, we now create a Maven project for a Pipes and Filters Java application using the refcodes-archetype-alt-filter archetype:

Please adjust my.corp with your actual Group-ID and myapp with your actual Artifact-ID:

mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-filter \
  -DarchetypeVersion=3.0.8 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

In the root folder of the newly created project you find the pom.xml alongside all required source code and configuration files as of the common Maven project layout.

You may have to invoke chmod +x *.sh on your project’s root folder to make the scripts executable!

This project already ships the required scripts bundle.sh, scriptify.sh, jexefy.sh as well as installer.sh for automating the processes described later.

Bundle

The bundle approach provides an executable bundling a JVM under the hood, which transparently is installed the first time the executable bundle is launched: For each targeted platform we need to produce a dedicated executable bundle e.g. for GNU Linux or for MS Windows accordingly. To achieve this, we use the warp tool found on GitHub. This tool packages a predefined directory structure together with an application launcher into a single executable. To create a bundle for our purposes, the following steps are necessary (and already automated by the bundle.sh script):

  1. A folder to be bundled is to be prepared, here we use the .bundle folder in our project’s root folder.
  2. The operating system specific JRE implementation to be bundled is to be placed into the .bundle folder.
  3. The fat JAR to be launched is to be placed into the .bundle folder.
  4. Finally a launcher file, e.g. launcher.sh for GNU Linux or launcher.cmd for MS Windows, launching the fat JAR file with the prepared JRE, is also to be placed into the .bundle folder.
  5. To finish off, the warp command is invoked with according arguments passed.

This got to be done for each targeted platform in question (e.g. GNU Linux or MS Windows), but no worries, this process can be automated by invoking the bundle.sh script!

For GNU Linux, the prepared .bundle folder structure may look as follows:

.bundle
│
├── application.jar
│
├── jre
│   │
│   ├── bin
│   │
│   ├── conf
│   │
│   ├── DISCLAIMER
│   │
│   ├── legal
│   │
│   ├── lib
│   │
│   ├── readme.txt
│   │
│   ├── release
│   │
│   └── Welcome.html
│
└── launcher.sh

For MS Windows, the prepared .bundle folder structure may look as follows:

.bundle
│
├── application.jar
│
├── jre
│   │
│   ├── bin
│   │
│   ├── conf
│   │
│   ├── DISCLAIMER
│   │
│   ├── legal
│   │
│   ├── lib
│   │
│   ├── readme.txt
│   │
│   ├── release
│   │
│   └── Welcome.html
│
└── launcher.cmd

To create a bundle on GNU Linux, the warp command is invoked as follows:

./linux-x64.warp-packer \
	--arch linux-x64 \
	--input_dir  ".bundle" \
	--exec "launcher.sh" \
	--output "target/bundle.elf"

To create a bundle on MS Windows, the warp command using CMD is invoked as follows:

 windows-x64.warp-packer.exe \
 	--arch windows-x64 \
 	--input_dir  ".bundle" \
 	--exec "launcher.cmd" \
 	--output "target/bundle.exe"

All those steps above are automated by the bundle.sh script to easily create bundles for MS Windows as well as for GNU Linux. The initial bundle.sh configuration is located in the bundle.conf file, both files being located in the root folder of your project.

Building bundles both for MS Windows as well as for GNU Linux means changing into your project’s root folder and executing the bundle.sh script:

./bundle.sh

This creates the following folder structure below your project’s root folder:

.bundle
│
├── linux_x86_64
│   │
│   ├── application.jar
│   │
│   ├── jre
│   │   │
│   │   ├── bin
│   │   │
│   │   ├── conf
│   │   │
│   │   ├── DISCLAIMER
│   │   │
│   │   ├── legal
│   │   │
│   │   ├── lib
│   │   │
│   │   ├── readme.txt
│   │   │
│   │   ├── release
│   │   │
│   │   └── Welcome.html
│   │
│   └── launcher.sh
│
└── windows_x86_64
    │
    ├── application.jar
    │
    ├── jre
    │   │
    │   ├── bin
    │   │
    │   ├── conf
    │   │
    │   ├── DISCLAIMER
    │   │
    │   ├── legal
    │   │
    │   ├── lib
    │   │
    │   ├── readme.txt
    │   │
    │   ├── release
    │   │
    │   └── Welcome.html
    │
    └── launcher.cmd

The bundle.sh script downloads (if not already done) a JRE for MS Windows as well as for GNU Linux (configured in the bundle.conf file) into the .bundle folder of your project and creates the resulting bundle files *.exe as well as*.elf in the target folder of your project. Your bundled application may now be launched by invoking the *.exe respectively the *.elf file, which upon the first launch, extracts the JRE alongside the fat JAR as well as the launcher script either below %USERPROFILE%\AppData on MS Windows or below ~/.local on GNU Linux in an bundle’s specific(!) subfolder.

Given that we go for myapp as Artifact-ID, in a bash shell on GNU Linux, launch as follows:

./target/myapp-bundle-x86_64-0.0.1.elf

Given that we go for myapp as Artifact-ID, using CMD on MS Windows, launch as follows:

target\myapp-bundle-x86_64-0.0.1.elf

Launcher

The launcher approach provides a shell or an operating system specific launcher prepended(!) to your fat JAR file, taking care of seeking a suitable JVM on the local machine which then is invoked for launching. This way only one file is being delivered, the fat JAR prepended with the according launcher!

This approach solely works because the JVM, when being provided with a fat JAR to be launched, considers this fat JAR as being a stream, which is scanned till the beginning of a valid JAR record is found. All bytes before the beginning of the valid JAR record are ignored by the JVM. On the other hand, a shell such as the bash shell, when executing a script, just reads that amount of data which is required to execute the current instruction. Anything appended to the end of the script is ignored (CMD on MS Windows actually does not work this way, so we cannot use the launcher mechanism here). Similar to the mechanism applied by the bash shell are the mechanics provided by GNU Linux and MS Windows when loading binary executable files (e.g. *.exe or *elf files): Only those pages from the executable file are loaded into memory which are required by the CPU to start execution. In case the CPU hits pages not yet mapped into memory, a page fault is issued, causing the according page to be loaded into memory. Pages not required by the execution of the binary executable are never loaded! Anything appended to the end of such a binary executable is ignored.

We now can take advantage of the fact that anything appended to a bash script or an *.exe or *elf binary executable is ignored by appending our fat JAR to either a launcher script or a binary executable. The launcher script or the binary executables just have to detect an appropriate JVM on the local machine and invoke this JVM with itself as argument, being the accordingly modified script or binary executable with the appended fat JAR file. The JVM will detect the embedded JAR file and execute it flawlessly.

A bash script launcher may look as follows:

#!/bin/bash [...]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

if [ ! -z "${TERM}" ] ; then
	if command -v "tput" &> /dev/null ; then
		if [ -z "$COLUMNS" ]; then
			export COLUMNS="$(tput cols)"
		fi
		if [ -z "$LINES" ]; then
			export LINES="$(tput lines)"
		fi
	fi
fi

# See "https://coderwall.com/p/ssuaxa/how-to-make-a-jar-file-linux-executable"
SCRIPT_PATH=`which "$0" 2>/dev/null`
[ $? -gt 0 -a -f "$0" ] && SCRIPT_PATH="./$0"
LAUNCHER_DIR="$(dirname $SCRIPT_PATH 2>/dev/null)"
if [ $? -ne 0 ]; then
	LAUNCHER_DIR="."
fi
if type "uname" &> /dev/null; then
	if [ `uname -o` = "Cygwin" ]; then
		SCRIPT_PATH=$(cygpath -w ${SCRIPT_PATH})
		LAUNCHER_DIR=$(cygpath -w ${LAUNCHER_DIR})
	fi
fi
java=java
if test -n "$JAVA_HOME"; then
	java="$JAVA_HOME/bin/java"
fi
javaArgs[0]="-Dlauncher.dir=$LAUNCHER_DIR"
javaIndex="1"
appIndex="0"
for var in "$@"; do
	if [[ ${var} == -Dlauncher.dir=* ]]; then
		javaArgs[0]="${var}"
	elif [[ ${var} == -D* ]]; then
		javaArgs[${javaIndex}]="${var}"
		javaIndex=$((javaIndex + 1))
	else
		appArgs[${appIndex}]="${var}"
		appIndex=$((appIndex + 1))
	fi
done
stty -echoctl &> /dev/null
exec "${java}" -XX:TieredStopAtLevel=1 "${javaArgs[@]}" -server -jar "$SCRIPT_PATH" "${appArgs[@]}"
exit $?

All those steps above are automated by the scriptify.sh script to easily create launchers for the bash shell.

To build a launcher, change into your project’s root folder and execute the scriptity.sh script:

./scriptify.sh

This results in a self contained launcher below the project’s target folder, which can be invoked as follows:

./target/myapp-launcher-0.0.1.sh

The *.exe and *elf launchers work by the same principle, with funcodes-jexefy being a project I actually created using the RUST programming language to create native launchers. This project compiles to launcher executables for GNU Linux and MS Windows.

All those steps above are automated by the jexefy.sh script to easily create launchers for GNU Linux and MS Windows.

Building launcher executables (*.exe and *.elf) means changing into your project’s root folder and executing the jexefy.sh script:

./jexefy.sh

This results in a self contained launcher below the project’s target folder, which can be invoked in a bash shell on GNU Linux as follows:

./target/myapp-bundle-x86_64-0.0.1.elf

Using CMD on MS Windows, the self contained launcher below the project’s target folder can be invoked as follows:

target\myapp-bundle-x86_64-0.0.1.exe

Installer

Creating an *.msi installer for MS Windows is as easy as simply using the jpackage command provided by the installed JDK:

jpackage \
	--win-dir-chooser \
	--win-shortcut \
	--win-console \
	--input "." --name <someAppName> \
	--app-version <someAppVersion> \
	--icon <path/to/your/application/icon.ico> \
	--main-jar <path/to/your/jar/file.jar>\
	--type msi

All those steps above are automated by the installer.sh script to easily create *.msi installers for MS Windows.

Building an installer means changing into the project’s root folder and executing the installer.sh script:

./installer.sh

The installer variant is, as of my opinion, the most uncomfortable one for end users but the most straight forward approach when building.

Further reading

The refcodes-archetype collection provides easy means do build native deliverables in addition to launcher, bundle or installer deliverables. See GraalVM: Native command line tools for Linux and Windows written in Java for detailed insights on how building native deliverables is achieved.

See also

  1. Java command line tools, aka public static void main( String[] args){ ... }

  2. Launching a fat JAR usually is achieved by invoking java -jar <the/path/to/your/fat/jar/file/in/question>.jar

  3. The command line tools can be found in the Downloads section of this site. 

  4. The JVM is either installed below %USERPROFILE%\AppData on MS Windows or ~/.local on GNU Linux in a bundle’s specific folder. 

  5. For native deliverables see GraalVM: Native command line tools for Linux and Windows written in Java 2

  6. As I often switch forth and back between different JVM and GraalVM installations, I preferably use SDKMAN!(e.g. sdk install java 17.0.5-tem, verify the available candidates via sdk list java).  2

  7. The basic setup of your JDKis described in this JDK installation Guide

  8. Maven may be either installed directly following the installation guide or by using SDKMAN! (e.g. sdk install maven 3.8.6, verify the available candidates via sdk list maven)6