Skip to content

Commit 222be99

Browse files
Copilothanniavalera
authored 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 948ce78 commit 222be99

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
@@ -4,6 +4,7 @@
44

55
Features:
66
- 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)
7+
- Add "CMake: Install Component" command for installing specific CMake install components. [#4281](https://github.com/microsoft/vscode-cmake-tools/issues/4281)
78
- triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate)
89
- Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691)
910
- 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();
@@ -2639,6 +2640,121 @@ export class CMakeProject {
26392640
return this.build(['install'], false, false, cancellationToken);
26402641
}
26412642

2643+
/**
2644+
* Parse cmake_install.cmake in the build directory to discover install component names.
2645+
*/
2646+
async getInstallComponents(): Promise<string[]> {
2647+
const binaryDir = await this.binaryDir;
2648+
if (!binaryDir) {
2649+
return [];
2650+
}
2651+
const cmakeInstallFile = path.join(binaryDir, 'cmake_install.cmake');
2652+
if (!await fs.exists(cmakeInstallFile)) {
2653+
return [];
2654+
}
2655+
try {
2656+
const content = await fs.readFile(cmakeInstallFile);
2657+
return parseInstallComponentsFromContent(content);
2658+
} catch {
2659+
return [];
2660+
}
2661+
}
2662+
2663+
/**
2664+
* Show a picker for selecting an install component. Falls back to input box when no components found.
2665+
*/
2666+
async showComponentSelector(): Promise<string | null> {
2667+
const components = await this.getInstallComponents();
2668+
if (components.length > 0) {
2669+
const sel = await vscode.window.showQuickPick(
2670+
components.map(c => ({ label: c, description: localize('install.component.description', 'Install component') })),
2671+
{ placeHolder: localize('select.install.component', 'Select an install component') }
2672+
);
2673+
return sel ? sel.label : null;
2674+
}
2675+
return await vscode.window.showInputBox({ prompt: localize('enter.component.name', 'Enter a component name') }) || null;
2676+
}
2677+
2678+
/**
2679+
* Implementation of `cmake.installComponent`
2680+
*/
2681+
async installComponent(component?: string): Promise<CommandResult> {
2682+
if (!component) {
2683+
const selected = await this.showComponentSelector();
2684+
if (!selected) {
2685+
return { exitCode: -1 };
2686+
}
2687+
component = selected;
2688+
}
2689+
2690+
const cmake = await this.getCMakeExecutable();
2691+
if (!cmake.isPresent) {
2692+
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"'));
2693+
return { exitCode: -1 };
2694+
}
2695+
2696+
const minInstallVersion = util.parseVersion('3.15.0');
2697+
if (!cmake.version || !util.versionGreaterOrEquals(cmake.version, minInstallVersion)) {
2698+
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'));
2699+
return { exitCode: -1 };
2700+
}
2701+
2702+
const drv = await this.getCMakeDriverInstance();
2703+
if (!drv) {
2704+
void vscode.window.showErrorMessage(localize('set.up.before.install.component', 'Set up your CMake project before installing a component.'));
2705+
return { exitCode: -1 };
2706+
}
2707+
2708+
const configResult = await this.ensureConfigured();
2709+
if (configResult === null || configResult.exitCode !== 0) {
2710+
return { exitCode: configResult?.exitCode ?? -1 };
2711+
}
2712+
2713+
// Re-fetch driver after configure
2714+
const driver = await this.getCMakeDriverInstance();
2715+
if (!driver) {
2716+
return { exitCode: -1 };
2717+
}
2718+
2719+
const binaryDir = driver.binaryDir;
2720+
const args: string[] = ['--install', binaryDir, '--component', component];
2721+
2722+
const buildType = await this.currentBuildType();
2723+
if (buildType) {
2724+
args.push('--config', buildType);
2725+
}
2726+
2727+
const installPrefix = this.workspaceContext.config.installPrefix;
2728+
if (installPrefix) {
2729+
const opts = driver.expansionOptions;
2730+
const expandedPrefix = await expandString(installPrefix, opts);
2731+
args.push('--prefix', expandedPrefix);
2732+
}
2733+
2734+
log.showChannel();
2735+
buildLogger.info(localize('starting.install.component', 'Installing component: {0}', component));
2736+
2737+
return vscode.window.withProgress(
2738+
{
2739+
location: vscode.ProgressLocation.Window,
2740+
title: localize('installing.component', 'Installing component: {0}', component),
2741+
cancellable: true
2742+
},
2743+
async (_progress, cancel) => {
2744+
cancel.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop()));
2745+
const child = driver.executeCommand(cmake.path, args, undefined, {});
2746+
const result = await child.result;
2747+
if (result.retc !== 0) {
2748+
buildLogger.error(localize('install.component.failed', 'Install component failed with exit code {0}', result.retc));
2749+
log.showChannel(true);
2750+
} else {
2751+
buildLogger.info(localize('install.component.finished', 'Install component finished successfully'));
2752+
}
2753+
return { exitCode: result.retc ?? -1 };
2754+
}
2755+
);
2756+
}
2757+
26422758
/**
26432759
* Implementation of `cmake.stop`
26442760
*/
@@ -3763,3 +3879,5 @@ export class CMakeProject {
37633879
}
37643880

37653881
export default CMakeProject;
3882+
3883+
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)