| 1 | use std::{ |
| 2 | borrow::Cow, |
| 3 | collections::HashMap, |
| 4 | env, |
| 5 | ffi::{OsStr, OsString}, |
| 6 | io::Write, |
| 7 | path::{Path, PathBuf}, |
| 8 | process::Command, |
| 9 | sync::RwLock, |
| 10 | }; |
| 11 | |
| 12 | use crate::{ |
| 13 | command_helpers::{run_output, CargoOutput}, |
| 14 | run, |
| 15 | tempfile::NamedTempfile, |
| 16 | Error, ErrorKind, OutputKind, |
| 17 | }; |
| 18 | |
| 19 | /// Configuration used to represent an invocation of a C compiler. |
| 20 | /// |
| 21 | /// This can be used to figure out what compiler is in use, what the arguments |
| 22 | /// to it are, and what the environment variables look like for the compiler. |
| 23 | /// This can be used to further configure other build systems (e.g. forward |
| 24 | /// along CC and/or CFLAGS) or the `to_command` method can be used to run the |
| 25 | /// compiler itself. |
| 26 | #[derive (Clone, Debug)] |
| 27 | #[allow (missing_docs)] |
| 28 | pub struct Tool { |
| 29 | pub(crate) path: PathBuf, |
| 30 | pub(crate) cc_wrapper_path: Option<PathBuf>, |
| 31 | pub(crate) cc_wrapper_args: Vec<OsString>, |
| 32 | pub(crate) args: Vec<OsString>, |
| 33 | pub(crate) env: Vec<(OsString, OsString)>, |
| 34 | pub(crate) family: ToolFamily, |
| 35 | pub(crate) cuda: bool, |
| 36 | pub(crate) removed_args: Vec<OsString>, |
| 37 | pub(crate) has_internal_target_arg: bool, |
| 38 | } |
| 39 | |
| 40 | impl Tool { |
| 41 | pub(crate) fn new( |
| 42 | path: PathBuf, |
| 43 | cached_compiler_family: &RwLock<HashMap<Box<Path>, ToolFamily>>, |
| 44 | cargo_output: &CargoOutput, |
| 45 | out_dir: Option<&Path>, |
| 46 | ) -> Self { |
| 47 | Self::with_features( |
| 48 | path, |
| 49 | None, |
| 50 | false, |
| 51 | cached_compiler_family, |
| 52 | cargo_output, |
| 53 | out_dir, |
| 54 | ) |
| 55 | } |
| 56 | |
| 57 | pub(crate) fn with_clang_driver( |
| 58 | path: PathBuf, |
| 59 | clang_driver: Option<&str>, |
| 60 | cached_compiler_family: &RwLock<HashMap<Box<Path>, ToolFamily>>, |
| 61 | cargo_output: &CargoOutput, |
| 62 | out_dir: Option<&Path>, |
| 63 | ) -> Self { |
| 64 | Self::with_features( |
| 65 | path, |
| 66 | clang_driver, |
| 67 | false, |
| 68 | cached_compiler_family, |
| 69 | cargo_output, |
| 70 | out_dir, |
| 71 | ) |
| 72 | } |
| 73 | |
| 74 | /// Explicitly set the `ToolFamily`, skipping name-based detection. |
| 75 | pub(crate) fn with_family(path: PathBuf, family: ToolFamily) -> Self { |
| 76 | Self { |
| 77 | path, |
| 78 | cc_wrapper_path: None, |
| 79 | cc_wrapper_args: Vec::new(), |
| 80 | args: Vec::new(), |
| 81 | env: Vec::new(), |
| 82 | family, |
| 83 | cuda: false, |
| 84 | removed_args: Vec::new(), |
| 85 | has_internal_target_arg: false, |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | pub(crate) fn with_features( |
| 90 | path: PathBuf, |
| 91 | clang_driver: Option<&str>, |
| 92 | cuda: bool, |
| 93 | cached_compiler_family: &RwLock<HashMap<Box<Path>, ToolFamily>>, |
| 94 | cargo_output: &CargoOutput, |
| 95 | out_dir: Option<&Path>, |
| 96 | ) -> Self { |
| 97 | fn is_zig_cc(path: &Path, cargo_output: &CargoOutput) -> bool { |
| 98 | run_output( |
| 99 | Command::new(path).arg("--version" ), |
| 100 | path, |
| 101 | // tool detection issues should always be shown as warnings |
| 102 | cargo_output, |
| 103 | ) |
| 104 | .map(|o| String::from_utf8_lossy(&o).contains("ziglang" )) |
| 105 | .unwrap_or_default() |
| 106 | } |
| 107 | |
| 108 | fn detect_family_inner( |
| 109 | path: &Path, |
| 110 | cargo_output: &CargoOutput, |
| 111 | out_dir: Option<&Path>, |
| 112 | ) -> Result<ToolFamily, Error> { |
| 113 | let out_dir = out_dir |
| 114 | .map(Cow::Borrowed) |
| 115 | .unwrap_or_else(|| Cow::Owned(env::temp_dir())); |
| 116 | |
| 117 | // Ensure all the parent directories exist otherwise temp file creation |
| 118 | // will fail |
| 119 | std::fs::create_dir_all(&out_dir).map_err(|err| Error { |
| 120 | kind: ErrorKind::IOError, |
| 121 | message: format!("failed to create OUT_DIR ' {}': {}" , out_dir.display(), err) |
| 122 | .into(), |
| 123 | })?; |
| 124 | |
| 125 | let mut tmp = |
| 126 | NamedTempfile::new(&out_dir, "detect_compiler_family.c" ).map_err(|err| Error { |
| 127 | kind: ErrorKind::IOError, |
| 128 | message: format!( |
| 129 | "failed to create detect_compiler_family.c temp file in ' {}': {}" , |
| 130 | out_dir.display(), |
| 131 | err |
| 132 | ) |
| 133 | .into(), |
| 134 | })?; |
| 135 | let mut tmp_file = tmp.take_file().unwrap(); |
| 136 | tmp_file.write_all(include_bytes!("detect_compiler_family.c" ))?; |
| 137 | // Close the file handle *now*, otherwise the compiler may fail to open it on Windows |
| 138 | // (#1082). The file stays on disk and its path remains valid until `tmp` is dropped. |
| 139 | tmp_file.flush()?; |
| 140 | tmp_file.sync_data()?; |
| 141 | drop(tmp_file); |
| 142 | |
| 143 | let stdout = run_output( |
| 144 | Command::new(path).arg("-E" ).arg(tmp.path()), |
| 145 | path, |
| 146 | // When expanding the file, the compiler prints a lot of information to stderr |
| 147 | // that it is not an error, but related to expanding itself. |
| 148 | // |
| 149 | // cc would have to disable warning here to prevent generation of too many warnings. |
| 150 | &{ |
| 151 | let mut cargo_output = cargo_output.clone(); |
| 152 | cargo_output.warnings = cargo_output.debug; |
| 153 | cargo_output |
| 154 | }, |
| 155 | )?; |
| 156 | let stdout = String::from_utf8_lossy(&stdout); |
| 157 | |
| 158 | cargo_output.print_debug(&stdout); |
| 159 | |
| 160 | // https://gitlab.kitware.com/cmake/cmake/-/blob/69a2eeb9dff5b60f2f1e5b425002a0fd45b7cadb/Modules/CMakeDetermineCompilerId.cmake#L267-271 |
| 161 | let accepts_cl_style_flags = run(Command::new(path).arg("-?" ), path, &{ |
| 162 | // the errors are not errors! |
| 163 | let mut cargo_output = cargo_output.clone(); |
| 164 | cargo_output.warnings = cargo_output.debug; |
| 165 | cargo_output.output = OutputKind::Discard; |
| 166 | cargo_output |
| 167 | }) |
| 168 | .is_ok(); |
| 169 | |
| 170 | let clang = stdout.contains(r#""clang""# ); |
| 171 | let gcc = stdout.contains(r#""gcc""# ); |
| 172 | let emscripten = stdout.contains(r#""emscripten""# ); |
| 173 | let vxworks = stdout.contains(r#""VxWorks""# ); |
| 174 | |
| 175 | match (clang, accepts_cl_style_flags, gcc, emscripten, vxworks) { |
| 176 | (clang_cl, true, _, false, false) => Ok(ToolFamily::Msvc { clang_cl }), |
| 177 | (true, _, _, _, false) | (_, _, _, true, false) => Ok(ToolFamily::Clang { |
| 178 | zig_cc: is_zig_cc(path, cargo_output), |
| 179 | }), |
| 180 | (false, false, true, _, false) | (_, _, _, _, true) => Ok(ToolFamily::Gnu), |
| 181 | (false, false, false, false, false) => { |
| 182 | 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" ); |
| 183 | Err(Error::new( |
| 184 | ErrorKind::ToolFamilyMacroNotFound, |
| 185 | "Expects macro `__clang__`, `__GNUC__` or `__EMSCRIPTEN__`, `__VXWORKS__` or accepts cl style flag `-?`, but found none" , |
| 186 | )) |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | let detect_family = |path: &Path| -> Result<ToolFamily, Error> { |
| 191 | if let Some(family) = cached_compiler_family.read().unwrap().get(path) { |
| 192 | return Ok(*family); |
| 193 | } |
| 194 | |
| 195 | let family = detect_family_inner(path, cargo_output, out_dir)?; |
| 196 | cached_compiler_family |
| 197 | .write() |
| 198 | .unwrap() |
| 199 | .insert(path.into(), family); |
| 200 | Ok(family) |
| 201 | }; |
| 202 | |
| 203 | let family = detect_family(&path).unwrap_or_else(|e| { |
| 204 | cargo_output.print_warning(&format_args!( |
| 205 | "Compiler family detection failed due to error: {}" , |
| 206 | e |
| 207 | )); |
| 208 | match path.file_name().map(OsStr::to_string_lossy) { |
| 209 | Some(fname) if fname.contains("clang-cl" ) => ToolFamily::Msvc { clang_cl: true }, |
| 210 | Some(fname) if fname.ends_with("cl" ) || fname == "cl.exe" => { |
| 211 | ToolFamily::Msvc { clang_cl: false } |
| 212 | } |
| 213 | Some(fname) if fname.contains("clang" ) => match clang_driver { |
| 214 | Some("cl" ) => ToolFamily::Msvc { clang_cl: true }, |
| 215 | _ => ToolFamily::Clang { |
| 216 | zig_cc: is_zig_cc(&path, cargo_output), |
| 217 | }, |
| 218 | }, |
| 219 | Some(fname) if fname.contains("zig" ) => ToolFamily::Clang { zig_cc: true }, |
| 220 | _ => ToolFamily::Gnu, |
| 221 | } |
| 222 | }); |
| 223 | |
| 224 | Tool { |
| 225 | path, |
| 226 | cc_wrapper_path: None, |
| 227 | cc_wrapper_args: Vec::new(), |
| 228 | args: Vec::new(), |
| 229 | env: Vec::new(), |
| 230 | family, |
| 231 | cuda, |
| 232 | removed_args: Vec::new(), |
| 233 | has_internal_target_arg: false, |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | /// Add an argument to be stripped from the final command arguments. |
| 238 | pub(crate) fn remove_arg(&mut self, flag: OsString) { |
| 239 | self.removed_args.push(flag); |
| 240 | } |
| 241 | |
| 242 | /// Push an "exotic" flag to the end of the compiler's arguments list. |
| 243 | /// |
| 244 | /// Nvidia compiler accepts only the most common compiler flags like `-D`, |
| 245 | /// `-I`, `-c`, etc. Options meant specifically for the underlying |
| 246 | /// host C++ compiler have to be prefixed with `-Xcompiler`. |
| 247 | /// [Another possible future application for this function is passing |
| 248 | /// clang-specific flags to clang-cl, which otherwise accepts only |
| 249 | /// MSVC-specific options.] |
| 250 | pub(crate) fn push_cc_arg(&mut self, flag: OsString) { |
| 251 | if self.cuda { |
| 252 | self.args.push("-Xcompiler" .into()); |
| 253 | } |
| 254 | self.args.push(flag); |
| 255 | } |
| 256 | |
| 257 | /// Checks if an argument or flag has already been specified or conflicts. |
| 258 | /// |
| 259 | /// Currently only checks optimization flags. |
| 260 | pub(crate) fn is_duplicate_opt_arg(&self, flag: &OsString) -> bool { |
| 261 | let flag = flag.to_str().unwrap(); |
| 262 | let mut chars = flag.chars(); |
| 263 | |
| 264 | // Only duplicate check compiler flags |
| 265 | if self.is_like_msvc() { |
| 266 | if chars.next() != Some('/' ) { |
| 267 | return false; |
| 268 | } |
| 269 | } else if (self.is_like_gnu() || self.is_like_clang()) && chars.next() != Some('-' ) { |
| 270 | return false; |
| 271 | } |
| 272 | |
| 273 | // Check for existing optimization flags (-O, /O) |
| 274 | if chars.next() == Some('O' ) { |
| 275 | return self |
| 276 | .args() |
| 277 | .iter() |
| 278 | .any(|a| a.to_str().unwrap_or("" ).chars().nth(1) == Some('O' )); |
| 279 | } |
| 280 | |
| 281 | // TODO Check for existing -m..., -m...=..., /arch:... flags |
| 282 | false |
| 283 | } |
| 284 | |
| 285 | /// Don't push optimization arg if it conflicts with existing args. |
| 286 | pub(crate) fn push_opt_unless_duplicate(&mut self, flag: OsString) { |
| 287 | if self.is_duplicate_opt_arg(&flag) { |
| 288 | eprintln!("Info: Ignoring duplicate arg {:?}" , &flag); |
| 289 | } else { |
| 290 | self.push_cc_arg(flag); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /// Converts this compiler into a `Command` that's ready to be run. |
| 295 | /// |
| 296 | /// This is useful for when the compiler needs to be executed and the |
| 297 | /// command returned will already have the initial arguments and environment |
| 298 | /// variables configured. |
| 299 | pub fn to_command(&self) -> Command { |
| 300 | let mut cmd = match self.cc_wrapper_path { |
| 301 | Some(ref cc_wrapper_path) => { |
| 302 | let mut cmd = Command::new(cc_wrapper_path); |
| 303 | cmd.arg(&self.path); |
| 304 | cmd |
| 305 | } |
| 306 | None => Command::new(&self.path), |
| 307 | }; |
| 308 | cmd.args(&self.cc_wrapper_args); |
| 309 | |
| 310 | let value = self |
| 311 | .args |
| 312 | .iter() |
| 313 | .filter(|a| !self.removed_args.contains(a)) |
| 314 | .collect::<Vec<_>>(); |
| 315 | cmd.args(&value); |
| 316 | |
| 317 | for (k, v) in self.env.iter() { |
| 318 | cmd.env(k, v); |
| 319 | } |
| 320 | cmd |
| 321 | } |
| 322 | |
| 323 | /// Returns the path for this compiler. |
| 324 | /// |
| 325 | /// Note that this may not be a path to a file on the filesystem, e.g. "cc", |
| 326 | /// but rather something which will be resolved when a process is spawned. |
| 327 | pub fn path(&self) -> &Path { |
| 328 | &self.path |
| 329 | } |
| 330 | |
| 331 | /// Returns the default set of arguments to the compiler needed to produce |
| 332 | /// executables for the target this compiler generates. |
| 333 | pub fn args(&self) -> &[OsString] { |
| 334 | &self.args |
| 335 | } |
| 336 | |
| 337 | /// Returns the set of environment variables needed for this compiler to |
| 338 | /// operate. |
| 339 | /// |
| 340 | /// This is typically only used for MSVC compilers currently. |
| 341 | pub fn env(&self) -> &[(OsString, OsString)] { |
| 342 | &self.env |
| 343 | } |
| 344 | |
| 345 | /// Returns the compiler command in format of CC environment variable. |
| 346 | /// Or empty string if CC env was not present |
| 347 | /// |
| 348 | /// This is typically used by configure script |
| 349 | pub fn cc_env(&self) -> OsString { |
| 350 | match self.cc_wrapper_path { |
| 351 | Some(ref cc_wrapper_path) => { |
| 352 | let mut cc_env = cc_wrapper_path.as_os_str().to_owned(); |
| 353 | cc_env.push(" " ); |
| 354 | cc_env.push(self.path.to_path_buf().into_os_string()); |
| 355 | for arg in self.cc_wrapper_args.iter() { |
| 356 | cc_env.push(" " ); |
| 357 | cc_env.push(arg); |
| 358 | } |
| 359 | cc_env |
| 360 | } |
| 361 | None => OsString::from("" ), |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | /// Returns the compiler flags in format of CFLAGS environment variable. |
| 366 | /// Important here - this will not be CFLAGS from env, its internal gcc's flags to use as CFLAGS |
| 367 | /// This is typically used by configure script |
| 368 | pub fn cflags_env(&self) -> OsString { |
| 369 | let mut flags = OsString::new(); |
| 370 | for (i, arg) in self.args.iter().enumerate() { |
| 371 | if i > 0 { |
| 372 | flags.push(" " ); |
| 373 | } |
| 374 | flags.push(arg); |
| 375 | } |
| 376 | flags |
| 377 | } |
| 378 | |
| 379 | /// Whether the tool is GNU Compiler Collection-like. |
| 380 | pub fn is_like_gnu(&self) -> bool { |
| 381 | self.family == ToolFamily::Gnu |
| 382 | } |
| 383 | |
| 384 | /// Whether the tool is Clang-like. |
| 385 | pub fn is_like_clang(&self) -> bool { |
| 386 | matches!(self.family, ToolFamily::Clang { .. }) |
| 387 | } |
| 388 | |
| 389 | /// Whether the tool is AppleClang under .xctoolchain |
| 390 | #[cfg (target_vendor = "apple" )] |
| 391 | pub(crate) fn is_xctoolchain_clang(&self) -> bool { |
| 392 | let path = self.path.to_string_lossy(); |
| 393 | path.contains(".xctoolchain/" ) |
| 394 | } |
| 395 | #[cfg (not(target_vendor = "apple" ))] |
| 396 | pub(crate) fn is_xctoolchain_clang(&self) -> bool { |
| 397 | false |
| 398 | } |
| 399 | |
| 400 | /// Whether the tool is MSVC-like. |
| 401 | pub fn is_like_msvc(&self) -> bool { |
| 402 | matches!(self.family, ToolFamily::Msvc { .. }) |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | /// Represents the family of tools this tool belongs to. |
| 407 | /// |
| 408 | /// Each family of tools differs in how and what arguments they accept. |
| 409 | /// |
| 410 | /// Detection of a family is done on best-effort basis and may not accurately reflect the tool. |
| 411 | #[derive (Copy, Clone, Debug, PartialEq)] |
| 412 | pub enum ToolFamily { |
| 413 | /// Tool is GNU Compiler Collection-like. |
| 414 | Gnu, |
| 415 | /// Tool is Clang-like. It differs from the GCC in a sense that it accepts superset of flags |
| 416 | /// and its cross-compilation approach is different. |
| 417 | Clang { zig_cc: bool }, |
| 418 | /// Tool is the MSVC cl.exe. |
| 419 | Msvc { clang_cl: bool }, |
| 420 | } |
| 421 | |
| 422 | impl ToolFamily { |
| 423 | /// What the flag to request debug info for this family of tools look like |
| 424 | pub(crate) fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option<u32>) { |
| 425 | match *self { |
| 426 | ToolFamily::Msvc { .. } => { |
| 427 | cmd.push_cc_arg("-Z7" .into()); |
| 428 | } |
| 429 | ToolFamily::Gnu | ToolFamily::Clang { .. } => { |
| 430 | cmd.push_cc_arg( |
| 431 | dwarf_version |
| 432 | .map_or_else(|| "-g" .into(), |v| format!("-gdwarf- {}" , v)) |
| 433 | .into(), |
| 434 | ); |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | /// What the flag to force frame pointers. |
| 440 | pub(crate) fn add_force_frame_pointer(&self, cmd: &mut Tool) { |
| 441 | match *self { |
| 442 | ToolFamily::Gnu | ToolFamily::Clang { .. } => { |
| 443 | cmd.push_cc_arg("-fno-omit-frame-pointer" .into()); |
| 444 | } |
| 445 | _ => (), |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | /// What the flags to enable all warnings |
| 450 | pub(crate) fn warnings_flags(&self) -> &'static str { |
| 451 | match *self { |
| 452 | ToolFamily::Msvc { .. } => "-W4" , |
| 453 | ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Wall" , |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | /// What the flags to enable extra warnings |
| 458 | pub(crate) fn extra_warnings_flags(&self) -> Option<&'static str> { |
| 459 | match *self { |
| 460 | ToolFamily::Msvc { .. } => None, |
| 461 | ToolFamily::Gnu | ToolFamily::Clang { .. } => Some("-Wextra" ), |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | /// What the flag to turn warning into errors |
| 466 | pub(crate) fn warnings_to_errors_flag(&self) -> &'static str { |
| 467 | match *self { |
| 468 | ToolFamily::Msvc { .. } => "-WX" , |
| 469 | ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Werror" , |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | pub(crate) fn verbose_stderr(&self) -> bool { |
| 474 | matches!(*self, ToolFamily::Clang { .. }) |
| 475 | } |
| 476 | } |
| 477 | |