diff --git a/ce/ce/amf/contact.ts b/ce/ce/amf/contact.ts index a4b3654725..0f905fbef7 100644 --- a/ce/ce/amf/contact.ts +++ b/ce/ce/amf/contact.ts @@ -3,7 +3,7 @@ import { Dictionary } from '../interfaces/collections'; import { Contact as IContact } from '../interfaces/metadata/contact'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { Entity } from '../yaml/Entity'; import { EntityMap } from '../yaml/EntityMap'; import { Strings } from '../yaml/strings'; @@ -15,7 +15,7 @@ export class Contact extends Entity implements IContact { readonly roles = new Strings(undefined, this, 'role'); /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['email', 'role']); yield* this.validateChild('email', 'string'); @@ -27,7 +27,7 @@ export class Contacts extends EntityMap implements Dict super(Contact, node, parent, key); } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); if (this.exists()) { for (const [key, contact] of this) { diff --git a/ce/ce/amf/demands.ts b/ce/ce/amf/demands.ts index 2c86513491..4356400e27 100644 --- a/ce/ce/amf/demands.ts +++ b/ce/ce/amf/demands.ts @@ -4,7 +4,7 @@ import { isMap, isScalar } from 'yaml'; import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { parseQuery } from '../mediaquery/media-query'; import { Session } from '../session'; import { Entity } from '../yaml/Entity'; @@ -30,7 +30,7 @@ export class Demands extends EntityMap { } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); for (const [mediaQuery, demandBlock] of this) { @@ -89,7 +89,7 @@ export class DemandBlock extends Entity { } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* this.validateChildKeys(['error', 'warning', 'message', 'seeAlso', 'requires', 'exports', 'install', 'unless']); yield* super.validate(); diff --git a/ce/ce/amf/exports.ts b/ce/ce/amf/exports.ts index 591f3a9a67..48f23b9ef8 100644 --- a/ce/ce/amf/exports.ts +++ b/ce/ce/amf/exports.ts @@ -3,7 +3,7 @@ import { Exports as IExports } from '../interfaces/metadata/exports'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { BaseMap } from '../yaml/BaseMap'; import { ScalarMap } from '../yaml/ScalarMap'; import { StringsMap } from '../yaml/strings'; @@ -20,7 +20,7 @@ export class Exports extends BaseMap implements IExports { contents: StringsMap = new StringsMap(undefined, this, 'contents'); /** @internal */ - override *validate(): Iterable { + 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/ce/amf/info.ts b/ce/ce/amf/info.ts index 47994a53f0..b57b65e566 100644 --- a/ce/ce/amf/info.ts +++ b/ce/ce/amf/info.ts @@ -3,35 +3,28 @@ import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; -import { Info as IInfo } from '../interfaces/metadata/info'; -import { ValidationError } from '../interfaces/validation-error'; +import { Validation } from '../interfaces/validation'; +import { ValidationMessage } from '../interfaces/validation-message'; import { Entity } from '../yaml/Entity'; -import { Flags } from '../yaml/Flags'; +import { Options } from '../yaml/Options'; -export class Info extends Entity implements IInfo { - get version(): string { return this.asString(this.getMember('version')) || ''; } - set version(value: string) { this.setMember('version', value); } - +export class Info extends Entity implements Validation { + // See corresponding properties in MetadataFile get id(): string { return this.asString(this.getMember('id')) || ''; } - set id(value: string) { this.setMember('id', value); } - get summary(): string | undefined { return this.asString(this.getMember('summary')); } - set summary(value: string | undefined) { this.setMember('summary', value); } + get version(): string { return this.asString(this.getMember('version')) || ''; } - get priority(): number | undefined { return this.asNumber(this.getMember('priority')) || 0; } - set priority(value: number | undefined) { this.setMember('priority', value); } + get summary(): string | undefined { return this.asString(this.getMember('summary')); } get description(): string | undefined { return this.asString(this.getMember('description')); } - set description(value: string | undefined) { this.setMember('description', value); } - readonly flags = new Flags(undefined, this, 'options'); + readonly options = new Options(undefined, this, 'options'); - get dependencyOnly(): boolean { return this.flags.has('dependencyOnly'); } - set dependencyOnly(value: boolean) { this.flags.set('dependencyOnly', value); } + get priority(): number { return this.asNumber(this.getMember('priority')) || 0; } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['version', 'id', 'summary', 'priority', 'description', 'options']); diff --git a/ce/ce/amf/installer.ts b/ce/ce/amf/installer.ts index 4233184114..d3d116b7a0 100644 --- a/ce/ce/amf/installer.ts +++ b/ce/ce/amf/installer.ts @@ -7,10 +7,10 @@ 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 { ValidationMessage } from '../interfaces/validation-message'; import { Entity } from '../yaml/Entity'; import { EntitySequence } from '../yaml/EntitySequence'; -import { Flags } from '../yaml/Flags'; +import { Options } from '../yaml/Options'; import { Strings } from '../yaml/strings'; import { Node, Yaml, YAMLDictionary } from '../yaml/yaml-types'; @@ -48,7 +48,7 @@ export class Installs extends EntitySequence { throw new Error('Unsupported node type'); } - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); for (const each of this) { yield* each.validate(); @@ -73,7 +73,7 @@ export class Installer extends Entity implements IInstaller { return this.asString(this.getMember('nametag')); } - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChild('lang', 'string'); yield* this.validateChild('nametag', 'string'); @@ -107,7 +107,7 @@ abstract class FileInstallerNode extends Installer { readonly transform = new Strings(undefined, this, 'transform'); - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChild('strip', 'number'); yield* this.validateChild('sha256', 'string'); @@ -119,7 +119,7 @@ class UnzipNode extends FileInstallerNode implements UnZipInstaller { override get installerKind() { return 'unzip'; } readonly location = new Strings(undefined, this, 'unzip'); - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['unzip', 'sha256', 'sha512', 'strip', 'transform', 'lang', 'nametag']); } @@ -128,7 +128,7 @@ class UnzipNode extends FileInstallerNode implements UnZipInstaller { class UnTarNode extends FileInstallerNode implements UnTarInstaller { override get installerKind() { return 'untar'; } location = new Strings(undefined, this, 'untar'); - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['untar', 'sha256', 'sha512', 'strip', 'transform']); } @@ -169,7 +169,7 @@ class NupkgNode extends Installer implements NupkgInstaller { } readonly transform = new Strings(undefined, this, 'transform'); - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['nupkg', 'sha256', 'sha512', 'strip', 'transform', 'lang', 'nametag']); } @@ -194,22 +194,22 @@ class GitCloneNode extends Installer implements GitInstaller { this.setMember('commit', value); } - private flags = new Flags(undefined, this, 'options'); + private options = new Options(undefined, this, 'options'); get full() { - return this.flags.has('full'); + return this.options.has('full'); } set full(value: boolean) { - this.flags.set('full', value); + this.options.set('full', value); } get recurse() { - return this.flags.has('recurse'); + return this.options.has('recurse'); } set recurse(value: boolean) { - this.flags.set('recurse', value); + this.options.set('recurse', value); } get subdirectory() { @@ -221,14 +221,14 @@ class GitCloneNode extends Installer implements GitInstaller { } get espidf() { - return this.flags.has('espidf'); + return this.options.has('espidf'); } set espidf(value: boolean) { - this.flags.set('espidf', value); + this.options.set('espidf', value); } - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); yield* this.validateChildKeys(['git', 'commit', 'subdirectory', 'options', 'lang', 'nametag']); yield* this.validateChild('commit', 'string'); diff --git a/ce/ce/amf/metadata-file.ts b/ce/ce/amf/metadata-file.ts index 5a2a96f66e..19b9e6724a 100644 --- a/ce/ce/amf/metadata-file.ts +++ b/ce/ce/amf/metadata-file.ts @@ -8,10 +8,11 @@ import { Registry } from '../artifacts/registry'; import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; import { Profile } from '../interfaces/metadata/metadata-format'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { Session } from '../session'; import { Uri } from '../util/uri'; import { BaseMap } from '../yaml/BaseMap'; +import { Options } from '../yaml/Options'; import { toYAML } from '../yaml/yaml'; import { Yaml, YAMLDictionary } from '../yaml/yaml-types'; import { Contacts } from './contact'; @@ -55,15 +56,48 @@ export class MetadataFile extends BaseMap implements Profile { return new MetadataFile(doc, filename, lc, registry).init(session); } - info = new Info(undefined, this, 'info'); + #info = new Info(undefined, this, 'info'); contacts = new Contacts(undefined, this, 'contacts'); registries = new Registries(undefined, this, 'registries'); globalSettings = new GlobalSettings(undefined, this, 'global'); - // rather than re-implement it, use encapsulatiob with a demand block + // rather than re-implement it, use encapsulation with a demand block private demandBlock = new DemandBlock(this.node, undefined); + /** Artifact identity + * + * this should be the 'path' to the artifact (following the guidelines) + * + * ie, 'compilers/microsoft/msvc' + * + * artifacts install to artifacts-root/// + */ + get id(): string { return this.asString(this.getMember('id')) || this.#info.id || ''; } + set id(value: string) { this.normalize(); this.setMember('id', value); } + + /** the version of this artifact */ + get version(): string { return this.asString(this.getMember('version')) || this.#info.version || ''; } + set version(value: string) { this.normalize(); this.setMember('version', value); } + + /** a short 1 line descriptive text */ + get summary(): string | undefined { return this.asString(this.getMember('summary')) || this.#info.summary; } + set summary(value: string | undefined) { this.normalize(); this.setMember('summary', value); } + + /** if a longer description is required, the value should go here */ + get description(): string | undefined { return this.asString(this.getMember('description')) || this.#info.description; } + set description(value: string | undefined) { this.normalize(); this.setMember('description', value); } + + readonly #options = new Options(undefined, this, 'options'); + + /** if true, intended to be used only as a dependency; for example, do not show in search results or lists */ + get dependencyOnly(): boolean { return this.#options.has('dependencyOnly') || this.#info.options.has('dependencyOnly'); } + get espidf(): boolean { return this.#options.has('espidf') || this.#info.options.has('espidf'); } + + /** higher priority artifacts should install earlier; the default is zero */ + get priority(): number { return this.asNumber(this.getMember('priority')) || this.#info.priority || 0; } + set priority(value: number) { this.normalize(); this.setMember('priority', value); } + get error(): string | undefined { return this.demandBlock.error; } set error(value: string | undefined) { this.demandBlock.error = value; } @@ -106,7 +140,7 @@ export class MetadataFile extends BaseMap implements Profile { throw new Error(`Unsupported file type ${extname(uri.path)}`); } if (!content || content === 'null') { - content = '{\n}'; + content = '{}\n'; } await uri.writeUTF8(content); } @@ -129,25 +163,30 @@ export class MetadataFile extends BaseMap implements Profile { } } - get isValid(): boolean { - return this.validationErrors.length === 0; - } - - #validationErrors!: Array; - get validationErrors(): Array { - if (this.#validationErrors) { - return this.#validationErrors; - } + formatVMessage(vMessage: ValidationMessage): string { + const message = vMessage.message; + const range = vMessage.range; + const rangeOffset = vMessage.rangeOffset; + const category = vMessage.category; + const r = Array.isArray(range) ? range : range?.sourcePosition(); + const { line, column } = this.positionAt(r, rangeOffset); - const errs = new Set(); - for (const { message, range, rangeOffset, category } of this.validate()) { - const r = Array.isArray(range) ? range : range?.sourcePosition(); + return this.formatMessage(category, message, line, column); + } - const { line, column } = this.positionAt(r, rangeOffset); - errs.add(this.formatMessage(category, message, line, column)); + *deprecationWarnings(): Iterable { + const node = this.node; + if (node) { + const info = node.get('info'); + if (info) { + const infoNode = info; + yield { + message: i`The info block is deprecated for consistency with vcpkg.json; move info members to the outside.`, + range: infoNode.range || undefined, + category: ErrorKind.InfoBlockPresent + }; + } } - this.#validationErrors = [...errs]; - return this.#validationErrors; } private positionAt(range?: [number, number, number?], offset?: { line: number, column: number }) { @@ -164,17 +203,44 @@ export class MetadataFile extends BaseMap implements Profile { } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); - yield* this.validateChildKeys(['info', 'contacts', 'registries', 'global', 'demands', 'apply', 'exports', 'requires', 'install', 'seeAlso', 'unless']); + const hasInfo = this.document.has('info'); + const allowedChildren = ['contacts', 'registries', 'global', 'demands', 'apply', 'exports', 'requires', 'install', 'seeAlso', 'unless']; - // verify that we have info - if (!this.document.has('info')) { - yield { message: i`Missing section '${'info'}'`, range: this, category: ErrorKind.SectionNotFound }; + if (hasInfo) { + // 2022-06-17 and earlier used a separate 'info' block for these fields + allowedChildren.push('info'); } else { - yield* this.info.validate(); + allowedChildren.push('version', 'id', 'summary', 'priority', 'description', 'options'); } + yield* this.validateChildKeys(allowedChildren); + + if (hasInfo) { + yield* this.#info.validate(); + } else { + if (!this.has('id')) { + yield { message: i`Missing identity '${'id'}'`, range: this, category: ErrorKind.FieldMissing }; + } else if (!this.childIs('id', 'string')) { + yield { message: i`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 '${'version'}'`, range: this, category: ErrorKind.FieldMissing }; + } else if (!this.childIs('version', 'string')) { + yield { message: i`version should be of type 'string', found '${this.kind('version')}'`, range: this.sourcePosition('version'), category: ErrorKind.IncorrectType }; + } + if (this.childIs('summary', 'string') === false) { + yield { message: i`summary should be of type 'string', found '${this.kind('summary')}'`, range: this.sourcePosition('summary'), category: ErrorKind.IncorrectType }; + } + if (this.childIs('description', 'string') === false) { + yield { message: i`description should be of type 'string', found '${this.kind('description')}'`, range: this.sourcePosition('description'), category: ErrorKind.IncorrectType }; + } + if (this.childIs('options', 'sequence') === false) { + yield { message: i`options should be a sequence, found '${this.kind('options')}'`, range: this.sourcePosition('options'), category: ErrorKind.IncorrectType }; + } + } if (this.document.has('contacts')) { for (const each of this.contacts.values) { @@ -183,14 +249,12 @@ export class MetadataFile extends BaseMap implements Profile { } const set = new Set(); - for (const [mediaQuery, demandBlock] of this.conditionalDemands) { - if (set.has(mediaQuery)) { yield { message: i`Duplicate keys detected in manifest: '${mediaQuery}'`, range: demandBlock, category: ErrorKind.DuplicateKey }; } - set.add(mediaQuery); + set.add(mediaQuery); yield* demandBlock.validate(); } yield* this.conditionalDemands.validate(); @@ -203,9 +267,28 @@ export class MetadataFile extends BaseMap implements Profile { yield* this.seeAlso.validate(); } + normalize() { + if (!this.node) { return; } + if (this.document.has('info')) { + this.setMember('id', this.#info.id); + this.setMember('version', this.#info.version); + this.setMember('summary', this.#info.summary); + this.setMember('description', this.#info.description); + const maybeOptions = this.#info.options.node?.items; + if (maybeOptions) { + for (const option of maybeOptions) { + this.#options.set(option.value, true); + } + } + + this.setMember('priority', this.#info.priority); + this.node.delete('info'); + } + } + /** @internal */override assert(recreateIfDisposed = false, node = this.node): asserts this is Yaml & { node: YAMLDictionary } { if (!isMap(this.node)) { - this.document = parseDocument('{\n}', { prettyErrors: false, lineCounter: this.context.lineCounter, strict: true }); + this.document = parseDocument('{}\n', { prettyErrors: false, lineCounter: this.context.lineCounter, strict: true }); this.node = >this.document.contents; } } diff --git a/ce/ce/amf/registries.ts b/ce/ce/amf/registries.ts index 51ff8ce5ba..fbb77db6fb 100644 --- a/ce/ce/amf/registries.ts +++ b/ce/ce/amf/registries.ts @@ -6,7 +6,7 @@ import { Dictionary } from '../interfaces/collections'; import { ErrorKind } from '../interfaces/error-kind'; import { RegistryDeclaration } from '../interfaces/metadata/metadata-format'; import { Registry as IRegistry } from '../interfaces/metadata/registries/artifact-registry'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { isFilePath, Uri } from '../util/uri'; import { Entity } from '../yaml/Entity'; import { Strings } from '../yaml/strings'; @@ -159,7 +159,7 @@ export class Registries extends Yaml implements D return undefined; } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); if (this.exists()) { for (const [key, registry] of this) { @@ -175,7 +175,7 @@ export class Registry extends Entity implements IRegistry { set registryKind(value: string | undefined) { this.setMember('kind', value); } /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); // if (this.registryKind === undefined) { @@ -191,7 +191,7 @@ export class Registry extends Entity implements IRegistry { class LocalRegistry extends Registry { readonly location = new Strings(undefined, this, 'location'); /** @internal */ - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); // @@ -207,7 +207,7 @@ class LocalRegistry extends Registry { class RemoteRegistry extends Registry { readonly location = new Strings(undefined, this, 'location'); - override *validate(): Iterable { + override *validate(): Iterable { yield* super.validate(); // diff --git a/ce/ce/artifacts/activation.ts b/ce/ce/artifacts/activation.ts index fef8752d83..6d8b91fc56 100644 --- a/ce/ce/artifacts/activation.ts +++ b/ce/ce/artifacts/activation.ts @@ -561,7 +561,7 @@ export class Activation { const propertyGroup = { $Label: 'Artifacts', Artifacts: { Artifact: [] } }; for (const artifact of artifacts) { - propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.info.id, '#text': artifact.targetLocation.fsPath }); + propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.id, '#text': artifact.targetLocation.fsPath }); } if (propertyGroup.Artifacts.Artifact.length > 0) { diff --git a/ce/ce/artifacts/artifact.ts b/ce/ce/artifacts/artifact.ts index 4f3dfeb921..04d1cd0c49 100644 --- a/ce/ce/artifacts/artifact.ts +++ b/ce/ce/artifacts/artifact.ts @@ -27,7 +27,7 @@ export type Selection = [Artifact, ID, VersionRange] export class ArtifactMap extends Map{ get artifacts() { - return Array.from([...linq.values(this).select(([artifact, id, range]) => artifact)].sort((a, b) => (b.metadata.info.priority || 0) - (a.metadata.info.priority || 0))); + return Array.from([...linq.values(this).select(([artifact, id, range]) => artifact)].sort((a, b) => b.metadata.priority - a.metadata.priority)); } } @@ -99,7 +99,7 @@ export class Artifact extends ArtifactBase { } get id() { - return this.metadata.info.id; + return this.metadata.id; } get reference() { @@ -107,7 +107,7 @@ export class Artifact extends ArtifactBase { } get version() { - return this.metadata.info.version; + return this.metadata.version; } get isInstalled() { @@ -181,10 +181,6 @@ export class Artifact extends ArtifactBase { } } - get name() { - return `${this.metadata.info.id.replace(/[^\w]+/g, '.')}-${this.metadata.info.version}`; - } - async writeManifest() { await this.targetLocation.createDirectory(); await this.metadata.save(this.targetLocation.join('artifact.yaml')); @@ -235,7 +231,7 @@ export class Artifact extends ArtifactBase { } // if espressif install - if (this.metadata.info.flags.has('espidf')) { + if (this.metadata.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); diff --git a/ce/ce/artifacts/registry.ts b/ce/ce/artifacts/registry.ts index bb7d0c5f74..50d08e4aca 100644 --- a/ce/ce/artifacts/registry.ts +++ b/ce/ce/artifacts/registry.ts @@ -21,5 +21,5 @@ export interface Registry { load(force?: boolean): Promise; save(): Promise; update(): Promise; - regenerate(): Promise; + regenerate(normalize?: boolean): Promise; } diff --git a/ce/ce/cli/artifacts.ts b/ce/ce/cli/artifacts.ts index 0185517b2e..f0594a0966 100644 --- a/ce/ce/cli/artifacts.ts +++ b/ce/ce/cli/artifacts.ts @@ -16,13 +16,11 @@ export async function showArtifacts(artifacts: Iterable, options?: { f const table = new Table(i`Artifact`, i`Version`, i`Status`, i`Dependency`, i`Summary`); for (const artifact of artifacts) { const name = artifactIdentity(artifact.registryId, artifact.id, artifact.shortName); - if (!artifact.metadata.isValid) { + for (const err of artifact.metadata.validate()) { failing = true; - for (const err of artifact.metadata.validationErrors) { - error(err); - } + error(artifact.metadata.formatVMessage(err)); } - table.push(name, artifact.version, options?.force || await artifact.isInstalled ? 'installed' : 'will install', artifact.isPrimary ? ' ' : '*', artifact.metadata.info.summary || ''); + table.push(name, artifact.version, options?.force || await artifact.isInstalled ? 'installed' : 'will install', artifact.isPrimary ? ' ' : '*', artifact.metadata.summary || ''); } log(table.toString()); log(); @@ -161,7 +159,7 @@ export async function installArtifacts(session: Session, artifacts: Array outputAmf.formatVMessage(error)); + if (errors.length !== 0) { + const errorMsg = errors.join('\n'); + session.channels.warning(i`After transformation, ${inputPath} did not result in a valid AMF; skipping:\n${outputContent}\n${errorMsg}`); return 0; } - const outputId = outputAmf.info.id; + const outputId = outputAmf.id; const outputIdLast = outputId.slice(outputId.lastIndexOf('/')); - const outputVersion = outputAmf.info.version; + const outputVersion = outputAmf.version; const outputRelativePath = `${outputId}/${outputIdLast}-${outputVersion}.yaml`; const outputFullPath = repoRoot.join(outputRelativePath); let doWrite = true; diff --git a/ce/ce/cli/commands/find.ts b/ce/ce/cli/commands/find.ts index f986054386..720cdb1b35 100644 --- a/ce/ce/cli/commands/find.ts +++ b/ce/ce/cli/commands/find.ts @@ -57,9 +57,9 @@ export class FindCommand extends Command { artifacts = [artifacts[0]]; } for (const result of artifacts) { - if (!result.metadata.info.dependencyOnly) { + if (!result.metadata.dependencyOnly) { const name = artifactIdentity(result.registryId, id, result.shortName); - table.push(name, result.metadata.info.version, result.metadata.info.summary || ''); + table.push(name, result.metadata.version, result.metadata.summary || ''); } } } diff --git a/ce/ce/cli/commands/list.ts b/ce/ce/cli/commands/list.ts index 0838b06dd6..2bf8e4b054 100644 --- a/ce/ce/cli/commands/list.ts +++ b/ce/ce/cli/commands/list.ts @@ -32,9 +32,8 @@ export class ListCommand extends Command { const table = new Table('Artifact', 'Version', 'Summary'); for (const { artifact, id, folder } of artifacts) { - const name = artifactIdentity('', id); //todo: fixme - table.push(name, artifact.version, artifact.metadata.info.summary || ''); + table.push(name, artifact.version, artifact.metadata.summary || ''); } log(table.toString()); log(); @@ -45,4 +44,4 @@ export class ListCommand extends Command { return true; } -} \ No newline at end of file +} diff --git a/ce/ce/cli/commands/regenerate-index.ts b/ce/ce/cli/commands/regenerate-index.ts index 5c0e1c7b88..95c508a090 100644 --- a/ce/ce/cli/commands/regenerate-index.ts +++ b/ce/ce/cli/commands/regenerate-index.ts @@ -10,6 +10,7 @@ import { Uri } from '../../util/uri'; import { Command } from '../command'; import { cli } from '../constants'; import { error, log, writeException } from '../styling'; +import { Normalize } from '../switches/normalize'; import { Project } from '../switches/project'; import { Registry as RegSwitch } from '../switches/registry'; import { WhatIf } from '../switches/whatIf'; @@ -19,6 +20,7 @@ export class RegenerateCommand extends Command { project = new Project(this); readonly aliases = ['regen']; readonly regSwitch = new RegSwitch(this, { required: true }); + readonly normalize = new Normalize(this); seeAlso = []; argumentsHelp = []; @@ -57,7 +59,7 @@ export class RegenerateCommand extends Command { return false; } log(i`Regenerating index for ${registry.location.formatted}`); - await registry.regenerate(); + await registry.regenerate(!!this.normalize); if (registry.count) { await registry.save(); log(i`Regeneration complete. Index contains ${registry.count} metadata files`); @@ -79,4 +81,4 @@ export class RegenerateCommand extends Command { } return true; } -} \ No newline at end of file +} diff --git a/ce/ce/cli/switches/normalize.ts b/ce/ce/cli/switches/normalize.ts new file mode 100644 index 0000000000..2343806d5c --- /dev/null +++ b/ce/ce/cli/switches/normalize.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Normalize extends Switch { + switch = 'normalize'; + override multipleAllowed = false; + get help() { + return [ + i`Apply any deprecation fixups.` + ]; + } +} diff --git a/ce/ce/interfaces/error-kind.ts b/ce/ce/interfaces/error-kind.ts index 4ab3d717db..625dc68534 100644 --- a/ce/ce/interfaces/error-kind.ts +++ b/ce/ce/interfaces/error-kind.ts @@ -13,4 +13,5 @@ export enum ErrorKind { InvalidDefinition = 'InvalidDefinition', InvalidChild = 'InvalidChild', InvalidExpression = 'InvalidExpression', + InfoBlockPresent = 'InfoBlockPresent', } diff --git a/ce/ce/interfaces/metadata/info.ts b/ce/ce/interfaces/metadata/info.ts deleted file mode 100644 index 83a6499b3f..0000000000 --- a/ce/ce/interfaces/metadata/info.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Validation } from '../validation'; - -/** Canonical Information about this artifact */ - -export interface Info extends Validation { - /** Artifact identity - * - * this should be the 'path' to the artifact (following the guidelines) - * - * ie, 'compilers/microsoft/msvc' - * - * FYI: artifacts install to $VCPKG_ROOT// or if from another artifact source: $VCPKG_ROOT/// - */ - id: string; - - /** the version of this artifact */ - version: string; - - /** a short 1 line descriptive text */ - summary?: string; - - /** if a longer description is required, the value should go here */ - description?: string; - - /** if true, intended to be used only as a dependency; for example, do not show in search results or lists */ - dependencyOnly: boolean; - - /** higher priority artifacts should install earlier */ - priority?: number; -} diff --git a/ce/ce/interfaces/metadata/profile-base.ts b/ce/ce/interfaces/metadata/profile-base.ts index 92d2cdd5db..1077ff0170 100644 --- a/ce/ce/interfaces/metadata/profile-base.ts +++ b/ce/ce/interfaces/metadata/profile-base.ts @@ -4,7 +4,6 @@ import { Dictionary } from '../collections'; import { Contact } from './contact'; import { Demands } from './demands'; -import { Info } from './info'; import { RegistryDeclaration } from './metadata-format'; @@ -21,9 +20,6 @@ type Primitive = string | number | boolean; */ export interface ProfileBase extends Demands { - /** this profile/package information/metadata */ - info: Info; - /** any contact information related to this profile/package */ contacts: Dictionary; // optional @@ -38,10 +34,4 @@ export interface ProfileBase extends Demands { /** parsing errors in this document */ readonly formatErrors: Array; - - /** does the document pass validation checks? */ - readonly isValid: boolean; - - /** what are the valiation check errors? */ - readonly validationErrors: Array; } diff --git a/ce/ce/interfaces/validation-error.ts b/ce/ce/interfaces/validation-message.ts similarity index 89% rename from ce/ce/interfaces/validation-error.ts rename to ce/ce/interfaces/validation-message.ts index 2bf7dbf600..a8e8ef0e64 100644 --- a/ce/ce/interfaces/validation-error.ts +++ b/ce/ce/interfaces/validation-message.ts @@ -3,8 +3,7 @@ import { ErrorKind } from './error-kind'; - -export interface ValidationError { +export interface ValidationMessage { message: string; range?: [number, number, number] | { sourcePosition(): [number, number, number] | undefined }; rangeOffset?: { line: number; column: number; }; diff --git a/ce/ce/interfaces/validation.ts b/ce/ce/interfaces/validation.ts index 7689fdea6b..fa5ef7f7db 100644 --- a/ce/ce/interfaces/validation.ts +++ b/ce/ce/interfaces/validation.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ValidationError } from './validation-error'; - +import { ValidationMessage } from './validation-message'; export interface Validation { /** @@ -10,5 +9,5 @@ export interface Validation { * * actively validate this node. */ - validate(): Iterable; + validate(): Iterable; } diff --git a/ce/ce/registries/ArtifactRegistry.ts b/ce/ce/registries/ArtifactRegistry.ts index a5cac55517..74356dcb6f 100644 --- a/ce/ce/registries/ArtifactRegistry.ts +++ b/ce/ce/registries/ArtifactRegistry.ts @@ -43,7 +43,7 @@ export abstract class ArtifactRegistry implements Registry { abstract update(): Promise; - async regenerate(): Promise { + async regenerate(normalize?: boolean): Promise { // reset the index to blank. this.index = new Index(ArtifactIndex); @@ -65,20 +65,35 @@ export abstract class ArtifactRegistry implements Registry { for (const err of amf.formatErrors) { repo.session.channels.warning(`Parse errors in metadata file ${err}}`); } - throw new Error('invalid yaml'); + throw new Error('invalid format'); } - amf.validate(); + let anyErrors = false; + for (const err of amf.validate()) { + repo.session.channels.warning(amf.formatVMessage(err)); + anyErrors = true; + } - if (!amf.isValid) { - for (const err of amf.validationErrors) { - repo.session.channels.warning(err); - } + if (anyErrors) { throw new Error('invalid manifest'); } + + let fileUpdated = false; + for (const warning of amf.deprecationWarnings()) { + if (normalize) { + amf.normalize(); + fileUpdated = true; + } else { + repo.session.channels.warning(amf.formatVMessage(warning)); + } + } + repo.session.channels.debug(`Inserting ${uri.formatted} into index.`); repo.index.insert(amf, repo.cacheFolder.relative(uri)); + if (fileUpdated) { + await amf.save(uri); + } } catch (e: any) { repo.session.channels.debug(e.toString()); repo.session.channels.warning(`skipping invalid metadata file ${uri.fsPath}`); @@ -130,11 +145,11 @@ export abstract class ArtifactRegistry implements Registry { private async openArtifact(manifestPath: string, parent: Registries): Promise { const metadata = await MetadataFile.parseMetadata(this.cacheFolder.join(manifestPath), this.session, this); - + const id = metadata.id; return new Artifact(this.session, metadata, - this.index.indexSchema.id.getShortNameOf(metadata.info.id) || metadata.info.id, - this.installationFolder.join(metadata.info.id.replace(/[^\w]+/g, '.'), metadata.info.version), + this.index.indexSchema.id.getShortNameOf(id) || id, + this.installationFolder.join(id.replace(/[^\w]+/g, '.'), metadata.version), parent.getRegistryName(this), this.location ).init(this.session); @@ -147,10 +162,10 @@ export abstract class ArtifactRegistry implements Registry { await manifestPaths.forEachAsync(async (manifest) => metadataFiles.push(await this.openArtifact(manifest, parent))).done; // sort the contents by version before grouping. (descending version) - metadataFiles = metadataFiles.sort((a, b) => compare(b.metadata.info.version, a.metadata.info.version)); + metadataFiles = metadataFiles.sort((a, b) => compare(b.metadata.version, a.metadata.version)); // return a map. - return metadataFiles.groupByMap(m => m.metadata.info.id, artifact => artifact); + return metadataFiles.groupByMap(m => m.metadata.id, artifact => artifact); } async save(): Promise { diff --git a/ce/ce/registries/artifact-index.ts b/ce/ce/registries/artifact-index.ts index f9c84d00ba..94c719668e 100644 --- a/ce/ce/registries/artifact-index.ts +++ b/ce/ce/registries/artifact-index.ts @@ -7,7 +7,7 @@ import { IdentityKey, IndexSchema, SemverKey, StringKey } from './indexer'; export class ArtifactIndex extends IndexSchema { - id = new IdentityKey(this, (i) => i.info.id); - version = new SemverKey(this, (i) => new SemVer(i.info.version)); - summary = new StringKey(this, (i) => i.info.summary); + id = new IdentityKey(this, (i) => i.id); + version = new SemverKey(this, (i) => new SemVer(i.version)); + summary = new StringKey(this, (i) => i.summary); } diff --git a/ce/ce/registries/standard-registry.ts b/ce/ce/registries/standard-registry.ts index 0c3b0b2a70..d5aeb308cd 100644 --- a/ce/ce/registries/standard-registry.ts +++ b/ce/ce/registries/standard-registry.ts @@ -17,11 +17,10 @@ export async function isIndexFile(uri: Uri): Promise { export async function isMetadataFile(uri: Uri, session: Session): Promise { if (await uri.isFile()) { try { - return (await MetadataFile.parseMetadata(uri, session))?.info?.exists(); + return (await MetadataFile.parseMetadata(uri, session))?.node?.has('id') || false; } catch { // nope. no worries. } } return false; } - diff --git a/ce/ce/session.ts b/ce/ce/session.ts index 99be02e2b2..8ec5ed7eea 100644 --- a/ce/ce/session.ts +++ b/ce/ce/session.ts @@ -363,7 +363,7 @@ export class Session { const metadata = await MetadataFile.parseMetadata(folder.join('artifact.yaml'), this); result.push({ folder, - id: metadata.info.id, + id: metadata.id, artifact: await new InstalledArtifact(this, metadata).init(this) }); } catch { diff --git a/ce/ce/util/checks.ts b/ce/ce/util/checks.ts index e97188209c..2a3c4a077f 100644 --- a/ce/ce/util/checks.ts +++ b/ce/ce/util/checks.ts @@ -4,7 +4,7 @@ import { isScalar, isSeq, YAMLMap } from 'yaml'; import { i } from '../i18n'; import { ErrorKind } from '../interfaces/error-kind'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { Uri } from './uri'; /** @internal */ @@ -19,8 +19,8 @@ export function isPrimitive(value: any): value is (string | number | boolean) { } /** @internal */ -export function isNullish(value: any): value is null | undefined { - return value === null || value === undefined || value === ''; +export function isNullish(value: any): value is null | undefined | '' | 0 { + return value === null || value === undefined || value === '' || value === 0; } /** @internal */ @@ -28,7 +28,7 @@ export function isIterable(source: any): source is Iterable { return !!source && typeof (source) !== 'string' && !!source[Symbol.iterator]; } -export function* checkOptionalString(parent: YAMLMap, range: [number, number, number], name: string): Iterable { +export function* checkOptionalString(parent: YAMLMap, range: [number, number, number], name: string): Iterable { switch (typeof parent.get(name)) { case 'string': case 'undefined': @@ -38,7 +38,7 @@ export function* checkOptionalString(parent: YAMLMap, range: [number, number, nu } } -export function* checkOptionalBool(parent: YAMLMap, range: [number, number, number], name: string): Iterable { +export function* checkOptionalBool(parent: YAMLMap, range: [number, number, number], name: string): Iterable { switch (typeof parent.get(name)) { case 'boolean': case 'undefined': @@ -63,7 +63,7 @@ function checkOptionalArrayOfStringsImpl(parent: YAMLMap, range: [number, number return false; } -export function* checkOptionalArrayOfStrings(parent: YAMLMap, range: [number, number, number], name: string): Iterable { +export function* checkOptionalArrayOfStrings(parent: YAMLMap, range: [number, number, number], name: string): Iterable { if (checkOptionalArrayOfStringsImpl(parent, range, name)) { yield { message: i`${name} must be an array of strings, or unset`, range: range, category: ErrorKind.IncorrectType }; } diff --git a/ce/ce/yaml/CustomScalarMap.ts b/ce/ce/yaml/CustomScalarMap.ts index 420a571d95..d419871e6b 100644 --- a/ce/ce/yaml/CustomScalarMap.ts +++ b/ce/ce/yaml/CustomScalarMap.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { isScalar, Scalar } from 'yaml'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { BaseMap } from './BaseMap'; import { EntityFactory, Yaml, YAMLDictionary } from './yaml-types'; @@ -53,7 +53,7 @@ export /** @internal */ class CustomScalarMap> ext this.node.set(key, new Scalar(value)); } - override *validate(): Iterable { + override *validate(): Iterable { yield* this.validateIsObject(); } } diff --git a/ce/ce/yaml/Entity.ts b/ce/ce/yaml/Entity.ts index f0391427d7..a288857152 100644 --- a/ce/ce/yaml/Entity.ts +++ b/ce/ce/yaml/Entity.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { isMap, isScalar, isSeq } from 'yaml'; -import { ValidationError } from '../interfaces/validation-error'; +import { isMap, isScalar, isSeq, Scalar } from 'yaml'; +import { ValidationMessage } from '../interfaces/validation-message'; import { isNullish } from '../util/checks'; import { Node, Primitive, Yaml, YAMLDictionary } from './yaml-types'; @@ -21,15 +21,14 @@ export /** @internal */ class Entity extends Yaml { return; } - this.node.set(name, value); + this.node.set(name, new Scalar(value)); } protected getMember(name: string): Primitive | undefined { - return this.exists() ? this.node?.get(name, false) : undefined; } - override /** @internal */ *validate(): Iterable { + override /** @internal */ *validate(): Iterable { yield* super.validate(); yield* this.validateIsObject(); } diff --git a/ce/ce/yaml/EntityMap.ts b/ce/ce/yaml/EntityMap.ts index fb63b24f45..22a94b7ad5 100644 --- a/ce/ce/yaml/EntityMap.ts +++ b/ce/ce/yaml/EntityMap.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Dictionary } from '../interfaces/collections'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { BaseMap } from './BaseMap'; import { EntityFactory, Node, Yaml, YAMLDictionary } from './yaml-types'; @@ -27,7 +27,7 @@ export /** @internal */ abstract class EntityMap { + override *validate(): Iterable { yield* super.validate(); yield* this.validateIsObject(); } diff --git a/ce/ce/yaml/Flags.ts b/ce/ce/yaml/Flags.ts deleted file mode 100644 index 3f9c073b9a..0000000000 --- a/ce/ce/yaml/Flags.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Scalar } from 'yaml'; -import { ValidationError } from '../interfaces/validation-error'; -import { Yaml, YAMLSequence } from './yaml-types'; - - -export /** @internal */ class Flags extends Yaml { - - has(flag: string) { - if (this.node) { - return this.node.items.some(each => each.value === flag); - } - return false; - } - - set(flag: string, value: boolean) { - this.assert(true); - if (value) { - this.node.add(new Scalar(flag)); - } else { - this.node.delete(flag); - } - } - - override *validate(): Iterable { - yield* super.validate(); - yield* this.validateIsSequence(); - } -} diff --git a/ce/ce/yaml/Options.ts b/ce/ce/yaml/Options.ts new file mode 100644 index 0000000000..b8f06ce316 --- /dev/null +++ b/ce/ce/yaml/Options.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Scalar } from 'yaml'; +import { ValidationMessage } from '../interfaces/validation-message'; +import { Yaml, YAMLSequence } from './yaml-types'; + + +export /** @internal */ class Options extends Yaml { + + has(option: string) { + if (this.node) { + return this.node.items.some(each => each.value === option); + } + return false; + } + + set(option: string, value: boolean) { + this.assert(true); + if (value) { + this.node.add(new Scalar(option)); + } else { + this.node.delete(option); + } + } + + override *validate(): Iterable { + yield* super.validate(); + yield* this.validateIsSequence(); + } +} diff --git a/ce/ce/yaml/ScalarSequence.ts b/ce/ce/yaml/ScalarSequence.ts index f78c75e6b3..6a996803ed 100644 --- a/ce/ce/yaml/ScalarSequence.ts +++ b/ce/ce/yaml/ScalarSequence.ts @@ -3,7 +3,7 @@ import { isScalar, isSeq, Scalar, YAMLSeq } from 'yaml'; import { ErrorKind } from '../interfaces/error-kind'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { Primitive, Yaml, YAMLScalar, YAMLSequence } from './yaml-types'; /** @@ -120,7 +120,7 @@ export /** @internal */ class ScalarSequence extends this.dispose(true); } - override *validate(): Iterable { + override *validate(): Iterable { if (this.node && !isSeq(this.node) && !isScalar(this.node)) { yield { message: `'${this.fullName}' is not an sequence or primitive value`, diff --git a/ce/ce/yaml/yaml-types.ts b/ce/ce/yaml/yaml-types.ts index 633701a73d..02f3ffa68e 100644 --- a/ce/ce/yaml/yaml-types.ts +++ b/ce/ce/yaml/yaml-types.ts @@ -3,7 +3,7 @@ import { isCollection, isMap, isScalar, isSeq, Scalar, YAMLMap, YAMLSeq } from 'yaml'; import { ErrorKind } from '../interfaces/error-kind'; -import { ValidationError } from '../interfaces/validation-error'; +import { ValidationMessage } from '../interfaces/validation-message'; import { isNullish } from '../util/checks'; export class YAMLDictionary extends YAMLMap { } @@ -35,7 +35,7 @@ export /** @internal */ abstract class Yaml { } /** - * Coersion function to string + * Coercion function to string * * This will pass the coercion up to the parent if it exists * (or otherwise overridden in the subclass) @@ -51,7 +51,7 @@ export /** @internal */ abstract class Yaml { } /** - * Coersion function to number + * Coercion function to number * * This will pass the coercion up to the parent if it exists * (or otherwise overridden in the subclass) @@ -70,7 +70,7 @@ export /** @internal */ abstract class Yaml { } /** - * Coersion function to boolean + * Coercion function to boolean * * This will pass the coercion up to the parent if it exists * (or otherwise overridden in the subclass) @@ -89,7 +89,7 @@ export /** @internal */ abstract class Yaml { } /** - * Coersion function to any primitive + * Coercion function to any primitive * * This will pass the coercion up to the parent if it exists * (or otherwise overridden in the subclass) @@ -269,11 +269,11 @@ export /** @internal */ abstract class Yaml { throw new Error('this node does not have children.'); } - *validate(): Iterable { + *validate(): Iterable { // shh. } - protected *validateChildKeys(keys: Array): Iterable { + protected *validateChildKeys(keys: Array): Iterable { if (isMap(this.node)) { for (const key of this.keys) { if (keys.indexOf(key) === -1) { @@ -287,7 +287,7 @@ export /** @internal */ abstract class Yaml { } } - protected *validateIsObject(): Iterable { + protected *validateIsObject(): Iterable { if (this.node && !isMap(this.node)) { yield { message: `'${this.fullName}' is not an object`, @@ -296,7 +296,7 @@ export /** @internal */ abstract class Yaml { }; } } - protected *validateIsSequence(): Iterable { + protected *validateIsSequence(): Iterable { if (this.node && !isSeq(this.node)) { yield { message: `'${this.fullName}' is not an object`, @@ -306,7 +306,7 @@ export /** @internal */ abstract class Yaml { } } - protected *validateIsSequenceOrPrimitive(): Iterable { + protected *validateIsSequenceOrPrimitive(): Iterable { if (this.node && (!isSeq(this.node) && !isScalar(this.node))) { yield { message: `'${this.fullName}' is not a sequence or value`, @@ -316,7 +316,7 @@ export /** @internal */ abstract class Yaml { } } - protected *validateIsObjectOrPrimitive(): Iterable { + protected *validateIsObjectOrPrimitive(): Iterable { if (this.node && (!isMap(this.node) && !isScalar(this.node))) { yield { message: `'${this.fullName}' is not an object or value`, @@ -326,7 +326,7 @@ export /** @internal */ abstract class Yaml { } } - protected *validateChild(child: string, kind: 'string' | 'boolean' | 'number'): Iterable { + 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); diff --git a/ce/test/core/amf-tests.ts b/ce/test/core/amf-tests.ts index 23cb7ffe2d..266d250a4c 100644 --- a/ce/test/core/amf-tests.ts +++ b/ce/test/core/amf-tests.ts @@ -23,10 +23,10 @@ describe('Amf', () => { const doc = await MetadataFile.parseConfiguration('./sample1.yaml', content, local.session); strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); - strict.ok(doc.isValid, 'Is it valid?'); + strict.sequenceEqual(doc.validate(), []); - strict.equal(doc.info.id, 'sample1', 'identity incorrect'); - strict.equal(doc.info.version, '1.2.3', 'version incorrect'); + strict.equal(doc.id, 'sample1', 'name incorrect'); + strict.equal(doc.version, '1.2.3', 'version incorrect'); }); it('reads file with nupkg', async () => { @@ -34,33 +34,28 @@ describe('Amf', () => { const doc = await MetadataFile.parseConfiguration('./windows.yaml', content, local.session); strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); - strict.ok(doc.isValid, 'Is it valid?'); + strict.sequenceEqual(doc.validate(), []); SuiteLocal.log(doc.content); }); - it('load/persist environment.yaml', async () => { - const content = await (await readFile(join(rootFolder(), 'resources', 'environment.yaml'))).toString('utf-8'); - const doc = await MetadataFile.parseConfiguration('./cenvironment.yaml', content, local.session); + it('load/persist an artifact', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'example-artifact.json'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./example-artifact.json', content, local.session); SuiteLocal.log(doc.content); - for (const each of doc.validationErrors) { - SuiteLocal.log(each); + strict.ok(doc.isFormatValid, 'Ensure it\'s valid'); + for (const each of doc.validate()) { + SuiteLocal.log(doc.formatVMessage(each)); } - - strict.ok(doc.isFormatValid, 'Ensure it\'s valid yaml'); - strict.ok(doc.isValid, 'better be valid!'); - - SuiteLocal.log(doc.content); }); it('profile checks', async () => { const content = await (await readFile(join(rootFolder(), 'resources', 'sample1.yaml'))).toString('utf-8'); const doc = await MetadataFile.parseConfiguration('./sample1.yaml', content, local.session); - strict.ok(doc.isFormatValid, 'Ensure it\'s valid yaml'); - SuiteLocal.log(doc.validationErrors); - strict.ok(doc.isValid, 'better be valid!'); + strict.ok(doc.isFormatValid, 'Ensure that it is valid yaml'); + strict.sequenceEqual(doc.validate(), []); // fixme: validate inputs again. // strict.throws(() => doc.info.version = '4.1', 'Setting invalid version should throw'); @@ -145,8 +140,8 @@ describe('Amf', () => { strict.equal(doc.isFormatValid, false, 'this document should have errors'); strict.equal(doc.formatErrors.length, 2, 'This document should have two error'); - strict.equal(doc.info.id, 'bob', 'identity incorrect'); - strict.equal(doc.info.version, '1.0.2', 'version incorrect'); + strict.equal(doc.id, 'bob', 'name incorrect'); + strict.equal(doc.version, '1.0.2', 'version incorrect'); }); it('read empty yaml file', async () => { @@ -155,8 +150,8 @@ describe('Amf', () => { strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); - strict.equal(doc.isValid, false, 'Should have some validation errors'); - strict.equal(doc.validationErrors[0], './empty.yaml:1:1 SectionMessing, Missing section \'info\'', 'Should have an error about info'); + const [firstError] = doc.validate(); + strict.equal(doc.formatVMessage(firstError), './empty.yaml:1:1 FieldMissing, Missing identity \'id\'', 'Should have an error about id'); }); it('validation errors', async () => { @@ -165,7 +160,7 @@ describe('Amf', () => { strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); - SuiteLocal.log(doc.validationErrors); - strict.equal(doc.validationErrors.length, 7, `Expecting two errors, found: ${JSON.stringify(doc.validationErrors, null, 2)}`); + const validationErrors = Array.from(doc.validate(), (error) => doc.formatVMessage(error)); + strict.equal(validationErrors.length, 7, `Expecting 7 errors, found: ${JSON.stringify(validationErrors, null, 2)}`); }); }); diff --git a/ce/test/core/index-tests.ts b/ce/test/core/index-tests.ts index a4a31fdd08..5e239f47c2 100644 --- a/ce/test/core/index-tests.ts +++ b/ce/test/core/index-tests.ts @@ -9,12 +9,10 @@ import { SemVer } from 'semver'; import { SuiteLocal } from './SuiteLocal'; interface TestData { - info: { - id: string, - version: SemVer; - summary?: string - description?: string; - }, + id: string, + version: SemVer; + summary?: string; + description?: string; contacts?: Record; @@ -24,10 +22,9 @@ interface TestData { /** An Index implementation for TestData */ class MyIndex extends IndexSchema { - - id = new StringKey(this, (i) => i.info.id); - version = new SemverKey(this, (i) => new SemVer(i.info.version)); - description = new StringKey(this, (i) => i.info.description); + id = new StringKey(this, (i) => i.id); + version = new SemverKey(this, (i) => new SemVer(i.version)); + description = new StringKey(this, (i) => i.description); contacts = new StringKey(this, (i) => keys(i.contacts)).with({ email: new StringKey(this, (i, index: string) => i.contacts?.[index]?.email) @@ -41,25 +38,19 @@ describe('Index Tests', () => { const index = new Index(MyIndex); index.insert({ - info: { - id: 'bob', - version: new SemVer('1.2.3') - } + id: 'bob', + version: new SemVer('1.2.3') }, 'foo/bob'); index.insert({ - info: { - id: 'wham/blam/sam', - version: new SemVer('0.0.4'), - description: 'this is a test' - } + id: 'wham/blam/sam', + version: new SemVer('0.0.4'), + description: 'this is a test' }, 'other/sam'); index.insert({ - info: { - id: 'tom', - version: new SemVer('2.3.4') - }, + id: 'tom', + version: new SemVer('2.3.4'), contacts: { 'bob Smith': { email: 'garrett@contoso.org' @@ -71,30 +62,18 @@ describe('Index Tests', () => { }, 'foo/tom'); index.insert({ - info: { - id: 'sam/blam/bam', - version: new SemVer('0.3.1'), - description: 'this is a test' - } + id: 'sam/blam/bam', + version: new SemVer('0.3.1'), + description: 'this is a test' }, 'sam/blam/bam'); - const results = index.where. - - version.greaterThan(new SemVer('0.3.0')). - items; - - // results); - // serialize(index.serialize())); - const data = index.serialize(); const index2 = new Index(MyIndex); index2.deserialize(data); const results2 = index.where. - version.greaterThan(new SemVer('0.3.0')). items; SuiteLocal.log(results2); }); - }); diff --git a/ce/test/core/repo-tests.ts b/ce/test/core/repo-tests.ts index 5fb49b53d6..d5e864e97c 100644 --- a/ce/test/core/repo-tests.ts +++ b/ce/test/core/repo-tests.ts @@ -54,13 +54,10 @@ function randomWords(from: Array, min = 3, max = 6) { } class Template { - info = { - id: randomWords(idwords).join('/'), - version: rndSemver(), - summary: sentence(), - description: paragraph(), - }; - + id = randomWords(idwords).join('/'); + version = rndSemver(); + summary = sentence(); + description = paragraph(); contacts = randomContacts(); install = { unzip: `https://${randomHost()}/${sentence().replace(/ /g, '/')}.zip`, @@ -86,8 +83,8 @@ describe('StandardRegistry Tests', () => { const p = { ...t, }; - p.info.version = rndSemver(); - const target = repoFolder.join(`${p.info.id}-${p.info.version}.yaml`); + p.version = rndSemver(); + const target = repoFolder.join(`${p.id}-${p.version}.yaml`); await target.writeFile(Buffer.from(serialize(p), 'utf8')); } } @@ -147,4 +144,4 @@ describe('StandardRegistry Tests', () => { strict.equal(cmakes.length, 5, 'should be 5 results'); }); */ -}); \ No newline at end of file +}); diff --git a/ce/test/resources/environment.yaml b/ce/test/resources/environment.yaml deleted file mode 100644 index b5e3745c38..0000000000 --- a/ce/test/resources/environment.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Environment configuration -info: - id: NAME - version: 1.0.0 - summary: My Project - -requires: - compilers/arm/gcc: 2020.10.0 - tools/kitware/cmake: 3.20.1 - \ No newline at end of file diff --git a/ce/test/resources/example-artifact.json b/ce/test/resources/example-artifact.json new file mode 100644 index 0000000000..45b45328fa --- /dev/null +++ b/ce/test/resources/example-artifact.json @@ -0,0 +1,8 @@ +{ + "id": "a/b", + "version": "1.0", + "requires": { + "compilers/arm/gcc": "2020.10.0", + "tools/kitware/cmake": "3.20.1" + } +} diff --git a/ce/test/resources/example-before-2022-06-17-artifact.json b/ce/test/resources/example-before-2022-06-17-artifact.json new file mode 100644 index 0000000000..729570ec46 --- /dev/null +++ b/ce/test/resources/example-before-2022-06-17-artifact.json @@ -0,0 +1,10 @@ +{ + "info": { + "id": "a/b", + "version": "1.0" + }, + "requires": { + "compilers/arm/gcc": "2020.10.0", + "tools/kitware/cmake": "3.20.1" + } +} diff --git a/src/vcpkg/commands.regenerate.cpp b/src/vcpkg/commands.regenerate.cpp index d54e075717..8a3fbd5de7 100644 --- a/src/vcpkg/commands.regenerate.cpp +++ b/src/vcpkg/commands.regenerate.cpp @@ -16,10 +16,12 @@ namespace constexpr StringLiteral DRY_RUN = "dry-run"; constexpr StringLiteral FORCE = "force"; + constexpr StringLiteral NORMALIZE = "normalize"; - constexpr std::array command_switches = {{ + constexpr std::array command_switches = {{ {FORCE, "proceeds with the (potentially dangerous) action without confirmation"}, {DRY_RUN, "does not actually perform the action, shows only what would be done"}, + {NORMALIZE, "apply any deprecation fixups"}, }}; static const CommandStructure command_structure = { @@ -38,6 +40,8 @@ namespace vcpkg std::vector forwarded_args; forwarded_args.push_back("regenerate"); const auto parsed = args.parse_arguments(command_structure); + forwarded_args.push_back(args.command_arguments[0]); + if (Util::Sets::contains(parsed.switches, FORCE)) { forwarded_args.push_back("--force"); @@ -48,7 +52,11 @@ namespace vcpkg forwarded_args.push_back("--what-if"); } - forwarded_args.push_back(args.command_arguments[0]); + if (Util::Sets::contains(parsed.switches, NORMALIZE)) + { + forwarded_args.push_back("--normalize"); + } + Checks::exit_with_code(VCPKG_LINE_INFO, run_configure_environment_command(paths, forwarded_args)); } }