use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use crate::drivers::{self, DRIVER_FAMILIES};
use crate::footer::HostLibsPolicy;
use crate::verdef::{self, Choice, VersionSet};
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Resolution {
pub farm: Option<PathBuf>,
pub host_interp: Option<PathBuf>,
pub incomparable: Vec<String>,
}
pub struct Request<'a> {
pub pkg_root: &'a Path,
pub lib_dirs: &'a [&'a str],
pub policy: HostLibsPolicy,
pub store: &'a Path,
pub ld_cache: &'a Path,
pub icd_dirs: &'a [&'a str],
pub extra_root: Option<&'a Path>,
pub set_roots: &'a [PathBuf],
pub host_libc: bool,
}
pub const GL_BUILD_LIB_DIRS: &[&str] = &["lib", "lib64"];
pub const GL_BUILD_ICD_DIRS: &[&str] = &["share/vulkan/icd.d", "share/glvnd/egl_vendor.d"];
const GL_BUILD_SEEDS: &[&str] = &[
"libGLX_",
"libEGL_",
"libgallium",
"libdril",
"libvulkan_",
"libVkLayer_",
];
const HOST_SEEDS: &[&str] = &["libGLX_"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gl {
Bundled,
Host,
Absent,
}
const GL_ENTRY: &[&str] = &["libGL.so", "libEGL.so", "libGLESv2.so", "libvulkan.so"];
fn has_gl_entry(libs: &BTreeMap<String, PathBuf>) -> bool {
libs.keys()
.any(|name| GL_ENTRY.iter().any(|p| name.starts_with(p)))
}
fn gl_entries_wanted(carried: &BTreeMap<String, PathBuf>) -> BTreeSet<String> {
let mut wanted = BTreeSet::new();
for path in carried.values() {
for needed in verdef::read_needed(path).unwrap_or_default() {
if GL_ENTRY.iter().any(|p| needed.starts_with(p)) && !carried.contains_key(&needed) {
wanted.insert(needed);
}
}
}
wanted
}
pub fn gl_situation(
pkg_root: &Path,
lib_dirs: &[&str],
extra_dirs: &[PathBuf],
ld_cache: &Path,
icd_dirs: &[&str],
) -> Gl {
let mut carried = bundled_libs(pkg_root, lib_dirs);
for dir in extra_dirs {
for (name, path) in libs_in(dir) {
carried.entry(name).or_insert(path);
}
}
if has_gl_entry(&carried) {
return Gl::Bundled;
}
let host = HostIndex::load(ld_cache, icd_dirs, None).libs;
let wanted = gl_entries_wanted(&carried);
if wanted.is_empty() {
return if has_gl_entry(&host) {
Gl::Host
} else {
Gl::Absent
};
}
if wanted.iter().all(|name| host.contains_key(name)) {
Gl::Host
} else {
Gl::Absent
}
}
const LIBC: &str = "libc.so.6";
const LIBC_FAMILY: &[&str] = &[
"libc.so",
"libm.so",
"libpthread.so",
"libdl.so",
"librt.so",
"libresolv.so",
"libutil.so",
"libanl.so",
"libnss_",
"libBrokenLocale.so",
"libmvec.so",
"libc_malloc_debug.so",
"libthread_db.so",
"ld-linux",
];
pub const ICD_DIRS: &[&str] = &[
"/run/opengl-driver/share/vulkan/icd.d",
"/etc/vulkan/icd.d",
"/usr/share/vulkan/icd.d",
"/run/opengl-driver/share/glvnd/egl_vendor.d",
"/etc/glvnd/egl_vendor.d",
"/usr/share/glvnd/egl_vendor.d",
];
pub fn is_libc_family(soname: &str) -> bool {
LIBC_FAMILY.iter().any(|p| soname.starts_with(p))
}
fn is_driver_family(soname: &str) -> bool {
DRIVER_FAMILIES.iter().any(|p| soname.starts_with(p))
}
pub fn resolve(req: &Request) -> Resolution {
if req.policy == HostLibsPolicy::Never {
return Resolution::default();
}
let host = HostIndex::load(req.ld_cache, req.icd_dirs, req.extra_root);
if host.libs.is_empty() {
return Resolution::default();
}
let fingerprint = fingerprint(
req.ld_cache,
req.icd_dirs,
req.policy,
req.extra_root,
req.host_libc,
);
if let Some(recorded) = load_recorded(req.store, &fingerprint) {
return recorded;
}
let decision = decide_on(req, &host);
let winners = &decision.winners;
let farm = match materialize(req.store, &fingerprint, winners) {
Ok(farm) => farm,
Err(e) => {
eprintln!(
"onelf-rt: resolver: cannot write {}: {e}",
req.store.display()
);
return Resolution::default();
}
};
let resolution = Resolution {
farm,
host_interp: decision.host_interp,
incomparable: decision.incomparable,
};
if let Err(e) = record(req.store, &fingerprint, &resolution, winners) {
eprintln!(
"onelf-rt: resolver: cannot record {}: {e}",
req.store.display()
);
}
resolution
}
#[derive(Debug, Default)]
pub struct Decision {
pub compared: Vec<(String, Option<Choice>)>,
pub winners: BTreeMap<String, PathBuf>,
pub host_interp: Option<PathBuf>,
pub incomparable: Vec<String>,
pub farm: Option<PathBuf>,
}
pub fn decide(req: &Request) -> Decision {
if req.policy == HostLibsPolicy::Never {
return Decision::default();
}
let host = HostIndex::load(req.ld_cache, req.icd_dirs, req.extra_root);
if host.libs.is_empty() {
return Decision::default();
}
let mut decision = decide_on(req, &host);
if !decision.winners.is_empty() {
let fingerprint = fingerprint(
req.ld_cache,
req.icd_dirs,
req.policy,
req.extra_root,
req.host_libc,
);
decision.farm = Some(farm_dir(req.store, &fingerprint, &decision.winners));
}
decision
}
fn decide_on(req: &Request, host: &HostIndex) -> Decision {
let mut bundled = bundled_libs(req.pkg_root, req.lib_dirs);
for root in req.set_roots {
for (name, path) in bundled_libs(root, GL_BUILD_LIB_DIRS) {
bundled.entry(name).or_insert(path);
}
}
let mut decision = Decision::default();
if let (true, Some(bundled_libc), Some(host_libc)) =
(req.host_libc, bundled.get(LIBC), host.libs.get(LIBC))
{
let choice = verdef::compare(&versions(bundled_libc), &versions(host_libc));
decision.compared.push((LIBC.to_string(), Some(choice)));
match choice {
Choice::Host => {
if let Some(interp) = host.loader_for(host_libc) {
for name in bundled.keys().filter(|n| is_libc_family(n)) {
let path = if name.starts_with("ld-linux") {
Some(interp.clone())
} else {
host.libs.get(name).cloned()
};
if let Some(path) = path {
decision.winners.insert(name.clone(), path);
}
}
decision.host_interp = Some(interp);
}
}
Choice::Incomparable => decision.incomparable.push(LIBC.to_string()),
Choice::Bundle => {}
}
}
let mut candidates: Vec<String> = host.driver_closure().into_iter().collect();
if req.policy == HostLibsPolicy::Always {
candidates.extend(bundled.keys().cloned());
}
candidates.sort();
candidates.dedup();
for name in candidates {
if is_libc_family(&name) {
continue;
}
let Some(host_path) = host.libs.get(&name) else {
continue;
};
let choice = bundled
.get(&name)
.map(|bundled_path| verdef::compare(&versions(bundled_path), &versions(host_path)));
decision.compared.push((name.clone(), choice));
match choice.unwrap_or(Choice::Host) {
Choice::Host => {
decision.winners.insert(name, host_path.clone());
}
Choice::Incomparable => decision.incomparable.push(name),
Choice::Bundle => {}
}
}
decision
}
fn versions(path: &Path) -> VersionSet {
verdef::read(path).unwrap_or_default()
}
struct HostIndex {
libs: BTreeMap<String, PathBuf>,
icd_libs: Vec<PathBuf>,
module_needs: BTreeSet<String>,
}
impl HostIndex {
fn load(ld_cache: &Path, icd_dirs: &[&str], extra_root: Option<&Path>) -> Self {
let mut libs = BTreeMap::new();
let mut icd_libs = Vec::new();
if let Some(root) = extra_root {
libs.extend(bundled_libs(root, GL_BUILD_LIB_DIRS));
icd_libs.extend(
libs.iter()
.filter(|(name, _)| GL_BUILD_SEEDS.iter().any(|p| name.starts_with(p)))
.map(|(_, path)| path.clone()),
);
let dirs: Vec<String> = GL_BUILD_ICD_DIRS
.iter()
.map(|d| root.join(d).to_string_lossy().into_owned())
.collect();
let dirs: Vec<&str> = dirs.iter().map(String::as_str).collect();
icd_libs.extend(icd_library_paths_in(&dirs, Some(&libs)));
}
for (soname, path) in drivers::cache_paths_in(ld_cache) {
libs.entry(soname).or_insert_with(|| PathBuf::from(path));
}
icd_libs.extend(icd_library_paths_in(icd_dirs, Some(&libs)));
for path in &icd_libs {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
libs.entry(name.to_string()).or_insert_with(|| path.clone());
}
}
let mut module_needs = BTreeSet::new();
for dir in module_dirs(ld_cache) {
let Ok(entries) = fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "so") {
module_needs.extend(verdef::read_needed(&path).unwrap_or_default());
}
}
}
HostIndex {
libs,
icd_libs,
module_needs,
}
}
fn loader_for(&self, host_libc: &Path) -> Option<PathBuf> {
if let Some(interp) = crate::elf::read_interp(host_libc) {
let interp = PathBuf::from(interp);
if interp.is_file() {
return Some(interp);
}
}
self.libs
.iter()
.find(|(name, path)| name.starts_with("ld-linux") && path.is_file())
.map(|(_, path)| path.clone())
}
fn driver_closure(&self) -> HashSet<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut queue: VecDeque<String> = self
.libs
.keys()
.filter(|name| is_driver_family(name) || HOST_SEEDS.iter().any(|p| name.starts_with(p)))
.cloned()
.collect();
for path in &self.icd_libs {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
queue.push_back(name.to_string());
}
}
queue.extend(self.module_needs.iter().cloned());
while let Some(name) = queue.pop_front() {
if !seen.insert(name.clone()) {
continue;
}
let Some(path) = self.libs.get(&name) else {
continue;
};
for needed in verdef::read_needed(path).unwrap_or_default() {
if !seen.contains(&needed) {
queue.push_back(needed);
}
}
}
seen
}
}
fn module_dirs(ld_cache: &Path) -> Vec<PathBuf> {
drivers::cache_dirs_in(ld_cache)
.into_iter()
.map(|dir| Path::new(&dir).join("dri"))
.filter(|dir| dir.is_dir())
.collect()
}
fn icd_library_paths_in(
icd_dirs: &[&str],
bare_names: Option<&BTreeMap<String, PathBuf>>,
) -> Vec<PathBuf> {
let mut out = Vec::new();
for dir in icd_dirs {
let Ok(entries) = fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let Ok(text) = fs::read_to_string(&path) else {
continue;
};
let Some(value) = json_library_path(&text) else {
continue;
};
if !value.contains('/') {
if let Some(lib) = bare_names.and_then(|names| names.get(value)) {
out.push(lib.clone());
}
continue;
}
let lib = if value.starts_with('/') {
PathBuf::from(value)
} else {
path.parent().unwrap_or(Path::new("/")).join(value)
};
if lib.is_file() {
out.push(lib);
}
}
}
out
}
pub fn json_library_path(text: &str) -> Option<&str> {
let at = text.find("\"library_path\"")?;
let rest = &text[at + "\"library_path\"".len()..];
let rest = rest.trim_start().strip_prefix(':')?.trim_start();
let rest = rest.strip_prefix('"')?;
let end = rest.find('"')?;
Some(&rest[..end])
}
fn bundled_libs(pkg_root: &Path, lib_dirs: &[&str]) -> BTreeMap<String, PathBuf> {
let mut out = BTreeMap::new();
for dir in lib_dirs {
for (name, path) in libs_in(&pkg_root.join(dir)) {
out.entry(name).or_insert(path);
}
}
out
}
fn libs_in(dir: &Path) -> Vec<(String, PathBuf)> {
let Ok(entries) = fs::read_dir(dir) else {
return Vec::new();
};
entries
.flatten()
.filter_map(|entry| {
let name = entry.file_name().to_str().map(String::from)?;
let path = entry.path();
let placed = fs::read_link(&path).is_ok_and(|t| t.is_absolute());
(name.contains(".so") && !placed && path.is_file()).then_some((name, path))
})
.collect()
}
fn fingerprint(
ld_cache: &Path,
icd_dirs: &[&str],
policy: HostLibsPolicy,
extra_root: Option<&Path>,
host_libc: bool,
) -> String {
let mut out = format!("onelf-resolve-1 {}", policy.as_str());
if !host_libc {
out.push_str(" bundled-libc");
}
let mut stamp = |path: &Path| {
out.push('\x1f');
out.push_str(&path.to_string_lossy());
match fs::metadata(path) {
Ok(md) => {
let mtime = md
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
out.push_str(&format!("={}:{mtime}", md.len()));
}
Err(_) => out.push_str("=missing"),
}
};
stamp(ld_cache);
for dir in drivers::cache_dirs_in(ld_cache) {
stamp(Path::new(&dir));
}
for dir in module_dirs(ld_cache) {
stamp(&dir);
}
for dir in icd_dirs {
stamp(Path::new(dir));
}
if let Some(root) = extra_root {
for dir in GL_BUILD_LIB_DIRS.iter().chain(GL_BUILD_ICD_DIRS) {
stamp(&root.join(dir));
}
}
out
}
const DECISION_FILE: &str = "decision";
fn farm_dir(store: &Path, fingerprint: &str, winners: &BTreeMap<String, PathBuf>) -> PathBuf {
let mix = |h: &mut u64, b: u8| {
*h ^= u64::from(b);
*h = h.wrapping_mul(0x0100_0000_01b3);
};
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in fingerprint.bytes() {
mix(&mut h, b);
}
let mut w: u64 = 0xcbf2_9ce4_8422_2325;
for (name, path) in winners {
for b in name.bytes().chain(path.to_string_lossy().bytes()) {
mix(&mut w, b);
}
mix(&mut w, 0x1f);
}
store.join(format!("farm-{h:016x}-{w:016x}"))
}
fn materialize(
store: &Path,
fingerprint: &str,
winners: &BTreeMap<String, PathBuf>,
) -> io::Result<Option<PathBuf>> {
if winners.is_empty() {
return Ok(None);
}
let farm = farm_dir(store, fingerprint, winners);
if farm.is_dir() {
return Ok(Some(farm));
}
let scratch = store.join(format!(
".{}.{}",
farm.file_name().unwrap_or_default().to_string_lossy(),
std::process::id()
));
let _ = fs::remove_dir_all(&scratch);
fs::create_dir_all(&scratch)?;
for (name, target) in winners {
std::os::unix::fs::symlink(target, scratch.join(name))?;
}
match fs::rename(&scratch, &farm) {
Ok(()) => {}
Err(_) if farm.is_dir() => {
let _ = fs::remove_dir_all(&scratch);
}
Err(e) => {
let _ = fs::remove_dir_all(&scratch);
return Err(e);
}
}
Ok(Some(farm))
}
fn record(
store: &Path,
fingerprint: &str,
resolution: &Resolution,
winners: &BTreeMap<String, PathBuf>,
) -> io::Result<()> {
fs::create_dir_all(store)?;
let mut text = String::new();
text.push_str("onelf-resolve 2\n");
text.push_str(&format!("fingerprint {fingerprint}\n"));
if let Some(interp) = &resolution.host_interp {
text.push_str(&format!("interp {}\n", interp.display()));
}
for (name, path) in winners {
text.push_str(&format!("host {name}\t{}\n", path.display()));
}
for (name, path) in winners {
text.push_str(&format!("stat {name}\t{}\n", stat_line(path)));
}
for name in &resolution.incomparable {
text.push_str(&format!("incomparable {name}\n"));
}
let tmp = store.join(format!(".{DECISION_FILE}.{}", std::process::id()));
fs::File::create(&tmp)?.write_all(text.as_bytes())?;
fs::rename(&tmp, store.join(DECISION_FILE))
}
fn stat_line(path: &Path) -> String {
match fs::metadata(path) {
Ok(md) => {
use std::os::unix::fs::MetadataExt;
format!(
"{}:{}:{}:{}:{}:{}",
md.len(),
md.ino(),
md.mtime(),
md.mtime_nsec(),
md.ctime(),
md.ctime_nsec()
)
}
Err(_) => "missing".to_string(),
}
}
fn load_recorded(store: &Path, fingerprint: &str) -> Option<Resolution> {
let text = fs::read_to_string(store.join(DECISION_FILE)).ok()?;
let mut lines = text.lines();
if lines.next()? != "onelf-resolve 2" {
return None;
}
if lines.next()?.strip_prefix("fingerprint ")? != fingerprint {
return None;
}
let mut resolution = Resolution::default();
let mut winners: BTreeMap<String, PathBuf> = BTreeMap::new();
let mut any_host = false;
for line in lines {
if let Some(interp) = line.strip_prefix("interp ") {
let interp = PathBuf::from(interp);
if !interp.is_file() {
return None;
}
resolution.host_interp = Some(interp);
} else if let Some(rest) = line.strip_prefix("host ") {
let (name, path) = rest.split_once('\t')?;
winners.insert(name.to_string(), PathBuf::from(path));
any_host = true;
} else if let Some(rest) = line.strip_prefix("stat ") {
let (name, recorded) = rest.split_once('\t')?;
let path = winners.get(name)?;
if stat_line(path) != recorded {
return None;
}
} else {
let name = line.strip_prefix("incomparable ")?;
resolution.incomparable.push(name.to_string());
}
}
if !any_host {
return Some(resolution);
}
let farm = farm_dir(store, fingerprint, &winners);
for (name, path) in &winners {
match fs::read_link(farm.join(name)) {
Ok(target) if target == path.as_path() && path.is_file() => {}
_ => return None,
}
}
resolution.farm = Some(farm);
Some(resolution)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verdef::fixture::{shared_object, shared_object_with_interp, temp_dir};
struct World {
host_lib: PathBuf,
cache: PathBuf,
pkg: PathBuf,
store: PathBuf,
icd_dir: PathBuf,
last_farm: std::cell::RefCell<Option<PathBuf>>,
}
impl World {
fn new() -> Self {
let root = temp_dir("resolve");
let host_lib = root.join("host");
let pkg = root.join("pkg");
fs::create_dir_all(&host_lib).unwrap();
fs::create_dir_all(pkg.join("lib")).unwrap();
let icd_dir = root.join("icd.d");
fs::create_dir_all(&icd_dir).unwrap();
World {
cache: root.join("ld.so.cache"),
store: root.join("store"),
host_lib,
pkg,
icd_dir,
last_farm: std::cell::RefCell::new(None),
}
}
fn host(&self, name: &str, bytes: &[u8]) -> PathBuf {
let path = self.host_lib.join(name);
fs::write(&path, bytes).unwrap();
self.write_cache();
path
}
fn host_module(&self, name: &str, bytes: &[u8]) -> PathBuf {
let dir = self.host_lib.join("dri");
fs::create_dir_all(&dir).unwrap();
let path = dir.join(name);
fs::write(&path, bytes).unwrap();
path
}
fn bundle(&self, name: &str, bytes: &[u8]) -> PathBuf {
let path = self.pkg.join("lib").join(name);
fs::write(&path, bytes).unwrap();
path
}
fn write_cache(&self) {
let mut names: Vec<String> = fs::read_dir(&self.host_lib)
.unwrap()
.flatten()
.filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
let paths: Vec<String> = names
.iter()
.map(|n| self.host_lib.join(n).to_string_lossy().into_owned())
.collect();
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
fs::write(&self.cache, new_cache(&refs)).unwrap();
}
fn resolve(&self, policy: HostLibsPolicy) -> Resolution {
self.resolve_with(policy, None)
}
fn resolve_with(&self, policy: HostLibsPolicy, extra_root: Option<&Path>) -> Resolution {
self.resolve_full(policy, extra_root, true)
}
fn resolve_full(
&self,
policy: HostLibsPolicy,
extra_root: Option<&Path>,
host_libc: bool,
) -> Resolution {
let icd_dir = self.icd_dir.to_string_lossy().into_owned();
let set_roots = self.set_roots();
let r = resolve(&Request {
pkg_root: &self.pkg,
lib_dirs: &["lib"],
policy,
store: &self.store,
ld_cache: &self.cache,
icd_dirs: &[icd_dir.as_str()],
extra_root,
set_roots: &set_roots,
host_libc,
});
*self.last_farm.borrow_mut() = r.farm.clone();
r
}
fn set_roots(&self) -> Vec<PathBuf> {
Vec::new()
}
fn gl(&self) -> Gl {
gl_situation(&self.pkg, &["lib"], &[], &self.cache, &[])
}
fn farm_link(&self, name: &str) -> Option<PathBuf> {
let farm = self.last_farm.borrow().clone()?;
fs::read_link(farm.join(name)).ok()
}
}
fn new_cache(paths: &[&str]) -> Vec<u8> {
let mut out = Vec::from(&b"glibc-ld.so.cache1.1"[..]);
out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.push(0);
out.extend_from_slice(&[0; 3]);
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&[0; 12]);
let strings_at = 48 + paths.len() * 24;
let mut strings: Vec<u8> = Vec::new();
for p in paths {
let off = (strings_at + strings.len()) as u32;
out.extend_from_slice(&drivers::native_entry_flags().to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&off.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0u64.to_le_bytes());
strings.extend_from_slice(p.as_bytes());
strings.push(0);
}
out.extend_from_slice(&strings);
out
}
fn versioned(names: &[&str]) -> Vec<u8> {
let defs: Vec<(u16, &str)> = names.iter().map(|n| (0, *n)).collect();
shared_object(&defs, &[])
}
fn driver_needing(needed: &[&str]) -> Vec<u8> {
shared_object(&[], needed)
}
#[test]
fn a_newer_host_dependency_of_a_driver_wins() {
let w = World::new();
let host_gl = w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
let r = w.resolve(HostLibsPolicy::Auto);
assert!(r.farm.as_ref().is_some_and(|f| f.starts_with(&w.store)));
assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo));
assert_eq!(w.farm_link("libGL.so.1"), Some(host_gl));
assert!(r.host_interp.is_none());
assert!(r.incomparable.is_empty());
}
#[test]
fn deciding_names_every_verdict_and_writes_nothing() {
let w = World::new();
let host_gl = w.host(
"libGL.so.1",
&driver_needing(&["libfoo.so.1", "libbar.so.1"]),
);
let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.host("libbar.so.1", &versioned(&["BAR_1.0"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
w.bundle("libbar.so.1", &versioned(&["BAR_1.0", "BAR_1.1"]));
let icd_dir = w.icd_dir.to_string_lossy().into_owned();
let req = Request {
pkg_root: &w.pkg,
lib_dirs: &["lib"],
policy: HostLibsPolicy::Auto,
store: &w.store,
ld_cache: &w.cache,
icd_dirs: &[icd_dir.as_str()],
extra_root: None,
set_roots: &[],
host_libc: true,
};
let d = decide(&req);
assert_eq!(
d.compared,
vec![
("libGL.so.1".to_string(), None),
("libbar.so.1".to_string(), Some(Choice::Bundle)),
("libfoo.so.1".to_string(), Some(Choice::Host)),
]
);
assert_eq!(d.winners.get("libfoo.so.1"), Some(&host_foo));
assert_eq!(d.winners.get("libGL.so.1"), Some(&host_gl));
assert!(!w.store.exists(), "deciding wrote the store");
let r = resolve(&req);
assert_eq!(
r.farm, d.farm,
"the farm a launch builds is the one reported"
);
}
#[test]
fn a_driver_dependency_the_bundle_lacks_comes_from_the_host() {
let w = World::new();
w.host("libvulkan.so.1", &driver_needing(&["libdrm.so.2"]));
let host_drm = w.host("libdrm.so.2", &versioned(&[]));
w.host("libunrelated.so.1", &versioned(&[]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libdrm.so.2"), Some(host_drm));
assert!(w.farm_link("libunrelated.so.1").is_none());
assert!(w.farm_link("libapp.so.1").is_none());
}
#[test]
fn auto_ignores_libraries_outside_the_driver_closure() {
let w = World::new();
let host_bar = w.host("libbar.so.1", &versioned(&["BAR_1.0", "BAR_2.0"]));
w.bundle("libbar.so.1", &versioned(&["BAR_1.0"]));
assert_eq!(w.resolve(HostLibsPolicy::Auto), Resolution::default());
assert!(w.farm_link("libbar.so.1").is_none());
let r = w.resolve(HostLibsPolicy::Always);
assert!(r.farm.is_some());
assert_eq!(w.farm_link("libbar.so.1"), Some(host_bar));
}
#[test]
fn a_newer_bundled_copy_stays() {
let w = World::new();
w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
w.host("libfoo.so.1", &versioned(&["FOO_1.0"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
assert!(w.farm_link("libGL.so.1").is_some());
}
#[test]
fn unversioned_and_unreadable_host_copies_stay_bundled() {
let w = World::new();
w.host(
"libGL.so.1",
&driver_needing(&["libfoo.so.1", "libbaz.so.1"]),
);
w.host("libfoo.so.1", &versioned(&[]));
w.bundle("libfoo.so.1", &versioned(&[]));
let broken = versioned(&["BAZ_9.0"]);
w.host("libbaz.so.1", &broken[..broken.len() / 2]);
w.bundle("libbaz.so.1", &versioned(&["BAZ_1.0"]));
let r = w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
assert!(w.farm_link("libbaz.so.1").is_none());
assert!(r.incomparable.is_empty());
}
#[test]
fn incomparable_copies_stay_bundled_and_are_named() {
let w = World::new();
w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
w.host("libfoo.so.1", &versioned(&["FOO_1.0", "VENDOR_1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
let r = w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
assert_eq!(r.incomparable, ["libfoo.so.1"]);
}
#[test]
fn never_takes_nothing() {
let w = World::new();
w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
assert_eq!(w.resolve(HostLibsPolicy::Never), Resolution::default());
assert!(!w.store.exists());
}
#[test]
fn closure_reaches_transitive_dependencies_and_stops_at_missing_ones() {
let w = World::new();
w.host(
"libvulkan.so.1",
&driver_needing(&["libdrm.so.2", "libgone.so.0"]),
);
w.host("libdrm.so.2", &driver_needing(&["libz.so.1"]));
w.host("libz.so.1", &versioned(&[]));
w.host("libunrelated.so.1", &versioned(&[]));
let closure = HostIndex::load(&w.cache, &[], None).driver_closure();
for name in ["libvulkan.so.1", "libdrm.so.2", "libz.so.1", "libgone.so.0"] {
assert!(closure.contains(name), "{name} missing from {closure:?}");
}
assert!(!closure.contains("libunrelated.so.1"));
}
#[test]
fn a_newer_host_glibc_brings_its_loader_and_family() {
let w = World::new();
let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
let host_libc = w.host(
"libc.so.6",
&shared_object_with_interp(
&[(0, "GLIBC_2.2.5"), (0, "GLIBC_2.34")],
&[],
Some(host_ld.to_str().unwrap()),
),
);
let host_libm = w.host("libm.so.6", &versioned(&["GLIBC_2.2.5", "GLIBC_2.34"]));
w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
w.bundle("libm.so.6", &versioned(&["GLIBC_2.2.5"]));
w.bundle("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
w.bundle("libother.so.1", &versioned(&["OTHER_1"]));
let r = w.resolve(HostLibsPolicy::Auto);
assert_eq!(r.host_interp, Some(host_ld.clone()));
assert_eq!(w.farm_link("libc.so.6"), Some(host_libc));
assert_eq!(w.farm_link("libm.so.6"), Some(host_libm));
assert_eq!(w.farm_link("ld-linux-x86-64.so.2"), Some(host_ld));
assert!(w.farm_link("libother.so.1").is_none());
}
#[test]
fn a_launch_that_cannot_shadow_keeps_the_bundled_glibc() {
let w = World::new();
let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
w.host(
"libc.so.6",
&shared_object_with_interp(
&[(0, "GLIBC_2.2.5"), (0, "GLIBC_2.34")],
&[],
Some(host_ld.to_str().unwrap()),
),
);
w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
w.bundle("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
let r = w.resolve_full(HostLibsPolicy::Auto, None, false);
assert!(r.host_interp.is_none());
assert!(w.farm_link("libc.so.6").is_none());
let r = w.resolve_full(HostLibsPolicy::Auto, None, true);
assert_eq!(r.host_interp, Some(host_ld));
}
#[test]
fn an_older_or_equal_host_glibc_keeps_the_bundled_pair() {
let w = World::new();
let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&[]));
w.host(
"libc.so.6",
&shared_object_with_interp(&[(0, "GLIBC_2.2.5")], &[], Some(host_ld.to_str().unwrap())),
);
w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5", "GLIBC_2.34"]));
assert_eq!(w.resolve(HostLibsPolicy::Always), Resolution::default());
w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
assert_eq!(w.resolve(HostLibsPolicy::Always), Resolution::default());
}
#[test]
fn a_host_without_glibc_takes_nothing_for_the_pair() {
let w = World::new();
w.host("libc.musl-x86_64.so.1", &versioned(&[]));
w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
let r = w.resolve(HostLibsPolicy::Always);
assert!(r.host_interp.is_none());
assert!(r.farm.is_none());
}
#[test]
fn the_decision_is_recorded_and_reused_until_the_host_changes() {
let w = World::new();
w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
let first = w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
assert_eq!(w.resolve(HostLibsPolicy::Auto), first);
assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
fs::write(&host_foo, versioned(&["FOO_1.0"])).unwrap();
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
fs::write(w.host_lib.join("libnew.so.1"), versioned(&[])).unwrap();
bump_mtime(&w.host_lib);
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
}
#[test]
fn an_in_place_same_size_swap_is_not_served_stale() {
let w = World::new();
w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
fs::write(&host_foo, versioned(&["FOO_0.9", "FOO_1.1"])).unwrap();
std::fs::File::options()
.write(true)
.open(&host_foo)
.unwrap()
.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000))
.unwrap();
w.resolve(HostLibsPolicy::Auto);
assert!(
w.farm_link("libfoo.so.1").is_none(),
"a swapped winner must be re-judged, not served from the record"
);
}
#[test]
fn a_vanished_winner_recomputes_instead_of_failing() {
let w = World::new();
w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
w.resolve(HostLibsPolicy::Auto);
let before = fs::metadata(&w.host_lib).unwrap().modified().unwrap();
fs::remove_file(&host_foo).unwrap();
fs::File::open(&w.host_lib)
.unwrap()
.set_modified(before)
.unwrap();
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libfoo.so.1").is_none());
assert!(w.farm_link("libGL.so.1").is_some());
}
#[test]
fn library_path_is_read_from_icd_json() {
assert_eq!(
json_library_path(
"{\n \"file_format_version\": \"1.0.0\",\n \"ICD\": {\n \"library_path\": \"/usr/lib64/libvulkan_radeon.so\",\n \"api_version\": \"1.3\"\n }\n}"
),
Some("/usr/lib64/libvulkan_radeon.so")
);
assert_eq!(
json_library_path("{\"ICD\":{\"library_path\":\"libEGL_nvidia.so.0\"}}"),
Some("libEGL_nvidia.so.0")
);
assert_eq!(json_library_path("{}"), None);
}
fn bump_mtime(dir: &Path) {
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(5);
fs::File::open(dir).unwrap().set_modified(later).unwrap();
}
#[test]
fn a_partial_host_stack_does_not_count() {
let w = World::new();
w.host("libGL.so.1", &shared_object(&[(1, "GL_1")], &[]));
w.bundle(
"libQtGui.so.6",
&shared_object(&[(1, "Qt_6")], &["libEGL.so.1"]),
);
assert_eq!(
w.gl(),
Gl::Absent,
"the host has no libEGL, which the bundle asks for"
);
w.host("libEGL.so.1", &shared_object(&[(1, "EGL_1")], &[]));
assert_eq!(w.gl(), Gl::Host, "both halves present");
}
#[test]
fn the_gl_situation_is_bundled_host_or_absent() {
let w = World::new();
w.host("libz.so.1", &shared_object(&[(1, "Z_1")], &[]));
assert_eq!(w.gl(), Gl::Absent, "no GL entry point anywhere");
w.host("libEGL.so.1", &shared_object(&[(1, "EGL_1")], &[]));
assert_eq!(w.gl(), Gl::Host);
w.bundle("libGL.so.1", &shared_object(&[(1, "GL_1")], &[]));
assert_eq!(w.gl(), Gl::Bundled, "the bundle's own stack comes first");
}
#[test]
fn an_extra_root_supplies_what_the_host_lacks() {
let w = World::new();
w.host("libz.so.1", &shared_object(&[(1, "Z_1")], &[]));
w.bundle("libc.so.6", &shared_object(&[(2, "GLIBC_2.30")], &[]));
let root = temp_dir("glbuild");
fs::create_dir_all(root.join("lib")).unwrap();
fs::create_dir_all(root.join("share/vulkan/icd.d")).unwrap();
let gl = root.join("lib/libGL.so.1");
fs::write(&gl, shared_object(&[(1, "GL_1")], &["libdrm.so.2"])).unwrap();
let drm = root.join("lib/libdrm.so.2");
fs::write(&drm, shared_object(&[(1, "DRM_1")], &[])).unwrap();
let icd = root.join("lib/libvulkan_fake.so");
fs::write(&icd, shared_object(&[(1, "ICD_1")], &[])).unwrap();
let vendor = root.join("lib/libEGL_fake.so.0");
fs::write(&vendor, shared_object(&[(1, "VENDOR_1")], &["libdrm.so.2"])).unwrap();
fs::write(
root.join("share/vulkan/icd.d/fake.json"),
format!("{{\"ICD\": {{\"library_path\": \"{}\"}}}}", icd.display()),
)
.unwrap();
let without = w.resolve(HostLibsPolicy::Auto);
assert_eq!(without.farm, None, "nothing to take from a host without GL");
let with = w.resolve_with(HostLibsPolicy::Auto, Some(&root));
assert_eq!(w.farm_link("libGL.so.1"), Some(gl));
assert_eq!(
w.farm_link("libdrm.so.2"),
Some(drm),
"the build's closure follows"
);
assert_eq!(
w.farm_link("libvulkan_fake.so"),
Some(icd),
"ICD files seed the walk"
);
assert_eq!(
w.farm_link("libEGL_fake.so.0"),
Some(vendor),
"a vendor library nothing names is farmed by its name"
);
assert!(with.farm.is_some());
assert_eq!(
with.host_interp, None,
"the extra root never supplies a loader"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_host_change_leaves_the_old_farm_for_running_instances() {
let w = World::new();
let host_gl = w.host("libGL.so.1", &versioned(&["GL_1.0", "GL_2.0"]));
w.bundle("libGL.so.1", &versioned(&["GL_1.0"]));
w.resolve(HostLibsPolicy::Auto);
let old_farm = w.last_farm.borrow().clone().expect("a farm");
assert_eq!(
fs::read_link(old_farm.join("libGL.so.1")).ok(),
Some(host_gl)
);
w.host("libnew.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
let new_farm = w.last_farm.borrow().clone().expect("a farm");
assert_ne!(old_farm, new_farm);
assert!(
old_farm.join("libGL.so.1").exists(),
"the old farm was torn down under a running instance"
);
assert!(new_farm.join("libGL.so.1").exists());
}
#[test]
fn an_icd_naming_its_driver_by_bare_soname_seeds_the_walk() {
let w = World::new();
w.host(
"libGLX_nvidia.so.0",
&driver_needing(&["libnvidia-glcore.so.1"]),
);
let host_glcore = w.host("libnvidia-glcore.so.1", &versioned(&[]));
fs::write(
w.icd_dir.join("nvidia_icd.json"),
r#"{ "ICD": { "library_path": "libGLX_nvidia.so.0" } }"#,
)
.unwrap();
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libGLX_nvidia.so.0").is_some());
assert_eq!(w.farm_link("libnvidia-glcore.so.1"), Some(host_glcore));
}
#[test]
fn a_glx_vendor_library_nothing_names_is_farmed_with_its_needs() {
let w = World::new();
let host_mesa = w.host("libGLX_mesa.so.0", &driver_needing(&["libxcb-glx.so.0"]));
let host_xcb_glx = w.host("libxcb-glx.so.0", &versioned(&[]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libGLX_mesa.so.0"), Some(host_mesa));
assert_eq!(w.farm_link("libxcb-glx.so.0"), Some(host_xcb_glx));
}
#[test]
fn the_glx_indirect_fallback_is_farmed() {
let w = World::new();
let host_indirect = w.host("libGLX_indirect.so.0", &versioned(&[]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libGLX_indirect.so.0"), Some(host_indirect));
}
#[test]
fn a_bundled_glx_vendor_stays_bundled() {
let w = World::new();
w.host("libGLX_mesa.so.0", &versioned(&[]));
w.bundle("libGLX_mesa.so.0", &versioned(&[]));
let icd_dir = w.icd_dir.to_string_lossy().into_owned();
let req = Request {
pkg_root: &w.pkg,
lib_dirs: &["lib"],
policy: HostLibsPolicy::Auto,
store: &w.store,
ld_cache: &w.cache,
icd_dirs: &[icd_dir.as_str()],
extra_root: None,
set_roots: &[],
host_libc: true,
};
let d = decide(&req);
assert_eq!(
d.compared,
vec![("libGLX_mesa.so.0".to_string(), Some(Choice::Bundle))]
);
assert!(d.winners.is_empty());
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libGLX_mesa.so.0").is_none());
}
#[test]
fn a_host_without_glx_vendors_is_unaffected() {
let w = World::new();
let host_gl = w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
let closure = HostIndex::load(&w.cache, &[], None).driver_closure();
assert!(!closure.iter().any(|n| n.starts_with("libGLX_")));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libGL.so.1"), Some(host_gl));
assert!(w.farm_link("libfoo.so.1").is_some());
}
#[test]
fn a_dri_module_brings_what_it_needs() {
let w = World::new();
let host_llvm = w.host("libLLVM-15.so.1", &versioned(&[]));
w.host_module("swrast_dri.so", &driver_needing(&["libLLVM-15.so.1"]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libLLVM-15.so.1"), Some(host_llvm));
assert!(w.farm_link("swrast_dri.so").is_none());
}
#[test]
fn an_unreadable_module_is_ignored() {
let w = World::new();
let host_llvm = w.host("libLLVM-15.so.1", &versioned(&[]));
w.host_module("broken_dri.so", b"not an ELF object");
w.host_module("swrast_dri.so", &driver_needing(&["libLLVM-15.so.1"]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libLLVM-15.so.1"), Some(host_llvm));
}
#[test]
fn a_new_module_resolves_afresh() {
let w = World::new();
w.host("libLLVM-15.so.1", &versioned(&[]));
let host_sensors = w.host("libsensors.so.5", &versioned(&[]));
w.host_module("swrast_dri.so", &driver_needing(&["libLLVM-15.so.1"]));
w.bundle("libapp.so.1", &versioned(&[]));
w.resolve(HostLibsPolicy::Auto);
assert!(w.farm_link("libsensors.so.5").is_none());
w.host_module("radeonsi_dri.so", &driver_needing(&["libsensors.so.5"]));
fs::File::open(w.host_lib.join("dri"))
.unwrap()
.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1))
.unwrap();
w.resolve(HostLibsPolicy::Auto);
assert_eq!(w.farm_link("libsensors.so.5"), Some(host_sensors));
}
}