Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion io.github.lithe-idea.cargo-build/backend/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
4 changes: 3 additions & 1 deletion io.github.lithe-idea.cargo-build/backend/provider.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions io.github.lithe-idea.cargo-build/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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+)" },
Expand Down
2 changes: 1 addition & 1 deletion sdk.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"source": {
"repository": "https://github.com/1lck/Lithe-IDEA",
"revision": "d8174307f9b5d970783ca785ae3fc62bd3762941"
"revision": "a972d4204ce4c178d0314872119c11db8d435341"
},
"files": [
{
Expand Down
4 changes: 3 additions & 1 deletion test/core.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
24 changes: 21 additions & 3 deletions test/integration.test.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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')], {
Expand All @@ -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'))) });
Expand All @@ -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
});
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/provider.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading