bootstrap/utils/
cc_detect.rs

1//! C-compiler probing and detection.
2//!
3//! This module will fill out the `cc` and `cxx` maps of `Build` by looking for
4//! C and C++ compilers for each target configured. A compiler is found through
5//! a number of vectors (in order of precedence)
6//!
7//! 1. Configuration via `target.$target.cc` in `bootstrap.toml`.
8//! 2. Configuration via `target.$target.android-ndk` in `bootstrap.toml`, if
9//!    applicable
10//! 3. Special logic to probe on OpenBSD
11//! 4. The `CC_$target` environment variable.
12//! 5. The `CC` environment variable.
13//! 6. "cc"
14//!
15//! Some of this logic is implemented here, but much of it is farmed out to the
16//! `cc` crate itself, so we end up having the same fallbacks as there.
17//! Similar logic is then used to find a C++ compiler, just some s/cc/c++/ is
18//! used.
19//!
20//! It is intended that after this module has run no C/C++ compiler will
21//! ever be probed for. Instead the compilers found here will be used for
22//! everything.
23
24use std::collections::HashSet;
25use std::path::{Path, PathBuf};
26use std::{env, iter};
27
28use crate::core::config::TargetSelection;
29use crate::utils::exec::{BootstrapCommand, command};
30use crate::{Build, CLang, GitRepo};
31
32/// Finds archiver tool for the given target if possible.
33/// FIXME(onur-ozkan): This logic should be replaced by calling into the `cc` crate.
34fn cc2ar(cc: &Path, target: TargetSelection, default_ar: PathBuf) -> Option<PathBuf> {
35    if let Some(ar) = env::var_os(format!("AR_{}", target.triple.replace('-', "_"))) {
36        Some(PathBuf::from(ar))
37    } else if let Some(ar) = env::var_os("AR") {
38        Some(PathBuf::from(ar))
39    } else if target.is_msvc() {
40        None
41    } else if target.contains("musl") || target.contains("openbsd") {
42        Some(PathBuf::from("ar"))
43    } else if target.contains("vxworks") {
44        Some(PathBuf::from("wr-ar"))
45    } else if target.contains("-nto-") {
46        if target.starts_with("i586") {
47            Some(PathBuf::from("ntox86-ar"))
48        } else if target.starts_with("aarch64") {
49            Some(PathBuf::from("ntoaarch64-ar"))
50        } else if target.starts_with("x86_64") {
51            Some(PathBuf::from("ntox86_64-ar"))
52        } else {
53            panic!("Unknown architecture, cannot determine archiver for Neutrino QNX");
54        }
55    } else if target.contains("android") || target.contains("-wasi") {
56        Some(cc.parent().unwrap().join(PathBuf::from("llvm-ar")))
57    } else {
58        Some(default_ar)
59    }
60}
61
62/// Creates and configures a new [`cc::Build`] instance for the given target.
63fn new_cc_build(build: &Build, target: TargetSelection) -> cc::Build {
64    let mut cfg = cc::Build::new();
65    cfg.cargo_metadata(false)
66        .opt_level(2)
67        .warnings(false)
68        .debug(false)
69        // Compress debuginfo
70        .flag_if_supported("-gz")
71        .target(&target.triple)
72        .host(&build.build.triple);
73    match build.crt_static(target) {
74        Some(a) => {
75            cfg.static_crt(a);
76        }
77        None => {
78            if target.is_msvc() {
79                cfg.static_crt(true);
80            }
81            if target.contains("musl") {
82                cfg.static_flag(true);
83            }
84        }
85    }
86    cfg
87}
88
89/// Probes for C and C++ compilers and configures the corresponding entries in the [`Build`]
90/// structure.
91///
92/// This function determines which targets need a C compiler (and, if needed, a C++ compiler)
93/// by combining the primary build target, host targets, and any additional targets. For
94/// each target, it calls [`find_target`] to configure the necessary compiler tools.
95pub fn find(build: &Build) {
96    let targets: HashSet<_> = match build.config.cmd {
97        // We don't need to check cross targets for these commands.
98        crate::Subcommand::Clean { .. }
99        | crate::Subcommand::Check { .. }
100        | crate::Subcommand::Suggest { .. }
101        | crate::Subcommand::Format { .. }
102        | crate::Subcommand::Setup { .. } => {
103            build.hosts.iter().cloned().chain(iter::once(build.build)).collect()
104        }
105
106        _ => {
107            // For all targets we're going to need a C compiler for building some shims
108            // and such as well as for being a linker for Rust code.
109            build
110                .targets
111                .iter()
112                .chain(&build.hosts)
113                .cloned()
114                .chain(iter::once(build.build))
115                .collect()
116        }
117    };
118
119    for target in targets.into_iter() {
120        find_target(build, target);
121    }
122}
123
124/// Probes and configures the C and C++ compilers for a single target.
125///
126/// This function uses both user-specified configuration (from `bootstrap.toml`) and auto-detection
127/// logic to determine the correct C/C++ compilers for the target. It also determines the appropriate
128/// archiver (`ar`) and sets up additional compilation flags (both handled and unhandled).
129pub fn find_target(build: &Build, target: TargetSelection) {
130    let mut cfg = new_cc_build(build, target);
131    let config = build.config.target_config.get(&target);
132    if let Some(cc) = config
133        .and_then(|c| c.cc.clone())
134        .or_else(|| default_compiler(&mut cfg, Language::C, target, build))
135    {
136        cfg.compiler(cc);
137    }
138
139    let compiler = cfg.get_compiler();
140    let ar = if let ar @ Some(..) = config.and_then(|c| c.ar.clone()) {
141        ar
142    } else {
143        cc2ar(compiler.path(), target, PathBuf::from(cfg.get_archiver().get_program()))
144    };
145
146    build.cc.borrow_mut().insert(target, compiler.clone());
147    let mut cflags = build.cc_handled_clags(target, CLang::C);
148    cflags.extend(build.cc_unhandled_cflags(target, GitRepo::Rustc, CLang::C));
149
150    // If we use llvm-libunwind, we will need a C++ compiler as well for all targets
151    // We'll need one anyways if the target triple is also a host triple
152    let mut cfg = new_cc_build(build, target);
153    cfg.cpp(true);
154    let cxx_configured = if let Some(cxx) = config
155        .and_then(|c| c.cxx.clone())
156        .or_else(|| default_compiler(&mut cfg, Language::CPlusPlus, target, build))
157    {
158        cfg.compiler(cxx);
159        true
160    } else {
161        // Use an auto-detected compiler (or one configured via `CXX_target_triple` env vars).
162        cfg.try_get_compiler().is_ok()
163    };
164
165    // for VxWorks, record CXX compiler which will be used in lib.rs:linker()
166    if cxx_configured || target.contains("vxworks") {
167        let compiler = cfg.get_compiler();
168        build.cxx.borrow_mut().insert(target, compiler);
169    }
170
171    build.verbose(|| println!("CC_{} = {:?}", target.triple, build.cc(target)));
172    build.verbose(|| println!("CFLAGS_{} = {cflags:?}", target.triple));
173    if let Ok(cxx) = build.cxx(target) {
174        let mut cxxflags = build.cc_handled_clags(target, CLang::Cxx);
175        cxxflags.extend(build.cc_unhandled_cflags(target, GitRepo::Rustc, CLang::Cxx));
176        build.verbose(|| println!("CXX_{} = {cxx:?}", target.triple));
177        build.verbose(|| println!("CXXFLAGS_{} = {cxxflags:?}", target.triple));
178    }
179    if let Some(ar) = ar {
180        build.verbose(|| println!("AR_{} = {ar:?}", target.triple));
181        build.ar.borrow_mut().insert(target, ar);
182    }
183
184    if let Some(ranlib) = config.and_then(|c| c.ranlib.clone()) {
185        build.ranlib.borrow_mut().insert(target, ranlib);
186    }
187}
188
189/// Determines the default compiler for a given target and language when not explicitly
190/// configured in `bootstrap.toml`.
191fn default_compiler(
192    cfg: &mut cc::Build,
193    compiler: Language,
194    target: TargetSelection,
195    build: &Build,
196) -> Option<PathBuf> {
197    match &*target.triple {
198        // When compiling for android we may have the NDK configured in the
199        // bootstrap.toml in which case we look there. Otherwise the default
200        // compiler already takes into account the triple in question.
201        t if t.contains("android") => {
202            build.config.android_ndk.as_ref().map(|ndk| ndk_compiler(compiler, &target.triple, ndk))
203        }
204
205        // The default gcc version from OpenBSD may be too old, try using egcc,
206        // which is a gcc version from ports, if this is the case.
207        t if t.contains("openbsd") => {
208            let c = cfg.get_compiler();
209            let gnu_compiler = compiler.gcc();
210            if !c.path().ends_with(gnu_compiler) {
211                return None;
212            }
213
214            let mut cmd = BootstrapCommand::from(c.to_command());
215            let output = cmd.arg("--version").run_capture_stdout(build).stdout();
216            let i = output.find(" 4.")?;
217            match output[i + 3..].chars().next().unwrap() {
218                '0'..='6' => {}
219                _ => return None,
220            }
221            let alternative = format!("e{gnu_compiler}");
222            if command(&alternative).run_capture(build).is_success() {
223                Some(PathBuf::from(alternative))
224            } else {
225                None
226            }
227        }
228
229        "mips-unknown-linux-musl" if compiler == Language::C => {
230            if cfg.get_compiler().path().to_str() == Some("gcc") {
231                Some(PathBuf::from("mips-linux-musl-gcc"))
232            } else {
233                None
234            }
235        }
236        "mipsel-unknown-linux-musl" if compiler == Language::C => {
237            if cfg.get_compiler().path().to_str() == Some("gcc") {
238                Some(PathBuf::from("mipsel-linux-musl-gcc"))
239            } else {
240                None
241            }
242        }
243
244        t if t.contains("musl") && compiler == Language::C => {
245            if let Some(root) = build.musl_root(target) {
246                let guess = root.join("bin/musl-gcc");
247                if guess.exists() { Some(guess) } else { None }
248            } else {
249                None
250            }
251        }
252
253        t if t.contains("-wasi") => {
254            let root = PathBuf::from(std::env::var_os("WASI_SDK_PATH")?);
255            let compiler = match compiler {
256                Language::C => format!("{t}-clang"),
257                Language::CPlusPlus => format!("{t}-clang++"),
258            };
259            let compiler = root.join("bin").join(compiler);
260            Some(compiler)
261        }
262
263        _ => None,
264    }
265}
266
267/// Constructs the path to the Android NDK compiler for the given target triple and language.
268///
269/// This helper function transform the target triple by converting certain architecture names
270/// (for example, translating "arm" to "arm7a"), appends the minimum API level (hardcoded as "21"
271/// for NDK r26d), and then constructs the full path based on the provided NDK directory and host
272/// platform.
273pub(crate) fn ndk_compiler(compiler: Language, triple: &str, ndk: &Path) -> PathBuf {
274    let mut triple_iter = triple.split('-');
275    let triple_translated = if let Some(arch) = triple_iter.next() {
276        let arch_new = match arch {
277            "arm" | "armv7" | "armv7neon" | "thumbv7" | "thumbv7neon" => "armv7a",
278            other => other,
279        };
280        std::iter::once(arch_new).chain(triple_iter).collect::<Vec<&str>>().join("-")
281    } else {
282        triple.to_string()
283    };
284
285    // The earliest API supported by NDK r26d is 21.
286    let api_level = "21";
287    let compiler = format!("{}{}-{}", triple_translated, api_level, compiler.clang());
288    let host_tag = if cfg!(target_os = "macos") {
289        // The NDK uses universal binaries, so this is correct even on ARM.
290        "darwin-x86_64"
291    } else if cfg!(target_os = "windows") {
292        "windows-x86_64"
293    } else {
294        // NDK r26d only has official releases for macOS, Windows and Linux.
295        // Try the Linux directory everywhere else, on the assumption that the OS has an
296        // emulation layer that can cope (e.g. BSDs).
297        "linux-x86_64"
298    };
299    ndk.join("toolchains").join("llvm").join("prebuilt").join(host_tag).join("bin").join(compiler)
300}
301
302/// Representing the target programming language for a native compiler.
303///
304/// This enum is used to indicate whether a particular compiler is intended for C or C++.
305/// It also provides helper methods for obtaining the standard executable names for GCC and
306/// clang-based compilers.
307#[derive(PartialEq)]
308pub(crate) enum Language {
309    /// The compiler is targeting C.
310    C,
311    /// The compiler is targeting C++.
312    CPlusPlus,
313}
314
315impl Language {
316    /// Returns the executable name for a GCC compiler corresponding to this language.
317    fn gcc(self) -> &'static str {
318        match self {
319            Language::C => "gcc",
320            Language::CPlusPlus => "g++",
321        }
322    }
323
324    /// Returns the executable name for a clang-based compiler corresponding to this language.
325    fn clang(self) -> &'static str {
326        match self {
327            Language::C => "clang",
328            Language::CPlusPlus => "clang++",
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests;