Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb28de4d5e | ||
|
|
eb754d12c3 |
+20
-43
@@ -26,14 +26,6 @@ vars:
|
||||
esac
|
||||
fi
|
||||
|
||||
env:
|
||||
# Desktop builds default to the smallest useful backend surface unless a task
|
||||
# explicitly opts into extra features or login.
|
||||
DISABLE_ADDITIONAL_FEATURES: '{{.DISABLE_ADDITIONAL_FEATURES | default "true"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default "false"}}'
|
||||
# Reuse the previously built backend JAR unless a clean rebuild is requested.
|
||||
JLINK_REUSE_CACHE: '{{.JLINK_REUSE_CACHE | default "true"}}'
|
||||
|
||||
tasks:
|
||||
prepare:
|
||||
desc: "Prepare desktop build dependencies"
|
||||
@@ -58,19 +50,6 @@ tasks:
|
||||
cmds:
|
||||
- npx tauri dev --no-watch
|
||||
|
||||
dev:login:
|
||||
desc: "Start Tauri desktop dev mode with login enabled and cache reuse"
|
||||
deps:
|
||||
- task: prepare
|
||||
vars:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
ignore_error: true
|
||||
dir: editor
|
||||
env:
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
cmds:
|
||||
- npx tauri dev --no-watch
|
||||
|
||||
build:
|
||||
desc: "Build Tauri desktop app (production)"
|
||||
deps: [prepare]
|
||||
@@ -127,7 +106,7 @@ tasks:
|
||||
|
||||
jlink:
|
||||
desc: "Build backend JAR and create JLink runtime for Tauri"
|
||||
deps: [jlink:cacheremove, jlink:jar, jlink:runtime]
|
||||
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
|
||||
@@ -136,40 +115,27 @@ tasks:
|
||||
- task: jlink:verify
|
||||
|
||||
jlink:verify:
|
||||
desc: "Verify the bundled JRE exists and matches the app JAR's required Java version"
|
||||
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
|
||||
|
||||
jlink:cacheremove:
|
||||
desc: "Remove cached backend JARs before a clean desktop rebuild"
|
||||
dir: ..
|
||||
if: '{{eq .JLINK_REUSE_CACHE "false"}}'
|
||||
cmds:
|
||||
- cmd: rm -rf app/core/build/libs
|
||||
- 'echo "rm -rf app/core/build/libs"'
|
||||
- task: jlink:clean
|
||||
- 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)"
|
||||
run: once
|
||||
dir: ..
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: '{{.DISABLE_ADDITIONAL_FEATURES}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
cmds:
|
||||
- 'echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"'
|
||||
- cmd: 'cmd /c "gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}"'
|
||||
- echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"
|
||||
- cmd: cmd /c gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
platforms: [windows]
|
||||
- cmd: './gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}'
|
||||
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
platforms: [linux, darwin]
|
||||
- mkdir -p frontend/editor/src-tauri/libs
|
||||
- cmd: cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
|
||||
platforms: [linux, darwin]
|
||||
- cmd: powershell -NoProfile -ExecutionPolicy Bypass -Command "New-Item -ItemType Directory -Force -Path 'frontend/editor/src-tauri/libs' | Out-Null; Copy-Item -Force (Get-ChildItem 'app/core/build/libs/stirling-pdf-*.jar' | Select-Object -First 1) 'frontend/editor/src-tauri/libs'"
|
||||
platforms: [windows]
|
||||
- cp app/core/build/libs/stirling-pdf-*.jar frontend/editor/src-tauri/libs/
|
||||
status:
|
||||
- test -f frontend/editor/src-tauri/libs/stirling-pdf-*.jar
|
||||
|
||||
@@ -187,7 +153,16 @@ tasks:
|
||||
# jdk.crypto.mscapi (the Windows certificate store / SunMSCAPI provider, used by
|
||||
# hardware-backed cert signing) is a Windows-only module - it only exists in a Windows
|
||||
# JDK's jmods, so it is added on Windows only or jlink fails to resolve it elsewhere.
|
||||
- cmd: powershell -NoProfile -ExecutionPolicy Bypass -File ../../../scripts/build-jlink-runtime.ps1 -Modules "{{.JLINK_MODULES}}"
|
||||
- cmd: |
|
||||
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}},jdk.crypto.mscapi \
|
||||
--strip-debug \
|
||||
--compress="$JLINK_COMPRESS" \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output runtime/jre
|
||||
platforms: [windows]
|
||||
- cmd: |
|
||||
JLINK="${JAVA_HOME:+$JAVA_HOME/bin/}jlink"
|
||||
@@ -219,6 +194,8 @@ tasks:
|
||||
# `desktop:build` run `jlink:clean` first to force a fresh build.
|
||||
- cmd: chmod -R u+w runtime/jre
|
||||
platforms: [linux, darwin]
|
||||
- cmd: powershell -NoProfile -Command "Get-ChildItem -Recurse runtime/jre | ForEach-Object { $_.IsReadOnly = $false }"
|
||||
platforms: [windows]
|
||||
status:
|
||||
- test -f runtime/jre/release
|
||||
|
||||
|
||||
@@ -80,6 +80,12 @@ tasks:
|
||||
OPEN: '{{.OPEN | default ""}}'
|
||||
env:
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
|
||||
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
|
||||
# full path, hostname, or user. Consumed at dev-serve time by vite.config
|
||||
# and dropped from production builds.
|
||||
STIRLING_DEV_LABEL:
|
||||
sh: basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cmds:
|
||||
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
|
||||
@@ -50,26 +50,6 @@ task desktop:dev
|
||||
|
||||
This ensures the JLink runtime and backend JAR exist (skipping if already built), then starts Tauri in dev mode.
|
||||
|
||||
For local login testing, use the login-enabled variant:
|
||||
|
||||
```bash
|
||||
task desktop:dev:login
|
||||
```
|
||||
|
||||
That task reuses the same desktop launcher, but it keeps the backend cache and enables the login flow through the desktop taskfile defaults.
|
||||
|
||||
You can override the desktop task defaults inline:
|
||||
|
||||
- `JLINK_REUSE_CACHE=false` - force a clean backend JAR/JRE rebuild before launch
|
||||
- `DISABLE_ADDITIONAL_FEATURES=false` - keep the full backend feature set for the bundled desktop run
|
||||
- `SECURITY_ENABLELOGIN=true` - enable the normal login flow in desktop mode
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
task desktop:dev:login JLINK_REUSE_CACHE=false DISABLE_ADDITIONAL_FEATURES=false SECURITY_ENABLELOGIN=true
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
@@ -98,10 +78,6 @@ task desktop:jlink:runtime # Create JLink custom JRE only
|
||||
task desktop:jlink:clean # Remove JLink artifacts
|
||||
```
|
||||
|
||||
`task desktop:jlink` verifies the bundled runtime after it is built. If the
|
||||
runtime release file is missing or too old, the verification script will
|
||||
rebuild `desktop:jlink:runtime` automatically and re-check the version.
|
||||
|
||||
### Clean
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,87 +9,24 @@
|
||||
//
|
||||
// Required major comes from REQUIRED_JAVA (wired from .taskfiles/desktop.yml,
|
||||
// which mirrors build.gradle `modernJavaVersion`). Keep them in sync.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const required = Number(process.env.REQUIRED_JAVA ?? "25");
|
||||
const releasePath = resolve(process.cwd(), "src-tauri/runtime/jre/release");
|
||||
|
||||
function rebuildRuntime(reason) {
|
||||
console.warn(
|
||||
`${reason} Rebuilding runtime with 'task desktop:jlink:runtime'.`,
|
||||
);
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
// Node does not reliably spawn the Task .cmd shim directly on Windows in
|
||||
// this repo environment, so use cmd.exe as the stable trampoline.
|
||||
const command = isWindows ? "cmd.exe" : "task";
|
||||
const args = isWindows
|
||||
? ["/d", "/s", "/c", "task desktop:jlink:runtime"]
|
||||
: ["desktop:jlink:runtime"];
|
||||
const result = spawnSync(command, args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(
|
||||
`FATAL: failed to launch 'task desktop:jlink:runtime': ${result.error.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(
|
||||
`FATAL: 'task desktop:jlink:runtime' exited with status ${result.status}.`,
|
||||
);
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
function readReleaseFile() {
|
||||
return readFileSync(releasePath, "utf8");
|
||||
}
|
||||
|
||||
function readReleaseOrFail() {
|
||||
try {
|
||||
return readReleaseFile();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`FATAL: cannot read bundled JRE release file at "${releasePath}" after rebuilding: ${err.message}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function parseMajor(raw) {
|
||||
// jlink writes release metadata as plain text, so we can validate the
|
||||
// embedded runtime without starting Java.
|
||||
const match = raw.match(/JAVA_VERSION="?(\d+)/);
|
||||
return match ? Number(match[1]) : 0;
|
||||
}
|
||||
const releasePath = process.argv[2] ?? "runtime/jre/release";
|
||||
|
||||
let raw;
|
||||
try {
|
||||
raw = readReleaseFile();
|
||||
raw = readFileSync(releasePath, "utf8");
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`WARN: cannot read bundled JRE release file at "${releasePath}": ${err.message}.`,
|
||||
`FATAL: cannot read bundled JRE release file at "${releasePath}": ${err.message}. ` +
|
||||
`Is the runtime built? Run 'task desktop:jlink'.`,
|
||||
);
|
||||
rebuildRuntime("Bundled runtime is missing.");
|
||||
raw = readReleaseOrFail();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let major = parseMajor(raw);
|
||||
if (!major || major < required) {
|
||||
// Rebuild stale or too-old runtimes automatically so desktop launches can
|
||||
// recover from a cached JRE instead of failing at app start.
|
||||
rebuildRuntime(
|
||||
`Bundled runtime/jre is Java ${major || "unknown"} but the app JAR requires Java ${required}.`,
|
||||
);
|
||||
raw = readReleaseOrFail();
|
||||
major = parseMajor(raw);
|
||||
}
|
||||
const match = raw.match(/JAVA_VERSION="?(\d+)/);
|
||||
const major = match ? Number(match[1]) : 0;
|
||||
|
||||
console.log(
|
||||
`Bundled JRE major: ${major || "unknown"} (required >= ${required})`,
|
||||
|
||||
@@ -54,18 +54,6 @@ fn check_backend_status() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allow desktop launches to opt into the normal login flow when either the
|
||||
/// new desktop-specific flag or the existing SECURITY_ENABLELOGIN flag is set.
|
||||
fn desktop_login_enabled_from_env() -> bool {
|
||||
matches!(
|
||||
std::env::var("STIRLING_PDF_DESKTOP_ENABLE_LOGIN")
|
||||
.or_else(|_| std::env::var("SECURITY_ENABLELOGIN"))
|
||||
.ok()
|
||||
.as_deref(),
|
||||
Some("true" | "TRUE" | "True" | "1" | "yes" | "YES" | "on" | "ON")
|
||||
)
|
||||
}
|
||||
|
||||
// Find the bundled JRE and return the java executable path
|
||||
fn find_bundled_jre(resource_dir: &PathBuf) -> Result<PathBuf, String> {
|
||||
let jre_dir = resource_dir.join("runtime").join("jre");
|
||||
@@ -225,11 +213,7 @@ fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &
|
||||
// No reverse proxy in front of the local sidecar, so don't trust forwarded headers.
|
||||
// Stops a LAN caller spoofing X-Forwarded-For to defeat the desktop-only signing gate.
|
||||
"-Dserver.forward-headers-strategy=none",
|
||||
if desktop_login_enabled_from_env() {
|
||||
"-Dsecurity.enableLogin=true"
|
||||
} else {
|
||||
"-Dsecurity.enableLogin=false"
|
||||
},
|
||||
"-Dsecurity.enableLogin=false", // Disable login for desktop mode
|
||||
"-Dsecurity.csrfDisabled=true", // Disable CSRF for desktop mode
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Prefixes the browser tab title with the current worktree name during local
|
||||
* development so multiple concurrently-running worktrees (e.g. wt1, wt2, spdf1)
|
||||
* are distinguishable at a glance instead of all showing "Stirling PDF".
|
||||
*
|
||||
* The label is injected as a build-time constant by vite.config, sourced from
|
||||
* the top-level dev tasks. It is an empty string in production builds, so this
|
||||
* whole feature compiles away to a no-op outside `vite` dev-serve.
|
||||
*/
|
||||
|
||||
const LABEL =
|
||||
typeof __DEV_WORKTREE_LABEL__ === "string" ? __DEV_WORKTREE_LABEL__ : "";
|
||||
|
||||
export function applyDevWorktreeLabel(): void {
|
||||
if (!LABEL || typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = `[${LABEL}] `;
|
||||
|
||||
const ensurePrefixed = () => {
|
||||
if (!document.title.startsWith(prefix)) {
|
||||
// The app rewrites document.title on route/tool changes; re-apply the
|
||||
// prefix on top of whatever the app just set.
|
||||
document.title = prefix + document.title;
|
||||
}
|
||||
};
|
||||
|
||||
ensurePrefixed();
|
||||
|
||||
const titleEl = document.querySelector("title");
|
||||
if (titleEl) {
|
||||
new MutationObserver(ensurePrefixed).observe(titleEl, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,12 @@ import { BrowserRouter } from "react-router-dom";
|
||||
import App from "@app/App";
|
||||
import "@app/i18n"; // Initialize i18next
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { applyDevWorktreeLabel } from "@app/utils/applyDevWorktreeLabel";
|
||||
|
||||
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
|
||||
|
||||
applyDevWorktreeLabel();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const scheduleCompilation = () => {
|
||||
if (typeof requestIdleCallback === "function") {
|
||||
|
||||
Vendored
+7
@@ -29,3 +29,10 @@ interface ImportMetaEnv {
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev-only worktree folder basename injected by vite.config at dev-serve time
|
||||
* (empty string in production builds). Used to prefix the browser tab title so
|
||||
* concurrent worktrees are distinguishable.
|
||||
*/
|
||||
declare const __DEV_WORKTREE_LABEL__: string;
|
||||
|
||||
@@ -182,7 +182,13 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
|
||||
prototypes: "./tsconfig.prototypes.vite.json",
|
||||
};
|
||||
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
export default defineConfig(async ({ mode, command }) => {
|
||||
// Dev-only browser-tab label (worktree folder basename) surfaced by the
|
||||
// top-level dev tasks so concurrent worktrees have distinguishable tabs.
|
||||
// Only injected during `vite` (dev serve) — never baked into a production
|
||||
// build — and carries only the folder name, no path/host/user info.
|
||||
const devWorktreeLabel =
|
||||
command === "serve" ? (process.env.STIRLING_DEV_LABEL ?? "") : "";
|
||||
// Load env files relative to this config (frontend/editor/), regardless of
|
||||
// where the build was invoked from. The previous `process.cwd()` worked when
|
||||
// this file lived at frontend/, but after the editor was moved under
|
||||
@@ -250,6 +256,9 @@ export default defineConfig(async ({ mode }) => {
|
||||
};
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_WORKTREE_LABEL__: JSON.stringify(devWorktreeLabel),
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
...(runSubpath ? [subpathBareRedirectPlugin(runSubpath)] : []),
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Modules
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$AllowedModules = @(
|
||||
"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"
|
||||
)
|
||||
|
||||
$RequestedModules = @($Modules -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
if ($RequestedModules.Count -eq 0) {
|
||||
throw "At least one jlink module is required."
|
||||
}
|
||||
|
||||
$InvalidModules = $RequestedModules | Where-Object {
|
||||
($_ -notmatch '^[A-Za-z0-9_.]+$') -or ($_ -notin $AllowedModules)
|
||||
}
|
||||
if ($InvalidModules) {
|
||||
throw "Unsupported jlink module(s): $($InvalidModules -join ', ')"
|
||||
}
|
||||
|
||||
$ModulesArg = $RequestedModules -join ","
|
||||
|
||||
$Jlink = if ($env:JAVA_HOME) {
|
||||
Join-Path $env:JAVA_HOME "bin/jlink.exe"
|
||||
} else {
|
||||
"jlink.exe"
|
||||
}
|
||||
|
||||
$HelpText = & $Jlink --help 2>&1 | Out-String
|
||||
# Older JDKs do not support the newer zip compressor selector, so fall back to
|
||||
# the numeric compression level they do understand.
|
||||
$Compress = if ($HelpText -match "zip-\[0-9\]") { "zip-6" } else { "2" }
|
||||
|
||||
& $Jlink `
|
||||
--add-modules "$ModulesArg,jdk.crypto.mscapi" `
|
||||
--strip-debug `
|
||||
--compress="$Compress" `
|
||||
--no-header-files `
|
||||
--no-man-pages `
|
||||
--output runtime/jre
|
||||
|
||||
# Tauri's resource staging preserves source permissions, so the bundled runtime
|
||||
# must be writable here or the next incremental build can fail on read-only JRE
|
||||
# files copied into the target directory.
|
||||
Get-ChildItem -Recurse runtime/jre -Force -File | ForEach-Object {
|
||||
$_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)
|
||||
}
|
||||
Reference in New Issue
Block a user