diff --git a/CHANGELOG.md b/CHANGELOG.md index f8996415b..14bd31689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # What's New? +## 1.20 + +Features: + +- Add support for Presets v9, which enables more macro expansion for the `include` field. [#3946](https://github.com/microsoft/vscode-cmake-tools/issues/3946) + ## 1.19.52 Improvements: diff --git a/src/expand.ts b/src/expand.ts index b3bc6fc10..d731098d0 100644 --- a/src/expand.ts +++ b/src/expand.ts @@ -48,13 +48,19 @@ export interface KitContextVars extends RequiredExpansionContextVars { buildKitVersionMinor: string; } -export interface PresetContextVars extends RequiredExpansionContextVars { +export interface PresetContextVars extends PresetContextNotPresetSpecificVars, RequiredExpansionContextVars { + [key: string]: string; + presetName: string; +} + +export interface PresetContextNotPresetSpecificVars { [key: string]: string; sourceDir: string; sourceParentDir: string; sourceDirName: string; - presetName: string; fileDir: string; + hostSystemName: string; + pathListSep: string; } export interface MinimalPresetContextVars extends RequiredExpansionContextVars { @@ -68,7 +74,7 @@ export interface ExpansionOptions { /** * Plain `${variable}` style expansions. */ - vars: KitContextVars | PresetContextVars | MinimalPresetContextVars; + vars: KitContextVars | PresetContextVars | MinimalPresetContextVars | PresetContextNotPresetSpecificVars; /** * Override the values used in `${env:var}`-style and `${env.var}`-style expansions. * diff --git a/src/preset.ts b/src/preset.ts index f21c86f7a..bfa0abac6 100644 --- a/src/preset.ts +++ b/src/preset.ts @@ -823,13 +823,13 @@ async function getExpansionOptions(workspaceFolder: string, sourceDir: string, p }; if (preset.__file && preset.__file.version >= 3) { - expansionOpts.vars['hostSystemName'] = await util.getHostSystemNameMemo(); + expansionOpts.vars.hostSystemName = await util.getHostSystemNameMemo(); } if (preset.__file && preset.__file.version >= 4) { - expansionOpts.vars['fileDir'] = path.dirname(preset.__file!.__path!); + expansionOpts.vars.fileDir = path.dirname(preset.__file!.__path!); } if (preset.__file && preset.__file.version >= 5) { - expansionOpts.vars['pathListSep'] = path.delimiter; + expansionOpts.vars.pathListSep = path.delimiter; } return expansionOpts; diff --git a/src/presetsController.ts b/src/presetsController.ts index c35c6adca..fc892da55 100644 --- a/src/presetsController.ts +++ b/src/presetsController.ts @@ -10,7 +10,7 @@ import { fs } from '@cmt/pr'; import * as preset from '@cmt/preset'; import * as util from '@cmt/util'; import rollbar from '@cmt/rollbar'; -import { ExpansionErrorHandler, ExpansionOptions, getParentEnvSubstitutions, substituteAll } from '@cmt/expand'; +import { expandString, ExpansionErrorHandler, ExpansionOptions, MinimalPresetContextVars } from '@cmt/expand'; import paths from '@cmt/paths'; import { KitsController } from '@cmt/kitsController'; import { descriptionForKit, Kit, SpecialKits } from '@cmt/kit'; @@ -187,18 +187,24 @@ export class PresetsController implements vscode.Disposable { setOriginalPresetsFile(this.folderPath, undefined); } + const expansionErrors: ExpansionErrorHandler = { errorList: [], tempErrorList: []}; + presetsFile = await this.validatePresetsFile(presetsFile, file); if (presetsFile) { // Private fields must be set after validation, otherwise validation would fail. this.populatePrivatePresetsFields(presetsFile, file); - await this.mergeIncludeFiles(presetsFile, file, referencedFiles); + await this.mergeIncludeFiles(presetsFile, file, referencedFiles, expansionErrors); - // add the include files to the original presets file - setPresetsPlusIncluded(this.folderPath, presetsFile); + if (expansionErrors.errorList.length > 0 || expansionErrors.tempErrorList.length > 0) { + presetsFile = undefined; + } else { + // add the include files to the original presets file + setPresetsPlusIncluded(this.folderPath, presetsFile); - // set the pre-expanded version so we can call expandPresetsFile on it - setExpandedPresets(this.folderPath, presetsFile); - presetsFile = await this.expandPresetsFile(presetsFile); + // set the pre-expanded version so we can call expandPresetsFile on it + setExpandedPresets(this.folderPath, presetsFile); + presetsFile = await this.expandPresetsFile(presetsFile, expansionErrors); + } } setExpandedPresets(this.folderPath, presetsFile); @@ -1605,22 +1611,47 @@ export class PresetsController implements vscode.Disposable { setFile(presetsFile.packagePresets); } - private async mergeIncludeFiles(presetsFile: preset.PresetsFile | undefined, file: string, referencedFiles: Map): Promise { + private async getExpandedInclude(presetsFile: preset.PresetsFile, include: string, file: string, hostSystemName: string, expansionErrors: ExpansionErrorHandler): Promise { + return presetsFile.version >= 9 + ? expandString(include, { + vars: { + sourceDir: this.folderPath, + sourceParentDir: path.dirname(this.folderPath), + sourceDirName: path.basename(this.folderPath), + hostSystemName: hostSystemName, + fileDir: path.dirname(file), + pathListSep: path.delimiter + }, + envOverride: {} // $env{} expansions are not supported in `include` v9 + }, expansionErrors) + : presetsFile.version >= 7 + ? // Version 7 and later support $penv{} expansions in include paths + expandString(include, { + // No vars are supported in Version 7 for include paths. + vars: {} as MinimalPresetContextVars, + envOverride: {} // $env{} expansions are not supported in `include` v9 + }, expansionErrors) + : include; + } + + private async mergeIncludeFiles(presetsFile: preset.PresetsFile | undefined, file: string, referencedFiles: Map, expansionErrors: ExpansionErrorHandler): Promise { if (!presetsFile) { return; } + const hostSystemName = await util.getHostSystemNameMemo(); + // CMakeUserPresets.json file should include CMakePresets.json file, by default. if (this.presetsFileExist && file === this.userPresetsPath) { presetsFile.include = presetsFile.include || []; - const filteredIncludes = presetsFile.include.filter(include => { - // Ensuring that we handle expansions. Duplicated from loop below. - const includePath = presetsFile.version >= 7 ? - // Version 7 and later support $penv{} expansions in include paths - substituteAll(include, getParentEnvSubstitutions(include, new Map())).result : - include; - path.normalize(path.resolve(path.dirname(file), includePath)) === this.presetsPath; - }); + const filteredIncludes = []; + for (const include of presetsFile.include) { + const expandedInclude = await this.getExpandedInclude(presetsFile, include, file, hostSystemName, expansionErrors); + if (path.normalize(path.resolve(path.dirname(file), expandedInclude)) === this.presetsPath) { + filteredIncludes.push(include); + } + } + if (filteredIncludes.length === 0) { presetsFile.include.push(this.presetsPath); } @@ -1633,10 +1664,7 @@ export class PresetsController implements vscode.Disposable { // Merge the includes in reverse order so that the final presets order is correct for (let i = presetsFile.include.length - 1; i >= 0; i--) { const rawInclude = presetsFile.include[i]; - const includePath = presetsFile.version >= 7 ? - // Version 7 and later support $penv{} expansions in include paths - substituteAll(rawInclude, getParentEnvSubstitutions(rawInclude, new Map())).result : - rawInclude; + const includePath = await this.getExpandedInclude(presetsFile, rawInclude, file, hostSystemName, expansionErrors); const fullIncludePath = path.normalize(path.resolve(path.dirname(file), includePath)); // Do not include files more than once @@ -1672,6 +1700,7 @@ export class PresetsController implements vscode.Disposable { const includeFileBuffer = await this.readPresetsFile(fullIncludePath); if (!includeFileBuffer) { log.error(localize('included.presets.file.not.found', 'Included presets file {0} cannot be found', fullIncludePath)); + expansionErrors.errorList.push([localize('included.presets.file.not.found', 'Included presets file {0} cannot be found', fullIncludePath), file]); continue; } @@ -1686,7 +1715,7 @@ export class PresetsController implements vscode.Disposable { this.populatePrivatePresetsFields(includeFile, fullIncludePath); // Recursively merge included files - await this.mergeIncludeFiles(includeFile, fullIncludePath, referencedFiles); + await this.mergeIncludeFiles(includeFile, fullIncludePath, referencedFiles, expansionErrors); if (includeFile.configurePresets) { presetsFile.configurePresets = lodash.unionWith(includeFile.configurePresets, presetsFile.configurePresets || [], (a, b) => a.name === b.name); @@ -1709,13 +1738,21 @@ export class PresetsController implements vscode.Disposable { } } } + + if (expansionErrors.errorList.length > 0 || expansionErrors.tempErrorList.length > 0) { + expansionErrors.tempErrorList.forEach((error) => expansionErrors.errorList.unshift(error)); + log.error(localize('expansion.errors', 'Expansion errors found in the presets file.')); + await this.reportPresetsFileErrors(presetsFile.__path, expansionErrors); + } else { + collections.presets.set(vscode.Uri.file(presetsFile.__path || ""), undefined); + } } /** * Returns the expanded presets file if there are no errors, otherwise returns undefined * Does not apply vsdevenv to the presets file */ - private async expandPresetsFile(presetsFile: preset.PresetsFile | undefined): Promise { + private async expandPresetsFile(presetsFile: preset.PresetsFile | undefined, expansionErrors: ExpansionErrorHandler): Promise { if (!presetsFile) { return undefined; @@ -1723,8 +1760,6 @@ export class PresetsController implements vscode.Disposable { log.info(localize('expanding.presets.file', 'Expanding presets file {0}', presetsFile?.__path || '')); - const expansionErrors: ExpansionErrorHandler = { errorList: [], tempErrorList: []}; - const expandedConfigurePresets: preset.ConfigurePreset[] = []; for (const configurePreset of presetsFile?.configurePresets || []) { const inheritedPreset = await preset.getConfigurePresetInherits( @@ -1884,7 +1919,7 @@ export class PresetsController implements vscode.Disposable { log.info(localize('validating.presets.file', 'Reading and validating the presets "file {0}"', file)); let schemaFile; - const maxSupportedVersion = 8; + const maxSupportedVersion = 9; const validationErrorsAreWarnings = presetsFile.version > maxSupportedVersion && this.project.workspaceContext.config.allowUnsupportedPresetsVersions; if (presetsFile.version < 2) { await this.showPresetsFileVersionError(file); @@ -1902,7 +1937,8 @@ export class PresetsController implements vscode.Disposable { } else if (presetsFile.version === 7) { schemaFile = './schemas/CMakePresets-v7-schema.json'; } else { - schemaFile = './schemas/CMakePresets-v8-schema.json'; + // This can be used for v9 as well, there is no schema difference. + schemaFile = "./schemas/CMakePresets-v8-schema.json"; } const validator = await loadSchema(schemaFile);