1//! Various utility functions used throughout bootstrap.2//!3//! Simple things like testing the various filesystem operations here and there,4//! not a lot of interesting happenings here unfortunately.56use std::ffi::OsStr;7use std::path::{Path, PathBuf};8use std::sync::OnceLock;9use std::thread::panicking;10use std::time::{Instant, SystemTime, UNIX_EPOCH};11use std::{env, fs, io, panic, str};1213use object::read::archive::ArchiveFile;1415use crate::BootstrapOverrideLld;16use crate::core::builder::Builder;17use crate::core::config::{Config, TargetSelection};18use crate::utils::exec::{BootstrapCommand, command};19pub use crate::utils::shared_helpers::{dylib_path, dylib_path_var};2021#[cfg(test)]22mod tests;2324/// A wrapper around `std::panic::Location` used to track the location of panics25/// triggered by `t` macro usage.26pub struct PanicTracker<'a>(pub &'a panic::Location<'a>);2728impl Drop for PanicTracker<'_> {29 fn drop(&mut self) {30 if panicking() {31 eprintln!(32 "Panic was initiated from {}:{}:{}",33 self.0.file(),34 self.0.line(),35 self.0.column()36 );37 }38 }39}4041/// A helper macro to `unwrap` a result except also print out details like:42///43/// * The file/line of the panic44/// * The expression that failed45/// * The error itself46///47/// This is currently used judiciously throughout the build system rather than48/// using a `Result` with `try!`, but this may change one day...49#[macro_export]50macro_rules! t {51 ($e:expr) => {{52 let _panic_guard = $crate::PanicTracker(std::panic::Location::caller());53 match $e {54 Ok(e) => e,55 Err(e) => panic!("{} failed with {}", stringify!($e), e),56 }57 }};58 // it can show extra info in the second parameter59 ($e:expr, $extra:expr) => {{60 let _panic_guard = $crate::PanicTracker(std::panic::Location::caller());61 match $e {62 Ok(e) => e,63 Err(e) => panic!("{} failed with {} ({:?})", stringify!($e), e, $extra),64 }65 }};66}6768pub use t;69pub fn exe(name: &str, target: TargetSelection) -> String {70 crate::utils::shared_helpers::exe(name, &target.triple)71}7273/// Returns the path to the split debug info for the specified file if it exists.74pub fn split_debuginfo(name: impl Into<PathBuf>) -> Option<PathBuf> {75 // FIXME: only msvc is currently supported7677 let path = name.into();78 let pdb = path.with_extension("pdb");79 if pdb.exists() {80 return Some(pdb);81 }8283 // pdbs get named with '-' replaced by '_'84 let file_name = pdb.file_name()?.to_str()?.replace("-", "_");8586 let pdb: PathBuf = [path.parent()?, Path::new(&file_name)].into_iter().collect();87 pdb.exists().then_some(pdb)88}8990/// Returns `true` if the file name given looks like a dynamic library.91pub fn is_dylib(path: &Path) -> bool {92 path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| {93 ext == "dylib" || ext == "so" || ext == "dll" || (ext == "a" && is_aix_shared_archive(path))94 })95}9697/// Return the path to the containing submodule if available.98pub fn submodule_path_of(builder: &Builder<'_>, path: &str) -> Option<String> {99 let submodule_paths = builder.submodule_paths();100 submodule_paths.iter().find_map(|submodule_path| {101 if path.starts_with(submodule_path) { Some(submodule_path.to_string()) } else { None }102 })103}104105fn is_aix_shared_archive(path: &Path) -> bool {106 let file = match fs::File::open(path) {107 Ok(file) => file,108 Err(_) => return false,109 };110 let reader = object::ReadCache::new(file);111 let archive = match ArchiveFile::parse(&reader) {112 Ok(result) => result,113 Err(_) => return false,114 };115116 archive117 .members()118 .filter_map(Result::ok)119 .any(|entry| String::from_utf8_lossy(entry.name()).contains(".so"))120}121122/// Returns `true` if the file name given looks like a debug info file123pub fn is_debug_info(name: &str) -> bool {124 // FIXME: consider split debug info on other platforms (e.g., Linux, macOS)125 name.ends_with(".pdb")126}127128/// Returns the corresponding relative library directory that the compiler's129/// dylibs will be found in.130pub fn libdir(target: TargetSelection) -> &'static str {131 if target.is_windows() || target.contains("cygwin") { "bin" } else { "lib" }132}133134/// Adds a list of lookup paths to `cmd`'s dynamic library lookup path.135/// If the dylib_path_var is already set for this cmd, the old value will be overwritten!136pub fn add_dylib_path(path: Vec<PathBuf>, cmd: &mut BootstrapCommand) {137 let mut list = dylib_path();138 for path in path {139 list.insert(0, path);140 }141 cmd.env(dylib_path_var(), t!(env::join_paths(list)));142}143144pub struct TimeIt(bool, Instant);145146/// Returns an RAII structure that prints out how long it took to drop.147pub fn timeit(builder: &Builder<'_>) -> TimeIt {148 TimeIt(builder.config.dry_run(), Instant::now())149}150151impl Drop for TimeIt {152 fn drop(&mut self) {153 let time = self.1.elapsed();154 if !self.0 {155 println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());156 }157 }158}159160/// Symlinks two directories, using junctions on Windows and normal symlinks on161/// Unix.162pub fn symlink_dir(config: &Config, original: &Path, link: &Path) -> io::Result<()> {163 if config.dry_run() {164 return Ok(());165 }166 let _ = fs::remove_dir_all(link);167 return symlink_dir_inner(original, link);168169 #[cfg(not(windows))]170 fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> {171 use std::os::unix::fs;172 fs::symlink(original, link)173 }174175 #[cfg(windows)]176 fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> {177 junction::create(target, junction)178 }179}180181/// Return the host target on which we are currently running.182pub fn get_host_target() -> TargetSelection {183 TargetSelection::from_user(env!("BUILD_TRIPLE"))184}185186/// Rename a file if from and to are in the same filesystem or187/// copy and remove the file otherwise188pub fn move_file<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {189 match fs::rename(&from, &to) {190 Err(e) if e.kind() == io::ErrorKind::CrossesDevices => {191 std::fs::copy(&from, &to)?;192 std::fs::remove_file(&from)193 }194 r => r,195 }196}197198pub fn forcing_clang_based_tests() -> bool {199 if let Some(var) = env::var_os("RUSTBUILD_FORCE_CLANG_BASED_TESTS") {200 match &var.to_string_lossy().to_lowercase()[..] {201 "1" | "yes" | "on" => true,202 "0" | "no" | "off" => false,203 other => {204 // Let's make sure typos don't go unnoticed205 panic!(206 "Unrecognized option '{other}' set in \207 RUSTBUILD_FORCE_CLANG_BASED_TESTS"208 )209 }210 }211 } else {212 false213 }214}215216pub fn use_host_linker(target: TargetSelection) -> bool {217 // FIXME: this information should be gotten by checking the linker flavor218 // of the rustc target219 !(target.contains("emscripten")220 || target.contains("wasm32")221 || target.contains("nvptx")222 || target.contains("fortanix")223 || target.contains("fuchsia")224 || target.contains("bpf")225 || target.contains("switch"))226}227228pub fn target_supports_cranelift_backend(target: TargetSelection) -> bool {229 if target.contains("linux") {230 target.contains("x86_64")231 || target.contains("aarch64")232 || target.contains("s390x")233 || target.contains("riscv64gc")234 } else if target.contains("darwin") {235 target.contains("x86_64") || target.contains("aarch64")236 } else if target.is_windows() {237 target.contains("x86_64")238 } else {239 false240 }241}242243/// Value returned from [`is_valid_test_suite_arg`], which figures out which paths start with the244/// suite name (and therefore which should be run).245pub enum TestFilterCategory<'a> {246 /// If a path is equal to the name of the suite, this is returned.247 Fullsuite,248 /// If a path starts with the suite, the suite prefix is stripped and the rest is returned as249 /// this variant.250 Arg(&'a str),251 /// For paths that don't start with the suite.252 Uninteresting,253}254255pub fn is_valid_test_suite_arg<'a, P: AsRef<Path>>(256 path: &'a Path,257 suite_path: P,258 builder: &Builder<'_>,259) -> TestFilterCategory<'a> {260 let suite_path = suite_path.as_ref();261 let path = match path.strip_prefix(".") {262 Ok(p) => p,263 Err(_) => path,264 };265 if !path.starts_with(suite_path) {266 return TestFilterCategory::Uninteresting;267 }268 let abs_path = builder.src.join(path);269 let exists = abs_path.is_dir() || abs_path.is_file();270 if !exists {271 panic!(272 "Invalid test suite filter \"{}\": file or directory does not exist",273 abs_path.display()274 );275 }276 // Since test suite paths are themselves directories, if we don't277 // specify a directory or file, we'll get an empty string here278 // (the result of the test suite directory without its suite prefix).279 // Therefore, we need to filter these out, as only the first --test-args280 // flag is respected, so providing an empty --test-args conflicts with281 // any following it.282 match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {283 Some(s) if !s.is_empty() => TestFilterCategory::Arg(s),284 _ => TestFilterCategory::Fullsuite,285 }286}287288pub fn make(host: &str) -> PathBuf {289 if host.contains("dragonfly")290 || host.contains("freebsd")291 || host.contains("netbsd")292 || host.contains("openbsd")293 {294 PathBuf::from("gmake")295 } else {296 PathBuf::from("make")297 }298}299300/// Returns the last-modified time for `path`, or zero if it doesn't exist.301pub fn mtime(path: &Path) -> SystemTime {302 fs::metadata(path).and_then(|f| f.modified()).unwrap_or(UNIX_EPOCH)303}304305/// Returns `true` if `dst` is up to date given that the file or files in `src`306/// are used to generate it.307///308/// Uses last-modified time checks to verify this.309pub fn up_to_date(src: &Path, dst: &Path) -> bool {310 if !dst.exists() {311 return false;312 }313 let threshold = mtime(dst);314 let meta = match fs::metadata(src) {315 Ok(meta) => meta,316 Err(e) => panic!("source {src:?} failed to get metadata: {e}"),317 };318 if meta.is_dir() {319 dir_up_to_date(src, threshold)320 } else {321 meta.modified().unwrap_or(UNIX_EPOCH) <= threshold322 }323}324325/// Returns the filename without the hash prefix added by the cc crate.326///327/// Since v1.0.78 of the cc crate, object files are prefixed with a 16-character hash328/// to avoid filename collisions.329pub fn unhashed_basename(obj: &Path) -> &str {330 let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");331 basename.split_once('-').unwrap().1332}333334fn dir_up_to_date(src: &Path, threshold: SystemTime) -> bool {335 t!(fs::read_dir(src)).map(|e| t!(e)).all(|e| {336 let meta = t!(e.metadata());337 if meta.is_dir() {338 dir_up_to_date(&e.path(), threshold)339 } else {340 meta.modified().unwrap_or(UNIX_EPOCH) < threshold341 }342 })343}344345/// Adapted from <https://github.com/llvm/llvm-project/blob/782e91224601e461c019e0a4573bbccc6094fbcd/llvm/cmake/modules/HandleLLVMOptions.cmake#L1058-L1079>346///347/// When `clang-cl` is used with instrumentation, we need to add clang's runtime library resource348/// directory to the linker flags, otherwise there will be linker errors about the profiler runtime349/// missing. This function returns the path to that directory.350pub fn get_clang_cl_resource_dir(builder: &Builder<'_>, clang_cl_path: &str) -> PathBuf {351 // Similar to how LLVM does it, to find clang's library runtime directory:352 // - we ask `clang-cl` to locate the `clang_rt.builtins` lib.353 let mut builtins_locator = command(clang_cl_path);354 builtins_locator.args(["/clang:-print-libgcc-file-name", "/clang:--rtlib=compiler-rt"]);355356 let clang_rt_builtins = builtins_locator.run_capture_stdout(builder).stdout();357 let clang_rt_builtins = Path::new(clang_rt_builtins.trim());358 assert!(359 clang_rt_builtins.exists(),360 "`clang-cl` must correctly locate the library runtime directory"361 );362363 // - the profiler runtime will be located in the same directory as the builtins lib, like364 // `$LLVM_DISTRO_ROOT/lib/clang/$LLVM_VERSION/lib/windows`.365 let clang_rt_dir = clang_rt_builtins.parent().expect("The clang lib folder should exist");366 clang_rt_dir.to_path_buf()367}368369/// Returns a flag that configures LLD to use only a single thread.370/// If we use an external LLD, we need to find out which version is it to know which flag should we371/// pass to it (LLD older than version 10 had a different flag).372fn lld_flag_no_threads(373 builder: &Builder<'_>,374 bootstrap_override_lld: BootstrapOverrideLld,375 is_windows: bool,376) -> &'static str {377 static LLD_NO_THREADS: OnceLock<(&'static str, &'static str)> = OnceLock::new();378379 let new_flags = ("/threads:1", "--threads=1");380 let old_flags = ("/no-threads", "--no-threads");381382 let (windows_flag, other_flag) = LLD_NO_THREADS.get_or_init(|| {383 let newer_version = match bootstrap_override_lld {384 BootstrapOverrideLld::External => {385 let mut cmd = command("lld");386 cmd.arg("-flavor").arg("ld").arg("--version");387 let out = cmd.run_capture_stdout(builder).stdout();388 match (out.find(char::is_numeric), out.find('.')) {389 (Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,390 _ => true,391 }392 }393 _ => true,394 };395 if newer_version { new_flags } else { old_flags }396 });397 if is_windows { windows_flag } else { other_flag }398}399400pub fn dir_is_empty(dir: &Path) -> bool {401 t!(std::fs::read_dir(dir), dir).next().is_none()402}403404/// Extract the beta revision from the full version string.405///406/// The full version string looks like "a.b.c-beta.y". And we need to extract407/// the "y" part from the string.408pub fn extract_beta_rev(version: &str) -> Option<String> {409 let parts = version.splitn(2, "-beta.").collect::<Vec<_>>();410 parts.get(1).and_then(|s| s.find(' ').map(|p| s[..p].to_string()))411}412413pub enum LldThreads {414 Yes,415 No,416}417418/// Returns the linker arguments for rustc/rustdoc for the given builder and target.419pub fn linker_args(420 builder: &Builder<'_>,421 target: TargetSelection,422 lld_threads: LldThreads,423) -> Vec<String> {424 let mut args = linker_flags(builder, target, lld_threads);425426 if let Some(linker) = builder.linker(target) {427 args.push(format!("-Clinker={}", linker.display()));428 }429430 args431}432433/// Returns the linker arguments for rustc/rustdoc for the given builder and target, without the434/// -Clinker flag.435pub fn linker_flags(436 builder: &Builder<'_>,437 target: TargetSelection,438 lld_threads: LldThreads,439) -> Vec<String> {440 let mut args = vec![];441 if !builder.is_lld_direct_linker(target) && builder.config.bootstrap_override_lld.is_used() {442 match builder.config.bootstrap_override_lld {443 BootstrapOverrideLld::External => {444 args.push("-Clinker-features=+lld".to_string());445 args.push("-Clink-self-contained=-linker".to_string());446 args.push("-Zunstable-options".to_string());447 }448 BootstrapOverrideLld::SelfContained => {449 args.push("-Clinker-features=+lld".to_string());450 args.push("-Clink-self-contained=+linker".to_string());451 args.push("-Zunstable-options".to_string());452 }453 BootstrapOverrideLld::None => unreachable!(),454 };455456 if matches!(lld_threads, LldThreads::No) {457 args.push(format!(458 "-Clink-arg=-Wl,{}",459 lld_flag_no_threads(460 builder,461 builder.config.bootstrap_override_lld,462 target.is_windows()463 )464 ));465 }466 }467 args468}469470pub fn add_rustdoc_cargo_linker_args(471 cmd: &mut BootstrapCommand,472 builder: &Builder<'_>,473 target: TargetSelection,474 lld_threads: LldThreads,475) {476 let args = linker_args(builder, target, lld_threads);477 let mut flags = cmd478 .get_envs()479 .find_map(|(k, v)| if k == OsStr::new("RUSTDOCFLAGS") { v } else { None })480 .unwrap_or_default()481 .to_os_string();482 for arg in args {483 if !flags.is_empty() {484 flags.push(" ");485 }486 flags.push(arg);487 }488 if !flags.is_empty() {489 cmd.env("RUSTDOCFLAGS", flags);490 }491}492493/// Converts `T` into a hexadecimal `String`.494pub fn hex_encode<T>(input: T) -> String495where496 T: AsRef<[u8]>,497{498 use std::fmt::Write;499500 input.as_ref().iter().fold(String::with_capacity(input.as_ref().len() * 2), |mut acc, &byte| {501 write!(&mut acc, "{byte:02x}").expect("Failed to write byte to the hex String.");502 acc503 })504}505506/// Create a `--check-cfg` argument invocation for a given name507/// and it's values.508pub fn check_cfg_arg(name: &str, values: Option<&[&str]>) -> String {509 // Creating a string of the values by concatenating each value:510 // ',values("tvos","watchos")' or '' (nothing) when there are no values.511 let next = match values {512 Some(values) => {513 let mut tmp = values.iter().flat_map(|val| [",", "\"", val, "\""]).collect::<String>();514515 tmp.insert_str(1, "values(");516 tmp.push(')');517 tmp518 }519 None => "".to_string(),520 };521 format!("--check-cfg=cfg({name}{next})")522}523524/// Prepares `BootstrapCommand` that runs git inside the source directory if given.525///526/// Whenever a git invocation is needed, this function should be preferred over527/// manually building a git `BootstrapCommand`. This approach allows us to manage528/// bootstrap-specific needs/hacks from a single source, rather than applying them on next to every529/// git command creation, which is painful to ensure that the required change is applied530/// on each one of them correctly.531#[track_caller]532pub fn git(source_dir: Option<&Path>) -> BootstrapCommand {533 let mut git = command("git");534 // git commands are almost always read-only, so cache them by default535 git.cached();536537 if let Some(source_dir) = source_dir {538 git.current_dir(source_dir);539 // If we are running inside git (e.g. via a hook), `GIT_DIR` is set and takes precedence540 // over the current dir. Un-set it to make the current dir matter.541 git.env_remove("GIT_DIR");542 // Also un-set some other variables, to be on the safe side (based on cargo's543 // `fetch_with_cli`). In particular un-setting `GIT_INDEX_FILE` is required to fix some odd544 // misbehavior.545 git.env_remove("GIT_WORK_TREE")546 .env_remove("GIT_INDEX_FILE")547 .env_remove("GIT_OBJECT_DIRECTORY")548 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES");549 }550551 git552}553554/// Sets the file times for a given file at `path`.555pub fn set_file_times<P: AsRef<Path>>(path: P, times: fs::FileTimes) -> io::Result<()> {556 // Windows requires file to be writable to modify file times. But on Linux CI the file does not557 // need to be writable to modify file times and might be read-only.558 let f = if cfg!(windows) {559 fs::File::options().write(true).open(path)?560 } else {561 fs::File::open(path)?562 };563 f.set_times(times)564}
Code quality findings 18
Warning: Ignoring a Result or Option using 'let _ =' can hide errors or unexpected None values. Ensure the value is handled appropriately (match, if let, ?, expect) unless intentionally discarded with justification.
warning
correctness
discarded-result
let _ = fs::remove_dir_all(link);
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
match &var.to_string_lossy().to_lowercase()[..] {
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
/// Value returned from [`is_valid_test_suite_arg`], which figures out which paths start with the
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning
correctness
expect-usage
let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");
Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning
correctness
unwrap-usage
basename.split_once('-').unwrap().1
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning
correctness
expect-usage
let clang_rt_dir = clang_rt_builtins.parent().expect("The clang lib folder should exist");
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
(Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning
correctness
unchecked-indexing
parts.get(1).and_then(|s| s.find(' ').map(|p| s[..p].to_string()))
Warning: '.expect()' will panic with a custom message on None/Err. While better than unwrap() for debugging, prefer non-panicking error handling in production code (match, if let, ?).
warning
correctness
expect-usage
write!(&mut acc, "{byte:02x}").expect("Failed to write byte to the hex String.");
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info
correctness
unhandled-result
let file = match fs::File::open(path) {
Info: Direct printing to stdout/stderr. For application logging, prefer using a logging facade like `log` or `tracing` for better control over levels, formatting, and output destinations.
info
maintainability
println-macro
println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
let newer_version = match bootstrap_override_lld {
Info: Ensure 'match' statements are exhaustive. If matching on enums, consider adding a wildcard arm `_ => {}` only if necessary and intentional, as it suppresses warnings about unhandled variants.
info
correctness
match-wildcard
match (out.find(char::is_numeric), out.find('.')) {
Info: This standard library function returns a Result. Ensure the Result is handled properly (e.g., using '?', match, if let) rather than potentially panicking with .unwrap() or .expect().
info
correctness
unhandled-result
(Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
flags.push(" ");
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info
performance
push-without-reserve
flags.push(arg);