git-ents.gitmain
⌘K
foforge
commit d234142
fix: relocate rustup toolchain libs so an extracted rustc actually runs

git-toolchain activates a toolchain by extracting bin as-is straight onto PATH, with no sibling lib beside it. A rustup sysroot’s bin/* binaries load lib/*.dylib (or .so) via a bin-relative rpath, so a bin-only import produced a rustc that could neither load its own runtime nor find its own standard library once extracted.

fix: stage bin’s executables flat alongside a lib/ subdirectory copied from the sysroot, with each binary’s rpath rewritten from ../lib to lib so it resolves relative to wherever the toolchain lands Assisted-by: Claude:claude-sonnet-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/git-ents/src/registry.rs @@ -6,17 +6,26 @@ //! to discover. This module only locates and describes what's already on //! disk; it never installs a toolchain. -use std::path::PathBuf; +use std::fs; +use std::os::unix::fs::PermissionsExt as _; +use std::path::{Path, PathBuf}; use std::process::Command; +use tempfile::TempDir; + /// What a recipe resolved from a local toolchain install, ready to hand to /// `git_toolchain::import`. +/// +/// `_staging`, when present, is a temporary directory `bin` (and `src`, if +/// under it) points into; it is kept alive only so the directory survives +/// until the caller's `import()` call has read it, and is deleted on drop. pub struct Resolved { pub bin: PathBuf, pub src: Option<PathBuf>, pub license: String, pub version: String, pub platform: String, + _staging: Option<TempDir>, } /// Resolve `recipe` against `spec` (a recipe-specific selector, e.g. a @@ -33,10 +42,23 @@ /// Resolve a rustup-managed toolchain named `spec` (e.g. `stable`, /// `1.75.0`, `nightly`) via `rustc +<spec> -vV`, which reports the /// toolchain's own `release` (its version) and `host` (its target platform) -/// without needing rustup's own metadata format. `bin` is `<sysroot>/bin`; -/// `src` is `<sysroot>/lib/rustlib/src/rust` when the `rust-src` component -/// is installed, else omitted. Rust's own toolchain is dual-licensed -/// `MIT OR Apache-2.0`. +/// without needing rustup's own metadata format. +/// +/// A rustup sysroot's `bin/*` binaries are linked against `lib/*.dylib` (or +/// `.so`) via an rpath relative to `bin`'s own parent (`@loader_path/../lib` +/// on macOS, `$ORIGIN/../lib` on Linux) — but `git-toolchain` activates a +/// toolchain by extracting `bin` as-is and putting *that* directory straight +/// on `PATH`, with no sibling `lib` beside it. Passing `sysroot/bin` alone +/// therefore produces a `rustc` that can neither load its own shared runtime +/// nor find its own standard library to link against. This function instead +/// stages a self-contained directory: `sysroot/bin`'s executables copied to +/// its top level (so `PATH` still finds them directly) plus the whole of +/// `sysroot/lib` copied under a `lib/` subdirectory inside it, with each +/// binary's rpath rewritten from `../lib` to `lib` so it resolves relative +/// to wherever the toolchain ends up extracted, not relative to `bin`'s +/// original location. `src` is `<sysroot>/lib/rustlib/src/rust`, unstaged, +/// when the `rust-src` component is installed, else omitted. Rust's own +/// toolchain is dual-licensed `MIT OR Apache-2.0`. fn rustup(spec: &str) -> Result<Resolved, String> { let toolchain_arg = format!("+{spec}"); let sysroot = rustc(&toolchain_arg, &["--print", "sysroot"])?; @@ -48,19 +70,123 @@ let platform = verbose_field(&verbose, "host") .ok_or_else(|| format!("rustc +{spec} -vV did not report a host"))?; - let bin = sysroot.join("bin"); + let staging = tempfile::tempdir() + .map_err(|error| format!("could not create a staging directory: {error}"))?; + stage_bin(&sysroot.join("bin"), &sysroot.join("lib"), staging.path())?; + let src = sysroot.join("lib/rustlib/src/rust"); let src = src.is_dir().then_some(src); Ok(Resolved { - bin, + bin: staging.path().to_owned(), src, license: "MIT OR Apache-2.0".to_owned(), version, platform, + _staging: Some(staging), }) } +/// Copy `bin_src`'s executables flat into `staging`, relink each one's rpath +/// from `bin`-relative (`../lib`) to `staging`-relative (`lib`), then copy +/// the whole of `lib_src` under `staging/lib`. +fn stage_bin(bin_src: &Path, lib_src: &Path, staging: &Path) -> Result<(), String> { + for entry in fs::read_dir(bin_src) + .map_err(|error| format!("could not read {}: {error}", bin_src.display()))? + { + let entry = + entry.map_err(|error| format!("could not read {}: {error}", bin_src.display()))?; + let dest = staging.join(entry.file_name()); + fs::copy(entry.path(), &dest) + .map_err(|error| format!("could not copy {}: {error}", entry.path().display()))?; + let mut perms = fs::metadata(&dest) + .map_err(|error| format!("could not read {}: {error}", dest.display()))? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(&dest, perms) + .map_err(|error| format!("could not set permissions on {}: {error}", dest.display()))?; + relink_rpath(&dest)?; + } + copy_dir_all(lib_src, &staging.join("lib")) +} + +/// Rewrite a copied rustup binary's rpath so it finds its runtime libraries +/// relative to wherever it ends up on disk (`staging/lib`, later +/// `<extracted-toolchain>/lib`) rather than relative to its original +/// `sysroot/bin` location. Failures are ignored: not every entry under +/// `bin/` is a binary carrying this rpath (`rust-gdb`, `rust-lldb`, ... are +/// shell scripts), and a tool that never dynamically links against `lib/` +/// needs no relinking. +fn relink_rpath(path: &Path) -> Result<(), String> { + match std::env::consts::OS { + "macos" => { + drop( + Command::new("install_name_tool") + .args(["-rpath", "@loader_path/../lib", "@loader_path/lib"]) + .arg(path) + .output(), + ); + Ok(()) + } + "linux" => { + let output = Command::new("patchelf") + .args(["--set-rpath", "$ORIGIN/lib"]) + .arg(path) + .output() + .map_err(|error| { + format!( + "could not run patchelf, required to relocate a rustup toolchain's \ + runtime library path on linux: {error}" + ) + })?; + let _ = output; + Ok(()) + } + other => Err(format!( + "the rustup recipe does not know how to relocate a toolchain's runtime library \ + path on {other}" + )), + } +} + +/// Recursively copy `src` to `dst`, preserving permissions and symlinks — +/// `std::fs` has no directory-copy of its own. +fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> { + fs::create_dir_all(dst) + .map_err(|error| format!("could not create {}: {error}", dst.display()))?; + for entry in + fs::read_dir(src).map_err(|error| format!("could not read {}: {error}", src.display()))? + { + let entry = entry.map_err(|error| format!("could not read {}: {error}", src.display()))?; + let file_type = entry + .file_type() + .map_err(|error| format!("could not read {}: {error}", entry.path().display()))?; + let dest_path = dst.join(entry.file_name()); + if file_type.is_dir() { + copy_dir_all(&entry.path(), &dest_path)?; + } else if file_type.is_symlink() { + let target = fs::read_link(entry.path()).map_err(|error| { + format!("could not read symlink {}: {error}", entry.path().display()) + })?; + std::os::unix::fs::symlink(&target, &dest_path) + .map_err(|error| format!("could not symlink {}: {error}", dest_path.display()))?; + } else { + fs::copy(entry.path(), &dest_path) + .map_err(|error| format!("could not copy {}: {error}", entry.path().display()))?; + let perms = fs::metadata(entry.path()) + .map_err(|error| format!("could not read {}: {error}", entry.path().display()))? + .permissions(); + fs::set_permissions(&dest_path, perms).map_err(|error| { + format!( + "could not set permissions on {}: {error}", + dest_path.display() + ) + })?; + } + } + Ok(()) +} + /// Run `rustc <toolchain_arg> <args>` and return its stdout, so a missing /// toolchain or missing `rustc`/`rustup` shim surfaces as a plain error /// rather than a panic.