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 | |