Skip to main content

ferro_cli/doctor/checks/
toolchain.rs

1//! Toolchain check (D-02): rustc/cargo version vs `rust-toolchain.toml`.
2//!
3//! Reuses `crate::project::resolve_rust_base_image` to read the declared
4//! channel from `rust-toolchain.toml` (no duplicate TOML parsing).
5
6use crate::doctor::check::{CheckResult, DoctorCheck};
7use crate::project::resolve_rust_base_image;
8use std::path::Path;
9use std::process::Command;
10
11pub struct ToolchainCheck;
12
13const NAME: &str = "toolchain_match";
14const DEFAULT_IMAGE: &str = "rust:1.88-slim-bookworm";
15
16impl DoctorCheck for ToolchainCheck {
17    fn name(&self) -> &'static str {
18        NAME
19    }
20    fn run(&self, root: &Path) -> CheckResult {
21        check_impl(root)
22    }
23}
24
25pub(crate) fn check_impl(root: &Path) -> CheckResult {
26    let image = resolve_rust_base_image(root);
27    let declared = if image == DEFAULT_IMAGE && !root.join("rust-toolchain.toml").exists() {
28        None
29    } else {
30        // image is "rust:<channel>-slim-bookworm" — strip prefix/suffix.
31        image
32            .strip_prefix("rust:")
33            .and_then(|s| s.strip_suffix("-slim-bookworm"))
34            .map(|s| s.to_string())
35    };
36
37    let installed = match Command::new("rustc").arg("--version").output() {
38        Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_string(),
39        Ok(_) => return CheckResult::error(NAME, "rustc invocation failed"),
40        Err(_) => return CheckResult::error(NAME, "rustc not found in PATH"),
41    };
42
43    match declared {
44        None => CheckResult::warn(NAME, format!("{installed}; rust-toolchain.toml missing"))
45            .with_details("Declare a pinned channel for reproducible builds"),
46        Some(channel) => {
47            if installed.contains(&channel) {
48                CheckResult::ok(NAME, format!("{installed} matches channel {channel}"))
49            } else {
50                CheckResult::warn(
51                    NAME,
52                    format!("installed {installed} differs from declared channel {channel}"),
53                )
54            }
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use tempfile::TempDir;
63
64    #[test]
65    fn name_is_toolchain() {
66        assert_eq!(ToolchainCheck.name(), "toolchain_match");
67    }
68
69    #[test]
70    fn missing_rust_toolchain_warns_gracefully() {
71        let tmp = TempDir::new().unwrap();
72        let result = check_impl(tmp.path());
73        // rustc is present in dev env; should be Warn (no toolchain.toml).
74        assert_eq!(result.name, "toolchain_match");
75        // Either Warn (rustc installed, no toml) or Error (rustc missing in CI).
76        // Never panics — that's the contract.
77    }
78}