Summary
Found while getting a real-world sst/opencode compile all the way to a working binary (companion investigation to #5918/#5919, #5922/#5924/#5927/#5923). After all import/namespace-resolution symbol issues were fixed, the final link still failed — first with ~5900, then (after #5920/#5921 landed on main, which helped) ~2150 duplicate-symbol errors from ld on macOS.
Root cause
opencode's dependency graph needs FIVE separate "well-known" native libraries simultaneously: http/https/http2 → perry-ext-http, fastify → perry-ext-fastify, ioredis → perry-ext-ioredis, net → perry-ext-net, ws → perry-ext-ws, each rebuilt "with shared tokio" per the well-known: routing log lines. Each of these is an independently-built Rust staticlib that bundles its OWN full copy of shared transitive dependencies (their exact codegen-unit object files, e.g. tokio-<hash>.tokio.<hash>-cgu.0.rcgu.o). libperry_stdlib.a ALSO bundles its own copies of the same crates (for its own fetch/https/websocket support). When multiple such libraries are linked together, every shared dependency they have in common — std, core, alloc, hashbrown, aho_corasick, regex_automata, tokio, hyper_util, h2, rustls, reqwest, ring, bytes, url, and perry's own perry_runtime codegen unit — ends up defined in more than one archive.
#5920/#5921 (already on main) added strip_bundled_runtime_from_well_known_lib, which safely drops duplicate perry_runtime-* member objects from a well-known lib when stdlib bundles the identical codegen unit (verified via two safety rules: Rule 1 — stdlib bundles the exact same codegen unit; Rule 2 — no sibling member in the same archive depends on a symbol only that copy provides). This fixed the perry_runtime duplicates specifically and reduced the failure from ~5900 to ~2150 duplicate symbols in opencode's case, but the SAME duplication shape recurs for every other shared crate above once TWO OR MORE well-known libraries both need a full HTTP-client stack (e.g. both http and fastify bundle tokio+hyper_util+h2+rustls+reqwest+ring).
Why there's no linker-flag escape hatch
Verified directly on this machine's toolchain (Xcode/CLT current as of 2026-07): neither -Wl,-multiply_defined,suppress nor -Wl,-ld_classic (with or without -multiply_defined) suppress the duplicate-symbol error anymore — both are reported as "obsolete"/"deprecated" and the link still fails. Unlike GNU ld/lld (which perry already uses -Wl,--allow-multiple-definition for on Android/HarmonyOS), Apple's current linker has no tolerance mode for this. The fix has to happen at the object/archive level before linking, mirroring what #5920/#5921 already does for perry_runtime.
What does NOT work: naively widening #5920's candidate set
I tried generalizing strip_bundled_runtime_from_well_known_lib's candidate filter from member.starts_with("perry_runtime-") to "every member" (relying on the existing Rule 1 + Rule 2 checks to gate safety). This broke the existing issue_5920_wrapper_bundled_runtime_async_starvation regression test (fire_and_forget_fetch_survives_recompile started failing with the same starvation symptom #5920 fixed).
Root cause of why the naive widening is unsafe: Rule 2's sibling_undefined set is computed as "the union of undefined symbol references from every member NOT in the candidate set." As the candidate set grows to include nearly every crate-derived member (std, core, tokio, etc. are present in almost every object in an HTTP-stack-bundling archive), the "sibling" set used to check for unsafe removals shrinks correspondingly — inter-candidate dependencies (candidate A referencing a symbol only candidate B defines, both of which are simultaneously being removed) are never checked at all, since Rule 2 only guards candidate-vs-non-candidate edges. A safe generalization needs either a smarter per-member reachability computation, or an iterative fixed-point approach (remove one "layer" of provably-safe duplicates at a time, recomputing sibling references between rounds) rather than a one-shot flat widening.
Suggested direction for a real fix
- Generalize the duplicate-detection to run per-crate (matching each member's crate-name prefix, e.g.
tokio-, hyper_util-, h2-, rustls-, reqwest-, ring-, bytes-, url-, std-, core-) similarly to perry_runtime-, but iterate to a fixed point (repeatedly compute removable members and re-derive sibling_undefined from the shrinking "kept" set) instead of a single pass, so inter-candidate dependencies are still caught.
- Alternatively, dedupe at the
ld-invocation level via -single_module/symbol-versioning-style tricks, or by post-processing with llvm-objcopy --keep-global-symbols per-archive against a computed "authoritative provider" map (whichever archive is linked first/is stdlib wins, in ALL other archives strip to only symbols not already provided elsewhere) — this needs the same fixed-point care as above.
- This is a genuinely deep,
strip_dedup.rs-architecture-level task; recommend whoever picks it up start from the #5920 test's design (spawn a real async task, assert it doesn't starve) and add an analogous "N well-known libs + fastify + http simultaneously" fixture to catch regressions in either direction.
Workaround used for now
None found that's both safe and general — @fastify/*/@ioredis/commands removal from compilePackages was tried and abandoned (it just routes those packages to interpreted-JS fallback, which this build of perry has removed support for entirely: "JavaScript runtime (V8) support has been removed"). opencode's full binary currently cannot complete linking with --enable-wasm-runtime and its real dependency set; all symbol-resolution issues (the actual multi-bug investigation across #5918/#5922/#5924/#5927) are fully fixed and this is the last remaining blocker.
Summary
Found while getting a real-world
sst/opencodecompile all the way to a working binary (companion investigation to #5918/#5919, #5922/#5924/#5927/#5923). After all import/namespace-resolution symbol issues were fixed, the final link still failed — first with ~5900, then (after #5920/#5921 landed on main, which helped) ~2150 duplicate-symbol errors fromldon macOS.Root cause
opencode's dependency graph needs FIVE separate "well-known" native libraries simultaneously:
http/https/http2→perry-ext-http,fastify→perry-ext-fastify,ioredis→perry-ext-ioredis,net→perry-ext-net,ws→perry-ext-ws, each rebuilt "with shared tokio" per thewell-known: routinglog lines. Each of these is an independently-built Rust staticlib that bundles its OWN full copy of shared transitive dependencies (their exact codegen-unit object files, e.g.tokio-<hash>.tokio.<hash>-cgu.0.rcgu.o).libperry_stdlib.aALSO bundles its own copies of the same crates (for its own fetch/https/websocket support). When multiple such libraries are linked together, every shared dependency they have in common —std,core,alloc,hashbrown,aho_corasick,regex_automata,tokio,hyper_util,h2,rustls,reqwest,ring,bytes,url, and perry's ownperry_runtimecodegen unit — ends up defined in more than one archive.#5920/#5921 (already on main) added
strip_bundled_runtime_from_well_known_lib, which safely drops duplicateperry_runtime-*member objects from a well-known lib when stdlib bundles the identical codegen unit (verified via two safety rules: Rule 1 — stdlib bundles the exact same codegen unit; Rule 2 — no sibling member in the same archive depends on a symbol only that copy provides). This fixed theperry_runtimeduplicates specifically and reduced the failure from ~5900 to ~2150 duplicate symbols in opencode's case, but the SAME duplication shape recurs for every other shared crate above once TWO OR MORE well-known libraries both need a full HTTP-client stack (e.g. bothhttpandfastifybundletokio+hyper_util+h2+rustls+reqwest+ring).Why there's no linker-flag escape hatch
Verified directly on this machine's toolchain (Xcode/CLT current as of 2026-07): neither
-Wl,-multiply_defined,suppressnor-Wl,-ld_classic(with or without-multiply_defined) suppress the duplicate-symbol error anymore — both are reported as "obsolete"/"deprecated" and the link still fails. Unlike GNU ld/lld (which perry already uses-Wl,--allow-multiple-definitionfor on Android/HarmonyOS), Apple's current linker has no tolerance mode for this. The fix has to happen at the object/archive level before linking, mirroring what #5920/#5921 already does forperry_runtime.What does NOT work: naively widening #5920's candidate set
I tried generalizing
strip_bundled_runtime_from_well_known_lib's candidate filter frommember.starts_with("perry_runtime-")to "every member" (relying on the existing Rule 1 + Rule 2 checks to gate safety). This broke the existingissue_5920_wrapper_bundled_runtime_async_starvationregression test (fire_and_forget_fetch_survives_recompilestarted failing with the same starvation symptom #5920 fixed).Root cause of why the naive widening is unsafe: Rule 2's
sibling_undefinedset is computed as "the union of undefined symbol references from every member NOT in the candidate set." As the candidate set grows to include nearly every crate-derived member (std, core, tokio, etc. are present in almost every object in an HTTP-stack-bundling archive), the "sibling" set used to check for unsafe removals shrinks correspondingly — inter-candidate dependencies (candidate A referencing a symbol only candidate B defines, both of which are simultaneously being removed) are never checked at all, since Rule 2 only guards candidate-vs-non-candidate edges. A safe generalization needs either a smarter per-member reachability computation, or an iterative fixed-point approach (remove one "layer" of provably-safe duplicates at a time, recomputing sibling references between rounds) rather than a one-shot flat widening.Suggested direction for a real fix
tokio-,hyper_util-,h2-,rustls-,reqwest-,ring-,bytes-,url-,std-,core-) similarly toperry_runtime-, but iterate to a fixed point (repeatedly compute removable members and re-derivesibling_undefinedfrom the shrinking "kept" set) instead of a single pass, so inter-candidate dependencies are still caught.ld-invocation level via-single_module/symbol-versioning-style tricks, or by post-processing withllvm-objcopy --keep-global-symbolsper-archive against a computed "authoritative provider" map (whichever archive is linked first/is stdlib wins, in ALL other archives strip to only symbols not already provided elsewhere) — this needs the same fixed-point care as above.strip_dedup.rs-architecture-level task; recommend whoever picks it up start from the#5920test's design (spawn a real async task, assert it doesn't starve) and add an analogous "N well-known libs + fastify + http simultaneously" fixture to catch regressions in either direction.Workaround used for now
None found that's both safe and general —
@fastify/*/@ioredis/commandsremoval fromcompilePackageswas tried and abandoned (it just routes those packages to interpreted-JS fallback, which this build of perry has removed support for entirely: "JavaScript runtime (V8) support has been removed"). opencode's full binary currently cannot complete linking with--enable-wasm-runtimeand its real dependency set; all symbol-resolution issues (the actual multi-bug investigation across #5918/#5922/#5924/#5927) are fully fixed and this is the last remaining blocker.