Skip to content

A Gradle plugin to package stand-alone Java applications for multiple operating systems and architectures with 'jpackage'.

License

Notifications You must be signed in to change notification settings

gradlex-org/java-module-packaging

Repository files navigation

Java Module Packaging Gradle plugin

Build Status Gradle Plugin Portal

A Gradle plugin to package modular Java application as standalone bundles/installers for Windows, macOS and Linux with jpackage.

This GradleX plugin is maintained by me, Jendrik Johannes. I offer consulting and training for Gradle and/or the Java Module System - please reach out if you are interested. There is also my YouTube channel on Gradle topics.

If you have a suggestion or a question, please open an issue.

Java Modules with Gradle

If you plan to build Java Modules with Gradle, you should consider using these plugins on top of Gradle core:

In episodes 31, 32, 33 of Understanding Gradle I explain what these plugins do and why they are needed.

Full Java Module System Project Setup is a full-fledged Java Module System project setup using these plugins.

How to use?

Working example projects to inspect:

For general information about how to structure Gradle builds and apply community plugins like this one to all subprojects you can check out my Understanding Gradle video series.

Plugin dependency

Add this to the build file of your convention plugin's build (e.g. build-logic/build.gradle(.kts) or buildSrc/build.gradle(.kts)).

dependencies {
    implementation("org.gradlex:java-module-packaging:1.1")
}

Apply and use the plugin

In your convention plugin, apply the plugin and configure the targets.

plugins {
    id("org.gradlex.java-module-packaging")
}

javaModulePackaging {
    target("ubuntu-22.04") {
        operatingSystem = OperatingSystemFamily.LINUX
        architecture = MachineArchitecture.X86_64
    }
    target("macos-13") {
        operatingSystem = OperatingSystemFamily.MACOS
        architecture = MachineArchitecture.X86_64
    }
    target("macos-14") {
        operatingSystem = OperatingSystemFamily.MACOS
        architecture = MachineArchitecture.ARM64
    }
    target("windows-2022") {
        operatingSystem = OperatingSystemFamily.WINDOWS
        architecture = MachineArchitecture.X86_64
    }
}

You can now run target-specific builds:

./gradlew jpackageWindows
./gradlew runWindows

Or, for convenience, let the plugin pick the target fitting the machine you run on:

./gradlew jpackage
./gradlew run

There are some additional configuration options that can be used if needed. All options have a default. Only configure what you need in addition. For more information about the available options, consult the jpackage and jlink (for jlinkOptions) documentation.

javaModulePackaging {
  // global options
  applicationName = "app" // defaults to project name
  applicationVersion = "1.0" // defaults to project version
  applicationDescription = "Awesome App"
  vendor = "My Company" 
  copyright = "(c) My Company" 
  jlinkOptions.addAll("--no-header-files", "--no-man-pages", "--bind-services")
  addModules.addAll("additional.module.to.include")
  jpackageResources = layout.projectDirectory.dir("res") // defaults to 'src/main/resourcesPackage'
  resources.from(layout.projectDirectory.dir("extra-res"))
  verbose = false

  // target specific options
  targetsWithOs("windows") {
    options.addAll("--win-dir-chooser", "--win-shortcut", "--win-menu")
    appImageOptions.addAll("--win-console")
    targetResources.from("windows-res")
  }
  targetsWithOs("macos") {
    options.addAll("--mac-sign", "--mac-signing-key-user-name", "gradlex")
    singleStepPackaging = true
  }
}

Using target specific variants of libraries (like JavaFX)

The plugin uses Gradle's variant-aware dependency management to select target-specific Jars based on the configured targets. For this, such a library needs to be published with Gradle Module Metadata and contain the necessary information about the available target-specific Jars. If the metadata is missing or incomplete, you should use the org.gradlex.jvm-dependency-conflict-resolution plugin to add the missing information via addTargetPlatformVariant.

For example, for JavaFX it may look like this:

jvmDependencyConflicts.patch {
  listOf("base", "graphics", "controls").forEach { jfxModule ->
    module("org.openjfx:javafx-$jfxModule") {
      addTargetPlatformVariant("linux", OperatingSystemFamily.LINUX, MachineArchitecture.X86_64)
      addTargetPlatformVariant("linux-aarch64", OperatingSystemFamily.LINUX, MachineArchitecture.ARM64)
      addTargetPlatformVariant("mac", OperatingSystemFamily.MACOS, MachineArchitecture.X86_64)
      addTargetPlatformVariant("mac-aarch64", OperatingSystemFamily.MACOS, MachineArchitecture.ARM64)
      addTargetPlatformVariant("win", OperatingSystemFamily.WINDOWS, MachineArchitecture.X86_64)
    }
  }   
}

Testing against multiple targets

Warning

Currently, the following only works in combination with Blackbox Test Suites configured by the org.gradlex.java-module-testing plugin.

Tests run against the primary target, which is either the local machine you run the build on, or what is configured via javaModulePackaging.primaryTarget(...). If you want to run the test multiple times against each target you configured, you can configure this as follows:

javaModulePackaging {
    multiTargetTestSuite(testing.suites["test"])
}

Then, there will be a test task available for each target, such as testWindows-2022 or testMacos-14.

Running on GitHub Actions

Target-specific tasks such as assembleWindows-2022 or assembleMacos-14 only run on the fitting operating system and architecture. If you want to build your software for multiple targets and have GitHub actions available, you can use different runners to create packages for the different targets. A setup for this can look like this (assuming your targets are named: ubuntu-22.04, windows-2022, macos-13, macos-14):

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version-file: ./gradle/java-version.txt
      - uses: gradle/actions/setup-gradle@v3
      - run: "./gradlew check"

  package:
    needs: check
    strategy:
      matrix:
        os: [ubuntu-22.04, windows-2022, macos-13, macos-14]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version-file: ./gradle/java-version.txt
      - uses: gradle/actions/setup-gradle@v3
      - run: "./gradlew assemble${{ matrix.os }}"
      - uses: actions/upload-artifact@v4
        with:
          name: Application Package ${{ matrix.os }}
          path: build/app/packages/*/*

To avoid re-compilation of the Java code on each of the runners, you can run a Gradle remote cache node.

The java-module-system project is an example that uses GitHub actions with a Gradle remote build cache.

FAQ

How does the plugin interact with the jpackage command?

By default, dhe plugin calls jpackage in two steps:

  1. Build --type app-image as a package-type independent image folder. This is where jlink is involved.
  2. Build OS-specific packages via --type <package-type>. This may be called several times for the same target (e.g. exe and msi on Windows).

OS-independent options can be configured through the extension:

javaModulePackaging {
  applicationName = "app" // defaults to project name
  applicationVersion = "1.0" // defaults to project version
  applicationDescription = "Awesome App"
  vendor = "My Company" 
  copyright = "(c) My Company" 
  jlinkOptions.addAll("--no-header-files", "--no-man-pages", "--bind-services")
  addModules.addAll("additional.module.to.include")
  verbose = false
}

OS-specific options can be defined inside a target:

javaModulePackaging {
  target("windows-2022") { // address target by name
    options.addAll("--win-dir-chooser", "--win-shortcut", "--win-menu")
  }
  targetsWithOs("windows") { // all targets of for a certain os
    // ...
  }
}

You can tell the plugin to perform packaging in one step by setting the singleStepPackaging = true option on a target.

Disclaimer

Gradle and the Gradle logo are trademarks of Gradle, Inc. The GradleX project is not endorsed by, affiliated with, or associated with Gradle or Gradle, Inc. in any way.

About

A Gradle plugin to package stand-alone Java applications for multiple operating systems and architectures with 'jpackage'.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •  

Languages