JVM apps¶
Overview¶
Conveyor has integrated support for apps that run on the JVM (for any supported language). You get the following:
- A custom launcher that replaces the
java
command and adds extra features. - Support for the module system:
- Usage of
jlink
andjdeps
to create minimal JVM distributions, containing only the modules that your app needs. - Fully modular JARs are detected and linked into the
modules
file, yielding faster startup and smaller downloads.
- Usage of
- Native libraries are extracted from JARs so they can be either signed, or discarded if they're for the wrong OS/CPU.
- Maven and Gradle integration:
- A Gradle plugin that automatically generates configuration snippets.
- Maven projects can have their classpath read directly, without needing a plugin.
- Integrated support for GUI frameworks like JavaFX and Compose Desktop.
To package an app that uses the JVM, you must choose a JDK and at least one JAR.
Java versions
Conveyor only supports apps that use Java 11 or later - Java 8 won't work. All classes must be inside a JAR. If you need Java 8 support please email contact@hydraulic.software.
Synopsis¶
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 48 49 50 51 52 53 54 55 56 57 58 |
|
Keys¶
app.jvm
An input hierarchy in the same manner as the top level app
object. The inputs will be resolved (copied/downloaded/extracted) and examined for a jmods
directory. If a jmods
directory is found then the contents will be used to create a JVM that contains only the code your application needs, using the jlink
tool. If you have jmods to add to the jlinked image (e.g. JavaFX), you should add them here alongside the JVM itself. If there is no jmods
directory then the JDK is used as-is and jlinking isn't done.
app.jvm.gui
The GUI launcher. See the launchers section.
app.jvm.cli
CLI launchers. See the launchers section.
app.jvm.constant-app-arguments
A list of arguments that will always be passed to the app in addition to whatever the user specifies. Can be useful to plumb metadata from the app definition through to the app itself, like by telling it its own version number.
app.jvm.system-properties
A map of system properties. The default properties are:
app.dir
- points at the install directory where input files are placed.app.displayName
- equal to the${app.display-name}
key.app.version
- equal to the${app.version}
key.app.revision
- equal to the${app.revision}
key.app.vendor
- equal to the${app.vendor}
key.
If you include the client enhancements config from the standard library, you also get:
jna.nosys
- set to false, which makes JNA work in the packaged environment.picocli.ansi
- set totty
, to make sure PicoCLI always uses colors even on Windows.java.net.useSystemProxies
- set to true, to make the JVM use the system HTTP proxy settings.
app.jvm.options
See JVM options
Warning
- Watch out for accidental mis-use of HOCON syntax when writing something like
constant-app-arguments = [ --one --two ]
. This creates a single argument containing "--one --two" which is unlikely to be what you want. Instead, writeconstant-app-arguments = [ --one, --two ]
or put each argument on a separate line. Conveyor will warn you if you seem to be doing this.
Important
When a JVM app is signed on macOS or Windows the JVM attach mechanism is disabled using the -XX:+DisableAttachMechanism
flag. That's because the attach mechanism allows any local user to overwrite the app's code in memory without needing to alter files on disk, thus defeating code signing.
As a consequence debuggers and profilers won't be able to find a signed JVM app, by design.
This rule doesn't apply on Linux because that platform doesn't use code signing in the same way. On macOS the OS forbids debugger attachment unless the app opts in to allowing this, thus apps cannot tamper with each other's memory even when running as the same user. On Windows anti-virus checks are done when code is loaded, and so programs that allow arbitrary code injection allow lateral movement by malware.
app.jvm.modules
List of modules to take from the underlying JDK for the usage of classpath JARs. The modules and their transitive dependencies will be included, all others will be dropped. Defaults to [ detect ]
. The special entry detect
is replaced with modules detected using the jdeps
tool (see below). Note that this is not the place to list modular JARs in your app - it's only for modules to take from the JDK or any other supplied JMODs. Modular JARs should be added to base inputs like any other JARs.
app.jvm.jlink-flags
Extra flags passed to the jlink
command. Can be used to invoke plugins and so on. See the output of jlink --help
and jlink --list-plugins
to see what's available. By default this adds --ignore-signing-information
and specifies a jimage file ordering, which helps with startup time.
app.jvm.mac.plist
HOCON structure converted to the Info.plist
file used for the linked JVM on macOS. You can normally ignore this.
app.jvm.strip-debug-info
If true (defaults to false) then JVM classfile debug attributes are stripped during repacking.
app.jvm.unwanted-jdk-files
A list of file names that are erased from the application after jlinking and launcher creation is done. This is useful for cleaning up files that are usually only needed for development purposes. You can remove default items from the list by prefixing them with a -
(minus). The list defaults to:
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 |
|
app.jvm.extract-native-libraries
If true (the default) then native libraries will be deleted from JARs and placed in the lib directory of the bundled JVM. If false, they are left alone. See below for more information.
Importing a JVM/JDK¶
These days there are many distributions of Java to choose from. You've probably already chosen your JVM/JDK for development purposes, but you'll need to specify which one you want so Conveyor can download the different versions for different platforms.
The standard library has many snippets for different distributions and versions of the JVM. Learn what's available or run conveyor jdk-table
.
You can also easily add your own custom JVM by just adding the right inputs:
1 2 3 4 5 6 7 8 |
|
JAR stripping¶
The JAR files that get shipped are rewritten in three ways:
- Native shared/JNI libraries are moved out of the JAR.
- The metadata of the files is canonicalized to eliminate non-determinism that would otherwise reduce the efficiency of update mechanisms.
- Debug info is (optionally) stripped.
Moving native libraries out of JARs has these benefits:
- On Windows and macOS the security systems want to see that all native code is signed. Libraries hidden inside JARs would not be signed by the regular processes and on macOS this can result in the OS refusing to load them.
- Unpacking shared libraries improves startup time.
- It improves the effectiveness of update delta compression.
- It reduces download sizes by deleting libraries meant for other operating systems or CPU architectures.
Therefore, your software should always attempt to load shared libraries by using System.loadLibrary
first, before trying to extract native libraries from a JAR. Alternatively you can use System.load
in combination with the java.home
system property but remember to add either lib
on UNIX or bin
on Windows.
Modules¶
Conveyor can use the Java Platform Module System (JPMS a.k.a. Jigsaw).
Linked JVMs. Conveyor always bundles a specialized JVM created using jlink
. That's why Java 8 isn't supported (if you need support for Java 8 please let us know). Linking is primarily a size and startup time optimization: it gets rid of parts of the JDK you don't need, and linked modules get incorporated into the single modules
file which uses an more optimized format called "jimage". Classes can be loaded more quickly and take up less space when processed this way. You can use the jimage
command line tool in any JDK to view the contents of a modules
file.
Modular JARs. Conveyor will link a JAR that provides a module-info.class
into the bundled JVM as long as it doesn't depend on any auto-modules. As a consequence those JARs won't be found in the app data directory - only JARs on the classpath will be placed there. Conveyor always puts automatic modules (those that declare a module name in their manifest) as ordinary classpath JARs. At this time you cannot control whether modules are linked or placed on the classpath, except by pre-processing JARs to add or remove module-info.class
files. So: Explicit modules are always put on the module path and linked, other JARs never are.
Automatic dependency detection. A modern JDK comes with many modules that your app probably doesn't use. The jdeps
tool tries to figure out what JDK modules a non-modular JAR needs by doing static analysis of the bytecode. Conveyor uses jdeps
automatically whenever it finds a pseudo-module called detect
in the app.jvm.modules
list, and the default list contains only detect
.
JDeps will look for both usage of JDK modules, and also internal packages. The output of jdeps
is a guess. You may need to correct these types of mistake:
- It might miss the usage of modules if they're only accessed by reflection/ServiceLoader, or by languages that aren't bytecode languages.
- It might pull in modules that you don't actually need, because some obscure feature of a library you use happens to need it but you never actually use that library feature.
- It might not notice the usage of internal APIs, if done via reflection.
As an example of point (2) the popular JNA library will pull in all of Swing, because it has a utility function to get the native window handle of an AWT window. If you don't use Swing then this is just pointless bloat.
Both problems are easy to fix. If a module is missed, just add it to the app.jvm.modules
list. If you know you won't hit the codepath that requires a module, get rid of it by adding the module name prefixed with -
. Like this:
1 2 3 4 5 6 7 8 9 |
|
Note that you can't delete a module non-optionally required by an explicit module - this will just be ignored. The JVM wouldn't start up otherwise.
Module system flags. If non-reflective usage of internal APIs is statically detected you'll get a warning and the necessary --add-opens
flags will be passed. If you need to access internal APIs that aren't detected by jdeps, you'll need to add the necessary --add-opens
flags yourself in the app.jvm.options
key. If you add an inferred flag then that will suppress the warning.
When adding module related flags to the app.jvm.options
key, be aware that there must not be a space between the flag name and value. Although that works with the normal Java launcher, the special launcher used by Conveyor-packaged apps requires them in native JVM form. Write e.g. app.jvm.options += "--add-modules=jdk.incubator.foreign"
and not app.jvm.options += --add-modules jdk.incubator.foreign
.
Diagnosing issues. If you'd like to see what decisions Conveyor has made about your app, you can make the repacked-jars
task for some machine and look at the files called required-jdk-modules.txt
(this is the output of jdeps run over your jars) and modular-jars.txt
which is a list of the JARs that will be linked in to the optimized JVM.
Launchers¶
JVM apps are started by a native executable that Conveyor supplies and customizes during the build (see below). You can have one GUI launcher and zero or more supplementary command line launchers. The GUI launcher is the one executed when the user starts the app via the start menu, by running the bundle on macOS (e.g. via Mission Control or the Finder) or using the desktop environment on Linux. CLI launchers can only be executed from the terminal, or by your own software. You're limited to a single GUI launcher because macOS doesn't normally have an installation concept, so the icon used to launch the software is the same as the directory that contains it. This makes it awkward to have more than one GUI entry point per app and it's not conventional to do so on that platform.
Launchers are defined using keys. If no launchers are defined then the JARs will be scanned to find a main class advertised in the manifest and that will become the GUI launcher. If more than one JAR advertises a main class, an error is reported. As such, for most apps you can simply ignore launchers and let Conveyor figure it out.
The GUI launcher is defined by app.jvm.gui
and CLI launchers are defined using the app.jvm.cli
list/map key. A launcher is defined by a config object, but there are shorthand syntaxes in which the contents of the object are inferred.
Launcher objects¶
A fully defined launcher uses the following object:
1 2 3 4 5 6 7 |
|
For each launcher you can specify these keys:
main-class
The fully qualified name of a class with a static main method, or for JavaFX apps, that inherits from javafx.application.Application
.
exe-name
The name of the binary executable on disk. You don't need a .exe
suffix, one will be added for you on Windows. If not specified, will use either the display name or the fsname, depending on platform conventions.
class-path
A list of file names or globs that select a set of JAR files from the inputs. Defaults to *.jar
which is usually good enough. Note that explicit JPMS modules don't have to be specified here, as they will be jlinked into the distribution JVM and thus are always available.
console
Controls whether the launcher uses console mode on Windows. See the documentation for the console key for more details. If not specified, defaults to true for CLI launchers and false for the GUI launcher. You should normally never need to set this, but it may be helpful in some cases if you have CLI launchers that aren't meant to be invoked by the end user directly. Note that setting console = false will suppress the terminal window from popping up, but won't make the app appear in the start menu. You can only have one entry in the start menu.
JVM options¶
JVM command line flags ("options") can be used to control heap size, the garbage collector and so on. Note that this mostly but not entirely the same as arguments to the java
program. Keys for setting JVM options are arranged in a hierarchy with less specific keys being used as defaults for more specific keys. Each launcher can define:
options
windows.options
windows.amd64.options
mac.options
mac.amd64.options
mac.aarch64.options
linux.options
linux.amd64.options
linux.amd64.glibc.options
Each launcher always includes JVM options defined by the app.jvm
object, i.e. you can also set options by configuring:
app.jvm.options
app.jvm.windows.options
app.jvm.windows.amd64.options
- ... etc ...
In this way you can set JVM flags for every entry point in your app whilst also specifying options that apply only to specific platforms and launchers.
Defining launchers¶
You can define them in several ways, depending on how many defaults you accept:
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 |
|
Launcher features¶
JVM apps packaged with Conveyor use a custom native program to start the JVM. It adds the following features:
- Initializes the Sparkle update engine on macOS.
- Improved command line support for Windows:
- ANSI escapes (colors) are enabled, and the popular PicoCLI framework is automatically configured to use them. Modern versions of Windows have much better terminal support than in the past, along with a modern tabbed terminal emulator that's also open source.
- The console and default character encoding are both set to UTF-8 by default. This matches Linux and macOS, and means that drawing with Unicode symbols and emoji should work. See here for details.
- The
$HOME
environment variable is set to the user's home directory (normally called%USERPROFILE%
on Windows). This makes it easier to use native code.
- Exceptions that escape
main
are shown in a GUI alert box on macOS and Windows. - Supports hard-wired command line arguments set from the build configuration, e.g. to tell the app its own version.
- Improves security by ensuring JVM configuration options are signed, and disabling features that can be used to inject code into a running program. This makes it harder for malware to use your app to subvert code signing.
- On Windows,
%APPDATA%
and%LOCALAPPDATA%
are rewritten to point to the private containerized locations used by MSIX packaging. This means files you create under these directories will be findable by other apps you start, rather than shielded from them as is the default. - Exposes the app's install location via the
app.dir
system property.
The launcher supports some of the same features as the java launcher, for example JavaFX apps don't need a main method, the Mac specific -XstartOnFirstThread
flag is understood and the initial stack size can be set.
Future features
The custom launcher enables many useful features to be added in the future. Ideas include startup time optimization via automatically
configured AppCDS, exposing APIs to control and monitor the update process, integrating
NodeJS and using JavaScript modules through it, automatically moving apps to /Applications
on macOS and regularizing how file/URL
open requests are exposed to the OS (which currently requires operating-system specific approaches and APIs).