Skip to content

Commit

Permalink
Add msbuild-specific exports to artifacts, as requested by Olga. (#672)
Browse files Browse the repository at this point in the history
* Add msbuild-specific exports to artifacts, as requested by Olga.

* Fix recent-ish regression that broke reading files without options blocks.

* Change $(ROOT) to $root$ and remove trailing slash.

* Delete labels, and don't emit things other than 'msbuildproperties' for now.

* Fix test name :)

Co-authored-by: Robert Schumacher <roschuma@microsoft.com>

* 😅

Co-authored-by: Robert Schumacher <roschuma@microsoft.com>

* Add XML special chars test.

* Use curly replacements rather than hard coding $root$ as requested by Robert.

* Add more tests demonstrating that the code is correct.

* Fix nuget comment

Co-authored-by: Robert Schumacher <roschuma@microsoft.com>

* Make mismatched {}s illegal.

Co-authored-by: Robert Schumacher <roschuma@microsoft.com>
  • Loading branch information
BillyONeal and ras0219-msft authored Aug 31, 2022
1 parent a9fce49 commit dfb8280
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 250 deletions.
22 changes: 15 additions & 7 deletions ce/ce/amf/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,27 @@ import { ScalarMap } from '../yaml/ScalarMap';
import { StringsMap } from '../yaml/strings';

export class Exports extends BaseMap implements IExports {
paths: StringsMap = new StringsMap(undefined, this, 'paths');
aliases: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'aliases');
defines: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'defines');
environment: StringsMap = new StringsMap(undefined, this, 'environment');
locations: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'locations');
msbuild_properties: ScalarMap<string> = new ScalarMap<string>(undefined, this, 'msbuild-properties');
paths: StringsMap = new StringsMap(undefined, this, 'paths');
properties: StringsMap = new StringsMap(undefined, this, 'properties');
environment: StringsMap = new StringsMap(undefined, this, 'environment');
tools: ScalarMap<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');

/** @internal */
override *validate(): Iterable<ValidationMessage> {
yield* super.validate();
yield* this.validateChildKeys(['paths', 'locations', 'properties', 'environment', 'tools', 'defines', 'aliases']);
// todo: what validations do we need?
yield* this.validateChildKeys([
'aliases',
'defines',
'environment',
'locations',
'msbuild-properties',
'paths',
'properties',
'tools'
]);
}
}
221 changes: 74 additions & 147 deletions ce/ce/artifacts/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ import { i } from '../i18n';
import { Exports } from '../interfaces/metadata/exports';
import { Session } from '../session';
import { isIterable } from '../util/checks';
import { replaceCurlyBraces } from '../util/curly-replacements';
import { linq, Record } from '../util/linq';
import { Queue } from '../util/promise';
import { Uri } from '../util/uri';
import { toXml } from '../util/xml';
import { Artifact } from './artifact';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const XMLWriterImpl = require('xml-writer');

export interface XmlWriter {
startDocument(version: string | undefined, encoding: string | undefined): XmlWriter;
writeElement(name: string, content: string): XmlWriter;
writeAttribute(name: string, value: string): XmlWriter;
startElement(name: string): XmlWriter;
endElement(): XmlWriter;
}

function findCaseInsensitiveOnWindows<V>(map: Map<string, V>, key: string): V | undefined {
return process.platform === 'win32' ? linq.find(map, key) : map.get(key);
Expand All @@ -28,6 +38,7 @@ export class Activation {
#aliases = new Map<string, string>();
#environment = new Map<string, Set<string>>();
#properties = new Map<string, Set<string>>();
#msbuild_properties = new Array<Tuple<string, string>>();

// Relative to the artifact install
#locations = new Map<string, string>();
Expand Down Expand Up @@ -103,6 +114,11 @@ export class Activation {
}
this.addAlias(name, alias);
}

// **** msbuild-properties ****
for (const [name, propertyValue] of exports.msbuild_properties) {
this.addMSBuildProperty(name, propertyValue, targetFolder);
}
}


Expand All @@ -121,10 +137,6 @@ export class Activation {
return linq.entries(this.#defines).selectAsync(async ([key, value]) => <Tuple<string, string>>[key, await this.resolveAndVerify(value)]);
}

get definesCount() {
return this.#defines.size;
}

async getDefine(name: string): Promise<string | undefined> {
const v = this.#defines.get(name);
return v ? await this.resolveAndVerify(v) : undefined;
Expand Down Expand Up @@ -152,10 +164,6 @@ export class Activation {
return undefined;
}

get toolCount() {
return this.#tools.size;
}

/** Aliases are tools that get exposed to the user as shell aliases */
addAlias(name: string, value: string) {
const a = findCaseInsensitiveOnWindows(this.#aliases, name);
Expand All @@ -181,7 +189,7 @@ export class Activation {
return this.#aliases.size;
}

/** a collection of 'published locations' from artifacts. useful for msbuild */
/** a collection of 'published locations' from artifacts */
addLocation(name: string, location: string | Uri) {
if (!name || !location) {
return;
Expand All @@ -203,9 +211,6 @@ export class Activation {
const l = this.#locations.get(name);
return l ? this.resolveAndVerify(l) : undefined;
}
get locationCount() {
return this.#locations.size;
}

/** a collection of environment variables from artifacts that are intended to be combinined into variables that have PATH delimiters */
addPath(name: string, location: string | Iterable<string> | Uri | Iterable<Uri>) {
Expand Down Expand Up @@ -233,10 +238,6 @@ export class Activation {
return linq.entries(this.#paths).selectAsync(async ([key, value]) => <Tuple<string, Set<string>>>[key, await this.resolveAndVerify(value)]);
}

get pathCount() {
return this.#paths.size;
}

async getPath(name: string) {
const set = this.#paths.get(name);
if (!set) {
Expand Down Expand Up @@ -270,11 +271,7 @@ export class Activation {
return linq.entries(this.#environment).selectAsync(async ([key, value]) => <Tuple<string, Set<string>>>[key, await this.resolveAndVerify(value)]);
}

get environmentVariableCount() {
return this.#environment.size;
}

/** a collection of arbitrary properties from artifacts. useful for msbuild */
/** a collection of arbitrary properties from artifacts */
addProperty(name: string, value: string | Iterable<string>) {
if (!name) {
return;
Expand Down Expand Up @@ -303,9 +300,20 @@ export class Activation {
return v ? await this.resolveAndVerify(v) : undefined;
}

get propertyCount() {
return this.#properties.size;
msBuildProcessPropertyValue(value: string, targetFolder: Uri) {
// note that this is intended to be consistent with vcpkg's handling:
// include/vcpkg/base/api_stable_format.h
const initialLocal = targetFolder.fsPath;
const endsWithSlash = initialLocal.endsWith('\\') || initialLocal.endsWith('/');
const root = endsWithSlash ? initialLocal.substring(0, initialLocal.length - 1) : initialLocal;
const replacements = new Map<string, string>([['root', root]]);
return replaceCurlyBraces(value, replacements);
}

addMSBuildProperty(name: string, value: string, targetFolder: Uri) {
this.#msbuild_properties.push([name, this.msBuildProcessPropertyValue(value, targetFolder)]);
}

async resolveAndVerify(value: string, locals?: Array<string>, refcheck?: Set<string>): Promise<string>
async resolveAndVerify(value: Set<string>, locals?: Array<string>, refcheck?: Set<string>): Promise<Set<string>>
async resolveAndVerify(value: string | Set<string>, locals: Array<string> = [], refcheck = new Set<string>()): Promise<string | Set<string>> {
Expand Down Expand Up @@ -335,7 +343,7 @@ export class Activation {
text = <any>text.value; // spews a --debug warning if a scalar makes its way thru for some reason
}

// short-ciruiting
// short-circuiting
if (!text || text.indexOf('$') === -1) {
return text;
}
Expand Down Expand Up @@ -474,101 +482,22 @@ export class Activation {
return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`);
}

async generateMSBuild(artifacts: Iterable<Artifact>): Promise<string> {
const msbuildFile = {
Project: {
$xmlns: 'http://schemas.microsoft.com/developer/msbuild/2003',
PropertyGroup: <Array<Record<string, any>>>[]
}
};

if (this.locationCount) {
const locations = <Record<string, any>>{
$Label: 'Locations'
};
for await (const [name, location] of this.locations) {
locations[name] = location;
}
msbuildFile.Project.PropertyGroup.push(locations);
}

if (this.propertyCount) {
const properties = <Record<string, any>>{
$Label: 'Properties'
};

for await (const [name, propertyValues] of this.properties) {
properties[name] = linq.join(propertyValues, ';');
}
msbuildFile.Project.PropertyGroup.push(properties);
}

if (this.toolCount) {
const tools = <Record<string, any>>{
$Label: 'Tools'
};

for await (const [name, tool] of this.tools) {
tools[name] = tool;
}
msbuildFile.Project.PropertyGroup.push(tools);
}

if (this.environmentVariableCount) {
const environment = <Record<string, any>>{
$Label: 'Environment'
};

for await (const [name, envValues] of this.environmentVariables) {
environment[name] = linq.join(envValues, ';');
generateMSBuild(): string {
const result : XmlWriter = new XMLWriterImpl(' ');
result.startDocument('1.0', 'utf-8');
result.startElement('Project');
result.writeAttribute('xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003');
if (this.#msbuild_properties.length) {
result.startElement('PropertyGroup');
for (const [key, value] of this.#msbuild_properties) {
result.writeElement(key, value);
}
msbuildFile.Project.PropertyGroup.push(environment);
}

if (this.pathCount) {
const paths = <Record<string, any>>{
$Label: 'Paths'
};

for await (const [name, pathValues] of this.paths) {
paths[name] = linq.join(pathValues, ';');
}
msbuildFile.Project.PropertyGroup.push(paths);
result.endElement(); // PropertyGroup
}

if (this.definesCount) {
const defines = <Record<string, any>>{
$Label: 'Defines'
};

for await (const [name, define] of this.defines) {
defines[name] = linq.join(define, ';');
}
msbuildFile.Project.PropertyGroup.push(defines);
}

if (this.aliasCount) {
const aliases = <Record<string, any>>{
$Label: 'Aliases'
};

for await (const [name, alias] of this.aliases) {
aliases[name] = alias;
}
msbuildFile.Project.PropertyGroup.push(aliases);
}

const propertyGroup = <any>{ $Label: 'Artifacts', Artifacts: { Artifact: [] } };

for (const artifact of artifacts) {
propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.id, '#text': artifact.targetLocation.fsPath });
}

if (propertyGroup.Artifacts.Artifact.length > 0) {
msbuildFile.Project.PropertyGroup.push(propertyGroup);
}

return toXml(msbuildFile);
result.endElement(); // Project
return result.toString();
}

protected async generateEnvironmentVariables(originalEnvironment: Record<string, string | undefined>): Promise<[Record<string, string>, Record<string, string>]> {
Expand Down Expand Up @@ -607,15 +536,13 @@ export class Activation {
}

// .defines get compiled into a single environment variable.
if (this.definesCount) {
let defines = '';
for await (const [name, value] of this.defines) {
defines += value ? `-D${name}=${value} ` : `-D${name} `;
}
if (defines) {
env['DEFINES'] = defines;
undo['DEFINES'] = originalEnvironment['DEFINES'] || '';
}
let defines = '';
for await (const [name, value] of this.defines) {
defines += value ? `-D${name}=${value} ` : `-D${name} `;
}
if (defines) {
env['DEFINES'] = defines;
undo['DEFINES'] = originalEnvironment['DEFINES'] || '';
}

return [env, undo];
Expand All @@ -630,23 +557,23 @@ export class Activation {
if (previous && undoEnvironmentFile) {
const deactivationDataFile = this.#session.parseUri(previous);
if (deactivationDataFile.scheme === 'file' && await deactivationDataFile.exists()) {
const deactivatationData = JSON.parse(await deactivationDataFile.readUTF8());
currentEnvironment = undoActivation(currentEnvironment, deactivatationData.environment || {});
const deactivationData = JSON.parse(await deactivationDataFile.readUTF8());
currentEnvironment = undoActivation(currentEnvironment, deactivationData.environment || {});
delete currentEnvironment[undoVariableName];
undoDeactivation = generateScriptContent(scriptKind, deactivatationData.environment || {}, deactivatationData.aliases || {});
undoDeactivation = generateScriptContent(scriptKind, deactivationData.environment || {}, deactivationData.aliases || {});
}
}

const [variables, undo] = await this.generateEnvironmentVariables(currentEnvironment);

async function transformtoRecord<T, U = T> (
orig: AsyncGenerator<Promise<Tuple<string, T>>, any, unknown>,
orig: AsyncGenerator<Promise<Tuple<string, T>>, any, unknown>,
// this type cast to U isn't *technically* correct but since it's locally scoped for this next block of code it shouldn't cause problems
func: (value: T) => U = (x => x as unknown as U)) {
func: (value: T) => U = (x => x as unknown as U)) {

return linq.values((await toArrayAsync(orig))).toObject(tuple => [tuple[0], func(tuple[1])]);
return linq.values((await toArrayAsync(orig))).toObject(tuple => [tuple[0], func(tuple[1])]);
}

const defines = await transformtoRecord(this.defines);
const aliases = await transformtoRecord(this.aliases);
const locations = await transformtoRecord(this.locations);
Expand Down Expand Up @@ -680,15 +607,15 @@ export class Activation {

// generate msbuild props file if requested
if (msbuildFile) {
const contents = await this.generateMSBuild(artifacts);
const contents = await this.generateMSBuild();
this.#session.channels.verbose(`--------[START MSBUILD FILE]--------\n${contents}\n--------[END MSBUILD FILE]---------`);
await msbuildFile.writeUTF8(await this.generateMSBuild(artifacts));
await msbuildFile.writeUTF8(contents);
}

if(json) {
if (json) {
const contents = generateJson(variables, defines, aliases, properties, locations, paths, tools);
this.#session.channels.verbose(`--------[START ENV VAR FILE]--------\n${contents}\n--------[END ENV VAR FILE]---------`);
await json.writeUTF8(contents);
await json.writeUTF8(contents);
}
}

Expand Down Expand Up @@ -759,20 +686,20 @@ function generateScriptContent(kind: string, variables: Record<string, string>,
return '';
}

function generateJson(variables: Record<string, string>, defines: Record<string, string>, aliases: Record<string, string>,
properties:Record<string, string[]>, locations: Record<string, string>, paths: Record<string, string[]>, tools: Record<string, string>): string {
var contents = {
"version": 1,
function generateJson(variables: Record<string, string>, defines: Record<string, string>, aliases: Record<string, string>,
properties:Record<string, Array<string>>, locations: Record<string, string>, paths: Record<string, Array<string>>, tools: Record<string, string>): string {

let contents = {
'version': 1,
variables,
defines,
aliases,
properties,
locations,
paths,
defines,
aliases,
properties,
locations,
paths,
tools
};

return JSON.stringify(contents);
}

Expand Down
Loading

0 comments on commit dfb8280

Please sign in to comment.