blob: 6873dd79c16b31f5b2dad1fa4745770a2f723403 [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'],
Primiano Tuccic8be6812021-02-09 18:08:49 +010085
86 // The fields below will be changed by main() after cmdline parsing.
87 // Directory structure:
Primiano Tucci7eead2e2021-02-16 11:46:04 +010088 // out/xxx/ -> outDir : Root build dir, for both ninja/wasm and UI.
89 // ui/ -> outUiDir : UI dir. All outputs from this script.
90 // tsc/ -> outTscDir : Transpiled .ts -> .js.
91 // gen/ -> outGenDir : Auto-generated .ts/.js (e.g. protos).
92 // dist/ -> outDistRootDir : Only index.html and service_worker.js
93 // v1.2/ -> outDistDir : JS bundles and assets
94 // chrome_extension/ : Chrome extension.
Primiano Tuccic8be6812021-02-09 18:08:49 +010095 outDir: pjoin(ROOT_DIR, 'out/ui'),
Primiano Tucci7eead2e2021-02-16 11:46:04 +010096 version: '', // v1.2.3, derived from the CHANGELOG + git.
Primiano Tuccic8be6812021-02-09 18:08:49 +010097 outUiDir: '',
Primiano Tucci7eead2e2021-02-16 11:46:04 +010098 outDistRootDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +010099 outTscDir: '',
100 outGenDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100101 outDistDir: '',
102 outExtDir: '',
103};
104
105const RULES = [
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100106 {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100107 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
108 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
109 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
110 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
111 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
112 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100113 {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100114 {r: /.*\/dist\/.*/, f: notifyLiveServer},
115];
116
117let tasks = [];
118let tasksTot = 0, tasksRan = 0;
Primiano Tuccic8be6812021-02-09 18:08:49 +0100119let httpWatches = [];
120let tStart = Date.now();
121let subprocesses = [];
122
Primiano Tuccia60ef182021-06-11 15:37:12 +0100123async function main() {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100124 const parser = new argparse.ArgumentParser();
125 parser.addArgument('--out', {help: 'Output directory'});
126 parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
127 parser.addArgument(['--serve', '-s'], {action: 'storeTrue'});
128 parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
129 parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
130 parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
Primiano Tuccia60ef182021-06-11 15:37:12 +0100131 parser.addArgument(['--run-unittests', '-t'], {action: 'storeTrue'});
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100132 parser.addArgument(['--run-integrationtests', '-T'], {action: 'storeTrue'});
Primiano Tuccic8be6812021-02-09 18:08:49 +0100133 parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
Primiano Tuccia60ef182021-06-11 15:37:12 +0100134 parser.addArgument(['--interactive', '-i'], {action: 'storeTrue'});
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100135 parser.addArgument(['--rebaseline', '-r'], {action: 'storeTrue'});
Primiano Tuccic8be6812021-02-09 18:08:49 +0100136
137 const args = parser.parseArgs();
138 const clean = !args.no_build;
139 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
140 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
141 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
142 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100143 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'});
144 cfg.version = proc.stdout.toString().trim();
145 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100146 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
147 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
148 cfg.watch = !!args.watch;
149 cfg.verbose = !!args.verbose;
150 cfg.debug = !!args.debug;
151 cfg.startHttpServer = args.serve;
Primiano Tuccia60ef182021-06-11 15:37:12 +0100152 if (args.interactive) {
153 process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
154 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100155 if (args.rebaseline) {
156 process.env.PERFETTO_UI_TESTS_REBASELINE = '1';
157 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100158
159 process.on('SIGINT', () => {
160 console.log('\nSIGINT received. Killing all child processes and exiting');
161 for (const proc of subprocesses) {
162 if (proc) proc.kill('SIGINT');
163 }
164 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT.
165 });
166
167 // Check that deps are current before starting.
168 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
Tuchila Octaviane48d3062021-04-22 13:58:14 +0100169 const checkDepsPath = pjoin(cfg.outDir, '.check_deps');
170 const depsArgs = [`--check-only=${checkDepsPath}`, '--ui'];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100171 exec(installBuildDeps, depsArgs);
172
173 console.log('Entering', cfg.outDir);
174 process.chdir(cfg.outDir);
175
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100176 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/
Primiano Tuccic8be6812021-02-09 18:08:49 +0100177
178 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
179 // server is started when the task queue reaches quiescence, but it takes at
180 // least one task for that.
181 addTask(() => {});
182
183 if (!args.no_build) {
184 buildWasm(args.no_wasm);
185 scanDir('ui/src/assets');
186 scanDir('ui/src/chrome_extension');
187 scanDir('buildtools/typefaces');
188 scanDir('buildtools/catapult_trace_viewer');
189 compileProtos();
190 genVersion();
191 transpileTsProject('ui');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100192 transpileTsProject('ui/src/service_worker');
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100193 bundleJs('rollup.config.js');
194 genServiceWorkerManifestJson();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100195
196 // Watches the /dist. When changed:
197 // - Notifies the HTTP live reload clients.
198 // - Regenerates the ServiceWorker file map.
199 scanDir(cfg.outDistRootDir);
200 }
201
Primiano Tuccia60ef182021-06-11 15:37:12 +0100202
203 // We should enter the loop only in watch mode, where tsc and rollup are
204 // asynchronous because they run in watch mode.
205 const tStart = Date.now();
206 while (!isDistComplete()) {
207 const secs = Math.ceil((Date.now() - tStart) / 1000);
208 process.stdout.write(`Waiting for first build to complete... ${secs} s\r`);
209 await new Promise(r => setTimeout(r, 500));
210 }
211 if (cfg.watch) console.log('\nFirst build completed!');
212
213 if (cfg.startHttpServer) {
214 startServer();
215 }
216 if (args.run_unittests) {
217 runTests('jest.unittest.config.js');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100218 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100219 if (args.run_integrationtests) {
220 runTests('jest.integrationtest.config.js');
221 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100222}
223
224// -----------
225// Build rules
226// -----------
227
Primiano Tuccia60ef182021-06-11 15:37:12 +0100228function runTests(cfgFile) {
229 const args = [
230 '--rootDir',
231 cfg.outTscDir,
232 '--verbose',
233 '--runInBand',
234 '--detectOpenHandles',
235 '--forceExit',
236 '--projects',
237 pjoin(ROOT_DIR, 'ui/config', cfgFile)
238 ];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100239 if (cfg.watch) {
240 args.push('--watchAll');
241 addTask(execNode, ['jest', args, {async: true}]);
242 } else {
243 addTask(execNode, ['jest', args]);
244 }
245}
246
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100247function copyIndexHtml(src) {
248 const index_html = () => {
249 let html = fs.readFileSync(src).toString();
250 // First copy the index.html as-is into the dist/v1.2.3/ directory. This is
251 // only used for archival purporses, so one can open
252 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic.
253 fs.writeFileSync(pjoin(cfg.outDistDir, 'index.html'), html);
254
255 // Then copy it into the dist/ root by patching the version code.
256 // TODO(primiano): in next CLs, this script should take a
257 // --release_map=xxx.json argument, to populate this with multiple channels.
258 const versionMap = JSON.stringify({'stable': cfg.version});
259 const bodyRegex = /data-perfetto_version='[^']*'/;
260 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`);
261 fs.writeFileSync(pjoin(cfg.outDistRootDir, 'index.html'), html);
262 };
263 addTask(index_html);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100264}
265
266function copyAssets(src, dst) {
267 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
268}
269
270function compileScss() {
271 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
272 const dst = pjoin(cfg.outDistDir, 'perfetto.css');
273 // In watch mode, don't exit(1) if scss fails. It can easily happen by
Tuchila Octavian885f6892021-04-06 10:44:13 +0100274 // having a typo in the css. It will still print an error.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100275 const noErrCheck = !!cfg.watch;
276 addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]);
277}
278
279function compileProtos() {
280 const dstJs = pjoin(cfg.outGenDir, 'protos.js');
281 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
282 const inputs = [
283 'protos/perfetto/trace_processor/trace_processor.proto',
284 'protos/perfetto/common/trace_stats.proto',
285 'protos/perfetto/common/tracing_service_capabilities.proto',
286 'protos/perfetto/config/perfetto_config.proto',
287 'protos/perfetto/ipc/consumer_port.proto',
288 'protos/perfetto/ipc/wire_protocol.proto',
289 'protos/perfetto/metrics/metrics.proto',
290 ];
291 const pbjsArgs = [
292 '--force-number',
293 '-t',
294 'static-module',
295 '-w',
296 'commonjs',
297 '-p',
298 ROOT_DIR,
299 '-o',
300 dstJs
301 ].concat(inputs);
302 addTask(execNode, ['pbjs', pbjsArgs]);
303 const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs];
304 addTask(execNode, ['pbts', pbtsArgs]);
305}
306
307// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
308function genVersion() {
309 const cmd = 'python3';
310 const args =
Tuchila Octavian885f6892021-04-06 10:44:13 +0100311 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100312 addTask(exec, [cmd, args]);
313}
314
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100315function updateSymlinks() {
Primiano Tuccia60ef182021-06-11 15:37:12 +0100316 // /ui/out -> /out/ui.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100317 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100318
Primiano Tuccia60ef182021-06-11 15:37:12 +0100319 // /out/ui/test/data -> /test/data (For UI tests).
320 mklink(
321 pjoin(ROOT_DIR, 'test/data'),
322 pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data'));
323
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100324 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
325 // can point to that without having to know the current version number.
326 mklink(
327 path.relative(cfg.outUiDir, cfg.outDistDir),
328 pjoin(cfg.outUiDir, 'dist_version'));
329
Primiano Tuccic8be6812021-02-09 18:08:49 +0100330 mklink(
331 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'))
332}
333
334// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules.
335// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
336// out/tsc/, so the typescript compiler and the bundler can pick them up.
337function buildWasm(skipWasmBuild) {
338 if (!skipWasmBuild) {
339 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
340 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
341
Tuchila Octavian885f6892021-04-06 10:44:13 +0100342 const ninjaArgs = ['-C', cfg.outDir];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100343 ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`));
344 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
345 }
346
347 const wasmOutDir = pjoin(cfg.outDir, 'wasm');
348 for (const wasmMod of cfg.wasmModules) {
349 // The .wasm file goes directly into the dist dir (also .map in debug)
350 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
351 const src = `${wasmOutDir}/${wasmMod}${ext}`;
352 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
353 }
354 // The .js / .ts go into intermediates, they will be bundled by rollup.
355 for (const ext of ['.js', '.d.ts']) {
356 const fname = `${wasmMod}${ext}`;
357 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
358 }
359 }
360}
361
362// This transpiles all the sources (frontend, controller, engine, extension) in
363// one go. The only project that has a dedicated invocation is service_worker.
364function transpileTsProject(project) {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100365 const args = ['--project', pjoin(ROOT_DIR, project)];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100366 if (cfg.watch) {
367 args.push('--watch', '--preserveWatchOutput');
368 addTask(execNode, ['tsc', args, {async: true}]);
369 } else {
370 addTask(execNode, ['tsc', args]);
371 }
372}
373
374// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
375function bundleJs(cfgName) {
Tuchila Octavian885f6892021-04-06 10:44:13 +0100376 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100377 const args = ['-c', rcfg, '--no-indent'];
378 args.push(...(cfg.verbose ? [] : ['--silent']));
379 if (cfg.watch) {
380 // --waitForBundleInput is so that we can run tsc --watch and rollup --watch
381 // together, without having to wait that tsc completes the first build.
382 args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen');
383 addTask(execNode, ['rollup', args, {async: true}]);
384 } else {
385 addTask(execNode, ['rollup', args]);
386 }
387}
388
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100389function genServiceWorkerManifestJson() {
390 function make_manifest() {
391 const manifest = {resources: {}};
392 // When building the subresource manifest skip source maps, the manifest
393 // itself and the copy of the index.html which is copied under /v1.2.3/.
394 // The root /index.html will be fetched by service_worker.js separately.
395 const skipRegex = /(\.map|manifest\.json|index.html)$/;
396 walk(cfg.outDistDir, absPath => {
397 const contents = fs.readFileSync(absPath);
398 const relPath = path.relative(cfg.outDistDir, absPath);
399 const b64 = crypto.createHash('sha256').update(contents).digest('base64');
400 manifest.resources[relPath] = 'sha256-' + b64;
401 }, skipRegex);
402 const manifestJson = JSON.stringify(manifest, null, 2);
403 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100404 }
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100405 addTask(make_manifest, []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100406}
407
408function startServer() {
409 const port = 10000;
Tuchila Octavian885f6892021-04-06 10:44:13 +0100410 console.log(`Starting HTTP server on http://localhost:${port}`);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100411 http.createServer(function(req, res) {
412 console.debug(req.method, req.url);
413 let uri = req.url.split('?', 1)[0];
414 if (uri.endsWith('/')) {
415 uri += 'index.html';
416 }
417
418 if (uri === '/live_reload') {
419 // Implements the Server-Side-Events protocol.
420 const head = {
421 'Content-Type': 'text/event-stream',
422 'Connection': 'keep-alive',
423 'Cache-Control': 'no-cache'
424 };
425 res.writeHead(200, head);
426 const arrayIdx = httpWatches.length;
427 // We never remove from the array, the delete leaves an undefined item
428 // around. It makes keeping track of the index easier at the cost of a
429 // small leak.
430 httpWatches.push(res);
431 req.on('close', () => delete httpWatches[arrayIdx]);
432 return;
433 }
434
435 const absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
436 fs.readFile(absPath, function(err, data) {
437 if (err) {
438 res.writeHead(404);
439 res.end(JSON.stringify(err));
440 return;
441 }
442
443 const mimeMap = {
444 'html': 'text/html',
445 'css': 'text/css',
446 'js': 'application/javascript',
447 'wasm': 'application/wasm',
448 };
449 const ext = uri.split('.').pop();
450 const cType = mimeMap[ext] || 'octect/stream';
451 const head = {
452 'Content-Type': cType,
453 'Content-Length': data.length,
454 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
455 'Cache-Control': 'no-cache',
456 };
457 res.writeHead(200, head);
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100458 res.write(data);
459 res.end();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100460 });
461 })
Primiano Tucci19068d12021-02-23 20:29:56 +0100462 .listen(port, '127.0.0.1');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100463}
464
Primiano Tuccia60ef182021-06-11 15:37:12 +0100465function isDistComplete() {
466 const requiredArtifacts = [
467 'controller_bundle.js',
468 'frontend_bundle.js',
469 'engine_bundle.js',
470 'trace_processor.wasm',
471 'perfetto.css',
472 ];
473 const relPaths = new Set();
474 walk(cfg.outDistDir, absPath => {
475 relPaths.add(path.relative(cfg.outDistDir, absPath));
476 });
477 for (const fName of requiredArtifacts) {
478 if (!relPaths.has(fName)) return false;
479 }
480 return true;
481}
482
Primiano Tuccic8be6812021-02-09 18:08:49 +0100483// Called whenever a change in the out/dist directory is detected. It sends a
484// Server-Side-Event to the live_reload.ts script.
485function notifyLiveServer(changedFile) {
486 for (const cli of httpWatches) {
487 if (cli === undefined) continue;
488 cli.write(
489 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
490 }
491}
492
493function copyExtensionAssets() {
494 addTask(cp, [
495 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
496 pjoin(cfg.outExtDir, 'logo-128.png')
497 ]);
498 addTask(cp, [
499 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
500 pjoin(cfg.outExtDir, 'manifest.json')
501 ]);
502}
503
504// -----------------------
505// Task chaining functions
506// -----------------------
507
508function addTask(func, args) {
509 const task = new Task(func, args);
510 for (const t of tasks) {
511 if (t.identity === task.identity) {
512 return;
513 }
514 }
515 tasks.push(task);
516 setTimeout(runTasks, 0);
517}
518
519function runTasks() {
520 const snapTasks = tasks.splice(0); // snap = std::move(tasks).
521 tasksTot += snapTasks.length;
522 for (const task of snapTasks) {
523 const DIM = '\u001b[2m';
524 const BRT = '\u001b[37m';
525 const RST = '\u001b[0m';
526 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
527 const ts = `[${DIM}${ms}${RST}]`;
528 const descr = task.description.substr(0, 80);
529 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
530 task.func.apply(/*this=*/ undefined, task.args);
531 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100532}
533
534// Executes all the RULES that match the given |absPath|.
535function scanFile(absPath) {
536 console.assert(fs.existsSync(absPath));
537 console.assert(path.isAbsolute(absPath));
538 const normPath = path.relative(ROOT_DIR, absPath);
539 for (const rule of RULES) {
540 const match = rule.r.exec(normPath);
541 if (!match || match[0] !== normPath) continue;
542 const captureGroup = match.length > 1 ? match[1] : undefined;
543 rule.f(absPath, captureGroup);
544 }
545}
546
547// Walks the passed |dir| recursively and, for each file, invokes the matching
Primiano Tucci9b567032021-02-12 14:18:12 +0100548// RULES. If --watch is used, it also installs a fswatch() and re-triggers the
Primiano Tuccic8be6812021-02-09 18:08:49 +0100549// matching RULES on each file change.
550function scanDir(dir, regex) {
551 const filterFn = regex ? absPath => regex.test(absPath) : () => true;
552 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
553 // Add a fs watch if in watch mode.
554 if (cfg.watch) {
Primiano Tucci9a92cf72021-02-13 17:42:34 +0100555 fswatch(absDir, {recursive: true}, (_eventType, filePath) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100556 if (!filterFn(filePath)) return;
557 if (cfg.verbose) {
558 console.log('File change detected', _eventType, filePath);
559 }
560 if (fs.existsSync(filePath)) {
561 scanFile(filePath, filterFn);
562 }
563 });
564 }
565 walk(absDir, f => {
566 if (filterFn(f)) scanFile(f);
567 });
568}
569
570function exec(cmd, args, opts) {
571 opts = opts || {};
572 opts.stdout = opts.stdout || 'inherit';
573 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
574 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
575 const checkExitCode = (code, signal) => {
576 if (signal === 'SIGINT' || signal === 'SIGTERM') return;
577 if (code !== 0 && !opts.noErrCheck) {
578 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
579 process.exit(1);
580 }
581 };
582 if (opts.async) {
583 const proc = child_process.spawn(cmd, args, spwOpts);
584 const procIndex = subprocesses.length;
585 subprocesses.push(proc);
586 return new Promise((resolve, _reject) => {
587 proc.on('exit', (code, signal) => {
588 delete subprocesses[procIndex];
589 checkExitCode(code, signal);
590 resolve();
591 });
592 });
593 } else {
594 const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
595 checkExitCode(spawnRes.status, spawnRes.signal);
596 return spawnRes;
597 }
598}
599
600function execNode(module, args, opts) {
601 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
602 const nodeBin = pjoin(ROOT_DIR, 'tools/node');
603 args = [modPath].concat(args || []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100604 return exec(nodeBin, args, opts);
605}
606
607// ------------------------------------------
608// File system & subprocess utility functions
609// ------------------------------------------
610
611class Task {
612 constructor(func, args) {
613 this.func = func;
614 this.args = args || [];
615 // |identity| is used to dedupe identical tasks in the queue.
616 this.identity = JSON.stringify([this.func.name, this.args]);
617 }
618
619 get description() {
620 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
621 const flattenedArgs = [].concat.apply([], this.args);
622 for (const arg of flattenedArgs) {
623 const argStr = `${arg}`;
624 if (argStr.startsWith('/')) {
625 ret.push(path.relative(cfg.outDir, arg));
626 } else {
627 ret.push(argStr);
628 }
629 }
630 return ret.join(' ');
631 }
632}
633
634function walk(dir, callback, skipRegex) {
635 for (const child of fs.readdirSync(dir)) {
636 const childPath = pjoin(dir, child);
637 const stat = fs.lstatSync(childPath);
638 if (skipRegex !== undefined && skipRegex.test(child)) continue;
639 if (stat.isDirectory()) {
640 walk(childPath, callback, skipRegex);
641 } else if (!stat.isSymbolicLink()) {
642 callback(childPath);
643 }
644 }
645}
646
647function ensureDir(dirPath, clean) {
648 const exists = fs.existsSync(dirPath);
649 if (exists && clean) {
650 console.log('rm', dirPath);
651 fs.rmSync(dirPath, {recursive: true});
652 }
653 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
654 return dirPath;
655}
656
657function cp(src, dst) {
658 ensureDir(path.dirname(dst));
659 if (cfg.verbose) {
660 console.log(
661 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
662 }
663 fs.copyFileSync(src, dst);
664}
665
666function mklink(src, dst) {
667 // If the symlink already points to the right place don't touch it. This is
668 // to avoid changing the mtime of the ui/ dir when unnecessary.
669 if (fs.existsSync(dst)) {
670 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
671 return;
672 } else {
673 fs.unlinkSync(dst);
674 }
675 }
676 fs.symlinkSync(src, dst);
677}
678
679main();