blob: cbf4985eb3ccedbed1ed1c2130d05e44e4f6fc4a [file] [edit]
// Copyright (C) 2026 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.
// Prebuild: everything Vite consumes before bundling. Wasm via ninja, protos
// via pbjs/pbts, stdlib docs JSON, static asset copy, and the patched root
// index.html.
import {spawnSync} from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import {buildWasm, copySyntaqliteRuntime} from './build_wasm.mjs';
import {
copyByPattern,
copyDir,
ensureDir,
listFilesRecursive,
} from './fs_utils.mjs';
import {runInProcStep, runStep} from './steps.mjs';
const pjoin = path.join;
const WASM_MODULES = [
'traceconv',
'proto_utils',
'trace_processor',
'trace_processor_memory64',
];
const PROTO_INPUTS = [
'protos/perfetto/ipc/consumer_port.proto',
'protos/perfetto/ipc/wire_protocol.proto',
'protos/perfetto/trace/perfetto/perfetto_metatrace.proto',
'protos/perfetto/perfetto_sql/structured_query.proto',
'protos/perfetto/trace_processor/trace_processor.proto',
];
export async function prebuild({
rootDir,
outDir,
version,
debug = false,
skipWasm = false,
skipDepscheck = false,
noOverrideGnArgs = false,
onlyWasmMemory64 = false,
titleOverride = '',
}) {
if (!skipDepscheck) {
await checkBuildDeps({rootDir, outDir});
}
// Wipe the UI out dir for a clean prod build. Done up front so both
// prebuild and prod write into a known-empty tree.
await runInProcStep('clean output directory', async () => {
await fs.promises.rm(outDir, {recursive: true, force: true});
ensureDir(outDir);
});
// outDir is the UI out dir (e.g. <repo>/out/ui/ui). ninja's root out dir is
// its parent.
const ninjaOutDir = path.dirname(outDir);
const distRootDir = ensureDir(pjoin(outDir, 'dist'));
// Versioned dist subdir: dist/v<version>/. Everything except the root
// index.html and service_worker.js lives in here so multiple versions can
// coexist in the GCS bucket and clients can swap atomically.
const distDir = ensureDir(pjoin(distRootDir, version));
const genDir = ensureDir(pjoin(outDir, 'tsc/gen'));
await runInProcStep('update symlinks', () =>
updateSymlinks({rootDir, outDir, genDir}),
);
const run = (label, cmd, args) =>
runStep(label, cmd, args, {cwd: pjoin(rootDir, 'ui')});
// memory64 always builds; the regular trace_processor is optional so
// --only-wasm-memory64 can shave time off when iterating on it.
const wasmModules = onlyWasmMemory64
? WASM_MODULES.filter((m) => m !== 'trace_processor')
: WASM_MODULES;
await buildWasm({
rootDir,
ninjaOutDir,
distDir,
genDir,
wasmModules,
debug,
skipBuild: skipWasm,
noOverrideGnArgs,
run,
});
await copySyntaqliteRuntime({rootDir, distRootDir: distDir, run});
await compileProtos({rootDir, genDir, run});
await generateStdlibDocs({rootDir, distDir, run});
await runInProcStep('copy static assets', () =>
copyStaticAssets({
rootDir,
distRootDir: distDir,
extDir: pjoin(outDir, 'chrome_extension'),
}),
);
await runInProcStep('write index.html', () =>
writeIndexHtml({rootDir, distRootDir, distDir, version, titleOverride}),
);
return {distRootDir, distDir, genDir};
}
// Sets up the symlinks that the rest of the build (Vite config, tsc, runtime
// asset loading) assumes. Recreated each build to keep them pointing at the
// current outDir.
function updateSymlinks({rootDir, outDir, genDir}) {
// ui/out → <outDir> (Vite uses ui/out as a stable path to the build dir).
mklink(outDir, pjoin(rootDir, 'ui/out'));
// ui/src/gen → <genDir> (TS imports resolve `gen/protos` etc through this).
mklink(genDir, pjoin(rootDir, 'ui/src/gen'));
// tsc/node_modules → ui/node_modules (the generated .js in tsc/gen does
// require('protobufjs'); Node walks up from the file's dir looking for
// node_modules).
mklink(
pjoin(rootDir, 'ui/node_modules'),
pjoin(outDir, 'tsc', 'node_modules'),
);
}
// Creates or updates a symlink at |dst| pointing to |src|. No-op if it
// already points at the right place (avoids touching mtimes).
function mklink(src, dst) {
if (fs.existsSync(dst)) {
if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) {
return;
}
fs.unlinkSync(dst);
}
ensureDir(path.dirname(dst));
fs.symlinkSync(src, dst);
}
// Checks buildtools/ matches the pinned versions in tools/install-build-deps.
// The script writes |checkDepsPath| as a stamp on success, then short-circuits
// on subsequent runs via --check-only. macOS Apple Silicon: force arm64 since
// some buildtools binaries are arm64-only. Stamp lives one level above outDir
// so the prebuild's clean step doesn't wipe it.
async function checkBuildDeps({rootDir, outDir}) {
const checkDepsPath = pjoin(path.dirname(outDir), '.check_deps');
let cmd = pjoin(rootDir, 'tools/install-build-deps');
let args = [`--check-only=${checkDepsPath}`, '--ui'];
if (process.platform === 'darwin') {
const arm = spawnSync('arch', ['-arm64', 'true']);
if (arm.status === 0) {
args = ['-arch', 'arm64', cmd, ...args];
cmd = 'arch';
}
}
await runStep('install-build-deps --check-only', cmd, args);
}
export async function compileProtos({rootDir, genDir, run}) {
const r = run ?? ((l, c, a) => runStep(l, c, a, {cwd: pjoin(rootDir, 'ui')}));
const dstJs = pjoin(genDir, 'protos.js');
const dstTs = pjoin(genDir, 'protos.d.ts');
const modBin = (bin) => pjoin(rootDir, 'ui/node_modules/.bin', bin);
// Can't put --no-comments on pbjs - the comments are load-bearing for the
// pbts invocation which follows.
await r('pbjs (protos.js)', modBin('pbjs'), [
'--no-beautify',
'--force-number',
'--no-delimited',
'--no-verify',
'-t', 'static-module',
'-w', 'es6',
'-p', rootDir,
'-o', dstJs,
...PROTO_INPUTS,
]);
// Note: pbts is slow because it shells out to jsdoc to parse the comments
// out of |dstJs|; catharsis (jsdoc's type parser) pins a CPU core throughout.
await r('pbts (protos.d.ts)', modBin('pbts'), [
'--no-comments',
'-p', rootDir,
'-o', dstTs,
dstJs,
]);
}
// Generates stdlib_docs.json from the PerfettoSQL stdlib .sql files. Consumed
// at runtime by the UI's SQL docs viewer.
async function generateStdlibDocs({rootDir, distDir, run}) {
const stdlibDir = pjoin(rootDir, 'src/trace_processor/perfetto_sql/stdlib');
const sqlFiles = listFilesRecursive(stdlibDir).filter((f) =>
f.endsWith('.sql'),
);
await run('gen_stdlib_docs_json', pjoin(rootDir, 'tools/gen_stdlib_docs_json.py'), [
'--json-out', pjoin(distDir, 'stdlib_docs.json'),
'--minify',
...sqlFiles,
]);
}
// Copies the static asset trees that aren't processed by Vite — PNGs and
// fonts loaded via runtime `assetSrc()` calls, data_explorer JSON/MD, and
// the chrome extension's manifest+icon.
function copyStaticAssets({rootDir, distRootDir, extDir}) {
const assetsDst = pjoin(distRootDir, 'assets');
copyByPattern(pjoin(rootDir, 'ui/src/assets'), assetsDst, /\.png$/);
copyDir(
pjoin(rootDir, 'ui/src/assets/data_explorer'),
pjoin(assetsDst, 'data_explorer'),
/\.(json|md)$/,
);
copyByPattern(pjoin(rootDir, 'buildtools/typefaces'), assetsDst, /\.woff2$/);
copyByPattern(
pjoin(rootDir, 'buildtools/catapult_trace_viewer'),
assetsDst,
/\.(js|html)$/,
);
ensureDir(extDir);
fs.copyFileSync(
pjoin(rootDir, 'ui/src/assets/logo-128.png'),
pjoin(extDir, 'logo-128.png'),
);
fs.copyFileSync(
pjoin(rootDir, 'ui/src/chrome_extension/manifest.json'),
pjoin(extDir, 'manifest.json'),
);
}
// Writes index.html twice:
// dist/v<version>/index.html — verbatim copy (lets /v<version>/ serve as a
// standalone archival entry point).
// dist/index.html — patched so data-perfetto_version maps the
// 'stable' channel to this build, ensuring the channel loader picks the
// locally-built bundles instead of whatever's cached in localStorage or
// baked into the source index.html.
// Walks |distDir| and writes a manifest.json mapping each relative path to
// its sha256. The service worker reads this to validate cached files. Skips
// source maps, the manifest itself, and the archival index.html (only the
// root /index.html is fetched by the SW).
export function genServiceWorkerManifestJson({distDir}) {
const manifest = {resources: {}};
const skipRegex = /(\.map|manifest\.json|index\.html)$/;
const walk = (dir) => {
for (const child of fs.readdirSync(dir)) {
const childPath = pjoin(dir, child);
const stat = fs.lstatSync(childPath);
if (skipRegex.test(child)) continue;
if (stat.isDirectory()) {
walk(childPath);
} else if (!stat.isSymbolicLink()) {
const contents = fs.readFileSync(childPath);
const relPath = path.relative(distDir, childPath);
const b64 = crypto
.createHash('sha256')
.update(contents)
.digest('base64');
manifest.resources[relPath] = 'sha256-' + b64;
}
}
};
walk(distDir);
fs.writeFileSync(
pjoin(distDir, 'manifest.json'),
JSON.stringify(manifest, null, 2),
);
}
function writeIndexHtml({rootDir, distRootDir, distDir, version, titleOverride}) {
const src = pjoin(rootDir, 'ui/src/assets/index.html');
let html = fs.readFileSync(src, 'utf8');
if (titleOverride) {
html = html.replace(
/<title>[^<]*<\/title>/,
`<title>${titleOverride}</title>`,
);
}
fs.writeFileSync(pjoin(distDir, 'index.html'), html);
const versionMap = JSON.stringify({stable: version});
const patched = html.replace(
/data-perfetto_version='[^']*'/,
`data-perfetto_version='${versionMap}'`,
);
fs.writeFileSync(pjoin(distRootDir, 'index.html'), patched);
}