UNPKG

13.8 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
7 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
8 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
9 * Google as part of the polymer project is also subject to an additional IP
10 * rights grant found at http://polymer.github.io/PATENTS.txt
11 */
12var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13 if (k2 === undefined) k2 = k;
14 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
15}) : (function(o, m, k, k2) {
16 if (k2 === undefined) k2 = k;
17 o[k2] = m[k];
18}));
19var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20 Object.defineProperty(o, "default", { enumerable: true, value: v });
21}) : function(o, v) {
22 o["default"] = v;
23});
24var __importStar = (this && this.__importStar) || function (mod) {
25 if (mod && mod.__esModule) return mod;
26 var result = {};
27 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
28 __setModuleDefault(result, mod);
29 return result;
30};
31Object.defineProperty(exports, "__esModule", { value: true });
32exports.installGitDependency = exports.prepareVersionDirectory = exports.tachometerVersion = exports.hashStrings = exports.makeServerPlans = exports.parsePackageVersions = void 0;
33const childProcess = __importStar(require("child_process"));
34const crypto = __importStar(require("crypto"));
35const fsExtra = __importStar(require("fs-extra"));
36const path = __importStar(require("path"));
37const util = __importStar(require("util"));
38const execFilePromise = util.promisify(childProcess.execFile);
39const execPromise = util.promisify(childProcess.exec);
40const util_1 = require("./util");
41/**
42 * Parse an array of strings of the form <package>@<version>.
43 */
44function parsePackageVersions(flags) {
45 const versions = [];
46 for (const flag of flags) {
47 const match = flag.match(/^(?:(.+)=)?(.+)@(.+)$/);
48 if (match === null) {
49 throw new Error(`Invalid package format ${flag}`);
50 }
51 const [, label, dep, version] = match;
52 versions.push({
53 label: label || `${dep}@${version}`,
54 dependencyOverrides: {
55 [dep]: version,
56 },
57 });
58 }
59 return versions;
60}
61exports.parsePackageVersions = parsePackageVersions;
62async function makeServerPlans(benchmarkRoot, npmInstallRoot, specs) {
63 const depSwaps = new Map();
64 const defaultSpecs = [];
65 const gitInstalls = new Map();
66 for (const spec of specs) {
67 if (spec.url.kind === 'remote') {
68 // No server needed for remote URLs.
69 continue;
70 }
71 if (spec.url.version === undefined) {
72 defaultSpecs.push(spec);
73 continue;
74 }
75 const diskPath = path.join(benchmarkRoot, spec.url.urlPath);
76 const kind = await util_1.fileKind(diskPath);
77 if (kind === undefined) {
78 throw new Error(`No such file or directory ${diskPath}`);
79 }
80 const originalPackageJsonPath = await findPackageJsonPath(kind === 'file' ? path.dirname(diskPath) : diskPath);
81 if (originalPackageJsonPath === undefined) {
82 throw new Error(`Could not find a package.json for ${diskPath}`);
83 }
84 const originalPackageJson = await fsExtra.readJson(originalPackageJsonPath);
85 const updatedDeps = Object.assign({}, originalPackageJson.dependencies);
86 for (const pkg of Object.keys(spec.url.version.dependencyOverrides)) {
87 const version = spec.url.version.dependencyOverrides[pkg];
88 if (typeof version === 'string') {
89 // NPM dependency syntax that can be handled directly by NPM without any
90 // help from us. This includes NPM packages, file paths, git repos (but
91 // not monorepos!), etc. (see
92 // https://docs.npmjs.com/configuring-npm/package-json.html#dependencies)
93 updatedDeps[pkg] = version;
94 }
95 else {
96 switch (version.kind) {
97 case 'git':
98 // NPM doesn't support directly installing from a sub-directory of a
99 // git repo, like in monorepos, so we handle those cases ourselves.
100 // Immediately resolve the git reference (branch, tag, etc.) to a
101 // SHA and use that going forward, so that we never fall behind the
102 // origin repo when re-using temp directories.
103 const sha = looksLikeGitSha(version.ref) ?
104 version.ref :
105 await remoteResolveGitRefToSha(version.repo, version.ref);
106 if (sha === undefined) {
107 throw new Error(`Git repo ${version.repo} could not resolve ref "${version.ref}"`);
108 }
109 // This hash uniquely identifies a `git clone` directory for some
110 // dependency at a particular SHA, with a particular setup routine.
111 const gitInstallHash = hashStrings(
112 // Include the tachometer version in case any changes or bugs
113 // would affect how we do this installation.
114 exports.tachometerVersion, version.repo, sha, JSON.stringify(version.setupCommands));
115 const tempDir = path.join(npmInstallRoot, gitInstallHash);
116 const tempPackageDir = version.subdir ? path.join(tempDir, version.subdir) : tempDir;
117 updatedDeps[pkg] = tempPackageDir;
118 // We're using a Map here because we want to de-duplicate git
119 // installations that have the exact same parameters, since they can
120 // be re-used across multiple benchmarks.
121 gitInstalls.set(gitInstallHash, Object.assign(Object.assign({}, version), { tempDir, sha }));
122 break;
123 default:
124 util_1.throwUnreachable(version.kind, 'Unknown dependency version kind: ' + version.kind);
125 }
126 }
127 }
128 // This hash uniquely identifes the `npm install` location for some
129 // `package.json` where some of its dependencies have been swapped.
130 const depSwapHash = hashStrings(
131 // Include the tachometer version in case any changes or bugs
132 // would affect how we do this installation.
133 exports.tachometerVersion, originalPackageJsonPath,
134 // Sort deps by package name for more temp-directory cache hits since
135 // the order declared in the dependencies object doesn't matter.
136 JSON.stringify(Object.entries(updatedDeps)
137 .sort(([pkgA], [pkgB]) => pkgA.localeCompare(pkgB))));
138 let swap = depSwaps.get(depSwapHash);
139 if (swap === undefined) {
140 swap = {
141 specs: [],
142 installDir: path.join(npmInstallRoot, depSwapHash),
143 dependencies: updatedDeps,
144 packageJsonPath: originalPackageJsonPath,
145 };
146 depSwaps.set(depSwapHash, swap);
147 }
148 swap.specs.push(spec);
149 }
150 const plans = [];
151 if (defaultSpecs.length > 0) {
152 plans.push({
153 specs: defaultSpecs,
154 npmInstalls: [],
155 mountPoints: [
156 {
157 urlPath: `/`,
158 diskPath: benchmarkRoot,
159 },
160 ],
161 });
162 }
163 for (const { specs, installDir, dependencies, packageJsonPath } of depSwaps
164 .values()) {
165 plans.push({
166 specs,
167 npmInstalls: [{
168 installDir,
169 packageJson: {
170 private: true,
171 dependencies,
172 }
173 }],
174 mountPoints: [
175 {
176 urlPath: path.posix.join('/', path.relative(benchmarkRoot, path.dirname(packageJsonPath))
177 .replace(path.win32.sep, '/'), 'node_modules'),
178 diskPath: path.join(installDir, 'node_modules'),
179 },
180 {
181 urlPath: `/`,
182 diskPath: benchmarkRoot,
183 },
184 ],
185 });
186 }
187 return { plans, gitInstalls: [...gitInstalls.values()] };
188}
189exports.makeServerPlans = makeServerPlans;
190// TODO(aomarks) Some consolidation with install.ts may be possible.
191async function findPackageJsonPath(startDir) {
192 let cur = path.resolve(startDir);
193 while (true) {
194 const possibleLocation = path.join(cur, 'package.json');
195 if (await fsExtra.pathExists(possibleLocation)) {
196 return possibleLocation;
197 }
198 const parentDir = path.resolve(cur, '..');
199 if (parentDir === cur) {
200 return undefined;
201 }
202 cur = parentDir;
203 }
204}
205function hashStrings(...strings) {
206 return crypto.createHash('sha256')
207 .update(JSON.stringify(strings))
208 .digest('hex');
209}
210exports.hashStrings = hashStrings;
211exports.tachometerVersion = require(path.join(__dirname, '..', 'package.json')).version;
212/**
213 * Name of special file used to indicate that an NPM or git install directory
214 * completed successfully.
215 */
216const installSuccessFile = '__TACHOMETER_INSTALL_SUCCESS__';
217/**
218 * Write the given package.json to the given directory and run "npm install" in
219 * it. If the directory already exists, don't do anything except log.
220 */
221async function prepareVersionDirectory({ installDir, packageJson }, forceCleanInstall) {
222 if (forceCleanInstall) {
223 await fsExtra.remove(installDir);
224 }
225 else if (await fsExtra.pathExists(installDir)) {
226 if (await fsExtra.pathExists(path.join(installDir, installSuccessFile))) {
227 console.log(`\nRe-using NPM install dir:\n ${installDir}\n`);
228 return;
229 }
230 else {
231 console.log(`\nCleaning up failed npm install:\n ${installDir}\n`);
232 await fsExtra.remove(installDir);
233 }
234 }
235 console.log(`\nRunning npm install in temp dir:\n ${installDir}\n`);
236 await fsExtra.ensureDir(installDir);
237 await fsExtra.writeFile(path.join(installDir, 'package.json'), JSON.stringify(packageJson, null, 2));
238 await util_1.runNpm(['install'], { cwd: installDir });
239 await fsExtra.writeFile(path.join(installDir, installSuccessFile), '');
240}
241exports.prepareVersionDirectory = prepareVersionDirectory;
242/**
243 * Check out the given commit from the given git repo, and run setup commands.
244 * If the directory already exists, don't do anything except log.
245 */
246async function installGitDependency(gitInstall, forceCleanInstall) {
247 if (forceCleanInstall) {
248 await fsExtra.remove(gitInstall.tempDir);
249 }
250 else if (await fsExtra.pathExists(gitInstall.tempDir)) {
251 if (await fsExtra.pathExists(path.join(gitInstall.tempDir, installSuccessFile))) {
252 console.log(`\nRe-using git checkout:\n` +
253 ` ${gitInstall.repo}#${gitInstall.ref}\n` +
254 ` ${gitInstall.tempDir}\n`);
255 return;
256 }
257 else {
258 console.log(`\nCleaning up failed git checkout:\n ${gitInstall.tempDir}\n`);
259 await fsExtra.remove(gitInstall.tempDir);
260 }
261 }
262 console.log(`\nFetching git commit to temp dir:\n` +
263 ` ${gitInstall.repo}#${gitInstall.ref}\n` +
264 ` ${gitInstall.tempDir}\n`);
265 // This approach only requires us to hit the remote repo once (as opposed to
266 // using `git clone`, which doesn't support fetching only one commit).
267 await fsExtra.ensureDir(gitInstall.tempDir);
268 const cwdOpts = { cwd: gitInstall.tempDir };
269 await execFilePromise('git', ['init'], cwdOpts);
270 await execFilePromise('git', ['remote', 'add', 'origin', gitInstall.repo], cwdOpts);
271 await execFilePromise('git', ['fetch', 'origin', '--depth=1', gitInstall.sha], cwdOpts);
272 await execFilePromise('git', ['checkout', gitInstall.sha], cwdOpts);
273 for (const setupCommand of gitInstall.setupCommands || []) {
274 console.log(`\nRunning setup command:\n ${setupCommand}\n`);
275 await execPromise(setupCommand, cwdOpts);
276 }
277 await fsExtra.writeFile(path.join(gitInstall.tempDir, installSuccessFile), '');
278}
279exports.installGitDependency = installGitDependency;
280/**
281 * Return whether the given string looks like a 40-characters of hexadecimal,
282 * i.e. a valid full length git commit SHA-1 hash.
283 */
284function looksLikeGitSha(ref) {
285 return ref.match(/^[a-fA-F0-9]{40}$/) !== null;
286}
287/**
288 * Use the `git ls-remote` command to remotely query the given git repo, and
289 * resolve the given ref (e.g. a branch or tag) to a commit SHA. Returns
290 * `undefined` if the ref does not resolve to anything in the repo. Throws if
291 * the repo is invalid or errors.
292 */
293async function remoteResolveGitRefToSha(repo, ref) {
294 const { stdout } = await execFilePromise('git', ['ls-remote', repo, '--symref', ref]);
295 if (stdout.trim() === '') {
296 return undefined;
297 }
298 const parts = stdout.trim().split(/\W/);
299 if (parts.length > 0 && looksLikeGitSha(parts[0])) {
300 return parts[0];
301 }
302 throw new Error(`Could not parse output of \`git ls-remote ${repo} --symref ${ref}\`:\n${stdout}`);
303}
304//# sourceMappingURL=versions.js.map
\No newline at end of file