Skip to main content

bootstrap/utils/
helpers.rs

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.
5
6use 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};
12
13use build_helper::ci::CiEnv;
14use object::read::archive::ArchiveFile;
15pub(crate) use shim_utils::{dylib_path, dylib_path_var};
16
17pub(crate) use self::macros::t;
18use crate::core::builder::{Builder, StepStack};
19use crate::core::config::{BootstrapOverrideLld, Config, TargetSelection};
20use crate::utils::exec::{BootstrapCommand, command};
21
22#[cfg(test)]
23mod tests;
24
25/// A wrapper around `std::panic::Location` used to track the location of panics
26/// triggered by `t` macro usage.
27pub struct PanicTracker<'a>(pub &'a panic::Location<'a>);
28
29impl Drop for PanicTracker<'_> {
30    fn drop(&mut self) {
31        if panicking() {
32            eprintln!(
33                "Panic was initiated from {}:{}:{}",
34                self.0.file(),
35                self.0.line(),
36                self.0.column()
37            );
38        }
39    }
40}
41
42mod macros {
43    /// A helper macro to `unwrap` a result except also print out details like:
44    ///
45    /// * The file/line of the panic
46    /// * The expression that failed
47    /// * The error itself
48    ///
49    /// This is currently used judiciously throughout the build system rather than
50    /// using a `Result` with `try!`, but this may change one day...
51    macro_rules! t {
52        ($e:expr) => {{
53            let _panic_guard = $crate::utils::helpers::PanicTracker(std::panic::Location::caller());
54            match $e {
55                Ok(e) => e,
56                Err(e) => panic!("{} failed with {}", stringify!($e), e),
57            }
58        }};
59        // it can show extra info in the second parameter
60        ($e:expr, $extra:expr) => {{
61            let _panic_guard = $crate::utils::helpers::PanicTracker(std::panic::Location::caller());
62            match $e {
63                Ok(e) => e,
64                Err(e) => panic!("{} failed with {} ({:?})", stringify!($e), e, $extra),
65            }
66        }};
67    }
68
69    pub(crate) use t;
70}
71
72pub fn exe(name: &str, target: TargetSelection) -> String {
73    shim_utils::exe(name, &target.triple)
74}
75
76/// Returns the path to the split debug info for the specified file if it exists.
77pub fn split_debuginfo(name: impl Into<PathBuf>) -> Option<PathBuf> {
78    // FIXME: only msvc is currently supported
79
80    let path = name.into();
81    let pdb = path.with_extension("pdb");
82    if pdb.exists() {
83        return Some(pdb);
84    }
85
86    // pdbs get named with '-' replaced by '_'
87    let file_name = pdb.file_name()?.to_str()?.replace("-", "_");
88
89    let pdb: PathBuf = [path.parent()?, Path::new(&file_name)].into_iter().collect();
90    pdb.exists().then_some(pdb)
91}
92
93/// Returns `true` if the file name given looks like a dynamic library.
94pub fn is_dylib(path: &Path) -> bool {
95    path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| {
96        ext == "dylib" || ext == "so" || ext == "dll" || (ext == "a" && is_aix_shared_archive(path))
97    })
98}
99
100/// Return the path to the containing submodule if available.
101pub fn submodule_path_of(builder: &Builder<'_>, path: &str) -> Option<String> {
102    submodule_path_of_paths(builder.submodule_paths(), path)
103}
104
105fn submodule_path_of_paths(submodule_paths: &[String], path: &str) -> Option<String> {
106    let path = Path::new(path);
107    submodule_paths.iter().find_map(|submodule_path| {
108        if path.starts_with(submodule_path) { Some(submodule_path.to_string()) } else { None }
109    })
110}
111
112fn is_aix_shared_archive(path: &Path) -> bool {
113    let file = match fs::File::open(path) {
114        Ok(file) => file,
115        Err(_) => return false,
116    };
117    let reader = object::ReadCache::new(file);
118    let archive = match ArchiveFile::parse(&reader) {
119        Ok(result) => result,
120        Err(_) => return false,
121    };
122
123    archive
124        .members()
125        .filter_map(Result::ok)
126        .any(|entry| String::from_utf8_lossy(entry.name()).contains(".so"))
127}
128
129/// Returns `true` if the file name given looks like a debug info file
130pub fn is_debug_info(name: &str) -> bool {
131    // FIXME: consider split debug info on other platforms (e.g., Linux, macOS)
132    name.ends_with(".pdb")
133}
134
135/// Returns the corresponding relative library directory that the compiler's
136/// dylibs will be found in.
137pub fn libdir(target: TargetSelection) -> &'static str {
138    if target.is_windows() || target.contains("cygwin") { "bin" } else { "lib" }
139}
140
141/// Adds a list of lookup paths to `cmd`'s dynamic library lookup path.
142/// If the dylib_path_var is already set for this cmd, the old value will be overwritten!
143pub fn add_dylib_path(path: Vec<PathBuf>, cmd: &mut BootstrapCommand) {
144    let mut list = dylib_path();
145    for path in path {
146        list.insert(0, path);
147    }
148    cmd.env(dylib_path_var(), t!(env::join_paths(list)));
149}
150
151pub struct TimeIt(bool, Instant);
152
153/// Returns an RAII structure that prints out how long it took to drop.
154pub fn timeit(builder: &Builder<'_>) -> TimeIt {
155    TimeIt(builder.config.dry_run(), Instant::now())
156}
157
158impl Drop for TimeIt {
159    fn drop(&mut self) {
160        let time = self.1.elapsed();
161        if !self.0 {
162            println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());
163        }
164    }
165}
166
167/// Symlinks two directories, using junctions on Windows and normal symlinks on
168/// Unix.
169pub fn symlink_dir(config: &Config, original: &Path, link: &Path) -> io::Result<()> {
170    if config.dry_run() {
171        return Ok(());
172    }
173    let _ = fs::remove_dir_all(link);
174    return symlink_dir_inner(original, link);
175
176    #[cfg(not(windows))]
177    fn symlink_dir_inner(original: &Path, link: &Path) -> io::Result<()> {
178        use std::os::unix::fs;
179        fs::symlink(original, link)
180    }
181
182    #[cfg(windows)]
183    fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> {
184        junction::create(target, junction)
185    }
186}
187
188/// Return the host target on which we are currently running.
189pub fn get_host_target() -> TargetSelection {
190    TargetSelection::from_user(env!("BUILD_TRIPLE"))
191}
192
193/// Rename a file if from and to are in the same filesystem or
194/// copy and remove the file otherwise
195pub fn move_file<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
196    match fs::rename(&from, &to) {
197        Err(e) if e.kind() == io::ErrorKind::CrossesDevices => {
198            std::fs::copy(&from, &to)?;
199            std::fs::remove_file(&from)
200        }
201        r => r,
202    }
203}
204
205pub fn forcing_clang_based_tests() -> bool {
206    if let Some(var) = env::var_os("RUSTBUILD_FORCE_CLANG_BASED_TESTS") {
207        match &var.to_string_lossy().to_lowercase()[..] {
208            "1" | "yes" | "on" => true,
209            "0" | "no" | "off" => false,
210            other => {
211                // Let's make sure typos don't go unnoticed
212                panic!(
213                    "Unrecognized option '{other}' set in \
214                        RUSTBUILD_FORCE_CLANG_BASED_TESTS"
215                )
216            }
217        }
218    } else {
219        false
220    }
221}
222
223pub fn use_host_linker(target: TargetSelection) -> bool {
224    // FIXME: this information should be gotten by checking the linker flavor
225    // of the rustc target
226    !(target.contains("emscripten")
227        || target.contains("wasm32")
228        || target.contains("nvptx")
229        || target.contains("fortanix")
230        || target.contains("fuchsia")
231        || target.contains("bpf")
232        || target.contains("switch")
233        || target.contains("l4re"))
234}
235
236pub fn target_supports_cranelift_backend(target: TargetSelection) -> bool {
237    if target.contains("linux") {
238        target.contains("x86_64")
239            || target.contains("aarch64")
240            || target.contains("s390x")
241            || target.contains("riscv64gc")
242    } else if target.contains("darwin") {
243        target.contains("x86_64") || target.contains("aarch64")
244    } else if target.is_windows() {
245        target.contains("x86_64")
246    } else {
247        false
248    }
249}
250
251/// Value returned from [`is_valid_test_suite_arg`], which figures out which paths start with the
252/// suite name (and therefore which should be run).
253pub enum TestFilterCategory<'a> {
254    /// If a path is equal to the name of the suite, this is returned.
255    Fullsuite,
256    /// If a path starts with the suite, the suite prefix is stripped and the rest is returned as
257    /// this variant.
258    Arg(&'a str),
259    /// For paths that don't start with the suite.
260    Uninteresting,
261}
262
263pub fn is_valid_test_suite_arg<'a, P: AsRef<Path>>(
264    path: &'a Path,
265    suite_path: P,
266    builder: &Builder<'_>,
267) -> TestFilterCategory<'a> {
268    let suite_path = suite_path.as_ref();
269    let path = match path.strip_prefix(".") {
270        Ok(p) => p,
271        Err(_) => path,
272    };
273    if !path.starts_with(suite_path) {
274        return TestFilterCategory::Uninteresting;
275    }
276    let abs_path = builder.src.join(path);
277    let exists = abs_path.is_dir() || abs_path.is_file();
278    if !exists {
279        panic!(
280            "Invalid test suite filter \"{}\": file or directory does not exist",
281            abs_path.display()
282        );
283    }
284    // Since test suite paths are themselves directories, if we don't
285    // specify a directory or file, we'll get an empty string here
286    // (the result of the test suite directory without its suite prefix).
287    // Therefore, we need to filter these out, as only the first --test-args
288    // flag is respected, so providing an empty --test-args conflicts with
289    // any following it.
290    match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {
291        Some(s) if !s.is_empty() => TestFilterCategory::Arg(s),
292        _ => TestFilterCategory::Fullsuite,
293    }
294}
295
296pub fn make(host: &str) -> PathBuf {
297    if host.contains("dragonfly")
298        || host.contains("freebsd")
299        || host.contains("netbsd")
300        || host.contains("openbsd")
301    {
302        PathBuf::from("gmake")
303    } else {
304        PathBuf::from("make")
305    }
306}
307
308/// Returns the last-modified time for `path`, or zero if it doesn't exist.
309pub fn mtime(path: &Path) -> SystemTime {
310    fs::metadata(path).and_then(|f| f.modified()).unwrap_or(UNIX_EPOCH)
311}
312
313/// Returns `true` if `dst` is up to date given that the file or files in `src`
314/// are used to generate it.
315///
316/// Uses last-modified time checks to verify this.
317pub fn up_to_date(src: &Path, dst: &Path) -> bool {
318    if !dst.exists() {
319        return false;
320    }
321    let threshold = mtime(dst);
322    let meta = match fs::metadata(src) {
323        Ok(meta) => meta,
324        Err(e) => panic!("source {src:?} failed to get metadata: {e}"),
325    };
326    if meta.is_dir() {
327        dir_up_to_date(src, threshold)
328    } else {
329        meta.modified().unwrap_or(UNIX_EPOCH) <= threshold
330    }
331}
332
333/// Returns the filename without the hash prefix added by the cc crate.
334///
335/// Since v1.0.78 of the cc crate, object files are prefixed with a 16-character hash
336/// to avoid filename collisions.
337pub fn unhashed_basename(obj: &Path) -> &str {
338    let basename = obj.file_stem().unwrap().to_str().expect("UTF-8 file name");
339    basename.split_once('-').unwrap().1
340}
341
342fn dir_up_to_date(src: &Path, threshold: SystemTime) -> bool {
343    t!(fs::read_dir(src)).map(|e| t!(e)).all(|e| {
344        let meta = t!(e.metadata());
345        if meta.is_dir() {
346            dir_up_to_date(&e.path(), threshold)
347        } else {
348            meta.modified().unwrap_or(UNIX_EPOCH) < threshold
349        }
350    })
351}
352
353/// Adapted from <https://github.com/llvm/llvm-project/blob/782e91224601e461c019e0a4573bbccc6094fbcd/llvm/cmake/modules/HandleLLVMOptions.cmake#L1058-L1079>
354///
355/// When `clang-cl` is used with instrumentation, we need to add clang's runtime library resource
356/// directory to the linker flags, otherwise there will be linker errors about the profiler runtime
357/// missing. This function returns the path to that directory.
358pub fn get_clang_cl_resource_dir(builder: &Builder<'_>, clang_cl_path: &str) -> PathBuf {
359    // Similar to how LLVM does it, to find clang's library runtime directory:
360    // - we ask `clang-cl` to locate the `clang_rt.builtins` lib.
361    let mut builtins_locator = command(clang_cl_path);
362    builtins_locator.args(["/clang:-print-libgcc-file-name", "/clang:--rtlib=compiler-rt"]);
363
364    let clang_rt_builtins = builtins_locator.run_capture_stdout(builder).stdout();
365    let clang_rt_builtins = Path::new(clang_rt_builtins.trim());
366    assert!(
367        clang_rt_builtins.exists(),
368        "`clang-cl` must correctly locate the library runtime directory"
369    );
370
371    // - the profiler runtime will be located in the same directory as the builtins lib, like
372    // `$LLVM_DISTRO_ROOT/lib/clang/$LLVM_VERSION/lib/windows`.
373    let clang_rt_dir = clang_rt_builtins.parent().expect("The clang lib folder should exist");
374    clang_rt_dir.to_path_buf()
375}
376
377/// Returns a flag that configures LLD to use only a single thread.
378/// If we use an external LLD, we need to find out which version is it to know which flag should we
379/// pass to it (LLD older than version 10 had a different flag).
380fn lld_flag_no_threads(
381    builder: &Builder<'_>,
382    bootstrap_override_lld: BootstrapOverrideLld,
383    is_windows: bool,
384) -> &'static str {
385    static LLD_NO_THREADS: OnceLock<(&'static str, &'static str)> = OnceLock::new();
386
387    let new_flags = ("/threads:1", "--threads=1");
388    let old_flags = ("/no-threads", "--no-threads");
389
390    let (windows_flag, other_flag) = LLD_NO_THREADS.get_or_init(|| {
391        let newer_version = match bootstrap_override_lld {
392            BootstrapOverrideLld::External => {
393                let mut cmd = command("lld");
394                cmd.arg("-flavor").arg("ld").arg("--version");
395                let out = cmd.run_capture_stdout(builder).stdout();
396                match (out.find(char::is_numeric), out.find('.')) {
397                    (Some(b), Some(e)) => out.as_str()[b..e].parse::<i32>().ok().unwrap_or(14) > 10,
398                    _ => true,
399                }
400            }
401            _ => true,
402        };
403        if newer_version { new_flags } else { old_flags }
404    });
405    if is_windows { windows_flag } else { other_flag }
406}
407
408pub fn dir_is_empty(dir: &Path) -> bool {
409    t!(std::fs::read_dir(dir), dir).next().is_none()
410}
411
412/// Extract the beta revision from the full version string.
413///
414/// The full version string looks like "a.b.c-beta.y". And we need to extract
415/// the "y" part from the string.
416pub fn extract_beta_rev(version: &str) -> Option<String> {
417    let parts = version.splitn(2, "-beta.").collect::<Vec<_>>();
418    parts.get(1).and_then(|s| s.find(' ').map(|p| s[..p].to_string()))
419}
420
421pub enum LldThreads {
422    Yes,
423    No,
424}
425
426/// Returns the linker arguments for rustc/rustdoc for the given builder and target.
427pub fn linker_args(
428    builder: &Builder<'_>,
429    target: TargetSelection,
430    lld_threads: LldThreads,
431) -> Vec<String> {
432    let mut args = linker_flags(builder, target, lld_threads);
433
434    if let Some(linker) = builder.linker(target) {
435        args.push(format!("-Clinker={}", linker.display()));
436    }
437
438    args
439}
440
441/// Returns the linker arguments for rustc/rustdoc for the given builder and target, without the
442/// -Clinker flag.
443pub fn linker_flags(
444    builder: &Builder<'_>,
445    target: TargetSelection,
446    lld_threads: LldThreads,
447) -> Vec<String> {
448    let mut args = vec![];
449    if !builder.is_lld_direct_linker(target) && builder.config.bootstrap_override_lld.is_used() {
450        match builder.config.bootstrap_override_lld {
451            BootstrapOverrideLld::External => {
452                args.push("-Clinker-features=+lld".to_string());
453                args.push("-Clink-self-contained=-linker".to_string());
454                args.push("-Zunstable-options".to_string());
455            }
456            BootstrapOverrideLld::SelfContained => {
457                args.push("-Clinker-features=+lld".to_string());
458                args.push("-Clink-self-contained=+linker".to_string());
459                args.push("-Zunstable-options".to_string());
460            }
461            BootstrapOverrideLld::None => unreachable!(),
462        };
463
464        if matches!(lld_threads, LldThreads::No) {
465            args.push(format!(
466                "-Clink-arg=-Wl,{}",
467                lld_flag_no_threads(
468                    builder,
469                    builder.config.bootstrap_override_lld,
470                    target.is_windows()
471                )
472            ));
473        }
474    }
475    args
476}
477
478pub fn add_rustdoc_cargo_linker_args(
479    cmd: &mut BootstrapCommand,
480    builder: &Builder<'_>,
481    target: TargetSelection,
482    lld_threads: LldThreads,
483) {
484    let args = linker_args(builder, target, lld_threads);
485    let mut flags = cmd
486        .get_envs()
487        .find_map(|(k, v)| if k == OsStr::new("RUSTDOCFLAGS") { v } else { None })
488        .unwrap_or_default()
489        .to_os_string();
490    for arg in args {
491        if !flags.is_empty() {
492            flags.push(" ");
493        }
494        flags.push(arg);
495    }
496    if !flags.is_empty() {
497        cmd.env("RUSTDOCFLAGS", flags);
498    }
499}
500
501/// Converts `T` into a hexadecimal `String`.
502pub fn hex_encode<T>(input: T) -> String
503where
504    T: AsRef<[u8]>,
505{
506    use std::fmt::Write;
507
508    input.as_ref().iter().fold(String::with_capacity(input.as_ref().len() * 2), |mut acc, &byte| {
509        write!(&mut acc, "{byte:02x}").expect("Failed to write byte to the hex String.");
510        acc
511    })
512}
513
514/// Create a `--check-cfg` argument invocation for a given name
515/// and it's values.
516pub fn check_cfg_arg(name: &str, values: Option<&[&str]>) -> String {
517    // Creating a string of the values by concatenating each value:
518    // ',values("tvos","watchos")' or '' (nothing) when there are no values.
519    let next = match values {
520        Some(values) => {
521            let mut tmp = values.iter().flat_map(|val| [",", "\"", val, "\""]).collect::<String>();
522
523            tmp.insert_str(1, "values(");
524            tmp.push(')');
525            tmp
526        }
527        None => "".to_string(),
528    };
529    format!("--check-cfg=cfg({name}{next})")
530}
531
532/// Prepares `BootstrapCommand` that runs git inside the source directory if given.
533///
534/// Whenever a git invocation is needed, this function should be preferred over
535/// manually building a git `BootstrapCommand`. This approach allows us to manage
536/// bootstrap-specific needs/hacks from a single source, rather than applying them on next to every
537/// git command creation, which is painful to ensure that the required change is applied
538/// on each one of them correctly.
539#[track_caller]
540pub fn git(source_dir: Option<&Path>) -> BootstrapCommand {
541    let mut git = command("git");
542    // git commands are almost always read-only, so cache them by default
543    git.cached();
544
545    if let Some(source_dir) = source_dir {
546        git.current_dir(source_dir);
547        // If we are running inside git (e.g. via a hook), `GIT_DIR` is set and takes precedence
548        // over the current dir. Un-set it to make the current dir matter.
549        git.env_remove("GIT_DIR");
550        // Also un-set some other variables, to be on the safe side (based on cargo's
551        // `fetch_with_cli`). In particular un-setting `GIT_INDEX_FILE` is required to fix some odd
552        // misbehavior.
553        git.env_remove("GIT_WORK_TREE")
554            .env_remove("GIT_INDEX_FILE")
555            .env_remove("GIT_OBJECT_DIRECTORY")
556            .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES");
557    }
558
559    git
560}
561
562/// Sets the file times for a given file at `path`.
563pub fn set_file_times<P: AsRef<Path>>(path: P, times: fs::FileTimes) -> io::Result<()> {
564    // Windows requires file to be writable to modify file times. But on Linux CI the file does not
565    // need to be writable to modify file times and might be read-only.
566    let f = if cfg!(windows) {
567        fs::File::options().write(true).open(path)?
568    } else {
569        fs::File::open(path)?
570    };
571    f.set_times(times)
572}
573
574/// Exits the process by calling [`std::process::exit`].
575///
576/// In CI, extra information will be printed to make failures easier to investigate.
577///
578/// If `cfg!(test)` is true, this will panic instead of exiting the process.
579/// Doing so avoids disturbing other tests in the process, and allows `#[should_panic]`
580/// to detect expected failures.
581pub(crate) fn exit_process(code: i32) -> ! {
582    // In bootstrap unit tests, panic instead of killing the whole test process.
583    if cfg!(test) {
584        panic!("status code: {code}");
585    } else {
586        // If we're in CI, print the current bootstrap invocation command, to make it easier to
587        // figure out what exactly has failed.
588        if CiEnv::is_ci() {
589            // Skip the first argument, as it will be some absolute path to the bootstrap binary.
590            let bootstrap_args =
591                std::env::args().skip(1).map(|a| a.to_string()).collect::<Vec<_>>().join(" ");
592            eprintln!("Bootstrap failed while executing `{bootstrap_args}`");
593            eprintln!("Currently active steps:");
594            StepStack::with_current(|stack| {
595                for step in stack.get_active_steps() {
596                    eprintln!("{} at {}", step.info, step.location);
597                }
598            });
599        }
600
601        // otherwise, exit with provided status code
602        std::process::exit(code);
603    }
604}
605
606pub fn fail(s: &str) -> ! {
607    eprintln!("\n\n{s}\n\n");
608    exit_process(1);
609}