use super::*;
pub(crate) fn find_existing_libs(dir: &Path) -> (HashSet<String>, Vec<PathBuf>) {
let mut libs = HashSet::new();
let mut stubs = Vec::new();
for entry in jwalk::WalkDir::new(dir).skip_hidden(false).sort(true) {
let Ok(entry) = entry else { continue };
let path = entry.path();
let Some(name) = path.file_name() else {
continue;
};
let name = name.to_string_lossy();
if !name.contains(".so") {
continue;
}
if is_nix_stub_ld(&path) {
stubs.push(path.clone());
continue;
}
libs.insert(name.into_owned());
}
(libs, stubs)
}
pub(crate) fn build_lib_cache() -> HashMap<String, Vec<PathBuf>> {
let cache = parse_ldconfig_cache();
if !cache.is_empty() {
return cache;
}
if Path::new("/nix/store").is_dir() {
return scan_nix_store_libs();
}
cache
}
pub(crate) fn parse_ldconfig_cache() -> HashMap<String, Vec<PathBuf>> {
let mut cache: HashMap<String, Vec<PathBuf>> = HashMap::new();
let output = ["ldconfig", "/sbin/ldconfig", "/usr/sbin/ldconfig"]
.into_iter()
.find_map(|prog| Command::new(prog).arg("-p").output().ok());
let Some(output) = output else {
return cache;
};
for line in output.stdout.lines().map_while(Result::ok) {
let line = line.trim();
if let Some((left, right)) = line.split_once(" => ") {
let soname = left.split_whitespace().next().unwrap_or("");
if !soname.is_empty() {
cache
.entry(soname.to_string())
.or_default()
.push(PathBuf::from(right.trim()));
}
}
}
cache
}
pub(crate) fn scan_nix_store_libs() -> HashMap<String, Vec<PathBuf>> {
let mut cache: HashMap<String, Vec<PathBuf>> = HashMap::new();
let store_paths = nix_closure_roots();
if store_paths.is_empty() {
return cache;
}
let lib_dirs: Vec<PathBuf> = store_paths
.iter()
.map(|p| PathBuf::from(p).join("lib"))
.filter(|p| p.is_dir())
.collect();
eprintln!(
"{} scanning {} store paths...",
color::dim("NixOS detected,"),
lib_dirs.len()
);
for lib_dir in &lib_dirs {
for entry in jwalk::WalkDir::new(lib_dir)
.max_depth(3)
.skip_hidden(false)
.sort(true)
{
let Ok(entry) = entry else { continue };
if !entry.path().is_file() {
continue;
}
if let Some(name) = entry.path().file_name() {
let name = name.to_string_lossy();
if name.contains(".so") {
cache
.entry(name.into_owned())
.or_default()
.push(entry.path());
}
}
}
}
cache
}
pub(crate) fn nix_store_path(path: &Path) -> Option<PathBuf> {
let s = path.to_string_lossy();
let rest = s.strip_prefix("/nix/store/")?;
let end = rest.find('/').unwrap_or(rest.len());
Some(PathBuf::from(format!("/nix/store/{}", &rest[..end])))
}
pub(crate) fn expand_nix_cache(
resolved: &Path,
cache: &mut HashMap<String, Vec<PathBuf>>,
expanded: &mut HashSet<PathBuf>,
) {
let store_path = match nix_store_path(resolved) {
Some(p) => p,
None => return,
};
if !expanded.insert(store_path.clone()) {
return; }
let Ok(output) = Command::new("nix-store")
.args(["-qR"])
.arg(&store_path)
.output()
else {
return;
};
if !output.status.success() {
return;
}
for line in output.stdout.lines().map_while(Result::ok) {
let lib_dir = PathBuf::from(line.trim()).join("lib");
if !lib_dir.is_dir() {
continue;
}
for entry in jwalk::WalkDir::new(&lib_dir)
.max_depth(3)
.skip_hidden(false)
.sort(true)
{
let Ok(entry) = entry else { continue };
if !entry.path().is_file() {
continue;
}
if let Some(name) = entry.path().file_name() {
let name = name.to_string_lossy();
if name.contains(".so") {
let paths = cache.entry(name.into_owned()).or_default();
let path = entry.path();
if !paths.contains(&path) {
paths.push(path);
}
}
}
}
}
}
pub(crate) fn locate_lib(
soname: &str,
ldconfig_cache: &HashMap<String, Vec<PathBuf>>,
search_paths: &[PathBuf],
target_class: Option<u8>,
target_machine: Option<u16>,
) -> Option<PathBuf> {
let class_matches = |path: &Path| -> bool {
match target_class {
Some(tc) => read_elf_class(path) == Some(tc),
None => true,
}
};
let machine_matches = |path: &Path| -> bool {
match target_machine {
Some(tm) => read_elf_machine(path) == Some(tm),
None => true,
}
};
let acceptable =
|path: &Path| class_matches(path) && machine_matches(path) && !is_nix_stub_ld(path);
for dir in search_paths {
let candidate = dir.join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
}
if let Some(paths) = ldconfig_cache.get(soname) {
let mut sorted: Vec<&PathBuf> = paths.iter().collect();
sorted.sort();
for path in sorted {
if path.exists() && acceptable(path) {
return Some(path.clone());
}
}
}
for dir in STANDARD_LIB_PATHS {
let candidate = Path::new(dir).join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
}
for var in ["LD_LIBRARY_PATH", "NIX_LD_LIBRARY_PATH"] {
if let Ok(val) = std::env::var(var) {
for dir in val.split(':') {
if dir.is_empty() {
continue;
}
let candidate = Path::new(dir).join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
}
}
}
if Path::new("/nix/store").is_dir() {
if let Ok(entries) = fs::read_dir("/nix/store") {
let mut dirs: Vec<PathBuf> = entries.filter_map(Result::ok).map(|e| e.path()).collect();
dirs.sort();
for dir in dirs {
let lib_dir = dir.join("lib");
let candidate = lib_dir.join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
if let Ok(subdirs) = fs::read_dir(&lib_dir) {
let mut subs: Vec<PathBuf> = subdirs
.filter_map(Result::ok)
.filter(|s| s.file_type().is_ok_and(|t| t.is_dir()))
.map(|s| s.path())
.collect();
subs.sort();
for subdir in subs {
let candidate = subdir.join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
}
}
}
}
}
None
}
pub(crate) fn is_nix_stub_ld(path: &Path) -> bool {
let is_loader_name = path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("ld-"));
if !is_loader_name {
return false;
}
if let Ok(real) = fs::canonicalize(path) {
if real.to_string_lossy().contains("stub-ld") {
return true;
}
}
let Ok(meta) = fs::metadata(path) else {
return false;
};
if !meta.is_file() || meta.len() > 128 * 1024 {
return false;
}
let Ok(bytes) = fs::read(path) else {
return false;
};
if bytes.len() < 4 || bytes[..4] != *b"\x7fELF" {
return false;
}
bytes
.windows(b"NixOS cannot run".len())
.any(|w| w == b"NixOS cannot run")
}
pub(crate) fn nix_closure_roots() -> Vec<String> {
let mut store_paths: HashSet<String> = HashSet::new();
let mut query_paths: Vec<PathBuf> = vec![PathBuf::from("/run/current-system")];
if let Ok(home) = std::env::var("HOME") {
query_paths.push(PathBuf::from(format!("{home}/.nix-profile")));
}
if let Ok(entries) = fs::read_dir("/etc/profiles/per-user") {
for entry in entries.flatten() {
query_paths.push(entry.path());
}
}
for qp in &query_paths {
if !qp.exists() {
continue;
}
let Ok(output) = Command::new("nix-store").arg("-qR").arg(qp).output() else {
continue;
};
if output.status.success() {
for line in output.stdout.lines().map_while(Result::ok) {
store_paths.insert(line.trim().to_string());
}
}
}
let mut result: Vec<String> = store_paths.into_iter().collect();
result.sort();
result
}