Packaging Java as a Native Application with a Self-Contained, Custom Runtime: A Manual WalkthroughINTENDED AUDIENCE:
Java programmers new to modular Java and deployment.
WHAT:
In this walk-through, we will start with a simple Java program ("
Pongish") and end with an
.exe file that, when downloaded and executed, will launch the Windows
Setup and install our program. The Java program that is installed will be self-contained with a runtime customized to hold only the modules needed to run the program. The steps taken will involve command line Java using the Windows
cmd.exe console shell, and
InnoSetup 5.
With this walk-through example, the resulting
setup.exe download file is 21 MB, and the installed program occupies 87.5MB. In comparison, the same program packed with a complete JRE has a
setup.exe download file of 40.5 MB and a footprint of 180 MB.
Note: Modular Java requires Java 9 or higher, and a 64-bit cpu on Windows systems.
GETTING STARTED1) Create a file folder for our deployment project on the Desktop. Name it
dpproject. We will refer to its absolute address in the following way:
1
| c:\...\Desktop\dpproject |
where the ellipsis will refer to specifics such as your Windows
User file folder.
2) For our application, I'm reusing code from another tutorial which might be described as an "in-progress"
Pong. This code, with its multiple packages, classes and use of graphics, should be more helpful than a single-class
HelloWorld example. The more involved file structure affects the syntax of several important commands.
In your IDE, create a project (I'm going to call it
Pongish) and import these two classes,
complete with package structure, and verify that they run.
Demo.java1 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| package com.adonax.deploydemo;
import javafx.animation.AnimationTimer; import javafx.application.Application; import javafx.event.EventHandler; import javafx.geometry.Dimension2D; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage;
public class Demo extends Application implements EventHandler <KeyEvent> { final int WIDTH = 600; final int HEIGHT = 400; private Ball ball; public static void main(String[] args) { launch(args); } @Override public void start(Stage stage) throws Exception { stage.setTitle("In progress Pong/Breakout"); Group root = new Group(); Scene scene = new Scene(root, WIDTH, HEIGHT); Rectangle playScreen = new Rectangle(WIDTH, HEIGHT); playScreen.setFill(Color.YELLOW); root.getChildren().add(playScreen); this.ball = new Ball(new Dimension2D(WIDTH, HEIGHT)); root.getChildren().add(ball); root.setFocusTraversable(true); root.requestFocus(); root.setOnKeyPressed(this); stage.setScene(scene); stage.show(); AnimationTimer animator = new AnimationTimer() {
@Override public void handle(long arg0) { ball.update(); } };
animator.start(); }
@Override public void handle(KeyEvent arg0) { if (arg0.getCode() == KeyCode.SPACE ) { ball.paddleHit(); } } } |
Ball.Java1 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package com.adonax.deploydemo;
import javafx.geometry.Dimension2D; import javafx.scene.paint.Color; import javafx.scene.shape.Circle;
public class Ball extends Circle { private double ballRadius = 40; private double ballX = 100; private double ballY = 200; private double xSpeed = 4; private double ySpeed = 3; private double height, width; private volatile boolean swatted; public Ball(Dimension2D playfieldDims) { setCenterX(ballX); setCenterY(ballY); setRadius(ballRadius); setFill(Color.BLUE); height = playfieldDims.getHeight(); width = playfieldDims.getWidth(); } public void paddleHit() { swatted = true; } public void update() { ballX += xSpeed; ballY += ySpeed; if (swatted) { xSpeed *= -1; swatted = false; } if (ballX + ballRadius >= width) { ballX = width - ballRadius; xSpeed *= -1; } else if (ballX - ballRadius < 0) { ballX = 0 + ballRadius; xSpeed *= -1; } if (ballY + ballRadius >= height) { ballY = height - ballRadius; ySpeed *= -1; } else if (ballY - ballRadius < 0) { ballY = 0 + ballRadius; ySpeed *= -1; }
setCenterX(ballX); setCenterY(ballY); } } |
SET UP THE SOURCE AS A MODULAR JAVA APPLICATION3A) Create a file folder named
source within our project folder
dpproject.
3B) Create file folder
moduleDemo within
source. The file folder
moduleDemo is going to be our module container to the application source.
3C)
Every module container must have a
module-info file. Create a text file and name it
module-info.java. Save it in the file folder
moduleDemo. The .java file should contain the following text:
Note that the name of our module's file folder matches the name of the class in this code. The body of the code (to be filled in) will be a series of
requires and
exports statements that make explicit all dependencies between our module and the required Java runtime modules. We will determine them in a future step.
D) Copy our example application's source code into the
moduleDemo folder. Our package statement, for
Demo.java, is the following:
1
| package com.adonax.deploydemo; |
So, we need to have the following file folder structure within
moduleDemo:
1 2 3 4 5 6 7 8
| moduleDemo | --> com | --> adonax | --> deploydemo : Demo.java Ball.java |
Probably the easiest way to do this is to copy directly from the top package file folder from where your IDE stores the source files for your application.
COMPILE THE MODULE4A) Because
module-info.java is incomplete, attempts to compile the project will fail. We will use information from the error messages to finish writing
module-info.java. From within the project folder
dpprject, run the following
cmd.exe command:
1
| c:\...\dpproject>"%JAVA9_HOME%\bin\javac" -d compiled --module-source-path source -m moduleDemo |
Note that I am using a Windows
Environment Variable to specify the location of the Java 9 JDK in order to address
javac.exe. You will likely have to create your own environment variable.
[A couple notes on cmd.exe:
(1) The cmd.exe program is a standard Windows program that creates a "console" or "shell" within which you can navigate the file system of your PC and issue commands. To invoke it, right-click the Start Menu and select Run. In the Open field, enter cmd and hit OK. This should open a console and place you in a file directory corresponding to your User name. To navigate from there to the project folder, enter the following command: cd Desktop/dpproject
(2) Notice that the fully addressed javac command is enclosed in quotes. This is because my JDK is located in a subfolder of C:\Program Files and cmd.exe interprets the space character between "Program" and "Files" as a delimiter. Enclosed in quotes, the space is interpreted as a character, and this allows cmd.exe to properly locate javac.exe.
(3) Windows now also comes with PowerShell which is similar to cmd. There are some differences between the two. I think you can use either, but I haven't verified PowerShell works with every step of this tutorial.]The parameter
d (
<directory>) designates the file folder where the compiled code will be put. Our command creates and places the result in a folder named
compiled.
The next section of the command designates the file folder which holds the modular Java source code. Our source code is in the folder
source.
The last clause gives the name of the module to be compiled:
moduleDemo.
4B) Because we haven't spelled out the module's dependencies, the result will be a listing consisting of many errors. Inspect the start of the output. You should have a something like the following (I've pasted my first three of 22 errors):
1 2 3 4 5 6 7 8 9 10 11 12 13
| C:\...\dpproject>"%JAVA9_HOME%\bin\javac" -d compiled --module-source-path source -m moduleDemo source\moduleDemo\com\adonax\deploydemo\Ball.java:3: error: package javafx.geometry is not visible import javafx.geometry.Dimension2D; ^ (package javafx.geometry is declared in module javafx.graphics, but module moduleDemo does not read it) source\moduleDemo\com\adonax\deploydemo\Ball.java:4: error: package javafx.scene.paint is not visible import javafx.scene.paint.Color; ^ (package javafx.scene.paint is declared in module javafx.graphics, but module moduleDemo does not read it) source\moduleDemo\com\adonax\deploydemo\Ball.java:5: error: package javafx.scene.shape is not visible import javafx.scene.shape.Circle; ^ (package javafx.scene.shape is declared in module javafx.graphics, but module moduleDemo does not read it) |
The first error occurs when the compiler is unable to find the package
javafx.graphics in line 3 of
Ball.java. The compiler gives us the following helpful error message:
1
| package javafx.animation is declared in module javafx.graphics, but module moduleDemo does not read it |
This tell us that our module is dependent upon module
javafx.graphics.
4C) Add the following
requires line to our module-info code block and save:
1 2 3
| module moduleDemo { requires javafx.graphics; } |
4D) With the
module-info.java update saved, run the compile line again.
Rather remarkably, the compilation now completes without error. A more involved project could easily require multiple iterations. However, sometimes dependencies don't show up at compile time, but come up at run time. Before declaring that our
module-info.java is finished, we need to give our compiled code a test run.
4E) Run the following command line to test if our compiled project runs:
1
| C:\...\dpproject>java -p compiled -m moduleDemo/com.adonax.deploydemo.Demo |
The option
-p is a shortened form of
--module-path. The module we want to run was just compiled into the
compiled folder. The option
-m is a shortening of
--module and the required value is
<module>/<mainclass>. A nice little gotcha here is the syntax specifying the module and its main class. Note the direction of the "/" and the use of "." as a separator for the package structure.
4F) When we run our compiled code, we get an error. Inspect the error code. A diagnostic points out a needed
exports dependency.
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
| Exception in Application constructor Exception in thread "main" java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source) at java.base/sun.launcher.LauncherHelper$FXHelper.main(Unknown Source) Caused by: java.lang.RuntimeException: Unable to construct Application instance: class com.adonax.deploydemo.Demo at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(Unknown Source) at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(Unknown Source) at java.base/java.lang.Thread.run(Unknown Source) Caused by: java.lang.IllegalAccessException: class com.sun.javafx.application.LauncherImpl (in module javafx.graphics) cannot access class com.adonax.deploydemo.Demo (in module moduleDemo) because module moduleDemo does not export com.adonax.deploydemo to module javafx.graphics at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Unknown Source) at java.base/java.lang.reflect.AccessibleObject.checkAccess(Unknown Source) at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source) at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$8(Unknown Source) at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(Unknown Source) at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(Unknown Source) at java.base/java.security.AccessController.doPrivileged(Native Method) at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(Unknown Source) at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(Unknown Source) at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method) at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source) ... 1 more |
The key phrase above is the following:
1
| module moduleDemo does not export com.adonax.deploydemo to module javafx.graphics |
In other words, code in the
javafx.graphics module (which makes use of reflection) needs to access code from our module.
4G) To fix this, add an
exports line to
module-info.java:
1 2 3 4
| module moduleDemo { exports com.adonax.deploydemo; requires javafx.graphics; } |
Save this revision of
module-info.java and redo the compilation line from before. (I like to delete the previously generated folders before redoing.)
1
| C:\...\dpproject>"%JAVA9_HOME%\bin\javac" -d compiled --module-source-path source -m moduleDemo |
Now, try another test run of our program.
1
| C:\...\dpproject>java -p compiled -m moduleDemo/com.adonax.deploydemo.Demo |
Did it work? Congratulations if it did!
If the
Pongish program fails to run, examine both the contents of
module-info.java and any typed commands very closely for typos. Small things, like a missing ";" or a inconsistency in capitalization or wrong direction on a slash can be quite enough to cause a modular compilation to fail.
*ONE MORE STEP* FOR COMPILATION
If your program makes use of resources, such as graphic or audio files, these have to be copied over manually into the compiled folder system. The
javac command does not do this for us.
CREATE CUSTOMIZED RUNTIME WITH jlink TOOLIn this step, we take the compiled source and create a distribution folder.
5A) Run the following command:
1
| C:\...\dpproject>"%JAVA9_HOME%\bin\jlink" --module-path compiled;"%JAVA9_HOME%\jmods" --add-modules moduleDemo --launcher LaunchDemo=moduleDemo/com.adonax.deploydemo.Demo --output dist |
The required
--module-path clause points to where the
jlink tool will look for modules. We have two arguments:
1
| --module-path compiled;"%JAVA9_HOME%\jmods" |
One argument (
compiled) is the file folder location of our compiled application. The other argument, "%JAVA9_HOME%\
jmods", is the file location within which the Java 9 JDK that holds the Java language modules.
The required
--add-modules clause names the root module for resolution. For our program,
moduleDemo is the only module that needs to be named. Its
module-info.class, with its list of requirements, will tell
jlink which other modules are needed.
The
--launcher clause is optional. The syntax we use is the following:
1
| --launcher command=module/main |
With the inclusion of this option, two files are created in
dist/bin, based on the name "
LaunchDemo". These files are named
LaunchDemo and
LaunchDemo.bat. The first is a
Bash script (for Unix), the second is a
Batch file (for DOS). We will use
LaunchDemo.bat for testing, to verify that our program runs correctly. But we won't actually need either of these files when we configure
Inno Setup 5.
The
--output option gives the name that will be assigned to the root folder of the files created by
jlink. In our command line, we name the folder
dist.
5B) Verify that the distribution application works. In the file folder
dpproject/dist/bin, along with many
.dll files and a few
.exe files, will be a file we specified with our
--launcher clause:
LaunchDemo.bat. Run this file.
The program should run correctly at this point. A console window is opened for the
.bat file and suspended while our program runs. We will configure
Inno Setup 5 so that there will be no
cmd.exe window opened.
PACKAGE WITH INNO SETUP 56A) Run the
Inno Setup Wizard to create an
.ISS file. This is done by the
Start Menu choice
Inno Setup Compiler. (Nothing says "I am a Wizard" like the program name "Compiler", yes?)
6B) Check the checkbox for the following when given the option:
1
| New File > Create a new script file using the Script Wizard |
and hit
Next for this, and for the "
Welcome Screen" that follows.
6C) The "
Application Information" screen that comes up next is self-explanatory. The reader can decide what to fill in, and proceed. I am naming the application
Pongish and giving it a version number of
0.1.
6D) For "
Application Folder", leave in the defaults. If you put
Pongish as the name in the previous step, it will appear now as the default
Application Folder Name. This will be the folder name created for our application in the
Program Files directory.
6E) Under "
Application Files" we are asked to enter the executable. Browse to the .../dpproject/dist/
bin folder. Select
javaw.exe. Why
javaw.exe and not
java.exe? The reason is that
java.exe is a console application, whereas
javaw.exe runs in a window and won't cause a console shell to open. Later on, when editing the completed
.ISS file, we will provide a
Parameters line that will provide the rest of the text needed to run our module.
6F) Leave the checkbox "
Allow user to start the application after Setup has finished" checked, and the checkbox for "
The application doesn't have a main executable file" unchecked.
6G) For the "
Application Shortcuts" tab, leave the settings so that a
Start Menu entry and an optional Desktop Shortcut are made.
6H) For the "
Application Documentation" section, I am leaving this blank for now. If you have documents ready to go, this is where they would be identified.
6I) For "
Languages" I'm leaving in the default "
English".
6J) For "
Compiler Settings" I recommend using the
Browse button to set the "
Custom compiler output folder" to our project folder: "C:\...\Desktop\dpproject". With the next options you can take the opportunity to specify a name for the resulting setup file that is generated, and to specify an icon for the setup file, and even to set a password. I am leaving the default name setup. After we run our compilation, the
dpproject folder will contain
setup.exe.
6K) On "
Inno Setup Preprocessor" if possible, the default will create
#define compiler directives. I recommend leaving the option checked if available.
6L) After all these screens, go ahead and generate the
.iss file. The wizard will create the file and leave it open for further editing.
EDIT THE .iss FILE7A) If
Inno Setup is able, the first section of the
.iss file will consist of a set of
#define statements. These should all be fine, except for one. Change the
#define for "
MyAppExeName" to the following:
1
| #define MyAppExeName "bin\javaw.exe" |
If you don't have
#define directives, then you will have to make the same change to every reference to
javaw.exe in the document, as the
{app} variable will point to the root folder,
dist.
7B) In the [Setup] tag section, add the following two lines:
1 2
| ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 |
These are needed because
Inno Setup 5 defaults to 32-bit, but Java 9 on Windows is 64-bit.
7C) In the [Files] tag section, edit the existing line to the following (as a single line):
1
| Source: "C:\...\dpproject\dist\*.*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs |
This line will direct the setup routine to copy the entire contents of
dist to the
Program Files/Pongish directory. Not every file needs to be brought over during the install. For example, the
Bash and
Batch files
bin\LaunchDemo and
bin\LaunchDemo.bat are not needed, nor is
bin\java.exe since we only use
bin\javaw.exe. There are likely other files that can be omitted, but I haven't figured out which. If you want to save a few KB, you can delete these files from
dist before running the compiler.
7D) In the [Icons] section we inline information pertaining to the icons used in the
Start Menu and desktop. The two lines should be edited to the following two single lines by adding a
Parameters option:
1
| Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "-m moduleDemo/com.adonax.deploydemo.Demo" |
1
| Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Parameters: "-m moduleDemo/com.adonax.deploydemo.Demo" |
The filename and parameter contents combine to give us the standard command line form for running a modular Java program. If you have no
#define satements, then substitute
Pongish for
{#MyAppName}, and
bin\javaw.exe for
{#MyAppExeName}.
When no icon is designated to the wizard, the compiler will default to using the icon stored in the
javaw.exe file. If you have an
.ico file to use, it should be included as the option
IconFilename. As an example, if the icon file is
pongish.ico, and you have saved it to the top folder,
dist, the resulting line for the
Start Menu icon would be the following:
1
| Name: "{commonprograms}\{#MyAppName}"; IconFileName: "{app}\pongish.ico"; Filename: "{app}\{#MyAppExeName}"; Parameters: "-m moduleDemo/com.adonax.deploydemo.Demo" |
7E) In the [Run] tag section, edit the existing line to the following:
1
| Filename: "{app}\bin\javaw"; Parameters: "-m moduleDemo/com.adonax.deploydemo.Demo"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: postinstall skipifsilent |
7F) Save all changes.
7G) Run the compile.
If all goes well, the program will first create the
setup.exe file and place it in our project folder, then offer to proceed with an install. Go ahead and run the install, then verify the program runs correctly and was installed in the correct location. I'd also then
uninstall and then reinstall by clicking on
setup.exe. The key test is taking
setup.exe to another PC and running it.
Congratulations on reaching the end of this walk-through!
I wish you the best of success with your publishing, and look forward to playing your Java games.
END