fix desktop bundles (#6773)

# Description of Changes

Changes
- Use 127.0.0.1 instead of localhost for the local backend. The bundled
backend starts on a random port and binds the IPv4 wildcard, but the
frontend health-checked http://localhost:{port}. On macOS (and some
Linux) localhost resolves to IPv6 ::1 first, so the connection is
refused and every backend-dependent tool shows "backend offline" even
though the backend started fine. Switched getBackendUrl() and the
health-check URL to the 127.0.0.1 loopback literal (already in the Tauri
HTTP capability allowlist, and what the OAuth loopback server already
uses). Client-side tools were unaffected, which matches the reports.
- Fail the desktop build when the bundled JRE is older than the app JAR.
The app JAR is compiled for Java 25, but the bundle could ship an older
runtime/jre (jlink:runtime short-circuits on an existing runtime, and
nothing checked its version), producing UnsupportedClassVersionError at
launch so the backend never starts. Added a jlink:verify task that reads
the jlink release file and fails the build if the bundled JRE major is
below REQUIRED_JAVA (25, kept in sync with build.gradle
modernJavaVersion). It runs after the runtime is staged - including the
short-circuit reuse path that lets a stale JRE slip through.
Cross-platform Node script, no new dependencies.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
This commit is contained in:
Anthony Stirling
2026-06-24 15:12:48 +00:00
committed by GitHub
parent f7f7b8790e
commit 5be9a0e1df
4 changed files with 75 additions and 6 deletions
+25 -2
View File
@@ -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" \
@@ -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);
}
@@ -308,7 +308,7 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
let mut port_guard = BACKEND_PORT.lock().unwrap();
*port_guard = Some(port);
add_log(format!("🎉 Backend started on port: {}", port));
add_log(format!("🔌 Navigate to: http://localhost:{}/", port));
add_log(format!("🔌 Navigate to: http://127.0.0.1:{}/", port));
}
}
@@ -43,7 +43,11 @@ export class TauriBackendService {
}
getBackendUrl(): string | null {
return this.backendPort ? `http://localhost:${this.backendPort}` : null;
// Use the IPv4 loopback literal, not "localhost": on macOS (and some Linux)
// "localhost" can resolve to IPv6 ::1 first, but the bundled backend binds
// the IPv4 wildcard, so a ::1 connection is refused and every tool shows
// "backend offline" even though the backend is up.
return this.backendPort ? `http://127.0.0.1:${this.backendPort}` : null;
}
subscribeToStatus(listener: (status: BackendStatus) => 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<boolean> {
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}`,
);