Skip to content

paginagmbh/NativeJavaApplicationStub

Repository files navigation

Native Java Application Stub

Latest Release macOS 11.0+ Architecture: arm64, x86_64 GitHub top language Swift 5 GitHub License

A direct replacement for tofi86/universalJavaApplicationStub. Create native Java apps on macOS.

Motivation

macOS apps are not single executable files like Windows .exe files, but instead bundles: directories with a specific structure and an executable file inside. This allows for multiple executables, resources, and metadata to be packaged together.

📂 MyApp.app/
└── 📂 Contents/
    ├── 📂 MacOS/
    │   └── 📄 MainExecutable
    ├── 📁 Resources/
    ├── 📄 Info.plist
    …

This project provides a native executable that can be used as the main executable in a Java app bundle. It locates and launches a Java runtime installed on the system, then starts the Java application. The jar files can then be included in the bundle’s Resources folder and specified in the Info.plist file. This way the bundle functions as a native Java application.

Important

This project is not a Java runtime itself, but a launcher for existing Java runtimes on the system.

Comparison to universalJavaApplicationStub

This project is a rewrite of tofi86/universalJavaApplicationStub. That project had the same motivation but is deprecated since 2023 and no longer maintained.

This new implementation is a native binary – the kind of executable that macOS expects. The old implementation was a shell script that was made executable, which caused various issues with macOS security features and app notarization. Reimplementing the stub as a native binary also allows better and more secure use of native APIs and system calls without workarounds.

It is mostly feature compatible with the old implementation and should function as a drop-in replacement in most cases.

How does this work?

The Info.plist file in the app bundle contains metadata for the app execution, relevant to both the “normal” macOS app bundle structure and the Java application configuration.

CFBundleExecutable

macOS reads the Info.plist key CFBundleExecutable to determine the executable (in the Contents/MacOS/ directory) to run when the app is launched. This executable is the main entry point of the app and is responsible for launching the Java application. When using NativeJavaApplicationStub, this executable is the provided binary. It is per default named NativeJavaApplicationStub but can be renamed, for example to match universalJavaApplicationStub for drop-in compatibility, or the app name for better error messages.

Java configuration

NativeJavaApplicationStub reads the Java configuration from the Info.plist file, using a custom key structure. There, the Java version requirement can be specified, as well as the main class and classpath for the Java application. The stub then locates a suitable Java runtime on the system that matches the specified version requirement, and launches the Java application using that runtime.

Various parameters can be configured in the Info.plist to control the Java application launch, such as JVM options and parameters.

If errors occur during the Java runtime search or application launch, the stub will display an error message to the user and exit with a non-zero exit code.

Configuration

The Java application configuration is specified in the Info.plist file of the app bundle, using a custom key structure. The custom keys are resolved in this order:

  1. First, the stub looks for the key Java in the Info.plist, which should contain a dictionary with the Java configuration.
  2. If the key Java is not found, it looks for key JavaX1, but expects the same structure there.
  3. If neither key is found, it looks for Oracle Java entries in the Info.plist.

This is performed on a key-by-key basis. So one key might be pulled from the JavaX dictionary, while another key might be pulled from the Java dictionary, and another key might be pulled from the Oracle Java entries.

In the entries $APP_PACKAGE, $APP_ROOT, $JAVAROOT and $USER_HOME are expanded. $APP_PACKAGE and $APP_ROOT both refer to the app bundle root directory, while $JAVAROOT refers to the ./Contents/Resources/Java path in the app bundle, which is a common location for Java resources. $USER_HOME refers to the user’s home directory. All paths are inserted as an absolute path without a trailing slash. ${VARIABLE} syntax is not supported, only $VARIABLE.

Main class (required)

  • Keys: MainClass (Java), JVMMainClassName (Oracle).
  • Type: String.

Specifies the main class of the Java application to launch, for example com.example.Main.

Classpath

  • Keys: ClassPath (Java), JVMClassPath (Oracle).
  • Type: Array of Strings or String (with : separator).

Specifies the classpath for the Java application, for example $JAVAROOT/*. This is passed to the Java runtime as the -cp argument, so it supports the same syntax as the -cp argument, such as using : as a separator for multiple entries, and using * to include all jar files in a directory.

Java version requirement

  • Keys: JVMVersion (Java), JVMVersions (Oracle).
  • Type: String.

Specifies the required Java version for the application. If the version requirement is not specified, any Java version is accepted.

  • 11 or 11* or 11.* means any Java version starting with 11, such as 11.0.1 or 11.0.2-ea.
  • 11+ means any Java version 11 or higher, such as 11.0.1, 12.0.2 or 17.0.1.
  • 11;21 means any Java version between 11 and 21, such as 11.0.1, 12.0.2 or 17.0.1, but not 10.0.2 or 21.0.1.

The versions can be as specific as a build number: 11.0.1_13 means Java 11 update 0, build 13.

Note

The universalJavaApplicationStub syntax of 11;21* is no longer supported.

The universalJavaApplicationStub syntax of accepting specification of -ea (early access) versions is no longer supported. If for example Java 21.0.0-ea is installed it is treated as 21.0.0 and matches the same version requirements as 21.0.0 would.

Unlike universalJavaApplicationStub, The syntax 11.* and 11.+ is supported in addition to 11* and 11+.

Note that Java 8 and below have a different versioning scheme, so they are specified as 1.8 for Java 8, 1.7 for Java 7 and so on. If an invalid version requirement is specified such as 1.9 or 8, the stub will reject it and display an error message, since these versions do not exist.

Working directory

  • Keys: WorkingDirectory (Java).
  • Type: String.

Specifies the working directory for the Java application. This is the directory from which the Java application is launched, and can be used to control where the application looks for resources or writes files to by default. If not specified, the working directory is the app bundle root directory.

Properties

  • Keys: Properties (Java), JVMOptions (Oracle).
  • Type: Map of String to String, or Strings (with -Dkey=value syntax, separated by spaces).

Specifies system properties to pass to the Java runtime, for example -Dfile.encoding=UTF-8 (<key>file.encoding</key> with value UTF-8 in the map syntax).

This can be specified as a map of key-value pairs, or as a string with space-separated -Dkey=value entries, which is supported for compatibility with the Oracle Java entries. The properties are passed to the Java runtime as -Dkey=value arguments.

JVM options

  • Keys: VMOptions (Java), JVMDefaultOptions (Oracle).
  • Type: Array of Strings or String (with space separator).

Specifies JVM options to pass to the Java runtime, for example -Xmx2G. This is passed to the Java runtime as-is, so it supports the same syntax as JVM options, such as using -X options or -XX options, and using spaces to separate multiple options. This is intended for non-property JVM options, while the Properties entry is intended for system properties, but there is no strict enforcement of this.

Here the -X has to be part of the key in the list syntax.

<key>VMOptions</key>
<array>
    <string>-Xmx2G</string>
    <string>-Xms512M</string>
    <string>-Dfile.encoding=UTF-8</string>
</array>

Start on main thread

  • Keys: StartOnMainThread (Java).
  • Type: Boolean.

Specifies whether the Java application should be started on the main thread. If this is set, the -XstartOnFirstThread option is passed to the Java runtime.

Main arguments

  • Keys: Arguments (Java), JVMArguments (Oracle).
  • Type: Array of Strings or String (with space separator).

Specifies the arguments to pass to the Java application’s main method. This can be specified as an array of strings, or as a single string with space-separated arguments, which is supported for compatibility with the Oracle Java entries.

Further command line arguments passed to the app bundle when launching the app are also passed to the Java application, so this can be used to specify default arguments while still allowing users to pass custom arguments when launching the app.

Splash file

  • Keys: SplashFile (Java), JVMSplashFile (Oracle).
  • Type: String.

Specifies a splash file to display while the Java application is launching. This is passed to the Java runtime as the -splash argument, so it supports the same syntax as the -splash argument, such as specifying a file in the app bundle resources using $JAVAROOT/splash.png.

App Icon and Name

The stub reads the app icon and name from the Info.plist file, using the standard keys CFBundleIconFile and CFBundleDisplayName / CFBundleName. This allows the stub to display error messages with the correct app name and icon, and also allows the app to have a custom icon and name when displayed in the dock or in error messages, instead of the default stub name and icon. These are passed to Java as -Xdock:name and -Xdock:icon arguments. The app icon is assumed to be in the resources folder as is the usual case on macOS.

Note

universalJavaApplicationStub searched an oracle resource folder for the app icon. This is no longer supported. The macOS native app icon is used.

Resulting Java Call

The resulting call to the Java runtime looks like this:

<java path>
    -cp <classpath> # if specified
    -splash:<splash file> # if specified
    -Xdock:icon=<app icon path> # if specified
    -Xdock:name=<app name> # if specified
    -Dkey=value # for each property specified
    <jvm options> # if specified
    -XstartOnFirstThread # if specified
    <main class>
    <main arguments> # if specified
    <original command line arguments> # if any

Java Search Order

The stub searches for a suitable Java runtime in the following order:

  1. First, it checks the JAVA_HOME environment variable.
  2. Then, it checks all java executables in the $PATH in order.
  3. Then, it checks the Java runtimes known to the system, via the /usr/libexec/java_home tool.
  4. Then, it checks the SDKMAN Java runtimes located in ~/.sdkman/candidates/java/
  5. Then, it checks the SDKMAN Java runtimes located in /opt/homebrew/opt/SDKMAN-cli/libexec/candidates/java/. This is the path when SDKMAN is installed via Homebrew.
  6. Then, it checks for Oracle Java runtimes in /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java/.
  7. Then, it checks for Apple Java runtimes in /Library/Java/Home/bin/java.

The first Java runtime that matches the specified version requirement is used to launch the Java application. This is different from universalJavaApplicationStub, which always returned the highest matching version. If no suitable Java runtime is found, an error message is displayed and the stub exits with a non-zero exit code:

Errors, Exit Codes and Debugging

If an error occurs during the Java runtime search or application launch, the stub will display an error message to the user and exit with a non-zero exit code. The error message is displayed in a native macOS alert dialog, and includes the app name and icon if they are specified in the Info.plist file, to provide a better user experience.

Additionally startup behavior is logged using NSLog, which can be viewed in the Console app for debugging purposes or by executing the stub from the command line and observing the output:

./MyApp.app/Contents/MacOS/NativeJavaApplicationStub
  • MainClass is missing: If the required MainClass key is missing in the configuration, the stub will display a popup with an english language error message and exit with code 2.
  • JVM version range could not be parsed: If the specified JVM version requirement cannot be parsed, the stub will display a popup with an english language error message and exit with code 4.

These are not localized since they indicate a configuration error that should be fixed by the developer, and thus the error message is intended for the developer and not the end user. However for one message it is important for the user to understand:

  • No suitable Java runtime found: If no suitable Java runtime is found that matches the specified version requirement, the stub will display a popup with a localized error message and exit with code 3. The localization is implemented in Swift code and not using string resources. This way it can be included directly in the singular binary.

    Currently the following localizations are included:

    • English
    • German
    • French
    • Spanish
    • Chinese (Simplified)
    • Portuguese (Brazilian)

    Feel free to contribute localizations for other languages by submitting a pull request. The relevant file is Localization.swift.

Usage in Java Projects

This stub has been developed to be used in conjunction with paginagmbh/Mac-App-Gradle for building native Java app bundles on macOS.

It should however be usable in any Java project that builds a macOS app bundle, as long as the provided binary is included in the Contents/MacOS/ directory of the app bundle and specified as the CFBundleExecutable in the Info.plist file. It should be able to be used as a drop-in replacement for universalJavaApplicationStub in most cases, as long as the configuration is compatible with the supported keys and syntax described above.

Example Info.plist

<?xml version="1.0" encoding="UTF-8"?><plist version="1.0">
  <dict>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleAllowMixedLocalizations</key>
    <true/>
    <key>CFBundleDisplayName</key>
    <string>MyApp</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleName</key>
    <string>MyApp</string>
    <key>CFBundleIdentifier</key>
    <string>com.example.myapp</string>
    <key>CFBundleExecutable</key>
    <string>NativeJavaApplicationStub</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0.0</string>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>NSHumanReadableCopyright</key>
    <string>© 2025–2026 MyCompany</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon</string>
    <key>CFBundleIconName</key>
    <string>AppIcon</string>
    <key>NSHighResolutionCapable</key>
    <true/>
    <key>JavaX</key>
    <dict>
      <key>MainClass</key>
      <string>com.example.myapp.Main</string>
      <key>JVMVersion</key>
      <string>11+</string>
      <key>SplashFile</key>
      <string>splash.png</string>
      <key>ClassPath</key>
      <array>
        <string>$JAVAROOT/*</string>
      </array>
      <key>Properties</key>
      <dict>
        <key>file.encoding</key>
        <string>utf8</string>
        <key>stdout.encoding</key>
        <string>UTF-8</string>
        <key>stderr.encoding</key>
        <string>UTF-8</string>
        <key>java.net.useSystemProxies</key>
        <string>true</string>
      </dict>
    </dict>
  </dict>
</plist>

Building from Source

The source code is written in Swift and can be built using Xcode or the Swift command line tools. After cloning the repository, it can be opened in Xcode and built for the desired architectures (arm64 and x86_64) using the provided Release scheme.

This creates a dummy app bundle in the Xcode build products, which contains the built binary in the Contents/MacOS/ directory. The binary can then be copied from there and included in the Contents/MacOS/ directory of the desired app bundle.

Publishing

The release process so far has to be done manually. Build the binary as described above, then create a new release on GitHub and upload the built binary as an asset to the release. The release should be tagged with the version number, for example v1.2.3.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Footnotes

  1. This is a feature introduced in universalJavaApplicationStub. The Java key was used by Apple in the past for JavaApplicationStub and has some legacy handling. Apparently Apple detects the presence of this key and prompts for a download of Java 6 before invoking the stub, which is not desired in modern Java applications. Thus the new JavaX key was introduced to avoid this legacy handling, while still supporting the old Java key for compatibility.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages