Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/perry/src/commands/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ use resolve::{
};
use strip_dedup::{
dedup_native_lib_for_tier3, dedup_runtime_for_tier3, dedup_stdlib_for_tier3,
localize_stdlib_stub_symbols_for_windows, strip_duplicate_objects_from_lib,
strip_duplicate_objects_from_well_known_lib,
localize_stdlib_stub_symbols, localize_stdlib_stub_symbols_for_windows,
strip_duplicate_objects_from_lib, strip_duplicate_objects_from_well_known_lib,
};
use targets::{
apple_sdk_version, find_visionos_swift_runtime, find_watchos_swift_runtime,
Expand Down
60 changes: 57 additions & 3 deletions crates/perry/src/commands/compile/link/build_and_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,26 @@ pub(crate) fn build_and_run_link(
// Native macOS/iOS via clang driver
cmd.arg("-Wl,-dead_strip");
}
// PERRY_LINK_MAP=<path> — emit a linker map (which archive each symbol
// resolves from) for diagnosing dup-symbol / shadowing bugs. Honor it on
// every non-Windows linker, not just native macOS. GNU ld (ELF) spells
// it `-Map=<file>`; ld64 (Apple) spells it `-map <file>`. The
// cross-Apple linkers are driven directly (no `-Wl,` prefix).
if let Some(map) = std::env::var_os("PERRY_LINK_MAP") {
let map = map.to_string_lossy();
if is_android || is_linux || is_harmonyos {
cmd.arg(format!("-Wl,-Map,{map}"));
} else if is_cross_ios || is_cross_visionos || is_cross_macos || is_cross_tvos {
cmd.arg("-map").arg(map.as_ref());
} else if is_watchos || is_visionos {
cmd.arg("-Xlinker")
.arg("-map")
.arg("-Xlinker")
.arg(map.as_ref());
} else {
cmd.arg(format!("-Wl,-map,{map}"));
}
}
} else {
// MSVC link.exe / lld-link equivalents:
// /OPT:REF — drop unreferenced functions/data (= --gc-sections)
Expand Down Expand Up @@ -367,7 +387,21 @@ pub(crate) fn build_and_run_link(
// Also link runtime for symbols DCE'd from stdlib's bundled
// perry-runtime; on tier-3 it's first stripped of stdlib's objects.
if !is_android && !is_windows {
cmd.arg(dedup_runtime_for_tier3(target, runtime_lib, stdlib));
// #5000 (macOS/Linux): the standalone runtime archive is built
// WITHOUT the `stdlib` feature, so it also defines the no-op
// stdlib_stubs (js_fetch_with_options, js_headers_new,
// js_request_new, js_ws_*, js_readline_*). With ELF/Mach-O
// first-definition-wins those stubs can satisfy the user's
// fetch ref before perry-stdlib's real impls, so `fetch()`
// no-ops and a program awaiting it hangs. When this build
// actually uses stdlib (fetch / ws / readline), localize those
// stub symbols in a copy of the runtime so perry-stdlib wins.
let runtime_for_link = if ctx.uses_fetch || ctx.needs_stdlib {
localize_stdlib_stub_symbols(runtime_lib, stdlib)
} else {
runtime_lib.to_path_buf()
};
cmd.arg(dedup_runtime_for_tier3(target, &runtime_for_link, stdlib));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} else {
if ctx.needs_stdlib {
Expand All @@ -383,8 +417,28 @@ pub(crate) fn build_and_run_link(
cmd.arg(runtime_lib);
}
} else {
// Runtime-only linking — no stdlib needed
cmd.arg(runtime_lib);
// Runtime-only linking — no stdlib needed.
//
// #5000 (Linux GTK4 UI): a bare UI program has
// `ctx.needs_stdlib == false`, so perry-stdlib isn't linked above —
// but the GTK4 UI branch below force-links it with
// `--whole-archive --allow-multiple-definition` to satisfy glib
// trampolines that call js_stdlib_process_pending /
// js_promise_run_microtasks. With first-definition-wins, the
// unlocalized runtime stubs linked here would shadow stdlib's real
// impls, leaving those pumps as no-ops. Localize the runtime's stub
// symbols first so stdlib wins, mirroring the needs_stdlib path.
let force_stdlib_for_linux_ui =
is_linux && ctx.needs_ui && find_ui_library(target).is_some();
let runtime_for_link = if force_stdlib_for_linux_ui {
match stdlib_lib.clone().or_else(|| find_stdlib_library(target)) {
Some(stdlib) => localize_stdlib_stub_symbols(runtime_lib, &stdlib),
None => runtime_lib.to_path_buf(),
}
} else {
runtime_lib.to_path_buf()
};
cmd.arg(&runtime_for_link);
}
} else if ctx.needs_stdlib {
// Android + UI: runtime is provided by UI lib, but stdlib must still be linked
Expand Down
6 changes: 3 additions & 3 deletions crates/perry/src/commands/compile/link/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ use super::{
find_geisterhand_stdlib, find_geisterhand_ui, find_lld_link, find_llvm_tool,
find_msvc_lib_paths, find_msvc_link_exe, find_perry_windows_sdk, find_stdlib_library,
find_ui_library, find_visionos_swift_runtime, find_watchos_swift_runtime,
localize_stdlib_stub_symbols_for_windows, rust_target_triple, strip_duplicate_objects_from_lib,
strip_duplicate_objects_from_well_known_lib, windows_pe_subsystem_flag,
windows_subsystem_needs_ui, CompilationContext,
localize_stdlib_stub_symbols, localize_stdlib_stub_symbols_for_windows, rust_target_triple,
strip_duplicate_objects_from_lib, strip_duplicate_objects_from_well_known_lib,
windows_pe_subsystem_flag, windows_subsystem_needs_ui, CompilationContext,
};

mod build_and_run;
Expand Down
174 changes: 173 additions & 1 deletion crates/perry/src/commands/compile/strip_dedup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,13 @@ fn try_localize_stdlib_stub_symbols(runtime_lib: &Path, stdlib_lib: &Path) -> Re
}
let member_path = extract_dir.join(member);
if !member_path.exists() {
continue;
// `llvm-ar x` returned success but produced no file (e.g. a member
// name that doesn't round-trip as a path). Don't silently skip: that
// would return a "localized" archive with this member's stubs still
// global. Fail so the caller falls back to the untouched runtime.
return Err(anyhow::anyhow!(
"failed to extract {member}: member file was not created"
));
}
let mut objcopy_cmd = Command::new(&objcopy);
for symbol in symbols {
Expand Down Expand Up @@ -999,6 +1005,172 @@ fn try_localize_stdlib_stub_symbols(runtime_lib: &Path, stdlib_lib: &Path) -> Re
Ok(trimmed_lib)
}

/// macOS/Linux (#5000) equivalent of [`localize_stdlib_stub_symbols_for_windows`].
///
/// The prebuilt standalone `libperry_runtime.a` is built WITHOUT the `stdlib`
/// Cargo feature, so it defines the no-op `stdlib_stubs` symbols. On the
/// macOS/Linux link line it is linked alongside the auto-optimized perry-stdlib
/// (which carries the REAL `js_fetch_with_options` / `js_headers_new` / `js_ws_*`
/// / `js_readline_*`), and with archive first-definition-wins the runtime stub
/// can satisfy the user's fetch reference first — so `fetch()` silently no-ops
/// (`[perry] warning: js_headers_new is a no-op stub`) and a program awaiting the
/// fetch hangs. Unlike COFF, ELF/Mach-O accept `--localize-symbol`, so localize
/// (global→local) exactly those stub symbols the runtime defines AND perry-stdlib
/// also provides; the now-local stub no longer satisfies the external reference,
/// the linker resolves it from perry-stdlib, and `-dead_strip`/`--gc-sections`
/// drops the unreferenced stub body. The stdlib cross-check guarantees a
/// runtime-only symbol is never localized. Best-effort: returns `runtime_lib`
/// unchanged on any failure, preserving pre-fix behavior.
pub(super) fn localize_stdlib_stub_symbols(runtime_lib: &Path, stdlib_lib: &Path) -> PathBuf {
match try_localize_stdlib_stub_symbols_unix(runtime_lib, stdlib_lib) {
Ok(p) => p,
Err(e) => {
eprintln!("[strip-dedup] runtime stdlib-stub localize skipped (non-fatal): {e}");
runtime_lib.to_path_buf()
}
}
}

fn try_localize_stdlib_stub_symbols_unix(runtime_lib: &Path, stdlib_lib: &Path) -> Result<PathBuf> {
let lib_name = runtime_lib
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("libperry_runtime.a");

// Mach-O requires a matched-LLVM `llvm-objcopy` for `--localize-symbol`
// (a mismatched one rejects it on Mach-O / `llvm-nm` mis-reads nightly
// bitcode), so prefer the nightly toolchain tool, mirroring
// `strip_duplicate_objects_from_well_known_lib`.
let llvm_ar = find_llvm_tool("llvm-ar")
.or_else(|| find_path_tool("ar"))
.ok_or_else(|| anyhow::anyhow!("llvm-ar not found"))?;
let objcopy = find_nightly_llvm_tool("llvm-objcopy")
.or_else(|| find_llvm_tool("llvm-objcopy"))
.or_else(|| find_path_tool("objcopy"))
.ok_or_else(|| anyhow::anyhow!("llvm-objcopy not found"))?;
let nm = find_nightly_llvm_tool("llvm-nm")
.or_else(|| find_llvm_tool("llvm-nm"))
.or_else(|| find_path_tool("nm"))
.ok_or_else(|| anyhow::anyhow!("llvm-nm not found"))?;

let abs_runtime = std::fs::canonicalize(runtime_lib)?;
let abs_stdlib = std::fs::canonicalize(stdlib_lib)?;

let stub_set: std::collections::HashSet<&str> = STDLIB_STUB_SYMBOLS.iter().copied().collect();

let runtime_member_syms = collect_archive_symbols_by_member(&nm, &abs_runtime)
.ok_or_else(|| anyhow::anyhow!("failed to inspect {lib_name} symbols"))?;
let mut candidates: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for (member, syms) in &runtime_member_syms {
let hits: Vec<String> = syms
.iter()
.filter(|s| stub_set.contains(s.as_str()))
.cloned()
.collect();
if !hits.is_empty() {
candidates.insert(member.clone(), hits);
}
}
if candidates.is_empty() {
// Runtime built without the stubs (e.g. `stdlib` feature on).
return Ok(runtime_lib.to_path_buf());
}

// Cross-check against perry-stdlib: only localize a stub the real stdlib also
// provides, so we never turn a runtime-only symbol into an undefined ref.
let stdlib_syms = collect_archive_symbols_flat(&nm, &abs_stdlib);
if stdlib_syms.is_empty() {
return Err(anyhow::anyhow!(
"llvm-nm reported no symbols for {}",
abs_stdlib.display()
));
}
let mut to_localize: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for (member, hits) in candidates {
let mut kept: Vec<String> = hits
.into_iter()
.filter(|s| stdlib_syms.contains(s))
.collect();
if !kept.is_empty() {
kept.sort();
kept.dedup();
to_localize.insert(member, kept);
}
}
if to_localize.is_empty() {
return Ok(runtime_lib.to_path_buf());
}

let tmp_base = std::env::temp_dir().join(format!("perry_strip_{}", std::process::id()));
std::fs::create_dir_all(&tmp_base).ok();
let extract_dir = tmp_base.join(format!("_{lib_name}_stub_localize_extract"));
let _ = std::fs::remove_dir_all(&extract_dir);
std::fs::create_dir_all(&extract_dir)?;
let trimmed_lib = tmp_base.join(format!("_{lib_name}_stub_localized.a"));
let _ = std::fs::remove_file(&trimmed_lib);
std::fs::copy(&abs_runtime, &trimmed_lib)?;

let mut localized = 0usize;
for (member, symbols) in &to_localize {
let extract_out = Command::new(&llvm_ar)
.arg("x")
.arg(&abs_runtime)
.arg(member)
.current_dir(&extract_dir)
.output()?;
if !extract_out.status.success() {
let stderr = String::from_utf8_lossy(&extract_out.stderr);
return Err(anyhow::anyhow!("failed to extract {member}: {stderr}"));
}
let member_path = extract_dir.join(member);
if !member_path.exists() {
// `llvm-ar x` returned success but produced no file (e.g. a member
// name that doesn't round-trip as a path). Don't silently skip: that
// would return a "localized" archive with this member's stubs still
// global. Fail so the caller falls back to the untouched runtime.
return Err(anyhow::anyhow!(
"failed to extract {member}: member file was not created"
));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let mut objcopy_cmd = Command::new(&objcopy);
for symbol in symbols {
objcopy_cmd.arg("--localize-symbol").arg(symbol);
}
let out = objcopy_cmd.arg(&member_path).output()?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(anyhow::anyhow!(
"failed to localize stub symbols in {member}: {stderr}"
));
}
let replace_out = Command::new(&llvm_ar)
.arg("r")
.arg(&trimmed_lib)
.arg(&member_path)
.output()?;
if !replace_out.status.success() {
let stderr = String::from_utf8_lossy(&replace_out.stderr);
return Err(anyhow::anyhow!("failed to splice {member}: {stderr}"));
}
localized += symbols.len();
}

let index_out = Command::new(&llvm_ar).arg("s").arg(&trimmed_lib).output()?;
if !index_out.status.success() {
let stderr = String::from_utf8_lossy(&index_out.stderr);
return Err(anyhow::anyhow!("failed to reindex {lib_name}: {stderr}"));
}

eprintln!(
"[strip-dedup] {lib_name}: localized {localized} stdlib-stub symbol(s) \
so perry-stdlib wins the link (#5000, macOS/Linux)"
);
let _ = std::fs::remove_dir_all(&extract_dir);
Ok(trimmed_lib)
}

/// Tier-3 (tvOS/watchOS, no prebuilt std): perry-stdlib is built with
/// `-Zbuild-std` and bundles its own copy of std's allocator/panic runtime
/// shims, which duplicate the ones in runtime_lib (the canonical provider) →
Expand Down
Loading