[go: up one dir, main page]

cc/
tool.rs

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