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::Suggest { .. }
100        | crate::Subcommand::Format { .. }
101        | crate::Subcommand::Setup { .. } => {
102            build.hosts.iter().cloned().chain(iter::once(build.build)).collect()
103        }
104
105        _ => {
106            // For all targets we're going to need a C compiler for building some shims
107            // and such as well as for being a linker for Rust code.
108            build
109                .targets
110                .iter()
111                .chain(&build.hosts)
112                .cloned()
113                .chain(iter::once(build.build))
114                .collect()
115        }
116    };
117
118    for target in targets.into_iter() {
119        find_target(build, target);
120    }
121}
122
123/// Probes and configures the C and C++ compilers for a single target.
124///
125/// This function uses both user-specified configuration (from `bootstrap.toml`) and auto-detection
126/// logic to determine the correct C/C++ compilers for the target. It also determines the appropriate
127/// archiver (`ar`) and sets up additional compilation flags (both handled and unhandled).
128pub fn find_target(build: &Build, target: TargetSelection) {
129    let mut cfg = new_cc_build(build, target);
130    let config = build.config.target_config.get(&target);
131    if let Some(cc) = config
132        .and_then(|c| c.cc.clone())
133        .or_else(|| default_compiler(&mut cfg, Language::C, target, build))
134    {
135        cfg.compiler(cc);
136    }
137
138    let compiler = cfg.get_compiler();
139    let ar = if let ar @ Some(..) = config.and_then(|c| c.ar.clone()) {
140        ar
141    } else {
142        cc2ar(compiler.path(), target, PathBuf::from(cfg.get_archiver().get_program()))
143    };
144
145    build.cc.borrow_mut().insert(target, compiler.clone());
146    let mut cflags = build.cc_handled_clags(target, CLang::C);
147    cflags.extend(build.cc_unhandled_cflags(target, GitRepo::Rustc, CLang::C));
148
149    // If we use llvm-libunwind, we will need a C++ compiler as well for all targets
150    // We'll need one anyways if the target triple is also a host triple
151    let mut cfg = new_cc_build(build, target);
152    cfg.cpp(true);
153    let cxx_configured = if let Some(cxx) = config
154        .and_then(|c| c.cxx.clone())
155        .or_else(|| default_compiler(&mut cfg, Language::CPlusPlus, target, build))
156    {
157        cfg.compiler(cxx);
158        true
159    } else {
160        // Use an auto-detected compiler (or one configured via `CXX_target_triple` env vars).
161        cfg.try_get_compiler().is_ok()
162    };
163
164    // for VxWorks, record CXX compiler which will be used in lib.rs:linker()
165    if cxx_configured || target.contains("vxworks") {
166        let compiler = cfg.get_compiler();
167        build.cxx.borrow_mut().insert(target, compiler);
168    }
169
170    build.verbose(|| println!("CC_{} = {:?}", target.triple, build.cc(target)));
171    build.verbose(|| println!("CFLAGS_{} = {cflags:?}", target.triple));
172    if let Ok(cxx) = build.cxx(target) {
173        let mut cxxflags = build.cc_handled_clags(target, CLang::Cxx);
174        cxxflags.extend(build.cc_unhandled_cflags(target, GitRepo::Rustc, CLang::Cxx));
175        build.verbose(|| println!("CXX_{} = {cxx:?}", target.triple));
176        build.verbose(|| println!("CXXFLAGS_{} = {cxxflags:?}", target.triple));
177    }
178    if let Some(ar) = ar {
179        build.verbose(|| println!("AR_{} = {ar:?}", target.triple));
180        build.ar.borrow_mut().insert(target, ar);
181    }
182
183    if let Some(ranlib) = config.and_then(|c| c.ranlib.clone()) {
184        build.ranlib.borrow_mut().insert(target, ranlib);
185    }
186}
187
188/// Determines the default compiler for a given target and language when not explicitly
189/// configured in `bootstrap.toml`.
190fn default_compiler(
191    cfg: &mut cc::Build,
192    compiler: Language,
193    target: TargetSelection,
194    build: &Build,
195) -> Option<PathBuf> {
196    match &*target.triple {
197        // When compiling for android we may have the NDK configured in the
198        // bootstrap.toml in which case we look there. Otherwise the default
199        // compiler already takes into account the triple in question.
200        t if t.contains("android") => {
201            build.config.android_ndk.as_ref().map(|ndk| ndk_compiler(compiler, &target.triple, ndk))
202        }
203
204        // The default gcc version from OpenBSD may be too old, try using egcc,
205        // which is a gcc version from ports, if this is the case.
206        t if t.contains("openbsd") => {
207            let c = cfg.get_compiler();
208            let gnu_compiler = compiler.gcc();
209            if !c.path().ends_with(gnu_compiler) {
210                return None;
211            }
212
213            let mut cmd = BootstrapCommand::from(c.to_command());
214            let output = cmd.arg("--version").run_capture_stdout(build).stdout();
215            let i = output.find(" 4.")?;
216            match output[i + 3..].chars().next().unwrap() {
217                '0'..='6' => {}
218                _ => return None,
219            }
220            let alternative = format!("e{gnu_compiler}");
221            if command(&alternative).run_capture(build).is_success() {
222                Some(PathBuf::from(alternative))
223            } else {
224                None
225            }
226        }
227
228        "mips-unknown-linux-musl" if compiler == Language::C => {
229            if cfg.get_compiler().path().to_str() == Some("gcc") {
230                Some(PathBuf::from("mips-linux-musl-gcc"))
231            } else {
232                None
233            }
234        }
235        "mipsel-unknown-linux-musl" if compiler == Language::C => {
236            if cfg.get_compiler().path().to_str() == Some("gcc") {
237                Some(PathBuf::from("mipsel-linux-musl-gcc"))
238            } else {
239                None
240            }
241        }
242
243        t if t.contains("musl") && compiler == Language::C => {
244            if let Some(root) = build.musl_root(target) {
245                let guess = root.join("bin/musl-gcc");
246                if guess.exists() { Some(guess) } else { None }
247            } else {
248                None
249            }
250        }
251
252        t if t.contains("-wasi") => {
253            let root = PathBuf::from(std::env::var_os("WASI_SDK_PATH")?);
254            let compiler = match compiler {
255                Language::C => format!("{t}-clang"),
256                Language::CPlusPlus => format!("{t}-clang++"),
257            };
258            let compiler = root.join("bin").join(compiler);
259            Some(compiler)
260        }
261
262        _ => None,
263    }
264}
265
266/// Constructs the path to the Android NDK compiler for the given target triple and language.
267///
268/// This helper function transform the target triple by converting certain architecture names
269/// (for example, translating "arm" to "arm7a"), appends the minimum API level (hardcoded as "21"
270/// for NDK r26d), and then constructs the full path based on the provided NDK directory and host
271/// platform.
272pub(crate) fn ndk_compiler(compiler: Language, triple: &str, ndk: &Path) -> PathBuf {
273    let mut triple_iter = triple.split('-');
274    let triple_translated = if let Some(arch) = triple_iter.next() {
275        let arch_new = match arch {
276            "arm" | "armv7" | "armv7neon" | "thumbv7" | "thumbv7neon" => "armv7a",
277            other => other,
278        };
279        std::iter::once(arch_new).chain(triple_iter).collect::<Vec<&str>>().join("-")
280    } else {
281        triple.to_string()
282    };
283
284    // The earliest API supported by NDK r26d is 21.
285    let api_level = "21";
286    let compiler = format!("{}{}-{}", triple_translated, api_level, compiler.clang());
287    let host_tag = if cfg!(target_os = "macos") {
288        // The NDK uses universal binaries, so this is correct even on ARM.
289        "darwin-x86_64"
290    } else if cfg!(target_os = "windows") {
291        "windows-x86_64"
292    } else {
293        // NDK r26d only has official releases for macOS, Windows and Linux.
294        // Try the Linux directory everywhere else, on the assumption that the OS has an
295        // emulation layer that can cope (e.g. BSDs).
296        "linux-x86_64"
297    };
298    ndk.join("toolchains").join("llvm").join("prebuilt").join(host_tag).join("bin").join(compiler)
299}
300
301/// Representing the target programming language for a native compiler.
302///
303/// This enum is used to indicate whether a particular compiler is intended for C or C++.
304/// It also provides helper methods for obtaining the standard executable names for GCC and
305/// clang-based compilers.
306#[derive(PartialEq)]
307pub(crate) enum Language {
308    /// The compiler is targeting C.
309    C,
310    /// The compiler is targeting C++.
311    CPlusPlus,
312}
313
314impl Language {
315    /// Returns the executable name for a GCC compiler corresponding to this language.
316    fn gcc(self) -> &'static str {
317        match self {
318            Language::C => "gcc",
319            Language::CPlusPlus => "g++",
320        }
321    }
322
323    /// Returns the executable name for a clang-based compiler corresponding to this language.
324    fn clang(self) -> &'static str {
325        match self {
326            Language::C => "clang",
327            Language::CPlusPlus => "clang++",
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests;