xref: /aosp_15_r20/external/perfetto/infra/perfetto.dev/build.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 builds the perfetto.dev docs website.
18
19const argparse = require('argparse');
20const child_process = require('child_process');
21const fs = require('fs');
22const http = require('http');
23const path = require('path');
24const pjoin = path.join;
25
26const ROOT_DIR = path.dirname(path.dirname(__dirname));  // The repo root.
27
28const cfg = {
29  watch: false,
30  verbose: false,
31  startHttpServer: false,
32
33  outDir: pjoin(ROOT_DIR, 'out/perfetto.dev'),
34};
35
36function main() {
37  const parser = new argparse.ArgumentParser();
38  parser.add_argument('--out', {help: 'Output directory'});
39  parser.add_argument('--watch', '-w', {action: 'store_true'});
40  parser.add_argument('--serve', '-s', {action: 'store_true'});
41  parser.add_argument('--verbose', '-v', {action: 'store_true'});
42
43  const args = parser.parse_args();
44  cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir, /*clean=*/ true));
45  cfg.watch = !!args.watch;
46  cfg.verbose = !!args.verbose;
47  cfg.startHttpServer = args.serve;
48
49  // Check that deps are current before starting.
50  const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps');
51
52  // --filter=nodejs --filter=pnpm --filter=gn --filter=ninja is to match what
53  // cloud_build_entrypoint.sh passes to install-build-deps. It doesn't bother
54  // installing the full toolchains because, unlike the Perfetto UI, it doesn't
55  // need Wasm.
56  const depsArgs = [
57    '--check-only=/dev/null',
58    '--ui',
59    '--filter=nodejs',
60    '--filter=pnpm',
61    '--filter=gn',
62    '--filter=ninja'
63  ];
64  exec(installBuildDeps, depsArgs);
65
66  ninjaBuild();
67
68  if (args.watch) {
69    watchDir('docs');
70    watchDir('infra/perfetto.dev/src/assets');
71    watchDir('protos');
72    watchDir('python');
73    watchDir('src/trace_processor/tables');
74  }
75  if (args.serve) {
76    startServer();
77  }
78}
79
80function ninjaBuild() {
81  exec(
82      pjoin(ROOT_DIR, 'tools/gn'),
83      ['gen', cfg.outDir, '--args=enable_perfetto_site=true']);
84  exec(pjoin(ROOT_DIR, 'tools/ninja'), ['-C', cfg.outDir, 'site']);
85}
86
87function startServer() {
88  const port = 8082;
89  console.log(`Starting HTTP server on http://localhost:${port}`)
90  const serveDir = path.join(cfg.outDir, 'site');
91  http.createServer(function(req, res) {
92        console.debug(req.method, req.url);
93        let uri = req.url.split('?', 1)[0];
94        uri += uri.endsWith('/') ? 'index.html' : '';
95
96        // Disallow serving anything outside out directory.
97        const absPath = path.normalize(path.join(serveDir, uri));
98        const relative = path.relative(serveDir, absPath);
99        if (relative.startsWith('..')) {
100          res.writeHead(404);
101          res.end();
102          return;
103        }
104
105        fs.readFile(absPath, function(err, data) {
106          if (err) {
107            res.writeHead(404);
108            res.end(JSON.stringify(err));
109            return;
110          }
111          const mimeMap = {
112            'css': 'text/css',
113            'png': 'image/png',
114            'svg': 'image/svg+xml',
115            'js': 'application/javascript',
116          };
117          const contentType = mimeMap[uri.split('.').pop()] || 'text/html';
118          const head = {
119            'Content-Type': contentType,
120            'Content-Length': data.length,
121            'Cache-Control': 'no-cache',
122          };
123          res.writeHead(200, head);
124          res.end(data);
125        });
126      })
127      .listen(port, 'localhost');
128}
129
130function watchDir(dir) {
131  const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir);
132  // Add a fs watch if in watch mode.
133  if (cfg.watch) {
134    fs.watch(absDir, {recursive: true}, (_eventType, filePath) => {
135      if (cfg.verbose) {
136        console.log('File change detected', _eventType, filePath);
137      }
138      ninjaBuild();
139    });
140  }
141}
142
143function exec(cmd, args, opts) {
144  opts = opts || {};
145  opts.stdout = opts.stdout || 'inherit';
146  if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`);
147  const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']};
148  const checkExitCode = (code, signal) => {
149    if (signal === 'SIGINT' || signal === 'SIGTERM') return;
150    if (code !== 0 && !opts.noErrCheck) {
151      console.error(`${cmd} ${args.join(' ')} failed with code ${code}`);
152      process.exit(1);
153    }
154  };
155  const spawnRes = child_process.spawnSync(cmd, args, spwOpts);
156  checkExitCode(spawnRes.status, spawnRes.signal);
157  return spawnRes;
158}
159
160function ensureDir(dirPath, clean) {
161  const exists = fs.existsSync(dirPath);
162  if (exists && clean) {
163    if (cfg.verbose) console.log('rm', dirPath);
164    fs.rmSync(dirPath, {recursive: true});
165  }
166  if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true});
167  return dirPath;
168}
169
170main();
171