=== Kevin's Gemini space ===

Making a Java JAR file self-executing (on Linux)

Introduction

This article describes a quick-and-dirty way to make a Java JAR file self-executing on Linux. I can't guarantee it will work on every flavour of Linux, but it's worked quite well in my tests. It can probably be adapted to work on other platforms, although I haven't tried.

I should point out from the outset that this article is not about compiling Java to a native executable. For more information on that topic, you need something like GrallVM. If it is practicable to compile to native code, that should probably be your first choice in most cases. Unfortunately, it isn't (yet) always possible.

In addition, some Linux versions claim to make Java JAR files self-executing automatically -- all you need to do on these platforms is set the executable flag on the JAR file. However, although I know this to be possible in principle, I have yet to come across a Linux installation where it actually works.

The method described in this article involves creating a 'broken' JAR file, which the `java` utility is nonetheless able to process. That's what makes this a dirty solution. I do not know if all Java implementations are this robust. At the end of the article, I describe how it should be possible to implement a 'clean' version of the method I describe here.

The problem

If you're a reasonably experienced Java developer, particular using Linux, you'll know that you can build an entire Java application into a single executable JAR file. It's just a matter of packing all the compiled class files, along with some meta-data, into a Zipfile, and calling it 'something.jar'. Then you can execute the self-contained JAR at the prompt like this:

$ java -jar /path/to/my_app.jar [options...]

That's fine, so long as you can find (and remember) a suitable location to install the JAR, and you don't mind typing the full path every time you want to run it.

For a little added convenience, you could create a shell script that runs the JAR, and copy it into, say, `/usr/bin`. In this example, the script would look something like this:

#!/bin/bash
java -jar /path/to/my_app.jar "$@" 

The `$@` token indicates to `bash` to pass any command-line arguments it received, apart from argument zero, straight through to the `java` command line. Your Java application might not take any arguments but, if it does, your script needs to handle that.

Wouldn't it be easier if we could just simply copy the JAR file to `/usr/bin/` and call it `my_app`, or whatever name is suitable?

The solution

The way to make the JAR self-executing is to prepend a variant of the script described above to the JAR file itself. Specifically, here is the script:

#!/bin/bash
# Prepend this script to a JAR to make it self-executing
# Don't forget to 'chmod 755'. The "java" command must 
#  be working
me=`realpath $0`
exec java -jar $me "$@"

Call this script `header`, for example. Now concatenate this script with the Java JAR file:

$ cat header my_app.jar > my_app

and make it executable:

$ chmod 755 my_app

Now you should be able to run the application just by running `my_app`, and you can copy this file (preserving the executable attribute, of course) to `/usr/bin` or some other location on the `$PATH`.

How does this work?

This rather strange procedure works because a JAR file, like any Zipfile, is indexed at the end, not the beginning. The final index entry, called the 'end of central directory' record, will always be in the last 64 kilobytes of the Zipfile. This record has a unique 32-bit signature, and the unzipping utility (the Java runtime, in this case) searches for the signature in that last 64 kilobytes. Within the end of central directory record is the offset of the _first_ central directory record. However, this record also has a unique signature, so it can be found without knowing the location of the end of central directory record. The difference between the position of the first central directory record as determined by its signature, and its position as indicated by the end of central directory record, indicates the amount of additional material there is at the start of the file. A robust unzipper will skip this material.

So the Java runtime can deal with the fact that there is a script prepended to the JAR file, but we still have to invoke the Java runtime itself. That's what the script does -- it invokes the `java -jar` on _itself_.

The script invokes `java` using `exec`. This saves memory because the script+JAR assembly does not have to be kept around until the `java` command completes, and also prevents `bash` trying to run the JAR as if it was part of a script.

To be fair, although the procedure I've described does work on every Linux system I've tried, it relies on undocumented behaviour of the Java runtime, so it should be used with caution.

Improving the method

The way to clean up the method I've described is to scan the JAR file to find the start of the central directory, then iterate through the central directory, correcting each entry by adding the size of the header to the address of the entry's _local_ directory. Effectively you'd be patching the Zipfile so all the offsets to data are correct, giving the header at the start of the file. This shouldn't be difficult to do, if you understand the format of a Zipfile, but so far I've found the quick-and-dirty method to work well enough.

[ Last updated Tue 22 Feb 19:27:11 GMT 2022 ]