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(()));
}
}