Skip to content
This repository has been archived by the owner on Apr 20, 2022. It is now read-only.

[draft/inprogress] Metadata changes #21

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions ce/amf/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export class Contact extends Entity implements IContact {
/** @internal */
override *validate(): Iterable<ValidationError> {
yield* super.validate();
yield* this.validateChildKeys(['email', 'role']);
yield* this.validateChild('email', 'string');
}
}

Expand Down
174 changes: 78 additions & 96 deletions ce/amf/demands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { safeEval, valiadateExpression } from '../util/safeEval';
import { safeEval, validateExpression } 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: <T>(code: string, context?: any) => T = createSandbox();
const hostFeatures = new Set<string>(['x64', 'x86', 'arm', 'arm64', 'windows', 'linux', 'osx', 'freebsd']);

const ignore = new Set<string>(['info', 'contacts', 'error', 'message', 'warning', 'requires', 'see-also']);
Expand Down Expand Up @@ -69,32 +66,15 @@ export class Demands extends EntityMap<YAMLDictionary, DemandBlock> {
}

export class DemandBlock extends Entity {
#environment: Record<string, string | number | boolean | undefined> = {};
#activation?: Activation;
#data?: Record<string, string>;
discoveredData = <Record<string, string>>{};

setActivation(activation?: Activation) {
this.#activation = activation;
}

setData(data: Record<string, string>) {
this.#data = data;
}

setEnvironment(env: Record<string, string | number | boolean | undefined>) {
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 {
Expand All @@ -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<string> {
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<string>(undefined, this, 'apply');

readonly unless!: Unless;

Expand All @@ -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<DemandBlock> {
this.#environment = session.environment;

if (this.usingAlternative === undefined && this.has('unless')) {
await this.unless.init(session);
this.usingAlternative = this.unless.usingAlternative;
Expand All @@ -146,20 +131,47 @@ export class DemandBlock extends Entity {

/** @internal */
override *validate(): Iterable<ValidationError> {
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 {
Expand All @@ -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<string, any>, 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 = <any>{
$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; }, <any>{}) ?? {};
}
return output;
return {};
}

export class Unless extends DemandBlock implements AlternativeFulfillment {
Expand All @@ -246,25 +219,33 @@ export class Unless extends DemandBlock implements AlternativeFulfillment {

/** @internal */
override *validate(): Iterable<ValidationError> {
// 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<Unless> {
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();

Expand Down Expand Up @@ -298,29 +279,30 @@ export class Unless extends DemandBlock implements AlternativeFulfillment {
}
})) {
// we found something that looks promising.
let filtered = <any>{ $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();
(<DemandBlock>(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
}

// it did match, or it's just presence check
this.usingAlternative = true;
// set the data output of the check
// this is used later to fill in the settings.
this.setData(filtered);

return this;
}
}
Expand All @@ -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 {
Expand Down
12 changes: 8 additions & 4 deletions ce/amf/settings.ts → ce/amf/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new ScalarMap<string>(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<string> = new ScalarMap<string>(undefined, this, 'tools');
defines: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'defines');

aliases: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'aliases');
contents: StringsMap = new StringsMap(undefined, this, 'contents');

/** @internal */
override *validate(): Iterable<ValidationError> {
yield* super.validate();
yield* this.validateChildKeys(['paths', 'locations', 'properties', 'environment', 'tools', 'defines', 'aliases', 'contents']);
// todo: what validations do we need?

}
}
Loading