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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Features:
- Add support for `${workspaceFolder}`, `${workspaceFolder:name}` variables and relative paths in `cmake.exclude` setting for multi-root workspaces. [#4689](https://github.com/microsoft/vscode-cmake-tools/pull/4689)
- Add `onConfigureResult` event to the CMake Tools API that fires after every configure attempt (success or failure), allowing dependent extensions to detect and react to configure failures. [#4021](https://github.com/microsoft/vscode-cmake-tools/issues/4021)
- Add `cmake.preConfigureTask` setting to execute a named VS Code task before every CMake configure. [#2449](https://github.com/microsoft/vscode-cmake-tools/issues/2449) [#4960](https://github.com/microsoft/vscode-cmake-tools/pull/4960) [@erdemiru](https://github.com/erdemiru)
- Add `cmake.environmentSetupScript` setting to source (Linux/macOS) or call (Windows) a script and use its environment as the parent environment when using CMake Presets, so preset macro/`$penv{}` expansion and configure/build/test can use environments that cannot be expressed as static variables. Cross-platform counterpart to the Visual Studio Developer Environment. [#PR](https://github.com/microsoft/vscode-cmake-tools/pull/PR) [@vasdal](https://github.com/vasdal)

Improvements:
- Add `${testName}` variable support for `cmake.ctestArgs` and `cmake.ctestDefaultArgs`, enabling per-test argument expansion (e.g., unique log file paths per test). [#4416](https://github.com/microsoft/vscode-cmake-tools/issues/4416)
Expand Down
6 changes: 6 additions & 0 deletions docs/cmake-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ Environment variables set in a Configure Preset also automatically flow to assoc

You can reference environment variables by using the `$env{<variable-name>}` and `$penv{<variable-name>}` syntax. For more information, see [Macro Expansion](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html#macro-expansion).

### Bootstrap the parent environment from a setup script

Some environments cannot be expressed as static `environment` entries — for example when the required variables are large, interdependent, or produced by a tool's own setup script. For these cases, set `cmake.environmentSetupScript` to a script that CMake Tools sources (Linux/macOS) or calls (Windows) before working with presets. The resulting environment becomes the parent environment, so it is available both when expanding preset macros (including `$penv{<variable-name>}` in `include` paths and toolchain files) and when running configure, build, and test.

This is the cross-platform counterpart to the Visual Studio Developer Environment, which CMake Tools sources automatically for Visual C++ toolsets (see `cmake.useVsDeveloperEnvironment`). The setting value supports variable substitution such as `${workspaceFolder}`.

### Select your target and host architecture when building with the Visual C++ toolset

The target architecture (x64, Win32, ARM64, or ARM) can be set with `architecture.value`. This is equivalent to passing `-A` to CMake from the command line. For more information, see [Platform Selection](https://cmake.org/cmake/help/latest/generator/Visual%20Studio%2016%202019.html#platform-selection).
Expand Down
1 change: 1 addition & 0 deletions docs/cmake-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Options that support substitution, in the table below, allow variable references
| `cmake.languageServerOnlyMode` | If `true`, keep CMake language services enabled while disabling CMake project, build, test, and kit integration. | `false` | no |
| `cmake.enableTraceLogging` | If `true`, enable trace logging. | `false` | no |
| `cmake.environment` | An object containing `key:value` pairs of environment variables, which will be available when configuring, building, or testing with CTest. | `{}` (no environment variables) | yes |
| `cmake.environmentSetupScript` | When using CMake Presets, source (Linux/macOS) or call (Windows) this script and use the resulting environment as the parent environment for preset expansion and for configure, build, and test. Use this to bootstrap environments that cannot be captured as static variables. | `""` (no script) | yes |
| `cmake.exclude` | CMake Tools will ignore the folders defined in this setting. | `[]` | yes |
| `cmake.exportCompileCommandsFile` | If `true`, generate the compile_commands.json file. | `true` | no |
| `cmake.generator` | Set to a string to override CMake Tools preferred generator logic. If set, CMake will unconditionally use it as the `-G` CMake generator command line argument. | `null` | no |
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4112,6 +4112,12 @@
"description": "%cmake-tools.configuration.cmake.useVsDeveloperEnvironment.description%",
"scope": "resource"
},
"cmake.environmentSetupScript": {
"type": "string",
"default": "",
"description": "%cmake-tools.configuration.cmake.environmentSetupScript.description%",
"scope": "resource"
},
"cmake.allowCommentsInPresetsFile": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@
"cmake-tools.configuration.cmake.exportCompileCommandsFile.description": "Enables exporting compile_commands.json. This only is used in Kits scenarios. In Presets scenarios, please set this by using CMakePresets.json",
"cmake-tools.configuration.cmake.useCMakePresets.description": "Use CMakePresets.json to configure drive CMake configure, build, and test. When using CMakePresets.json, kits, variants, and some settings in settings.json will be ignored.",
"cmake-tools.configuration.cmake.useVsDeveloperEnvironment.description": "When using CMake Presets on Windows, use the Visual Studio environment as the parent environment. Selecting auto will only apply the Visual Studio environment when we detect a supported compiler (cl, clang, clang-cl, clang-cpp, clang++), or the Ninja generator is being used.",
"cmake-tools.configuration.cmake.environmentSetupScript.description": "When using CMake Presets, source (Linux/macOS) or call (Windows) this script and use the resulting environment as the parent environment for preset expansion and for configure, build, and test. Use this to bootstrap environments that cannot be captured as static variables. Supports variable substitution (e.g. ${workspaceFolder}).",
"cmake-tools.configuration.cmake.allowCommentsInPresetsFile.description": "Allow the use of JSON extensions such as comments in CMakePresets.json. Please note that your CMakePresets.json file may be considered invalid by other IDEs or on the command line if you use non-standard JSON.",
"cmake-tools.configuration.cmake.allowUnsupportedPresetsVersions.description": "Enables the use of presets files that are using features from the versions that CMake Tools extension doesn't currently support. Unknown properties and macros will be ignored.",
"cmake-tools.configuration.cmake.ignoreCMakeListsMissing.description": {
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export interface ExtensionConfigurationSettings {
options: OptionConfig;
useCMakePresets: UseCMakePresets;
useVsDeveloperEnvironment: UseVsDeveloperEnvironment;
environmentSetupScript: string;
allowCommentsInPresetsFile: boolean;
allowUnsupportedPresetsVersions: boolean;
launchBehavior: string;
Expand Down Expand Up @@ -529,6 +530,9 @@ export class ConfigurationReader implements vscode.Disposable {
get useVsDeveloperEnvironment(): UseVsDeveloperEnvironment {
return this.configData.useVsDeveloperEnvironment;
}
get environmentSetupScript(): string {
return this.configData.environmentSetupScript;
}
get allowCommentsInPresetsFile(): boolean {
return this.configData.allowCommentsInPresetsFile;
}
Expand Down Expand Up @@ -768,6 +772,7 @@ export class ConfigurationReader implements vscode.Disposable {
options: new vscode.EventEmitter<OptionConfig>(),
useCMakePresets: new vscode.EventEmitter<UseCMakePresets>(),
useVsDeveloperEnvironment: new vscode.EventEmitter<UseVsDeveloperEnvironment>(),
environmentSetupScript: new vscode.EventEmitter<string>(),
allowCommentsInPresetsFile: new vscode.EventEmitter<boolean>(),
allowUnsupportedPresetsVersions: new vscode.EventEmitter<boolean>(),
ignoreCMakeListsMissing: new vscode.EventEmitter<boolean>(),
Expand Down
18 changes: 14 additions & 4 deletions src/kits/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,16 @@ export const MSVC_HOST_ARCHES: MsvcHostArches[] = ['x86', 'x64'];
*/
export async function getShellScriptEnvironment(kit: Kit, opts?: expand.ExpansionOptions): Promise<Environment | undefined> {
console.assert(kit.environmentSetupScript);
return getEnvironmentFromSetupScript(kit.environmentSetupScript!, opts);
}

/**
* Gets the environment variables produced by sourcing (Linux/macOS) or calling (Windows) a setup script.
* Shared by kits (`environmentSetupScript`) and, in preset mode, by the `cmake.environmentSetupScript` setting.
* @param setupScript Path to the setup script (optionally followed by args, in which case it must be quoted).
* @param opts Expansion options applied to the script string before it is run.
*/
export async function getEnvironmentFromSetupScript(setupScript: string, opts?: expand.ExpansionOptions): Promise<Environment | undefined> {
const filename = Math.random().toString() + (process.platform === 'win32' ? '.bat' : '.sh');
const script_filename = `vs-cmt-${filename}`;
const environment_filename = script_filename + '.env';
Expand All @@ -752,7 +762,7 @@ export async function getShellScriptEnvironment(kit: Kit, opts?: expand.Expansio
let script = '';
let run_command = '';

let environmentSetupScript = kit.environmentSetupScript!.trim();
let environmentSetupScript = setupScript.trim();
if (opts) {
environmentSetupScript = await expand.expandString(environmentSetupScript, opts);
}
Expand Down Expand Up @@ -786,7 +796,7 @@ export async function getShellScriptEnvironment(kit: Kit, opts?: expand.Expansio
const output = (res.stdout) ? res.stdout + (res.stderr || '') : res.stderr;

if (res.retc !== 0) {
log.error(localize('error.running.setup.script', 'Error running {0} with: {1}', kit.environmentSetupScript, output));
log.error(localize('error.running.setup.script', 'Error running {0} with: {1}', setupScript, output));
return;
}

Expand All @@ -799,7 +809,7 @@ export async function getShellScriptEnvironment(kit: Kit, opts?: expand.Expansio
log.error(error as Error);
}
if (!env || env === '') {
console.log(`Error running ${kit.environmentSetupScript} with:`, output);
console.log(`Error running ${setupScript} with:`, output);
return;
}

Expand All @@ -814,7 +824,7 @@ export async function getShellScriptEnvironment(kit: Kit, opts?: expand.Expansio
}
return acc;
}, EnvironmentUtils.create());
log.debug(localize('ok.running', 'OK running {0}, env vars: {1}', kit.environmentSetupScript, JSON.stringify(vars)));
log.debug(localize('ok.running', 'OK running {0}, env vars: {1}', setupScript, JSON.stringify(vars)));
return vars;
}

Expand Down
76 changes: 66 additions & 10 deletions src/presets/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { execute } from '@cmt/proc';
import { errorHandlerHelper, expandString, ExpansionErrorHandler, ExpansionOptions } from '@cmt/expand';
import paths from '@cmt/paths';
import { compareVersions, VSInstallation, vsInstallations, enumerateMsvcToolsets, varsForVSInstallation, getVcVarsBatScript } from '@cmt/installs/visualStudio';
import { EnvironmentUtils, EnvironmentWithNull } from '@cmt/environmentVariables';
import { Environment, EnvironmentUtils, EnvironmentWithNull } from '@cmt/environmentVariables';
import { UseVsDeveloperEnvironment } from '@cmt/config';
import { getEnvironmentFromSetupScript } from '@cmt/kits/kit';
import { fs } from '@cmt/pr';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
Expand Down Expand Up @@ -681,8 +683,8 @@ async function getVendorForConfigurePresetHelper(folder: string, preset: Configu
return preset.vendor || null;
}

async function getExpansionOptions(workspaceFolder: string, sourceDir: string, preset: ConfigurePreset | BuildPreset | TestPreset | PackagePreset, envOverride?: EnvironmentWithNull, penvOverride?: EnvironmentWithNull, includeGenerator: boolean = true) {
const generator = includeGenerator ? 'generator' in preset
async function getExpansionOptions(workspaceFolder: string, sourceDir: string, preset?: ConfigurePreset | BuildPreset | TestPreset | PackagePreset, envOverride?: EnvironmentWithNull, penvOverride?: EnvironmentWithNull, includeGenerator: boolean = true) {
const generator = includeGenerator && preset ? 'generator' in preset
? preset.generator
: ('__generator' in preset ? preset.__generator : undefined) : undefined;

Expand All @@ -698,9 +700,9 @@ async function getExpansionOptions(workspaceFolder: string, sourceDir: string, p
sourceDir,
sourceParentDir: path.dirname(sourceDir),
sourceDirName: path.basename(sourceDir),
presetName: preset.name
presetName: preset?.name ?? ''
},
envOverride: envOverride ?? preset.environment,
envOverride: envOverride ?? preset?.environment,
penvOverride: penvOverride,
recursive: true,
// Don't support commands since expansion might be called on activation. If there is
Expand All @@ -709,13 +711,13 @@ async function getExpansionOptions(workspaceFolder: string, sourceDir: string, p
doNotSupportCommands: true
};

if (preset.__file && preset.__file.version >= 3) {
if (preset?.__file && preset.__file.version >= 3) {
expansionOpts.vars.hostSystemName = await util.getHostSystemNameMemo();
}
if (preset.__file && preset.__file.version >= 4) {
if (preset?.__file && preset.__file.version >= 4) {
expansionOpts.vars.fileDir = path.dirname(preset.__file!.__path!);
}
if (preset.__file && preset.__file.version >= 5) {
if (preset?.__file && preset.__file.version >= 5) {
expansionOpts.vars.pathListSep = path.delimiter;
}

Expand Down Expand Up @@ -990,10 +992,64 @@ export function getVsDevEnvAutoDetectionInfo(preset: ConfigurePreset): VsDevEnvA
* @param sourceDir The source dir of the CMake project.
* @returns Void. We don't return as we are modifying the preset by reference.
*/
// Cache of environments produced by `cmake.environmentSetupScript`, keyed by the
// expanded script string plus its file mtime, so the script is not re-sourced on
// every configure/build/test. A changed setting yields a different key; a rebuilt
// setup file changes the mtime and thereby invalidates the entry.
const setupScriptEnvCache = new Map<string, Environment>();

/**
* Sources (Linux/macOS) or calls (Windows) the `cmake.environmentSetupScript`, if configured,
* and returns the resulting environment. Cross-platform counterpart to the VS Dev Env: it lets
* users bootstrap a parent environment that cannot be expressed as static variables.
*
* This is used in two places so a single script bootstraps everything (they share the cache):
* - preset include/`$penv{}` resolution (via the presets controller's settings environment), and
* - per-preset expansion + the configure/build/test spawn environment (via `tryApplyVsDevEnv`).
*
* @returns The environment produced by the script, or undefined if the setting is empty or the script fails.
*/
export async function getEnvironmentSetupScriptEnv(workspaceFolder: string, sourceDir: string, preset?: ConfigurePreset): Promise<Environment | undefined> {
const setupScript = vscode.workspace.getConfiguration("cmake", vscode.Uri.file(workspaceFolder)).get<string>("environmentSetupScript");
if (!setupScript) {
return undefined;
}

// `preset` is passed when expanding for a specific preset (so ${presetName}/generator resolve),
// and omitted for the file-level include resolution, where no preset has been selected yet.
const opts = await getExpansionOptions(workspaceFolder, sourceDir, preset);
const expandedScript = await expandString(setupScript, opts);

// Best-effort mtime so a rebuilt setup file invalidates the cache. If the value is not a bare
// path (e.g. it carries arguments or surrounding quotes) we simply cache by the string alone.
let cacheKey = expandedScript;
try {
const stat = await fs.stat(expandedScript);
cacheKey = `${expandedScript}\0${stat.mtimeMs}`;
} catch {
// Not a plain, existing file path; fall back to string-only key.
}

const cached = setupScriptEnvCache.get(cacheKey);
if (cached) {
return cached;
}

const env = await getEnvironmentFromSetupScript(expandedScript, opts);
if (env) {
setupScriptEnvCache.set(cacheKey, env);
}
return env;
}

export async function tryApplyVsDevEnv(preset: ConfigurePreset, workspaceFolder: string, sourceDir: string): Promise<void> {
// The environment-setup-script environment is applied regardless of the VS Dev Env mode (including
// "never"), since it is the cross-platform mechanism for bootstrapping a parent environment.
const setupScriptEnvironment = await getEnvironmentSetupScriptEnv(workspaceFolder, sourceDir, preset);

const useVsDeveloperEnvironmentMode = vscode.workspace.getConfiguration("cmake", vscode.Uri.file(workspaceFolder)).get("useVsDeveloperEnvironment") as UseVsDeveloperEnvironment;
if (useVsDeveloperEnvironmentMode === "never") {
preset.__parentEnvironment = process.env;
preset.__parentEnvironment = EnvironmentUtils.mergePreserveNull([process.env, setupScriptEnvironment]);
return;
}

Expand Down Expand Up @@ -1075,7 +1131,7 @@ export async function tryApplyVsDevEnv(preset: ConfigurePreset, workspaceFolder:
preset.__developerEnvironmentArchitecture = getArchitecture(preset);
}

preset.__parentEnvironment = EnvironmentUtils.mergePreserveNull([process.env, preset.__parentEnvironment, developerEnvironment]);
preset.__parentEnvironment = EnvironmentUtils.mergePreserveNull([process.env, preset.__parentEnvironment, developerEnvironment, setupScriptEnvironment]);
}

/**
Expand Down
Loading