From 9e1740cfa02be5dcb53833f227d3c723348c5b9c Mon Sep 17 00:00:00 2001 From: lildengzi <2580862656@qq.com> Date: Sun, 9 Aug 2026 01:20:59 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20findXlingsExecutable=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8F=91=E7=8E=B0=20mcpp=20=E5=86=85=E7=BD=AE=20xling?= =?UTF-8?q?s=EF=BC=88$MCPP=5FHOME/registry/bin=20=E4=B8=8E=20MCPP=5FVENDOR?= =?UTF-8?q?ED=5FXLINGS=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/llvmTools.ts | 40 ++++++++++++++++++++++++++++----- test/llvmTools.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/llvmTools.ts b/src/llvmTools.ts index be064f9..9d7785f 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -60,8 +60,16 @@ export function xlingsInstallArgs(version?: string): string[] { return ["update", "llvm-tools"]; } -export function findXlingsExecutable(): string | undefined { - const home = os.homedir(); +export interface FindXlingsOptions { + /** Override for tests: base home directory instead of os.homedir(). */ + home?: string; + /** Override for tests: environment instead of process.env. */ + env?: NodeJS.ProcessEnv; +} + +export function findXlingsExecutable(options?: FindXlingsOptions): string | undefined { + const home = options?.home ?? os.homedir(); + const env = options?.env ?? process.env; const knownPaths = [ path.join(home, ".xlings", "subos", "current", "bin", "xlings"), path.join(home, ".xlings", "bin", "xlings"), @@ -72,6 +80,26 @@ export function findXlingsExecutable(): string | undefined { ); } + // mcpp (install.sh / AUR / mcpp-m) bundles xlings inside its own registry + // sandbox instead of installing to ~/.xlings. The AUR launcher pins the + // path via MCPP_VENDORED_XLINGS; otherwise it lives at + // $MCPP_HOME/registry/bin/xlings. Without probing both, the one-click + // module setup can never auto-install llvm-tools after a standard install. + const vendored = env.MCPP_VENDORED_XLINGS?.trim(); + if (vendored !== undefined && vendored.length > 0) { + knownPaths.push(vendored); + } + const mcppHome = env.MCPP_HOME?.trim(); + const extension = process.platform === "win32" ? ".exe" : ""; + knownPaths.push( + path.join( + mcppHome !== undefined && mcppHome.length > 0 ? mcppHome : path.join(home, ".mcpp"), + "registry", + "bin", + `xlings${extension}`, + ), + ); + // Check known install paths first for (const candidate of knownPaths) { if (existsSync(candidate)) { @@ -82,15 +110,15 @@ export function findXlingsExecutable(): string | undefined { // Fall back to PATH, but only when "xlings" actually resolves there. Always // returning "xlings" hid the not-installed case, so callers could never show // the "xlings 未安装" guidance. - return xlingsResolvableOnPath() ? "xlings" : undefined; + return xlingsResolvableOnPath(env.PATH) ? "xlings" : undefined; } -function xlingsResolvableOnPath(): boolean { +function xlingsResolvableOnPath(pathValue?: string): boolean { const names = process.platform === "win32" ? ["xlings.exe", "xlings.cmd", "xlings.bat"] : ["xlings"]; - const pathValue = process.env.PATH ?? ""; - for (const dir of pathValue.split(path.delimiter)) { + const pathEnv = pathValue ?? ""; + for (const dir of pathEnv.split(path.delimiter)) { if (dir.length === 0) { continue; } diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index b6348f5..e87750b 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import test from "node:test"; import { @@ -68,3 +71,50 @@ test("findXlingsExecutable returns a string or undefined", () => { // Returns string (PATH fallback or known path) or undefined if xlings not found assert.ok(result === undefined || typeof result === "string"); }); + +test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-")); + const registryBin = path.join(home, "registry", "bin"); + const xlingsPath = path.join(registryBin, "xlings"); + mkdirSync(registryBin, { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home, env: { MCPP_HOME: home } }), + xlingsPath, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); + +test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-")); + const registryBin = path.join(home, ".mcpp", "registry", "bin"); + const xlingsPath = path.join(registryBin, "xlings"); + mkdirSync(registryBin, { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home, env: {} }), + xlingsPath, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } +}); + +test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-")); + const vendored = path.join(root, "opt-mcpp", "registry", "bin", "xlings"); + mkdirSync(path.dirname(vendored), { recursive: true }); + writeFileSync(vendored, "#!/bin/sh\n"); + try { + assert.equal( + findXlingsExecutable({ home: root, env: { MCPP_VENDORED_XLINGS: vendored } }), + vendored, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); From b11ce64550d7798dfce08e2ae11e7cc80d2faa58 Mon Sep 17 00:00:00 2001 From: lildengzi <2580862656@qq.com> Date: Sun, 9 Aug 2026 01:30:19 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20xlings=20=E5=8F=91=E7=8E=B0=E4=BB=A5?= =?UTF-8?q?=20`mcpp=20self=20env`=20=E4=B8=BA=E6=9D=83=E5=A8=81=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=EF=BC=88=E9=A1=B9=E7=9B=AE=E7=BA=A7=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E8=B7=AF=E5=BE=84=E6=8E=A2=E6=B5=8B=E4=BB=85?= =?UTF-8?q?=E4=BD=9C=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cliController.ts | 2 +- src/extension.ts | 6 ++++-- src/llvmTools.ts | 26 +++++++++++++++++++++++++- test/llvmTools.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/cliController.ts b/src/cliController.ts index ef77e6a..2416b72 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -630,7 +630,7 @@ export class McppCliController { return false; } - private mcppExecutable(project: McppProjectDiscovery | undefined): string { + public mcppExecutable(project: McppProjectDiscovery | undefined): string { const uri = project === undefined ? vscode.workspace.workspaceFolders?.[0]?.uri : vscode.Uri.file(project.root); diff --git a/src/extension.ts b/src/extension.ts index f16d760..23fb2de 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,7 +19,7 @@ import { type McppProjectDiscovery, } from "./discovery"; import { - findXlingsExecutable, + resolveXlingsExecutable, llvmToolsVersionSpec, xlingsInstallArgs, } from "./llvmTools"; @@ -763,7 +763,9 @@ async function autoConfigureModulesWizard( : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; } - const xlingsPath = findXlingsExecutable(); + const xlingsPath = await resolveXlingsExecutable( + cliController.mcppExecutable(currentContext.project), + ); const compilerPath = currentContext.analysis.compilerPath; if (xlingsPath === undefined || compilerPath === undefined) { return { diff --git a/src/llvmTools.ts b/src/llvmTools.ts index 9d7785f..e268b5a 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -4,7 +4,11 @@ import path from "node:path"; import process from "node:process"; import type { ToolIdentity } from "./analysis"; -import { runProcess, type ProcessResult } from "./process"; +import { + runProcess, + type ProcessResult, + type ProcessRunner, +} from "./process"; export function llvmToolsVersionSpec(identity: ToolIdentity): string { return `${identity.major}.${identity.minor}.${identity.patch}`; @@ -131,6 +135,26 @@ function xlingsResolvableOnPath(pathValue?: string): boolean { return false; } +const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im; + +// Source of truth is mcpp itself, not the filesystem or PATH: `mcpp self env` +// reports the exact xlings bundled with THIS mcpp (mcpp is a project-level +// environment; it owns its tool paths). Works for install.sh, AUR and any +// custom MCPP_PREFIX layout. Falls back to the historical path heuristics for +// standalone ~/.xlings installs and for mcpp versions without the line. +export async function resolveXlingsExecutable( + mcppExecutable: string, + runner: ProcessRunner = runProcess, +): Promise { + const result = await runner(mcppExecutable, ["self", "env"]); + const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE); + const reported = match?.[1]?.trim(); + if (reported !== undefined && reported.length > 0 && existsSync(reported)) { + return reported; + } + return findXlingsExecutable(); +} + export async function runXlingsCommand( xlingsPath: string, args: string[], diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index e87750b..a7528a9 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -10,6 +10,7 @@ import { xlingsInstallArgs, deriveInstalledClangdPath, findXlingsExecutable, + resolveXlingsExecutable, } from "../src/llvmTools"; test("extracts version string from ToolIdentity", () => { @@ -118,3 +119,41 @@ test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-")); + const xlingsPath = path.join(root, "registry", "bin", "xlings"); + mkdirSync(path.dirname(xlingsPath), { recursive: true }); + writeFileSync(xlingsPath, "#!/bin/sh\n"); + const runner = async () => ({ + exitCode: 0, + stdout: `MCPP_HOME = ${root}\nxlings binary = ${xlingsPath}\nxlings pinned = 2026.8.8.1\n`, + stderr: "", + }); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner), + xlingsPath, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("resolveXlingsExecutable falls back when the reported path does not exist", async () => { + const runner = async () => ({ + exitCode: 0, + stdout: "xlings binary = /no/such/xlings\n", + stderr: "", + }); + const result = await resolveXlingsExecutable("/tools/mcpp", runner); + // Fallback heuristics find nothing in this environment, so the result is + // undefined unless a standalone ~/.xlings or PATH xlings happens to exist. + assert.ok(result === undefined || typeof result === "string"); +}); + +test("resolveXlingsExecutable falls back when `mcpp self env` fails", async () => { + const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" }); + const result = await resolveXlingsExecutable("/tools/mcpp", runner); + assert.ok(result === undefined || typeof result === "string"); +}); From c2f5507d79b30ae98cfd69da48b6c643aed3f215 Mon Sep 17 00:00:00 2001 From: wellwei Date: Sun, 9 Aug 2026 01:58:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20PR=20#11=20=E6=89=93=E7=A3=A8?= =?UTF-8?q?=E2=80=94=E2=80=94`mcpp=20self=20env`=20=E5=8A=A0=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E4=BF=9D=E6=8A=A4=E3=80=81=E5=9B=9E=E9=80=80=E6=8E=A2?= =?UTF-8?q?=E6=B5=8B=E6=94=AF=E6=8C=81=E6=B3=A8=E5=85=A5=E3=80=81=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=B9=B3=E5=8F=B0=E5=8F=AF=E7=A7=BB=E6=A4=8D=E3=80=81?= =?UTF-8?q?=E8=A1=A5=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++++ src/llvmTools.ts | 13 ++++++++-- test/llvmTools.test.ts | 54 ++++++++++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4177e..0015cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +## 0.2.7 + +- 修复「一键配置模块代码提示」在标准 mcpp 安装(install.sh / AUR)下无法发现 mcpp 内置 + xlings 的问题:xlings 发现以 `mcpp self env` 为权威来源(项目级契约),路径探测仅作回退; + 为 `mcpp self env` 调用增加超时保护,并补齐测试(PR #11)。 + ## 0.2.6 - 新增 **mcpp: 一键配置模块代码提示** 向导:按「安装/切换工具链 → 构建 → 重载 → clangd diff --git a/src/llvmTools.ts b/src/llvmTools.ts index e268b5a..f573f05 100644 --- a/src/llvmTools.ts +++ b/src/llvmTools.ts @@ -142,17 +142,26 @@ const XLINGS_BINARY_LINE = /^\s*xlings binary\s*=\s*(.+?)\s*$/im; // environment; it owns its tool paths). Works for install.sh, AUR and any // custom MCPP_PREFIX layout. Falls back to the historical path heuristics for // standalone ~/.xlings installs and for mcpp versions without the line. +// +// The subprocess is bounded by MCPP_SELF_ENV_TIMEOUT_MS: the wizard reaches +// this step only after mcpp is initialized (toolchain list / build already +// ran), so 60s is generous while still guarding against an extreme hang. +const MCPP_SELF_ENV_TIMEOUT_MS = 60_000; + export async function resolveXlingsExecutable( mcppExecutable: string, runner: ProcessRunner = runProcess, + options?: FindXlingsOptions, ): Promise { - const result = await runner(mcppExecutable, ["self", "env"]); + const result = await runner(mcppExecutable, ["self", "env"], undefined, { + timeoutMs: MCPP_SELF_ENV_TIMEOUT_MS, + }); const match = `${result.stdout}\n${result.stderr}`.match(XLINGS_BINARY_LINE); const reported = match?.[1]?.trim(); if (reported !== undefined && reported.length > 0 && existsSync(reported)) { return reported; } - return findXlingsExecutable(); + return findXlingsExecutable(options); } export async function runXlingsCommand( diff --git a/test/llvmTools.test.ts b/test/llvmTools.test.ts index a7528a9..8e967d7 100644 --- a/test/llvmTools.test.ts +++ b/test/llvmTools.test.ts @@ -13,6 +13,9 @@ import { resolveXlingsExecutable, } from "../src/llvmTools"; +// `xlings` on POSIX, `xlings.exe` on Windows — mirrors mcpp's exe_suffix. +const xlingsBinaryName = process.platform === "win32" ? "xlings.exe" : "xlings"; + test("extracts version string from ToolIdentity", () => { assert.equal( llvmToolsVersionSpec({ major: 22, minor: 1, patch: 8, revision: "abc1234" }), @@ -76,7 +79,7 @@ test("findXlingsExecutable returns a string or undefined", () => { test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", () => { const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-mcpp-home-")); const registryBin = path.join(home, "registry", "bin"); - const xlingsPath = path.join(registryBin, "xlings"); + const xlingsPath = path.join(registryBin, xlingsBinaryName); mkdirSync(registryBin, { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); try { @@ -92,7 +95,7 @@ test("findXlingsExecutable finds the xlings bundled in $MCPP_HOME/registry/bin", test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME is unset", () => { const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-home-")); const registryBin = path.join(home, ".mcpp", "registry", "bin"); - const xlingsPath = path.join(registryBin, "xlings"); + const xlingsPath = path.join(registryBin, xlingsBinaryName); mkdirSync(registryBin, { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); try { @@ -107,7 +110,7 @@ test("findXlingsExecutable falls back to $HOME/.mcpp/registry/bin when MCPP_HOME test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-vendored-")); - const vendored = path.join(root, "opt-mcpp", "registry", "bin", "xlings"); + const vendored = path.join(root, "opt-mcpp", "registry", "bin", xlingsBinaryName); mkdirSync(path.dirname(vendored), { recursive: true }); writeFileSync(vendored, "#!/bin/sh\n"); try { @@ -122,7 +125,7 @@ test("findXlingsExecutable honors MCPP_VENDORED_XLINGS", () => { test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-selfenv-")); - const xlingsPath = path.join(root, "registry", "bin", "xlings"); + const xlingsPath = path.join(root, "registry", "bin", xlingsBinaryName); mkdirSync(path.dirname(xlingsPath), { recursive: true }); writeFileSync(xlingsPath, "#!/bin/sh\n"); const runner = async () => ({ @@ -140,20 +143,47 @@ test("resolveXlingsExecutable reads the xlings binary from `mcpp self env`", asy } }); -test("resolveXlingsExecutable falls back when the reported path does not exist", async () => { +test("resolveXlingsExecutable passes a timeout to `mcpp self env`", async () => { + let captured: { timeoutMs?: number } | undefined; + const runner = async ( + _executable: string, + _args: string[], + _cwd?: string, + options?: { timeoutMs?: number }, + ) => { + captured = options; + return { exitCode: 0, stdout: "", stderr: "" }; + }; + await resolveXlingsExecutable("/tools/mcpp", runner); + assert.equal(captured?.timeoutMs, 60_000); +}); + +test("resolveXlingsExecutable falls back to path probing when the reported path does not exist", async () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-missing-")); const runner = async () => ({ exitCode: 0, stdout: "xlings binary = /no/such/xlings\n", stderr: "", }); - const result = await resolveXlingsExecutable("/tools/mcpp", runner); - // Fallback heuristics find nothing in this environment, so the result is - // undefined unless a standalone ~/.xlings or PATH xlings happens to exist. - assert.ok(result === undefined || typeof result === "string"); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), + undefined, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } }); -test("resolveXlingsExecutable falls back when `mcpp self env` fails", async () => { +test("resolveXlingsExecutable falls back to path probing when `mcpp self env` fails", async () => { + const home = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-fallback-fail-")); const runner = async () => ({ exitCode: 1, stdout: "", stderr: "boom\n" }); - const result = await resolveXlingsExecutable("/tools/mcpp", runner); - assert.ok(result === undefined || typeof result === "string"); + try { + assert.equal( + await resolveXlingsExecutable("/tools/mcpp", runner, { home, env: {} }), + undefined, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } });