diff --git a/ce/ce/amf/exports.ts b/ce/ce/amf/exports.ts index ab0b212048..73ea235ca0 100644 --- a/ce/ce/amf/exports.ts +++ b/ce/ce/amf/exports.ts @@ -9,19 +9,27 @@ import { ScalarMap } from '../yaml/ScalarMap'; import { StringsMap } from '../yaml/strings'; export class Exports extends BaseMap implements IExports { - paths: StringsMap = new StringsMap(undefined, this, 'paths'); + aliases: ScalarMap = new ScalarMap(undefined, this, 'aliases'); + defines: ScalarMap = new ScalarMap(undefined, this, 'defines'); + environment: StringsMap = new StringsMap(undefined, this, 'environment'); locations: ScalarMap = new ScalarMap(undefined, this, 'locations'); + msbuild_properties: ScalarMap = new ScalarMap(undefined, this, 'msbuild-properties'); + paths: StringsMap = new StringsMap(undefined, this, 'paths'); properties: StringsMap = new StringsMap(undefined, this, 'properties'); - environment: StringsMap = new StringsMap(undefined, this, 'environment'); tools: ScalarMap = new ScalarMap(undefined, this, 'tools'); - defines: ScalarMap = new ScalarMap(undefined, this, 'defines'); - - aliases: ScalarMap = new ScalarMap(undefined, this, 'aliases'); /** @internal */ override *validate(): Iterable { yield* super.validate(); - yield* this.validateChildKeys(['paths', 'locations', 'properties', 'environment', 'tools', 'defines', 'aliases']); - // todo: what validations do we need? + yield* this.validateChildKeys([ + 'aliases', + 'defines', + 'environment', + 'locations', + 'msbuild-properties', + 'paths', + 'properties', + 'tools' + ]); } } diff --git a/ce/ce/artifacts/activation.ts b/ce/ce/artifacts/activation.ts index 6d8b91fc56..b05be0a04d 100644 --- a/ce/ce/artifacts/activation.ts +++ b/ce/ce/artifacts/activation.ts @@ -11,11 +11,21 @@ import { i } from '../i18n'; import { Exports } from '../interfaces/metadata/exports'; import { Session } from '../session'; import { isIterable } from '../util/checks'; +import { replaceCurlyBraces } from '../util/curly-replacements'; import { linq, Record } from '../util/linq'; import { Queue } from '../util/promise'; import { Uri } from '../util/uri'; -import { toXml } from '../util/xml'; import { Artifact } from './artifact'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const XMLWriterImpl = require('xml-writer'); + +export interface XmlWriter { + startDocument(version: string | undefined, encoding: string | undefined): XmlWriter; + writeElement(name: string, content: string): XmlWriter; + writeAttribute(name: string, value: string): XmlWriter; + startElement(name: string): XmlWriter; + endElement(): XmlWriter; +} function findCaseInsensitiveOnWindows(map: Map, key: string): V | undefined { return process.platform === 'win32' ? linq.find(map, key) : map.get(key); @@ -28,6 +38,7 @@ export class Activation { #aliases = new Map(); #environment = new Map>(); #properties = new Map>(); + #msbuild_properties = new Array>(); // Relative to the artifact install #locations = new Map(); @@ -103,6 +114,11 @@ export class Activation { } this.addAlias(name, alias); } + + // **** msbuild-properties **** + for (const [name, propertyValue] of exports.msbuild_properties) { + this.addMSBuildProperty(name, propertyValue, targetFolder); + } } @@ -121,10 +137,6 @@ export class Activation { return linq.entries(this.#defines).selectAsync(async ([key, value]) => >[key, await this.resolveAndVerify(value)]); } - get definesCount() { - return this.#defines.size; - } - async getDefine(name: string): Promise { const v = this.#defines.get(name); return v ? await this.resolveAndVerify(v) : undefined; @@ -152,10 +164,6 @@ export class Activation { return undefined; } - get toolCount() { - return this.#tools.size; - } - /** Aliases are tools that get exposed to the user as shell aliases */ addAlias(name: string, value: string) { const a = findCaseInsensitiveOnWindows(this.#aliases, name); @@ -181,7 +189,7 @@ export class Activation { return this.#aliases.size; } - /** a collection of 'published locations' from artifacts. useful for msbuild */ + /** a collection of 'published locations' from artifacts */ addLocation(name: string, location: string | Uri) { if (!name || !location) { return; @@ -203,9 +211,6 @@ export class Activation { const l = this.#locations.get(name); return l ? this.resolveAndVerify(l) : undefined; } - get locationCount() { - return this.#locations.size; - } /** a collection of environment variables from artifacts that are intended to be combinined into variables that have PATH delimiters */ addPath(name: string, location: string | Iterable | Uri | Iterable) { @@ -233,10 +238,6 @@ export class Activation { return linq.entries(this.#paths).selectAsync(async ([key, value]) => >>[key, await this.resolveAndVerify(value)]); } - get pathCount() { - return this.#paths.size; - } - async getPath(name: string) { const set = this.#paths.get(name); if (!set) { @@ -270,11 +271,7 @@ export class Activation { return linq.entries(this.#environment).selectAsync(async ([key, value]) => >>[key, await this.resolveAndVerify(value)]); } - get environmentVariableCount() { - return this.#environment.size; - } - - /** a collection of arbitrary properties from artifacts. useful for msbuild */ + /** a collection of arbitrary properties from artifacts */ addProperty(name: string, value: string | Iterable) { if (!name) { return; @@ -303,9 +300,20 @@ export class Activation { return v ? await this.resolveAndVerify(v) : undefined; } - get propertyCount() { - return this.#properties.size; + msBuildProcessPropertyValue(value: string, targetFolder: Uri) { + // note that this is intended to be consistent with vcpkg's handling: + // include/vcpkg/base/api_stable_format.h + const initialLocal = targetFolder.fsPath; + const endsWithSlash = initialLocal.endsWith('\\') || initialLocal.endsWith('/'); + const root = endsWithSlash ? initialLocal.substring(0, initialLocal.length - 1) : initialLocal; + const replacements = new Map([['root', root]]); + return replaceCurlyBraces(value, replacements); } + + addMSBuildProperty(name: string, value: string, targetFolder: Uri) { + this.#msbuild_properties.push([name, this.msBuildProcessPropertyValue(value, targetFolder)]); + } + async resolveAndVerify(value: string, locals?: Array, refcheck?: Set): Promise async resolveAndVerify(value: Set, locals?: Array, refcheck?: Set): Promise> async resolveAndVerify(value: string | Set, locals: Array = [], refcheck = new Set()): Promise> { @@ -335,7 +343,7 @@ export class Activation { text = text.value; // spews a --debug warning if a scalar makes its way thru for some reason } - // short-ciruiting + // short-circuiting if (!text || text.indexOf('$') === -1) { return text; } @@ -474,101 +482,22 @@ export class Activation { return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`); } - async generateMSBuild(artifacts: Iterable): Promise { - const msbuildFile = { - Project: { - $xmlns: 'http://schemas.microsoft.com/developer/msbuild/2003', - PropertyGroup: >>[] - } - }; - - if (this.locationCount) { - const locations = >{ - $Label: 'Locations' - }; - for await (const [name, location] of this.locations) { - locations[name] = location; - } - msbuildFile.Project.PropertyGroup.push(locations); - } - - if (this.propertyCount) { - const properties = >{ - $Label: 'Properties' - }; - - for await (const [name, propertyValues] of this.properties) { - properties[name] = linq.join(propertyValues, ';'); - } - msbuildFile.Project.PropertyGroup.push(properties); - } - - if (this.toolCount) { - const tools = >{ - $Label: 'Tools' - }; - - for await (const [name, tool] of this.tools) { - tools[name] = tool; - } - msbuildFile.Project.PropertyGroup.push(tools); - } - - if (this.environmentVariableCount) { - const environment = >{ - $Label: 'Environment' - }; - - for await (const [name, envValues] of this.environmentVariables) { - environment[name] = linq.join(envValues, ';'); + generateMSBuild(): string { + const result : XmlWriter = new XMLWriterImpl(' '); + result.startDocument('1.0', 'utf-8'); + result.startElement('Project'); + result.writeAttribute('xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003'); + if (this.#msbuild_properties.length) { + result.startElement('PropertyGroup'); + for (const [key, value] of this.#msbuild_properties) { + result.writeElement(key, value); } - msbuildFile.Project.PropertyGroup.push(environment); - } - - if (this.pathCount) { - const paths = >{ - $Label: 'Paths' - }; - for await (const [name, pathValues] of this.paths) { - paths[name] = linq.join(pathValues, ';'); - } - msbuildFile.Project.PropertyGroup.push(paths); + result.endElement(); // PropertyGroup } - if (this.definesCount) { - const defines = >{ - $Label: 'Defines' - }; - - for await (const [name, define] of this.defines) { - defines[name] = linq.join(define, ';'); - } - msbuildFile.Project.PropertyGroup.push(defines); - } - - if (this.aliasCount) { - const aliases = >{ - $Label: 'Aliases' - }; - - for await (const [name, alias] of this.aliases) { - aliases[name] = alias; - } - msbuildFile.Project.PropertyGroup.push(aliases); - } - - const propertyGroup = { $Label: 'Artifacts', Artifacts: { Artifact: [] } }; - - for (const artifact of artifacts) { - propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.id, '#text': artifact.targetLocation.fsPath }); - } - - if (propertyGroup.Artifacts.Artifact.length > 0) { - msbuildFile.Project.PropertyGroup.push(propertyGroup); - } - - return toXml(msbuildFile); + result.endElement(); // Project + return result.toString(); } protected async generateEnvironmentVariables(originalEnvironment: Record): Promise<[Record, Record]> { @@ -607,15 +536,13 @@ export class Activation { } // .defines get compiled into a single environment variable. - if (this.definesCount) { - let defines = ''; - for await (const [name, value] of this.defines) { - defines += value ? `-D${name}=${value} ` : `-D${name} `; - } - if (defines) { - env['DEFINES'] = defines; - undo['DEFINES'] = originalEnvironment['DEFINES'] || ''; - } + let defines = ''; + for await (const [name, value] of this.defines) { + defines += value ? `-D${name}=${value} ` : `-D${name} `; + } + if (defines) { + env['DEFINES'] = defines; + undo['DEFINES'] = originalEnvironment['DEFINES'] || ''; } return [env, undo]; @@ -630,23 +557,23 @@ export class Activation { if (previous && undoEnvironmentFile) { const deactivationDataFile = this.#session.parseUri(previous); if (deactivationDataFile.scheme === 'file' && await deactivationDataFile.exists()) { - const deactivatationData = JSON.parse(await deactivationDataFile.readUTF8()); - currentEnvironment = undoActivation(currentEnvironment, deactivatationData.environment || {}); + const deactivationData = JSON.parse(await deactivationDataFile.readUTF8()); + currentEnvironment = undoActivation(currentEnvironment, deactivationData.environment || {}); delete currentEnvironment[undoVariableName]; - undoDeactivation = generateScriptContent(scriptKind, deactivatationData.environment || {}, deactivatationData.aliases || {}); + undoDeactivation = generateScriptContent(scriptKind, deactivationData.environment || {}, deactivationData.aliases || {}); } } const [variables, undo] = await this.generateEnvironmentVariables(currentEnvironment); async function transformtoRecord ( - orig: AsyncGenerator>, any, unknown>, + orig: AsyncGenerator>, any, unknown>, // this type cast to U isn't *technically* correct but since it's locally scoped for this next block of code it shouldn't cause problems - func: (value: T) => U = (x => x as unknown as U)) { + func: (value: T) => U = (x => x as unknown as U)) { - return linq.values((await toArrayAsync(orig))).toObject(tuple => [tuple[0], func(tuple[1])]); + return linq.values((await toArrayAsync(orig))).toObject(tuple => [tuple[0], func(tuple[1])]); } - + const defines = await transformtoRecord(this.defines); const aliases = await transformtoRecord(this.aliases); const locations = await transformtoRecord(this.locations); @@ -680,15 +607,15 @@ export class Activation { // generate msbuild props file if requested if (msbuildFile) { - const contents = await this.generateMSBuild(artifacts); + const contents = await this.generateMSBuild(); this.#session.channels.verbose(`--------[START MSBUILD FILE]--------\n${contents}\n--------[END MSBUILD FILE]---------`); - await msbuildFile.writeUTF8(await this.generateMSBuild(artifacts)); + await msbuildFile.writeUTF8(contents); } - if(json) { + if (json) { const contents = generateJson(variables, defines, aliases, properties, locations, paths, tools); this.#session.channels.verbose(`--------[START ENV VAR FILE]--------\n${contents}\n--------[END ENV VAR FILE]---------`); - await json.writeUTF8(contents); + await json.writeUTF8(contents); } } @@ -759,20 +686,20 @@ function generateScriptContent(kind: string, variables: Record, return ''; } -function generateJson(variables: Record, defines: Record, aliases: Record, - properties:Record, locations: Record, paths: Record, tools: Record): string { - - var contents = { - "version": 1, +function generateJson(variables: Record, defines: Record, aliases: Record, + properties:Record>, locations: Record, paths: Record>, tools: Record): string { + + let contents = { + 'version': 1, variables, - defines, - aliases, - properties, - locations, - paths, + defines, + aliases, + properties, + locations, + paths, tools }; - + return JSON.stringify(contents); } diff --git a/ce/ce/interfaces/metadata/exports.ts b/ce/ce/interfaces/metadata/exports.ts index 18643466a6..9666123eb8 100644 --- a/ce/ce/interfaces/metadata/exports.ts +++ b/ce/ce/interfaces/metadata/exports.ts @@ -7,25 +7,8 @@ import { Validation } from '../validation'; /** settings that should be applied to the context */ export interface Exports extends Validation { - /** a map of path categories to one or more values */ - paths: Dictionary; - - /** a map of the known tools to actual tool executable name */ - tools: Dictionary; - - /** - * a map of (environment) variables that should be set in the context. - * - * arrays mean that the values should be joined with spaces - */ - environment: Dictionary; - - /** a map of properties that are activation-type specific (ie, msbuild) */ - properties: Dictionary; - - /** a map of locations that are activation-type specific (ie, msbuild) */ - locations: Dictionary; - + /** shell aliases (aka functions/etc) for exposing specific commands */ + aliases: Dictionary; // this is where we'd see things like // CFLAGS: [...] where you can have a bunch of things that would end up in the CFLAGS variable (or used to set values in a vcxproj/cmake settings file.) // @@ -36,7 +19,20 @@ export interface Exports extends Validation { * it's significant enough that we need them separately */ defines: Dictionary; - - /** shell aliases (aka functions/etc) for exposing specific commands */ - aliases: Dictionary; + /** + * a map of (environment) variables that should be set in the context. + * + * arrays mean that the values should be joined with spaces + */ + environment: Dictionary; + /** a map of locations that are activation-type specific */ + locations: Dictionary; + /** a map of key/values to emit into an MSBuild */ + msbuild_properties: Dictionary; + /** a map of path categories to one or more values */ + paths: Dictionary; + /** a map of properties that are activation-type specific */ + properties: Dictionary; + /** a map of the known tools to actual tool executable name */ + tools: Dictionary; } diff --git a/ce/ce/package.json b/ce/ce/package.json index c1eb27b842..0c9a6926e4 100644 --- a/ce/ce/package.json +++ b/ce/ce/package.json @@ -73,6 +73,6 @@ "cli-progress": "3.11.1", "applicationinsights": "2.2.1", "fast-glob": "3.2.11", - "fast-xml-parser": "4.0.3" + "xml-writer": "1.7.0" } } diff --git a/ce/ce/util/curly-replacements.ts b/ce/ce/util/curly-replacements.ts new file mode 100644 index 0000000000..ea9202a09c --- /dev/null +++ b/ce/ce/util/curly-replacements.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from "../i18n"; + +export function replaceCurlyBraces(subject: string, properties: Map) { + // One of these tokens: + // {{ + // }} + // {variable} + // { + // } + // (anything that has no {}s) + const tokenRegex = /{{|}}|{([^}]+)}|{|}|[^{}]+/y; + const resultElements : Array = []; + for (;;) { + const thisMatch = tokenRegex.exec(subject); + if (thisMatch === null) { + return resultElements.join(''); + } + + const wholeMatch = thisMatch[0]; + if (wholeMatch === '{{') { + resultElements.push('{'); + continue; + } + + if (wholeMatch === '}}') { + resultElements.push('}'); + continue; + } + + if (wholeMatch === '{' || wholeMatch === '}') { + throw new Error(i`Found a mismatched ${wholeMatch} in '${subject}'. For a literal ${wholeMatch}, use ${wholeMatch}${wholeMatch} instead.`); + } + + const variableName = thisMatch[1]; + if (variableName) { + const variableValue = properties.get(variableName); + if (typeof variableValue !== 'string') { + throw new Error(i`Could not find a value for {${variableName}} in '${subject}'. To write the literal value, use '{{${variableName}}}' instead.`); + } + + resultElements.push(variableValue); + continue; + } + + resultElements.push(wholeMatch); + } +} diff --git a/ce/ce/util/xml.ts b/ce/ce/util/xml.ts deleted file mode 100644 index 4c7e7f307b..0000000000 --- a/ce/ce/util/xml.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { XMLBuilder, XmlBuilderOptionsOptional } from 'fast-xml-parser'; - -const defaultOptions = { - attributeNamePrefix: '$', - textNodeName: '#text', - ignoreAttributes: false, - ignoreNameSpace: false, - allowBooleanAttributes: false, - parseNodeValue: false, - parseAttributeValue: true, - trimValues: true, - cdataTagName: '__cdata', - cdataPositionChar: '\\c', - parseTrueNumberOnly: false, - arrayMode: false, - format: true, -}; - -export function toXml(content: Record, options: XmlBuilderOptionsOptional = {}) { - return `\n${new XMLBuilder({ ...defaultOptions, ...options }).build(content)}`.trim(); -} - -export function toXmlFragment(content: Record, options: XmlBuilderOptionsOptional = {}) { - return new XMLBuilder({ ...defaultOptions, ...options }).build(content); -} - diff --git a/ce/common/config/rush/pnpm-lock.yaml b/ce/common/config/rush/pnpm-lock.yaml index 58dbdefad6..ec4efc0022 100644 --- a/ce/common/config/rush/pnpm-lock.yaml +++ b/ce/common/config/rush/pnpm-lock.yaml @@ -23,7 +23,6 @@ specifiers: eslint: 8.8.0 eslint-plugin-notice: 0.9.10 fast-glob: 3.2.11 - fast-xml-parser: 4.0.3 fs-constants: ^1.0.0 got: 11.8.5 inherits: ^2.0.3 @@ -68,7 +67,6 @@ dependencies: eslint: 8.8.0 eslint-plugin-notice: 0.9.10_eslint@8.8.0 fast-glob: 3.2.11 - fast-xml-parser: 4.0.3 fs-constants: 1.0.0 got: 11.8.5 inherits: 2.0.4 @@ -2616,6 +2614,11 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false + /xml-writer/1.7.0: + resolution: {integrity: sha512-elFVMRiV5jb59fbc87zzVa0C01QLBEWP909mRuWqFqrYC5wNTH5QW4AaKMNv7d6zAsuOulkD7wnztZNLQW0Nfg==} + engines: {node: '>=0.4.0'} + dev: false + /xml2js/0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -2711,7 +2714,7 @@ packages: dev: false file:projects/vcpkg-ce.tgz: - resolution: {integrity: sha512-2WsNzA0z2eXnP56x7qMzgEXObbbvc+CYUf703snEU1nPwCXjl0VrZSEXByXwOlA5Ov4Jh1BpRmeyX91+gSLE7g==, tarball: file:projects/vcpkg-ce.tgz} + resolution: {integrity: sha512-PKXMytuWY5e8evBEHth9x1Hxoo8GTiCvn5rLB7xZtV4abNPSPYkptk1v3B4VndefCKN2ofH965BA8asWxwcrMQ==, tarball: file:projects/vcpkg-ce.tgz} name: '@rush-temp/vcpkg-ce' version: 0.0.0 dependencies: @@ -2747,6 +2750,7 @@ packages: typescript: 4.5.5 unbzip2-stream: 1.4.3 vscode-uri: 3.0.3 + xml-writer: 1.7.0 yaml: 2.0.0-10 transitivePeerDependencies: - applicationinsights-native-metrics diff --git a/ce/test/core/msbuild-tests.ts b/ce/test/core/msbuild-tests.ts index 29b8de922e..162178a7db 100644 --- a/ce/test/core/msbuild-tests.ts +++ b/ce/test/core/msbuild-tests.ts @@ -8,14 +8,22 @@ import { SuiteLocal } from './SuiteLocal'; describe('MSBuild Generator', () => { const local = new SuiteLocal(); - const fs = local.fs; after(local.after.bind(local)); - it('Generates locations in order', async () => { + it('Generates roots without a trailing slash', () => { + const activation = new Activation(local.session); + const expectedPosix = 'c:/tmp'; + const expected = (platform() === 'win32') ? expectedPosix.replaceAll('/', '\\') : expectedPosix; + strict.equal(activation.msBuildProcessPropertyValue('{root}', local.fs.file('c:/tmp')), expected); + strict.equal(activation.msBuildProcessPropertyValue('{root}', local.fs.file('c:/tmp/')), expected); + }); + it('Generates locations in order', () => { const activation = new Activation(local.session); + // Note that only "addMSBuildProperty" has an effect on the output for now but that we'll probably + // need to respond to the others in the future. (]>>[ ['z', 'zse&tting'], ['a', 'ase { activation.addLocation('somepath', local.fs.file('c:/tmp')); activation.addPath('include', [local.fs.file('c:/tmp'), local.fs.file('c:/tmp2')]); + activation.addDefine('VERY_POSIX', '1'); - const expected = (platform() === 'win32') ? ` - - - c:\\tmp - - - zse&tting - ase<tting - csetting - bsetting - first;seco>nd;third - - - c:\\tmp;c:\\tmp2 - -` : ` + const fileWithNoSlash = local.fs.file('c:/tmp'); + const fileWithSlash = local.fs.file('c:/tmp/'); + activation.addMSBuildProperty('a', '$(a);fir{root}st', fileWithNoSlash); + activation.addMSBuildProperty('a', '$(a);second', fileWithNoSlash); + activation.addMSBuildProperty('a', '$(a);{root}hello', fileWithNoSlash); + activation.addMSBuildProperty('b', '$(x);first', fileWithSlash); + activation.addMSBuildProperty('b', '$(b);se{root}cond', fileWithSlash); + activation.addMSBuildProperty('a', '$(a);third', fileWithNoSlash); + activation.addMSBuildProperty('b', 'third', fileWithSlash); + activation.addMSBuildProperty('b', '$(b);{root}world', fileWithSlash); + + activation.addMSBuildProperty('xml chars', '\'"<>& and $ look funny when escaped', fileWithSlash); + + const expectedPosix = ` - - c:/tmp - - - zse&tting - ase<tting - csetting - bsetting - first;seco>nd;third - - - c:/tmp;c:/tmp2 + + $(a);firc:/tmpst + $(a);second + $(a);c:/tmphello + $(x);first + $(b);sec:/tmpcond + $(a);third + third + $(b);c:/tmpworld + '"<>& and $ look funny when escaped -` ; +`; - strict.equal(await activation.generateMSBuild([]), expected); + const expected = (platform() === 'win32') + ? expectedPosix.replaceAll('c:/tmp', 'c:\\tmp').replaceAll('c:/', 'c:\\') + : expectedPosix; + strict.equal(activation.generateMSBuild(), expected); }); }); diff --git a/ce/test/core/util/curly-replacements-tests.ts b/ce/test/core/util/curly-replacements-tests.ts new file mode 100644 index 0000000000..44cc3ee6cc --- /dev/null +++ b/ce/test/core/util/curly-replacements-tests.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { replaceCurlyBraces } from '@microsoft/vcpkg-ce/dist/util/curly-replacements'; +import { strict } from 'assert'; + +describe('replaceCurlyBraces', () => { + const replacements = new Map(); + replacements.set('exists', 'exists-replacement'); + replacements.set('another', 'some other replacement text'); + + it('DoesNotTouchLiterals', () => { + strict.equal(replaceCurlyBraces('some literal text', replacements), 'some literal text'); + }); + + it('DoesVariableReplacements', () => { + strict.equal(replaceCurlyBraces('some {exists} text', replacements), 'some exists-replacement text'); + }); + + it('DoesMultipleVariableReplacements', () => { + strict.equal(replaceCurlyBraces('some {exists} {another} text', replacements), 'some exists-replacement some other replacement text text'); + }); + + it('ThrowsForLeadingOnlyEscapes', () => { + strict.throws(() => { + replaceCurlyBraces('some {{exists} text', replacements); + }, new Error('Found a mismatched } in \'some {{exists} text\'. For a literal }, use }} instead.')); + }); + + it('ConsidersTerminalCurlyAsPartOfVariable', () => { + strict.throws(() => { + replaceCurlyBraces('some {exists}} text', replacements); + }, new Error('Found a mismatched } in \'some {exists}} text\'. For a literal }, use }} instead.')); + }); + + it('AllowsDoubleEscapes', () => { + strict.equal(replaceCurlyBraces('some {{{exists} text', replacements), 'some {exists-replacement text'); + strict.equal(replaceCurlyBraces('some {exists}}} text', replacements), 'some exists-replacement} text'); + strict.equal(replaceCurlyBraces('some {{exists}} text', replacements), 'some {exists} text'); + strict.equal(replaceCurlyBraces('some {{{exists}}} text', replacements), 'some {exists-replacement} text'); + strict.equal(replaceCurlyBraces('some {{{{{exists}}} text', replacements), 'some {{exists-replacement} text'); + }); + + it('ThrowsForUnmatchedCurlies', () => { + strict.throws(() => { + replaceCurlyBraces('these are }{ not matched', replacements); + }, new Error('Found a mismatched } in \'these are }{ not matched\'. For a literal }, use }} instead.')); + }); + + it('ThrowsForBadValues', () => { + strict.throws(() => { + replaceCurlyBraces('some {nonexistent} text', replacements); + }, new Error('Could not find a value for {nonexistent} in \'some {nonexistent} text\'. To write the literal value, use \'{{nonexistent}}\' instead.')); + }); + + it('ThrowsForMismatchedBeginCurlies', () => { + strict.throws(() => { + replaceCurlyBraces('some {nonexistent', replacements); + }, new Error('Found a mismatched { in \'some {nonexistent\'. For a literal {, use {{ instead.')); + }); + + it('ThrowsForMismatchedEndCurlies', () => { + strict.throws(() => { + replaceCurlyBraces('some }nonexistent', replacements); + }, new Error('Found a mismatched } in \'some }nonexistent\'. For a literal }, use }} instead.')); + }); +}); diff --git a/docs/vcpkg-schema-definitions.schema.json b/docs/vcpkg-schema-definitions.schema.json index f6b147dbdb..c2ad92c5e7 100644 --- a/docs/vcpkg-schema-definitions.schema.json +++ b/docs/vcpkg-schema-definitions.schema.json @@ -250,26 +250,35 @@ "exports": { "type": "object", "properties": { - "paths": { + "aliases": { + "$ref": "#/definitions/string-map" + }, + "defines": { + "$ref": "#/definitions/string-map" + }, + "environment": { "$ref": "#/definitions/strings-map" }, "locations": { "$ref": "#/definitions/string-map" }, - "properties": { + "msbuild-properties": { + "description": "Properties written verbatim when generating msbuild props for an activation, with $root$ replaced with the ultimate extraction root for this artifact. $root$ never contains a trailing backslash.", + "type": "object", + "patternProperties": { + "": { + "type": "string" + } + } + }, + "paths": { "$ref": "#/definitions/strings-map" }, - "environment": { + "properties": { "$ref": "#/definitions/strings-map" }, "tools": { "$ref": "#/definitions/string-map" - }, - "defines": { - "$ref": "#/definitions/string-map" - }, - "aliases": { - "$ref": "#/definitions/string-map" } }, "additionalProperties": false