From 6358fb450fdcb098e4f04c80b843108805354458 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sat, 10 Oct 2026 19:26:33 +0200 Subject: [PATCH 1/2] Ship the native binary in the VS Code extension The platform-specific VS Code package now carries the language server as the native binary from ADR 0016 instead of a jlink Java runtime plus JAR (ADR 0017). The extension starts the binary directly. The JAR stays as a fallback: a configured oft.java.path runs it with that Java, and a package built without a binary runs it with java from the PATH as before. The package shrinks from 24 MB to 13 MB. CI hands the smoke-tested native build artifact to the VS Code job, so a package for every platform can be built without jlink on that platform. The jlink script is gone; ADR 0011 stays in force for the IntelliJ plugin only. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 8 +- .github/workflows/release.yml | 7 ++ README.md | 14 ++- ...java-runtimes-instead-of-a-native-image.md | 4 +- ...nary-for-editors-without-a-java-runtime.md | 2 +- ...-native-binary-in-the-vs-code-extension.md | 41 +++++++ .../lsp/intellij/OftLspServerDescriptor.kt | 1 + vscode-extension/.gitignore | 1 - vscode-extension/README.md | 6 +- vscode-extension/package.json | 6 +- vscode-extension/scripts/build-runtime.js | 82 -------------- .../scripts/copy-server-binary.js | 91 +++++++++++++++ vscode-extension/scripts/package.js | 24 ++-- vscode-extension/src/extension.ts | 104 +++++++++++------- 14 files changed, 242 insertions(+), 149 deletions(-) create mode 100644 doc/decisions/0017-ship-the-native-binary-in-the-vs-code-extension.md delete mode 100644 vscode-extension/scripts/build-runtime.js create mode 100644 vscode-extension/scripts/copy-server-binary.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb6ec11..e3df5bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: vscode-extension: name: Build VS Code extension (${{ matrix.target }}) runs-on: ${{ matrix.os }} - needs: server + needs: [server, native-binary] strategy: fail-fast: false matrix: @@ -99,6 +99,12 @@ jobs: - name: Package server JAR run: mvn --batch-mode -DskipTests package + - name: Download the native server binary for this platform + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: native-binary-${{ matrix.target }} + path: vscode-extension/server + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 269db18..9da30d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,7 @@ jobs: vscode-packages: name: Build VS Code extension (${{ matrix.target }}) runs-on: ${{ matrix.os }} + needs: native-binaries strategy: fail-fast: true matrix: @@ -49,6 +50,12 @@ jobs: - name: Build standalone server JAR run: mvn --batch-mode -DskipTests package + - name: Download the native server binary for this platform + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: native-binary-${{ matrix.target }} + path: vscode-extension/server + - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: diff --git a/README.md b/README.md index 21d46d7..5352d5a 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ or from a downloaded package: 1. Download the `.vsix` for your platform from the [releases](https://github.com/fgorke/openfasttrace-language-server/releases/) page. 2. Open the Extensions view, click the `···` menu and choose **Install from VSIX…**, pick the file. -Each package carries its own Java runtime, so the file name ends in the platform, -for example `-win32-x64` or `-darwin-arm64`. +Each package carries the language server as a native binary for its platform, so the file +name ends in the platform, for example `-win32-x64` or `-darwin-arm64`. No Java is needed. ### Vim, Neovim and other editors @@ -185,15 +185,17 @@ cd intellij-plugin ### VS Code extension ```bash -mvn package +mvn -Pnative -DskipTests package # native binary and JAR; `mvn package` for the JAR alone cd vscode-extension npm install npm run watch # then press F5 in VS Code to launch an extension host -npm run package # .vsix for the current platform +npm run package # .vsix for the current platform, with the native binary ``` -`jlink` only builds a runtime for the platform it runs on, so a package built here -carries a runtime for this machine only. The releases are built per platform in CI. +The package carries the native binary from `target/`, which Native Image builds for this +machine only. A package for another platform needs that platform's binary dropped into +`vscode-extension/server/`, which is what CI does with its build artifacts: +`npm run package -- linux-arm64`. Without a binary, F5 runs the JAR with `java` from the `PATH`. diff --git a/doc/decisions/0011-bundle-java-runtimes-instead-of-a-native-image.md b/doc/decisions/0011-bundle-java-runtimes-instead-of-a-native-image.md index 1485da7..7b1a127 100644 --- a/doc/decisions/0011-bundle-java-runtimes-instead-of-a-native-image.md +++ b/doc/decisions/0011-bundle-java-runtimes-instead-of-a-native-image.md @@ -1,5 +1,5 @@ --- -status: accepted +status: superseded by [ADR-0017](0017-ship-the-native-binary-in-the-vs-code-extension.md) for the VS Code extension, in force for the IntelliJ plugin date: 2026-08-07 decision-makers: Felix Gorke --- @@ -44,4 +44,4 @@ The extension prefers a configured `oft.java.path`, then the bundled runtime, th ## More Information -[ADR 0016](0016-provide-a-native-binary-for-editors-without-a-java-runtime.md) revisits the GraalVM option for editors that bring no runtime of their own. This decision stays in force for the IntelliJ plugin and the VS Code extension. +[ADR 0016](0016-provide-a-native-binary-for-editors-without-a-java-runtime.md) revisits the GraalVM option for editors that bring no runtime of their own, and [ADR 0017](0017-ship-the-native-binary-in-the-vs-code-extension.md) moves the VS Code extension onto that binary. This decision stays in force for the IntelliJ plugin. diff --git a/doc/decisions/0016-provide-a-native-binary-for-editors-without-a-java-runtime.md b/doc/decisions/0016-provide-a-native-binary-for-editors-without-a-java-runtime.md index 7bac3a4..93375a9 100644 --- a/doc/decisions/0016-provide-a-native-binary-for-editors-without-a-java-runtime.md +++ b/doc/decisions/0016-provide-a-native-binary-for-editors-without-a-java-runtime.md @@ -35,7 +35,7 @@ A jlink bundle would have been five archives of 24 MB each that still need an un * Good, because the binary runs on any x86-64 or AArch64 machine, not only on CPUs as new as the build machine (`-march=compatibility`). * Bad, because the release carries five more artifacts and each takes minutes to build. * Bad, because a reflection gap shows only at run time. The smoke test in CI exists for that. -* Neutral, because the IntelliJ plugin and the VS Code extension keep the approach of ADR 0011. The VS Code extension could switch to the binary later and shrink from 24 MB to the size of the binary. +* Neutral, because the IntelliJ plugin keeps the approach of ADR 0011. The VS Code extension switched to the binary in [ADR 0017](0017-ship-the-native-binary-in-the-vs-code-extension.md). ### Confirmation diff --git a/doc/decisions/0017-ship-the-native-binary-in-the-vs-code-extension.md b/doc/decisions/0017-ship-the-native-binary-in-the-vs-code-extension.md new file mode 100644 index 0000000..0bf9274 --- /dev/null +++ b/doc/decisions/0017-ship-the-native-binary-in-the-vs-code-extension.md @@ -0,0 +1,41 @@ +--- +status: accepted +date: 2026-10-10 +decision-makers: Oliver Kopp +--- + +# Ship the Native Binary in the VS Code Extension +`adr~ship-the-native-binary-in-the-vs-code-extension~1` + +Needs: impl + +## Context and Problem Statement + +[ADR 0011](0011-bundle-java-runtimes-instead-of-a-native-image.md) gave the VS Code extension a jlink runtime per platform, which grew the package from 1.7 MB to 24 MB and tied every package to a CI runner that can run jlink for it. [ADR 0016](0016-provide-a-native-binary-for-editors-without-a-java-runtime.md) then produced a native binary of the server for every platform the extension is released for. Carrying both a Java runtime and a binary that needs none makes no sense, so which one does the extension ship? + +## Considered Options + +* Keep the jlink runtime and the JAR +* Ship the native binary, keep the JAR as fallback +* Ship the native binary only +* Download the binary on first start, as the [JabRef extension](https://github.com/JabRef/lsp-vscode-extension) does with its server + +## Decision Outcome + +Chosen option: **ship the native binary, keep the JAR as fallback**. + +The platform-specific package carries `server/openfasttrace-language-server` (`.exe` on Windows) with a marker file naming its platform, and the extension starts it directly. The 1.8 MB JAR stays in the package for two cases: a configured `oft.java.path`, which now means "run the JAR with this Java instead of the binary", and a package built without a binary, which runs the JAR with `java` from the `PATH` as before. + +Downloading on first start was rejected again for the reasons of ADR 0011: it needs a host and a cache, and the first start depends on the network. + +### Consequences + +* Good, because the package shrinks from 24 MB to 13 MB and starts the server in milliseconds. +* Good, because a package for any platform can be built on any machine once that platform's binary is at hand, which is how CI does it from the native build artifacts. jlink could only build for the machine it ran on. +* Good, because the VS Code extension and the editors of ADR 0016 run the same binary, so the CI smoke test covers both. +* Bad, because the VS Code package now waits for the native build in CI. +* Neutral, because the IntelliJ plugin keeps running the JAR on the IDE's runtime (ADR 0011), as the IDE always brings one. + +### Confirmation + +`scripts/package.js` fails when no binary for the target exists. CI downloads the binary that the native job built and smoke tested, and packages it for each of the five platforms. diff --git a/intellij-plugin/src/main/kotlin/org/itsallcode/openfasttrace/lsp/intellij/OftLspServerDescriptor.kt b/intellij-plugin/src/main/kotlin/org/itsallcode/openfasttrace/lsp/intellij/OftLspServerDescriptor.kt index 2ccdae6..daeb527 100644 --- a/intellij-plugin/src/main/kotlin/org/itsallcode/openfasttrace/lsp/intellij/OftLspServerDescriptor.kt +++ b/intellij-plugin/src/main/kotlin/org/itsallcode/openfasttrace/lsp/intellij/OftLspServerDescriptor.kt @@ -67,6 +67,7 @@ internal class OftLspServerDescriptor(project: Project) : return GeneralCommandLine(java, "-jar", jarPath) } + // [impl->adr~bundle-java-runtimes-instead-of-a-native-image~1] private fun javaExecutable(): String { val launcher = Path.of(System.getProperty("java.home"), "bin", if (SystemInfo.isWindows) "java.exe" else "java") diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index 3bec90b..4ebed26 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -2,4 +2,3 @@ out/ node_modules/ server/ *.vsix -runtime/ diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 933d93a..55c1550 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -22,10 +22,10 @@ The server starts automatically when a supported file is opened. No configuratio ## Requirements -Every released package bundles a Java runtime, so nothing else needs to be installed. Packages exist for `win32-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` and `darwin-arm64`. +Every released package carries the language server as a native binary, so nothing else needs to be installed. Packages exist for `win32-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` and `darwin-arm64`. -On a platform outside that list, the extension falls back to Java 25 or later on the `PATH`. Setting `oft.java.path` always takes precedence over the bundled runtime. +On a platform outside that list, the extension runs the server JAR with Java 25 or later from the `PATH`. Setting `oft.java.path` always runs the JAR with that Java instead of the bundled binary. ## Settings -* `oft.java.path`: path to the `java` executable used to run the language server. Defaults to `java` on `PATH`. +* `oft.java.path`: path to a `java` executable. When set, the language server JAR runs with it instead of the bundled native binary. Empty by default. diff --git a/vscode-extension/package.json b/vscode-extension/package.json index c8680f2..2c6dfa1 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -24,7 +24,7 @@ "precompile": "npm run copy-server-jar", "prewatch": "npm run copy-server-jar", "copy-server-jar": "node scripts/copy-server-jar.js", - "build-runtime": "node scripts/build-runtime.js", + "copy-server-binary": "node scripts/copy-server-binary.js", "package": "node scripts/package.js", "compile": "tsc -p ./", "watch": "tsc -watch -p ./" @@ -44,8 +44,8 @@ "properties": { "oft.java.path": { "type": "string", - "default": "java", - "description": "Path to the java executable used to run the language server. Defaults to 'java' on PATH." + "default": "", + "description": "Path to a java executable. When set, the extension runs the language server JAR with it instead of the bundled native binary. Leave empty to use the bundled binary, or 'java' from the PATH in a build without one." } } }, diff --git a/vscode-extension/scripts/build-runtime.js b/vscode-extension/scripts/build-runtime.js deleted file mode 100644 index 85ac461..0000000 --- a/vscode-extension/scripts/build-runtime.js +++ /dev/null @@ -1,82 +0,0 @@ -// Builds a minimal platform-specific Java runtime with jlink and places it in runtime/, so the extension -// can run the language server without a JDK installed on the machine. - -// [impl->adr~bundle-java-runtimes-instead-of-a-native-image~1] -const { execFileSync, spawnSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); - -const destDir = path.join(__dirname, "..", "runtime"); -const markerFile = path.join(destDir, "oft-runtime.json"); - -const REQUIRED_MODULES = ["java.base", "java.management", "java.naming", "java.sql"]; - -function javaHome() { - if (process.env.JAVA_HOME) { - return process.env.JAVA_HOME; - } - const fromPath = javaHomeFromPath(); - if (fromPath !== undefined) { - return fromPath; - } - throw new Error( - "Could not locate a JDK. Set JAVA_HOME, or put a JDK (not just a JRE) on the PATH." - ); -} - -function javaHomeFromPath() { - const result = spawnSync("java", ["-XshowSettings:properties", "-version"], { - encoding: "utf8", - }); - if (result.error !== undefined) { - return undefined; - } - const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; - return /java\.home = (.+)/.exec(output)?.[1].trim(); -} - -function toolPath(name) { - const exe = process.platform === "win32" ? `${name}.exe` : name; - const candidate = path.join(javaHome(), "bin", exe); - if (!fs.existsSync(candidate)) { - throw new Error(`Could not find ${exe} in ${path.join(javaHome(), "bin")}`); - } - return candidate; -} - -function runtimeTarget() { - const platform = process.platform === "win32" ? "win32" : process.platform; - const arch = process.arch; - return `${platform}-${arch}`; -} - -function runJlink(compression) { - fs.rmSync(destDir, { recursive: true, force: true }); - execFileSync(toolPath("jlink"), [ - "--add-modules", REQUIRED_MODULES.join(","), - "--strip-debug", - "--no-header-files", - "--no-man-pages", - "--compress", compression, - "--output", destDir, - ], { stdio: "inherit" }); -} - -function build() { - try { - runJlink("zip-6"); - } catch { - runJlink("2"); - } - - const launcher = path.join(destDir, "bin", process.platform === "win32" ? "java.exe" : "java"); - if (!fs.existsSync(launcher)) { - throw new Error(`jlink produced no launcher at ${launcher}`); - } - - fs.writeFileSync(markerFile, JSON.stringify({ target: runtimeTarget() }, null, 2)); - - console.log(`Bundled Java runtime for ${runtimeTarget()}: ${destDir}`); -} - -build(); diff --git a/vscode-extension/scripts/copy-server-binary.js b/vscode-extension/scripts/copy-server-binary.js new file mode 100644 index 0000000..a37ca35 --- /dev/null +++ b/vscode-extension/scripts/copy-server-binary.js @@ -0,0 +1,91 @@ +// Puts the native language server binary for a target platform into server/, so the extension +// starts the server without any Java installation. +// +// Usage: node scripts/copy-server-binary.js [target] (target defaults to this machine) +// +// The binary is taken from the first of: +// 1. server/openfasttrace-language-server--[.exe]: a CI build artifact or a +// release download, which is how packages for other platforms are built. +// 2. ../target/openfasttrace-language-server[.exe]: the output of `mvn -Pnative package`, +// accepted only for this machine's target since Native Image builds for the build host. +// 3. server/openfasttrace-language-server[.exe] left by an earlier run for the same target. +// +// [impl->adr~ship-the-native-binary-in-the-vs-code-extension~1] +const fs = require("fs"); +const path = require("path"); + +const serverDir = path.join(__dirname, "..", "server"); +const mavenTargetDir = path.join(__dirname, "..", "..", "target"); +const markerFile = path.join(serverDir, "oft-server.json"); + +function currentTarget() { + const platform = process.platform === "win32" ? "win32" : process.platform; + return `${platform}-${process.arch}`; +} + +function binaryName(target) { + return target.startsWith("win32") ? "openfasttrace-language-server.exe" : "openfasttrace-language-server"; +} + +function artifactInServerDir(target) { + if (!fs.existsSync(serverDir)) { + return undefined; + } + const suffix = target.startsWith("win32") ? `-${target}.exe` : `-${target}`; + const pattern = new RegExp(`^openfasttrace-language-server-.+${suffix.replace(/[.]/g, "\.")}$`); + const matches = fs.readdirSync(serverDir).filter((name) => pattern.test(name)); + if (matches.length === 0) { + return undefined; + } + return matches + .map((name) => ({ fullPath: path.join(serverDir, name), mtime: fs.statSync(path.join(serverDir, name)).mtimeMs })) + .sort((a, b) => b.mtime - a.mtime)[0].fullPath; +} + +function binaryInMavenTarget(target) { + if (target !== currentTarget()) { + return undefined; + } + const candidate = path.join(mavenTargetDir, binaryName(target)); + return fs.existsSync(candidate) ? candidate : undefined; +} + +function binaryAlreadyInPlace(target) { + const candidate = path.join(serverDir, binaryName(target)); + if (!fs.existsSync(candidate) || !fs.existsSync(markerFile)) { + return undefined; + } + try { + const marker = JSON.parse(fs.readFileSync(markerFile, "utf8")); + return marker.target === target ? candidate : undefined; + } catch { + return undefined; + } +} + +const target = process.argv[2] ?? currentTarget(); +const source = artifactInServerDir(target) ?? binaryInMavenTarget(target) ?? binaryAlreadyInPlace(target); +if (source === undefined) { + console.error( + `No native server binary for ${target} found.\n` + + `Either run 'mvn -Pnative -DskipTests package' in the project root (builds for ${currentTarget()}),\n` + + `or put openfasttrace-language-server--${target}${target.startsWith("win32") ? ".exe" : ""} into ${serverDir}.` + ); + process.exit(1); +} + +fs.mkdirSync(serverDir, { recursive: true }); +const destination = path.join(serverDir, binaryName(target)); +if (path.resolve(source) === path.resolve(destination)) { + // already in place +} else if (path.dirname(path.resolve(source)) === path.resolve(serverDir)) { + // An artifact dropped into server/ is renamed, so the package does not carry the binary twice. + fs.renameSync(source, destination); +} else { + fs.copyFileSync(source, destination); +} +if (!target.startsWith("win32")) { + fs.chmodSync(destination, 0o755); +} +fs.writeFileSync(markerFile, JSON.stringify({ target }, null, 2)); +console.log(`Bundled native server for ${target}: ${source} -> ${destination}`); diff --git a/vscode-extension/scripts/package.js b/vscode-extension/scripts/package.js index a1140b1..5b51e24 100644 --- a/vscode-extension/scripts/package.js +++ b/vscode-extension/scripts/package.js @@ -1,6 +1,12 @@ -// Builds a VSIX for the platform this runs on, with the Java runtime bundled. - -const { execFileSync, spawnSync } = require("child_process"); +// Builds a platform-specific VSIX that carries the language server as a native binary. +// +// Usage: node scripts/package.js [target] (target defaults to this machine) +// +// The binary comes from scripts/copy-server-binary.js. The server JAR is bundled as well, as the +// fallback for a configured `oft.java.path`. + +const { spawnSync } = require("child_process"); +const fs = require("fs"); const path = require("path"); const SUPPORTED_TARGETS = [ @@ -41,18 +47,14 @@ const target = process.argv[2] ?? currentTarget(); if (!SUPPORTED_TARGETS.includes(target)) { console.error( `Unsupported target '${target}'. Supported: ${SUPPORTED_TARGETS.join(", ")}.\n` + - "Run 'npx vsce package' without a runtime to build a package that needs Java on the PATH." + "Run 'npx vsce package' to build a package without a native binary that needs Java on the PATH." ); process.exit(1); } -if (target !== currentTarget()) { - console.error( - `Cannot build for '${target}' on a ${currentTarget()} machine.` - ); - process.exit(1); -} +// Left over from builds before the native binary replaced the bundled Java runtime. +fs.rmSync(path.join(__dirname, "..", "runtime"), { recursive: true, force: true }); console.log(`Building VS Code extension for ${target}`); -run([path.join(__dirname, "build-runtime.js")]); +run([path.join(__dirname, "copy-server-binary.js"), target]); run([vsceEntryPoint(), "package", "--target", target]); diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 297df40..b0cceb1 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -40,24 +40,33 @@ const SUPPORTED_EXTENSIONS = [ ]; const BUNDLED_JAR_NAME = "openfasttrace-language-server.jar"; +const BUNDLED_BINARY_NAME = + process.platform === "win32" ? "openfasttrace-language-server.exe" : "openfasttrace-language-server"; +const BUNDLED_SERVER_MARKER = "oft-server.json"; + +/** How the language server process is started. */ +interface ServerLaunch { + command: string; + args: string[]; + /** Where the command came from, for the error message when it does not start. */ + source: "bundled-binary" | "configured-java" | "path-java"; +} let client: LanguageClient | undefined; export async function activate(context: vscode.ExtensionContext): Promise { - const jarPath = resolveServerJar(context); - if (jarPath === undefined) { + const launch = resolveServerLaunch(context); + if (launch === undefined) { vscode.window.showErrorMessage( - "OpenFastTrace: no server JAR found. Run 'mvn package' in the project root " + - "and recompile this extension." + "OpenFastTrace: this extension carries no language server. Run 'mvn -Pnative package' " + + "(native binary) or 'mvn package' (JAR) in the project root and recompile the extension." ); return; } - const javaPath = resolveJavaExecutable(context); - const serverOptions: ServerOptions = { - command: javaPath, - args: ["-jar", jarPath], + command: launch.command, + args: launch.args, transport: TransportKind.stdio, }; @@ -81,21 +90,20 @@ export async function activate(context: vscode.ExtensionContext): Promise try { await client.start(); } catch (error) { - vscode.window.showErrorMessage(startupErrorMessage(javaPath, error)); + vscode.window.showErrorMessage(startupErrorMessage(launch, error)); } } -function startupErrorMessage(javaPath: string, error: unknown): string { - const usesPathJava = javaPath === "java"; +function startupErrorMessage(launch: ServerLaunch, error: unknown): string { const isMissingExecutable = String(error).includes("ENOENT"); - if (usesPathJava && isMissingExecutable) { + if (launch.source === "path-java" && isMissingExecutable) { return ( - "OpenFastTrace: no Java found. This build of the extension does not bundle a Java " + - "runtime, so it needs Java 25 or later on the PATH, or a path configured in " + - "'oft.java.path'. Installing the platform-specific build of this extension avoids that." + "OpenFastTrace: no Java found. This build of the extension bundles no native language " + + "server for this platform, so it needs Java 25 or later on the PATH, or a path configured " + + "in 'oft.java.path'. The platform-specific builds of this extension need no Java." ); } - return `OpenFastTrace: failed to start language server using '${javaPath}': ${error}`; + return `OpenFastTrace: failed to start language server using '${launch.command}': ${error}`; } export function deactivate(): Thenable | undefined { @@ -214,43 +222,61 @@ function showRenderedReport(uri: vscode.Uri): void { panel.webview.html = fs.readFileSync(uri.fsPath, "utf8"); } +/** + * Picks how to start the server, in this order: + * 1. the JAR with the Java configured in `oft.java.path`, when both exist, + * 2. the native binary bundled with the platform-specific package, + * 3. the JAR with `java` from the PATH, for a package built without a binary. + */ +// [impl->adr~ship-the-native-binary-in-the-vs-code-extension~1] +function resolveServerLaunch(context: vscode.ExtensionContext): ServerLaunch | undefined { + const jarPath = resolveServerJar(context); + const configuredJava = configuredJavaExecutable(); + if (configuredJava !== undefined && jarPath !== undefined) { + return { command: configuredJava, args: ["-jar", jarPath], source: "configured-java" }; + } + if (configuredJava !== undefined) { + console.warn("'oft.java.path' is set but this extension carries no server JAR, using the bundled binary."); + } + + const binary = bundledServerBinary(context); + if (binary !== undefined) { + return { command: binary, args: [], source: "bundled-binary" }; + } + + if (jarPath !== undefined) { + return { command: "java", args: ["-jar", jarPath], source: "path-java" }; + } + return undefined; +} + function resolveServerJar(context: vscode.ExtensionContext): string | undefined { const bundledJar = path.join(context.extensionPath, "server", BUNDLED_JAR_NAME); return fs.existsSync(bundledJar) ? bundledJar : undefined; } -function resolveJavaExecutable(context: vscode.ExtensionContext): string { +function configuredJavaExecutable(): string | undefined { const setting = vscode.workspace.getConfiguration("oft").inspect("java.path"); const configured = setting?.workspaceFolderValue ?? setting?.workspaceValue ?? setting?.globalValue; if (configured !== undefined && configured.trim() !== "") { return configured; } - - const bundled = bundledJavaExecutable(context); - if (bundled !== undefined) { - return bundled; - } - - return "java"; + return undefined; } -function bundledJavaExecutable(context: vscode.ExtensionContext): string | undefined { - const runtimeDir = path.join(context.extensionPath, "runtime"); - const launcher = path.join( - runtimeDir, - "bin", - process.platform === "win32" ? "java.exe" : "java" - ); - if (!fs.existsSync(launcher)) { +function bundledServerBinary(context: vscode.ExtensionContext): string | undefined { + const serverDir = path.join(context.extensionPath, "server"); + const binary = path.join(serverDir, BUNDLED_BINARY_NAME); + if (!fs.existsSync(binary)) { return undefined; } - if (!matchesCurrentPlatform(runtimeDir)) { - console.warn("Bundled Java runtime was built for a different platform, ignoring it."); + if (!matchesCurrentPlatform(serverDir)) { + console.warn("Bundled language server was built for a different platform, ignoring it."); return undefined; } - ensureExecutable(launcher); - return launcher; + ensureExecutable(binary); + return binary; } function ensureExecutable(file: string): void { @@ -263,13 +289,13 @@ function ensureExecutable(file: string): void { try { fs.chmodSync(file, 0o755); } catch (error) { - console.warn(`Could not make the bundled Java runtime executable: ${error}`); + console.warn(`Could not make the bundled language server executable: ${error}`); } } } -function matchesCurrentPlatform(runtimeDir: string): boolean { - const markerFile = path.join(runtimeDir, "oft-runtime.json"); +function matchesCurrentPlatform(serverDir: string): boolean { + const markerFile = path.join(serverDir, BUNDLED_SERVER_MARKER); if (!fs.existsSync(markerFile)) { return true; } From e7ec51880cf7ea14908a339766b4851b652e9f29 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sat, 10 Oct 2026 19:29:46 +0200 Subject: [PATCH 2/2] Keep runtime/ in the VS Code .gitignore A checkout of an older branch still produces the jlink runtime there, and it should not show up as untracked when switching back. Co-Authored-By: Claude Fable 5.1 --- vscode-extension/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index 4ebed26..3bec90b 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -2,3 +2,4 @@ out/ node_modules/ server/ *.vsix +runtime/