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