Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

366 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Build every Capacitor / Electrobun native plugin package under
* `eliza/packages/native/plugins/` whose `pkg.elizaos.platforms` allowlist
* matches the current build host (or omits an OS allowlist entirely).
*
* Designed to be invoked from any elizaOS-based fork:
* node eliza/packages/app-core/scripts/build-native-plugins.mjs
*
* Forks that previously used `pkg.eliza.platforms` should rename to
* `pkg.elizaos.platforms`, or wrap this script with a 1-line preprocessor
* that mirrors the field before invocation.
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import {
CAPACITOR_PLUGIN_NAMES,
NATIVE_PLUGINS_ROOT,
} from "./lib/capacitor-plugin-names.mjs";
const scriptFile = fileURLToPath(import.meta.url);
const verbosePluginBuild = process.env.ELIZA_VERBOSE_PLUGIN_BUILD === "1";
// Only these values in a plugin's `platforms` array are treated as build-host
// gates. Anything else (e.g. "node", "browser") is a runtime hint and does
// not block building on the current host.
export const OS_PLATFORMS = new Set(["darwin", "linux", "win32"]);
/**
* Decide whether a plugin should be built on the current host, based on the
* `elizaos.platforms` allowlist in its package.json, or by detecting Capacitor
* mobile plugins via their peer dependency.
*
* Rules (in order):
* 1. Explicit `platforms` pure-OS allowlist → build only when host is listed.
* 2. `platforms` mixing runtime hints (e.g. "node", "browser") → build everywhere.
* 3. No `platforms` but `@capacitor/core` peer dep → mobile-only, skip on desktop.
* 4. No signal → build everywhere.
*
* @param {unknown} pkg — parsed package.json (or undefined)
* @param {string} hostPlatform — the current `process.platform` value
* @returns {boolean}
*/
export function shouldBuildPluginForHost(pkg, hostPlatform) {
const platforms = pkg && typeof pkg === "object" && pkg.elizaos?.platforms;
if (Array.isArray(platforms) && platforms.length > 0) {
const isPureOsAllowlist = platforms.every((p) => OS_PLATFORMS.has(p));
if (!isPureOsAllowlist) {
return true;
}
return platforms.includes(hostPlatform);
}
// No explicit metadata — @capacitor/core peer dep is a reliable mobile-only
// signal (every proper Capacitor plugin lists it). Skip on all desktop hosts.
const peerDeps =
(pkg && typeof pkg === "object" && pkg.peerDependencies) ?? {};
if ("@capacitor/core" in peerDeps) {
return false;
}
return true;
}
const NATIVE_PLUGIN_DIR_PREFIX = "plugin-native-";
const WORKSPACE_RUNTIME_PACKAGES = new Map([
[
"@elizaos/core",
path.resolve(NATIVE_PLUGINS_ROOT, "..", "packages", "core"),
],
]);
function pluginDirFor(pluginsDir, name) {
return path.join(pluginsDir, `${NATIVE_PLUGIN_DIR_PREFIX}${name}`);
}
function readPluginPackageJson(pluginsDir, name) {
const pkgPath = path.join(pluginDirFor(pluginsDir, name), "package.json");
const raw = fs.readFileSync(pkgPath, "utf8");
try {
return JSON.parse(raw);
} catch (error) {
throw new Error(
`[plugins] ${pkgPath} is not valid JSON: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
function run(command, args, cwd) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: "inherit",
env: process.env,
});
child.on("error", (error) => {
reject(new Error(`${command} failed to start: ${error.message}`));
});
child.on("exit", (code, signal) => {
if (signal) {
reject(new Error(`${command} exited due to signal ${signal}`));
return;
}
if ((code ?? 1) !== 0) {
reject(new Error(`${command} exited with code ${code ?? 1}`));
return;
}
resolve();
});
});
}
function logVerbose(message) {
if (verbosePluginBuild) {
console.log(message);
}
}
// Force a full rebuild regardless of on-disk freshness (escape hatch for the
// rare case where output is stale but its mtime says otherwise).
const forcePluginBuild = process.env.ELIZA_FORCE_PLUGIN_BUILD === "1";
// Files outside `src/` that, when changed, invalidate a package's build.
const BUILD_INPUT_FILES = [
"package.json",
"tsconfig.json",
"tsconfig.build.json",
"rollup.config.mjs",
"build.ts",
];
// Directories under `src/` that hold build-generated artifacts, not authored
// inputs. Some packages (notably @elizaos/core) emit generated TS and `.d.ts`
// back into `src/` during their build, so these must be excluded when deciding
// whether a package's source has changed.
const BUILD_INPUT_IGNORED_DIRS = new Set([
"node_modules",
"dist",
".turbo",
".cache",
"generated",
]);
function isDeclarationFile(name) {
return name.endsWith(".d.ts") || name.endsWith(".d.ts.map");
}
/**
* Largest file mtime (ms) under a path, walking directories iteratively.
* Symlinks are skipped to avoid cycles. Returns 0 when nothing is found.
*
* @param {string} target
* @param {object} [options]
* @param {Set<string>} [options.ignoreDirs] directory basenames to skip
* @param {boolean} [options.skipDeclarations] ignore emitted `.d.ts(.map)` files
* @returns {number}
*/
function newestMtimeMs(target, options = {}) {
const { ignoreDirs, skipDeclarations = false } = options;
let newest = 0;
const stack = [target];
while (stack.length > 0) {
const current = stack.pop();
let stat;
try {
stat = fs.lstatSync(current);
} catch {
continue;
}
if (stat.isSymbolicLink()) {
continue;
}
if (stat.isDirectory()) {
if (ignoreDirs?.has(path.basename(current))) {
continue;
}
let entries;
try {
entries = fs.readdirSync(current);
} catch {
continue;
}
for (const entry of entries) {
stack.push(path.join(current, entry));
}
} else if (skipDeclarations && isDeclarationFile(path.basename(current))) {
// emitted declaration file — a build output, not an authored input
} else if (stat.mtimeMs > newest) {
newest = stat.mtimeMs;
}
}
return newest;
}
/**
* A package build is "fresh" when its `dist/` output exists and every output
* file is at least as new as the newest build input (`src/` plus the config
* files in {@link BUILD_INPUT_FILES}). Lets warm dev boots skip rebuilding
* unchanged packages — the single largest boot-time cost.
*
* @param {string} packageDir absolute path to the package root
* @returns {boolean}
*/
function isPackageBuildFresh(packageDir) {
if (forcePluginBuild) {
return false;
}
const distDir = path.join(packageDir, "dist");
let distEntries;
try {
distEntries = fs.readdirSync(distDir);
} catch {
return false;
}
if (distEntries.length === 0) {
return false;
}
const outputNewest = newestMtimeMs(distDir);
if (outputNewest === 0) {
return false;
}
let inputNewest = 0;
const srcDir = path.join(packageDir, "src");
if (fs.existsSync(srcDir)) {
inputNewest = newestMtimeMs(srcDir, {
ignoreDirs: BUILD_INPUT_IGNORED_DIRS,
skipDeclarations: true,
});
}
for (const file of BUILD_INPUT_FILES) {
try {
const stat = fs.statSync(path.join(packageDir, file));
if (stat.mtimeMs > inputNewest) {
inputNewest = stat.mtimeMs;
}
} catch {
// input file absent — not part of this package's build
}
}
// No recognizable inputs — build to stay safe.
if (inputNewest === 0) {
return false;
}
return outputNewest >= inputNewest;
}
function hasPackageDependency(pkg, packageName) {
if (!pkg || typeof pkg !== "object") return false;
for (const field of [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
]) {
const deps = pkg[field];
if (deps && typeof deps === "object" && packageName in deps) {
return true;
}
}
return false;
}
async function buildWorkspaceRuntimePackagesForPlugins(pluginEntries) {
const requiredPackages = new Set();
for (const { pkg } of pluginEntries) {
for (const packageName of WORKSPACE_RUNTIME_PACKAGES.keys()) {
if (hasPackageDependency(pkg, packageName)) {
requiredPackages.add(packageName);
}
}
}
for (const packageName of requiredPackages) {
const packageDir = WORKSPACE_RUNTIME_PACKAGES.get(packageName);
const packageJsonPath = path.join(packageDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
throw new Error(
`[plugins] ${packageName} dependency is required but ${packageJsonPath} does not exist`,
);
}
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
if (!pkg?.scripts?.build) {
throw new Error(
`[plugins] ${packageName} dependency is required but has no build script`,
);
}
if (isPackageBuildFresh(packageDir)) {
console.log(
`[plugins] workspace dependency ${packageName} up to date — skipping`,
);
continue;
}
console.log(`[plugins] building workspace dependency ${packageName}`);
await run("bun", ["run", "build"], packageDir);
}
}
async function main() {
const pluginsDir = NATIVE_PLUGINS_ROOT;
const pluginNames = CAPACITOR_PLUGIN_NAMES;
const skipPlugins =
process.env.SKIP_NATIVE_PLUGINS === "1" || process.env.CI === "true";
if (skipPlugins) {
console.log(
"[plugins] skipping native plugin builds (CI or explicitly disabled)",
);
return;
}
const buildablePlugins = pluginNames
.map((name) => ({ name, pkg: readPluginPackageJson(pluginsDir, name) }))
.filter(({ name, pkg }) => {
// Type-only / source-consumed packages (e.g. shared-types) have no build
// script. Skip them so `bun run build` does not abort the whole batch.
if (!pkg?.scripts?.build) {
logVerbose(`[plugin:${name}] skipping — no build script declared`);
return false;
}
if (shouldBuildPluginForHost(pkg, process.platform)) {
return true;
}
const platforms = pkg?.elizaos?.platforms;
logVerbose(
`[plugin:${name}] skipping — declares platforms=${JSON.stringify(
platforms,
)}, host is ${process.platform}`,
);
return false;
});
await buildWorkspaceRuntimePackagesForPlugins(buildablePlugins);
let builtCount = 0;
let freshCount = 0;
await Promise.all(
buildablePlugins.map(async ({ name }) => {
const pluginDir = pluginDirFor(pluginsDir, name);
if (isPackageBuildFresh(pluginDir)) {
freshCount += 1;
logVerbose(`[plugin:${name}] up to date — skipping`);
return;
}
builtCount += 1;
logVerbose(`[plugin:${name}] building...`);
await run("bun", ["run", "build"], pluginDir);
logVerbose(`[plugin:${name}] done`);
}),
);
console.log(
`[plugins] native plugins: built ${builtCount}, skipped ${freshCount} (up to date)`,
);
}
const isDirectRun =
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptFile);
if (isDirectRun) {
await main();
}