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