Skip to content

Commit 0a5086e

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 400a5f8 commit 0a5086e

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
- Add individual CTest test nodes to the Project Outline with inline run/debug buttons, and enable debugging tests from both the Outline and Test Explorer without requiring a launch.json. [#4721](https://github.com/microsoft/vscode-cmake-tools/pull/4721)

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();
@@ -2542,6 +2543,121 @@ export class CMakeProject {
25422543
return this.build(['install'], false, false, cancellationToken);
25432544
}
25442545

2546+
/**
2547+
* Parse cmake_install.cmake in the build directory to discover install component names.
2548+
*/
2549+
async getInstallComponents(): Promise<string[]> {
2550+
const binaryDir = await this.binaryDir;
2551+
if (!binaryDir) {
2552+
return [];
2553+
}
2554+
const cmakeInstallFile = path.join(binaryDir, 'cmake_install.cmake');
2555+
if (!await fs.exists(cmakeInstallFile)) {
2556+
return [];
2557+
}
2558+
try {
2559+
const content = await fs.readFile(cmakeInstallFile);
2560+
return parseInstallComponentsFromContent(content);
2561+
} catch {
2562+
return [];
2563+
}
2564+
}
2565+
2566+
/**
2567+
* Show a picker for selecting an install component. Falls back to input box when no components found.
2568+
*/
2569+
async showComponentSelector(): Promise<string | null> {
2570+
const components = await this.getInstallComponents();
2571+
if (components.length > 0) {
2572+
const sel = await vscode.window.showQuickPick(
2573+
components.map(c => ({ label: c, description: localize('install.component.description', 'Install component') })),
2574+
{ placeHolder: localize('select.install.component', 'Select an install component') }
2575+
);
2576+
return sel ? sel.label : null;
2577+
}
2578+
return await vscode.window.showInputBox({ prompt: localize('enter.component.name', 'Enter a component name') }) || null;
2579+
}
2580+
2581+
/**
2582+
* Implementation of `cmake.installComponent`
2583+
*/
2584+
async installComponent(component?: string): Promise<CommandResult> {
2585+
if (!component) {
2586+
const selected = await this.showComponentSelector();
2587+
if (!selected) {
2588+
return { exitCode: -1 };
2589+
}
2590+
component = selected;
2591+
}
2592+
2593+
const cmake = await this.getCMakeExecutable();
2594+
if (!cmake.isPresent) {
2595+
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"'));
2596+
return { exitCode: -1 };
2597+
}
2598+
2599+
const minInstallVersion = util.parseVersion('3.15.0');
2600+
if (!cmake.version || !util.versionGreaterOrEquals(cmake.version, minInstallVersion)) {
2601+
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'));
2602+
return { exitCode: -1 };
2603+
}
2604+
2605+
const drv = await this.getCMakeDriverInstance();
2606+
if (!drv) {
2607+
void vscode.window.showErrorMessage(localize('set.up.before.install.component', 'Set up your CMake project before installing a component.'));
2608+
return { exitCode: -1 };
2609+
}
2610+
2611+
const configResult = await this.ensureConfigured();
2612+
if (configResult === null || configResult.exitCode !== 0) {
2613+
return { exitCode: configResult?.exitCode ?? -1 };
2614+
}
2615+
2616+
// Re-fetch driver after configure
2617+
const driver = await this.getCMakeDriverInstance();
2618+
if (!driver) {
2619+
return { exitCode: -1 };
2620+
}
2621+
2622+
const binaryDir = driver.binaryDir;
2623+
const args: string[] = ['--install', binaryDir, '--component', component];
2624+
2625+
const buildType = await this.currentBuildType();
2626+
if (buildType) {
2627+
args.push('--config', buildType);
2628+
}
2629+
2630+
const installPrefix = this.workspaceContext.config.installPrefix;
2631+
if (installPrefix) {
2632+
const opts = driver.expansionOptions;
2633+
const expandedPrefix = await expandString(installPrefix, opts);
2634+
args.push('--prefix', expandedPrefix);
2635+
}
2636+
2637+
log.showChannel();
2638+
buildLogger.info(localize('starting.install.component', 'Installing component: {0}', component));
2639+
2640+
return vscode.window.withProgress(
2641+
{
2642+
location: vscode.ProgressLocation.Window,
2643+
title: localize('installing.component', 'Installing component: {0}', component),
2644+
cancellable: true
2645+
},
2646+
async (_progress, cancel) => {
2647+
cancel.onCancellationRequested(() => rollbar.invokeAsync(localize('stop.on.cancellation', 'Stop on cancellation'), () => this.stop()));
2648+
const child = driver.executeCommand(cmake.path, args, undefined, {});
2649+
const result = await child.result;
2650+
if (result.retc !== 0) {
2651+
buildLogger.error(localize('install.component.failed', 'Install component failed with exit code {0}', result.retc));
2652+
log.showChannel(true);
2653+
} else {
2654+
buildLogger.info(localize('install.component.finished', 'Install component finished successfully'));
2655+
}
2656+
return { exitCode: result.retc ?? -1 };
2657+
}
2658+
);
2659+
}
2660+
25452661
/**
25462662
* Implementation of `cmake.stop`
25472663
*/
@@ -3621,3 +3737,5 @@ export class CMakeProject {
36213737
}
36223738

36233739
export default CMakeProject;
3740+
3741+
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)