blob: 3363d5327c85e2ba8830893c53746183be108c96 [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');
69const fs = require('fs');
70const http = require('http');
71const path = require('path');
Primiano Tucci9b567032021-02-12 14:18:12 +010072const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux.
Primiano Tuccic8be6812021-02-09 18:08:49 +010073const pjoin = path.join;
74
75const ROOT_DIR = path.dirname(__dirname); // The repo root.
76const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py');
77
78const cfg = {
79 watch: false,
80 verbose: false,
81 debug: false,
82 startHttpServer: false,
83 wasmModules: ['trace_processor', 'trace_to_text'],
84 testConfigs: ['jest.unit.config.js'],
85
86 // The fields below will be changed by main() after cmdline parsing.
87 // Directory structure:
88 // 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/ -> outDistDir : Final artifacts (JS bundles, assets).
93 // chrome_extension/ : Chrome extension.
94 outDir: pjoin(ROOT_DIR, 'out/ui'),
95 outUiDir: '',
96 outTscDir: '',
97 outGenDir: '',
98 outDistRootDir: '',
99 outDistDir: '',
100 outExtDir: '',
101};
102
103const RULES = [
104 {r: /ui\/src\/assets\/(index.html)/, f: copyIntoDistRoot},
105 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
106 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
107 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
108 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
109 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss},
110 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets},
111 {r: /.*\/dist\/(?!service_worker).*/, f: genServiceWorkerDistHashes},
112 {r: /.*\/dist\/.*/, f: notifyLiveServer},
113];
114
115let tasks = [];
116let tasksTot = 0, tasksRan = 0;
117let serverStarted = false;
118let httpWatches = [];
119let tStart = Date.now();
120let subprocesses = [];
121
122function main() {
123 const parser = new argparse.ArgumentParser();
124 parser.addArgument('--out', {help: 'Output directory'});
125 parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
126 parser.addArgument(['--serve', '-s'], {action: 'storeTrue'});
127 parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
128 parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
129 parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
130 parser.addArgument(['--run-tests', '-t'], {action: 'storeTrue'});
131 parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
132
133 const args = parser.parseArgs();
134 const clean = !args.no_build;
135 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir));
136 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean);
137 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension'));
138 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist'));
139 // TODO(primiano): for now distDir == distRootDir. In next CLs distDir will
140 // become dist/v1.2.3/.
141 cfg.outDistDir = cfg.outDistRootDir;
142 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc'));
143 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen'));
144 cfg.watch = !!args.watch;
145 cfg.verbose = !!args.verbose;
146 cfg.debug = !!args.debug;
147 cfg.startHttpServer = args.serve;
148
149 process.on('SIGINT', () => {
150 console.log('\nSIGINT received. Killing all child processes and exiting');
151 for (const proc of subprocesses) {
152 if (proc) proc.kill('SIGINT');
153 }
154 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT.
155 });
156
157 // Check that deps are current before starting.
158 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
159 const depsArgs = ['--check-only', pjoin(cfg.outDir, '.check_deps'), '--ui'];
160 exec(installBuildDeps, depsArgs);
161
162 console.log('Entering', cfg.outDir);
163 process.chdir(cfg.outDir);
164
165 updateSymilnks(); // Links //ui/out -> //out/xxx/ui/
166
167 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP
168 // server is started when the task queue reaches quiescence, but it takes at
169 // least one task for that.
170 addTask(() => {});
171
172 if (!args.no_build) {
173 buildWasm(args.no_wasm);
174 scanDir('ui/src/assets');
175 scanDir('ui/src/chrome_extension');
176 scanDir('buildtools/typefaces');
177 scanDir('buildtools/catapult_trace_viewer');
178 compileProtos();
179 genVersion();
180 transpileTsProject('ui');
181 bundleJs('rollup.config.js');
182
183 // ServiceWorker.
184 genServiceWorkerDistHashes();
185 transpileTsProject('ui/src/service_worker');
186 bundleJs('rollup-serviceworker.config.js');
187
188 // Watches the /dist. When changed:
189 // - Notifies the HTTP live reload clients.
190 // - Regenerates the ServiceWorker file map.
191 scanDir(cfg.outDistRootDir);
192 }
193
194 if (args.run_tests) {
195 runTests();
196 }
197}
198
199// -----------
200// Build rules
201// -----------
202
203function runTests() {
204 const args =
205 ['--rootDir', cfg.outTscDir, '--verbose', '--runInBand', '--forceExit'];
206 for (const cfgFile of cfg.testConfigs) {
207 args.push('--projects', pjoin(ROOT_DIR, 'ui/config', cfgFile));
208 }
209 if (cfg.watch) {
210 args.push('--watchAll');
211 addTask(execNode, ['jest', args, {async: true}]);
212 } else {
213 addTask(execNode, ['jest', args]);
214 }
215}
216
217function copyIntoDistRoot(src, dst) {
218 addTask(cp, [src, pjoin(cfg.outDistRootDir, dst)]);
219}
220
221function copyAssets(src, dst) {
222 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
223}
224
225function compileScss() {
226 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss');
227 const dst = pjoin(cfg.outDistDir, 'perfetto.css');
228 // In watch mode, don't exit(1) if scss fails. It can easily happen by
229 // having a typo in the css. It will still print an errror.
230 const noErrCheck = !!cfg.watch;
231 addTask(execNode, ['node-sass', ['--quiet', src, dst], {noErrCheck}]);
232}
233
234function compileProtos() {
235 const dstJs = pjoin(cfg.outGenDir, 'protos.js');
236 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts');
237 const inputs = [
238 'protos/perfetto/trace_processor/trace_processor.proto',
239 'protos/perfetto/common/trace_stats.proto',
240 'protos/perfetto/common/tracing_service_capabilities.proto',
241 'protos/perfetto/config/perfetto_config.proto',
242 'protos/perfetto/ipc/consumer_port.proto',
243 'protos/perfetto/ipc/wire_protocol.proto',
244 'protos/perfetto/metrics/metrics.proto',
245 ];
246 const pbjsArgs = [
247 '--force-number',
248 '-t',
249 'static-module',
250 '-w',
251 'commonjs',
252 '-p',
253 ROOT_DIR,
254 '-o',
255 dstJs
256 ].concat(inputs);
257 addTask(execNode, ['pbjs', pbjsArgs]);
258 const pbtsArgs = ['-p', ROOT_DIR, '-o', dstTs, dstJs];
259 addTask(execNode, ['pbts', pbtsArgs]);
260}
261
262// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
263function genVersion() {
264 const cmd = 'python3';
265 const args =
266 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')]
267 addTask(exec, [cmd, args]);
268}
269
270function updateSymilnks() {
271 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
272 mklink(
273 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules'))
274}
275
276// Invokes ninja for building the {trace_processor, trace_to_text} Wasm modules.
277// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into
278// out/tsc/, so the typescript compiler and the bundler can pick them up.
279function buildWasm(skipWasmBuild) {
280 if (!skipWasmBuild) {
281 const gnArgs = ['gen', `--args=is_debug=${cfg.debug}`, cfg.outDir];
282 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]);
283
284 const ninjaArgs = ['-C', cfg.outDir]
285 ninjaArgs.push(...cfg.wasmModules.map(x => `${x}_wasm`));
286 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]);
287 }
288
289 const wasmOutDir = pjoin(cfg.outDir, 'wasm');
290 for (const wasmMod of cfg.wasmModules) {
291 // The .wasm file goes directly into the dist dir (also .map in debug)
292 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) {
293 const src = `${wasmOutDir}/${wasmMod}${ext}`;
294 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]);
295 }
296 // The .js / .ts go into intermediates, they will be bundled by rollup.
297 for (const ext of ['.js', '.d.ts']) {
298 const fname = `${wasmMod}${ext}`;
299 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]);
300 }
301 }
302}
303
304// This transpiles all the sources (frontend, controller, engine, extension) in
305// one go. The only project that has a dedicated invocation is service_worker.
306function transpileTsProject(project) {
307 const args = [
308 '--project',
309 pjoin(ROOT_DIR, project),
310 '--outDir',
311 cfg.outTscDir,
312 ];
313 if (cfg.watch) {
314 args.push('--watch', '--preserveWatchOutput');
315 addTask(execNode, ['tsc', args, {async: true}]);
316 } else {
317 addTask(execNode, ['tsc', args]);
318 }
319}
320
321// Creates the three {frontend, controller, engine}_bundle.js in one invocation.
322function bundleJs(cfgName) {
323 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName)
324 const args = ['-c', rcfg, '--no-indent'];
325 args.push(...(cfg.verbose ? [] : ['--silent']));
326 if (cfg.watch) {
327 // --waitForBundleInput is so that we can run tsc --watch and rollup --watch
328 // together, without having to wait that tsc completes the first build.
329 args.push('--watch', '--waitForBundleInput', '--no-watch.clearScreen');
330 addTask(execNode, ['rollup', args, {async: true}]);
331 } else {
332 addTask(execNode, ['rollup', args]);
333 }
334}
335
336// Generates a map of {"dist_file_name" -> "sha256-01234"} used by the SW.
337function genServiceWorkerDistHashes() {
338 function write_ui_dist_file_map() {
339 const distFiles = [];
340 const skipRegex = /(service_worker\.js)|(\.map$)/;
341 walk(cfg.outDistDir, f => distFiles.push(f), skipRegex);
342 const dst = pjoin(cfg.outGenDir, 'dist_file_map.ts');
343 const cmd = 'python3';
344 const args = [
345 pjoin(ROOT_DIR, 'gn/standalone/write_ui_dist_file_map.py'),
346 '--out',
347 dst,
348 '--strip',
349 cfg.outDistDir,
350 ].concat(distFiles);
351 exec(cmd, args);
352 }
353 addTask(write_ui_dist_file_map, []);
354}
355
356function startServer() {
357 const port = 10000;
358 console.log(`Starting HTTP server on http://localhost:${port}`)
359 http.createServer(function(req, res) {
360 console.debug(req.method, req.url);
361 let uri = req.url.split('?', 1)[0];
362 if (uri.endsWith('/')) {
363 uri += 'index.html';
364 }
365
366 if (uri === '/live_reload') {
367 // Implements the Server-Side-Events protocol.
368 const head = {
369 'Content-Type': 'text/event-stream',
370 'Connection': 'keep-alive',
371 'Cache-Control': 'no-cache'
372 };
373 res.writeHead(200, head);
374 const arrayIdx = httpWatches.length;
375 // We never remove from the array, the delete leaves an undefined item
376 // around. It makes keeping track of the index easier at the cost of a
377 // small leak.
378 httpWatches.push(res);
379 req.on('close', () => delete httpWatches[arrayIdx]);
380 return;
381 }
382
383 const absPath = path.normalize(path.join(cfg.outDistRootDir, uri));
384 fs.readFile(absPath, function(err, data) {
385 if (err) {
386 res.writeHead(404);
387 res.end(JSON.stringify(err));
388 return;
389 }
390
391 const mimeMap = {
392 'html': 'text/html',
393 'css': 'text/css',
394 'js': 'application/javascript',
395 'wasm': 'application/wasm',
396 };
397 const ext = uri.split('.').pop();
398 const cType = mimeMap[ext] || 'octect/stream';
399 const head = {
400 'Content-Type': cType,
401 'Content-Length': data.length,
402 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(),
403 'Cache-Control': 'no-cache',
404 };
405 res.writeHead(200, head);
406 res.end(data);
407 });
408 })
409 .listen(port);
410}
411
412// Called whenever a change in the out/dist directory is detected. It sends a
413// Server-Side-Event to the live_reload.ts script.
414function notifyLiveServer(changedFile) {
415 for (const cli of httpWatches) {
416 if (cli === undefined) continue;
417 cli.write(
418 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n');
419 }
420}
421
422function copyExtensionAssets() {
423 addTask(cp, [
424 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'),
425 pjoin(cfg.outExtDir, 'logo-128.png')
426 ]);
427 addTask(cp, [
428 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'),
429 pjoin(cfg.outExtDir, 'manifest.json')
430 ]);
431}
432
433// -----------------------
434// Task chaining functions
435// -----------------------
436
437function addTask(func, args) {
438 const task = new Task(func, args);
439 for (const t of tasks) {
440 if (t.identity === task.identity) {
441 return;
442 }
443 }
444 tasks.push(task);
445 setTimeout(runTasks, 0);
446}
447
448function runTasks() {
449 const snapTasks = tasks.splice(0); // snap = std::move(tasks).
450 tasksTot += snapTasks.length;
451 for (const task of snapTasks) {
452 const DIM = '\u001b[2m';
453 const BRT = '\u001b[37m';
454 const RST = '\u001b[0m';
455 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1);
456 const ts = `[${DIM}${ms}${RST}]`;
457 const descr = task.description.substr(0, 80);
458 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
459 task.func.apply(/*this=*/ undefined, task.args);
460 }
461 // Start the web server once reaching quiescence.
462 if (tasks.length === 0 && !serverStarted && cfg.startHttpServer) {
463 serverStarted = true;
464 startServer();
465 }
466}
467
468// Executes all the RULES that match the given |absPath|.
469function scanFile(absPath) {
470 console.assert(fs.existsSync(absPath));
471 console.assert(path.isAbsolute(absPath));
472 const normPath = path.relative(ROOT_DIR, absPath);
473 for (const rule of RULES) {
474 const match = rule.r.exec(normPath);
475 if (!match || match[0] !== normPath) continue;
476 const captureGroup = match.length > 1 ? match[1] : undefined;
477 rule.f(absPath, captureGroup);
478 }
479}
480
481// Walks the passed |dir| recursively and, for each file, invokes the matching
Primiano Tucci9b567032021-02-12 14:18:12 +0100482// RULES. If --watch is used, it also installs a fswatch() and re-triggers the
Primiano Tuccic8be6812021-02-09 18:08:49 +0100483// matching RULES on each file change.
484function scanDir(dir, regex) {
485 const filterFn = regex ? absPath => regex.test(absPath) : () => true;
486 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
487 // Add a fs watch if in watch mode.
488 if (cfg.watch) {
Primiano Tucci9a92cf72021-02-13 17:42:34 +0100489 fswatch(absDir, {recursive: true}, (_eventType, filePath) => {
Primiano Tuccic8be6812021-02-09 18:08:49 +0100490 if (!filterFn(filePath)) return;
491 if (cfg.verbose) {
492 console.log('File change detected', _eventType, filePath);
493 }
494 if (fs.existsSync(filePath)) {
495 scanFile(filePath, filterFn);
496 }
497 });
498 }
499 walk(absDir, f => {
500 if (filterFn(f)) scanFile(f);
501 });
502}
503
504function exec(cmd, args, opts) {
505 opts = opts || {};
506 opts.stdout = opts.stdout || 'inherit';
507 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
508 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
509 const checkExitCode = (code, signal) => {
510 if (signal === 'SIGINT' || signal === 'SIGTERM') return;
511 if (code !== 0 && !opts.noErrCheck) {
512 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
513 process.exit(1);
514 }
515 };
516 if (opts.async) {
517 const proc = child_process.spawn(cmd, args, spwOpts);
518 const procIndex = subprocesses.length;
519 subprocesses.push(proc);
520 return new Promise((resolve, _reject) => {
521 proc.on('exit', (code, signal) => {
522 delete subprocesses[procIndex];
523 checkExitCode(code, signal);
524 resolve();
525 });
526 });
527 } else {
528 const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
529 checkExitCode(spawnRes.status, spawnRes.signal);
530 return spawnRes;
531 }
532}
533
534function execNode(module, args, opts) {
535 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module);
536 const nodeBin = pjoin(ROOT_DIR, 'tools/node');
537 args = [modPath].concat(args || []);
538 const argsJson = JSON.stringify(args);
539 return exec(nodeBin, args, opts);
540}
541
542// ------------------------------------------
543// File system & subprocess utility functions
544// ------------------------------------------
545
546class Task {
547 constructor(func, args) {
548 this.func = func;
549 this.args = args || [];
550 // |identity| is used to dedupe identical tasks in the queue.
551 this.identity = JSON.stringify([this.func.name, this.args]);
552 }
553
554 get description() {
555 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name];
556 const flattenedArgs = [].concat.apply([], this.args);
557 for (const arg of flattenedArgs) {
558 const argStr = `${arg}`;
559 if (argStr.startsWith('/')) {
560 ret.push(path.relative(cfg.outDir, arg));
561 } else {
562 ret.push(argStr);
563 }
564 }
565 return ret.join(' ');
566 }
567}
568
569function walk(dir, callback, skipRegex) {
570 for (const child of fs.readdirSync(dir)) {
571 const childPath = pjoin(dir, child);
572 const stat = fs.lstatSync(childPath);
573 if (skipRegex !== undefined && skipRegex.test(child)) continue;
574 if (stat.isDirectory()) {
575 walk(childPath, callback, skipRegex);
576 } else if (!stat.isSymbolicLink()) {
577 callback(childPath);
578 }
579 }
580}
581
582function ensureDir(dirPath, clean) {
583 const exists = fs.existsSync(dirPath);
584 if (exists && clean) {
585 console.log('rm', dirPath);
586 fs.rmSync(dirPath, {recursive: true});
587 }
588 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
589 return dirPath;
590}
591
592function cp(src, dst) {
593 ensureDir(path.dirname(dst));
594 if (cfg.verbose) {
595 console.log(
596 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst));
597 }
598 fs.copyFileSync(src, dst);
599}
600
601function mklink(src, dst) {
602 // If the symlink already points to the right place don't touch it. This is
603 // to avoid changing the mtime of the ui/ dir when unnecessary.
604 if (fs.existsSync(dst)) {
605 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
606 return;
607 } else {
608 fs.unlinkSync(dst);
609 }
610 }
611 fs.symlinkSync(src, dst);
612}
613
614main();