use std::env;
use std::error::Error;
use std::fmt;
use std::path;
use std::process;
#[doc(inline)]
pub use crate::cargo_bin;
#[doc(inline)]
pub use crate::cargo_bin_cmd;
pub trait CommandCargoExt
where
Self: Sized,
{
fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError>;
}
impl CommandCargoExt for crate::cmd::Command {
fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError> {
crate::cmd::Command::cargo_bin(name)
}
}
impl CommandCargoExt for process::Command {
fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError> {
cargo_bin_cmd(name)
}
}
pub(crate) fn cargo_bin_cmd<S: AsRef<str>>(name: S) -> Result<process::Command, CargoError> {
let path = cargo_bin(name);
if path.is_file() {
if let Some(runner) = cargo_runner() {
let mut cmd = process::Command::new(&runner[0]);
cmd.args(&runner[1..]).arg(path);
Ok(cmd)
} else {
Ok(process::Command::new(path))
}
} else {
Err(CargoError::with_cause(NotFoundError { path }))
}
}
pub(crate) fn cargo_runner() -> Option<Vec<String>> {
let runner_env = format!(
"CARGO_TARGET_{}_RUNNER",
CURRENT_TARGET.replace('-', "_").to_uppercase()
);
let runner = env::var(runner_env).ok()?;
Some(runner.split(' ').map(str::to_string).collect())
}
#[derive(Debug)]
pub struct CargoError {
cause: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl CargoError {
pub fn with_cause<E>(cause: E) -> Self
where
E: Error + Send + Sync + 'static,
{
let cause = Box::new(cause);
Self { cause: Some(cause) }
}
}
impl Error for CargoError {}
impl fmt::Display for CargoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref cause) = self.cause {
writeln!(f, "Cause: {cause}")?;
}
Ok(())
}
}
#[derive(Debug)]
struct NotFoundError {
path: path::PathBuf,
}
impl Error for NotFoundError {}
impl fmt::Display for NotFoundError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Cargo command not found: {}", self.path.display())
}
}
pub fn cargo_bin<S: AsRef<str>>(name: S) -> path::PathBuf {
cargo_bin_str(name.as_ref())
}
fn cargo_bin_str(name: &str) -> path::PathBuf {
let env_var = format!("{CARGO_BIN_EXE_}{name}");
env::var_os(env_var)
.map(|p| p.into())
.or_else(|| legacy_cargo_bin(name))
.unwrap_or_else(|| missing_cargo_bin(name))
}
const CARGO_BIN_EXE_: &str = "CARGO_BIN_EXE_";
fn missing_cargo_bin(name: &str) -> ! {
let possible_names: Vec<_> = env::vars_os()
.filter_map(|(k, _)| k.into_string().ok())
.filter_map(|k| k.strip_prefix(CARGO_BIN_EXE_).map(|s| s.to_owned()))
.collect();
if possible_names.is_empty() {
panic!("`CARGO_BIN_EXE_{name}` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_{name}`")
} else {
let mut names = String::new();
for (i, name) in possible_names.iter().enumerate() {
use std::fmt::Write as _;
if i != 0 {
let _ = write!(&mut names, ", ");
}
let _ = write!(&mut names, "\"{name}\"");
}
panic!(
"`CARGO_BIN_EXE_{name}` is unset
help: available binary names are {names}"
)
}
}
fn legacy_cargo_bin(name: &str) -> Option<path::PathBuf> {
let target_dir = target_dir()?;
let bin_path = target_dir.join(format!("{}{}", name, env::consts::EXE_SUFFIX));
if !bin_path.exists() {
return None;
}
Some(bin_path)
}
fn target_dir() -> Option<path::PathBuf> {
let mut path = env::current_exe().ok()?;
let _test_bin_name = path.pop();
if path.ends_with("deps") {
let _deps = path.pop();
}
Some(path)
}
const CURRENT_TARGET: &str = include_str!(concat!(env!("OUT_DIR"), "/current_target.txt"));
#[test]
#[should_panic = "`CARGO_BIN_EXE_non-existent` is unset
help: if this is running within a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_non-existent`"]
fn cargo_bin_in_unit_test() {
cargo_bin("non-existent");
}