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()?); Ok(command.status()?) } #[derive(PartialEq, Eq, Clone, Debug)] pub struct CommandLine { /// If the command prefix contains a , or the -i option is present, or /// the special target .IGNORE has either the current target as a prerequisite or has /// no prerequisites, any error found while executing the command shall be ignored. ignore_errors: bool, /// If the command prefix contains an at-sign and the make utility command line -n /// option is not specified, or the -s option is present, or the special target /// .SILENT has either the current target as a prerequisite or has no prerequisites, /// the command shall not be written to standard output before it is executed. silent: bool, /// If the command prefix contains a , this indicates a makefile command /// line that shall be executed even if -n, -q, or -t is specified. always_execute: bool, execution_line: TokenString, } impl CommandLine { pub fn from(mut line: TokenString) -> Self { let mut ignore_errors = false; let mut silent = false; let mut always_execute = false; if let Token::Text(text) = line.first_token_mut() { let mut text_chars = text.chars().peekable(); while let Some(x) = text_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { '-' => ignore_errors = true, '@' => silent = true, '+' => always_execute = true, _ => unreachable!(), } } *text = text_chars.collect(); } Self { ignore_errors, silent, always_execute, 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| match x { Token::MacroExpansion { .. } => true, Token::FunctionCall { .. } => true, _ => false, }); // 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 = 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 self_ignore_errors = self.ignore_errors; let mut self_silent = self.silent; let mut self_always_execute = self.always_execute; // apparently some makefiles will just throw this shit in in macros? bruh moment tbh let execution_line: String = { let mut line_chars = execution_line.chars().peekable(); while let Some(x) = line_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { '-' => self_ignore_errors = true, '@' => self_silent = true, '+' => self_always_execute = true, _ => unreachable!(), } } line_chars.collect() }; let ignore_error = self_ignore_errors || file.args.ignore_errors || file.special_target_has_prereq(".IGNORE", &target.name); let silent = (self_silent && !file.args.dry_run) || file.args.silent || file.special_target_has_prereq(".SILENT", &target.name); if !silent { println!("{}", execution_line); } let should_execute = self_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 { if self.ignore_errors { write!(f, "-")?; } if self.silent { write!(f, "@")?; } if self.always_execute { write!(f, "+")?; } let execution_line = format!("{}", &self.execution_line); let execution_line = execution_line.replace("\n", "↵\n"); write!(f, "{}", execution_line)?; Ok(()) } }