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