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;
mod color {
use std::io::IsTerminal;
use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
fn enabled() -> bool {
*ENABLED.get_or_init(|| {
std::env::var_os("NO_COLOR").is_none() && std::io::stderr().is_terminal()
})
}
pub fn bold(s: &str) -> String {
if enabled() {
format!("\x1b[1m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn red(s: &str) -> String {
if enabled() {
format!("\x1b[31m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn cyan(s: &str) -> String {
if enabled() {
format!("\x1b[36m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn dim(s: &str) -> String {
if enabled() {
format!("\x1b[2m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn bold_green(s: &str) -> String {
if enabled() {
format!("\x1b[1;32m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn bold_red(s: &str) -> String {
if enabled() {
format!("\x1b[1;31m{s}\x1b[0m")
} else {
s.to_string()
}
}
}
pub(crate) fn format_size(bytes: u64) -> String {
if bytes >= 1_073_741_824 {
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
} else if bytes >= 1_048_576 {
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{bytes} B")
}
}
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));
}
}
}
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 copy_prefixed_libs(
search_dirs: &[PathBuf],
prefixes: &[&str],
dest: &Path,
target_class: Option<u8>,
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 !prefixes.iter().any(|p| name.starts_with(p)) {
continue;
}
if !seen.insert(name.clone()) {
continue;
}
let resolved = fs::canonicalize(&path).unwrap_or(path.clone());
if !resolved.is_file() {
continue;
}
if let Some(tc) = target_class {
if read_elf_class(&resolved) != Some(tc) {
continue;
}
}
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);
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);
}
}
copied += 1;
total_bytes += size;
}
}
Ok((copied, total_bytes))
}
const DEFAULT_EXCLUDES: &[&str] = &[
"libnss_",
"libcuda.so",
"libnvidia",
"libamdhip64.so",
"libze_loader.so",
"linux-vdso.so",
];
const STANDARD_LIB_PATHS: &[&str] = &[
"/usr/lib",
"/usr/lib64",
"/usr/lib/x86_64-linux-gnu",
"/lib",
"/lib64",
"/lib/x86_64-linux-gnu",
];
pub struct BundleOptions {
pub directory: PathBuf,
pub target: 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 strip: bool,
pub strict_libc: bool,
pub scan_dlopen: bool,
pub dlopen_extra: Vec<String>,
}
fn strip_debug(path: &Path) {
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 detected = detect_frameworks(&opts.directory, opts.target.as_deref());
let want_gl = opts.gl || detected.gl;
let want_dri = opts.dri || detected.dri;
let want_vulkan = opts.vulkan || detected.vulkan;
let want_wayland = opts.wayland || detected.wayland;
let want_gtk = opts.gtk || detected.gtk;
if (detected.gl || detected.dri || detected.vulkan || detected.wayland || detected.gtk)
&& (!opts.gl || !opts.dri || !opts.vulkan || !opts.wayland || !opts.gtk)
{
let mut parts = Vec::new();
if detected.gl && !opts.gl {
parts.push("gl");
}
if detected.dri && !opts.dri {
parts.push("dri");
}
if detected.vulkan && !opts.vulkan {
parts.push("vulkan");
}
if detected.wayland && !opts.wayland {
parts.push("wayland");
}
if detected.gtk && !opts.gtk {
parts.push("gtk");
}
if !parts.is_empty() {
eprintln!(
" {} auto-enabled: {}",
color::bold("Frameworks:"),
parts.join(", ")
);
}
}
if want_gl || want_dri || want_vulkan {
bundle_gpu(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
opts.dry_run,
opts.strip,
want_gl,
want_dri,
want_vulkan,
)?;
}
if want_wayland {
bundle_wayland(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
opts.dry_run,
opts.strip,
)?;
}
if want_gtk {
bundle_gtk_data(&opts.directory, opts.dry_run)?;
}
let excludes: Vec<&str> = DEFAULT_EXCLUDES
.iter()
.copied()
.chain(opts.exclude.iter().map(|s| s.as_str()))
.collect();
let elf_files = 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()),
));
}
vec![path]
} 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) {
if 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)"));
}
}
for dir in parse_rpaths(path) {
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.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 existing = find_existing_libs(&opts.directory);
needed_by.retain(|soname, _| !existing.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 mut rewritten = 0usize;
for path in find_elf_files(&opts.directory) {
let perms = fs::metadata(&path)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
let needs_chmod = perms & 0o200 == 0;
if needs_chmod {
let _ = fs::set_permissions(
&path,
std::os::unix::fs::PermissionsExt::from_mode(perms | 0o200),
);
}
if set_origin_runpath(&path).is_ok() {
rewritten += 1;
}
let _ = scrub_nix_store_paths(&path);
let _ = strip_absolute_needed(&path);
if needs_chmod {
let _ = fs::set_permissions(
&path,
std::os::unix::fs::PermissionsExt::from_mode(perms),
);
}
}
if rewritten > 0 {
eprintln!(
"{} RUNPATH to $ORIGIN/../lib in {} binaries",
color::bold("Rewrote"),
rewritten
);
}
match patch_interps_to_bundled(&opts.directory, &lib_dest) {
Ok(n) if n > 0 => eprintln!(
"{} PT_INTERP of {} binaries",
color::bold("Patched"),
n
),
Ok(_) => {}
Err(e) => eprintln!(
"{} PT_INTERP patching failed: {e}",
color::bold_red("warning:"),
),
}
}
return Ok(());
}
let target_class = elf_files.iter().find_map(|f| read_elf_class(f));
let target_libc = elf_files
.iter()
.find_map(|f| parse_interp(f).as_deref().and_then(libc_family_from_interp));
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 = build_lib_cache();
let mut search_paths: Vec<PathBuf> = opts.search_path.clone();
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 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();
if Path::new("/nix/store").is_dir() {
for lib_name in find_existing_libs(&lib_dest) {
if let Some(src) = locate_lib(&lib_name, &ldconfig_cache, &search_paths, target_class) {
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) {
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) {
Some(src) => {
let resolved = fs::canonicalize(&src).unwrap_or(src.clone());
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 content_hash: Option<[u8; 32]> = fs::read(&resolved)
.ok()
.map(|bytes| blake3::hash(&bytes).into());
if let Some(hash) = content_hash {
if 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)?;
ensure_writable(&dest);
fs::copy(&resolved, &dest)?;
let _ = fs::set_permissions(
&dest,
std::os::unix::fs::PermissionsExt::from_mode(0o755),
);
if let Err(e) = set_origin_runpath(&dest) {
eprintln!(
" {} failed to rewrite RUNPATH of {}: {e}",
color::bold_red("warning:"),
soname
);
}
if is_dynamic_loader(&soname) {
if let Err(e) = scrub_loader_paths(&dest) {
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));
for dir in parse_rpaths(&resolved) {
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)
|| existing.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 !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 mut rewritten = 0usize;
let mut scrubbed = 0usize;
for path in find_elf_files(&opts.directory) {
let perms = fs::metadata(&path)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
let needs_chmod = perms & 0o200 == 0;
if needs_chmod {
let _ = fs::set_permissions(
&path,
std::os::unix::fs::PermissionsExt::from_mode(perms | 0o200),
);
}
if set_origin_runpath(&path).is_ok() {
rewritten += 1;
}
let before = fs::metadata(&path).and_then(|m| m.modified()).ok();
let _ = scrub_nix_store_paths(&path);
let _ = strip_absolute_needed(&path);
let after = fs::metadata(&path).and_then(|m| m.modified()).ok();
if before.is_some() && before != after {
scrubbed += 1;
}
if needs_chmod {
let _ =
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(perms));
}
}
if rewritten > 0 {
eprintln!(
"{} RUNPATH to $ORIGIN/../lib in {} binaries",
color::bold("Rewrote"),
rewritten
);
}
if scrubbed > 0 {
eprintln!(
"{} /nix/store paths in {} binaries",
color::bold("Scrubbed"),
scrubbed
);
}
}
if !opts.dry_run {
match patch_interps_to_bundled(&opts.directory, &lib_dest) {
Ok(n) if n > 0 => eprintln!(
"{} PT_INTERP of {} binaries",
color::bold("Patched"),
n
),
Ok(_) => {}
Err(e) => eprintln!(
"{} PT_INTERP patching failed: {e}",
color::bold_red("warning:"),
),
}
}
Ok(())
}
fn find_elf_files(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
for entry in jwalk::WalkDir::new(dir).skip_hidden(false) {
let Ok(entry) = entry else { continue };
let path = entry.path();
if !path.is_file() {
continue;
}
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(directory: &Path, target: Option<&Path>) -> FrameworkFlags {
let files = match target {
Some(t) => {
let p = if t.is_absolute() {
t.to_path_buf()
} else {
directory.join(t)
};
if p.is_file() {
vec![p]
} else {
return FrameworkFlags::default();
}
}
None => find_elf_files(directory),
};
let mut flags = FrameworkFlags::default();
for path in &files {
if let Ok(needed) = parse_needed(path) {
for soname in needed {
inspect_soname_for_frameworks(&soname, &mut flags);
}
}
if let Ok(bytes) = fs::read(path) {
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-3.so")
|| soname.starts_with("libgtk-4.so")
|| soname.starts_with("libgtk-")
{
flags.gtk = true;
}
}
fn scan_framework_strings(bytes: &[u8], flags: &mut FrameworkFlags) {
const PREFIXES: &[&[u8]] = &[
b"libGL.so",
b"libEGL.so",
b"libGLESv",
b"libOpenGL.so",
b"libgbm.so",
b"libvulkan.so",
b"libwayland-client.so",
b"libwayland-egl.so",
b"libwayland-cursor.so",
b"libwayland-server.so",
b"libdecor-0.so",
b"libgtk-3.so",
b"libgtk-4.so",
];
let needles: Vec<&[u8]> = PREFIXES.iter().copied().collect();
let max_len = needles.iter().map(|n| n.len()).max().unwrap_or(0);
let mut i = 0;
while i + max_len <= bytes.len() {
if bytes[i] != b'l' {
i += 1;
continue;
}
for needle in &needles {
if i + needle.len() <= bytes.len() && &bytes[i..i + needle.len()] == *needle {
if let Ok(soname) = std::str::from_utf8(&bytes[i..i + needle.len()]) {
inspect_soname_for_frameworks(soname, flags);
}
break;
}
}
i += 1;
}
}
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() {
if i - s >= 5 {
if let Ok(text) = std::str::from_utf8(&data[s..i]) {
let match_builtin = DLOPEN_CANDIDATES.iter().any(|c| *c == 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)
}
fn parse_needed(path: &Path) -> io::Result<Vec<String>> {
let data = fs::read(path)?;
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
Ok(elf
.libraries
.iter()
.map(|s| {
if s.starts_with('/') {
Path::new(s)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(s)
.to_string()
} else {
s.to_string()
}
})
.collect())
}
fn parse_interp(path: &Path) -> Option<String> {
let data = fs::read(path).ok()?;
let elf = goblin::elf::Elf::parse(&data).ok()?;
elf.interpreter
.map(|s| s.trim_end_matches('\0').to_string())
}
fn libc_alias_for(interp_name: &str) -> Option<String> {
interp_name
.strip_prefix("ld-musl-")
.map(|rest| format!("libc.musl-{rest}"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LibcFamily {
Musl,
Glibc,
}
fn libc_family_from_interp(interp: &str) -> Option<LibcFamily> {
let name = Path::new(interp).file_name()?.to_str()?;
if name.starts_with("ld-musl-") {
Some(LibcFamily::Musl)
} else if name.starts_with("ld-linux") {
Some(LibcFamily::Glibc)
} else {
None
}
}
fn libc_family_of_soname(soname: &str) -> Option<LibcFamily> {
if soname == "libc.so.6" || soname.starts_with("ld-linux") {
Some(LibcFamily::Glibc)
} else if soname.starts_with("libc.musl-")
|| soname.starts_with("ld-musl-")
|| soname == "libc.so"
{
Some(LibcFamily::Musl)
} else {
None
}
}
fn parse_rpaths(path: &Path) -> Vec<PathBuf> {
let Ok(data) = fs::read(path) else {
return Vec::new();
};
let Ok(elf) = goblin::elf::Elf::parse(&data) else {
return Vec::new();
};
elf.runpaths
.iter()
.chain(elf.rpaths.iter())
.map(|s| PathBuf::from(s))
.filter(|p| p.is_absolute() && p.is_dir())
.collect()
}
fn set_origin_runpath(path: &Path) -> io::Result<()> {
const NEW: &[u8] = b"$ORIGIN/../lib:$ORIGIN/../../lib:$ORIGIN/../../../lib";
let data = fs::read(path)?;
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let dynstr_offset = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
.map(|sh| sh.sh_offset as usize);
let Some(dynstr_offset) = dynstr_offset else {
return Ok(());
};
let Some(dynamic) = &elf.dynamic else {
return Ok(());
};
let mut modified = data;
let mut changed = false;
for dyn_entry in &dynamic.dyns {
if dyn_entry.d_tag == goblin::elf::dynamic::DT_RPATH
|| dyn_entry.d_tag == goblin::elf::dynamic::DT_RUNPATH
{
let file_pos = dynstr_offset + dyn_entry.d_val as usize;
if file_pos >= modified.len() {
continue;
}
let mut end = file_pos;
while end < modified.len() && modified[end] != 0 {
end += 1;
}
while end < modified.len() && modified[end] == 0 {
end += 1;
}
let slot_size = end - file_pos;
if NEW.len() + 1 > slot_size {
continue;
}
modified[file_pos..file_pos + NEW.len()].copy_from_slice(NEW);
for i in NEW.len()..slot_size {
modified[file_pos + i] = 0;
}
changed = true;
}
}
if changed {
fs::write(path, &modified)?;
}
Ok(())
}
fn strip_absolute_needed(path: &Path) -> io::Result<()> {
let data = fs::read(path)?;
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let dynstr_offset = elf
.section_headers
.iter()
.find(|sh| elf.shdr_strtab.get_at(sh.sh_name) == Some(".dynstr"))
.map(|sh| sh.sh_offset as usize);
let Some(dynstr_offset) = dynstr_offset else {
return Ok(());
};
let Some(dynamic) = &elf.dynamic else {
return Ok(());
};
let mut modified = data;
let mut changed = false;
for dyn_entry in &dynamic.dyns {
if dyn_entry.d_tag != goblin::elf::dynamic::DT_NEEDED {
continue;
}
let file_pos = dynstr_offset + dyn_entry.d_val as usize;
if file_pos >= modified.len() || modified[file_pos] != b'/' {
continue;
}
let mut end = file_pos;
while end < modified.len() && modified[end] != 0 {
end += 1;
}
let original = &modified[file_pos..end];
let slot_size = end - file_pos;
let basename_start = match original.iter().rposition(|&b| b == b'/') {
Some(p) => p + 1,
None => 0,
};
let basename_len = original.len() - basename_start;
if basename_len == 0 || basename_len >= slot_size {
continue;
}
let basename: Vec<u8> = original[basename_start..].to_vec();
modified[file_pos..file_pos + basename_len].copy_from_slice(&basename);
for i in basename_len..slot_size {
modified[file_pos + i] = 0;
}
changed = true;
}
if changed {
fs::write(path, &modified)?;
}
Ok(())
}
fn is_excluded(soname: &str, excludes: &[&str]) -> bool {
excludes.iter().any(|pat| soname.starts_with(pat))
}
fn is_dynamic_loader(soname: &str) -> bool {
soname.starts_with("ld-linux") || soname.starts_with("ld-musl-") || soname == "ld.so"
}
fn scrub_loader_paths(path: &Path) -> io::Result<()> {
let mut data = fs::read(path)?;
let mut changed = false;
let replacements: &[(&[u8], &[u8])] = &[
(b"/etc/", b"/XXX/"),
(b"/usr/", b"/XXX/"),
(b"/nix/", b"/XXX/"),
(b"/lib/", b"/XXX/"),
(b"/lib64/", b"/XXX///"),
];
for (needle, replace) in replacements {
debug_assert_eq!(needle.len(), replace.len());
let len = needle.len();
let mut i = 0;
while i + len <= data.len() {
if &data[i..i + len] == *needle {
data[i..i + len].copy_from_slice(replace);
changed = true;
i += len;
} else {
i += 1;
}
}
}
if changed {
fs::write(path, &data)?;
}
Ok(())
}
fn scrub_nix_store_paths(path: &Path) -> io::Result<()> {
let mut data = fs::read(path)?;
let mut changed = false;
let rewrites: &[(&[u8], &[u8])] = &[
(b"/share/zoneinfo", b"/usr/share/zoneinfo"),
(b"/bin/locale", b"/usr/bin/locale"),
];
for (suffix, replacement) in rewrites {
let mut i = 0;
while i + suffix.len() <= data.len() {
if &data[i..i + suffix.len()] != *suffix {
i += 1;
continue;
}
let mut start = i;
while start > 0 && data[start - 1] != 0 {
start -= 1;
}
if start + 11 > data.len() || &data[start..start + 11] != b"/nix/store/" {
i = i + suffix.len();
continue;
}
let mut end = i + suffix.len();
while end < data.len() && data[end] != 0 {
end += 1;
}
let slot = end - start;
if replacement.len() + 1 > slot {
i = end;
continue;
}
data[start..start + replacement.len()].copy_from_slice(replacement);
for b in &mut data[start + replacement.len()..end] {
*b = 0;
}
changed = true;
i = end;
}
}
if changed {
fs::write(path, &data)?;
}
Ok(())
}
fn patch_interp(path: &Path, new_interp: &str) -> io::Result<bool> {
let data = fs::read(path)?;
let elf = goblin::elf::Elf::parse(&data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let phdr_idx = elf
.program_headers
.iter()
.position(|p| p.p_type == goblin::elf::program_header::PT_INTERP);
let Some(phdr_idx) = phdr_idx else {
return Ok(false);
};
let ph = &elf.program_headers[phdr_idx];
let offset = ph.p_offset as usize;
let slot = ph.p_filesz as usize;
if offset + slot > data.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"PT_INTERP offset out of file bounds",
));
}
let new_bytes = new_interp.as_bytes();
let existing = &data[offset..offset + slot];
let existing_str = match existing.iter().position(|&b| b == 0) {
Some(n) => &existing[..n],
None => existing,
};
if existing_str == new_bytes {
return Ok(false);
}
let header = elf.header;
drop(elf);
if new_bytes.len() + 1 <= slot {
let mut modified = data;
modified[offset..offset + new_bytes.len()].copy_from_slice(new_bytes);
for b in &mut modified[offset + new_bytes.len()..offset + slot] {
*b = 0;
}
rewrite_interp_phdr(
&mut modified,
&header,
phdr_idx,
offset as u64,
(new_bytes.len() + 1) as u64,
)?;
fs::write(path, &modified)?;
return Ok(true);
}
patch_interp_expand(path, data, &header, phdr_idx, new_bytes)?;
Ok(true)
}
fn rewrite_interp_phdr(
data: &mut [u8],
header: &goblin::elf::Header,
phdr_idx: usize,
new_offset: u64,
new_size: u64,
) -> io::Result<()> {
use goblin::elf::header;
let is_64 = header.e_ident[header::EI_CLASS] == header::ELFCLASS64;
if header.e_ident[header::EI_DATA] != header::ELFDATA2LSB {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"big-endian ELF PT_INTERP rewrite not implemented",
));
}
let e_phoff = header.e_phoff as usize;
let e_phentsize = header.e_phentsize as usize;
let phdr_off = e_phoff + phdr_idx * e_phentsize;
if phdr_off + e_phentsize > data.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"program header table out of bounds",
));
}
if is_64 {
let off_p_offset = phdr_off + 8;
let off_p_filesz = phdr_off + 32;
let off_p_memsz = phdr_off + 40;
data[off_p_offset..off_p_offset + 8].copy_from_slice(&new_offset.to_le_bytes());
data[off_p_filesz..off_p_filesz + 8].copy_from_slice(&new_size.to_le_bytes());
data[off_p_memsz..off_p_memsz + 8].copy_from_slice(&new_size.to_le_bytes());
} else {
let off_p_offset = phdr_off + 4;
let off_p_filesz = phdr_off + 16;
let off_p_memsz = phdr_off + 20;
let new_offset_u32: u32 = new_offset
.try_into()
.map_err(|_| io::Error::other("PT_INTERP offset does not fit in 32 bits"))?;
let new_size_u32: u32 = new_size
.try_into()
.map_err(|_| io::Error::other("PT_INTERP size does not fit in 32 bits"))?;
data[off_p_offset..off_p_offset + 4].copy_from_slice(&new_offset_u32.to_le_bytes());
data[off_p_filesz..off_p_filesz + 4].copy_from_slice(&new_size_u32.to_le_bytes());
data[off_p_memsz..off_p_memsz + 4].copy_from_slice(&new_size_u32.to_le_bytes());
}
Ok(())
}
fn patch_interp_expand(
path: &Path,
data: Vec<u8>,
header: &goblin::elf::Header,
phdr_idx: usize,
new_bytes: &[u8],
) -> io::Result<()> {
let mut modified = data;
let new_offset = modified.len() as u64;
modified.extend_from_slice(new_bytes);
modified.push(0);
rewrite_interp_phdr(
&mut modified,
header,
phdr_idx,
new_offset,
(new_bytes.len() + 1) as u64,
)?;
fs::write(path, &modified)?;
Ok(())
}
fn patch_interps_to_bundled(app_dir: &Path, lib_dest: &Path) -> io::Result<usize> {
let rel_lib = lib_dest
.strip_prefix(app_dir)
.unwrap_or(lib_dest)
.to_path_buf();
let mut patched = 0usize;
for path in find_elf_files(app_dir) {
let Some(interp) = parse_interp(&path) else {
continue;
};
let Some(basename) = Path::new(&interp).file_name().and_then(|n| n.to_str()) else {
continue;
};
let bundled = lib_dest.join(basename);
if !bundled.exists() {
continue;
}
if path.strip_prefix(app_dir).is_err() {
continue;
}
let new_interp = rel_lib
.join(basename)
.to_string_lossy()
.into_owned();
let perms = fs::metadata(&path)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
let needs_chmod = perms & 0o200 == 0;
if needs_chmod {
let _ = fs::set_permissions(
&path,
std::os::unix::fs::PermissionsExt::from_mode(perms | 0o200),
);
}
match patch_interp(&path, &new_interp) {
Ok(true) => patched += 1,
Ok(false) => {}
Err(e) => {
eprintln!(
" {} could not patch PT_INTERP of {}: {e}",
color::bold_red("warning:"),
path.display()
);
}
}
if needs_chmod {
let _ =
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(perms));
}
}
Ok(patched)
}
fn find_existing_libs(dir: &Path) -> HashSet<String> {
let mut libs = HashSet::new();
for entry in jwalk::WalkDir::new(dir).skip_hidden(false) {
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) {
let _ = fs::remove_file(&path);
continue;
}
libs.insert(name.into_owned());
}
libs
}
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
}
fn parse_ldconfig_cache() -> HashMap<String, Vec<PathBuf>> {
let mut cache: HashMap<String, Vec<PathBuf>> = HashMap::new();
let Ok(output) = Command::new("ldconfig").arg("-p").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
}
fn scan_nix_store_libs() -> HashMap<String, Vec<PathBuf>> {
let mut cache: HashMap<String, Vec<PathBuf>> = HashMap::new();
let mut store_paths: HashSet<String> = HashSet::new();
let roots: Vec<&str> = vec![
"/run/current-system",
"~/.nix-profile",
"/etc/profiles/per-user",
];
for root in &roots {
let expanded = if root.starts_with('~') {
if let Ok(home) = std::env::var("HOME") {
root.replacen('~', &home, 1)
} else {
continue;
}
} else {
root.to_string()
};
if !Path::new(&expanded).exists() {
continue;
}
let Ok(output) = Command::new("nix-store").args(["-qR", &expanded]).output() else {
continue;
};
if output.status.success() {
for line in output.stdout.lines().map_while(Result::ok) {
store_paths.insert(line.trim().to_string());
}
}
}
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) {
let Ok(entry) = entry else { continue };
if !entry.file_type().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
}
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])))
}
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)
{
let Ok(entry) = entry else { continue };
if !entry.file_type().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);
}
}
}
}
}
}
fn locate_lib(
soname: &str,
ldconfig_cache: &HashMap<String, Vec<PathBuf>>,
search_paths: &[PathBuf],
target_class: Option<u8>,
) -> Option<PathBuf> {
let class_matches = |path: &Path| -> bool {
match target_class {
Some(tc) => read_elf_class(path) == Some(tc),
None => true,
}
};
let acceptable = |path: &Path| class_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) {
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() {
if let Ok(entries) = fs::read_dir("/nix/store") {
for entry in entries.filter_map(Result::ok) {
let lib_dir = entry.path().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) {
for subdir in subdirs.filter_map(Result::ok) {
if subdir.file_type().map_or(false, |t| t.is_dir()) {
let candidate = subdir.path().join(soname);
if candidate.exists() && acceptable(&candidate) {
return Some(candidate);
}
}
}
}
}
}
}
None
}
fn is_nix_stub_ld(path: &Path) -> bool {
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;
};
bytes
.windows(b"NixOS cannot run".len())
.any(|w| w == b"NixOS cannot run")
}
const DRI_SEARCH_PATHS: &[&str] = &[
"/usr/lib/dri",
"/usr/lib64/dri",
"/usr/lib/x86_64-linux-gnu/dri",
];
const GBM_SEARCH_PATHS: &[&str] = &[
"/usr/lib/gbm",
"/usr/lib64/gbm",
"/usr/lib/x86_64-linux-gnu/gbm",
];
const EGL_SEARCH_PATHS: &[&str] = &["/usr/share/glvnd/egl_vendor.d"];
const VK_SEARCH_PATHS: &[&str] = &["/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d"];
fn bundle_gpu(
directory: &Path,
lib_dir: &Path,
extra_search: &[PathBuf],
dry_run: bool,
strip: bool,
include_gl: bool,
include_dri: bool,
include_vulkan: bool,
) -> io::Result<()> {
eprintln!("{} GPU drivers...", color::bold("Bundling"));
let elf_files = find_elf_files(directory);
let target_class = elf_files.iter().find_map(|f| read_elf_class(f));
let target_machine = elf_files.iter().find_map(|f| read_elf_machine(f));
let mut rpath_dri: Vec<PathBuf> = Vec::new();
let mut rpath_gbm: Vec<PathBuf> = Vec::new();
let mut rpath_egl: Vec<PathBuf> = Vec::new();
let mut rpath_vk: Vec<PathBuf> = Vec::new();
for elf in &elf_files {
for rdir in parse_rpaths(elf) {
let dri = rdir.join("dri");
if dri.is_dir() && !rpath_dri.contains(&dri) {
rpath_dri.push(dri);
}
let gbm = rdir.join("gbm");
if gbm.is_dir() && !rpath_gbm.contains(&gbm) {
rpath_gbm.push(gbm);
}
if let Some(parent) = rdir.parent() {
let egl = parent.join("share/glvnd/egl_vendor.d");
if egl.is_dir() && !rpath_egl.contains(&egl) {
rpath_egl.push(egl);
}
let vk = parent.join("share/vulkan/icd.d");
if vk.is_dir() && !rpath_vk.contains(&vk) {
rpath_vk.push(vk);
}
}
}
}
let mut dri_dirs = rpath_dri;
let mut gbm_dirs = rpath_gbm;
let mut egl_dirs = rpath_egl;
let mut vk_dirs = rpath_vk;
dri_dirs.extend(DRI_SEARCH_PATHS.iter().map(PathBuf::from));
gbm_dirs.extend(GBM_SEARCH_PATHS.iter().map(PathBuf::from));
egl_dirs.extend(EGL_SEARCH_PATHS.iter().map(PathBuf::from));
vk_dirs.extend(VK_SEARCH_PATHS.iter().map(PathBuf::from));
for dir in extra_search {
let dri = dir.join("dri");
if dri.is_dir() && !dri_dirs.contains(&dri) {
dri_dirs.push(dri);
}
let gbm = dir.join("gbm");
if gbm.is_dir() && !gbm_dirs.contains(&gbm) {
gbm_dirs.push(gbm);
}
}
let store_paths = if Path::new("/nix/store").is_dir() {
collect_nix_store_paths()
} else {
Vec::new()
};
if !store_paths.is_empty() {
for sp in &store_paths {
let sp = PathBuf::from(sp);
let dri = sp.join("lib/dri");
if dri.is_dir() && !dri_dirs.contains(&dri) {
dri_dirs.push(dri);
}
let gbm = sp.join("lib/gbm");
if gbm.is_dir() && !gbm_dirs.contains(&gbm) {
gbm_dirs.push(gbm);
}
let egl = sp.join("share/glvnd/egl_vendor.d");
if egl.is_dir() && !egl_dirs.contains(&egl) {
egl_dirs.push(egl);
}
let vk = sp.join("share/vulkan/icd.d");
if vk.is_dir() && !vk_dirs.contains(&vk) {
vk_dirs.push(vk);
}
}
}
let lib_dest = directory.join(lib_dir);
let mesa_lib_dirs: Vec<PathBuf> = dri_dirs
.iter()
.filter_map(|dri_path| {
let parent = dri_path.parent()?;
if parent.is_dir() {
Some(parent.to_path_buf())
} else {
None
}
})
.collect();
let mut gl_search_dirs = mesa_lib_dirs.clone();
for dir in build_lib_search_dirs(&elf_files, extra_search, &store_paths) {
if !gl_search_dirs.contains(&dir) {
gl_search_dirs.push(dir);
}
}
let mut gpu_total_bytes = 0u64;
if include_gl {
remove_conflicting_gl_libs(directory, &lib_dest, dry_run);
}
let mut mesa_count = 0;
if include_gl {
let all_gl: Vec<&str> = MESA_IMPL_PREFIXES
.iter()
.chain(GLVND_PREFIXES.iter())
.copied()
.collect();
let (count, bytes) = copy_prefixed_libs(
&gl_search_dirs,
&all_gl,
&lib_dest,
target_class,
dry_run,
strip,
)?;
mesa_count = count;
gpu_total_bytes += bytes;
if count > 0 {
eprintln!(
" {} {} Mesa/glvnd lib(s) ({})",
verb_str(dry_run),
count,
format_size(bytes)
);
}
}
let mut dri_count = 0;
if include_dri {
let dri_filter = driver_filter(target_machine, DRI_DRIVERS_X86, DRI_DRIVERS_ARM);
let dri_dest = lib_dest.join("dri");
let (count, bytes) = copy_so_dir(
&dri_dirs,
&dri_dest,
target_class,
dri_filter,
dry_run,
strip,
)?;
dri_count = count;
gpu_total_bytes += bytes;
if count > 0 {
eprintln!(
" {} {} DRI driver(s) ({})",
verb_str(dry_run),
count,
format_size(bytes)
);
}
}
let mut gbm_count = 0;
if include_gl {
let gbm_dest = lib_dest.join("gbm");
let (count, bytes) = copy_so_dir(&gbm_dirs, &gbm_dest, target_class, None, dry_run, strip)?;
gbm_count = count;
gpu_total_bytes += bytes;
if count > 0 {
eprintln!(
" {} {} GBM backend(s) ({})",
verb_str(dry_run),
count,
format_size(bytes)
);
}
}
let mut egl_count = 0;
if include_gl {
let egl_dest = directory.join("share/glvnd/egl_vendor.d");
let (count, bytes) =
copy_vendor_json(&egl_dirs, &egl_dest, &lib_dest, target_class, None, dry_run)?;
egl_count = count;
gpu_total_bytes += bytes;
if count > 0 {
eprintln!(
" {} {} EGL vendor config(s) ({})",
verb_str(dry_run),
count,
format_size(bytes)
);
}
}
let mut vk_count = 0;
if include_vulkan {
let vk_filter = driver_filter(target_machine, VULKAN_DRIVERS_X86, VULKAN_DRIVERS_ARM);
let vk_dest = directory.join("share/vulkan/icd.d");
let (count, bytes) = copy_vendor_json(
&vk_dirs,
&vk_dest,
&lib_dest,
target_class,
vk_filter,
dry_run,
)?;
vk_count = count;
gpu_total_bytes += bytes;
if count > 0 {
eprintln!(
" {} {} Vulkan ICD config(s) ({})",
verb_str(dry_run),
count,
format_size(bytes)
);
}
}
let mut data_count = 0u64;
if include_gl || include_dri {
let share_dirs: Vec<PathBuf> = mesa_lib_dirs
.iter()
.filter_map(|lib_dir| {
lib_dir.parent().map(|p| p.join("share"))
})
.filter(|p| p.is_dir())
.collect();
let mut all_share = share_dirs;
for path in &["/usr/share", "/usr/local/share"] {
let p = PathBuf::from(path);
if p.is_dir() && !all_share.contains(&p) {
all_share.push(p);
}
}
for share in &all_share {
let drirc = share.join("drirc.d");
if drirc.is_dir() {
let dest = directory.join("share/drirc.d");
let count = copy_data_dir(&drirc, &dest, dry_run)?;
data_count += count;
if count > 0 {
break;
}
}
}
for share in &all_share {
let libdrm = share.join("libdrm");
if libdrm.is_dir() {
let dest = directory.join("share/libdrm");
let count = copy_data_dir(&libdrm, &dest, dry_run)?;
data_count += count;
if count > 0 {
break;
}
}
}
if data_count > 0 {
eprintln!(" {} {} Mesa data file(s)", verb_str(dry_run), data_count);
}
}
let total_count =
mesa_count + dri_count + gbm_count + egl_count + vk_count + data_count as usize;
if total_count == 0 {
eprintln!(
" {} no GPU assets found on this system",
color::bold_red("warning:")
);
} else if gpu_total_bytes > 0 {
eprintln!(
" {} {}",
color::bold("GPU total:"),
format_size(gpu_total_bytes)
);
}
Ok(())
}
fn copy_so_dir(
src_dirs: &[PathBuf],
dest: &Path,
target_class: Option<u8>,
name_filter: Option<&[&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 src_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() {
continue;
}
let name = match path.file_name() {
Some(n) => n.to_string_lossy().into_owned(),
None => continue,
};
if !name.contains(".so") {
continue;
}
if let Some(allowed) = name_filter {
if !allowed.iter().any(|a| name.starts_with(a)) {
continue;
}
}
if !seen.insert(name.clone()) {
continue;
}
if let Some(tc) = target_class {
if read_elf_class(&path) != Some(tc) {
continue;
}
}
let resolved = fs::canonicalize(&path).unwrap_or(path.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);
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);
}
}
copied += 1;
total_bytes += size;
}
}
Ok((copied, total_bytes))
}
const MESA_IMPL_PREFIXES: &[&str] = &[
"libGLX_mesa.so",
"libEGL_mesa.so",
"libglapi.so",
"libgbm.so",
"libxatracker.so",
];
const GLVND_PREFIXES: &[&str] = &[
"libGL.so",
"libGLX.so",
"libEGL.so",
"libGLESv2.so",
"libOpenGL.so",
"libGLdispatch.so",
];
const ALL_GL_PREFIXES: &[&str] = &[
"libGL.so",
"libGLX.so",
"libEGL.so",
"libGLESv2.so",
"libOpenGL.so",
"libGLdispatch.so",
"libGLX_mesa.so",
"libEGL_mesa.so",
"libglapi.so",
"libgbm.so",
"libxatracker.so",
"libGLU.so",
];
fn remove_conflicting_gl_libs(directory: &Path, lib_dest: &Path, dry_run: bool) {
let lib_dest_canon = fs::canonicalize(lib_dest).unwrap_or_else(|_| {
fs::canonicalize(directory)
.unwrap_or_else(|_| directory.to_path_buf())
.join(&lib_dest.strip_prefix(directory).unwrap_or(lib_dest))
});
let mut to_remove: Vec<PathBuf> = Vec::new();
collect_gl_conflicts(directory, &lib_dest_canon, &mut to_remove);
for path in &to_remove {
let rel = path.strip_prefix(directory).unwrap_or(path);
let label = if path.is_symlink() && !path.exists() {
"dangling symlink"
} else {
"conflicts with bundled glvnd"
};
eprintln!(
" {} {} ({})",
color::bold_red("Removing"),
rel.display(),
label,
);
if !dry_run {
let _ = fs::remove_file(path);
}
}
}
fn collect_gl_conflicts(dir: &Path, lib_dest_canon: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let is_symlink = path.is_symlink();
if path.is_dir() && !is_symlink {
collect_gl_conflicts(&path, lib_dest_canon, out);
continue;
}
if !is_symlink && !path.is_file() {
continue;
}
let name = match path.file_name() {
Some(n) => n.to_string_lossy(),
None => continue,
};
if !ALL_GL_PREFIXES.iter().any(|p| name.starts_with(p)) {
continue;
}
if let Some(parent) = path.parent() {
let parent_canon = fs::canonicalize(parent).unwrap_or(parent.to_path_buf());
if parent_canon == *lib_dest_canon {
continue;
}
}
out.push(path);
}
}
const WAYLAND_LIB_PREFIXES: &[&str] = &[
"libwayland-client.so",
"libwayland-server.so",
"libwayland-cursor.so",
"libwayland-egl.so",
"libdecor-0.so",
"libxkbcommon.so",
];
fn bundle_wayland(
directory: &Path,
lib_dir: &Path,
extra_search: &[PathBuf],
dry_run: bool,
strip: bool,
) -> io::Result<()> {
eprintln!("{} Wayland libraries...", color::bold("Bundling"));
let elf_files = find_elf_files(directory);
let target_class = elf_files.iter().find_map(|f| read_elf_class(f));
let nix_paths = if Path::new("/nix/store").is_dir() {
collect_nix_store_paths()
} else {
Vec::new()
};
let search_dirs = build_lib_search_dirs(&elf_files, extra_search, &nix_paths);
let lib_dest = directory.join(lib_dir);
let (copied, total_bytes) = copy_prefixed_libs(
&search_dirs,
WAYLAND_LIB_PREFIXES,
&lib_dest,
target_class,
dry_run,
strip,
)?;
if copied > 0 {
eprintln!(
" {} {} Wayland lib(s) ({})",
verb_str(dry_run),
copied,
format_size(total_bytes)
);
}
let plugin_dirs: Vec<PathBuf> = search_dirs
.iter()
.map(|d| d.join("libdecor/plugins-1"))
.filter(|d| d.is_dir())
.collect();
let plugin_dest = directory.join("share/libdecor/plugins-1");
let (plugin_count, _) = copy_so_dir(
&plugin_dirs,
&plugin_dest,
target_class,
None,
dry_run,
strip,
)?;
if plugin_count > 0 {
eprintln!(
" {} {} libdecor plugin(s)",
verb_str(dry_run),
plugin_count
);
}
if copied == 0 && plugin_count == 0 {
eprintln!(
" {} no Wayland libraries found on this system",
color::bold_red("warning:")
);
}
Ok(())
}
fn bundle_gtk_data(directory: &Path, dry_run: bool) -> io::Result<()> {
eprintln!("{} GTK data...", color::bold("Bundling"));
let dest = directory.join("share/glib-2.0/schemas");
if dest.join("gschemas.compiled").exists() {
eprintln!(" {} already present", color::dim("gschemas.compiled"));
return Ok(());
}
let mut schema_dirs: Vec<PathBuf> = Vec::new();
for path in &[
"/usr/share/glib-2.0/schemas",
"/usr/local/share/glib-2.0/schemas",
] {
let p = PathBuf::from(path);
if p.is_dir() && !schema_dirs.contains(&p) {
schema_dirs.push(p);
}
}
if Path::new("/nix/store").is_dir() {
for sp in &collect_nix_store_paths() {
let p = PathBuf::from(sp);
let standard = p.join("share/glib-2.0/schemas");
if standard.is_dir() && !schema_dirs.contains(&standard) {
schema_dirs.push(standard);
}
let gs_dir = p.join("share/gsettings-schemas");
if gs_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&gs_dir) {
for entry in entries.filter_map(Result::ok) {
let schemas = entry.path().join("glib-2.0/schemas");
if schemas.is_dir() && !schema_dirs.contains(&schemas) {
schema_dirs.push(schemas);
}
}
}
}
}
}
if let Ok(xdg) = std::env::var("XDG_DATA_DIRS") {
for dir in xdg.split(':').filter(|d| !d.is_empty()) {
let schemas = PathBuf::from(dir).join("glib-2.0/schemas");
if schemas.is_dir() && !schema_dirs.contains(&schemas) {
schema_dirs.push(schemas);
}
let gs_dir = PathBuf::from(dir).join("gsettings-schemas");
if gs_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&gs_dir) {
for entry in entries.filter_map(Result::ok) {
let schemas = entry.path().join("glib-2.0/schemas");
if schemas.is_dir() && !schema_dirs.contains(&schemas) {
schema_dirs.push(schemas);
}
}
}
}
}
}
if schema_dirs.is_empty() {
eprintln!(
" {} no GSettings schema directories found",
color::bold_red("warning:")
);
return Ok(());
}
let tmp = directory.join(".onelf-schemas-tmp");
if !dry_run {
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp)?;
}
let mut xml_count = 0usize;
let mut seen: HashSet<String> = HashSet::new();
for schema_dir in &schema_dirs {
let Ok(entries) = fs::read_dir(schema_dir) else {
continue;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let name = match path.file_name() {
Some(n) => n.to_string_lossy().into_owned(),
None => continue,
};
if !name.ends_with(".gschema.xml") && !name.ends_with(".enums.xml") {
continue;
}
if !seen.insert(name.clone()) {
continue;
}
if !dry_run {
fs::copy(&path, tmp.join(&name))?;
}
xml_count += 1;
}
}
if xml_count == 0 {
eprintln!(
" {} no .gschema.xml files found",
color::bold_red("warning:")
);
let _ = fs::remove_dir_all(&tmp);
return Ok(());
}
eprintln!(
" Collected {} schema XML files from {} source(s)",
xml_count,
schema_dirs.len()
);
if dry_run {
eprintln!(
" {} compile {} schema files",
color::bold("Would"),
xml_count
);
let _ = fs::remove_dir_all(&tmp);
return Ok(());
}
let compiler = find_glib_compile_schemas();
fs::create_dir_all(&dest)?;
let output = Command::new(&compiler)
.arg("--targetdir")
.arg(&dest)
.arg(&tmp)
.output();
let _ = fs::remove_dir_all(&tmp);
match output {
Ok(out) if out.status.success() => {
let size = fs::metadata(dest.join("gschemas.compiled"))
.map(|m| m.len())
.unwrap_or(0);
eprintln!(
" {} GSettings schemas ({}, {} sources)",
color::bold_green("Compiled"),
format_size(size),
xml_count
);
}
Ok(out) => {
eprintln!(
" {} glib-compile-schemas failed: {}",
color::bold_red("error:"),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Err(e) => {
eprintln!(
" {} glib-compile-schemas not found: {e}",
color::bold_red("error:")
);
eprintln!(" hint: install glib development tools");
}
}
Ok(())
}
fn find_glib_compile_schemas() -> PathBuf {
if let Ok(output) = Command::new("which").arg("glib-compile-schemas").output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return PathBuf::from(path);
}
}
}
if Path::new("/nix/store").is_dir() {
if let Ok(entries) = fs::read_dir("/nix/store") {
for entry in entries.filter_map(Result::ok) {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.contains("glib-") && name.ends_with("-dev") {
let candidate = entry.path().join("bin/glib-compile-schemas");
if candidate.is_file() {
return candidate;
}
}
}
}
}
PathBuf::from("glib-compile-schemas")
}
fn copy_vendor_json(
src_dirs: &[PathBuf],
json_dest: &Path,
lib_dest: &Path,
target_class: Option<u8>,
driver_filter: Option<&[&str]>,
dry_run: bool,
) -> io::Result<(usize, u64)> {
let mut copied = 0usize;
let mut total_bytes = 0u64;
let mut seen: HashSet<String> = HashSet::new();
for dir in src_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() {
continue;
}
let name = match path.file_name() {
Some(n) => n.to_string_lossy().into_owned(),
None => continue,
};
if !name.ends_with(".json") {
continue;
}
if !seen.insert(name.clone()) {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let (rewritten, so_path) = rewrite_library_path(&content, &path);
if let Some(ref so_src) = so_path {
let resolved = fs::canonicalize(so_src).unwrap_or(so_src.clone());
if let Some(allowed) = driver_filter {
let so_fname = resolved.file_name().unwrap_or_default().to_string_lossy();
if !allowed.iter().any(|a| so_fname.starts_with(a)) {
continue;
}
}
if let Some(tc) = target_class {
if read_elf_class(&resolved) != Some(tc) {
continue;
}
}
let so_name = resolved
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let so_size = fs::metadata(&resolved).map(|m| m.len()).unwrap_or(0);
eprintln!(
" {} <- {} ({})",
color::bold_green(&so_name),
resolved.display(),
color::dim(&format_size(so_size))
);
if !dry_run {
fs::create_dir_all(lib_dest)?;
let dest_so = lib_dest.join(&so_name);
if !dest_so.exists() {
fs::copy(&resolved, &dest_so)?;
let _ = fs::set_permissions(&dest_so, PermissionsExt::from_mode(0o755));
}
}
total_bytes += so_size;
}
eprintln!(" {} <- {}", color::bold_green(&name), path.display());
if !dry_run {
fs::create_dir_all(json_dest)?;
let dest_json = json_dest.join(&name);
ensure_writable(&dest_json);
fs::write(&dest_json, &rewritten)?;
}
copied += 1;
}
}
Ok((copied, total_bytes))
}
fn rewrite_library_path(content: &str, json_path: &Path) -> (String, Option<PathBuf>) {
let key = "\"library_path\"";
let Some(key_pos) = content.find(key) else {
return (content.to_string(), None);
};
let after_key = &content[key_pos + key.len()..];
let after_colon = match after_key.find(':') {
Some(i) => &after_key[i + 1..],
None => return (content.to_string(), None),
};
let Some(open_quote) = after_colon.find('"') else {
return (content.to_string(), None);
};
let value_start = after_colon[open_quote + 1..].as_ptr() as usize - content.as_ptr() as usize;
let value_slice = &content[value_start..];
let Some(close_quote) = value_slice.find('"') else {
return (content.to_string(), None);
};
let lib_path_str = &content[value_start..value_start + close_quote];
let lib_path = Path::new(lib_path_str);
let resolved = if lib_path.is_absolute() {
PathBuf::from(lib_path_str)
} else {
let dir = json_path.parent().unwrap_or(Path::new("."));
dir.join(lib_path_str)
};
let filename = resolved
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let mut rewritten = String::with_capacity(content.len());
rewritten.push_str(&content[..value_start]);
rewritten.push_str(&filename);
rewritten.push_str(&content[value_start + close_quote..]);
(rewritten, Some(resolved))
}
fn copy_data_dir(src: &Path, dest: &Path, dry_run: bool) -> io::Result<u64> {
let mut count = 0u64;
let entries = fs::read_dir(src)?;
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !path.is_file() {
continue;
}
let name = path.file_name().unwrap();
eprintln!(
" {} <- {}",
color::bold_green(&name.to_string_lossy()),
path.display()
);
if !dry_run {
fs::create_dir_all(dest)?;
let dest_path = dest.join(name);
ensure_writable(&dest_path);
fs::copy(&path, &dest_path)?;
let _ = fs::set_permissions(&dest_path, PermissionsExt::from_mode(0o644));
}
count += 1;
}
Ok(count)
}
fn collect_nix_store_paths() -> Vec<String> {
let mut store_paths: HashSet<String> = HashSet::new();
let roots: &[&str] = &["/run/current-system", "/etc/profiles/per-user"];
let home_profile = std::env::var("HOME")
.ok()
.map(|h| format!("{h}/.nix-profile"));
for root in roots.iter().copied().chain(home_profile.as_deref()) {
if !Path::new(root).exists() {
continue;
}
let Ok(output) = Command::new("nix-store").args(["-qR", root]).output() else {
continue;
};
if output.status.success() {
for line in output.stdout.lines().map_while(Result::ok) {
store_paths.insert(line.trim().to_string());
}
}
}
store_paths.into_iter().collect()
}