secure_exec_build_support/
v8_bridge_build.rs1use std::env;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7const ENV_NODE: &str = "SECURE_EXEC_NODE";
8const LEGACY_ENV_NODE: &str = "AGENTOS_NODE";
9const ENV_BUILD_SCRIPT: &str = "SECURE_EXEC_V8_BRIDGE_BUILD_SCRIPT";
10const LEGACY_ENV_BUILD_SCRIPT: &str = "AGENTOS_V8_BRIDGE_BUILD_SCRIPT";
11const ENV_DEBUG: &str = "SECURE_EXEC_GENERATED_ASSET_DEBUG";
12const LEGACY_ENV_DEBUG: &str = "AGENTOS_GENERATED_ASSET_DEBUG";
13const DEFAULT_BUILD_SCRIPTS: &[&str] = &[
14 "packages/build-tools/scripts/build-v8-bridge.mjs",
15 "packages/secure-exec-core/scripts/build-v8-bridge.mjs",
16 "packages/core/scripts/build-v8-bridge.mjs",
17];
18const BUILD_SCRIPT_CANDIDATES: &str = "packages/build-tools/scripts/build-v8-bridge.mjs, packages/secure-exec-core/scripts/build-v8-bridge.mjs, or packages/core/scripts/build-v8-bridge.mjs";
19
20pub fn build_v8_bridge(crate_manifest_dir: &Path, out_dir: &Path) {
21 let bridge_output = out_dir.join("v8-bridge.js");
22 let zlib_output = out_dir.join("v8-bridge-zlib.js");
23
24 println!("cargo:rerun-if-env-changed={ENV_NODE}");
25 println!("cargo:rerun-if-env-changed={LEGACY_ENV_NODE}");
26 println!("cargo:rerun-if-env-changed={ENV_BUILD_SCRIPT}");
27 println!("cargo:rerun-if-env-changed={LEGACY_ENV_BUILD_SCRIPT}");
28 println!("cargo:rerun-if-env-changed={ENV_DEBUG}");
29 println!("cargo:rerun-if-env-changed={LEGACY_ENV_DEBUG}");
30
31 if let Some(repo_root) = monorepo_root(crate_manifest_dir) {
32 build_from_monorepo(&repo_root, out_dir);
33 } else {
34 copy_vendored_bundle(crate_manifest_dir, &bridge_output, &zlib_output);
35 }
36
37 if !bridge_output.exists() || !zlib_output.exists() {
38 panic!(
39 "V8 bridge build completed but expected outputs are missing: {}, {}",
40 bridge_output.display(),
41 zlib_output.display()
42 );
43 }
44}
45
46fn monorepo_root(crate_manifest_dir: &Path) -> Option<PathBuf> {
50 if env_var_os(ENV_BUILD_SCRIPT, LEGACY_ENV_BUILD_SCRIPT).is_some() {
51 return crate_manifest_dir
53 .parent()
54 .and_then(Path::parent)
55 .map(Path::to_path_buf);
56 }
57
58 let repo_root = crate_manifest_dir.parent().and_then(Path::parent)?;
59 if DEFAULT_BUILD_SCRIPTS
60 .iter()
61 .any(|script| repo_root.join(script).exists())
62 {
63 Some(repo_root.to_path_buf())
64 } else {
65 None
66 }
67}
68
69fn copy_vendored_bundle(crate_manifest_dir: &Path, bridge_output: &Path, zlib_output: &Path) {
70 let vendored_dir = crate_manifest_dir.join("assets/generated");
71 let vendored_bridge = vendored_dir.join("v8-bridge.js");
72 let vendored_zlib = vendored_dir.join("v8-bridge-zlib.js");
73
74 println!("cargo:rerun-if-changed={}", vendored_bridge.display());
75 println!("cargo:rerun-if-changed={}", vendored_zlib.display());
76
77 if !vendored_bridge.exists() || !vendored_zlib.exists() {
78 panic!(
79 "the V8 bridge build toolchain ({BUILD_SCRIPT_CANDIDATES}) was not \
80 found and no vendored bundle exists at {}. Published crates must ship the prebuilt \
81 bundle; run the release tooling to stage it.",
82 vendored_dir.display()
83 );
84 }
85
86 fs::copy(&vendored_bridge, bridge_output).unwrap_or_else(|error| {
87 panic!(
88 "failed to copy vendored V8 bridge bundle from {} to {}: {}",
89 vendored_bridge.display(),
90 bridge_output.display(),
91 error
92 )
93 });
94 fs::copy(&vendored_zlib, zlib_output).unwrap_or_else(|error| {
95 panic!(
96 "failed to copy vendored V8 bridge zlib bundle from {} to {}: {}",
97 vendored_zlib.display(),
98 zlib_output.display(),
99 error
100 )
101 });
102}
103
104fn build_from_monorepo(repo_root: &Path, out_dir: &Path) {
105 let script_path = resolve_build_script(repo_root);
106 let package_root = script_path
107 .parent()
108 .and_then(Path::parent)
109 .unwrap_or_else(|| {
110 panic!(
111 "failed to resolve package root from V8 bridge build script path {}",
112 script_path.display()
113 )
114 });
115 let node_modules = package_root.join("node_modules");
116 let node = env_var_os(ENV_NODE, LEGACY_ENV_NODE).unwrap_or_else(|| "node".into());
117 let node_path = PathBuf::from(node);
118 let debug = env_var_os(ENV_DEBUG, LEGACY_ENV_DEBUG).is_some();
119
120 emit_rerun_inputs(repo_root, &script_path, package_root);
121
122 if !node_modules.exists() {
123 panic!(
124 "missing Node dependencies at {}. Run `pnpm install` from {} before building V8 bridge assets.",
125 node_modules.display(),
126 repo_root.display()
127 );
128 }
129
130 require_pnpm(repo_root, debug);
131
132 if debug {
133 println!(
134 "cargo:warning=building V8 bridge with node={} script={} out_dir={}",
135 node_path.display(),
136 script_path.display(),
137 out_dir.display()
138 );
139 }
140
141 let output = Command::new(&node_path)
142 .arg(&script_path)
143 .arg("--out-dir")
144 .arg(out_dir)
145 .current_dir(repo_root)
146 .output()
147 .unwrap_or_else(|error| match error.kind() {
148 io::ErrorKind::NotFound => panic!(
149 "failed to build V8 bridge assets because `{}` was not found. Install Node.js or set {ENV_NODE} to the Node binary.",
150 node_path.display()
151 ),
152 _ => panic!(
153 "failed to spawn V8 bridge build with `{}`: {}",
154 node_path.display(),
155 error
156 ),
157 });
158
159 if !output.status.success() {
160 let stdout = String::from_utf8_lossy(&output.stdout);
161 let stderr = String::from_utf8_lossy(&output.stderr);
162 let dependency_hint = if stderr.contains("ERR_MODULE_NOT_FOUND")
163 || stderr.contains("Cannot find package")
164 || stderr.contains("Cannot find module")
165 {
166 "\nNode dependencies appear to be missing or incomplete. Run `pnpm install` from the repo root."
167 } else {
168 ""
169 };
170
171 panic!(
172 "failed to build V8 bridge assets with `{}` (status: {}).{}\nstdout:\n{}\nstderr:\n{}",
173 node_path.display(),
174 output.status,
175 dependency_hint,
176 stdout.trim(),
177 stderr.trim()
178 );
179 }
180}
181
182fn resolve_build_script(repo_root: &Path) -> PathBuf {
183 match env_var_os(ENV_BUILD_SCRIPT, LEGACY_ENV_BUILD_SCRIPT) {
184 Some(path) => {
185 let path = PathBuf::from(path);
186 if path.is_absolute() {
187 path
188 } else {
189 repo_root.join(path)
190 }
191 }
192 None => DEFAULT_BUILD_SCRIPTS
193 .iter()
194 .map(|script| repo_root.join(script))
195 .find(|script| script.exists())
196 .unwrap_or_else(|| repo_root.join(DEFAULT_BUILD_SCRIPTS[0])),
197 }
198}
199
200fn env_var_os(primary: &str, legacy: &str) -> Option<std::ffi::OsString> {
201 env::var_os(primary).or_else(|| env::var_os(legacy))
202}
203
204fn require_pnpm(repo_root: &Path, debug: bool) {
205 let output = Command::new("pnpm")
206 .arg("--version")
207 .current_dir(repo_root)
208 .output()
209 .unwrap_or_else(|error| match error.kind() {
210 io::ErrorKind::NotFound => {
211 panic!(
212 "failed to build V8 bridge assets because `pnpm` was not found. Install pnpm and run `pnpm install` from {}.",
213 repo_root.display()
214 )
215 }
216 _ => panic!("failed to check pnpm availability: {}", error),
217 });
218
219 if !output.status.success() {
220 panic!(
221 "failed to build V8 bridge assets because `pnpm --version` failed with status {}. Run `pnpm install` from {} after fixing pnpm.",
222 output.status,
223 repo_root.display()
224 );
225 }
226
227 if debug {
228 println!(
229 "cargo:warning=pnpm version {}",
230 String::from_utf8_lossy(&output.stdout).trim()
231 );
232 }
233}
234
235fn emit_rerun_inputs(repo_root: &Path, script_path: &Path, package_root: &Path) {
236 let inputs = [
237 repo_root.join("crates/build-support/v8_bridge_build.rs"),
238 script_path.to_path_buf(),
239 package_root.join("package.json"),
240 repo_root.join("pnpm-lock.yaml"),
241 ];
242
243 for input in inputs {
244 println!("cargo:rerun-if-changed={}", input.display());
245 }
246
247 let bridge_src_dir = repo_root.join("packages/build-tools/bridge-src");
248 emit_rerun_dir(&bridge_src_dir).unwrap_or_else(|error| {
249 panic!(
250 "failed to enumerate V8 bridge source inputs under {}: {}",
251 bridge_src_dir.display(),
252 error
253 )
254 });
255
256 let shim_dir = repo_root.join("crates/execution/assets/undici-shims");
257 emit_rerun_dir(&shim_dir).unwrap_or_else(|error| {
258 panic!(
259 "failed to enumerate V8 bridge shim inputs under {}: {}",
260 shim_dir.display(),
261 error
262 )
263 });
264}
265
266fn emit_rerun_dir(dir: &Path) -> io::Result<()> {
267 let mut entries = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?;
268 entries.sort_by_key(|entry| entry.path());
269
270 for entry in entries {
271 let path = entry.path();
272 let file_type = entry.file_type()?;
273 if file_type.is_dir() {
274 emit_rerun_dir(&path)?;
275 } else {
276 println!("cargo:rerun-if-changed={}", path.display());
277 }
278 }
279
280 Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285 use super::emit_rerun_dir;
286 use std::fs;
287 use std::io;
288 use std::path::PathBuf;
289
290 fn temp_test_dir(name: &str) -> io::Result<PathBuf> {
291 let mut path = std::env::temp_dir();
292 path.push(format!(
293 "secure-exec-v8-bridge-build-{name}-{}",
294 std::process::id()
295 ));
296 let _ = fs::remove_dir_all(&path);
297 fs::create_dir(&path)?;
298 Ok(path)
299 }
300
301 #[cfg(unix)]
302 #[test]
303 fn emit_rerun_dir_does_not_follow_directory_symlinks() -> io::Result<()> {
304 let dir = temp_test_dir("symlink-cycle")?;
305 fs::write(dir.join("shim.js"), b"export {};")?;
306 std::os::unix::fs::symlink(&dir, dir.join("self"))?;
307
308 let result = emit_rerun_dir(&dir);
309 let cleanup = fs::remove_dir_all(&dir);
310
311 result?;
312 cleanup?;
313 Ok(())
314 }
315}