-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathbuild.js
More file actions
418 lines (359 loc) · 14.4 KB
/
build.js
File metadata and controls
418 lines (359 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
const child_process = require('child_process');
const os = require('os');
const fs = require('fs');
const glob = require('glob');
const moment = require('moment');
const path = require('path');
const util = require('util');
const yargs = require('yargs');
fs.readdir = util.promisify(fs.readdir);
fs.copyFile = util.promisify(fs.copyFile);
fs.rename = util.promisify(fs.rename);
fs.readFile = util.promisify(fs.readFile);
fs.writeFile = util.promisify(fs.writeFile);
fs.mkdir = util.promisify(fs.mkdir);
fs.unlink = util.promisify(fs.unlink);
yargs.version(false);
const buildGroup = 'Build Options:';
const testGroup = 'Test Options:';
yargs.option('verbosity', { desc: 'MSBuild verbosity', string: true, group: buildGroup });
yargs.option('configuration', {
desc: 'MSBuild configuration',
choices: ['Debug', 'Release'],
group: buildGroup,
});
yargs.option('release', {
desc: 'Use MSBuild Release configuration',
boolean: true,
group: buildGroup,
});
yargs.option('framework', {
desc: 'Specify .net application framework',
choices: ['net48', 'net8.0', 'netstandard2.1'],
group: buildGroup,
});
yargs.option('msbuild', {
desc: 'Use MSBuild instead of dotnet CLI', // Signing requires msbuild
boolean: true,
group: buildGroup,
});
yargs.option('filter', { desc: 'Filter test cases', string: true, group: testGroup });
yargs.option('serial', { desc: 'Run tests serially (slower)', boolean: true, group: testGroup });
yargs.option('coverage', {
desc: 'Collect code coverage when testing',
boolean: true,
group: testGroup,
});
yargs.option('race', { desc: 'Enable Go race detector', boolean: true, group: testGroup });
const namespace = 'Microsoft.DevTunnels.Ssh';
const srcDir = path.join(__dirname, 'src');
const binDir = path.join(__dirname, 'out', 'bin');
const libDir = path.join(__dirname, 'out', 'lib');
const intermediateDir = path.join(__dirname, 'out', 'obj');
const packageDir = path.join(__dirname, 'out', 'pkg');
const packageJsonFile = path.join(__dirname, 'package.json');
const testResultsDir = path.join(__dirname, 'out', 'testresults');
function getPackageFileName(packageJson, buildVersion) {
// '@scope/' gets converted to a 'scope-' prefix of the package filename.
return `${packageJson.name.replace('@', '').replace('/', '-')}-${buildVersion}.tgz`;
}
yargs.command('build', 'Build C#, TypeScript, and Go code', async () => {
await forkCommand('build-cs');
await forkCommand('build-ts');
await forkCommand('build-browsertest');
await forkCommand('build-go');
});
yargs.command('pack', 'Build C# and TypeScript packages', async () => {
await forkCommand('pack-cs');
await forkCommand('pack-ts');
});
yargs.command('test', 'Test C#, TypeScript, and Go code', async () => {
await forkCommand('test-cs');
await forkCommand('test-ts');
await forkCommand('test-go');
});
yargs.command('build-cs', 'Build C# code', async (yargs) => {
const configuration = yargs.argv.configuration || (yargs.argv.release ? 'Release' : 'Debug');
const verbosity = yargs.argv.verbosity || 'minimal';
const command = yargs.argv.msbuild
? `msbuild -nologo -v:${verbosity} -p:RestorePackages=false -p:Configuration=${configuration} -t:Build`
: `dotnet build --nologo --no-restore -v ${verbosity} -c ${configuration}`;
await executeCommand(__dirname, command);
});
yargs.command('build-ts', 'Build TypeScript code', async (yargs) => {
const tsPackageNames = ['ssh', 'ssh-keys', 'ssh-tcp'];
for (let packageName of tsPackageNames) {
await linkLib('@microsoft/dev-tunnels-' + packageName, packageName);
}
await executeCommand(__dirname, `npm run --silent compile`);
await executeCommand(__dirname, `npm run --silent eslint`);
const buildVersion = await getBuildVersion();
const majorMinorBuildVersion = buildVersion.replace(/\.(\d+)(-.*)?$/, '');
const rootPackageJson = JSON.parse(await fs.readFile(path.join(__dirname, 'package.json')));
// Update the package.json and README for each built package.
for (let packageName of tsPackageNames) {
const sourceDir = path.join(srcDir, 'ts', packageName);
const targetDir = path.join(libDir, packageName);
const builtPackageJsonFile = path.join(targetDir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(builtPackageJsonFile));
packageJson.author = rootPackageJson.author;
packageJson.version = buildVersion;
packageJson.scripts = undefined;
packageJson.main = './index.js';
// Force the dependencies on other packages in this project to match the major.minor version.
for (let packageName of Object.keys(packageJson.dependencies)) {
if (packageName.startsWith(rootPackageJson.name)) {
packageJson.dependencies[packageName] = `~` + majorMinorBuildVersion;
}
}
await fs.writeFile(builtPackageJsonFile, JSON.stringify(packageJson, null, '\t'));
await fs.copyFile(path.join(sourceDir, 'README.md'), path.join(targetDir, 'README.md'));
}
});
yargs.command('build-browsertest', 'Build browser test bundle', async (yargs) => {
const testLibDir = path.join(libDir, 'ssh-test');
const skipModules = ['interopTests', 'portForwardingTests', 'tcpUtils', 'cli', 'bundle'];
const testFiles = (await fs.readdir(testLibDir)).filter(
(f) => f.endsWith('.js') && !skipModules.includes(f.replace(/\.js$/, '')),
);
const testFilesList = testFiles.join(' ');
const excludeNodeAlgModules = (await fs.readdir(path.join(libDir, 'ssh', 'algorithms', 'node')))
.filter((f) => f.endsWith('.js'))
.map((f) => './node/' + path.basename(f, '.js'));
const excludeModulesList = excludeNodeAlgModules.map((m) => `-u ${m}`).join(' ');
const stubModulesList = ['fs', 'net'].map((m) => `-i ${m}`).join(' ');
const browserifyCmd = path.join(__dirname, 'node_modules', 'browserify', 'bin', 'cmd.js');
await executeCommand(
testLibDir,
`node "${browserifyCmd}" ${testFilesList} ${stubModulesList} ${excludeModulesList} ` +
'-t brfs --debug -o ./bundle.js',
);
const testSrcDir = path.join(__dirname, 'test', 'ts', 'ssh-test');
console.log('To run browser tests, browse to: ' + path.join(testSrcDir, 'test.html'));
});
yargs.command('pack-cs', 'Build C# NuGet packages', async (yargs) => {
const configuration = yargs.argv.configuration || (yargs.argv.release ? 'Release' : 'Debug');
const verbosity = yargs.argv.verbosity || 'minimal';
const command = yargs.argv.msbuild
? `msbuild -nologo -v:${verbosity} -p:RestorePackages=false -p:Configuration=${configuration} -t:Pack`
: `dotnet pack --nologo --no-restore --no-build -v ${verbosity} -c ${configuration}`;
await executeCommand(__dirname, command);
});
yargs.command('pack-ts', 'Build TypeScript npm packages', async (yargs) => {
const buildVersion = await getBuildVersion();
await mkdirp(packageDir);
let packageFiles = [];
for (let packageName of ['ssh', 'ssh-keys', 'ssh-tcp']) {
const targetDir = path.join(libDir, packageName);
await executeCommand(targetDir, `npm pack`);
const packageJsonFile = path.join(targetDir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonFile));
const prefixedPackageFileName = getPackageFileName(packageJson, buildVersion);
const packageFileName = prefixedPackageFileName.replace(/\w+-/, '');
await fs.rename(
path.join(targetDir, prefixedPackageFileName),
path.join(packageDir, packageFileName),
);
packageFiles.push(packageFileName);
}
console.log(`Created packages [${packageFiles.join(', ')}] at ${packageDir}`);
});
yargs.command('publish-ts', 'Publish TypeScrypt npm packages', async (yargs) => {
const buildVersion = await getBuildVersion();
const packageJson = JSON.parse(await fs.readFile(packageJsonFile));
const packageFileName = getPackageFileName(packageJson, buildVersion);
const packageFilePath = path.join(packageDir, packageFileName);
const publishCommand = `npm publish "${packageFilePath}"`;
await executeCommand(__dirname, publishCommand);
});
yargs.command('test-cs', 'Run C# tests', async (yargs) => {
await mkdirp(testResultsDir);
const coverageSummaryFile = path.join(testResultsDir, 'CodeCoverage', 'Summary.txt');
if (yargs.argv.coverage && fs.existsSync(coverageSummaryFile)) {
await fs.unlink(coverageSummaryFile);
}
const configuration = yargs.argv.configuration || (yargs.argv.release ? 'Release' : 'Debug');
// Updating the config file is the only way to control whether tests run in parallel.
const testConfigFilesGlob = path.join(binDir, configuration) + '/**/xunit.runner.json';
for (let testConfigFile of glob.sync(testConfigFilesGlob)) {
const testConfig = JSON.parse(fs.readFileSync(testConfigFile));
testConfig.parallelizeTestCollections = !yargs.argv.serial;
fs.writeFileSync(testConfigFile, JSON.stringify(testConfig, null, '\t'));
}
const targetAppFramework = getTargetAppFramework(yargs.argv.framework);
const trxBaseFileName = path.join(testResultsDir, `SSH-CS-${targetAppFramework}.trx`);
const verbosity = yargs.argv.verbosity || 'normal';
let command =
'dotnet test --no-restore --no-build' +
` -v ${verbosity}` +
` -c ${configuration}` +
` -p:CodeCoverage=${yargs.argv.coverage}` +
` -l:"trx;LogFileName=${trxBaseFileName}"`;
if (yargs.argv.framework) {
command += ` --framework ${targetAppFramework}`;
}
if (yargs.argv.filter) {
command += ` --filter ${yargs.argv.filter}`;
}
// On macOS, use a .runsettings file to inject DYLD_FALLBACK_LIBRARY_PATH into the test
// host process, so .NET can find OpenSSL libraries needed for AES-GCM and ECDH algorithms.
// Setting the env var on the parent process is not sufficient because macOS SIP strips
// DYLD_* variables when spawning child processes through a shell.
if (process.platform === 'darwin') {
const runsettingsPath = path.join(__dirname, 'test', 'cs', 'Ssh.Test', 'test.runsettings');
if (fs.existsSync(runsettingsPath)) {
command += ` --settings "${runsettingsPath}"`;
}
}
await executeCommand(__dirname, command);
if (yargs.argv.coverage && fs.existsSync(coverageSummaryFile)) {
const coverageSummary = await fs.readFile(coverageSummaryFile);
console.log(coverageSummary.toString());
}
});
yargs.command('test-ts', 'Run TypeScript tests', async (yargs) => {
await mkdirp(testResultsDir);
const testResultsFile = path.join(
testResultsDir,
`SSH-TS_${moment().format('YYYY-MM-DD_HH-mm-ss-SSS')}.xml`,
);
const reporterConfig = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: testResultsFile,
},
};
const reporterConfigFile = path.join(testResultsDir, 'mocha-multi-reporters.config');
await fs.writeFile(reporterConfigFile, JSON.stringify(reporterConfig));
let command =
'npm run --silent mocha -- --reporter mocha-multi-reporters ' +
`--reporter-options configFile="${reporterConfigFile}"`;
if (yargs.argv.filter) {
command += ` --grep /${yargs.argv.filter}/i`;
}
try {
await executeCommand(__dirname, command);
} finally {
await fs.unlink(reporterConfigFile);
}
});
yargs.command('bench-cs', 'Run C# benchmarks', async (yargs) => {
const benchConfiguration = yargs.argv.configuration || (yargs.argv.debug ? 'Debug' : 'Release');
const benchTarget = getTargetAppFramework(yargs.argv.framework);
const benchmarkAssembly = path.join(
binDir,
benchConfiguration,
'Ssh.Benchmark',
benchTarget,
namespace + '.Benchmark.dll',
);
const args = ['"' + benchmarkAssembly + '"'];
if (yargs.argv.scenario) {
args.push(yargs.argv.scenario);
}
await executeCommand(__dirname, 'dotnet', args);
});
yargs.command('bench-ts', 'Run TypeScript benchmarks', async (yargs) => {
const benchmarkModule = path.join(libDir, 'ssh-bench', 'main.js');
const args = [benchmarkModule];
if (yargs.argv.scenario) {
args.push(yargs.argv.scenario);
}
await executeCommand(__dirname, 'node', args);
});
yargs.command('bench-go', 'Run Go benchmarks', async () => {
const goBenchDir = path.join(__dirname, 'bench', 'go', 'cmd', 'bench');
const extraArgs = process.argv.slice(3).filter((a) => a !== '--');
const args = ['run', '.', ...extraArgs];
await executeCommand(goBenchDir, 'go', args);
});
yargs.command('bench', 'Run all benchmarks', async () => {
await forkCommand('bench-cs');
await forkCommand('bench-ts');
await forkCommand('bench-go');
});
const goSrcDir = path.join(srcDir, 'go');
const goTestDir = path.join(__dirname, 'test', 'go', 'ssh-test');
yargs.command('build-go', 'Build Go code', async () => {
await executeCommand(goSrcDir, 'go build ./...');
await executeCommand(goSrcDir, 'go vet ./...');
});
yargs.command('test-go', 'Run Go tests', async (yargs) => {
let command = 'go test';
if (yargs.argv.race) {
command += ' -race';
}
command += ' ./...';
if (yargs.argv.filter) {
command += ` -run ${yargs.argv.filter}`;
}
await executeCommand(goSrcDir, command);
await executeCommand(goTestDir, command);
});
function forkCommand(command) {
const args = [command, ...process.argv.slice(3)];
return new Promise((resolve) => {
const options = { stdio: 'inherit', shell: true };
const p = child_process.fork(process.argv[1], args, options);
p.on('close', (code) => {
if (code) process.exit(code);
resolve();
});
});
}
function executeCommand(cwd, command, args) {
if (!args) {
const parts = command.split(' ');
command = parts[0];
args = parts.slice(1);
}
console.log(`${command} ${args.join(' ')}`);
return new Promise((resolve, reject) => {
const options = { cwd: cwd, stdio: 'inherit', shell: true };
const p = child_process.spawn(command, args, options, (err) => {
if (err) {
err.showStack = false;
reject(err);
}
resolve();
});
p.on('close', (code) => {
if (code) process.exit(code);
resolve();
});
});
}
async function mkdirp(dir) {
try {
await fs.mkdir(dir, { recursive: true });
} catch (e) {
if (e.code !== 'EEXIST') throw e;
}
}
async function getBuildVersion() {
const nbgv = require('nerdbank-gitversioning');
const buildVersion = await nbgv.getVersion();
return buildVersion.semVer2;
}
async function linkLib(packageName, dirName) {
const libModuleFile = path.join(libDir, 'node_modules', packageName + '.js');
await mkdirp(path.dirname(libModuleFile));
await fs.writeFile(
libModuleFile,
'// Enable referencing this lib by package name instead of by relative path.\n' +
`module.exports = require('../../${dirName}');\n`,
);
}
function getTargetAppFramework(framework) {
if (!framework || framework == 'netstandard2.1') {
return 'net8.0';
} else if (framework == 'net8.0' || framework == 'net48') {
return framework;
} else {
throw new Error('Invalid target framework: ' + framework);
}
}
yargs.parseAsync().catch(console.error);