From 222be9920f588be4f1399c905e170c6be6ca8206 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:01:37 +0000 Subject: [PATCH 1/5] feat: add CMake: Install Component command (#4281) Add a new 'CMake: Install Component' command that uses 'cmake --install --component ' to install specific CMake install components. - Add parseInstallComponentsFromContent() to parse cmake_install.cmake - Add getInstallComponents() method on CMakeProject - Add showComponentSelector() with QuickPick and InputBox fallback - Add installComponent() method on CMakeProject and ExtensionManager - Register cmake.installComponent command in package.json - Add unit tests for component parsing - Require CMake >= 3.15 with version guard Co-authored-by: hanniavalera <90047725+hanniavalera@users.noreply.github.com> --- CHANGELOG.md | 1 + package.json | 6 + package.nls.json | 1 + src/cmakeProject.ts | 118 +++++++++++++++++ src/extension.ts | 14 ++ src/installUtils.ts | 18 +++ .../backend/installComponents.test.ts | 123 ++++++++++++++++++ 7 files changed, 281 insertions(+) create mode 100644 src/installUtils.ts create mode 100644 test/unit-tests/backend/installComponents.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a8bb9870..7120c815c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Features: - Add `cmake.shell` setting to route CMake/CTest/CPack subprocess invocations through a custom shell (e.g., Git Bash, MSYS2), enabling embedded toolchains that require POSIX path translation on Windows. [#1750](https://github.com/microsoft/vscode-cmake-tools/issues/1750) +- Add "CMake: Install Component" command for installing specific CMake install components. [#4281](https://github.com/microsoft/vscode-cmake-tools/issues/4281) - triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate) - Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691) - Clear build diagnostics from the Problems pane when a new build starts and populate them incrementally during the build. [#4608](https://github.com/microsoft/vscode-cmake-tools/issues/4608) diff --git a/package.json b/package.json index 374d08b8f9..86bd92727d 100644 --- a/package.json +++ b/package.json @@ -490,6 +490,12 @@ "when": "cmake:enableFullFeatureSet", "category": "CMake" }, + { + "command": "cmake.installComponent", + "title": "%cmake-tools.command.cmake.installComponent.title%", + "when": "cmake:enableFullFeatureSet", + "category": "CMake" + }, { "command": "cmake.buildWithTarget", "title": "%cmake-tools.command.cmake.buildWithTarget.title%", diff --git a/package.nls.json b/package.nls.json index 0959fe5455..adf7758ff5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -40,6 +40,7 @@ "cmake-tools.command.cmake.outline.compileFile.title": "Compile File", "cmake-tools.command.cmake.install.title": "Install", "cmake-tools.command.cmake.installAll.title": "Install All Projects", + "cmake-tools.command.cmake.installComponent.title": "Install Component", "cmake-tools.command.cmake.buildWithTarget.title": "Build Target", "cmake-tools.command.cmake.setDefaultTarget.title": "Set Build Target", "cmake-tools.command.cmake.cleanConfigure.title": "Delete Cache and Reconfigure", diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index 36bc7ba527..833395b935 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -54,6 +54,7 @@ import { DebugTrackerFactory, DebuggerInformation, getDebuggerPipeName } from '@ import { NamedTarget, RichTarget, FolderTarget } from '@cmt/drivers/cmakeDriver'; import { CommandResult, ConfigurationType } from 'vscode-cmake-tools'; +import { parseInstallComponentsFromContent } from '@cmt/installUtils'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @@ -2639,6 +2640,121 @@ export class CMakeProject { return this.build(['install'], false, false, cancellationToken); } + /** + * Parse cmake_install.cmake in the build directory to discover install component names. + */ + async getInstallComponents(): Promise { + const binaryDir = await this.binaryDir; + if (!binaryDir) { + return []; + } + const cmakeInstallFile = path.join(binaryDir, 'cmake_install.cmake'); + if (!await fs.exists(cmakeInstallFile)) { + return []; + } + try { + const content = await fs.readFile(cmakeInstallFile); + return parseInstallComponentsFromContent(content); + } catch { + return []; + } + } + + /** + * Show a picker for selecting an install component. Falls back to input box when no components found. + */ + async showComponentSelector(): Promise { + const components = await this.getInstallComponents(); + if (components.length > 0) { + const sel = await vscode.window.showQuickPick( + components.map(c => ({ label: c, description: localize('install.component.description', 'Install component') })), + { placeHolder: localize('select.install.component', 'Select an install component') } + ); + return sel ? sel.label : null; + } + return await vscode.window.showInputBox({ prompt: localize('enter.component.name', 'Enter a component name') }) || null; + } + + /** + * Implementation of `cmake.installComponent` + */ + async installComponent(component?: string): Promise { + if (!component) { + const selected = await this.showComponentSelector(); + if (!selected) { + return { exitCode: -1 }; + } + component = selected; + } + + const cmake = await this.getCMakeExecutable(); + if (!cmake.isPresent) { + void vscode.window.showErrorMessage(localize('bad.executable', 'Bad CMake executable: {0}. Check to make sure it is installed or the value of the {1} setting contains the correct path', `"${cmake.path}"`, '"cmake.cmakePath"')); + return { exitCode: -1 }; + } + + const minInstallVersion = util.parseVersion('3.15.0'); + if (!cmake.version || !util.versionGreaterOrEquals(cmake.version, minInstallVersion)) { + void vscode.window.showErrorMessage(localize('cmake.install.component.version.error', 'CMake version 3.15 or later is required for component-based install. Current version: {0}', cmake.version ? util.versionToString(cmake.version) : 'unknown')); + return { exitCode: -1 }; + } + + const drv = await this.getCMakeDriverInstance(); + if (!drv) { + void vscode.window.showErrorMessage(localize('set.up.before.install.component', 'Set up your CMake project before installing a component.')); + return { exitCode: -1 }; + } + + const configResult = await this.ensureConfigured(); + if (configResult === null || configResult.exitCode !== 0) { + return { exitCode: configResult?.exitCode ?? -1 }; + } + + // Re-fetch driver after configure + const driver = await this.getCMakeDriverInstance(); + if (!driver) { + return { exitCode: -1 }; + } + + const binaryDir = driver.binaryDir; + const args: string[] = ['--install', binaryDir, '--component', component]; + + const buildType = await this.currentBuildType(); + if (buildType) { + args.push('--config', buildType); + } + + const installPrefix = this.workspaceContext.config.installPrefix; + if (installPrefix) { + const opts = driver.expansionOptions; + const expandedPrefix = await expandString(installPrefix, opts); + args.push('--prefix', expandedPrefix); + } + + log.showChannel(); + buildLogger.info(localize('starting.install.component', 'Installing component: {0}', component)); + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: localize('installing.component', 'Installing component: {0}', component), + cancellable: true + }, + async (_progress, cancel) => { + cancel.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop())); + const child = driver.executeCommand(cmake.path, args, undefined, {}); + const result = await child.result; + if (result.retc !== 0) { + buildLogger.error(localize('install.component.failed', 'Install component failed with exit code {0}', result.retc)); + log.showChannel(true); + } else { + buildLogger.info(localize('install.component.finished', 'Install component finished successfully')); + } + return { exitCode: result.retc ?? -1 }; + } + ); + } + /** * Implementation of `cmake.stop` */ @@ -3763,3 +3879,5 @@ export class CMakeProject { } export default CMakeProject; + +export { parseInstallComponentsFromContent } from '@cmt/installUtils'; diff --git a/src/extension.ts b/src/extension.ts index d36f7df37f..01d86b83c2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1541,6 +1541,19 @@ export class ExtensionManager implements vscode.Disposable { return this.runCMakeCommandForAll(cmakeProject => cmakeProject.install(), undefined, true); } + async installComponent(component?: string) { + telemetry.logEvent("install", { command: "installComponent"}); + this.cleanOutputChannel(); + let activeProject: CMakeProject | undefined = this.getActiveProject(); + if (!activeProject) { + activeProject = await this.pickCMakeProject(); + if (!activeProject) { + return; // Error or nothing is opened + } + } + return activeProject.installComponent(component); + } + editCache(folder: vscode.WorkspaceFolder) { telemetry.logEvent("editCMakeCache", { command: "editCMakeCache" }); return this.runCMakeCommand(cmakeProject => cmakeProject.editCache(), folder); @@ -2425,6 +2438,7 @@ async function setup(context: vscode.ExtensionContext, progress?: ProgressHandle 'setVariantAll', 'install', 'installAll', + 'installComponent', 'editCache', 'clean', 'cleanAll', diff --git a/src/installUtils.ts b/src/installUtils.ts new file mode 100644 index 0000000000..6ba01b67dd --- /dev/null +++ b/src/installUtils.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parse install component names from cmake_install.cmake file content. + * Extracts component names from `if(CMAKE_INSTALL_COMPONENT STREQUAL "")` blocks. + */ +export function parseInstallComponentsFromContent(content: string): string[] { + const regex = /if\(CMAKE_INSTALL_COMPONENT\s+STREQUAL\s+"([^"]+)"/g; + const components = new Set(); + let match: RegExpExecArray | null; + while ((match = regex.exec(content)) !== null) { + components.add(match[1]); + } + return Array.from(components).sort(); +} diff --git a/test/unit-tests/backend/installComponents.test.ts b/test/unit-tests/backend/installComponents.test.ts new file mode 100644 index 0000000000..8441768587 --- /dev/null +++ b/test/unit-tests/backend/installComponents.test.ts @@ -0,0 +1,123 @@ +import { expect } from 'chai'; +import { parseInstallComponentsFromContent } from '@cmt/installUtils'; + +suite('parseInstallComponentsFromContent', () => { + test('parses single component', () => { + const content = ` +if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['Runtime']); + }); + + test('parses multiple components', () => { + const content = ` +if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Development") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Documentation") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/share/doc" TYPE FILE FILES "/path/to/readme.md") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['Development', 'Documentation', 'Runtime']); + }); + + test('deduplicates repeated components', () => { + const content = ` +if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app1") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app2") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['Runtime']); + }); + + test('returns empty array when no components found', () => { + const content = ` +# No install components in this file +cmake_minimum_required(VERSION 3.15) +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal([]); + }); + + test('returns empty array for empty content', () => { + const components = parseInstallComponentsFromContent(''); + expect(components).to.deep.equal([]); + }); + + test('returns sorted components', () => { + const content = ` +if(CMAKE_INSTALL_COMPONENT STREQUAL "Zebra") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/z") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Alpha") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/a") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Middle") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/m") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['Alpha', 'Middle', 'Zebra']); + }); + + test('handles typical cmake_install.cmake content', () => { + const content = `# Install script for directory: /home/user/project + +# Set the install prefix +if(NOT DEFINED CMAKE_INSTALL_PREFIX) + set(CMAKE_INSTALL_PREFIX "/usr/local") +endif() +string(REGEX REPLACE "/\$" "" CMAKE_INSTALL_PREFIX "\${CMAKE_INSTALL_PREFIX}") + +# Set the install configuration name. +if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) + if(BUILD_TYPE) + string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" CMAKE_INSTALL_CONFIG_NAME "\${BUILD_TYPE}") + else() + set(CMAKE_INSTALL_CONFIG_NAME "") + endif() + message(STATUS "Install configuration: \\"\${CMAKE_INSTALL_CONFIG_NAME}\\"") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime" OR NOT CMAKE_INSTALL_COMPONENT) + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Development" OR NOT CMAKE_INSTALL_COMPONENT) + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/lib" TYPE STATIC_LIBRARY FILES "/path/to/lib.a") +endif() + +if(CMAKE_INSTALL_COMPONENT STREQUAL "Headers" OR NOT CMAKE_INSTALL_COMPONENT) + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['Development', 'Headers', 'Runtime']); + }); + + test('handles components with special characters in names', () => { + const content = ` +if(CMAKE_INSTALL_COMPONENT STREQUAL "my-component_v2") + file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/file") +endif() +`; + const components = parseInstallComponentsFromContent(content); + expect(components).to.deep.equal(['my-component_v2']); + }); +}); From b9c91f2f520148d8deed81fa5e3070bd37b58de1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:02:21 +0000 Subject: [PATCH 2/5] refactor: remove unnecessary re-export from cmakeProject.ts Co-authored-by: hanniavalera <90047725+hanniavalera@users.noreply.github.com> --- src/cmakeProject.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index 833395b935..6abe30a23e 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -3879,5 +3879,3 @@ export class CMakeProject { } export default CMakeProject; - -export { parseInstallComponentsFromContent } from '@cmt/installUtils'; From 1f74583ae6fde2f76bf7053915d035f97606a97a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:15:17 +0000 Subject: [PATCH 3/5] refactor: make cmake.install a first-class operation using cmake --install Previously, cmake.install was implemented as `this.build(['install'])`, routing through `cmake --build --target install`. This didn't match CMake's actual command model where build and install are distinct CLI actions. Now install uses `cmake --install ` (CMake >= 3.15): - Add CMakeDriver.getCMakeInstallCommand() for command construction - Add CMakeDriver.install() as a first-class driver method with its own concurrency gating, telemetry, and output handling - Refactor CMakeProject.install() to use drv.install() with proper progress/cancellation/output UX - Add CMakeTaskProvider.runInstallTask() so install tasks use the new path instead of routing through runBuildTask - Falls back to cmake --build --target install for CMake < 3.15 - Add 6 unit tests for install command construction All existing API surface, telemetry, cancellation, and output behavior is preserved. The install task and command-palette install now use the same coherent code path. Co-authored-by: hanniavalera <90047725+hanniavalera@users.noreply.github.com> --- src/cmakeProject.ts | 57 +++++++- src/cmakeTaskProvider.ts | 39 +++++- src/drivers/cmakeDriver.ts | 98 +++++++++++++ .../unit-tests/backend/installCommand.test.ts | 132 ++++++++++++++++++ 4 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 test/unit-tests/backend/installCommand.test.ts diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index 6abe30a23e..db3f515e22 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -2636,8 +2636,61 @@ export class CMakeProject { /** * Implementation of `cmake.install` */ - install(cancellationToken?: vscode.CancellationToken): Promise { - return this.build(['install'], false, false, cancellationToken); + async install(cancellationToken?: vscode.CancellationToken): Promise { + log.info(localize('run.install', 'Installing folder: {0}', await this.binaryDir || this.folderName)); + + const configResult = await this.ensureConfigured(cancellationToken); + if (configResult === null) { + throw new Error(localize('unable.to.configure', 'Build failed: Unable to configure the project')); + } else if (configResult.exitCode !== 0) { + return { + exitCode: configResult.exitCode, + stdout: configResult.stdout, + stderr: configResult.stderr + }; + } + const drv = await this.getCMakeDriverInstance(); + if (!drv) { + throw new Error(localize('driver.died.after.successful.configure', 'CMake driver died immediately after successful configure')); + } + + const isBuildingKey = 'cmake:isBuilding'; + try { + this.statusMessage.set(localize('installing.status', 'Installing')); + this.isBusy.set(true); + + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: localize('installing', 'Installing'), + cancellable: true + }, + async (_progress, cancel) => { + const combinedToken = util.createCombinedCancellationToken(cancel, cancellationToken); + combinedToken.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop())); + log.showChannel(); + buildLogger.info(localize('starting.install', 'Starting install')); + await setContextAndStore(isBuildingKey, true); + const rc = await drv!.install(undefined, false); + await setContextAndStore(isBuildingKey, false); + if (rc !== 0) { + log.showChannel(true); + } + if (rc === null) { + buildLogger.info(localize('install.was.terminated', 'Install was terminated')); + } else { + buildLogger.info(localize('install.finished.with.code', 'Install finished with exit code {0}', rc)); + } + return { + exitCode: rc === null ? -1 : rc + }; + } + ); + } finally { + await setContextAndStore(isBuildingKey, false); + this.statusMessage.set(localize('ready.status', 'Ready')); + this.isBusy.set(false); + } } /** diff --git a/src/cmakeTaskProvider.ts b/src/cmakeTaskProvider.ts index 972664a25e..08a3dec74d 100644 --- a/src/cmakeTaskProvider.ts +++ b/src/cmakeTaskProvider.ts @@ -398,7 +398,7 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc await this.runBuildTask(CommandType.build); break; case CommandType.install: - await this.runBuildTask(CommandType.install); + await this.runInstallTask(); break; case CommandType.test: await this.runTestTask(); @@ -441,9 +441,7 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc this.writeEmitter.fire(localize("target.is.ignored", "The defined targets in this task are being ignored.") + endOfLine); } - if (commandType === CommandType.install) { - targets = ['install']; - } else if (commandType === CommandType.clean) { + if (commandType === CommandType.clean) { targets = ['clean']; } else if (!shouldIgnore && !targetIsDefined && !project.useCMakePresets) { targets = [await project.buildTargetName() || await project.allTargetName]; @@ -622,6 +620,39 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc } } + private async runInstallTask(): Promise { + this.writeEmitter.fire(localize("install.started", "Install task started...") + endOfLine); + + const project: CMakeProject | undefined = await this.getProject(); + if (!project || !await this.isTaskCompatibleWithPresets(project)) { + return; + } + telemetry.logEvent("task", { taskType: "install", useCMakePresets: String(project.useCMakePresets) }); + const cmakeDriver: CMakeDriver | undefined = (await project?.getCMakeDriverInstance()) || undefined; + + if (cmakeDriver) { + const installCmd = cmakeDriver.getCMakeInstallCommand(); + if (installCmd) { + this.writeEmitter.fire(proc.buildCmdStr(installCmd.command, installCmd.args) + endOfLine); + } + const result: number | null = await cmakeDriver.install(this, false); + if (result === null || result === undefined) { + this.writeEmitter.fire(localize('install.terminated', 'Install was terminated') + endOfLine); + this.closeEmitter.fire(-1); + } else if (result !== 0) { + this.writeEmitter.fire(localize("install.finished.with.error", "Install finished with error(s).") + endOfLine); + this.closeEmitter.fire(result); + } else { + this.writeEmitter.fire(localize('install.finished', 'Install finished successfully') + endOfLine); + this.closeEmitter.fire(0); + } + } else { + log.debug(localize("cmake.driver.not.found", 'CMake driver not found.')); + this.writeEmitter.fire(localize("install.failed", "Install failed.") + endOfLine); + this.closeEmitter.fire(-1); + } + } + private async runTestTask(): Promise { this.writeEmitter.fire(localize("test.started", "Test task started...") + endOfLine); diff --git a/src/drivers/cmakeDriver.ts b/src/drivers/cmakeDriver.ts index 7b6b5b8f67..6049bf804e 100644 --- a/src/drivers/cmakeDriver.ts +++ b/src/drivers/cmakeDriver.ts @@ -2066,6 +2066,104 @@ export abstract class CMakeDriver implements vscode.Disposable { } } + /** + * Whether this CMake version supports `cmake --install` (>= 3.15). + */ + get supportsInstallCommand(): boolean { + return !!this.cmake.version && util.versionGreaterOrEquals(this.cmake.version, util.parseVersion('3.15.0')); + } + + /** + * Construct the command line for `cmake --install `. + * Returns null if cmake --install is not supported. + */ + getCMakeInstallCommand(): proc.BuildCommand | null { + if (!this.supportsInstallCommand) { + return null; + } + const args: string[] = ['--install', this.binaryDir]; + + // Multi-config generators need --config + if (this.isMultiConfFast || this.isMultiConfig) { + args.push('--config', this.currentBuildType); + } + + // Honor cmake.installPrefix for --prefix + if (this.installDir) { + args.push('--prefix', this.installDir); + } + + return { command: this.cmake.path, args, build_env: {} }; + } + + /** + * Run cmake --install. Uses the build runner for concurrency gating and cancellation. + * Falls back to `cmake --build --target install` on CMake < 3.15. + */ + async install(consumer?: proc.OutputConsumer, isBuildCommand?: boolean): Promise { + log.debug(localize('start.install', 'Start install')); + if (this.isConfigInProgress) { + await this.preconditionHandler(CMakePreconditionProblems.ConfigureIsAlreadyRunning); + return -1; + } + if (this.cmakeBuildRunner.isBuildInProgress()) { + await this.preconditionHandler(CMakePreconditionProblems.BuildIsAlreadyRunning); + return -1; + } + + const installCmd = this.getCMakeInstallCommand(); + if (!installCmd) { + // Fallback for CMake < 3.15: use cmake --build --target install + return this.build(['install'], consumer, isBuildCommand); + } + + this.cmakeBuildRunner.setBuildInProgress(true); + + const pre_build_ok = await this.doPreBuild(); + if (!pre_build_ok) { + this.cmakeBuildRunner.setBuildInProgress(false); + return -1; + } + + const timeStart: number = new Date().getTime(); + + let outputEnc = this.config.outputLogEncoding; + const isAutoEncoding = outputEnc === 'auto'; + if (isAutoEncoding) { + if (process.platform === 'win32') { + outputEnc = await codepages.getWindowsCodepage(); + } else { + outputEnc = 'utf8'; + } + } + const exeOpt: proc.ExecutionOptions = { environment: installCmd.build_env, outputEncoding: outputEnc, useAutoEncoding: isAutoEncoding }; + this.cmakeBuildRunner.setBuildProcess(this.executeCommand(installCmd.command, installCmd.args, consumer, exeOpt)); + + const child = await this.cmakeBuildRunner.getResult(); + + const timeEnd: number = new Date().getTime(); + const duration: number = timeEnd - timeStart; + log.info(localize('install.duration', 'Install completed: {0}', util.msToString(duration))); + const telemetryMeasures: telemetry.Measures = { Duration: duration }; + + if (child) { + telemetry.logEvent('install', undefined, telemetryMeasures); + } else { + telemetryMeasures['ErrorCount'] = 1; + telemetry.logEvent('install', undefined, telemetryMeasures); + this.cmakeBuildRunner.setBuildInProgress(false); + return -1; + } + + if (!this.m_stop_process) { + await this._refreshExpansions(); + } + + return (await child.result.finally(() => { + this.cmakeBuildRunner.setBuildInProgress(false); + })).retc; + } + private async _doCMakeBuild(targets?: string[], consumer?: proc.OutputConsumer, isBuildCommand?: boolean): Promise { const buildcmd = await this.getCMakeBuildCommand(targets); if (buildcmd) { diff --git a/test/unit-tests/backend/installCommand.test.ts b/test/unit-tests/backend/installCommand.test.ts new file mode 100644 index 0000000000..2297337ae8 --- /dev/null +++ b/test/unit-tests/backend/installCommand.test.ts @@ -0,0 +1,132 @@ +import { expect } from 'chai'; + +/** + * Mirror of the install command construction logic from CMakeDriver.getCMakeInstallCommand(). + * This tests the command construction independently from the vscode-dependent driver class. + */ +interface BuildCommand { + command: string; + args: string[]; + build_env: Record; +} + +interface InstallCommandParams { + cmakePath: string; + binaryDir: string; + isMultiConf: boolean; + currentBuildType: string; + installDir: string | null; + supportsInstallCommand: boolean; +} + +function getCMakeInstallCommand(params: InstallCommandParams): BuildCommand | null { + if (!params.supportsInstallCommand) { + return null; + } + const args: string[] = ['--install', params.binaryDir]; + + if (params.isMultiConf) { + args.push('--config', params.currentBuildType); + } + + if (params.installDir) { + args.push('--prefix', params.installDir); + } + + return { command: params.cmakePath, args, build_env: {} }; +} + +suite('[Install Command Construction]', () => { + test('basic install command for single-config generator', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: '/usr/bin/cmake', + binaryDir: '/home/user/project/build', + isMultiConf: false, + currentBuildType: 'Release', + installDir: null, + supportsInstallCommand: true + }); + expect(cmd).to.not.be.null; + expect(cmd!.command).to.equal('/usr/bin/cmake'); + expect(cmd!.args).to.deep.equal(['--install', '/home/user/project/build']); + }); + + test('install command includes --config for multi-config generator', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: '/usr/bin/cmake', + binaryDir: '/home/user/project/build', + isMultiConf: true, + currentBuildType: 'Debug', + installDir: null, + supportsInstallCommand: true + }); + expect(cmd).to.not.be.null; + expect(cmd!.args).to.deep.equal([ + '--install', '/home/user/project/build', + '--config', 'Debug' + ]); + }); + + test('install command includes --prefix when installDir is set', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: '/usr/bin/cmake', + binaryDir: '/home/user/project/build', + isMultiConf: false, + currentBuildType: 'Release', + installDir: '/home/user/project/_install', + supportsInstallCommand: true + }); + expect(cmd).to.not.be.null; + expect(cmd!.args).to.deep.equal([ + '--install', '/home/user/project/build', + '--prefix', '/home/user/project/_install' + ]); + }); + + test('install command includes both --config and --prefix when needed', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: 'C:\\cmake\\bin\\cmake.exe', + binaryDir: 'C:\\project\\build', + isMultiConf: true, + currentBuildType: 'Release', + installDir: 'C:\\project\\_install', + supportsInstallCommand: true + }); + expect(cmd).to.not.be.null; + expect(cmd!.args).to.deep.equal([ + '--install', 'C:\\project\\build', + '--config', 'Release', + '--prefix', 'C:\\project\\_install' + ]); + }); + + test('returns null when cmake version does not support --install', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: '/usr/bin/cmake', + binaryDir: '/home/user/project/build', + isMultiConf: false, + currentBuildType: 'Release', + installDir: null, + supportsInstallCommand: false + }); + expect(cmd).to.be.null; + }); + + test('installDir null does not produce --prefix', () => { + const cmd = getCMakeInstallCommand({ + cmakePath: '/usr/bin/cmake', + binaryDir: '/build', + isMultiConf: true, + currentBuildType: 'RelWithDebInfo', + installDir: null, + supportsInstallCommand: true + }); + expect(cmd).to.not.be.null; + expect(cmd!.args).to.deep.equal([ + '--install', '/build', + '--config', 'RelWithDebInfo' + ]); + // Ensure no --prefix argument when installDir is null + expect(cmd!.args).to.not.include('--prefix'); + }); +}); From 829abe27af35c78c838a7a47095036bff4386356 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:24:55 +0000 Subject: [PATCH 4/5] revert: remove old installComponent code, keep only install refactor Remove the installComponent/installUtils code from previous PR iterations. This branch should only contain the install refactor that makes cmake.install a first-class operation. Co-authored-by: hanniavalera <90047725+hanniavalera@users.noreply.github.com> --- CHANGELOG.md | 1 - package.json | 6 - package.nls.json | 1 - src/cmakeProject.ts | 116 ----------------- src/extension.ts | 14 -- src/installUtils.ts | 18 --- .../backend/installComponents.test.ts | 123 ------------------ 7 files changed, 279 deletions(-) delete mode 100644 src/installUtils.ts delete mode 100644 test/unit-tests/backend/installComponents.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7120c815c7..b2a8bb9870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ Features: - Add `cmake.shell` setting to route CMake/CTest/CPack subprocess invocations through a custom shell (e.g., Git Bash, MSYS2), enabling embedded toolchains that require POSIX path translation on Windows. [#1750](https://github.com/microsoft/vscode-cmake-tools/issues/1750) -- Add "CMake: Install Component" command for installing specific CMake install components. [#4281](https://github.com/microsoft/vscode-cmake-tools/issues/4281) - triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate) - Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691) - Clear build diagnostics from the Problems pane when a new build starts and populate them incrementally during the build. [#4608](https://github.com/microsoft/vscode-cmake-tools/issues/4608) diff --git a/package.json b/package.json index 86bd92727d..374d08b8f9 100644 --- a/package.json +++ b/package.json @@ -490,12 +490,6 @@ "when": "cmake:enableFullFeatureSet", "category": "CMake" }, - { - "command": "cmake.installComponent", - "title": "%cmake-tools.command.cmake.installComponent.title%", - "when": "cmake:enableFullFeatureSet", - "category": "CMake" - }, { "command": "cmake.buildWithTarget", "title": "%cmake-tools.command.cmake.buildWithTarget.title%", diff --git a/package.nls.json b/package.nls.json index adf7758ff5..0959fe5455 100644 --- a/package.nls.json +++ b/package.nls.json @@ -40,7 +40,6 @@ "cmake-tools.command.cmake.outline.compileFile.title": "Compile File", "cmake-tools.command.cmake.install.title": "Install", "cmake-tools.command.cmake.installAll.title": "Install All Projects", - "cmake-tools.command.cmake.installComponent.title": "Install Component", "cmake-tools.command.cmake.buildWithTarget.title": "Build Target", "cmake-tools.command.cmake.setDefaultTarget.title": "Set Build Target", "cmake-tools.command.cmake.cleanConfigure.title": "Delete Cache and Reconfigure", diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index db3f515e22..ea54bf905e 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -54,7 +54,6 @@ import { DebugTrackerFactory, DebuggerInformation, getDebuggerPipeName } from '@ import { NamedTarget, RichTarget, FolderTarget } from '@cmt/drivers/cmakeDriver'; import { CommandResult, ConfigurationType } from 'vscode-cmake-tools'; -import { parseInstallComponentsFromContent } from '@cmt/installUtils'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @@ -2693,121 +2692,6 @@ export class CMakeProject { } } - /** - * Parse cmake_install.cmake in the build directory to discover install component names. - */ - async getInstallComponents(): Promise { - const binaryDir = await this.binaryDir; - if (!binaryDir) { - return []; - } - const cmakeInstallFile = path.join(binaryDir, 'cmake_install.cmake'); - if (!await fs.exists(cmakeInstallFile)) { - return []; - } - try { - const content = await fs.readFile(cmakeInstallFile); - return parseInstallComponentsFromContent(content); - } catch { - return []; - } - } - - /** - * Show a picker for selecting an install component. Falls back to input box when no components found. - */ - async showComponentSelector(): Promise { - const components = await this.getInstallComponents(); - if (components.length > 0) { - const sel = await vscode.window.showQuickPick( - components.map(c => ({ label: c, description: localize('install.component.description', 'Install component') })), - { placeHolder: localize('select.install.component', 'Select an install component') } - ); - return sel ? sel.label : null; - } - return await vscode.window.showInputBox({ prompt: localize('enter.component.name', 'Enter a component name') }) || null; - } - - /** - * Implementation of `cmake.installComponent` - */ - async installComponent(component?: string): Promise { - if (!component) { - const selected = await this.showComponentSelector(); - if (!selected) { - return { exitCode: -1 }; - } - component = selected; - } - - const cmake = await this.getCMakeExecutable(); - if (!cmake.isPresent) { - void vscode.window.showErrorMessage(localize('bad.executable', 'Bad CMake executable: {0}. Check to make sure it is installed or the value of the {1} setting contains the correct path', `"${cmake.path}"`, '"cmake.cmakePath"')); - return { exitCode: -1 }; - } - - const minInstallVersion = util.parseVersion('3.15.0'); - if (!cmake.version || !util.versionGreaterOrEquals(cmake.version, minInstallVersion)) { - void vscode.window.showErrorMessage(localize('cmake.install.component.version.error', 'CMake version 3.15 or later is required for component-based install. Current version: {0}', cmake.version ? util.versionToString(cmake.version) : 'unknown')); - return { exitCode: -1 }; - } - - const drv = await this.getCMakeDriverInstance(); - if (!drv) { - void vscode.window.showErrorMessage(localize('set.up.before.install.component', 'Set up your CMake project before installing a component.')); - return { exitCode: -1 }; - } - - const configResult = await this.ensureConfigured(); - if (configResult === null || configResult.exitCode !== 0) { - return { exitCode: configResult?.exitCode ?? -1 }; - } - - // Re-fetch driver after configure - const driver = await this.getCMakeDriverInstance(); - if (!driver) { - return { exitCode: -1 }; - } - - const binaryDir = driver.binaryDir; - const args: string[] = ['--install', binaryDir, '--component', component]; - - const buildType = await this.currentBuildType(); - if (buildType) { - args.push('--config', buildType); - } - - const installPrefix = this.workspaceContext.config.installPrefix; - if (installPrefix) { - const opts = driver.expansionOptions; - const expandedPrefix = await expandString(installPrefix, opts); - args.push('--prefix', expandedPrefix); - } - - log.showChannel(); - buildLogger.info(localize('starting.install.component', 'Installing component: {0}', component)); - - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Window, - title: localize('installing.component', 'Installing component: {0}', component), - cancellable: true - }, - async (_progress, cancel) => { - cancel.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop())); - const child = driver.executeCommand(cmake.path, args, undefined, {}); - const result = await child.result; - if (result.retc !== 0) { - buildLogger.error(localize('install.component.failed', 'Install component failed with exit code {0}', result.retc)); - log.showChannel(true); - } else { - buildLogger.info(localize('install.component.finished', 'Install component finished successfully')); - } - return { exitCode: result.retc ?? -1 }; - } - ); - } - /** * Implementation of `cmake.stop` */ diff --git a/src/extension.ts b/src/extension.ts index 01d86b83c2..d36f7df37f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1541,19 +1541,6 @@ export class ExtensionManager implements vscode.Disposable { return this.runCMakeCommandForAll(cmakeProject => cmakeProject.install(), undefined, true); } - async installComponent(component?: string) { - telemetry.logEvent("install", { command: "installComponent"}); - this.cleanOutputChannel(); - let activeProject: CMakeProject | undefined = this.getActiveProject(); - if (!activeProject) { - activeProject = await this.pickCMakeProject(); - if (!activeProject) { - return; // Error or nothing is opened - } - } - return activeProject.installComponent(component); - } - editCache(folder: vscode.WorkspaceFolder) { telemetry.logEvent("editCMakeCache", { command: "editCMakeCache" }); return this.runCMakeCommand(cmakeProject => cmakeProject.editCache(), folder); @@ -2438,7 +2425,6 @@ async function setup(context: vscode.ExtensionContext, progress?: ProgressHandle 'setVariantAll', 'install', 'installAll', - 'installComponent', 'editCache', 'clean', 'cleanAll', diff --git a/src/installUtils.ts b/src/installUtils.ts deleted file mode 100644 index 6ba01b67dd..0000000000 --- a/src/installUtils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Parse install component names from cmake_install.cmake file content. - * Extracts component names from `if(CMAKE_INSTALL_COMPONENT STREQUAL "")` blocks. - */ -export function parseInstallComponentsFromContent(content: string): string[] { - const regex = /if\(CMAKE_INSTALL_COMPONENT\s+STREQUAL\s+"([^"]+)"/g; - const components = new Set(); - let match: RegExpExecArray | null; - while ((match = regex.exec(content)) !== null) { - components.add(match[1]); - } - return Array.from(components).sort(); -} diff --git a/test/unit-tests/backend/installComponents.test.ts b/test/unit-tests/backend/installComponents.test.ts deleted file mode 100644 index 8441768587..0000000000 --- a/test/unit-tests/backend/installComponents.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { expect } from 'chai'; -import { parseInstallComponentsFromContent } from '@cmt/installUtils'; - -suite('parseInstallComponentsFromContent', () => { - test('parses single component', () => { - const content = ` -if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['Runtime']); - }); - - test('parses multiple components', () => { - const content = ` -if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Development") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Documentation") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/share/doc" TYPE FILE FILES "/path/to/readme.md") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['Development', 'Documentation', 'Runtime']); - }); - - test('deduplicates repeated components', () => { - const content = ` -if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app1") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app2") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['Runtime']); - }); - - test('returns empty array when no components found', () => { - const content = ` -# No install components in this file -cmake_minimum_required(VERSION 3.15) -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal([]); - }); - - test('returns empty array for empty content', () => { - const components = parseInstallComponentsFromContent(''); - expect(components).to.deep.equal([]); - }); - - test('returns sorted components', () => { - const content = ` -if(CMAKE_INSTALL_COMPONENT STREQUAL "Zebra") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/z") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Alpha") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/a") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Middle") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/m") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['Alpha', 'Middle', 'Zebra']); - }); - - test('handles typical cmake_install.cmake content', () => { - const content = `# Install script for directory: /home/user/project - -# Set the install prefix -if(NOT DEFINED CMAKE_INSTALL_PREFIX) - set(CMAKE_INSTALL_PREFIX "/usr/local") -endif() -string(REGEX REPLACE "/\$" "" CMAKE_INSTALL_PREFIX "\${CMAKE_INSTALL_PREFIX}") - -# Set the install configuration name. -if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME) - if(BUILD_TYPE) - string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" CMAKE_INSTALL_CONFIG_NAME "\${BUILD_TYPE}") - else() - set(CMAKE_INSTALL_CONFIG_NAME "") - endif() - message(STATUS "Install configuration: \\"\${CMAKE_INSTALL_CONFIG_NAME}\\"") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime" OR NOT CMAKE_INSTALL_COMPONENT) - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Development" OR NOT CMAKE_INSTALL_COMPONENT) - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/lib" TYPE STATIC_LIBRARY FILES "/path/to/lib.a") -endif() - -if(CMAKE_INSTALL_COMPONENT STREQUAL "Headers" OR NOT CMAKE_INSTALL_COMPONENT) - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['Development', 'Headers', 'Runtime']); - }); - - test('handles components with special characters in names', () => { - const content = ` -if(CMAKE_INSTALL_COMPONENT STREQUAL "my-component_v2") - file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/file") -endif() -`; - const components = parseInstallComponentsFromContent(content); - expect(components).to.deep.equal(['my-component_v2']); - }); -}); From 98021c4a06dfb28f40beab4823635cfb01f782ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:26:23 +0000 Subject: [PATCH 5/5] style: address code review feedback - clarify parameter names and test docs Co-authored-by: hanniavalera <90047725+hanniavalera@users.noreply.github.com> --- src/cmakeTaskProvider.ts | 2 +- test/unit-tests/backend/installCommand.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmakeTaskProvider.ts b/src/cmakeTaskProvider.ts index 08a3dec74d..1884d0a14c 100644 --- a/src/cmakeTaskProvider.ts +++ b/src/cmakeTaskProvider.ts @@ -635,7 +635,7 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc if (installCmd) { this.writeEmitter.fire(proc.buildCmdStr(installCmd.command, installCmd.args) + endOfLine); } - const result: number | null = await cmakeDriver.install(this, false); + const result: number | null = await cmakeDriver.install(this, /* isBuildCommand */ false); if (result === null || result === undefined) { this.writeEmitter.fire(localize('install.terminated', 'Install was terminated') + endOfLine); this.closeEmitter.fire(-1); diff --git a/test/unit-tests/backend/installCommand.test.ts b/test/unit-tests/backend/installCommand.test.ts index 2297337ae8..76ba76c320 100644 --- a/test/unit-tests/backend/installCommand.test.ts +++ b/test/unit-tests/backend/installCommand.test.ts @@ -2,7 +2,9 @@ import { expect } from 'chai'; /** * Mirror of the install command construction logic from CMakeDriver.getCMakeInstallCommand(). - * This tests the command construction independently from the vscode-dependent driver class. + * Backend tests cannot import modules that depend on 'vscode', so we mirror the pure + * command-construction logic here. If the driver implementation changes, update this mirror + * to match. See also: targetMap.test.ts and shell-propagation.test.ts for the same pattern. */ interface BuildCommand { command: string;