use std::collections::{BTreeMap, HashMap};
use std::convert::Infallible;
use std::ffi::{OsStr, OsString, c_char};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::Command as StdCommand;
use std::{env, iter};
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);
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();
command.args(self.get_args());
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()
}
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)
.checked_status()
.context("Failed to execute cargo")?;
Ok(())
}
pub fn exec(&self) -> ! {
match self.exec_impl() {
Err(e) => {
eprintln!("{e:?}");
std::process::exit(101);
}
}
}
fn exec_impl(&self) -> anyhow::Result<Infallible> {
let args = self.build_args();
args.prepare_sysroot()
.context("Failed to prepare sysroot")?;
let mut command = self.command();
command.populate_from_args(&args);
if let Some(cwd) = self.get_current_dir() {
env::set_current_dir(cwd).context("Failed to change current directory")?;
}
Ok(exec(
command.get_program(),
command.get_args(),
command.resolve_env(self.base_env()),
)?)
}
}
fn exec(
program: impl AsRef<OsStr>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
) -> std::io::Result<Infallible> {
let mut env_bytes = vec![];
let mut env_offsets = vec![];
for (k, v) in envs.into_iter() {
env_offsets.push(env_bytes.len());
env_bytes.extend_from_slice(k.as_ref().as_encoded_bytes());
env_bytes.push(b'=');
env_bytes.extend_from_slice(v.as_ref().as_encoded_bytes());
env_bytes.push(0);
}
let env_ptrs = env_offsets
.into_iter()
.map(|offset| env_bytes[offset..].as_ptr() as *const c_char)
.chain(iter::once(std::ptr::null()))
.collect::<Vec<_>>();
let mut arg_bytes = vec![];
let mut arg_offsets = vec![];
arg_offsets.push(arg_bytes.len());
arg_bytes.extend_from_slice(program.as_ref().as_encoded_bytes());
arg_bytes.push(0);
for arg in args {
arg_offsets.push(arg_bytes.len());
arg_bytes.extend_from_slice(arg.as_ref().as_encoded_bytes());
arg_bytes.push(0);
}
let arg_ptrs = arg_offsets
.into_iter()
.map(|offset| arg_bytes[offset..].as_ptr() as *const c_char)
.chain(iter::once(std::ptr::null()))
.collect::<Vec<_>>();
unsafe { libc::execvpe(arg_ptrs[0], arg_ptrs.as_ptr(), env_ptrs.as_ptr()) };
Err(std::io::Error::last_os_error())
}
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_")
}