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