1// 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 28// rollup) and auto-triggering-on-file-change rules via node-watch. 29// 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. 32// When invoked with --watch, it mounts a pipeline of tasks based on node-watch 33// 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 | +---------+ | engine_bundle.js || 62// +----------------------+ | | *.woff2 | +--------------------+| 63// | buildtools/legacy_tv | | | tv.html | |traceconv_bundle.js || 64// +----------------------+ | +----------+ +--------------------+| 65// +------------------------------------------------+ 66 67const argparse = require('argparse'); 68const child_process = require('child_process'); 69const crypto = require('crypto'); 70const fs = require('fs'); 71const http = require('http'); 72const path = require('path'); 73const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux. 74const pjoin = path.join; 75 76const ROOT_DIR = path.dirname(__dirname); // The repo root. 77const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py'); 78const GEN_IMPORTS_SCRIPT = pjoin(ROOT_DIR, 'tools/gen_ui_imports'); 79 80const cfg = { 81 watch: false, 82 verbose: false, 83 debug: false, 84 startHttpServer: false, 85 httpServerListenHost: '127.0.0.1', 86 httpServerListenPort: 10000, 87 wasmModules: ['trace_processor', 'trace_to_text'], 88 89 // The fields below will be changed by main() after cmdline parsing. 90 // Directory structure: 91 // out/xxx/ -> outDir : Root build dir, for both ninja/wasm and UI. 92 // ui/ -> outUiDir : UI dir. All outputs from this script. 93 // tsc/ -> outTscDir : Transpiled .ts -> .js. 94 // gen/ -> outGenDir : Auto-generated .ts/.js (e.g. protos). 95 // dist/ -> outDistRootDir : Only index.html and service_worker.js 96 // v1.2/ -> outDistDir : JS bundles and assets 97 // chrome_extension/ : Chrome extension. 98 outDir: pjoin(ROOT_DIR, 'out/ui'), 99 version: '', // v1.2.3, derived from the CHANGELOG + git. 100 outUiDir: '', 101 outDistRootDir: '', 102 outTscDir: '', 103 outGenDir: '', 104 outDistDir: '', 105 outExtDir: '', 106}; 107 108const RULES = [ 109 {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml}, 110 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets}, 111 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets}, 112 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets}, 113 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss}, 114 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss}, 115 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets}, 116 {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson}, 117 {r: /.*\/dist\/.*/, f: notifyLiveServer}, 118]; 119 120let tasks = []; 121let tasksTot = 0, tasksRan = 0; 122let httpWatches = []; 123let tStart = Date.now(); 124let subprocesses = []; 125 126async function main() { 127 const parser = new argparse.ArgumentParser(); 128 parser.addArgument('--out', {help: 'Output directory'}); 129 parser.addArgument(['--watch', '-w'], {action: 'storeTrue'}); 130 parser.addArgument(['--serve', '-s'], {action: 'storeTrue'}); 131 parser.addArgument('--serve-host', {help: '--serve bind host'}); 132 parser.addArgument('--serve-port', {help: '--serve bind port', type: 'int'}); 133 parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'}); 134 parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'}); 135 parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'}); 136 parser.addArgument(['--run-unittests', '-t'], {action: 'storeTrue'}); 137 parser.addArgument(['--run-integrationtests', '-T'], {action: 'storeTrue'}); 138 parser.addArgument(['--debug', '-d'], {action: 'storeTrue'}); 139 parser.addArgument(['--interactive', '-i'], {action: 'storeTrue'}); 140 parser.addArgument(['--rebaseline', '-r'], {action: 'storeTrue'}); 141 parser.addArgument(['--no-depscheck'], {action: 'storeTrue'}); 142 143 const args = parser.parseArgs(); 144 const clean = !args.no_build; 145 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir)); 146 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean); 147 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension')); 148 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist')); 149 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'}); 150 cfg.version = proc.stdout.toString().trim(); 151 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version)); 152 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc')); 153 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen')); 154 cfg.watch = !!args.watch; 155 cfg.verbose = !!args.verbose; 156 cfg.debug = !!args.debug; 157 cfg.startHttpServer = args.serve; 158 if (args.serve_host) { 159 cfg.httpServerListenHost = args.serve_host 160 } 161 if (args.serve_port) { 162 cfg.httpServerListenPort = args.serve_port 163 } 164 if (args.interactive) { 165 process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1'; 166 } 167 if (args.rebaseline) { 168 process.env.PERFETTO_UI_TESTS_REBASELINE = '1'; 169 } 170 171 process.on('SIGINT', () => { 172 console.log('\nSIGINT received. Killing all child processes and exiting'); 173 for (const proc of subprocesses) { 174 if (proc) proc.kill('SIGINT'); 175 } 176 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT. 177 }); 178 179 if (!args.no_depscheck) { 180 // Check that deps are current before starting. 181 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps'); 182 const checkDepsPath = pjoin(cfg.outDir, '.check_deps'); 183 let args = [installBuildDeps, `--check-only=${checkDepsPath}`, '--ui']; 184 185 if (process.platform === "darwin") { 186 const result = child_process.spawnSync("arch", ["-arm64", "true"]); 187 const isArm64Capable = result.status === 0; 188 if (isArm64Capable) { 189 const archArgs = [ 190 "arch", 191 "-arch", 192 "arm64", 193 ]; 194 args = archArgs.concat(args); 195 } 196 } 197 const cmd = args.shift(); 198 exec(cmd, args); 199 } 200 201 console.log('Entering', cfg.outDir); 202 process.chdir(cfg.outDir); 203 204 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/ 205 206 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP 207 // server is started when the task queue reaches quiescence, but it takes at 208 // least one task for that. 209 addTask(() => {}); 210 211 if (!args.no_build) { 212 buildWasm(args.no_wasm); 213 scanDir('ui/src/assets'); 214 scanDir('ui/src/chrome_extension'); 215 scanDir('buildtools/typefaces'); 216 scanDir('buildtools/catapult_trace_viewer'); 217 generateImports('ui/src/tracks', 'all_tracks.ts'); 218 compileProtos(); 219 genVersion(); 220 transpileTsProject('ui'); 221 transpileTsProject('ui/src/service_worker'); 222 bundleJs('rollup.config.js'); 223 genServiceWorkerManifestJson(); 224 225 // Watches the /dist. When changed: 226 // - Notifies the HTTP live reload clients. 227 // - Regenerates the ServiceWorker file map. 228 scanDir(cfg.outDistRootDir); 229 } 230 231 232 // We should enter the loop only in watch mode, where tsc and rollup are 233 // asynchronous because they run in watch mode. 234 const tStart = Date.now(); 235 while (!isDistComplete()) { 236 const secs = Math.ceil((Date.now() - tStart) / 1000); 237 process.stdout.write(`Waiting for first build to complete... ${secs} s\r`); 238 await new Promise(r => setTimeout(r, 500)); 239 } 240 if (cfg.watch) console.log('\nFirst build completed!'); 241 242 if (cfg.startHttpServer) { 243 startServer(); 244 } 245 if (args.run_unittests) { 246 runTests('jest.unittest.config.js'); 247 } 248 if (args.run_integrationtests) { 249 runTests('jest.integrationtest.config.js'); 250 } 251} 252 253// ----------- 254// Build rules 255// ----------- 256 257function runTests(cfgFile) { 258 const args = [ 259 '--rootDir', 260 cfg.outTscDir, 261 '--verbose', 262 '--runInBand', 263 '--detectOpenHandles', 264 '--forceExit', 265 '--projects', 266 pjoin(ROOT_DIR, 'ui/config', cfgFile) 267 ]; 268 if (cfg.watch) { 269 args.push('--watchAll'); 270 addTask(execNode, ['jest', args, {async: true}]); 271 } else { 272 addTask(execNode, ['jest', args]); 273 } 274} 275 276function copyIndexHtml(src) { 277 const index_html = () => { 278 let html = fs.readFileSync(src).toString(); 279 // First copy the index.html as-is into the dist/v1.2.3/ directory. This is 280 // only used for archival purporses, so one can open 281 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic. 282 fs.writeFileSync(pjoin(cfg.outDistDir, 'index.html'), html); 283 284 // Then copy it into the dist/ root by patching the version code. 285 // TODO(primiano): in next CLs, this script should take a 286 // --release_map=xxx.json argument, to populate this with multiple channels. 287 const versionMap = JSON.stringify({'stable': cfg.version}); 288 const bodyRegex = /data-perfetto_version='[^']*'/; 289 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`); 290 fs.writeFileSync(pjoin(cfg.outDistRootDir, 'index.html'), html); 291 }; 292 addTask(index_html); 293} 294 295function copyAssets(src, dst) { 296 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]); 297} 298 299function compileScss() { 300 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss'); 301 const dst = pjoin(cfg.outDistDir, 'perfetto.css'); 302 // In watch mode, don't exit(1) if scss fails. It can easily happen by 303 // having a typo in the css. It will still print an error. 304 const noErrCheck = !!cfg.watch; 305 addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]); 306} 307 308function compileProtos() { 309 const dstJs = pjoin(cfg.outGenDir, 'protos.js'); 310 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts'); 311 const inputs = [ 312 'protos/perfetto/trace_processor/trace_processor.proto', 313 'protos/perfetto/common/trace_stats.proto', 314 'protos/perfetto/common/tracing_service_capabilities.proto', 315 'protos/perfetto/config/perfetto_config.proto', 316 'protos/perfetto/ipc/consumer_port.proto', 317 'protos/perfetto/ipc/wire_protocol.proto', 318 'protos/perfetto/metrics/metrics.proto', 319 ]; 320 const pbjsArgs = [ 321 '--force-number', 322 '-t', 323 'static-module', 324 '-w', 325 'commonjs', 326 '-p', 327 ROOT_DIR, 328 '-o', 329 dstJs 330 ].concat(inputs); 331 addTask(execNode, ['pbjs', pbjsArgs]); 332 const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs]; 333 addTask(execNode, ['pbts', pbtsArgs]); 334} 335 336function generateImports(dir, name) { 337 // We have to use the symlink (ui/src/gen) rather than cfg.outGenDir 338 // below since we want to generate the correct relative imports. For example: 339 // ui/src/frontend/foo.ts 340 // import '../gen/all_plugins.ts'; 341 // ui/src/gen/all_plugins.ts (aka ui/out/tsc/gen/all_plugins.ts) 342 // import '../frontend/some_plugin.ts'; 343 const dstTs = pjoin(ROOT_DIR, 'ui/src/gen', name); 344 const inputDir = pjoin(ROOT_DIR, dir); 345 const args = [GEN_IMPORTS_SCRIPT, inputDir, '--out', dstTs]; 346 addTask(exec, ['python3', args]); 347} 348 349// Generates a .ts source that defines the VERSION and SCM_REVISION constants. 350function genVersion() { 351 const cmd = 'python3'; 352 const args = 353 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')]; 354 addTask(exec, [cmd, args]); 355} 356 357function updateSymlinks() { 358 // /ui/out -> /out/ui. 359 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out')); 360 361 // /out/ui/test/data -> /test/data (For UI tests). 362 mklink( 363 pjoin(ROOT_DIR, 'test/data'), 364 pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data')); 365 366 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config 367 // can point to that without having to know the current version number. 368 mklink( 369 path.relative(cfg.outUiDir, cfg.outDistDir), 370 pjoin(cfg.outUiDir, 'dist_version')); 371 372 mklink( 373 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules')) 374} 375 376// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules. 377// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into 378// out/tsc/, so the typescript compiler and the bundler can pick them up. 379function buildWasm(skipWasmBuild) { 380 if (!skipWasmBuild) { 381 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir]; 382 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]); 383 384 const ninjaArgs = ['-C', cfg.outDir]; 385 ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`)); 386 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]); 387 } 388 389 const wasmOutDir = pjoin(cfg.outDir, 'wasm'); 390 for (const wasmMod of cfg.wasmModules) { 391 // The .wasm file goes directly into the dist dir (also .map in debug) 392 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) { 393 const src = `${wasmOutDir}/${wasmMod}${ext}`; 394 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]); 395 } 396 // The .js / .ts go into intermediates, they will be bundled by rollup. 397 for (const ext of ['.js', '.d.ts']) { 398 const fname = `${wasmMod}${ext}`; 399 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]); 400 } 401 } 402} 403 404// This transpiles all the sources (frontend, controller, engine, extension) in 405// one go. The only project that has a dedicated invocation is service_worker. 406function transpileTsProject(project) { 407 const args = ['--project', pjoin(ROOT_DIR, project)]; 408 if (cfg.watch) { 409 args.push('--watch', '--preserveWatchOutput'); 410 addTask(execNode, ['tsc', args, {async: true}]); 411 } else { 412 addTask(execNode, ['tsc', args]); 413 } 414} 415 416// Creates the three {frontend, controller, engine}_bundle.js in one invocation. 417function bundleJs(cfgName) { 418 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName); 419 const args = ['-c', rcfg, '--no-indent']; 420 args.push(...(cfg.verbose ? [] : ['--silent'])); 421 if (cfg.watch) { 422 // --waitForBundleInput is so that we can run tsc --watch and rollup --watch 423 // together, without having to wait that tsc completes the first build. 424 args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen'); 425 addTask(execNode, ['rollup', args, {async: true}]); 426 } else { 427 addTask(execNode, ['rollup', args]); 428 } 429} 430 431function genServiceWorkerManifestJson() { 432 function make_manifest() { 433 const manifest = {resources: {}}; 434 // When building the subresource manifest skip source maps, the manifest 435 // itself and the copy of the index.html which is copied under /v1.2.3/. 436 // The root /index.html will be fetched by service_worker.js separately. 437 const skipRegex = /(\.map|manifest\.json|index.html)$/; 438 walk(cfg.outDistDir, absPath => { 439 const contents = fs.readFileSync(absPath); 440 const relPath = path.relative(cfg.outDistDir, absPath); 441 const b64 = crypto.createHash('sha256').update(contents).digest('base64'); 442 manifest.resources[relPath] = 'sha256-' + b64; 443 }, skipRegex); 444 const manifestJson = JSON.stringify(manifest, null, 2); 445 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson); 446 } 447 addTask(make_manifest, []); 448} 449 450function startServer() { 451 console.log( 452 'Starting HTTP server on', 453 `http://${cfg.httpServerListenHost}:${cfg.httpServerListenPort}`); 454 http.createServer(function(req, res) { 455 console.debug(req.method, req.url); 456 let uri = req.url.split('?', 1)[0]; 457 if (uri.endsWith('/')) { 458 uri += 'index.html'; 459 } 460 461 if (uri === '/live_reload') { 462 // Implements the Server-Side-Events protocol. 463 const head = { 464 'Content-Type': 'text/event-stream', 465 'Connection': 'keep-alive', 466 'Cache-Control': 'no-cache' 467 }; 468 res.writeHead(200, head); 469 const arrayIdx = httpWatches.length; 470 // We never remove from the array, the delete leaves an undefined item 471 // around. It makes keeping track of the index easier at the cost of a 472 // small leak. 473 httpWatches.push(res); 474 req.on('close', () => delete httpWatches[arrayIdx]); 475 return; 476 } 477 478 let absPath = path.normalize(path.join(cfg.outDistRootDir, uri)); 479 // We want to be able to use the data in '/test/' for e2e tests. 480 // However, we don't want do create a symlink into the 'dist/' dir, 481 // because 'dist/' gets shipped on the production server. 482 if (uri.startsWith('/test/')) { 483 absPath = pjoin(ROOT_DIR, uri); 484 } 485 486 // Don't serve contents outside of the project root (b/221101533). 487 if (path.relative(ROOT_DIR, absPath).startsWith('..')) { 488 res.writeHead(403); 489 res.end('403 Forbidden - Request path outside of the repo root'); 490 return; 491 } 492 493 fs.readFile(absPath, function(err, data) { 494 if (err) { 495 res.writeHead(404); 496 res.end(JSON.stringify(err)); 497 return; 498 } 499 500 const mimeMap = { 501 'html': 'text/html', 502 'css': 'text/css', 503 'js': 'application/javascript', 504 'wasm': 'application/wasm', 505 }; 506 const ext = uri.split('.').pop(); 507 const cType = mimeMap[ext] || 'octect/stream'; 508 const head = { 509 'Content-Type': cType, 510 'Content-Length': data.length, 511 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(), 512 'Cache-Control': 'no-cache', 513 }; 514 res.writeHead(200, head); 515 res.write(data); 516 res.end(); 517 }); 518 }) 519 .listen(cfg.httpServerListenPort, cfg.httpServerListenHost); 520} 521 522function isDistComplete() { 523 const requiredArtifacts = [ 524 'frontend_bundle.js', 525 'engine_bundle.js', 526 'traceconv_bundle.js', 527 'trace_processor.wasm', 528 'perfetto.css', 529 ]; 530 const relPaths = new Set(); 531 walk(cfg.outDistDir, absPath => { 532 relPaths.add(path.relative(cfg.outDistDir, absPath)); 533 }); 534 for (const fName of requiredArtifacts) { 535 if (!relPaths.has(fName)) return false; 536 } 537 return true; 538} 539 540// Called whenever a change in the out/dist directory is detected. It sends a 541// Server-Side-Event to the live_reload.ts script. 542function notifyLiveServer(changedFile) { 543 for (const cli of httpWatches) { 544 if (cli === undefined) continue; 545 cli.write( 546 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n'); 547 } 548} 549 550function copyExtensionAssets() { 551 addTask(cp, [ 552 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'), 553 pjoin(cfg.outExtDir, 'logo-128.png') 554 ]); 555 addTask(cp, [ 556 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'), 557 pjoin(cfg.outExtDir, 'manifest.json') 558 ]); 559} 560 561// ----------------------- 562// Task chaining functions 563// ----------------------- 564 565function addTask(func, args) { 566 const task = new Task(func, args); 567 for (const t of tasks) { 568 if (t.identity === task.identity) { 569 return; 570 } 571 } 572 tasks.push(task); 573 setTimeout(runTasks, 0); 574} 575 576function runTasks() { 577 const snapTasks = tasks.splice(0); // snap = std::move(tasks). 578 tasksTot += snapTasks.length; 579 for (const task of snapTasks) { 580 const DIM = '\u001b[2m'; 581 const BRT = '\u001b[37m'; 582 const RST = '\u001b[0m'; 583 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1); 584 const ts = `[${DIM}${ms}${RST}]`; 585 const descr = task.description.substr(0, 80); 586 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`); 587 task.func.apply(/*this=*/ undefined, task.args); 588 } 589} 590 591// Executes all the RULES that match the given |absPath|. 592function scanFile(absPath) { 593 console.assert(fs.existsSync(absPath)); 594 console.assert(path.isAbsolute(absPath)); 595 const normPath = path.relative(ROOT_DIR, absPath); 596 for (const rule of RULES) { 597 const match = rule.r.exec(normPath); 598 if (!match || match[0] !== normPath) continue; 599 const captureGroup = match.length > 1 ? match[1] : undefined; 600 rule.f(absPath, captureGroup); 601 } 602} 603 604// Walks the passed |dir| recursively and, for each file, invokes the matching 605// RULES. If --watch is used, it also installs a fswatch() and re-triggers the 606// matching RULES on each file change. 607function scanDir(dir, regex) { 608 const filterFn = regex ? absPath => regex.test(absPath) : () => true; 609 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir); 610 // Add a fs watch if in watch mode. 611 if (cfg.watch) { 612 fswatch(absDir, {recursive: true}, (_eventType, filePath) => { 613 if (!filterFn(filePath)) return; 614 if (cfg.verbose) { 615 console.log('File change detected', _eventType, filePath); 616 } 617 if (fs.existsSync(filePath)) { 618 scanFile(filePath, filterFn); 619 } 620 }); 621 } 622 walk(absDir, f => { 623 if (filterFn(f)) scanFile(f); 624 }); 625} 626 627function exec(cmd, args, opts) { 628 opts = opts || {}; 629 opts.stdout = opts.stdout || 'inherit'; 630 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`); 631 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']}; 632 const checkExitCode = (code, signal) => { 633 if (signal === 'SIGINT' || signal === 'SIGTERM') return; 634 if (code !== 0 && !opts.noErrCheck) { 635 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`); 636 process.exit(1); 637 } 638 }; 639 if (opts.async) { 640 const proc = child_process.spawn(cmd, args, spwOpts); 641 const procIndex = subprocesses.length; 642 subprocesses.push(proc); 643 return new Promise((resolve, _reject) => { 644 proc.on('exit', (code, signal) => { 645 delete subprocesses[procIndex]; 646 checkExitCode(code, signal); 647 resolve(); 648 }); 649 }); 650 } else { 651 const spawnRes = child_process.spawnSync(cmd, args, spwOpts); 652 checkExitCode(spawnRes.status, spawnRes.signal); 653 return spawnRes; 654 } 655} 656 657function execNode(module, args, opts) { 658 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module); 659 const nodeBin = pjoin(ROOT_DIR, 'tools/node'); 660 args = [modPath].concat(args || []); 661 return exec(nodeBin, args, opts); 662} 663 664// ------------------------------------------ 665// File system & subprocess utility functions 666// ------------------------------------------ 667 668class Task { 669 constructor(func, args) { 670 this.func = func; 671 this.args = args || []; 672 // |identity| is used to dedupe identical tasks in the queue. 673 this.identity = JSON.stringify([this.func.name, this.args]); 674 } 675 676 get description() { 677 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name]; 678 const flattenedArgs = [].concat.apply([], this.args); 679 for (const arg of flattenedArgs) { 680 const argStr = `${arg}`; 681 if (argStr.startsWith('/')) { 682 ret.push(path.relative(cfg.outDir, arg)); 683 } else { 684 ret.push(argStr); 685 } 686 } 687 return ret.join(' '); 688 } 689} 690 691function walk(dir, callback, skipRegex) { 692 for (const child of fs.readdirSync(dir)) { 693 const childPath = pjoin(dir, child); 694 const stat = fs.lstatSync(childPath); 695 if (skipRegex !== undefined && skipRegex.test(child)) continue; 696 if (stat.isDirectory()) { 697 walk(childPath, callback, skipRegex); 698 } else if (!stat.isSymbolicLink()) { 699 callback(childPath); 700 } 701 } 702} 703 704function ensureDir(dirPath, clean) { 705 const exists = fs.existsSync(dirPath); 706 if (exists && clean) { 707 console.log('rm', dirPath); 708 fs.rmSync(dirPath, {recursive: true}); 709 } 710 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true}); 711 return dirPath; 712} 713 714function cp(src, dst) { 715 ensureDir(path.dirname(dst)); 716 if (cfg.verbose) { 717 console.log( 718 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst)); 719 } 720 fs.copyFileSync(src, dst); 721} 722 723function mklink(src, dst) { 724 // If the symlink already points to the right place don't touch it. This is 725 // to avoid changing the mtime of the ui/ dir when unnecessary. 726 if (fs.existsSync(dst)) { 727 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) { 728 return; 729 } else { 730 fs.unlinkSync(dst); 731 } 732 } 733 fs.symlinkSync(src, dst); 734} 735 736main(); 737