use crate::plan::execution::ExecutionPlanSet;
use crate::print_warning;
use crate::target::TargetTriple;
use color_eyre::eyre;
use std::io;
use std::process::{Command, Stdio};
const CROSS_TARGET_DRIVER: &str = "cargo-zigbuild";
pub(crate) fn finalize_plan_drivers(plan_set: &mut ExecutionPlanSet) -> eyre::Result<()> {
let needs_default = plan_set
.plans
.iter()
.flat_map(|plan| &plan.package_plans)
.any(|pp| pp.driver.is_none());
let mut default = needs_default.then(|| DefaultDriver::new(plan_set.host.clone()));
for plan in &mut plan_set.plans {
let fallback = default
.as_mut()
.and_then(|default| default.for_target(&plan.target));
for pp in &mut plan.package_plans {
pp.driver = finalize_driver(pp.driver.as_deref(), fallback.as_deref())?;
}
}
Ok(())
}
struct DefaultDriver {
host: Option<TargetTriple>,
cross: CrossDriverProbe,
}
enum CrossDriverProbe {
Unprobed,
Available,
Missing,
}
impl DefaultDriver {
fn new(host: Option<TargetTriple>) -> Self {
Self {
host,
cross: CrossDriverProbe::Unprobed,
}
}
fn needs_cross_driver(&self, target: &TargetTriple) -> bool {
self.host.as_ref().is_some_and(|host| host != target)
}
fn for_target(&mut self, target: &TargetTriple) -> Option<String> {
if !self.needs_cross_driver(target) {
return None;
}
if matches!(self.cross, CrossDriverProbe::Unprobed) {
self.cross = if driver_is_available(CROSS_TARGET_DRIVER) {
CrossDriverProbe::Available
} else {
print_warning!(
"build driver `{CROSS_TARGET_DRIVER}` was selected automatically for a cross-target run but was not found; using plain cargo"
);
CrossDriverProbe::Missing
};
}
match self.cross {
CrossDriverProbe::Available => Some(CROSS_TARGET_DRIVER.to_string()),
CrossDriverProbe::Unprobed | CrossDriverProbe::Missing => None,
}
}
}
fn driver_is_available(driver: &str) -> bool {
match Command::new(driver)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
{
Ok(_) => true,
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => {
print_warning!("could not probe build driver `{driver}`: {err}; using plain cargo");
false
}
}
}
pub(crate) fn finalize_driver(
configured: Option<&str>,
default: Option<&str>,
) -> eyre::Result<Option<String>> {
match configured {
Some(driver) => normalize_driver(driver),
None => Ok(default.map(ToString::to_string)),
}
}
fn normalize_driver(driver: &str) -> eyre::Result<Option<String>> {
let driver = driver.trim();
if driver.is_empty() {
eyre::bail!("build driver (`--driver` or `driver`) must not be empty");
}
if driver == "cargo" {
Ok(None)
} else {
Ok(Some(driver.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::{DefaultDriver, finalize_driver};
use crate::target::{self, TargetTriple};
use color_eyre::eyre;
struct DriverTestEnv {
host: Option<&'static str>,
}
impl target::TargetEnvironment for DriverTestEnv {
fn cargo_build_target(&self) -> Option<String> {
None
}
fn host_target(&self) -> eyre::Result<target::TargetTriple> {
let Some(host) = self.host else {
eyre::bail!("host failed");
};
Ok(target::TargetTriple(host.to_string()))
}
}
#[test]
fn default_driver_is_plain_cargo_for_the_host_target() {
let default =
DefaultDriver::new(target::detect_host(&DriverTestEnv { host: Some("host") }));
assert!(!default.needs_cross_driver(&TargetTriple("host".to_string())));
}
#[test]
fn default_driver_is_per_target_not_per_run() {
let default =
DefaultDriver::new(target::detect_host(&DriverTestEnv { host: Some("host") }));
assert!(!default.needs_cross_driver(&TargetTriple("host".to_string())));
assert!(default.needs_cross_driver(&TargetTriple("wasm".to_string())));
}
#[test]
fn finalize_driver_treats_explicit_cargo_as_plain_cargo() -> eyre::Result<()> {
assert_eq!(
finalize_driver(Some("cargo"), Some("cargo-zigbuild"))?,
None
);
Ok(())
}
#[test]
fn finalize_driver_uses_explicit_custom_driver() -> eyre::Result<()> {
assert_eq!(
finalize_driver(Some("cross"), None)?,
Some("cross".to_string())
);
Ok(())
}
#[test]
fn finalize_driver_uses_default_only_when_unset() -> eyre::Result<()> {
assert_eq!(
finalize_driver(None, Some("cargo-zigbuild"))?,
Some("cargo-zigbuild".to_string())
);
assert_eq!(finalize_driver(None, None)?, None);
Ok(())
}
#[test]
fn finalize_driver_rejects_empty_driver() {
assert!(finalize_driver(Some(" "), None).is_err());
}
#[test]
fn default_driver_falls_back_to_plain_cargo_when_host_detection_fails() {
let default = DefaultDriver::new(target::detect_host(&DriverTestEnv { host: None }));
assert!(!default.needs_cross_driver(&TargetTriple("wasm".to_string())));
}
}