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
123 changes: 123 additions & 0 deletions crates/perry/src/commands/compile/library_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,84 @@ pub(super) fn find_llvm_tool(tool_name: &str) -> Option<PathBuf> {
}
}

// 4. Well-known LLVM install directories (#5779). The prebuilt runtime/stdlib
// and well-known-wrapper archives carry LLVM *bitcode* emitted by the
// active Rust toolchain's LLVM (thin-LTO). Inspecting/rewriting them for
// the runtime-dedup strip (see `strip_bundled_runtime_from_well_known_lib`)
// needs an `llvm-nm`/`llvm-ar`/`llvm-objcopy` whose LLVM is >= the
// toolchain's — an older one fails (`nm: ... Unknown attribute kind`) and
// the strip silently no-ops, leaving two disjoint copies of perry-runtime's
// event-loop globals (the #5779 in-process server+fetch deadlock). The
// system `nm` on macOS (Apple LLVM) is routinely too old, and Homebrew's
// LLVM is keg-only (not on PATH), so it is missed by the checks above.
// Search the standard keg/versioned locations, preferring the newest, so a
// matching tool is found without the user having to add the toolchain's
// `llvm-tools` component or put Homebrew LLVM on PATH.
if let Some(p) = find_tool_in_well_known_llvm_dirs(tool_name) {
return Some(p);
}

None
}

/// Fallback LLVM-tool locator used by [`find_llvm_tool`]: scan the standard
/// LLVM install directories that are commonly NOT on `PATH` — Homebrew's
/// keg-only LLVM on macOS (`/opt/homebrew/opt/llvm`, `/usr/local/opt/llvm`,
/// including versioned `llvm@NN` kegs) and Debian/Ubuntu's `/usr/lib/llvm-NN`.
/// Versioned directories are tried newest-first so the returned tool is as new
/// as possible (bitcode inspection needs LLVM >= the Rust toolchain's).
fn find_tool_in_well_known_llvm_dirs(tool_name: &str) -> Option<PathBuf> {
let exe = format!("{tool_name}{}", std::env::consts::EXE_SUFFIX);

// Unversioned "current" kegs/prefixes first.
let mut dirs: Vec<PathBuf> = vec![
PathBuf::from("/opt/homebrew/opt/llvm/bin"),
PathBuf::from("/usr/local/opt/llvm/bin"),
];

// Versioned kegs/installs, newest first. `(parent, prefix)`: e.g.
// `/opt/homebrew/opt/llvm@18` and `/usr/lib/llvm-18`.
for (parent, prefix) in [
("/opt/homebrew/opt", "llvm@"),
("/usr/local/opt", "llvm@"),
("/usr/lib", "llvm-"),
] {
let names: Vec<String> = std::fs::read_dir(parent)
.map(|entries| {
entries
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect()
})
.unwrap_or_default();
dirs.extend(versioned_llvm_bin_dirs(parent, prefix, &names));
}

dirs.into_iter()
.map(|d| d.join(&exe))
.find(|candidate| candidate.is_file())
}

/// Pure helper for [`find_tool_in_well_known_llvm_dirs`]: given the entry
/// `names` found directly under `parent`, return the `<parent>/<name>/bin`
/// paths for entries shaped `<prefix><MAJOR>[...]` (e.g. `llvm@18`, `llvm-17`),
/// ordered newest major version first. Non-matching or non-numeric entries are
/// ignored. Kept separate (no filesystem access) so the version ordering is
/// unit-testable.
fn versioned_llvm_bin_dirs(parent: &str, prefix: &str, names: &[String]) -> Vec<PathBuf> {
let mut versioned: Vec<(u32, PathBuf)> = names
.iter()
.filter_map(|name| {
let ver = name.strip_prefix(prefix)?;
let end = ver.find(|c: char| !c.is_ascii_digit()).unwrap_or(ver.len());
let n = ver[..end].parse::<u32>().ok()?;
Some((n, Path::new(parent).join(name).join("bin")))
})
.collect();
versioned.sort_by_key(|k| std::cmp::Reverse(k.0));
versioned.into_iter().map(|(_, p)| p).collect()
}

/// Find MSVC link.exe by searching Visual Studio installation directories.
/// On Windows, the PATH may contain a GNU `link` utility (e.g. from Git Bash/MSYS2)
/// which is not the MSVC linker. This function searches for the real MSVC link.exe.
Expand Down Expand Up @@ -1698,6 +1773,54 @@ mod native_lib_artifact_tests {
}
}

#[cfg(test)]
mod llvm_tool_discovery_tests {
use super::versioned_llvm_bin_dirs;
use std::path::PathBuf;

// #5779: the well-known-LLVM-dir fallback must return versioned kegs
// newest-first, so bitcode inspection uses an LLVM at least as new as the
// Rust toolchain's. Ordering is the only non-trivial part, and it is pure,
// so exercise it directly without touching the filesystem.
#[test]
fn versioned_llvm_dirs_are_newest_first() {
let names = vec![
"llvm@15".to_string(),
"llvm@18".to_string(),
"llvm@9".to_string(),
"llvm".to_string(), // unversioned — ignored here
"node@20".to_string(), // wrong prefix — ignored
];
let dirs = versioned_llvm_bin_dirs("/opt/homebrew/opt", "llvm@", &names);
assert_eq!(
dirs,
vec![
PathBuf::from("/opt/homebrew/opt/llvm@18/bin"),
PathBuf::from("/opt/homebrew/opt/llvm@15/bin"),
PathBuf::from("/opt/homebrew/opt/llvm@9/bin"),
]
);
}

#[test]
fn versioned_llvm_dirs_handles_debian_prefix_and_ignores_nonmatching() {
let names = vec![
"llvm-17".to_string(),
"llvm-16.0".to_string(),
"gcc-12".to_string(),
"llvm-".to_string(), // no numeric version — ignored
];
let dirs = versioned_llvm_bin_dirs("/usr/lib", "llvm-", &names);
assert_eq!(
dirs,
vec![
PathBuf::from("/usr/lib/llvm-17/bin"),
PathBuf::from("/usr/lib/llvm-16.0/bin"),
]
);
}
}

#[cfg(all(test, target_os = "windows"))]
mod windows_toolchain_tests {
use super::msvc_vswhere_installation_path_args;
Expand Down
11 changes: 11 additions & 0 deletions crates/perry/src/commands/compile/link/build_and_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ pub(crate) fn build_and_run_link(
"[strip-dedup] bundled-runtime drop skipped for {} (non-fatal): {e}",
wk.display()
);
// #5779: skipping this leaves two copies of perry-runtime's
// mutable globals; an in-process HTTP server + fetch program
// can then deadlock. It fails when no LLVM tool new enough to
// read the archives' bitcode is found — install the toolchain's
// llvm-tools (`rustup component add llvm-tools`) or a current LLVM.
eprintln!(
"[strip-dedup] warning: could not de-duplicate the bundled \
perry-runtime — an in-process server+fetch program may \
deadlock (#5779). Install `rustup component add llvm-tools` \
or a current LLVM (e.g. `brew install llvm`)."
);
wk.clone()
}),
None => wk.clone(),
Expand Down
Loading