diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml index 0b37df9f6f..239602beb5 100644 --- a/.taskfiles/desktop.yml +++ b/.taskfiles/desktop.yml @@ -5,6 +5,11 @@ vars: # NoClassDefFoundError: jdk/dynalink/Namespace at runtime in get-info-on-pdf and verify-pdf JLINK_MODULES: "java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported,jdk.dynalink" + # Minimum Java major the bundled JRE must be. Keep in sync with build.gradle + # `modernJavaVersion` - the app JAR is compiled for this, so an older runtime + # fails at launch with UnsupportedClassVersionError. Enforced by jlink:verify. + REQUIRED_JAVA: "25" + # Override via JPDFIUM_PLATFORMS env (csv of platform keys, or 'all'). JPDFIUM_PLATFORMS: sh: | @@ -102,6 +107,20 @@ tasks: jlink: desc: "Build backend JAR and create JLink runtime for Tauri" deps: [jlink:jar, jlink:runtime] + # Runs after the runtime is in place. Lives here (not in jlink:runtime's + # cmds) so it still fires when jlink:runtime short-circuits on its `status:` + # check and reuses an existing runtime/jre - that reuse path is exactly how + # a stale, too-old JRE slips through. + cmds: + - task: jlink:verify + + jlink:verify: + desc: "Fail the build if the bundled JRE is older than the app JAR requires" + dir: editor + env: + REQUIRED_JAVA: "{{.REQUIRED_JAVA}}" + cmds: + - node scripts/verify-bundled-jre.mjs src-tauri/runtime/jre/release jlink:jar: desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)" @@ -127,9 +146,13 @@ tasks: cmds: - rm -rf runtime/jre - mkdir -p runtime + # Pin jlink to JAVA_HOME so the bundled JRE matches the JDK the build + # uses. Bare `jlink` on PATH can resolve to an older system Java (the + # ubuntu runner ships Java 11), producing a runtime jlink:verify rejects. - | - JLINK_COMPRESS="$(jlink --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)" - jlink \ + JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink" + JLINK_COMPRESS="$("$JLINK" --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)" + "$JLINK" \ --add-modules {{.JLINK_MODULES}} \ --strip-debug \ --compress="$JLINK_COMPRESS" \ diff --git a/frontend/editor/scripts/verify-bundled-jre.mjs b/frontend/editor/scripts/verify-bundled-jre.mjs new file mode 100644 index 0000000000..b826a0292d --- /dev/null +++ b/frontend/editor/scripts/verify-bundled-jre.mjs @@ -0,0 +1,42 @@ +// Fail the desktop build if the bundled jlink runtime is older than the Java +// version the app JAR is compiled for. A too-old runtime ships happily today +// (the jlink task short-circuits on a stale runtime/jre, and nothing checks the +// version), then dies at launch with UnsupportedClassVersionError -> the +// backend never starts and every tool shows "backend offline". +// +// Reads JAVA_VERSION from the jlink `release` file (always present in a jlink +// output) so it needs no shell and behaves identically on Windows/macOS/Linux. +// +// Required major comes from REQUIRED_JAVA (wired from .taskfiles/desktop.yml, +// which mirrors build.gradle `modernJavaVersion`). Keep them in sync. +import { readFileSync } from "node:fs"; + +const required = Number(process.env.REQUIRED_JAVA ?? "25"); +const releasePath = process.argv[2] ?? "runtime/jre/release"; + +let raw; +try { + raw = readFileSync(releasePath, "utf8"); +} catch (err) { + console.error( + `FATAL: cannot read bundled JRE release file at "${releasePath}": ${err.message}. ` + + `Is the runtime built? Run 'task desktop:jlink'.`, + ); + process.exit(1); +} + +const match = raw.match(/JAVA_VERSION="?(\d+)/); +const major = match ? Number(match[1]) : 0; + +console.log( + `Bundled JRE major: ${major || "unknown"} (required >= ${required})`, +); + +if (!major || major < required) { + console.error( + `FATAL: bundled runtime/jre is Java ${major || "unknown"} but the app JAR requires ` + + `Java ${required}. Run 'task desktop:jlink:clean' and rebuild with JDK ${required} active ` + + `(check 'java -version' / JAVA_HOME).`, + ); + process.exit(1); +} diff --git a/frontend/editor/src-tauri/src/commands/backend.rs b/frontend/editor/src-tauri/src/commands/backend.rs index 8d5a9764f1..00dc1bb486 100644 --- a/frontend/editor/src-tauri/src/commands/backend.rs +++ b/frontend/editor/src-tauri/src/commands/backend.rs @@ -308,7 +308,7 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver void): () => void { @@ -227,7 +231,7 @@ export class TauriBackendService { }); } - /** Always checks the local bundled backend at localhost:{port}. */ + /** Always checks the local bundled backend at 127.0.0.1:{port}. */ async checkBackendHealth(): Promise { if (!this.backendStarted) { console.debug("[TauriBackendService] Health check: backend not started"); @@ -241,7 +245,7 @@ export class TauriBackendService { return false; } - const configUrl = `http://localhost:${this.backendPort}/api/v1/config/app-config`; + const configUrl = `http://127.0.0.1:${this.backendPort}/api/v1/config/app-config`; console.debug( `[TauriBackendService] Checking local backend health at: ${configUrl}`, );