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>,
sysroot: Option<&Path>,
) -> 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);
let real = match sysroot.and_then(|root| Some((root, candidate.strip_prefix(root).ok()?))) {
Some((root, rel)) => match onelf_sysroot::path::in_root(root, rel) {
Ok(real) => real,
Err(_) => continue,
},
None => candidate.clone(),
};
if real.exists() && acceptable(&real) {
return Some(candidate);
}
}
if sysroot.is_some() {
return None;
}
if let Some(paths) = ldconfig_cache.get(soname) {
for path in paths {
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()
&& 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)
&& 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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sysroot_candidate_is_read_through_links_inside_the_sysroot() {
let td = std::env::temp_dir().join(format!("onelf-locate-{}", std::process::id()));
let _ = fs::remove_dir_all(&td);
let root = td.join("sysroot");
let host = td.join("host");
fs::create_dir_all(root.join("usr/lib")).unwrap();
fs::create_dir_all(&host).unwrap();
fs::write(host.join("libhost.so.1"), b"host").unwrap();
fs::write(root.join("usr/lib/libfoo.so.1.2"), b"sysroot").unwrap();
std::os::unix::fs::symlink("/usr/lib/libfoo.so.1.2", root.join("usr/lib/libfoo.so.1"))
.unwrap();
std::os::unix::fs::symlink(host.join("libhost.so.1"), root.join("usr/lib/libhost.so.1"))
.unwrap();
let dirs = [root.join("usr/lib")];
let locate = |soname| locate_lib(soname, &HashMap::new(), &dirs, None, None, Some(&root));
assert_eq!(
locate("libfoo.so.1"),
Some(root.join("usr/lib/libfoo.so.1"))
);
assert_eq!(
locate("libhost.so.1"),
None,
"the packer's file is not the sysroot's"
);
let _ = fs::remove_dir_all(&td);
}
#[test]
fn the_cache_order_decides_between_copies() {
let root = std::env::temp_dir().join(format!("onelf-cacheorder-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
let local = root.join("usr/local/lib");
let multiarch = root.join("usr/lib/x86_64-linux-gnu");
for dir in [&local, &multiarch] {
fs::create_dir_all(dir).unwrap();
fs::write(dir.join("libfoo.so.1"), b"lib").unwrap();
}
let cache = HashMap::from([(
"libfoo.so.1".to_string(),
vec![local.join("libfoo.so.1"), multiarch.join("libfoo.so.1")],
)]);
let found = locate_lib("libfoo.so.1", &cache, &[], None, None, None);
let _ = fs::remove_dir_all(&root);
assert_eq!(found, Some(local.join("libfoo.so.1")));
}
}