use std::process::{Command, Stdio};
use crate::error::{CliError, CliResult};
struct Tool {
bin: &'static str,
krate: &'static str,
}
const TOOLCHAIN: &[Tool] = &[
Tool {
bin: "just",
krate: "just",
},
Tool {
bin: "bacon",
krate: "bacon",
},
Tool {
bin: "cargo-nextest",
krate: "cargo-nextest",
},
];
const NO_BOOTSTRAP_ENV: &str = "NESTRS_NO_BOOTSTRAP";
pub fn ensure_toolchain(no_bootstrap: bool) -> CliResult<()> {
let missing: Vec<&Tool> = TOOLCHAIN
.iter()
.filter(|tool| !tool_available(tool.bin))
.collect();
if missing.is_empty() {
return Ok(());
}
let crates: Vec<&str> = missing.iter().map(|tool| tool.krate).collect();
if no_bootstrap || env_disables_bootstrap() {
if missing.iter().any(|tool| tool.bin == "just") {
return Err(CliError::Anyhow(anyhow::anyhow!(
"missing dev tools: {names}. Bootstrap is disabled — install them manually:\n cargo install --locked {names}",
names = crates.join(" ")
)));
}
return Ok(());
}
install(&crates)
}
fn env_disables_bootstrap() -> bool {
std::env::var(NO_BOOTSTRAP_ENV)
.map(|value| is_truthy(&value))
.unwrap_or(false)
}
fn is_truthy(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
pub fn tool_available(bin: &str) -> bool {
Command::new(bin)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn install(crates: &[&str]) -> CliResult<()> {
eprintln!(
"nestrs: installing dev toolchain ({}) — first run only…",
crates.join(", ")
);
let mut cmd = Command::new("cargo");
if binstall_available() {
cmd.args(["binstall", "--no-confirm", "--locked"]);
} else {
cmd.args(["install", "--locked"]);
}
cmd.args(crates);
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let status = cmd.status().map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
CliError::Anyhow(anyhow::anyhow!(
"cargo is not on PATH — install Rust from https://rustup.rs"
))
} else {
CliError::Io(err)
}
})?;
if !status.success() {
return Err(CliError::Anyhow(anyhow::anyhow!(
"toolchain install failed — install manually:\n cargo install --locked {}",
crates.join(" ")
)));
}
Ok(())
}
fn binstall_available() -> bool {
Command::new("cargo")
.args(["binstall", "--version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toolchain_lists_just_bacon_nextest() {
let crates: Vec<&str> = TOOLCHAIN.iter().map(|tool| tool.krate).collect();
assert_eq!(crates, ["just", "bacon", "cargo-nextest"]);
}
#[test]
fn truthy_values_enable_opt_out() {
for value in ["1", "true", "TRUE", "yes", "On", " true "] {
assert!(is_truthy(value), "{value:?} should be truthy");
}
for value in ["", "0", "false", "no", "off"] {
assert!(!is_truthy(value), "{value:?} should be falsy");
}
}
#[test]
fn missing_binary_probes_false() {
assert!(!tool_available("nestrs-definitely-not-a-real-binary-xyz"));
}
}