use std::collections::{BTreeMap, HashMap};
use std::env;
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;
use anyhow::{Context, Result};
use os_str_bytes::OsStrBytesExt;
use crate::CargoCommandExt;
use crate::cargo_cmd::{CargoBinary, CargoCmd as _, find_cargo, merge_env};
use crate::cli::{Args, Warning};
#[derive(Clone)]
pub struct Command {
cargo: CargoBinary,
args: Vec<OsString>,
inherit_envs: bool,
inherit_cargo_envs: bool,
envs: BTreeMap<OsString, Option<OsString>>,
current_dir: Option<PathBuf>,
}
impl Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let args = self.build_args_infallible();
let mut cmd = self.command();
cmd.populate_from_args(&args, false);
write!(f, "env ")?;
if let Some(current_dir) = &self.current_dir {
write!(f, "-C {current_dir:?} ")?;
}
if !self.inherit_envs {
write!(f, "-i ")?;
}
for (k, v) in cmd.get_envs() {
if v.is_none() {
write!(f, "-u {} ", k.to_string_lossy())?;
}
}
for (k, v) in cmd.get_envs() {
if let Some(v) = v {
write!(f, "{}={:?} ", k.to_string_lossy(), v)?;
}
}
write!(f, "{:?} ", self.get_program())?;
for arg in &self.args {
write!(f, "{arg:?} ")?;
}
writeln!(f)
}
}
impl Command {
pub(crate) fn new() -> Result<Self> {
let cargo = find_cargo()?;
Ok(Self {
cargo,
args: Vec::new(),
envs: BTreeMap::new(),
inherit_envs: true,
inherit_cargo_envs: true,
current_dir: None,
})
}
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.args.push(arg.as_ref().to_os_string());
self
}
pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
for arg in args {
self.arg(arg);
}
self
}
pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
self.current_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
self.envs
.insert(key.as_ref().to_owned(), Some(value.as_ref().to_owned()));
self
}
pub fn env_clear(&mut self) -> &mut Self {
self.inherit_envs = false;
self.envs.clear();
self
}
pub fn env_clear_cargo(&mut self) -> &mut Self {
self.inherit_cargo_envs = false;
self.envs.retain(|k, _| should_preserve_cargo_env(k));
self
}
#[doc(hidden)]
#[deprecated(note = "use `env_clear_cargo` instead")]
pub fn env_clear_cargo_vars(&mut self) -> &mut Self {
self.env_clear_cargo()
}
pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
self.envs.insert(key.as_ref().to_owned(), None);
self
}
pub fn envs(
&mut self,
envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
) -> &mut Self {
for (k, v) in envs {
self.env(k, v);
}
self
}
pub fn get_args(&'_ self) -> impl Iterator<Item = &OsStr> {
self.args.iter().map(|s| s.as_os_str())
}
pub fn get_current_dir(&self) -> Option<&Path> {
self.current_dir.as_deref()
}
pub fn get_envs(&'_ self) -> impl Iterator<Item = (&OsStr, Option<&OsStr>)> {
self.envs.iter().map(|(k, v)| (k.as_os_str(), v.as_deref()))
}
fn base_env(&self) -> impl Iterator<Item = (OsString, OsString)> {
env::vars_os().filter(|(k, _)| {
if !self.inherit_envs {
false
} else if !self.inherit_cargo_envs {
should_preserve_cargo_env(k)
} else {
true
}
})
}
fn resolve_env(&self) -> HashMap<OsString, OsString> {
merge_env(self.base_env(), self.get_envs())
}
fn command(&self) -> StdCommand {
let mut command = self.cargo.command();
let args: Vec<_> = self.get_args().map(|a| a.to_owned()).collect();
let mut skip_next = false;
for arg in args.iter() {
if skip_next {
skip_next = false;
continue;
}
let arg_str = arg.to_string_lossy();
if arg_str == "--target" || arg_str == "--target-dir" {
skip_next = true; continue;
}
if arg_str.starts_with("--target=") || arg_str.starts_with("--target-dir=") {
continue;
}
if arg_str == "--host" {
skip_next = true;
continue;
}
if arg_str.starts_with("--host=") {
continue;
}
if arg_str.starts_with("--with-guest-capi") {
continue;
}
command.arg(arg);
}
if let Some(cwd) = &self.current_dir {
command.current_dir(cwd);
}
if !self.inherit_envs {
command.env_clear();
}
if !self.inherit_cargo_envs {
for (k, _) in env::vars_os() {
if !should_preserve_cargo_env(&k) {
command.env_remove(k);
}
}
}
if let Some(rustup_toolchain) = &self.cargo.rustup_toolchain {
command.env("RUSTUP_TOOLCHAIN", rustup_toolchain);
}
for (k, v) in self.get_envs() {
match v {
Some(v) => command.env(k, v),
None => command.env_remove(k),
};
}
command
}
pub fn get_program(&self) -> &OsStr {
self.cargo.path.as_os_str()
}
pub fn build_args(&self) -> Args {
match Args::parse(
self.get_args(),
self.resolve_env(),
self.get_current_dir(),
Warning::WARN,
) {
Ok(args) => args,
}
}
fn build_args_infallible(&self) -> Args {
match Args::parse(
self.get_args(),
self.resolve_env(),
self.get_current_dir(),
Warning::IGNORE,
) {
Ok(args) => args,
Err(err) => {
eprintln!("Failed to parse arguments: {err}");
std::process::exit(1);
}
}
}
pub fn status(&self) -> anyhow::Result<()> {
let args = self.build_args();
args.prepare_sysroot()
.context("Failed to prepare sysroot")?;
self.command()
.populate_from_args(&args, false)
.checked_status()
.context("Failed to execute cargo")?;
Ok(())
}
}
fn should_preserve_cargo_env(key: &OsStr) -> bool {
!key.starts_with("CARGO_")
|| key == "CARGO_HOME"
|| key.starts_with("CARGO_REGISTRIES_")
|| key.starts_with("CARGO_REGISTRY_")
|| key.starts_with("CARGO_HTTP_")
|| key.starts_with("CARGO_NET_")
|| key.starts_with("CARGO_ALIAS_")
|| key.starts_with("CARGO_TERM_")
}