cc/
tool.rs

1use crate::{
2    command_helpers::{run_output, spawn, CargoOutput},
3    run,
4    tempfile::NamedTempfile,
5    Error, ErrorKind, OutputKind,
6};
7use std::io::Read;
8use std::{
9    borrow::Cow,
10    collections::HashMap,
11    env,
12    ffi::{OsStr, OsString},
13    io::Write,
14    path::{Path, PathBuf},
15    process::{Command, Stdio},
16    sync::RwLock,
17};
18
19pub(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)]
30pub 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
42impl 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 mut cmd = Command::new(path);
202            cmd.arg("-E").arg(tmp.path());
203
204            // The -Wslash-u-filename warning is normally part of stdout.
205            // But with clang-cl it can be part of stderr instead and exit with a
206            // non-zero exit code.
207            let mut captured_cargo_output = compiler_detect_output.clone();
208            captured_cargo_output.output = OutputKind::Capture;
209            captured_cargo_output.warnings = true;
210            let mut child = spawn(&mut cmd, &captured_cargo_output)?;
211
212            let mut out = vec![];
213            let mut err = vec![];
214            child.stdout.take().unwrap().read_to_end(&mut out)?;
215            child.stderr.take().unwrap().read_to_end(&mut err)?;
216
217            let status = child.wait()?;
218
219            let stdout = if [&out, &err]
220                .iter()
221                .any(|o| String::from_utf8_lossy(o).contains("-Wslash-u-filename"))
222            {
223                run_output(
224                    Command::new(path).arg("-E").arg("--").arg(tmp.path()),
225                    &compiler_detect_output,
226                )?
227            } else {
228                if !status.success() {
229                    return Err(Error::new(
230                        ErrorKind::ToolExecError,
231                        format!(
232                            "command did not execute successfully (status code {status}): {cmd:?}"
233                        ),
234                    ));
235                }
236
237                out
238            };
239
240            let stdout = String::from_utf8_lossy(&stdout);
241            guess_family_from_stdout(&stdout, path, args, cargo_output)
242        }
243        let detect_family = |path: &Path, args: &[String]| -> Result<ToolFamily, Error> {
244            let cache_key = [path.as_os_str()]
245                .iter()
246                .cloned()
247                .chain(args.iter().map(OsStr::new))
248                .map(Into::into)
249                .collect();
250            if let Some(family) = cached_compiler_family.read().unwrap().get(&cache_key) {
251                return Ok(*family);
252            }
253
254            let family = detect_family_inner(path, args, cargo_output, out_dir)?;
255            cached_compiler_family
256                .write()
257                .unwrap()
258                .insert(cache_key, family);
259            Ok(family)
260        };
261
262        let family = detect_family(&path, &args).unwrap_or_else(|e| {
263            cargo_output.print_warning(&format_args!(
264                "Compiler family detection failed due to error: {e}"
265            ));
266            match path.file_name().map(OsStr::to_string_lossy) {
267                Some(fname) if fname.contains("clang-cl") => ToolFamily::Msvc { clang_cl: true },
268                Some(fname) if fname.ends_with("cl") || fname == "cl.exe" => {
269                    ToolFamily::Msvc { clang_cl: false }
270                }
271                Some(fname) if fname.contains("clang") => {
272                    let is_clang_cl = args
273                        .iter()
274                        .any(|a| a.strip_prefix("--driver-mode=") == Some("cl"));
275                    if is_clang_cl {
276                        ToolFamily::Msvc { clang_cl: true }
277                    } else {
278                        ToolFamily::Clang {
279                            zig_cc: is_zig_cc(&path, cargo_output),
280                        }
281                    }
282                }
283                Some(fname) if fname.contains("zig") => ToolFamily::Clang { zig_cc: true },
284                _ => ToolFamily::Gnu,
285            }
286        });
287
288        Tool {
289            path,
290            cc_wrapper_path: None,
291            cc_wrapper_args: Vec::new(),
292            args: Vec::new(),
293            env: Vec::new(),
294            family,
295            cuda,
296            removed_args: Vec::new(),
297            has_internal_target_arg: false,
298        }
299    }
300
301    /// Add an argument to be stripped from the final command arguments.
302    pub(crate) fn remove_arg(&mut self, flag: OsString) {
303        self.removed_args.push(flag);
304    }
305
306    /// Push an "exotic" flag to the end of the compiler's arguments list.
307    ///
308    /// Nvidia compiler accepts only the most common compiler flags like `-D`,
309    /// `-I`, `-c`, etc. Options meant specifically for the underlying
310    /// host C++ compiler have to be prefixed with `-Xcompiler`.
311    /// [Another possible future application for this function is passing
312    /// clang-specific flags to clang-cl, which otherwise accepts only
313    /// MSVC-specific options.]
314    pub(crate) fn push_cc_arg(&mut self, flag: OsString) {
315        if self.cuda {
316            self.args.push("-Xcompiler".into());
317        }
318        self.args.push(flag);
319    }
320
321    /// Checks if an argument or flag has already been specified or conflicts.
322    ///
323    /// Currently only checks optimization flags.
324    pub(crate) fn is_duplicate_opt_arg(&self, flag: &OsString) -> bool {
325        let flag = flag.to_str().unwrap();
326        let mut chars = flag.chars();
327
328        // Only duplicate check compiler flags
329        if self.is_like_msvc() {
330            if chars.next() != Some('/') {
331                return false;
332            }
333        } else if (self.is_like_gnu() || self.is_like_clang()) && chars.next() != Some('-') {
334            return false;
335        }
336
337        // Check for existing optimization flags (-O, /O)
338        if chars.next() == Some('O') {
339            return self
340                .args()
341                .iter()
342                .any(|a| a.to_str().unwrap_or("").chars().nth(1) == Some('O'));
343        }
344
345        // TODO Check for existing -m..., -m...=..., /arch:... flags
346        false
347    }
348
349    /// Don't push optimization arg if it conflicts with existing args.
350    pub(crate) fn push_opt_unless_duplicate(&mut self, flag: OsString) {
351        if self.is_duplicate_opt_arg(&flag) {
352            eprintln!("Info: Ignoring duplicate arg {:?}", &flag);
353        } else {
354            self.push_cc_arg(flag);
355        }
356    }
357
358    /// Converts this compiler into a `Command` that's ready to be run.
359    ///
360    /// This is useful for when the compiler needs to be executed and the
361    /// command returned will already have the initial arguments and environment
362    /// variables configured.
363    pub fn to_command(&self) -> Command {
364        let mut cmd = match self.cc_wrapper_path {
365            Some(ref cc_wrapper_path) => {
366                let mut cmd = Command::new(cc_wrapper_path);
367                cmd.arg(&self.path);
368                cmd
369            }
370            None => Command::new(&self.path),
371        };
372        cmd.args(&self.cc_wrapper_args);
373
374        let value = self
375            .args
376            .iter()
377            .filter(|a| !self.removed_args.contains(a))
378            .collect::<Vec<_>>();
379        cmd.args(&value);
380
381        for (k, v) in self.env.iter() {
382            cmd.env(k, v);
383        }
384        cmd
385    }
386
387    /// Returns the path for this compiler.
388    ///
389    /// Note that this may not be a path to a file on the filesystem, e.g. "cc",
390    /// but rather something which will be resolved when a process is spawned.
391    pub fn path(&self) -> &Path {
392        &self.path
393    }
394
395    /// Returns the default set of arguments to the compiler needed to produce
396    /// executables for the target this compiler generates.
397    pub fn args(&self) -> &[OsString] {
398        &self.args
399    }
400
401    /// Returns the set of environment variables needed for this compiler to
402    /// operate.
403    ///
404    /// This is typically only used for MSVC compilers currently.
405    pub fn env(&self) -> &[(OsString, OsString)] {
406        &self.env
407    }
408
409    /// Returns the compiler command in format of CC environment variable.
410    /// Or empty string if CC env was not present
411    ///
412    /// This is typically used by configure script
413    pub fn cc_env(&self) -> OsString {
414        match self.cc_wrapper_path {
415            Some(ref cc_wrapper_path) => {
416                let mut cc_env = cc_wrapper_path.as_os_str().to_owned();
417                cc_env.push(" ");
418                cc_env.push(self.path.to_path_buf().into_os_string());
419                for arg in self.cc_wrapper_args.iter() {
420                    cc_env.push(" ");
421                    cc_env.push(arg);
422                }
423                cc_env
424            }
425            None => OsString::from(""),
426        }
427    }
428
429    /// Returns the compiler flags in format of CFLAGS environment variable.
430    /// Important here - this will not be CFLAGS from env, its internal gcc's flags to use as CFLAGS
431    /// This is typically used by configure script
432    pub fn cflags_env(&self) -> OsString {
433        let mut flags = OsString::new();
434        for (i, arg) in self.args.iter().enumerate() {
435            if i > 0 {
436                flags.push(" ");
437            }
438            flags.push(arg);
439        }
440        flags
441    }
442
443    /// Whether the tool is GNU Compiler Collection-like.
444    pub fn is_like_gnu(&self) -> bool {
445        self.family == ToolFamily::Gnu
446    }
447
448    /// Whether the tool is Clang-like.
449    pub fn is_like_clang(&self) -> bool {
450        matches!(self.family, ToolFamily::Clang { .. })
451    }
452
453    /// Whether the tool is AppleClang under .xctoolchain
454    #[cfg(target_vendor = "apple")]
455    pub(crate) fn is_xctoolchain_clang(&self) -> bool {
456        let path = self.path.to_string_lossy();
457        path.contains(".xctoolchain/")
458    }
459    #[cfg(not(target_vendor = "apple"))]
460    pub(crate) fn is_xctoolchain_clang(&self) -> bool {
461        false
462    }
463
464    /// Whether the tool is MSVC-like.
465    pub fn is_like_msvc(&self) -> bool {
466        matches!(self.family, ToolFamily::Msvc { .. })
467    }
468
469    /// Whether the tool is `clang-cl`-based MSVC-like.
470    pub fn is_like_clang_cl(&self) -> bool {
471        matches!(self.family, ToolFamily::Msvc { clang_cl: true })
472    }
473
474    /// Supports using `--` delimiter to separate arguments and path to source files.
475    pub(crate) fn supports_path_delimiter(&self) -> bool {
476        // homebrew clang and zig-cc does not support this while stock version does
477        matches!(self.family, ToolFamily::Msvc { clang_cl: true }) && !self.cuda
478    }
479}
480
481/// Represents the family of tools this tool belongs to.
482///
483/// Each family of tools differs in how and what arguments they accept.
484///
485/// Detection of a family is done on best-effort basis and may not accurately reflect the tool.
486#[derive(Copy, Clone, Debug, PartialEq)]
487pub enum ToolFamily {
488    /// Tool is GNU Compiler Collection-like.
489    Gnu,
490    /// Tool is Clang-like. It differs from the GCC in a sense that it accepts superset of flags
491    /// and its cross-compilation approach is different.
492    Clang { zig_cc: bool },
493    /// Tool is the MSVC cl.exe.
494    Msvc { clang_cl: bool },
495}
496
497impl ToolFamily {
498    /// What the flag to request debug info for this family of tools look like
499    pub(crate) fn add_debug_flags(&self, cmd: &mut Tool, dwarf_version: Option<u32>) {
500        match *self {
501            ToolFamily::Msvc { .. } => {
502                cmd.push_cc_arg("-Z7".into());
503            }
504            ToolFamily::Gnu | ToolFamily::Clang { .. } => {
505                cmd.push_cc_arg(
506                    dwarf_version
507                        .map_or_else(|| "-g".into(), |v| format!("-gdwarf-{v}"))
508                        .into(),
509                );
510            }
511        }
512    }
513
514    /// What the flag to force frame pointers.
515    pub(crate) fn add_force_frame_pointer(&self, cmd: &mut Tool) {
516        match *self {
517            ToolFamily::Gnu | ToolFamily::Clang { .. } => {
518                cmd.push_cc_arg("-fno-omit-frame-pointer".into());
519            }
520            _ => (),
521        }
522    }
523
524    /// What the flags to enable all warnings
525    pub(crate) fn warnings_flags(&self) -> &'static str {
526        match *self {
527            ToolFamily::Msvc { .. } => "-W4",
528            ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Wall",
529        }
530    }
531
532    /// What the flags to enable extra warnings
533    pub(crate) fn extra_warnings_flags(&self) -> Option<&'static str> {
534        match *self {
535            ToolFamily::Msvc { .. } => None,
536            ToolFamily::Gnu | ToolFamily::Clang { .. } => Some("-Wextra"),
537        }
538    }
539
540    /// What the flag to turn warning into errors
541    pub(crate) fn warnings_to_errors_flag(&self) -> &'static str {
542        match *self {
543            ToolFamily::Msvc { .. } => "-WX",
544            ToolFamily::Gnu | ToolFamily::Clang { .. } => "-Werror",
545        }
546    }
547
548    pub(crate) fn verbose_stderr(&self) -> bool {
549        matches!(*self, ToolFamily::Clang { .. })
550    }
551}