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