use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use super::{Level, Report, cmd};
use crate::wrapper;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Health {
#[default]
NotConfigured,
Missing(String),
NotExecutable(PathBuf),
Version(PathBuf, Option<String>),
Running(PathBuf),
}
pub fn parse_version(text: &str) -> Option<String> {
let word = text.lines().next()?.split_whitespace().last()?;
word.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
.then(|| word.to_owned())
}
pub fn probe(configured: Option<&str>) -> Health {
let Some(configured) = configured.filter(|w| w.contains("cargo-steward")) else {
return Health::NotConfigured;
};
let Some(path) = wrapper::find_program(configured.as_ref()) else {
return Health::Missing(configured.to_owned());
};
if !is_executable(&path) {
return Health::NotExecutable(path);
}
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
let running = env::current_exe().ok().and_then(|e| e.canonicalize().ok());
if running.as_ref() == Some(&canonical) {
return Health::Running(path);
}
let version = cmd::stdout(Command::new(&path).arg("--version").env_remove("CARGO"))
.and_then(|t| parse_version(&t));
Health::Version(path, version)
}
fn is_executable(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
{
path.is_file()
}
}
pub fn report(out: &mut Report, health: &Health) {
match health {
Health::NotConfigured => {}
Health::Missing(configured) => out.say(
Level::Warn,
format!(
"The configured wrapper {configured} doesn't exist, so every build fails.\n\
Fix: reinstall cargo-steward, or run `cargo steward setup --undo`"
),
),
Health::NotExecutable(path) => out.say(
Level::Warn,
format!(
"The configured wrapper {} isn't executable, so every build fails.\nFix: chmod +x {}",
path.display(),
path.display()
),
),
Health::Running(path) => out.say(
Level::Ok,
format!("The wrapper is this binary ({}), {VERSION}", path.display()),
),
Health::Version(path, Some(v)) if v == VERSION => out.say(
Level::Ok,
format!(
"The wrapper {} is {v}, the same as this one",
path.display()
),
),
Health::Version(path, other) => out.say(
Level::Warn,
format!(
"The wrapper {} is {}, but this binary is {VERSION}.\n\
Fix: reinstall (scripts/install.sh, or `cargo install --locked cargo-steward`), then `cargo steward setup`",
path.display(),
other.as_deref().unwrap_or("an unknown version"),
),
),
}
if wrapper::find_program("cargo-steward".as_ref()).is_none() {
out.say(
Level::Warn,
"cargo-steward isn't on PATH, so `cargo steward` can't find it.\n\
Fix: add ~/.cargo/bin (or wherever it is installed) to PATH",
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_version_output() {
assert_eq!(
parse_version("cargo steward 0.1.0\n").as_deref(),
Some("0.1.0")
);
assert_eq!(
parse_version("cargo-steward 1.2.3-beta.1").as_deref(),
Some("1.2.3-beta.1")
);
assert_eq!(parse_version("error: unexpected argument"), None);
assert_eq!(parse_version(""), None);
}
#[test]
fn reports_a_missing_or_foreign_wrapper() {
assert_eq!(probe(None), Health::NotConfigured);
assert_eq!(probe(Some("sccache")), Health::NotConfigured);
assert_eq!(
probe(Some("/nonexistent/bin/cargo-steward")),
Health::Missing("/nonexistent/bin/cargo-steward".into())
);
}
#[cfg(unix)]
#[test]
fn notices_a_wrapper_that_cant_run() {
let tmp = tempfile::tempdir().unwrap();
let exe = tmp.path().join("cargo-steward");
std::fs::write(&exe, "#!/bin/sh\necho 'cargo steward 0.0.1'\n").unwrap();
let configured = exe.to_str().unwrap();
assert_eq!(probe(Some(configured)), Health::NotExecutable(exe.clone()));
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
let mut health = probe(Some(configured));
for _ in 0..20 {
if health != Health::Version(exe.clone(), None) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
health = probe(Some(configured));
}
assert_eq!(health, Health::Version(exe.clone(), Some("0.0.1".into())));
}
}