blob: 110bdb25d1c86627290d91d68705268284468176 [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.
// Wasm build steps invoked from build.mjs. All functions are synchronous and
// exit the process on failure.
// - buildWasm(): runs gn+ninja for trace_processor / traceconv
// / proto_utils Wasm modules and stages their
// .wasm/.js/.d.ts outputs.
// - copySyntaqliteRuntime(): copies the prebuilt syntaqlite runtime out of
// node_modules and compiles our SQL dialect
// side module via emcc.
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const pjoin = path.join;
function defaultRun(_label, cmd, args, opts = {}) {
const res = spawnSync(cmd, args, {stdio: 'inherit', ...opts});
if (res.status !== 0) {
console.error(`${cmd} ${args.join(' ')} failed (status=${res.status})`);
process.exit(res.status ?? 1);
}
}
function ensureDir(p) {
fs.mkdirSync(p, {recursive: true});
return p;
}
function cp(src, dst) {
ensureDir(path.dirname(dst));
fs.copyFileSync(src, dst);
}
// |ninjaOutDir| is the root out/ dir that ninja targets (e.g. <repo>/out/ui).
// |distDir| versioned dist (where .wasm files end up).
// |genDir| intermediates dir (where .js/.d.ts wasm glue is staged).
// |wasmModules| list of ninja target basenames (no _wasm suffix).
// |run| (optional) is an async (label, cmd, args, opts) => void runner used
// for long-running subprocesses with their own output (gn, ninja, emcc).
// Defaults to a blocking spawnSync; build.mjs passes runStepStream for spicy
// terminal feedback.
export async function buildWasm({
rootDir,
ninjaOutDir,
distDir,
genDir,
wasmModules,
debug = false,
skipBuild = false,
noOverrideGnArgs = false,
run = defaultRun,
}) {
if (!skipBuild) {
if (!noOverrideGnArgs) {
let gnVars = `is_debug=${debug}`;
if (spawnSync('which', ['ccache']).status === 0) {
gnVars += ` cc_wrapper="ccache"`;
}
await run('gn gen (wasm)', pjoin(rootDir, 'tools/gn'), [
'gen',
`--args=${gnVars}`,
ninjaOutDir,
]);
}
const ninjaArgs = ['-C', ninjaOutDir];
ninjaArgs.push(...wasmModules.map((x) => `${x}_wasm`));
await run('ninja (wasm)', pjoin(rootDir, 'tools/ninja'), ninjaArgs);
}
for (const wasmMod of wasmModules) {
const isMem64 = wasmMod.endsWith('_memory64');
const wasmOutDir = pjoin(ninjaOutDir, isMem64 ? 'wasm_memory64' : 'wasm');
// The .wasm file goes directly into the dist dir (also .map in debug).
for (const ext of ['.wasm'].concat(debug ? ['.wasm.map'] : [])) {
cp(pjoin(wasmOutDir, `${wasmMod}${ext}`), pjoin(distDir, wasmMod + ext));
}
// The .js / .d.ts go into intermediates, picked up by the bundler.
for (const ext of ['.js', '.d.ts']) {
cp(pjoin(wasmOutDir, `${wasmMod}${ext}`), pjoin(genDir, `${wasmMod}${ext}`));
}
}
}
// |distRootDir| is the unversioned dist dir (where assets/ lives).
export async function copySyntaqliteRuntime({
rootDir,
distRootDir,
run = defaultRun,
}) {
const srcDir = pjoin(rootDir, 'ui/node_modules/syntaqlite/wasm');
const dstDir = pjoin(distRootDir, 'assets');
for (const fname of [
'syntaqlite-runtime.js',
'syntaqlite-runtime.wasm',
'syntaqlite-sqlite.wasm',
]) {
cp(pjoin(srcDir, fname), pjoin(dstDir, fname));
}
await buildSyntaqlitePerfettoDialect({rootDir, distRootDir, run});
}
function getBuildToolsBinDir(rootDir) {
const binDirName = {darwin: 'mac', linux: 'linux64'}[process.platform];
if (!binDirName) {
throw new Error(`Unsupported platform: ${process.platform}`);
}
return pjoin(rootDir, 'buildtools', binDirName);
}
async function buildSyntaqlitePerfettoDialect({rootDir, distRootDir, run}) {
const emcc = pjoin(getBuildToolsBinDir(rootDir), 'emsdk/emscripten/emcc');
const src = pjoin(
rootDir,
'src/trace_processor/perfetto_sql/syntaqlite/syntaqlite_perfetto.c',
);
const dst = pjoin(distRootDir, 'assets', 'syntaqlite-perfetto.wasm');
try {
if (fs.statSync(dst).mtimeMs >= fs.statSync(src).mtimeMs) return;
} catch (e) {
/* dst missing → rebuild */
}
ensureDir(path.dirname(dst));
const prevEmConfig = process.env.EM_CONFIG;
process.env.EM_CONFIG = pjoin(rootDir, 'gn/standalone/.emscripten');
try {
await run('emcc (syntaqlite dialect)', emcc, [
'-O2',
'-sSIDE_MODULE=2',
'-sEXPORTED_FUNCTIONS=_syntaqlite_perfetto_dialect_template',
'-o',
dst,
src,
]);
} finally {
if (prevEmConfig === undefined) delete process.env.EM_CONFIG;
else process.env.EM_CONFIG = prevEmConfig;
}
}