426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
265 lines
8.5 KiB
JavaScript
265 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
||
// One-shot Cuttlefish simulator runner.
|
||
//
|
||
// Brand-aware: product name + lunch target come from the brand config
|
||
// (--brand-config / $DISTRO_ANDROID_BRAND_CONFIG). Override with
|
||
// --product / --variant if needed.
|
||
//
|
||
// Sequences the post-build steps so that once `m` finishes, a single
|
||
// command brings up the simulator and validates it end-to-end:
|
||
//
|
||
// 1. Confirm system.img exists at the expected target-product path.
|
||
// 2. Stop any prior cvd instance (so we always boot from a clean state).
|
||
// 3. Source build/envsetup.sh + lunch (so $ANDROID_HOST_OUT is set).
|
||
// 4. cvd start --daemon to launch Cuttlefish.
|
||
// 5. Wait for adb to see the device (sys.boot_completed=1).
|
||
// 6. Run boot-validate against the booted device.
|
||
// 7. Capture screenshots of HOME / Dialer / SMS / Assistant / launcher.
|
||
// 8. Optionally stop cvd at the end (--stop-after).
|
||
//
|
||
// Usage:
|
||
// node packages/scripts/distro-android/sim.mjs
|
||
// node packages/scripts/distro-android/sim.mjs --aosp-root ~/aosp --out reports/aosp-sim
|
||
// node packages/scripts/distro-android/sim.mjs --no-launch # validate against an already-running cvd
|
||
// node packages/scripts/distro-android/sim.mjs --stop-after # tear down cvd after validation
|
||
// node packages/scripts/distro-android/sim.mjs --wait-for-build # poll until system.img exists
|
||
//
|
||
// Flags:
|
||
// --brand-config <PATH> Brand config JSON (default brand.eliza.json).
|
||
// --aosp-root <DIR> AOSP checkout root. Default $HOME/aosp.
|
||
// --product <NAME> Target product. Default brand.productName.
|
||
// --variant <STR> Target variant. Default trunk_staging-userdebug.
|
||
// --out <DIR> Screenshot + report output. Default reports/aosp-sim.
|
||
// --no-launch Don't start cvd; assume one is already running.
|
||
// --stop-after Stop cvd at the end (clean tear-down).
|
||
// --wait-for-build Poll until system.img appears (build still running).
|
||
// --boot-timeout-ms <N> Wait for sys.boot_completed (default 300000).
|
||
|
||
import { spawn, spawnSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import process from "node:process";
|
||
import { fileURLToPath } from "node:url";
|
||
import { loadBrandFromArgv } from "./brand-config.mjs";
|
||
|
||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||
const repoRoot = path.resolve(here, "../..");
|
||
|
||
const DEFAULT_AOSP_ROOT = path.join(os.homedir(), "aosp");
|
||
const DEFAULT_VARIANT = "trunk_staging-userdebug";
|
||
const DEFAULT_DEVICE_DIR = "vsoc_x86_64_only";
|
||
|
||
// The Cuttlefish product output lives at out/target/product/<deviceDir>, where
|
||
// deviceDir is the `vsoc_*` segment of the brand's AOSP device tree path (e.g.
|
||
// "device/google/cuttlefish/vsoc_riscv64/phone/aosp_cf.mk" -> "vsoc_riscv64").
|
||
// Deriving it from the brand keeps arm64/riscv64 from falling back to the
|
||
// x86_64 default and pointing at the wrong (or missing) system.img.
|
||
function deriveDeviceDir(brand) {
|
||
for (const treePath of brand.aospDeviceTreePaths ?? []) {
|
||
const segment = treePath.split("/").find((s) => s.startsWith("vsoc_"));
|
||
if (segment) return segment;
|
||
}
|
||
return DEFAULT_DEVICE_DIR;
|
||
}
|
||
|
||
export function parseSubArgs(argv, brand) {
|
||
const args = {
|
||
aospRoot: DEFAULT_AOSP_ROOT,
|
||
product: brand.productName,
|
||
variant: DEFAULT_VARIANT,
|
||
deviceDir: deriveDeviceDir(brand),
|
||
outDir: path.join(repoRoot, "reports", "aosp-sim"),
|
||
noLaunch: false,
|
||
stopAfter: false,
|
||
waitForBuild: false,
|
||
bootTimeoutMs: 300_000,
|
||
};
|
||
const readFlagValue = (flag, index) => {
|
||
const value = argv[index + 1];
|
||
if (!value || value.startsWith("--")) {
|
||
throw new Error(`${flag} requires a value`);
|
||
}
|
||
return value;
|
||
};
|
||
for (let i = 0; i < argv.length; i += 1) {
|
||
const arg = argv[i];
|
||
if (arg === "--aosp-root") {
|
||
args.aospRoot = path.resolve(readFlagValue(arg, i));
|
||
i += 1;
|
||
} else if (arg === "--product") {
|
||
args.product = readFlagValue(arg, i);
|
||
i += 1;
|
||
} else if (arg === "--variant") {
|
||
args.variant = readFlagValue(arg, i);
|
||
i += 1;
|
||
} else if (arg === "--device-dir") {
|
||
args.deviceDir = readFlagValue(arg, i);
|
||
i += 1;
|
||
} else if (arg === "--out") {
|
||
args.outDir = path.resolve(readFlagValue(arg, i));
|
||
i += 1;
|
||
} else if (arg === "--no-launch") {
|
||
args.noLaunch = true;
|
||
} else if (arg === "--stop-after") {
|
||
args.stopAfter = true;
|
||
} else if (arg === "--wait-for-build") {
|
||
args.waitForBuild = true;
|
||
} else if (arg === "--boot-timeout-ms") {
|
||
args.bootTimeoutMs = Number.parseInt(readFlagValue(arg, i), 10);
|
||
i += 1;
|
||
} else if (arg === "-h" || arg === "--help") {
|
||
console.log(
|
||
"Usage: node packages/scripts/distro-android/sim.mjs [--brand-config PATH] [--aosp-root DIR] [--out DIR] [--no-launch] [--stop-after] [--wait-for-build]",
|
||
);
|
||
process.exit(0);
|
||
} else {
|
||
throw new Error(`Unknown argument: ${arg}`);
|
||
}
|
||
}
|
||
return args;
|
||
}
|
||
|
||
function systemImgPath(args) {
|
||
return path.join(
|
||
args.aospRoot,
|
||
"out",
|
||
"target",
|
||
"product",
|
||
args.deviceDir,
|
||
"system.img",
|
||
);
|
||
}
|
||
|
||
function lunchTarget(args) {
|
||
return `${args.product}-${args.variant}`;
|
||
}
|
||
|
||
async function sleep(ms) {
|
||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function waitForSystemImage(args) {
|
||
const target = systemImgPath(args);
|
||
if (fs.existsSync(target)) {
|
||
console.log(`[sim] Found ${target}.`);
|
||
return target;
|
||
}
|
||
if (!args.waitForBuild) {
|
||
throw new Error(
|
||
`system.img not found at ${target}. Run \`m -j4\` first or pass --wait-for-build.`,
|
||
);
|
||
}
|
||
console.log(
|
||
`[sim] Waiting for ${target} (poll every 30s; AOSP build typically takes 1–4h)...`,
|
||
);
|
||
for (;;) {
|
||
if (fs.existsSync(target)) {
|
||
console.log(`[sim] ${target} appeared. Continuing.`);
|
||
return target;
|
||
}
|
||
await sleep(30_000);
|
||
}
|
||
}
|
||
|
||
function bashLogin(aospRoot, command) {
|
||
const result = spawnSync(
|
||
"bash",
|
||
["-lc", `source build/envsetup.sh >/dev/null && ${command}`],
|
||
{ cwd: aospRoot, stdio: "inherit" },
|
||
);
|
||
if (result.error) {
|
||
throw new Error(`bash failed: ${result.error.message}`);
|
||
}
|
||
if (result.status !== 0) {
|
||
throw new Error(`bash command exited with code ${result.status}`);
|
||
}
|
||
}
|
||
|
||
function bashLoginCapture(aospRoot, command) {
|
||
const result = spawnSync(
|
||
"bash",
|
||
["-lc", `source build/envsetup.sh >/dev/null && ${command}`],
|
||
{ cwd: aospRoot, encoding: "utf8" },
|
||
);
|
||
if (result.error) {
|
||
throw new Error(`bash failed: ${result.error.message}`);
|
||
}
|
||
return {
|
||
status: result.status ?? -1,
|
||
stdout: result.stdout,
|
||
stderr: result.stderr,
|
||
};
|
||
}
|
||
|
||
function startCuttlefish(args) {
|
||
console.log(`[sim] lunch ${lunchTarget(args)}`);
|
||
// Stop first to avoid "already running" guard rails. Best-effort —
|
||
// ignore failures since "no instance running" is also exit-non-zero.
|
||
bashLoginCapture(
|
||
args.aospRoot,
|
||
`lunch ${lunchTarget(args)} >/dev/null && (cvd stop --clean 2>/dev/null || stop_cvd 2>/dev/null || true)`,
|
||
);
|
||
console.log(`[sim] cvd start --daemon`);
|
||
bashLogin(
|
||
args.aospRoot,
|
||
`lunch ${lunchTarget(args)} >/dev/null && (cvd start --daemon || launch_cvd --daemon)`,
|
||
);
|
||
}
|
||
|
||
function stopCuttlefish(args) {
|
||
console.log(`[sim] cvd stop --clean`);
|
||
bashLoginCapture(
|
||
args.aospRoot,
|
||
`lunch ${lunchTarget(args)} >/dev/null 2>&1 && (cvd stop --clean 2>/dev/null || stop_cvd 2>/dev/null || true)`,
|
||
);
|
||
}
|
||
|
||
async function runE2eValidate(args, brand) {
|
||
// Spawn the existing e2e script — keeps the boot-validate + capture
|
||
// logic in one place rather than duplicating it here.
|
||
const child = spawn(
|
||
"node",
|
||
[
|
||
path.join(here, "e2e-validate.mjs"),
|
||
"--brand-config",
|
||
brand.brandConfigPath,
|
||
"--out",
|
||
args.outDir,
|
||
"--timeout-ms",
|
||
String(args.bootTimeoutMs),
|
||
],
|
||
{ stdio: "inherit" },
|
||
);
|
||
await new Promise((resolve, reject) => {
|
||
child.on("exit", (code) => {
|
||
if (code === 0) resolve();
|
||
else reject(new Error(`e2e-validate exited with code ${code}`));
|
||
});
|
||
});
|
||
}
|
||
|
||
export async function main(argv = process.argv.slice(2)) {
|
||
const { brand, remaining } = loadBrandFromArgv(argv);
|
||
const args = parseSubArgs(remaining, brand);
|
||
if (!args.noLaunch) {
|
||
await waitForSystemImage(args);
|
||
startCuttlefish(args);
|
||
}
|
||
try {
|
||
await runE2eValidate(args, brand);
|
||
} finally {
|
||
if (args.stopAfter) {
|
||
stopCuttlefish(args);
|
||
}
|
||
}
|
||
console.log(`[sim] Done. Reports: ${args.outDir}`);
|
||
}
|
||
|
||
const isMain =
|
||
process.argv[1] &&
|
||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||
if (isMain) {
|
||
await main();
|
||
}
|