blob: e9d6c601f3dccc3ff4f4913a3f81b3825b89bec1 [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 | +--------------------+|
Hector Dearman995d1912021-07-13 16:51:38 +010061// | buildtools/typefaces |-->| | *.png | +---------+ | engine_bundle.js ||
Primiano Tuccic8be6812021-02-09 18:08:49 +010062// +----------------------+ | | *.woff2 | +--------------------+|
Hector Dearman995d1912021-07-13 16:51:38 +010063// | buildtools/legacy_tv | | | tv.html | |traceconv_bundle.js ||
Primiano Tuccic8be6812021-02-09 18:08:49 +010064// +----------------------+ | +----------+ +--------------------+|
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');
Hector Dearman07ed69a2021-08-23 11:12:25 +010078const GEN_IMPORTS_SCRIPT = pjoin(ROOT_DIR, 'tools/gen_ui_imports');
Primiano Tuccic8be6812021-02-09 18:08:49 +010079
80const cfg = {
81 watch: false,
82 verbose: false,
83 debug: false,
84 startHttpServer: false,
85 wasmModules: ['trace_processor', 'trace_to_text'],
Primiano Tuccic8be6812021-02-09 18:08:49 +010086
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;
Primiano Tuccic8be6812021-02-09 18:08:49 +0100120let httpWatches = [];
121let tStart = Date.now();
122let subprocesses = [];
123
Primiano Tuccia60ef182021-06-11 15:37:12 +0100124async function main() {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100125 const parser = new argparse.ArgumentParser();
126 parser.addArgument('--out', {help: 'Output directory'});
127 parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
128 parser.addArgument(['--serve', '-s'], {action: 'storeTrue'});
129 parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
130 parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
131 parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
Primiano Tuccia60ef182021-06-11 15:37:12 +0100132 parser.addArgument(['--run-unittests', '-t'], {action: 'storeTrue'});
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100133 parser.addArgument(['--run-integrationtests', '-T'], {action: 'storeTrue'});
Primiano Tuccic8be6812021-02-09 18:08:49 +0100134 parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
Primiano Tuccia60ef182021-06-11 15:37:12 +0100135 parser.addArgument(['--interactive', '-i'], {action: 'storeTrue'});
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100136 parser.addArgument(['--rebaseline', '-r'], {action: 'storeTrue'});
Primiano Tuccic8be6812021-02-09 18:08:49 +0100137
138 const args = parser.parseArgs();
139 const clean = !args.no_build;
140 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
141 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
142 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
143 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100144 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'});
145 cfg.version = proc.stdout.toString().trim();
146 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100147 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
148 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
149 cfg.watch = !!args.watch;
150 cfg.verbose = !!args.verbose;
151 cfg.debug = !!args.debug;
152 cfg.startHttpServer = args.serve;
Primiano Tuccia60ef182021-06-11 15:37:12 +0100153 if (args.interactive) {
154 process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
155 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100156 if (args.rebaseline) {
157 process.env.PERFETTO_UI_TESTS_REBASELINE = '1';
158 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100159
160 process.on('SIGINT', () => {
161 console.log('\nSIGINT received. Killing all child processes and exiting');
162 for (const proc of subprocesses) {
163 if (proc) proc.kill('SIGINT');
164 }
165 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT.
166 });
167
168 // Check that deps are current before starting.
169 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
Tuchila Octaviane48d3062021-04-22 13:58:14 +0100170 const checkDepsPath = pjoin(cfg.outDir, '.check_deps');
171 const depsArgs = [`--check-only=${checkDepsPath}`, '--ui'];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100172 exec(installBuildDeps, depsArgs);
173
174 console.log('Entering', cfg.outDir);
175 process.chdir(cfg.outDir);
176
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100177 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/
Primiano Tuccic8be6812021-02-09 18:08:49 +0100178
179 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
180 // server is started when the task queue reaches quiescence, but it takes at
181 // least one task for that.
182 addTask(() => {});
183
184 if (!args.no_build) {
185 buildWasm(args.no_wasm);
186 scanDir('ui/src/assets');
187 scanDir('ui/src/chrome_extension');
188 scanDir('buildtools/typefaces');
189 scanDir('buildtools/catapult_trace_viewer');
Hector Dearman07ed69a2021-08-23 11:12:25 +0100190 generateImports('ui/src/tracks', 'all_tracks.ts');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100191 compileProtos();
192 genVersion();
193 transpileTsProject('ui');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100194 transpileTsProject('ui/src/service_worker');
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100195 bundleJs('rollup.config.js');
196 genServiceWorkerManifestJson();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100197
198 // Watches the /dist. When changed:
199 // - Notifies the HTTP live reload clients.
200 // - Regenerates the ServiceWorker file map.
201 scanDir(cfg.outDistRootDir);
202 }
203
Primiano Tuccia60ef182021-06-11 15:37:12 +0100204
205 // We should enter the loop only in watch mode, where tsc and rollup are
206 // asynchronous because they run in watch mode.
207 const tStart = Date.now();
208 while (!isDistComplete()) {
209 const secs = Math.ceil((Date.now() - tStart) / 1000);
210 process.stdout.write(`Waiting for first build to complete... ${secs} s\r`);
211 await new Promise(r => setTimeout(r, 500));
212 }
213 if (cfg.watch) console.log('\nFirst build completed!');
214
215 if (cfg.startHttpServer) {
216 startServer();
217 }
218 if (args.run_unittests) {
219 runTests('jest.unittest.config.js');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100220 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100221 if (args.run_integrationtests) {
222 runTests('jest.integrationtest.config.js');
223 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100224}
225
226// -----------
227// Build rules
228// -----------
229
Primiano Tuccia60ef182021-06-11 15:37:12 +0100230function runTests(cfgFile) {
231 const args = [
232 '--rootDir',
233 cfg.outTscDir,
234 '--verbose',
235 '--runInBand',
236 '--detectOpenHandles',
237 '--forceExit',
238 '--projects',
239 pjoin(ROOT_DIR, 'ui/config', cfgFile)
240 ];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100241 if (cfg.watch) {
242 args.push('--watchAll');
243 addTask(execNode, ['jest', args, {async: true}]);
244 } else {
245 addTask(execNode, ['jest', args]);
246 }
247}
248
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100249function copyIndexHtml(src) {
250 const index_html = () => {
251 let html = fs.readFileSync(src).toString();
252 // First copy the index.html as-is into the dist/v1.2.3/ directory. This is
253 // only used for archival purporses, so one can open
254 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic.
255 fs.writeFileSync(pjoin(cfg.outDistDir, 'index.html'), html);
256
257 // Then copy it into the dist/ root by patching the version code.
258 // TODO(primiano): in next CLs, this script should take a
259 // --release_map=xxx.json argument, to populate this with multiple channels.
260 const versionMap = JSON.stringify({'stable': cfg.version});
261 const bodyRegex = /data-perfetto_version='[^']*'/;
262 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`);
263 fs.writeFileSync(pjoin(cfg.outDistRootDir, 'index.html'), html);
264 };
265 addTask(index_html);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100266}
267
268function copyAssets(src, dst) {
269 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
270}
271
272function compileScss() {
273 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
274 const dst = pjoin(cfg.outDistDir, 'perfetto.css');
275 // In watch mode, don't exit(1) if scss fails. It can easily happen by
Tuchila Octavian885f6892021-04-06 10:44:13 +0100276 // having a typo in the css. It will still print an error.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100277 const noErrCheck = !!cfg.watch;
278 addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]);
279}
280
281function compileProtos() {
282 const dstJs = pjoin(cfg.outGenDir, 'protos.js');
283 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
284 const inputs = [
285 'protos/perfetto/trace_processor/trace_processor.proto',
286 'protos/perfetto/common/trace_stats.proto',
287 'protos/perfetto/common/tracing_service_capabilities.proto',
288 'protos/perfetto/config/perfetto_config.proto',
289 'protos/perfetto/ipc/consumer_port.proto',
290 'protos/perfetto/ipc/wire_protocol.proto',
291 'protos/perfetto/metrics/metrics.proto',
292 ];
293 const pbjsArgs = [
294 '--force-number',
295 '-t',
296 'static-module',
297 '-w',
298 'commonjs',
299 '-p',
300 ROOT_DIR,
301 '-o',
302 dstJs
303 ].concat(inputs);
304 addTask(execNode, ['pbjs', pbjsArgs]);
305 const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs];
306 addTask(execNode, ['pbts', pbtsArgs]);
307}
308
Hector Dearman07ed69a2021-08-23 11:12:25 +0100309function generateImports(dir, name) {
310 // We have to use the symlink (ui/src/gen) rather than cfg.outGenDir
311 // below since we want to generate the correct relative imports. For example:
312 // ui/src/frontend/foo.ts
313 // import '../gen/all_plugins.ts';
314 // ui/src/gen/all_plugins.ts (aka ui/out/tsc/gen/all_plugins.ts)
315 // import '../frontend/some_plugin.ts';
316 const dstTs = pjoin(ROOT_DIR, 'ui/src/gen', name);
317 const inputDir = pjoin(ROOT_DIR, dir);
318 const args = [GEN_IMPORTS_SCRIPT, inputDir, '--out', dstTs];
319 addTask(exec, ['python3', args]);
320}
321
Primiano Tuccic8be6812021-02-09 18:08:49 +0100322// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
323function genVersion() {
324 const cmd = 'python3';
325 const args =
Tuchila Octavian885f6892021-04-06 10:44:13 +0100326 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100327 addTask(exec, [cmd, args]);
328}
329
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100330function updateSymlinks() {
Primiano Tuccia60ef182021-06-11 15:37:12 +0100331 // /ui/out -> /out/ui.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100332 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100333
Primiano Tuccia60ef182021-06-11 15:37:12 +0100334 // /out/ui/test/data -> /test/data (For UI tests).
335 mklink(
336 pjoin(ROOT_DIR, 'test/data'),
337 pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data'));
338
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100339 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
340 // can point to that without having to know the current version number.
341 mklink(
342 path.relative(cfg.outUiDir, cfg.outDistDir),
343 pjoin(cfg.outUiDir, 'dist_version'));
344
Primiano Tuccic8be6812021-02-09 18:08:49 +0100345 mklink(
346 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'))
347}
348
349// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules.
350// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
351// out/tsc/, so the typescript compiler and the bundler can pick them up.
352function buildWasm(skipWasmBuild) {
353 if (!skipWasmBuild) {
354 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
355 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
356
Tuchila Octavian885f6892021-04-06 10:44:13 +0100357 const ninjaArgs = ['-C', cfg.outDir];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100358 ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`));
359 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
360 }
361
362 const wasmOutDir = pjoin(cfg.outDir, 'wasm');
363 for (const wasmMod of cfg.wasmModules) {
364 // The .wasm file goes directly into the dist dir (also .map in debug)
365 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
366 const src = `${wasmOutDir}/${wasmMod}${ext}`;
367 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
368 }
369 // The .js / .ts go into intermediates, they will be bundled by rollup.
370 for (const ext of ['.js', '.d.ts']) {
371 const fname = `${wasmMod}${ext}`;
372 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
373 }
374 }
375}
376
377// This transpiles all the sources (frontend, controller, engine, extension) in
378// one go. The only project that has a dedicated invocation is service_worker.
379function transpileTsProject(project) {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100380 const args = ['--project', pjoin(ROOT_DIR, project)];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100381 if (cfg.watch) {
382 args.push('--watch', '--preserveWatchOutput');
383 addTask(execNode, ['tsc', args, {async: true}]);
384 } else {
385 addTask(execNode, ['tsc', args]);
386 }
387}
388
389// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
390function bundleJs(cfgName) {
Tuchila Octavian885f6892021-04-06 10:44:13 +0100391 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100392 const args = ['-c', rcfg, '--no-indent'];
393 args.push(...(cfg.verbose ? [] : ['--silent']));
394 if (cfg.watch) {
395 // --waitForBundleInput is so that we can run tsc --watch and rollup --watch
396 // together, without having to wait that tsc completes the first build.
397 args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen');
398 addTask(execNode, ['rollup', args, {async: true}]);
399 } else {
400 addTask(execNode, ['rollup', args]);
401 }
402}
403
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100404function genServiceWorkerManifestJson() {
405 function make_manifest() {
406 const manifest = {resources: {}};
407 // When building the subresource manifest skip source maps, the manifest
408 // itself and the copy of the index.html which is copied under /v1.2.3/.
409 // The root /index.html will be fetched by service_worker.js separately.
410 const skipRegex = /(\.map|manifest\.json|index.html)$/;
411 walk(cfg.outDistDir, absPath => {
412 const contents = fs.readFileSync(absPath);
413 const relPath = path.relative(cfg.outDistDir, absPath);
414 const b64 = crypto.createHash('sha256').update(contents).digest('base64');
415 manifest.resources[relPath] = 'sha256-' + b64;
416 }, skipRegex);
417 const manifestJson = JSON.stringify(manifest, null, 2);
418 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100419 }
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100420 addTask(make_manifest, []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100421}
422
423function startServer() {
424 const port = 10000;
Tuchila Octavian885f6892021-04-06 10:44:13 +0100425 console.log(`Starting HTTP server on http://localhost:${port}`);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100426 http.createServer(function(req, res) {
427 console.debug(req.method, req.url);
428 let uri = req.url.split('?', 1)[0];
429 if (uri.endsWith('/')) {
430 uri += 'index.html';
431 }
432
433 if (uri === '/live_reload') {
434 // Implements the Server-Side-Events protocol.
435 const head = {
436 'Content-Type': 'text/event-stream',
437 'Connection': 'keep-alive',
438 'Cache-Control': 'no-cache'
439 };
440 res.writeHead(200, head);
441 const arrayIdx = httpWatches.length;
442 // We never remove from the array, the delete leaves an undefined item
443 // around. It makes keeping track of the index easier at the cost of a
444 // small leak.
445 httpWatches.push(res);
446 req.on('close', () => delete httpWatches[arrayIdx]);
447 return;
448 }
449
Tuchila Octavian1d423b22021-07-01 09:39:54 +0100450 let absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
451 // We want to be able to use the data in '/test/' for e2e tests.
452 // However, we don't want do create a symlink into the 'dist/' dir,
453 // because 'dist/' gets shipped on the production server.
454 if (uri.startsWith('/test/')) {
455 absPath = pjoin(ROOT_DIR, uri);
456 }
457
Primiano Tuccic8be6812021-02-09 18:08:49 +0100458 fs.readFile(absPath, function(err, data) {
459 if (err) {
460 res.writeHead(404);
461 res.end(JSON.stringify(err));
462 return;
463 }
464
465 const mimeMap = {
466 'html': 'text/html',
467 'css': 'text/css',
468 'js': 'application/javascript',
469 'wasm': 'application/wasm',
470 };
471 const ext = uri.split('.').pop();
472 const cType = mimeMap[ext] || 'octect/stream';
473 const head = {
474 'Content-Type': cType,
475 'Content-Length': data.length,
476 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
477 'Cache-Control': 'no-cache',
478 };
479 res.writeHead(200, head);
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100480 res.write(data);
481 res.end();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100482 });
483 })
Primiano Tucci19068d12021-02-23 20:29:56 +0100484 .listen(port, '127.0.0.1');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100485}
486
Primiano Tuccia60ef182021-06-11 15:37:12 +0100487function isDistComplete() {
488 const requiredArtifacts = [
Primiano Tuccia60ef182021-06-11 15:37:12 +0100489 'frontend_bundle.js',
490 'engine_bundle.js',
Hector Dearman995d1912021-07-13 16:51:38 +0100491 'traceconv_bundle.js',
Primiano Tuccia60ef182021-06-11 15:37:12 +0100492 'trace_processor.wasm',
493 'perfetto.css',
494 ];
495 const relPaths = new Set();
496 walk(cfg.outDistDir, absPath => {
497 relPaths.add(path.relative(cfg.outDistDir, absPath));
498 });
499 for (const fName of requiredArtifacts) {
500 if (!relPaths.has(fName)) return false;
501 }
502 return true;
503}
504
Primiano Tuccic8be6812021-02-09 18:08:49 +0100505// Called whenever a change in the out/dist directory is detected. It sends a
506// Server-Side-Event to the live_reload.ts script.
507function notifyLiveServer(changedFile) {
508 for (const cli of httpWatches) {
509 if (cli === undefined) continue;
510 cli.write(
511 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
512 }
513}
514
515function copyExtensionAssets() {
516 addTask(cp, [
517 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
518 pjoin(cfg.outExtDir, 'logo-128.png')
519 ]);
520 addTask(cp, [
521 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
522 pjoin(cfg.outExtDir, 'manifest.json')
523 ]);
524}
525
526// -----------------------
527// Task chaining functions
528// -----------------------
529
530function addTask(func, args) {
531 const task = new Task(func, args);
532 for (const t of tasks) {
533 if (t.identity === task.identity) {
534 return;
535 }
536 }
537 tasks.push(task);
538 setTimeout(runTasks, 0);
539}
540
541function runTasks() {
542 const snapTasks = tasks.splice(0); // snap = std::move(tasks).
543 tasksTot += snapTasks.length;
544 for (const task of snapTasks) {
545 const DIM = '\u001b[2m';
546 const BRT = '\u001b[37m';
547 const RST = '\u001b[0m';
548 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
549 const ts = `[${DIM}${ms}${RST}]`;
550 const descr = task.description.substr(0, 80);
551 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
552 task.func.apply(/*this=*/ undefined, task.args);
553 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100554}
555
556// Executes all the RULES that match the given |absPath|.
557function scanFile(absPath) {
558 console.assert(fs.existsSync(absPath));
559 console.assert(path.isAbsolute(absPath));
560 const normPath = path.relative(ROOT_DIR, absPath);
561 for (const rule of RULES) {
562 const match = rule.r.exec(normPath);
563 if (!match || match[0] !== normPath) continue;
564 const captureGroup = match.length > 1 ? match[1] : undefined;
565 rule.f(absPath, captureGroup);
566 }
567}
568
569// Walks the passed |dir| recursively and, for each file, invokes the matching
Primiano Tucci9b567032021-02-12 14:18:12 +0100570// RULES. If --watch is used, it also installs a fswatch() and re-triggers the
Primiano Tuccic8be6812021-02-09 18:08:49 +0100571// matching RULES on each file change.
572function scanDir(dir, regex) {
573 const filterFn = regex ? absPath => regex.test(absPath) : () => true;
574 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
575 // Add a fs watch if in watch mode.
576 if (cfg.watch) {
Primiano Tucci9a92cf72021-02-13 17:42:34 +0100577 fswatch(absDir, {recursive: true}, (_eventType, filePath) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100578 if (!filterFn(filePath)) return;
579 if (cfg.verbose) {
580 console.log('File change detected', _eventType, filePath);
581 }
582 if (fs.existsSync(filePath)) {
583 scanFile(filePath, filterFn);
584 }
585 });
586 }
587 walk(absDir, f => {
588 if (filterFn(f)) scanFile(f);
589 });
590}
591
592function exec(cmd, args, opts) {
593 opts = opts || {};
594 opts.stdout = opts.stdout || 'inherit';
595 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
596 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
597 const checkExitCode = (code, signal) => {
598 if (signal === 'SIGINT' || signal === 'SIGTERM') return;
599 if (code !== 0 && !opts.noErrCheck) {
600 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
601 process.exit(1);
602 }
603 };
604 if (opts.async) {
605 const proc = child_process.spawn(cmd, args, spwOpts);
606 const procIndex = subprocesses.length;
607 subprocesses.push(proc);
608 return new Promise((resolve, _reject) => {
609 proc.on('exit', (code, signal) => {
610 delete subprocesses[procIndex];
611 checkExitCode(code, signal);
612 resolve();
613 });
614 });
615 } else {
616 const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
617 checkExitCode(spawnRes.status, spawnRes.signal);
618 return spawnRes;
619 }
620}
621
622function execNode(module, args, opts) {
623 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
624 const nodeBin = pjoin(ROOT_DIR, 'tools/node');
625 args = [modPath].concat(args || []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100626 return exec(nodeBin, args, opts);
627}
628
629// ------------------------------------------
630// File system & subprocess utility functions
631// ------------------------------------------
632
633class Task {
634 constructor(func, args) {
635 this.func = func;
636 this.args = args || [];
637 // |identity| is used to dedupe identical tasks in the queue.
638 this.identity = JSON.stringify([this.func.name, this.args]);
639 }
640
641 get description() {
642 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
643 const flattenedArgs = [].concat.apply([], this.args);
644 for (const arg of flattenedArgs) {
645 const argStr = `${arg}`;
646 if (argStr.startsWith('/')) {
647 ret.push(path.relative(cfg.outDir, arg));
648 } else {
649 ret.push(argStr);
650 }
651 }
652 return ret.join(' ');
653 }
654}
655
656function walk(dir, callback, skipRegex) {
657 for (const child of fs.readdirSync(dir)) {
658 const childPath = pjoin(dir, child);
659 const stat = fs.lstatSync(childPath);
660 if (skipRegex !== undefined && skipRegex.test(child)) continue;
661 if (stat.isDirectory()) {
662 walk(childPath, callback, skipRegex);
663 } else if (!stat.isSymbolicLink()) {
664 callback(childPath);
665 }
666 }
667}
668
669function ensureDir(dirPath, clean) {
670 const exists = fs.existsSync(dirPath);
671 if (exists && clean) {
672 console.log('rm', dirPath);
673 fs.rmSync(dirPath, {recursive: true});
674 }
675 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
676 return dirPath;
677}
678
679function cp(src, dst) {
680 ensureDir(path.dirname(dst));
681 if (cfg.verbose) {
682 console.log(
683 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
684 }
685 fs.copyFileSync(src, dst);
686}
687
688function mklink(src, dst) {
689 // If the symlink already points to the right place don't touch it. This is
690 // to avoid changing the mtime of the ui/ dir when unnecessary.
691 if (fs.existsSync(dst)) {
692 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
693 return;
694 } else {
695 fs.unlinkSync(dst);
696 }
697 }
698 fs.symlinkSync(src, dst);
699}
700
701main();