diff --git a/changelog.d/8942-elf-split-unit-string-constants.md b/changelog.d/8942-elf-split-unit-string-constants.md new file mode 100644 index 0000000000..4c6d4a07ac --- /dev/null +++ b/changelog.d/8942-elf-split-unit-string-constants.md @@ -0,0 +1,4 @@ +### Fixed + +- **Codegen units: `add_string_constant` globals and the null-guard global are now module-unique, so a multi-module Linux link of split modules no longer fails with `multiple definition of .str.N`** (#8942). Every `LlModule` numbered its anonymous rodata constants from `.str.0`, which is fine while they stay `private`. Codegen-unit splitting (#5391/#7174) promotes them so sibling units can reference them, and on every non-Mach-O target the owning unit's copy is a plain strong definition with default visibility (`make_unique_owner_global` — whose doc said "COFF" but whose branch runs for ELF too). Two modules large enough to split therefore both exported `.str.375` with different contents; GNU ld refused a Next.js App Route bundle's `--output-type dylib` link with 2,188 `multiple definition` errors (`app-page.runtime.prod.js` ↔ `route.js`, `jsonwebtoken/index.js` ↔ `route.js`). macOS was not clean either, only quiet: Mach-O keeps the replicated `linkonce_odr` copies (`weak external automatically hidden`) and ld64 coalesces weak definitions by name, so one module's bytes silently stood in for another's whenever a `.str.N` index was shared — `fn.name`, class names and `Function.prototype.toString` came out wrong, reproduced on macOS with a three-module program and `PERRY_CODEGEN_UNITS=2`. `compile_module` now installs the module symbol prefix on the `LlModule` (`set_symbol_prefix`) and `add_string_constant` mints `@_.str.N`, mirroring what `strings.rs` already did for `_.str.N.bytes`. The unprefixed `perry_null_guard_zero` (the safe-dereference target of `safe_load_i32_from_ptr`) takes the identical promotion path and is renamed `perry_null_guard_zero_`, injected per function through the `RegCounter` cell so no call site changes. Unique names rather than `linkonce_odr hidden` because a COMDAT is folded by name and would reproduce ld64's silent merge on ELF; the #7174 one-definition-per-module layout is unchanged. Fixtures that never set a prefix keep the bare names. Verified on macOS end to end (split output byte-identical to Node after, miscompiled before) and on ELF by retargeting the dumped units to `x86_64-unknown-linux-gnu` under the owner policy and linking with `ld.lld` (10 duplicate symbols before, 0 after); a real Linux build is the remaining proof. `crates/perry-codegen/src/module.rs`, `block.rs`, `function.rs`, `codegen/mod.rs`. +- **`--output-type dylib` on Linux now links `-lm -lpthread -ldl`, like the executable link always has** (#8942). The `cc -shared` plugin link in `run_pipeline.rs` never carried the system libraries: a plugin resolves every `perry_*`/`js_*` symbol from the host at `dlopen` time, so the omission was invisible until a real-app ELF dylib link — the same Next.js route bundle, right after the string-constant collisions were fixed — failed with `undefined reference to 'floor'` / `'log10'` from `perry_closure_*` functions, where LLVM had lowered `llvm.floor`/`llvm.log10` to libm calls. New `link/linux_dylib_libs.rs::push_unix_dylib_output` appends the libraries AFTER the objects (GNU ld only resolves what precedes a `-l`, and Ubuntu's default `--as-needed` drops an earlier one) and then `-o`; unit tests pin the order and that macOS (`-lSystem` already carries libm) gets no extra flags. `crates/perry/src/commands/compile/run_pipeline.rs`, `crates/perry/src/commands/compile/link/linux_dylib_libs.rs`, `link/mod.rs`. diff --git a/crates/perry-codegen/src/block.rs b/crates/perry-codegen/src/block.rs index be659c1264..51823c6240 100644 --- a/crates/perry-codegen/src/block.rs +++ b/crates/perry-codegen/src/block.rs @@ -55,6 +55,10 @@ impl Default for FpFlags { } } +/// Name of the null-guard global in a module without a symbol prefix. A +/// prefixed module uses [`crate::module::LlModule::null_guard_global`]. +pub(crate) const DEFAULT_NULL_GUARD_GLOBAL: &str = "perry_null_guard_zero"; + /// Function-wide register counter shared between all blocks in a function. /// /// Registers are `%r1`, `%r2`, … unique across the entire function body — @@ -92,6 +96,15 @@ pub struct RegCounter { /// callee is UB, so the registry, not the emitting code, is the single /// source of truth. `None` for functions built outside a module (tests). preserve_none_fns: RefCell>>>>, + /// `@`-prefixed symbol of the module's null-guard global (the zeroed + /// `i32` that [`LlBlock::safe_load_i32_from_ptr`] dereferences in place + /// of a bad handle). Injected by `LlModule::define_function` so the name + /// can carry the module prefix: codegen-unit splitting promotes the + /// global to a strong link-visible symbol on ELF/COFF, and an unprefixed + /// `perry_null_guard_zero` in two split modules is a GNU ld + /// `multiple definition`. Functions built outside a module (tests) keep + /// the bare name. + null_guard_symbol: RefCell>, /// Compiler-private validity bits for nested stable-packed loop proofs. /// /// A guarded inner receiver may keep its raw address across call-free @@ -113,10 +126,24 @@ impl RegCounter { eh_unwind_labels: RefCell::new(Vec::new()), shadow_slot_allocas: RefCell::new(HashSet::new()), preserve_none_fns: RefCell::new(None), + null_guard_symbol: RefCell::new(None), stable_packed_revalidation_slots: RefCell::new(Vec::new()), } } + /// Point this function's null-guard loads at `global` (a bare symbol + /// name, no `@`). See the `null_guard_symbol` field. + pub(crate) fn set_null_guard_global(&self, global: &str) { + *self.null_guard_symbol.borrow_mut() = Some(format!("@{global}")); + } + + fn null_guard_symbol(&self) -> String { + self.null_guard_symbol + .borrow() + .clone() + .unwrap_or_else(|| format!("@{DEFAULT_NULL_GUARD_GLOBAL}")) + } + pub(crate) fn push_stable_packed_revalidation_slot(&self, slot: String) { self.stable_packed_revalidation_slots .borrow_mut() @@ -1028,8 +1055,10 @@ impl LlBlock { /// a small handle), returns 0 instead of dereferencing. /// Used for .length reads and bounds checks on arrays/strings. /// - /// Uses `@perry_null_guard_zero` — a module-global i32 initialized - /// to 0 that serves as a safe dereference target. + /// Uses the module's null-guard global (`@perry_null_guard_zero`, or + /// its module-prefixed spelling under `LlModule::set_symbol_prefix`) — a + /// module-global i32 initialized to 0 that serves as a safe dereference + /// target. /// /// (Issue #52) The length load is tagged `!invariant.load` — once /// resolved, an Array/Buffer's length field at offset 0 of the @@ -1054,7 +1083,7 @@ impl LlBlock { cond_ty: "i1", cond: is_bad.clone(), ty: "ptr", - a: "@perry_null_guard_zero".to_string(), + a: self.counter.null_guard_symbol(), b: handle_ptr.clone(), }); r diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 3af1a6358a..85d4a803d7 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -426,10 +426,6 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> let _opt_report_module_scope = crate::opt_report::enter_module(hir); let mut llmod = LlModule::new_with_fp_flags(&triple, fp_flags); - // Null guard global: a zeroed i32 used as a safe dereference target - // when a NaN-unboxed pointer is null/invalid. Prevents segfaults from - // uninitialized locals or unhandled expressions producing 0.0/TAG_UNDEFINED. - llmod.add_internal_global("perry_null_guard_zero", crate::types::I32, "0"); runtime_decls::declare_phase1(&mut llmod); // Derive a per-module symbol prefix from the HIR module name: @@ -443,6 +439,18 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> // `main` is the only globally-named symbol — non-entry modules emit // `__init` instead. let module_prefix = sanitize(&hir.name); + // Anonymous rodata constants (`add_string_constant`) carry the prefix + // too: codegen-unit splitting promotes them to link-visible symbols, + // and their per-module counter would otherwise collide across modules + // at the final link (GNU ld: `multiple definition of .str.N`). + llmod.set_symbol_prefix(&module_prefix); + // Null guard global: a zeroed i32 used as a safe dereference target + // when a NaN-unboxed pointer is null/invalid. Prevents segfaults from + // uninitialized locals or unhandled expressions producing 0.0/TAG_UNDEFINED. + // Defined AFTER the prefix is installed so its name is module-unique + // (`perry_null_guard_zero_`) — split units export it strong on + // ELF/COFF, exactly like the string constants above. + llmod.add_internal_global(&llmod.null_guard_global(), crate::types::I32, "0"); // Imports are no longer a hard error — Phase F.1 supports multi- // module compilation. Cross-module function CALLS via ExternFuncRef diff --git a/crates/perry-codegen/src/function.rs b/crates/perry-codegen/src/function.rs index 06d26d1845..44b01475ab 100644 --- a/crates/perry-codegen/src/function.rs +++ b/crates/perry-codegen/src/function.rs @@ -786,6 +786,12 @@ impl LlFunction { self.reg_counter.set_preserve_none_fns(fns); } + /// Point this function's null-guard loads at the module's (possibly + /// module-prefixed) null-guard global. See `RegCounter::null_guard_symbol`. + pub(crate) fn set_null_guard_global(&self, global: &str) { + self.reg_counter.set_null_guard_global(global); + } + /// Whether this function's define header (and every declare of it) must /// carry the `preserve_nonecc` calling convention (#8175). Render-time /// lookup against the module registry, so define order never matters. diff --git a/crates/perry-codegen/src/module.rs b/crates/perry-codegen/src/module.rs index 00d461c359..467e7b2636 100644 --- a/crates/perry-codegen/src/module.rs +++ b/crates/perry-codegen/src/module.rs @@ -121,10 +121,16 @@ fn promote_global_for_units(line: &str) -> String { } } -/// Give a generated global one non-discardable definition. On COFF each -/// global has a unique owning codegen unit; leaving that sole definition as +/// Give a generated global one non-discardable definition. On every +/// non-Mach-O target (ELF and COFF — see `replicate_globals`) each global has +/// a unique owning codegen unit; leaving that sole definition as /// `linkonce_odr` lets LLVM discard it when all references in the owner happen /// to optimize away, even though other object files still reference it. +/// +/// The result is a plain STRONG definition with default visibility, so the +/// symbol's NAME must be unique across the whole program, not just the +/// module: `.str.N` constants only satisfy that through +/// [`LlModule::set_symbol_prefix`]. fn make_unique_owner_global(line: &str) -> String { if line.contains(" = external ") { return line.to_string(); @@ -347,6 +353,21 @@ pub struct LlModule { globals: Vec, string_constants: Vec, string_counter: u32, + /// Module symbol prefix folded into every anonymous rodata constant this + /// module mints (`add_string_constant` → `@_.str.N`). Empty (the + /// bare `@.str.N`) only for modules that never call + /// [`Self::set_symbol_prefix`] — unit tests and other single-module + /// fixtures. + /// + /// Load-bearing under codegen-unit splitting: `render_codegen_units` + /// promotes every `private` constant so sibling units can reference it, + /// and on ELF/COFF the owning unit's copy is a plain STRONG global. The + /// `.str.N` counter restarts at 0 per module, so two split modules used to + /// export the same `.str.375` with different contents — GNU ld rejects + /// that as a multiple definition, and ld64's weak coalescing silently kept + /// whichever copy it saw first. The prefix makes the name module-unique, + /// exactly as `strings.rs` already does for `_.str.N.bytes`. + symbol_prefix: String, /// Extra numbered metadata nodes emitted after `!0 = !{}`. Used by /// the buffer alias-scope system to declare per-buffer scopes and /// noalias sets so LLVM's LoopVectorizer can prove different buffers @@ -407,6 +428,7 @@ impl LlModule { globals: Vec::new(), string_constants: Vec::new(), string_counter: 0, + symbol_prefix: String::new(), metadata_lines: Vec::new(), ic_counter: 0, buffer_alias_counter: 0, @@ -416,6 +438,39 @@ impl LlModule { } } + /// Install the per-module symbol prefix that [`Self::add_string_constant`] + /// folds into every anonymous constant it mints. Must run before the + /// first string constant is added — a prefix that only covers part of + /// the pool would leave the earlier `.str.N` names colliding across + /// modules again. + pub fn set_symbol_prefix(&mut self, prefix: &str) { + debug_assert!( + self.string_constants.is_empty() && self.functions.is_empty(), + "set_symbol_prefix must precede the first add_string_constant/define_function" + ); + self.symbol_prefix = prefix.to_string(); + } + + /// Name (no `@`) of this module's null-guard global — the zeroed `i32` + /// that `LlBlock::safe_load_i32_from_ptr` reads instead of a bad handle. + /// The caller defines it (`add_internal_global(.., I32, "0")`); every + /// function defined afterwards references it by this name. Module-prefixed + /// for the same reason as `add_string_constant`'s names: unit splitting + /// promotes it to a strong link-visible symbol on ELF/COFF, and the bare + /// `perry_null_guard_zero` in two split modules is a GNU ld + /// `multiple definition`. + pub fn null_guard_global(&self) -> String { + if self.symbol_prefix.is_empty() { + crate::block::DEFAULT_NULL_GUARD_GLOBAL.to_string() + } else { + format!( + "{}_{}", + crate::block::DEFAULT_NULL_GUARD_GLOBAL, + self.symbol_prefix + ) + } + } + /// Register the module's `preserve_nonecc` symbols (#8175). Must be /// called before any call site to one of them is emitted — in practice, /// right after the specialization plan is selected and before any user @@ -571,6 +626,7 @@ impl LlModule { // #8175: every function shares the module's preserve_nonecc registry, // so its call sites and its own define header agree on the convention. func.set_preserve_none_fns(Rc::clone(&self.preserve_none_fns)); + func.set_null_guard_global(&self.null_guard_global()); self.functions.push(func); self.functions.last_mut().unwrap() } @@ -680,8 +736,18 @@ impl LlModule { /// Add a UTF-8 string constant to the module's constant pool. Returns /// `(global_name, byte_length)` — the byte length is what Perry passes as /// the `len` argument to `js_string_from_bytes`. + /// + /// The name is `@_.str.N` once [`Self::set_symbol_prefix`] has + /// run (bare `@.str.N` otherwise). The constant is `private` here, but + /// codegen-unit splitting promotes it to a link-visible symbol, so the + /// name must already be unique across the whole program — see the + /// `symbol_prefix` field. pub fn add_string_constant(&mut self, value: &str) -> (String, usize) { - let name = format!(".str.{}", self.string_counter); + let name = if self.symbol_prefix.is_empty() { + format!(".str.{}", self.string_counter) + } else { + format!("{}_.str.{}", self.symbol_prefix, self.string_counter) + }; self.string_counter += 1; let bytes = value.as_bytes(); @@ -1385,6 +1451,153 @@ mod tests { } } + /// Every `@sym` a rendered unit DEFINES with linkage the linker treats + /// as strong: not a `private`/`internal` local, not a `linkonce`/`weak` + /// COMDAT the linker folds, not an `external`/`appending` declaration. + /// Two of these with the same name in one link is GNU ld's + /// `multiple definition of ...`. + fn strong_global_definitions(unit: &str) -> Vec { + unit.lines() + .filter_map(|line| { + let name = global_symbol_name(line)?; + let rhs = line[name.len()..].trim_start().strip_prefix("= ")?; + let weak_or_local = [ + "private ", + "internal ", + "linkonce_odr ", + "linkonce ", + "weak_odr ", + "weak ", + "external ", + "appending ", + "available_externally ", + "common ", + ] + .iter() + .any(|kw| rhs.starts_with(kw)); + (!weak_or_local).then(|| name.to_string()) + }) + .collect() + } + + #[test] + fn split_modules_do_not_export_colliding_string_constants() { + // A Next.js route bundle compiled to a Linux shared library: five + // modules large enough to split into codegen units. Splitting + // promotes every `add_string_constant` global so sibling units can + // reference it, and on ELF/COFF the owning unit's copy is a plain + // STRONG global (`make_unique_owner_global`). The `.str.N` counter + // restarts at 0 per module, so `app-page.runtime.prod.js` and + // `route.js` both exported `.str.375` — with different contents — and + // GNU ld refused the final link with 2,188 `multiple definition` + // errors. (ld64 accepts the Mach-O `linkonce_odr` copies and + // coalesces them by name, which is worse: one module's bytes silently + // stand in for the other's.) The fix folds the module prefix into the + // name, as `strings.rs` already does for `_.str.N.bytes`. + fn split_module(prefix: &str, literal: &str) -> (String, Vec) { + let mut m = LlModule::new("x86_64-unknown-linux-gnu"); + m.set_symbol_prefix(prefix); + // The null-guard global is the other unprefixed per-module + // definition `compile_module` used to mint; it rides the same + // prefix and the same strong-definition assertion below. + let null_guard = m.null_guard_global(); + assert_eq!(null_guard, format!("perry_null_guard_zero_{prefix}")); + m.add_internal_global(&null_guard, I32, "0"); + let (name, len) = m.add_string_constant(literal); + assert_eq!(len, literal.len()); + // Two functions, each referencing the constant, so a 2-way split + // has one owning unit and one unit that must resolve it across + // the unit boundary. + for fname in ["f", "g"] { + let f = m.define_function(format!("perry_fn_{prefix}__{fname}"), PTR, vec![]); + let e = f.create_block("entry"); + let _len = e.safe_load_i32_from_ptr("0"); + e.ret(PTR, &format!("@{name}")); + } + let units = m.render_codegen_units(2); + assert_eq!(units.len(), 2, "two functions → two units"); + (name, units) + } + let (name_a, units_a) = split_module("app_page_runtime_prod_js", "alpha"); + let (name_b, units_b) = split_module("route_js", "beta"); + + // The name is module-unique (both would have been `.str.0`), and it + // is what the functions reference. + assert_eq!(name_a, "app_page_runtime_prod_js_.str.0"); + assert_eq!(name_b, "route_js_.str.0"); + assert_ne!(name_a, name_b); + for (prefix, name, literal, units) in [ + ("app_page_runtime_prod_js", &name_a, "alpha", &units_a), + ("route_js", &name_b, "beta", &units_b), + ] { + let ty = format!("[{} x i8]", literal.len() + 1); + let def = format!("@{name} = unnamed_addr constant {ty}"); + let decl = format!("@{name} = external constant {ty}"); + assert_eq!( + units.iter().filter(|u| u.contains(&def)).count(), + 1, + "#7174: the constant is DEFINED in exactly one unit" + ); + assert_eq!( + units.iter().filter(|u| u.contains(&decl)).count(), + 1, + "the other unit resolves it through an external declaration" + ); + for u in units { + assert!( + u.contains(&format!("ret ptr @{name}")), + "both units reference the constant by its prefixed name" + ); + } + // The bare per-module names never leak into a link-visible symbol. + assert!(!units.iter().any(|u| u.contains("@.str.0"))); + assert!(!units + .iter() + .any(|u| u.contains("@perry_null_guard_zero ") + || u.contains("@perry_null_guard_zero,"))); + let guard_def = format!("@perry_null_guard_zero_{prefix} = global i32 0"); + assert_eq!( + units.iter().filter(|u| u.contains(&guard_def)).count(), + 1, + "the null guard is DEFINED (strong, prefixed) in exactly one unit" + ); + } + + // The GNU ld property: across every unit of both modules, no strong + // symbol is defined more than once. + let mut strong: Vec = units_a + .iter() + .chain(units_b.iter()) + .flat_map(|u| strong_global_definitions(u)) + .collect(); + assert!( + strong.iter().any(|s| s == &format!("@{name_a}")), + "subject is live: the owning unit's copy is a strong ELF definition" + ); + let n = strong.len(); + strong.sort(); + strong.dedup(); + assert_eq!( + strong.len(), + n, + "a strong global is defined in two units — GNU ld would reject the link" + ); + } + + #[test] + fn string_constants_without_a_prefix_keep_the_bare_name() { + // Single-module fixtures never set a prefix; their `@.str.N` spelling + // stays exactly as before so nothing downstream shifts. + let mut m = LlModule::new("x86_64-unknown-linux-gnu"); + let (first, _) = m.add_string_constant("a"); + let (second, _) = m.add_string_constant("b"); + assert_eq!(first, ".str.0"); + assert_eq!(second, ".str.1"); + assert!(m + .to_ir() + .contains("@.str.1 = private unnamed_addr constant [2 x i8] c\"b\\00\"")); + } + #[test] fn split_modules_keep_unknown_fallback_wrappers_distinct_at_shared_link() { // #8064: splitting promotes internal definitions so sibling units can diff --git a/crates/perry/src/commands/compile/link/linux_dylib_libs.rs b/crates/perry/src/commands/compile/link/linux_dylib_libs.rs new file mode 100644 index 0000000000..bac4cc5d8e --- /dev/null +++ b/crates/perry/src/commands/compile/link/linux_dylib_libs.rs @@ -0,0 +1,91 @@ +//! System libraries for a Linux shared-library (`--output-type dylib`) link. +//! +//! The executable link (`build_and_run.rs`) has always added +//! `-lm -lpthread -ldl` on Linux. The `cc -shared` plugin link in +//! `run_pipeline.rs` never did: a plugin resolves every `perry_*`/`js_*` +//! symbol from the host at `dlopen` time, so nothing *Perry* provides is +//! linked in, and the omission was invisible as long as the objects only +//! referenced host symbols. LLVM lowers `llvm.floor`/`llvm.log10`/… in +//! generated closures to libm calls, though, and glibc's `floor`/`log10` +//! live in `libm.so`, so the first real-app ELF dylib link (a Next.js route +//! bundle, #8942) failed with `undefined reference to 'floor'` right after +//! the string-constant collisions were fixed. macOS never sees this because +//! `-lSystem` (implicit in `cc -dynamiclib`) carries libm. +//! +//! Kept next to `windows_link::add_system_libs` / `linux_ui_libs` rather than +//! inline in the 7k-line pipeline so the command shape is unit-testable. + +use std::path::Path; +use std::process::Command; + +/// The libraries, in link order. Same set the Linux executable link uses +/// (glibc ≥ 2.34 folds pthread/dl into libc, older glibc and the arm64 +/// cross sysroots do not — the flags are harmless where redundant). +pub(crate) const LINUX_DYLIB_SYSTEM_LIBS: &[&str] = &["-lm", "-lpthread", "-ldl"]; + +/// Finish a Unix plugin link (`cc -shared` on Linux, `cc -dynamiclib` on +/// macOS) after the caller has pushed the object files: on Linux append the +/// system libraries, then the output path. +/// +/// Order is load-bearing on GNU ld: `-l` flags resolve only references seen +/// *before* them on the command line (and Ubuntu's default `--as-needed` +/// drops a library nothing preceding it needs), so the libraries must +/// follow the objects. They therefore belong here, not in the per-platform +/// command prologue. +pub(crate) fn push_unix_dylib_output(cmd: &mut Command, is_linux: bool, exe_path: &Path) { + if is_linux { + for lib in LINUX_DYLIB_SYSTEM_LIBS { + cmd.arg(lib); + } + } + cmd.arg("-o").arg(exe_path); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn args(cmd: &Command) -> Vec { + cmd.get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect() + } + + #[test] + fn linux_shared_library_link_carries_libm_after_the_objects() { + // #8942: `undefined reference to 'floor'` / `'log10'` from + // `perry_closure_*` in a `--output-type dylib` link on Ubuntu. + let mut cmd = Command::new("cc"); + cmd.arg("-shared").arg("route.o").arg("app-page.o"); + push_unix_dylib_output(&mut cmd, true, Path::new("out.so")); + let args = args(&cmd); + let pos = |flag: &str| { + args.iter() + .position(|a| a == flag) + .unwrap_or_else(|| panic!("`{flag}` missing from {args:?}")) + }; + let last_object = pos("app-page.o"); + for lib in LINUX_DYLIB_SYSTEM_LIBS { + assert!( + pos(lib) > last_object, + "`{lib}` must follow the objects or GNU ld ignores it: {args:?}" + ); + assert!(pos(lib) < pos("-o"), "`{lib}` must precede `-o`: {args:?}"); + } + assert_eq!(&args[args.len() - 2..], ["-o", "out.so"]); + } + + #[test] + fn macos_shared_library_link_adds_no_system_libs() { + // libSystem (implicit in `-dynamiclib`) already provides libm; the + // Linux flags would only add noise to the ld64 command line. + let mut cmd = Command::new("cc"); + cmd.arg("-dynamiclib").arg("plugin.o"); + push_unix_dylib_output(&mut cmd, false, Path::new("out.dylib")); + let args = args(&cmd); + assert!(!args + .iter() + .any(|a| LINUX_DYLIB_SYSTEM_LIBS.contains(&a.as_str()))); + assert_eq!(args, ["-dynamiclib", "plugin.o", "-o", "out.dylib"]); + } +} diff --git a/crates/perry/src/commands/compile/link/mod.rs b/crates/perry/src/commands/compile/link/mod.rs index 7b917a6ca9..3c0b1d3413 100644 --- a/crates/perry/src/commands/compile/link/mod.rs +++ b/crates/perry/src/commands/compile/link/mod.rs @@ -47,6 +47,7 @@ use super::{ mod archive_cache; mod build_and_run; mod link_cache; +mod linux_dylib_libs; mod linux_ui_libs; mod native_features; mod pkg_config; @@ -59,6 +60,7 @@ use archive_cache::{prepare_well_known_archives, PreparedArchiveInputs}; pub(super) use build_and_run::build_and_run_link; use link_cache::prepare_link_cache_status; pub(super) use link_cache::{write_link_cache_manifest, LinkCacheStatus}; +pub(super) use linux_dylib_libs::push_unix_dylib_output; pub use platform_cmd::select_linker_command; #[cfg(test)] pub(super) use windows_link::WINDOWS_APP_MANIFEST; // consumed only by windows_link_tests diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 562cdb9fcc..1c20fe290e 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -6830,7 +6830,14 @@ pub fn run_with_parse_cache( // everything in the plugin itself. cmd.arg("/defaultlib:libcmt"); } else { - cmd.arg("-o").arg(&exe_path); + // Linux also needs the system libraries the executable link has + // always carried (`-lm -lpthread -ldl`): LLVM lowers the math + // intrinsics in generated closures to libm calls, and glibc's + // `floor`/`log10` live in libm.so, so a real-app ELF dylib link + // failed with `undefined reference to 'floor'` (#8942). They go + // AFTER the objects — GNU ld only resolves what precedes a `-l`. + // macOS gets libm through the implicit `-lSystem`. + link::push_unix_dylib_output(&mut cmd, is_linux, &exe_path); } let status = tool_output::run_internal_tool(&mut cmd, verbose)?;