Skip to content

Commit 433d1f8

Browse files
authored
fix(pacmak): .NET build downloading packages from NuGet (#949)
Fix situation where the .NET build accidentally downloads dependency packages from NuGet, instead of using the locally built packages. Caused by packages being built in the order given on the command line, and if a consumer is built before a dependency, the NuGet build would fall back to loading the package from the online repository. Apply a topological sort to package building to prevent this from happening.
1 parent fc02abd commit 433d1f8

6 files changed

Lines changed: 148 additions & 6 deletions

File tree

packages/jsii-pacmak/lib/npm-modules.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ import { resolveDependencyDirectory } from './util';
55

66
import logging = require('../lib/logging');
77
import { JsiiModule } from './packaging';
8+
import { topologicalSort } from './toposort';
89

910

1011
/**
1112
* Find all modules that need to be packagerd
1213
*
13-
* If the input list is empty, include the current directory. The result
14-
* is NOT topologically sorted.
14+
* If the input list is empty, include the current directory.
15+
*
16+
* The result is topologically sorted.
1517
*/
1618
export async function findJsiiModules(directories: string[], recurse: boolean) {
1719
const ret: JsiiModule[] = [];
1820
const visited = new Set<string>();
1921
for (const dir of directories.length > 0 ? directories : ['.']) {
2022
await visitPackage(dir, true);
2123
}
22-
return ret;
24+
return topologicalSort(ret, m => m.name, m => m.dependencyNames);
2325

2426
async function visitPackage(dir: string, isRoot: boolean) {
2527
const realPath = await fs.realpath(dir);
@@ -35,9 +37,15 @@ export async function findJsiiModules(directories: string[], recurse: boolean) {
3537
}
3638
}
3739

40+
if (!pkg.name) {
41+
throw new Error(`package.json does not have a 'name' field: ${JSON.stringify(pkg, undefined, 2)}`);
42+
}
43+
44+
const dependencyNames = Object.keys(pkg.dependencies || {});
45+
3846
// if --recurse is set, find dependency dirs and build them.
3947
if (recurse) {
40-
for (const dep of Object.keys(pkg.dependencies || {})) {
48+
for (const dep of dependencyNames) {
4149
const depDir = resolveDependencyDirectory(realPath, dep);
4250
await visitPackage(depDir, false);
4351
}
@@ -51,10 +59,10 @@ export async function findJsiiModules(directories: string[], recurse: boolean) {
5159
name: pkg.name,
5260
moduleDirectory: realPath,
5361
defaultOutputDirectory: outputDirectory,
54-
availableTargets: targets
62+
availableTargets: targets,
63+
dependencyNames
5564
}));
5665
}
57-
5866
}
5967

6068
export async function updateAllNpmIgnores(packages: JsiiModule[]) {

packages/jsii-pacmak/lib/packaging.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ export interface JsiiModuleOptions {
2626
* Output directory where to package everything
2727
*/
2828
defaultOutputDirectory: string;
29+
30+
/**
31+
* Names of packages this package depends on, if any
32+
*/
33+
dependencyNames?: string[];
2934
}
3035
export class JsiiModule {
3136
public readonly name: string;
37+
public readonly dependencyNames: string[];
3238
public readonly moduleDirectory: string;
3339
public readonly availableTargets: string[];
3440
public outputDirectory: string;
@@ -41,6 +47,7 @@ export class JsiiModule {
4147
this.moduleDirectory = options.moduleDirectory;
4248
this.availableTargets = options.availableTargets;
4349
this.outputDirectory = options.defaultOutputDirectory;
50+
this.dependencyNames = options.dependencyNames || [];
4451
}
4552

4653
/**
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export type KeyFunc<T> = (x: T) => string;
2+
export type DepFunc<T> = (x: T) => string[];
3+
4+
/**
5+
* Return a topological sort of all elements of xs, according to the given dependency functions
6+
*
7+
* Dependencies outside the referenced set are ignored.
8+
*
9+
* Not a stable sort, but in order to keep the order as stable as possible, we'll sort by key
10+
* among elements of equal precedence.
11+
*
12+
* @param xs - The elements to sort
13+
* @param keyFn - Return an element's identifier
14+
* @param depFn - Return the identifiers of an element's dependencies
15+
*/
16+
export function topologicalSort<T>(xs: Iterable<T>, keyFn: KeyFunc<T>, depFn: DepFunc<T>): T[] {
17+
const remaining = new Map<string, TopoElement<T>>();
18+
for (const element of xs) {
19+
const key = keyFn(element);
20+
remaining.set(key, { key, element, dependencies: depFn(element) });
21+
}
22+
23+
const ret = new Array<T>();
24+
while (remaining.size > 0) {
25+
// All elements with no more deps in the set can be ordered
26+
const selectable = Array.from(remaining.values()).filter(e => e.dependencies.every(d => !remaining.has(d)));
27+
28+
selectable.sort((a, b) => a.key < b.key ? -1 : b.key < a.key ? 1 : 0);
29+
30+
for (const selected of selectable) {
31+
ret.push(selected.element);
32+
remaining.delete(selected.key);
33+
}
34+
35+
// If we didn't make any progress, we got stuck
36+
if (selectable.length === 0) {
37+
throw new Error(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`);
38+
}
39+
}
40+
41+
return ret;
42+
}
43+
44+
interface TopoElement<T> {
45+
key: string;
46+
dependencies: string[];
47+
element: T;
48+
}

packages/jsii-pacmak/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"@types/fs-extra": "^8.0.1",
5454
"@types/jest": "^24.0.22",
5555
"@types/node": "^10.17.4",
56+
"mock-fs": "^4.10.2",
57+
"@types/mock-fs": "^4.10.0",
5658
"@types/yargs": "^13.0.3",
5759
"@typescript-eslint/eslint-plugin": "^2.6.1",
5860
"@typescript-eslint/parser": "^2.6.1",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import mockfs = require('mock-fs');
2+
import { findJsiiModules } from '../lib/npm-modules';
3+
4+
test('findJsiiModules is sorted topologically', async () => {
5+
mockfs({
6+
'/packageA/package.json': JSON.stringify({
7+
name: 'packageA',
8+
jsii: {
9+
outdir: 'dist',
10+
targets: {
11+
python: {}
12+
}
13+
},
14+
dependencies: {
15+
packageB: '*'
16+
}
17+
}),
18+
'/packageB/package.json': JSON.stringify({
19+
name: 'packageB',
20+
jsii: {
21+
outdir: 'dist',
22+
targets: {
23+
python: {}
24+
}
25+
}
26+
}),
27+
});
28+
29+
try {
30+
const mods = await findJsiiModules(['/packageA', '/packageB'], false);
31+
expect(mods.map(m => m.name)).toEqual(['packageB', 'packageA']);
32+
} finally {
33+
mockfs.restore();
34+
}
35+
});
36+
37+
test('findJsiiModules without deps loads packages in given order', async () => {
38+
mockfs({
39+
'/packageA/package.json': JSON.stringify({
40+
name: 'packageA',
41+
jsii: {
42+
outdir: 'dist',
43+
targets: {
44+
python: {}
45+
}
46+
},
47+
}),
48+
'/packageB/package.json': JSON.stringify({
49+
name: 'packageB',
50+
jsii: {
51+
outdir: 'dist',
52+
targets: {
53+
python: {}
54+
}
55+
}
56+
}),
57+
});
58+
59+
try {
60+
const mods = await findJsiiModules(['/packageA', '/packageB'], false);
61+
expect(mods.map(m => m.name)).toEqual(['packageA', 'packageB']);
62+
} finally {
63+
mockfs.restore();
64+
}
65+
});

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,13 @@
12251225
dependencies:
12261226
"@types/node" "*"
12271227

1228+
"@types/mock-fs@^4.10.0":
1229+
version "4.10.0"
1230+
resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b"
1231+
integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg==
1232+
dependencies:
1233+
"@types/node" "*"
1234+
12281235
"@types/node@*":
12291236
version "12.11.1"
12301237
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.1.tgz#1fd7b821f798b7fa29f667a1be8f3442bb8922a3"
@@ -5414,6 +5421,11 @@ mkdirp@*, mkdirp@^0.5.0, mkdirp@^0.5.1:
54145421
dependencies:
54155422
minimist "0.0.8"
54165423

5424+
mock-fs@^4.10.2:
5425+
version "4.10.3"
5426+
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.10.3.tgz#d0550663dd2b5d33a7c1b8713c6925aab07a04ae"
5427+
integrity sha512-bcukePBvuA3qovmq0Qtqu9+1APCIGkFHnsozrPIVromt5XFGGgkQSfaN0H6RI8gStHkO/hRgimvS3tooNes4pQ==
5428+
54175429
modify-values@^1.0.0:
54185430
version "1.0.1"
54195431
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"

0 commit comments

Comments
 (0)