[go: up one dir, main page]

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