Skip to main content

secure_exec_build_support/
v8_bridge_build.rs

1use 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
46/// Resolve the monorepo root when the in-tree build toolchain is available.
47/// Returns `None` when building the published crate so the caller falls back
48/// to the vendored prebuilt bundle.
49fn monorepo_root(crate_manifest_dir: &Path) -> Option<PathBuf> {
50    if env_var_os(ENV_BUILD_SCRIPT, LEGACY_ENV_BUILD_SCRIPT).is_some() {
51        // An explicit override always implies an in-tree build.
52        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}