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