fix: Fix runnables being incorrectly constructed

This commit is contained in:
Lukas Wirth 2024-07-06 20:20:21 +02:00
parent 7733403056
commit bb9678ea3b
8 changed files with 223 additions and 113 deletions

View file

@ -323,14 +323,6 @@
{ {
"title": "general", "title": "general",
"properties": { "properties": {
"rust-analyzer.cargoRunner": {
"type": [
"null",
"string"
],
"default": null,
"description": "Custom cargo runner extension ID."
},
"rust-analyzer.restartServerOnConfigChange": { "rust-analyzer.restartServerOnConfigChange": {
"markdownDescription": "Whether to restart the server automatically when certain settings that require a restart are changed.", "markdownDescription": "Whether to restart the server automatically when certain settings that require a restart are changed.",
"default": false, "default": false,

View file

@ -36,6 +36,12 @@ async function getServer(
config: Config, config: Config,
state: PersistentState, state: PersistentState,
): Promise<string | undefined> { ): Promise<string | undefined> {
const packageJson: {
version: string;
releaseTag: string | null;
enableProposedApi: boolean | undefined;
} = context.extension.packageJSON;
const explicitPath = process.env["__RA_LSP_SERVER_DEBUG"] ?? config.serverPath; const explicitPath = process.env["__RA_LSP_SERVER_DEBUG"] ?? config.serverPath;
if (explicitPath) { if (explicitPath) {
if (explicitPath.startsWith("~/")) { if (explicitPath.startsWith("~/")) {
@ -43,7 +49,7 @@ async function getServer(
} }
return explicitPath; return explicitPath;
} }
if (config.package.releaseTag === null) return "rust-analyzer"; if (packageJson.releaseTag === null) return "rust-analyzer";
const ext = process.platform === "win32" ? ".exe" : ""; const ext = process.platform === "win32" ? ".exe" : "";
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`); const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
@ -54,8 +60,15 @@ async function getServer(
if (bundledExists) { if (bundledExists) {
let server = bundled; let server = bundled;
if (await isNixOs()) { if (await isNixOs()) {
server = await getNixOsServer(config, ext, state, bundled, server); server = await getNixOsServer(
await state.updateServerVersion(config.package.version); context.globalStorageUri,
packageJson.version,
ext,
state,
bundled,
server,
);
await state.updateServerVersion(packageJson.version);
} }
return server.fsPath; return server.fsPath;
} }
@ -86,19 +99,20 @@ export function isValidExecutable(path: string, extraEnv: Env): boolean {
} }
async function getNixOsServer( async function getNixOsServer(
config: Config, globalStorageUri: vscode.Uri,
version: string,
ext: string, ext: string,
state: PersistentState, state: PersistentState,
bundled: vscode.Uri, bundled: vscode.Uri,
server: vscode.Uri, server: vscode.Uri,
) { ) {
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then(); await vscode.workspace.fs.createDirectory(globalStorageUri).then();
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`); const dest = vscode.Uri.joinPath(globalStorageUri, `rust-analyzer${ext}`);
let exists = await vscode.workspace.fs.stat(dest).then( let exists = await vscode.workspace.fs.stat(dest).then(
() => true, () => true,
() => false, () => false,
); );
if (exists && config.package.version !== state.serverVersion) { if (exists && version !== state.serverVersion) {
await vscode.workspace.fs.delete(dest); await vscode.workspace.fs.delete(dest);
exists = false; exists = false;
} }

View file

@ -4,6 +4,7 @@ import * as path from "path";
import * as vscode from "vscode"; import * as vscode from "vscode";
import { type Env, log, unwrapUndefinable, expectNotUndefined } from "./util"; import { type Env, log, unwrapUndefinable, expectNotUndefined } from "./util";
import type { JsonProject } from "./rust_project"; import type { JsonProject } from "./rust_project";
import type { Disposable } from "./ctx";
export type RunnableEnvCfgItem = { export type RunnableEnvCfgItem = {
mask?: string; mask?: string;
@ -29,22 +30,9 @@ export class Config {
(opt) => `${this.rootSection}.${opt}`, (opt) => `${this.rootSection}.${opt}`,
); );
readonly package: { constructor(disposables: Disposable[]) {
version: string;
releaseTag: string | null;
enableProposedApi: boolean | undefined;
} = vscode.extensions.getExtension(this.extensionId)!.packageJSON;
readonly globalStorageUri: vscode.Uri;
constructor(ctx: vscode.ExtensionContext) {
this.globalStorageUri = ctx.globalStorageUri;
this.discoveredWorkspaces = []; this.discoveredWorkspaces = [];
vscode.workspace.onDidChangeConfiguration( vscode.workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, disposables);
this.onDidChangeConfiguration,
this,
ctx.subscriptions,
);
this.refreshLogging(); this.refreshLogging();
this.configureLanguage(); this.configureLanguage();
} }
@ -55,7 +43,10 @@ export class Config {
private refreshLogging() { private refreshLogging() {
log.setEnabled(this.traceExtension ?? false); log.setEnabled(this.traceExtension ?? false);
log.info("Extension version:", this.package.version); log.info(
"Extension version:",
vscode.extensions.getExtension(this.extensionId)!.packageJSON.version,
);
const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function)); const cfg = Object.entries(this.cfg).filter(([_, val]) => !(val instanceof Function));
log.info("Using configuration", Object.fromEntries(cfg)); log.info("Using configuration", Object.fromEntries(cfg));
@ -277,10 +268,6 @@ export class Config {
return this.get<string[]>("runnables.problemMatcher") || []; return this.get<string[]>("runnables.problemMatcher") || [];
} }
get cargoRunner() {
return this.get<string | undefined>("cargoRunner");
}
get testExplorer() { get testExplorer() {
return this.get<boolean | undefined>("testExplorer"); return this.get<boolean | undefined>("testExplorer");
} }

View file

@ -118,7 +118,7 @@ export class Ctx implements RustAnalyzerExtensionApi {
extCtx.subscriptions.push(this); extCtx.subscriptions.push(this);
this.version = extCtx.extension.packageJSON.version ?? "<unknown>"; this.version = extCtx.extension.packageJSON.version ?? "<unknown>";
this._serverVersion = "<not running>"; this._serverVersion = "<not running>";
this.config = new Config(extCtx); this.config = new Config(extCtx.subscriptions);
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
if (this.config.testExplorer) { if (this.config.testExplorer) {
this.testController = vscode.tests.createTestController( this.testController = vscode.tests.createTestController(

View file

@ -111,26 +111,31 @@ export async function createTaskFromRunnable(
runnable: ra.Runnable, runnable: ra.Runnable,
config: Config, config: Config,
): Promise<vscode.Task> { ): Promise<vscode.Task> {
let definition: tasks.RustTargetDefinition; const target = vscode.workspace.workspaceFolders?.[0];
let definition: tasks.TaskDefinition;
let options;
let cargo;
if (runnable.kind === "cargo") { if (runnable.kind === "cargo") {
const runnableArgs = runnable.args; const runnableArgs = runnable.args;
let args = createCargoArgs(runnableArgs); let args = createCargoArgs(runnableArgs);
let program: string;
if (runnableArgs.overrideCargo) { if (runnableArgs.overrideCargo) {
// Split on spaces to allow overrides like "wrapper cargo". // Split on spaces to allow overrides like "wrapper cargo".
const cargoParts = runnableArgs.overrideCargo.split(" "); const cargoParts = runnableArgs.overrideCargo.split(" ");
program = unwrapUndefinable(cargoParts[0]); cargo = unwrapUndefinable(cargoParts[0]);
args = [...cargoParts.slice(1), ...args]; args = [...cargoParts.slice(1), ...args];
} else { } else {
program = await toolchain.cargoPath(); cargo = await toolchain.cargoPath();
} }
definition = { definition = {
type: tasks.CARGO_TASK_TYPE, type: tasks.CARGO_TASK_TYPE,
command: program, command: unwrapUndefinable(args[0]),
args, args: args.slice(1),
};
options = {
cwd: runnableArgs.workspaceRoot || ".", cwd: runnableArgs.workspaceRoot || ".",
env: prepareEnv(runnable.label, runnableArgs, config.runnablesExtraEnv), env: prepareEnv(runnable.label, runnableArgs, config.runnablesExtraEnv),
}; };
@ -140,13 +145,14 @@ export async function createTaskFromRunnable(
type: tasks.SHELL_TASK_TYPE, type: tasks.SHELL_TASK_TYPE,
command: runnableArgs.program, command: runnableArgs.program,
args: runnableArgs.args, args: runnableArgs.args,
};
options = {
cwd: runnableArgs.cwd, cwd: runnableArgs.cwd,
env: prepareBaseEnv(), env: prepareBaseEnv(),
}; };
} }
const target = vscode.workspace.workspaceFolders?.[0]; const exec = await tasks.targetToExecution(definition, options, cargo);
const exec = await tasks.targetToExecution(definition, config.cargoRunner, true);
const task = await tasks.buildRustTask( const task = await tasks.buildRustTask(
target, target,
definition, definition,

View file

@ -1,6 +1,5 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import type { Config } from "./config"; import type { Config } from "./config";
import { log, unwrapUndefinable } from "./util";
import * as toolchain from "./toolchain"; import * as toolchain from "./toolchain";
// This ends up as the `type` key in tasks.json. RLS also uses `cargo` and // This ends up as the `type` key in tasks.json. RLS also uses `cargo` and
@ -10,21 +9,21 @@ export const SHELL_TASK_TYPE = "shell";
export const RUST_TASK_SOURCE = "rust"; export const RUST_TASK_SOURCE = "rust";
export type RustTargetDefinition = { export type TaskDefinition = vscode.TaskDefinition & {
readonly type: typeof CARGO_TASK_TYPE | typeof SHELL_TASK_TYPE; readonly type: typeof CARGO_TASK_TYPE | typeof SHELL_TASK_TYPE;
} & vscode.TaskDefinition &
RustTarget;
export type RustTarget = {
// The command to run, usually `cargo`.
command: string;
// Additional arguments passed to the command.
args?: string[]; args?: string[];
// The working directory to run the command in. command: string;
cwd?: string;
// The shell environment.
env?: { [key: string]: string };
}; };
export type CargoTaskDefinition = {
env?: Record<string, string>;
type: typeof CARGO_TASK_TYPE;
} & TaskDefinition;
function isCargoTask(definition: vscode.TaskDefinition): definition is CargoTaskDefinition {
return definition.type === CARGO_TASK_TYPE;
}
class RustTaskProvider implements vscode.TaskProvider { class RustTaskProvider implements vscode.TaskProvider {
private readonly config: Config; private readonly config: Config;
@ -58,13 +57,13 @@ class RustTaskProvider implements vscode.TaskProvider {
for (const workspaceTarget of vscode.workspace.workspaceFolders) { for (const workspaceTarget of vscode.workspace.workspaceFolders) {
for (const def of defs) { for (const def of defs) {
const definition = { const definition = {
command: cargo, command: def.command,
args: [def.command], type: CARGO_TASK_TYPE,
}; } as const;
const exec = await targetToExecution(definition, this.config.cargoRunner); const exec = await targetToExecution(definition, {}, cargo);
const vscodeTask = await buildRustTask( const vscodeTask = await buildRustTask(
workspaceTarget, workspaceTarget,
{ ...definition, type: CARGO_TASK_TYPE }, definition,
`cargo ${def.command}`, `cargo ${def.command}`,
this.config.problemMatcher, this.config.problemMatcher,
exec, exec,
@ -81,23 +80,13 @@ class RustTaskProvider implements vscode.TaskProvider {
// VSCode calls this for every cargo task in the user's tasks.json, // VSCode calls this for every cargo task in the user's tasks.json,
// we need to inform VSCode how to execute that command by creating // we need to inform VSCode how to execute that command by creating
// a ShellExecution for it. // a ShellExecution for it.
if (task.definition.type === CARGO_TASK_TYPE) { if (isCargoTask(task.definition)) {
const taskDefinition = task.definition as RustTargetDefinition; const exec = await targetToExecution(task.definition, { env: task.definition.env });
const cargo = await toolchain.cargoPath(); return buildRustTask(
const exec = await targetToExecution(
{
command: cargo,
args: [taskDefinition.command].concat(taskDefinition.args || []),
cwd: taskDefinition.cwd,
env: taskDefinition.env,
},
this.config.cargoRunner,
);
return await buildRustTask(
task.scope, task.scope,
taskDefinition, task.definition,
task.name, task.name,
this.config.problemMatcher, task.problemMatchers,
exec, exec,
); );
} }
@ -108,7 +97,7 @@ class RustTaskProvider implements vscode.TaskProvider {
export async function buildRustTask( export async function buildRustTask(
scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined, scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
definition: RustTargetDefinition, definition: TaskDefinition,
name: string, name: string,
problemMatcher: string[], problemMatcher: string[],
exec: vscode.ProcessExecution | vscode.ShellExecution, exec: vscode.ProcessExecution | vscode.ShellExecution,
@ -126,40 +115,23 @@ export async function buildRustTask(
} }
export async function targetToExecution( export async function targetToExecution(
definition: RustTarget, definition: TaskDefinition,
customRunner?: string, options?: {
throwOnError: boolean = false, env?: { [key: string]: string };
cwd?: string;
},
cargo?: string,
): Promise<vscode.ProcessExecution | vscode.ShellExecution> { ): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
if (customRunner) { let command, args;
const runnerCommand = `${customRunner}.buildShellExecution`; if (isCargoTask(definition)) {
// FIXME: The server should provide cargo
try { command = cargo || (await toolchain.cargoPath());
const runnerArgs = { args = [definition.command].concat(definition.args || []);
kind: CARGO_TASK_TYPE, } else {
args: definition.args, command = definition.command;
cwd: definition.cwd, args = definition.args || [];
env: definition.env,
};
const customExec = await vscode.commands.executeCommand(runnerCommand, runnerArgs);
if (customExec) {
if (customExec instanceof vscode.ShellExecution) {
return customExec;
} else {
log.debug("Invalid cargo ShellExecution", customExec);
throw "Invalid cargo ShellExecution.";
}
}
// fallback to default processing
} catch (e) {
if (throwOnError) throw `Cargo runner '${customRunner}' failed! ${e}`;
// fallback to default processing
}
} }
const args = unwrapUndefinable(definition.args); return new vscode.ProcessExecution(command, args, options);
return new vscode.ProcessExecution(definition.command, args, {
cwd: definition.cwd,
env: definition.env,
});
} }
export function activateTaskProvider(config: Config): vscode.Disposable { export function activateTaskProvider(config: Config): vscode.Disposable {

View file

@ -13,7 +13,7 @@ export async function getTests(ctx: Context) {
USING_MY_VAR: "test test test", USING_MY_VAR: "test test test",
MY_VAR: "test", MY_VAR: "test",
}; };
const actualEnv = await substituteVariablesInEnv(envJson); const actualEnv = substituteVariablesInEnv(envJson);
assert.deepStrictEqual(actualEnv, expectedEnv); assert.deepStrictEqual(actualEnv, expectedEnv);
}); });
@ -34,7 +34,7 @@ export async function getTests(ctx: Context) {
E_IS_ISOLATED: "test", E_IS_ISOLATED: "test",
F_USES_E: "test", F_USES_E: "test",
}; };
const actualEnv = await substituteVariablesInEnv(envJson); const actualEnv = substituteVariablesInEnv(envJson);
assert.deepStrictEqual(actualEnv, expectedEnv); assert.deepStrictEqual(actualEnv, expectedEnv);
}); });
@ -47,7 +47,7 @@ export async function getTests(ctx: Context) {
USING_EXTERNAL_VAR: "test test test", USING_EXTERNAL_VAR: "test test test",
}; };
const actualEnv = await substituteVariablesInEnv(envJson); const actualEnv = substituteVariablesInEnv(envJson);
assert.deepStrictEqual(actualEnv, expectedEnv); assert.deepStrictEqual(actualEnv, expectedEnv);
delete process.env["TEST_VARIABLE"]; delete process.env["TEST_VARIABLE"];
}); });
@ -56,7 +56,7 @@ export async function getTests(ctx: Context) {
const envJson = { const envJson = {
USING_VSCODE_VAR: "${workspaceFolderBasename}", USING_VSCODE_VAR: "${workspaceFolderBasename}",
}; };
const actualEnv = await substituteVariablesInEnv(envJson); const actualEnv = substituteVariablesInEnv(envJson);
assert.deepStrictEqual(actualEnv["USING_VSCODE_VAR"], "code"); assert.deepStrictEqual(actualEnv["USING_VSCODE_VAR"], "code");
}); });
}); });

View file

@ -0,0 +1,139 @@
import type { Context } from ".";
import * as vscode from "vscode";
import * as assert from "assert";
import { targetToExecution } from "../../src/tasks";
export async function getTests(ctx: Context) {
await ctx.suite("Tasks", (suite) => {
suite.addTest("cargo targetToExecution", async () => {
assert.deepStrictEqual(
await targetToExecution({
type: "cargo",
command: "check",
args: ["foo"],
}).then(executionToSimple),
{
process: "cargo",
args: ["check", "foo"],
},
);
});
suite.addTest("shell targetToExecution", async () => {
assert.deepStrictEqual(
await targetToExecution({
type: "shell",
command: "thing",
args: ["foo"],
}).then(executionToSimple),
{
process: "thing",
args: ["foo"],
},
);
});
suite.addTest("base tasks", async () => {
const tasks = await vscode.tasks.fetchTasks({ type: "cargo" });
const expectedTasks = [
{
definition: { type: "cargo", command: "build" },
name: "cargo build",
execution: {
process: "cargo",
args: ["build"],
},
},
{
definition: {
type: "cargo",
command: "check",
},
name: "cargo check",
execution: {
process: "cargo",
args: ["check"],
},
},
{
definition: { type: "cargo", command: "clippy" },
name: "cargo clippy",
execution: {
process: "cargo",
args: ["clippy"],
},
},
{
definition: { type: "cargo", command: "test" },
name: "cargo test",
execution: {
process: "cargo",
args: ["test"],
},
},
{
definition: {
type: "cargo",
command: "clean",
},
name: "cargo clean",
execution: {
process: "cargo",
args: ["clean"],
},
},
{
definition: { type: "cargo", command: "run" },
name: "cargo run",
execution: {
process: "cargo",
args: ["run"],
},
},
];
tasks.map(f).forEach((actual, i) => {
const expected = expectedTasks[i];
assert.deepStrictEqual(actual, expected);
});
});
});
}
function f(task: vscode.Task): {
definition: vscode.TaskDefinition;
name: string;
execution: {
args: string[];
} & ({ command: string } | { process: string });
} {
const execution = executionToSimple(task.execution!);
return {
definition: task.definition,
name: task.name,
execution,
};
}
function executionToSimple(
taskExecution: vscode.ProcessExecution | vscode.ShellExecution | vscode.CustomExecution,
): {
args: string[];
} & ({ command: string } | { process: string }) {
const exec = taskExecution as vscode.ProcessExecution | vscode.ShellExecution;
if (exec instanceof vscode.ShellExecution) {
return {
command: typeof exec.command === "string" ? exec.command : exec.command.value,
args: exec.args.map((arg) => {
if (typeof arg === "string") {
return arg;
}
return arg.value;
}),
};
} else {
return {
process: exec.process,
args: exec.args,
};
}
}