use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
const ENV_NODE: &str = "AGENT_OS_NODE";
const ENV_BUILD_SCRIPT: &str = "AGENT_OS_V8_BRIDGE_BUILD_SCRIPT";
const ENV_DEBUG: &str = "AGENT_OS_GENERATED_ASSET_DEBUG";
pub fn build_v8_bridge(crate_manifest_dir: &Path, out_dir: &Path) {
let bridge_output = out_dir.join("v8-bridge.js");
let zlib_output = out_dir.join("v8-bridge-zlib.js");
println!("cargo:rerun-if-env-changed={ENV_NODE}");
println!("cargo:rerun-if-env-changed={ENV_BUILD_SCRIPT}");
println!("cargo:rerun-if-env-changed={ENV_DEBUG}");
if let Some(repo_root) = monorepo_root(crate_manifest_dir) {
build_from_monorepo(&repo_root, out_dir);
} else {
copy_vendored_bundle(crate_manifest_dir, &bridge_output, &zlib_output);
}
if !bridge_output.exists() || !zlib_output.exists() {
panic!(
"V8 bridge build completed but expected outputs are missing: {}, {}",
bridge_output.display(),
zlib_output.display()
);
}
}
fn monorepo_root(crate_manifest_dir: &Path) -> Option<PathBuf> {
if env::var_os(ENV_BUILD_SCRIPT).is_some() {
return crate_manifest_dir
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf);
}
let repo_root = crate_manifest_dir.parent().and_then(Path::parent)?;
if repo_root
.join("packages/core/scripts/build-v8-bridge.mjs")
.exists()
{
Some(repo_root.to_path_buf())
} else {
None
}
}
fn copy_vendored_bundle(crate_manifest_dir: &Path, bridge_output: &Path, zlib_output: &Path) {
let vendored_dir = crate_manifest_dir.join("assets/generated");
let vendored_bridge = vendored_dir.join("v8-bridge.js");
let vendored_zlib = vendored_dir.join("v8-bridge-zlib.js");
println!("cargo:rerun-if-changed={}", vendored_bridge.display());
println!("cargo:rerun-if-changed={}", vendored_zlib.display());
if !vendored_bridge.exists() || !vendored_zlib.exists() {
panic!(
"the V8 bridge build toolchain (packages/core/scripts/build-v8-bridge.mjs) was not \
found and no vendored bundle exists at {}. Published crates must ship the prebuilt \
bundle; run the release tooling to stage it.",
vendored_dir.display()
);
}
fs::copy(&vendored_bridge, bridge_output).unwrap_or_else(|error| {
panic!(
"failed to copy vendored V8 bridge bundle from {} to {}: {}",
vendored_bridge.display(),
bridge_output.display(),
error
)
});
fs::copy(&vendored_zlib, zlib_output).unwrap_or_else(|error| {
panic!(
"failed to copy vendored V8 bridge zlib bundle from {} to {}: {}",
vendored_zlib.display(),
zlib_output.display(),
error
)
});
}
fn build_from_monorepo(repo_root: &Path, out_dir: &Path) {
let script_path = resolve_build_script(repo_root);
let package_root = script_path
.parent()
.and_then(Path::parent)
.unwrap_or_else(|| {
panic!(
"failed to resolve package root from V8 bridge build script path {}",
script_path.display()
)
});
let node_modules = package_root.join("node_modules");
let node = env::var_os(ENV_NODE).unwrap_or_else(|| "node".into());
let node_path = PathBuf::from(node);
let debug = env::var_os(ENV_DEBUG).is_some();
emit_rerun_inputs(repo_root, &script_path);
if !node_modules.exists() {
panic!(
"missing Node dependencies at {}. Run `pnpm install` from {} before building V8 bridge assets.",
node_modules.display(),
repo_root.display()
);
}
require_pnpm(repo_root, debug);
if debug {
println!(
"cargo:warning=building V8 bridge with node={} script={} out_dir={}",
node_path.display(),
script_path.display(),
out_dir.display()
);
}
let output = Command::new(&node_path)
.arg(&script_path)
.arg("--out-dir")
.arg(out_dir)
.current_dir(repo_root)
.output()
.unwrap_or_else(|error| match error.kind() {
io::ErrorKind::NotFound => panic!(
"failed to build V8 bridge assets because `{}` was not found. Install Node.js or set {ENV_NODE} to the Node binary.",
node_path.display()
),
_ => panic!(
"failed to spawn V8 bridge build with `{}`: {}",
node_path.display(),
error
),
});
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let dependency_hint = if stderr.contains("ERR_MODULE_NOT_FOUND")
|| stderr.contains("Cannot find package")
|| stderr.contains("Cannot find module")
{
"\nNode dependencies appear to be missing or incomplete. Run `pnpm install` from the repo root."
} else {
""
};
panic!(
"failed to build V8 bridge assets with `{}` (status: {}).{}\nstdout:\n{}\nstderr:\n{}",
node_path.display(),
output.status,
dependency_hint,
stdout.trim(),
stderr.trim()
);
}
}
fn resolve_build_script(repo_root: &Path) -> PathBuf {
match env::var_os(ENV_BUILD_SCRIPT) {
Some(path) => {
let path = PathBuf::from(path);
if path.is_absolute() {
path
} else {
repo_root.join(path)
}
}
None => repo_root.join("packages/core/scripts/build-v8-bridge.mjs"),
}
}
fn require_pnpm(repo_root: &Path, debug: bool) {
let output = Command::new("pnpm")
.arg("--version")
.current_dir(repo_root)
.output()
.unwrap_or_else(|error| match error.kind() {
io::ErrorKind::NotFound => {
panic!(
"failed to build V8 bridge assets because `pnpm` was not found. Install pnpm and run `pnpm install` from {}.",
repo_root.display()
)
}
_ => panic!("failed to check pnpm availability: {}", error),
});
if !output.status.success() {
panic!(
"failed to build V8 bridge assets because `pnpm --version` failed with status {}. Run `pnpm install` from {} after fixing pnpm.",
output.status,
repo_root.display()
);
}
if debug {
println!(
"cargo:warning=pnpm version {}",
String::from_utf8_lossy(&output.stdout).trim()
);
}
}
fn emit_rerun_inputs(repo_root: &Path, script_path: &Path) {
let inputs = [
repo_root.join("crates/build-support/v8_bridge_build.rs"),
repo_root.join("crates/execution/build_support.rs"),
repo_root.join("crates/v8-runtime/build_support.rs"),
script_path.to_path_buf(),
repo_root.join("crates/execution/assets/v8-bridge.source.js"),
repo_root.join("packages/core/package.json"),
repo_root.join("pnpm-lock.yaml"),
];
for input in inputs {
println!("cargo:rerun-if-changed={}", input.display());
}
let shim_dir = repo_root.join("crates/execution/assets/undici-shims");
emit_rerun_dir(&shim_dir).unwrap_or_else(|error| {
panic!(
"failed to enumerate V8 bridge shim inputs under {}: {}",
shim_dir.display(),
error
)
});
}
fn emit_rerun_dir(dir: &Path) -> io::Result<()> {
let mut entries = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(|entry| entry.path());
for entry in entries {
let path = entry.path();
if path.is_dir() {
emit_rerun_dir(&path)?;
} else {
println!("cargo:rerun-if-changed={}", path.display());
}
}
Ok(())
}