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/doctor.rs

//! Is this box ready to fill a code room? One line per requirement, each
//! with the command that fixes it (pro-list item 3: "installing the server is
//! one step and says what is missing").
//!
//! Asked by `dwarves-server --check` and at the end of `--install`, as the
//! user the service runs as: the driver, the toolchain and the model are
//! looked up in THAT user's home and PATH. GraphBLAS is not a line — a binary
//! that links the room does not start without it, so the check never runs on
//! a box that lacks it.

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

use mimir_ingest::DriverUnavailable;

/// How one requirement stands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mark {
    /// There and working.
    Ok,
    /// Missing: the room cannot be filled until it is fixed.
    Missing,
    /// Missing, and the room works without it (a feature is off).
    Optional,
}

/// One requirement: what, how it stands, what was seen, how to fix it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Check {
    pub what: &'static str,
    pub mark: Mark,
    pub detail: String,
    pub fix: Option<String>,
}

impl Check {
    fn ok(what: &'static str, detail: impl Into<String>) -> Self {
        Self { what, mark: Mark::Ok, detail: detail.into(), fix: None }
    }
    fn missing(what: &'static str, detail: impl Into<String>, fix: impl Into<String>) -> Self {
        Self { what, mark: Mark::Missing, detail: detail.into(), fix: Some(fix.into()) }
    }
}

/// Every requirement of a server that fills code rooms, checked on this box
/// for the running user.
pub fn server_checks() -> Vec<Check> {
    let mut out = Vec::new();
    out.push(cargo_on_path(std::env::var_os("PATH").as_deref()));
    let settings = super::driver::Settings::from_env();
    match settings.locate() {
        Err(r) => out.push(Check::missing(
            "Mímir driver",
            r.to_string(),
            format!(
                "build cargo-mimir-dump and mimir-driver (`cargo build --release` in nornir's crates/mimir-driver, on the pinned nightly) and put them into {}",
                crate::config::nornir_home().join("mimir-driver").display()
            ),
        )),
        Ok(spec) => match spec.probe() {
            Ok(ready) => {
                out.push(Check::ok(
                    "Mímir driver",
                    format!("{} — {}", spec.cargo_mimir_dump.display(), ready.rustc),
                ));
                out.push(std_sources(&ready.sysroot, &spec.toolchain));
            }
            Err(e) => out.push(Check::missing("Mímir driver", e.to_string(), driver_fix(&e))),
        },
    }
    out.push(embed_model());
    out
}

/// The command that fixes a driver probe failure.
fn driver_fix(e: &DriverUnavailable) -> String {
    match e {
        DriverUnavailable::MissingBinary { path, .. } => format!(
            "put cargo-mimir-dump and mimir-driver (built in nornir's crates/mimir-driver) into {}",
            path.parent().map(|p| p.display().to_string()).unwrap_or_default()
        ),
        DriverUnavailable::HostFailed { .. } => {
            "install rustup for this user (https://rustup.rs), or fix NORNIR_MIMIR_DRIVER_HOST".to_string()
        }
        DriverUnavailable::ToolchainMissing { toolchain, .. } | DriverUnavailable::NoCompilerLibs { toolchain, .. } => {
            format!("rustup toolchain install {toolchain} --profile minimal --component rustc-dev,rust-src,llvm-tools")
        }
        DriverUnavailable::DriverDoesNotLoad { toolchain, .. } => format!(
            "the driver was built for another nightly: rebuild it on {toolchain}, or set NORNIR_MIMIR_TOOLCHAIN to the one it was built with"
        ),
    }
}

/// `cargo` is what the dumper starts in every member's tree.
fn cargo_on_path(path: Option<&std::ffi::OsStr>) -> Check {
    let found = path
        .map(|p| std::env::split_paths(p).map(|d| d.join("cargo")).find(|c| c.is_file()))
        .unwrap_or(None);
    match found {
        Some(c) => Check::ok("cargo", c.display().to_string()),
        None => Check::missing(
            "cargo",
            "no `cargo` on PATH",
            "install rustup for this user (https://rustup.rs) or link ~/.cargo/bin/cargo into /usr/local/bin",
        ),
    }
}

/// The standard library's sources: the std dump (once per toolchain) reads them.
fn std_sources(sysroot: &Path, toolchain: &str) -> Check {
    let lib: PathBuf = sysroot.join("lib/rustlib/src/rust/library/std");
    if lib.is_dir() {
        Check::ok("std sources", lib.display().to_string())
    } else {
        Check::missing(
            "std sources",
            format!("no {}", lib.display()),
            format!("rustup component add rust-src --toolchain {toolchain}"),
        )
    }
}

/// The embedding model: without it the vector lenses answer that vectors are off.
fn embed_model() -> Check {
    #[cfg(any(feature = "embed-tract", feature = "embed-ort"))]
    {
        let dir = crate::vector::embed_support::model_dir();
        if dir.join("tokenizer.json").is_file() {
            return Check::ok("embedding model", dir.display().to_string());
        }
        Check {
            what: "embedding model",
            mark: Mark::Optional,
            detail: format!("no model at {} — vector lenses are off", dir.display()),
            fix: Some("copy the model directory to /opt/nornir/models (or set NORNIR_MODEL_DIR)".to_string()),
        }
    }
    #[cfg(not(any(feature = "embed-tract", feature = "embed-ort")))]
    Check {
        what: "embedding model",
        mark: Mark::Optional,
        detail: "this build has no embedder — vector lenses are off".to_string(),
        fix: None,
    }
}

/// True when nothing required is missing.
pub fn ready(checks: &[Check]) -> bool {
    checks.iter().all(|c| c.mark != Mark::Missing)
}

/// The checks as lines a person reads: `✓ what — detail`, `✗ what — detail`
/// and the fix under it.
pub fn render(checks: &[Check]) -> String {
    let mut s = String::new();
    for c in checks {
        let sign = match c.mark {
            Mark::Ok => "✓",
            Mark::Missing => "✗",
            Mark::Optional => "–",
        };
        s.push_str(&format!("{sign} {} — {}\n", c.what, c.detail));
        if let (Some(fix), false) = (&c.fix, c.mark == Mark::Ok) {
            s.push_str(&format!("    fix: {fix}\n"));
        }
    }
    s
}

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

    #[test]
    fn cargo_is_found_on_the_path_or_said_missing_with_a_fix() {
        let dir = std::env::temp_dir().join(format!("nornir-doctor-cargo-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let none = cargo_on_path(Some(dir.as_os_str()));
        assert_eq!(none.mark, Mark::Missing);
        assert!(none.fix.as_deref().unwrap().contains("rustup"), "{none:?}");
        std::fs::write(dir.join("cargo"), "").unwrap();
        let some = cargo_on_path(Some(dir.as_os_str()));
        assert_eq!(some.mark, Mark::Ok, "{some:?}");
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn missing_std_sources_name_the_rustup_command() {
        let c = std_sources(Path::new("/nonexistent-sysroot"), "nightly-2026-10-05");
        assert_eq!(c.mark, Mark::Missing);
        assert_eq!(c.fix.as_deref(), Some("rustup component add rust-src --toolchain nightly-2026-10-05"));
    }

    #[test]
    fn each_driver_failure_has_its_own_fix() {
        let tc = "nightly-2026-10-05".to_string();
        let missing = driver_fix(&DriverUnavailable::MissingBinary { which: "mimir-driver", path: PathBuf::from("/h/.nornir/mimir-driver/mimir-driver") });
        assert!(missing.contains("/h/.nornir/mimir-driver"), "{missing}");
        let toolchain = driver_fix(&DriverUnavailable::ToolchainMissing { toolchain: tc.clone(), detail: String::new() });
        assert!(toolchain.starts_with("rustup toolchain install nightly-2026-10-05"), "{toolchain}");
        assert!(toolchain.contains("rustc-dev") && toolchain.contains("rust-src"), "{toolchain}");
        let libs = driver_fix(&DriverUnavailable::NoCompilerLibs { toolchain: tc, sysroot: PathBuf::new() });
        assert_eq!(libs, toolchain, "rustc-dev missing is fixed by the same install");
    }

    #[test]
    fn render_shows_the_fix_only_where_something_is_wrong_and_ready_ignores_optional() {
        let checks = vec![
            Check::ok("cargo", "/usr/bin/cargo"),
            Check::missing("std sources", "no /x", "rustup component add rust-src --toolchain t"),
            Check { what: "embedding model", mark: Mark::Optional, detail: "off".into(), fix: Some("copy".into()) },
        ];
        let text = render(&checks);
        assert_eq!(
            text,
            "✓ cargo — /usr/bin/cargo\n✗ std sources — no /x\n    fix: rustup component add rust-src --toolchain t\n– embedding model — off\n    fix: copy\n"
        );
        assert!(!ready(&checks));
        assert!(ready(&[checks[0].clone(), checks[2].clone()]), "an optional line does not block");
    }
}