From 79faa463811e62320f3ef4b71bac13f62f702315 Mon Sep 17 00:00:00 2001 From: Mucheen <145200703+Mucheen@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:02:55 +0800 Subject: [PATCH] feat: configure Cargo executable path and prepare 0.1.1 --- .github/workflows/release.yml | 4 ++-- README.md | 21 +++++++++++++--- .../backend/main.mjs | 2 +- .../backend/provider.mjs | 4 +++- io.github.lithe-idea.cargo-build/plugin.json | 12 ++++++++-- sdk.lock.json | 2 +- test/core.test.mjs | 4 +++- test/integration.test.mjs | 24 ++++++++++++++++--- test/provider.test.mjs | 1 + 9 files changed, 60 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 449ea18..efee75a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,9 +67,9 @@ jobs: exit 0 fi cat > "$RUNNER_TEMP/notes.md" <<'NOTES' - macOS Cargo integration for a Lithe preview with API 3 buildSystems, buildProviders and serverProcess support. + macOS Cargo integration for a Lithe preview with API 3 buildSystems 2, settings 2, buildProviders 1 and serverProcess 1 support. - Requires Cargo 1.70+ and Node.js 20.3+. Includes workspace/package build, check and test, clean, binary/example runs, Release, Features and Offline options. + Adds a native Cargo executable path setting: select a file or leave it empty for discovery. Metadata, builds, tests, run and clean share the selected executable; an invalid selection fails instead of falling back. Requires Cargo 1.70+ and Node.js 20.3+. Existing workspace/package tasks and Release, Features and Offline options remain available. Preview status: automated integration and contract checks pass; native installation and full UI acceptance are pending. Windows is not supported. Published ZIP bytes must not be replaced. NOTES diff --git a/README.md b/README.md index e27839d..c54cbd2 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ Cargo is a community build plugin for Lithe. It uses Lithe's native Cargo tool window and Build Output to build, check, test, clean, and run Rust projects. -**0.1.0 is a preview release.** Native disk-installation and full UI acceptance +**0.1.1 is a preview release.** Native disk-installation and full UI acceptance are still pending. Automated integration and host compatibility run in this repository's CI. -The plugin requires a Lithe preview with API 3 `buildSystems: 1`, +The plugin requires a Lithe preview with API 3 `buildSystems: 2`, `settings: 2`, `buildProviders: 1` and `serverProcess: 1`. Windows is not supported yet. ## Install @@ -15,13 +15,28 @@ Install Rust/Cargo 1.70+ and Node.js 20.3+ first. Node runs the metadata provide Cargo and rust-analyzer are not bundled. Existing Rust language intelligence continues to use Lithe's Rust language service. -Download `cargo-build-0.1.0-darwin.zip` from [Releases](https://github.com/Lithe-IDEA/lithe-plugin-cargo/releases). +Download `cargo-build-0.1.1-darwin.zip` from [Releases](https://github.com/Lithe-IDEA/lithe-plugin-cargo/releases). In Lithe, choose Settings → Plugins → Install from Disk, select the ZIP, review Cargo/Node and its permissions, and restart. Enable Cargo, open the Cargo workspace root, then open the Cargo tool window and load the project model. After the [Lithe Store](https://github.com/Lithe-IDEA/Lithe-store) entry is merged, compatible preview builds can also install it from the plugin marketplace. +## Cargo path + +Settings → Plugins → Cargo → Settings contains **Cargo 路径** with a native +file picker. It also appears in Build, Execution, Deployment → Cargo. Leave it +empty for automatic discovery, or select the Cargo executable (not rustc/rustup). +Saving reloads the model; metadata, build, check, test, run and clean use that +same executable. A missing, non-executable or too-old selection fails visibly +instead of silently choosing a different Cargo installation. The executable's +parent directory is placed first on PATH for companion tools. + +Version 0.1.1 requires a preview host with `buildSystems: 2` and `settings: 2`. +Its exact tested host revision is recorded in `sdk.lock.json`; older hosts keep +using the compatible 0.1.0 release through the Store. Existing published ZIPs +are immutable and are never replaced. + ## Features - Cargo supplies workspace members, explicit/automatic binaries, libraries, diff --git a/io.github.lithe-idea.cargo-build/backend/main.mjs b/io.github.lithe-idea.cargo-build/backend/main.mjs index 5d57eed..5184b39 100644 --- a/io.github.lithe-idea.cargo-build/backend/main.mjs +++ b/io.github.lithe-idea.cargo-build/backend/main.mjs @@ -2,6 +2,6 @@ import { createBackend, PluginError } from './vendor/lithe-plugin-sdk/index.mjs'; import { registerCargoProvider } from './provider.mjs'; -const backend = createBackend({ name: 'cargo-build', version: '0.1.0' }); +const backend = createBackend({ name: 'cargo-build', version: '0.1.1' }); registerCargoProvider(backend, PluginError); await backend.listen(); diff --git a/io.github.lithe-idea.cargo-build/backend/provider.mjs b/io.github.lithe-idea.cargo-build/backend/provider.mjs index d241769..a442659 100644 --- a/io.github.lithe-idea.cargo-build/backend/provider.mjs +++ b/io.github.lithe-idea.cargo-build/backend/provider.mjs @@ -70,9 +70,11 @@ export function registerCargoProvider(backend, PluginError) { } const args = ['metadata', '--format-version', '1', '--no-deps', '--manifest-path', resolve(directory, 'Cargo.toml')]; if (options.offline === true) args.push('--offline'); + const processEnvironment = { ...process.env, ...environment }; + if (tool) processEnvironment.PATH = `${dirname(executable)}:${processEnvironment.PATH ?? ''}`; let result; try { - result = await context.run(executable, args, { cwd: directory, env: { ...process.env, ...environment }, maxOutputBytes: 8 * 1024 * 1024 }); + result = await context.run(executable, args, { cwd: directory, env: processEnvironment, maxOutputBytes: 8 * 1024 * 1024 }); } catch (error) { context.throwIfCancelled(); context.log(error); diff --git a/io.github.lithe-idea.cargo-build/plugin.json b/io.github.lithe-idea.cargo-build/plugin.json index 9a04a00..b2ebca3 100644 --- a/io.github.lithe-idea.cargo-build/plugin.json +++ b/io.github.lithe-idea.cargo-build/plugin.json @@ -2,18 +2,26 @@ "schemaVersion": 2, "id": "io.github.lithe-idea.cargo-build", "displayName": "Cargo", - "version": "0.1.0", + "version": "0.1.1", "pluginAPIVersion": 3, - "requires": { "buildProviders": 1, "buildSystems": 1, "serverProcess": 1 }, + "requires": { "buildProviders": 1, "buildSystems": 2, "serverProcess": 1, "settings": 2 }, "vendor": { "id": "io.github.lithe-idea", "displayName": "Lithe Community", "signatureRequirement": "none" }, "permissions": ["languageServer.launch", "process.build"], "activationEvents": ["onBuildSystem:cargo"], "languages": [], + "settings": [{ + "id": "cargoPath", "type": "file", "title": "Cargo 路径", + "description": "选择 Cargo 可执行文件的绝对路径。留空自动查找;保存后重新加载项目模型,后续构建、测试和运行使用此路径。请勿选择 rustc 或 rustup。", + "default": "", "scope": "global", "effect": "reload", + "page": { "buildSystemID": "cargo" }, "section": "工具链", + "placeholder": "自动查找 Cargo" + }], "buildSystems": [{ "id": "cargo", "displayName": "Cargo", "detection": { "markers": ["Cargo.toml"], "nesting": "outermost" }, "invocation": { + "executableSetting": "cargoPath", "executables": [{ "kind": "command", "command": "cargo" }], "globalArguments": ["--color", "never"], "version": { "arguments": ["--version"], "pattern": "cargo (\\d+\\.\\d+\\.\\d+)" }, diff --git a/sdk.lock.json b/sdk.lock.json index 2b46187..2ed76ea 100644 --- a/sdk.lock.json +++ b/sdk.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "source": { "repository": "https://github.com/1lck/Lithe-IDEA", - "revision": "d8174307f9b5d970783ca785ae3fc62bd3762941" + "revision": "a972d4204ce4c178d0314872119c11db8d435341" }, "files": [ { diff --git a/test/core.test.mjs b/test/core.test.mjs index 9f1ea56..76cc4c8 100644 --- a/test/core.test.mjs +++ b/test/core.test.mjs @@ -7,11 +7,13 @@ import { build, core, integrationSkip, workspace } from './support/core.mjs'; test('Core validates the distributable manifest and requires a provider-capable host', { timeout: 15000, skip: integrationSkip }, async () => { const manifest = JSON.parse(await readFile(join(repository, 'io.github.lithe-idea.cargo-build/plugin.json'), 'utf8')); - const hostCapabilities = { buildSystems: [1], buildProviders: [1], serverProcess: [1] }; + const hostCapabilities = { buildSystems: [1, 2], buildProviders: [1], serverProcess: [1], settings: [1, 2] }; assert.equal((await core('plugins.declarative.validate', { manifest, hostCapabilities })).status, 'ok'); const review = await core('plugins.marketplace.installReview', { manifest, hostCapabilities }); assert.equal(review.status, 'ok'); for (const kind of ['runtimeBuildProvider', 'buildSystem']) assert.ok(review.review.programs.some((item) => item.kind === kind)); + const oldHost = await core('plugins.declarative.validate', { manifest, hostCapabilities: { ...hostCapabilities, buildSystems: [1] } }); + assert.equal(oldHost.error.code, 'plugin.api.unsupported'); const unsupported = await core('plugins.declarative.validate', { manifest, hostCapabilities: { buildSystems: [1], serverProcess: [1] } }); assert.equal(unsupported.error.code, 'plugin.api.unsupported'); }); diff --git a/test/integration.test.mjs b/test/integration.test.mjs index 326a713..142daa8 100644 --- a/test/integration.test.mjs +++ b/test/integration.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { cp, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -35,6 +35,21 @@ test('packaged provider loads a real Cargo workspace; build/test/run/clean leave const workspace = join(scratch, 'workspace'); await cp(join(here, 'fixtures/workspace'), workspace, { recursive: true }); const root = await realpath(workspace); + // A path with spaces and a different executable name catches shell splitting + // and accidental fallback to `cargo` on PATH. Record metadata and every task. + const toolDirectory = join(scratch, 'custom tools'); + await mkdir(toolDirectory); + const tool = join(await realpath(toolDirectory), 'selected-cargo'); + const calls = join(scratch, 'cargo-calls'); + const discovered = await runProcess('which', ['cargo'], { signal: AbortSignal.timeout(5000) }); + assert.equal(discovered.exitCode, 0, discovered.stderr); + const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`; + await writeFile(tool, `#!/bin/sh\nprintf '%s\\n' "$1" >> ${quote(calls)}\nexec ${quote(discovered.stdout.trim())} "$@"\n`, { mode: 0o755 }); + const configured = await build('build.invocationPlan', { root: '.', platform: 'macos', settings: { cargoPath: tool } }); + assert.equal(configured.invocation.configuredExecutable, tool); + const automatic = await build('build.invocationPlan', { root: '.', platform: 'macos', settings: { cargoPath: '' } }); + assert.equal(automatic.invocation.configuredExecutable, undefined); + assert.deepEqual(automatic.invocation.executables, [{ kind: 'command', command: 'cargo' }]); const built = await buildPackage(join(scratch, 'packages')); const baseline = await snapshot(built.directory); const backend = spawn(process.execPath, [join(built.directory, 'backend/main.mjs')], { @@ -47,7 +62,7 @@ test('packaged provider loads a real Cargo workspace; build/test/run/clean leave }); const initialized = await host.initialize({ litheBuild: { versions: [1] } }, pathToFileURL(root).href + '/'); assert.equal(initialized.result.capabilities.experimental.litheBuild.version, 1); - const modelPlan = await build('build.modelPlan', { root: '.', options: { offline: true } }); + const modelPlan = await build('build.modelPlan', { root: '.', options: { offline: true }, tool: { executable: configured.invocation.configuredExecutable } }); const loaded = await host.request(modelPlan.plan.provider.method, modelPlan.plan.provider.params); assert.equal(loaded.error, undefined, JSON.stringify(loaded.error)); assert.deepEqual(loaded.result, { format: 'litheBuildModel', version: 1, ...JSON.parse(await readFile(join(here, 'fixtures/model.json'))) }); @@ -59,7 +74,7 @@ test('packaged provider loads a real Cargo workspace; build/test/run/clean leave // separately hand-written Cargo command lines in this integration test. const cargo = async (command, request) => { const { plan } = await build(command, { root: '.', options, ...request }); - return runProcess('cargo', plan.arguments, { + return runProcess(configured.invocation.configuredExecutable, plan.arguments, { cwd: join(root, plan.workingDirectory), env: { ...process.env, ...plan.environment, CARGO_TARGET_DIR: join(scratch, 'target') }, signal: AbortSignal.timeout(20000), maxOutputBytes: 1024 * 1024 }); @@ -91,6 +106,9 @@ test('packaged provider loads a real Cargo workspace; build/test/run/clean leave const diagnostics = await build('build.decodeDiagnostics', { workspaceRoot: root, root: '.', output: invalid.stderr }); assert.ok(diagnostics.diagnostics.some((item) => item.path === 'app/src/main.rs' && item.severity === 'error')); assert.equal((await cargo('build.taskPlan', { task: 'clean' })).exitCode, 0); + const commands = (await readFile(calls, 'utf8')).trim().split('\n'); + for (const command of ['metadata', '--color']) assert.ok(commands.includes(command), `custom Cargo missed ${command}`); + assert.equal(commands.length, 9, 'metadata and all eight task/test plans must use the selected executable'); assert.equal((await host.request('shutdown', null)).error, undefined); host.notify('exit', null); assert.equal(await host.close(), 0); diff --git a/test/provider.test.mjs b/test/provider.test.mjs index 9462791..b19178d 100644 --- a/test/provider.test.mjs +++ b/test/provider.test.mjs @@ -55,6 +55,7 @@ test('initialization is passive; metadata uses the host tool and only supported assert.equal(calls[0][0], '/fake-tools/cargo'); assert.deepEqual(calls[0][1], ['metadata', '--format-version', '1', '--no-deps', '--manifest-path', join(root, 'Cargo.toml'), '--offline']); assert.equal(calls[0][2].cwd, root); + assert.ok(calls[0][2].env.PATH.startsWith('/fake-tools:')); }); test('relative and symlink build-root escapes are refused before Cargo starts', { timeout: 5000 }, async (t) => {