cc/
tool.rs

1use 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
12use crate::{
13    command_helpers::{run_output, CargoOutput},
14    run,
15    tempfile::NamedTempfile,
16    Error, ErrorKind, OutputKind,
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 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)]
463pub 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
473impl 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}