]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/util.rs
Rollup merge of #97627 - lcnr:comment-tick, r=Dylan-DPC
[rust.git] / src / bootstrap / util.rs
1 //! Various utility functions used throughout rustbuild.
2 //!
3 //! Simple things like testing the various filesystem operations here and there,
4 //! not a lot of interesting happenings here unfortunately.
5
6 use std::env;
7 use std::fs;
8 use std::io;
9 use std::path::{Path, PathBuf};
10 use std::process::{Command, Stdio};
11 use std::str;
12 use std::time::{Instant, SystemTime, UNIX_EPOCH};
13
14 use crate::builder::Builder;
15 use crate::config::{Config, TargetSelection};
16
17 /// A helper macro to `unwrap` a result except also print out details like:
18 ///
19 /// * The file/line of the panic
20 /// * The expression that failed
21 /// * The error itself
22 ///
23 /// This is currently used judiciously throughout the build system rather than
24 /// using a `Result` with `try!`, but this may change one day...
25 macro_rules! t {
26     ($e:expr) => {
27         match $e {
28             Ok(e) => e,
29             Err(e) => panic!("{} failed with {}", stringify!($e), e),
30         }
31     };
32     // it can show extra info in the second parameter
33     ($e:expr, $extra:expr) => {
34         match $e {
35             Ok(e) => e,
36             Err(e) => panic!("{} failed with {} ({:?})", stringify!($e), e, $extra),
37         }
38     };
39 }
40 pub(crate) use t;
41
42 /// Given an executable called `name`, return the filename for the
43 /// executable for a particular target.
44 pub fn exe(name: &str, target: TargetSelection) -> String {
45     if target.contains("windows") { format!("{}.exe", name) } else { name.to_string() }
46 }
47
48 /// Returns `true` if the file name given looks like a dynamic library.
49 pub fn is_dylib(name: &str) -> bool {
50     name.ends_with(".dylib") || name.ends_with(".so") || name.ends_with(".dll")
51 }
52
53 /// Returns `true` if the file name given looks like a debug info file
54 pub fn is_debug_info(name: &str) -> bool {
55     // FIXME: consider split debug info on other platforms (e.g., Linux, macOS)
56     name.ends_with(".pdb")
57 }
58
59 /// Returns the corresponding relative library directory that the compiler's
60 /// dylibs will be found in.
61 pub fn libdir(target: TargetSelection) -> &'static str {
62     if target.contains("windows") { "bin" } else { "lib" }
63 }
64
65 /// Adds a list of lookup paths to `cmd`'s dynamic library lookup path.
66 /// If the dylib_path_var is already set for this cmd, the old value will be overwritten!
67 pub fn add_dylib_path(path: Vec<PathBuf>, cmd: &mut Command) {
68     let mut list = dylib_path();
69     for path in path {
70         list.insert(0, path);
71     }
72     cmd.env(dylib_path_var(), t!(env::join_paths(list)));
73 }
74
75 include!("dylib_util.rs");
76
77 /// Adds a list of lookup paths to `cmd`'s link library lookup path.
78 pub fn add_link_lib_path(path: Vec<PathBuf>, cmd: &mut Command) {
79     let mut list = link_lib_path();
80     for path in path {
81         list.insert(0, path);
82     }
83     cmd.env(link_lib_path_var(), t!(env::join_paths(list)));
84 }
85
86 /// Returns the environment variable which the link library lookup path
87 /// resides in for this platform.
88 fn link_lib_path_var() -> &'static str {
89     if cfg!(target_env = "msvc") { "LIB" } else { "LIBRARY_PATH" }
90 }
91
92 /// Parses the `link_lib_path_var()` environment variable, returning a list of
93 /// paths that are members of this lookup path.
94 fn link_lib_path() -> Vec<PathBuf> {
95     let var = match env::var_os(link_lib_path_var()) {
96         Some(v) => v,
97         None => return vec![],
98     };
99     env::split_paths(&var).collect()
100 }
101
102 pub struct TimeIt(bool, Instant);
103
104 /// Returns an RAII structure that prints out how long it took to drop.
105 pub fn timeit(builder: &Builder<'_>) -> TimeIt {
106     TimeIt(builder.config.dry_run, Instant::now())
107 }
108
109 impl Drop for TimeIt {
110     fn drop(&mut self) {
111         let time = self.1.elapsed();
112         if !self.0 {
113             println!("\tfinished in {}.{:03} seconds", time.as_secs(), time.subsec_millis());
114         }
115     }
116 }
117
118 /// Used for download caching
119 pub(crate) fn program_out_of_date(stamp: &Path, key: &str) -> bool {
120     if !stamp.exists() {
121         return true;
122     }
123     t!(fs::read_to_string(stamp)) != key
124 }
125
126 /// Symlinks two directories, using junctions on Windows and normal symlinks on
127 /// Unix.
128 pub fn symlink_dir(config: &Config, src: &Path, dest: &Path) -> io::Result<()> {
129     if config.dry_run {
130         return Ok(());
131     }
132     let _ = fs::remove_dir(dest);
133     return symlink_dir_inner(src, dest);
134
135     #[cfg(not(windows))]
136     fn symlink_dir_inner(src: &Path, dest: &Path) -> io::Result<()> {
137         use std::os::unix::fs;
138         fs::symlink(src, dest)
139     }
140
141     // Creating a directory junction on windows involves dealing with reparse
142     // points and the DeviceIoControl function, and this code is a skeleton of
143     // what can be found here:
144     //
145     // http://www.flexhex.com/docs/articles/hard-links.phtml
146     #[cfg(windows)]
147     fn symlink_dir_inner(target: &Path, junction: &Path) -> io::Result<()> {
148         use std::ffi::OsStr;
149         use std::os::windows::ffi::OsStrExt;
150         use std::ptr;
151
152         use winapi::shared::minwindef::{DWORD, WORD};
153         use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
154         use winapi::um::handleapi::CloseHandle;
155         use winapi::um::ioapiset::DeviceIoControl;
156         use winapi::um::winbase::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT};
157         use winapi::um::winioctl::FSCTL_SET_REPARSE_POINT;
158         use winapi::um::winnt::{
159             FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_WRITE,
160             IO_REPARSE_TAG_MOUNT_POINT, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, WCHAR,
161         };
162
163         #[allow(non_snake_case)]
164         #[repr(C)]
165         struct REPARSE_MOUNTPOINT_DATA_BUFFER {
166             ReparseTag: DWORD,
167             ReparseDataLength: DWORD,
168             Reserved: WORD,
169             ReparseTargetLength: WORD,
170             ReparseTargetMaximumLength: WORD,
171             Reserved1: WORD,
172             ReparseTarget: WCHAR,
173         }
174
175         fn to_u16s<S: AsRef<OsStr>>(s: S) -> io::Result<Vec<u16>> {
176             Ok(s.as_ref().encode_wide().chain(Some(0)).collect())
177         }
178
179         // We're using low-level APIs to create the junction, and these are more
180         // picky about paths. For example, forward slashes cannot be used as a
181         // path separator, so we should try to canonicalize the path first.
182         let target = fs::canonicalize(target)?;
183
184         fs::create_dir(junction)?;
185
186         let path = to_u16s(junction)?;
187
188         unsafe {
189             let h = CreateFileW(
190                 path.as_ptr(),
191                 GENERIC_WRITE,
192                 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
193                 ptr::null_mut(),
194                 OPEN_EXISTING,
195                 FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
196                 ptr::null_mut(),
197             );
198
199             let mut data = [0u8; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
200             let db = data.as_mut_ptr() as *mut REPARSE_MOUNTPOINT_DATA_BUFFER;
201             let buf = &mut (*db).ReparseTarget as *mut u16;
202             let mut i = 0;
203             // FIXME: this conversion is very hacky
204             let v = br"\??\";
205             let v = v.iter().map(|x| *x as u16);
206             for c in v.chain(target.as_os_str().encode_wide().skip(4)) {
207                 *buf.offset(i) = c;
208                 i += 1;
209             }
210             *buf.offset(i) = 0;
211             i += 1;
212             (*db).ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
213             (*db).ReparseTargetMaximumLength = (i * 2) as WORD;
214             (*db).ReparseTargetLength = ((i - 1) * 2) as WORD;
215             (*db).ReparseDataLength = (*db).ReparseTargetLength as DWORD + 12;
216
217             let mut ret = 0;
218             let res = DeviceIoControl(
219                 h as *mut _,
220                 FSCTL_SET_REPARSE_POINT,
221                 data.as_ptr() as *mut _,
222                 (*db).ReparseDataLength + 8,
223                 ptr::null_mut(),
224                 0,
225                 &mut ret,
226                 ptr::null_mut(),
227             );
228
229             let out = if res == 0 { Err(io::Error::last_os_error()) } else { Ok(()) };
230             CloseHandle(h);
231             out
232         }
233     }
234 }
235
236 /// The CI environment rustbuild is running in. This mainly affects how the logs
237 /// are printed.
238 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
239 pub enum CiEnv {
240     /// Not a CI environment.
241     None,
242     /// The Azure Pipelines environment, for Linux (including Docker), Windows, and macOS builds.
243     AzurePipelines,
244     /// The GitHub Actions environment, for Linux (including Docker), Windows and macOS builds.
245     GitHubActions,
246 }
247
248 impl CiEnv {
249     /// Obtains the current CI environment.
250     pub fn current() -> CiEnv {
251         if env::var("TF_BUILD").map_or(false, |e| e == "True") {
252             CiEnv::AzurePipelines
253         } else if env::var("GITHUB_ACTIONS").map_or(false, |e| e == "true") {
254             CiEnv::GitHubActions
255         } else {
256             CiEnv::None
257         }
258     }
259
260     /// If in a CI environment, forces the command to run with colors.
261     pub fn force_coloring_in_ci(self, cmd: &mut Command) {
262         if self != CiEnv::None {
263             // Due to use of stamp/docker, the output stream of rustbuild is not
264             // a TTY in CI, so coloring is by-default turned off.
265             // The explicit `TERM=xterm` environment is needed for
266             // `--color always` to actually work. This env var was lost when
267             // compiling through the Makefile. Very strange.
268             cmd.env("TERM", "xterm").args(&["--color", "always"]);
269         }
270     }
271 }
272
273 pub fn forcing_clang_based_tests() -> bool {
274     if let Some(var) = env::var_os("RUSTBUILD_FORCE_CLANG_BASED_TESTS") {
275         match &var.to_string_lossy().to_lowercase()[..] {
276             "1" | "yes" | "on" => true,
277             "0" | "no" | "off" => false,
278             other => {
279                 // Let's make sure typos don't go unnoticed
280                 panic!(
281                     "Unrecognized option '{}' set in \
282                         RUSTBUILD_FORCE_CLANG_BASED_TESTS",
283                     other
284                 )
285             }
286         }
287     } else {
288         false
289     }
290 }
291
292 pub fn use_host_linker(target: TargetSelection) -> bool {
293     // FIXME: this information should be gotten by checking the linker flavor
294     // of the rustc target
295     !(target.contains("emscripten")
296         || target.contains("wasm32")
297         || target.contains("nvptx")
298         || target.contains("fortanix")
299         || target.contains("fuchsia")
300         || target.contains("bpf"))
301 }
302
303 pub fn is_valid_test_suite_arg<'a, P: AsRef<Path>>(
304     path: &'a Path,
305     suite_path: P,
306     builder: &Builder<'_>,
307 ) -> Option<&'a str> {
308     let suite_path = suite_path.as_ref();
309     let path = match path.strip_prefix(".") {
310         Ok(p) => p,
311         Err(_) => path,
312     };
313     if !path.starts_with(suite_path) {
314         return None;
315     }
316     let abs_path = builder.src.join(path);
317     let exists = abs_path.is_dir() || abs_path.is_file();
318     if !exists {
319         panic!(
320             "Invalid test suite filter \"{}\": file or directory does not exist",
321             abs_path.display()
322         );
323     }
324     // Since test suite paths are themselves directories, if we don't
325     // specify a directory or file, we'll get an empty string here
326     // (the result of the test suite directory without its suite prefix).
327     // Therefore, we need to filter these out, as only the first --test-args
328     // flag is respected, so providing an empty --test-args conflicts with
329     // any following it.
330     match path.strip_prefix(suite_path).ok().and_then(|p| p.to_str()) {
331         Some(s) if !s.is_empty() => Some(s),
332         _ => None,
333     }
334 }
335
336 pub fn run(cmd: &mut Command, print_cmd_on_fail: bool) {
337     if !try_run(cmd, print_cmd_on_fail) {
338         std::process::exit(1);
339     }
340 }
341
342 pub fn try_run(cmd: &mut Command, print_cmd_on_fail: bool) -> bool {
343     let status = match cmd.status() {
344         Ok(status) => status,
345         Err(e) => fail(&format!("failed to execute command: {:?}\nerror: {}", cmd, e)),
346     };
347     if !status.success() && print_cmd_on_fail {
348         println!(
349             "\n\ncommand did not execute successfully: {:?}\n\
350              expected success, got: {}\n\n",
351             cmd, status
352         );
353     }
354     status.success()
355 }
356
357 pub fn check_run(cmd: &mut Command, print_cmd_on_fail: bool) -> bool {
358     let status = match cmd.status() {
359         Ok(status) => status,
360         Err(e) => {
361             println!("failed to execute command: {:?}\nerror: {}", cmd, e);
362             return false;
363         }
364     };
365     if !status.success() && print_cmd_on_fail {
366         println!(
367             "\n\ncommand did not execute successfully: {:?}\n\
368              expected success, got: {}\n\n",
369             cmd, status
370         );
371     }
372     status.success()
373 }
374
375 pub fn run_suppressed(cmd: &mut Command) {
376     if !try_run_suppressed(cmd) {
377         std::process::exit(1);
378     }
379 }
380
381 pub fn try_run_suppressed(cmd: &mut Command) -> bool {
382     let output = match cmd.output() {
383         Ok(status) => status,
384         Err(e) => fail(&format!("failed to execute command: {:?}\nerror: {}", cmd, e)),
385     };
386     if !output.status.success() {
387         println!(
388             "\n\ncommand did not execute successfully: {:?}\n\
389              expected success, got: {}\n\n\
390              stdout ----\n{}\n\
391              stderr ----\n{}\n\n",
392             cmd,
393             output.status,
394             String::from_utf8_lossy(&output.stdout),
395             String::from_utf8_lossy(&output.stderr)
396         );
397     }
398     output.status.success()
399 }
400
401 pub fn make(host: &str) -> PathBuf {
402     if host.contains("dragonfly")
403         || host.contains("freebsd")
404         || host.contains("netbsd")
405         || host.contains("openbsd")
406     {
407         PathBuf::from("gmake")
408     } else {
409         PathBuf::from("make")
410     }
411 }
412
413 #[track_caller]
414 pub fn output(cmd: &mut Command) -> String {
415     let output = match cmd.stderr(Stdio::inherit()).output() {
416         Ok(status) => status,
417         Err(e) => fail(&format!("failed to execute command: {:?}\nerror: {}", cmd, e)),
418     };
419     if !output.status.success() {
420         panic!(
421             "command did not execute successfully: {:?}\n\
422              expected success, got: {}",
423             cmd, output.status
424         );
425     }
426     String::from_utf8(output.stdout).unwrap()
427 }
428
429 /// Returns the last-modified time for `path`, or zero if it doesn't exist.
430 pub fn mtime(path: &Path) -> SystemTime {
431     fs::metadata(path).and_then(|f| f.modified()).unwrap_or(UNIX_EPOCH)
432 }
433
434 /// Returns `true` if `dst` is up to date given that the file or files in `src`
435 /// are used to generate it.
436 ///
437 /// Uses last-modified time checks to verify this.
438 pub fn up_to_date(src: &Path, dst: &Path) -> bool {
439     if !dst.exists() {
440         return false;
441     }
442     let threshold = mtime(dst);
443     let meta = match fs::metadata(src) {
444         Ok(meta) => meta,
445         Err(e) => panic!("source {:?} failed to get metadata: {}", src, e),
446     };
447     if meta.is_dir() {
448         dir_up_to_date(src, threshold)
449     } else {
450         meta.modified().unwrap_or(UNIX_EPOCH) <= threshold
451     }
452 }
453
454 fn dir_up_to_date(src: &Path, threshold: SystemTime) -> bool {
455     t!(fs::read_dir(src)).map(|e| t!(e)).all(|e| {
456         let meta = t!(e.metadata());
457         if meta.is_dir() {
458             dir_up_to_date(&e.path(), threshold)
459         } else {
460             meta.modified().unwrap_or(UNIX_EPOCH) < threshold
461         }
462     })
463 }
464
465 fn fail(s: &str) -> ! {
466     eprintln!("\n\n{}\n\n", s);
467     std::process::exit(1);
468 }
469
470 /// Copied from `std::path::absolute` until it stabilizes.
471 ///
472 /// FIXME: this shouldn't exist.
473 pub(crate) fn absolute(path: &Path) -> PathBuf {
474     if path.as_os_str().is_empty() {
475         panic!("can't make empty path absolute");
476     }
477     #[cfg(unix)]
478     {
479         t!(absolute_unix(path), format!("could not make path absolute: {}", path.display()))
480     }
481     #[cfg(windows)]
482     {
483         t!(absolute_windows(path), format!("could not make path absolute: {}", path.display()))
484     }
485     #[cfg(not(any(unix, windows)))]
486     {
487         println!("warning: bootstrap is not supported on non-unix platforms");
488         t!(std::fs::canonicalize(t!(std::env::current_dir()))).join(path)
489     }
490 }
491
492 #[cfg(unix)]
493 /// Make a POSIX path absolute without changing its semantics.
494 fn absolute_unix(path: &Path) -> io::Result<PathBuf> {
495     // This is mostly a wrapper around collecting `Path::components`, with
496     // exceptions made where this conflicts with the POSIX specification.
497     // See 4.13 Pathname Resolution, IEEE Std 1003.1-2017
498     // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
499
500     use std::os::unix::prelude::OsStrExt;
501     let mut components = path.components();
502     let path_os = path.as_os_str().as_bytes();
503
504     let mut normalized = if path.is_absolute() {
505         // "If a pathname begins with two successive <slash> characters, the
506         // first component following the leading <slash> characters may be
507         // interpreted in an implementation-defined manner, although more than
508         // two leading <slash> characters shall be treated as a single <slash>
509         // character."
510         if path_os.starts_with(b"//") && !path_os.starts_with(b"///") {
511             components.next();
512             PathBuf::from("//")
513         } else {
514             PathBuf::new()
515         }
516     } else {
517         env::current_dir()?
518     };
519     normalized.extend(components);
520
521     // "Interfaces using pathname resolution may specify additional constraints
522     // when a pathname that does not name an existing directory contains at
523     // least one non- <slash> character and contains one or more trailing
524     // <slash> characters".
525     // A trailing <slash> is also meaningful if "a symbolic link is
526     // encountered during pathname resolution".
527
528     if path_os.ends_with(b"/") {
529         normalized.push("");
530     }
531
532     Ok(normalized)
533 }
534
535 #[cfg(windows)]
536 fn absolute_windows(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
537     use std::ffi::OsString;
538     use std::io::Error;
539     use std::os::windows::ffi::{OsStrExt, OsStringExt};
540     use std::ptr::null_mut;
541     #[link(name = "kernel32")]
542     extern "system" {
543         fn GetFullPathNameW(
544             lpFileName: *const u16,
545             nBufferLength: u32,
546             lpBuffer: *mut u16,
547             lpFilePart: *mut *const u16,
548         ) -> u32;
549     }
550
551     unsafe {
552         // encode the path as UTF-16
553         let path: Vec<u16> = path.as_os_str().encode_wide().chain([0]).collect();
554         let mut buffer = Vec::new();
555         // Loop until either success or failure.
556         loop {
557             // Try to get the absolute path
558             let len = GetFullPathNameW(
559                 path.as_ptr(),
560                 buffer.len().try_into().unwrap(),
561                 buffer.as_mut_ptr(),
562                 null_mut(),
563             );
564             match len as usize {
565                 // Failure
566                 0 => return Err(Error::last_os_error()),
567                 // Buffer is too small, resize.
568                 len if len > buffer.len() => buffer.resize(len, 0),
569                 // Success!
570                 len => {
571                     buffer.truncate(len);
572                     return Ok(OsString::from_wide(&buffer).into());
573                 }
574             }
575         }
576     }
577 }