Skip to content

Commit de43dc9

Browse files
Support kit environment variables in cmake.cmakePath resolution (#4475) (#4867)
* Enable environmentsetupscript for kits * cache calls to avoid redundant calls * remove popup and log event instead --------- Co-authored-by: Hannia Valera <[email protected]>
1 parent 24f6112 commit de43dc9

5 files changed

Lines changed: 113 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Improvements:
4242
- Set the `VSCODE_CMAKE_TOOLS` environment variable for all spawned subprocesses so that `CMakeLists.txt` can detect when CMake is run from VS Code. [#4233](https://github.com/microsoft/vscode-cmake-tools/issues/4233)
4343
- Ensure kit `environmentVariables` expansions (for example, `${env:PATH}` or `${env.PATH}`) use the environment produced by `environmentSetupScript`, so script-updated values are preserved during expansion. [#4091](https://github.com/microsoft/vscode-cmake-tools/issues/4091)
4444
- Honor `debugger.workingDirectory` from the CMake File API when debugging a target, so that the `DEBUGGER_WORKING_DIRECTORY` target property is used as the debugger working directory. [#4595](https://github.com/microsoft/vscode-cmake-tools/issues/4595)
45+
- Honor the active kit environment (including `environmentSetupScript` and kit-defined environment variables) when resolving `cmake.cmakePath` in kits mode, enabling `auto`/`cmake` mode discovery and `${env:...}` substitutions to use kit-provided environment values. [#4475](https://github.com/microsoft/vscode-cmake-tools/issues/4475)
4546
- Add `cmake.removeStaleKitsOnScan` setting to optionally remove stale compiler kits from the kit picker after a "Scan for Kits" when they are no longer rediscovered. This is useful after compiler upgrades that leave older versions outside `PATH`. Set `"keep": true` in a kit entry to prevent automatic removal. [#3852](https://github.com/microsoft/vscode-cmake-tools/issues/3852)
4647
- Add `pr-readiness` Copilot skill to verify PRs have a descriptive title, meaningful description, and a properly formatted CHANGELOG entry. [#4862](https://github.com/microsoft/vscode-cmake-tools/pull/4862)
4748

docs/kits.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,40 @@ The following additional options may be specified:
190190
> The absolute path to a script or a string in form of `"script path" [arg ...]`that modifies/adds environment variables for the kit.
191191
Uses `call` on Windows and `source` in `bash` otherwise.
192192
193+
### Kit environment and CMake executable resolution
194+
195+
When using kits mode (not CMake Presets), the active kit's environment—including variables set via `environmentSetupScript` and `environmentVariables`—influences how CMake Tools resolves the CMake executable path:
196+
197+
1. **Auto discovery (`cmake.cmakePath: auto` or `cmake`):**
198+
The active kit's `PATH` is searched first before falling back to the system `PATH`. This allows a kit's setup script to prepend a custom directory containing a CMake binary, and that binary will be discovered and used.
199+
200+
2. **Environment variable substitution:**
201+
If `cmake.cmakePath` uses `${env:VARIABLE_NAME}` syntax, the substitution resolves to values from the active kit's environment. For example, if a kit defines `MY_CMAKE_PATH` via `environmentVariables` or `environmentSetupScript`, you can set `"cmake.cmakePath": "${env:MY_CMAKE_PATH}/cmake"` and it will resolve using the kit-provided value.
202+
203+
**Example:**
204+
205+
In `.vscode/cmake-kits.json`, define the environment variable in the kit:
206+
```json
207+
[
208+
{
209+
"name": "Cross-Compile Kit",
210+
"environmentSetupScript": "${workspaceFolder}/scripts/setup.sh",
211+
"environmentVariables": {
212+
"MY_CMAKE_PATH": "/custom/cmake/path"
213+
}
214+
}
215+
]
216+
```
217+
218+
In `.vscode/settings.json`, reference the variable (do not define it here):
219+
```json
220+
{
221+
"cmake.cmakePath": "${env:MY_CMAKE_PATH}/cmake"
222+
}
223+
```
224+
225+
When this kit is selected, `cmake.cmakePath` will resolve to `/custom/cmake/path/cmake` using the kit's environment variable. Environment variables are resolved from the active kit's context, not from workspace settings.
226+
193227
`description`
194228

195229
> A short description of the kit, which will appear next to its name in the selection menu.

src/cmakeProject.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { CMakeBuildConsumer } from '@cmt/diagnostics/build';
3030
import { CMakeOutputConsumer } from '@cmt/diagnostics/cmake';
3131
import { addDiagnosticToCollection, diagnosticSeverity, populateCollection } from '@cmt/diagnostics/util';
3232
import { expandStrings, expandString, ExpansionOptions } from '@cmt/expand';
33-
import { CMakeGenerator, Kit, SpecialKits } from '@cmt/kits/kit';
33+
import { CMakeGenerator, Kit, SpecialKits, effectiveKitEnvironment } from '@cmt/kits/kit';
3434
import * as logging from '@cmt/logging';
3535
import { fs } from '@cmt/pr';
3636
import { buildCmdStr, DebuggerEnvironmentVariable, ExecutionResult, ExecutionOptions } from './proc';
@@ -843,7 +843,9 @@ export class CMakeProject {
843843
// Force re-reading of cmake exe, this will ensure that the debugger capabilities are updated.
844844
const cmakeInfo = await this.getCMakeExecutable();
845845
if (!cmakeInfo.isPresent) {
846-
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', `"${cmakeInfo.path}"`, '"cmake.cmakePath"'));
846+
// Do not show a popup here to avoid duplicate "Bad CMake executable" messages.
847+
// The canonical user-facing error is shown when a command actually needs a driver
848+
// and getCMakeDriverInstance() validates the executable.
847849
telemetry.logEvent('CMakeExecutableNotFound');
848850
}
849851

@@ -1399,21 +1401,34 @@ export class CMakeProject {
13991401
}
14001402

14011403
async setKit(kit: Kit | null) {
1404+
const priorCMakePath = await this.getCMakePathofProject(); // used for later comparison to determine if we need to update the driver's cmake.
14021405
this._activeKit = kit;
1406+
this.cachedCMakePathEnvironment = null; // Invalidate cache on kit change
1407+
this.cachedCMakePathEnvironmentKit = null;
14031408
if (kit) {
14041409
log.debug(localize('injecting.new.kit', 'Injecting new Kit into CMake driver'));
14051410
const drv = await this.cmakeDriver; // Use only an existing driver, do not create one
14061411
if (drv) {
14071412
try {
14081413
this.statusMessage.set(localize('reloading.status', 'Reloading...'));
14091414
await drv.setKit(kit, this.getPreferredGenerators());
1415+
1416+
const updatedCMakePath = await this.getCMakePathofProject();
1417+
1418+
// check if we need to update the driver's cmake, if so, update.
1419+
if (priorCMakePath !== updatedCMakePath) {
1420+
drv.cmake = await this.getCMakeExecutable();
1421+
}
1422+
14101423
await this.workspaceContext.state.setActiveKitName(this.folderName, kit.name, this.isMultiProjectFolder);
14111424
this.statusMessage.set(localize('ready.status', 'Ready'));
14121425
} catch (error: any) {
14131426
void vscode.window.showErrorMessage(localize('unable.to.set.kit', 'Unable to set kit {0}.', `"${error.message}"`));
14141427
this.statusMessage.set(localize('error.on.switch.status', 'Error on switch of kit ({0})', error.message));
14151428
this.cmakeDriver = Promise.resolve(null);
14161429
this._activeKit = null;
1430+
this.cachedCMakePathEnvironment = null; // Invalidate cache on error
1431+
this.cachedCMakePathEnvironmentKit = null;
14171432
}
14181433
} else {
14191434
// Remember the selected kit for the next session.
@@ -1422,9 +1437,36 @@ export class CMakeProject {
14221437
}
14231438
}
14241439

1440+
private async getCMakePathEnvironment(): Promise<Environment | undefined> {
1441+
if (this.useCMakePresets || !this.activeKit) {
1442+
return undefined;
1443+
}
1444+
1445+
// Return cached result if kit hasn't changed
1446+
if (this.cachedCMakePathEnvironmentKit === this.activeKit && this.cachedCMakePathEnvironment !== null) {
1447+
return this.cachedCMakePathEnvironment === undefined ? undefined : this.cachedCMakePathEnvironment;
1448+
}
1449+
1450+
try {
1451+
const expansionOptions = await this.getExpansionOptions();
1452+
const result = await effectiveKitEnvironment(this.activeKit, expansionOptions);
1453+
// Cache the result: store undefined as a marker to avoid re-computation
1454+
this.cachedCMakePathEnvironment = result || undefined;
1455+
this.cachedCMakePathEnvironmentKit = this.activeKit;
1456+
return result;
1457+
} catch (e: any) {
1458+
log.warning(localize('failed.to.compute.kit.env.for.cmake.path', 'Unable to evaluate the active kit environment while resolving cmake.cmakePath: {0}', e?.message || e));
1459+
// Cache the error result as undefined
1460+
this.cachedCMakePathEnvironment = undefined;
1461+
this.cachedCMakePathEnvironmentKit = this.activeKit;
1462+
return undefined;
1463+
}
1464+
}
1465+
14251466
async getCMakePathofProject(): Promise<string> {
14261467
const overWriteCMakePathSetting = this.useCMakePresets ? this.configurePreset?.cmakeExecutable : undefined;
1427-
return await this.workspaceContext.getCMakePath(overWriteCMakePathSetting) || '';
1468+
const envOverride = await this.getCMakePathEnvironment();
1469+
return await this.workspaceContext.getCMakePath(overWriteCMakePathSetting, envOverride) || '';
14281470
}
14291471

14301472
async getCMakeExecutable() {
@@ -1539,6 +1581,12 @@ export class CMakeProject {
15391581
return this._activeKit;
15401582
}
15411583

1584+
/**
1585+
* Cache for CMake path environment to avoid redundant shell script executions
1586+
*/
1587+
private cachedCMakePathEnvironment: Environment | undefined | null = null;
1588+
private cachedCMakePathEnvironmentKit: Kit | null = null;
1589+
15421590
/**
15431591
* The compilation database for this driver.
15441592
*/

src/paths.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { vsInstallations } from '@cmt/installs/visualStudio';
1111
import { expandString } from '@cmt/expand';
1212
import { fs } from '@cmt/pr';
1313
import * as util from '@cmt/util';
14+
import { Environment } from '@cmt/environmentVariables';
1415

1516
interface VSCMakePaths {
1617
cmake?: string;
@@ -189,16 +190,30 @@ class Paths {
189190
return this._ninjaPath;
190191
}
191192

192-
async which(name: string): Promise<string | null> {
193+
private pathFromEnvironment(envOverride?: Environment): string | undefined {
194+
if (!envOverride) {
195+
return undefined;
196+
}
197+
// EnvironmentUtils.create() normalizes key casing on Windows, but keep a fallback for plain process env records.
198+
return envOverride['PATH'] || envOverride['Path'] || undefined;
199+
}
200+
201+
async which(name: string, envOverride?: Environment): Promise<string | null> {
193202
return new Promise<string | null>(resolve => {
194-
which(name, (err, resolved) => {
203+
const pathOverride = this.pathFromEnvironment(envOverride);
204+
const cb = (err: Error | null, resolved?: string) => {
195205
if (err) {
196206
resolve(null);
197207
} else {
198208
console.assert(resolved, '`which` didn\'t do what it should have.');
199209
resolve(resolved!);
200210
}
201-
});
211+
};
212+
if (pathOverride) {
213+
which(name, { path: pathOverride }, cb);
214+
} else {
215+
which(name, cb);
216+
}
202217
});
203218
}
204219

@@ -248,17 +263,17 @@ class Paths {
248263
}
249264
}
250265

251-
async getCMakePath(wsc: DirectoryContext, overWriteCMakePathSetting?: string): Promise<string | null> {
266+
async getCMakePath(wsc: DirectoryContext, overWriteCMakePathSetting?: string, envOverride?: Environment): Promise<string | null> {
252267
this._ninjaPath = undefined;
253268

254269
let raw = overWriteCMakePathSetting;
255270
if (!raw) {
256-
raw = await this.expandStringPath(wsc.config.rawCMakePath, wsc);
271+
raw = await this.expandStringPath(wsc.config.rawCMakePath, wsc, envOverride);
257272
}
258273

259274
if (raw === 'auto' || raw === 'cmake') {
260275
// We start by searching $PATH for cmake
261-
const on_path = await this.which('cmake');
276+
const on_path = await this.which('cmake', envOverride);
262277
if (on_path) {
263278
return on_path;
264279
}
@@ -289,7 +304,7 @@ class Paths {
289304
return raw;
290305
}
291306

292-
async expandStringPath(raw_path: string, wsc: DirectoryContext): Promise<string> {
307+
async expandStringPath(raw_path: string, wsc: DirectoryContext, envOverride?: Environment): Promise<string> {
293308
return expandString(raw_path, {
294309
vars: {
295310
buildKit: '${buildKit}',
@@ -310,7 +325,8 @@ class Paths {
310325
workspaceHash: util.makeHashString(wsc.folder.uri.fsPath),
311326
workspaceRoot: wsc.folder.uri.fsPath,
312327
workspaceRootFolderName: path.basename(wsc.folder.uri.fsPath)
313-
}
328+
},
329+
envOverride
314330
});
315331
}
316332

src/workspace.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import * as vscode from 'vscode';
66

77
import { ConfigurationReader } from '@cmt/config';
8+
import { Environment } from '@cmt/environmentVariables';
89
import paths from '@cmt/paths';
910
import { StateManager } from '@cmt/state';
1011

@@ -43,8 +44,8 @@ export class DirectoryContext {
4344
* be used over `ConfigurationReader.cmakePath` because it will do additional
4445
* path expansion and searching.
4546
*/
46-
getCMakePath(overWriteCMakePathSetting?: string): Promise<string | null> {
47-
return paths.getCMakePath(this, overWriteCMakePathSetting);
47+
getCMakePath(overWriteCMakePathSetting?: string, envOverride?: Environment): Promise<string | null> {
48+
return paths.getCMakePath(this, overWriteCMakePathSetting, envOverride);
4849
}
4950
/**
5051
* The CTest executable for the directory. See `cmakePath` for more

0 commit comments

Comments
 (0)