use std::{ borrow::Cow, collections::HashMap, env, ffi::{OsStr, OsString}, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}, sync::RwLock, }; use crate::{ command_helpers::{run_output, CargoOutput}, run, tempfile::NamedTempfile, Error, ErrorKind, OutputKind, }; /// Configuration used to represent an invocation of a C compiler. /// /// This can be used to figure out what compiler is in use, what the arguments /// to it are, and what the environment variables look like for the compiler. /// This can be used to further configure other build systems (e.g. forward /// along CC and/or CFLAGS) or the `to_command` method can be used to run the /// compiler itself. #[derive(Clone, Debug)] #[allow(missing_docs)] pub struct Tool { pub(crate) path: PathBuf, pub(crate) cc_wrapper_path: Option, pub(crate) cc_wrapper_args: Vec, pub(crate) args: Vec, pub(crate) env: Vec<(OsString, OsString)>, pub(crate) family: ToolFamily, pub(crate) cuda: bool, pub(crate) removed_args: Vec, pub(crate) has_internal_target_arg: bool, } impl Tool { pub(crate) fn new( path: PathBuf, cached_compiler_family: &RwLock, ToolFamily>>, cargo_output: &CargoOutput, out_dir: Option<&Path>, ) -> Self { Self::with_features( path, None, false, cached_compiler_family, cargo_output, out_dir, ) } pub(crate) fn with_clang_driver( path: PathBuf, clang_driver: Option<&str>, cached_compiler_family: &RwLock, ToolFamily>>, cargo_output: &CargoOutput, out_dir: Option<&Path>, ) -> Self { Self::with_features( path, clang_driver, false, cached_compiler_family, cargo_output, out_dir, ) } /// Explicitly set the `ToolFamily`, skipping name-based detection. pub(crate) fn with_family(path: PathBuf, family: ToolFamily) -> Self { Self { path, cc_wrapper_path: None, cc_wrapper_args: Vec::new(), args: Vec::new(), env: Vec::new(), family, cuda: false, removed_args: Vec::new(), has_internal_target_arg: false, } } pub(crate) fn with_features( path: PathBuf, clang_driver: Option<&str>, cuda: bool, cached_compiler_family: &RwLock, ToolFamily>>, cargo_output: &CargoOutput, out_dir: Option<&Path>, ) -> Self { fn is_zig_cc(path: &Path, cargo_output: &CargoOutput) -> bool { run_output( Command::new(path).arg("--version"), path, // tool detection issues should always be shown as warnings cargo_output, ) .map(|o| String::from_utf8_lossy(&o).contains("ziglang")) .unwrap_or_default() || { match path.file_name().map(OsStr::to_string_lossy) { Some(fname) => fname.contains("zig"), _ => false, } } } fn guess_family_from_stdout( stdout: &str, path: &Path, cargo_output: &CargoOutput, ) -> Result { cargo_output.print_debug(&stdout); // https://gitlab.kitware.com/cmake/cmake/-/blob/69a2eeb9dff5b60f2f1e5b425002a0fd45b7cadb/Modules/CMakeDetermineCompilerId.cmake#L267-271 // stdin is set to null to ensure that the help output is never paginated. let accepts_cl_style_flags = run(Command::new(path).arg("-?").stdin(Stdio::null()), path, &{ // the errors are not errors! let mut cargo_output = cargo_output.clone(); cargo_output.warnings = cargo_output.debug; cargo_output.output = OutputKind::Discard; cargo_output }) .is_ok(); let clang = stdout.contains(r#""clang""#); let gcc = stdout.contains(r#""gcc""#); let emscripten = stdout.contains(r#""emscripten""#); let vxworks = stdout.contains(r#""VxWorks""#); match (clang, accepts_cl_style_flags, gcc, emscripten, vxworks) { (clang_cl, true, _, false, false) => Ok(ToolFamily::Msvc { clang_cl }), (true, _, _, _, false) | (_, _, _, true, false) => Ok(ToolFamily::Clang { zig_cc: is_zig_cc(path, cargo_output), }), (false, false, true, _, false) | (_, _, _, _, true) => Ok(ToolFamily::Gnu), (false, false, false, false, false) => { cargo_output.print_warning(&"Compiler family detection failed since it does not define `__clang__`, `__GNUC__`, `__EMSCRIPTEN__` or `__VXWORKS__`, also does not accept cl style flag `-?`, fallback to treating it as GNU"); Err(Error::new( ErrorKind::ToolFamilyMacroNotFound, "Expects macro `__clang__`, `__GNUC__` or `__EMSCRIPTEN__`, `__VXWORKS__` or accepts cl style flag `-?`, but found none", )) } } } fn detect_family_inner( path: &Path, cargo_output: &CargoOutput, out_dir: Option<&Path>, ) -> Result { let out_dir = out_dir .map(Cow::Borrowed) .unwrap_or_else(|| Cow::Owned(env::temp_dir())); // Ensure all the parent directories exist otherwise temp file creation // will fail std::fs::create_dir_all(&out_dir).map_err(|err| Error { kind: ErrorKind::IOError, message: format!("failed to create OUT_DIR '{}': {}", out_dir.display(), err) .into(), })?; let mut tmp = NamedTempfile::new(&out_dir, "detect_compiler_family.c").map_err(|err| Error { kind: ErrorKind::IOError, message: format!( "failed to create detect_compiler_family.c temp file in '{}': {}", out_dir.display(), err ) .into(), })?; let mut tmp_file = tmp.take_file().unwrap(); tmp_file.write_all(include_bytes!("detect_compiler_family.c"))?; // Close the file handle *now*, otherwise the compiler may fail to open it on Windows // (#1082). The file stays on disk and its path remains valid until `tmp` is dropped. tmp_file.flush()?; tmp_file.sync_data()?; drop(tmp_file); // When expanding the file, the compiler prints a lot of information to stderr // that it is not an error, but related to expanding itself. // // cc would have to disable warning here to prevent generation of too many warnings. let mut compiler_detect_output = cargo_output.clone(); compiler_detect_output.warnings = compiler_detect_output.debug; let stdout = run_output( Command::new(path).arg("-E").arg(tmp.path()), path, &compiler_detect_output, )?; let stdout = String::from_utf8_lossy(&stdout); if stdout.contains("-Wslash-u-filename") { let stdout = run_output( Command::new(path).arg("-E").arg("--").arg(tmp.path()), path, &compiler_detect_output, )?; let stdout = String::from_utf8_lossy(&stdout); guess_family_from_stdout(&stdout, path, cargo_output) } else { guess_family_from_stdout(&stdout, path, cargo_output) } } let detect_family = |path: &Path| -> Result { if let Some(family) = cached_compiler_family.read().unwrap().get(path) { return Ok(*family); } let family = detect_family_inner(path, cargo_output, out_dir)?; cached_compiler_family .write() .unwrap() .insert(path.into(), family); Ok(family) }; let family = detect_family(&path).unwrap_or_else(|e| { cargo_output.print_warning(&format_args!( "Compiler family detection failed due to error: {}", e )); match path.file_name().map(OsStr::to_string_lossy) { Some(fname) if fname.contains("clang-cl") => ToolFamily::Msvc { clang_cl: true }, Some(fname) if fname.ends_with("cl") || fname == "cl.exe" => { ToolFamily::Msvc { clang_cl: false } } Some(fname) if fname.contains("clang") => match clang_driver { Some("cl") => ToolFamily::Msvc { clang_cl: true }, _ => ToolFamily::Clang { zig_cc: is_zig_cc(&path, cargo_output), }, }, Some(fname) if fname.contains("zig") => ToolFamily::Clang { zig_cc: true }, _ => ToolFamily::Gnu, } }); Tool { path, cc_wrapper_path: None, cc_wrapper_args: Vec::new(), args: Vec::new(), env: Vec::new(), family, cuda, removed_args: Vec::new(), has_internal_target_arg: false, } } /// Add an argument to be stripped from the final command arguments. pub(crate) fn remove_arg(&mut self, flag: OsString) { self.removed_args.push(flag); } /// Push an "exotic" flag to the end of the compiler's arguments list. /// /// Nvidia compiler accepts only the most common compiler flags like `-D`, /// `-I`, `-c`, etc. Options meant specifically for the underlying /// host C++ compiler have to be prefixed with `-Xcompiler`. /// [Another possible future application for this function is passing /// clang-specific flags to clang-cl, which otherwise accepts only /// MSVC-specific options.] pub(crate) fn push_cc_arg(&mut self, flag: OsString) { if self.cuda { self.args.push("-Xcompiler".into()); } self.args.push(flag); } /// Checks if an argument or flag has already been specified or conflicts. /// /// Currently only checks optimization flags. pub(crate) fn is_duplicate_opt_arg(&self, flag: &OsString) -> bool { let flag = flag.to_str().unwrap(); let mut chars = flag.chars(); // Only duplicate check compiler flags if self.is_like_msvc() { if chars.next() != Some('/') { return false; } } else if (self.is_like_gnu() || self.is_like_clang()) && chars.next() != Some('-') { return false; } // Check for existing optimization flags (-O, /O) if chars.next() == Some('O') { return self .args() .iter() .any(|a| a.to_str().unwrap_or("").chars().nth(1) == Some('O')); } // TODO Check for existing -m..., -m...=..., /arch:... flags false } /// Don't push optimization arg if it conflicts with existing args. pub(crate) fn push_opt_unless_duplicate(&mut self, flag: OsString) { if self.is_duplicate_opt_arg(&flag) { eprintln!("Info: Ignoring duplicate arg {:?}", &flag); } else { self.push_cc_arg(flag); } } /// Converts this compiler into a `Command` that's ready to be run. /// /// This is useful for when the compiler needs to be executed and the /// command returned will already have the initial arguments and environment /// variables configured. pub fn to_command(&self) -> Command { let mut cmd = match self.cc_wrapper_path { Some(ref cc_wrapper_path) => { let mut cmd = Command::new(cc_wrapper_path); cmd.arg(&self.path); cmd } None => Command::new(&self.path), }; cmd.args(&self.cc_wrapper_args); let value = self .args .iter() .filter(|a| !self.removed_args.contains(a)) .collect::>(); cmd.args(&value); for (k, v) in self.env.iter() { cmd.env(k, v); } cmd } /// Returns the path for this compiler. /// /// Note that this may not be a path to a file on the filesystem, e.g. "cc", /// but rather something which will be resolved when a process is spawned. pub fn path(&self) -> &Path { &self.path } /// Returns the default set of arguments to the compiler needed to produce /// executables for the target this compiler generates. pub fn args(&self) -> &[OsString] { &self.args } /// Returns the set of environment variables needed for this compiler to /// operate. /// /// This is typically only used for MSVC compilers currently. pub fn env(&self) -> &[(OsString, OsString)] { &self.env } /// Returns the compiler command in format of CC environment variable. /// Or empty string if CC env was not present /// /// This is typically used by configure script pub fn cc_env(&self) -> OsString { match self.cc_wrapper_path { Some(ref cc_wrapper_path) => { let mut cc_env = cc_wrapper_path.as_os_str().to_owned(); cc_env.push(" "); cc_env.push(self.path.to_path_buf().into_os_string()); for arg in self.cc_wrapper_args.iter() { cc_env.push(" "); cc_env.push(arg); } cc_env } None => OsString::from(""), } } /// Returns the compiler flags in format of CFLAGS environment variable. /// Important here - this will not be CFLAGS from env, its internal gcc's flags to use as CFLAGS /// This is typically used by configure script pub fn cflags_env(&self) -> OsString { let mut flags = OsString::new(); for (i, arg) in self.args.iter().enumerate() { if i > 0 { flags.push(" "); } flags.push(arg); } flags } /// Whether the tool is GNU Compiler Collection-like. pub fn is_like_gnu(&self) -> bool { self.family == ToolFamily::Gnu } /// Whether the tool is Clang-like. pub fn is_like_clang(&self) -> bool { matches!(self.family, ToolFamily::Clang { .. }) } /// Whether the tool is AppleClang under .xctoolchain #[cfg(target_vendor = "apple")] pub(crate) fn is_xctoolchain_clang(&self) -> bool { let path = self.path.to_string_lossy(); path.contains(".xctoolchain/") } #[cfg(not(target_vendor = "apple"))] pub(crate) fn is_xctoolchain_clang(&self) -> bool { false } /// Whether the tool is MSVC-like. pub fn is_like_msvc(&self) -> bool { matches!(self.family, ToolFamily::Msvc { .. }) } /// Whether the tool is `clang-cl`-based MSVC-like. pub fn is_like_clang_cl(&self) -> bool { matches!(self.family, ToolFamily::Msvc { clang_cl: true }) } /// Supports using `--` delimiter to separate arguments and path to source files. pub(crate) fn supports_path_delimiter(&self) -> bool { // homebrew clang and zig-cc does not support this while stock version does matches!(self.family, ToolFamily::Msvc { clang_cl: true }) && !self.cuda } } /// Represents the family of tools this tool belongs to. /// /// Each family of tools differs in how and what arguments they accept. /// /// Detection of a family is done on best-effort basis and may not accurately reflect the tool. #[derive(Copy, Clone, Debug, PartialEq)] pub enum ToolFamily { /// Tool is GNU Compiler Collection-like. Gnu, /// Tool is Clang-like. It differs from the GCC in a sense that it accepts superset of flags /// and its cross-compilation approach is different. Clang { zig_cc: bool }, /// Tool is the MSVC cl.exe. Msvc { clang_cl: bool }, } impl ToolFamily { /// What the flag to request debug info for this family of tools look like pub(crate) fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option) { match *self { ToolFamily::Msvc { .. } => { cmd.push_cc_arg("-Z7".into()); } ToolFamily::Gnu | ToolFamily::Clang { .. } => { cmd.push_cc_arg( dwarf_version .map_or_else(|| "-g".into(), |v| format!("-gdwarf-{}", v)) .into(), ); } } } /// What the flag to force frame pointers. pub(crate) fn add_force_frame_pointer(&self, cmd: &mut Tool) { match *self { ToolFamily::Gnu | ToolFamily::Clang { .. } => { cmd.push_cc_arg("-fno-omit-frame-pointer".into()); } _ => (), } } /// What the flags to enable all warnings pub(crate) fn warnings_flags(&self) -> &'static str { match *self { ToolFamily::Msvc { .. } => "-W4", ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Wall", } } /// What the flags to enable extra warnings pub(crate) fn extra_warnings_flags(&self) -> Option<&'static str> { match *self { ToolFamily::Msvc { .. } => None, ToolFamily::Gnu | ToolFamily::Clang { .. } => Some("-Wextra"), } } /// What the flag to turn warning into errors pub(crate) fn warnings_to_errors_flag(&self) -> &'static str { match *self { ToolFamily::Msvc { .. } => "-WX", ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Werror", } } pub(crate) fn verbose_stderr(&self) -> bool { matches!(*self, ToolFamily::Clang { .. }) } }