Skip to main content

onelf_format/
resolve.rs

1//! Per-library choice between the bundle and the host, made at launch.
2//!
3//! A bundle that reaches a host GPU driver has to load that driver's
4//! dependencies too, and the host built them against its own glibc and
5//! libstdc++. Whether they can share a process with the bundled copies
6//! depends on which side is newer, and that differs per library, so the
7//! answer cannot be a directory order. Each soname present on both sides
8//! is compared by the versions it defines and the superset wins.
9//!
10//! The loader and libc are a unit: a libc only runs under the loader it
11//! shipped with. They are compared first, and when the host's pair wins
12//! the whole glibc family comes from the host so nothing bundled is loaded
13//! beside a foreign loader.
14//!
15//! Winners from the host are materialized as symlinks in a link farm, and
16//! the runtime shows each one the bundle lacks in the bundle's library
17//! directory. No host directory is ever searched, so a soname the bundle
18//! lacks and the resolver did not choose fails by name instead of being
19//! satisfied by whatever the host has.
20
21use 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/// What a launch takes from the host.
31#[derive(Debug, Default, PartialEq, Eq)]
32pub struct Resolution {
33    /// Directory of symlinks to the chosen host libraries, when any were
34    /// chosen.
35    pub farm: Option<PathBuf>,
36    /// The host loader to run the entrypoint under, when the host's libc
37    /// won. The bundled loader is used otherwise.
38    pub host_interp: Option<PathBuf>,
39    /// Sonames whose two copies could not be ordered. The bundled copy is
40    /// used; these are reported so the publisher can see them.
41    pub incomparable: Vec<String>,
42}
43
44/// One launch's inputs.
45pub struct Request<'a> {
46    /// The package root as the entrypoint will see it.
47    pub pkg_root: &'a Path,
48    /// Library directories relative to `pkg_root`.
49    pub lib_dirs: &'a [&'a str],
50    pub policy: HostLibsPolicy,
51    /// Where the link farm and the recorded decision live. Created on
52    /// demand; reused across launches of the same package on the same
53    /// host.
54    pub store: &'a Path,
55    /// The loader cache image describing the host.
56    pub ld_cache: &'a Path,
57    /// Directories of Vulkan ICD and EGL vendor files describing the
58    /// host's drivers. [`ICD_DIRS`] outside of tests.
59    pub icd_dirs: &'a [&'a str],
60    /// A fetched GL build's extracted root, indexed ahead of the host:
61    /// its [`GL_BUILD_LIB_DIRS`] and [`GL_BUILD_ICD_DIRS`] supply what
62    /// the host could not.
63    pub extra_root: Option<&'a Path>,
64    /// Extracted roots of the dependency sets the package pins. Their
65    /// libraries are the ones the bundle left out, so they are compared
66    /// against the host exactly as a bundled copy would be.
67    pub set_roots: &'a [PathBuf],
68    /// Whether the launch can place host files over their bundled copies.
69    /// Without that the host's glibc is never chosen: the bundled
70    /// executables' run path would still find the bundled libc, and it
71    /// cannot run under the host loader.
72    pub host_libc: bool,
73}
74
75/// Library directories of a GL build, relative to its root.
76pub const GL_BUILD_LIB_DIRS: &[&str] = &["lib", "lib64"];
77
78/// Driver description directories of a GL build, relative to its root.
79pub const GL_BUILD_ICD_DIRS: &[&str] = &["share/vulkan/icd.d", "share/glvnd/egl_vendor.d"];
80
81/// Libraries a GL build carries that nothing names in `DT_NEEDED`: the
82/// glvnd vendor libraries, Mesa's gallium and DRI megadrivers, Vulkan
83/// ICDs and layers are all opened by name at runtime. They seed the
84/// closure walk so they and their dependencies reach the farm, since
85/// the build's directories are never put on the search path themselves.
86const GL_BUILD_SEEDS: &[&str] = &[
87    "libGLX_",
88    "libEGL_",
89    "libgallium",
90    "libdril",
91    "libvulkan_",
92    "libVkLayer_",
93];
94
95/// Where a launch gets its GL stack from, if anywhere.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Gl {
98    Bundled,
99    Host,
100    Absent,
101}
102
103/// The entry points of a GL or Vulkan stack. One of them present, on
104/// either side, is a stack the application can load.
105const 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
112/// The GL entry points `carried` objects name and `carried` does not
113/// itself hold. This is what the launch will ask the host, or a pinned
114/// build, to supply.
115fn 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
127/// Whether the bundle or the host carries a GL stack. Decided before a
128/// pinned build is fetched, so the host is indexed without one.
129///
130/// `extra_dirs` are library directories beside the package, which is
131/// where a shared dependency set puts a toolkit, and a toolkit is
132/// usually what names a GL entry point.
133///
134/// A host counts only when it has every entry point the package asks
135/// for. Some hosts have part of a stack and not the rest: an image with
136/// `libGL.so.1` and no `libEGL.so.1` is one an X server pulls in, and
137/// treating that as a stack leaves the application to fail on the
138/// missing half.
139pub 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    // Nothing names one outright, so a plugin may still ask for one by
158    // dlopen and any entry point is worth having.
159    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
175/// The glibc family: everything that must come from the same build as
176/// the loader. Matched as soname prefixes.
177const 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
194/// Where the host records the graphics drivers it loads by absolute path.
195/// Those never appear as `DT_NEEDED`, so the walk has to be seeded from
196/// here as well as from the driver families.
197pub 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
206/// Whether `soname` belongs to the glibc family, which a package always
207/// carries as a unit with its loader.
208pub 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
216/// Decide what this launch takes from the host.
217pub 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/// What the resolver decides for a request, worked out afresh and with
263/// nothing written.
264#[derive(Debug, Default)]
265pub struct Decision {
266    /// Every soname weighed, in order, with the copy that won: `None` for
267    /// one the bundle lacks, which only the host can supply.
268    pub compared: Vec<(String, Option<Choice>)>,
269    /// The host libraries chosen, by soname.
270    pub winners: BTreeMap<String, PathBuf>,
271    pub host_interp: Option<PathBuf>,
272    pub incomparable: Vec<String>,
273    /// Where in the store the link farm for these winners lives.
274    pub farm: Option<PathBuf>,
275}
276
277/// [`resolve`] without the store: neither the recorded decision is read
278/// nor a farm or record written. For reporting what a launch would do.
279pub 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    // A name the bundle carries itself wins: the set supplies what was
304    // left out, not a second copy of what was kept.
305    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    // The driver closure is what the host is expected to supply. A member
339    // the bundle also carries is compared; one it does not carry can only
340    // come from the host. Under `always` every bundled soname is compared
341    // as well.
342    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
370/// A library that cannot be read is treated as defining nothing: an
371/// unreadable host library loses to the bundled copy, and an unreadable
372/// bundled library loses to the host copy.
373fn versions(path: &Path) -> VersionSet {
374    verdef::read(path).unwrap_or_default()
375}
376
377/// The host's libraries by soname, from its loader cache and the drivers
378/// its ICD files name by path.
379struct HostIndex {
380    libs: BTreeMap<String, PathBuf>,
381    /// Drivers reached through ICD files rather than the cache, and a GL
382    /// build's dlopen-only libraries. They seed the closure walk alongside
383    /// the driver families.
384    icd_libs: Vec<PathBuf>,
385}
386
387impl HostIndex {
388    /// The extra root goes in first, so a soname it carries shadows the
389    /// host's copy and its drivers seed the closure walk.
390    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            // The build's ICD files name their drivers by bare soname, so
406            // they relocate; those resolve against the build's own
407            // directories rather than a cache.
408            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        // After the cache, so an ICD file naming its driver by bare soname
414        // (NVIDIA's `libGLX_nvidia.so.0`, which no driver family names)
415        // resolves through it and seeds the walk like one named by path.
416        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    /// The loader the host's libc was built with: its own `PT_INTERP`,
426    /// or failing that any loader the cache names.
427    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    /// Every soname reachable from the driver families through
441    /// `DT_NEEDED`, seeds included.
442    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
472/// `library_path` values from Vulkan ICD and EGL vendor files under
473/// `icd_dirs`. A value with a `/` names a file, absolute or relative to
474/// the file; a bare soname is looked up in `bare_names` and dropped when
475/// that is `None` or lacks it.
476fn 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
515/// The string value of the first `"library_path"` key in `text`. The files
516/// are small and regular enough that a scan is safer than a parser the
517/// runtime would otherwise not need.
518pub 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
527/// The shared objects the bundle carries in its library directories, by
528/// soname.
529fn 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
539/// Every shared object directly in `dir`, by file name.
540///
541/// A symlink to an absolute path is not one of them. A package's own links
542/// stay inside it, and an absolute one is a link the runtime made to a host
543/// or set library, which a mount or a cache tree still shows when a later
544/// launch looks.
545fn 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
560/// A description of everything on the host the decision depends on: the
561/// cache image, the directories it names, and the driver description
562/// files, each with its size and modification time. Compared whole, so
563/// no hash is needed and nothing has to be added to the format crate's
564/// dependencies.
565fn 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
609/// The link farm for one host state. Keyed by the fingerprint and the
610/// winners themselves: two decisions can share a fingerprint (an
611/// in-place library swap leaves the directory stamps alone) yet pick
612/// different winners, and a farm reused across them would serve the
613/// previous decision's stale links. A launch after the host changed
614/// builds a new one beside the old rather than replacing it: an
615/// instance started earlier may still be loading through the old farm,
616/// and two instances starting together build the same one and race for
617/// nothing.
618fn farm_dir(store: &Path, fingerprint: &str, winners: &BTreeMap<String, PathBuf>) -> PathBuf {
619    // FNV-1a, so the names are stable across runs without a hash
620    // dependency.
621    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
639/// The link farm for `fingerprint` holding `winners`, built if it does not
640/// exist yet. An empty set leaves no farm.
641///
642/// Built in a scratch directory and renamed into place. A rename that finds
643/// the farm already there lost to another instance building the same
644/// thing, which is fine. Nothing is ever removed.
645fn 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    // The identity of every winner, so the next launch can tell an
697    // in-place same-size swap (invisible to the directory stamps) from
698    // the host the decision was made about.
699    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
710/// The identity of one file: size, inode, and both times, separated by
711/// colons. `missing` when the file cannot be stat'ed at all.
712fn 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
730/// The recorded decision, when it was made for this exact host state and
731/// everything it names still exists.
732fn 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    // The farm is keyed by the winners as recorded, so the links are
770    // checked where this decision says they are, not where a stale
771    // earlier decision left them.
772    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    /// A host with a library directory and a loader cache naming its
789    /// contents, and a package root with one `lib/` directory.
790    struct World {
791        host_lib: PathBuf,
792        cache: PathBuf,
793        pkg: PathBuf,
794        store: PathBuf,
795        /// Where the host's ICD files go; empty unless a test writes one.
796        icd_dir: PathBuf,
797        /// The farm the last resolution produced, where `farm_link` looks.
798        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        /// Rewrite the cache image from the host directory's contents.
834        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    /// A new-format loader cache image naming `paths`.
895    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        // The driver itself is not bundled, so it can only come from the
939        // host, and it must be reachable without a host directory on the
940        // search path.
941        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    /// A report has to show each verdict and where the farm would be,
947    /// and must not leave a farm or a recorded decision behind.
948    #[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    /// Under the host loader the bundled executables' run path still finds
1113    /// the bundled libc, so a launch that cannot put the host's copy in its
1114    /// place must keep the bundled pair rather than crash at start.
1115    #[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        // The decision recorded for it is not the one a launch that can
1135        // shadow gets.
1136        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        // Nothing changed: the recorded decision is reused as it stands.
1175        assert_eq!(w.resolve(HostLibsPolicy::Auto), first);
1176        assert_eq!(w.farm_link("libfoo.so.1"), Some(host_foo.clone()));
1177
1178        // Rewriting the file in place changes neither the cache nor the
1179        // directory, but the decision records the winner's own stat, so
1180        // this is re-judged. The rewritten copy now matches the bundle's
1181        // version exactly, and the bundle keeps its own.
1182        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        // A new entry in the directory is a host change; the fresh
1187        // comparison agrees with what the re-judgement found.
1188        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        // Same byte count, different versions: neither side's version set
1204        // contains the other's, so the recorded decision is void. The
1205        // directory stamps see nothing here. The mtime is pinned to a
1206        // distinct instant because two writes in one kernel timestamp
1207        // tick share ctime; a real swap always lands later.
1208        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    /// Move a directory's mtime well past "now", so a change made within
1257    /// the same filesystem timestamp granularity still reads as a change.
1258    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    /// An image that has `libGL.so.1` and not `libEGL.so.1`, which is
1264    /// what an X server pulls in, is not a stack for a package whose
1265    /// toolkit links `libEGL`. Counting it as one leaves the launch to
1266    /// fail on the missing half instead of fetching the pinned build.
1267    #[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        // The host changes, so the next launch decides afresh. An instance
1359        // started before is still loading through the old farm.
1360        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        // NVIDIA's files name the vendor library by soname alone, and no
1375        // driver family prefix matches `libGLX_nvidia`.
1376        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}