Gradle plugin - fakeivchenko/lwjwae GitHub Wiki

The lwjwae Gradle plugin, dev.ivchenko.lwjwae, sets a Java project up as an lwjwae application. One line in plugins {} replaces the dependencies, the JVM flags, the native image configuration, and the packaging scripts that an application would otherwise keep by hand.

What it does

  • Dependencies. lwjwae-core to compile against, the backend of the machine that runs the build at runtime, a codec from lwjwae-codecs when you ask for one, and the lwjwae-processor annotation processor for @BridgeType. The versions come with the plugin, so there is nothing to keep in sync.
  • The JVM. gradle run gets --enable-native-access=ALL-UNNAMED, even when the build script sets its own applicationDefaultJvmArgs.
  • The native image. The plugin applies the GraalVM Native Build Tools plugin and configures nativeCompile: native access, -Os, a capped heap, the resources of the project, and the name of the executable.
  • Windows. A GUI executable that opens no console, with the icon and a version block for the properties dialog, compiled by rc.exe.
  • macOS. An Info.plist in the executable, with the bundle identifier that the processes of WebKit need.
  • The page. A frontend in frontend/, such as a Vite project, installed and built with npm into the resources, and served with hot reload for ./gradlew runDev.
  • Packages. A .deb, an Arch Linux package, and an AppImage on Linux, an .app bundle and a .dmg on macOS, and an .msi installer on Windows.
  • Signing. The Windows executable and installer signed with signtool, and the macOS bundle and disk image signed with codesign and notarized by Apple.
  • Links and files. The schemes of links and the types of files of the application, registered in every package.
  • Updates. The keys, the release directory, and the signed manifest that application.updater() reads.

Apply the plugin

The plugin and the library are published to https://repo.ivchenko.dev/releases. In settings.gradle.kts:

pluginManagement {
    repositories {
        maven("https://repo.ivchenko.dev/releases")
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven("https://repo.ivchenko.dev/releases")
    }
}

In build.gradle.kts:

plugins {
    application
    id("dev.ivchenko.lwjwae").version("0.3.1")
}

application {
    mainClass = "com.example.Main"
}

That is a whole build script: ./gradlew run starts the application, and ./gradlew nativeCompile builds the executable.

The short form

Most applications need a name, a codec, and packages. Each has a one-line form in the lwjwae {} block:

lwjwae {
    displayName = "My app"
    vendor = "Example <[email protected]>"
    jackson()
    packaging.all()
}
Line What it does
displayName = "My app" The name that the menus, the taskbar, the Dock, and the installers show.
vendor = "Example <[email protected]>" The company on Windows, and the maintainer of the Debian package.
jackson() Adds the Jackson codec. gson(), jsonb(), and jsonb("group:artifact:version") pick another one.
packaging.all() Turns on every package format. deb(), arch(), appImage(), dmg(), and msi() turn on one.

The icon needs no line: the plugin takes src/main/icons/app.png when the project has it. Every short form sets a property that you can also set on its own, next to every other setting in the Gradle plugin configuration.

Types that cross the bridge

bind(name, Class, handler), emit(name, Object), and typed events need a codec, and the types that they carry need reflection metadata in a native image. Mark the outermost ones with @BridgeType:

@BridgeType
public record Outline(String name, List<Point> points) {}

The lwjwae-processor annotation processor, which the plugin adds, writes the metadata for them and for every type that they hold. The plugin adds it from lwjwae 0.8.0, the first release that has it. With an older version, write the metadata yourself in src/main/resources/META-INF/native-image/GROUP/NAME/reachability-metadata.json.

Build the page with npm

A project with frontend/package.json needs no configuration. The plugin finds it there and:

  1. Runs npm ci in installFrontend, or npm install without a lock file.
  2. Runs npm run build in buildFrontend. The task is up to date, and comes from the build cache, while the files of the frontend don't change.
  3. Puts frontend/dist into the resources under app/ in frontendResources, so run, jar, nativeCompile, and every package carry the page. window.loadResource("app/index.html") finds it.

./gradlew runDev starts npm run dev, waits for the development server, and runs the application against it with hot reload. When the window closes, the task stops the server and every process that it started. The output of the server goes to the build log, marked [dev].

npm comes from the PATH. The directory, the scripts, and the URL of the server are settings of frontend {}; see Gradle plugin configuration. For what the application does with the URL, see Handle links and page loads.

Package the application

./gradlew packageAll

packageAll builds the formats that are turned on and that the current operating system can make, and puts them in build/lwjwae/dist:

On You get Needs
Linux my-app_1.0.0_amd64.deb: the executable in /usr/bin, a menu entry, the icon; depends on the GTK and WebKitGTK packages Nothing
Linux my-app-1.0.0-1-x86_64.pkg.tar.xz: for pacman -U on Arch Linux and the distributions based on it, with the GTK and WebKitGTK packages as dependencies Nothing
Linux my-app-1.0.0-x86_64.AppImage: one file that runs on any Linux with WebKitGTK appimagetool and the AppImage runtime, pinned releases that the plugin downloads once into the Gradle cache and checks by SHA-256
macOS My app.app and My app-1.0.0.dmg hdiutil, part of macOS; codesign when macos.signingIdentity is set, and notarytool and stapler of Xcode for notarization
Windows my-app-1.0.0.msi: installs for the current user without administrator rights, with a Start menu shortcut and an uninstaller The WiX Toolset, which the plugin installs once into the Gradle cache with dotnet tool install, so the .NET SDK; signtool of the Windows SDK when a certificate is set

The plugin writes the .deb and the Arch package itself, so it needs neither dpkg nor makepkg. The AppImage carries the executable, the menu entry, and the icon, but not WebKitGTK: the file stays small, and pages render with the WebKit of the system, which gets its security updates from the distribution. The MSI replaces an older installation of the same application; its upgrade code comes from the group and the name of the project, and must stay the same for the life of the application.

On Linux, the icon is installed under the name GROUP.NAME, such as com.example.my-app, so that an icon theme with a generic icon of the same name as the executable can't replace it.

Sign the packages

Without a signature, Windows warns about an unknown publisher, and macOS opens the application only through the context menu. Signing is off until the build script names a certificate or an identity.

On Windows, give signtool a certificate from the store of the user by its SHA-1 thumbprint, or a .pfx file:

lwjwae {
    windows {
        signing {
            certificateThumbprint = "0123456789ABCDEF0123456789ABCDEF01234567"
            // or: certificateFile = file("certificate.pfx")
        }
    }
}

signWindowsExecutable signs a copy of the executable, and packageMsi packs the signed copy and signs the .msi too. The signature uses SHA-256 and a timestamp, so it outlives the certificate. The password of a .pfx file comes from the Gradle property lwjwae.windowsCertificatePassword or the LWJWAE_WINDOWS_CERTIFICATE_PASSWORD environment variable, and never from the build script.

On macOS, give codesign an identity of your keychain, and the notary service of Apple a way in:

lwjwae {
    macos {
        signingIdentity = "Developer ID Application: Example (TEAMID)"
        notarization {
            keychainProfile = "notary"
        }
    }
}

packageApp and packageDmg sign with the hardened runtime, which notarization requires. notarytool submit --wait sends the bundle and the disk image to Apple and waits for the verdict, and stapler staples the ticket to them, so Gatekeeper opens them without a warning and without a network. Instead of a profile that xcrun notarytool store-credentials keeps in the keychain, notarization {} takes an API key of App Store Connect, which suits CI, or an Apple ID with an app-specific password from LWJWAE_NOTARY_PASSWORD. For an application that needs an entitlement under the hardened runtime, such as the camera, set entitlements = file("entitlements.plist").

Register links and files

urlScheme("notes") and fileType("note") register a scheme of links and a type of files in every package, and the application hears of each through Application.onOpen. See Open your own links and files.

Publish updates

updates { manifestUrl; publicKey } gives the application its updater. generateUpdateKeys makes the keys once, packageUpdate puts the package of each platform into the release directory, and updateManifest writes the manifest over them and signs it. See Update the application.

Ship one JAR file for every platform

A native image is built for one platform, but a JAR file can carry every backend:

lwjwae {
    backends = dev.ivchenko.lwjwae.gradle.Backends.ALL
}

Each backend checks the operating system and its libraries before it loads anything, so the ones that don't apply step aside. Where both WebKitGTK versions are installed, the GTK 3 backend wins.

Tasks

Task Runs on Output
generateWindowsIcon Any platform, when there is an icon build/lwjwae/windows/app.ico
generateWindowsResourceScript Any platform build/lwjwae/windows/app.rc
compileWindowsResources Windows, before nativeCompile build/lwjwae/windows/app.res
signWindowsExecutable Windows, when a certificate is set build/lwjwae/windows/signed/*.exe
generateInfoPlist Any platform; before nativeCompile on macOS build/lwjwae/macos/Info.plist
installFrontend Any platform, with a frontend frontend/node_modules
buildFrontend Any platform, with a frontend frontend/dist
frontendResources Any platform, with a frontend; before the resources The page under app/ among the resources
runDev Any platform, with a frontend The application against the development server
packageDeb Linux build/lwjwae/dist/*.deb
packageArch Linux build/lwjwae/dist/*.pkg.tar.xz
packageAppImage Linux build/lwjwae/dist/*.AppImage
packageApp Any platform; signs on macOS only build/lwjwae/dist/*.app
packageDmg macOS build/lwjwae/dist/*.dmg
packageMsi Windows build/lwjwae/dist/*.msi
packageAll The formats that are turned on, on the current platform All of the above that apply
generateUpdateKeys Any platform A new pair of keys, printed
generateUpdateProperties Any platform, with updates {}; before the resources META-INF/lwjwae/update.properties among the resources
packageUpdate Any platform, with updates {} build/lwjwae/update/PLATFORM.msi, .zip, or .AppImage
updateManifest Any platform, with updates {} build/lwjwae/update/manifest.json and manifest.json.sig

Each package* task runs when you call it, whether its format is turned on or not.

Use lwjwae without the plugin

To declare the modules yourself, add the library, a backend for each platform that you want to run on, and the JVM flag:

plugins {
    application
}

repositories {
    mavenCentral()
    maven("https://repo.ivchenko.dev/releases")
}

dependencies {
    implementation("dev.ivchenko.lwjwae:lwjwae-core:0.11.0")
    runtimeOnly("dev.ivchenko.lwjwae:lwjwae-gtk:0.11.0")
    runtimeOnly("dev.ivchenko.lwjwae:lwjwae-windows:0.11.0")
    runtimeOnly("dev.ivchenko.lwjwae:lwjwae-macos:0.11.0")
}

application {
    mainClass = "com.example.Main"
    applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}

For a native image, apply org.graalvm.buildtools.native and add --enable-native-access=ALL-UNNAMED to its buildArgs. List the pages of the application in src/main/resources/META-INF/native-image/GROUP/NAME/reachability-metadata.json, such as {"resources": [{"glob": "app/**"}]}; the lwjwae modules ship the metadata of their own native calls.

⚠️ **GitHub.com Fallback** ⚠️