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.
-
Dependencies.
lwjwae-coreto compile against, the backend of the machine that runs the build at runtime, a codec from lwjwae-codecs when you ask for one, and thelwjwae-processorannotation processor for@BridgeType. The versions come with the plugin, so there is nothing to keep in sync. -
The JVM.
gradle rungets--enable-native-access=ALL-UNNAMED, even when the build script sets its ownapplicationDefaultJvmArgs. -
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.plistin 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.appbundle and a.dmgon macOS, and an.msiinstaller on Windows. -
Signing. The Windows executable and installer signed with
signtool, and the macOS bundle and disk image signed withcodesignand 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.
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.
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.
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.
A project with frontend/package.json needs no configuration. The plugin finds it there and:
- Runs
npm ciininstallFrontend, ornpm installwithout a lock file. - Runs
npm run buildinbuildFrontend. The task is up to date, and comes from the build cache, while the files of the frontend don't change. - Puts
frontend/distinto the resources underapp/infrontendResources, sorun,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.
./gradlew packageAllpackageAll 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.
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").
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.
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.
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.
| 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.
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.