use std::collections::HashMap; use std::env; use std::fmt; use std::fs::File; use std::io::{self, Write}; use std::process::exit; use crate::flags::{FlagParseError, Flags, ParseOutcome}; use crate::rustc; use crate::util::*; #[derive(Debug)] pub(crate) enum OptionError { FlagError(FlagParseError), Generic(String), } impl fmt::Display for OptionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::FlagError(e) => write!(f, "error parsing flags: {e}"), Self::Generic(s) => write!(f, "{s}"), } } } #[derive(Debug)] pub(crate) struct Options { // Contains the path to the child executable pub(crate) executable: String, // Contains arguments for the child process fetched from files. pub(crate) child_arguments: Vec, // Contains environment variables for the child process fetched from files. pub(crate) child_environment: HashMap, // If set, create the specified file after the child process successfully // terminated its execution. pub(crate) touch_file: Option, // If set to (source, dest) copies the source file to dest. pub(crate) copy_output: Option<(String, String)>, // If set, redirects the child process stdout to this file. pub(crate) stdout_file: Option, // If set, redirects the child process stderr to this file. pub(crate) stderr_file: Option, // If set, also logs all unprocessed output from the rustc output to this file. // Meant to be used to get json output out of rustc for tooling usage. pub(crate) output_file: Option, // If set, it configures rustc to emit an rmeta file and then // quit. pub(crate) rustc_quit_on_rmeta: bool, // This controls the output format of rustc messages. pub(crate) rustc_output_format: Option, } pub(crate) fn options() -> Result { // Process argument list until -- is encountered. // Everything after is sent to the child process. let mut subst_mapping_raw = None; let mut stable_status_file_raw = None; let mut volatile_status_file_raw = None; let mut env_file_raw = None; let mut arg_file_raw = None; let mut touch_file = None; let mut copy_output_raw = None; let mut stdout_file = None; let mut stderr_file = None; let mut output_file = None; let mut rustc_quit_on_rmeta_raw = None; let mut rustc_output_format_raw = None; let mut flags = Flags::new(); flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw); flags.define_flag("--stable-status-file", "", &mut stable_status_file_raw); flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw); flags.define_repeated_flag( "--env-file", "File(s) containing environment variables to pass to the child process.", &mut env_file_raw, ); flags.define_repeated_flag( "--arg-file", "File(s) containing command line arguments to pass to the child process.", &mut arg_file_raw, ); flags.define_flag( "--touch-file", "Create this file after the child process runs successfully.", &mut touch_file, ); flags.define_repeated_flag("--copy-output", "", &mut copy_output_raw); flags.define_flag( "--stdout-file", "Redirect subprocess stdout in this file.", &mut stdout_file, ); flags.define_flag( "--stderr-file", "Redirect subprocess stderr in this file.", &mut stderr_file, ); flags.define_flag( "--output-file", "Log all unprocessed subprocess stderr in this file.", &mut output_file, ); flags.define_flag( "--rustc-quit-on-rmeta", "If enabled, this wrapper will terminate rustc after rmeta has been emitted.", &mut rustc_quit_on_rmeta_raw, ); flags.define_flag( "--rustc-output-format", "Controls the rustc output format if --rustc-quit-on-rmeta is set.\n\ 'json' will cause the json output to be output, \ 'rendered' will extract the rendered message and print that.\n\ Default: `rendered`", &mut rustc_output_format_raw, ); let mut child_args = match flags .parse(env::args().collect()) .map_err(OptionError::FlagError)? { ParseOutcome::Help(help) => { eprintln!("{help}"); exit(0); } ParseOutcome::Parsed(p) => p, }; let current_dir = std::env::current_dir() .map_err(|e| OptionError::Generic(format!("failed to get current directory: {e}")))? .to_str() .ok_or_else(|| OptionError::Generic("current directory not utf-8".to_owned()))? .to_owned(); let subst_mappings = subst_mapping_raw .unwrap_or_default() .into_iter() .map(|arg| { let (key, val) = arg.split_once('=').ok_or_else(|| { OptionError::Generic(format!("empty key for substitution '{arg}'")) })?; let v = if val == "${pwd}" { current_dir.as_str() } else { val } .to_owned(); Ok((key.to_owned(), v)) }) .collect::, OptionError>>()?; let stable_stamp_mappings = stable_status_file_raw.map_or_else(Vec::new, |s| read_stamp_status_to_array(s).unwrap()); let volatile_stamp_mappings = volatile_status_file_raw.map_or_else(Vec::new, |s| read_stamp_status_to_array(s).unwrap()); let environment_file_block = env_from_files(env_file_raw.unwrap_or_default())?; let mut file_arguments = args_from_file(arg_file_raw.unwrap_or_default())?; // Process --copy-output let copy_output = copy_output_raw .map(|co| { if co.len() != 2 { return Err(OptionError::Generic(format!( "\"--copy-output\" needs exactly 2 parameters, {} provided", co.len() ))); } let copy_source = &co[0]; let copy_dest = &co[1]; if copy_source == copy_dest { return Err(OptionError::Generic(format!( "\"--copy-output\" source ({copy_source}) and dest ({copy_dest}) need to be different.", ))); } Ok((copy_source.to_owned(), copy_dest.to_owned())) }) .transpose()?; let rustc_quit_on_rmeta = rustc_quit_on_rmeta_raw.map_or(false, |s| s == "true"); let rustc_output_format = rustc_output_format_raw .map(|v| match v.as_str() { "json" => Ok(rustc::ErrorFormat::Json), "rendered" => Ok(rustc::ErrorFormat::Rendered), _ => Err(OptionError::Generic(format!( "invalid --rustc-output-format '{v}'", ))), }) .transpose()?; // Prepare the environment variables, unifying those read from files with the ones // of the current process. let vars = environment_block( environment_file_block, &stable_stamp_mappings, &volatile_stamp_mappings, &subst_mappings, ); // Append all the arguments fetched from files to those provided via command line. child_args.append(&mut file_arguments); let child_args = prepare_args(child_args, &subst_mappings)?; // Split the executable path from the rest of the arguments. let (exec_path, args) = child_args.split_first().ok_or_else(|| { OptionError::Generic( "at least one argument after -- is required (the child process path)".to_owned(), ) })?; Ok(Options { executable: exec_path.to_owned(), child_arguments: args.to_vec(), child_environment: vars, touch_file, copy_output, stdout_file, stderr_file, output_file, rustc_quit_on_rmeta, rustc_output_format, }) } fn args_from_file(paths: Vec) -> Result, OptionError> { let mut args = vec![]; for path in paths.iter() { let mut lines = read_file_to_array(path).map_err(|err| { OptionError::Generic(format!( "{} while processing args from file paths: {:?}", err, &paths )) })?; args.append(&mut lines); } Ok(args) } fn env_from_files(paths: Vec) -> Result, OptionError> { let mut env_vars = HashMap::new(); for path in paths.into_iter() { let lines = read_file_to_array(&path).map_err(OptionError::Generic)?; for line in lines.into_iter() { let (k, v) = line .split_once('=') .ok_or_else(|| OptionError::Generic("environment file invalid".to_owned()))?; env_vars.insert(k.to_owned(), v.to_owned()); } } Ok(env_vars) } fn prepare_arg(mut arg: String, subst_mappings: &[(String, String)]) -> String { for (f, replace_with) in subst_mappings { let from = format!("${{{f}}}"); arg = arg.replace(&from, replace_with); } arg } /// Apply substitutions to the given param file. Returns the new filename. fn prepare_param_file( filename: &str, subst_mappings: &[(String, String)], ) -> Result { let expanded_file = format!("{filename}.expanded"); let format_err = |err: io::Error| { OptionError::Generic(format!( "{} writing path: {:?}, current directory: {:?}", err, expanded_file, std::env::current_dir() )) }; let mut out = io::BufWriter::new(File::create(&expanded_file).map_err(format_err)?); fn process_file( filename: &str, out: &mut io::BufWriter, subst_mappings: &[(String, String)], format_err: &impl Fn(io::Error) -> OptionError, ) -> Result<(), OptionError> { for arg in read_file_to_array(filename).map_err(OptionError::Generic)? { let arg = prepare_arg(arg, subst_mappings); if let Some(arg_file) = arg.strip_prefix('@') { process_file(arg_file, out, subst_mappings, format_err)?; } else { writeln!(out, "{arg}").map_err(format_err)?; } } Ok(()) } process_file(filename, &mut out, subst_mappings, &format_err)?; Ok(expanded_file) } /// Apply substitutions to the provided arguments, recursing into param files. fn prepare_args( args: Vec, subst_mappings: &[(String, String)], ) -> Result, OptionError> { args.into_iter() .map(|arg| { let arg = prepare_arg(arg, subst_mappings); if let Some(param_file) = arg.strip_prefix('@') { // Note that substitutions may also apply to the param file path! prepare_param_file(param_file, subst_mappings) .map(|filename| format!("@{filename}")) } else { Ok(arg) } }) .collect() } fn environment_block( environment_file_block: HashMap, stable_stamp_mappings: &[(String, String)], volatile_stamp_mappings: &[(String, String)], subst_mappings: &[(String, String)], ) -> HashMap { // Taking all environment variables from the current process // and sending them down to the child process let mut environment_variables: HashMap = std::env::vars().collect(); // Have the last values added take precedence over the first. // This is simpler than needing to track duplicates and explicitly override // them. environment_variables.extend(environment_file_block); for (f, replace_with) in &[stable_stamp_mappings, volatile_stamp_mappings].concat() { for value in environment_variables.values_mut() { let from = format!("{{{f}}}"); let new = value.replace(from.as_str(), replace_with); *value = new; } } for (f, replace_with) in subst_mappings { for value in environment_variables.values_mut() { let from = format!("${{{f}}}"); let new = value.replace(from.as_str(), replace_with); *value = new; } } environment_variables }