use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{self, BufRead};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use onelf_sysroot::{Database, PlatformLine, Policy};
pub(crate) mod ui;
pub(crate) use ui::{color, format_size};
mod datadirs;
mod discover;
pub(crate) mod gpu;
use gpu::{bundle_gpu, bundle_gtk_data, bundle_wayland};
mod resolve;
pub(crate) use resolve::*;
pub(crate) mod elf;
pub mod sysroot;
pub(crate) use elf::*;
pub use sysroot::SysrootOptions;
fn ensure_writable(path: &Path) {
if let Ok(meta) = fs::metadata(path) {
let mode = meta.permissions().mode();
if mode & 0o200 == 0 {
let _ = fs::set_permissions(path, PermissionsExt::from_mode(mode | 0o200));
}
}
}
pub(crate) fn private_temp_dir(tag: &str) -> io::Result<PathBuf> {
use std::os::unix::fs::DirBuilderExt;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let base = std::env::temp_dir();
for _ in 0..100 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = base.join(format!(
"onelf-{tag}-{}-{nanos:08x}{n:x}",
std::process::id()
));
match fs::DirBuilder::new().mode(0o700).create(&dir) {
Ok(()) => return Ok(dir),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("no free scratch directory name under {}", base.display()),
))
}
fn verb_str(dry_run: bool) -> String {
if dry_run {
color::bold("Would copy")
} else {
color::bold_green("Copied")
}
}
fn build_lib_search_dirs(
elf_files: &[PathBuf],
extra_search: &[PathBuf],
nix_store_paths: &[String],
) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
for elf in elf_files {
for rdir in parse_rpaths(elf) {
if rdir.is_dir() && !dirs.contains(&rdir) {
dirs.push(rdir);
}
}
}
for path in STANDARD_LIB_PATHS {
let p = PathBuf::from(path);
if p.is_dir() && !dirs.contains(&p) {
dirs.push(p);
}
}
for sp in nix_store_paths {
let lib = PathBuf::from(sp).join("lib");
if lib.is_dir() && !dirs.contains(&lib) {
dirs.push(lib);
}
}
for dir in extra_search {
if dir.is_dir() && !dirs.contains(dir) {
dirs.push(dir.clone());
}
}
dirs
}
fn is_same_file(src: &Path, dest: &Path) -> bool {
fs::canonicalize(dest).map(|c| c == src).unwrap_or(false)
}
#[allow(clippy::too_many_arguments)]
fn copy_libs(
search_dirs: &[PathBuf],
dest: &Path,
name_pred: impl Fn(&str) -> bool,
target_class: Option<u8>,
target_machine: Option<u16>,
excludes: &[&str],
dry_run: bool,
strip: bool,
) -> io::Result<(usize, u64)> {
let mut copied = 0usize;
let mut total_bytes = 0u64;
let mut seen: HashSet<String> = HashSet::new();
for dir in search_dirs {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !path.is_file() && !path.is_symlink() {
continue;
}
let name = match path.file_name() {
Some(n) => n.to_string_lossy().into_owned(),
None => continue,
};
if !name_pred(&name) {
continue;
}
if is_excluded(&name, excludes) || seen.contains(&name) {
continue;
}
let resolved = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
if !resolved.is_file() {
continue;
}
if let Some(tc) = target_class
&& read_elf_class(&resolved) != Some(tc)
{
continue;
}
if let Some(tm) = target_machine
&& read_elf_machine(&resolved) != Some(tm)
{
continue;
}
seen.insert(name.clone());
let size = fs::metadata(&resolved).map(|m| m.len()).unwrap_or(0);
eprintln!(
" {} <- {} ({})",
color::bold_green(&name),
resolved.display(),
color::dim(&format_size(size))
);
if !dry_run {
fs::create_dir_all(dest)?;
let dest_path = dest.join(&name);
if dest_path.is_symlink() {
fs::remove_file(&dest_path)?;
}
if !is_same_file(&resolved, &dest_path) {
ensure_writable(&dest_path);
fs::copy(&resolved, &dest_path)?;
let _ = fs::set_permissions(&dest_path, PermissionsExt::from_mode(0o755));
if strip {
strip_debug(&dest_path);
}
normalize_mtime(&dest_path);
}
}
copied += 1;
total_bytes += size;
}
}
Ok((copied, total_bytes))
}
#[allow(clippy::too_many_arguments)]
fn copy_prefixed_libs(
search_dirs: &[PathBuf],
prefixes: &[&str],
dest: &Path,
target_class: Option<u8>,
target_machine: Option<u16>,
excludes: &[&str],
dry_run: bool,
strip: bool,
) -> io::Result<(usize, u64)> {
copy_libs(
search_dirs,
dest,
|name| prefixes.iter().any(|p| name.starts_with(p)),
target_class,
target_machine,
excludes,
dry_run,
strip,
)
}
const GCONV_MODULES: &[&str] = &[
"UTF", "UNICODE", "ANSI", "CP", "LATIN", "ISO8859", "SJIS", "EUC-JP", "EUC-KR", "EUC-CN", "lib",
];
fn bundle_gconv(libc: &Path, lib_dest: &Path, strip: bool) -> io::Result<Vec<String>> {
let Some(src) = libc.parent().map(|d| d.join("gconv")) else {
return Ok(Vec::new());
};
if !src.is_dir() {
return Ok(Vec::new());
}
let dest = lib_dest.join("gconv");
let mut copied = Vec::new();
let mut copy = |from: &Path, rel: &str| -> io::Result<()> {
let to = dest.join(rel);
if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}
ensure_writable(&to);
fs::copy(from, &to)?;
normalize_mtime(&to);
copied.push(format!("gconv/{rel}"));
Ok(())
};
let modules_conf = src.join("gconv-modules");
if modules_conf.is_file() {
copy(&modules_conf, "gconv-modules")?;
}
if let Ok(entries) = fs::read_dir(src.join("gconv-modules.d")) {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if entry.path().is_file() && name.ends_with(".conf") {
copy(&entry.path(), &format!("gconv-modules.d/{name}"))?;
}
}
}
for entry in fs::read_dir(&src)?.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.ends_with(".so") || !GCONV_MODULES.iter().any(|p| name.starts_with(p)) {
continue;
}
copy(&entry.path(), &name)?;
if strip {
strip_debug(&dest.join(&*name));
}
}
copied.sort();
Ok(copied)
}
fn bundle_c_utf8(libc: &Path, lib_dest: &Path) -> io::Result<Vec<String>> {
let Some(libc_dir) = libc.parent() else {
return Ok(Vec::new());
};
let found = ["locale", "../locale", "../lib/locale"]
.iter()
.flat_map(|rel| ["C.utf8", "C.UTF-8"].map(|name| (libc_dir.join(rel).join(name), name)))
.find(|(dir, _)| dir.join("LC_CTYPE").is_file());
let Some((src, name)) = found else {
return Ok(Vec::new());
};
let mut copied = Vec::new();
let mut pending = vec![(src, PathBuf::from("locale").join(name))];
while let Some((from, rel)) = pending.pop() {
for entry in fs::read_dir(&from)?.flatten() {
let rel = rel.join(entry.file_name());
let kind = entry.file_type()?;
if kind.is_dir() {
pending.push((entry.path(), rel));
} else if kind.is_file() {
let to = lib_dest.join(&rel);
if let Some(parent) = to.parent() {
fs::create_dir_all(parent)?;
}
ensure_writable(&to);
fs::copy(entry.path(), &to)?;
normalize_mtime(&to);
copied.push(rel.to_string_lossy().into_owned());
}
}
}
copied.sort();
Ok(copied)
}
const DEFAULT_EXCLUDES: &[&str] = &[
"libnss_",
"libcuda.so",
"libnvidia",
"libamdhip64.so",
"libze_loader.so",
"linux-vdso.so",
];
pub(crate) use onelf_sysroot::LIB_PATHS as STANDARD_LIB_PATHS;
pub struct BundleOptions {
pub directory: PathBuf,
pub target: Option<PathBuf>,
pub primary: Option<PathBuf>,
pub lib_dir: PathBuf,
pub exclude: Vec<String>,
pub include: Vec<String>,
pub search_path: Vec<PathBuf>,
pub dry_run: bool,
pub recursive: bool,
pub gl: bool,
pub dri: bool,
pub vulkan: bool,
pub wayland: bool,
pub gtk: bool,
pub no_gl: bool,
pub no_dri: bool,
pub no_vulkan: bool,
pub no_wayland: bool,
pub no_gtk: bool,
pub strip: bool,
pub strict_libc: bool,
pub scan_dlopen: bool,
pub trace_run: bool,
pub trace_seconds: u64,
pub trace_args: Vec<String>,
pub dlopen_extra: Vec<String>,
pub sysroot: Option<SysrootOptions>,
pub sets: Vec<sysroot::SharedSet>,
pub platform_pin: Option<onelf_sysroot::platform::Pin>,
}
fn strip_debug(path: &Path) {
if fs::read(path)
.map(|d| has_embedded_payload(&d))
.unwrap_or(false)
{
return;
}
match Command::new("strip")
.arg("--strip-unneeded")
.arg(path)
.output()
{
Ok(out) if !out.status.success() => {
eprintln!(
" {} strip failed for {}: {}",
color::bold_red("warning:"),
path.display(),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Err(e) => {
eprintln!(
" {} strip failed for {}: {e}",
color::bold_red("warning:"),
path.display()
);
}
_ => {}
}
}
pub fn bundle_libs(opts: &BundleOptions) -> io::Result<()> {
if !opts.directory.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("{}: not a directory", opts.directory.display()),
));
}
let mut host_files: HashSet<String> = HashSet::new();
let mut set_sonames: HashSet<String> = HashSet::new();
let sysroot_platform = match &opts.sysroot {
Some(sr) => {
let (report, platform) = sysroot::populate(&opts.directory, sr)?;
sysroot::print_report(sr, &report);
host_files = report.host_files;
set_sonames = report.set_sonames;
Some(platform)
}
None => {
if !opts.dry_run {
let _ = fs::remove_file(opts.directory.join(sysroot::PROVENANCE_FILE));
sysroot::record_sets(&opts.directory, &opts.sets, opts.platform_pin.as_ref())?;
}
None
}
};
let frameworks = opts.sysroot.is_none();
let target_elfs = resolve_target_elfs(opts)?;
let primary = resolve_primary(opts);
let target_class = primary
.as_deref()
.and_then(read_elf_class)
.or_else(|| target_elfs.iter().find_map(|f| read_elf_class(f)));
let target_machine = primary
.as_deref()
.and_then(read_elf_machine)
.or_else(|| target_elfs.iter().find_map(|f| read_elf_machine(f)));
let detected = detect_frameworks(&target_elfs);
let want_gl = frameworks && opts.gl && !opts.no_gl;
let want_dri = frameworks && opts.dri && !opts.no_dri;
let want_vulkan = frameworks && opts.vulkan && !opts.no_vulkan;
let want_wayland = frameworks && (opts.wayland || detected.wayland) && !opts.no_wayland;
let want_gtk = frameworks && (opts.gtk || detected.gtk) && !opts.no_gtk;
let auto: Vec<&str> = [
(detected.wayland && !opts.wayland && want_wayland, "wayland"),
(detected.gtk && !opts.gtk && want_gtk, "gtk"),
]
.into_iter()
.filter_map(|(on, name)| on.then_some(name))
.collect();
if !auto.is_empty() {
eprintln!(
" {} auto-enabled: {}",
color::bold("Frameworks:"),
auto.join(", ")
);
}
let driver_stack: Vec<&str> = [
(detected.gl && !want_gl, "gl"),
(detected.dri && !want_dri, "dri"),
(detected.vulkan && !want_vulkan, "vulkan"),
]
.into_iter()
.filter_map(|(on, name)| on.then_some(name))
.collect();
if frameworks && !driver_stack.is_empty() {
eprintln!(
" {} {} detected; the host's drivers are used at launch. \
Pass --gl, --dri or --vulkan to bundle Mesa instead.",
color::bold("Frameworks:"),
driver_stack.join(", ")
);
}
let suppressed: Vec<&str> = [
(detected.gl && opts.no_gl, "gl"),
(detected.dri && opts.no_dri, "dri"),
(detected.vulkan && opts.no_vulkan, "vulkan"),
(detected.wayland && opts.no_wayland, "wayland"),
(detected.gtk && opts.no_gtk, "gtk"),
]
.into_iter()
.filter_map(|(on, name)| on.then_some(name))
.collect();
if !suppressed.is_empty() {
eprintln!(
" {} suppressed: {}",
color::bold("Frameworks:"),
suppressed.join(", ")
);
}
let excludes: Vec<&str> = DEFAULT_EXCLUDES
.iter()
.copied()
.chain(opts.exclude.iter().map(|s| s.as_str()))
.chain(
sysroot_platform
.iter()
.flat_map(|p| p.prefixes().iter().map(String::as_str)),
)
.collect();
if want_gl || want_dri || want_vulkan {
bundle_gpu(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
&excludes,
target_class,
target_machine,
opts.dry_run,
opts.strip,
want_gl,
want_dri,
want_vulkan,
)?;
}
if want_wayland {
bundle_wayland(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
&excludes,
target_class,
target_machine,
opts.dry_run,
opts.strip,
)?;
}
if want_gtk {
bundle_gtk_data(&opts.directory, opts.dry_run)?;
}
let elf_files = if opts.target.is_some() {
target_elfs
} else {
find_elf_files(&opts.directory)
};
if elf_files.is_empty() {
eprintln!(
"{} no ELF files found in {}",
color::bold_red("warning:"),
opts.directory.display()
);
return Ok(());
}
eprintln!(
"{} {} ELF file(s)...",
color::bold("Scanning"),
elf_files.len()
);
let mut needed_by: HashMap<String, String> = HashMap::new();
let mut rpath_dirs: Vec<PathBuf> = Vec::new();
for path in &elf_files {
let requirer = path
.strip_prefix(&opts.directory)
.unwrap_or(path)
.to_string_lossy()
.into_owned();
match parse_needed(path) {
Ok(libs) => {
for lib in libs {
needed_by.entry(lib).or_insert_with(|| requirer.clone());
}
}
Err(e) => {
eprintln!("warning: {}: {e}", path.display());
}
}
if let Some(interp) = parse_interp(path)
&& let Some(name) = Path::new(&interp).file_name().and_then(|n| n.to_str())
{
needed_by
.entry(name.to_string())
.or_insert_with(|| format!("{requirer} (PT_INTERP)"));
}
let sysroot_root = opts.sysroot.as_ref().map(|sr| sr.root.as_path());
for dir in parse_rpaths_under(path, sysroot_root)
.into_iter()
.chain(parse_needed_dirs_under(path, sysroot_root))
{
if !rpath_dirs.contains(&dir) {
rpath_dirs.push(dir);
}
}
}
for lib in &opts.include {
needed_by
.entry(lib.clone())
.or_insert_with(|| "--include".into());
}
if opts.trace_run {
if opts.sysroot.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"--trace-run runs the target against this machine, which a sysroot build does not use; record a trace with ONELF_TRACE instead",
));
}
let Some(target) = resolve_primary(opts) else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"--trace-run needs --target to name what to run",
));
};
let family = program_interp(&target)
.as_deref()
.and_then(libc_family_from_interp);
eprintln!(
"{} {} for up to {}s to see what it loads...",
color::bold("Running"),
target
.strip_prefix(&opts.directory)
.unwrap_or(&target)
.display(),
opts.trace_seconds
);
let tree = opts.directory.canonicalize()?;
let mut library_path: Vec<PathBuf> = Vec::new();
let tree_dirs = std::iter::once(opts.directory.join(&opts.lib_dir))
.chain(elf_files.iter().flat_map(|f| parse_rpaths(f)))
.filter_map(|d| d.canonicalize().ok())
.filter(|d| d.starts_with(&tree));
let host_dirs = onelf_format::drivers::cache_dirs()
.into_iter()
.map(PathBuf::from)
.chain(STANDARD_LIB_PATHS.iter().map(PathBuf::from));
for dir in tree_dirs
.chain(opts.search_path.iter().cloned())
.chain(host_dirs)
{
if dir.is_dir() && !library_path.contains(&dir) {
library_path.push(dir);
}
}
let loader = trace_loader(&target, &library_path, target_class, target_machine)?;
let loaded = discover::loaded_libraries(
&target,
&opts.trace_args,
opts.trace_seconds,
family,
&opts.directory,
&library_path,
loader.as_deref(),
)?;
let (loaded, gpu_stack) = split_gpu_stack(loaded);
if !gpu_stack.is_empty() {
let names: Vec<&str> = gpu_stack
.iter()
.filter_map(|p| p.file_name().and_then(|n| n.to_str()))
.collect();
eprintln!(
" {} the host's GPU stack: {}. Pass --gl, --dri or --vulkan \
to bundle Mesa instead.",
color::bold("Left to"),
names.join(", ")
);
}
let mut traced = 0usize;
for path in loaded {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if needed_by.contains_key(name) {
continue;
}
needed_by.insert(name.to_string(), "trace run".into());
if let Some(dir) = path.parent()
&& !rpath_dirs.contains(&dir.to_path_buf())
{
rpath_dirs.push(dir.to_path_buf());
}
eprintln!(" {} {}", color::bold_green("Loaded"), name);
traced += 1;
}
if traced == 0 {
eprintln!(" nothing beyond what DT_NEEDED already names");
}
}
if opts.scan_dlopen {
let mut scanned: HashSet<String> = HashSet::new();
for path in &elf_files {
if let Ok(hits) = scan_dlopen(path, &opts.dlopen_extra) {
for soname in hits {
if scanned.insert(soname.clone()) {
let requirer = path
.strip_prefix(&opts.directory)
.unwrap_or(path)
.to_string_lossy()
.into_owned();
let label = format!("--scan-dlopen in {requirer}");
needed_by.entry(soname).or_insert(label);
}
}
}
}
if !scanned.is_empty() {
eprintln!(
" {} {} dlopen candidate(s): {}",
color::bold("Scanned:"),
scanned.len(),
scanned.iter().cloned().collect::<Vec<_>>().join(", ")
);
}
}
needed_by.retain(|soname, _| !is_excluded(soname, &excludes));
let (_, stubs) = find_existing_libs(&opts.directory);
if !opts.dry_run {
for stub in &stubs {
fs::remove_file(stub).map_err(|e| {
io::Error::new(
e.kind(),
format!("removing stub loader {}: {e}", stub.display()),
)
})?;
}
}
let reachable: HashSet<String> = fs::read_dir(opts.directory.join(&opts.lib_dir))
.into_iter()
.flatten()
.flatten()
.filter_map(|e| e.file_name().to_str().map(String::from))
.filter(|name| name.contains(".so"))
.collect();
needed_by.retain(|soname, _| !reachable.contains(soname));
if needed_by.is_empty() {
eprintln!("All dependencies satisfied, nothing to bundle.");
if !opts.dry_run {
let lib_dest = opts.directory.join(&opts.lib_dir);
let (rewritten, _scrubbed, unguaranteed, self_extract) =
finalize_tree(&opts.directory, &opts.lib_dir);
if rewritten > 0 {
eprintln!(
"{} RUNPATH to reach {} in {} binaries",
color::bold("Rewrote"),
opts.lib_dir.display(),
rewritten
);
}
report_unguaranteed_runpath(&unguaranteed, &self_extract);
match inject_bootstraps(&opts.directory, &lib_dest) {
Ok(n) if n > 0 => eprintln!(
"{} AT_EXECFN bootstrap into {} binaries",
color::bold("Injected"),
n
),
Ok(_) => {}
Err(e) => eprintln!(
"{} bootstrap injection failed: {e}",
color::bold_red("warning:"),
),
}
verify_needs(opts, sysroot_platform.as_ref(), &set_sonames)?;
}
return Ok(());
}
let target_libc = primary
.as_deref()
.and_then(program_interp)
.as_deref()
.and_then(libc_family_from_interp)
.or_else(|| {
elf_files.iter().find_map(|f| {
program_interp(f)
.as_deref()
.and_then(libc_family_from_interp)
})
});
if let Some(chosen) = target_libc {
let others: Vec<&PathBuf> = elf_files
.iter()
.filter(|f| {
program_interp(f)
.as_deref()
.and_then(libc_family_from_interp)
.is_some_and(|fam| fam != chosen)
})
.collect();
if !others.is_empty() {
eprintln!(
" {} tree mixes libc families; bundling for {:?}{}",
color::bold("Note:"),
chosen,
primary
.as_deref()
.and_then(|p| p.strip_prefix(&opts.directory).ok())
.map(|p| format!(" (from {})", p.display()))
.unwrap_or_default()
);
for f in others.iter().take(5) {
eprintln!(
" other family: {}",
f.strip_prefix(&opts.directory).unwrap_or(f).display()
);
}
}
}
if let Some(target) = target_libc {
needed_by.retain(|soname, _| libc_family_of_soname(soname).is_none_or(|fam| fam == target));
}
let mut ldconfig_cache = if opts.sysroot.is_some() {
HashMap::new()
} else {
build_lib_cache()
};
let mut search_paths: Vec<PathBuf> = opts
.sysroot
.iter()
.flat_map(|sr| {
["usr/lib", "usr/lib64", "lib", "lib64"]
.into_iter()
.filter_map(move |d| onelf_sysroot::path::in_root(&sr.root, Path::new(d)).ok())
.filter(|d| d.is_dir())
.chain(onelf_sysroot::ldconf::search_dirs(&sr.root))
})
.collect();
search_paths.extend(opts.search_path.iter().cloned());
search_paths.extend(rpath_dirs);
let lib_dest = opts.directory.join(&opts.lib_dir);
let mut copied: Vec<(String, PathBuf, u64, String)> = Vec::new();
let mut not_found: Vec<(String, String)> = Vec::new();
let policy = opts
.sysroot
.as_ref()
.and_then(|sr| sr.policy.as_deref())
.map(Policy::load)
.transpose()?;
let pruned_by_policy = |src: &Path| -> bool {
let (Some(sr), Some(policy)) = (opts.sysroot.as_ref(), policy.as_ref()) else {
return false;
};
src.strip_prefix(&sr.root)
.ok()
.is_some_and(|rel| policy.matches(&rel.to_string_lossy()))
};
let with_host_package = |src: &Path| -> bool {
opts.sysroot.as_ref().is_some_and(|sr| {
src.strip_prefix(&sr.root)
.ok()
.is_some_and(|rel| host_files.contains(rel.to_string_lossy().as_ref()))
})
};
let sysroot_root = opts.sysroot.as_ref().map(|sr| sr.root.as_path());
let real_path = |src: &Path| -> PathBuf {
match sysroot_root.and_then(|root| Some((root, src.strip_prefix(root).ok()?))) {
Some((root, rel)) => {
onelf_sysroot::path::in_root(root, rel).unwrap_or_else(|_| src.to_path_buf())
}
None => fs::canonicalize(src).unwrap_or_else(|_| src.to_path_buf()),
}
};
let mut left_out: Vec<(String, String)> = Vec::new();
let mut left_with_host: Vec<(String, String)> = Vec::new();
let mut already_processed: HashSet<String> = HashSet::new();
let mut expanded_nix: HashSet<PathBuf> = HashSet::new();
let mut bundled_by_hash: HashMap<[u8; 32], String> = HashMap::new();
let mut queue: Vec<String> = needed_by.keys().cloned().collect();
queue.sort();
let mut nss_allow: HashSet<String> = HashSet::new();
let mut libc_data_files: Vec<String> = Vec::new();
if Path::new("/nix/store").is_dir() && opts.sysroot.is_none() {
let (bundled, stubs) = find_existing_libs(&lib_dest);
if !opts.dry_run {
for stub in &stubs {
fs::remove_file(stub).map_err(|e| {
io::Error::new(
e.kind(),
format!("removing stub loader {}: {e}", stub.display()),
)
})?;
}
}
for lib_name in bundled {
if let Some(src) = locate_lib(
&lib_name,
&ldconfig_cache,
&search_paths,
target_class,
target_machine,
None,
) {
let resolved = fs::canonicalize(&src).unwrap_or(src);
expand_nix_cache(&resolved, &mut ldconfig_cache, &mut expanded_nix);
}
}
}
while let Some(soname) = queue.pop() {
if already_processed.contains(&soname)
|| (is_excluded(&soname, &excludes) && !nss_allow.contains(&soname))
{
continue;
}
already_processed.insert(soname.clone());
if lib_dest.join(&soname).exists() {
continue;
}
let requirer = needed_by
.get(&soname)
.cloned()
.unwrap_or_else(|| "?".into());
match locate_lib(
&soname,
&ldconfig_cache,
&search_paths,
target_class,
target_machine,
sysroot_root,
)
.map(|src| {
let resolved = real_path(&src);
(src, resolved)
}) {
Some((src, resolved)) if pruned_by_policy(&src) || pruned_by_policy(&resolved) => {
left_out.push((soname, requirer));
}
Some((src, resolved)) if with_host_package(&src) || with_host_package(&resolved) => {
left_with_host.push((soname, requirer));
}
Some((_, resolved)) => {
let size = fs::metadata(&resolved).map(|m| m.len()).unwrap_or(0);
let dest = lib_dest.join(&soname);
let lib_needed = parse_needed(&resolved).unwrap_or_default();
let lib_family = lib_needed.iter().find_map(|d| libc_family_of_soname(d));
let mismatch = matches!(
(target_libc, lib_family),
(Some(t), Some(f)) if t != f
);
if mismatch {
let msg = format!(
"{} links against {:?} libc but target is {:?}",
soname,
lib_family.unwrap(),
target_libc.unwrap()
);
if opts.strict_libc {
eprintln!(
" {} skipping {} ({})",
color::bold_red("skip:"),
color::cyan(&soname),
msg,
);
not_found.push((soname.clone(), format!("{requirer} ({msg})")));
continue;
}
eprintln!(
" {} {}; this bundle may not work at runtime",
color::bold_red("warning:"),
msg,
);
}
expand_nix_cache(&resolved, &mut ldconfig_cache, &mut expanded_nix);
let bytes = fs::read(&resolved).ok();
let content_hash: Option<[u8; 32]> =
bytes.as_deref().map(|b| blake3::hash(b).into());
if soname == "libc.so.6" {
let old = bytes
.as_deref()
.is_some_and(|b| !b.windows(10).any(|w| w == b"GLIBC_2.34"));
let modules: &[&str] = if old {
&["libnss_compat.so.2", "libnss_files.so.2", "libnss_dns.so.2"]
} else {
&["libnss_compat.so.2"]
};
for module in modules {
nss_allow.insert(module.to_string());
needed_by
.entry(module.to_string())
.or_insert_with(|| "libc.so.6 (nsswitch)".into());
queue.push(module.to_string());
}
}
if let Some(hash) = content_hash
&& let Some(existing_name) = bundled_by_hash.get(&hash).cloned()
{
eprintln!(
" {} {} -> {} (alias for {}, {})",
color::bold_green("Linked"),
soname,
existing_name,
color::cyan(&requirer),
color::dim(&format_size(size))
);
if !opts.dry_run {
fs::create_dir_all(&lib_dest)?;
let dest = lib_dest.join(&soname);
if dest.exists() || dest.is_symlink() {
let _ = fs::remove_file(&dest);
}
if let Err(e) = std::os::unix::fs::symlink(&existing_name, &dest) {
eprintln!(
" {} failed to symlink {} -> {}: {e}",
color::bold_red("warning:"),
soname,
existing_name
);
}
}
continue;
}
eprintln!(
" {} <- {} (needed by {}, {})",
color::bold_green(&soname),
resolved.display(),
color::cyan(&requirer),
color::dim(&format_size(size))
);
if !opts.dry_run {
fs::create_dir_all(&lib_dest)?;
if dest.is_symlink() {
let _ = fs::remove_file(&dest);
}
ensure_writable(&dest);
fs::copy(&resolved, &dest)?;
let _ = fs::set_permissions(
&dest,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
);
normalize_mtime(&dest);
let is_loader = is_dynamic_loader(&soname)
|| libc_family_of_soname(&soname) == Some(LibcFamily::Musl);
if is_loader
&& let Err(e) = scrub_loader_paths(
&dest,
libc_family_of_soname(&soname).unwrap_or(LibcFamily::Glibc),
)
{
eprintln!(
" {} failed to scrub loader paths in {}: {e}",
color::bold_red("warning:"),
soname
);
}
if opts.strip {
strip_debug(&dest);
}
}
if let Some(hash) = content_hash {
bundled_by_hash.insert(hash, soname.clone());
}
copied.push((soname.clone(), resolved.clone(), size, requirer));
if soname == "libc.so.6" && !opts.dry_run {
match bundle_gconv(&resolved, &lib_dest, opts.strip) {
Ok(files) if !files.is_empty() => {
eprintln!(
" {} {} gconv module(s) beside libc",
color::bold_green("Copied"),
files.len()
);
libc_data_files = files;
}
Ok(_) => {}
Err(e) => eprintln!(
" {} could not copy gconv modules: {e}",
color::bold_red("warning:")
),
}
match bundle_c_utf8(&resolved, &lib_dest) {
Ok(files) if !files.is_empty() => {
eprintln!(" {} the C.UTF-8 locale", color::bold_green("Copied"));
libc_data_files.extend(files);
}
Ok(_) => {}
Err(e) => eprintln!(
" {} could not copy the C.UTF-8 locale: {e}",
color::bold_red("warning:")
),
}
}
let sysroot_root = opts.sysroot.as_ref().map(|sr| sr.root.as_path());
for dir in parse_rpaths_under(&resolved, sysroot_root) {
if !search_paths.contains(&dir) {
search_paths.push(dir);
}
}
if opts.recursive {
for dep in lib_needed {
if already_processed.contains(&dep)
|| is_excluded(&dep, &excludes)
|| reachable.contains(&dep)
{
continue;
}
if libc_family_of_soname(&dep).is_some() {
continue;
}
needed_by
.entry(dep.clone())
.or_insert_with(|| soname.clone());
queue.push(dep);
}
}
}
None => {
not_found.push((soname, requirer));
}
}
}
copied.sort_by(|a, b| a.0.cmp(&b.0));
not_found.sort();
let total_size: u64 = copied.iter().map(|(_, _, s, _)| s).sum();
if opts.dry_run {
eprintln!(
"\n{} would copy {} libraries ({})",
color::bold("Dry run:"),
color::bold_green(&copied.len().to_string()),
color::bold(&format_size(total_size))
);
} else if !copied.is_empty() {
eprintln!(
"\n{} {} libraries ({}) to {}",
color::bold_green("Copied"),
copied.len(),
color::bold(&format_size(total_size)),
lib_dest.display()
);
}
if opts.sysroot.is_some() && !opts.dry_run {
let generated = copied
.iter()
.map(|(soname, _, _, _)| opts.lib_dir.join(soname).to_string_lossy().into_owned())
.chain(
libc_data_files
.iter()
.map(|f| opts.lib_dir.join(f).to_string_lossy().into_owned()),
)
.collect();
sysroot::record_generated(&opts.directory, generated)?;
}
if !left_with_host.is_empty() {
eprintln!(
"\n{} ({})",
color::bold("Left out with a host package"),
left_with_host.len()
);
for (lib, requirer) in &left_with_host {
eprintln!(
" {} {}",
lib,
color::dim(&format!("(needed by {})", color::cyan(requirer)))
);
}
}
if !left_out.is_empty() {
eprintln!(
"\n{} ({})",
color::bold("Left out by policy"),
left_out.len()
);
for (lib, requirer) in &left_out {
eprintln!(
" {} {}",
lib,
color::dim(&format!("(needed by {})", color::cyan(requirer)))
);
}
}
if !not_found.is_empty() {
eprintln!("\n{} ({})", color::bold_red("Not found"), not_found.len());
for (lib, requirer) in ¬_found {
eprintln!(
" {} {}",
color::red(lib),
color::dim(&format!("(needed by {})", color::cyan(requirer)))
);
}
}
if !opts.dry_run {
let mut interp_names: Vec<String> = elf_files
.iter()
.filter_map(|p| {
parse_interp(p).and_then(|i| {
Path::new(&i)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
})
})
.collect();
interp_names.sort();
interp_names.dedup();
for interp_name in interp_names {
let target = lib_dest.join(&interp_name);
if target.exists() || target.is_symlink() {
continue;
}
let Some(libc_name) = libc_alias_for(&interp_name) else {
continue;
};
let libc_path = lib_dest.join(&libc_name);
if !libc_path.exists() {
continue;
}
if let Err(e) = std::os::unix::fs::symlink(&libc_name, &target) {
eprintln!(
" {} failed to create {} -> {}: {e}",
color::bold_red("warning:"),
interp_name,
libc_name
);
} else {
eprintln!(
" {} {} -> {}",
color::bold_green("Linked"),
interp_name,
libc_name
);
}
}
}
if !opts.dry_run {
let (rewritten, scrubbed, unguaranteed, self_extract) =
finalize_tree(&opts.directory, &opts.lib_dir);
if rewritten > 0 {
eprintln!(
"{} RUNPATH to reach {} in {} binaries",
color::bold("Rewrote"),
opts.lib_dir.display(),
rewritten
);
}
if scrubbed > 0 {
eprintln!(
"{} /nix/store paths in {} binaries",
color::bold("Scrubbed"),
scrubbed
);
}
report_unguaranteed_runpath(&unguaranteed, &self_extract);
}
if !opts.dry_run {
match inject_bootstraps(&opts.directory, &lib_dest) {
Ok(n) if n > 0 => eprintln!(
"{} AT_EXECFN bootstrap into {} binaries",
color::bold("Injected"),
n
),
Ok(_) => {}
Err(e) => eprintln!(
"{} bootstrap injection failed: {e}",
color::bold_red("warning:"),
),
}
datadirs::report(&datadirs::unanswered(
&opts.directory,
&find_elf_files(&opts.directory),
));
verify_needs(opts, sysroot_platform.as_ref(), &set_sonames)?;
}
Ok(())
}
fn drop_unloadable(
opts: &BundleOptions,
root: &Path,
findings: &mut Vec<(PathBuf, Vec<String>)>,
audit: impl Fn() -> Vec<(PathBuf, Vec<String>)>,
) -> io::Result<Vec<(PathBuf, String)>> {
let held: HashSet<String> = Database::read(root)?
.packages()
.flat_map(|p| p.files.iter())
.filter_map(|f| Path::new(f).file_name())
.map(|n| n.to_string_lossy().into_owned())
.collect();
let generated: HashSet<PathBuf> =
fs::read_to_string(opts.directory.join(sysroot::GENERATED_FILE))
.unwrap_or_default()
.lines()
.filter(|l| !l.is_empty())
.map(PathBuf::from)
.collect();
let entrypoint = opts
.primary
.as_deref()
.or(opts.target.as_deref())
.map(|t| t.strip_prefix(&opts.directory).unwrap_or(t).to_path_buf());
let mut gone: HashSet<String> = HashSet::new();
let mut dropped: Vec<(PathBuf, String)> = Vec::new();
loop {
let mut progressed = false;
for (object, libs) in findings.iter() {
let rel = object.strip_prefix(&opts.directory).unwrap_or(object);
if entrypoint.as_deref() == Some(rel) || !generated.contains(rel) {
continue;
}
let Some(soname) = libs
.iter()
.find(|s| !held.contains(*s) || gone.contains(*s))
else {
continue;
};
let dropped_soname = crate::bundle::elf::parse_soname(object);
fs::remove_file(object)?;
remove_dangling_links_beside(object);
if let Some(name) = object.file_name() {
gone.insert(name.to_string_lossy().into_owned());
}
gone.extend(dropped_soname);
dropped.push((rel.to_path_buf(), soname.clone()));
progressed = true;
}
if !progressed {
break;
}
*findings = audit();
}
dropped.sort();
Ok(dropped)
}
pub(crate) fn remove_dangling_links_beside(object: &Path) {
let (Some(dir), Some(name)) = (object.parent(), object.file_name()) else {
return;
};
let mut gone: HashSet<std::ffi::OsString> = HashSet::from([name.to_os_string()]);
loop {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
let mut removed = false;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_symlink() || path.exists() {
continue;
}
let Ok(target) = fs::read_link(&path) else {
continue;
};
let led_here = target.parent().is_none_or(|p| p.as_os_str().is_empty())
&& target.file_name().is_some_and(|t| gone.contains(t));
if led_here && fs::remove_file(&path).is_ok() {
gone.insert(entry.file_name());
removed = true;
}
}
if !removed {
return;
}
}
}
fn verify_needs(
opts: &BundleOptions,
platform: Option<&PlatformLine>,
set_sonames: &HashSet<String>,
) -> io::Result<()> {
let audit = || {
let mut findings = audit_unbundled_needs(&opts.directory, &opts.lib_dir);
for (_, libs) in &mut findings {
libs.retain(|s| {
!platform.is_some_and(|p| p.matches_soname(s)) && !set_sonames.contains(s)
});
}
findings.retain(|(_, libs)| !libs.is_empty());
findings
};
let mut findings = audit();
if let Some(sr) = &opts.sysroot {
let dropped = drop_unloadable(opts, &sr.root, &mut findings, audit)?;
if !dropped.is_empty() {
eprintln!(
"\n{} {} file(s) needing libraries the sysroot does not hold:",
color::bold("Dropped"),
dropped.len()
);
for (object, soname) in &dropped {
eprintln!(
" {} {}",
object.display(),
color::dim(&format!("(needs {soname})"))
);
}
}
}
if opts.sysroot.is_none() && !opts.sets.is_empty() {
let (provides, unchecked) = sysroot::take_set_provides(&opts.sets, &mut findings);
sysroot::report_set_provides(&provides, &unchecked);
}
report_unbundled_needs(&findings);
if opts.sysroot.is_some()
&& let Some((object, libs)) = findings.first()
{
let object = object
.strip_prefix(&opts.directory)
.unwrap_or(object)
.display();
let more = match findings.len() {
1 => String::new(),
n => format!(" ({} more listed above)", n - 1),
};
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"{} needs {}, which is neither in the sysroot closure, on the platform line, nor in a shared set{more}",
object, libs[0]
),
));
}
Ok(())
}
pub(crate) fn normalize_mtime(path: &Path) {
let secs = crate::pack::source_date_epoch()
.and_then(|s| i64::try_from(s).ok())
.unwrap_or(0);
let t = rustix::fs::Timespec {
tv_sec: secs,
tv_nsec: 0,
};
let _ = rustix::fs::utimensat(
rustix::fs::CWD,
path,
&rustix::fs::Timestamps {
last_access: t,
last_modification: t,
},
rustix::fs::AtFlags::empty(),
);
}
fn resolve_target_elfs(opts: &BundleOptions) -> io::Result<Vec<PathBuf>> {
if let Some(ref target) = opts.target {
let path = if target.is_absolute() {
target.clone()
} else {
opts.directory.join(target)
};
if !path.is_file() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{}: not a file", path.display()),
));
}
Ok(vec![path])
} else {
Ok(find_elf_files(&opts.directory))
}
}
fn split_gpu_stack(libs: Vec<PathBuf>) -> (Vec<PathBuf>, Vec<PathBuf>) {
const IMPLEMENTATIONS: &[&str] = &[
"libGLX_",
"libEGL_",
"libglapi.so",
"libgallium",
"libLLVM",
"libvulkan_",
"libVkLayer_",
"libxatracker",
"libvdpau",
];
const DRIVER_SUFFIXES: &[&str] = &["_dri.so", "_drv_video.so"];
let name = |p: &Path| {
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string()
};
let mut gpu: HashSet<String> = libs
.iter()
.map(|p| name(p))
.filter(|n| {
onelf_format::drivers::DRIVER_FAMILIES
.iter()
.chain(IMPLEMENTATIONS)
.any(|f| n.starts_with(f))
|| DRIVER_SUFFIXES.iter().any(|s| n.ends_with(s))
})
.collect();
let needs: Vec<(String, Vec<String>)> = libs
.iter()
.map(|p| (name(p), parse_needed(p).unwrap_or_default()))
.collect();
loop {
let more: Vec<String> = needs
.iter()
.map(|(n, _)| n)
.filter(|n| !gpu.contains(*n))
.filter(|n| {
let mut needers = needs.iter().filter(|(_, deps)| deps.contains(n)).peekable();
needers.peek().is_some() && needers.all(|(m, _)| gpu.contains(m))
})
.cloned()
.collect();
if more.is_empty() {
break;
}
gpu.extend(more);
}
libs.into_iter().partition(|p| !gpu.contains(&name(p)))
}
fn trace_loader(
target: &Path,
dirs: &[PathBuf],
class: Option<u8>,
machine: Option<u16>,
) -> io::Result<Option<PathBuf>> {
let Some(interp) = parse_interp(target) else {
return Ok(None);
};
if Path::new(&interp).exists() {
return Ok(None);
}
let Some(name) = Path::new(&interp).file_name().and_then(|n| n.to_str()) else {
return Ok(None);
};
let mut names = vec![name.to_string()];
names.extend(libc_alias_for(name));
if libc_family_from_interp(name) == Some(LibcFamily::Musl) {
names.push("libc.so".to_string());
}
let found = dirs
.iter()
.flat_map(|d| names.iter().map(move |n| d.join(n)))
.find(|c| {
c.is_file()
&& read_elf_class(c).is_some_and(|k| class.is_none_or(|t| k == t))
&& read_elf_machine(c).is_some_and(|m| machine.is_none_or(|t| m == t))
});
match found {
Some(loader) => Ok(Some(loader)),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"--trace-run cannot start {}: its interpreter {interp} is not on this \
machine; pass --search-path with a directory that holds it",
target.display()
),
)),
}
}
fn resolve_primary(opts: &BundleOptions) -> Option<PathBuf> {
let named = opts.primary.as_ref().or(opts.target.as_ref())?;
let path = if named.is_absolute() {
named.clone()
} else {
opts.directory.join(named)
};
path.is_file().then_some(path)
}
fn find_elf_files(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
for entry in jwalk::WalkDir::new(dir).skip_hidden(false).sort(true) {
let Ok(entry) = entry else { continue };
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if is_elf(&path) {
result.push(path);
}
}
result
}
fn is_elf(path: &Path) -> bool {
fs::File::open(path)
.and_then(|mut f| {
let mut magic = [0u8; 4];
io::Read::read_exact(&mut f, &mut magic)?;
Ok(magic == *b"\x7fELF")
})
.unwrap_or(false)
}
fn read_elf_class(path: &Path) -> Option<u8> {
let mut f = fs::File::open(path).ok()?;
let mut header = [0u8; 5];
io::Read::read_exact(&mut f, &mut header).ok()?;
if header[0..4] == *b"\x7fELF" {
Some(header[4])
} else {
None
}
}
fn read_elf_machine(path: &Path) -> Option<u16> {
let mut f = fs::File::open(path).ok()?;
let mut header = [0u8; 20];
io::Read::read_exact(&mut f, &mut header).ok()?;
if header[0..4] != *b"\x7fELF" {
return None;
}
Some(u16::from_le_bytes([header[18], header[19]]))
}
const EM_X86_64: u16 = 62;
const EM_386: u16 = 3;
const EM_AARCH64: u16 = 183;
const EM_ARM: u16 = 40;
const VULKAN_DRIVERS_X86: &[&str] = &[
"libvulkan_intel.so",
"libvulkan_radeon.so",
"libvulkan_nouveau.so",
"libvulkan_lvp.so",
"libvulkan_virtio.so",
];
const VULKAN_DRIVERS_ARM: &[&str] = &[
"libvulkan_panfrost.so",
"libvulkan_asahi.so",
"libvulkan_freedreno.so",
"libvulkan_broadcom.so",
"libvulkan_powervr_mesa.so",
"libvulkan_lvp.so",
"libvulkan_virtio.so",
];
const DRI_DRIVERS_X86: &[&str] = &[
"iris_dri.so",
"i915_dri.so",
"i965_dri.so",
"radeonsi_dri.so",
"r600_dri.so",
"r300_dri.so",
"nouveau_dri.so",
"swrast_dri.so",
"kms_swrast_dri.so",
"vmwgfx_dri.so",
"virtio_gpu_dri.so",
"zink_dri.so",
];
const DRI_DRIVERS_ARM: &[&str] = &[
"panfrost_dri.so",
"asahi_dri.so",
"freedreno_dri.so",
"v3d_dri.so",
"vc4_dri.so",
"etnaviv_dri.so",
"lima_dri.so",
"tegra_dri.so",
"swrast_dri.so",
"kms_swrast_dri.so",
"virtio_gpu_dri.so",
"zink_dri.so",
];
fn driver_filter(
machine: Option<u16>,
x86_list: &'static [&'static str],
arm_list: &'static [&'static str],
) -> Option<&'static [&'static str]> {
match machine {
Some(EM_X86_64) | Some(EM_386) => Some(x86_list),
Some(EM_AARCH64) | Some(EM_ARM) => Some(arm_list),
_ => None,
}
}
#[derive(Default, Debug, Clone, Copy)]
struct FrameworkFlags {
gl: bool,
dri: bool,
vulkan: bool,
wayland: bool,
gtk: bool,
}
fn detect_frameworks(files: &[PathBuf]) -> FrameworkFlags {
let mut flags = FrameworkFlags::default();
for path in files {
let Ok(bytes) = fs::read(path) else {
continue;
};
if let Ok(needed) = parse_needed_bytes(&bytes) {
for soname in needed {
inspect_soname_for_frameworks(&soname, &mut flags);
}
}
scan_framework_strings(&bytes, &mut flags);
}
flags
}
fn inspect_soname_for_frameworks(soname: &str, flags: &mut FrameworkFlags) {
if soname.starts_with("libGL.so")
|| soname.starts_with("libEGL.so")
|| soname.starts_with("libGLESv")
|| soname.starts_with("libOpenGL.so")
{
flags.gl = true;
flags.dri = true;
}
if soname.starts_with("libgbm.so") {
flags.dri = true;
}
if soname.starts_with("libvulkan.so") {
flags.vulkan = true;
}
if soname.starts_with("libwayland-client.so")
|| soname.starts_with("libwayland-egl.so")
|| soname.starts_with("libwayland-cursor.so")
|| soname.starts_with("libwayland-server.so")
|| soname.starts_with("libdecor-0.so")
{
flags.wayland = true;
}
if soname.starts_with("libgtk-") {
flags.gtk = true;
}
}
fn scan_framework_strings(bytes: &[u8], flags: &mut FrameworkFlags) {
const STEMS: &[&[u8]] = &[
b"libGL",
b"libEGL",
b"libGLESv",
b"libOpenGL",
b"libgbm",
b"libvulkan",
b"libwayland-client",
b"libwayland-egl",
b"libwayland-cursor",
b"libwayland-server",
b"libdecor-0",
b"libgtk-",
];
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'l' {
i += 1;
continue;
}
if STEMS.iter().any(|stem| bytes[i..].starts_with(stem))
&& let Some(soname) = versioned_soname_at(bytes, i)
{
inspect_soname_for_frameworks(&soname, flags);
}
i += 1;
}
}
fn versioned_soname_at(bytes: &[u8], start: usize) -> Option<String> {
let mut j = start;
while j < bytes.len()
&& matches!(bytes[j], b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-')
{
j += 1;
}
if !bytes[j..].starts_with(b".so") {
return None;
}
j += 3;
if j + 1 >= bytes.len() || bytes[j] != b'.' || !bytes[j + 1].is_ascii_digit() {
return None;
}
while j < bytes.len() && (bytes[j] == b'.' || bytes[j].is_ascii_digit()) {
j += 1;
}
std::str::from_utf8(&bytes[start..j])
.ok()
.map(str::to_string)
}
const DLOPEN_CANDIDATES: &[&str] = &[
"libGL.so.1",
"libEGL.so.1",
"libGLX.so.0",
"libGLdispatch.so.0",
"libOpenGL.so.0",
"libGLESv1_CM.so.1",
"libGLESv2.so.2",
"libgbm.so.1",
"libvulkan.so.1",
"libwayland-client.so.0",
"libwayland-cursor.so.0",
"libwayland-egl.so.1",
"libdecor-0.so.0",
"libX11.so.6",
"libxcb.so.1",
"libxkbcommon.so.0",
"libxkbcommon-x11.so.0",
"libva.so.2",
"libva-drm.so.2",
"libva-x11.so.2",
"libva-wayland.so.2",
"libpulse.so.0",
"libasound.so.2",
"libjack.so.0",
"libdbus-1.so.3",
"libcuda.so.1",
"libnvidia-ml.so.1",
"libnvidia-encode.so.1",
"libnvidia-fbc.so.1",
"libfontconfig.so.1",
"libfreetype.so.6",
"libharfbuzz.so.0",
];
fn scan_dlopen(path: &Path, extra: &[String]) -> io::Result<Vec<String>> {
let data = fs::read(path)?;
let mut found: Vec<String> = Vec::new();
let mut start = None;
for (i, &b) in data.iter().enumerate() {
let printable = (0x20..=0x7e).contains(&b);
if printable {
if start.is_none() {
start = Some(i);
}
} else if let Some(s) = start.take()
&& i - s >= 5
&& let Ok(text) = std::str::from_utf8(&data[s..i])
{
let match_builtin = DLOPEN_CANDIDATES.contains(&text);
let match_extra = extra.iter().any(|c| c == text);
if (match_builtin || match_extra) && !found.iter().any(|x| x == text) {
found.push(text.to_string());
}
}
}
Ok(found)
}
#[cfg(test)]
mod framework_scan_tests {
use super::*;
fn scan(bytes: &[u8]) -> FrameworkFlags {
let mut flags = FrameworkFlags::default();
scan_framework_strings(bytes, &mut flags);
flags
}
#[test]
fn detects_versioned_soname_merged_without_nul_separators() {
let blob = b"eglWaitSynclibEGL.so.1libEGL.so";
let flags = scan(blob);
assert!(
flags.gl,
"versioned libEGL.so.1 inside a merged blob should flag gl"
);
}
#[test]
fn detects_versioned_wayland_in_merged_blob() {
let blob = b"some junklibwayland-client.so.0libwayland-client.so";
let flags = scan(blob);
assert!(flags.wayland);
}
#[test]
fn ignores_unversioned_soname_in_prose() {
let blob = b"Library libwayland-client.so could not be loaded.";
let flags = scan(blob);
assert!(
!flags.wayland,
"unversioned soname in prose must not flag wayland"
);
}
#[test]
fn ignores_bare_soname_without_version() {
let blob = b"\0libvulkan.so\0";
let flags = scan(blob);
assert!(
!flags.vulkan,
"bare libvulkan.so without a version must not flag"
);
}
#[test]
fn detects_nul_terminated_versioned_soname() {
let blob = b"\0libvulkan.so.1\0";
let flags = scan(blob);
assert!(flags.vulkan);
}
#[test]
fn classifies_glesv_stem_with_inner_version() {
let blob = b"\0libGLESv2.so.2\0";
let flags = scan(blob);
assert!(flags.gl, "libGLESv2.so.2 should flag gl");
}
#[test]
fn gtk_versioned_soname() {
let blob = b"prefixlibgtk-3.so.0suffix";
let flags = scan(blob);
assert!(flags.gtk);
}
}
#[cfg(test)]
mod correctness_tests {
use super::*;
fn tmp(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("onelf-bundle-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
fn fake_elf(class: u8, machine: u16) -> Vec<u8> {
let mut v = vec![0u8; 64];
v[0..4].copy_from_slice(b"\x7fELF");
v[4] = class;
v[18..20].copy_from_slice(&machine.to_le_bytes());
v
}
#[test]
fn nix_stub_matcher_requires_loader_shape() {
let d = tmp("stub");
let mut stub = b"\x7fELF".to_vec();
stub.extend_from_slice(b" ... NixOS cannot run this binary ... ");
let stub_path = d.join("ld-linux-x86-64.so.2");
fs::write(&stub_path, &stub).unwrap();
assert!(is_nix_stub_ld(&stub_path));
let benign = d.join("notes.txt");
fs::write(&benign, b"the NixOS cannot run message appears here").unwrap();
assert!(!is_nix_stub_ld(&benign));
let noelf = d.join("ld-fake.so");
fs::write(&noelf, b"NixOS cannot run").unwrap();
assert!(!is_nix_stub_ld(&noelf));
let _ = fs::remove_dir_all(&d);
}
#[test]
fn find_existing_libs_reports_stubs_without_deleting() {
let d = tmp("dryscan");
let mut stub = b"\x7fELF".to_vec();
stub.extend_from_slice(b" NixOS cannot run ");
let stub_path = d.join("ld-linux-x86-64.so.2");
fs::write(&stub_path, &stub).unwrap();
fs::write(d.join("libfoo.so.1"), fake_elf(2, 62)).unwrap();
let (libs, stubs) = find_existing_libs(&d);
assert!(stub_path.exists(), "the scan must not delete the stub");
assert!(stubs.iter().any(|p| p == &stub_path));
assert!(libs.contains("libfoo.so.1"));
assert!(!libs.contains("ld-linux-x86-64.so.2"));
let _ = fs::remove_dir_all(&d);
}
#[test]
fn runpath_entry_expands_origin() {
let origin = Path::new("/app/bin");
assert_eq!(
expand_runpath_entry("$ORIGIN/../lib", origin),
PathBuf::from("/app/bin/../lib")
);
assert_eq!(
expand_runpath_entry("${ORIGIN}/lib", origin),
PathBuf::from("/app/bin/lib")
);
assert_eq!(
expand_runpath_entry("/usr/lib", origin),
PathBuf::from("/usr/lib")
);
}
#[test]
fn runpath_splits_colons_and_resolves_origin() {
let d = tmp("rpath");
fs::create_dir_all(d.join("a/lib")).unwrap();
fs::create_dir_all(d.join("a/lib64")).unwrap();
fs::create_dir_all(d.join("a/bin")).unwrap();
let list = format!(
"{}:{}",
d.join("a/lib").display(),
d.join("a/lib64").display()
);
let dirs = resolve_runpath_dirs(std::iter::once(list.as_str()), Path::new("/"));
assert!(dirs.contains(&d.join("a/lib")));
assert!(dirs.contains(&d.join("a/lib64")));
let origin = d.join("a/bin");
let dirs = resolve_runpath_dirs(std::iter::once("$ORIGIN/../lib"), &origin);
assert_eq!(dirs.len(), 1);
assert_eq!(
fs::canonicalize(&dirs[0]).unwrap(),
fs::canonicalize(d.join("a/lib")).unwrap()
);
let _ = fs::remove_dir_all(&d);
}
#[test]
fn copy_dedupes_after_filters_and_honors_arch_and_excludes() {
let d = tmp("copy");
let dir1 = d.join("dir1");
let dir2 = d.join("dir2");
let dest = d.join("dest");
fs::create_dir_all(&dir1).unwrap();
fs::create_dir_all(&dir2).unwrap();
std::os::unix::fs::symlink(d.join("nonexistent"), dir1.join("libGL.so.1")).unwrap();
fs::write(dir2.join("libGL.so.1"), fake_elf(2, 62)).unwrap();
fs::write(dir2.join("libwrong.so"), fake_elf(2, 183)).unwrap();
let (copied, _) = copy_prefixed_libs(
&[dir1.clone(), dir2.clone()],
&["libGL", "libwrong"],
&dest,
Some(2), Some(62), &[],
false,
false,
)
.unwrap();
assert!(
dest.join("libGL.so.1").is_file(),
"the valid libGL must not be shadowed by the earlier dangling one"
);
assert!(
!dest.join("libwrong.so").exists(),
"a wrong-arch library must be rejected"
);
assert_eq!(copied, 1);
let dest2 = d.join("dest2");
let (copied2, _) = copy_prefixed_libs(
std::slice::from_ref(&dir2),
&["libGL"],
&dest2,
Some(2),
Some(62),
&["libGL"],
false,
false,
)
.unwrap();
assert_eq!(copied2, 0, "an excluded soname must not be copied");
let _ = fs::remove_dir_all(&d);
}
#[test]
fn bootstrap_alignment_is_16k_safe_on_aarch64() {
assert_eq!(bootstrap_page_align(true), 0x10000);
assert_eq!(bootstrap_page_align(false), 0x1000);
let align = bootstrap_page_align(true);
for (vend, len) in [(0x1234u64, 0x5678u64), (0, 1), (0xffff, 0x10001)] {
let new_vaddr = (vend + align - 1) & !(align - 1);
let file_off = (len + align - 1) & !(align - 1);
for page in [0x1000u64, 0x4000, 0x10000] {
assert_eq!(new_vaddr % page, file_off % page);
}
}
}
#[test]
fn copy_does_not_truncate_a_file_onto_itself() {
let d = tmp("selfcopy");
fs::write(d.join("libself.so.1"), fake_elf(2, 62)).unwrap();
let before = fs::read(d.join("libself.so.1")).unwrap();
assert!(!before.is_empty());
let (_copied, _) = copy_prefixed_libs(
std::slice::from_ref(&d),
&["libself"],
&d, Some(2),
Some(62),
&[],
false,
false,
)
.unwrap();
let after = fs::read(d.join("libself.so.1")).unwrap();
assert_eq!(before, after, "a self-copy must not truncate the file");
let _ = fs::remove_dir_all(&d);
}
}
#[cfg(test)]
mod mtime_tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn read_only_files_and_directories_are_pinned() {
let dir = std::env::temp_dir().join(format!("onelf-mtime-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("sub")).unwrap();
let file = dir.join("ro");
fs::write(&file, b"x").unwrap();
fs::set_permissions(&file, fs::Permissions::from_mode(0o444)).unwrap();
let expected = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.unwrap_or(0);
for path in [&file, &dir.join("sub")] {
normalize_mtime(path);
let mtime = fs::metadata(path)
.unwrap()
.modified()
.unwrap()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
assert_eq!(mtime, expected, "{}", path.display());
}
let _ = fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod dangling_link_tests {
use super::*;
#[test]
fn only_links_that_led_to_the_dropped_object_go() {
let dir = std::env::temp_dir().join(format!("onelf-dangling-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("libfoo.so.1.2"), b"x").unwrap();
std::os::unix::fs::symlink("libfoo.so.1.2", dir.join("libfoo.so.1")).unwrap();
std::os::unix::fs::symlink("libfoo.so.1", dir.join("libfoo.so")).unwrap();
std::os::unix::fs::symlink("libbar.so.9", dir.join("libbar.so")).unwrap();
fs::remove_file(dir.join("libfoo.so.1.2")).unwrap();
remove_dangling_links_beside(&dir.join("libfoo.so.1.2"));
assert!(!dir.join("libfoo.so.1").is_symlink(), "the chain goes");
assert!(!dir.join("libfoo.so").is_symlink(), "the whole chain goes");
assert!(
dir.join("libbar.so").is_symlink(),
"an unrelated link stays"
);
let _ = fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod private_temp_dir_tests {
use super::*;
#[test]
fn each_scratch_directory_is_new_and_private() {
let a = private_temp_dir("probe").unwrap();
let b = private_temp_dir("probe").unwrap();
assert_ne!(a, b);
for dir in [&a, &b] {
let meta = fs::symlink_metadata(dir).unwrap();
assert!(meta.is_dir());
assert_eq!(meta.permissions().mode() & 0o777, 0o700);
}
let _ = fs::remove_dir(&a);
let _ = fs::remove_dir(&b);
}
}
#[cfg(test)]
mod gpu_stack_tests {
use super::*;
#[test]
fn a_traced_gpu_stack_is_left_to_the_host() {
let paths = [
"/usr/lib/libGLX_mesa.so.0",
"/usr/lib/libEGL_nvidia.so.0",
"/usr/lib/libgallium-25.2.so",
"/usr/lib/dri/radeonsi_dri.so",
"/usr/lib/libvulkan_radeon.so",
"/usr/lib/libSDL2-2.0.so.0",
];
let (app, host) = split_gpu_stack(paths.iter().map(PathBuf::from).collect());
assert_eq!(app, vec![PathBuf::from("/usr/lib/libSDL2-2.0.so.0")]);
assert_eq!(host.len(), 5);
}
}
#[cfg(test)]
mod locale_tests {
use super::*;
#[test]
fn c_utf8_is_carried_from_beside_a_multiarch_libc() {
let work = std::env::temp_dir().join(format!("onelf-cutf8-{}", std::process::id()));
let _ = fs::remove_dir_all(&work);
let libc_dir = work.join("usr/lib/x86_64-linux-gnu");
let locale = work.join("usr/lib/locale/C.utf8");
fs::create_dir_all(&libc_dir).unwrap();
fs::create_dir_all(locale.join("LC_MESSAGES")).unwrap();
fs::write(libc_dir.join("libc.so.6"), "").unwrap();
fs::write(locale.join("LC_CTYPE"), "ctype").unwrap();
fs::write(locale.join("LC_MESSAGES/SYS_LC_MESSAGES"), "msg").unwrap();
let dest = work.join("tree/lib");
let copied = bundle_c_utf8(&libc_dir.join("libc.so.6"), &dest).unwrap();
assert_eq!(
copied,
[
"locale/C.utf8/LC_CTYPE",
"locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES"
]
);
assert_eq!(
fs::read_to_string(dest.join("locale/C.utf8/LC_CTYPE")).unwrap(),
"ctype"
);
let _ = fs::remove_dir_all(&work);
}
}