
Java: Generate Windows, Linux and MacOS executables
If, like me, you are tired of making command line programs in Java and having to run them with this kind of things:
$ java -jar demo-app-v1.0.0-SNAPSHOT.jar arg1 arg2 arg3`
This little article should please you. You will then be able to do this directly:
$ ./demo-app arg1 arg2 arg3
Appetizing!
In this little tutorial, I will generate an executable file :
- for Linux and MacOSX,
- for Windows, an
.EXE
, - as a classic Runnable JAR.
However, this requires the presence of a JAVA Runtime (JRE and/or JDK) on the machine that will run it, although there are also solutions to embed a JRE.
The solutions I will describe do not use GRAALVM and its native compilation.
The JAVA program to be transformed
I’m going to make it very simple for this example, using the [PICOCLI] library (https://picocli.info/) to make something fast and (almost) clean.
If you don’t know picocli yet, I strongly encourage you to discover it quickly !
Here is the “beast”: fr.fxjavadevblog.mvngenexec.MainProg
package fr.fxjavadevblog.mvngenexec;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
@Command(name = "demo-app", version = "1.0.0",
mixinStandardHelpOptions = true,
description = "Demo Command-line App written in Java with Picocli")
public final class MainProg implements Callable<Integer>
{
private static Logger log;
static
{
log = Logger.getLogger(MainProg.class.toString());
System.setProperty("java.util.logging.SimpleFormatter.format", "[%1$tF %1$tT] [%4$-7s] %5$s %n");
}
@Parameters(index = "0..*")
private String[] allArguments;
@Override
public Integer call() throws Exception
{
log.info("Running ...");
if (allArguments != null && log.isLoggable(Level.INFO))
{
log.info("Arguments : " + String.join(",", allArguments));
}
log.info("Finished!");
return 0;
}
private MainProg()
{
// protecting constructor
}
public static void main(String... args)
{
int exitCode = new CommandLine(new MainProg()).execute(args);
System.exit(exitCode);
}
}
For the moment, its pom.xml
is quite succinct.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.fxjavadevblog</groupId>
<artifactId>demo-app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.0</version>
</dependency>
</dependencies>
</project>
Transformation en JAR exécutable : rien de bien nouveau
To generate the executable JAR, I will use the MAVEN plugin maven-shade-plugin
.
So I add the plugin configuration to the pom.xml
indicating which class
which will be the entry point with its static void main(String... args)
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>fr.fxjavadevblog.mvngenexec.MainProg</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.MF</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
I launch the compilation and the packaging:
$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< fr.fxjavadevblog:maven-generer-executables >-------------
[INFO] Building maven-generer-executables 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ maven-generer-executables ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ maven-generer-executables ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to ./maven-gener-executables/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ maven-generer-executables ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ maven-generer-executables ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ maven-generer-executables ---
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ maven-generer-executables ---
[INFO] Building jar: ./maven-gener-executables/target/maven-generer-executables-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-shade-plugin:3.4.1:shade (default) @ maven-generer-executables ---
[INFO] Including info.picocli:picocli:jar:4.7.0 in the shaded jar.
[INFO] Dependency-reduced POM written at: ./maven-gener-executables/dependency-reduced-pom.xml
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing ./maven-gener-executables/target/maven-generer-executables-0.0.1-SNAPSHOT.jar with ./maven-gener-executables/target/maven-generer-executables-0.0.1-SNAPSHOT-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.525 s
[INFO] Finished at: 2022-12-12T16:14:17+01:00
[INFO] ------------------------------------------------------------------------
and I check that I can run my standalone JAVA program, as usual:
$ java -jar ./target/maven-generer-executables-0.0.1-SNAPSHOT.jar arg1 arg2 arg3
[2022-12-12 16:16:16] [INFOS ] Running ...
[2022-12-12 16:16:16] [INFOS ] Arguments : arg1,arg2,arg3
[2022-12-12 16:16:16] [INFOS ] Finished!
For the moment, nothing new on the horizon, that’s the subject of the next paragraphs.
Generation of the Linux and MacOSX executable
To generate the executable for Linux and MaxOSX I will use the following principle: a JAR file can contain an executable part in preamble, in particular a shell script.
This is actually the same mechanism that is offered by the ZIP format and that allows to obtain self-extracting ZIP files.
The idea behind this solution is to have a shell script at the beginning of the file that loads and launches the JAR that is located just behind it (in the same file). The technique is described here: https://skife.org/java/unix/2011/06/20/really_executable_jars.html
This is not a recent technique, but it is not well known.
Fortunately, we have a Maven plugin to help us with this task:
<plugin>
<groupId>org.skife.maven</groupId>
<artifactId>really-executable-jar-maven-plugin</artifactId>
<version>1.5.0</version>
<configuration>
<flags>-Xmx1G</flags>
<programFile>${project.artifactId}</programFile>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>really-executable-jar</goal>
</goals>
</execution>
</executions>
</plugin>
And I’m re-launching the application’s packaging:
$ mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< fr.fxjavadevblog:demo-app >----------------------
[INFO] Building demo-app 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ demo-app ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ demo-app ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to ./maven-gener-executables/target/classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ demo-app ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ demo-app ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ demo-app ---
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ demo-app ---
[INFO] Building jar: ./maven-gener-executables/target/demo-app-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-shade-plugin:3.4.1:shade (default) @ demo-app ---
[INFO] Including info.picocli:picocli:jar:4.7.0 in the shaded jar.
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing ./maven-gener-executables/target/demo-app-0.0.1-SNAPSHOT.jar with ./maven-gener-executables/target/demo-app-0.0.1-SNAPSHOT-shaded.jar
[INFO]
[INFO] --- really-executable-jar-maven-plugin:1.5.0:really-executable-jar (default) @ demo-app ---
[INFO] Successfully made JAR [./maven-gener-executables/target/demo-app] executable
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.353 s
[INFO] Finished at: 2022-12-12T16:35:38+01:00
[INFO] ------------------------------------------------------------------------
I get a Linux and MacOSX executable in the target directory:
$ ll target
total 848
drwxrwxr-x 7 robin robin 4096 déc. 12 16:35 ./
drwxrwxr-x 5 robin robin 4096 déc. 12 16:35 ../
drwxrwxr-x 3 robin robin 4096 déc. 12 16:35 classes/
-rwxrwxr-x 1 robin robin 416303 déc. 12 16:35 demo-app*
-rw-rw-r-- 1 robin robin 416259 déc. 12 16:35 demo-app-0.0.1-SNAPSHOT.jar
drwxrwxr-x 3 robin robin 4096 déc. 12 16:35 generated-sources/
drwxrwxr-x 2 robin robin 4096 déc. 12 16:35 maven-archiver/
drwxrwxr-x 3 robin robin 4096 déc. 12 16:35 maven-status/
-rw-rw-r-- 1 robin robin 3858 déc. 12 16:35 original-demo-app-0.0.1-SNAPSHOT.jar
drwxrwxr-x 2 robin robin 4096 déc. 12 16:35 test-classes/
That I hasten to launch to see if it works:
$ ./target/demo-app arg1 arg2 arg3
[2022-12-12 16:37:41] [INFOS ] Running ...
[2022-12-12 16:37:41] [INFOS ] Arguments : arg1,arg2,arg3
[2022-12-12 16:37:41] [INFOS ] Finished!
Yes ! No need to do the classic $ java -jar ...
! Very nice. It requires at least a JVM present on the system able to launch the program, which is, obviously, my case.
And it also works as is on MacOSX. Thanks for being a UNIX derivative! Just like on RaspBerry, since it’s still script and Java: no native code.
Generating .EXE for Windows
The solution of the previous paragraph requires a script execution environment (bash, sh, zsh, etc.).
On Windows one could also use it as is with Cygwin or MinGW. On Windows 10 I could even enable a BASH interpreter using a LINUX subsystem.
But I will use something else: Launch4J. It will be able to generate an .EXE
even from my Linux, and all with a Maven plugin. It just needs a little bit of configuration.
<plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId>
<artifactId>launch4j-maven-plugin</artifactId>
<executions>
<execution>
<id>l4j-clui</id>
<phase>package</phase>
<goals>
<goal>launch4j</goal>
</goals>
<configuration>
<headerType>console</headerType>
<jar>target/${project.artifactId}-${project.version}.jar</jar>
<outfile>target/${project.artifactId}.exe</outfile>
<classPath>
<mainClass>fr.fxjavadevblog.mvngenexec.MainProg</mainClass>
</classPath>
<jre>
<path>${JAVA_HOME}</path>
<minVersion>11</minVersion>
</jre>
<versionInfo>
<fileVersion>1.0.0.0</fileVersion>
<txtFileVersion>1.0.0.0</txtFileVersion>
<fileDescription>${project.name}</fileDescription>
<copyright>C</copyright>
<productVersion>1.0.0.0</productVersion>
<txtProductVersion>1.0.0.0</txtProductVersion>
<productName>${project.name}</productName>
<internalName>${project.name}</internalName>
<originalFilename>${project.artifactId}.exe</originalFilename>
</versionInfo>
</configuration>
</execution>
</executions>
</plugin>
I trigger the generation:
$ mvn package
...
[INFO] --- launch4j-maven-plugin:2.1.2:launch4j (l4j-clui) @ demo-app ---
[INFO] Platform-specific work directory already exists: /home/robin/.m2/repository/net/sf/launch4j/launch4j/3.14/launch4j-3.14-workdir-linux64
[INFO] launch4j: Compiling resources
[INFO] launch4j: Linking
[INFO] launch4j: Wrapping
WARNING: Sign the executable to minimize antivirus false positives or use launching instead of wrapping.
[INFO] launch4j: Successfully created ./maven-gener-executables/target/demo-app.exe
...
I get a nice executable file for windows demo-app.exe
:
$ ll target
total 1296
drwxrwxr-x 7 robin robin 4096 déc. 12 16:49 ./
drwxrwxr-x 5 robin robin 4096 déc. 12 16:47 ../
drwxrwxr-x 4 robin robin 4096 déc. 12 16:48 classes/
-rwxrwxr-x 1 robin robin 416733 déc. 12 16:48 demo-app*
-rw-rw-r-- 1 robin robin 416689 déc. 12 16:48 demo-app-0.0.1-SNAPSHOT.jar
-rwxrwxr-x 1 robin robin 453041 déc. 12 16:49 demo-app.exe*
drwxrwxr-x 3 robin robin 4096 déc. 12 16:47 generated-sources/
drwxrwxr-x 2 robin robin 4096 déc. 12 16:47 maven-archiver/
drwxrwxr-x 3 robin robin 4096 déc. 12 16:47 maven-status/
-rw-rw-r-- 1 robin robin 4288 déc. 12 16:48 original-demo-app-0.0.1-SNAPSHOT.jar
drwxrwxr-x 2 robin robin 4096 déc. 12 16:47 test-classes/
The Icing Or Cherry On The Cake : ZIP of the 3 executables
I’m going to package the 3 executables (Linux/MaxOS, Windows, Java) in a single ZIP file to be able to “distribute” it to users, for example.
To do this, I will use the maven-assembly
plugin with a descriptor.
The descriptor placed in ./src/main/assembly/zip.xml
:
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>zip</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>zip</format>
</formats>
<files>
<!--fichier JAR executable -->
<file>
<source>${project.build.directory}/${project.artifactId}-${project.version}.jar</source>
<outputDirectory>/</outputDirectory>
</file>
<!--fichier EXE Windows -->
<file>
<source>${project.build.directory}/${project.artifactId}.exe</source>
<outputDirectory>/</outputDirectory>
</file>
<!--fichier LINUX et MacOSX -->
<file>
<source>${project.build.directory}/${project.artifactId}</source>
<outputDirectory>/</outputDirectory>
</file>
</files>
</assembly>
and the declaration of the plugin in the pom.xml
:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/zip.xml</descriptor>
</descriptors>
<finalName>${project.name}-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
I launch the creation of the package:
$ mvn package
...
[INFO] --- maven-assembly-plugin:3.4.2:single (make-assembly) @ demo-app ---
[INFO] Reading assembly descriptor: src/main/assembly/zip.xml
[INFO] Building zip: ./maven-gener-executables/pom.xml/target/demo-app-0.0.1-SNAPSHOT.zip
...
and I get a nice ZIP file in ./target
:
$ ll target/*.zip
-rw-rw-r-- 1 robin robin 1174675 déc. 12 17:28 target/demo-app-0.0.1-SNAPSHOT.zip
It contains my executables:
$ unzip -v ./target/demo-app-0.0.1-SNAPSHOT.zip
Archive: ./target/demo-app-0.0.1-SNAPSHOT.zip
Length Method Size Cmpr Date Time CRC-32 Name
-------- ------ ------- ---- ---------- ----- -------- ----
416684 Defl:N 385722 7% 2022-12-12 17:29 7d992b35 demo-app-0.0.1-SNAPSHOT.jar
453036 Defl:N 402854 11% 2022-12-12 17:29 b876a4a6 demo-app.exe
416728 Defl:N 385755 7% 2022-12-12 17:29 4bd62989 demo-app
-------- ------- --- -------
1286448 1174331 9% 3 files
Conclusion
A simple article, a simple conclusion: it’s cool, isn’t it? Thank you Maven and all its plugins!
Nothing more to add, thanks for reading this far!