Create Native Java Executable using jpackage – Sort of

I have always been the kind of developer who prefers to use native code and write native code. My background is in C++, and I have worked with Microsoft’s WinAPI early in my career. That is to say: I like it fast, and I do not mind going to lower levels.

I am not stuck in the past, though, and as such, I, too, have evolved with the times. I still like C++, but I also see how languages like Java and its great tooling can boost productivity in comparison. As a result, I write code fast. Java is the tool of the trade at my current job, and performance usually is not a problem anymore. The JVM has improved, and computer hardware has, so performance is usually not an issue anymore.

There is one little problem, however: Usage. Let me explain.

The Convenience of Native Code

If your program is a native executable file, e.g., a Windows EXE, invoking the program is a trivial task. If it is a GUI application, double click it, and it starts up. Now imagine your application is a command-line utility. Let me use curl as an example. To call a website, you would write the following:

curl -L https://google.com

It is as simple as calling the executable and passing the necessary arguments to it. This is the basis of the Unix design philosophy. Small utilities, very specialized, and very efficient. If curl were a Java application, how would it work then?

java -jar curl.jar -L https://google.com

It isn’t the worst thing ever, but equally inconvenient and undiscoverable. The story can be different for GUI applications, as long as the JAR extension is registered to start with the Java runtime. This is what I mean when I talk about “Usage”. It does not feel native, especially on the command line, which is my biggest issue with Java.

(You can also run Java applications from the individual compiled files if they are not packaged as a JAR. That user experience is even worse.)

Build a Native Java App Executable

And this is where I expected jpackage to step in and alleviate the problem. Java 16 introduced it as generally available, and with Java 17 being an LTS release, I figured it was time to try it out.

My hope: jpackage creates a single binary that includes the JAR and the Java runtime.

I started with a very simple application to conduct my tests, and because of the results I found, I will not dive deep into all of its options.

I created a command-line utility that accepts two parameters and prints them to the terminal.

Impressive, right? 😉

jpackage can produce several types of binaries as output, depending on the platform.

--type or -t <type string>
        The type of package to create
        Valid values are: {"app-image", "exe", "msi", "rpm", "deb", "pkg", "dmg"}
        If this option is not specified a platform dependent default type will be created.

The platform-independent option seems to be “app-image”. EXE and MSI create Windows installers, RPM and DEB are Linux distro-specific package formats, PKG is the macOS installer, and DMG is the macOS drag-app-to-application-folder distribution mode.

Except for the Linux package manager formats, I have tried them all. I will get to the results later. First, here is how you can use jpackage with a Fat-JAR.

OptionDescription
--typeDescribes the type of output to create (see the docs excerpt earlier).
--nameDetermines the name of the application.
-iSpecifies that all files from the given directory will be included.
--main-class and -–main-jarThese two should be self-explanatory to every Java developer.
-dSpecifies the output directory. It is important to create the result in a different folder. I have run into “File name too long” errors trying to make a DMG file. I ran into similar issues on Linux.

Irrespective of the platform, the output is similar to some extent. jpackage creates a directory structure that includes a native binary, some configuration, the JAR file, and the Java runtime. Here is how the contents of a macOS app-bundle look.

Mess with this structure, and your application breaks. It seems like all the paths relative to the app-image root are “hard-coded”. For comparison, here are the Linux and Windows results.

The bottom EXE and MSI files are the different types of Windows installers that jpackage can create. They are not part of the app-image output.

So, how does it work? How do I run the JPackageDemo.app on macOS? Well, if it were a GUI application, I would expect that double-clicking the app just works as intended. Since I was interested in a console utility, it is a bit more complicated. Ultimately, you run the executable that is hidden inside the APP folder.

As you can see, doing the java -jar dance is actually much more concise. The same approach can be used on Linux and Windows.

The Oddities of Windows

Everything worked fine on macOS and Linux. However, not so much on Microsoft’s operating system. The Windows 10 installation on my work laptop created the output, but work it did not. The installers ran in the background without ever showing a user interface, so I had to force-quit them using the Task Manager. The command line was similarly broken. It could not be convinced to print anything to the console.

An idea that came to mind was that Java 11 is configured as the default in my environment. It is what I require for work. I called the Java 17 version of jpackage directly from its installation directory to create my test output. Maybe the requirement of Java 17 and the default of Java 11 somehow clashed in this case. I did not bother to dig deeper into this. I could have removed Java 11 from the environment to make Windows behave like it had never seen a version of Java before. But because I need that system for work, I did not want to mess with it. Just for the fun of it, I also rebooted. No dice.

Either way, I expected it to use the jpackage-bundled Java version. As far as I understand this tool, that is its purpose.

Automation

I have found two Maven packages, JPackage Maven Plugin and jpackage-maven-plugin. Gotta love the creative names 😅

I have tried the first one, but I could not get it to run on macOS.

It is vital for this plugin that the JAVA_HOME environment variable is set. Otherwise, the plugin will not find the correct Java version, even when you set it as a plugin option. Since I do not plan to utilize jpackage in the near term, I stopped here.

I took a quick peek at the other plugin’s documentation, but that was it.

Long Story Short

As you have seen, although jpackage creates native executables, neither are single-binary programs. It looks like it is a tool that requires the proper use case to be helpful. If all you are doing is writing small command-line utilities, you should either look at a natively compiled language, write a wrapper shell-script, or stick with java -jar.

One benefit that is worth considering is that jpackage bundles a Java runtime with your application. If you plan to distribute your creation to the world wide web, this may be one of those use cases. It removes the requirement of installing Java before using your application, making it more accessible to many more people.

For what I am trying to do, jpackage only complicates things. I chose to test with a CLI utility very purposefully, and as a result, I will not be putting this little tool in my toolbox just yet.

I hope this blog post managed to help you find what you were looking for or just satisfy your curiosity about jpackage. Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.