Skip to content

Commit c39bce5

Browse files
committed
Add custom ruff/ty binary path support
Closes #408 Users in offline environments cannot use the extension's managed ruff/ty language servers because `uv pip install` tries to download packages from the internet. This adds a 3-tier binary resolution strategy so these users can point to locally-installed binaries instead. Binary resolution now follows this priority for both ruff and ty: 1. User-configured path via `marimo.ruff.path` / `marimo.ty.path` 2. Companion extension discovery — checks the `charliermarsh.ruff` or `astral-sh.ty` extension's configured path setting, then its bundled binary at `<extensionPath>/bundled/libs/bin/<binary>` 3. Existing `uv pip install` fallback (unchanged behavior) Each candidate binary is validated by running `<binary> --version` and checking the result meets the minimum version requirement (ruff >= 0.15.0, ty >= 0.0.15). If validation fails at any tier, resolution falls through to the next.
1 parent ebeea42 commit c39bce5

File tree

9 files changed

+415
-61
lines changed

9 files changed

+415
-61
lines changed

extension/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ icon in the editor title bar to open it as a notebook (see image above).
6464
| --------------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6565
| `marimo.lsp.path` | `array` | `[]` | Path to a custom `marimo-lsp` executable, e.g., `["/path/to/marimo-lsp"]`. Leave empty to use the bundled version via `uvx`. |
6666
| `marimo.uv.path` | `string` | | Path to the `uv` binary, e.g., `/Users/me/.local/bin/uv`. Leave empty to use `uv` from the system PATH. |
67+
| `marimo.ruff.path` | `string` | | Path to a custom `ruff` binary, e.g., `/usr/local/bin/ruff`. Useful for offline environments. Leave empty to auto-discover or install via uv. |
68+
| `marimo.ty.path` | `string` | | Path to a custom `ty` binary, e.g., `/usr/local/bin/ty`. Useful for offline environments. Leave empty to auto-discover or install via uv. |
6769
| `marimo.disableUvIntegration` | `boolean` | `false` | Disable uv integration features such as automatic package installation prompts. |
6870
| `marimo.disableManagedLanguageFeatures` | `boolean` | `false` | Disable marimo's managed Python language features (completions, diagnostics, formatting). When enabled, notebook cells use the standard `python` language ID and rely on external language servers. |
6971
| `marimo.telemetry` | `boolean` | `true` | Anonymous usage data. This helps us prioritize features for the marimo VSCode extension. |

extension/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@
9494
"default": true,
9595
"description": "Anonymous usage data. This helps us prioritize features for the marimo VSCode extension."
9696
},
97+
"marimo.ruff.path": {
98+
"type": "string",
99+
"default": "",
100+
"markdownDescription": "Path to a custom `ruff` binary, e.g., `/usr/local/bin/ruff`. Useful for offline environments. Leave empty to auto-discover or install via uv.",
101+
"scope": "resource"
102+
},
103+
"marimo.ty.path": {
104+
"type": "string",
105+
"default": "",
106+
"markdownDescription": "Path to a custom `ty` binary, e.g., `/usr/local/bin/ty`. Useful for offline environments. Leave empty to auto-discover or install via uv.",
107+
"scope": "resource"
108+
},
97109
"marimo.disableManagedLanguageFeatures": {
98110
"type": "boolean",
99111
"default": false,

extension/src/services/Config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export class Config extends Effect.Service<Config>()("Config", {
1818
path: Effect.succeed(Option.none<string>()),
1919
enabled: Effect.succeed(false),
2020
},
21+
ruff: {
22+
path: Effect.succeed(Option.none<string>()),
23+
},
24+
ty: {
25+
path: Effect.succeed(Option.none<string>()),
26+
},
2127
lsp: {
2228
executable: Effect.succeed(Option.none()),
2329
},
@@ -45,6 +51,28 @@ export class Config extends Effect.Service<Config>()("Config", {
4551
);
4652
},
4753
},
54+
ruff: {
55+
get path() {
56+
return Effect.map(
57+
code.value.workspace.getConfiguration("marimo.ruff"),
58+
(config) =>
59+
Option.fromNullable(config.get<string>("path")).pipe(
60+
Option.filter((p) => p.length > 0),
61+
),
62+
);
63+
},
64+
},
65+
ty: {
66+
get path() {
67+
return Effect.map(
68+
code.value.workspace.getConfiguration("marimo.ty"),
69+
(config) =>
70+
Option.fromNullable(config.get<string>("path")).pipe(
71+
Option.filter((p) => p.length > 0),
72+
),
73+
);
74+
},
75+
},
4876
lsp: {
4977
get executable() {
5078
return Effect.gen(function* () {

extension/src/services/Uv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,6 @@ const handleUvNotInstalled = Effect.fn("handleUvNotInstalled")(function* (
687687
return yield* Effect.die(error);
688688
});
689689

690-
function resolvePlatformBinaryName(name: "uv" | "ruff" | "ty") {
690+
export function resolvePlatformBinaryName(name: "uv" | "ruff" | "ty") {
691691
return NodeProcess.platform === "win32" ? `${name}.exe` : name;
692692
}

extension/src/services/completions/RuffLanguageServer.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as NodePath from "node:path";
2+
13
import {
24
type Cause,
35
Data,
@@ -11,6 +13,7 @@ import {
1113
import type * as vscode from "vscode";
1214
import * as lsp from "vscode-languageclient/node";
1315

16+
import { validateBinary } from "../../utils/binaryResolution.ts";
1417
import {
1518
createManagedLanguageClient,
1619
type ManagedLanguageClient,
@@ -19,7 +22,8 @@ import { showErrorAndPromptLogs } from "../../utils/showErrorAndPromptLogs.ts";
1922
import { Config } from "../Config.ts";
2023
import { OutputChannel } from "../OutputChannel.ts";
2124
import { Sentry } from "../Sentry.ts";
22-
import { Uv } from "../Uv.ts";
25+
import { ExtensionContext } from "../Storage.ts";
26+
import { resolvePlatformBinaryName, Uv } from "../Uv.ts";
2327
import { VariablesService } from "../variables/VariablesService.ts";
2428
import { VsCode } from "../VsCode.ts";
2529
import {
@@ -91,14 +95,16 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
9195
return;
9296
}
9397

94-
// Phase 1: Install the language server
98+
// Phase 1: Resolve the ruff binary using 3-tier strategy
9599
yield* Effect.logInfo("Starting language server").pipe(
96100
Effect.annotateLogs({
97101
server: RUFF_SERVER.name,
98102
version: RUFF_SERVER.version,
99103
}),
100104
);
101105

106+
const binaryPath = yield* resolveRuffBinary();
107+
102108
// Create isolated sync instance with its own cell count tracking.
103109
const notebookSync = yield* sync.forClient();
104110

@@ -116,7 +122,7 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
116122
});
117123

118124
const installExit = yield* Effect.exit(
119-
createManagedLanguageClient(RUFF_SERVER, {
125+
createManagedLanguageClient(RUFF_SERVER, binaryPath, {
120126
notebookSync,
121127
clientOptions: {
122128
outputChannelName: "marimo (ruff)",
@@ -224,6 +230,85 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
224230
},
225231
) {}
226232

233+
/**
234+
* Resolves the ruff binary path using a 3-tier strategy:
235+
* 1. User-configured path (marimo.ruff.path)
236+
* 2. Companion extension discovery (charliermarsh.ruff bundled binary or ruff.path setting)
237+
* 3. Fallback to uv installation
238+
*/
239+
const resolveRuffBinary = Effect.fn(function* () {
240+
const code = yield* VsCode;
241+
const config = yield* Config;
242+
const uv = yield* Uv;
243+
const context = yield* ExtensionContext;
244+
245+
// Tier 1: User-configured path
246+
const userPath = yield* config.ruff.path;
247+
if (Option.isSome(userPath)) {
248+
const validated = yield* validateBinary(
249+
userPath.value,
250+
RUFF_SERVER.version,
251+
);
252+
if (Option.isSome(validated)) {
253+
yield* Effect.logInfo(
254+
`Using user-configured ruff binary: ${validated.value}`,
255+
);
256+
return validated.value;
257+
}
258+
yield* Effect.logWarning(
259+
`User-configured ruff path "${userPath.value}" is invalid, falling back to discovery`,
260+
);
261+
}
262+
263+
// Tier 2: Companion extension discovery
264+
const ruffExtension = code.extensions.getExtension(RUFF_EXTENSION_ID);
265+
if (Option.isSome(ruffExtension)) {
266+
// First check the companion extension's ruff.path setting
267+
const ruffExtConfig = yield* code.workspace.getConfiguration("ruff");
268+
const extConfiguredPath = Option.fromNullable(
269+
ruffExtConfig.get<string>("path"),
270+
).pipe(Option.filter((p) => p.length > 0));
271+
272+
if (Option.isSome(extConfiguredPath)) {
273+
const validated = yield* validateBinary(
274+
extConfiguredPath.value,
275+
RUFF_SERVER.version,
276+
);
277+
if (Option.isSome(validated)) {
278+
yield* Effect.logInfo(
279+
`Using ruff binary from ruff.path setting: ${validated.value}`,
280+
);
281+
return validated.value;
282+
}
283+
}
284+
285+
// Then check bundled binary in the extension's install directory
286+
const bundledPath = NodePath.join(
287+
ruffExtension.value.extensionPath,
288+
"bundled",
289+
"libs",
290+
"bin",
291+
resolvePlatformBinaryName("ruff"),
292+
);
293+
const validated = yield* validateBinary(bundledPath, RUFF_SERVER.version);
294+
if (Option.isSome(validated)) {
295+
yield* Effect.logInfo(
296+
`Using bundled ruff binary from ${RUFF_EXTENSION_ID}: ${validated.value}`,
297+
);
298+
return validated.value;
299+
}
300+
}
301+
302+
// Tier 3: Fallback to uv installation
303+
yield* Effect.logInfo(
304+
"No custom or companion ruff binary found, falling back to uv installation",
305+
);
306+
const targetPath = NodePath.resolve(context.globalStorageUri.fsPath, "libs");
307+
return yield* uv.ensureLanguageServerBinaryInstalled(RUFF_SERVER, {
308+
targetPath,
309+
});
310+
});
311+
227312
/**
228313
* Read ruff.* settings from a WorkspaceConfiguration and return them in the format
229314
* expected by the Ruff language server's initializationOptions.

0 commit comments

Comments
 (0)