Skip to content

Commit d4fd363

Browse files
authored
Merge pull request #11 from lildengzi/fix/find-xlings-mcpp-bundled
fix: xlings 发现以 `mcpp self env` 为权威来源(项目级契约),路径探测仅作回退
2 parents 4367b95 + c2f5507 commit d4fd363

5 files changed

Lines changed: 198 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 更新日志
22

3+
## 0.2.7
4+
5+
- 修复「一键配置模块代码提示」在标准 mcpp 安装(install.sh / AUR)下无法发现 mcpp 内置
6+
xlings 的问题:xlings 发现以 `mcpp self env` 为权威来源(项目级契约),路径探测仅作回退;
7+
`mcpp self env` 调用增加超时保护,并补齐测试(PR #11)。
8+
39
## 0.2.6
410

511
- 新增 **mcpp: 一键配置模块代码提示** 向导:按「安装/切换工具链 → 构建 → 重载 → clangd

src/cliController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ export class McppCliController {
630630
return false;
631631
}
632632

633-
private mcppExecutable(project: McppProjectDiscovery | undefined): string {
633+
public mcppExecutable(project: McppProjectDiscovery | undefined): string {
634634
const uri = project === undefined
635635
? vscode.workspace.workspaceFolders?.[0]?.uri
636636
: vscode.Uri.file(project.root);

src/extension.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
type McppProjectDiscovery,
2020
} from "./discovery";
2121
import {
22-
findXlingsExecutable,
22+
resolveXlingsExecutable,
2323
llvmToolsVersionSpec,
2424
xlingsInstallArgs,
2525
} from "./llvmTools";
@@ -763,7 +763,9 @@ async function autoConfigureModulesWizard(
763763
: { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" };
764764
}
765765

766-
const xlingsPath = findXlingsExecutable();
766+
const xlingsPath = await resolveXlingsExecutable(
767+
cliController.mcppExecutable(currentContext.project),
768+
);
767769
const compilerPath = currentContext.analysis.compilerPath;
768770
if (xlingsPath === undefined || compilerPath === undefined) {
769771
return {

src/llvmTools.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import path from "node:path";
44
import process from "node:process";
55

66
import type { ToolIdentity } from "./analysis";
7-
import { runProcess, type ProcessResult } from "./process";
7+
import {
8+
runProcess,
9+
type ProcessResult,
10+
type ProcessRunner,
11+
} from "./process";
812

913
export function llvmToolsVersionSpec(identity: ToolIdentity): string {
1014
return `${identity.major}.${identity.minor}.${identity.patch}`;
@@ -60,8 +64,16 @@ export function xlingsInstallArgs(version?: string): string[] {
6064
return ["update", "llvm-tools"];
6165
}
6266

63-
export function findXlingsExecutable(): string | undefined {
64-
const home = os.homedir();
67+
export interface FindXlingsOptions {
68+
/** Override for tests: base home directory instead of os.homedir(). */
69+
home?: string;
70+
/** Override for tests: environment instead of process.env. */
71+
env?: NodeJS.ProcessEnv;
72+
}
73+
74+
export function findXlingsExecutable(options?: FindXlingsOptions): string | undefined {
75+
const home = options?.home ?? os.homedir();
76+
const env = options?.env ?? process.env;
6577
const knownPaths = [
6678
path.join(home, ".xlings", "subos", "current", "bin", "xlings"),
6779
path.join(home, ".xlings", "bin", "xlings"),
@@ -72,6 +84,26 @@ export function findXlingsExecutable(): string | undefined {
7284
);
7385
}
7486

87+
// mcpp (install.sh / AUR / mcpp-m) bundles xlings inside its own registry
88+
// sandbox instead of installing to ~/.xlings. The AUR launcher pins the
89+
// path via MCPP_VENDORED_XLINGS; otherwise it lives at
90+
// $MCPP_HOME/registry/bin/xlings. Without probing both, the one-click
91+
// module setup can never auto-install llvm-tools after a standard install.
92+
const vendored = env.MCPP_VENDORED_XLINGS?.trim();
93+
if (vendored !== undefined && vendored.length > 0) {
94+
knownPaths.push(vendored);
95+
}
96+
const mcppHome = env.MCPP_HOME?.trim();
97+
const extension = process.platform === "win32" ? ".exe" : "";
98+
knownPaths.push(
99+
path.join(
100+
mcppHome !== undefined && mcppHome.length > 0 ? mcppHome : path.join(home, ".mcpp"),
101+
"registry",
102+
"bin",
103+
`xlings${extension}`,
104+
),
105+
);
106+
75107
// Check known install paths first
76108
for (const candidate of knownPaths) {
77109
if (existsSync(candidate)) {
@@ -82,15 +114,15 @@ export function findXlingsExecutable(): string | undefined {
82114
// Fall back to PATH, but only when "xlings" actually resolves there. Always
83115
// returning "xlings" hid the not-installed case, so callers could never show
84116
// the "xlings 未安装" guidance.
85-
return xlingsResolvableOnPath() ? "xlings" : undefined;
117+
return xlingsResolvableOnPath(env.PATH) ? "xlings" : undefined;
86118
}
87119

88-
function xlingsResolvableOnPath(): boolean {
120+
function xlingsResolvableOnPath(pathValue?: string): boolean {
89121
const names = process.platform === "win32"
90122
? ["xlings.exe", "xlings.cmd", "xlings.bat"]
91123
: ["xlings"];
92-
const pathValue = process.env.PATH ?? "";
93-
for (const dir of pathValue.split(path.delimiter)) {
124+
const pathEnv = pathValue ?? "";
125+
for (const dir of pathEnv.split(path.delimiter)) {
94126
if (dir.length === 0) {
95127
continue;
96128
}
@@ -103,6 +135,35 @@ function xlingsResolvableOnPath(): boolean {
103135
return false;
104136
}
105137

138+
const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im;
139+
140+
// Source of truth is mcpp itself, not the filesystem or PATH: `mcpp self env`
141+
// reports the exact xlings bundled with THIS mcpp (mcpp is a project-level
142+
// environment; it owns its tool paths). Works for install.sh, AUR and any
143+
// custom MCPP_PREFIX layout. Falls back to the historical path heuristics for
144+
// standalone ~/.xlings installs and for mcpp versions without the line.
145+
//
146+
// The subprocess is bounded by MCPP_SELF_ENV_TIMEOUT_MS: the wizard reaches
147+
// this step only after mcpp is initialized (toolchain list / build already
148+
// ran), so 60s is generous while still guarding against an extreme hang.
149+
const MCPP_SELF_ENV_TIMEOUT_MS = 60_000;
150+
151+
export async function resolveXlingsExecutable(
152+
mcppExecutable: string,
153+
runner: ProcessRunner = runProcess,
154+
options?: FindXlingsOptions,
155+
): Promise<string | undefined> {
156+
const result = await runner(mcppExecutable, ["self", "env"], undefined, {
157+
timeoutMs: MCPP_SELF_ENV_TIMEOUT_MS,
158+
});
159+
const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE);
160+
const reported = match?.[1]?.trim();
161+
if (reported !== undefined && reported.length > 0 && existsSync(reported)) {
162+
return reported;
163+
}
164+
return findXlingsExecutable(options);
165+
}
166+
106167
export async function runXlingsCommand(
107168
xlingsPath: string,
108169
args: string[],

test/llvmTools.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import assert from "node:assert/strict";
2+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
25
import test from "node:test";
36

47
import {
@@ -7,8 +10,12 @@ import {
710
xlingsInstallArgs,
811
deriveInstalledClangdPath,
912
findXlingsExecutable,
13+
resolveXlingsExecutable,
1014
} from "../src/llvmTools";
1115

16+
// `xlings` on POSIX, `xlings.exe` on Windows — mirrors mcpp's exe_suffix.
17+
const xlingsBinaryName = process.platform === "win32" ? "xlings.exe" : "xlings";
18+
1219
test("extracts version string from ToolIdentity", () => {
1320
assert.equal(
1421
llvmToolsVersionSpec({ major: 22, minor: 1, patch: 8, revision: "abc1234" }),
@@ -68,3 +75,115 @@ test("findXlingsExecutable returns a string or undefined", () => {
6875
// Returns string (PATH fallback or known path) or undefined if xlings not found
6976
assert.ok(result === undefined || typeof result === "string");
7077
});
78+
79+
test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => {
80+
const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-"));
81+
const registryBin = path.join(home, "registry", "bin");
82+
const xlingsPath = path.join(registryBin, xlingsBinaryName);
83+
mkdirSync(registryBin, { recursive: true });
84+
writeFileSync(xlingsPath, "#!/bin/sh\n");
85+
try {
86+
assert.equal(
87+
findXlingsExecutable({ home, env: { MCPP_HOME: home } }),
88+
xlingsPath,
89+
);
90+
} finally {
91+
rmSync(home, { recursive: true, force: true });
92+
}
93+
});
94+
95+
test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => {
96+
const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-"));
97+
const registryBin = path.join(home, ".mcpp", "registry", "bin");
98+
const xlingsPath = path.join(registryBin, xlingsBinaryName);
99+
mkdirSync(registryBin, { recursive: true });
100+
writeFileSync(xlingsPath, "#!/bin/sh\n");
101+
try {
102+
assert.equal(
103+
findXlingsExecutable({ home, env: {} }),
104+
xlingsPath,
105+
);
106+
} finally {
107+
rmSync(home, { recursive: true, force: true });
108+
}
109+
});
110+
111+
test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => {
112+
const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-"));
113+
const vendored = path.join(root, "opt-mcpp", "registry", "bin", xlingsBinaryName);
114+
mkdirSync(path.dirname(vendored), { recursive: true });
115+
writeFileSync(vendored, "#!/bin/sh\n");
116+
try {
117+
assert.equal(
118+
findXlingsExecutable({ home: root, env: { MCPP_VENDORED_XLINGS: vendored } }),
119+
vendored,
120+
);
121+
} finally {
122+
rmSync(root, { recursive: true, force: true });
123+
}
124+
});
125+
126+
test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => {
127+
const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-"));
128+
const xlingsPath = path.join(root, "registry", "bin", xlingsBinaryName);
129+
mkdirSync(path.dirname(xlingsPath), { recursive: true });
130+
writeFileSync(xlingsPath, "#!/bin/sh\n");
131+
const runner = async () => ({
132+
exitCode: 0,
133+
stdout: `MCPP_HOME = ${root}\nxlings binary = ${xlingsPath}\nxlings pinned = 2026.8.8.1\n`,
134+
stderr: "",
135+
});
136+
try {
137+
assert.equal(
138+
await resolveXlingsExecutable("/tools/mcpp", runner),
139+
xlingsPath,
140+
);
141+
} finally {
142+
rmSync(root, { recursive: true, force: true });
143+
}
144+
});
145+
146+
test("resolveXlingsExecutable passes a timeout to `mcpp self env`", async () => {
147+
let captured: { timeoutMs?: number } | undefined;
148+
const runner = async (
149+
_executable: string,
150+
_args: string[],
151+
_cwd?: string,
152+
options?: { timeoutMs?: number },
153+
) => {
154+
captured = options;
155+
return { exitCode: 0, stdout: "", stderr: "" };
156+
};
157+
await resolveXlingsExecutable("/tools/mcpp", runner);
158+
assert.equal(captured?.timeoutMs, 60_000);
159+
});
160+
161+
test("resolveXlingsExecutable falls back to path probing when the reported path does not exist", async () => {
162+
const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-missing-"));
163+
const runner = async () => ({
164+
exitCode: 0,
165+
stdout: "xlings binary = /no/such/xlings\n",
166+
stderr: "",
167+
});
168+
try {
169+
assert.equal(
170+
await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }),
171+
undefined,
172+
);
173+
} finally {
174+
rmSync(home, { recursive: true, force: true });
175+
}
176+
});
177+
178+
test("resolveXlingsExecutable falls back to path probing when `mcpp self env` fails", async () => {
179+
const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-fail-"));
180+
const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" });
181+
try {
182+
assert.equal(
183+
await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }),
184+
undefined,
185+
);
186+
} finally {
187+
rmSync(home, { recursive: true, force: true });
188+
}
189+
});

0 commit comments

Comments
 (0)