Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions src/cmakeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2635,8 +2635,61 @@ export class CMakeProject {
/**
* Implementation of `cmake.install`
*/
install(cancellationToken?: vscode.CancellationToken): Promise<CommandResult> {
return this.build(['install'], false, false, cancellationToken);
async install(cancellationToken?: vscode.CancellationToken): Promise<CommandResult> {
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);
}
}

/**
Expand Down
39 changes: 35 additions & 4 deletions src/cmakeTaskProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -622,6 +620,39 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
}
}

private async runInstallTask(): Promise<any> {
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<any> {
this.writeEmitter.fire(localize("test.started", "Test task started...") + endOfLine);

Expand Down
98 changes: 98 additions & 0 deletions src/drivers/cmakeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir>`.
* 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<number | null> {
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<proc.Subprocess | null> {
const buildcmd = await this.getCMakeBuildCommand(targets);
if (buildcmd) {
Expand Down
134 changes: 134 additions & 0 deletions test/unit-tests/backend/installCommand.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
}

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');
});
});