1use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
22use std::fs;
23use std::io::{self, Write};
24use std::path::{Path, PathBuf};
25
26use crate::drivers::{self, DRIVER_FAMILIES};
27use crate::footer::HostLibsPolicy;
28use crate::verdef::{self, Choice, VersionSet};
29
30#[derive(Debug, Default, PartialEq, Eq)]
32pub struct Resolution {
33 pub farm: Option<PathBuf>,
36 pub host_interp: Option<PathBuf>,
39 pub incomparable: Vec<String>,
42}
43
44pub struct Request<'a> {
46 pub pkg_root: &'a Path,
48 pub lib_dirs: &'a [&'a str],
50 pub policy: HostLibsPolicy,
51 pub store: &'a Path,
55 pub ld_cache: &'a Path,
57 pub icd_dirs: &'a [&'a str],
60 pub extra_root: Option<&'a Path>,
64 pub set_roots: &'a [PathBuf],
68 pub host_libc: bool,
73}
74
75pub const GL_BUILD_LIB_DIRS: &[&str] = &["lib", "lib64"];
77
78pub const GL_BUILD_ICD_DIRS: &[&str] = &["share/vulkan/icd.d", "share/glvnd/egl_vendor.d"];
80
81const GL_BUILD_SEEDS: &[&str] = &[
87 "libGLX_",
88 "libEGL_",
89 "libgallium",
90 "libdril",
91 "libvulkan_",
92 "libVkLayer_",
93];
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Gl {
98 Bundled,
99 Host,
100 Absent,
101}
102
103const GL_ENTRY: &[&str] = &["libGL.so", "libEGL.so", "libGLESv2.so", "libvulkan.so"];
106
107fn has_gl_entry(libs: &BTreeMap<String, PathBuf>) -> bool {
108 libs.keys()
109 .any(|name| GL_ENTRY.iter().any(|p| name.starts_with(p)))
110}
111
112fn gl_entries_wanted(carried: &BTreeMap<String, PathBuf>) -> BTreeSet<String> {
116 let mut wanted = BTreeSet::new();
117 for path in carried.values() {
118 for needed in verdef::read_needed(path).unwrap_or_default() {
119 if GL_ENTRY.iter().any(|p| needed.starts_with(p)) && !carried.contains_key(&needed) {
120 wanted.insert(needed);
121 }
122 }
123 }
124 wanted
125}
126
127pub fn gl_situation(
140 pkg_root: &Path,
141 lib_dirs: &[&str],
142 extra_dirs: &[PathBuf],
143 ld_cache: &Path,
144 icd_dirs: &[&str],
145) -> Gl {
146 let mut carried = bundled_libs(pkg_root, lib_dirs);
147 for dir in extra_dirs {
148 for (name, path) in libs_in(dir) {
149 carried.entry(name).or_insert(path);
150 }
151 }
152 if has_gl_entry(&carried) {
153 return Gl::Bundled;
154 }
155 let host = HostIndex::load(ld_cache, icd_dirs, None).libs;
156 let wanted = gl_entries_wanted(&carried);
157 if wanted.is_empty() {
160 return if has_gl_entry(&host) {
161 Gl::Host
162 } else {
163 Gl::Absent
164 };
165 }
166 if wanted.iter().all(|name| host.contains_key(name)) {
167 Gl::Host
168 } else {
169 Gl::Absent
170 }
171}
172
173const LIBC: &str = "libc.so.6";
174
175const LIBC_FAMILY: &[&str] = &[
178 "libc.so",
179 "libm.so",
180 "libpthread.so",
181 "libdl.so",
182 "librt.so",
183 "libresolv.so",
184 "libutil.so",
185 "libanl.so",
186 "libnss_",
187 "libBrokenLocale.so",
188 "libmvec.so",
189 "libc_malloc_debug.so",
190 "libthread_db.so",
191 "ld-linux",
192];
193
194pub const ICD_DIRS: &[&str] = &[
198 "/run/opengl-driver/share/vulkan/icd.d",
199 "/etc/vulkan/icd.d",
200 "/usr/share/vulkan/icd.d",
201 "/run/opengl-driver/share/glvnd/egl_vendor.d",
202 "/etc/glvnd/egl_vendor.d",
203 "/usr/share/glvnd/egl_vendor.d",
204];
205
206pub fn is_libc_family(soname: &str) -> bool {
209 LIBC_FAMILY.iter().any(|p| soname.starts_with(p))
210}
211
212fn is_driver_family(soname: &str) -> bool {
213 DRIVER_FAMILIES.iter().any(|p| soname.starts_with(p))
214}
215
216pub fn resolve(req: &Request) -> Resolution {
218 if req.policy == HostLibsPolicy::Never {
219 return Resolution::default();
220 }
221 let host = HostIndex::load(req.ld_cache, req.icd_dirs, req.extra_root);
222 if host.libs.is_empty() {
223 return Resolution::default();
224 }
225 let fingerprint = fingerprint(
226 req.ld_cache,
227 req.icd_dirs,
228 req.policy,
229 req.extra_root,
230 req.host_libc,
231 );
232 if let Some(recorded) = load_recorded(req.store, &fingerprint) {
233 return recorded;
234 }
235
236 let decision = decide_on(req, &host);
237 let winners = &decision.winners;
238 let farm = match materialize(req.store, &fingerprint, winners) {
239 Ok(farm) => farm,
240 Err(e) => {
241 eprintln!(
242 "onelf-rt: resolver: cannot write {}: {e}",
243 req.store.display()
244 );
245 return Resolution::default();
246 }
247 };
248 let resolution = Resolution {
249 farm,
250 host_interp: decision.host_interp,
251 incomparable: decision.incomparable,
252 };
253 if let Err(e) = record(req.store, &fingerprint, &resolution, winners) {
254 eprintln!(
255 "onelf-rt: resolver: cannot record {}: {e}",
256 req.store.display()
257 );
258 }
259 resolution
260}
261
262#[derive(Debug, Default)]
265pub struct Decision {
266 pub compared: Vec<(String, Option<Choice>)>,
269 pub winners: BTreeMap<String, PathBuf>,
271 pub host_interp: Option<PathBuf>,
272 pub incomparable: Vec<String>,
273 pub farm: Option<PathBuf>,
275}
276
277pub fn decide(req: &Request) -> Decision {
280 if req.policy == HostLibsPolicy::Never {
281 return Decision::default();
282 }
283 let host = HostIndex::load(req.ld_cache, req.icd_dirs, req.extra_root);
284 if host.libs.is_empty() {
285 return Decision::default();
286 }
287 let mut decision = decide_on(req, &host);
288 if !decision.winners.is_empty() {
289 let fingerprint = fingerprint(
290 req.ld_cache,
291 req.icd_dirs,
292 req.policy,
293 req.extra_root,
294 req.host_libc,
295 );
296 decision.farm = Some(farm_dir(req.store, &fingerprint, &decision.winners));
297 }
298 decision
299}
300
301fn decide_on(req: &Request, host: &HostIndex) -> Decision {
302 let mut bundled = bundled_libs(req.pkg_root, req.lib_dirs);
303 for root in req.set_roots {
306 for (name, path) in bundled_libs(root, GL_BUILD_LIB_DIRS) {
307 bundled.entry(name).or_insert(path);
308 }
309 }
310 let mut decision = Decision::default();
311
312 if let (true, Some(bundled_libc), Some(host_libc)) =
313 (req.host_libc, bundled.get(LIBC), host.libs.get(LIBC))
314 {
315 let choice = verdef::compare(&versions(bundled_libc), &versions(host_libc));
316 decision.compared.push((LIBC.to_string(), Some(choice)));
317 match choice {
318 Choice::Host => {
319 if let Some(interp) = host.loader_for(host_libc) {
320 for name in bundled.keys().filter(|n| is_libc_family(n)) {
321 let path = if name.starts_with("ld-linux") {
322 Some(interp.clone())
323 } else {
324 host.libs.get(name).cloned()
325 };
326 if let Some(path) = path {
327 decision.winners.insert(name.clone(), path);
328 }
329 }
330 decision.host_interp = Some(interp);
331 }
332 }
333 Choice::Incomparable => decision.incomparable.push(LIBC.to_string()),
334 Choice::Bundle => {}
335 }
336 }
337
338 let mut candidates: Vec<String> = host.driver_closure().into_iter().collect();
343 if req.policy == HostLibsPolicy::Always {
344 candidates.extend(bundled.keys().cloned());
345 }
346 candidates.sort();
347 candidates.dedup();
348 for name in candidates {
349 if is_libc_family(&name) {
350 continue;
351 }
352 let Some(host_path) = host.libs.get(&name) else {
353 continue;
354 };
355 let choice = bundled
356 .get(&name)
357 .map(|bundled_path| verdef::compare(&versions(bundled_path), &versions(host_path)));
358 decision.compared.push((name.clone(), choice));
359 match choice.unwrap_or(Choice::Host) {
360 Choice::Host => {
361 decision.winners.insert(name, host_path.clone());
362 }
363 Choice::Incomparable => decision.incomparable.push(name),
364 Choice::Bundle => {}
365 }
366 }
367 decision
368}
369
370fn versions(path: &Path) -> VersionSet {
374 verdef::read(path).unwrap_or_default()
375}
376
377struct HostIndex {
380 libs: BTreeMap<String, PathBuf>,
381 icd_libs: Vec<PathBuf>,
385}
386
387impl HostIndex {
388 fn load(ld_cache: &Path, icd_dirs: &[&str], extra_root: Option<&Path>) -> Self {
391 let mut libs = BTreeMap::new();
392 let mut icd_libs = Vec::new();
393 if let Some(root) = extra_root {
394 libs.extend(bundled_libs(root, GL_BUILD_LIB_DIRS));
395 icd_libs.extend(
396 libs.iter()
397 .filter(|(name, _)| GL_BUILD_SEEDS.iter().any(|p| name.starts_with(p)))
398 .map(|(_, path)| path.clone()),
399 );
400 let dirs: Vec<String> = GL_BUILD_ICD_DIRS
401 .iter()
402 .map(|d| root.join(d).to_string_lossy().into_owned())
403 .collect();
404 let dirs: Vec<&str> = dirs.iter().map(String::as_str).collect();
405 icd_libs.extend(icd_library_paths_in(&dirs, Some(&libs)));
409 }
410 for (soname, path) in drivers::cache_paths_in(ld_cache) {
411 libs.entry(soname).or_insert_with(|| PathBuf::from(path));
412 }
413 icd_libs.extend(icd_library_paths_in(icd_dirs, Some(&libs)));
417 for path in &icd_libs {
418 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
419 libs.entry(name.to_string()).or_insert_with(|| path.clone());
420 }
421 }
422 HostIndex { libs, icd_libs }
423 }
424
425 fn loader_for(&self, host_libc: &Path) -> Option<PathBuf> {
428 if let Some(interp) = crate::elf::read_interp(host_libc) {
429 let interp = PathBuf::from(interp);
430 if interp.is_file() {
431 return Some(interp);
432 }
433 }
434 self.libs
435 .iter()
436 .find(|(name, path)| name.starts_with("ld-linux") && path.is_file())
437 .map(|(_, path)| path.clone())
438 }
439
440 fn driver_closure(&self) -> HashSet<String> {
443 let mut seen: HashSet<String> = HashSet::new();
444 let mut queue: VecDeque<String> = self
445 .libs
446 .keys()
447 .filter(|name| is_driver_family(name))
448 .cloned()
449 .collect();
450 for path in &self.icd_libs {
451 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
452 queue.push_back(name.to_string());
453 }
454 }
455 while let Some(name) = queue.pop_front() {
456 if !seen.insert(name.clone()) {
457 continue;
458 }
459 let Some(path) = self.libs.get(&name) else {
460 continue;
461 };
462 for needed in verdef::read_needed(path).unwrap_or_default() {
463 if !seen.contains(&needed) {
464 queue.push_back(needed);
465 }
466 }
467 }
468 seen
469 }
470}
471
472fn icd_library_paths_in(
477 icd_dirs: &[&str],
478 bare_names: Option<&BTreeMap<String, PathBuf>>,
479) -> Vec<PathBuf> {
480 let mut out = Vec::new();
481 for dir in icd_dirs {
482 let Ok(entries) = fs::read_dir(dir) else {
483 continue;
484 };
485 for entry in entries.flatten() {
486 let path = entry.path();
487 if path.extension().is_none_or(|e| e != "json") {
488 continue;
489 }
490 let Ok(text) = fs::read_to_string(&path) else {
491 continue;
492 };
493 let Some(value) = json_library_path(&text) else {
494 continue;
495 };
496 if !value.contains('/') {
497 if let Some(lib) = bare_names.and_then(|names| names.get(value)) {
498 out.push(lib.clone());
499 }
500 continue;
501 }
502 let lib = if value.starts_with('/') {
503 PathBuf::from(value)
504 } else {
505 path.parent().unwrap_or(Path::new("/")).join(value)
506 };
507 if lib.is_file() {
508 out.push(lib);
509 }
510 }
511 }
512 out
513}
514
515pub fn json_library_path(text: &str) -> Option<&str> {
519 let at = text.find("\"library_path\"")?;
520 let rest = &text[at + "\"library_path\"".len()..];
521 let rest = rest.trim_start().strip_prefix(':')?.trim_start();
522 let rest = rest.strip_prefix('"')?;
523 let end = rest.find('"')?;
524 Some(&rest[..end])
525}
526
527fn bundled_libs(pkg_root: &Path, lib_dirs: &[&str]) -> BTreeMap<String, PathBuf> {
530 let mut out = BTreeMap::new();
531 for dir in lib_dirs {
532 for (name, path) in libs_in(&pkg_root.join(dir)) {
533 out.entry(name).or_insert(path);
534 }
535 }
536 out
537}
538
539fn libs_in(dir: &Path) -> Vec<(String, PathBuf)> {
546 let Ok(entries) = fs::read_dir(dir) else {
547 return Vec::new();
548 };
549 entries
550 .flatten()
551 .filter_map(|entry| {
552 let name = entry.file_name().to_str().map(String::from)?;
553 let path = entry.path();
554 let placed = fs::read_link(&path).is_ok_and(|t| t.is_absolute());
555 (name.contains(".so") && !placed && path.is_file()).then_some((name, path))
556 })
557 .collect()
558}
559
560fn fingerprint(
566 ld_cache: &Path,
567 icd_dirs: &[&str],
568 policy: HostLibsPolicy,
569 extra_root: Option<&Path>,
570 host_libc: bool,
571) -> String {
572 let mut out = format!("onelf-resolve-1 {}", policy.as_str());
573 if !host_libc {
574 out.push_str(" bundled-libc");
575 }
576 let mut stamp = |path: &Path| {
577 out.push('\x1f');
578 out.push_str(&path.to_string_lossy());
579 match fs::metadata(path) {
580 Ok(md) => {
581 let mtime = md
582 .modified()
583 .ok()
584 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
585 .map(|d| d.as_nanos())
586 .unwrap_or(0);
587 out.push_str(&format!("={}:{mtime}", md.len()));
588 }
589 Err(_) => out.push_str("=missing"),
590 }
591 };
592 stamp(ld_cache);
593 for dir in drivers::cache_dirs_in(ld_cache) {
594 stamp(Path::new(&dir));
595 }
596 for dir in icd_dirs {
597 stamp(Path::new(dir));
598 }
599 if let Some(root) = extra_root {
600 for dir in GL_BUILD_LIB_DIRS.iter().chain(GL_BUILD_ICD_DIRS) {
601 stamp(&root.join(dir));
602 }
603 }
604 out
605}
606
607const DECISION_FILE: &str = "decision";
608
609fn farm_dir(store: &Path, fingerprint: &str, winners: &BTreeMap<String, PathBuf>) -> PathBuf {
619 let mix = |h: &mut u64, b: u8| {
622 *h ^= u64::from(b);
623 *h = h.wrapping_mul(0x0100_0000_01b3);
624 };
625 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
626 for b in fingerprint.bytes() {
627 mix(&mut h, b);
628 }
629 let mut w: u64 = 0xcbf2_9ce4_8422_2325;
630 for (name, path) in winners {
631 for b in name.bytes().chain(path.to_string_lossy().bytes()) {
632 mix(&mut w, b);
633 }
634 mix(&mut w, 0x1f);
635 }
636 store.join(format!("farm-{h:016x}-{w:016x}"))
637}
638
639fn materialize(
646 store: &Path,
647 fingerprint: &str,
648 winners: &BTreeMap<String, PathBuf>,
649) -> io::Result<Option<PathBuf>> {
650 if winners.is_empty() {
651 return Ok(None);
652 }
653 let farm = farm_dir(store, fingerprint, winners);
654 if farm.is_dir() {
655 return Ok(Some(farm));
656 }
657 let scratch = store.join(format!(
658 ".{}.{}",
659 farm.file_name().unwrap_or_default().to_string_lossy(),
660 std::process::id()
661 ));
662 let _ = fs::remove_dir_all(&scratch);
663 fs::create_dir_all(&scratch)?;
664 for (name, target) in winners {
665 std::os::unix::fs::symlink(target, scratch.join(name))?;
666 }
667 match fs::rename(&scratch, &farm) {
668 Ok(()) => {}
669 Err(_) if farm.is_dir() => {
670 let _ = fs::remove_dir_all(&scratch);
671 }
672 Err(e) => {
673 let _ = fs::remove_dir_all(&scratch);
674 return Err(e);
675 }
676 }
677 Ok(Some(farm))
678}
679
680fn record(
681 store: &Path,
682 fingerprint: &str,
683 resolution: &Resolution,
684 winners: &BTreeMap<String, PathBuf>,
685) -> io::Result<()> {
686 fs::create_dir_all(store)?;
687 let mut text = String::new();
688 text.push_str("onelf-resolve 2\n");
689 text.push_str(&format!("fingerprint {fingerprint}\n"));
690 if let Some(interp) = &resolution.host_interp {
691 text.push_str(&format!("interp {}\n", interp.display()));
692 }
693 for (name, path) in winners {
694 text.push_str(&format!("host {name}\t{}\n", path.display()));
695 }
696 for (name, path) in winners {
700 text.push_str(&format!("stat {name}\t{}\n", stat_line(path)));
701 }
702 for name in &resolution.incomparable {
703 text.push_str(&format!("incomparable {name}\n"));
704 }
705 let tmp = store.join(format!(".{DECISION_FILE}.{}", std::process::id()));
706 fs::File::create(&tmp)?.write_all(text.as_bytes())?;
707 fs::rename(&tmp, store.join(DECISION_FILE))
708}
709
710fn stat_line(path: &Path) -> String {
713 match fs::metadata(path) {
714 Ok(md) => {
715 use std::os::unix::fs::MetadataExt;
716 format!(
717 "{}:{}:{}:{}:{}:{}",
718 md.len(),
719 md.ino(),
720 md.mtime(),
721 md.mtime_nsec(),
722 md.ctime(),
723 md.ctime_nsec()
724 )
725 }
726 Err(_) => "missing".to_string(),
727 }
728}
729
730fn load_recorded(store: &Path, fingerprint: &str) -> Option<Resolution> {
733 let text = fs::read_to_string(store.join(DECISION_FILE)).ok()?;
734 let mut lines = text.lines();
735 if lines.next()? != "onelf-resolve 2" {
736 return None;
737 }
738 if lines.next()?.strip_prefix("fingerprint ")? != fingerprint {
739 return None;
740 }
741 let mut resolution = Resolution::default();
742 let mut winners: BTreeMap<String, PathBuf> = BTreeMap::new();
743 let mut any_host = false;
744 for line in lines {
745 if let Some(interp) = line.strip_prefix("interp ") {
746 let interp = PathBuf::from(interp);
747 if !interp.is_file() {
748 return None;
749 }
750 resolution.host_interp = Some(interp);
751 } else if let Some(rest) = line.strip_prefix("host ") {
752 let (name, path) = rest.split_once('\t')?;
753 winners.insert(name.to_string(), PathBuf::from(path));
754 any_host = true;
755 } else if let Some(rest) = line.strip_prefix("stat ") {
756 let (name, recorded) = rest.split_once('\t')?;
757 let path = winners.get(name)?;
758 if stat_line(path) != recorded {
759 return None;
760 }
761 } else {
762 let name = line.strip_prefix("incomparable ")?;
763 resolution.incomparable.push(name.to_string());
764 }
765 }
766 if !any_host {
767 return Some(resolution);
768 }
769 let farm = farm_dir(store, fingerprint, &winners);
773 for (name, path) in &winners {
774 match fs::read_link(farm.join(name)) {
775 Ok(target) if target == path.as_path() && path.is_file() => {}
776 _ => return None,
777 }
778 }
779 resolution.farm = Some(farm);
780 Some(resolution)
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786 use crate::verdef::fixture::{shared_object, shared_object_with_interp, temp_dir};
787
788 struct World {
791 host_lib: PathBuf,
792 cache: PathBuf,
793 pkg: PathBuf,
794 store: PathBuf,
795 icd_dir: PathBuf,
797 last_farm: std::cell::RefCell<Option<PathBuf>>,
799 }
800
801 impl World {
802 fn new() -> Self {
803 let root = temp_dir("resolve");
804 let host_lib = root.join("host");
805 let pkg = root.join("pkg");
806 fs::create_dir_all(&host_lib).unwrap();
807 fs::create_dir_all(pkg.join("lib")).unwrap();
808 let icd_dir = root.join("icd.d");
809 fs::create_dir_all(&icd_dir).unwrap();
810 World {
811 cache: root.join("ld.so.cache"),
812 store: root.join("store"),
813 host_lib,
814 pkg,
815 icd_dir,
816 last_farm: std::cell::RefCell::new(None),
817 }
818 }
819
820 fn host(&self, name: &str, bytes: &[u8]) -> PathBuf {
821 let path = self.host_lib.join(name);
822 fs::write(&path, bytes).unwrap();
823 self.write_cache();
824 path
825 }
826
827 fn bundle(&self, name: &str, bytes: &[u8]) -> PathBuf {
828 let path = self.pkg.join("lib").join(name);
829 fs::write(&path, bytes).unwrap();
830 path
831 }
832
833 fn write_cache(&self) {
835 let mut names: Vec<String> = fs::read_dir(&self.host_lib)
836 .unwrap()
837 .flatten()
838 .map(|e| e.file_name().to_string_lossy().into_owned())
839 .collect();
840 names.sort();
841 let paths: Vec<String> = names
842 .iter()
843 .map(|n| self.host_lib.join(n).to_string_lossy().into_owned())
844 .collect();
845 let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
846 fs::write(&self.cache, new_cache(&refs)).unwrap();
847 }
848
849 fn resolve(&self, policy: HostLibsPolicy) -> Resolution {
850 self.resolve_with(policy, None)
851 }
852
853 fn resolve_with(&self, policy: HostLibsPolicy, extra_root: Option<&Path>) -> Resolution {
854 self.resolve_full(policy, extra_root, true)
855 }
856
857 fn resolve_full(
858 &self,
859 policy: HostLibsPolicy,
860 extra_root: Option<&Path>,
861 host_libc: bool,
862 ) -> Resolution {
863 let icd_dir = self.icd_dir.to_string_lossy().into_owned();
864 let set_roots = self.set_roots();
865 let r = resolve(&Request {
866 pkg_root: &self.pkg,
867 lib_dirs: &["lib"],
868 policy,
869 store: &self.store,
870 ld_cache: &self.cache,
871 icd_dirs: &[icd_dir.as_str()],
872 extra_root,
873 set_roots: &set_roots,
874 host_libc,
875 });
876 *self.last_farm.borrow_mut() = r.farm.clone();
877 r
878 }
879
880 fn set_roots(&self) -> Vec<PathBuf> {
881 Vec::new()
882 }
883
884 fn gl(&self) -> Gl {
885 gl_situation(&self.pkg, &["lib"], &[], &self.cache, &[])
886 }
887
888 fn farm_link(&self, name: &str) -> Option<PathBuf> {
889 let farm = self.last_farm.borrow().clone()?;
890 fs::read_link(farm.join(name)).ok()
891 }
892 }
893
894 fn new_cache(paths: &[&str]) -> Vec<u8> {
896 let mut out = Vec::from(&b"glibc-ld.so.cache1.1"[..]);
897 out.extend_from_slice(&(paths.len() as u32).to_le_bytes());
898 out.extend_from_slice(&0u32.to_le_bytes());
899 out.push(0);
900 out.extend_from_slice(&[0; 3]);
901 out.extend_from_slice(&0u32.to_le_bytes());
902 out.extend_from_slice(&[0; 12]);
903 let strings_at = 48 + paths.len() * 24;
904 let mut strings: Vec<u8> = Vec::new();
905 for p in paths {
906 let off = (strings_at + strings.len()) as u32;
907 out.extend_from_slice(&drivers::native_entry_flags().to_le_bytes());
908 out.extend_from_slice(&0u32.to_le_bytes());
909 out.extend_from_slice(&off.to_le_bytes());
910 out.extend_from_slice(&0u32.to_le_bytes());
911 out.extend_from_slice(&0u64.to_le_bytes());
912 strings.extend_from_slice(p.as_bytes());
913 strings.push(0);
914 }
915 out.extend_from_slice(&strings);
916 out
917 }
918
919 fn versioned(names: &[&str]) -> Vec<u8> {
920 let defs: Vec<(u16, &str)> = names.iter().map(|n| (0, *n)).collect();
921 shared_object(&defs, &[])
922 }
923
924 fn driver_needing(needed: &[&str]) -> Vec<u8> {
925 shared_object(&[], needed)
926 }
927
928 #[test]
929 fn a_newer_host_dependency_of_a_driver_wins() {
930 let w = World::new();
931 let host_gl = w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
932 let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
933 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
934
935 let r = w.resolve(HostLibsPolicy::Auto);
936 assert!(r.farm.as_ref().is_some_and(|f| f.starts_with(&w.store)));
937 assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo));
938 assert_eq!(w.farm_link("libGL.so.1"), Some(host_gl));
942 assert!(r.host_interp.is_none());
943 assert!(r.incomparable.is_empty());
944 }
945
946 #[test]
949 fn deciding_names_every_verdict_and_writes_nothing() {
950 let w = World::new();
951 let host_gl = w.host(
952 "libGL.so.1",
953 &driver_needing(&["libfoo.so.1", "libbar.so.1"]),
954 );
955 let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
956 w.host("libbar.so.1", &versioned(&["BAR_1.0"]));
957 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
958 w.bundle("libbar.so.1", &versioned(&["BAR_1.0", "BAR_1.1"]));
959 let icd_dir = w.icd_dir.to_string_lossy().into_owned();
960 let req = Request {
961 pkg_root: &w.pkg,
962 lib_dirs: &["lib"],
963 policy: HostLibsPolicy::Auto,
964 store: &w.store,
965 ld_cache: &w.cache,
966 icd_dirs: &[icd_dir.as_str()],
967 extra_root: None,
968 set_roots: &[],
969 host_libc: true,
970 };
971
972 let d = decide(&req);
973 assert_eq!(
974 d.compared,
975 vec![
976 ("libGL.so.1".to_string(), None),
977 ("libbar.so.1".to_string(), Some(Choice::Bundle)),
978 ("libfoo.so.1".to_string(), Some(Choice::Host)),
979 ]
980 );
981 assert_eq!(d.winners.get("libfoo.so.1"), Some(&host_foo));
982 assert_eq!(d.winners.get("libGL.so.1"), Some(&host_gl));
983 assert!(!w.store.exists(), "deciding wrote the store");
984
985 let r = resolve(&req);
986 assert_eq!(
987 r.farm, d.farm,
988 "the farm a launch builds is the one reported"
989 );
990 }
991
992 #[test]
993 fn a_driver_dependency_the_bundle_lacks_comes_from_the_host() {
994 let w = World::new();
995 w.host("libvulkan.so.1", &driver_needing(&["libdrm.so.2"]));
996 let host_drm = w.host("libdrm.so.2", &versioned(&[]));
997 w.host("libunrelated.so.1", &versioned(&[]));
998 w.bundle("libapp.so.1", &versioned(&[]));
999
1000 w.resolve(HostLibsPolicy::Auto);
1001 assert_eq!(w.farm_link("libdrm.so.2"), Some(host_drm));
1002 assert!(w.farm_link("libunrelated.so.1").is_none());
1003 assert!(w.farm_link("libapp.so.1").is_none());
1004 }
1005
1006 #[test]
1007 fn auto_ignores_libraries_outside_the_driver_closure() {
1008 let w = World::new();
1009 let host_bar = w.host("libbar.so.1", &versioned(&["BAR_1.0", "BAR_2.0"]));
1010 w.bundle("libbar.so.1", &versioned(&["BAR_1.0"]));
1011
1012 assert_eq!(w.resolve(HostLibsPolicy::Auto), Resolution::default());
1013 assert!(w.farm_link("libbar.so.1").is_none());
1014
1015 let r = w.resolve(HostLibsPolicy::Always);
1016 assert!(r.farm.is_some());
1017 assert_eq!(w.farm_link("libbar.so.1"), Some(host_bar));
1018 }
1019
1020 #[test]
1021 fn a_newer_bundled_copy_stays() {
1022 let w = World::new();
1023 w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
1024 w.host("libfoo.so.1", &versioned(&["FOO_1.0"]));
1025 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1026 w.resolve(HostLibsPolicy::Auto);
1027 assert!(w.farm_link("libfoo.so.1").is_none());
1028 assert!(w.farm_link("libGL.so.1").is_some());
1029 }
1030
1031 #[test]
1032 fn unversioned_and_unreadable_host_copies_stay_bundled() {
1033 let w = World::new();
1034 w.host(
1035 "libGL.so.1",
1036 &driver_needing(&["libfoo.so.1", "libbaz.so.1"]),
1037 );
1038 w.host("libfoo.so.1", &versioned(&[]));
1039 w.bundle("libfoo.so.1", &versioned(&[]));
1040 let broken = versioned(&["BAZ_9.0"]);
1041 w.host("libbaz.so.1", &broken[..broken.len() / 2]);
1042 w.bundle("libbaz.so.1", &versioned(&["BAZ_1.0"]));
1043 let r = w.resolve(HostLibsPolicy::Auto);
1044 assert!(w.farm_link("libfoo.so.1").is_none());
1045 assert!(w.farm_link("libbaz.so.1").is_none());
1046 assert!(r.incomparable.is_empty());
1047 }
1048
1049 #[test]
1050 fn incomparable_copies_stay_bundled_and_are_named() {
1051 let w = World::new();
1052 w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
1053 w.host("libfoo.so.1", &versioned(&["FOO_1.0", "VENDOR_1"]));
1054 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1055 let r = w.resolve(HostLibsPolicy::Auto);
1056 assert!(w.farm_link("libfoo.so.1").is_none());
1057 assert_eq!(r.incomparable, ["libfoo.so.1"]);
1058 }
1059
1060 #[test]
1061 fn never_takes_nothing() {
1062 let w = World::new();
1063 w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1064 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
1065 assert_eq!(w.resolve(HostLibsPolicy::Never), Resolution::default());
1066 assert!(!w.store.exists());
1067 }
1068
1069 #[test]
1070 fn closure_reaches_transitive_dependencies_and_stops_at_missing_ones() {
1071 let w = World::new();
1072 w.host(
1073 "libvulkan.so.1",
1074 &driver_needing(&["libdrm.so.2", "libgone.so.0"]),
1075 );
1076 w.host("libdrm.so.2", &driver_needing(&["libz.so.1"]));
1077 w.host("libz.so.1", &versioned(&[]));
1078 w.host("libunrelated.so.1", &versioned(&[]));
1079 let closure = HostIndex::load(&w.cache, &[], None).driver_closure();
1080 for name in ["libvulkan.so.1", "libdrm.so.2", "libz.so.1", "libgone.so.0"] {
1081 assert!(closure.contains(name), "{name} missing from {closure:?}");
1082 }
1083 assert!(!closure.contains("libunrelated.so.1"));
1084 }
1085
1086 #[test]
1087 fn a_newer_host_glibc_brings_its_loader_and_family() {
1088 let w = World::new();
1089 let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
1090 let host_libc = w.host(
1091 "libc.so.6",
1092 &shared_object_with_interp(
1093 &[(0, "GLIBC_2.2.5"), (0, "GLIBC_2.34")],
1094 &[],
1095 Some(host_ld.to_str().unwrap()),
1096 ),
1097 );
1098 let host_libm = w.host("libm.so.6", &versioned(&["GLIBC_2.2.5", "GLIBC_2.34"]));
1099 w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
1100 w.bundle("libm.so.6", &versioned(&["GLIBC_2.2.5"]));
1101 w.bundle("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
1102 w.bundle("libother.so.1", &versioned(&["OTHER_1"]));
1103
1104 let r = w.resolve(HostLibsPolicy::Auto);
1105 assert_eq!(r.host_interp, Some(host_ld.clone()));
1106 assert_eq!(w.farm_link("libc.so.6"), Some(host_libc));
1107 assert_eq!(w.farm_link("libm.so.6"), Some(host_libm));
1108 assert_eq!(w.farm_link("ld-linux-x86-64.so.2"), Some(host_ld));
1109 assert!(w.farm_link("libother.so.1").is_none());
1110 }
1111
1112 #[test]
1116 fn a_launch_that_cannot_shadow_keeps_the_bundled_glibc() {
1117 let w = World::new();
1118 let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
1119 w.host(
1120 "libc.so.6",
1121 &shared_object_with_interp(
1122 &[(0, "GLIBC_2.2.5"), (0, "GLIBC_2.34")],
1123 &[],
1124 Some(host_ld.to_str().unwrap()),
1125 ),
1126 );
1127 w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
1128 w.bundle("ld-linux-x86-64.so.2", &versioned(&["GLIBC_2.2.5"]));
1129
1130 let r = w.resolve_full(HostLibsPolicy::Auto, None, false);
1131 assert!(r.host_interp.is_none());
1132 assert!(w.farm_link("libc.so.6").is_none());
1133
1134 let r = w.resolve_full(HostLibsPolicy::Auto, None, true);
1137 assert_eq!(r.host_interp, Some(host_ld));
1138 }
1139
1140 #[test]
1141 fn an_older_or_equal_host_glibc_keeps_the_bundled_pair() {
1142 let w = World::new();
1143 let host_ld = w.host("ld-linux-x86-64.so.2", &versioned(&[]));
1144 w.host(
1145 "libc.so.6",
1146 &shared_object_with_interp(&[(0, "GLIBC_2.2.5")], &[], Some(host_ld.to_str().unwrap())),
1147 );
1148 w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5", "GLIBC_2.34"]));
1149 assert_eq!(w.resolve(HostLibsPolicy::Always), Resolution::default());
1150
1151 w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
1152 assert_eq!(w.resolve(HostLibsPolicy::Always), Resolution::default());
1153 }
1154
1155 #[test]
1156 fn a_host_without_glibc_takes_nothing_for_the_pair() {
1157 let w = World::new();
1158 w.host("libc.musl-x86_64.so.1", &versioned(&[]));
1159 w.bundle("libc.so.6", &versioned(&["GLIBC_2.2.5"]));
1160 let r = w.resolve(HostLibsPolicy::Always);
1161 assert!(r.host_interp.is_none());
1162 assert!(r.farm.is_none());
1163 }
1164
1165 #[test]
1166 fn the_decision_is_recorded_and_reused_until_the_host_changes() {
1167 let w = World::new();
1168 w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
1169 let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1170 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
1171 let first = w.resolve(HostLibsPolicy::Auto);
1172 assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
1173
1174 assert_eq!(w.resolve(HostLibsPolicy::Auto), first);
1176 assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
1177
1178 fs::write(&host_foo, versioned(&["FOO_1.0"])).unwrap();
1183 w.resolve(HostLibsPolicy::Auto);
1184 assert!(w.farm_link("libfoo.so.1").is_none());
1185
1186 fs::write(w.host_lib.join("libnew.so.1"), versioned(&[])).unwrap();
1189 bump_mtime(&w.host_lib);
1190 w.resolve(HostLibsPolicy::Auto);
1191 assert!(w.farm_link("libfoo.so.1").is_none());
1192 }
1193
1194 #[test]
1195 fn an_in_place_same_size_swap_is_not_served_stale() {
1196 let w = World::new();
1197 w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
1198 let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1199 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
1200 w.resolve(HostLibsPolicy::Auto);
1201 assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
1202
1203 fs::write(&host_foo, versioned(&["FOO_0.9", "FOO_1.1"])).unwrap();
1209 std::fs::File::options()
1210 .write(true)
1211 .open(&host_foo)
1212 .unwrap()
1213 .set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000))
1214 .unwrap();
1215 w.resolve(HostLibsPolicy::Auto);
1216 assert!(
1217 w.farm_link("libfoo.so.1").is_none(),
1218 "a swapped winner must be re-judged, not served from the record"
1219 );
1220 }
1221
1222 #[test]
1223 fn a_vanished_winner_recomputes_instead_of_failing() {
1224 let w = World::new();
1225 w.host("libGL.so.1", &driver_needing(&["libfoo.so.1"]));
1226 let host_foo = w.host("libfoo.so.1", &versioned(&["FOO_1.0", "FOO_1.1"]));
1227 w.bundle("libfoo.so.1", &versioned(&["FOO_1.0"]));
1228 w.resolve(HostLibsPolicy::Auto);
1229
1230 let before = fs::metadata(&w.host_lib).unwrap().modified().unwrap();
1231 fs::remove_file(&host_foo).unwrap();
1232 fs::File::open(&w.host_lib)
1233 .unwrap()
1234 .set_modified(before)
1235 .unwrap();
1236 w.resolve(HostLibsPolicy::Auto);
1237 assert!(w.farm_link("libfoo.so.1").is_none());
1238 assert!(w.farm_link("libGL.so.1").is_some());
1239 }
1240
1241 #[test]
1242 fn library_path_is_read_from_icd_json() {
1243 assert_eq!(
1244 json_library_path(
1245 "{\n \"file_format_version\": \"1.0.0\",\n \"ICD\": {\n \"library_path\": \"/usr/lib64/libvulkan_radeon.so\",\n \"api_version\": \"1.3\"\n }\n}"
1246 ),
1247 Some("/usr/lib64/libvulkan_radeon.so")
1248 );
1249 assert_eq!(
1250 json_library_path("{\"ICD\":{\"library_path\":\"libEGL_nvidia.so.0\"}}"),
1251 Some("libEGL_nvidia.so.0")
1252 );
1253 assert_eq!(json_library_path("{}"), None);
1254 }
1255
1256 fn bump_mtime(dir: &Path) {
1259 let later = std::time::SystemTime::now() + std::time::Duration::from_secs(5);
1260 fs::File::open(dir).unwrap().set_modified(later).unwrap();
1261 }
1262
1263 #[test]
1268 fn a_partial_host_stack_does_not_count() {
1269 let w = World::new();
1270 w.host("libGL.so.1", &shared_object(&[(1, "GL_1")], &[]));
1271 w.bundle(
1272 "libQtGui.so.6",
1273 &shared_object(&[(1, "Qt_6")], &["libEGL.so.1"]),
1274 );
1275 assert_eq!(
1276 w.gl(),
1277 Gl::Absent,
1278 "the host has no libEGL, which the bundle asks for"
1279 );
1280 w.host("libEGL.so.1", &shared_object(&[(1, "EGL_1")], &[]));
1281 assert_eq!(w.gl(), Gl::Host, "both halves present");
1282 }
1283
1284 #[test]
1285 fn the_gl_situation_is_bundled_host_or_absent() {
1286 let w = World::new();
1287 w.host("libz.so.1", &shared_object(&[(1, "Z_1")], &[]));
1288 assert_eq!(w.gl(), Gl::Absent, "no GL entry point anywhere");
1289 w.host("libEGL.so.1", &shared_object(&[(1, "EGL_1")], &[]));
1290 assert_eq!(w.gl(), Gl::Host);
1291 w.bundle("libGL.so.1", &shared_object(&[(1, "GL_1")], &[]));
1292 assert_eq!(w.gl(), Gl::Bundled, "the bundle's own stack comes first");
1293 }
1294
1295 #[test]
1296 fn an_extra_root_supplies_what_the_host_lacks() {
1297 let w = World::new();
1298 w.host("libz.so.1", &shared_object(&[(1, "Z_1")], &[]));
1299 w.bundle("libc.so.6", &shared_object(&[(2, "GLIBC_2.30")], &[]));
1300 let root = temp_dir("glbuild");
1301 fs::create_dir_all(root.join("lib")).unwrap();
1302 fs::create_dir_all(root.join("share/vulkan/icd.d")).unwrap();
1303 let gl = root.join("lib/libGL.so.1");
1304 fs::write(&gl, shared_object(&[(1, "GL_1")], &["libdrm.so.2"])).unwrap();
1305 let drm = root.join("lib/libdrm.so.2");
1306 fs::write(&drm, shared_object(&[(1, "DRM_1")], &[])).unwrap();
1307 let icd = root.join("lib/libvulkan_fake.so");
1308 fs::write(&icd, shared_object(&[(1, "ICD_1")], &[])).unwrap();
1309 let vendor = root.join("lib/libEGL_fake.so.0");
1310 fs::write(&vendor, shared_object(&[(1, "VENDOR_1")], &["libdrm.so.2"])).unwrap();
1311 fs::write(
1312 root.join("share/vulkan/icd.d/fake.json"),
1313 format!("{{\"ICD\": {{\"library_path\": \"{}\"}}}}", icd.display()),
1314 )
1315 .unwrap();
1316
1317 let without = w.resolve(HostLibsPolicy::Auto);
1318 assert_eq!(without.farm, None, "nothing to take from a host without GL");
1319
1320 let with = w.resolve_with(HostLibsPolicy::Auto, Some(&root));
1321 assert_eq!(w.farm_link("libGL.so.1"), Some(gl));
1322 assert_eq!(
1323 w.farm_link("libdrm.so.2"),
1324 Some(drm),
1325 "the build's closure follows"
1326 );
1327 assert_eq!(
1328 w.farm_link("libvulkan_fake.so"),
1329 Some(icd),
1330 "ICD files seed the walk"
1331 );
1332 assert_eq!(
1333 w.farm_link("libEGL_fake.so.0"),
1334 Some(vendor),
1335 "a vendor library nothing names is farmed by its name"
1336 );
1337 assert!(with.farm.is_some());
1338 assert_eq!(
1339 with.host_interp, None,
1340 "the extra root never supplies a loader"
1341 );
1342
1343 let _ = fs::remove_dir_all(&root);
1344 }
1345
1346 #[test]
1347 fn a_host_change_leaves_the_old_farm_for_running_instances() {
1348 let w = World::new();
1349 let host_gl = w.host("libGL.so.1", &versioned(&["GL_1.0", "GL_2.0"]));
1350 w.bundle("libGL.so.1", &versioned(&["GL_1.0"]));
1351 w.resolve(HostLibsPolicy::Auto);
1352 let old_farm = w.last_farm.borrow().clone().expect("a farm");
1353 assert_eq!(
1354 fs::read_link(old_farm.join("libGL.so.1")).ok(),
1355 Some(host_gl)
1356 );
1357
1358 w.host("libnew.so.1", &versioned(&[]));
1361 w.resolve(HostLibsPolicy::Auto);
1362 let new_farm = w.last_farm.borrow().clone().expect("a farm");
1363 assert_ne!(old_farm, new_farm);
1364 assert!(
1365 old_farm.join("libGL.so.1").exists(),
1366 "the old farm was torn down under a running instance"
1367 );
1368 assert!(new_farm.join("libGL.so.1").exists());
1369 }
1370
1371 #[test]
1372 fn an_icd_naming_its_driver_by_bare_soname_seeds_the_walk() {
1373 let w = World::new();
1374 w.host(
1377 "libGLX_nvidia.so.0",
1378 &driver_needing(&["libnvidia-glcore.so.1"]),
1379 );
1380 let host_glcore = w.host("libnvidia-glcore.so.1", &versioned(&[]));
1381 fs::write(
1382 w.icd_dir.join("nvidia_icd.json"),
1383 r#"{ "ICD": { "library_path": "libGLX_nvidia.so.0" } }"#,
1384 )
1385 .unwrap();
1386 w.bundle("libapp.so.1", &versioned(&[]));
1387
1388 w.resolve(HostLibsPolicy::Auto);
1389 assert!(w.farm_link("libGLX_nvidia.so.0").is_some());
1390 assert_eq!(w.farm_link("libnvidia-glcore.so.1"), Some(host_glcore));
1391 }
1392}