Skip to content

Commit 8d2ec0b

Browse files
bmeurerDevtools-frontend LUCI CQ
authored andcommitted
[npm] Allow to abort builds.
When there's a new change detected, there's no point in finishing the previous build (when using the watch mode). Bug: 393063467 Change-Id: I09a2bfc2d24aa02d8f07fb4141f73a6097d12569 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6429846 Reviewed-by: Nikolay Vitkov <[email protected]> Auto-Submit: Benedikt Meurer <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]> Commit-Queue: Benedikt Meurer <[email protected]>
1 parent dfda8bb commit 8d2ec0b

File tree

3 files changed

+141
-31
lines changed

3 files changed

+141
-31
lines changed

scripts/devtools_build.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import childProcess from 'node:child_process';
6+
import fs from 'node:fs/promises';
7+
import path from 'node:path';
8+
import {performance} from 'node:perf_hooks';
9+
import util from 'node:util';
10+
11+
import {autoninjaExecutablePath, gnExecutablePath, rootPath} from './devtools_paths.js';
12+
13+
const execFile = util.promisify(childProcess.execFile);
14+
515
/**
616
* Representation of the feature set that is configured for Chrome. This
717
* keeps track of enabled and disabled features and generates the correct
@@ -107,3 +117,83 @@ export class FeatureSet {
107117
return features;
108118
}
109119
}
120+
121+
export const BuildStep = {
122+
GN: 'gn-gen',
123+
AUTONINJA: 'autoninja',
124+
};
125+
126+
export class BuildError extends Error {
127+
/**
128+
* Constructs a new `BuildError` with the given parameters.
129+
*
130+
* @param {BuildStep} step the build step that failed.
131+
* @param {Object} options additional options for the `BuildError`.
132+
* @param {Error} options.cause the actual cause for the build error.
133+
* @param {string} options.outDir the absolute path to the `target` out directory.
134+
* @param {string} options.target the target relative to `//out`.
135+
*/
136+
constructor(step, options) {
137+
const {cause, outDir, target} = options;
138+
super(`Failed to build target ${target} in ${outDir}`, {cause});
139+
this.name = 'BuildError';
140+
this.step = step;
141+
this.target = target;
142+
this.outDir = outDir;
143+
}
144+
145+
toString() {
146+
const {stdout} = this.cause;
147+
return stdout;
148+
}
149+
}
150+
151+
/**
152+
* @typedef BuildResult
153+
* @type {object}
154+
* @property {number} time - wall clock time (in seconds) for the build.
155+
*/
156+
157+
/**
158+
* @param {string} target
159+
* @param {AbortSignal=} signal
160+
* @return {Promise<BuildResult>} a `BuildResult` with statistics for the build.
161+
*/
162+
export async function build(target, signal) {
163+
const startTime = performance.now();
164+
const outDir = path.join(rootPath(), 'out', target);
165+
166+
// Prepare the build directory first.
167+
const outDirStat = await fs.stat(outDir).catch(() => null);
168+
if (!outDirStat?.isDirectory()) {
169+
// Use GN to (optionally create and) initialize the |outDir|.
170+
try {
171+
const gnExe = gnExecutablePath();
172+
const gnArgs = ['-q', 'gen', outDir];
173+
await execFile(gnExe, gnArgs, {signal});
174+
} catch (cause) {
175+
if (cause.name === 'AbortError') {
176+
throw cause;
177+
}
178+
throw new BuildError(BuildStep.GN, {cause, outDir, target});
179+
}
180+
}
181+
182+
// Build just the devtools-frontend resources in |outDir|. This is important
183+
// since we might be running in a full Chromium checkout and certainly don't
184+
// want to build all of Chromium first.
185+
try {
186+
const autoninjaExe = autoninjaExecutablePath();
187+
const autoninjaArgs = ['-C', outDir, '--quiet', 'devtools_all_files'];
188+
await execFile(autoninjaExe, autoninjaArgs, {signal});
189+
} catch (cause) {
190+
if (cause.name === 'AbortError') {
191+
throw cause;
192+
}
193+
throw new BuildError(BuildStep.AUTONINJA, {cause, outDir, target});
194+
}
195+
196+
// Report the build result.
197+
const time = (performance.now() - startTime) / 1000;
198+
return {time};
199+
}

scripts/devtools_paths.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ function nodeModulesPath() {
127127
return path.join(devtoolsRootPath(), 'node_modules');
128128
}
129129

130+
function autoninjaExecutablePath() {
131+
return path.join(thirdPartyPath(), 'depot_tools', 'autoninja');
132+
}
133+
134+
function gnExecutablePath() {
135+
return path.join(thirdPartyPath(), 'depot_tools', 'gn');
136+
}
137+
130138
function stylelintExecutablePath() {
131139
return path.join(nodeModulesPath(), 'stylelint', 'bin', 'stylelint.js');
132140
}
@@ -164,9 +172,11 @@ function downloadedChromeBinaryPath() {
164172
}
165173

166174
module.exports = {
175+
autoninjaExecutablePath,
167176
devtoolsRootPath,
168177
downloadedChromeBinaryPath,
169178
isInChromiumDirectory,
179+
gnExecutablePath,
170180
litAnalyzerExecutablePath,
171181
mochaExecutablePath,
172182
nodeModulesPath,

scripts/run_build.mjs

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import childProcess from 'node:child_process';
65
import fs from 'node:fs';
76
import path from 'node:path';
87
import yargs from 'yargs';
98
import {hideBin} from 'yargs/helpers';
109

11-
import {rootPath} from './devtools_paths.js';
10+
import {build} from './devtools_build.mjs';
1211

1312
const argv = yargs(hideBin(process.argv))
1413
.option('target', {
@@ -35,50 +34,61 @@ const argv = yargs(hideBin(process.argv))
3534
.parseSync();
3635

3736
const {target, watch, skipInitialBuild} = argv;
38-
const cwd = process.cwd();
39-
const {env} = process;
40-
41-
// Create and initialize the `out/<target>` directory as needed.
42-
const outDir = path.join(rootPath(), 'out', target);
43-
if (!fs.existsSync(outDir)) {
44-
const gnExe = path.join(cwd, 'third_party', 'depot_tools', 'gn');
45-
fs.mkdirSync(outDir, {recursive: true});
46-
childProcess.spawnSync(gnExe, ['-q', 'gen', outDir], {
47-
cwd,
48-
env,
49-
stdio: 'inherit',
50-
});
51-
console.log(`Initialized output directory ${outDir}`);
52-
}
53-
54-
function build() {
55-
const autoninjaExe = path.join(cwd, 'third_party', 'depot_tools', 'autoninja');
56-
childProcess.spawnSync(autoninjaExe, ['-C', outDir, 'devtools_all_files'], {
57-
cwd,
58-
env,
59-
stdio: 'inherit',
60-
});
61-
}
37+
const timeFormatter = new Intl.NumberFormat('en-US', {
38+
style: 'unit',
39+
unit: 'second',
40+
unitDisplay: 'narrow',
41+
maximumFractionDigits: 2,
42+
});
6243

6344
// Perform an initial build (unless we should skip).
6445
if (!skipInitialBuild) {
65-
build();
46+
console.log('Building…');
47+
try {
48+
const {time} = await build(target);
49+
console.log(`Build ready (${timeFormatter.format(time)})`);
50+
} catch (error) {
51+
console.log(error.toString());
52+
process.exit(1);
53+
}
6654
}
6755

6856
if (watch) {
6957
let timeoutId = -1;
58+
let buildPromise = Promise.resolve();
59+
let abortController = new AbortController();
7060

7161
function watchCallback(eventType, filename) {
7262
if (eventType !== 'change') {
7363
return;
7464
}
75-
if (['BUILD.gn'].includes(filename) || ['.css', '.js', '.ts'].includes(path.extname(filename))) {
76-
clearTimeout(timeoutId);
77-
timeoutId = setTimeout(build, 250);
65+
if (!/^(BUILD\.gn)|(.*\.(css|js|ts))$/.test(filename)) {
66+
return;
7867
}
68+
clearTimeout(timeoutId);
69+
timeoutId = setTimeout(watchRebuild, 250);
70+
}
71+
72+
function watchRebuild() {
73+
// Abort any currently running build.
74+
abortController.abort();
75+
abortController = new AbortController();
76+
const {signal} = abortController;
77+
78+
buildPromise = buildPromise.then(async () => {
79+
try {
80+
console.log('Rebuilding...');
81+
const {time} = await build(target, signal);
82+
console.log(`Rebuild successfully (${timeFormatter.format(time)})`);
83+
} catch (error) {
84+
if (error.name !== 'AbortError') {
85+
console.log(error.toString());
86+
}
87+
}
88+
});
7989
}
8090

81-
const WATCHLIST = ['front_end', 'inspector_overlay', 'test'];
91+
const WATCHLIST = ['front_end', 'test'];
8292
for (const dirname of WATCHLIST) {
8393
fs.watch(
8494
dirname,

0 commit comments

Comments
 (0)