Skip to content

add docs and ut for bootstrap util cc-detect #137012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/bootstrap/src/utils/cc_detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::core::config::TargetSelection;
use crate::utils::exec::{BootstrapCommand, command};
use crate::{Build, CLang, GitRepo};

/// Finds archiver tool for the given target if possible.
/// FIXME(onur-ozkan): This logic should be replaced by calling into the `cc` crate.
fn cc2ar(cc: &Path, target: TargetSelection, default_ar: PathBuf) -> Option<PathBuf> {
if let Some(ar) = env::var_os(format!("AR_{}", target.triple.replace('-', "_"))) {
Expand Down Expand Up @@ -58,6 +59,7 @@ fn cc2ar(cc: &Path, target: TargetSelection, default_ar: PathBuf) -> Option<Path
}
}

/// Creates and configures a new [`cc::Build`] instance for the given target.
fn new_cc_build(build: &Build, target: TargetSelection) -> cc::Build {
let mut cfg = cc::Build::new();
cfg.cargo_metadata(false)
Expand All @@ -84,6 +86,12 @@ fn new_cc_build(build: &Build, target: TargetSelection) -> cc::Build {
cfg
}

/// Probes for C and C++ compilers and configures the corresponding entries in the [`Build`]
/// structure.
///
/// This function determines which targets need a C compiler (and, if needed, a C++ compiler)
/// by combining the primary build target, host targets, and any additional targets. For
/// each target, it calls [`find_target`] to configure the necessary compiler tools.
pub fn find(build: &Build) {
let targets: HashSet<_> = match build.config.cmd {
// We don't need to check cross targets for these commands.
Expand Down Expand Up @@ -112,6 +120,11 @@ pub fn find(build: &Build) {
}
}

/// Probes and configures the C and C++ compilers for a single target.
///
/// This function uses both user-specified configuration (from `config.toml`) and auto-detection
/// logic to determine the correct C/C++ compilers for the target. It also determines the appropriate
/// archiver (`ar`) and sets up additional compilation flags (both handled and unhandled).
pub fn find_target(build: &Build, target: TargetSelection) {
let mut cfg = new_cc_build(build, target);
let config = build.config.target_config.get(&target);
Expand Down Expand Up @@ -172,6 +185,8 @@ pub fn find_target(build: &Build, target: TargetSelection) {
}
}

/// Determines the default compiler for a given target and language when not explicitly
/// configured in `config.toml`.
fn default_compiler(
cfg: &mut cc::Build,
compiler: Language,
Expand Down Expand Up @@ -248,6 +263,12 @@ fn default_compiler(
}
}

/// Constructs the path to the Android NDK compiler for the given target triple and language.
///
/// This helper function transform the target triple by converting certain architecture names
/// (for example, translating "arm" to "arm7a"), appends the minimum API level (hardcoded as "21"
/// for NDK r26d), and then constructs the full path based on the provided NDK directory and host
/// platform.
pub(crate) fn ndk_compiler(compiler: Language, triple: &str, ndk: &Path) -> PathBuf {
let mut triple_iter = triple.split('-');
let triple_translated = if let Some(arch) = triple_iter.next() {
Expand Down Expand Up @@ -277,7 +298,11 @@ pub(crate) fn ndk_compiler(compiler: Language, triple: &str, ndk: &Path) -> Path
ndk.join("toolchains").join("llvm").join("prebuilt").join(host_tag).join("bin").join(compiler)
}

/// The target programming language for a native compiler.
/// Representing the target programming language for a native compiler.
///
/// This enum is used to indicate whether a particular compiler is intended for C or C++.
/// It also provides helper methods for obtaining the standard executable names for GCC and
/// clang-based compilers.
#[derive(PartialEq)]
pub(crate) enum Language {
/// The compiler is targeting C.
Expand All @@ -287,19 +312,22 @@ pub(crate) enum Language {
}

impl Language {
/// Obtains the name of a compiler in the GCC collection.
/// Returns the executable name for a GCC compiler corresponding to this language.
fn gcc(self) -> &'static str {
match self {
Language::C => "gcc",
Language::CPlusPlus => "g++",
}
}

/// Obtains the name of a compiler in the clang suite.
/// Returns the executable name for a clang-based compiler corresponding to this language.
fn clang(self) -> &'static str {
match self {
Language::C => "clang",
Language::CPlusPlus => "clang++",
}
}
}

#[cfg(test)]
mod tests;
254 changes: 254 additions & 0 deletions src/bootstrap/src/utils/cc_detect/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use std::path::{Path, PathBuf};
use std::{env, iter};

use super::*;
use crate::core::config::{Target, TargetSelection};
use crate::{Build, Config, Flags};

#[test]
fn test_cc2ar_env_specific() {
let triple = "x86_64-unknown-linux-gnu";
let key = "AR_x86_64_unknown_linux_gnu";
env::set_var(key, "custom-ar");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
env::remove_var(key);
assert_eq!(result, Some(PathBuf::from("custom-ar")));
}

#[test]
fn test_cc2ar_musl() {
let triple = "x86_64-unknown-linux-musl";
env::remove_var("AR_x86_64_unknown_linux_musl");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("ar")));
}

#[test]
fn test_cc2ar_openbsd() {
let triple = "x86_64-unknown-openbsd";
env::remove_var("AR_x86_64_unknown_openbsd");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/cc");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("ar")));
}

#[test]
fn test_cc2ar_vxworks() {
let triple = "armv7-wrs-vxworks";
env::remove_var("AR_armv7_wrs_vxworks");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("wr-ar")));
}

#[test]
fn test_cc2ar_nto_i586() {
let triple = "i586-unknown-nto-something";
env::remove_var("AR_i586_unknown_nto_something");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("ntox86-ar")));
}

#[test]
fn test_cc2ar_nto_aarch64() {
let triple = "aarch64-unknown-nto-something";
env::remove_var("AR_aarch64_unknown_nto_something");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("ntoaarch64-ar")));
}

#[test]
fn test_cc2ar_nto_x86_64() {
let triple = "x86_64-unknown-nto-something";
env::remove_var("AR_x86_64_unknown_nto_something");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let result = cc2ar(cc, target, default_ar);
assert_eq!(result, Some(PathBuf::from("ntox86_64-ar")));
}

#[test]
#[should_panic(expected = "Unknown architecture, cannot determine archiver for Neutrino QNX")]
fn test_cc2ar_nto_unknown() {
let triple = "powerpc-unknown-nto-something";
env::remove_var("AR_powerpc_unknown_nto_something");
env::remove_var("AR");
let target = TargetSelection::from_user(triple);
let cc = Path::new("/usr/bin/clang");
let default_ar = PathBuf::from("default-ar");
let _ = cc2ar(cc, target, default_ar);
}

#[test]
fn test_ndk_compiler_c() {
let ndk_path = PathBuf::from("/ndk");
let target_triple = "arm-unknown-linux-android";
let expected_triple_translated = "armv7a-unknown-linux-android";
let expected_compiler = format!("{}21-{}", expected_triple_translated, Language::C.clang());
let host_tag = if cfg!(target_os = "macos") {
"darwin-x86_64"
} else if cfg!(target_os = "windows") {
"windows-x86_64"
} else {
"linux-x86_64"
};
let expected_path = ndk_path
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(host_tag)
.join("bin")
.join(&expected_compiler);
let result = ndk_compiler(Language::C, target_triple, &ndk_path);
assert_eq!(result, expected_path);
}

#[test]
fn test_ndk_compiler_cpp() {
let ndk_path = PathBuf::from("/ndk");
let target_triple = "arm-unknown-linux-android";
let expected_triple_translated = "armv7a-unknown-linux-android";
let expected_compiler =
format!("{}21-{}", expected_triple_translated, Language::CPlusPlus.clang());
let host_tag = if cfg!(target_os = "macos") {
"darwin-x86_64"
} else if cfg!(target_os = "windows") {
"windows-x86_64"
} else {
"linux-x86_64"
};
let expected_path = ndk_path
.join("toolchains")
.join("llvm")
.join("prebuilt")
.join(host_tag)
.join("bin")
.join(&expected_compiler);
let result = ndk_compiler(Language::CPlusPlus, target_triple, &ndk_path);
assert_eq!(result, expected_path);
}

#[test]
fn test_language_gcc() {
assert_eq!(Language::C.gcc(), "gcc");
assert_eq!(Language::CPlusPlus.gcc(), "g++");
}

#[test]
fn test_language_clang() {
assert_eq!(Language::C.clang(), "clang");
assert_eq!(Language::CPlusPlus.clang(), "clang++");
}

#[test]
fn test_new_cc_build() {
let build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target = TargetSelection::from_user("x86_64-unknown-linux-gnu");
let cfg = new_cc_build(&build, target.clone());
let compiler = cfg.get_compiler();
assert!(!compiler.path().to_str().unwrap().is_empty(), "Compiler path should not be empty");
}

#[test]
fn test_default_compiler_wasi() {
let build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target = TargetSelection::from_user("wasm32-wasi");
let wasi_sdk = PathBuf::from("/wasi-sdk");
env::set_var("WASI_SDK_PATH", &wasi_sdk);
let mut cfg = cc::Build::new();
if let Some(result) = default_compiler(&mut cfg, Language::C, target.clone(), &build) {
let expected = {
let compiler = format!("{}-clang", target.triple);
wasi_sdk.join("bin").join(compiler)
};
assert_eq!(result, expected);
} else {
panic!(
"default_compiler should return a compiler path for wasi target when WASI_SDK_PATH is set"
);
}
env::remove_var("WASI_SDK_PATH");
}

#[test]
fn test_default_compiler_fallback() {
let build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target = TargetSelection::from_user("x86_64-unknown-linux-gnu");
let mut cfg = cc::Build::new();
let result = default_compiler(&mut cfg, Language::C, target, &build);
assert!(result.is_none(), "default_compiler should return None for generic targets");
}

#[test]
fn test_find_target_with_config() {
let mut build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target = TargetSelection::from_user("x86_64-unknown-linux-gnu");
let mut target_config = Target::default();
target_config.cc = Some(PathBuf::from("dummy-cc"));
target_config.cxx = Some(PathBuf::from("dummy-cxx"));
target_config.ar = Some(PathBuf::from("dummy-ar"));
target_config.ranlib = Some(PathBuf::from("dummy-ranlib"));
build.config.target_config.insert(target.clone(), target_config);
find_target(&build, target.clone());
let binding = build.cc.borrow();
let cc_tool = binding.get(&target).unwrap();
assert_eq!(cc_tool.path(), &PathBuf::from("dummy-cc"));
let binding = build.cxx.borrow();
let cxx_tool = binding.get(&target).unwrap();
assert_eq!(cxx_tool.path(), &PathBuf::from("dummy-cxx"));
let binding = build.ar.borrow();
let ar = binding.get(&target).unwrap();
assert_eq!(ar, &PathBuf::from("dummy-ar"));
let binding = build.ranlib.borrow();
let ranlib = binding.get(&target).unwrap();
assert_eq!(ranlib, &PathBuf::from("dummy-ranlib"));
}

#[test]
fn test_find_target_without_config() {
let mut build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target = TargetSelection::from_user("x86_64-unknown-linux-gnu");
build.config.target_config.clear();
find_target(&build, target.clone());
assert!(build.cc.borrow().contains_key(&target));
if !target.triple.contains("vxworks") {
assert!(build.cxx.borrow().contains_key(&target));
}
assert!(build.ar.borrow().contains_key(&target));
}

#[test]
fn test_find() {
let mut build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
let target1 = TargetSelection::from_user("x86_64-unknown-linux-gnu");
let target2 = TargetSelection::from_user("arm-linux-androideabi");
build.targets.push(target1.clone());
build.hosts.push(target2.clone());
find(&build);
for t in build.hosts.iter().chain(build.targets.iter()).chain(iter::once(&build.build)) {
assert!(build.cc.borrow().contains_key(t), "CC not set for target {}", t.triple);
}
}
Loading