use std::env; use std::fmt; use std::process::{Command, ExitStatus}; use eyre::{bail, Error}; #[cfg(feature = "full")] use lazy_static::lazy_static; #[cfg(feature = "full")] use regex::Regex; #[cfg(feature = "full")] use super::r#macro::Set as MacroSet; use super::target::Target; use super::token::{Token, TokenString}; use super::Makefile; // inspired by python's subprocess module fn execute_command_line( command_line: &str, ignore_errors: bool, #[cfg(feature = "full")] macros: &MacroSet, ) -> Result { let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); let args = vec!["/c", command_line]; (cmd, args) } else { let sh = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()); let args = if ignore_errors { vec!["-c", command_line] } else { vec!["-e", "-c", command_line] }; (sh, args) }; let mut command = Command::new(program); command.args(args); #[cfg(feature = "full")] command.envs(macros.resolve_exports::<&[u8]>(None)?); Ok(command.status()?) } #[derive(PartialEq, Eq, Clone, Debug)] pub struct CommandLine { execution_line: TokenString, } impl CommandLine { pub const fn from(line: TokenString) -> Self { Self { execution_line: line, } } pub fn execute(&self, file: &Makefile, target: &Target) -> eyre::Result<()> { let is_recursive = self.execution_line.tokens().any(|x| match x { Token::MacroExpansion { name, .. } => name == "MAKE", _ => false, }); log::trace!("executing {}", &self.execution_line); let execution_line = file.expand_macros(&self.execution_line, Some(target))?; #[cfg(feature = "full")] { let is_just_one_macro_expansion = self.execution_line.tokens().count() == 1 && self.execution_line.tokens().all(|x| { matches!(x, Token::MacroExpansion { .. } | Token::FunctionCall { .. }) }); // unfortunately, if we had a multiline macro somewhere with non-escaped newlines, now we have to run each of them as separate lines lazy_static! { static ref UNESCAPED_NEWLINE: Regex = #[allow(clippy::unwrap_used)] Regex::new(r"([^\\])\n").unwrap(); } if is_just_one_macro_expansion && UNESCAPED_NEWLINE.is_match(&execution_line) { let lines = UNESCAPED_NEWLINE .split(&execution_line) .map(|x| Self::from(TokenString::text(x.trim_start()))); for line in lines { line.execute(file, target)?; } return Ok(()); } } log::trace!("executing {}", &execution_line); let mut ignore_errors = false; let mut silent = false; let mut always_execute = false; // sometimes this is defined in macros rather than statically let execution_line: String = { let mut line_chars = execution_line .chars() .skip_while(char::is_ascii_whitespace) .peekable(); while let Some(x) = line_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { '-' => ignore_errors = true, '@' => silent = true, '+' => always_execute = true, _ => unreachable!(), } } line_chars.collect() }; let ignore_error = ignore_errors || file.args.ignore_errors || file.special_target_has_prereq(".IGNORE", &target.name); let silent = (silent && !file.args.dry_run) || file.args.silent || file.special_target_has_prereq(".SILENT", &target.name); if !silent { println!("{}", execution_line); } let should_execute = always_execute || is_recursive || !(file.args.dry_run || file.args.question || file.args.touch); if !should_execute { return Ok(()); } let return_value = execute_command_line( &execution_line, ignore_error, #[cfg(feature = "full")] &file.macros, ); let errored = return_value.map_or(true, |status| !status.success()); if errored { // apparently there was an error. do we care? if !ignore_error { bail!("error from command execution!"); } } Ok(()) } } impl fmt::Display for CommandLine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let execution_line = format!("{}", &self.execution_line); let execution_line = execution_line.replace("\n", "↵\n"); write!(f, "{}", execution_line)?; Ok(()) } }