// Copyright (C) 2021 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; // This script builds the perfetto.dev docs website. const argparse = require('argparse'); const child_process = require('child_process'); const fs = require('fs'); const http = require('http'); const path = require('path'); const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux. const pjoin = path.join; const ROOT_DIR = path.dirname(path.dirname(__dirname)); // The repo root. const cfg = { watch: false, verbose: false, startHttpServer: false, outDir: pjoin(ROOT_DIR, 'out/perfetto.dev'), }; function main() { const parser = new argparse.ArgumentParser(); parser.add_argument('--out', {help: 'Output directory'}); parser.add_argument('--watch', '-w', {action: 'store_true'}); parser.add_argument('--serve', '-s', {action: 'store_true'}); parser.add_argument('--verbose', '-v', {action: 'store_true'}); const args = parser.parse_args(); cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir, /*clean=*/ true)); cfg.watch = !!args.watch; cfg.verbose = !!args.verbose; cfg.startHttpServer = args.serve; // Check that deps are current before starting. const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps'); // --filter=nodejs --filter=gn --filter=ninja is to match what // cloud_build_entrypoint.sh passes to install-build-deps. It doesn't bother // installing the full toolchains because, unlike the Perfetto UI, it doesn't // need Wasm. const depsArgs = ['--check-only=/dev/null', '--ui', '--filter=nodejs', '--filter=gn', '--filter=ninja']; exec(installBuildDeps, depsArgs); ninjaBuild(); if (args.watch) { watchDir('docs'); watchDir('infra/perfetto.dev/src/assets'); watchDir('protos'); watchDir('python'); watchDir('src/trace_processor/tables'); } if (args.serve) { startServer(); } } function ninjaBuild() { exec( pjoin(ROOT_DIR, 'tools/gn'), ['gen', cfg.outDir, '--args=enable_perfetto_site=true']); exec(pjoin(ROOT_DIR, 'tools/ninja'), ['-C', cfg.outDir, 'site']); } function startServer() { const port = 8082; console.log(`Starting HTTP server on http://localhost:${port}`) const serveDir = path.join(cfg.outDir, 'site'); http.createServer(function(req, res) { console.debug(req.method, req.url); let uri = req.url.split('?', 1)[0]; uri += uri.endsWith('/') ? 'index.html' : ''; // Disallow serving anything outside out directory. const absPath = path.normalize(path.join(serveDir, uri)); const relative = path.relative(serveDir, absPath); if (relative.startsWith('..')) { res.writeHead(404); res.end(); return; } fs.readFile(absPath, function(err, data) { if (err) { res.writeHead(404); res.end(JSON.stringify(err)); return; } const mimeMap = { 'css': 'text/css', 'png': 'image/png', 'svg': 'image/svg+xml', 'js': 'application/javascript', }; const contentType = mimeMap[uri.split('.').pop()] || 'text/html'; const head = { 'Content-Type': contentType, 'Content-Length': data.length, 'Cache-Control': 'no-cache', }; res.writeHead(200, head); res.end(data); }); }) .listen(port, 'localhost'); } function watchDir(dir) { const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir); // Add a fs watch if in watch mode. if (cfg.watch) { fswatch(absDir, {recursive: true}, (_eventType, filePath) => { if (cfg.verbose) { console.log('File change detected', _eventType, filePath); } ninjaBuild(); }); } } function exec(cmd, args, opts) { opts = opts || {}; opts.stdout = opts.stdout || 'inherit'; if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`); const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']}; const checkExitCode = (code, signal) => { if (signal === 'SIGINT' || signal === 'SIGTERM') return; if (code !== 0 && !opts.noErrCheck) { console.error(`${cmd} ${args.join(' ')} failed with code ${code}`); process.exit(1); } }; const spawnRes = child_process.spawnSync(cmd, args, spwOpts); checkExitCode(spawnRes.status, spawnRes.signal); return spawnRes; } function ensureDir(dirPath, clean) { const exists = fs.existsSync(dirPath); if (exists && clean) { if (cfg.verbose) console.log('rm', dirPath); fs.rmSync(dirPath, {recursive: true}); } if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true}); return dirPath; } main();