use crate::core::config::extras::Language;
use crate::core::config::tools::{ToolchainCommand, ToolsConfig, required_tools_for_language};
use super::format::is_tool_available;
pub fn enforce_required_toolchains(
command: ToolchainCommand,
languages: &[Language],
tools: &ToolsConfig,
) -> anyhow::Result<()> {
enforce_required_toolchains_with(command, languages, tools, &is_tool_available)
}
pub(crate) fn enforce_required_toolchains_with(
command: ToolchainCommand,
languages: &[Language],
tools: &ToolsConfig,
is_available: &dyn Fn(&str) -> bool,
) -> anyhow::Result<()> {
for &lang in languages {
for tool in required_tools_for_language(command, lang, tools) {
bail_if_missing(&tool, lang, is_available)?;
}
}
Ok(())
}
pub fn enforce_required_toolchains_for_all(languages: &[Language]) -> anyhow::Result<()> {
enforce_required_toolchains_for_all_with(languages, &is_tool_available)
}
pub(crate) fn enforce_required_toolchains_for_all_with(
languages: &[Language],
is_available: &dyn Fn(&str) -> bool,
) -> anyhow::Result<()> {
let needs_cargo = languages
.iter()
.any(|lang| matches!(lang, Language::Ffi | Language::Swift));
if needs_cargo {
bail_if_missing("cargo", Language::Ffi, is_available)?;
}
Ok(())
}
fn bail_if_missing(tool: &str, lang: Language, is_available: &dyn Fn(&str) -> bool) -> anyhow::Result<()> {
if !is_available(tool) {
anyhow::bail!(
"{tool} not found on PATH; {lang} is enabled for this crate and requires it -- install \
{tool}, or remove {lang} from this crate's languages if it should not be enabled"
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn tools() -> ToolsConfig {
ToolsConfig::default()
}
#[test]
fn bails_when_a_required_tool_is_missing_for_rust() {
let error =
enforce_required_toolchains_with(ToolchainCommand::Build, &[Language::Rust], &tools(), &|_tool| false)
.expect_err("a missing cargo must fail, not report clean");
assert!(error.to_string().contains("cargo"), "{error}");
}
#[test]
fn bails_when_a_required_tool_is_missing_for_python() {
let error =
enforce_required_toolchains_with(ToolchainCommand::Build, &[Language::Python], &tools(), &|_tool| false)
.expect_err("a missing python package manager must fail, not report clean");
assert!(error.to_string().contains("uv"), "{error}");
}
#[test]
fn bails_when_a_required_tool_is_missing_for_ruby() {
let error =
enforce_required_toolchains_with(ToolchainCommand::Test, &[Language::Ruby], &tools(), &|_tool| false)
.expect_err("a missing ruby must fail, not report clean");
assert!(error.to_string().contains("ruby"), "{error}");
}
#[test]
fn bails_for_ruby_when_only_the_interpreter_is_present_and_bundler_is_not() {
let error = enforce_required_toolchains_with(ToolchainCommand::Test, &[Language::Ruby], &tools(), &|tool| {
tool == "ruby"
})
.expect_err("a present ruby with no bundler must still fail");
assert!(error.to_string().contains("bundle"), "{error}");
}
#[test]
fn test_does_not_require_cargo_upgrade_for_rust() {
enforce_required_toolchains_with(ToolchainCommand::Test, &[Language::Rust], &tools(), &|tool| {
tool == "cargo"
})
.expect("alef test never runs `cargo upgrade`; a missing cargo-upgrade must not fail it");
}
#[test]
fn build_setup_and_clean_do_not_require_cargo_upgrade_for_rust() {
for command in [
ToolchainCommand::Build,
ToolchainCommand::Setup,
ToolchainCommand::Clean,
] {
enforce_required_toolchains_with(command, &[Language::Rust], &tools(), &|tool| tool == "cargo")
.unwrap_or_else(|error| panic!("{command:?} must not require cargo-upgrade: {error}"));
}
}
#[test]
fn update_still_hard_fails_without_cargo_upgrade_for_rust() {
let error = enforce_required_toolchains_with(ToolchainCommand::Update, &[Language::Rust], &tools(), &|tool| {
tool == "cargo"
})
.expect_err("alef update's Rust arm runs `cargo upgrade`; a missing cargo-upgrade must still fail it");
assert!(error.to_string().contains("cargo-upgrade"), "{error}");
}
#[test]
fn bails_on_the_missing_cargo_edit_subcommand_for_rust_update() {
let error = enforce_required_toolchains_with(ToolchainCommand::Update, &[Language::Rust], &tools(), &|tool| {
tool == "cargo"
})
.expect_err("a missing cargo-upgrade (cargo-edit) must fail even when cargo itself is present");
assert!(error.to_string().contains("cargo-upgrade"), "{error}");
}
#[test]
fn does_not_require_a_tool_for_a_language_that_is_not_enabled() {
enforce_required_toolchains_with(ToolchainCommand::Build, &[Language::Python], &tools(), &|_tool| false)
.expect_err("python is enabled, so its own probe must still bail");
enforce_required_toolchains_with(ToolchainCommand::Build, &[], &tools(), &|_tool| false)
.expect("no languages enabled means no toolchain is required");
enforce_required_toolchains_with(ToolchainCommand::Build, &[Language::Python], &tools(), &|tool| {
tool == "uv"
})
.expect("a crate that does not enable ruby must not require ruby or bundler");
}
#[test]
fn all_does_not_require_dart_even_when_enabled() {
enforce_required_toolchains_for_all_with(&[Language::Dart], &|_tool| false)
.expect("alef all never shells out to `dart` directly; it must not require it");
}
#[test]
fn all_requires_cargo_when_ffi_is_enabled() {
let error = enforce_required_toolchains_for_all_with(&[Language::Ffi], &|_tool| false)
.expect_err("a missing cargo must fail alef all when ffi is enabled");
assert!(error.to_string().contains("cargo"), "{error}");
}
#[test]
fn all_requires_cargo_when_swift_is_enabled() {
let error = enforce_required_toolchains_for_all_with(&[Language::Swift], &|_tool| false)
.expect_err("a missing cargo must fail alef all when swift is enabled");
assert!(error.to_string().contains("cargo"), "{error}");
}
#[test]
fn all_requires_nothing_for_a_crate_with_neither_ffi_nor_swift() {
enforce_required_toolchains_for_all_with(&[Language::Python, Language::Node], &|_tool| false)
.expect("alef all needs no external tool for a crate with neither ffi nor swift enabled");
}
}