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,
}
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()),
));
}
if opts.gl || opts.dri || opts.vulkan {
bundle_gpu(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
opts.dry_run,
opts.strip,
opts.gl,
opts.dri,
opts.vulkan,
)?;
}
if opts.wayland {
bundle_wayland(
&opts.directory,
&opts.lib_dir,
&opts.search_path,
opts.dry_run,
opts.strip,
)?;
}
if opts.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 {
match parse_needed(path) {
Ok(libs) => {
let requirer = path
.strip_prefix(&opts.directory)
.unwrap_or(path)
.to_string_lossy()
.into_owned();
for lib in libs {
needed_by.entry(lib).or_insert_with(|| requirer.clone());
}
}
Err(e) => {
eprintln!("warning: {}: {e}", path.display());
}
}
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());
}
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.");
return Ok(());
}
let target_class = elf_files.iter().find_map(|f| read_elf_class(f));
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 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);
expand_nix_cache(&resolved, &mut ldconfig_cache, &mut expanded_nix);
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) = strip_rpath(&dest) {
eprintln!(
" {} failed to strip rpath from {}: {e}",
color::bold_red("warning:"),
soname
);
}
if opts.strip {
strip_debug(&dest);
}
}
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 {
if let Ok(transitive) = parse_needed(&resolved) {
for dep in transitive {
if !already_processed.contains(&dep)
&& !is_excluded(&dep, &excludes)
&& !existing.contains(&dep)
{
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 stripped = 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 strip_rpath(&path).is_ok() {
stripped += 1;
}
if needs_chmod {
let _ =
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(perms));
}
}
if stripped > 0 {
eprintln!(
"{} RPATHs from {} binaries",
color::bold("Stripped"),
stripped
);
}
}
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,
}
}
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| s.to_string()).collect())
}
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 strip_rpath(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_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() && modified[file_pos] != 0 {
let mut pos = file_pos;
while pos < modified.len() && modified[pos] != 0 {
modified[pos] = 0;
pos += 1;
}
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 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 };
if let Some(name) = entry.path().file_name() {
let name = name.to_string_lossy();
if name.contains(".so") {
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,
}
};
if let Some(paths) = ldconfig_cache.get(soname) {
for path in paths {
if path.exists() && class_matches(path) {
return Some(path.clone());
}
}
}
for dir in STANDARD_LIB_PATHS {
let candidate = Path::new(dir).join(soname);
if candidate.exists() && class_matches(&candidate) {
return Some(candidate);
}
}
for dir in search_paths {
let candidate = dir.join(soname);
if candidate.exists() && class_matches(&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() && class_matches(&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() && class_matches(&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() && class_matches(&candidate) {
return Some(candidate);
}
}
}
}
}
}
}
None
}
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()
}