diff --git a/packages/adapter-rust/src/rust-debug-adapter.ts b/packages/adapter-rust/src/rust-debug-adapter.ts index 71d8bc51..21ff0dd1 100644 --- a/packages/adapter-rust/src/rust-debug-adapter.ts +++ b/packages/adapter-rust/src/rust-debug-adapter.ts @@ -38,7 +38,7 @@ import { } from '@debugmcp/shared'; import { DebugLanguage } from '@debugmcp/shared'; import { AdapterDependencies } from '@debugmcp/shared'; -import { resolveCodeLLDBExecutable } from './utils/codelldb-resolver.js'; +import { resolveCodeLLDBExecutable, resolveCodeLLDBExecutableSyncImpl } from './utils/codelldb-resolver.js'; import { checkCargoInstallation, checkRustInstallation, @@ -747,53 +747,12 @@ export class RustDebugAdapter extends EventEmitter implements IDebugAdapter { } private resolveCodeLLDBExecutableSync(): string | null { - // Determine platform directory (same logic as async resolver) - const platform = this.platform; - const arch = process.arch; - - let platformDir = ''; - if (platform === 'win32') { - platformDir = 'win32-x64'; - } else if (platform === 'darwin') { - platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; - } else if (platform === 'linux') { - platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; - } else { - return null; - } - - const executableName = platform === 'win32' ? 'codelldb.exe' : 'codelldb'; - const candidatePaths = [ - // When executing via ts-node/ts-node-esm within the package - path.resolve(__dirname, '..', 'vendor', 'codelldb', platformDir, 'adapter', executableName), - // When executing from the compiled workspace distribution (dist/packages/adapter-rust/src) - path.resolve(__dirname, '..', '..', '..', '..', 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'adapter', executableName), - // Fallback to workspace-relative resolution from CWD (handles unusual launchers) - path.resolve(process.cwd(), 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'adapter', executableName) - ]; - - for (const candidate of candidatePaths) { - try { - if (existsSync(candidate)) { - return candidate; - } - } catch { - // Try next candidate - } - } - - // Check environment variable as fallback - if (process.env.CODELLDB_PATH) { - try { - if (existsSync(process.env.CODELLDB_PATH)) { - return process.env.CODELLDB_PATH; - } - } catch { - // Fall through - } - } - - return null; + // Shared candidate walk (issue #265). This file compiles one directory + // shallower than the resolver, so the package root is one hop up. + return resolveCodeLLDBExecutableSyncImpl({ + platform: this.platform, + packageRoot: path.resolve(__dirname, '..') + }); } getAdapterModuleName(): string { diff --git a/packages/adapter-rust/src/utils/codelldb-resolver.ts b/packages/adapter-rust/src/utils/codelldb-resolver.ts index 857423ac..1bc2baf9 100644 --- a/packages/adapter-rust/src/utils/codelldb-resolver.ts +++ b/packages/adapter-rust/src/utils/codelldb-resolver.ts @@ -1,9 +1,13 @@ /** * CodeLLDB executable resolver + * + * Single home for the platform-dir mapping and vendor candidate-path walk + * (issue #265). The sync entry point exists for callers that cannot await + * (adapter command construction); both share the same candidate list. */ import * as fs from 'fs/promises'; -import { constants as fsConstants } from 'fs'; +import { constants as fsConstants, existsSync } from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; @@ -14,36 +18,140 @@ const __dirname = path.dirname(__filename); export const DEFAULT_CODELLDB_VERSION = '1.11.8'; /** - * Resolve the CodeLLDB executable path based on platform + * The vendor directory names produced by scripts/vendor-codelldb.js. + * Drift-guarded against the script's PLATFORMS table (and the root + * scripts/check-adapters.js list) in codelldb-resolver.test.ts. */ -export async function resolveCodeLLDBExecutable(): Promise { - const platform = process.platform; - const arch = process.arch; - - // Determine platform directory - let platformDir = ''; +export const SUPPORTED_CODELLDB_PLATFORM_DIRS = [ + 'win32-x64', + 'darwin-x64', + 'darwin-arm64', + 'linux-x64', + 'linux-arm64' +] as const; + +export type CodeLLDBPlatformDir = (typeof SUPPORTED_CODELLDB_PLATFORM_DIRS)[number]; + +/** + * Map platform/arch to the vendored CodeLLDB directory name. + * Windows always maps to x64 — there is no win32-arm64 vendor build. + */ +export function getCodeLLDBPlatformDir( + platform: NodeJS.Platform, + arch: string +): CodeLLDBPlatformDir | null { if (platform === 'win32') { - platformDir = 'win32-x64'; - } else if (platform === 'darwin') { - platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; - } else if (platform === 'linux') { - platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; - } else { - return null; + return 'win32-x64'; + } + if (platform === 'darwin') { + return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; } - - // Build path to vendored CodeLLDB - const executableName = platform === 'win32' ? 'codelldb.exe' : 'codelldb'; - const candidatePaths = [ - // Package root (production install) - path.resolve(__dirname, '..', '..', 'vendor', 'codelldb', platformDir, 'adapter', executableName), + if (platform === 'linux') { + return arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; + } + return null; +} + +export function getCodeLLDBExecutableName(platform: NodeJS.Platform): string { + return platform === 'win32' ? 'codelldb.exe' : 'codelldb'; +} + +/** + * Build the ordered candidate paths for a file under the vendored CodeLLDB + * tree. `packageRoot` must be the adapter-rust package root — callers compute + * it from their own module location (source files sit at different depths, so + * a shared __dirname-relative walk would resolve differently per caller). + */ +export function buildVendorCandidatePaths( + packageRoot: string, + platformDir: string, + ...suffix: string[] +): string[] { + return [ + // Package root (production install: vendor/ ships next to dist/) + path.resolve(packageRoot, 'vendor', 'codelldb', platformDir, ...suffix), // Backward compatibility for older builds that expected vendor under dist/ - path.resolve(__dirname, '..', 'vendor', 'codelldb', platformDir, 'adapter', executableName), + path.resolve(packageRoot, 'dist', 'vendor', 'codelldb', platformDir, ...suffix), // Monorepo source tree fallbacks - path.resolve(__dirname, '..', '..', '..', '..', 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'adapter', executableName), - path.resolve(process.cwd(), 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'adapter', executableName) + path.resolve(packageRoot, '..', '..', 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, ...suffix), + path.resolve(process.cwd(), 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, ...suffix) ]; - +} + +/** Package root as seen from this compiled file (dist/utils → two hops up). */ +function defaultPackageRoot(): string { + return path.resolve(__dirname, '..', '..'); +} + +/** + * Synchronous resolver core. Platform/arch default to the live process values + * READ AT CALL TIME (tests stub the process global; RustDebugAdapter injects + * its constructor platform override, issue #186). The existence probe is + * injectable so tests stay hermetic without mocking `fs`. + */ +export function resolveCodeLLDBExecutableSyncImpl(options?: { + platform?: NodeJS.Platform; + arch?: string; + packageRoot?: string; + exists?: (p: string) => boolean; +}): string | null { + const platform = options?.platform ?? process.platform; + const arch = options?.arch ?? process.arch; + const packageRoot = options?.packageRoot ?? defaultPackageRoot(); + const exists = options?.exists ?? existsSync; + + const platformDir = getCodeLLDBPlatformDir(platform, arch); + if (!platformDir) { + return null; + } + + const candidatePaths = buildVendorCandidatePaths( + packageRoot, + platformDir, + 'adapter', + getCodeLLDBExecutableName(platform) + ); + + for (const candidate of candidatePaths) { + try { + if (exists(candidate)) { + return candidate; + } + } catch { + // Try next candidate + } + } + + // Check environment variable as fallback (after vendored candidates) + if (process.env.CODELLDB_PATH) { + try { + if (exists(process.env.CODELLDB_PATH)) { + return process.env.CODELLDB_PATH; + } + } catch { + // Fall through + } + } + + return null; +} + +/** + * Resolve the CodeLLDB executable path based on platform + */ +export async function resolveCodeLLDBExecutable(): Promise { + const platformDir = getCodeLLDBPlatformDir(process.platform, process.arch); + if (!platformDir) { + return null; + } + + const candidatePaths = buildVendorCandidatePaths( + defaultPackageRoot(), + platformDir, + 'adapter', + getCodeLLDBExecutableName(process.platform) + ); + for (const candidate of candidatePaths) { try { await fs.access(candidate, fsConstants.F_OK); @@ -52,7 +160,7 @@ export async function resolveCodeLLDBExecutable(): Promise { // Try next candidate } } - + // Check environment variable as fallback if (process.env.CODELLDB_PATH) { try { @@ -62,7 +170,7 @@ export async function resolveCodeLLDBExecutable(): Promise { // Fall through } } - + return null; } @@ -71,34 +179,22 @@ export async function resolveCodeLLDBExecutable(): Promise { */ export async function getCodeLLDBVersion(): Promise { const codelldbPath = await resolveCodeLLDBExecutable(); - + if (!codelldbPath) { return null; } - - // Try to get version from manifest file - // NOTE: Platform detection is intentionally duplicated from resolveCodeLLDBExecutable() - // because the two functions may be called independently, and extracting a shared helper - // would add coupling without meaningful benefit for this small mapping. - const platform = process.platform; - const arch = process.arch; - - let platformDir = ''; - if (platform === 'win32') { - platformDir = 'win32-x64'; - } else if (platform === 'darwin') { - platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; - } else if (platform === 'linux') { - platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'; + + const platformDir = getCodeLLDBPlatformDir(process.platform, process.arch); + if (!platformDir) { + return DEFAULT_CODELLDB_VERSION; } - - const versionFileCandidates = [ - path.resolve(__dirname, '..', '..', 'vendor', 'codelldb', platformDir, 'version.json'), - path.resolve(__dirname, '..', 'vendor', 'codelldb', platformDir, 'version.json'), - path.resolve(__dirname, '..', '..', '..', '..', 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'version.json'), - path.resolve(process.cwd(), 'packages', 'adapter-rust', 'vendor', 'codelldb', platformDir, 'version.json') - ]; - + + const versionFileCandidates = buildVendorCandidatePaths( + defaultPackageRoot(), + platformDir, + 'version.json' + ); + for (const versionFile of versionFileCandidates) { try { const versionData = await fs.readFile(versionFile, 'utf-8'); @@ -108,6 +204,6 @@ export async function getCodeLLDBVersion(): Promise { // Continue to next candidate } } - + return DEFAULT_CODELLDB_VERSION; // Default version fallback } diff --git a/packages/adapter-rust/tests/codelldb-resolver.test.ts b/packages/adapter-rust/tests/codelldb-resolver.test.ts index 519570d9..18ec58e7 100644 --- a/packages/adapter-rust/tests/codelldb-resolver.test.ts +++ b/packages/adapter-rust/tests/codelldb-resolver.test.ts @@ -19,7 +19,12 @@ vi.mock('fs/promises', () => ({ import { resolveCodeLLDBExecutable, getCodeLLDBVersion, - DEFAULT_CODELLDB_VERSION + DEFAULT_CODELLDB_VERSION, + getCodeLLDBPlatformDir, + getCodeLLDBExecutableName, + SUPPORTED_CODELLDB_PLATFORM_DIRS, + buildVendorCandidatePaths, + resolveCodeLLDBExecutableSyncImpl } from '../src/utils/codelldb-resolver.js'; const realProcess = process; @@ -181,4 +186,146 @@ describe('codelldb-resolver', () => { expect(match![1]).toBe(DEFAULT_CODELLDB_VERSION); }); }); + + describe('getCodeLLDBPlatformDir', () => { + it.each([ + ['win32', 'x64', 'win32-x64'], + ['win32', 'arm64', 'win32-x64'], // win32 always maps to x64 (no arm64 vendor build) + ['darwin', 'x64', 'darwin-x64'], + ['darwin', 'arm64', 'darwin-arm64'], + ['linux', 'x64', 'linux-x64'], + ['linux', 'arm64', 'linux-arm64'] + ])('maps %s/%s to %s', (platform, arch, expected) => { + expect(getCodeLLDBPlatformDir(platform as NodeJS.Platform, arch)).toBe(expected); + }); + + it('returns null for unsupported platforms', () => { + expect(getCodeLLDBPlatformDir('freebsd' as NodeJS.Platform, 'x64')).toBeNull(); + expect(getCodeLLDBPlatformDir('aix' as NodeJS.Platform, 'ppc64')).toBeNull(); + }); + }); + + describe('getCodeLLDBExecutableName', () => { + it('appends .exe on Windows only', () => { + expect(getCodeLLDBExecutableName('win32')).toBe('codelldb.exe'); + expect(getCodeLLDBExecutableName('linux')).toBe('codelldb'); + expect(getCodeLLDBExecutableName('darwin')).toBe('codelldb'); + }); + }); + + describe('buildVendorCandidatePaths', () => { + it('produces the four vendored candidates in precedence order', () => { + const pkgRoot = path.resolve('/repo/packages/adapter-rust'); + const candidates = buildVendorCandidatePaths(pkgRoot, 'linux-x64', 'adapter', 'codelldb'); + + expect(candidates).toEqual([ + path.join(pkgRoot, 'vendor', 'codelldb', 'linux-x64', 'adapter', 'codelldb'), + path.join(pkgRoot, 'dist', 'vendor', 'codelldb', 'linux-x64', 'adapter', 'codelldb'), + path.resolve(pkgRoot, '..', '..', 'packages', 'adapter-rust', 'vendor', 'codelldb', 'linux-x64', 'adapter', 'codelldb'), + path.resolve(process.cwd(), 'packages', 'adapter-rust', 'vendor', 'codelldb', 'linux-x64', 'adapter', 'codelldb') + ]); + }); + + it('supports arbitrary suffix segments (version.json)', () => { + const candidates = buildVendorCandidatePaths(path.resolve('/pkg'), 'win32-x64', 'version.json'); + + expect(candidates).toHaveLength(4); + for (const candidate of candidates) { + expect(candidate.endsWith(path.join('codelldb', 'win32-x64', 'version.json'))).toBe(true); + } + }); + }); + + describe('resolveCodeLLDBExecutableSyncImpl', () => { + it('returns null on unsupported platforms without probing', () => { + const exists = vi.fn(); + + expect( + resolveCodeLLDBExecutableSyncImpl({ platform: 'freebsd' as NodeJS.Platform, exists }) + ).toBeNull(); + expect(exists).not.toHaveBeenCalled(); + }); + + it('returns the first existing candidate', () => { + const pkgRoot = path.resolve('/pkg'); + const expected = buildVendorCandidatePaths(pkgRoot, 'linux-x64', 'adapter', 'codelldb'); + const exists = vi.fn((p: string) => p === expected[1]); + + const result = resolveCodeLLDBExecutableSyncImpl({ + platform: 'linux', + arch: 'x64', + packageRoot: pkgRoot, + exists + }); + + expect(result).toBe(expected[1]); + expect(exists).toHaveBeenCalledTimes(2); + }); + + it('probes candidates rooted at the provided packageRoot', () => { + const pkgRoot = path.resolve('/elsewhere/adapter-rust'); + const exists = vi.fn().mockReturnValue(false); + + resolveCodeLLDBExecutableSyncImpl({ platform: 'linux', arch: 'arm64', packageRoot: pkgRoot, exists }); + + const probed = exists.mock.calls.map((c) => c[0] as string); + expect(probed[0]).toBe(path.join(pkgRoot, 'vendor', 'codelldb', 'linux-arm64', 'adapter', 'codelldb')); + expect(probed[1]).toBe(path.join(pkgRoot, 'dist', 'vendor', 'codelldb', 'linux-arm64', 'adapter', 'codelldb')); + }); + + it('probes codelldb.exe when the injected platform is win32', () => { + const exists = vi.fn().mockReturnValue(true); + + const result = resolveCodeLLDBExecutableSyncImpl({ platform: 'win32', arch: 'x64', exists }); + + expect(result?.endsWith(path.join('win32-x64', 'adapter', 'codelldb.exe'))).toBe(true); + }); + + it('falls back to CODELLDB_PATH only after all four candidates miss', () => { + vi.stubEnv('CODELLDB_PATH', '/custom/sync/codelldb'); + const exists = vi.fn((p: string) => p === '/custom/sync/codelldb'); + + const result = resolveCodeLLDBExecutableSyncImpl({ platform: 'linux', arch: 'x64', exists }); + + expect(result).toBe('/custom/sync/codelldb'); + expect(exists).toHaveBeenCalledTimes(5); + }); + + it('returns null when no candidate nor CODELLDB_PATH exists', () => { + vi.stubEnv('CODELLDB_PATH', '/missing/codelldb'); + const exists = vi.fn().mockReturnValue(false); + + expect(resolveCodeLLDBExecutableSyncImpl({ platform: 'linux', arch: 'x64', exists })).toBeNull(); + expect(exists).toHaveBeenCalledTimes(5); + }); + + it('defaults platform and arch from process at call time', () => { + stubPlatform('darwin', 'arm64'); + const exists = vi.fn().mockReturnValue(true); + + const result = resolveCodeLLDBExecutableSyncImpl({ exists }); + + expect(result).toContain('darwin-arm64'); + }); + }); + + describe('platform table drift guards', () => { + it('matches the PLATFORMS keys in scripts/vendor-codelldb.js', () => { + const source = readFileSync(new URL('../scripts/vendor-codelldb.js', import.meta.url), 'utf-8'); + const block = source.match(/const PLATFORMS = \{([\s\S]*?)\n\};/); + + expect(block).not.toBeNull(); + const keys = [...block![1].matchAll(/'([a-z0-9-]+)':\s*\{/g)].map((m) => m[1]); + expect([...keys].sort()).toEqual([...SUPPORTED_CODELLDB_PLATFORM_DIRS].sort()); + }); + + it('matches the Rust platforms list in scripts/check-adapters.js', () => { + const source = readFileSync(new URL('../../../scripts/check-adapters.js', import.meta.url), 'utf-8'); + const match = source.match(/platforms:\s*\[([^\]]+)\]/); + + expect(match).not.toBeNull(); + const keys = match![1].split(',').map((s) => s.trim().replace(/['"]/g, '')); + expect([...keys].sort()).toEqual([...SUPPORTED_CODELLDB_PLATFORM_DIRS].sort()); + }); + }); });