diff --git a/changelog.d/9243-ext-http-cgu-link-regression-test.md b/changelog.d/9243-ext-http-cgu-link-regression-test.md new file mode 100644 index 0000000000..3c2829af23 --- /dev/null +++ b/changelog.d/9243-ext-http-cgu-link-regression-test.md @@ -0,0 +1,13 @@ +Add an end-to-end regression test for #8907: a program that references a +`cgu.0`-only stdlib symbol (`new Blob([...])`) with no `node:http` import must +link and run. + +The bug (released v0.5.1220): #5831 leaked `external-http-client-pump` into +perry-stdlib's `full` feature, which declares `js_ext_http_*` symbols defined +only in `perry-ext-http`. That archive is linked per-program by the compile +driver, and only when the program imports `node:http` — so a full-stdlib link +without an http import left those references unresolved. The release packs +stdlib into one monolithic `cgu.0` object, so any reference that pulls that +member drags the http references in with it. Fixed by #5983 (v0.5.1239), which +drops the pump from `full`; this pins the end-to-end link that the manifest-level +guard in `issue_8587_prebuilt_stdlib_http_isolation` does not exercise. diff --git a/crates/perry/tests/issue_8907_ext_http_cgu_link.rs b/crates/perry/tests/issue_8907_ext_http_cgu_link.rs new file mode 100644 index 0000000000..afe7ea71f2 --- /dev/null +++ b/crates/perry/tests/issue_8907_ext_http_cgu_link.rs @@ -0,0 +1,77 @@ +//! Regression test for #8907. +//! +//! Reported as "any program importing node:http fails to link" on the released +//! 0.5.1220 macOS arm64 binary, with ~17 undefined `_js_ext_http_*` / +//! `_js_http_*` symbols referenced from `libperry_stdlib.a`. The node:http +//! framing is a red herring. The real trigger: prebuilt `libperry_stdlib.a` +//! packs a large slice of stdlib into one monolithic `cgu.0` object, and that +//! object *also* carries the unresolved `js_ext_http_*` references. Pulling the +//! member for any symbol it uniquely defines drags those references into the +//! link. Only a `node:http` import adds `libperry_ext_http.a` (which defines +//! them), so a program that touches `cgu.0` without importing `node:http` +//! links with the symbols unresolved. +//! +//! `new Blob(...)` is the minimal trigger: `js_blob_new` is defined only in +//! `cgu.0`. No http, no compiled package — one line reproduces it on 0.5.1220 +//! and links on a fixed compiler. The original report hit it through a compiled +//! `effect` package, which happens to reference a `cgu.0`-only symbol. +//! +//! A link failure makes `perry compile` exit non-zero, so asserting the compile +//! succeeds is the test; running it confirms the member linked coherently. + +use std::path::PathBuf; +use std::process::Command; + +/// Path to the `perry` compiler binary built for this integration-test crate. +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +/// Compiles a program that references a `cgu.0`-only stdlib symbol (`Blob`) +/// with no `node:http` import, and asserts it links and runs. Guards the +/// #8907 regression where full-stdlib carried unresolved `js_ext_http_*` +/// references unless `node:http` pulled `perry-ext-http` onto the link line. +#[test] +fn cgu0_program_links_without_http_ext_archive() { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + let entry = root.join("blob.ts"); + // Blob pulls js_blob_new, which lives only in the cgu.0 object that carries + // the ext-http references. No node:http import. + std::fs::write( + &entry, + "const b = new Blob([\"x\"]); console.log(b.size);\n", + ) + .expect("write blob.ts"); + let output = root.join("blob_bin"); + + let compile = Command::new(perry_bin()) + .current_dir(root) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "a Blob program with no node:http import failed to link — the #8907 \ + ext-http / cgu.0 coupling regressed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output).output().expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed at runtime\nstatus: {:?}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "1\n", + "Blob([\"x\"]).size is 1 byte" + ); +}