blob: 28f0456b8d5f760432615a8a153c9babf13e331c [file] [log] [blame]
Primiano Tuccic8be6812021-02-09 18:08:49 +01001// Copyright (C) 2021 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15'use strict';
16
17// This script takes care of:
18// - The build process for the whole UI and the chrome extension.
19// - The HTTP dev-server with live-reload capabilities.
20// The reason why this is a hand-rolled script rather than a conventional build
21// system is keeping incremental build fast and maintaining the set of
22// dependencies contained.
23// The only way to keep incremental build fast (i.e. O(seconds) for the
24// edit-one-line -> reload html cycles) is to run both the TypeScript compiler
25// and the rollup bundler in --watch mode. Any other attempt, leads to O(10s)
26// incremental-build times.
27// This script allows mixing build tools that support --watch mode (tsc and
Primiano Tucci9b567032021-02-12 14:18:12 +010028// rollup) and auto-triggering-on-file-change rules via node-watch.
Primiano Tuccic8be6812021-02-09 18:08:49 +010029// When invoked without any argument (e.g., for production builds), this script
30// just runs all the build tasks serially. It doesn't to do any mtime-based
31// check, it always re-runs all the tasks.
Primiano Tucci9b567032021-02-12 14:18:12 +010032// When invoked with --watch, it mounts a pipeline of tasks based on node-watch
Primiano Tuccic8be6812021-02-09 18:08:49 +010033// and runs them together with tsc --watch and rollup --watch.
34// The output directory structure is carefully crafted so that any change to UI
35// sources causes cascading triggers of the next steps.
36// The overall build graph looks as follows:
37// +----------------+ +-----------------------------+
38// | protos/*.proto |----->| pbjs out/tsc/gen/protos.js |--+
39// +----------------+ +-----------------------------+ |
40// +-----------------------------+ |
41// | pbts out/tsc/gen/protos.d.ts|<-+
42// +-----------------------------+
43// |
44// V +-------------------------+
45// +---------+ +-----+ | out/tsc/frontend/*.js |
46// | ui/*.ts |------------->| tsc |-> +-------------------------+ +--------+
47// +---------+ +-----+ | out/tsc/controller/*.js |-->| rollup |
48// ^ +-------------------------+ +--------+
49// +------------+ | out/tsc/engine/*.js | |
50// +-----------+ |*.wasm.js | +-------------------------+ |
51// |ninja *.cc |->|*.wasm.d.ts | |
52// +-----------+ |*.wasm |-----------------+ |
53// +------------+ | |
54// V V
55// +-----------+ +------+ +------------------------------------------------+
56// | ui/*.scss |->| scss |--->| Final out/dist/ dir |
57// +-----------+ +------+ +------------------------------------------------+
58// +----------------------+ | +----------+ +---------+ +--------------------+|
59// | src/assets/*.png | | | assets/ | |*.wasm.js| | frontend_bundle.js ||
60// +----------------------+ | | *.css | |*.wasm | +--------------------+|
61// | buildtools/typefaces |-->| | *.png | +---------+ |controller_bundle.js||
62// +----------------------+ | | *.woff2 | +--------------------+|
63// | buildtools/legacy_tv | | | tv.html | | engine_bundle.js ||
64// +----------------------+ | +----------+ +--------------------+|
65// +------------------------------------------------+
66
67const argparse = require('argparse');
68const child_process = require('child_process');
Tuchila Octavian885f6892021-04-06 10:44:13 +010069const crypto = require('crypto');
Primiano Tuccic8be6812021-02-09 18:08:49 +010070const fs = require('fs');
71const http = require('http');
72const path = require('path');
Primiano Tucci9b567032021-02-12 14:18:12 +010073const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux.
Primiano Tuccic8be6812021-02-09 18:08:49 +010074const pjoin = path.join;
75
76const ROOT_DIR = path.dirname(__dirname); // The repo root.
77const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py');
78
79const cfg = {
80 watch: false,
81 verbose: false,
82 debug: false,
83 startHttpServer: false,
84 wasmModules: ['trace_processor', 'trace_to_text'],
85 testConfigs: ['jest.unit.config.js'],
86
87 // The fields below will be changed by main() after cmdline parsing.
88 // Directory structure:
Primiano Tucci7eead2e2021-02-16 11:46:04 +010089 // out/xxx/ -> outDir : Root build dir, for both ninja/wasm and UI.
90 // ui/ -> outUiDir : UI dir. All outputs from this script.
91 // tsc/ -> outTscDir : Transpiled .ts -> .js.
92 // gen/ -> outGenDir : Auto-generated .ts/.js (e.g. protos).
93 // dist/ -> outDistRootDir : Only index.html and service_worker.js
94 // v1.2/ -> outDistDir : JS bundles and assets
95 // chrome_extension/ : Chrome extension.
Primiano Tuccic8be6812021-02-09 18:08:49 +010096 outDir: pjoin(ROOT_DIR, 'out/ui'),
Primiano Tucci7eead2e2021-02-16 11:46:04 +010097 version: '', // v1.2.3, derived from the CHANGELOG + git.
Primiano Tuccic8be6812021-02-09 18:08:49 +010098 outUiDir: '',
Primiano Tucci7eead2e2021-02-16 11:46:04 +010099 outDistRootDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100100 outTscDir: '',
101 outGenDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100102 outDistDir: '',
103 outExtDir: '',
104};
105
106const RULES = [
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100107 {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100108 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
109 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
110 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
111 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
112 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
113 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100114 {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100115 {r: /.*\/dist\/.*/, f: notifyLiveServer},
116];
117
118let tasks = [];
119let tasksTot = 0, tasksRan = 0;
120let serverStarted = false;
121let httpWatches = [];
122let tStart = Date.now();
123let subprocesses = [];
124
125function main() {
126 const parser = new argparse.ArgumentParser();
127 parser.addArgument('--out', {help: 'Output directory'});
128 parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
129 parser.addArgument(['--serve', '-s'], {action: 'storeTrue'});
130 parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
131 parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
132 parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
133 parser.addArgument(['--run-tests', '-t'], {action: 'storeTrue'});
134 parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
135
136 const args = parser.parseArgs();
137 const clean = !args.no_build;
138 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
139 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
140 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
141 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100142 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'});
143 cfg.version = proc.stdout.toString().trim();
144 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100145 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
146 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
147 cfg.watch = !!args.watch;
148 cfg.verbose = !!args.verbose;
149 cfg.debug = !!args.debug;
150 cfg.startHttpServer = args.serve;
151
152 process.on('SIGINT', () => {
153 console.log('\nSIGINT received. Killing all child processes and exiting');
154 for (const proc of subprocesses) {
155 if (proc) proc.kill('SIGINT');
156 }
157 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT.
158 });
159
160 // Check that deps are current before starting.
161 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
Tuchila Octaviane48d3062021-04-22 13:58:14 +0100162 const checkDepsPath = pjoin(cfg.outDir, '.check_deps');
163 const depsArgs = [`--check-only=${checkDepsPath}`, '--ui'];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100164 exec(installBuildDeps, depsArgs);
165
166 console.log('Entering', cfg.outDir);
167 process.chdir(cfg.outDir);
168
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100169 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/
Primiano Tuccic8be6812021-02-09 18:08:49 +0100170
171 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
172 // server is started when the task queue reaches quiescence, but it takes at
173 // least one task for that.
174 addTask(() => {});
175
176 if (!args.no_build) {
177 buildWasm(args.no_wasm);
178 scanDir('ui/src/assets');
179 scanDir('ui/src/chrome_extension');
180 scanDir('buildtools/typefaces');
181 scanDir('buildtools/catapult_trace_viewer');
182 compileProtos();
183 genVersion();
184 transpileTsProject('ui');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100185 transpileTsProject('ui/src/service_worker');
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100186 bundleJs('rollup.config.js');
187 genServiceWorkerManifestJson();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100188
189 // Watches the /dist. When changed:
190 // - Notifies the HTTP live reload clients.
191 // - Regenerates the ServiceWorker file map.
192 scanDir(cfg.outDistRootDir);
193 }
194
195 if (args.run_tests) {
196 runTests();
197 }
198}
199
200// -----------
201// Build rules
202// -----------
203
204function runTests() {
205 const args =
206 ['--rootDir', cfg.outTscDir, '--verbose', '--runInBand', '--forceExit'];
207 for (const cfgFile of cfg.testConfigs) {
208 args.push('--projects', pjoin(ROOT_DIR, 'ui/config', cfgFile));
209 }
210 if (cfg.watch) {
211 args.push('--watchAll');
212 addTask(execNode, ['jest', args, {async: true}]);
213 } else {
214 addTask(execNode, ['jest', args]);
215 }
216}
217
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100218function copyIndexHtml(src) {
219 const index_html = () => {
220 let html = fs.readFileSync(src).toString();
221 // First copy the index.html as-is into the dist/v1.2.3/ directory. This is
222 // only used for archival purporses, so one can open
223 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic.
224 fs.writeFileSync(pjoin(cfg.outDistDir, 'index.html'), html);
225
226 // Then copy it into the dist/ root by patching the version code.
227 // TODO(primiano): in next CLs, this script should take a
228 // --release_map=xxx.json argument, to populate this with multiple channels.
229 const versionMap = JSON.stringify({'stable': cfg.version});
230 const bodyRegex = /data-perfetto_version='[^']*'/;
231 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`);
232 fs.writeFileSync(pjoin(cfg.outDistRootDir, 'index.html'), html);
233 };
234 addTask(index_html);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100235}
236
237function copyAssets(src, dst) {
238 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
239}
240
241function compileScss() {
242 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
243 const dst = pjoin(cfg.outDistDir, 'perfetto.css');
244 // In watch mode, don't exit(1) if scss fails. It can easily happen by
Tuchila Octavian885f6892021-04-06 10:44:13 +0100245 // having a typo in the css. It will still print an error.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100246 const noErrCheck = !!cfg.watch;
247 addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]);
248}
249
250function compileProtos() {
251 const dstJs = pjoin(cfg.outGenDir, 'protos.js');
252 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
253 const inputs = [
254 'protos/perfetto/trace_processor/trace_processor.proto',
255 'protos/perfetto/common/trace_stats.proto',
256 'protos/perfetto/common/tracing_service_capabilities.proto',
257 'protos/perfetto/config/perfetto_config.proto',
258 'protos/perfetto/ipc/consumer_port.proto',
259 'protos/perfetto/ipc/wire_protocol.proto',
260 'protos/perfetto/metrics/metrics.proto',
261 ];
262 const pbjsArgs = [
263 '--force-number',
264 '-t',
265 'static-module',
266 '-w',
267 'commonjs',
268 '-p',
269 ROOT_DIR,
270 '-o',
271 dstJs
272 ].concat(inputs);
273 addTask(execNode, ['pbjs', pbjsArgs]);
274 const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs];
275 addTask(execNode, ['pbts', pbtsArgs]);
276}
277
278// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
279function genVersion() {
280 const cmd = 'python3';
281 const args =
Tuchila Octavian885f6892021-04-06 10:44:13 +0100282 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100283 addTask(exec, [cmd, args]);
284}
285
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100286function updateSymlinks() {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100287 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100288
289 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
290 // can point to that without having to know the current version number.
291 mklink(
292 path.relative(cfg.outUiDir, cfg.outDistDir),
293 pjoin(cfg.outUiDir, 'dist_version'));
294
Primiano Tuccic8be6812021-02-09 18:08:49 +0100295 mklink(
296 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'))
297}
298
299// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules.
300// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
301// out/tsc/, so the typescript compiler and the bundler can pick them up.
302function buildWasm(skipWasmBuild) {
303 if (!skipWasmBuild) {
304 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
305 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
306
Tuchila Octavian885f6892021-04-06 10:44:13 +0100307 const ninjaArgs = ['-C', cfg.outDir];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100308 ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`));
309 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
310 }
311
312 const wasmOutDir = pjoin(cfg.outDir, 'wasm');
313 for (const wasmMod of cfg.wasmModules) {
314 // The .wasm file goes directly into the dist dir (also .map in debug)
315 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
316 const src = `${wasmOutDir}/${wasmMod}${ext}`;
317 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
318 }
319 // The .js / .ts go into intermediates, they will be bundled by rollup.
320 for (const ext of ['.js', '.d.ts']) {
321 const fname = `${wasmMod}${ext}`;
322 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
323 }
324 }
325}
326
327// This transpiles all the sources (frontend, controller, engine, extension) in
328// one go. The only project that has a dedicated invocation is service_worker.
329function transpileTsProject(project) {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100330 const args = ['--project', pjoin(ROOT_DIR, project)];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100331 if (cfg.watch) {
332 args.push('--watch', '--preserveWatchOutput');
333 addTask(execNode, ['tsc', args, {async: true}]);
334 } else {
335 addTask(execNode, ['tsc', args]);
336 }
337}
338
339// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
340function bundleJs(cfgName) {
Tuchila Octavian885f6892021-04-06 10:44:13 +0100341 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100342 const args = ['-c', rcfg, '--no-indent'];
343 args.push(...(cfg.verbose ? [] : ['--silent']));
344 if (cfg.watch) {
345 // --waitForBundleInput is so that we can run tsc --watch and rollup --watch
346 // together, without having to wait that tsc completes the first build.
347 args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen');
348 addTask(execNode, ['rollup', args, {async: true}]);
349 } else {
350 addTask(execNode, ['rollup', args]);
351 }
352}
353
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100354function genServiceWorkerManifestJson() {
355 function make_manifest() {
356 const manifest = {resources: {}};
357 // When building the subresource manifest skip source maps, the manifest
358 // itself and the copy of the index.html which is copied under /v1.2.3/.
359 // The root /index.html will be fetched by service_worker.js separately.
360 const skipRegex = /(\.map|manifest\.json|index.html)$/;
361 walk(cfg.outDistDir, absPath => {
362 const contents = fs.readFileSync(absPath);
363 const relPath = path.relative(cfg.outDistDir, absPath);
364 const b64 = crypto.createHash('sha256').update(contents).digest('base64');
365 manifest.resources[relPath] = 'sha256-' + b64;
366 }, skipRegex);
367 const manifestJson = JSON.stringify(manifest, null, 2);
368 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100369 }
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100370 addTask(make_manifest, []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100371}
372
373function startServer() {
374 const port = 10000;
Tuchila Octavian885f6892021-04-06 10:44:13 +0100375 console.log(`Starting HTTP server on http://localhost:${port}`);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100376 http.createServer(function(req, res) {
377 console.debug(req.method, req.url);
378 let uri = req.url.split('?', 1)[0];
379 if (uri.endsWith('/')) {
380 uri += 'index.html';
381 }
382
383 if (uri === '/live_reload') {
384 // Implements the Server-Side-Events protocol.
385 const head = {
386 'Content-Type': 'text/event-stream',
387 'Connection': 'keep-alive',
388 'Cache-Control': 'no-cache'
389 };
390 res.writeHead(200, head);
391 const arrayIdx = httpWatches.length;
392 // We never remove from the array, the delete leaves an undefined item
393 // around. It makes keeping track of the index easier at the cost of a
394 // small leak.
395 httpWatches.push(res);
396 req.on('close', () => delete httpWatches[arrayIdx]);
397 return;
398 }
399
400 const absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
401 fs.readFile(absPath, function(err, data) {
402 if (err) {
403 res.writeHead(404);
404 res.end(JSON.stringify(err));
405 return;
406 }
407
408 const mimeMap = {
409 'html': 'text/html',
410 'css': 'text/css',
411 'js': 'application/javascript',
412 'wasm': 'application/wasm',
413 };
414 const ext = uri.split('.').pop();
415 const cType = mimeMap[ext] || 'octect/stream';
416 const head = {
417 'Content-Type': cType,
418 'Content-Length': data.length,
419 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
420 'Cache-Control': 'no-cache',
421 };
422 res.writeHead(200, head);
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100423 res.write(data);
424 res.end();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100425 });
426 })
Primiano Tucci19068d12021-02-23 20:29:56 +0100427 .listen(port, '127.0.0.1');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100428}
429
430// Called whenever a change in the out/dist directory is detected. It sends a
431// Server-Side-Event to the live_reload.ts script.
432function notifyLiveServer(changedFile) {
433 for (const cli of httpWatches) {
434 if (cli === undefined) continue;
435 cli.write(
436 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
437 }
438}
439
440function copyExtensionAssets() {
441 addTask(cp, [
442 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
443 pjoin(cfg.outExtDir, 'logo-128.png')
444 ]);
445 addTask(cp, [
446 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
447 pjoin(cfg.outExtDir, 'manifest.json')
448 ]);
449}
450
451// -----------------------
452// Task chaining functions
453// -----------------------
454
455function addTask(func, args) {
456 const task = new Task(func, args);
457 for (const t of tasks) {
458 if (t.identity === task.identity) {
459 return;
460 }
461 }
462 tasks.push(task);
463 setTimeout(runTasks, 0);
464}
465
466function runTasks() {
467 const snapTasks = tasks.splice(0); // snap = std::move(tasks).
468 tasksTot += snapTasks.length;
469 for (const task of snapTasks) {
470 const DIM = '\u001b[2m';
471 const BRT = '\u001b[37m';
472 const RST = '\u001b[0m';
473 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
474 const ts = `[${DIM}${ms}${RST}]`;
475 const descr = task.description.substr(0, 80);
476 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
477 task.func.apply(/*this=*/ undefined, task.args);
478 }
479 // Start the web server once reaching quiescence.
480 if (tasks.length === 0 && !serverStarted && cfg.startHttpServer) {
481 serverStarted = true;
482 startServer();
483 }
484}
485
486// Executes all the RULES that match the given |absPath|.
487function scanFile(absPath) {
488 console.assert(fs.existsSync(absPath));
489 console.assert(path.isAbsolute(absPath));
490 const normPath = path.relative(ROOT_DIR, absPath);
491 for (const rule of RULES) {
492 const match = rule.r.exec(normPath);
493 if (!match || match[0] !== normPath) continue;
494 const captureGroup = match.length > 1 ? match[1] : undefined;
495 rule.f(absPath, captureGroup);
496 }
497}
498
499// Walks the passed |dir| recursively and, for each file, invokes the matching
Primiano Tucci9b567032021-02-12 14:18:12 +0100500// RULES. If --watch is used, it also installs a fswatch() and re-triggers the
Primiano Tuccic8be6812021-02-09 18:08:49 +0100501// matching RULES on each file change.
502function scanDir(dir, regex) {
503 const filterFn = regex ? absPath => regex.test(absPath) : () => true;
504 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
505 // Add a fs watch if in watch mode.
506 if (cfg.watch) {
Primiano Tucci9a92cf72021-02-13 17:42:34 +0100507 fswatch(absDir, {recursive: true}, (_eventType, filePath) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100508 if (!filterFn(filePath)) return;
509 if (cfg.verbose) {
510 console.log('File change detected', _eventType, filePath);
511 }
512 if (fs.existsSync(filePath)) {
513 scanFile(filePath, filterFn);
514 }
515 });
516 }
517 walk(absDir, f => {
518 if (filterFn(f)) scanFile(f);
519 });
520}
521
522function exec(cmd, args, opts) {
523 opts = opts || {};
524 opts.stdout = opts.stdout || 'inherit';
525 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
526 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
527 const checkExitCode = (code, signal) => {
528 if (signal === 'SIGINT' || signal === 'SIGTERM') return;
529 if (code !== 0 && !opts.noErrCheck) {
530 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
531 process.exit(1);
532 }
533 };
534 if (opts.async) {
535 const proc = child_process.spawn(cmd, args, spwOpts);
536 const procIndex = subprocesses.length;
537 subprocesses.push(proc);
538 return new Promise((resolve, _reject) => {
539 proc.on('exit', (code, signal) => {
540 delete subprocesses[procIndex];
541 checkExitCode(code, signal);
542 resolve();
543 });
544 });
545 } else {
546 const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
547 checkExitCode(spawnRes.status, spawnRes.signal);
548 return spawnRes;
549 }
550}
551
552function execNode(module, args, opts) {
553 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
554 const nodeBin = pjoin(ROOT_DIR, 'tools/node');
555 args = [modPath].concat(args || []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100556 return exec(nodeBin, args, opts);
557}
558
559// ------------------------------------------
560// File system & subprocess utility functions
561// ------------------------------------------
562
563class Task {
564 constructor(func, args) {
565 this.func = func;
566 this.args = args || [];
567 // |identity| is used to dedupe identical tasks in the queue.
568 this.identity = JSON.stringify([this.func.name, this.args]);
569 }
570
571 get description() {
572 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
573 const flattenedArgs = [].concat.apply([], this.args);
574 for (const arg of flattenedArgs) {
575 const argStr = `${arg}`;
576 if (argStr.startsWith('/')) {
577 ret.push(path.relative(cfg.outDir, arg));
578 } else {
579 ret.push(argStr);
580 }
581 }
582 return ret.join(' ');
583 }
584}
585
586function walk(dir, callback, skipRegex) {
587 for (const child of fs.readdirSync(dir)) {
588 const childPath = pjoin(dir, child);
589 const stat = fs.lstatSync(childPath);
590 if (skipRegex !== undefined && skipRegex.test(child)) continue;
591 if (stat.isDirectory()) {
592 walk(childPath, callback, skipRegex);
593 } else if (!stat.isSymbolicLink()) {
594 callback(childPath);
595 }
596 }
597}
598
599function ensureDir(dirPath, clean) {
600 const exists = fs.existsSync(dirPath);
601 if (exists && clean) {
602 console.log('rm', dirPath);
603 fs.rmSync(dirPath, {recursive: true});
604 }
605 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
606 return dirPath;
607}
608
609function cp(src, dst) {
610 ensureDir(path.dirname(dst));
611 if (cfg.verbose) {
612 console.log(
613 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
614 }
615 fs.copyFileSync(src, dst);
616}
617
618function mklink(src, dst) {
619 // If the symlink already points to the right place don't touch it. This is
620 // to avoid changing the mtime of the ui/ dir when unnecessary.
621 if (fs.existsSync(dst)) {
622 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
623 return;
624 } else {
625 fs.unlinkSync(dst);
626 }
627 }
628 fs.symlinkSync(src, dst);
629}
630
631main();