diff --git a/assets/package.json b/assets/package.json index 684061b..c558b7b 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,6 @@ { "name": "vcpkg-ce", - "version": "0.7.0", + "version": "0.8.0", "description": "vcpkg-ce CLI", "main": "ce/dist/main.js", "bin": { diff --git a/ce/amf/contact.ts b/ce/amf/contact.ts index c296a73..a4b3654 100644 --- a/ce/amf/contact.ts +++ b/ce/amf/contact.ts @@ -17,6 +17,8 @@ export class Contact extends Entity implements IContact { /** @internal */ override *validate(): Iterable { yield* super.validate(); + yield* this.validateChildKeys(['email', 'role']); + yield* this.validateChild('email', 'string'); } } diff --git a/ce/amf/demands.ts b/ce/amf/demands.ts index 551faa2..7698f1e 100644 --- a/ce/amf/demands.ts +++ b/ce/amf/demands.ts @@ -3,28 +3,25 @@ import { stream } from 'fast-glob'; import { lstat, Stats } from 'fs'; -import { delimiter, join, resolve } from 'path'; +import { join, resolve } from 'path'; import { isMap, isScalar } from 'yaml'; -import { Activation } from '../artifacts/activation'; import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; import { AlternativeFulfillment } from '../interfaces/metadata/alternative-fulfillment'; import { ValidationError } from '../interfaces/validation-error'; import { parseQuery } from '../mediaquery/media-query'; import { Session } from '../session'; -import { Evaluator } from '../util/evaluator'; import { cmdlineToArray, execute } from '../util/exec-cmd'; -import { createSandbox } from '../util/safeEval'; +import { safeEval, valiadateExpression } from '../util/safeEval'; import { Entity } from '../yaml/Entity'; import { EntityMap } from '../yaml/EntityMap'; +import { ScalarMap } from '../yaml/ScalarMap'; import { Strings } from '../yaml/strings'; import { Primitive, Yaml, YAMLDictionary } from '../yaml/yaml-types'; +import { Exports } from './exports'; import { Installs } from './installer'; import { Requires } from './Requires'; -import { Settings } from './settings'; -/** sandboxed eval function for evaluating expressions */ -const safeEval: (code: string, context?: any) => T = createSandbox(); const hostFeatures = new Set(['x64', 'x86', 'arm', 'arm64', 'windows', 'linux', 'osx', 'freebsd']); const ignore = new Set(['info', 'contacts', 'error', 'message', 'warning', 'requires', 'see-also']); @@ -69,32 +66,15 @@ export class Demands extends EntityMap { } export class DemandBlock extends Entity { - #environment: Record = {}; - #activation?: Activation; - #data?: Record; + discoveredData = >{}; - setActivation(activation?: Activation) { - this.#activation = activation; - } - - setData(data: Record) { - this.#data = data; - } - - setEnvironment(env: Record) { - this.#environment = env; - } - - protected get evaluationBlock() { - return new Evaluator(this.#data || {}, this.#environment, this.#activation?.output || {}); - } get error(): string | undefined { return this.usingAlternative ? this.unless.error : this.asString(this.getMember('error')); } set error(value: string | undefined) { this.setMember('error', value); } get warning(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('warning')); } set warning(value: string | undefined) { this.setMember('warning', value); } - get message(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('message')); } + get message(): string | undefined { return this.usingAlternative ? this.unless.message : this.asString(this.getMember('message')); } set message(value: string | undefined) { this.setMember('message', value); } get seeAlso(): Requires { @@ -105,18 +85,23 @@ export class DemandBlock extends Entity { return this.usingAlternative ? this.unless.requires : this._requires; } - get settings(): Settings { - return this.usingAlternative ? this.unless.settings : this._settings; + get exports(): Exports { + return this.usingAlternative ? this.unless.exports : this._exports; } get install(): Installs { return this.usingAlternative ? this.unless.install : this._install; } + get apply(): ScalarMap { + return this.usingAlternative ? this.unless.apply : this._apply; + } + protected readonly _seeAlso = new Requires(undefined, this, 'seeAlso'); protected readonly _requires = new Requires(undefined, this, 'requires'); - protected readonly _settings = new Settings(undefined, this, 'settings'); + protected readonly _exports = new Exports(undefined, this, 'exports'); protected readonly _install = new Installs(undefined, this, 'install'); + protected readonly _apply = new ScalarMap(undefined, this, 'apply'); readonly unless!: Unless; @@ -136,7 +121,7 @@ export class DemandBlock extends Entity { * when this runs, if the alternative is met, the rest of the demand is redirected to the alternative. */ async init(session: Session): Promise { - this.#environment = session.environment; + if (this.usingAlternative === undefined && this.has('unless')) { await this.unless.init(session); this.usingAlternative = this.unless.usingAlternative; @@ -146,20 +131,47 @@ export class DemandBlock extends Entity { /** @internal */ override *validate(): Iterable { + yield* this.validateChildKeys(['error', 'warning', 'message', 'seeAlso', 'requires', 'exports', 'install', 'apply', 'unless']); + yield* super.validate(); if (this.exists()) { - yield* this.settings.validate(); + yield* this.validateChild('error', 'string'); + yield* this.validateChild('warning', 'string'); + yield* this.validateChild('message', 'string'); + + yield* this.exports.validate(); yield* this.requires.validate(); yield* this.seeAlso.validate(); yield* this.install.validate(); + if (this.unless) { + yield* this.unless.validate(); + } } } + private evaluate(value: string) { + if (!value || value.indexOf('$') === -1) { + // quick exit if no expression or no variables + return value; + } + + // $$ -> escape for $ + value = value.replace(/\$\$/g, '\uffff'); + + // $0 ... $9 -> replace contents with the values from the artifact + value = value.replace(/\$([0-9])/g, (match, index) => this.discoveredData[match] || match); + + // restore escaped $ + return value.replace(/\uffff/g, '$'); + } + override asString(value: any): string | undefined { if (value === undefined) { return value; } - return this.evaluationBlock.evaluate(isScalar(value) ? value.value : value); + value = isScalar(value) ? value.value : value; + + return this.evaluate(value); } override asPrimitive(value: any): Primitive | undefined { @@ -175,59 +187,20 @@ export class DemandBlock extends Entity { return value; case 'string': { - return this.evaluationBlock.evaluate(value); + return this.evaluate(value); } } return undefined; } } -/** Expands string variables in a string */ -function expandStrings(sandboxData: Record, value: string) { - let n = undefined; - - // allow $PATH instead of ${PATH} -- simplifies YAML strings - value = value.replace(/\$([a-zA-Z0-9.]+)/g, '${$1}'); - - const parts = value.split(/(\${\S+?})/g).filter(each => each).map((each, i) => { - const v = each.replace(/^\${(.*)}$/, (m, match) => safeEval(match, sandboxData) ?? each); - - if (v.indexOf(delimiter) !== -1) { - n = i; - } - - return v; - }); - - if (n === undefined) { - return parts.join(''); - } - - const front = parts.slice(0, n).join(''); - const back = parts.slice(n + 1).join(''); - - return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`).join(delimiter); -} - /** filters output and produces a sandbox context object */ -function filter(expression: string, content: string) { +function filterOutput(expression: string, content: string) { const parsed = /^\/(.*)\/(\w*)$/.exec(expression); - const output = { - $content: content - }; if (parsed) { - const filtered = new RegExp(parsed[1], parsed[2]).exec(content); - - if (filtered) { - for (const [i, v] of filtered.entries()) { - if (i === 0) { - continue; - } - output[`$${i}`] = v; - } - } + return new RegExp(parsed[1], parsed[2]).exec(content)?.reduce((p, c, i) => { p[`$${i}`] = c; return p; }, {}) ?? {}; } - return output; + return {}; } export class Unless extends DemandBlock implements AlternativeFulfillment { @@ -246,25 +219,33 @@ export class Unless extends DemandBlock implements AlternativeFulfillment { /** @internal */ override *validate(): Iterable { - // todo: what other validations do we need? - yield* super.validate(); - if (this.has('unless')) { - yield { - message: '"unless" is not supported in an unless block', - range: this.sourcePosition('unless'), - category: ErrorKind.InvalidDefinition - }; + if (this.exists()) { + // todo: what other validations do we need? + // yield* super.validate(); + if (this.has('unless')) { + yield { + message: i`"unless" is not supported in an unless block`, + range: this.sourcePosition('unless'), + category: ErrorKind.InvalidDefinition + }; + } + if (this.matches && !valiadateExpression(this.matches)) { + yield { + message: i`'is' expression ("${this.matches}") is not a valid comparison expression.`, + range: this.sourcePosition('is'), + category: ErrorKind.InvalidExpression + }; + } } } override async init(session: Session): Promise { - this.setEnvironment(session.environment); if (this.usingAlternative === undefined) { this.usingAlternative = false; if (this.from.length > 0 && this.where.length > 0) { // we're doing some kind of check. - const locations = [...this.from].map(each => expandStrings(this.evaluationBlock, each).split(delimiter)).flat(); - const binaries = [...this.where].map(each => expandStrings(this.evaluationBlock, each)); + const locations = [...this.from].map(each => session.activation.expandPathLikeVariableExpressions(each)).flat(); + const binaries = [...this.where]; const search = locations.map(location => binaries.map(binary => join(location, binary).replace(/\\/g, '/'))).flat(); @@ -298,21 +279,22 @@ export class Unless extends DemandBlock implements AlternativeFulfillment { } })) { // we found something that looks promising. - let filtered = { $0: item }; - this.setData(filtered); - if (this.run) { + this.discoveredData = { $0: item.toString() }; + const run = this.run?.replace('$0', item.toString()); - const commandline = cmdlineToArray(this.run.replace('$0', item.toString())); + if (run) { + const commandline = cmdlineToArray(run); const result = await execute(resolve(commandline[0]), commandline.slice(1)); if (result.code !== 0) { continue; } - filtered = filter(this.select || '', result.log); - filtered.$0 = item; + this.discoveredData = filterOutput(this.select || '', result.log) || []; + this.discoveredData['$0'] = item.toString(); + ((this.parent)).discoveredData = this.discoveredData; // if we have a match expression, let's check it. - if (this.matches && !safeEval(this.matches, filtered)) { + if (this.matches && !safeEval(this.matches, this.discoveredData)) { continue; // not a match, move on } @@ -320,7 +302,7 @@ export class Unless extends DemandBlock implements AlternativeFulfillment { this.usingAlternative = true; // set the data output of the check // this is used later to fill in the settings. - this.setData(filtered); + return this; } } @@ -341,8 +323,8 @@ export class Unless extends DemandBlock implements AlternativeFulfillment { return this._requires; } - override get settings(): Settings { - return this._settings; + override get exports(): Exports { + return this._exports; } override get install(): Installs { diff --git a/ce/amf/settings.ts b/ce/amf/exports.ts similarity index 60% rename from ce/amf/settings.ts rename to ce/amf/exports.ts index 68ae300..591f3a9 100644 --- a/ce/amf/settings.ts +++ b/ce/amf/exports.ts @@ -2,23 +2,27 @@ // Licensed under the MIT License. -import { Settings as ISettings } from '../interfaces/metadata/Settings'; +import { Exports as IExports } from '../interfaces/metadata/exports'; import { ValidationError } from '../interfaces/validation-error'; import { BaseMap } from '../yaml/BaseMap'; import { ScalarMap } from '../yaml/ScalarMap'; import { StringsMap } from '../yaml/strings'; -export class Settings extends BaseMap implements ISettings { +export class Exports extends BaseMap implements IExports { paths: StringsMap = new StringsMap(undefined, this, 'paths'); locations: ScalarMap = new ScalarMap(undefined, this, 'locations'); properties: StringsMap = new StringsMap(undefined, this, 'properties'); - variables: StringsMap = new StringsMap(undefined, this, 'variables'); + 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'); + contents: StringsMap = new StringsMap(undefined, this, 'contents'); + /** @internal */ override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChildKeys(['paths', 'locations', 'properties', 'environment', 'tools', 'defines', 'aliases', 'contents']); // todo: what validations do we need? - } } diff --git a/ce/amf/info.ts b/ce/amf/info.ts index 011e5c8..47994a5 100644 --- a/ce/amf/info.ts +++ b/ce/amf/info.ts @@ -25,7 +25,7 @@ export class Info extends Entity implements IInfo { get description(): string | undefined { return this.asString(this.getMember('description')); } set description(value: string | undefined) { this.setMember('description', value); } - private flags = new Flags(undefined, this, 'options'); + readonly flags = new Flags(undefined, this, 'options'); get dependencyOnly(): boolean { return this.flags.has('dependencyOnly'); } set dependencyOnly(value: boolean) { this.flags.set('dependencyOnly', value); } @@ -33,25 +33,26 @@ export class Info extends Entity implements IInfo { /** @internal */ override *validate(): Iterable { yield* super.validate(); + yield* this.validateChildKeys(['version', 'id', 'summary', 'priority', 'description', 'options']); if (!this.has('id')) { yield { message: i`Missing identity '${'info.id'}'`, range: this, category: ErrorKind.FieldMissing }; - } else if (!this.is('id', 'string')) { + } else if (!this.childIs('id', 'string')) { yield { message: i`info.id should be of type 'string', found '${this.kind('id')}'`, range: this.sourcePosition('id'), category: ErrorKind.IncorrectType }; } if (!this.has('version')) { yield { message: i`Missing version '${'info.version'}'`, range: this, category: ErrorKind.FieldMissing }; - } else if (!this.is('version', 'string')) { + } else if (!this.childIs('version', 'string')) { yield { message: i`info.version should be of type 'string', found '${this.kind('version')}'`, range: this.sourcePosition('version'), category: ErrorKind.IncorrectType }; } - if (this.is('summary', 'string') === false) { + if (this.childIs('summary', 'string') === false) { yield { message: i`info.summary should be of type 'string', found '${this.kind('summary')}'`, range: this.sourcePosition('summary'), category: ErrorKind.IncorrectType }; } - if (this.is('description', 'string') === false) { + if (this.childIs('description', 'string') === false) { yield { message: i`info.description should be of type 'string', found '${this.kind('description')}'`, range: this.sourcePosition('description'), category: ErrorKind.IncorrectType }; } - if (this.is('options', 'sequence') === false) { + if (this.childIs('options', 'sequence') === false) { yield { message: i`info.options should be a sequence, found '${this.kind('options')}'`, range: this.sourcePosition('options'), category: ErrorKind.IncorrectType }; } } diff --git a/ce/amf/installer.ts b/ce/amf/installer.ts index 8497d47..e6781aa 100644 --- a/ce/amf/installer.ts +++ b/ce/amf/installer.ts @@ -7,6 +7,7 @@ import { Installer as IInstaller } from '../interfaces/metadata/installers/Insta import { NupkgInstaller } from '../interfaces/metadata/installers/nupkg'; import { UnTarInstaller } from '../interfaces/metadata/installers/tar'; import { UnZipInstaller } from '../interfaces/metadata/installers/zip'; +import { ValidationError } from '../interfaces/validation-error'; import { Entity } from '../yaml/Entity'; import { EntitySequence } from '../yaml/EntitySequence'; import { Flags } from '../yaml/Flags'; @@ -46,14 +47,24 @@ export class Installs extends EntitySequence { } throw new Error('Unsupported node type'); } -} + override *validate(): Iterable { + yield* super.validate(); + for (const each of this) { + yield* each.validate(); + } + } +} export class Installer extends Entity implements IInstaller { get installerKind(): string { throw new Error('abstract type, should not get here.'); } + override get fullName(): string { + return `${super.fullName}.${this.installerKind}`; + } + get lang() { return this.asString(this.getMember('lang')); } @@ -61,6 +72,12 @@ export class Installer extends Entity implements IInstaller { get nametag() { return this.asString(this.getMember('nametag')); } + + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChild('lang', 'string'); + yield* this.validateChild('nametag', 'string'); + } } abstract class FileInstallerNode extends Installer { @@ -89,17 +106,32 @@ abstract class FileInstallerNode extends Installer { } readonly transform = new Strings(undefined, this, 'transform'); + + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChild('strip', 'number'); + yield* this.validateChild('sha256', 'string'); + yield* this.validateChild('sha512', 'string'); + } + } class UnzipNode extends FileInstallerNode implements UnZipInstaller { override get installerKind() { return 'unzip'; } readonly location = new Strings(undefined, this, 'unzip'); + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChildKeys(['unzip', 'sha256', 'sha512', 'strip', 'transform', 'lang', 'nametag']); + } } class UnTarNode extends FileInstallerNode implements UnTarInstaller { override get installerKind() { return 'untar'; } location = new Strings(undefined, this, 'untar'); - + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChildKeys(['untar', 'sha256', 'sha512', 'strip', 'transform']); + } } class NupkgNode extends Installer implements NupkgInstaller { get location() { @@ -137,6 +169,10 @@ class NupkgNode extends Installer implements NupkgInstaller { } readonly transform = new Strings(undefined, this, 'transform'); + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChildKeys(['nupkg', 'sha256', 'sha512', 'strip', 'transform', 'lang', 'nametag']); + } } class GitCloneNode extends Installer implements GitInstaller { @@ -191,4 +227,9 @@ class GitCloneNode extends Installer implements GitInstaller { set espidf(value: boolean) { this.flags.set('espidf', value); } + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateChildKeys(['git', 'commit', 'subdirectory', 'options', 'lang', 'nametag']); + yield* this.validateChild('commit', 'string'); + } } diff --git a/ce/amf/metadata-file.ts b/ce/amf/metadata-file.ts index e74c233..5e903ac 100644 --- a/ce/amf/metadata-file.ts +++ b/ce/amf/metadata-file.ts @@ -4,7 +4,6 @@ import { extname } from 'path'; import { Document, isMap, LineCounter, parseDocument, YAMLMap } from 'yaml'; -import { Activation } from '../artifacts/activation'; import { Registry } from '../artifacts/registry'; import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; @@ -39,6 +38,7 @@ export class MetadataFile extends BaseMap implements Profile { this.context.session = session; this.context.file = session.parseUri(this.context.filename); this.context.folder = this.context.file.parent; + await this.demandBlock.init(session); return this; } @@ -52,7 +52,9 @@ export class MetadataFile extends BaseMap implements Profile { content = '{\n}'; } const doc = parseDocument(content, { prettyErrors: false, lineCounter: lc, strict: true }); - return new MetadataFile(doc, filename, lc, registry).init(session); + const result = new MetadataFile(doc, filename, lc, registry).init(session); + (await result).validationErrors; + return result; } info = new Info(undefined, this, 'info'); @@ -75,13 +77,11 @@ export class MetadataFile extends BaseMap implements Profile { get seeAlso() { return this.demandBlock.seeAlso; } get requires() { return this.demandBlock.requires; } - get settings() { return this.demandBlock.settings; } + get exports() { return this.demandBlock.exports; } get install() { return this.demandBlock.install; } get unless() { return this.demandBlock.unless; } + get apply() { return this.demandBlock.apply; } - setActivation(activation: Activation): void { - this.demandBlock.setActivation(activation); - } conditionalDemands = new Demands(undefined, this, 'demands'); @@ -171,6 +171,7 @@ export class MetadataFile extends BaseMap implements Profile { /** @internal */ override *validate(): Iterable { yield* super.validate(); + yield* this.validateChildKeys(['info', 'contacts', 'registries', 'global', 'demands', 'apply', 'exports', 'requires', 'install', 'seeAlso', 'unless']); // verify that we have info if (!this.document.has('info')) { @@ -201,7 +202,7 @@ export class MetadataFile extends BaseMap implements Profile { yield* this.install.validate(); yield* this.registries.validate(); yield* this.contacts.validate(); - yield* this.settings.validate(); + yield* this.exports.validate(); yield* this.globalSettings.validate(); yield* this.requires.validate(); yield* this.seeAlso.validate(); diff --git a/ce/amf/registries.ts b/ce/amf/registries.ts index 0791504..51ff8ce 100644 --- a/ce/amf/registries.ts +++ b/ce/amf/registries.ts @@ -120,7 +120,7 @@ export class Registries extends Yaml implements D } return 0; } - get keys(): Array { + override get keys(): Array { if (isMap(this.node)) { return this.node.items.map(({ key }) => this.asString(key) || ''); } @@ -160,6 +160,7 @@ export class Registries extends Yaml implements D } /** @internal */ override *validate(): Iterable { + yield* super.validate(); if (this.exists()) { for (const [key, registry] of this) { yield* registry.validate(); @@ -175,6 +176,7 @@ export class Registry extends Entity implements IRegistry { /** @internal */ override *validate(): Iterable { + yield* super.validate(); // if (this.registryKind === undefined) { yield { @@ -190,6 +192,8 @@ class LocalRegistry extends Registry { readonly location = new Strings(undefined, this, 'location'); /** @internal */ override *validate(): Iterable { + yield* super.validate(); + // if (this.registryKind !== 'artifact') { yield { @@ -204,6 +208,8 @@ class LocalRegistry extends Registry { class RemoteRegistry extends Registry { readonly location = new Strings(undefined, this, 'location'); override *validate(): Iterable { + yield* super.validate(); + // if (this.registryKind !== 'artifact') { yield { diff --git a/ce/artifacts/SetOfDemands.ts b/ce/artifacts/SetOfDemands.ts index 58d3ac4..8a578d3 100644 --- a/ce/artifacts/SetOfDemands.ts +++ b/ce/artifacts/SetOfDemands.ts @@ -7,8 +7,7 @@ import { VersionReference } from '../interfaces/metadata/version-reference'; import { parseQuery } from '../mediaquery/media-query'; import { Session } from '../session'; import { MultipleInstallsMatched } from '../util/exceptions'; -import { Dictionary, linq } from '../util/linq'; -import { Activation } from './activation'; +import { linq, Record } from '../util/linq'; export class SetOfDemands { @@ -25,12 +24,6 @@ export class SetOfDemands { } } - setActivation(activation: Activation) { - for (const [, demandBlock] of this._demands.entries()) { - demandBlock.setActivation(activation); - } - } - /** Async Initializer */ async init(session: Session) { for (const [query, demands] of this._demands) { @@ -58,8 +51,8 @@ export class SetOfDemands { get messages() { return linq.values(this._demands).selectNonNullable(d => d.message).toArray(); } - get settings() { - return linq.values(this._demands).selectNonNullable(d => d.settings).toArray(); + get exports() { + return linq.values(this._demands).selectNonNullable(d => d.exports).toArray(); } get seeAlso() { return linq.values(this._demands).selectNonNullable(d => d.seeAlso).toArray(); @@ -68,7 +61,7 @@ export class SetOfDemands { get requires() { const d = this._demands; const rq1 = linq.values(d).selectNonNullable(d => d.requires).toArray(); - const result = new Dictionary(); + const result = new Record(); for (const dict of rq1) { for (const [query, demands] of dict) { result[query] = demands; diff --git a/ce/artifacts/activation.ts b/ce/artifacts/activation.ts index acd4611..f957741 100644 --- a/ce/artifacts/activation.ts +++ b/ce/artifacts/activation.ts @@ -1,34 +1,480 @@ -/* eslint-disable keyword-spacing */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { delimiter } from 'path'; +/* eslint-disable prefer-const */ + +import { lstat } from 'fs/promises'; +import { delimiter, extname, resolve } from 'path'; +import { isScalar } from 'yaml'; +import { undo as undoVariableName } from '../constants'; +import { i } from '../i18n'; +import { Exports } from '../interfaces/metadata/exports'; import { Session } from '../session'; -import { linq } from '../util/linq'; +import { isIterable } from '../util/checks'; +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'; +function findCaseInsensitiveOnWindows(map: Map, key: string): V | undefined { + return process.platform === 'win32' ? linq.find(map, key) : map.get(key); +} +export type Tuple = [K, V]; + export class Activation { + + #defines = new Map(); + #aliases = new Map(); + #environment = new Map>(); + #properties = new Map>(); + + // Relative to the artifact install + #locations = new Map(); + #paths = new Map>(); + #tools = new Map(); + #session: Session; constructor(session: Session) { this.#session = session; } - /** gets a flattend object representation of the activation */ - get output() { - return { - defines: Object.fromEntries(this.defines), - locations: Object.fromEntries([... this.locations.entries()].map(([k, v]) => [k, v.fsPath])), - properties: Object.fromEntries([... this.properties.entries()].map(([k, v]) => [k, v.join(',')])), - environment: { ...process.env, ...Object.fromEntries([... this.environment.entries()].map(([k, v]) => [k, v.join(' ')])) }, - tools: Object.fromEntries(this.tools), - paths: Object.fromEntries([...this.paths.entries()].map(([k, v]) => [k, v.map(each => each.fsPath).join(delimiter)])), - aliases: Object.fromEntries(this.aliases) - }; + addExports(exports: Exports, targetFolder: Uri) { + for (let [define, defineValue] of exports.defines) { + if (!define) { + continue; + } + + if (defineValue === 'true') { + defineValue = '1'; + } + this.addDefine(define, defineValue); + } + + // **** paths **** + for (const [pathName, values] of exports.paths) { + if (!pathName || !values || values.length === 0) { + continue; + } + + // the folder is relative to the artifact install + for (const folder of values) { + this.addPath(pathName, resolve(targetFolder.fsPath, folder)); + } + } + + // **** tools **** + for (let [toolName, toolPath] of exports.tools) { + if (!toolName || !toolPath) { + continue; + } + this.addTool(toolName, resolve(targetFolder.fsPath, toolPath)); + } + + // **** locations **** + for (const [name, location] of exports.locations) { + if (!name || !location) { + continue; + } + + this.addLocation(name, resolve(targetFolder.fsPath, location)); + } + + // **** variables **** + for (const [name, environmentVariableValues] of exports.environment) { + if (!name || environmentVariableValues.length === 0) { + continue; + } + this.addEnvironmentVariable(name, environmentVariableValues); + } + + // **** properties **** + for (const [name, propertyValues] of exports.properties) { + if (!name || propertyValues.length === 0) { + continue; + } + this.addProperty(name, propertyValues); + } + + // **** aliases **** + for (const [name, alias] of exports.aliases) { + if (!name || !alias) { + continue; + } + this.addAlias(name, alias); + } + } + + + /** a collection of #define declarations that would assumably be applied to all compiler calls. */ + addDefine(name: string, value: string) { + const v = findCaseInsensitiveOnWindows(this.#defines, name); + + if (v && v !== value) { + // conflict. todo: what do we want to do? + this.#session.channels.warning(i`Duplicate define ${name} during activation. New value will replace old.`); + } + this.#defines.set(name, value); + } + + get defines() { + 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; + } + + /** a collection of tool locations from artifacts */ + addTool(name: string, value: string) { + const t = findCaseInsensitiveOnWindows(this.#tools, name); + if (t && t !== value) { + this.#session.channels.error(i`Duplicate tool declared ${name} during activation. New value will replace old.`); + } + this.#tools.set(name, value); + } + + get tools() { + return linq.entries(this.#tools).selectAsync(async ([key, value]) => >[key, await this.resolveAndVerify(value)]); + } + + async getTool(name: string): Promise { + const t = findCaseInsensitiveOnWindows(this.#tools, name); + if (t) { + const path = await this.resolveAndVerify(t); + return await this.validatePath(path) ? path : undefined; + } + 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); + if (a && a !== value) { + this.#session.channels.error(i`Duplicate alias declared ${name} during activation. New value will replace old.`); + } + this.#aliases.set(name, value); + } + + async getAlias(name: string, refcheck = new Set()): Promise { + const v = findCaseInsensitiveOnWindows(this.#aliases, name); + if (v !== undefined) { + return this.resolveAndVerify(v, [], refcheck); + } + return undefined; + } + + get aliases() { + return linq.entries(this.#aliases).selectAsync(async ([key, value]) => >[key, await this.resolveAndVerify(value)]); + } + + get aliasCount() { + return this.#aliases.size; + } + + /** a collection of 'published locations' from artifacts. useful for msbuild */ + addLocation(name: string, location: string | Uri) { + if (!name || !location) { + return; + } + location = typeof location === 'string' ? location : location.fsPath; + + const l = this.#locations.get(name); + if (l !== location) { + this.#session.channels.error(i`Duplicate location declared ${name} during activation. New value will replace old.`); + } + this.#locations.set(name, location); + } + + get locations() { + return linq.entries(this.#locations).selectAsync(async ([key, value]) => >[key, await this.resolveAndVerify(value)]); + } + + getLocation(name: string) { + 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) { + if (!name || !location) { + return; + } + + let set = findCaseInsensitiveOnWindows(this.#paths, name); + + if (!set) { + set = new Set(); + this.#paths.set(name, set); + } + + if (isIterable(location)) { + for (const l of location) { + set.add(typeof l === 'string' ? l : l.fsPath); + } + } else { + set.add(typeof location === 'string' ? location : location.fsPath); + } + } + + get paths() { + 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) { + return undefined; + } + return this.resolveAndVerify(set); + } + + /** environment variables from artifacts */ + addEnvironmentVariable(name: string, value: string | Iterable) { + if (!name) { + return; + } + + let v = findCaseInsensitiveOnWindows(this.#environment, name); + if (!v) { + v = new Set(); + this.#environment.set(name, v); + } + + if (typeof value === 'string') { + v.add(value); + } else { + for (const each of value) { + v.add(each); + } + } + } + + get environmentVariables() { + 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 */ + addProperty(name: string, value: string | Iterable) { + if (!name) { + return; + } + let v = this.#properties.get(name); + if (!v) { + v = new Set(); + this.#properties.set(name, v); + } + + if (typeof value === 'string') { + v.add(value); + } else { + for (const each of value) { + v.add(each); + } + } + } + + get properties() { + return linq.entries(this.#properties).selectAsync(async ([key, value]) => >>[key, await this.resolveAndVerify(value)]); + } + + async getProperty(name: string) { + const v = this.#properties.get(name); + return v ? await this.resolveAndVerify(v) : undefined; + } + + get propertyCount() { + return this.#properties.size; + } + 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> { + if (typeof value === 'string') { + value = this.resolveVariables(value, locals, refcheck); + + if (value.indexOf('{') === -1) { + return value; + } + const parts = value.split(/\{+(.+?)\}+/g); + const result = []; + for (let index = 0; index < parts.length; index += 2) { + result.push(parts[index]); + result.push(await this.validatePath(parts[index + 1])); + } + return result.join(''); + } + // for sets + const result = new Set(); + await new Queue().enqueueMany(value, async (v) => result.add(await this.resolveAndVerify(v, locals))).done; + return result; + } + + private resolveVariables(text: string, locals: Array = [], refcheck = new Set()): string { + if (isScalar(text)) { + this.#session.channels.debug(`internal warning: scalar value being used directly : ${text.value}`); + text = text.value; // spews a --debug warning if a scalar makes its way thru for some reason + } + + // short-ciruiting + if (!text || text.indexOf('$') === -1) { + return text; + } + + // prevent circular resolution + if (refcheck.has(text)) { + this.#session.channels.warning(i`Circular variable reference detected: ${text}`); + this.#session.channels.debug(i`Circular variable reference detected: ${text} - ${linq.join(refcheck, ' -> ')}`); + return text; + } + + return text.replace(/(\$\$)|(\$)([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)|(\$)([a-zA-Z_][a-zA-Z0-9_]*)/g, (wholeMatch, isDoubleDollar, isObjectMember, obj, member, isSimple, variable) => { + return isDoubleDollar ? '$' : isObjectMember ? this.getValueForVariableSubstitution(obj, member, locals, refcheck) : this.resolveVariables(locals[variable], locals, refcheck); + }); + } + + private getValueForVariableSubstitution(obj: string, member: string, locals: Array, refcheck: Set): string { + switch (obj) { + case 'environment': { + // lookup environment variable value + const v = findCaseInsensitiveOnWindows(this.#environment, member); + if (v) { + return this.resolveVariables(linq.join(v, ' '), [], refcheck); + } + + // lookup the environment variable in the original environment + const orig = this.#session.environment[member]; + if (orig) { + return orig; + } + break; + } + + case 'defines': { + const v = findCaseInsensitiveOnWindows(this.#defines, member); + if (v !== undefined) { + return this.resolveVariables(v, locals, refcheck); + } + break; + } + + case 'aliases': { + const v = findCaseInsensitiveOnWindows(this.#aliases, member); + if (v !== undefined) { + return this.resolveVariables(v, locals, refcheck); + } + break; + } + + case 'locations': { + const v = findCaseInsensitiveOnWindows(this.#locations, member); + if (v !== undefined) { + return this.resolveVariables(v, locals, refcheck); + } + break; + } + + case 'paths': { + const v = findCaseInsensitiveOnWindows(this.#paths, member); + if (v !== undefined) { + return this.resolveVariables(linq.join(v, delimiter), locals, refcheck); + } + break; + } + + case 'properties': { + const v = findCaseInsensitiveOnWindows(this.#properties, member); + if (v !== undefined) { + return this.resolveVariables(linq.join(v, ';'), locals, refcheck); + } + break; + } + + case 'tools': { + const v = findCaseInsensitiveOnWindows(this.#tools, member); + if (v !== undefined) { + return this.resolveVariables(v, locals, refcheck); + } + break; + } + + default: + this.#session.channels.warning(i`Variable reference found '$${obj}.${member}' that is referencing an unknown base object.`); + return `$${obj}.${member}`; + } + + this.#session.channels.debug(i`Unresolved varible reference found ($${obj}.${member}) during variable substitution.`); + return `$${obj}.${member}`; + } + + + private async validatePath(path: string) { + if (path) { + try { + if (path[0] === '"') { + path = path.substr(1, path.length - 2); + } + path = resolve(path); + await lstat(path); + + // if the path has spaces, we need to quote it + if (path.indexOf(' ') !== -1) { + path = `"${path}"`; + } + + return path; + } catch { + // does not exist + this.#session.channels.error(i`Invalid path - does not exist: ${path}`); + } + } + return ''; + } + + expandPathLikeVariableExpressions(value: string): Array { + let n = undefined; + const parts = value.split(/(\$[a-zA-Z0-9_.]+)/g).filter(each => each).map((part, i) => { + + const value = this.resolveVariables(part).replace(/\{(.*?)\}/g, (match, expression) => expression); + + if (value.indexOf(delimiter) !== -1) { + n = i; + } + + return value; + }); + + if (n === undefined) { + // if the value didn't have a path separator, then just return the value + return [parts.join('')]; + } + + const front = parts.slice(0, n).join(''); + const back = parts.slice(n + 1).join(''); + + return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`); } - generateMSBuild(artifacts: Iterable): string { + async generateMSBuild(artifacts: Iterable): Promise { const msbuildFile = { Project: { $xmlns: 'http://schemas.microsoft.com/developer/msbuild/2003', @@ -36,32 +482,80 @@ export class Activation { } }; - if (this.locations.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Locations', ...linq.entries(this.locations).toObject(([key, value]) => [key, value.fsPath]) }); + 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.properties.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Properties', ...linq.entries(this.properties).toObject(([key, value]) => [key, value.join(';')]) }); + 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.tools.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Tools', ...linq.entries(this.tools).toObject(each => each) }); + 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.environment.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Environment', ...linq.entries(this.environment).toObject(each => each) }); + if (this.environmentVariableCount) { + const environment = >{ + $Label: 'Environment' + }; + + for await (const [name, envValues] of this.environmentVariables) { + environment[name] = linq.join(envValues, ';'); + } + msbuildFile.Project.PropertyGroup.push(environment); } - if (this.paths.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Paths', ...linq.entries(this.paths).toObject(([key, value]) => [key, value.map(each => each.fsPath).join(';')]) }); + 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); } - if (this.defines.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Defines', DEFINES: linq.entries(this.defines).select(([key, value]) => `${key}=${value}`).join(';') }); + 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.aliases.size) { - msbuildFile.Project.PropertyGroup.push({ $Label: 'Aliases', ...linq.entries(this.environment).toObject(each => each) }); + 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: [] } }; @@ -77,90 +571,190 @@ export class Activation { return toXml(msbuildFile); } - /** a collection of #define declarations that would assumably be applied to all compiler calls. */ - defines = new Map(); + protected async generateEnvironmentVariables(originalEnvironment: Record): Promise<[Record, Record]> { - /** a collection of tool definitions from artifacts (think shell 'aliases') */ - tools = new Map(); + const undo = new Record(); + const env = new Record(); - /** Aliases are tools that get exposed to the user as shell aliases */ - aliases = new Map(); + for await (const [pathVariable, locations] of this.paths) { + if (locations.size) { + const originalVariable = linq.find(originalEnvironment, pathVariable) || ''; + if (originalVariable) { + for (const p of originalVariable.split(delimiter)) { + if (p) { + locations.add(p); + } + } + } + // compose the final value + env[pathVariable] = linq.join(locations, delimiter); - /** a collection of 'published locations' from artifacts. useful for msbuild */ - locations = new Map(); + // set the undo data + undo[pathVariable] = originalVariable || ''; + } + } - /** a collection of environment variables from artifacts that are intended to be combinined into variables that have PATH delimiters */ - paths = new Map>(); + // combine environment variables with multiple values with spaces (uses: CFLAGS, etc) + for await (const [variable, values] of this.environmentVariables) { + env[variable] = linq.join(values, ' '); + undo[variable] = originalEnvironment[variable] || ''; + } - /** environment variables from artifacts */ - environment = new Map>(); + // .tools get defined as environment variables too. + for await (const [variable, value] of this.tools) { + env[variable] = value; + undo[variable] = originalEnvironment[variable] || ''; + } - /** a collection of arbitrary properties from artifacts. useful for msbuild */ - properties = new Map>(); + // .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'] || ''; + } + } - get Paths() { - // return just paths that have contents. - return [... this.paths.entries()].filter(([k, v]) => v.length > 0); + return [env, undo]; } - get Variables() { - // tools + environment - const result = new Array<[string, string]>(); - - // combine variables with spaces - for (const [key, values] of this.environment) { - result.push([key, values.join(' ')]); + async activate(artifacts: Iterable, currentEnvironment: Record, shellScriptFile: Uri | undefined, undoEnvironmentFile: Uri | undefined, msbuildFile: Uri | undefined) { + let undoDeactivation = ''; + const scriptKind = extname(shellScriptFile?.fsPath || ''); + + // load previous activation undo data + const previous = currentEnvironment[undoVariableName]; + 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 || {}); + delete currentEnvironment[undoVariableName]; + undoDeactivation = generateScriptContent(scriptKind, deactivatationData.environment || {}, deactivatationData.aliases || {}); + } } - // add tools to the list - for (const [key, value] of this.tools) { - result.push([key, value]); + const [variables, undo] = await this.generateEnvironmentVariables(currentEnvironment); + + const aliases = (await toArrayAsync(this.aliases)).reduce((aliases, [name, alias]) => { aliases[name] = alias; return aliases; }, >{}); + + // generate undo file if requested + if (undoEnvironmentFile) { + const undoContents = { + environment: undo, + aliases: linq.keys(aliases).select(each => <[string, string]>[each, '']).toObject(each => each) + }; + + // make a note of the location + variables[undoVariableName] = undoEnvironmentFile.fsPath; + + // create the file on disk + await undoEnvironmentFile.writeUTF8(JSON.stringify(undoContents, (k, v) => this.#session.serializer(k, v), 2)); } - return result; - } - get Defines(): Array<[string, string]> { - return linq.entries(this.defines).toArray(); - } + // generate shell script if requested + if (shellScriptFile) { + await shellScriptFile.writeUTF8(undoDeactivation + generateScriptContent(scriptKind, variables, aliases)); + } - get Locations(): Array<[string, string]> { - return linq.entries(this.locations).select(([k, v]) => <[string, string]>[k, v.fsPath]).where(([k, v]) => v.length > 0).toArray(); + // generate msbuild props file if requested + if (msbuildFile) { + await msbuildFile.writeUTF8(await this.generateMSBuild(artifacts)); + } } - get Properties(): Array<[string, Array]> { - return linq.entries(this.properties).toArray(); - } /** produces an environment block that can be passed to child processes to leverage dependent artifacts during installtion/activation. */ - get environmentBlock(): NodeJS.ProcessEnv { - const result = this.#session.environment; + async getEnvironmentBlock(): Promise { + const result = { ... this.#session.environment }; // add environment variables - for (const [k, v] of this.Variables) { - result[k] = v; + for await (const [k, v] of this.environmentVariables) { + result[k] = linq.join(v, ' '); } - // update environment paths - for (const [variable, values] of this.Paths) { - if (values.length) { - const s = new Set(values.map(each => each.fsPath)); - const originalVariable = result[variable] || ''; + // update environment path variables + for await (const [pathVariable, locations] of this.paths) { + if (locations.size) { + const originalVariable = linq.find(result, pathVariable) || ''; if (originalVariable) { for (const p of originalVariable.split(delimiter)) { if (p) { - s.add(p); + locations.add(p); } } } - result[variable] = originalVariable; + result[pathVariable] = linq.join(locations, delimiter); } } // define tool environment variables - for (const [key, value] of this.tools) { - result[key] = value; + for await (const [toolName, toolLocation] of this.tools) { + result[toolName] = toolLocation; } return result; } +} + +function generateCmdScript(variables: Record, aliases: Record): string { + return linq.entries(variables).select(([k, v]) => { return v ? `set ${k}=${v}` : `set ${k}=`; }).join('\r\n') + + '\r\n' + + linq.entries(aliases).select(([k, v]) => { return v ? `doskey ${k}=${v} $*` : `doskey ${k}=`; }).join('\r\n') + + '\r\n'; +} + +function generatePowerShellScript(variables: Record, aliases: Record): string { + return linq.entries(variables).select(([k, v]) => { return v ? `$\{ENV:${k}}="${v}"` : `$\{ENV:${k}}=$null`; }).join('\n') + + '\n' + + linq.entries(aliases).select(([k, v]) => { return v ? `function global:${k} { & ${v} @args }` : `remove-item -ea 0 "function:${k}"`; }).join('\n') + + '\n'; +} + +function generatePosixScript(variables: Record, aliases: Record): string { + return linq.entries(variables).select(([k, v]) => { return v ? `export ${k}="${v}"` : `unset ${k[0]}`; }).join('\n') + + '\n' + + linq.entries(aliases).select(([k, v]) => { return v ? `${k}() {\n ${v} $* \n}` : `unset -f ${v} > /dev/null 2>&1`; }).join('\n') + + '\n'; +} + +function generateScriptContent(kind: string, variables: Record, aliases: Record) { + switch (kind) { + case '.ps1': + return generatePowerShellScript(variables, aliases); + case '.cmd': + return generateCmdScript(variables, aliases); + case '.sh': + return generatePosixScript(variables, aliases); + } + return ''; +} + + +export async function deactivate(shellScriptFile: Uri, variables: Record, aliases: Record) { + const kind = extname(shellScriptFile.fsPath); + await shellScriptFile.writeUTF8(generateScriptContent(kind, variables, aliases)); +} + +function undoActivation(currentEnvironment: Record, variables: Record) { + const result = { ...currentEnvironment }; + for (const [key, value] of linq.entries(variables)) { + if (value) { + result[key] = value; + } else { + delete result[key]; + } + } + return result; +} + +async function toArrayAsync(iterable: AsyncIterable) { + const result = []; + for await (const item of iterable) { + result.push(item); + } + return result; } \ No newline at end of file diff --git a/ce/artifacts/artifact.ts b/ce/artifacts/artifact.ts index e493f68..31ef131 100644 --- a/ce/artifacts/artifact.ts +++ b/ce/artifacts/artifact.ts @@ -1,17 +1,20 @@ +/* eslint-disable prefer-const */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { fail } from 'assert'; -import { resolve } from 'path'; +import { match } from 'micromatch'; +import { delimiter, resolve } from 'path'; import { MetadataFile } from '../amf/metadata-file'; -import { gitArtifact, gitUniqueIdPrefix, latestVersion } from '../constants'; +import { gitArtifact, latestVersion } from '../constants'; +import { FileType } from '../fs/filesystem'; import { i } from '../i18n'; +import { activateEspIdf, installEspIdf } from '../installers/espidf'; import { InstallEvents } from '../interfaces/events'; import { Registries } from '../registries/registries'; import { Session } from '../session'; import { linq } from '../util/linq'; import { Uri } from '../util/uri'; -import { Activation } from './activation'; import { Registry } from './registry'; import { SetOfDemands } from './SetOfDemands'; @@ -81,8 +84,10 @@ class ArtifactBase { } } + // check if system/git is already requested + if (!linq.entries(artifacts).first(([id, [artifact, name, version]]) => artifact.registryId === 'microsoft' && artifact.shortName === 'system/git')) { - if (!linq.startsWith(artifacts, gitUniqueIdPrefix)) { + // nope, should it be? // check if anyone needs git and add it if it isn't there for (const each of this.applicableDemands.installer) { if (each.installerKind === 'git') { @@ -100,6 +105,7 @@ class ArtifactBase { export class Artifact extends ArtifactBase { isPrimary = false; + allPaths: Array = []; constructor(session: Session, metadata: MetadataFile, public shortName: string = '', public targetLocation: Uri, public readonly registryId: string, public readonly registryUri: Uri) { super(session, metadata); @@ -125,35 +131,25 @@ export class Artifact extends ArtifactBase { return `${this.registryUri.toString()}::${this.id}::${this.version}`; } - async install(activation: Activation, events: Partial, options: { force?: boolean, allLanguages?: boolean, language?: string }): Promise { + async install(events: Partial, options: { force?: boolean, allLanguages?: boolean, language?: string }): Promise { let installing = false; try { // is it installed? const applicableDemands = this.applicableDemands; - applicableDemands.setActivation(activation); - let isFailing = false; - for (const error of applicableDemands.errors) { - this.session.channels.error(error); - isFailing = true; - } - - if (isFailing) { - throw Error('errors present'); - } + this.session.channels.error(applicableDemands.errors, this); - // warnings - for (const warning of applicableDemands.warnings) { - this.session.channels.warning(warning); + if (applicableDemands.errors.length) { + throw Error('Error message from Artifact'); } - // messages - for (const message of applicableDemands.messages) { - this.session.channels.message(message); - } + this.session.channels.warning(applicableDemands.warnings, this); + this.session.channels.message(applicableDemands.messages, this); if (await this.isInstalled && !options.force) { - await this.loadActivationSettings(activation); + if (!await this.loadActivationSettings(events)) { + throw new Error(i`Failed during artifact activation`); + } return false; } installing = true; @@ -176,12 +172,14 @@ export class Artifact extends ArtifactBase { if (!installer) { fail(i`Unknown installer type ${installInfo!.installerKind}`); } - await installer(this.session, activation, this.id, this.targetLocation, installInfo, events, options); + await installer(this.session, this.id, this.targetLocation, installInfo, events, options); } // after we unpack it, write out the installed manifest await this.writeManifest(); - await this.loadActivationSettings(activation); + if (!await this.loadActivationSettings(events)) { + throw new Error(i`Failed during artifact activation`); + } return true; } catch (err) { if (installing) { @@ -209,109 +207,67 @@ export class Artifact extends ArtifactBase { await this.targetLocation.delete({ recursive: true, useTrash: false }); } - async loadActivationSettings(activation: Activation) { - // construct paths (bin, lib, include, etc.) - // construct tools - // compose variables - // defines - - const l = this.targetLocation.toString().length + 1; - const allPaths = (await this.targetLocation.readDirectory(undefined, { recursive: true })).select(([name, stat]) => name.toString().substr(l)); - - for (const settingBlock of this.applicableDemands.settings) { - // **** defines **** - // eslint-disable-next-line prefer-const - for (let [key, value] of settingBlock.defines) { - if (value === 'true') { - value = '1'; - } - - const v = activation.defines.get(key); - if (v && v !== value) { - // conflict. todo: what do we want to do? - this.session.channels.warning(i`Duplicate define ${key} during activation. New value will replace old `); - } - activation.defines.set(key, value); - } - - // **** paths **** - for (const key of settingBlock.paths.keys) { - if (!key) { - continue; - } + matchFilesInArtifact(glob: string) { + const results = match(this.allPaths, glob.trim(), { dot: true, cwd: this.targetLocation.fsPath, unescape: true }); + if (results.length === 0) { + this.session.channels.warning(i`Unable to resolve '${glob}' to files in the artifact folder`, this); + return []; + } + return results; + } - const pathEnvVariable = key.toUpperCase(); - const p = activation.paths.getOrDefault(pathEnvVariable, []); - const l = settingBlock.paths.get(key); - const uris = new Set(); - - for (const location of l ?? []) { - // check that each path is an actual path. - const path = await this.sanitizeAndValidatePath(location); - if (path && !uris.has(path)) { - uris.add(path); - p.push(path); - } - } + resolveBraces(text: string, mustBeSingle = false) { + return text.replace(/\{(.*?)\}/g, (m, e) => { + const results = this.matchFilesInArtifact(e); + if (mustBeSingle && results.length > 1) { + this.session.channels.warning(i`Glob ${m} resolved to multiple locations. Using first location.`, this); + return results[0]; } + return results.join(delimiter); + }); + } - // **** tools **** - for (const key of settingBlock.tools.keys) { - const envVariable = key.toUpperCase(); + resolveBracesAndSplit(text: string): Array { + return this.resolveBraces(text).split(delimiter); + } - if (activation.tools.has(envVariable)) { - this.session.channels.error(i`Duplicate tool declared ${key} during activation. Probably not a good thing?`); - } + isGlob(path: string) { + return path.indexOf('*') !== -1 || path.indexOf('?') !== -1; + } - const p = settingBlock.tools.get(key) || ''; - const uri = await this.sanitizeAndValidatePath(p); - if (uri) { - activation.tools.set(envVariable, uri.fsPath); - } else { - if (p) { - activation.tools.set(envVariable, p); - // this.session.channels.warning(i`Invalid tool path '${p}'`); - } - } - } + async loadActivationSettings(events: Partial) { + // construct paths (bin, lib, include, etc.) + // construct tools + // compose variables + // defines - // **** variables **** - for (const [key, value] of settingBlock.variables) { - const envKey = activation.environment.getOrDefault(key, []); - envKey.push(...value); - } + // record all the files in the artifact + this.allPaths = (await this.targetLocation.readDirectory(undefined, { recursive: true })).select(([name, stat]) => stat === FileType.Directory ? name.fsPath + '/' : name.fsPath); + for (const exportsBlock of this.applicableDemands.exports) { + this.session.activation.addExports(exportsBlock, this.targetLocation); + } - // **** properties **** - for (const [key, value] of settingBlock.properties) { - const envKey = activation.properties.getOrDefault(key, []); - envKey.push(...value); + // if espressif install + if (this.metadata.info.flags.has('espidf')) { + // check for some file that espressif installs to see if it's installed. + if (!await this.targetLocation.exists('.espressif')) { + await installEspIdf(this.session, events, this.targetLocation); } - // **** locations **** - for (const locationName of settingBlock.locations.keys) { - if (activation.locations.has(locationName)) { - this.session.channels.error(i`Duplicate location declared ${locationName} during activation. Probably not a good thing?`); - } - - const p = settingBlock.locations.get(locationName) || ''; - const uri = await this.sanitizeAndValidatePath(p); - if (uri) { - activation.locations.set(locationName, uri); - } - } + // activate + return await activateEspIdf(this.session, this.targetLocation); } + return true; } async sanitizeAndValidatePath(path: string) { - if (!path.startsWith('.')) { - try { - const loc = this.session.fileSystem.file(resolve(path)); - if (await loc.exists()) { - return loc; - } - } catch { - // no worries, treat it like a relative path. + try { + const loc = this.session.fileSystem.file(resolve(this.targetLocation.fsPath, path)); + if (await loc.exists()) { + return loc; } + } catch { + // no worries, treat it like a relative path. } const loc = this.targetLocation.join(sanitizePath(path)); if (await loc.exists()) { @@ -353,6 +309,6 @@ export class ProjectManifest extends ArtifactBase { export class InstalledArtifact extends Artifact { constructor(session: Session, metadata: MetadataFile) { - super(session, metadata, '', Uri.invalid, 'OnDisk?', Uri.invalid); /* fixme ? */ + super(session, metadata, '', Uri.invalid, 'OnDisk?', Uri.invalid); } } \ No newline at end of file diff --git a/ce/cli/artifacts.ts b/ce/cli/artifacts.ts index 08782b7..c9f6dd8 100644 --- a/ce/cli/artifacts.ts +++ b/ce/cli/artifacts.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { MultiBar, SingleBar } from 'cli-progress'; -import { Activation } from '../artifacts/activation'; import { Artifact, ArtifactMap } from '../artifacts/artifact'; import { i } from '../i18n'; import { trackAcquire } from '../insights'; @@ -26,6 +25,7 @@ export async function showArtifacts(artifacts: Iterable, options?: { f table.push(name, artifact.version, options?.force || await artifact.isInstalled ? 'installed' : 'will install', artifact.isPrimary ? ' ' : '*', artifact.metadata.info.summary || ''); } log(table.toString()); + log(); return !failing; } @@ -40,6 +40,15 @@ export async function selectArtifacts(selections: Selections, registries: Regist if (!artifact) { error(`Unable to resolve artifact: ${artifactReference('', identity, version)}`); + + const results = await registries.search({ keyword: identity, version: version }); + if (results.length) { + log('\nPossible matches:'); + for (const [reg, key, arts] of results) { + log(` ${artifactReference(registries.getRegistryName(reg), key, '')}`); + } + } + return false; } @@ -50,10 +59,9 @@ export async function selectArtifacts(selections: Selections, registries: Regist return artifacts; } -export async function installArtifacts(session: Session, artifacts: Iterable, options?: { force?: boolean, allLanguages?: boolean, language?: string }): Promise<[boolean, Map, Activation]> { +export async function installArtifacts(session: Session, artifacts: Iterable, options?: { force?: boolean, allLanguages?: boolean, language?: string }): Promise<[boolean, Map]> { // resolve the full set of artifacts to install. const installed = new Map(); - const activation = new Activation(session); const bar = new MultiBar({ clearOnComplete: true, hideCursor: true, format: '{name} {bar}\u25A0 {percentage}% {action} {current}', @@ -70,7 +78,7 @@ export async function installArtifacts(session: Session, artifacts: Iterable { if (percent >= 100) { p?.update(percent); @@ -134,20 +142,10 @@ export async function installArtifacts(session: Session, artifacts: Iterable) { - const activation = new Activation(session); - for (const artifact of artifacts) { - if (await artifact.isInstalled) { - await artifact.loadActivationSettings(activation); - } - } - return activation; + return [true, installed]; } diff --git a/ce/cli/commands/activate.ts b/ce/cli/commands/activate.ts index df2d8f7..4300ba4 100644 --- a/ce/cli/commands/activate.ts +++ b/ce/cli/commands/activate.ts @@ -4,9 +4,8 @@ import { i } from '../../i18n'; import { session } from '../../main'; import { Command } from '../command'; -import { projectFile } from '../format'; import { activateProject } from '../project'; -import { debug, error } from '../styling'; +import { error } from '../styling'; import { MSBuildProps } from '../switches/msbuild-props'; import { Project } from '../switches/project'; import { WhatIf } from '../switches/whatIf'; @@ -38,9 +37,6 @@ export class ActivateCommand extends Command { return false; } - debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); - await session.deactivate(); - return await activateProject(projectManifest, { force: this.commandLine.force, allLanguages: this.commandLine.allLanguages, diff --git a/ce/cli/commands/add.ts b/ce/cli/commands/add.ts index 986c12d..e219861 100644 --- a/ce/cli/commands/add.ts +++ b/ce/cli/commands/add.ts @@ -5,9 +5,9 @@ import { i } from '../../i18n'; import { session } from '../../main'; import { selectArtifacts } from '../artifacts'; import { Command } from '../command'; -import { cmdSwitch, projectFile } from '../format'; +import { cmdSwitch } from '../format'; import { activateProject } from '../project'; -import { debug, error } from '../styling'; +import { error } from '../styling'; import { Project } from '../switches/project'; import { Registry } from '../switches/registry'; import { Version } from '../switches/version'; @@ -82,9 +82,6 @@ export class AddCommand extends Command { // write the file out. await projectManifest.metadata.save(); - debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); - await session.deactivate(); - return await activateProject(projectManifest, this.commandLine); } } \ No newline at end of file diff --git a/ce/cli/commands/find.ts b/ce/cli/commands/find.ts index 5f1b42f..649b91e 100644 --- a/ce/cli/commands/find.ts +++ b/ce/cli/commands/find.ts @@ -42,7 +42,7 @@ export class FindCommand extends Command { const table = new Table('Artifact', 'Version', 'Summary'); for (const each of this.inputs) { - for (const [registry, id, artifacts] of await registries.search({ idOrShortName: each, version: this.version.value })) { + for (const [registry, id, artifacts] of await registries.search({ keyword: each, version: this.version.value })) { const latest = artifacts[0]; if (!latest.metadata.info.dependencyOnly) { const name = artifactIdentity(latest.registryId, id, latest.shortName); diff --git a/ce/cli/commands/remove.ts b/ce/cli/commands/remove.ts index 328f41b..b1b32fd 100644 --- a/ce/cli/commands/remove.ts +++ b/ce/cli/commands/remove.ts @@ -4,9 +4,8 @@ import { i } from '../../i18n'; import { session } from '../../main'; import { Command } from '../command'; -import { projectFile } from '../format'; import { activateProject } from '../project'; -import { debug, error, log } from '../styling'; +import { error, log } from '../styling'; import { Project } from '../switches/project'; import { WhatIf } from '../switches/whatIf'; @@ -56,9 +55,6 @@ export class RemoveCommand extends Command { // write the file out. await projectManifest.metadata.save(); - debug(`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); - await session.deactivate(); - return await activateProject(projectManifest, this.commandLine); } } \ No newline at end of file diff --git a/ce/cli/commands/use.ts b/ce/cli/commands/use.ts index 4b8bdad..1645faa 100644 --- a/ce/cli/commands/use.ts +++ b/ce/cli/commands/use.ts @@ -4,9 +4,10 @@ import { i } from '../../i18n'; import { session } from '../../main'; import { Registries } from '../../registries/registries'; -import { installArtifacts, selectArtifacts, showArtifacts } from '../artifacts'; +import { selectArtifacts, showArtifacts } from '../artifacts'; import { Command } from '../command'; import { cmdSwitch } from '../format'; +import { activate } from '../project'; import { error, log, warning } from '../styling'; import { MSBuildProps } from '../switches/msbuild-props'; import { Project } from '../switches/project'; @@ -63,14 +64,9 @@ export class UseCommand extends Command { return false; } - const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages }); + const success = await activate(artifacts, false, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages }); if (success) { log(i`Activating individual artifacts`); - await session.setActivationInPostscript(activation, false); - const propsFile = this.msbuildProps.value; - if (propsFile) { - await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys())); - } } else { return false; } diff --git a/ce/cli/format.ts b/ce/cli/format.ts index 720914b..b328254 100644 --- a/ce/cli/format.ts +++ b/ce/cli/format.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { bold, cyan, gray, green, greenBright, grey, underline, whiteBright, yellowBright } from 'chalk'; +import { bold, cyan, gray, green, greenBright, grey, underline, whiteBright, yellow, yellowBright } from 'chalk'; import { Uri } from '../util/uri'; export function projectFile(uri: Uri): string { @@ -10,13 +10,13 @@ export function projectFile(uri: Uri): string { export function artifactIdentity(registryName: string, identity: string, alias?: string) { if (alias) { - return `${registryName}:${identity.substr(0, identity.length - alias.length)}${yellowBright(alias)}`; + return `${whiteBright(registryName)}:${yellow.dim(identity.substr(0, identity.length - alias.length))}${yellowBright(alias)}`; } - return yellowBright(identity); + return registryName ? `${whiteBright(registryName)}:${yellowBright(identity)}` : yellowBright(identity); } export function artifactReference(registryName: string, identity: string, version: string) { - return `${artifactIdentity(registryName, identity)}-v${gray(version)}`; + return version && version !== '*' ? `${artifactIdentity(registryName, identity)}-v${gray(version)}` : artifactIdentity(registryName, identity); } export function heading(text: string, level = 1) { diff --git a/ce/cli/markdown-table.ts b/ce/cli/markdown-table.ts index 5359dec..b89f380 100644 --- a/ce/cli/markdown-table.ts +++ b/ce/cli/markdown-table.ts @@ -2,6 +2,36 @@ // Licensed under the MIT License. import { strict } from 'assert'; +import { blue, gray, green, white } from 'chalk'; +import * as renderer from 'marked-terminal'; + + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const marked = require('marked'); + +// setup markdown renderer +marked.setOptions({ + renderer: new renderer({ + tab: 2, + emoji: true, + showSectionPrefix: false, + firstHeading: green.underline.bold, + heading: green.underline, + codespan: white.bold, + link: blue.bold, + href: blue.bold.underline, + code: gray, + tableOptions: { + chars: { + 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '' + , 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '' + , 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '' + , 'right': '', 'right-mid': '', 'middle': '' + } + } + }), + gfm: true, +}); export class Table { private readonly rows = new Array(); @@ -16,6 +46,6 @@ export class Table { this.rows.push(`|${values.join('|')}|`); } toString() { - return this.rows.join('\n'); + return marked.marked(this.rows.join('\n')); } } \ No newline at end of file diff --git a/ce/cli/project.ts b/ce/cli/project.ts index 0ad6b5b..a9d52d1 100644 --- a/ce/cli/project.ts +++ b/ce/cli/project.ts @@ -23,19 +23,13 @@ export async function openProject(location: Uri): Promise { return new ProjectManifest(session, await session.openManifest(location)); } -export async function activate(artifacts: ArtifactMap, options?: ActivationOptions) { +export async function activate(artifacts: ArtifactMap, createUndoFile: boolean, options?: ActivationOptions) { // install the items in the project - const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, options); + const [success, artifactStatus] = await installArtifacts(session, artifacts.artifacts, options); if (success) { - // create an MSBuild props file if indicated by the user - const propsFile = options?.msbuildProps; - if (propsFile) { - await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys())); - } - - // activate all the tools in the project - await session.setActivationInPostscript(activation); + const backupFile = createUndoFile ? session.tmpFolder.join(`previous-environment-${Date.now().toFixed()}.json`) : undefined; + await session.activation.activate(artifacts.artifacts, session.environment, session.postscriptFile, backupFile, options?.msbuildProps); } return success; @@ -51,7 +45,7 @@ export async function activateProject(project: ProjectManifest, options?: Activa return false; } - if (await activate(artifacts, options)) { + if (await activate(artifacts, true, options)) { trackActivation(); log(blank); log(i`Project ${projectFile(project.metadata.context.folder)} activated`); diff --git a/ce/cli/styling.ts b/ce/cli/styling.ts index 8b9676b..9a0130c 100644 --- a/ce/cli/styling.ts +++ b/ce/cli/styling.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { blue, cyan, gray, green, red, white, yellow } from 'chalk'; -import * as renderer from 'marked-terminal'; +import { cyan, green, red, yellow } from 'chalk'; import { argv } from 'process'; +import { Artifact } from '../artifacts/artifact'; import { Session } from '../session'; import { CommandLine } from './command-line'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const marked = require('marked'); +import { artifactIdentity } from './format'; function formatTime(t: number) { return ( @@ -17,30 +15,6 @@ function formatTime(t: number) { [Math.floor(t / 86400000), Math.floor(t / 3600000) % 24, Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000]).map(each => each.toString().padStart(2, '0')).join(':').replace(/(.*):(\d)/, '$1.$2'); } -// setup markdown renderer -marked.setOptions({ - renderer: new renderer({ - tab: 2, - emoji: true, - showSectionPrefix: false, - firstHeading: green.underline.bold, - heading: green.underline, - codespan: white.bold, - link: blue.bold, - href: blue.bold.underline, - code: gray, - tableOptions: { - chars: { - 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '' - , 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '' - , 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '' - , 'right': '', 'right-mid': '', 'middle': '' - } - } - }), - gfm: true, -}); - export function indent(text: string): string export function indent(text: Array): Array export function indent(text: string | Array): string | Array { @@ -50,9 +24,9 @@ export function indent(text: string | Array): string | Array { return ` ${text}`; } -function md(text = '', session?: Session): string { +function reformatText(text = '', session?: Session): string { if (text) { - text = marked.marked(`${text}`.replace(/\\\./g, '\\\\.')); // work around md messing up paths with .\ in them. + text = `${text}`.replace(/\\\./g, '\\\\.'); // rewrite file:// urls to be locl filesystem urls. return (!!text && !!session) ? text.replace(/(file:\/\/\S*)/g, (s, a) => yellow.dim(session.parseUri(a).fsPath)) : text; @@ -81,24 +55,40 @@ export function writeException(e: any) { } export function initStyling(commandline: CommandLine, session: Session) { - log = (text) => stdout((md(text, session).trim())); - error = (text) => stdout(`${red.bold('ERROR: ')}${md(text, session).trim()}`); - warning = (text) => stdout(`${yellow.bold('WARNING: ')}${md(text, session).trim()}`); - debug = (text) => { if (commandline.debug) { stdout(`${cyan.bold('DEBUG: ')}${md(text, session).trim()}`); } }; + log = (text) => stdout((reformatText(text, session).trim())); + error = (text) => stdout(`${red.bold('\nERROR: ')}${reformatText(text, session).trim()}`); + warning = (text) => stdout(`${yellow.bold('\nWARNING: ')}${reformatText(text, session).trim()}`); + debug = (text) => { if (commandline.debug) { stdout(`${cyan.bold('DEBUG: ')}${reformatText(text, session).trim()}`); } }; session.channels.on('message', (text: string, context: any, msec: number) => { - log(text); + if (context && context instanceof Artifact) { + log(`${green.bold('NOTE: ')}[${artifactIdentity(context.registryId, context.name)}] - ${text}`); + } else { + log(text); + } }); session.channels.on('error', (text: string, context: any, msec: number) => { - error(text); + if (context && context instanceof Artifact) { + error(`[${artifactIdentity(context.registryId, context.name)}] - ${text}`); + } else { + error(text); + } }); session.channels.on('debug', (text: string, context: any, msec: number) => { - debug(`${cyan.bold(`[${formatTime(msec)}]`)} ${md(text, session)}`); + if (context && context instanceof Artifact) { + debug(`[${artifactIdentity(context.registryId, context.name)}] - ${text}`); + } else { + debug(`${cyan.bold(`[${formatTime(msec)}]`)} ${reformatText(text, session)}`); + } }); session.channels.on('warning', (text: string, context: any, msec: number) => { - warning(text); + if (context && context instanceof Artifact) { + warning(`[${artifactIdentity(context.registryId, context.name)}] - ${text}`); + } else { + warning(text); + } }); } diff --git a/ce/constants.ts b/ce/constants.ts index b9b6511..3842d0d 100644 --- a/ce/constants.ts +++ b/ce/constants.ts @@ -5,8 +5,7 @@ export const project = 'environment.yaml'; export const undo = 'Z_VCPKG_UNDO'; export const postscriptVarible = 'Z_VCPKG_POSTSCRIPT'; export const blank = '\n'; -export const gitUniqueIdPrefix = 'https://aka.ms/vcpkg-ce-default::tools/git::'; -export const gitArtifact = 'microsoft:tools/git'; +export const gitArtifact = 'microsoft:system/git'; export const latestVersion = '*'; export const vcpkgDownloadFolder = 'VCPKG_DOWNLOADS'; export const globalConfigurationFile = 'vcpkg-configuration.global.json'; diff --git a/ce/fs/unified-filesystem.ts b/ce/fs/unified-filesystem.ts index c6b7257..b920dd8 100644 --- a/ce/fs/unified-filesystem.ts +++ b/ce/fs/unified-filesystem.ts @@ -4,7 +4,7 @@ import { strict } from 'assert'; import { Readable, Writable } from 'stream'; import { i } from '../i18n'; -import { Dictionary } from '../util/linq'; +import { Record } from '../util/linq'; import { Uri } from '../util/uri'; import { FileStat, FileSystem, FileType, ReadHandle, WriteStreamOptions } from './filesystem'; @@ -20,7 +20,7 @@ export function schemeOf(uri: string) { export class UnifiedFileSystem extends FileSystem { - private filesystems: Dictionary = {}; + private filesystems = new Record(); /** registers a scheme to a given filesystem * diff --git a/ce/installers/espidf.ts b/ce/installers/espidf.ts new file mode 100644 index 0000000..12b4c5f --- /dev/null +++ b/ce/installers/espidf.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { delimiter } from 'path'; +import { log } from '../cli/styling'; +import { i } from '../i18n'; +import { InstallEvents } from '../interfaces/events'; +import { Session } from '../session'; +import { execute } from '../util/exec-cmd'; +import { isFilePath, Uri } from '../util/uri'; + +export async function installEspIdf(session: Session, events: Partial, targetLocation: Uri) { + + // create the .espressif folder for the espressif installation + await targetLocation.createDirectory('.espressif'); + session.activation.addTool('IDF_TOOLS_PATH', targetLocation.join('.espressif').fsPath); + + const pythonPath = await session.activation.getAlias('python'); + if (!pythonPath) { + throw new Error(i`Python is not installed`); + } + + const directoryLocation = await isFilePath(targetLocation) ? targetLocation.fsPath : targetLocation.toString(); + + const extendedEnvironment: NodeJS.ProcessEnv = { + ... await session.activation.getEnvironmentBlock(), + IDF_PATH: directoryLocation, + IDF_TOOLS_PATH: `${directoryLocation}/.espressif` + }; + + const installResult = await execute(pythonPath, [ + `${directoryLocation}/tools/idf_tools.py`, + 'install', + '--targets=all' + ], { + env: extendedEnvironment, + onStdOutData: (chunk) => { + const regex = /\s(100)%/; + chunk.toString().split('\n').forEach((line: string) => { + const match_array = line.match(regex); + if (match_array !== null) { + events.heartbeat?.('Installing espidf'); + } + }); + } + }); + + if (installResult.code) { + return false; + } + + const installPythonEnv = await execute(pythonPath, [ + `${directoryLocation}/tools/idf_tools.py`, + 'install-python-env' + ], { + env: extendedEnvironment + }); + + if (installPythonEnv.code) { + return false; + } + + // call activate, extrapolate what environment is changed + // change it in the session object. + + log('installing espidf commands post-git is implemented, but post activation of the necessary esp-idf path / environment variables is not.'); + return true; +} + +export async function activateEspIdf(session: Session, targetLocation: Uri) { + const pythonPath = await session.activation.getAlias('python'); + if (!pythonPath) { + throw new Error(i`Python is not installed`); + } + + const directoryLocation = await isFilePath(targetLocation) ? targetLocation.fsPath : targetLocation.toString(); + + const activateIdf = await execute(pythonPath, [ + `${directoryLocation}/tools/idf_tools.py`, + 'export', + '--format', + 'key-value' + ], { + env: await session.activation.getEnvironmentBlock(), + onStdOutData: (chunk) => { + chunk.toString().split('\n').forEach((line: string) => { + const splitLine = line.split('='); + if (splitLine[0]) { + if (splitLine[0] !== 'PATH') { + session.activation.addEnvironmentVariable(splitLine[0].trim(), [splitLine[1].trim()]); + } + else { + const pathValues = splitLine[1].split(delimiter); + for (const path of pathValues) { + if (path.trim() !== '%PATH%' && path.trim() !== '$PATH') { + session.activation.addPath(splitLine[0].trim(), session.fileSystem.file(path)); + } + } + } + } + }); + } + }); + + if (activateIdf.code) { + throw new Error(`Failed to activate esp-idf - ${activateIdf.stderr}`); + } + + return true; +} \ No newline at end of file diff --git a/ce/installers/git.ts b/ce/installers/git.ts index f9c3f7f..37588d0 100644 --- a/ce/installers/git.ts +++ b/ce/installers/git.ts @@ -2,16 +2,14 @@ // Licensed under the MIT License. import { CloneOptions, Git } from '../archivers/git'; -import { Activation } from '../artifacts/activation'; import { i } from '../i18n'; import { InstallEvents, InstallOptions } from '../interfaces/events'; import { CloneSettings, GitInstaller } from '../interfaces/metadata/installers/git'; import { Session } from '../session'; -import { linq } from '../util/linq'; import { Uri } from '../util/uri'; -export async function installGit(session: Session, activation: Activation, name: string, targetLocation: Uri, install: GitInstaller, events: Partial, options: Partial): Promise { - const gitPath = linq.find(activation.tools, 'git'); +export async function installGit(session: Session, name: string, targetLocation: Uri, install: GitInstaller, events: Partial, options: Partial): Promise { + const gitPath = await session.activation.getAlias('git'); if (!gitPath) { throw new Error(i`Git is not installed`); @@ -20,7 +18,7 @@ export async function installGit(session: Session, activation: Activation, name: const repo = session.parseUri(install.location); const targetDirectory = targetLocation.join(options.subdirectory ?? ''); - const gitTool = new Git(session, gitPath, activation.environmentBlock, targetDirectory); + const gitTool = new Git(session, gitPath, await session.activation.getEnvironmentBlock(), targetDirectory); await gitTool.clone(repo, events, { recursive: install.recurse, diff --git a/ce/installers/nuget.ts b/ce/installers/nuget.ts index da4c712..2936b37 100644 --- a/ce/installers/nuget.ts +++ b/ce/installers/nuget.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { ZipUnpacker } from '../archivers/ZipUnpacker'; -import { Activation } from '../artifacts/activation'; import { acquireNugetFile } from '../fs/acquire'; import { InstallEvents, InstallOptions } from '../interfaces/events'; import { NupkgInstaller } from '../interfaces/metadata/installers/nupkg'; @@ -10,7 +9,7 @@ import { Session } from '../session'; import { Uri } from '../util/uri'; import { applyAcquireOptions } from './util'; -export async function installNuGet(session: Session, activation: Activation, name: string, targetLocation: Uri, install: NupkgInstaller, events: Partial, options: Partial): Promise { +export async function installNuGet(session: Session, name: string, targetLocation: Uri, install: NupkgInstaller, events: Partial, options: Partial): Promise { const file = await acquireNugetFile(session, install.location, `${name}.zip`, events, applyAcquireOptions(options, install)); return new ZipUnpacker(session).unpack( diff --git a/ce/installers/untar.ts b/ce/installers/untar.ts index 3224a69..e66650c 100644 --- a/ce/installers/untar.ts +++ b/ce/installers/untar.ts @@ -3,7 +3,6 @@ import { TarBzUnpacker, TarGzUnpacker, TarUnpacker } from '../archivers/tar'; import { Unpacker } from '../archivers/unpacker'; -import { Activation } from '../artifacts/activation'; import { acquireArtifactFile } from '../fs/acquire'; import { InstallEvents, InstallOptions } from '../interfaces/events'; import { UnTarInstaller } from '../interfaces/metadata/installers/tar'; @@ -12,7 +11,7 @@ import { Uri } from '../util/uri'; import { applyAcquireOptions, artifactFileName } from './util'; -export async function installUnTar(session: Session, activation: Activation, name: string, targetLocation: Uri, install: UnTarInstaller, events: Partial, options: Partial): Promise { +export async function installUnTar(session: Session, name: string, targetLocation: Uri, install: UnTarInstaller, events: Partial, options: Partial): Promise { const file = await acquireArtifactFile(session, [...install.location].map(each => session.parseUri(each)), artifactFileName(name, install, '.tar'), events, applyAcquireOptions(options, install)); const x = await file.readBlock(0, 128); let unpacker: Unpacker; diff --git a/ce/installers/unzip.ts b/ce/installers/unzip.ts index 4201f19..08bcf92 100644 --- a/ce/installers/unzip.ts +++ b/ce/installers/unzip.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { ZipUnpacker } from '../archivers/ZipUnpacker'; -import { Activation } from '../artifacts/activation'; import { acquireArtifactFile } from '../fs/acquire'; import { InstallEvents, InstallOptions } from '../interfaces/events'; import { UnZipInstaller } from '../interfaces/metadata/installers/zip'; @@ -10,7 +9,7 @@ import { Session } from '../session'; import { Uri } from '../util/uri'; import { applyAcquireOptions, artifactFileName } from './util'; -export async function installUnZip(session: Session, activation: Activation, name: string, targetLocation: Uri, install: UnZipInstaller, events: Partial, options: Partial): Promise { +export async function installUnZip(session: Session, name: string, targetLocation: Uri, install: UnZipInstaller, events: Partial, options: Partial): Promise { const file = await acquireArtifactFile(session, [...install.location].map(each => session.parseUri(each)), artifactFileName(name, install, '.zip'), events, applyAcquireOptions(options, install)); await new ZipUnpacker(session).unpack( file, diff --git a/ce/interfaces/error-kind.ts b/ce/interfaces/error-kind.ts index c8acaa9..4ab3d71 100644 --- a/ce/interfaces/error-kind.ts +++ b/ce/interfaces/error-kind.ts @@ -11,4 +11,6 @@ export enum ErrorKind { HostOnly = 'HostOnly', MissingHash = 'MissingHashValue', InvalidDefinition = 'InvalidDefinition', + InvalidChild = 'InvalidChild', + InvalidExpression = 'InvalidExpression', } diff --git a/ce/interfaces/metadata/alternative-fulfillment.ts b/ce/interfaces/metadata/alternative-fulfillment.ts index bfcc6ee..6342c7d 100644 --- a/ce/interfaces/metadata/alternative-fulfillment.ts +++ b/ce/interfaces/metadata/alternative-fulfillment.ts @@ -3,7 +3,7 @@ import { Strings } from '../collections'; import { Demands } from './demands'; -import { Settings } from './Settings'; +import { Exports } from './exports'; export interface AlternativeFulfillment extends Demands { @@ -23,5 +23,5 @@ export interface AlternativeFulfillment extends Demands { matches: string | undefined; /** settings that should be applied to the context when activated if this is a match */ - settings: Settings; + exports: Exports; } diff --git a/ce/interfaces/metadata/demands.ts b/ce/interfaces/metadata/demands.ts index 90621b8..8968aeb 100644 --- a/ce/interfaces/metadata/demands.ts +++ b/ce/interfaces/metadata/demands.ts @@ -2,13 +2,12 @@ // Licensed under the MIT License. -import { Activation } from '../../artifacts/activation'; import { Session } from '../../session'; import { Dictionary, Sequence } from '../collections'; import { Validation } from '../validation'; import { AlternativeFulfillment } from './alternative-fulfillment'; +import { Exports } from './exports'; import { Installer } from './installers/Installer'; -import { Settings } from './Settings'; import { VersionReference } from './version-reference'; /** @@ -35,7 +34,7 @@ export interface Demands extends Validation { seeAlso: Dictionary; /** settings that should be applied to the context when activated */ - settings: Settings; + exports: Exports; /** * defines what should be physically laid out on disk for this artifact @@ -51,7 +50,9 @@ export interface Demands extends Validation { /** a means to an alternative fulfillment */ unless: AlternativeFulfillment; + /** files that should be (non-overwrite) copied to the output project folder. */ + apply: Dictionary; + init(session: Session): Promise; - setActivation(activation: Activation): void; } diff --git a/ce/interfaces/metadata/Settings.ts b/ce/interfaces/metadata/exports.ts similarity index 80% rename from ce/interfaces/metadata/Settings.ts rename to ce/interfaces/metadata/exports.ts index afbfd8f..5833c67 100644 --- a/ce/interfaces/metadata/Settings.ts +++ b/ce/interfaces/metadata/exports.ts @@ -6,7 +6,7 @@ import { Validation } from '../validation'; /** settings that should be applied to the context */ -export interface Settings extends Validation { +export interface Exports extends Validation { /** a map of path categories to one or more values */ paths: Dictionary; @@ -18,7 +18,7 @@ export interface Settings extends Validation { * * arrays mean that the values should be joined with spaces */ - variables: Dictionary; + environment: Dictionary; /** a map of properties that are activation-type specific (ie, msbuild) */ properties: Dictionary; @@ -36,4 +36,10 @@ export interface Settings extends Validation { * it's significant enough that we need them separately */ defines: Dictionary; + + /** allows contents of the artifact to be exported in categories. */ + contents: Dictionary; + + /** shell aliases (aka functions/etc) for exposing specific commands */ + aliases: Dictionary; } diff --git a/ce/interfaces/metadata/metadata-format.ts b/ce/interfaces/metadata/metadata-format.ts index 9e7b1b8..4479d12 100644 --- a/ce/interfaces/metadata/metadata-format.ts +++ b/ce/interfaces/metadata/metadata-format.ts @@ -12,7 +12,7 @@ import { NugetRegistry } from './registries/nuget-registry'; * * Any other keys are considered HostQueries and a matching set of Demands * A HostQuery is a query string that can be used to qualify - * 'requires'/'see-also'/'settings'/'install'/'use' objects + * 'requires'/'see-also'/'exports'/'install'/'use' objects */ export type Profile = ProfileBase; diff --git a/ce/interfaces/metadata/profile-base.ts b/ce/interfaces/metadata/profile-base.ts index 6e4e6d4..92d2cdd 100644 --- a/ce/interfaces/metadata/profile-base.ts +++ b/ce/interfaces/metadata/profile-base.ts @@ -15,7 +15,7 @@ type Primitive = string | number | boolean; * * Any other keys are considered HostQueries and a matching set of Demands * A HostQuery is a query string that can be used to qualify - * 'requires'/'see-also'/'settings'/'install'/'use' objects + * 'requires'/'see-also'/'exports'/'install'/'use' objects * * @see the section below in this document entitled 'Host/Environment Queries" */ diff --git a/ce/main.ts b/ce/main.ts index 5e43509..6f61e78 100644 --- a/ce/main.ts +++ b/ce/main.ts @@ -135,7 +135,7 @@ async function main() { result = await command.run(); log(blank); - await session.writePostscript(); + /// await session.writePostscript(); } catch (e) { // in --debug mode we want to see the stack trace(s). if (commandline.debug && e instanceof Error) { @@ -146,11 +146,18 @@ async function main() { } error(e); - return process.exitCode = 1; + + if (session.telemetryEnabled) { + flushTelemetry(); + } + return process.exit(result ? 0 : 1); } finally { - flushTelemetry(); + if (session.telemetryEnabled) { + flushTelemetry(); + } } - return process.exitCode = (result ? 0 : 1); + + return process.exit(result ? 0 : 1); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/ce/mediaquery/character-codes.ts b/ce/mediaquery/character-codes.ts index 34810f5..9a80a48 100644 --- a/ce/mediaquery/character-codes.ts +++ b/ce/mediaquery/character-codes.ts @@ -188,7 +188,7 @@ export function isIdentifierPart(ch: number,): boolean { return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || - ch === CharacterCodes.$ || ch === CharacterCodes._ || + ch === CharacterCodes.$ || ch === CharacterCodes._ || ch === CharacterCodes.minus || ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch); } diff --git a/ce/mediaquery/media-query.ts b/ce/mediaquery/media-query.ts index 332ac24..5ab9498 100644 --- a/ce/mediaquery/media-query.ts +++ b/ce/mediaquery/media-query.ts @@ -10,7 +10,7 @@ export function parseQuery(text: string) { return QueryList.parse(cursor); } -function takeWhitespace(cursor: Scanner) { +export function takeWhitespace(cursor: Scanner) { while (!cursor.eof && isWhiteSpace(cursor)) { cursor.take(); } diff --git a/ce/mediaquery/scanner.ts b/ce/mediaquery/scanner.ts index be45bcb..e686d4d 100644 --- a/ce/mediaquery/scanner.ts +++ b/ce/mediaquery/scanner.ts @@ -593,13 +593,19 @@ export class Scanner implements Token { return result; } + takeWhitespace() { + while (!this.eof && this.kind === Kind.Whitespace) { + this.take(); + } + } + /** - * When the current token is greaterThan, this will return any tokens with characters - * after the greater than character. This has to be scanned separately because greater - * thans appear in positions where longer tokens are incorrect, e.g. `model x=y;`. - * The solution is to call rescanGreaterThan from the parser in contexts where longer - * tokens starting with `>` are allowed (i.e. when parsing binary expressions). - */ + * When the current token is greaterThan, this will return any tokens with characters + * after the greater than character. This has to be scanned separately because greater + * thans appear in positions where longer tokens are incorrect, e.g. `model x=y;`. + * The solution is to call rescanGreaterThan from the parser in contexts where longer + * tokens starting with `>` are allowed (i.e. when parsing binary expressions). + */ rescanGreaterThan(): Kind { if (this.kind === Kind.GreaterThan) { return this.#ch === CharacterCodes.greaterThan ? @@ -856,10 +862,10 @@ export class Scanner implements Token { } /** - * Returns the zero-based line/column from the given offset - * (binary search thru the token start locations) - * @param offset the character position in the document - */ + * Returns the zero-based line/column from the given offset + * (binary search thru the token start locations) + * @param offset the character position in the document + */ positionFromOffset(offset: number): Position { let position = { line: 0, column: 0, offset: 0 }; @@ -888,7 +894,7 @@ export class Scanner implements Token { return { line: position.line, column: position.column + (offset - position.offset) }; } - static *TokensFrom(text: string): Iterable { + static * TokensFrom(text: string): Iterable { const scanner = new Scanner(text).start(); while (!scanner.eof) { yield scanner.take(); diff --git a/ce/package.json b/ce/package.json index 8046b01..f2b0d71 100644 --- a/ce/package.json +++ b/ce/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/vcpkg-ce", - "version": "0.7.0", + "version": "0.8.0", "description": "vcpkg-ce", "main": "dist/main.js", "typings": "dist/exports.d.ts", diff --git a/ce/registries/ArtifactRegistry.ts b/ce/registries/ArtifactRegistry.ts index bf91e4a..f7b69ab 100644 --- a/ce/registries/ArtifactRegistry.ts +++ b/ce/registries/ArtifactRegistry.ts @@ -72,7 +72,7 @@ export abstract class ArtifactRegistry implements Registry { if (!amf.isValid) { for (const err of amf.validationErrors) { - repo.session.channels.warning(`Validation errors in metadata file ${err}}`); + repo.session.channels.warning(err); } throw new Error('invalid manifest'); } @@ -120,7 +120,7 @@ export abstract class ArtifactRegistry implements Registry { } if (criteria?.keyword) { - query.summary.contains(criteria.keyword); + query.id.contains(criteria.keyword); } return [...(await this.openArtifacts(query.items, parent)).entries()].map(each => [this, ...each]); diff --git a/ce/registries/indexer.ts b/ce/registries/indexer.ts index 056bee9..65b1fb3 100644 --- a/ce/registries/indexer.ts +++ b/ce/registries/indexer.ts @@ -6,7 +6,7 @@ import { Range, SemVer } from 'semver'; import BTree from 'sorted-btree'; import { isIterable } from '../util/checks'; import { intersect } from '../util/intersect'; -import { Dictionary, entries, keys, ManyMap } from '../util/linq'; +import { entries, keys, ManyMap, Record } from '../util/linq'; /* eslint-disable @typescript-eslint/ban-types */ @@ -127,7 +127,7 @@ abstract class Key; /** attaches a nested key in the index. */ - with>>(nestedKey: TNestedKey): Key & TNestedKey { + with>>(nestedKey: TNestedKey): Key & TNestedKey { for (const child of keys(nestedKey)) { this.nestedKeys.push(nestedKey[child]); } diff --git a/ce/session.ts b/ce/session.ts index 6a2fe4c..df76ca2 100644 --- a/ce/session.ts +++ b/ce/session.ts @@ -1,10 +1,10 @@ +/* eslint-disable prefer-const */ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { strict } from 'assert'; -import { delimiter } from 'path'; import { MetadataFile } from './amf/metadata-file'; -import { Activation } from './artifacts/activation'; +import { Activation, deactivate } from './artifacts/activation'; import { Artifact, InstalledArtifact } from './artifacts/artifact'; import { Registry } from './artifacts/registry'; import { defaultConfig, globalConfigurationFile, postscriptVarible, profileNames, registryIndexFile, undo, vcpkgDownloadFolder } from './constants'; @@ -26,15 +26,14 @@ import { Registries } from './registries/registries'; import { RemoteRegistry } from './registries/RemoteRegistry'; import { isIndexFile, isMetadataFile } from './registries/standard-registry'; import { Channels, Stopwatch } from './util/channels'; -import { Dictionary, entries } from './util/linq'; import { Queue } from './util/promise'; import { isFilePath, Uri } from './util/uri'; import { isYAML } from './yaml/yaml'; + /** The definition for an installer tool function */ type InstallerTool = ( session: Session, - activation: Activation, name: string, targetLocation: Uri, install: T, @@ -56,11 +55,6 @@ export type Context = { [key: string]: Array | undefined; } & { readonly arm64: boolean; } -interface BackupFile { - environment: Dictionary; - activation: Activation; -} - /** * The Session class is used to hold a reference to the * message channels, @@ -77,6 +71,7 @@ export class Session { readonly tmpFolder: Uri; readonly installFolder: Uri; readonly registryFolder: Uri; + readonly activation: Activation = new Activation(this); readonly globalConfig: Uri; readonly cache: Uri; @@ -96,7 +91,7 @@ export class Session { telemetryEnabled = false; - constructor(currentDirectory: string, public readonly context: Context, public readonly settings: Dictionary, public readonly environment: NodeJS.ProcessEnv) { + constructor(currentDirectory: string, public readonly context: Context, public readonly settings: Record, public readonly environment: NodeJS.ProcessEnv) { this.fileSystem = new UnifiedFileSystem(this). register('file', new LocalFileSystem(this)). register('vsix', new VsixLocalFilesystem(this)). @@ -327,134 +322,20 @@ export class Session { return undefined; } - #postscript = new Dictionary(); - addPostscript(variableName: string, value: string) { - this.#postscript[variableName] = value; - } - async deactivate() { - // get the deactivation information - const lastEnv = this.environment[undo]; - - // remove the variable first. - delete this.environment[undo]; - this.addPostscript(undo, ''); - - if (lastEnv) { - const fileUri = this.parseUri(lastEnv); - if (await fileUri.exists()) { - const contents = await fileUri.readUTF8(); - await fileUri.delete(); - - if (contents) { - try { - const original = JSON.parse(contents, (k, v) => this.deserializer(k, v)); - - // reset the environment variables - // and queue them up in the postscript - for (const [variable, value] of entries(original.environment)) { - if (value) { - this.environment[variable] = value; - this.addPostscript(variable, value); - } else { - delete this.environment[variable]; - this.addPostscript(variable, ''); - } - } - - // in the paths, let's remove all the entries - for (const [variable, uris] of original.activation.paths.entries()) { - let pathLikeVariable = this.environment[variable]; - if (pathLikeVariable) { - for (const uri of uris) { - pathLikeVariable = pathLikeVariable.replace(uri.fsPath, ''); - } - const rx = new RegExp(`${delimiter}+`, 'g'); - pathLikeVariable = pathLikeVariable.replace(rx, delimiter).replace(/^;|;$/g, ''); - // persist that. - this.environment[variable] = pathLikeVariable; - this.addPostscript(variable, pathLikeVariable); - } - } - } catch { - // file not valid, bail. - } - } + const previous = this.environment[undo]; + if (previous && this.postscriptFile) { + const deactivationDataFile = this.parseUri(previous); + if (deactivationDataFile.scheme === 'file' && await deactivationDataFile.exists()) { + + const deactivatationData = JSON.parse(await deactivationDataFile.readUTF8()); + delete deactivatationData.environment[undo]; + await deactivate(this.postscriptFile, deactivatationData.environment || {}, deactivatationData.aliases || {}); + await deactivationDataFile.delete(); } } } - async setActivationInPostscript(activation: Activation, backupEnvironment = true) { - - // capture any variables that we set. - const contents = { environment: {}, activation }; - - // build PATH style variable for the environment - for (const [variable, values] of activation.Paths) { - - if (values.length) { - // add the new values first; existing values are added after. - const s = new Set(values.map(each => each.fsPath)); - const originalVariable = this.environment[variable] || ''; - if (originalVariable) { - for (const p of originalVariable.split(delimiter)) { - if (p) { - s.add(p); - } - } - } - contents.environment[variable] = originalVariable; - this.addPostscript(variable, [...s.values()].join(delimiter)); - } - // for path activations, we undo specific entries, so we don't store the variable here (in case the path is modified after) - } - - for (const [variable, value] of activation.Variables) { - this.addPostscript(variable, value); - contents.environment[variable] = this.environment[variable] || ''; // track the original value - } - - // for now. - if (activation.defines.size > 0) { - this.addPostscript('DEFINES', activation.Defines.map(([define, value]) => `${define}=${value}`).join(' ')); - } - - if (backupEnvironment) { - // create the environment backup file - const backupFile = this.tmpFolder.join(`previous-environment-${Date.now().toFixed()}.json`); - - await backupFile.writeUTF8(JSON.stringify(contents, (k, v) => this.serializer(k, v), 2)); - this.addPostscript(undo, backupFile.toString()); - } - } - - async writePostscript() { - let content = ''; - const psf = this.postscriptFile; - if (psf) { - switch (psf?.fsPath.substr(-3)) { - case 'ps1': - // update environment variables. (powershell) - content += [...entries(this.#postscript)].map((k, v) => { return `$\{ENV:${k[0]}}="${k[1]}"`; }).join('\n'); - break; - - case 'cmd': - // update environment variables. (cmd) - content += [...entries(this.#postscript)].map((k) => { return `set ${k[0]}=${k[1]}`; }).join('\r\n'); - break; - - case '.sh': - // update environment variables. (posix)' - content += [...entries(this.#postscript)].map((k, v) => { - return k[1] ? `export ${k[0]}="${k[1]}"` : `unset ${k[0]}`; - }).join('\n'); - } - - if (content) { - await psf.writeUTF8(content); - } - } - } setupLogging() { // at this point, we can subscribe to the events in the export * from './lib/version';FileSystem and Channels @@ -513,4 +394,5 @@ export class Session { } return value; } + } diff --git a/ce/util/channels.ts b/ce/util/channels.ts index 8e5b593..457b27d 100644 --- a/ce/util/channels.ts +++ b/ce/util/channels.ts @@ -43,17 +43,17 @@ export class Channels extends EventEmitter { /** @internal */ readonly stopwatch: Stopwatch; - warning(text: string, context?: any) { - this.emit('warning', text, context, this.stopwatch.total); + warning(text: string | Array, context?: any) { + typeof text === 'string' ? this.emit('warning', text, context, this.stopwatch.total) : text.forEach(t => this.emit('warning', t, context, this.stopwatch.total)); } - error(text: string, context?: any) { - this.emit('error', text, context, this.stopwatch.total); + error(text: string | Array, context?: any) { + typeof text === 'string' ? this.emit('error', text, context, this.stopwatch.total) : text.forEach(t => this.emit('error', t, context, this.stopwatch.total)); } - message(text: string, context?: any) { - this.emit('message', text, context, this.stopwatch.total); + message(text: string | Array, context?: any) { + typeof text === 'string' ? this.emit('message', text, context, this.stopwatch.total) : text.forEach(t => this.emit('message', t, context, this.stopwatch.total)); } - debug(text: string, context?: any) { - this.emit('debug', text, context, this.stopwatch.total); + debug(text: string | Array, context?: any) { + typeof text === 'string' ? this.emit('debug', text, context, this.stopwatch.total) : text.forEach(t => this.emit('debug', t, context, this.stopwatch.total)); } constructor(session: Session) { super(); diff --git a/ce/util/evaluator.ts b/ce/util/evaluator.ts deleted file mode 100644 index 1dabc70..0000000 --- a/ce/util/evaluator.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { delimiter } from 'path'; -import { createSandbox } from '../util/safeEval'; -import { isPrimitive } from './checks'; - -/** sandboxed eval function for evaluating expressions */ -const safeEval: (code: string, context?: any) => T = createSandbox(); - -function proxifyObject(obj: Record): any { - return new Proxy(obj, { - get(target, prop) { - if (typeof prop === 'string') { - let result = target[prop]; - // check for a direct match first - if (!result) { - // go thru the properties and check for a case-insensitive match - for (const each of Object.keys(target)) { - if (each.toLowerCase() === prop.toLowerCase()) { - result = target[each]; - break; - } - } - } - if (result) { - if (Array.isArray(result)) { - return result; - } - if (typeof result === 'object') { - return proxifyObject(result); - } - if (isPrimitive(result)) { - return result; - } - } - return undefined; - } - }, - }); - -} - -export class Evaluator { - private activation: any; - private host: any; - - constructor(private artifactData: Record, host: Record, activation: Record) { - this.host = proxifyObject(host); - this.activation = proxifyObject(activation); - - } - - evaluate(text: string | undefined): string | undefined { - if (!text || text.indexOf('$') === -1) { - // quick exit if no expression or no variables - return text; - } - - // $$ -> escape for $ - text = text.replace(/\$\$/g, '\uffff'); - - // $0 ... $9 -> replace contents with the values from the artifact - text = text.replace(/\$([0-9])/g, (match, index) => this.artifactData[match] || match); - - // $ -> expression value - text = text.replace(/\$([a-zA-Z_.][a-zA-Z0-9_.]*)/g, (match, expression) => { - - if (expression.startsWith('host.')) { - // this is getting something from the host context (ie, environment variable) - return safeEval(expression.substr(5), this.host) || match; - } - - // otherwise, assume it is a property on the activation object - return safeEval(expression, this.activation) || match; - }); - - // ${ ...} in non-verify mode, the contents are just returned - text = text.replace(/\$\{(.*?)\}/g, '$1'); - - // restore escaped $ - text = text.replace(/\uffff/g, '$'); - - return text; - } - - expandPaths(value: string, delim = delimiter): Array { - let n = undefined; - - const parts = value.split(/(\$[a-zA-Z0-9.]+?)/g).filter(each => each).map((part, i) => { - - const value = this.evaluate(part) || ''; - - if (value.indexOf(delim) !== -1) { - n = i; - } - - return value; - }); - - if (n === undefined) { - // if the value didn't have a path separator, then just return the value - return [parts.join('')]; - } - - const front = parts.slice(0, n).join(''); - const back = parts.slice(n + 1).join(''); - - return parts[n].split(delim).filter(each => each).map(each => `${front}${each}${back}`); - } - - async evaluateAndVerify(expression: string | undefined): Promise { - return ''; - } -} \ No newline at end of file diff --git a/ce/util/exec-cmd.ts b/ce/util/exec-cmd.ts index eee1332..01708ba 100644 --- a/ce/util/exec-cmd.ts +++ b/ce/util/exec-cmd.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ChildProcess, ProcessEnvOptions, spawn, SpawnOptions } from 'child_process'; +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; +import { lstat } from 'fs/promises'; export interface ExecOptions extends SpawnOptions { onCreate?(cp: ChildProcess): void; @@ -12,6 +13,7 @@ export interface ExecOptions extends SpawnOptions { export interface ExecResult { stdout: string; stderr: string; + env: NodeJS.ProcessEnv | undefined; /** * Union of stdout and stderr. @@ -19,9 +21,10 @@ export interface ExecResult { log: string; error: Error | null; code: number | null; + command: string, + args: Array, } - export function cmdlineToArray(text: string, result: Array = [], matcher = /[^\s"]+|"([^"]*)"/gi, count = 0): Array { text = text.replace(/\\"/g, '\ufffe'); const match = matcher.exec(text); @@ -35,7 +38,18 @@ export function cmdlineToArray(text: string, result: Array = [], matcher : result; } -export function execute(command: string, cmdlineargs: Array, options: ExecOptions = {}): Promise { +export async function execute(command: string, cmdlineargs: Array, options: ExecOptions = {}): Promise { + try { + command = command.replace(/"/g, ''); + const k = await lstat(command); + if (k.isDirectory()) { + throw new Error(`Unable to call ${command} ${cmdlineargs.join(' ')} -- ${command} is a directory`); + } + } catch (e) { + throw new Error(`Unable to call ${command} ${cmdlineargs.join(' ')} - -- ${command} is not a file `); + + } + return new Promise((resolve, reject) => { const cp = spawn(command, cmdlineargs.filter(each => each), { ...options, stdio: 'pipe' }); if (options.onCreate) { @@ -61,14 +75,18 @@ export function execute(command: string, cmdlineargs: Array, options: Ex reject(err); }); - cp.on('close', (code, signal) => - resolve({ + cp.on('close', (code, signal) => { + return resolve({ + env: options.env, stdout: out, stderr: err, log: all, error: code ? new Error('Process Failed.') : null, code, - }), + command: command, + args: cmdlineargs, + }); + } ); }); } @@ -84,11 +102,10 @@ export function execute(command: string, cmdlineargs: Array, options: Ex */ export const execute_shell = ( command: string, - options: ExecOptions = {}, - environmentOptions?: ProcessEnvOptions + options: ExecOptions = {} ): Promise => { return new Promise((resolve, reject) => { - const cp = spawn(command, environmentOptions ? { ...options, ...environmentOptions, stdio: 'pipe', shell: true } : { ...options, stdio: 'pipe', shell: true }); + const cp = spawn(command, { ...options, stdio: 'pipe', shell: true }); if (options.onCreate) { options.onCreate(cp); } @@ -113,11 +130,14 @@ export const execute_shell = ( }); cp.on('close', (code, signal) => resolve({ + env: options.env, stdout: out, stderr: err, log: all, error: code ? new Error('Process Failed.') : null, code, + command: command, + args: [], }), ); }); diff --git a/ce/util/linq.ts b/ce/util/linq.ts index 8c9ba01..40b2cdb 100644 --- a/ce/util/linq.ts +++ b/ce/util/linq.ts @@ -1,19 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export interface Dictionary { +export class Record implements Record { [key: string]: T; } -export class Dictionary implements Dictionary { -} - -export function ToDictionary(keys: Array, each: (index: string) => T) { - const result = new Dictionary(); - keys.map((v, i, a) => result[v] = each(v)); - return result; -} - export type IndexOf = T extends Map ? T : T extends Array ? number : string; /** performs a truthy check on the value, and calls onTrue when the condition is true,and onFalse when it's not */ @@ -32,6 +23,7 @@ export interface IterableWithLinq extends Iterable { first(predicate?: (each: T) => boolean): T | undefined; selectNonNullable(selector: (each: T) => V): IterableWithLinq>; select(selector: (each: T) => V): IterableWithLinq; + selectAsync(selector: (each: T) => V): AsyncGenerator; selectMany(selector: (each: T) => Iterable): IterableWithLinq; where(predicate: (each: T) => boolean): IterableWithLinq; forEach(action: (each: T) => void): void; @@ -39,7 +31,7 @@ export interface IterableWithLinq extends Iterable { toArray(): Array; toObject(selector: (each: T) => [V, U]): Record; results(): Promise; - toDictionary(keySelector: (each: T) => string, selector: (each: T) => TValue): Dictionary; + toRecord(keySelector: (each: T) => string, selector: (each: T) => TValue): Record; toMap(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map; groupBy(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map>; @@ -82,9 +74,9 @@ function linqify(iterable: Iterable | IterableIterator): IterableWithLi join: join.bind(iterable), count: len.bind(iterable), results: results.bind(iterable), - toDictionary: toDictionary.bind(iterable), toMap: toMap.bind(iterable), groupBy: groupBy.bind(iterable), + selectAsync: selectAsync.bind(iterable), }; r.linq = r; return r; @@ -95,7 +87,7 @@ function len(this: Iterable): number { } export function keys(source: Map | null | undefined): Iterable -export function keys>(source: Dictionary | null | undefined): Iterable +export function keys>(source: Record | null | undefined): Iterable export function keys>(source: Array | null | undefined): Iterable export function keys(source: any | undefined | null): Iterable export function keys(source: any): Iterable { @@ -122,11 +114,11 @@ export function keys(source: any): Iterable { /** returns an IterableWithLinq<> for keys in the collection */ function _keys(source: Map | null | undefined): IterableWithLinq -function _keys>(source: Dictionary | null | undefined): IterableWithLinq +function _keys>(source: Record | null | undefined): IterableWithLinq function _keys>(source: Array | null | undefined): IterableWithLinq function _keys(source: any | undefined | null): IterableWithLinq function _keys(source: any): IterableWithLinq { - //export function keys | Dictionary | Map)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): IterableWithLinq> { + //export function keys | Record | Map)>(source: TSrc & (Array | Record | Map) | null | undefined): IterableWithLinq> { if (source) { if (Array.isArray(source)) { return >>linqify((>source).keys()); @@ -149,7 +141,7 @@ function isIterable(source: any): source is Iterable { return !!source && !!source[Symbol.iterator]; } -export function values | Dictionary | Map)>(source: (Iterable | Array | Dictionary | Map | Set) | null | undefined): Iterable { +export function values | Record | Map)>(source: (Iterable | Array | Record | Map | Set) | null | undefined): Iterable { if (source) { // map if (source instanceof Map || source instanceof Set) { @@ -174,17 +166,18 @@ export const linq = { keys: _keys, find: _find, startsWith: _startsWith, + join: _join }; /** returns an IterableWithLinq<> for values in the collection * * @note - null/undefined/empty values are considered 'empty' */ -function _values(source: (Array | Dictionary | Map | Set | Iterable) | null | undefined): IterableWithLinq { +function _values(source: (Array | Record | Map | Set | Iterable) | null | undefined): IterableWithLinq { return (source) ? linqify(values(source)) : linqify([]); } -export function entries | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): Iterable<[IndexOf, T]> { +export function entries | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined): Iterable<[IndexOf, T]> { if (source) { if (Array.isArray(source)) { return , T]>>source.entries(); @@ -205,23 +198,26 @@ export function entries | Dictionary | Map } /** returns an IterableWithLinq<{key,value}> for the source */ -function _entries | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): IterableWithLinq<[IndexOf, T]> { +function _entries | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined): IterableWithLinq<[IndexOf, T]> { return linqify(source ? entries(source) : []) } /** returns the first value where the key equals the match value (case-insensitive) */ -function _find | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined, match: string): T | undefined { +function _find | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined, match: string): T | undefined { return _entries(source).first(([key,]) => key.toString().localeCompare(match, undefined, { sensitivity: 'base' }) === 0)?.[1]; } /** returns the first value where the key starts with the match value (case-insensitive) */ -function _startsWith | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined, match: string): T | undefined { +function _startsWith | Record | Map | undefined | null)>(source: TSrc & (Array | Record | Map) | null | undefined, match: string): T | undefined { match = match.toLowerCase(); return _entries(source).first(([key,]) => key.toString().toLowerCase().startsWith(match))?.[1]; } +function _join(source: (Array | Record | Map | Set | Iterable) | null | undefined, delimiter: string): string { + return source ? _values(source).join(delimiter) : ''; +} -export function length(source?: string | Iterable | Dictionary | Array | Map | Set): number { +export function length(source?: string | Iterable | Record | Array | Map | Set): number { if (source) { if (Array.isArray(source) || typeof (source) === 'string') { return source.length; @@ -237,14 +233,6 @@ export function length(source?: string | Iterable | Dictionary | Arr return 0; } -function toDictionary(this: Iterable, keySelector: (each: TElement) => string, selector: (each: TElement) => TValue): Dictionary { - const result = new Dictionary(); - for (const each of this) { - result[keySelector(each)] = selector(each); - } - return result; -} - function toMap(this: Iterable, keySelector: (each: TElement) => TKey, selector: (each: TElement) => TValue): Map { const result = new Map(); for (const each of this) { @@ -298,6 +286,13 @@ function select(this: Iterable, selector: (each: T) => V): IterableWith }.bind(this)()); } +async function* selectAsync(this: Iterable, selector: (each: T) => Promise) { + for (const each of this) { + yield selector(each) + } +} + + function selectMany(this: Iterable, selector: (each: T) => Iterable): IterableWithLinq { return linqify(function* (this: Iterable) { for (const each of this) { @@ -369,7 +364,7 @@ function toArray(this: Iterable): Array { } function toObject(this: Iterable, selector: (each: T) => [string, V]): Record { - const result = >{}; + const result = new Record(); for (const each of this) { const [key, value] = selector(each); result[key] = value; @@ -395,7 +390,7 @@ function bifurcate(this: Iterable, predicate: (each: T) => boolean): Array } function distinct(this: Iterable, selector?: (each: T) => any): IterableWithLinq { - const hash = new Dictionary(); + const hash = new Record(); return linqify(function* (this: Iterable) { if (!selector) { @@ -412,7 +407,7 @@ function distinct(this: Iterable, selector?: (each: T) => any): IterableWi } function duplicates(this: Iterable, selector?: (each: T) => any): IterableWithLinq { - const hash = new Dictionary(); + const hash = new Record(); return linqify(function* (this: Iterable) { if (!selector) { @@ -467,4 +462,5 @@ export function countWhere(from: Iterable, predicate: (e: T) => boolean | return Promise.all(all).then(() => v); } return v; -} \ No newline at end of file +} + diff --git a/ce/util/promise.ts b/ce/util/promise.ts index 339e21b..35f5444 100644 --- a/ce/util/promise.ts +++ b/ce/util/promise.ts @@ -133,8 +133,8 @@ export class Queue { return action().catch(async (e) => { this.rejections.push(e); throw e; }).finally(() => this.next()); } - enqueueMany(array: Array, fn: (v: S) => Promise) { - for (const each of array) { + enqueueMany(iterable: Iterable, fn: (v: S) => Promise) { + for (const each of iterable) { void this.enqueue(() => fn(each)); } return this; diff --git a/ce/util/safeEval.ts b/ce/util/safeEval.ts index 13d0e14..b776bec 100644 --- a/ce/util/safeEval.ts +++ b/ce/util/safeEval.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import * as vm from 'vm'; +import { Kind, Scanner } from '../mediaquery/scanner'; +import { isPrimitive } from './checks'; /** * Creates a reusable safe-eval sandbox to execute code in. @@ -12,9 +14,7 @@ export function createSandbox(): (code: string, context?: any) => T { const response = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000); sandbox[response] = {}; if (context) { - for (const key of Object.keys(context)) { - sandbox[key] = context[key]; - } + Object.keys(context).forEach(key => sandbox[key] = context[key]); vm.runInContext(`try { ${response} = ${code} } catch (e) { ${response} = undefined }`, sandbox); for (const key of Object.keys(context)) { delete sandbox[key]; @@ -25,3 +25,116 @@ export function createSandbox(): (code: string, context?: any) => T { return sandbox[response]; }; } + +export const safeEval = createSandbox(); + +function isValue(cursor: Scanner) { + switch (cursor.kind) { + case Kind.NumericLiteral: + case Kind.StringLiteral: + case Kind.BooleanLiteral: + cursor.take(); + return true; + } + cursor.takeWhitespace(); + + while (cursor.kind === Kind.Identifier) { + cursor.take(); + cursor.takeWhitespace(); + if (cursor.eof) { + // at the end of an identifier, so it's good + return true; + } + + // otherwise, it had better be a dot + if (cursor.kind !== Kind.Dot) { + // the end of the value + return true; + } + + // it's a dot, so take it and keep going + cursor.take(); + } + + // if it's not an identifier, not it's valid + return false; +} + +const comparisons = [Kind.EqualsEquals, Kind.ExclamationEquals, Kind.LessThan, Kind.LessThanEquals, Kind.GreaterThan, Kind.GreaterThanEquals, Kind.EqualsEqualsEquals, Kind.ExclamationEqualsEquals]; + +export function valiadateExpression(expression: string) { + // supports + if (!expression) { + return true; + } + + const cursor = new Scanner(expression); + + cursor.scan(); + cursor.takeWhitespace(); + if (cursor.eof) { + // the expression is empty + return true; + } + if (!isValue(cursor)) { + return false; + } + cursor.takeWhitespace(); + if (cursor.eof) { + // techincally just a value, so it's valid + return true; + } + + if (!(comparisons.indexOf(cursor.kind) !== -1)) { + // can only be a comparitor at this point + return false; + } + + cursor.take(); + cursor.takeWhitespace(); + + if (!(isValue(cursor))) { + // can only be a value at this point + return false; + } + + cursor.take(); + cursor.takeWhitespace(); + if (!cursor.eof) { + return false; + } + return true; +} + +export function proxifyObject(obj: Record): any { + return new Proxy(obj, { + get(target, prop) { + + if (typeof prop === 'string') { + let result = target[prop]; + // check for a direct match first + if (!result) { + // go thru the properties and check for a case-insensitive match + for (const each of Object.keys(target)) { + if (each.toLowerCase() === prop.toLowerCase()) { + result = target[each]; + break; + } + } + } + if (result) { + if (Array.isArray(result)) { + return result; + } + if (typeof result === 'object') { + return proxifyObject(result); + } + if (isPrimitive(result)) { + return result; + } + } + return undefined; + } + }, + }); +} diff --git a/ce/util/text.ts b/ce/util/text.ts index 3b3b018..ffc5629 100644 --- a/ce/util/text.ts +++ b/ce/util/text.ts @@ -14,4 +14,4 @@ export function encode(content: string): Uint8Array { export function equalsIgnoreCase(s1: string | undefined, s2: string | undefined): boolean { return s1 === s2 || !!s1 && !!s2 && s1.localeCompare(s2, undefined, { sensitivity: 'base' }) === 0; -} +} \ No newline at end of file diff --git a/ce/yaml/BaseMap.ts b/ce/yaml/BaseMap.ts index 87d5471..89e86d5 100644 --- a/ce/yaml/BaseMap.ts +++ b/ce/yaml/BaseMap.ts @@ -8,9 +8,7 @@ import { EntityFactory, Node, Primitive, Yaml, YAMLSequence } from './yaml-types export /** @internal */ abstract class BaseMap extends Entity { - get keys(): Array { - return this.exists() ? this.node.items.map(each => this.asString(each.key)!) : []; - } + get length(): number { return this.exists() ? this.node.items.length : 0; diff --git a/ce/yaml/CustomScalarMap.ts b/ce/yaml/CustomScalarMap.ts index 5786e90..420a571 100644 --- a/ce/yaml/CustomScalarMap.ts +++ b/ce/yaml/CustomScalarMap.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { isScalar, Scalar } from 'yaml'; +import { ValidationError } from '../interfaces/validation-error'; import { BaseMap } from './BaseMap'; import { EntityFactory, Yaml, YAMLDictionary } from './yaml-types'; @@ -51,4 +52,8 @@ export /** @internal */ class CustomScalarMap> ext this.node.set(key, new Scalar(value)); } + + override *validate(): Iterable { + yield* this.validateIsObject(); + } } diff --git a/ce/yaml/Entity.ts b/ce/yaml/Entity.ts index f5db200..f039142 100644 --- a/ce/yaml/Entity.ts +++ b/ce/yaml/Entity.ts @@ -2,8 +2,6 @@ // Licensed under the MIT License. import { isMap, isScalar, isSeq } from 'yaml'; -import { i } from '../i18n'; -import { ErrorKind } from '../interfaces/error-kind'; import { ValidationError } from '../interfaces/validation-error'; import { isNullish } from '../util/checks'; import { Node, Primitive, Yaml, YAMLDictionary } from './yaml-types'; @@ -32,9 +30,8 @@ export /** @internal */ class Entity extends Yaml { } override /** @internal */ *validate(): Iterable { - if (this.node && !isMap(this.node)) { - yield { message: i`Incorrect type for '${this.key}' - should be an object`, range: this.sourcePosition(), category: ErrorKind.IncorrectType }; - } + yield* super.validate(); + yield* this.validateIsObject(); } has(key: string, kind?: 'sequence' | 'entity' | 'scalar'): boolean { @@ -77,7 +74,7 @@ export /** @internal */ class Entity extends Yaml { return undefined; } - is(key: string, kind: 'sequence' | 'entity' | 'scalar' | 'string' | 'number' | 'boolean'): boolean | undefined { + childIs(key: string, kind: 'sequence' | 'entity' | 'scalar' | 'string' | 'number' | 'boolean'): boolean | undefined { if (this.node) { const v = this.node.get(key, true); if (v === undefined) { diff --git a/ce/yaml/EntityMap.ts b/ce/yaml/EntityMap.ts index a1dbaf8..fb63b24 100644 --- a/ce/yaml/EntityMap.ts +++ b/ce/yaml/EntityMap.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { Dictionary } from '../interfaces/collections'; +import { ValidationError } from '../interfaces/validation-error'; import { BaseMap } from './BaseMap'; import { EntityFactory, Node, Yaml, YAMLDictionary } from './yaml-types'; @@ -20,12 +21,17 @@ export /** @internal */ abstract class EntityMap { + yield* super.validate(); + yield* this.validateIsObject(); + } + add(key: string): TElement { if (this.has(key)) { return this.get(key)!; diff --git a/ce/yaml/EntitySequence.ts b/ce/yaml/EntitySequence.ts index c26a054..fd98631 100644 --- a/ce/yaml/EntitySequence.ts +++ b/ce/yaml/EntitySequence.ts @@ -17,6 +17,7 @@ export /** @internal */ class EntitySequence { this.node.delete(flag); } } + + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateIsSequence(); + } } diff --git a/ce/yaml/ScalarMap.ts b/ce/yaml/ScalarMap.ts index 7afb8db..e16f7d7 100644 --- a/ce/yaml/ScalarMap.ts +++ b/ce/yaml/ScalarMap.ts @@ -16,7 +16,6 @@ export /** @internal */ class ScalarMap this.node.set(key, value); } - add(key: string): TElement { this.assert(true); this.node.set(key, ''); @@ -26,9 +25,8 @@ export /** @internal */ class ScalarMap *[Symbol.iterator](): Iterator<[string, TElement]> { if (this.node) { for (const { key, value } of this.node.items) { - const v = isScalar(value) ? this.asPrimitive(value) : undefined; - if (v) { - yield [key, value]; + if (isScalar(value)) { + yield [this.asString(key)!, this.asPrimitive(value)]; } } } diff --git a/ce/yaml/ScalarSequence.ts b/ce/yaml/ScalarSequence.ts index 434e036..f78c75e 100644 --- a/ce/yaml/ScalarSequence.ts +++ b/ce/yaml/ScalarSequence.ts @@ -2,6 +2,8 @@ // Licensed under the MIT License. import { isScalar, isSeq, Scalar, YAMLSeq } from 'yaml'; +import { ErrorKind } from '../interfaces/error-kind'; +import { ValidationError } from '../interfaces/validation-error'; import { Primitive, Yaml, YAMLScalar, YAMLSequence } from './yaml-types'; /** @@ -100,9 +102,10 @@ export /** @internal */ class ScalarSequence extends return yield this.asPrimitive(this.node.value); } if (isSeq(this.node)) { + for (const each of this.node.items.values()) { const v = this.asPrimitive(each); - if (v) { + if (v !== undefined) { yield v; } } @@ -116,4 +119,14 @@ export /** @internal */ class ScalarSequence extends } this.dispose(true); } + + override *validate(): Iterable { + if (this.node && !isSeq(this.node) && !isScalar(this.node)) { + yield { + message: `'${this.fullName}' is not an sequence or primitive value`, + range: this, + category: ErrorKind.IncorrectType + }; + } + } } diff --git a/ce/yaml/yaml-types.ts b/ce/yaml/yaml-types.ts index 141318c..633701a 100644 --- a/ce/yaml/yaml-types.ts +++ b/ce/yaml/yaml-types.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { isCollection, isMap, isScalar, isSeq, Scalar, YAMLMap, YAMLSeq } from 'yaml'; +import { ErrorKind } from '../interfaces/error-kind'; import { ValidationError } from '../interfaces/validation-error'; import { isNullish } from '../util/checks'; @@ -20,11 +21,19 @@ export /** @internal */ abstract class Yaml { } } + get fullName(): string { + return !this.node ? '' : this.parent ? this.key ? `${this.parent.fullName}.${this.key}` : this.parent.fullName : this.key || '$'; + } + /** returns the current node as a JSON string */ toString(): string { return this.node?.toJSON() ?? ''; } + get keys(): Array { + return this.exists() && isMap(this.node) ? this.node.items.map(each => this.asString(each.key)!) : []; + } + /** * Coersion function to string * @@ -141,7 +150,7 @@ export /** @internal */ abstract class Yaml { } if (key !== undefined) { if ((isMap(this.node) || isSeq(this.node))) { - const node = this.node.get(key); + const node = this.node.get(key, true); if (node) { return node.range || undefined; } @@ -263,6 +272,74 @@ export /** @internal */ abstract class Yaml { *validate(): Iterable { // shh. } + + protected *validateChildKeys(keys: Array): Iterable { + if (isMap(this.node)) { + for (const key of this.keys) { + if (keys.indexOf(key) === -1) { + yield { + message: `Unexpected '${key}' found in ${this.fullName}`, + range: this.sourcePosition(key), + category: ErrorKind.InvalidChild, + }; + } + } + } + } + + protected *validateIsObject(): Iterable { + if (this.node && !isMap(this.node)) { + yield { + message: `'${this.fullName}' is not an object`, + range: this, + category: ErrorKind.IncorrectType + }; + } + } + protected *validateIsSequence(): Iterable { + if (this.node && !isSeq(this.node)) { + yield { + message: `'${this.fullName}' is not an object`, + range: this, + category: ErrorKind.IncorrectType + }; + } + } + + protected *validateIsSequenceOrPrimitive(): Iterable { + if (this.node && (!isSeq(this.node) && !isScalar(this.node))) { + yield { + message: `'${this.fullName}' is not a sequence or value`, + range: this, + category: ErrorKind.IncorrectType + }; + } + } + + protected *validateIsObjectOrPrimitive(): Iterable { + if (this.node && (!isMap(this.node) && !isScalar(this.node))) { + yield { + message: `'${this.fullName}' is not an object or value`, + range: this, + category: ErrorKind.IncorrectType + }; + } + } + + protected *validateChild(child: string, kind: 'string' | 'boolean' | 'number'): Iterable { + if (this.node && isMap(this.node)) { + if (this.node.has(child)) { + const c = this.node.get(child, true); + if (!isScalar(c) || typeof c.value !== kind) { + yield { + message: `'${this.fullName}.${child}' is not a ${kind} value`, + range: c.range!, + category: ErrorKind.IncorrectType + }; + } + } + } + } } export /** @internal */ interface EntityFactory> extends NodeFactory { @@ -272,4 +349,3 @@ export /** @internal */ interface EntityFactory extends Function { /**@internal*/ create(): TNode; } - diff --git a/test/core/amf-tests.ts b/test/core/amf-tests.ts index 9c0ccc8..23cb7ff 100644 --- a/test/core/amf-tests.ts +++ b/test/core/amf-tests.ts @@ -112,21 +112,21 @@ describe('Amf', () => { doc.requires.set('range/with/resolved', { range: '1.*', resolved: '1.0.0' }); strict.equal(doc.requires.get('range/with/resolved')!.raw, '1.* 1.0.0'); - strict.equal(doc.settings.tools.get('CC'), 'foo/bar/cl.exe', 'should have a value'); - strict.equal(doc.settings.tools.get('CXX'), 'bin/baz/cl.exe', 'should have a value'); - strict.equal(doc.settings.tools.get('Whatever'), 'some/tool/path/foo', 'should have a value'); + strict.equal(doc.exports.tools.get('CC'), 'foo/bar/cl.exe', 'should have a value'); + strict.equal(doc.exports.tools.get('CXX'), 'bin/baz/cl.exe', 'should have a value'); + strict.equal(doc.exports.tools.get('Whatever'), 'some/tool/path/foo', 'should have a value'); - doc.settings.tools.delete('CXX'); - strict.equal(doc.settings.tools.keys.length, 2, 'should only have two tools now'); + doc.exports.tools.delete('CXX'); + strict.equal(doc.exports.tools.keys.length, 2, 'should only have two tools now'); - strict.sequenceEqual(doc.settings.variables.get('test'), ['abc'], 'variables should be an array'); - strict.sequenceEqual(doc.settings.variables.get('cxxflags'), ['foo=bar', 'bar=baz'], 'variables should be an array'); + strict.sequenceEqual(doc.exports.environment.get('test'), ['abc'], 'variables should be an array'); + strict.sequenceEqual(doc.exports.environment.get('cxxflags'), ['foo=bar', 'bar=baz'], 'variables should be an array'); - doc.settings.variables.add('test').add('another value'); - strict.sequenceEqual(doc.settings.variables.get('test'), ['abc', 'another value'], 'variables should be an array of two items now'); + doc.exports.environment.add('test').add('another value'); + strict.sequenceEqual(doc.exports.environment.get('test'), ['abc', 'another value'], 'variables should be an array of two items now'); - doc.settings.paths.add('bin').add('hello/there'); - strict.deepEqual(doc.settings.paths.get('bin')?.length, 3, 'there should be three paths in bin now'); + doc.exports.paths.add('bin').add('hello/there'); + strict.deepEqual(doc.exports.paths.get('bin')?.length, 3, 'there should be three paths in bin now'); strict.sequenceEqual(doc.conditionalDemands.keys, ['windows and arm'], 'should have one conditional demand'); /* @@ -166,6 +166,6 @@ describe('Amf', () => { strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); SuiteLocal.log(doc.validationErrors); - strict.equal(doc.validationErrors.length, 2, `Expecting two errors, found: ${JSON.stringify(doc.validationErrors, null, 2)}`); + strict.equal(doc.validationErrors.length, 7, `Expecting two errors, found: ${JSON.stringify(doc.validationErrors, null, 2)}`); }); }); diff --git a/test/core/evaluator.ts b/test/core/evaluator.ts deleted file mode 100644 index 74a5e76..0000000 --- a/test/core/evaluator.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Activation } from '@microsoft/vcpkg-ce/dist/artifacts/activation'; -import { Evaluator } from '@microsoft/vcpkg-ce/dist/util/evaluator'; -import { linq } from '@microsoft/vcpkg-ce/dist/util/linq'; -import { equalsIgnoreCase } from '@microsoft/vcpkg-ce/dist/util/text'; -import { strict, strictEqual } from 'assert'; -import { SuiteLocal } from './SuiteLocal'; - -describe('Evaluator', () => { - const local = new SuiteLocal(); - const fs = local.fs; - const session = local.session; - - after(local.after.bind(local)); - - it('evaluates', () => { - const activation = new Activation(session); - activation.environment.set('foo', ['bar']); - const e = new Evaluator({ $0: 'c:/foo/bar/python.exe' }, process.env, activation.output); - - // handle expressions that use the artifact data - strictEqual(e.evaluate('$0'), 'c:/foo/bar/python.exe', 'Should return $0 from artifact data'); - - // unmatched variables should be passed thru - strictEqual(e.evaluate('$1'), '$1', 'items with no value are not replaced'); - - // handle expressions that use the environment - const pathVar = linq.keys(process.env).first(each => equalsIgnoreCase(each, 'path')); - strict(pathVar); - strictEqual(e.evaluate(`$host.${pathVar}`), process.env[pathVar], 'Should be able to get environment variables from host'); - - // handle expressions that use the activation's output - strictEqual(e.evaluate('$environment.foo'), 'bar', 'Should be able to get environment variables from activation'); - }); -}); \ No newline at end of file diff --git a/test/core/expressions.ts b/test/core/expressions.ts new file mode 100644 index 0000000..ac4670a --- /dev/null +++ b/test/core/expressions.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { valiadateExpression } from '@microsoft/vcpkg-ce/dist/util/safeEval'; +import { strictEqual } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +describe('Expressions', () => { + const local = new SuiteLocal(); + const session = local.session; + + after(local.after.bind(local)); + + it('Testing the expression parser', () => { + strictEqual(valiadateExpression(' 1 = 2'), false, 'Assignments not supported'); + strictEqual(valiadateExpression(' 1 == 2'), true, 'Equality supported, numeric literals'); + strictEqual(valiadateExpression(' 1 === 2'), true, 'Strict equality supported'); + strictEqual(valiadateExpression(' $1 === $2'), true, 'Supports variables'); + strictEqual(valiadateExpression(' "abc" === $2'), true, 'Supports strings literals'); + strictEqual(valiadateExpression(' $1 === true'), true, 'Supports boolean literals'); + strictEqual(valiadateExpression(' true'), true, 'literals value'); + strictEqual(valiadateExpression(' true =='), false, 'partial expressions'); + }); +}); \ No newline at end of file diff --git a/test/core/index-tests.ts b/test/core/index-tests.ts index 8df44be..a4a31fd 100644 --- a/test/core/index-tests.ts +++ b/test/core/index-tests.ts @@ -3,7 +3,7 @@ import { Index, IndexSchema, SemverKey, StringKey } from '@microsoft/vcpkg-ce/dist/registries/indexer'; -import { Dictionary, keys } from '@microsoft/vcpkg-ce/dist/util/linq'; +import { keys, Record } from '@microsoft/vcpkg-ce/dist/util/linq'; import { describe, it } from 'mocha'; import { SemVer } from 'semver'; import { SuiteLocal } from './SuiteLocal'; @@ -15,7 +15,7 @@ interface TestData { summary?: string description?: string; }, - contacts?: Dictionary<{ + contacts?: Record; }> diff --git a/test/core/msbuild-tests.ts b/test/core/msbuild-tests.ts index 2d3d56f..29b8de9 100644 --- a/test/core/msbuild-tests.ts +++ b/test/core/msbuild-tests.ts @@ -12,7 +12,7 @@ describe('MSBuild Generator', () => { after(local.after.bind(local)); - it('Generates locations in order', () => { + it('Generates locations in order', async () => { const activation = new Activation(local.session); @@ -22,10 +22,10 @@ describe('MSBuild Generator', () => { ['c', 'csetting'], ['b', 'bsetting'], ['prop', ['first', 'seco>nd', 'third']] - ]).forEach(([key, value]) => activation.properties.set(key, typeof value === 'string' ? [value] : value)); + ]).forEach(([key, value]) => activation.addProperty(key, typeof value === 'string' ? [value] : value)); - activation.locations.set('somepath', local.fs.file('c:/tmp')); - activation.paths.set('include', [local.fs.file('c:/tmp'), local.fs.file('c:/tmp2')]); + activation.addLocation('somepath', local.fs.file('c:/tmp')); + activation.addPath('include', [local.fs.file('c:/tmp'), local.fs.file('c:/tmp2')]); const expected = (platform() === 'win32') ? ` @@ -59,6 +59,6 @@ describe('MSBuild Generator', () => { ` ; - strict.equal(activation.generateMSBuild([]), expected); + strict.equal(await activation.generateMSBuild([]), expected); }); }); diff --git a/test/package.json b/test/package.json index bf29874..9fd9cac 100644 --- a/test/package.json +++ b/test/package.json @@ -1,6 +1,6 @@ { "name": "vcpkg-ce.test", - "version": "0.7.0", + "version": "0.8.0", "description": "ce test project", "directories": { "doc": "docs" @@ -47,7 +47,7 @@ "shx": "0.3.4" }, "dependencies": { - "@microsoft/vcpkg-ce": "~0.7.0", + "@microsoft/vcpkg-ce": "~0.8.0", "yaml": "2.0.0-10", "semver": "7.3.5", "txtgen": "2.2.8" diff --git a/test/resources/cmake.yaml b/test/resources/cmake.yaml index 3166caa..caec926 100644 --- a/test/resources/cmake.yaml +++ b/test/resources/cmake.yaml @@ -16,52 +16,53 @@ contacts: email: kitware@kitware.com role: originator -windows and x64: - install: - unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip - sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c - strip: 1 # +demands: + windows and x64: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # -windows and x86: - install: - unzip: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-windows-i386.zip - sha256: 650026534e66dabe0ed6be3422e86fabce5fa86d43927171ea8b8dfd0877fc9d - strip: 1 # + windows and x86: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-windows-i386.zip + sha256: 650026534e66dabe0ed6be3422e86fabce5fa86d43927171ea8b8dfd0877fc9d + strip: 1 # -windows: - settings: - tools: - cmake: bin/cmake.exe - cmake_gui: bin/cmake-gui.exe - ctest: bin/ctest.exe + windows: + environment: + tools: + cmake: bin/cmake.exe + cmake_gui: bin/cmake-gui.exe + ctest: bin/ctest.exe - paths: - path: bin + paths: + path: bin -osx: - install: - untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-macos-universal.tar.gz - sha256: 89afcb79f58bb1f0bb840047c146c3fac8051829b6025c3dbe9b75799b27deb4 - strip: 3 + osx: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-macos-universal.tar.gz + sha256: 89afcb79f58bb1f0bb840047c146c3fac8051829b6025c3dbe9b75799b27deb4 + strip: 3 -linux and x64: - install: - untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-x86_64.tar.gz - sha256: B8C141BD7A6D335600AB0A8A35E75AF79F95B837F736456B5532F4D717F20A09 - strip: 1 + linux and x64: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-x86_64.tar.gz + sha256: B8C141BD7A6D335600AB0A8A35E75AF79F95B837F736456B5532F4D717F20A09 + strip: 1 -linux and arm64: - install: - untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-aarch64.tar.gz - sha256: 2761a222c14a15b9bdf1bdb4a17c10806757b7ed3bc26a84523f042ec212b76c - strip: 1 + linux and arm64: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-aarch64.tar.gz + sha256: 2761a222c14a15b9bdf1bdb4a17c10806757b7ed3bc26a84523f042ec212b76c + strip: 1 -not windows: - settings: - tools: - cmake: bin/cmake - cmake_gui: bin/cmake-gui - ctest: bin/ctest - - paths: - path: bin \ No newline at end of file + not windows: + exports: + tools: + cmake: bin/cmake + cmake_gui: bin/cmake-gui + ctest: bin/ctest + + paths: + path: bin \ No newline at end of file diff --git a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml index 4962c6e..755c856 100644 --- a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml +++ b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml @@ -18,7 +18,7 @@ windows: not windows: error: not yet supported -settings: +environment: tools: CC: bin/arm-none-eabi-gcc.exe CXX: bin/arm-none-eabi-g++.exe diff --git a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml index e6548af..4928f61 100644 --- a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml +++ b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml @@ -18,7 +18,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: CC: bin/arm-none-eabi-gcc.exe CXX: bin/arm-none-eabi-g++.exe diff --git a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml index 1900ea4..0c501b3 100644 --- a/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml +++ b/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml @@ -18,7 +18,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: CC: bin/arm-none-eabi-gcc.exe CXX: bin/arm-none-eabi-g++.exe diff --git a/test/resources/repo/sdks/microsoft/windows.x64.yaml b/test/resources/repo/sdks/microsoft/windows.x64.yaml index 017e1eb..d23b1b2 100644 --- a/test/resources/repo/sdks/microsoft/windows.x64.yaml +++ b/test/resources/repo/sdks/microsoft/windows.x64.yaml @@ -17,11 +17,11 @@ install: sha256: fluffyKittenBunnies x64: - settings: + exports: paths: - ./**/bin/x64 not x64: - settings: + exports: paths: - ./**/bin/x64 diff --git a/test/resources/repo/sdks/microsoft/windows.yaml b/test/resources/repo/sdks/microsoft/windows.yaml index 179a5af..ca8386b 100644 --- a/test/resources/repo/sdks/microsoft/windows.yaml +++ b/test/resources/repo/sdks/microsoft/windows.yaml @@ -16,19 +16,19 @@ install: nupkg: Microsoft.Windows.SDK.cpp/10.0.19041.5 sha256: fluffyKittenBunnies # https://www.nuget.org/api/v2/package/Microsoft.Windows.SDK.CPP/10.0.19041.5 +demands: + windows and target:x64: + requires: + sdks/microsoft/windows/x64: 10.0.19041 -windows and target:x64: - requires: - sdks/microsoft/windows/x64: 10.0.19041 + windows and target:x86: + requires: + sdks/microsoft/windows/x86: 10.0.19041 + + windows and target:arm: + requires: + sdks/microsoft/windows/arm: 10.0.19041 -windows and target:x86: - requires: - sdks/microsoft/windows/x86: 10.0.19041 - -windows and target:arm: - requires: - sdks/microsoft/windows/arm: 10.0.19041 - -windows and target:arm64: - requires: - sdks/microsoft/windows/arm64: 10.0.19041 + windows and target:arm64: + requires: + sdks/microsoft/windows/arm64: 10.0.19041 diff --git a/test/resources/repo/tools/kitware/cmake-3.15.0.yaml b/test/resources/repo/tools/kitware/cmake-3.15.0.yaml index 2ab2831..0a62226 100644 --- a/test/resources/repo/tools/kitware/cmake-3.15.0.yaml +++ b/test/resources/repo/tools/kitware/cmake-3.15.0.yaml @@ -26,7 +26,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: cmake: bin/cmake.exe diff --git a/test/resources/repo/tools/kitware/cmake-3.15.1.yaml b/test/resources/repo/tools/kitware/cmake-3.15.1.yaml index ac4700f..0584926 100644 --- a/test/resources/repo/tools/kitware/cmake-3.15.1.yaml +++ b/test/resources/repo/tools/kitware/cmake-3.15.1.yaml @@ -22,7 +22,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: cmake: bin/cmake.exe diff --git a/test/resources/repo/tools/kitware/cmake-3.17.0.yaml b/test/resources/repo/tools/kitware/cmake-3.17.0.yaml index ec76399..0d70d82 100644 --- a/test/resources/repo/tools/kitware/cmake-3.17.0.yaml +++ b/test/resources/repo/tools/kitware/cmake-3.17.0.yaml @@ -22,7 +22,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: cmake: bin/cmake.exe diff --git a/test/resources/repo/tools/kitware/cmake-3.19.0.yaml b/test/resources/repo/tools/kitware/cmake-3.19.0.yaml index 08208d5..7320e34 100644 --- a/test/resources/repo/tools/kitware/cmake-3.19.0.yaml +++ b/test/resources/repo/tools/kitware/cmake-3.19.0.yaml @@ -22,7 +22,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: cmake: bin/cmake.exe diff --git a/test/resources/repo/tools/kitware/cmake-3.20.0.yaml b/test/resources/repo/tools/kitware/cmake-3.20.0.yaml index e17ff34..6ac9bc2 100644 --- a/test/resources/repo/tools/kitware/cmake-3.20.0.yaml +++ b/test/resources/repo/tools/kitware/cmake-3.20.0.yaml @@ -22,7 +22,7 @@ windows: not windows: error: not yet supported -settings: +exports: tools: cmake: bin/cmake.exe diff --git a/test/resources/sample1.yaml b/test/resources/sample1.yaml index 5334034..454c157 100644 --- a/test/resources/sample1.yaml +++ b/test/resources/sample1.yaml @@ -20,13 +20,13 @@ requires: weird/range: '>= 1.0 <= 2.0 2.0.0' nuget/range: (2.0,3.0] 2.3.4 -settings: +exports: tools: CC: foo/bar/cl.exe CXX: bin/baz/cl.exe Whatever: some/tool/path/foo - variables: + environment: test: abc cxxflags: - foo=bar