blob: 1a65e522e1e8a742d2d264bd9ad8d05ede7c2fd4 [file] [edit]
// 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.
// Entry point for the Perfetto UI build. Subcommands:
// pre Wasm + protos + asset copy (everything Vite consumes).
// build pre + `vite build` for the production bundles.
// dev pre + in-process Vite dev server with HMR.
// preview Static HTTP server over an already-built dist/.
// test pre + Vitest.
// typecheck Just enough prebuild to run tsc --noEmit.
//
// Vite owns TS transpile, bundling, and dev-time HMR (see ui/vite.config.mjs).
// This script glues together the surrounding tasks; the heavy lifting lives
// under ui/scripts/.
import argparse from 'argparse';
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {startVitest} from 'vitest/node';
import {acquireBuildLock, warnIfBuildLockHeld} from './scripts/build_lock.mjs';
import {ensureDir} from './scripts/fs_utils.mjs';
import {
compileProtos,
genServiceWorkerManifestJson,
prebuild,
} from './scripts/prebuild.mjs';
import {startStaticServer} from './scripts/static_server.mjs';
import {runInProcStep, runStep} from './scripts/steps.mjs';
import {
ALL_BUNDLES,
OPEN_PERFETTO_TRACE_BUNDLE,
WORKER_BUNDLES,
viteBuild,
viteDev,
} from './scripts/vite_runner.mjs';
const pjoin = path.join;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.dirname(__dirname); // The repo root.
const DEFAULT_PORT = 10000;
// Compute the build version once and export to env so vite.config.mjs picks
// it up. All bundles and static assets live under dist/v<version>/.
const VERSION = spawnSync(
'python3',
[pjoin(ROOT_DIR, 'tools/write_version_header.py'), '--stdout'],
{encoding: 'utf8'},
).stdout.trim();
if (!VERSION) throw new Error('Failed to compute UI version');
process.env.PERFETTO_UI_VERSION = VERSION;
// Loads ~/.config/perfetto/ui-dev-server.env and injects any KEY=VALUE pairs
// into process.env, without overriding variables already set in the environment.
function loadDevServerEnvFile() {
const home = process.env.HOME || process.env.USERPROFILE || '';
const envFile = path.join(home, '.config', 'perfetto', 'ui-dev-server.env');
let content;
try {
content = fs.readFileSync(envFile, 'utf8');
} catch (e) {
return; // File absent or unreadable — not an error.
}
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx < 0) continue;
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
if (!(key in process.env)) process.env[key] = value;
}
}
async function main() {
const parser = new argparse.ArgumentParser({
formatter_class: argparse.RawDescriptionHelpFormatter,
epilog: `
Env-var overrides:
Any flag can be set via a PERFETTO_UI_<FLAG> environment variable,
where <FLAG> is the flag name uppercased with hyphens replaced by
underscores. Boolean flags are activated by "1" or "true". CLI flags
always take precedence over environment variables.
Examples:
PERFETTO_UI_OUT=/tmp/perfetto-out
PERFETTO_UI_NO_BUILD=1
PERFETTO_UI_SERVE_HOST=0.0.0.0
PERFETTO_UI_TITLE=my-instance
Defaults can also be persisted in:
~/.config/perfetto/ui-dev-server.env
(one KEY=VALUE per line, # comments supported). Shell env vars take
precedence over the file.
`,
});
parser.add_argument('--out', {help: 'Output directory'});
parser.add_argument('--no-build', '-n', {
action: 'store_true',
help: 'Skip prebuild (wasm/protos/assets) — useful for fast iteration',
});
parser.add_argument('--no-wasm', '-W', {
action: 'store_true',
help: 'Skip the ninja wasm build (assumes outputs already exist)',
});
parser.add_argument('--no-depscheck', {
action: 'store_true',
help: 'Skip install-build-deps check',
});
parser.add_argument('--no-override-gn-args', {
action: 'store_true',
help: "Don't auto-set gn args (preserves manual configuration)",
});
parser.add_argument('--debug', '-d', {
action: 'store_true',
help: 'Debug wasm build (is_debug=true, copies .wasm.map files)',
});
parser.add_argument('--only-wasm-memory64', {
action: 'store_true',
help: 'Skip the non-memory64 trace_processor wasm build',
});
parser.add_argument('--minify-js', {
choices: ['preserve_comments', 'all'],
help: 'Minify JS bundles',
});
parser.add_argument('--no-source-maps', {
action: 'store_true',
help: 'Skip source map generation',
});
parser.add_argument('--no-treeshake', {
action: 'store_true',
help: 'Disable rollup tree-shaking (faster incremental rebuilds)',
});
parser.add_argument('--cross-origin-isolation', {
action: 'store_true',
help: 'Send COOP/COEP headers (needed for SharedArrayBuffer)',
});
parser.add_argument('--serve-host', {help: 'dev/preview bind host'});
parser.add_argument('--serve-port', {
type: 'int',
help: 'dev/preview bind port',
});
parser.add_argument('--title', {help: 'Override <title> tag'});
parser.add_argument('--open-perfetto-trace', {
action: 'store_true',
help: 'Also build the open_perfetto_trace bundle',
});
parser.add_argument('--test-filter', '-f', {
help: "filter tests by pattern, e.g. 'chrome_render'",
});
parser.add_argument('--interactive', '-i', {
action: 'store_true',
help: 'Run playwright tests in interactive mode',
});
parser.add_argument('--rebaseline', '-r', {
action: 'store_true',
help: 'Rebaseline screenshot tests',
});
const sub = parser.add_subparsers({dest: 'command'});
sub.add_parser('pre', {help: 'Run pre-build steps and exit'});
sub.add_parser('build', {help: 'Build for production'});
sub.add_parser('dev', {help: 'Start the dev server'});
sub.add_parser('preview', {help: 'Preview production build'});
sub.add_parser('test', {help: 'Run unit tests'});
sub.add_parser('typecheck', {help: 'Run type checking only'});
loadDevServerEnvFile();
const envPrefix = 'PERFETTO_UI_';
const syntheticArgv = [];
for (const [key, val] of Object.entries(process.env)) {
if (!key.startsWith(envPrefix)) continue;
const flag =
'--' + key.slice(envPrefix.length).toLowerCase().replace(/_/g, '-');
const action = parser._actions.find((a) =>
(a.option_strings || []).includes(flag),
);
if (!action) continue;
const isBoolFlag = action.nargs === 0;
if (isBoolFlag) {
if (val === '1' || val.toLowerCase() === 'true') syntheticArgv.push(flag);
} else {
syntheticArgv.push(`${flag}=${val}`);
}
}
const args = parser.parse_args([...syntheticArgv, ...process.argv.slice(2)]);
const outRootDir = path.resolve(args.out || pjoin(ROOT_DIR, 'out/ui'));
const outDir = ensureDir(pjoin(outRootDir, 'ui'));
// Plumb bundler-shaping flags through to vite.config.mjs via env. Set them
// unconditionally so they cover both `vite build` and the in-process dev
// server (which reads env at config import time).
if (args.no_source_maps) process.env.NO_SOURCE_MAPS = 'true';
if (args.no_treeshake) process.env.NO_TREESHAKE = 'true';
if (args.minify_js) process.env.MINIFY_JS = args.minify_js;
if (args.only_wasm_memory64) process.env.IS_MEMORY64_ONLY = 'true';
if (args.open_perfetto_trace) process.env.ENABLE_OPEN_PERFETTO_TRACE = 'true';
if (args.title) process.env.PERFETTO_DEV_TITLE_OVERRIDE = args.title;
// Test-runner shaping: pickup-by-env-var convention preserved from main.
if (args.interactive) process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
if (args.rebaseline) process.env.PERFETTO_UI_TESTS_REBASELINE = '1';
const prebuildOpts = {
rootDir: ROOT_DIR,
outDir,
version: VERSION,
debug: args.debug,
skipWasm: args.no_wasm,
skipDepscheck: args.no_depscheck,
noOverrideGnArgs: args.no_override_gn_args,
onlyWasmMemory64: args.only_wasm_memory64,
titleOverride: args.title || '',
};
const serverHost = args.serve_host || '127.0.0.1';
const serverPort = args.serve_port;
const portWasExplicit = serverPort !== undefined;
// Bundles to feed `vite build`. Open-perfetto-trace is opt-in; bigtrace is
// intentionally not wired in this version of the script.
const bundlesForProd = [...ALL_BUNDLES];
if (args.open_perfetto_trace) bundlesForProd.push(OPEN_PERFETTO_TRACE_BUNDLE);
// Lock policy: anything that needs outDir to stay stable acquires the
// build lock for its lifetime.
// pre/build/test: acquire iff they're going to wipe (i.e. !--no-build).
// dev/preview: always acquire — they hold outDir live, --no-build or not.
// typecheck: doesn't touch outDir/dist, no lock.
const wipingCommands = ['pre', 'build', 'test'];
const serverCommands = ['dev', 'preview'];
if (
(wipingCommands.includes(args.command) && !args.no_build) ||
serverCommands.includes(args.command)
) {
acquireBuildLock({outDir});
} else if (args.no_build && wipingCommands.includes(args.command)) {
warnIfBuildLockHeld({outDir});
}
switch (args.command) {
case 'pre':
await prebuild(prebuildOpts);
break;
case 'build':
await prebuild(prebuildOpts);
await viteBuild({rootDir: ROOT_DIR, bundles: bundlesForProd});
await runInProcStep('service worker manifest', () =>
genServiceWorkerManifestJson({
distDir: pjoin(outDir, 'dist', VERSION),
}),
);
break;
case 'dev':
if (!args.no_build) {
await prebuild(prebuildOpts);
// Worker bundles are loaded via `new Worker(url)` at runtime, so they
// need to exist as real files in dist/v<version>/ in dev too.
await viteBuild({rootDir: ROOT_DIR, bundles: WORKER_BUNDLES});
}
await viteDev({
rootDir: ROOT_DIR,
outDir,
version: VERSION,
host: serverHost,
port: serverPort ?? DEFAULT_PORT,
crossOriginIsolation: args.cross_origin_isolation,
});
break;
case 'preview':
startStaticServer({
rootDir: ROOT_DIR,
distRootDir: pjoin(outDir, 'dist'),
host: serverHost,
port: serverPort,
defaultPort: DEFAULT_PORT,
portWasExplicit,
crossOriginIsolation: args.cross_origin_isolation,
});
break;
case 'test':
if (!args.no_build) await prebuild(prebuildOpts);
await runUnitTests({testFilter: args.test_filter});
break;
case 'typecheck': {
// typecheck only touches tsc/gen, not outDir/dist — safe to coexist
// with a running dev/preview, so no lock needed.
const genDir = ensureDir(pjoin(outDir, 'tsc/gen'));
await compileProtos({rootDir: ROOT_DIR, genDir});
await runStep(
'tsc --noEmit',
pjoin(ROOT_DIR, 'ui/node_modules/.bin/tsc'),
['--noEmit'],
{cwd: pjoin(ROOT_DIR, 'ui')},
);
break;
}
default:
throw new Error(`Unknown command: ${args.command}`);
}
}
async function runUnitTests({testFilter} = {}) {
const prevCwd = process.cwd();
process.chdir(pjoin(ROOT_DIR, 'ui'));
try {
await runInProcStep('vitest', async () => {
const cliFilters = testFilter ? [testFilter] : [];
const vitest = await startVitest('test', cliFilters, {
config: pjoin(ROOT_DIR, 'ui/vitest.config.mjs'),
watch: false,
});
const failed = vitest?.state.getCountOfFailedTests() ?? 0;
await vitest?.close();
if (failed > 0) throw new Error(`${failed} test(s) failed`);
});
} catch (e) {
process.chdir(prevCwd);
process.exit(1);
}
process.chdir(prevCwd);
}
main();