blob: e6b794e9cd417ca5000d586d14ae5833f35f9c15 [file] [log] [blame]
Benedikt Meurer1022e322025-03-28 13:39:581// Copyright 2025 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Benedikt Meurer8d2ec0b2025-04-03 09:43:245import childProcess from 'node:child_process';
6import fs from 'node:fs/promises';
7import path from 'node:path';
Benedikt Meurerb308e622025-04-17 11:24:298import {performance} from 'node:perf_hooks';
Benedikt Meurer8d2ec0b2025-04-03 09:43:249import util from 'node:util';
10
Nikolay Vitkova5686a82025-04-08 09:06:0011import {
Benedikt Meureredac67e2025-04-24 11:15:0712 autoninjaPyPath,
13 gnPyPath,
Nikolay Vitkova5686a82025-04-08 09:06:0014 rootPath,
Benedikt Meureredac67e2025-04-24 11:15:0715 vpython3ExecutablePath,
Nikolay Vitkova5686a82025-04-08 09:06:0016} from './devtools_paths.js';
Benedikt Meurer8d2ec0b2025-04-03 09:43:2417
18const execFile = util.promisify(childProcess.execFile);
19
Benedikt Meurer1022e322025-03-28 13:39:5820/**
21 * Representation of the feature set that is configured for Chrome. This
22 * keeps track of enabled and disabled features and generates the correct
23 * combination of `--enable-features` / `--disable-features` command line
24 * flags.
25 *
26 * There are unit tests for this in `./devtools_build.test.mjs`.
27 */
28export class FeatureSet {
29 #disabled = new Set();
30 #enabled = new Map();
31
32 /**
33 * Disables the given `feature`.
34 *
35 * @param {string} feature the name of the feature to disable.
36 */
37 disable(feature) {
38 this.#disabled.add(feature);
39 this.#enabled.delete(feature);
40 }
41
42 /**
43 * Enables the given `feature`, and optionally adds the `parameters` to it.
44 * For example:
45 * ```js
46 * featureSet.enable('DevToolsFreestyler', {patching: true});
47 * ```
48 * The parameters are additive.
49 *
50 * @param {string} feature the name of the feature to enable.
51 * @param {object} parameters the additional parameters to pass to it, in
52 * the form of key/value pairs.
53 */
54 enable(feature, parameters = {}) {
55 this.#disabled.delete(feature);
56 if (!this.#enabled.has(feature)) {
57 this.#enabled.set(feature, Object.create(null));
58 }
59 for (const [key, value] of Object.entries(parameters)) {
60 this.#enabled.get(feature)[key] = value;
61 }
62 }
63
64 /**
65 * Merge the other `featureSet` into this.
66 *
67 * @param featureSet the other `FeatureSet` to apply.
68 */
69 merge(featureSet) {
70 for (const feature of featureSet.#disabled) {
71 this.disable(feature);
72 }
73 for (const [feature, parameters] of featureSet.#enabled) {
74 this.enable(feature, parameters);
75 }
76 }
77
78 /**
79 * Yields the command line parameters to pass to the invocation of
80 * a Chrome binary for achieving the state of the feature set.
81 */
Benedikt Meurerb308e622025-04-17 11:24:2982 * [Symbol.iterator]() {
Benedikt Meurer1022e322025-03-28 13:39:5883 const disabledFeatures = [...this.#disabled];
84 if (disabledFeatures.length) {
85 yield `--disable-features=${disabledFeatures.sort().join(',')}`;
86 }
87 const enabledFeatures = [...this.#enabled].map(([feature, parameters]) => {
88 parameters = Object.entries(parameters);
89 if (parameters.length) {
90 parameters = parameters.map(([key, value]) => `${key}/${value}`);
91 feature = `${feature}:${parameters.sort().join('/')}`;
92 }
93 return feature;
94 });
95 if (enabledFeatures.length) {
96 yield `--enable-features=${enabledFeatures.sort().join(',')}`;
97 }
98 }
99
100 static parse(text) {
Nikolay Vitkov28c7c022025-04-17 15:04:27101 if (!text) {
102 return [];
103 }
Benedikt Meurer1022e322025-03-28 13:39:58104 const features = [];
105 for (const str of text.split(',')) {
106 const parts = str.split(':');
107 if (parts.length < 1 || parts.length > 2) {
108 throw new Error(`Invalid feature declaration '${str}'`);
109 }
110 const feature = parts[0];
111 const parameters = Object.create(null);
112 if (parts.length > 1) {
113 const args = parts[1].split('/');
114 if (args.length % 2 !== 0) {
Nikolay Vitkova5686a82025-04-08 09:06:00115 throw new Error(
Benedikt Meurerb308e622025-04-17 11:24:29116 `Invalid parameters '${parts[1]}' for feature ${feature}`,
Nikolay Vitkova5686a82025-04-08 09:06:00117 );
Benedikt Meurer1022e322025-03-28 13:39:58118 }
119 for (let i = 0; i < args.length; i += 2) {
120 const key = args[i + 0];
121 const value = args[i + 1];
122 parameters[key] = value;
123 }
124 }
Benedikt Meurerb308e622025-04-17 11:24:29125 features.push({feature, parameters});
Benedikt Meurer1022e322025-03-28 13:39:58126 }
127 return features;
128 }
129}
Benedikt Meurer8d2ec0b2025-04-03 09:43:24130
Benedikt Meurer867b0b42025-04-17 16:31:19131/**
132 * Constructs a human readable error message for the given build `error`.
133 *
134 * @param {Error} error the `Error` from the failed `autoninja` invocation.
135 * @param {string} outDir the absolute path to the `target` out directory.
136 * @param {string} target the targe relative to `//out`.
137 * @return {string} the human readable error message.
138 */
139function buildErrorMessageForNinja(error, outDir, target) {
140 const {message, stderr, stdout} = error;
141 if (stderr) {
142 // Anything that went to stderr has precedence.
143 return `Failed to build \`${target}' in \`${outDir}'
144
145${stderr}
146`;
147 }
148 if (stdout) {
149 // Check for `tsc` or `esbuild` errors in the stdout.
150 const tscErrors = [...stdout.matchAll(/^[^\s].*\(\d+,\d+\): error TS\d+:\s+.*$/gm)].map(([tscError]) => tscError);
151 if (!tscErrors.length) {
152 // We didn't find any `tsc` errors, but maybe there are `esbuild` errors.
153 // Transform these into the `tsc` format (with a made up error code), so
154 // we can report all TypeScript errors consistently in `tsc` format (which
155 // is well-known and understood by tools).
156 const esbuildErrors = stdout.matchAll(/^✘ \[ERROR\] ([^\n]+)\n\n\s+\.\.\/\.\.\/(.+):(\d+):(\d+):/gm);
157 for (const [, message, filename, line, column] of esbuildErrors) {
158 tscErrors.push(`${filename}(${line},${column}): error TS0000: ${message}`);
159 }
160 }
161 if (tscErrors.length) {
162 return `TypeScript compilation failed for \`${target}'
163
164${tscErrors.join('\n')}
165`;
166 }
167
168 // At the very least we strip `ninja: Something, something` lines from the
169 // standard output, since that's not particularly helpful.
170 const output = stdout.replaceAll(/^ninja: [^\n]+\n+/mg, '').trim();
171 return `Failed to build \`${target}' in \`${outDir}'
172
173${output}
174`;
175 }
176 return `Failed to build \`${target}' in \`${outDir}' (${message.substring(0, message.indexOf('\n'))})`;
177}
178
Benedikt Meurer8d2ec0b2025-04-03 09:43:24179export const BuildStep = {
Benedikt Meurer867b0b42025-04-17 16:31:19180 GN: 'gn',
Benedikt Meurer8d2ec0b2025-04-03 09:43:24181 AUTONINJA: 'autoninja',
182};
183
184export class BuildError extends Error {
185 /**
186 * Constructs a new `BuildError` with the given parameters.
187 *
188 * @param {BuildStep} step the build step that failed.
189 * @param {Object} options additional options for the `BuildError`.
190 * @param {Error} options.cause the actual cause for the build error.
191 * @param {string} options.outDir the absolute path to the `target` out directory.
192 * @param {string} options.target the target relative to `//out`.
193 */
194 constructor(step, options) {
Benedikt Meurerb308e622025-04-17 11:24:29195 const {cause, outDir, target} = options;
Benedikt Meurer867b0b42025-04-17 16:31:19196 const message = step === BuildStep.GN ? `\`gn' failed to initialize out directory ${outDir}` :
197 buildErrorMessageForNinja(cause, outDir, target);
198 super(message, {cause});
Benedikt Meurer8d2ec0b2025-04-03 09:43:24199 this.step = step;
Benedikt Meurer867b0b42025-04-17 16:31:19200 this.name = 'BuildError';
Benedikt Meurer8d2ec0b2025-04-03 09:43:24201 this.target = target;
202 this.outDir = outDir;
203 }
Benedikt Meurer8d2ec0b2025-04-03 09:43:24204}
205
206/**
207 * @typedef BuildResult
208 * @type {object}
209 * @property {number} time - wall clock time (in seconds) for the build.
210 */
211
212/**
213 * @param {string} target
Nikolay Vitkova5686a82025-04-08 09:06:00214 * @return {Promise<void>}
Benedikt Meurer8d2ec0b2025-04-03 09:43:24215 */
Nikolay Vitkova5686a82025-04-08 09:06:00216export async function prepareBuild(target) {
Benedikt Meurer8d2ec0b2025-04-03 09:43:24217 const outDir = path.join(rootPath(), 'out', target);
218
219 // Prepare the build directory first.
220 const outDirStat = await fs.stat(outDir).catch(() => null);
221 if (!outDirStat?.isDirectory()) {
222 // Use GN to (optionally create and) initialize the |outDir|.
223 try {
Benedikt Meureredac67e2025-04-24 11:15:07224 const gnExe = vpython3ExecutablePath();
225 const gnArgs = [gnPyPath(), '-q', 'gen', outDir];
Nikolay Vitkova5686a82025-04-08 09:06:00226 await execFile(gnExe, gnArgs);
Benedikt Meurer8d2ec0b2025-04-03 09:43:24227 } catch (cause) {
Benedikt Meurerb308e622025-04-17 11:24:29228 throw new BuildError(BuildStep.GN, {cause, outDir, target});
Benedikt Meurer8d2ec0b2025-04-03 09:43:24229 }
230 }
Nikolay Vitkova5686a82025-04-08 09:06:00231}
232
233/**
234 * @param {string} target
235 * @param {AbortSignal=} signal
236 * @return {Promise<BuildResult>} a `BuildResult` with statistics for the build.
237 */
238export async function build(target, signal) {
239 const startTime = performance.now();
240 const outDir = path.join(rootPath(), 'out', target);
Benedikt Meurer8d2ec0b2025-04-03 09:43:24241
242 // Build just the devtools-frontend resources in |outDir|. This is important
243 // since we might be running in a full Chromium checkout and certainly don't
244 // want to build all of Chromium first.
245 try {
Benedikt Meureredac67e2025-04-24 11:15:07246 const autoninjaExe = vpython3ExecutablePath();
247 const autoninjaArgs = [autoninjaPyPath(), '-C', outDir, 'devtools_all_files'];
Benedikt Meurerb308e622025-04-17 11:24:29248 await execFile(autoninjaExe, autoninjaArgs, {signal});
Benedikt Meurer8d2ec0b2025-04-03 09:43:24249 } catch (cause) {
250 if (cause.name === 'AbortError') {
251 throw cause;
252 }
Benedikt Meurerb308e622025-04-17 11:24:29253 throw new BuildError(BuildStep.AUTONINJA, {cause, outDir, target});
Benedikt Meurer8d2ec0b2025-04-03 09:43:24254 }
255
256 // Report the build result.
257 const time = (performance.now() - startTime) / 1000;
Benedikt Meurerb308e622025-04-17 11:24:29258 return {time};
Benedikt Meurer8ae0adc2025-04-17 08:32:24259}