From e28c4643b855e80627443c5bdabacc341c4880b5 Mon Sep 17 00:00:00 2001 From: FunToHard Date: Sat, 7 Nov 2026 19:26:14 +0530 Subject: [PATCH 1/5] fix(prober): prioritize desktop Qt kits over cross-compilation toolchains Resolves issue where Qt filesystem probing selected android_arm64_v8a over mingw_64 due to alphabetical ordering and unpenalized cross-compilation kits. Implements intelligent scoring favoring desktop toolchains matching active compiler (MinGW vs MSVC), adds automatic CMakeCache/workspace compiler inspection, purges obsolete cross-target Qt includes from .clangd, and adds c-cpp-pro.discovery.qtToolchain setting. --- package.json | 7 +- src/intelligence/header-hover-provider.ts | 2 +- src/prober/external-sdk-detector.ts | 378 ++++++++++++++++-- src/prober/flag-synthesizer.ts | 14 +- .../compilation-database-generator.ts | 5 +- src/substrate/daemon-manager.ts | 9 +- test/external-sdk-detector.test.ts | 144 ++++++- wiki/Compiler-Toolchain-Setup.md | 13 + 8 files changed, 533 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 0ca74df..db09df2 100644 --- a/package.json +++ b/package.json @@ -427,7 +427,12 @@ "c-cpp-pro.discovery.detectExternalSdks": { "type": "boolean", "default": true, - "description": "Auto-detect common external SDKs (Vulkan SDK, raylib, CUDA Toolkit, Boost, SDL) for fallback IntelliSense flags and flag synthesis." + "description": "Auto-detect common external SDKs (Qt, Vulkan SDK, raylib, CUDA Toolkit, Boost, SDL) for fallback IntelliSense flags and flag synthesis." + }, + "c-cpp-pro.discovery.qtToolchain": { + "type": "string", + "default": "", + "description": "Preferred Qt toolchain kit subfolder (e.g. 'mingw_64', 'msvc2022_64', 'gcc_64'). If empty, automatically selects the best matching desktop toolchain based on the active compiler." }, "c-cpp-pro.default.forcedInclude": { "type": "array", diff --git a/src/intelligence/header-hover-provider.ts b/src/intelligence/header-hover-provider.ts index 4eeb509..46b8203 100644 --- a/src/intelligence/header-hover-provider.ts +++ b/src/intelligence/header-hover-provider.ts @@ -1250,7 +1250,7 @@ export function getCachedWorkspaceSdkIncludes(wsRoot: string): string[] { if (cached && (now - cached.timestamp) < SDK_INCLUDE_CACHE_TTL_MS) { return cached.paths; } - const paths = ExternalSdkDetector.getWorkspaceIncludePaths(wsRoot); + const paths = ExternalSdkDetector.getWorkspaceIncludePaths(wsRoot, { workspaceRoot: wsRoot }); workspaceSdkIncludeCache.set(canon, { timestamp: now, paths }); return paths; } diff --git a/src/prober/external-sdk-detector.ts b/src/prober/external-sdk-detector.ts index b45d77a..4d9be67 100644 --- a/src/prober/external-sdk-detector.ts +++ b/src/prober/external-sdk-detector.ts @@ -14,6 +14,228 @@ export interface ExternalSdkDetectorOptions { env?: Record; allowFsScan?: boolean; requireWorkspaceMatch?: boolean; + workspaceRoot?: string; + preferredCompiler?: 'mingw' | 'gcc' | 'msvc' | 'clang' | string; + preferredToolchain?: string; + preferredArch?: 'x64' | 'x86' | 'arm64' | string; + targetPlatform?: 'desktop' | 'android' | 'wasm' | 'ios' | string; + qtInstallDir?: string; +} + +/** + * Detects preferred C/C++ compiler toolchain ('mingw', 'msvc', or 'clang') from options, + * environment variables, workspace build configuration files, or system PATH. + */ +export function detectCompilerPreference( + options: ExternalSdkDetectorOptions = {} +): 'mingw' | 'msvc' | 'clang' | undefined { + if (options.preferredCompiler) { + const norm = options.preferredCompiler.toLowerCase(); + if (norm.includes('gcc') || norm.includes('mingw')) return 'mingw'; + if (norm.includes('msvc') || norm.includes('cl')) return 'msvc'; + if (norm.includes('clang') || norm.includes('llvm')) return 'clang'; + } + + const env = options.env ?? process.env; + + // 1. Check explicit Qt kit environment variables + const envToolchain = (env.QT_TOOLCHAIN || env.QT_KIT || '').toLowerCase(); + if (envToolchain.includes('mingw') || envToolchain.includes('gcc')) return 'mingw'; + if (envToolchain.includes('msvc') || envToolchain.includes('cl')) return 'msvc'; + if (envToolchain.includes('clang') || envToolchain.includes('llvm')) return 'clang'; + + // 2. Check MSYSTEM or MinGW-specific environment variables + if (env.MSYSTEM || env.MINGW_PREFIX || env.MINGW_CHOST) return 'mingw'; + + // 3. Check Visual Studio / MSVC developer environment variables + if (env.VCINSTALLDIR || env.VSINSTALLDIR || env.VisualStudioVersion) return 'msvc'; + + // 4. Check CC and CXX environment variables + const cxx = (env.CXX || env.CC || '').toLowerCase(); + if (cxx.includes('g++') || cxx.includes('gcc') || cxx.includes('mingw')) return 'mingw'; + if (/(?:^|[\\/])cl(?:\.exe)?$/i.test(cxx)) return 'msvc'; + if (cxx.includes('clang')) return 'clang'; + + // 5. Inspect workspace build files if workspaceRoot is available + const wsRoot = options.workspaceRoot; + if (wsRoot) { + try { + // CMakeCache.txt + const cmakeCacheCandidates = [ + path.join(wsRoot, 'CMakeCache.txt'), + path.join(wsRoot, 'build', 'CMakeCache.txt'), + path.join(wsRoot, 'out', 'CMakeCache.txt'), + path.join(wsRoot, 'build', 'Debug', 'CMakeCache.txt'), + path.join(wsRoot, 'build', 'Release', 'CMakeCache.txt') + ]; + for (const cacheFile of cmakeCacheCandidates) { + if (fs.existsSync(cacheFile)) { + const cacheContent = fs.readFileSync(cacheFile, 'utf8'); + if ( + /CMAKE_CXX_COMPILER:FILEPATH=.*(?:g\+\+|gcc|mingw)/i.test(cacheContent) || + /CMAKE_GENERATOR:INTERNAL=.*MinGW/i.test(cacheContent) + ) { + return 'mingw'; + } + if ( + /CMAKE_CXX_COMPILER:FILEPATH=.*(?:cl(?:\.exe)?)/i.test(cacheContent) || + /CMAKE_GENERATOR:INTERNAL=.*Visual Studio/i.test(cacheContent) + ) { + return 'msvc'; + } + if (/CMAKE_CXX_COMPILER:FILEPATH=.*clang/i.test(cacheContent)) { + return 'clang'; + } + } + } + + // Visual Studio solutions + const rootFiles = fs.readdirSync(wsRoot); + if (rootFiles.some((f) => f.endsWith('.sln') || f.endsWith('.slnx') || f.endsWith('.vcxproj'))) { + return 'msvc'; + } + + // Makefile on Windows without solution files strongly indicates MinGW/GCC + if (process.platform === 'win32' && rootFiles.some((f) => /^makefile$/i.test(f) || /^gnumakefile$/i.test(f))) { + return 'mingw'; + } + + // compile_commands.json + const compDbCandidates = [ + path.join(wsRoot, 'compile_commands.json'), + path.join(wsRoot, 'build', 'compile_commands.json') + ]; + for (const compDb of compDbCandidates) { + if (fs.existsSync(compDb)) { + const firstChunk = fs.readFileSync(compDb, { encoding: 'utf8', flag: 'r' }).slice(0, 4096); + if ( + firstChunk.includes('g++') || + firstChunk.includes('gcc') || + firstChunk.includes('--driver-mode=g++') || + firstChunk.includes('mingw') + ) { + return 'mingw'; + } + if (firstChunk.includes('cl.exe') || firstChunk.includes('--driver-mode=cl')) { + return 'msvc'; + } + if (firstChunk.includes('clang')) { + return 'clang'; + } + } + } + } catch { + // Ignore filesystem inspection errors + } + } + + // 6. Inspect system PATH for toolchains + const pathEnv = (env.PATH || env.Path || '').toLowerCase(); + const hasMinGwOnPath = pathEnv.includes('mingw') || pathEnv.includes('msys64'); + const hasMsvcOnPath = pathEnv.includes('vc\\tools\\msvc') || pathEnv.includes('microsoft visual studio'); + + if (hasMinGwOnPath && !hasMsvcOnPath) return 'mingw'; + if (hasMsvcOnPath && !hasMinGwOnPath) return 'msvc'; + + return undefined; +} + +/** + * Computes a suitability score for a Qt toolchain kit directory name. + * Heavily penalizes cross-compilation / mobile / web toolchains (android, wasm, ios, qnx) + * during desktop development to ensure desktop kits (mingw, msvc, gcc, clang) are preferred. + */ +export function scoreQtToolchain( + tcName: string, + options: ExternalSdkDetectorOptions = {} +): number { + const lower = tcName.toLowerCase(); + + // 1. Cross-compilation / mobile / embedded target detection + const isCrossTarget = /^(?:android|wasm|ios|qnx|integrity|winrt)/i.test(lower); + const targetPlatform = options.targetPlatform?.toLowerCase(); + const wantCross = Boolean( + targetPlatform && + targetPlatform !== 'desktop' && + lower.startsWith(targetPlatform) + ); + + if (isCrossTarget && !wantCross) { + // Severe penalty: mobile/embedded cross-compilation toolchains should never be selected for desktop projects + return -100_000; + } + if (wantCross) { + return 50_000; + } + + // 2. Explicit toolchain match + const env = options.env ?? process.env; + const explicitTc = (options.preferredToolchain || env.QT_TOOLCHAIN || env.QT_KIT || '').toLowerCase(); + if (explicitTc && lower.includes(explicitTc)) { + return 80_000; + } + + let score = 10_000; // Base score for host desktop toolchains + + // 3. Compiler preference matching + const prefCompiler = detectCompilerPreference(options); + if (prefCompiler === 'mingw') { + if (lower.includes('mingw')) score += 40_000; + else if (lower.includes('gcc')) score += 30_000; + else if (lower.includes('clang') || lower.includes('llvm')) score += 10_000; + else if (lower.includes('msvc')) score += 2_000; + } else if (prefCompiler === 'msvc') { + if (lower.includes('msvc')) score += 40_000; + else if (lower.includes('clang') || lower.includes('llvm')) score += 10_000; + else if (lower.includes('mingw')) score += 2_000; + } else if (prefCompiler === 'clang') { + if (lower.includes('clang') || lower.includes('llvm')) score += 40_000; + else if (lower.includes('mingw')) score += 10_000; + else if (lower.includes('msvc')) score += 10_000; + } else { + // Default desktop priority by operating system + if (process.platform === 'win32') { + if (lower.includes('mingw')) score += 20_000; + if (lower.includes('msvc')) score += 20_000; + if (lower.includes('clang') || lower.includes('llvm')) score += 15_000; + } else if (process.platform === 'linux') { + if (lower.includes('gcc')) score += 20_000; + if (lower.includes('clang')) score += 15_000; + } else if (process.platform === 'darwin') { + if (lower.includes('macos') || lower.includes('clang')) score += 20_000; + } + } + + // 4. Architecture preference matching + const prefArch = options.preferredArch?.toLowerCase() || process.arch; + if (prefArch === 'x64') { + if (lower.includes('64') || lower.includes('x86_64') || lower.includes('amd64')) { + score += 5_000; + } + if ((lower.includes('32') || lower.includes('x86')) && !lower.includes('64')) { + score -= 3_000; + } + } else if (prefArch === 'arm64') { + if (lower.includes('arm64')) { + score += 5_000; + } + } else if (prefArch === 'ia32' || prefArch === 'x86') { + if (lower.includes('32') || lower.includes('x86')) { + score += 5_000; + } + } + + // 5. Toolchain recency / version ties + if (lower.includes('msvc2022')) score += 500; + else if (lower.includes('msvc2019')) score += 400; + else if (lower.includes('msvc2017')) score += 300; + else if (lower.includes('msvc2015')) score += 200; + + if (lower.includes('mingw13')) score += 500; + else if (lower.includes('mingw11')) score += 400; + else if (lower.includes('mingw8')) score += 300; + + return score; } /** @@ -592,7 +814,9 @@ export class ExternalSdkDetector { if (!allowFsScan) return null; // 2. Scan standard installation folders + const customBase = options.qtInstallDir || env.QT_BASE_DIR; const baseCandidates = [ + ...(customBase ? [customBase] : []), 'C:\\Qt', 'D:\\Qt', 'F:\\Apps\\Qt', @@ -635,40 +859,88 @@ export class ExternalSdkDetector { }; } - // Base is a Qt installation directory with version folders + // Base is a Qt installation directory with version folders (e.g. C:\Qt\6.8.0, C:\Qt\6.5.3) const entries = fs.readdirSync(base, { withFileTypes: true }); const versionDirs = entries .filter((e) => e.isDirectory() && /^\d+(\.\d+)+$/.test(e.name)) .map((e) => e.name) .sort(compareSemverDesc); + interface QtCandidate { + version: string; + toolchain: string; + rootPath: string; + includePaths: string[]; + libraryPaths?: string[]; + isDesktop: boolean; + score: number; + } + + const candidates: QtCandidate[] = []; + for (const vDir of versionDirs) { const vPath = path.join(base, vDir); - const toolchains = fs.readdirSync(vPath, { withFileTypes: true }); - const preferredTc = toolchains - .filter((t) => t.isDirectory()) - .map((t) => t.name) - .sort((a, b) => { - if (a.includes('msvc') && !b.includes('msvc')) return -1; - if (!a.includes('msvc') && b.includes('msvc')) return 1; - return 0; - }); - - for (const tc of preferredTc) { - const tcPath = path.join(vPath, tc); - const incDir = path.join(tcPath, 'include'); - if (fs.existsSync(incDir)) { - const includePaths = collectQtIncludes(incDir); - const libDir = path.join(tcPath, 'lib'); - return { - name: 'Qt', - version: vDir, - rootPath: tcPath, - includePaths, - libraryPaths: fs.existsSync(libDir) ? [libDir] : undefined, - source: 'filesystem' - }; + try { + const toolchains = fs.readdirSync(vPath, { withFileTypes: true }); + for (const tc of toolchains) { + if (!tc.isDirectory()) continue; + const tcPath = path.join(vPath, tc.name); + const incDir = path.join(tcPath, 'include'); + if (fs.existsSync(incDir)) { + const includePaths = collectQtIncludes(incDir); + const hasQtIndicators = + includePaths.length > 1 || + fs.existsSync(path.join(incDir, 'qglobal.h')) || + fs.existsSync(path.join(incDir, 'QtCore', 'qstring.h')) || + fs.existsSync(path.join(incDir, 'qtcore-config.h')); + if (!hasQtIndicators) continue; + + const isCross = /^(?:android|wasm|ios|qnx|integrity|winrt)/i.test(tc.name); + const score = scoreQtToolchain(tc.name, options); + const libDir = path.join(tcPath, 'lib'); + + candidates.push({ + version: vDir, + toolchain: tc.name, + rootPath: tcPath, + includePaths, + libraryPaths: fs.existsSync(libDir) ? [libDir] : undefined, + isDesktop: !isCross, + score + }); + } } + } catch { + // Ignore folder read error + } + } + + if (candidates.length > 0) { + // If desktop toolchains exist, eliminate cross-compilation toolchains unless cross-compilation was explicitly requested + const wantCross = Boolean( + options.targetPlatform && + options.targetPlatform !== 'desktop' && + ['android', 'wasm', 'ios', 'qnx'].includes(options.targetPlatform.toLowerCase()) + ); + const validCandidates = (!wantCross && candidates.some((c) => c.isDesktop)) + ? candidates.filter((c) => c.isDesktop) + : candidates; + + validCandidates.sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + return compareSemverDesc(a.version, b.version); + }); + + const best = validCandidates[0]; + if (best) { + return { + name: 'Qt', + version: best.version, + rootPath: best.rootPath, + includePaths: best.includePaths, + libraryPaths: best.libraryPaths, + source: 'filesystem' + }; } } } catch { @@ -786,7 +1058,9 @@ export class ExternalSdkDetector { ], Qt: [ /#\s*include\s*[<"]Q[A-Z]/, + /#\s*include\s*[<"]q[a-z0-9_]+\.h[>"]/i, /#\s*include\s*[<"]Qt[A-Za-z]+\//i, + /\bQT\s*\+?=/i, /find_package\s*\(\s*Qt[56]?/i, /qt_add_executable/i, /qt_add_resources/i, @@ -853,7 +1127,13 @@ export class ExternalSdkDetector { usedSdks.add('CUDA'); } - if (sourceExts.has(ext) || buildFileNames.has(lowerName) || lowerName.endsWith('.vcxproj')) { + if ( + sourceExts.has(ext) || + buildFileNames.has(lowerName) || + lowerName.endsWith('.vcxproj') || + ext === '.pro' || + ext === '.pri' + ) { filesChecked++; try { const filePath = path.join(currentDir, entry.name); @@ -904,7 +1184,11 @@ export class ExternalSdkDetector { workspaceRoot: string, options: ExternalSdkDetectorOptions = {} ): string[] { - const sdks = ExternalSdkDetector.detectAll(options); + const mergedOpts: ExternalSdkDetectorOptions = { + workspaceRoot, + ...options + }; + const sdks = ExternalSdkDetector.detectAll(mergedOpts); const usedSdkNames = ExternalSdkDetector.getUsedSdksInWorkspace( workspaceRoot, sdks.map((s) => s.name) @@ -931,15 +1215,21 @@ export class ExternalSdkDetector { * Synchronizes discovered external SDK include paths into the workspace .clangd YAML configuration. * Only synchronizes SDKs that are actually used in the workspace, avoiding polluting non-Vulkan/SDK projects. * If .clangd exists, appends flags non-destructively without overriding existing user configuration. + * Automatically purges any obsolete cross-compilation Qt include paths (e.g. android_arm64_v8a) when + * a desktop Qt kit (MinGW, MSVC) is active. */ public static syncWorkspaceClangdConfig( workspaceRoot: string, options: ExternalSdkDetectorOptions = {} ): boolean { try { + const mergedOpts: ExternalSdkDetectorOptions = { + workspaceRoot, + ...options + }; const includes = options.requireWorkspaceMatch === false - ? ExternalSdkDetector.getAllIncludePaths(options) - : ExternalSdkDetector.getWorkspaceIncludePaths(workspaceRoot, options); + ? ExternalSdkDetector.getAllIncludePaths(mergedOpts) + : ExternalSdkDetector.getWorkspaceIncludePaths(workspaceRoot, mergedOpts); if (includes.length === 0) return false; @@ -958,9 +1248,35 @@ export class ExternalSdkDetector { return true; } - const existing = fs.readFileSync(clangdPath, 'utf8'); + let existing = fs.readFileSync(clangdPath, 'utf8'); + + // If desktop Qt is detected, purge any obsolete cross-compilation Qt flags (e.g. android, wasm) + const hasDesktopQt = addFlags.some((f) => /(?:mingw|msvc|clang|gcc)[a-z0-9_]*\/include/i.test(f)); + let hadObsoleteCrossQt = false; + if (hasDesktopQt) { + const cleaned = existing + .split('\n') + .filter((line) => { + if (/-(?:I|isystem)["']?.*[/\\](?:android|wasm|ios|qnx|integrity)[a-z0-9_]*[/\\]include/i.test(line)) { + hadObsoleteCrossQt = true; + return false; + } + return true; + }) + .join('\n'); + if (hadObsoleteCrossQt) { + existing = cleaned; + } + } + const missingFlags = addFlags.filter((f) => !existing.includes(f)); - if (missingFlags.length === 0) return false; + if (missingFlags.length === 0) { + if (hadObsoleteCrossQt) { + fs.writeFileSync(clangdPath, existing, 'utf8'); + return true; + } + return false; + } if (/CompileFlags:\s*\r?\n\s*Add:/i.test(existing)) { const replacement = missingFlags.map((f) => ` - "${f}"`).join('\n'); diff --git a/src/prober/flag-synthesizer.ts b/src/prober/flag-synthesizer.ts index 0ece1fa..7363617 100644 --- a/src/prober/flag-synthesizer.ts +++ b/src/prober/flag-synthesizer.ts @@ -111,9 +111,19 @@ export class FlagSynthesizer { } if (options.detectExternalSdks !== false) { + const preferredCompiler = + compiler.type === 'gcc' || compiler.type === 'wsl-gcc' + ? 'mingw' + : compiler.type === 'msvc' + ? 'msvc' + : compiler.type; + const sdkOpts = { + workspaceRoot: options.workspaceRoot, + preferredCompiler + }; const sdkIncludes = options.workspaceRoot - ? ExternalSdkDetector.getWorkspaceIncludePaths(options.workspaceRoot) - : ExternalSdkDetector.getAllIncludePaths(); + ? ExternalSdkDetector.getWorkspaceIncludePaths(options.workspaceRoot, sdkOpts) + : ExternalSdkDetector.getAllIncludePaths(sdkOpts); for (const inc of sdkIncludes) { const flag = `-I${inc.replace(/\\/g, '/')}`; if (!flags.includes(flag)) { diff --git a/src/solution/compilation-database-generator.ts b/src/solution/compilation-database-generator.ts index 3967c6c..2d7a003 100644 --- a/src/solution/compilation-database-generator.ts +++ b/src/solution/compilation-database-generator.ts @@ -27,7 +27,10 @@ export class CompilationDatabaseGenerator { (solution.configurations.length > 0 ? solution.configurations[0].key : 'Debug|x64'); const solutionDir = path.dirname(solution.filePath); - const externalSdkIncludes = ExternalSdkDetector.getWorkspaceIncludePaths(solutionDir); + const externalSdkIncludes = ExternalSdkDetector.getWorkspaceIncludePaths(solutionDir, { + workspaceRoot: solutionDir, + preferredCompiler: compiler.type === 'msvc' ? 'msvc' : compiler.type + }); for (const project of projects) { const projectDir = isWslCompiler(compiler) diff --git a/src/substrate/daemon-manager.ts b/src/substrate/daemon-manager.ts index 301df9c..bdcf2a0 100644 --- a/src/substrate/daemon-manager.ts +++ b/src/substrate/daemon-manager.ts @@ -209,14 +209,19 @@ export class DaemonManager implements vscode.Disposable { } if (detectExternalSdks) { - const sdkIncludes = ExternalSdkDetector.getWorkspaceIncludePaths(folder.uri.fsPath); + const qtToolchain = config.get('discovery.qtToolchain', ''); + const sdkOpts = { + workspaceRoot: folder.uri.fsPath, + preferredToolchain: qtToolchain.trim() ? qtToolchain.trim() : undefined + }; + const sdkIncludes = ExternalSdkDetector.getWorkspaceIncludePaths(folder.uri.fsPath, sdkOpts); for (const inc of sdkIncludes) { const flag = `-I${inc.replace(/\\/g, '/')}`; if (!fallbackFlags.includes(flag)) { fallbackFlags.push(flag); } } - ExternalSdkDetector.syncWorkspaceClangdConfig(folder.uri.fsPath); + ExternalSdkDetector.syncWorkspaceClangdConfig(folder.uri.fsPath, sdkOpts); } } } diff --git a/test/external-sdk-detector.test.ts b/test/external-sdk-detector.test.ts index 2f61209..f7434aa 100644 --- a/test/external-sdk-detector.test.ts +++ b/test/external-sdk-detector.test.ts @@ -5,7 +5,9 @@ import * as fs from 'fs'; import * as os from 'os'; import { ExternalSdkDetector, - compareSemverDesc + compareSemverDesc, + detectCompilerPreference, + scoreQtToolchain } from '../src/prober/external-sdk-detector'; import { FlagSynthesizer } from '../src/prober/flag-synthesizer'; import { VcpkgAdvisor } from '../src/ecosystem/vcpkg-advisor'; @@ -223,6 +225,146 @@ describe('External SDKs Auto-Discovery Subsystem', () => { assert.strictEqual(result.rootPath, qtPrefix); assert.ok(result.includePaths.includes(incDir)); }); + + it('should score desktop toolchains higher than cross-compilation android toolchains', () => { + const androidScore = scoreQtToolchain('android_arm64_v8a', { targetPlatform: 'desktop' }); + const mingwScore = scoreQtToolchain('mingw_64', { targetPlatform: 'desktop' }); + const msvcScore = scoreQtToolchain('msvc2022_64', { targetPlatform: 'desktop' }); + + assert.ok(androidScore < -50000, `Expected severe penalty for android, got ${androidScore}`); + assert.ok(mingwScore > 0, `Expected positive score for mingw, got ${mingwScore}`); + assert.ok(msvcScore > 0, `Expected positive score for msvc, got ${msvcScore}`); + assert.ok(mingwScore > androidScore); + assert.ok(msvcScore > androidScore); + }); + + it('should prioritize mingw when preferredCompiler is mingw', () => { + const mingwScore = scoreQtToolchain('mingw_64', { preferredCompiler: 'mingw' }); + const msvcScore = scoreQtToolchain('msvc2022_64', { preferredCompiler: 'mingw' }); + + assert.ok(mingwScore > msvcScore, `Expected mingw (${mingwScore}) > msvc (${msvcScore})`); + }); + + it('should prioritize msvc when preferredCompiler is msvc', () => { + const mingwScore = scoreQtToolchain('mingw_64', { preferredCompiler: 'msvc' }); + const msvcScore = scoreQtToolchain('msvc2022_64', { preferredCompiler: 'msvc' }); + + assert.ok(msvcScore > mingwScore, `Expected msvc (${msvcScore}) > mingw (${mingwScore})`); + }); + + it('should prioritize mingw_64 over android_arm64_v8a in filesystem scan', () => { + const qtBase = path.join(tempDir, 'QtRoot'); + const verDir = path.join(qtBase, '6.8.0'); + + // Create android_arm64_v8a + const androidInc = path.join(verDir, 'android_arm64_v8a', 'include', 'QtCore'); + fs.mkdirSync(androidInc, { recursive: true }); + fs.writeFileSync(path.join(androidInc, 'qstring.h'), '// android qstring'); + + // Create mingw_64 + const mingwInc = path.join(verDir, 'mingw_64', 'include', 'QtCore'); + fs.mkdirSync(mingwInc, { recursive: true }); + fs.writeFileSync(path.join(mingwInc, 'qstring.h'), '// mingw qstring'); + + const result = ExternalSdkDetector.detectQt({ + qtInstallDir: qtBase, + allowFsScan: true + }); + + assert.ok(result); + assert.strictEqual(result.name, 'Qt'); + assert.strictEqual(result.version, '6.8.0'); + assert.ok( + result.rootPath.includes('mingw_64'), + `Expected mingw_64 rootPath, got: ${result.rootPath}` + ); + assert.ok( + !result.rootPath.includes('android_arm64_v8a'), + `Should not have selected android_arm64_v8a: ${result.rootPath}` + ); + }); + + it('should select older desktop Qt version over newer cross-compilation android Qt version', () => { + const qtBase = path.join(tempDir, 'QtMultiVersion'); + + // 6.8.0 only has android_arm64_v8a + const androidInc = path.join(qtBase, '6.8.0', 'android_arm64_v8a', 'include', 'QtCore'); + fs.mkdirSync(androidInc, { recursive: true }); + fs.writeFileSync(path.join(androidInc, 'qstring.h'), '// android qstring'); + + // 6.7.2 has desktop mingw_64 + const mingwInc = path.join(qtBase, '6.7.2', 'mingw_64', 'include', 'QtCore'); + fs.mkdirSync(mingwInc, { recursive: true }); + fs.writeFileSync(path.join(mingwInc, 'qstring.h'), '// mingw qstring'); + + const result = ExternalSdkDetector.detectQt({ + qtInstallDir: qtBase, + allowFsScan: true + }); + + assert.ok(result); + assert.strictEqual(result.name, 'Qt'); + assert.strictEqual(result.version, '6.7.2'); + assert.ok( + result.rootPath.includes('mingw_64'), + `Expected 6.7.2/mingw_64, got: ${result.rootPath}` + ); + }); + + it('should detect compiler preference from CMakeCache.txt referencing MinGW', () => { + const wsDir = path.join(tempDir, 'QtMinGwWs'); + fs.mkdirSync(wsDir, { recursive: true }); + fs.writeFileSync( + path.join(wsDir, 'CMakeCache.txt'), + 'CMAKE_GENERATOR:INTERNAL=MinGW Makefiles\nCMAKE_CXX_COMPILER:FILEPATH=C:/msys64/mingw64/bin/g++.exe\n' + ); + + const pref = detectCompilerPreference({ workspaceRoot: wsDir }); + assert.strictEqual(pref, 'mingw'); + }); + + it('should purge obsolete android_arm64_v8a flags when synchronizing .clangd with desktop Qt', () => { + const wsDir = path.join(tempDir, 'WsCleanClangd'); + const qtBase = path.join(tempDir, 'QtPurge'); + const verDir = path.join(qtBase, '6.8.0'); + + const mingwInc = path.join(verDir, 'mingw_64', 'include', 'QtCore'); + fs.mkdirSync(mingwInc, { recursive: true }); + fs.writeFileSync(path.join(mingwInc, 'qstring.h'), '// mingw'); + + fs.mkdirSync(wsDir, { recursive: true }); + // Create project file referencing Qt + fs.writeFileSync(path.join(wsDir, 'main.cpp'), '#include \n#include \n'); + + // Create .clangd that contains old obsolete android_arm64_v8a flags + const clangdFile = path.join(wsDir, '.clangd'); + fs.writeFileSync( + clangdFile, + [ + 'CompileFlags:', + ' Add:', + ' - "-std=c++20"', + ' - "-IC:/Qt/6.8.0/android_arm64_v8a/include"', + ' - "-IC:/Qt/6.8.0/android_arm64_v8a/include/QtCore"' + ].join('\n') + ); + + const synced = ExternalSdkDetector.syncWorkspaceClangdConfig(wsDir, { + qtInstallDir: qtBase, + allowFsScan: true + }); + + assert.ok(synced); + const updatedClangd = fs.readFileSync(clangdFile, 'utf8'); + assert.ok( + !updatedClangd.includes('android_arm64_v8a'), + 'Obsolete android_arm64_v8a flag was not purged from .clangd' + ); + assert.ok( + updatedClangd.includes('mingw_64'), + 'Desktop mingw_64 flag was not added to .clangd' + ); + }); }); describe('getAllIncludePaths & Normalization', () => { diff --git a/wiki/Compiler-Toolchain-Setup.md b/wiki/Compiler-Toolchain-Setup.md index d391056..5f4b92c 100644 --- a/wiki/Compiler-Toolchain-Setup.md +++ b/wiki/Compiler-Toolchain-Setup.md @@ -55,9 +55,21 @@ C/C++ Pro automatically detects common system and graphics SDKs. To avoid flag p | **CUDA Toolkit** | `CUDA_PATH` | `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\*`, `/usr/local/cuda` | | **Boost C++** | `BOOST_ROOT`, `BOOST_DIR` | `C:\local\boost_*`, `/usr/include/boost`, Homebrew paths | | **SDL2 / SDL3** | `SDL2_DIR`, `SDL3_DIR` | `C:\SDL*`, `/usr/include/SDL2`, `/usr/local/include/SDL2` | +| **Qt Framework** | `QTDIR`, `Qt6_DIR`, `Qt5_DIR`, `CMAKE_PREFIX_PATH` | `C:\Qt\\`, `D:\Qt`, `/usr/include/qt6`, `/usr/include/qt5`, Homebrew | | **Node-API** | — | Automatic inspection of `package.json` for `node-addon-api` or `nan` | | **vcpkg** | `VCPKG_ROOT` | Automatic detection of active vcpkg installed directories | +### Intelligent Qt Toolchain Prioritization +When multiple Qt toolchains are installed (e.g. `android_arm64_v8a`, `mingw_64`, `msvc2022_64`): +- **Desktop Priority**: Cross-compilation kits (`android_*`, `wasm_*`, `ios`, `qnx_*`) are automatically deprioritized for desktop projects, ensuring desktop MinGW or MSVC headers are selected. +- **Compiler Affinity**: The toolchain matching your active compiler (MinGW/GCC vs MSVC) is automatically preferred based on workspace build files (`CMakeCache.txt`, `.sln`, `Makefile`) or system `PATH`. +- **Explicit Override**: You can explicitly select a specific Qt kit in settings: + ```json + { + "c-cpp-pro.discovery.qtToolchain": "mingw_64" + } + ``` + ### Customizing SDK Probing SDK auto-detection can be toggled in your VS Code settings: ```json @@ -90,3 +102,4 @@ C/C++ Pro manages this environment automatically: | `c-cpp-pro.cStandard` | `string` | `"c17"` | Default C standard for flag synthesis (`c89` through `c23`). | | `c-cpp-pro.default.forcedInclude` | `array` | `[]` | Headers to force-include before compilation (`/FI` on MSVC, `-include` on GCC/Clang). | | `c-cpp-pro.discovery.detectExternalSdks` | `boolean` | `true` | Auto-detect common external SDKs for IntelliSense fallback flags. | +| `c-cpp-pro.discovery.qtToolchain` | `string` | `""` | Preferred Qt toolchain kit subfolder (e.g. 'mingw_64', 'msvc2022_64', 'gcc_64'). Auto-detects if empty. | From 5e9894f17d55c80b2a1cc4cc1ed2f156d88c1c3e Mon Sep 17 00:00:00 2001 From: FunToHard Date: Sat, 7 Nov 2026 19:31:14 +0530 Subject: [PATCH 2/5] feat(prober): add automatic Qt Android project detection and ABI selection Adds detectTargetPlatform to automatically detect Qt Android projects via AndroidManifest.xml, CMakeLists.txt (CMAKE_SYSTEM_NAME Android, ANDROID_PACKAGE_SOURCE_DIR), CMakeCache.txt, and .pro files. Automatically routes Qt headers to android toolchains (with ABI matching for arm64-v8a vs x86_64) when targeting Android, while maintaining strict desktop prioritization for desktop projects. Also honors explicit preferredToolchain overrides. --- src/prober/external-sdk-detector.ts | 189 +++++++++++++++++++++++----- test/external-sdk-detector.test.ts | 83 ++++++++++++ 2 files changed, 244 insertions(+), 28 deletions(-) diff --git a/src/prober/external-sdk-detector.ts b/src/prober/external-sdk-detector.ts index 4d9be67..56a0aa4 100644 --- a/src/prober/external-sdk-detector.ts +++ b/src/prober/external-sdk-detector.ts @@ -140,39 +140,160 @@ export function detectCompilerPreference( return undefined; } +/** + * Detects target operating platform ('desktop', 'android', 'wasm', or 'ios') from options, + * environment variables, or workspace indicator files (AndroidManifest.xml, CMakeLists.txt, .pro). + */ +export function detectTargetPlatform( + options: ExternalSdkDetectorOptions = {} +): 'desktop' | 'android' | 'wasm' | 'ios' | string { + if (options.targetPlatform) { + return options.targetPlatform.toLowerCase(); + } + + const env = options.env ?? process.env; + + // 1. Check explicit toolchain override + const explicitTc = (options.preferredToolchain || env.QT_TOOLCHAIN || env.QT_KIT || '').toLowerCase(); + if (explicitTc.startsWith('android')) return 'android'; + if (explicitTc.startsWith('wasm')) return 'wasm'; + if (explicitTc.startsWith('ios')) return 'ios'; + + // 2. Check Android environment variables + if (env.ANDROID_ABI || env.ANDROID_PLATFORM || env.ANDROID_TARGET_ARCH) return 'android'; + + // 3. Inspect workspaceRoot if available + const wsRoot = options.workspaceRoot; + if (wsRoot) { + try { + // Direct AndroidManifest.xml presence + if ( + fs.existsSync(path.join(wsRoot, 'AndroidManifest.xml')) || + fs.existsSync(path.join(wsRoot, 'android', 'AndroidManifest.xml')) + ) { + return 'android'; + } + + // CMakeCache.txt check + const cmakeCacheCandidates = [ + path.join(wsRoot, 'CMakeCache.txt'), + path.join(wsRoot, 'build', 'CMakeCache.txt'), + path.join(wsRoot, 'out', 'CMakeCache.txt') + ]; + for (const cacheFile of cmakeCacheCandidates) { + if (fs.existsSync(cacheFile)) { + const content = fs.readFileSync(cacheFile, 'utf8'); + if ( + /CMAKE_SYSTEM_NAME:STRING=Android/i.test(content) || + /ANDROID_ABI:STRING=/i.test(content) || + /ANDROID_NDK:PATH=/i.test(content) + ) { + return 'android'; + } + if (/CMAKE_SYSTEM_NAME:STRING=Emscripten/i.test(content)) { + return 'wasm'; + } + if (/CMAKE_SYSTEM_NAME:STRING=iOS/i.test(content)) { + return 'ios'; + } + } + } + + // CMakeLists.txt check + const cmakeLists = path.join(wsRoot, 'CMakeLists.txt'); + if (fs.existsSync(cmakeLists)) { + const content = fs.readFileSync(cmakeLists, 'utf8'); + if ( + /set\s*\(\s*CMAKE_SYSTEM_NAME\s+Android\s*\)/i.test(content) || + /ANDROID_PACKAGE_SOURCE_DIR/i.test(content) || + /qt_android_/i.test(content) + ) { + return 'android'; + } + } + + // .pro / .pri project files + const rootFiles = fs.readdirSync(wsRoot); + for (const file of rootFiles) { + if (file.endsWith('.pro') || file.endsWith('.pri')) { + const content = fs.readFileSync(path.join(wsRoot, file), 'utf8'); + if ( + /ANDROID_PACKAGE_SOURCE_DIR/i.test(content) || + /contains\s*\(\s*ANDROID_TARGET_ARCH/i.test(content) || + /android\s*\{/i.test(content) + ) { + return 'android'; + } + } + } + } catch { + // Ignore filesystem inspection errors + } + } + + return 'desktop'; +} + /** * Computes a suitability score for a Qt toolchain kit directory name. - * Heavily penalizes cross-compilation / mobile / web toolchains (android, wasm, ios, qnx) - * during desktop development to ensure desktop kits (mingw, msvc, gcc, clang) are preferred. + * If the project targets Android (or another cross-target), matching toolchains receive top priority. + * If developing a desktop project, cross-compilation toolchains (android, wasm, ios, qnx) receive a severe + * penalty to ensure desktop kits (mingw, msvc, gcc, clang) are preferred. */ export function scoreQtToolchain( tcName: string, options: ExternalSdkDetectorOptions = {} ): number { const lower = tcName.toLowerCase(); + const env = options.env ?? process.env; - // 1. Cross-compilation / mobile / embedded target detection + // 1. Explicit toolchain match has the absolute highest priority + const explicitTc = (options.preferredToolchain || env.QT_TOOLCHAIN || env.QT_KIT || '').toLowerCase(); + if (explicitTc && lower.includes(explicitTc)) { + return 100_000; + } + + // 2. Detect target platform (desktop vs android vs wasm vs ios) + const targetPlatform = detectTargetPlatform(options); const isCrossTarget = /^(?:android|wasm|ios|qnx|integrity|winrt)/i.test(lower); - const targetPlatform = options.targetPlatform?.toLowerCase(); - const wantCross = Boolean( - targetPlatform && - targetPlatform !== 'desktop' && - lower.startsWith(targetPlatform) - ); - - if (isCrossTarget && !wantCross) { - // Severe penalty: mobile/embedded cross-compilation toolchains should never be selected for desktop projects - return -100_000; + + if (targetPlatform === 'android') { + if (!lower.startsWith('android')) { + // When targeting Android, non-Android toolchains should not be selected + return -100_000; + } + let androidScore = 50_000; + + // ABI matching + const prefAbi = (env.ANDROID_ABI || options.preferredArch || '').toLowerCase(); + if (prefAbi) { + const cleanAbi = prefAbi.replace(/[-_]/g, ''); + const cleanTc = lower.replace(/[-_]/g, ''); + if (cleanTc.includes(cleanAbi)) { + androidScore += 20_000; + } + } else { + // Default Android ABI preference: 64-bit ARM first, then 64-bit x86 emulator, then 32-bit + if (lower.includes('arm64_v8a')) androidScore += 10_000; + else if (lower.includes('x86_64')) androidScore += 8_000; + else if (lower.includes('armv7')) androidScore += 5_000; + } + return androidScore; } - if (wantCross) { + + if (targetPlatform === 'wasm') { + if (!lower.startsWith('wasm')) return -100_000; + return 50_000 + (lower.includes('multithread') ? 5_000 : 0); + } + + if (targetPlatform === 'ios') { + if (!lower.startsWith('ios')) return -100_000; return 50_000; } - // 2. Explicit toolchain match - const env = options.env ?? process.env; - const explicitTc = (options.preferredToolchain || env.QT_TOOLCHAIN || env.QT_KIT || '').toLowerCase(); - if (explicitTc && lower.includes(explicitTc)) { - return 80_000; + // Desktop platform: penalize cross-compilation toolchains heavily + if (isCrossTarget) { + return -100_000; } let score = 10_000; // Base score for host desktop toolchains @@ -916,15 +1037,27 @@ export class ExternalSdkDetector { } if (candidates.length > 0) { - // If desktop toolchains exist, eliminate cross-compilation toolchains unless cross-compilation was explicitly requested - const wantCross = Boolean( - options.targetPlatform && - options.targetPlatform !== 'desktop' && - ['android', 'wasm', 'ios', 'qnx'].includes(options.targetPlatform.toLowerCase()) - ); - const validCandidates = (!wantCross && candidates.some((c) => c.isDesktop)) - ? candidates.filter((c) => c.isDesktop) - : candidates; + const effectivePlatform = detectTargetPlatform(options); + const isExplicit = Boolean(options.preferredToolchain || env.QT_TOOLCHAIN || env.QT_KIT); + + let validCandidates: QtCandidate[]; + if (isExplicit) { + validCandidates = candidates; + } else if (effectivePlatform === 'android') { + const androidCandidates = candidates.filter((c) => c.toolchain.toLowerCase().startsWith('android')); + validCandidates = androidCandidates.length > 0 ? androidCandidates : candidates; + } else if (effectivePlatform === 'wasm') { + const wasmCandidates = candidates.filter((c) => c.toolchain.toLowerCase().startsWith('wasm')); + validCandidates = wasmCandidates.length > 0 ? wasmCandidates : candidates; + } else if (effectivePlatform === 'ios') { + const iosCandidates = candidates.filter((c) => c.toolchain.toLowerCase().startsWith('ios')); + validCandidates = iosCandidates.length > 0 ? iosCandidates : candidates; + } else { + // Desktop: eliminate cross-compilation toolchains if desktop toolchains exist + validCandidates = candidates.some((c) => c.isDesktop) + ? candidates.filter((c) => c.isDesktop) + : candidates; + } validCandidates.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; diff --git a/test/external-sdk-detector.test.ts b/test/external-sdk-detector.test.ts index f7434aa..c5082db 100644 --- a/test/external-sdk-detector.test.ts +++ b/test/external-sdk-detector.test.ts @@ -7,6 +7,7 @@ import { ExternalSdkDetector, compareSemverDesc, detectCompilerPreference, + detectTargetPlatform, scoreQtToolchain } from '../src/prober/external-sdk-detector'; import { FlagSynthesizer } from '../src/prober/flag-synthesizer'; @@ -365,6 +366,88 @@ describe('External SDKs Auto-Discovery Subsystem', () => { 'Desktop mingw_64 flag was not added to .clangd' ); }); + + it('should automatically detect Android targetPlatform when AndroidManifest.xml exists', () => { + const wsDir = path.join(tempDir, 'QtAndroidWs'); + fs.mkdirSync(path.join(wsDir, 'android'), { recursive: true }); + fs.writeFileSync( + path.join(wsDir, 'android', 'AndroidManifest.xml'), + '' + ); + + const platform = detectTargetPlatform({ workspaceRoot: wsDir }); + assert.strictEqual(platform, 'android'); + }); + + it('should automatically detect Android targetPlatform from CMakeLists.txt setting CMAKE_SYSTEM_NAME Android', () => { + const wsDir = path.join(tempDir, 'QtAndroidCMakeWs'); + fs.mkdirSync(wsDir, { recursive: true }); + fs.writeFileSync( + path.join(wsDir, 'CMakeLists.txt'), + 'cmake_minimum_required(VERSION 3.22)\nset(CMAKE_SYSTEM_NAME Android)\nfind_package(Qt6 REQUIRED COMPONENTS Core)\n' + ); + + const platform = detectTargetPlatform({ workspaceRoot: wsDir }); + assert.strictEqual(platform, 'android'); + }); + + it('should select android_arm64_v8a when developing an Android Qt project', () => { + const wsDir = path.join(tempDir, 'QtAndroidAppProject'); + const qtBase = path.join(tempDir, 'QtAndroidInstall'); + const verDir = path.join(qtBase, '6.8.0'); + + // Both desktop and android are installed + const mingwInc = path.join(verDir, 'mingw_64', 'include', 'QtCore'); + fs.mkdirSync(mingwInc, { recursive: true }); + fs.writeFileSync(path.join(mingwInc, 'qstring.h'), '// mingw'); + + const androidInc = path.join(verDir, 'android_arm64_v8a', 'include', 'QtCore'); + fs.mkdirSync(androidInc, { recursive: true }); + fs.writeFileSync(path.join(androidInc, 'qstring.h'), '// android'); + + // Project has AndroidManifest.xml + fs.mkdirSync(wsDir, { recursive: true }); + fs.writeFileSync(path.join(wsDir, 'AndroidManifest.xml'), ''); + + const result = ExternalSdkDetector.detectQt({ + workspaceRoot: wsDir, + qtInstallDir: qtBase, + allowFsScan: true + }); + + assert.ok(result); + assert.strictEqual(result.name, 'Qt'); + assert.ok( + result.rootPath.includes('android_arm64_v8a'), + `Expected android_arm64_v8a for Android project, got: ${result.rootPath}` + ); + }); + + it('should respect explicit preferredToolchain=android_arm64_v8a override even on desktop', () => { + const qtBase = path.join(tempDir, 'QtExplicitAndroid'); + const verDir = path.join(qtBase, '6.8.0'); + + const mingwInc = path.join(verDir, 'mingw_64', 'include', 'QtCore'); + fs.mkdirSync(mingwInc, { recursive: true }); + fs.writeFileSync(path.join(mingwInc, 'qstring.h'), '// mingw'); + + const androidInc = path.join(verDir, 'android_arm64_v8a', 'include', 'QtCore'); + fs.mkdirSync(androidInc, { recursive: true }); + fs.writeFileSync(path.join(androidInc, 'qstring.h'), '// android'); + + const result = ExternalSdkDetector.detectQt({ + preferredToolchain: 'android_arm64_v8a', + qtInstallDir: qtBase, + allowFsScan: true + }); + + assert.ok(result); + assert.strictEqual(result.name, 'Qt'); + assert.ok( + result.rootPath.includes('android_arm64_v8a'), + `Expected explicit android_arm64_v8a to win, got: ${result.rootPath}` + ); + }); }); describe('getAllIncludePaths & Normalization', () => { From 24543857b78bf5146b35ed7abe8c4d9dee27dd95 Mon Sep 17 00:00:00 2001 From: FunToHard Date: Sat, 7 Nov 2026 19:40:22 +0530 Subject: [PATCH 3/5] feat(scripts): add install.bat, install.ps1, and install.sh for local testing Provides turnkey local development installation scripts that automate clean building, packaging, locating the latest VSIX artifact, and force-installing directly into VS Code (or code-insiders/codium). --- scripts/install.bat | 70 ++++++++++++++++++++++++++++++++++++++++++ scripts/install.ps1 | 75 +++++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 scripts/install.bat create mode 100644 scripts/install.ps1 create mode 100644 scripts/install.sh diff --git a/scripts/install.bat b/scripts/install.bat new file mode 100644 index 0000000..f8562c0 --- /dev/null +++ b/scripts/install.bat @@ -0,0 +1,70 @@ +@echo off +setlocal enabledelayedexpansion + +echo =================================================== +echo C/C++ Pro - Local Development Installer +echo =================================================== + +set "SCRIPT_DIR=%~dp0" +set "REPO_ROOT=%SCRIPT_DIR%.." +cd /d "%REPO_ROOT%" + +REM Check for npm +where npm >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo Error: npm is not found in PATH. + exit /b 1 +) + +REM Locate VS Code executable (code, code-insiders, or codium) +set "CODE_BIN=code" +where code >nul 2>nul +if %ERRORLEVEL% neq 0 ( + where code-insiders >nul 2>nul + if %ERRORLEVEL% equ 0 ( + set "CODE_BIN=code-insiders" + ) else ( + where codium >nul 2>nul + if %ERRORLEVEL% equ 0 ( + set "CODE_BIN=codium" + ) else ( + echo Error: VS Code executable ^(code^) was not found in PATH. + exit /b 1 + ) + ) +) + +echo [1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)... +call npm run package +if %ERRORLEVEL% neq 0 ( + echo Error: Failed to package VSIX bundle. + exit /b %ERRORLEVEL% +) + +echo [2/3] Locating packaged VSIX extension... +set "VSIX_FILE=" +for /f "delims=" %%F in ('dir /b /o:-d c-cpp-pro-*.vsix 2^>nul') do ( + set "VSIX_FILE=%%F" + goto :found_vsix +) + +:found_vsix +if "%VSIX_FILE%"=="" ( + echo Error: Could not locate packaged VSIX file in %REPO_ROOT%. + exit /b 1 +) + +echo Target VSIX: %VSIX_FILE% + +echo [3/3] Installing %VSIX_FILE% into %CODE_BIN%... +call %CODE_BIN% --install-extension "%VSIX_FILE%" --force +if %ERRORLEVEL% neq 0 ( + echo Error: Failed to install VSIX into %CODE_BIN%. + exit /b %ERRORLEVEL% +) + +echo --------------------------------------------------- +echo Successfully installed %VSIX_FILE% into %CODE_BIN%. +echo To activate, reload your VS Code window: 'Developer: Reload Window'. +echo --------------------------------------------------- +exit /b 0 diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..09da8bf --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,75 @@ +# C/C++ Pro - Local Extension Builder & Installer +# Builds the native addon, compiles extension bundle, packages VSIX, and installs locally into VS Code. + +[CmdletBinding()] +param( + [switch]$SkipBuild, + [switch]$PreRelease, + [string]$CodeBin = "code" +) + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = Split-Path -Parent $ScriptDir + +Set-Location $RepoRoot + +Write-Host "===================================================" -ForegroundColor Cyan +Write-Host " C/C++ Pro - Local Development Installer" -ForegroundColor Cyan +Write-Host "===================================================" -ForegroundColor Cyan + +# 1. Verify prerequisite tooling +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Write-Error "npm is not installed or not available in PATH." +} + +$VscodeCli = Get-Command $CodeBin -ErrorAction SilentlyContinue +if (-not $VscodeCli) { + # Check fallback editors: code-insiders, codium + $FallbackCli = @("code", "code-insiders", "codium") | Where-Object { Get-Command $_ -ErrorAction SilentlyContinue } | Select-Object -First 1 + if ($FallbackCli) { + $CodeBin = $FallbackCli + $VscodeCli = Get-Command $CodeBin + Write-Host "Using discovered VS Code CLI: $CodeBin" -ForegroundColor Yellow + } else { + Write-Error "VS Code executable ($CodeBin) not found in PATH. Please ensure 'code' command is in your PATH." + } +} + +# 2. Package VSIX extension +if (-not $SkipBuild) { + Write-Host "[1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." -ForegroundColor Green + if ($PreRelease) { + npm run package:pre + } else { + npm run package + } + if ($LASTEXITCODE -ne 0) { + Write-Error "Packaging failed with exit code $LASTEXITCODE." + } +} else { + Write-Host "[1/3] Skipping build/packaging step (-SkipBuild requested)..." -ForegroundColor Yellow +} + +# 3. Locate generated VSIX file +Write-Host "[2/3] Locating packaged VSIX extension..." -ForegroundColor Green +$VsixFile = Get-ChildItem -Path $RepoRoot -Filter "c-cpp-pro-*.vsix" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + +if (-not $VsixFile -or -not (Test-Path $VsixFile.FullName)) { + Write-Error "Could not find packaged VSIX file in $RepoRoot." +} + +Write-Host "Target VSIX: $($VsixFile.Name) ($([math]::Round($VsixFile.Length / 1MB, 2)) MB)" -ForegroundColor Cyan + +# 4. Install VSIX into VS Code +Write-Host "[3/3] Installing $($VsixFile.Name) into $CodeBin..." -ForegroundColor Green +& $CodeBin --install-extension $VsixFile.FullName --force + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install VSIX into VS Code (exit code $LASTEXITCODE)." +} + +Write-Host "---------------------------------------------------" -ForegroundColor Cyan +Write-Host "Successfully installed $($VsixFile.Name) into $CodeBin." -ForegroundColor Green +Write-Host "To activate, reload your VS Code window: 'Developer: Reload Window' (Ctrl+R / Cmd+R)." -ForegroundColor Cyan +Write-Host "---------------------------------------------------" -ForegroundColor Cyan diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..93ee109 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +cd "${REPO_ROOT}" + +echo "===================================================" +echo " C/C++ Pro - Local Development Installer" +echo "===================================================" + +# 1. Verify npm presence +if ! command -v npm &>/dev/null; then + echo "Error: npm is not installed or not available in PATH." >&2 + exit 1 +fi + +# 2. Locate VS Code CLI executable +CODE_BIN="code" +if ! command -v "${CODE_BIN}" &>/dev/null; then + if command -v code-insiders &>/dev/null; then + CODE_BIN="code-insiders" + elif command -v codium &>/dev/null; then + CODE_BIN="codium" + else + echo "Error: VS Code executable ('code') not found in PATH." >&2 + exit 1 + fi +fi + +# 3. Package extension VSIX +echo "[1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." +npm run package + +# 4. Locate the latest generated VSIX file +echo "[2/3] Locating packaged VSIX extension..." +VSIX_FILE=$(ls -t c-cpp-pro-*.vsix 2>/dev/null | head -n 1 || true) + +if [ -z "${VSIX_FILE}" ] || [ ! -f "${VSIX_FILE}" ]; then + echo "Error: Could not find packaged VSIX file in ${REPO_ROOT}." >&2 + exit 1 +fi + +echo "Target VSIX: ${VSIX_FILE}" + +# 5. Install VSIX into VS Code +echo "[3/3] Installing ${VSIX_FILE} into ${CODE_BIN}..." +"${CODE_BIN}" --install-extension "${VSIX_FILE}" --force + +echo "---------------------------------------------------" +echo "Successfully installed ${VSIX_FILE} into ${CODE_BIN}." +echo "To activate, reload your VS Code window: 'Developer: Reload Window'." +echo "---------------------------------------------------" From 18448862c3bb0f218af71b12b480e8536cc08159 Mon Sep 17 00:00:00 2001 From: FunToHard Date: Sat, 7 Nov 2026 19:46:51 +0530 Subject: [PATCH 4/5] feat(scripts): auto-bump version in local install scripts to prevent VS Code caching --- scripts/bump-version.js | 52 +++++++++++++++++++++++++++++++++++++++ scripts/install.bat | 54 +++++++++++++++++++++++++++++++++-------- scripts/install.ps1 | 28 +++++++++++++++------ scripts/install.sh | 47 +++++++++++++++++++++++++++++------ 4 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 scripts/bump-version.js diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 0000000..fc986a5 --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const pkgPath = path.join(repoRoot, 'package.json'); +const lockPath = path.join(repoRoot, 'package-lock.json'); +const cargoPath = path.join(repoRoot, 'crates', 'c-cpp-pro-native', 'Cargo.toml'); + +if (!fs.existsSync(pkgPath)) { + console.error(`Error: package.json not found at ${pkgPath}`); + process.exit(1); +} + +const pkgContent = fs.readFileSync(pkgPath, 'utf8'); +const pkg = JSON.parse(pkgContent); + +const currentVersion = pkg.version || '0.0.0'; +const versionParts = currentVersion.split('.'); + +const major = parseInt(versionParts[0] || '0', 10); +const minor = parseInt(versionParts[1] || '0', 10); +const patch = parseInt(versionParts[2] || '0', 10) + 1; + +const newVersion = `${major}.${minor}.${patch}`; + +pkg.version = newVersion; +fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); + +if (fs.existsSync(lockPath)) { + try { + const lockContent = fs.readFileSync(lockPath, 'utf8'); + const lock = JSON.parse(lockContent); + lock.version = newVersion; + if (lock.packages && lock.packages['']) { + lock.packages[''].version = newVersion; + } + fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf8'); + } catch (err) { + console.warn(`Warning: failed to update package-lock.json: ${err.message}`); + } +} + +if (fs.existsSync(cargoPath)) { + let cargoContent = fs.readFileSync(cargoPath, 'utf8'); + cargoContent = cargoContent.replace( + /^version\s*=\s*"[^"]+"/m, + `version = "${newVersion}"` + ); + fs.writeFileSync(cargoPath, cargoContent, 'utf8'); +} + +console.log(`Bumped version: ${currentVersion} -> ${newVersion}`); diff --git a/scripts/install.bat b/scripts/install.bat index f8562c0..ec11d49 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -9,14 +9,25 @@ set "SCRIPT_DIR=%~dp0" set "REPO_ROOT=%SCRIPT_DIR%.." cd /d "%REPO_ROOT%" -REM Check for npm +REM Parse flags +set "DO_BUMP=1" +set "DO_BUILD=1" + +for %%A in (%*) do ( + if /i "%%A"=="--no-bump" set "DO_BUMP=0" + if /i "%%A"=="-NoBump" set "DO_BUMP=0" + if /i "%%A"=="--skip-build" set "DO_BUILD=0" + if /i "%%A"=="-SkipBuild" set "DO_BUILD=0" +) + +REM 1. Check for npm where npm >nul 2>nul if %ERRORLEVEL% neq 0 ( echo Error: npm is not found in PATH. exit /b 1 ) -REM Locate VS Code executable (code, code-insiders, or codium) +REM 2. Locate VS Code executable (code, code-insiders, or codium) set "CODE_BIN=code" where code >nul 2>nul if %ERRORLEVEL% neq 0 ( @@ -28,20 +39,42 @@ if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% equ 0 ( set "CODE_BIN=codium" ) else ( - echo Error: VS Code executable ^(code^) was not found in PATH. + echo Error: VS Code executable was not found in PATH. exit /b 1 ) ) ) -echo [1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)... -call npm run package -if %ERRORLEVEL% neq 0 ( - echo Error: Failed to package VSIX bundle. - exit /b %ERRORLEVEL% +REM 3. Bump patch version +if "%DO_BUILD%"=="1" ( + if "%DO_BUMP%"=="1" ( + echo [1/4] Bumping extension patch version... + call node "%SCRIPT_DIR%bump-version.js" + if !ERRORLEVEL! neq 0 ( + echo Error: Failed to bump version. + exit /b !ERRORLEVEL! + ) + ) else ( + echo [1/4] Skipping version bump: --no-bump requested... + ) +) else ( + echo [1/4] Skipping version bump: --skip-build requested... +) + +REM 4. Package VSIX extension +if "%DO_BUILD%"=="1" ( + echo [2/4] Packaging VSIX bundle: clean, native Rust build, production esbuild... + call npm run package + if !ERRORLEVEL! neq 0 ( + echo Error: Failed to package VSIX bundle. + exit /b !ERRORLEVEL! + ) +) else ( + echo [2/4] Skipping packaging step: --skip-build requested... ) -echo [2/3] Locating packaged VSIX extension... +REM 5. Locate packaged VSIX extension +echo [3/4] Locating packaged VSIX extension... set "VSIX_FILE=" for /f "delims=" %%F in ('dir /b /o:-d c-cpp-pro-*.vsix 2^>nul') do ( set "VSIX_FILE=%%F" @@ -56,7 +89,8 @@ if "%VSIX_FILE%"=="" ( echo Target VSIX: %VSIX_FILE% -echo [3/3] Installing %VSIX_FILE% into %CODE_BIN%... +REM 6. Install VSIX into VS Code +echo [4/4] Installing %VSIX_FILE% into %CODE_BIN%... call %CODE_BIN% --install-extension "%VSIX_FILE%" --force if %ERRORLEVEL% neq 0 ( echo Error: Failed to install VSIX into %CODE_BIN%. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 09da8bf..49906d7 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,9 +1,10 @@ # C/C++ Pro - Local Extension Builder & Installer -# Builds the native addon, compiles extension bundle, packages VSIX, and installs locally into VS Code. +# Bumps version, builds native addon, compiles extension bundle, packages VSIX, and installs locally into VS Code. [CmdletBinding()] param( [switch]$SkipBuild, + [switch]$NoBump, [switch]$PreRelease, [string]$CodeBin = "code" ) @@ -36,9 +37,20 @@ if (-not $VscodeCli) { } } -# 2. Package VSIX extension +# 2. Bump patch version +if (-not $NoBump -and -not $SkipBuild) { + Write-Host "[1/4] Bumping extension patch version..." -ForegroundColor Green + node (Join-Path $ScriptDir "bump-version.js") + if ($LASTEXITCODE -ne 0) { + Write-Error "Version bump failed with exit code $LASTEXITCODE." + } +} else { + Write-Host "[1/4] Skipping version bump (NoBump or SkipBuild specified)..." -ForegroundColor Yellow +} + +# 3. Package VSIX extension if (-not $SkipBuild) { - Write-Host "[1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." -ForegroundColor Green + Write-Host "[2/4] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." -ForegroundColor Green if ($PreRelease) { npm run package:pre } else { @@ -48,11 +60,11 @@ if (-not $SkipBuild) { Write-Error "Packaging failed with exit code $LASTEXITCODE." } } else { - Write-Host "[1/3] Skipping build/packaging step (-SkipBuild requested)..." -ForegroundColor Yellow + Write-Host "[2/4] Skipping build/packaging step (-SkipBuild requested)..." -ForegroundColor Yellow } -# 3. Locate generated VSIX file -Write-Host "[2/3] Locating packaged VSIX extension..." -ForegroundColor Green +# 4. Locate generated VSIX file +Write-Host "[3/4] Locating packaged VSIX extension..." -ForegroundColor Green $VsixFile = Get-ChildItem -Path $RepoRoot -Filter "c-cpp-pro-*.vsix" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $VsixFile -or -not (Test-Path $VsixFile.FullName)) { @@ -61,8 +73,8 @@ if (-not $VsixFile -or -not (Test-Path $VsixFile.FullName)) { Write-Host "Target VSIX: $($VsixFile.Name) ($([math]::Round($VsixFile.Length / 1MB, 2)) MB)" -ForegroundColor Cyan -# 4. Install VSIX into VS Code -Write-Host "[3/3] Installing $($VsixFile.Name) into $CodeBin..." -ForegroundColor Green +# 5. Install VSIX into VS Code +Write-Host "[4/4] Installing $($VsixFile.Name) into $CodeBin..." -ForegroundColor Green & $CodeBin --install-extension $VsixFile.FullName --force if ($LASTEXITCODE -ne 0) { diff --git a/scripts/install.sh b/scripts/install.sh index 93ee109..01f0f23 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -10,6 +10,23 @@ echo "===================================================" echo " C/C++ Pro - Local Development Installer" echo "===================================================" +# Parse flags +DO_BUMP=1 +DO_BUILD=1 + +for arg in "$@"; do + case "${arg}" in + --no-bump|-NoBump) + DO_BUMP=0 + ;; + --skip-build|-SkipBuild) + DO_BUILD=0 + ;; + *) + ;; + esac +done + # 1. Verify npm presence if ! command -v npm &>/dev/null; then echo "Error: npm is not installed or not available in PATH." >&2 @@ -29,12 +46,28 @@ if ! command -v "${CODE_BIN}" &>/dev/null; then fi fi -# 3. Package extension VSIX -echo "[1/3] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." -npm run package +# 3. Bump patch version +if [ "${DO_BUILD}" -eq 1 ]; then + if [ "${DO_BUMP}" -eq 1 ]; then + echo "[1/4] Bumping extension patch version..." + node "${SCRIPT_DIR}/bump-version.js" + else + echo "[1/4] Skipping version bump (--no-bump requested)..." + fi +else + echo "[1/4] Skipping version bump (--skip-build requested)..." +fi + +# 4. Package extension VSIX +if [ "${DO_BUILD}" -eq 1 ]; then + echo "[2/4] Packaging VSIX bundle (clean, native Rust build, production esbuild)..." + npm run package +else + echo "[2/4] Skipping build/packaging step (--skip-build requested)..." +fi -# 4. Locate the latest generated VSIX file -echo "[2/3] Locating packaged VSIX extension..." +# 5. Locate the latest generated VSIX file +echo "[3/4] Locating packaged VSIX extension..." VSIX_FILE=$(ls -t c-cpp-pro-*.vsix 2>/dev/null | head -n 1 || true) if [ -z "${VSIX_FILE}" ] || [ ! -f "${VSIX_FILE}" ]; then @@ -44,8 +77,8 @@ fi echo "Target VSIX: ${VSIX_FILE}" -# 5. Install VSIX into VS Code -echo "[3/3] Installing ${VSIX_FILE} into ${CODE_BIN}..." +# 6. Install VSIX into VS Code +echo "[4/4] Installing ${VSIX_FILE} into ${CODE_BIN}..." "${CODE_BIN}" --install-extension "${VSIX_FILE}" --force echo "---------------------------------------------------" From 0ffc2822737cf090619343497861283680e52ea5 Mon Sep 17 00:00:00 2001 From: FunToHard Date: Sat, 7 Nov 2026 19:59:38 +0530 Subject: [PATCH 5/5] refactor(prober): optimize compilation database read and PATH regex in external sdk prober --- scripts/install.sh | 0 src/prober/external-sdk-detector.ts | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/install.sh diff --git a/scripts/install.sh b/scripts/install.sh old mode 100644 new mode 100755 diff --git a/src/prober/external-sdk-detector.ts b/src/prober/external-sdk-detector.ts index 56a0aa4..4d9bc67 100644 --- a/src/prober/external-sdk-detector.ts +++ b/src/prober/external-sdk-detector.ts @@ -107,7 +107,18 @@ export function detectCompilerPreference( ]; for (const compDb of compDbCandidates) { if (fs.existsSync(compDb)) { - const firstChunk = fs.readFileSync(compDb, { encoding: 'utf8', flag: 'r' }).slice(0, 4096); + let fd: number | undefined; + let firstChunk = ''; + try { + fd = fs.openSync(compDb, 'r'); + const buf = Buffer.alloc(4096); + const bytesRead = fs.readSync(fd, buf, 0, 4096, 0); + firstChunk = buf.toString('utf8', 0, bytesRead); + } finally { + if (fd !== undefined) { + fs.closeSync(fd); + } + } if ( firstChunk.includes('g++') || firstChunk.includes('gcc') || @@ -132,7 +143,7 @@ export function detectCompilerPreference( // 6. Inspect system PATH for toolchains const pathEnv = (env.PATH || env.Path || '').toLowerCase(); const hasMinGwOnPath = pathEnv.includes('mingw') || pathEnv.includes('msys64'); - const hasMsvcOnPath = pathEnv.includes('vc\\tools\\msvc') || pathEnv.includes('microsoft visual studio'); + const hasMsvcOnPath = /(?:vc[\\/]tools[\\/]msvc|microsoft visual studio)/.test(pathEnv); if (hasMinGwOnPath && !hasMsvcOnPath) return 'mingw'; if (hasMsvcOnPath && !hasMinGwOnPath) return 'msvc';