blob: 4afe0cd4da243a93ab867ffcfd20abfc7f2faec1 [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
Hector Dearman969ecb92022-06-06 12:18:48 +010017
Primiano Tuccic8be6812021-02-09 18:08:49 +010018// This script takes care of:
19// - The build process for the whole UI and the chrome extension.
20// - The HTTP dev-server with live-reload capabilities.
21// The reason why this is a hand-rolled script rather than a conventional build
22// system is keeping incremental build fast and maintaining the set of
23// dependencies contained.
24// The only way to keep incremental build fast (i.e. O(seconds) for the
25// edit-one-line -> reload html cycles) is to run both the TypeScript compiler
26// and the rollup bundler in --watch mode. Any other attempt, leads to O(10s)
27// incremental-build times.
28// This script allows mixing build tools that support --watch mode (tsc and
Primiano Tucci9b567032021-02-12 14:18:12 +010029// rollup) and auto-triggering-on-file-change rules via node-watch.
Primiano Tuccic8be6812021-02-09 18:08:49 +010030// When invoked without any argument (e.g., for production builds), this script
31// just runs all the build tasks serially. It doesn't to do any mtime-based
32// check, it always re-runs all the tasks.
Primiano Tucci9b567032021-02-12 14:18:12 +010033// When invoked with --watch, it mounts a pipeline of tasks based on node-watch
Primiano Tuccic8be6812021-02-09 18:08:49 +010034// and runs them together with tsc --watch and rollup --watch.
35// The output directory structure is carefully crafted so that any change to UI
36// sources causes cascading triggers of the next steps.
37// The overall build graph looks as follows:
38// +----------------+ +-----------------------------+
39// | protos/*.proto |----->| pbjs out/tsc/gen/protos.js |--+
40// +----------------+ +-----------------------------+ |
41// +-----------------------------+ |
42// | pbts out/tsc/gen/protos.d.ts|<-+
43// +-----------------------------+
44// |
45// V +-------------------------+
46// +---------+ +-----+ | out/tsc/frontend/*.js |
47// | ui/*.ts |------------->| tsc |-> +-------------------------+ +--------+
48// +---------+ +-----+ | out/tsc/controller/*.js |-->| rollup |
49// ^ +-------------------------+ +--------+
50// +------------+ | out/tsc/engine/*.js | |
51// +-----------+ |*.wasm.js | +-------------------------+ |
52// |ninja *.cc |->|*.wasm.d.ts | |
53// +-----------+ |*.wasm |-----------------+ |
54// +------------+ | |
55// V V
56// +-----------+ +------+ +------------------------------------------------+
57// | ui/*.scss |->| scss |--->| Final out/dist/ dir |
58// +-----------+ +------+ +------------------------------------------------+
59// +----------------------+ | +----------+ +---------+ +--------------------+|
60// | src/assets/*.png | | | assets/ | |*.wasm.js| | frontend_bundle.js ||
61// +----------------------+ | | *.css | |*.wasm | +--------------------+|
Hector Dearman995d1912021-07-13 16:51:38 +010062// | buildtools/typefaces |-->| | *.png | +---------+ | engine_bundle.js ||
Primiano Tuccic8be6812021-02-09 18:08:49 +010063// +----------------------+ | | *.woff2 | +--------------------+|
Hector Dearman995d1912021-07-13 16:51:38 +010064// | buildtools/legacy_tv | | | tv.html | |traceconv_bundle.js ||
Primiano Tuccic8be6812021-02-09 18:08:49 +010065// +----------------------+ | +----------+ +--------------------+|
66// +------------------------------------------------+
67
68const argparse = require('argparse');
Hector Dearman969ecb92022-06-06 12:18:48 +010069const childProcess = require('child_process');
Tuchila Octavian885f6892021-04-06 10:44:13 +010070const crypto = require('crypto');
Primiano Tuccic8be6812021-02-09 18:08:49 +010071const fs = require('fs');
72const http = require('http');
73const path = require('path');
Primiano Tucci9b567032021-02-12 14:18:12 +010074const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux.
Primiano Tuccic8be6812021-02-09 18:08:49 +010075const pjoin = path.join;
76
77const ROOT_DIR = path.dirname(__dirname); // The repo root.
78const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py');
Hector Dearman07ed69a2021-08-23 11:12:25 +010079const GEN_IMPORTS_SCRIPT = pjoin(ROOT_DIR, 'tools/gen_ui_imports');
Primiano Tuccic8be6812021-02-09 18:08:49 +010080
81const cfg = {
82 watch: false,
83 verbose: false,
84 debug: false,
Primiano Tuccid60b8d82023-11-15 22:52:10 +000085 bigtrace: false,
Primiano Tuccic8be6812021-02-09 18:08:49 +010086 startHttpServer: false,
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +000087 httpServerListenHost: '127.0.0.1',
88 httpServerListenPort: 10000,
Hector Dearmana9545e52022-05-17 12:23:25 +010089 wasmModules: ['trace_processor', 'traceconv'],
Alexander Timinb5d09d02022-07-26 21:10:02 +000090 crossOriginIsolation: false,
Primiano Tucci1bbaa532022-06-08 19:22:45 +010091 testFilter: '',
Ankit Guptac2904532022-10-07 18:57:39 +053092 noOverrideGnArgs: false,
Primiano Tuccic8be6812021-02-09 18:08:49 +010093
94 // The fields below will be changed by main() after cmdline parsing.
95 // Directory structure:
Primiano Tucci7eead2e2021-02-16 11:46:04 +010096 // out/xxx/ -> outDir : Root build dir, for both ninja/wasm and UI.
97 // ui/ -> outUiDir : UI dir. All outputs from this script.
98 // tsc/ -> outTscDir : Transpiled .ts -> .js.
99 // gen/ -> outGenDir : Auto-generated .ts/.js (e.g. protos).
100 // dist/ -> outDistRootDir : Only index.html and service_worker.js
101 // v1.2/ -> outDistDir : JS bundles and assets
102 // chrome_extension/ : Chrome extension.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100103 outDir: pjoin(ROOT_DIR, 'out/ui'),
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100104 version: '', // v1.2.3, derived from the CHANGELOG + git.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100105 outUiDir: '',
Andrew Shulaevbb4e6b22022-07-04 10:18:04 +0100106 outUiTestArtifactsDir: '',
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100107 outDistRootDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100108 outTscDir: '',
109 outGenDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100110 outDistDir: '',
111 outExtDir: '',
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000112 outBigtraceDistDir: '',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100113};
114
115const RULES = [
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100116 {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml},
Lalit Magantib1d5a982023-11-17 17:54:12 +0000117 {r: /ui\/src\/assets\/bigtrace.html/, f: copyBigtraceHtml},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100118 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
119 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
120 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
121 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100122 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
Andrew Shulaevbb4e6b22022-07-04 10:18:04 +0100123 {
124 r: /ui\/src\/test\/diff_viewer\/(.+[.](?:html|js))/,
125 f: copyUiTestArtifactsAssets,
126 },
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100127 {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
Primiano Tuccic8be6812021-02-09 18:08:49 +0100128 {r: /.*\/dist\/.*/, f: notifyLiveServer},
129];
130
Hector Dearman969ecb92022-06-06 12:18:48 +0100131const tasks = [];
132let tasksTot = 0;
133let tasksRan = 0;
134const httpWatches = [];
135const tStart = Date.now();
136const subprocesses = [];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100137
Primiano Tuccia60ef182021-06-11 15:37:12 +0100138async function main() {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100139 const parser = new argparse.ArgumentParser();
Hector Dearman04750c12023-03-27 15:14:40 +0100140 parser.add_argument('--out', {help: 'Output directory'});
141 parser.add_argument('--watch', '-w', {action: 'store_true'});
142 parser.add_argument('--serve', '-s', {action: 'store_true'});
143 parser.add_argument('--serve-host', {help: '--serve bind host'});
144 parser.add_argument('--serve-port', {help: '--serve bind port', type: 'int'});
145 parser.add_argument('--verbose', '-v', {action: 'store_true'});
146 parser.add_argument('--no-build', '-n', {action: 'store_true'});
147 parser.add_argument('--no-wasm', '-W', {action: 'store_true'});
148 parser.add_argument('--run-unittests', '-t', {action: 'store_true'});
149 parser.add_argument('--run-integrationtests', '-T', {action: 'store_true'});
150 parser.add_argument('--debug', '-d', {action: 'store_true'});
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000151 parser.add_argument('--bigtrace', {action: 'store_true'});
Hector Dearman04750c12023-03-27 15:14:40 +0100152 parser.add_argument('--interactive', '-i', {action: 'store_true'});
153 parser.add_argument('--rebaseline', '-r', {action: 'store_true'});
154 parser.add_argument('--no-depscheck', {action: 'store_true'});
155 parser.add_argument('--cross-origin-isolation', {action: 'store_true'});
156 parser.add_argument('--test-filter', '-f', {
157 help: 'filter Jest tests by regex, e.g. \'chrome_render\'',
158 });
159 parser.add_argument('--no-override-gn-args', {action: 'store_true'});
Primiano Tuccic8be6812021-02-09 18:08:49 +0100160
Hector Dearman04750c12023-03-27 15:14:40 +0100161 const args = parser.parse_args();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100162 const clean = !args.no_build;
163 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
164 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
Andrew Shulaevbb4e6b22022-07-04 10:18:04 +0100165 cfg.outUiTestArtifactsDir = ensureDir(pjoin(cfg.outDir, 'ui-test-artifacts'));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100166 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
167 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100168 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'});
169 cfg.version = proc.stdout.toString().trim();
170 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100171 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
172 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
Primiano Tucci1bbaa532022-06-08 19:22:45 +0100173 cfg.testFilter = args.test_filter || '';
Primiano Tuccic8be6812021-02-09 18:08:49 +0100174 cfg.watch = !!args.watch;
175 cfg.verbose = !!args.verbose;
176 cfg.debug = !!args.debug;
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000177 cfg.bigtrace = !!args.bigtrace;
Primiano Tuccic8be6812021-02-09 18:08:49 +0100178 cfg.startHttpServer = args.serve;
Ankit Guptac2904532022-10-07 18:57:39 +0530179 cfg.noOverrideGnArgs = !!args.no_override_gn_args;
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000180 if (args.bigtrace) {
181 cfg.outBigtraceDistDir = ensureDir(pjoin(cfg.outDistDir, 'bigtrace'));
182 }
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +0000183 if (args.serve_host) {
Hector Dearman969ecb92022-06-06 12:18:48 +0100184 cfg.httpServerListenHost = args.serve_host;
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +0000185 }
186 if (args.serve_port) {
Hector Dearman969ecb92022-06-06 12:18:48 +0100187 cfg.httpServerListenPort = args.serve_port;
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +0000188 }
Primiano Tuccia60ef182021-06-11 15:37:12 +0100189 if (args.interactive) {
190 process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
191 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100192 if (args.rebaseline) {
193 process.env.PERFETTO_UI_TESTS_REBASELINE = '1';
194 }
Alexander Timinb5d09d02022-07-26 21:10:02 +0000195 if (args.cross_origin_isolation) {
196 cfg.crossOriginIsolation = true;
197 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100198
199 process.on('SIGINT', () => {
200 console.log('\nSIGINT received. Killing all child processes and exiting');
201 for (const proc of subprocesses) {
202 if (proc) proc.kill('SIGINT');
203 }
204 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT.
205 });
206
Lalit Maganti43bc33d2022-03-07 15:27:07 +0000207 if (!args.no_depscheck) {
208 // Check that deps are current before starting.
209 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
210 const checkDepsPath = pjoin(cfg.outDir, '.check_deps');
Hector Dearmanc11ab532022-03-15 18:01:27 +0000211 let args = [installBuildDeps, `--check-only=${checkDepsPath}`, '--ui'];
212
Hector Dearman969ecb92022-06-06 12:18:48 +0100213 if (process.platform === 'darwin') {
214 const result = childProcess.spawnSync('arch', ['-arm64', 'true']);
Hector Dearmanc11ab532022-03-15 18:01:27 +0000215 const isArm64Capable = result.status === 0;
216 if (isArm64Capable) {
217 const archArgs = [
Hector Dearman969ecb92022-06-06 12:18:48 +0100218 'arch',
219 '-arch',
220 'arm64',
Hector Dearmanc11ab532022-03-15 18:01:27 +0000221 ];
222 args = archArgs.concat(args);
223 }
224 }
225 const cmd = args.shift();
226 exec(cmd, args);
Lalit Maganti43bc33d2022-03-07 15:27:07 +0000227 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100228
229 console.log('Entering', cfg.outDir);
230 process.chdir(cfg.outDir);
231
Primiano Tuccic8be6812021-02-09 18:08:49 +0100232 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
233 // server is started when the task queue reaches quiescence, but it takes at
234 // least one task for that.
235 addTask(() => {});
236
237 if (!args.no_build) {
Andrew Shulaev6cf14622023-02-02 15:20:36 +0000238 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/
239
Primiano Tuccic8be6812021-02-09 18:08:49 +0100240 buildWasm(args.no_wasm);
241 scanDir('ui/src/assets');
242 scanDir('ui/src/chrome_extension');
Andrew Shulaevbb4e6b22022-07-04 10:18:04 +0100243 scanDir('ui/src/test/diff_viewer');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100244 scanDir('buildtools/typefaces');
245 scanDir('buildtools/catapult_trace_viewer');
Hector Dearman07ed69a2021-08-23 11:12:25 +0100246 generateImports('ui/src/tracks', 'all_tracks.ts');
Steve Golton24a0d912023-07-01 19:34:02 +0100247 generateImports('ui/src/plugins', 'all_plugins.ts');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100248 compileProtos();
249 genVersion();
250 transpileTsProject('ui');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100251 transpileTsProject('ui/src/service_worker');
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000252 if (cfg.bigtrace) {
253 transpileTsProject('ui/src/bigtrace');
254 }
Hector Dearman5f9da792023-03-27 20:12:49 +0100255
256 if (cfg.watch) {
257 transpileTsProject('ui', {watch: cfg.watch});
258 transpileTsProject('ui/src/service_worker', {watch: cfg.watch});
Lalit Magantib1d5a982023-11-17 17:54:12 +0000259 if (cfg.bigtrace) {
260 transpileTsProject('ui/src/bigtrace', {watch: cfg.watch});
261 }
Hector Dearman5f9da792023-03-27 20:12:49 +0100262 }
263
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100264 bundleJs('rollup.config.js');
265 genServiceWorkerManifestJson();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100266
267 // Watches the /dist. When changed:
268 // - Notifies the HTTP live reload clients.
269 // - Regenerates the ServiceWorker file map.
270 scanDir(cfg.outDistRootDir);
271 }
272
Primiano Tuccia60ef182021-06-11 15:37:12 +0100273 // We should enter the loop only in watch mode, where tsc and rollup are
274 // asynchronous because they run in watch mode.
Andrew Shulaev6cf14622023-02-02 15:20:36 +0000275 if (args.no_build && !isDistComplete()) {
276 console.log('No build was requested, but artifacts are not available.');
277 console.log('In case of execution error, re-run without --no-build.');
278 }
279 if (!args.no_build) {
280 const tStart = Date.now();
281 while (!isDistComplete()) {
282 const secs = Math.ceil((Date.now() - tStart) / 1000);
283 process.stdout.write(
284 `\t\tWaiting for first build to complete... ${secs} s\r`);
285 await new Promise((r) => setTimeout(r, 500));
286 }
Primiano Tuccia60ef182021-06-11 15:37:12 +0100287 }
288 if (cfg.watch) console.log('\nFirst build completed!');
289
290 if (cfg.startHttpServer) {
291 startServer();
292 }
293 if (args.run_unittests) {
294 runTests('jest.unittest.config.js');
Primiano Tuccic8be6812021-02-09 18:08:49 +0100295 }
Primiano Tucci8bcf6082021-06-11 17:15:22 +0100296 if (args.run_integrationtests) {
297 runTests('jest.integrationtest.config.js');
298 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100299}
300
301// -----------
302// Build rules
303// -----------
304
Primiano Tuccia60ef182021-06-11 15:37:12 +0100305function runTests(cfgFile) {
306 const args = [
307 '--rootDir',
308 cfg.outTscDir,
309 '--verbose',
310 '--runInBand',
311 '--detectOpenHandles',
312 '--forceExit',
313 '--projects',
Hector Dearman969ecb92022-06-06 12:18:48 +0100314 pjoin(ROOT_DIR, 'ui/config', cfgFile),
Primiano Tuccia60ef182021-06-11 15:37:12 +0100315 ];
Primiano Tucci1bbaa532022-06-08 19:22:45 +0100316 if (cfg.testFilter.length > 0) {
317 args.push('-t', cfg.testFilter);
318 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100319 if (cfg.watch) {
320 args.push('--watchAll');
Hector Dearman48d34082023-06-21 15:43:08 +0100321 addTask(execModule, ['jest', args, {async: true}]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100322 } else {
Hector Dearman48d34082023-06-21 15:43:08 +0100323 addTask(execModule, ['jest', args]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100324 }
325}
326
Lalit Magantib1d5a982023-11-17 17:54:12 +0000327function cpHtml(src, filename) {
328 let html = fs.readFileSync(src).toString();
329 // First copy the html as-is into the dist/v1.2.3/ directory. This is
330 // only used for archival purporses, so one can open
331 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic.
332 fs.writeFileSync(pjoin(cfg.outDistDir, filename), html);
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100333
Lalit Magantib1d5a982023-11-17 17:54:12 +0000334 // Then copy it into the dist/ root by patching the version code.
335 // TODO(primiano): in next CLs, this script should take a
336 // --release_map=xxx.json argument, to populate this with multiple channels.
337 const versionMap = JSON.stringify({'stable': cfg.version});
338 const bodyRegex = /data-perfetto_version='[^']*'/;
339 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`);
340 fs.writeFileSync(pjoin(cfg.outDistRootDir, filename), html);
341}
342
343function copyIndexHtml(src) {
344 addTask(cpHtml, [src, 'index.html']);
345}
346
347function copyBigtraceHtml(src) {
348 if (cfg.bigtrace) {
349 addTask(cpHtml, [src, 'bigtrace.html']);
350 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100351}
352
353function copyAssets(src, dst) {
354 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
Lalit Magantiac807c92023-12-04 12:19:43 +0000355 if (cfg.bigtrace) {
356 addTask(cp, [src, pjoin(cfg.outBigtraceDistDir, 'assets', dst)]);
357 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100358}
359
Andrew Shulaevbb4e6b22022-07-04 10:18:04 +0100360function copyUiTestArtifactsAssets(src, dst) {
361 addTask(cp, [src, pjoin(cfg.outUiTestArtifactsDir, dst)]);
362}
363
Primiano Tuccic8be6812021-02-09 18:08:49 +0100364function compileScss() {
365 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
366 const dst = pjoin(cfg.outDistDir, 'perfetto.css');
367 // In watch mode, don't exit(1) if scss fails. It can easily happen by
Tuchila Octavian885f6892021-04-06 10:44:13 +0100368 // having a typo in the css. It will still print an error.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100369 const noErrCheck = !!cfg.watch;
Hector Dearman48d34082023-06-21 15:43:08 +0100370 const args = [src, dst];
371 if (!cfg.verbose) {
372 args.unshift('--quiet');
373 }
374 addTask(execModule, ['sass', args, {noErrCheck}]);
Lalit Magantib1d5a982023-11-17 17:54:12 +0000375 if (cfg.bigtrace) {
376 addTask(cp, [dst, pjoin(cfg.outBigtraceDistDir, 'perfetto.css')]);
377 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100378}
379
380function compileProtos() {
381 const dstJs = pjoin(cfg.outGenDir, 'protos.js');
382 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
Hector Dearman5f9da792023-03-27 20:12:49 +0100383 // We've ended up pulling in all the protos (via trace.proto,
384 // trace_packet.proto) below which means |dstJs| ends up being
385 // 23k lines/12mb. We should probably not do that.
386 // TODO(hjd): Figure out how to use lazy with pbjs/pbts.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100387 const inputs = [
Primiano Tuccic8be6812021-02-09 18:08:49 +0100388 'protos/perfetto/common/trace_stats.proto',
389 'protos/perfetto/common/tracing_service_capabilities.proto',
390 'protos/perfetto/config/perfetto_config.proto',
391 'protos/perfetto/ipc/consumer_port.proto',
392 'protos/perfetto/ipc/wire_protocol.proto',
393 'protos/perfetto/metrics/metrics.proto',
Alexander Timinb5102912022-10-12 22:18:35 +0100394 'protos/perfetto/trace/perfetto/perfetto_metatrace.proto',
395 'protos/perfetto/trace/trace.proto',
396 'protos/perfetto/trace/trace_packet.proto',
397 'protos/perfetto/trace_processor/trace_processor.proto',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100398 ];
Hector Dearman4a9b6922022-10-19 18:07:11 +0100399 // Can't put --no-comments here - The comments are load bearing for
400 // the pbts invocation which follows.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100401 const pbjsArgs = [
Hector Dearman4a9b6922022-10-19 18:07:11 +0100402 '--no-beautify',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100403 '--force-number',
Hector Dearman5f9da792023-03-27 20:12:49 +0100404 '--no-delimited',
405 '--no-verify',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100406 '-t',
407 'static-module',
408 '-w',
409 'commonjs',
410 '-p',
411 ROOT_DIR,
412 '-o',
Hector Dearman969ecb92022-06-06 12:18:48 +0100413 dstJs,
Primiano Tuccic8be6812021-02-09 18:08:49 +0100414 ].concat(inputs);
Hector Dearman48d34082023-06-21 15:43:08 +0100415 addTask(execModule, ['pbjs', pbjsArgs]);
Hector Dearman5f9da792023-03-27 20:12:49 +0100416
417 // Note: If you are looking into slowness of pbts it is not pbts
418 // itself that is slow. It invokes jsdoc to parse the comments out of
419 // the |dstJs| with https://github.com/hegemonic/catharsis which is
420 // pinning a CPU core the whole time.
Hector Dearman4a9b6922022-10-19 18:07:11 +0100421 const pbtsArgs = ['--no-comments', '-p', ROOT_DIR, '-o', dstTs, dstJs];
Hector Dearman48d34082023-06-21 15:43:08 +0100422 addTask(execModule, ['pbts', pbtsArgs]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100423}
424
Hector Dearman07ed69a2021-08-23 11:12:25 +0100425function generateImports(dir, name) {
426 // We have to use the symlink (ui/src/gen) rather than cfg.outGenDir
427 // below since we want to generate the correct relative imports. For example:
428 // ui/src/frontend/foo.ts
429 // import '../gen/all_plugins.ts';
430 // ui/src/gen/all_plugins.ts (aka ui/out/tsc/gen/all_plugins.ts)
431 // import '../frontend/some_plugin.ts';
432 const dstTs = pjoin(ROOT_DIR, 'ui/src/gen', name);
433 const inputDir = pjoin(ROOT_DIR, dir);
434 const args = [GEN_IMPORTS_SCRIPT, inputDir, '--out', dstTs];
435 addTask(exec, ['python3', args]);
436}
437
Primiano Tuccic8be6812021-02-09 18:08:49 +0100438// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
439function genVersion() {
440 const cmd = 'python3';
441 const args =
Tuchila Octavian885f6892021-04-06 10:44:13 +0100442 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')];
Primiano Tuccic8be6812021-02-09 18:08:49 +0100443 addTask(exec, [cmd, args]);
444}
445
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100446function updateSymlinks() {
Primiano Tuccia60ef182021-06-11 15:37:12 +0100447 // /ui/out -> /out/ui.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100448 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100449
Hector Dearman1811ec42022-05-26 14:45:18 +0100450 // /ui/src/gen -> /out/ui/ui/tsc/gen)
Hector Dearman969ecb92022-06-06 12:18:48 +0100451 mklink(cfg.outGenDir, pjoin(ROOT_DIR, 'ui/src/gen'));
Hector Dearman1811ec42022-05-26 14:45:18 +0100452
Primiano Tuccia60ef182021-06-11 15:37:12 +0100453 // /out/ui/test/data -> /test/data (For UI tests).
454 mklink(
455 pjoin(ROOT_DIR, 'test/data'),
456 pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data'));
457
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100458 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
459 // can point to that without having to know the current version number.
460 mklink(
461 path.relative(cfg.outUiDir, cfg.outDistDir),
462 pjoin(cfg.outUiDir, 'dist_version'));
463
Primiano Tuccic8be6812021-02-09 18:08:49 +0100464 mklink(
Hector Dearman969ecb92022-06-06 12:18:48 +0100465 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100466}
467
Hector Dearmana9545e52022-05-17 12:23:25 +0100468// Invokes ninja for building the {trace_processor, traceconv} Wasm modules.
Primiano Tuccic8be6812021-02-09 18:08:49 +0100469// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
470// out/tsc/, so the typescript compiler and the bundler can pick them up.
471function buildWasm(skipWasmBuild) {
472 if (!skipWasmBuild) {
Ankit Guptac2904532022-10-07 18:57:39 +0530473 if (!cfg.noOverrideGnArgs) {
474 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
475 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
476 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100477
Tuchila Octavian885f6892021-04-06 10:44:13 +0100478 const ninjaArgs = ['-C', cfg.outDir];
Hector Dearman969ecb92022-06-06 12:18:48 +0100479 ninjaArgs.push(...cfg.wasmModules.map((x) => `${x}_wasm`));
Primiano Tuccic8be6812021-02-09 18:08:49 +0100480 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
481 }
482
483 const wasmOutDir = pjoin(cfg.outDir, 'wasm');
484 for (const wasmMod of cfg.wasmModules) {
485 // The .wasm file goes directly into the dist dir (also .map in debug)
486 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
487 const src = `${wasmOutDir}/${wasmMod}${ext}`;
488 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
489 }
490 // The .js / .ts go into intermediates, they will be bundled by rollup.
491 for (const ext of ['.js', '.d.ts']) {
492 const fname = `${wasmMod}${ext}`;
493 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
494 }
495 }
496}
497
498// This transpiles all the sources (frontend, controller, engine, extension) in
499// one go. The only project that has a dedicated invocation is service_worker.
Hector Dearman5f9da792023-03-27 20:12:49 +0100500function transpileTsProject(project, options) {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100501 const args = ['--project', pjoin(ROOT_DIR, project)];
Hector Dearman5f9da792023-03-27 20:12:49 +0100502
Hector Dearman7b47a242023-03-28 13:31:41 +0100503 if (options !== undefined && options.watch) {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100504 args.push('--watch', '--preserveWatchOutput');
Hector Dearman48d34082023-06-21 15:43:08 +0100505 addTask(execModule, ['tsc', args, {async: true}]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100506 } else {
Hector Dearman48d34082023-06-21 15:43:08 +0100507 addTask(execModule, ['tsc', args]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100508 }
509}
510
511// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
512function bundleJs(cfgName) {
Tuchila Octavian885f6892021-04-06 10:44:13 +0100513 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100514 const args = ['-c', rcfg, '--no-indent'];
Primiano Tuccid60b8d82023-11-15 22:52:10 +0000515 if (cfg.bigtrace) {
516 args.push('--environment', 'ENABLE_BIGTRACE:true');
517 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100518 args.push(...(cfg.verbose ? [] : ['--silent']));
519 if (cfg.watch) {
Hector Dearman5f9da792023-03-27 20:12:49 +0100520 // --waitForBundleInput is sadly quite busted so it is required ts
521 // has build at least once before invoking this.
522 args.push('--watch', '--no-watch.clearScreen');
Hector Dearman48d34082023-06-21 15:43:08 +0100523 addTask(execModule, ['rollup', args, {async: true}]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100524 } else {
Hector Dearman48d34082023-06-21 15:43:08 +0100525 addTask(execModule, ['rollup', args]);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100526 }
527}
528
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100529function genServiceWorkerManifestJson() {
Hector Dearman969ecb92022-06-06 12:18:48 +0100530 function makeManifest() {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100531 const manifest = {resources: {}};
532 // When building the subresource manifest skip source maps, the manifest
533 // itself and the copy of the index.html which is copied under /v1.2.3/.
534 // The root /index.html will be fetched by service_worker.js separately.
535 const skipRegex = /(\.map|manifest\.json|index.html)$/;
Hector Dearman969ecb92022-06-06 12:18:48 +0100536 walk(cfg.outDistDir, (absPath) => {
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100537 const contents = fs.readFileSync(absPath);
538 const relPath = path.relative(cfg.outDistDir, absPath);
539 const b64 = crypto.createHash('sha256').update(contents).digest('base64');
540 manifest.resources[relPath] = 'sha256-' + b64;
541 }, skipRegex);
542 const manifestJson = JSON.stringify(manifest, null, 2);
543 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100544 }
Hector Dearman969ecb92022-06-06 12:18:48 +0100545 addTask(makeManifest, []);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100546}
547
548function startServer() {
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +0000549 console.log(
550 'Starting HTTP server on',
551 `http://${cfg.httpServerListenHost}:${cfg.httpServerListenPort}`);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100552 http.createServer(function(req, res) {
553 console.debug(req.method, req.url);
554 let uri = req.url.split('?', 1)[0];
555 if (uri.endsWith('/')) {
556 uri += 'index.html';
557 }
558
559 if (uri === '/live_reload') {
560 // Implements the Server-Side-Events protocol.
561 const head = {
562 'Content-Type': 'text/event-stream',
563 'Connection': 'keep-alive',
Hector Dearman969ecb92022-06-06 12:18:48 +0100564 'Cache-Control': 'no-cache',
Primiano Tuccic8be6812021-02-09 18:08:49 +0100565 };
566 res.writeHead(200, head);
567 const arrayIdx = httpWatches.length;
568 // We never remove from the array, the delete leaves an undefined item
569 // around. It makes keeping track of the index easier at the cost of a
570 // small leak.
571 httpWatches.push(res);
572 req.on('close', () => delete httpWatches[arrayIdx]);
573 return;
574 }
575
Tuchila Octavian1d423b22021-07-01 09:39:54 +0100576 let absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
577 // We want to be able to use the data in '/test/' for e2e tests.
578 // However, we don't want do create a symlink into the 'dist/' dir,
579 // because 'dist/' gets shipped on the production server.
580 if (uri.startsWith('/test/')) {
581 absPath = pjoin(ROOT_DIR, uri);
582 }
583
Primiano Tuccid24cfc52022-02-24 21:24:49 +0000584 // Don't serve contents outside of the project root (b/221101533).
585 if (path.relative(ROOT_DIR, absPath).startsWith('..')) {
586 res.writeHead(403);
587 res.end('403 Forbidden - Request path outside of the repo root');
588 return;
589 }
590
Primiano Tuccic8be6812021-02-09 18:08:49 +0100591 fs.readFile(absPath, function(err, data) {
592 if (err) {
593 res.writeHead(404);
594 res.end(JSON.stringify(err));
595 return;
596 }
597
598 const mimeMap = {
599 'html': 'text/html',
600 'css': 'text/css',
601 'js': 'application/javascript',
602 'wasm': 'application/wasm',
603 };
604 const ext = uri.split('.').pop();
605 const cType = mimeMap[ext] || 'octect/stream';
606 const head = {
607 'Content-Type': cType,
608 'Content-Length': data.length,
609 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
610 'Cache-Control': 'no-cache',
611 };
Alexander Timinb5d09d02022-07-26 21:10:02 +0000612 if (cfg.crossOriginIsolation) {
613 head['Cross-Origin-Opener-Policy'] = 'same-origin';
614 head['Cross-Origin-Embedder-Policy'] = 'require-corp';
615 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100616 res.writeHead(200, head);
Primiano Tucci7eead2e2021-02-16 11:46:04 +0100617 res.write(data);
618 res.end();
Primiano Tuccic8be6812021-02-09 18:08:49 +0100619 });
620 })
Daniele Di Proiettoff0f6e52021-11-11 17:53:21 +0000621 .listen(cfg.httpServerListenPort, cfg.httpServerListenHost);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100622}
623
Primiano Tuccia60ef182021-06-11 15:37:12 +0100624function isDistComplete() {
625 const requiredArtifacts = [
Primiano Tuccia60ef182021-06-11 15:37:12 +0100626 'frontend_bundle.js',
627 'engine_bundle.js',
Hector Dearman995d1912021-07-13 16:51:38 +0100628 'traceconv_bundle.js',
Primiano Tuccia60ef182021-06-11 15:37:12 +0100629 'trace_processor.wasm',
630 'perfetto.css',
631 ];
632 const relPaths = new Set();
Hector Dearman969ecb92022-06-06 12:18:48 +0100633 walk(cfg.outDistDir, (absPath) => {
Primiano Tuccia60ef182021-06-11 15:37:12 +0100634 relPaths.add(path.relative(cfg.outDistDir, absPath));
635 });
636 for (const fName of requiredArtifacts) {
637 if (!relPaths.has(fName)) return false;
638 }
639 return true;
640}
641
Primiano Tuccic8be6812021-02-09 18:08:49 +0100642// Called whenever a change in the out/dist directory is detected. It sends a
643// Server-Side-Event to the live_reload.ts script.
644function notifyLiveServer(changedFile) {
645 for (const cli of httpWatches) {
646 if (cli === undefined) continue;
647 cli.write(
648 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
649 }
650}
651
652function copyExtensionAssets() {
653 addTask(cp, [
654 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
Hector Dearman969ecb92022-06-06 12:18:48 +0100655 pjoin(cfg.outExtDir, 'logo-128.png'),
Primiano Tuccic8be6812021-02-09 18:08:49 +0100656 ]);
657 addTask(cp, [
658 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
Hector Dearman969ecb92022-06-06 12:18:48 +0100659 pjoin(cfg.outExtDir, 'manifest.json'),
Primiano Tuccic8be6812021-02-09 18:08:49 +0100660 ]);
661}
662
663// -----------------------
664// Task chaining functions
665// -----------------------
666
667function addTask(func, args) {
668 const task = new Task(func, args);
669 for (const t of tasks) {
670 if (t.identity === task.identity) {
671 return;
672 }
673 }
674 tasks.push(task);
675 setTimeout(runTasks, 0);
676}
677
678function runTasks() {
679 const snapTasks = tasks.splice(0); // snap = std::move(tasks).
680 tasksTot += snapTasks.length;
681 for (const task of snapTasks) {
682 const DIM = '\u001b[2m';
683 const BRT = '\u001b[37m';
684 const RST = '\u001b[0m';
685 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
686 const ts = `[${DIM}${ms}${RST}]`;
687 const descr = task.description.substr(0, 80);
688 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
Hector Dearman969ecb92022-06-06 12:18:48 +0100689 task.func.apply(/* this=*/ undefined, task.args);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100690 }
Primiano Tuccic8be6812021-02-09 18:08:49 +0100691}
692
693// Executes all the RULES that match the given |absPath|.
694function scanFile(absPath) {
695 console.assert(fs.existsSync(absPath));
696 console.assert(path.isAbsolute(absPath));
697 const normPath = path.relative(ROOT_DIR, absPath);
698 for (const rule of RULES) {
699 const match = rule.r.exec(normPath);
700 if (!match || match[0] !== normPath) continue;
701 const captureGroup = match.length > 1 ? match[1] : undefined;
702 rule.f(absPath, captureGroup);
703 }
704}
705
706// Walks the passed |dir| recursively and, for each file, invokes the matching
Primiano Tucci9b567032021-02-12 14:18:12 +0100707// RULES. If --watch is used, it also installs a fswatch() and re-triggers the
Primiano Tuccic8be6812021-02-09 18:08:49 +0100708// matching RULES on each file change.
709function scanDir(dir, regex) {
Hector Dearman969ecb92022-06-06 12:18:48 +0100710 const filterFn = regex ? (absPath) => regex.test(absPath) : () => true;
Primiano Tuccic8be6812021-02-09 18:08:49 +0100711 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
712 // Add a fs watch if in watch mode.
713 if (cfg.watch) {
Primiano Tucci9a92cf72021-02-13 17:42:34 +0100714 fswatch(absDir, {recursive: true}, (_eventType, filePath) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100715 if (!filterFn(filePath)) return;
716 if (cfg.verbose) {
717 console.log('File change detected', _eventType, filePath);
718 }
719 if (fs.existsSync(filePath)) {
720 scanFile(filePath, filterFn);
721 }
722 });
723 }
Hector Dearman969ecb92022-06-06 12:18:48 +0100724 walk(absDir, (f) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100725 if (filterFn(f)) scanFile(f);
726 });
727}
728
729function exec(cmd, args, opts) {
730 opts = opts || {};
731 opts.stdout = opts.stdout || 'inherit';
732 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
733 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
734 const checkExitCode = (code, signal) => {
735 if (signal === 'SIGINT' || signal === 'SIGTERM') return;
736 if (code !== 0 && !opts.noErrCheck) {
737 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
738 process.exit(1);
739 }
740 };
741 if (opts.async) {
Hector Dearman969ecb92022-06-06 12:18:48 +0100742 const proc = childProcess.spawn(cmd, args, spwOpts);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100743 const procIndex = subprocesses.length;
744 subprocesses.push(proc);
745 return new Promise((resolve, _reject) => {
746 proc.on('exit', (code, signal) => {
747 delete subprocesses[procIndex];
748 checkExitCode(code, signal);
749 resolve();
750 });
751 });
752 } else {
Hector Dearman969ecb92022-06-06 12:18:48 +0100753 const spawnRes = childProcess.spawnSync(cmd, args, spwOpts);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100754 checkExitCode(spawnRes.status, spawnRes.signal);
755 return spawnRes;
756 }
757}
758
Hector Dearman48d34082023-06-21 15:43:08 +0100759function execModule(module, args, opts) {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100760 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
Hector Dearman48d34082023-06-21 15:43:08 +0100761 return exec(modPath, args || [], opts);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100762}
763
764// ------------------------------------------
765// File system & subprocess utility functions
766// ------------------------------------------
767
768class Task {
769 constructor(func, args) {
770 this.func = func;
771 this.args = args || [];
772 // |identity| is used to dedupe identical tasks in the queue.
773 this.identity = JSON.stringify([this.func.name, this.args]);
774 }
775
776 get description() {
777 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
Hector Dearman969ecb92022-06-06 12:18:48 +0100778 const flattenedArgs = [].concat(...this.args);
Primiano Tuccic8be6812021-02-09 18:08:49 +0100779 for (const arg of flattenedArgs) {
780 const argStr = `${arg}`;
781 if (argStr.startsWith('/')) {
782 ret.push(path.relative(cfg.outDir, arg));
783 } else {
784 ret.push(argStr);
785 }
786 }
787 return ret.join(' ');
788 }
789}
790
791function walk(dir, callback, skipRegex) {
792 for (const child of fs.readdirSync(dir)) {
793 const childPath = pjoin(dir, child);
794 const stat = fs.lstatSync(childPath);
795 if (skipRegex !== undefined && skipRegex.test(child)) continue;
796 if (stat.isDirectory()) {
797 walk(childPath, callback, skipRegex);
798 } else if (!stat.isSymbolicLink()) {
799 callback(childPath);
800 }
801 }
802}
803
804function ensureDir(dirPath, clean) {
805 const exists = fs.existsSync(dirPath);
806 if (exists && clean) {
807 console.log('rm', dirPath);
808 fs.rmSync(dirPath, {recursive: true});
809 }
810 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
811 return dirPath;
812}
813
814function cp(src, dst) {
815 ensureDir(path.dirname(dst));
816 if (cfg.verbose) {
817 console.log(
818 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
819 }
820 fs.copyFileSync(src, dst);
821}
822
823function mklink(src, dst) {
824 // If the symlink already points to the right place don't touch it. This is
825 // to avoid changing the mtime of the ui/ dir when unnecessary.
826 if (fs.existsSync(dst)) {
827 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
828 return;
829 } else {
830 fs.unlinkSync(dst);
831 }
832 }
833 fs.symlinkSync(src, dst);
834}
835
836main();