fix(dso): allow specifying extra lib folders on the cmdline

Before we were relying on LD_LIBRARY_PATH, but it is misleading
as it will most likely also affect the sysroot-cleaner binary itself.

This instead introduces a separate command line argument,
`--ld-path`, to specify a list of paths to search.

Also includes some minor refactoring.
This commit is contained in:
Matteo Settenvini 2025-06-02 19:04:14 +02:00
parent 6d06377154
commit 1922297b4a
Signed by: matteo
GPG key ID: 1C1B12600D81DE05
5 changed files with 112 additions and 95 deletions

View file

@ -4,8 +4,8 @@
use std::path::PathBuf;
use clap::{
Parser,
builder::{PathBufValueParser, TypedValueParser},
Parser,
};
#[derive(Clone, Default)]
@ -59,4 +59,20 @@ pub struct Args {
/// The location of the sysroot to clean up
pub sysroot_location: PathBuf,
/// An additional path to consider when resolving
/// libraries, relative to the sysroot.
/// Their behavior is similar of the one of the
/// `LD_LIBRARY_PATH` environment variable when
/// specified to the dynamic linker.
#[arg(long = "ld-path", value_parser = relativize_path)]
pub extra_library_paths: Vec<PathBuf>,
}
fn relativize_path(arg: &str) -> anyhow::Result<PathBuf> {
let mut p = PathBuf::from(arg);
if p.is_absolute() {
p = p.strip_prefix("/")?.into();
}
Ok(p)
}

View file

@ -64,7 +64,10 @@ impl Runner {
cleaners.push(Box::new(ListCleaner::new(list::ListType::Block, bl)));
}
cleaners.push(Box::new(DsoCleaner::new(args.output_dotfile)));
cleaners.push(Box::new(DsoCleaner::new(
args.extra_library_paths,
args.output_dotfile,
)));
Self {
cleaners,

View file

@ -27,17 +27,22 @@ type InodeGraph = DiGraphMap<ino_t, ()>;
/// Cleans up unused shared libraries
/// and warns about broken dependencies as well
pub struct DsoCleaner {
extra_library_paths: Vec<PathBuf>,
output_dot: Option<PathBuf>,
}
struct State {
ld_library_path: Vec<String>,
paths_map: InodeMap,
graph: InodeGraph,
}
impl DsoCleaner {
pub fn new(output_dot: Option<PathBuf>) -> Self {
Self { output_dot }
pub fn new(extra_library_paths: Vec<PathBuf>, output_dot: Option<PathBuf>) -> Self {
Self {
extra_library_paths,
output_dot,
}
}
}
@ -52,6 +57,11 @@ impl Cleaner for DsoCleaner {
output: mpsc::Sender<Decision>,
) -> Result<()> {
let mut state = State::default();
state.ld_library_path = self
.extra_library_paths
.iter()
.map(|p| p.to_str().unwrap().to_owned())
.collect();
let mut inodes_to_keep = HashSet::new();
inodes_to_keep.insert(ROOT_NODE);
@ -127,7 +137,11 @@ impl Default for State {
paths_map.insert(ROOT_NODE, HashSet::from([fake_root_node]));
graph.add_node(ROOT_NODE);
Self { paths_map, graph }
Self {
ld_library_path: vec![],
paths_map,
graph,
}
}
}
@ -196,7 +210,7 @@ impl State {
self.update_graph("".into(), ROOT_NODE, path.to_owned(), src.st_ino);
}
let search_paths = determine_lib_search_paths(path, elf)?;
let search_paths = self.determine_lib_search_paths(path, elf)?;
log::trace!("determined search paths {:#?}", search_paths);
'next_lib: for &library in elf.libraries.iter() {
@ -292,61 +306,45 @@ impl State {
)?;
Ok(())
}
}
fn determine_lib_search_paths(path: &Path, elf: &Elf<'_>) -> Result<Vec<String>> {
log::trace!("elf.runpaths = {:#?}", elf.runpaths);
log::trace!("elf.rpaths = {:#?}", elf.rpaths);
// Contract: only relative paths starting from the sysroot dir should be returned from this function
fn determine_lib_search_paths(&self, path: &Path, elf: &Elf<'_>) -> Result<Vec<String>> {
log::trace!(
"{}: elf.runpaths = {:#?}, elf.rpaths = {:#?}",
path.display(),
elf.runpaths,
elf.rpaths
);
let mut search_paths = vec![];
let current_dir = std::env::current_dir()?;
let origin = fs::canonicalize(path)?
.parent()
.unwrap()
.strip_prefix(current_dir)?
.to_path_buf()
.into_os_string()
.into_string()
.map_err(|s| anyhow::anyhow!("cannot represent {:?} as a UTF-8 string", s))?;
let current_dir = std::env::current_dir()?;
let origin = fs::canonicalize(path)?
.parent()
.unwrap()
.strip_prefix(current_dir)?
.to_path_buf()
.into_os_string()
.into_string()
.map_err(|s| anyhow::anyhow!("cannot represent {:?} as a UTF-8 string", s))?;
let mut search_paths = vec![];
if elf.runpaths.is_empty() {
search_paths.extend(collect_paths(&elf.rpaths, &origin));
}
if elf.runpaths.is_empty() {
collect_paths(&elf.rpaths, &mut search_paths, &origin);
}
search_paths.append(&mut get_env_library_paths());
collect_paths(&elf.runpaths, &mut search_paths, &origin);
search_paths.push("usr/local/lib".into());
search_paths.push("lib".into());
search_paths.push("usr/lib".into());
Ok(search_paths)
}
fn collect_paths(elf_paths: &Vec<&str>, search_paths: &mut Vec<String>, origin: &str) {
if elf_paths.iter().any(|p| !p.is_empty()) {
let mut paths = elf_paths
.iter()
.flat_map(|p| p.split(':'))
.map(|p| p.replace("$ORIGIN", origin))
.map(|p| p.trim_start_matches('/').to_string())
.collect::<Vec<_>>();
search_paths.append(&mut paths);
search_paths.extend(self.ld_library_path.clone());
search_paths.extend(collect_paths(&elf.runpaths, &origin));
search_paths.extend(["usr/local/lib".into(), "lib".into(), "usr/lib".into()]);
Ok(search_paths)
}
}
fn get_env_library_paths() -> Vec<String> {
let ld_config_path = std::env::var("LD_LIBRARY_PATH");
ld_config_path
.as_ref()
.map(|env| {
env.split(':')
.filter(|s| s.is_empty())
.map(|s| s.into())
.collect()
})
.unwrap_or_default()
fn collect_paths(elf_paths: &Vec<&str>, origin: &str) -> impl Iterator<Item = String> {
elf_paths
.iter()
.flat_map(|&p| p.split(':')) // Split multiple elements in r(un)?path separated by ':'
.filter(|&p| !p.is_empty()) // ignore empty items
.map(|p| p.replace("$ORIGIN", origin)) // replace $ORIGIN with path rel to sysroot
.map(|p| p.trim_start_matches('/').to_string()) // relativize paths from sysroot
}
fn is_elf(f: &mut File) -> Result<bool> {