From c906641e2ca8a677ab38a68ab052666d0c83c1c1 Mon Sep 17 00:00:00 2001 From: Astontur Date: Mon, 2 Mar 2026 09:25:25 +0100 Subject: [PATCH] feat: add jpackage native installer task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Gradle jpackage task that creates a standalone desktop app with a bundled JRE — recipients don't need Java installed. Includes auto-cleanup of previous output on rebuild. Also adds a VS Code task and updates the README with build instructions. --- .vscode/tasks.json | 8 ++++++ MazeGame/build.gradle | 65 +++++++++++++++++++++++++++++++++++++++++++ README.md | 16 +++++++++++ 3 files changed, 89 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a68af00..f6ab43d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -45,6 +45,14 @@ "windows": { "command": ".\\gradlew.bat browserJar; Copy-Item build\\libs\\MazeGame-browser-1.0.0.jar ..\\docs\\MazeGame.jar -Force" } + }, + { + "label": "Build Native Installer", + "type": "shell", + "command": "./gradlew jpackage", + "options": { "cwd": "${workspaceFolder}/MazeGame" }, + "problemMatcher": ["$javac"], + "detail": "Create standalone .exe with bundled JRE (no Java needed)" } ] } diff --git a/MazeGame/build.gradle b/MazeGame/build.gradle index a9f9b91..05bff3e 100644 --- a/MazeGame/build.gradle +++ b/MazeGame/build.gradle @@ -108,6 +108,71 @@ tasks.register('browserJar', Jar) { destinationDirectory = layout.buildDirectory.dir('libs') } +// --- Native installer (jpackage) --- +// Creates a platform-specific installer that bundles a private JRE. +// Recipients do NOT need Java installed. +// Usage: +// ./gradlew jpackage → app-image (folder with .exe / .app) +// ./gradlew jpackage -PinstallerType=msi → MSI installer (Windows, needs WiX 3) +// ./gradlew jpackage -PinstallerType=exe → EXE installer (Windows, needs WiX 3) +// ./gradlew jpackage -PinstallerType=dmg → DMG disk image (macOS) +// ./gradlew jpackage -PinstallerType=deb → DEB package (Linux) +tasks.register('jpackage') { + description = 'Create a native installer that bundles a private JRE' + dependsOn 'installDist' + doLast { + // Resolve jpackage from the toolchain JDK + def javaInstallDir = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + }.get().metadata.installationPath.asFile + def isWindows = System.getProperty('os.name').toLowerCase().contains('win') + def jpackageExec = new File(javaInstallDir, "bin/jpackage${isWindows ? '.exe' : ''}") + + def installLib = layout.buildDirectory.dir('install/MazeGame/lib').get().asFile + def outputDir = layout.buildDirectory.dir('installers').get().asFile + + // Clean previous output so jpackage doesn't fail on existing directory + def prevOutput = new File(outputDir, 'WesleysWayOut') + if (prevOutput.exists()) { + prevOutput.deleteDir() + } + outputDir.mkdirs() + + def type = project.findProperty('installerType') ?: 'app-image' + + // Use the favicon.ico from docs/ as the Windows icon + def iconFile = file("${rootProject.projectDir}/docs/favicon.ico") + + def args = [ + jpackageExec.path, + '--type', type, + '--name', 'WesleysWayOut', + '--app-version', project.version.toString(), + '--input', installLib.path, + '--main-jar', "MazeGame-${project.version}.jar".toString(), + '--main-class', 'mazegame.Start', + '--dest', outputDir.path, + '--description', 'Wesley\'s Way Out - a maze puzzle game', + '--vendor', 'Wesley' + ] + if (isWindows && iconFile.exists()) { + args.add('--icon') + args.add(iconFile.path) + } + + def pb = new ProcessBuilder(args.collect { it.toString() }) + pb.inheritIO() + def process = pb.start() + def exitCode = process.waitFor() + if (exitCode != 0) { + throw new GradleException("jpackage failed with exit code ${exitCode}") + } + + println "" + println "Installer created in: ${outputDir.path}" + } +} + // --- Local browser testing --- // Builds the browser JAR, copies it to docs/, and starts a local HTTP server. // Usage: ./gradlew runBrowser diff --git a/README.md b/README.md index 4559019..2ca0964 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ cd MazeGame | `gradlew jar` | Build the JAR (output: `build/libs/MazeGame-.jar`) | | `gradlew browserJar` | Build Java-8 JAR for CheerpJ browser play | | `gradlew runBrowser` | Build browser JAR, copy to `docs/`, and start local HTTP server | +| `gradlew jpackage` | Create a native app image with bundled JRE (no Java needed to run) | | `gradlew clean` | Delete all build artifacts | ## Running the JAR Directly @@ -146,6 +147,21 @@ cd MazeGame ./gradlew run ``` +### Native Installer (jpackage) + +Create a standalone desktop app that bundles a private JRE — recipients **don't need Java installed**. + +```bash +# Build an app image (folder with .exe / .app) +./gradlew jpackage +# Output: build/installers/WesleysWayOut/ + +# Build a Windows MSI installer (requires WiX Toolset 3.x) +./gradlew jpackage -PinstallerType=msi +``` + +The task recompiles sources automatically, so the output always reflects the latest code. + ### Browser Build (CheerpJ) The game runs in the browser via [CheerpJ 3.0](https://cheerpj.com/), which requires Java 8 bytecode.