Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

src/code_room/preflight.rs

//! One line instead of a linker wall: is SuiteSparse:GraphBLAS where a build
//! that links the code room will find it? (decision D5, land map §5.3)
//!
//! `graphblas-sys` links the SYSTEM library dynamically (`-lgraphblas`). Its
//! build script only warns when the header is missing, so on a box without
//! `libgraphblas-dev` nothing fails until the first LINK — hundreds of lines
//! of `rust-lld` / `cc` output per test binary. Every gate in this repo that
//! links a workspace calls [`check`] first and fails with the sentence below.
//!
//! GraphBLAS is a BUILD REQUIREMENT of everything that links the engine, not
//! a feature: the `code-room` cargo feature only keeps the thin atoms from
//! linking it.

use std::path::{Path, PathBuf};

/// The major version the checked-in bindings speak (`graphblas-sys`).
pub const GRAPHBLAS_MAJOR: u32 = 7;

/// What a gate prints when the library is not there.
pub const INSTALL_HINT: &str = "SuiteSparse:GraphBLAS 7.x not found — apt-get install libgraphblas-dev";

/// The library as the linker will see it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Found {
    /// `…/libgraphblas.so` — the name `-lgraphblas` resolves.
    pub lib: PathBuf,
    /// From the file the link name points at (`libgraphblas.so.7.4.0` → 7.4.0).
    pub version: Option<String>,
}

/// Does the workspace at `root` link GraphBLAS? Read from what cargo itself
/// resolved: its lockfile names `graphblas-sys` — or, with no lockfile yet, a
/// manifest in it does.
pub fn links_graphblas(root: &Path) -> bool {
    if let Ok(lock) = std::fs::read_to_string(root.join("Cargo.lock")) {
        return lock.lines().any(|l| l.trim() == "name = \"graphblas-sys\"");
    }
    root.join("crates/graphblas-sys/Cargo.toml").is_file()
        || std::fs::read_to_string(root.join("Cargo.toml")).is_ok_and(|m| m.contains("graphblas-sys") || m.contains("nornir-mimir-room"))
}

/// Where the linker looks, in its order: `GRAPHBLAS_LIB_DIR` (what
/// `graphblas-sys` adds to the search path), `LIBRARY_PATH`, then the system
/// directories (the multiarch ones included).
pub fn search_dirs() -> Vec<PathBuf> {
    let mut dirs: Vec<PathBuf> = Vec::new();
    for var in ["GRAPHBLAS_LIB_DIR", "LIBRARY_PATH"] {
        if let Some(v) = std::env::var_os(var) {
            dirs.extend(std::env::split_paths(&v).filter(|p| !p.as_os_str().is_empty()));
        }
    }
    for base in ["/usr/local/lib", "/usr/lib", "/lib", "/usr/lib64", "/lib64"] {
        let base = PathBuf::from(base);
        // multiarch: /usr/lib/x86_64-linux-gnu, /usr/lib/aarch64-linux-gnu …
        if let Ok(rd) = std::fs::read_dir(&base) {
            let mut multi: Vec<PathBuf> = rd.flatten().map(|e| e.path()).filter(|p| p.is_dir() && p.file_name().is_some_and(|n| n.to_string_lossy().contains("-linux-"))).collect();
            multi.sort();
            dirs.extend(multi);
        }
        dirs.push(base);
    }
    dirs
}

/// Find `libgraphblas.so` in `dirs` and check it is the 7.x the bindings speak.
pub fn graphblas_in(dirs: &[PathBuf]) -> Result<Found, String> {
    let mut runtime_only = None;
    for dir in dirs {
        let lib = dir.join("libgraphblas.so");
        if lib.exists() {
            // the link name is a symlink chain to the versioned file
            let version = std::fs::canonicalize(&lib)
                .ok()
                .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
                .and_then(|n| n.strip_prefix("libgraphblas.so.").map(str::to_string));
            let major = version.as_deref().and_then(|v| v.split('.').next()).and_then(|m| m.parse::<u32>().ok());
            return match major {
                Some(m) if m != GRAPHBLAS_MAJOR => Err(format!(
                    "{INSTALL_HINT} (found {} = GraphBLAS {}, the bindings speak {GRAPHBLAS_MAJOR}.x)",
                    lib.display(),
                    version.unwrap_or_default()
                )),
                _ => Ok(Found { lib, version }),
            };
        }
        if runtime_only.is_none()
            && let Ok(rd) = std::fs::read_dir(dir)
        {
            runtime_only = rd.flatten().map(|e| e.path()).find(|p| p.file_name().is_some_and(|n| n.to_string_lossy().starts_with("libgraphblas.so.")));
        }
    }
    Err(match runtime_only {
        // the runtime package alone cannot be linked against
        Some(rt) => format!("{INSTALL_HINT} (only the runtime library {} is here: the `-dev` package brings the `libgraphblas.so` a build links)", rt.display()),
        None => INSTALL_HINT.to_string(),
    })
}

/// The library on this box.
pub fn graphblas() -> Result<Found, String> {
    graphblas_in(&search_dirs())
}

/// THE preflight of a gate that links the workspace at `root`: `Ok` when the
/// workspace does not link GraphBLAS, or the library is there; else the one
/// line to print instead of running cargo into a linker error.
pub fn check(root: &Path) -> Result<(), String> {
    if !links_graphblas(root) {
        return Ok(());
    }
    graphblas().map(|_| ())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn dir_with(files: &[&str], link: Option<(&str, &str)>) -> tempfile::TempDir {
        let d = tempfile::tempdir().unwrap();
        for f in files {
            std::fs::write(d.path().join(f), b"").unwrap();
        }
        if let Some((name, target)) = link {
            std::os::unix::fs::symlink(d.path().join(target), d.path().join(name)).unwrap();
        }
        d
    }

    #[test]
    fn a_box_without_the_library_gets_the_one_line() {
        let empty = tempfile::tempdir().unwrap();
        assert_eq!(graphblas_in(&[empty.path().to_path_buf()]).unwrap_err(), INSTALL_HINT);
        assert_eq!(INSTALL_HINT, "SuiteSparse:GraphBLAS 7.x not found — apt-get install libgraphblas-dev");
    }

    #[test]
    fn the_dev_symlink_to_a_7x_library_is_found_with_its_version() {
        let d = dir_with(&["libgraphblas.so.7.4.0"], Some(("libgraphblas.so", "libgraphblas.so.7.4.0")));
        let found = graphblas_in(&[d.path().to_path_buf()]).unwrap();
        assert_eq!((found.lib, found.version.as_deref()), (d.path().join("libgraphblas.so"), Some("7.4.0")));
    }

    #[test]
    fn the_runtime_package_alone_and_another_major_are_refused_by_name() {
        let rt = dir_with(&["libgraphblas.so.7.4.0"], None);
        let e = graphblas_in(&[rt.path().to_path_buf()]).unwrap_err();
        assert!(e.starts_with(INSTALL_HINT) && e.contains("only the runtime library") && e.contains("-dev"), "{e}");
        let v9 = dir_with(&["libgraphblas.so.9.0.1"], Some(("libgraphblas.so", "libgraphblas.so.9.0.1")));
        let e = graphblas_in(&[v9.path().to_path_buf()]).unwrap_err();
        assert!(e.contains("GraphBLAS 9.0.1") && e.contains("7.x"), "{e}");
    }

    #[test]
    fn only_a_workspace_that_links_graphblas_is_checked() {
        let plain = tempfile::tempdir().unwrap();
        std::fs::write(plain.path().join("Cargo.lock"), "[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n").unwrap();
        assert!(!links_graphblas(plain.path()));
        assert_eq!(check(plain.path()), Ok(()), "a repo that does not link it needs no library");
        let ours = tempfile::tempdir().unwrap();
        std::fs::write(ours.path().join("Cargo.lock"), "[[package]]\nname = \"graphblas-sys\"\nversion = \"0.1.0\"\n").unwrap();
        assert!(links_graphblas(ours.path()));
        // this very repo links it
        assert!(links_graphblas(Path::new(env!("CARGO_MANIFEST_DIR"))));
    }

    /// The box these tests run on built the engine: the probe must agree with
    /// the linker (a probe that says "missing" where the link works is a gate
    /// that blocks green builds).
    #[cfg(feature = "code-room")]
    #[test]
    fn the_probe_agrees_with_the_linker_on_this_box() {
        let found = graphblas().expect("this test binary links libgraphblas, so the probe must find it");
        assert!(found.version.as_deref().is_none_or(|v| v.starts_with("7.")), "{found:?}");
        assert_eq!(check(Path::new(env!("CARGO_MANIFEST_DIR"))), Ok(()));
    }
}