Skip to content

Commit fb4a236

Browse files
Copilothanniavalera
andcommitted
feat: add CMake: Install Component command (#4281)
Add a new 'CMake: Install Component' command that uses 'cmake --install <dir> --component <name>' 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 <[email protected]>
1 parent 779c505 commit fb4a236

7 files changed

Lines changed: 281 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.23
44

55
Features:
6+
- Add "CMake: Install Component" command for installing specific CMake install components. [#4281](https://github.com/microsoft/vscode-cmake-tools/issues/4281)
67
- triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate)
78
- Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691)
89
- 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)

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,12 @@
490490
"when": "cmake:enableFullFeatureSet",
491491
"category": "CMake"
492492
},
493+
{
494+
"command": "cmake.installComponent",
495+
"title": "%cmake-tools.command.cmake.installComponent.title%",
496+
"when": "cmake:enableFullFeatureSet",
497+
"category": "CMake"
498+
},
493499
{
494500
"command": "cmake.buildWithTarget",
495501
"title": "%cmake-tools.command.cmake.buildWithTarget.title%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"cmake-tools.command.cmake.outline.compileFile.title": "Compile File",
4141
"cmake-tools.command.cmake.install.title": "Install",
4242
"cmake-tools.command.cmake.installAll.title": "Install All Projects",
43+
"cmake-tools.command.cmake.installComponent.title": "Install Component",
4344
"cmake-tools.command.cmake.buildWithTarget.title": "Build Target",
4445
"cmake-tools.command.cmake.setDefaultTarget.title": "Set Build Target",
4546
"cmake-tools.command.cmake.cleanConfigure.title": "Delete Cache and Reconfigure",

src/cmakeProject.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { DebugTrackerFactory, DebuggerInformation, getDebuggerPipeName } from '@
5454
import { NamedTarget, RichTarget, FolderTarget } from '@cmt/drivers/cmakeDriver';
5555

5656
import { CommandResult, ConfigurationType } from 'vscode-cmake-tools';
57+
import { parseInstallComponentsFromContent } from '@cmt/installUtils';
5758

5859
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
5960
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
@@ -2584,6 +2585,121 @@ export class CMakeProject {
25842585
return this.build(['install'], false, false, cancellationToken);
25852586
}
25862587

2588+
/**
2589+
* Parse cmake_install.cmake in the build directory to discover install component names.
2590+
*/
2591+
async getInstallComponents(): Promise<string[]> {
2592+
const binaryDir = await this.binaryDir;
2593+
if (!binaryDir) {
2594+
return [];
2595+
}
2596+
const cmakeInstallFile = path.join(binaryDir, 'cmake_install.cmake');
2597+
if (!await fs.exists(cmakeInstallFile)) {
2598+
return [];
2599+
}
2600+
try {
2601+
const content = await fs.readFile(cmakeInstallFile);
2602+
return parseInstallComponentsFromContent(content);
2603+
} catch {
2604+
return [];
2605+
}
2606+
}
2607+
2608+
/**
2609+
* Show a picker for selecting an install component. Falls back to input box when no components found.
2610+
*/
2611+
async showComponentSelector(): Promise<string | null> {
2612+
const components = await this.getInstallComponents();
2613+
if (components.length > 0) {
2614+
const sel = await vscode.window.showQuickPick(
2615+
components.map(c => ({ label: c, description: localize('install.component.description', 'Install component') })),
2616+
{ placeHolder: localize('select.install.component', 'Select an install component') }
2617+
);
2618+
return sel ? sel.label : null;
2619+
}
2620+
return await vscode.window.showInputBox({ prompt: localize('enter.component.name', 'Enter a component name') }) || null;
2621+
}
2622+
2623+
/**
2624+
* Implementation of `cmake.installComponent`
2625+
*/
2626+
async installComponent(component?: string): Promise<CommandResult> {
2627+
if (!component) {
2628+
const selected = await this.showComponentSelector();
2629+
if (!selected) {
2630+
return { exitCode: -1 };
2631+
}
2632+
component = selected;
2633+
}
2634+
2635+
const cmake = await this.getCMakeExecutable();
2636+
if (!cmake.isPresent) {
2637+
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"'));
2638+
return { exitCode: -1 };
2639+
}
2640+
2641+
const minInstallVersion = util.parseVersion('3.15.0');
2642+
if (!cmake.version || !util.versionGreaterOrEquals(cmake.version, minInstallVersion)) {
2643+
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'));
2644+
return { exitCode: -1 };
2645+
}
2646+
2647+
const drv = await this.getCMakeDriverInstance();
2648+
if (!drv) {
2649+
void vscode.window.showErrorMessage(localize('set.up.before.install.component', 'Set up your CMake project before installing a component.'));
2650+
return { exitCode: -1 };
2651+
}
2652+
2653+
const configResult = await this.ensureConfigured();
2654+
if (configResult === null || configResult.exitCode !== 0) {
2655+
return { exitCode: configResult?.exitCode ?? -1 };
2656+
}
2657+
2658+
// Re-fetch driver after configure
2659+
const driver = await this.getCMakeDriverInstance();
2660+
if (!driver) {
2661+
return { exitCode: -1 };
2662+
}
2663+
2664+
const binaryDir = driver.binaryDir;
2665+
const args: string[] = ['--install', binaryDir, '--component', component];
2666+
2667+
const buildType = await this.currentBuildType();
2668+
if (buildType) {
2669+
args.push('--config', buildType);
2670+
}
2671+
2672+
const installPrefix = this.workspaceContext.config.installPrefix;
2673+
if (installPrefix) {
2674+
const opts = driver.expansionOptions;
2675+
const expandedPrefix = await expandString(installPrefix, opts);
2676+
args.push('--prefix', expandedPrefix);
2677+
}
2678+
2679+
log.showChannel();
2680+
buildLogger.info(localize('starting.install.component', 'Installing component: {0}', component));
2681+
2682+
return vscode.window.withProgress(
2683+
{
2684+
location: vscode.ProgressLocation.Window,
2685+
title: localize('installing.component', 'Installing component: {0}', component),
2686+
cancellable: true
2687+
},
2688+
async (_progress, cancel) => {
2689+
cancel.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop()));
2690+
const child = driver.executeCommand(cmake.path, args, undefined, {});
2691+
const result = await child.result;
2692+
if (result.retc !== 0) {
2693+
buildLogger.error(localize('install.component.failed', 'Install component failed with exit code {0}', result.retc));
2694+
log.showChannel(true);
2695+
} else {
2696+
buildLogger.info(localize('install.component.finished', 'Install component finished successfully'));
2697+
}
2698+
return { exitCode: result.retc ?? -1 };
2699+
}
2700+
);
2701+
}
2702+
25872703
/**
25882704
* Implementation of `cmake.stop`
25892705
*/
@@ -3711,3 +3827,5 @@ export class CMakeProject {
37113827
}
37123828

37133829
export default CMakeProject;
3830+
3831+
export { parseInstallComponentsFromContent } from '@cmt/installUtils';

src/extension.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,19 @@ export class ExtensionManager implements vscode.Disposable {
15411541
return this.runCMakeCommandForAll(cmakeProject => cmakeProject.install(), undefined, true);
15421542
}
15431543

1544+
async installComponent(component?: string) {
1545+
telemetry.logEvent("install", { command: "installComponent"});
1546+
this.cleanOutputChannel();
1547+
let activeProject: CMakeProject | undefined = this.getActiveProject();
1548+
if (!activeProject) {
1549+
activeProject = await this.pickCMakeProject();
1550+
if (!activeProject) {
1551+
return; // Error or nothing is opened
1552+
}
1553+
}
1554+
return activeProject.installComponent(component);
1555+
}
1556+
15441557
editCache(folder: vscode.WorkspaceFolder) {
15451558
telemetry.logEvent("editCMakeCache", { command: "editCMakeCache" });
15461559
return this.runCMakeCommand(cmakeProject => cmakeProject.editCache(), folder);
@@ -2425,6 +2438,7 @@ async function setup(context: vscode.ExtensionContext, progress?: ProgressHandle
24252438
'setVariantAll',
24262439
'install',
24272440
'installAll',
2441+
'installComponent',
24282442
'editCache',
24292443
'clean',
24302444
'cleanAll',

src/installUtils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* Parse install component names from cmake_install.cmake file content.
8+
* Extracts component names from `if(CMAKE_INSTALL_COMPONENT STREQUAL "<name>")` blocks.
9+
*/
10+
export function parseInstallComponentsFromContent(content: string): string[] {
11+
const regex = /if\(CMAKE_INSTALL_COMPONENT\s+STREQUAL\s+"([^"]+)"/g;
12+
const components = new Set<string>();
13+
let match: RegExpExecArray | null;
14+
while ((match = regex.exec(content)) !== null) {
15+
components.add(match[1]);
16+
}
17+
return Array.from(components).sort();
18+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { expect } from 'chai';
2+
import { parseInstallComponentsFromContent } from '@cmt/installUtils';
3+
4+
suite('parseInstallComponentsFromContent', () => {
5+
test('parses single component', () => {
6+
const content = `
7+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime")
8+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app")
9+
endif()
10+
`;
11+
const components = parseInstallComponentsFromContent(content);
12+
expect(components).to.deep.equal(['Runtime']);
13+
});
14+
15+
test('parses multiple components', () => {
16+
const content = `
17+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime")
18+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app")
19+
endif()
20+
21+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Development")
22+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h")
23+
endif()
24+
25+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Documentation")
26+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/share/doc" TYPE FILE FILES "/path/to/readme.md")
27+
endif()
28+
`;
29+
const components = parseInstallComponentsFromContent(content);
30+
expect(components).to.deep.equal(['Development', 'Documentation', 'Runtime']);
31+
});
32+
33+
test('deduplicates repeated components', () => {
34+
const content = `
35+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime")
36+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app1")
37+
endif()
38+
39+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime")
40+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app2")
41+
endif()
42+
`;
43+
const components = parseInstallComponentsFromContent(content);
44+
expect(components).to.deep.equal(['Runtime']);
45+
});
46+
47+
test('returns empty array when no components found', () => {
48+
const content = `
49+
# No install components in this file
50+
cmake_minimum_required(VERSION 3.15)
51+
`;
52+
const components = parseInstallComponentsFromContent(content);
53+
expect(components).to.deep.equal([]);
54+
});
55+
56+
test('returns empty array for empty content', () => {
57+
const components = parseInstallComponentsFromContent('');
58+
expect(components).to.deep.equal([]);
59+
});
60+
61+
test('returns sorted components', () => {
62+
const content = `
63+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Zebra")
64+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/z")
65+
endif()
66+
67+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Alpha")
68+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/a")
69+
endif()
70+
71+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Middle")
72+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/m")
73+
endif()
74+
`;
75+
const components = parseInstallComponentsFromContent(content);
76+
expect(components).to.deep.equal(['Alpha', 'Middle', 'Zebra']);
77+
});
78+
79+
test('handles typical cmake_install.cmake content', () => {
80+
const content = `# Install script for directory: /home/user/project
81+
82+
# Set the install prefix
83+
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
84+
set(CMAKE_INSTALL_PREFIX "/usr/local")
85+
endif()
86+
string(REGEX REPLACE "/\$" "" CMAKE_INSTALL_PREFIX "\${CMAKE_INSTALL_PREFIX}")
87+
88+
# Set the install configuration name.
89+
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
90+
if(BUILD_TYPE)
91+
string(REGEX REPLACE "^[^A-Za-z0-9_]+" "" CMAKE_INSTALL_CONFIG_NAME "\${BUILD_TYPE}")
92+
else()
93+
set(CMAKE_INSTALL_CONFIG_NAME "")
94+
endif()
95+
message(STATUS "Install configuration: \\"\${CMAKE_INSTALL_CONFIG_NAME}\\"")
96+
endif()
97+
98+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Runtime" OR NOT CMAKE_INSTALL_COMPONENT)
99+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE EXECUTABLE FILES "/path/to/app")
100+
endif()
101+
102+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Development" OR NOT CMAKE_INSTALL_COMPONENT)
103+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/lib" TYPE STATIC_LIBRARY FILES "/path/to/lib.a")
104+
endif()
105+
106+
if(CMAKE_INSTALL_COMPONENT STREQUAL "Headers" OR NOT CMAKE_INSTALL_COMPONENT)
107+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/include" TYPE FILE FILES "/path/to/header.h")
108+
endif()
109+
`;
110+
const components = parseInstallComponentsFromContent(content);
111+
expect(components).to.deep.equal(['Development', 'Headers', 'Runtime']);
112+
});
113+
114+
test('handles components with special characters in names', () => {
115+
const content = `
116+
if(CMAKE_INSTALL_COMPONENT STREQUAL "my-component_v2")
117+
file(INSTALL DESTINATION "\${CMAKE_INSTALL_PREFIX}/bin" TYPE FILE FILES "/path/to/file")
118+
endif()
119+
`;
120+
const components = parseInstallComponentsFromContent(content);
121+
expect(components).to.deep.equal(['my-component_v2']);
122+
});
123+
});

0 commit comments

Comments
 (0)