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 default = DefaultDriver::resolve(plan_set);
for plan in &mut plan_set.plans {
let fallback = default.for_target(&plan.target);
for pp in &mut plan.package_plans {
pp.driver = finalize_driver(pp.driver.as_deref(), fallback)?;
}
}
Ok(())
}
struct DefaultDriver {
host: Option<TargetTriple>,
cross_driver: Option<String>,
}
impl DefaultDriver {
fn resolve(plan_set: &ExecutionPlanSet<'_>) -> Self {
let mut default = Self {
host: plan_set.host.clone(),
cross_driver: None,
};
let needs_cross_default = plan_set.plans.iter().any(|plan| {
default.needs_cross_driver(&plan.target)
&& plan.package_plans.iter().any(|pp| pp.driver.is_none())
});
if needs_cross_default {
default.cross_driver = match probe_driver(CROSS_TARGET_DRIVER) {
Ok(()) => Some(CROSS_TARGET_DRIVER.to_string()),
Err(failure) => {
let detail = failure
.detail
.map(|detail| format!("\n{detail}"))
.unwrap_or_default();
print_warning!(
"build driver `{CROSS_TARGET_DRIVER}` was selected automatically for a cross-target run but {}; using plain cargo{detail}",
failure.reason
);
None
}
};
}
default
}
fn needs_cross_driver(&self, target: &TargetTriple) -> bool {
self.host
.as_ref()
.is_some_and(|host| crate::target::is_cross(host, target))
}
fn for_target(&self, target: &TargetTriple) -> Option<&str> {
if self.needs_cross_driver(target) {
self.cross_driver.as_deref()
} else {
None
}
}
}
struct ProbeFailure {
reason: String,
detail: Option<String>,
}
fn probe_driver(driver: &str) -> Result<(), ProbeFailure> {
match Command::new(driver)
.arg("--version")
.stdin(Stdio::null())
.output()
{
Ok(output) if output.status.success() => Ok(()),
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let stderr = stderr.trim();
Err(ProbeFailure {
reason: format!("`{driver} --version` failed with {}", output.status),
detail: (!stderr.is_empty()).then(|| stderr.to_string()),
})
}
Err(err) if err.kind() == io::ErrorKind::NotFound => Err(ProbeFailure {
reason: "was not found".to_string(),
detail: None,
}),
Err(err) => Err(ProbeFailure {
reason: format!("could not be probed: {err}"),
detail: None,
}),
}
}
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, probe_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 {
host: target::detect_host(&DriverTestEnv { host: Some("host") }),
cross_driver: None,
};
assert!(!default.needs_cross_driver(&TargetTriple("host".to_string())));
}
#[test]
fn default_driver_is_per_target_not_per_run() {
let default = DefaultDriver {
host: target::detect_host(&DriverTestEnv { host: Some("host") }),
cross_driver: Some("cargo-zigbuild".to_string()),
};
assert_eq!(default.for_target(&TargetTriple("host".to_string())), None);
assert_eq!(
default.for_target(&TargetTriple("wasm".to_string())),
Some("cargo-zigbuild")
);
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 {
host: target::detect_host(&DriverTestEnv { host: None }),
cross_driver: Some("cargo-zigbuild".to_string()),
};
assert!(!default.needs_cross_driver(&TargetTriple("wasm".to_string())));
assert_eq!(default.for_target(&TargetTriple("wasm".to_string())), None);
}
#[test]
fn probe_driver_accepts_working_driver() {
assert!(probe_driver("rustc").is_ok());
}
#[test]
fn probe_driver_reports_missing_driver() {
let failure = probe_driver("cargo-fc-test-missing-driver").unwrap_err();
assert_eq!(failure.reason, "was not found");
assert!(failure.detail.is_none());
}
#[cfg(unix)]
#[test]
fn probe_driver_rejects_failing_version_probe() -> eyre::Result<()> {
use std::os::unix::fs::PermissionsExt as _;
let dir = assert_fs::TempDir::new()?;
let script = dir.path().join("broken-driver");
std::fs::write(
&script,
"#!/bin/sh\necho 'shim: no version set' >&2\nexit 3\n",
)?;
let mut permissions = std::fs::metadata(&script)?.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&script, permissions)?;
let failure = probe_driver(&script.to_string_lossy()).unwrap_err();
assert!(failure.reason.contains("failed with"), "{}", failure.reason);
assert_eq!(failure.detail.as_deref(), Some("shim: no version set"));
Ok(())
}
}