Skip to content

Maven plugin for creating a native macOS bundle containing all dependencies required by a Maven project

License

Notifications You must be signed in to change notification settings

perdian/macosappbundler-maven-plugin

Repository files navigation

macOS app bundler Maven plugin

Maven plugin for creating a native macOS bundle containing all dependencies declared by a Maven project.

Maven~~~~ Central License Build

Requirements

  • Java 9 or newer

Usage

Minimum example

...
    <plugin>
        <groupId>de.perdian.maven.plugins</groupId>
        <artifactId>macosappbundler-maven-plugin</artifactId>
        <version>1.21.1</version>
        <configuration>
            <plist>
                <JVMMainClassName>de.perdian.test.YourApplication</JVMMainClassName>
            </plist>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>bundle</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
...

Extended example

...
    <plugin>
        <groupId>de.perdian.maven.plugins</groupId>
        <artifactId>macosappbundler-maven-plugin</artifactId>
        <version>1.21.1</version>
        <configuration>
            <plist>
                <CFBundleIconFile>src/bundle/test.icns</CFBundleIconFile>
                <CFBundleDisplayName>My supercool application</CFBundleDisplayName>
                <CFBundleDevelopmentRegion>English</CFBundleDevelopmentRegion>
                <CFBundleURLTypes>
                    <string>msa</string>
                </CFBundleURLTypes>
                <JVMMainClassName>de.perdian.test.YourApplication</JVMMainClassName>
                <JVMVersion>11+</JVMVersion>
                <JVMOptions>
                    <string>-Dfoo=bar</string>
                    <string>-Dx=y</string>
                </JVMOptions>
                <JVMArguments>
                    <string>-example</string>
                    <string>${someProperty}</string>
                </JVMArguments>
            </plist>
            <dmg>
                <generate>true</generate>
                <additionalResources>
                    <additionalResource>
                        <directory>src/bundle/macos/distribution</directory>
                    </additionalResource>
                </additionalResources>
            </dmg>
            <codesign>
                <identity>3rd Party Mac Developer Application: MyName (MyNumber)</identity>
            </codesign>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>bundle</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
...

Example with Java Module Path

...
    <plugin>
        <groupId>de.perdian.maven.plugins</groupId>
        <artifactId>macosappbundler-maven-plugin</artifactId>
        <version>1.21.1</version>
        <configuration>
            <plist>
                <CFBundleIconFile>src/bundle/test.icns</CFBundleIconFile>
                <CFBundleDisplayName>My supercool application</CFBundleDisplayName>
                <CFBundleDevelopmentRegion>English</CFBundleDevelopmentRegion>
                <CFBundleURLTypes>
                    <string>msa</string>
                </CFBundleURLTypes>
                <JVMMainModuleName>de.perdian.somemodule/de.perdian.test.YourApplication</JVMMainModuleName>
                <JVMVersion>11+</JVMVersion>
                <JVMOptions>
                    <string>-Dfoo=bar</string>
                    <string>-Dx=y</string>
                </JVMOptions>
                <JVMArguments>
                    <string>-example</string>
                    <string>${someProperty}</string>
                </JVMArguments>
            </plist>
            <dmg>
                <generate>true</generate>
                <additionalResources>
                    <additionalResource>
                        <directory>src/bundle/macos/distribution</directory>
                    </additionalResource>
                </additionalResources>
            </dmg>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>bundle</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
...

Features

After executing the goal during (e.g. during the package phase as shown in the example above) the macOS application bundle will be located in the PROJECT_NAME.app directory inside the target directory, where PROJECT_NAME equals the bundle name entered within the CFBundleName setting inside the plist configuration, or the name of the Maven project (${project.name}) if the value is not present inside the plist configuration.

The plugin will detect whether the project is a Java module by checking if the plist property JVMMainModuleName is present. If that's the case the launcher will use the modulepath. Otherwise the regular classpath will be used.

Configuration

Property list Configuration

The values within the plist element are directly transferred to the Info.plist file within the application bundle. To keep the usage within the code consistent they use the same keys within the pom.xml configuration as they do within the Info.plist.

The following values can be configured:

Key Type Required? Default Description
CFBundleDevelopmentRegion String No English The default language and region for the bundle, as a language ID.
CFBundleDisplayName String No ${project.name} The published name of your application.
CFBundleExecutable String No JavaLauncher The name of the executable within the application bundle. No regular user will ever see this but you may want to change it for debugging purposes when analyzing your application.
CFBundleIconFile File No The icns file that should be used as main icon for the application. The location must be entered relatively to the root of the project in which the plugin is used.
CFBundleIdentifier String No ${groupId}.${artifactId} The macOS bundle identifier of your application.
CFBundleName String No ${project.name} The internal name of your application.
CFBundlePackageType String No APPL A four-letter code specifying the bundle type. For apps, the code is APPL, for frameworks, it' FMWK, and for bundles, it's BNDL (Details)
CFBundleShortVersionString String No ${version} The version of your application.
CFBundleDocumentTypes Array of CFBundleDocumentTypes No Additional information for document types (see details for an extended example).
CFBundleURLTypes Array of Strings No A list of URL schemes (http, ftp, etc.) supported by the application.
JVMArguments Array of Strings No Additional arguments to be passed to the Java runtime.
JVMLogLevel String No INFO The amount of details the launcher will print to the console if called directly from the command line. Possible values: TRACE, DEBUG, INFO, WARN, ERROR.
JVMMainClassName String Yes (if the application is a classic classpath based application) The main class whose main method should be invoked when starting your application.
JVMMainModuleName String Yes (if the application is a module based application) The main module that should be invoked when starting your application.
JVMOptions Array of Strings No Additional parameters (-D parameters) to be passed to the Java runtime.
JVMRuntimePath String No The exact location of the Java runtime.
JVMVersion String No The Java version your application needs to work. Can either be an explicit version String like 11.0.1, a major version like 11 (signalizing that any Java 11 runtime is sufficient) or a value like 11+ (signalizing that any Java 11 or higher runtime is sufficient).
LSUIElement Boolean No Declares if the application is an agent app that runs in the background and doesn't appear in the Dock (Details).
NSAppleMusicUsageDescription String No A message that tells the user why the app is requesting access to the user’s media library.
NSAppSleepDisabled Boolean No Declares if the app is allowed to nap or not.
NSCameraUsageDescription String No A message that tells the user why the app is requesting access to the device's camera (Details).
NSHighResolutionCapable Boolean No true Declares if the application supports rendering in HiDPI (Retina) (Details).
NSHumanReadableCopyright String No A human-readable copyright notice for the bundle (Details).
NSMicrophoneUsageDescription String No A message that tells the user why the application is requesting access to the device's microphone (Details).
NSSupportsAutomatic GraphicsSwitching Boolean No true Declares whether an OpenGL app may utilize the integrated GPU (Details).

CFBundleDocumentTypes configuration example

    <configuration>
        <plist>
            ...
            <CFBundleDocumentTypes>
                <CFBundleDocumentTypes>
                    <CFBundleTypeName>MyDocumentType</CFBundleTypeName>
                    <CFBundleTypeRole>Editor</CFBundleTypeRole>
                    <CFBundleTypeExtensions>
                        <string>foo</string>
                        <string>foobar</string>
                    </CFBundleTypeExtensions>
                </CFBundleDocumentTypes>
                <CFBundleDocumentTypes>
                    <CFBundleTypeName>AnotherDocumentType</CFBundleTypeName>
                    <CFBundleTypeRole>Editor</CFBundleTypeRole>
                    <CFBundleTypeExtensions>
                        <string>x</string>
                        <string>y</string>
                    </CFBundleTypeExtensions>
                </CFBundleDocumentTypes>
            </CFBundleDocumentTypes>
            ...
        </plist>
    </configuration>

Yes, the CFBundleDocumentTypes has to be entered twice: First as the parent for additional configurations and then for each configuration you want to define.

DMG configuration

The following other properties can be added to the dmg element configuring the generation of the DMG file at the end of the build:

Key Type Required? Default Description
generate Boolean No false Whether or not to create a DMG archive.
additionalResources List<Fileset> No List of additional files to be copied into the archive.
createApplicationsSymlink Boolean No true Whether or not to include a link to the Applications folder inside the archive.
useGenIsoImage Boolean No false Whether or not to use genisoimage to create the archive. Default is hdiutil.
autoFallback Boolean No false If true, try the other archive generation method when the first one fails. (e.g. run hdiutil when genisoimage fails and vice-versa)
appendVersion Boolean No true If true, append the version to the .dmg name
dmgFileName String No null If not null and not empty, the supplied string will be used as the file name (.dmg will be appended).

APP configuration

The following other properties can be added to the app element configuring additional files to be included in the app bundle:

Key Type Required? Default Description
additionalResources List<Fileset> No Additional files to be copied into the app bundle.
...
    <configuration>
        <app>
            <additionalResources>
                <resource>
                    <directory>${project.basedir}/src/main/resources</directory>
                    <outputDirectory>Contents/Resources</outputDirectory>
                    <includes>
                        <include>**</include>
                    </includes>
                </resource>
            </additionalResources>
        </app>
    </configuration>
...

Code signing

The plugin can automatically sign the created application bundle if a codesign identiy is given:

...
    <configuration>
        <codesign>
            <identity>3rd Party Mac Developer Application: MyName (MyNumber)</identity>
        </codesign>
    </configuration>
...

The following other properties can be added to the codesign element configuring additional options for signing:

Key Type Required? Default Description
enable Boolean No true Whether or not to sign the created application bundle.
identity String Yes The identity of the signer. Required if the codesign element is present.
preserveMetadata List<String> No entitlements

To sign the application using a local dummy identity (which will only work on the machine where the signing was performed) you can use:

...
    <configuration>
        <codesign>
            <identity>-</identity>
        </codesign>
    </configuration>
...

JDK inclusion

Usually the application bundle built by the plugin will depend upon a Java runtime being available on the machine where the application is executed. To be completely self-sustaining, the plugin supports including the runtime into the target application. That runtime will then be used to launch the application, so there are no dependencies to a JDK being installed locally.

...
    <configuration>
        <jdk>
            <include>true</include>
            <location>/where/your/jdk/is/installed</location>
        </jdk>
    </configuration>
...

The following parameters can be set below the jdk configuration element:

Key Type Required? Default Description
include Boolean No false Whether or not to include the JDK in the generated application bundle.
location String No The location of the JDK to be included. If no location is provided then the currently used JDK (which is the JDK that is used by the Maven binary) will be added to the application.

Dependencies exclusion

By default all declared dependencies (both direct dependencies as well as transient dependencies) are included in the generated application bundle.

If you only want to include the direct application JAR file without any dependencies (e.g. because you've already included the dependencies into the application JAR itself) then you can set the includeDependencies flag of the app configuration to false:

...
    <configuration>
        <app>
            <includeDependencies>false</includeDependencies>
        </app>
    </configuration>
...

Native binary selection

By default the launcher contains a universal binary that allows running the application on both the classic x86_64 as well as the new arm64 architecture.

In case any problems occur with the universal binary (or if you want to support only a specific architecture) you can select which binary should be bundled with your application via the nativeBinary setting:

 ...
    <configuration>
        <nativeBinary>X86_64</nativeBinary>
    </configuration>
 ...

The available values are:

  • UNIVERSAL (the default if no explicit value is given)
  • X86_64
  • ARM_64

Development

Changes are documented in the CHANGELOG.md file.

The project consists of two main parts: The regular Maven plugin (written in Java) and the native macOS launcher (written in Objective C).

Building the native part is fully integrated into the Maven lifecycle, so all you need to do to build the plugin is:

$ git clone https://github.com/perdian/macosappbundler-maven-plugin.git
$ mvn clean install

I am aware that my understanding of Objective C is very basic - I'm not an Objective C developer by heart and going back to using pointers and (somewhat) manual memory management feels pretty strange. So a lot of what's in the code is highly cargo culted from tutorials and answers on Stackoverflow, but hey: It works!

Authors

Donate

See also the list of contributors who participated in this project.

License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.

Acknowledgments

I originally used and have been highly influenced by the appbundle-maven-plugin from federkasten. Unfortunately the plugin stopped working with Java versions 10 and above (and didn't provide support for Java 9+ module projects).