diff --git a/src/cmakeProject.ts b/src/cmakeProject.ts index 36bc7ba527..ea54bf905e 100644 --- a/src/cmakeProject.ts +++ b/src/cmakeProject.ts @@ -2635,8 +2635,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..1884d0a14c 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, /* isBuildCommand */ 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 b1a94a5ee4..e6d6bbc5a9 100644 --- a/src/drivers/cmakeDriver.ts +++ b/src/drivers/cmakeDriver.ts @@ -2067,6 +2067,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..76ba76c320 --- /dev/null +++ b/test/unit-tests/backend/installCommand.test.ts @@ -0,0 +1,134 @@ +import { expect } from 'chai'; + +/** + * Mirror of the install command construction logic from CMakeDriver.getCMakeInstallCommand(). + * 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; + 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'); + }); +});