Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/cache@v4
with:
path: ~/.cargo/.rusty_v8
key: ${{ runner.os }}-rusty-v8-${{ hashFiles('Cargo.lock', 'crates/v8-runtime/Cargo.toml', 'crates/v8-runtime/build.rs') }}
key: ${{ runner.os }}-rusty-v8-${{ hashFiles('Cargo.lock', 'crates/v8-runtime/Cargo.toml', 'crates/v8-runtime/build.rs', 'crates/build-support/v8_bridge_build.rs', 'packages/core/scripts/build-v8-bridge.mjs') }}
restore-keys: |
${{ runner.os }}-rusty-v8-
- run: pnpm install --frozen-lockfile
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ scripts/ralph/.codex-last-msg-*
.claude/scheduled_tasks.lock

# Local caches and stray outputs
crates/execution/assets/v8-bridge.js
crates/execution/assets/v8-bridge-zlib.js
crates/execution/.agent-os-pyodide-cache/
crates/execution/async-out.txt
crates/sidecar/.tmp-sidecar-tests/
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,6 @@ pnpm lint # biome check
```

- **Always run tests and agent-os-heavy commands through `just test-bounded '<command>'`** so the whole process tree is constrained by systemd. This keeps test runners, sidecars, agent sessions, builds, and their subprocesses from overwhelming the host by capping them to 60% of logical CPUs and 60% of currently available memory with lower CPU/IO priority. Use `just test-bounded` for the default `pnpm test`, or pass an explicit shell string such as `just test-bounded 'pnpm --dir packages/core exec vitest run tests/pi-sdk-adapter.test.ts'`.
- CI and release automation must install the pnpm workspace with `--frozen-lockfile`, and any job that runs Rust tests against generated JS assets (for example `v8-bridge.js`) must run `pnpm build` before those cargo steps. Fork pull requests should run the same `pnpm test` command without `AGENTOS_E2E_NETWORK=1`.
- CI and release automation must install the pnpm workspace with `--frozen-lockfile` before Cargo builds that generate V8 bridge assets into `OUT_DIR`. Fork pull requests should run the same `pnpm test` command without `AGENTOS_E2E_NETWORK=1`.
- When changing V8 bridge registration or snapshot bootstrap code under `crates/v8-runtime/`, rebuild `agent-os-v8-runtime` before rerunning sidecar V8 integration tests. `cargo test -p agent-os-sidecar` can otherwise reuse stale embedded-runtime objects from `target/`.
- The `crates/v8-runtime` snapshot test (`snapshot::tests::snapshot_consolidated_tests`) currently has to run in isolation: use `cargo test -p agent-os-v8-runtime -- --test-threads=1` for the main suite and `cargo test -p agent-os-v8-runtime snapshot::tests::snapshot_consolidated_tests -- --exact --ignored` separately until the shared test binary teardown SIGSEGV is fixed.
193 changes: 193 additions & 0 deletions crates/build-support/v8_bridge_build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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 repo_root = crate_manifest_dir
.parent()
.and_then(Path::parent)
.unwrap_or_else(|| {
panic!(
"failed to resolve repo root from CARGO_MANIFEST_DIR={}",
crate_manifest_dir.display()
)
});
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);
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 !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()
);
}

let bridge_output = out_dir.join("v8-bridge.js");
let zlib_output = out_dir.join("v8-bridge-zlib.js");
if !bridge_output.exists() || !zlib_output.exists() {
panic!(
"V8 bridge build completed but expected outputs are missing: {}, {}",
bridge_output.display(),
zlib_output.display()
);
}
}

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"),
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(())
}
Loading
Loading