1use 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
12use 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)]
28pub 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
40impl 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)]
412pub 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
422impl 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