Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.

</details>

Expand Down
Original file line number Diff line number Diff line change
@@ -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
---
Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions vscode-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 3 additions & 3 deletions vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./"
Expand All @@ -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."
}
}
},
Expand Down
82 changes: 0 additions & 82 deletions vscode-extension/scripts/build-runtime.js

This file was deleted.

91 changes: 91 additions & 0 deletions vscode-extension/scripts/copy-server-binary.js
Original file line number Diff line number Diff line change
@@ -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-<version>-<target>[.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-<version>-${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}`);
24 changes: 13 additions & 11 deletions vscode-extension/scripts/package.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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]);
Loading
Loading