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