tidy/
bins.rs

1//! Tidy check to ensure that there are no binaries checked into the source tree
2//! by accident.
3//!
4//! In the past we've accidentally checked in test binaries and such which add a
5//! huge amount of bloat to the Git history, so it's good to just ensure we
6//! don't do that again.
7
8pub use os_impl::*;
9
10// All files are executable on Windows, so just check on Unix.
11#[cfg(windows)]
12mod os_impl {
13    use std::path::Path;
14
15    use crate::diagnostics::DiagCtx;
16
17    pub fn check_filesystem_support(_sources: &[&Path], _output: &Path) -> bool {
18        return false;
19    }
20
21    pub fn check(_path: &Path, _diag_ctx: DiagCtx) {}
22}
23
24#[cfg(unix)]
25mod os_impl {
26    use std::fs;
27    use std::os::unix::prelude::*;
28    use std::path::Path;
29    use std::process::{Command, Stdio};
30
31    use crate::walk::{filter_dirs, walk_no_read};
32
33    enum FilesystemSupport {
34        Supported,
35        Unsupported,
36        ReadOnlyFs,
37    }
38
39    use FilesystemSupport::*;
40
41    use crate::diagnostics::DiagCtx;
42
43    fn is_executable(path: &Path) -> std::io::Result<bool> {
44        Ok(path.metadata()?.mode() & 0o111 != 0)
45    }
46
47    pub fn check_filesystem_support(sources: &[&Path], output: &Path) -> bool {
48        // We want to avoid false positives on filesystems that do not support the
49        // executable bit. This occurs on some versions of Window's linux subsystem,
50        // for example.
51        //
52        // We try to create the temporary file first in the src directory, which is
53        // the preferred location as it's most likely to be on the same filesystem,
54        // and then in the output (`build`) directory if that fails. Sometimes we
55        // see the source directory mounted as read-only which means we can't
56        // readily create a file there to test.
57        //
58        // See #36706 and #74753 for context.
59
60        fn check_dir(dir: &Path) -> FilesystemSupport {
61            let path = dir.join("tidy-test-file");
62            match fs::File::create(&path) {
63                Ok(file) => {
64                    let exec = is_executable(&path).unwrap_or(false);
65                    drop(file);
66                    fs::remove_file(&path).expect("Deleted temp file");
67                    // If the file is executable, then we assume that this
68                    // filesystem does not track executability, so skip this check.
69                    if exec { Unsupported } else { Supported }
70                }
71                Err(e) => {
72                    // If the directory is read-only or we otherwise don't have rights,
73                    // just don't run this check.
74                    //
75                    // 30 is the "Read-only filesystem" code at least in one CI
76                    //    environment.
77                    if e.raw_os_error() == Some(30) {
78                        eprintln!("tidy: Skipping binary file check, read-only filesystem");
79                        return ReadOnlyFs;
80                    }
81
82                    panic!("unable to create temporary file `{path:?}`: {e:?}");
83                }
84            }
85        }
86
87        for &source_dir in sources {
88            match check_dir(source_dir) {
89                Unsupported => return false,
90                ReadOnlyFs => return matches!(check_dir(output), Supported),
91                _ => {}
92            }
93        }
94
95        true
96    }
97
98    // FIXME: check when rust-installer test sh files will be removed,
99    // and then remove them from exclude list
100    const RI_EXCLUSION_LIST: &[&str] = &[
101        "src/tools/rust-installer/test/image1/bin/program",
102        "src/tools/rust-installer/test/image1/bin/program2",
103        "src/tools/rust-installer/test/image1/bin/bad-bin",
104        "src/tools/rust-installer/test/image2/bin/oldprogram",
105        "src/tools/rust-installer/test/image3/bin/cargo",
106    ];
107
108    fn filter_rust_installer_no_so_bins(path: &Path) -> bool {
109        RI_EXCLUSION_LIST.iter().any(|p| path.ends_with(p))
110    }
111
112    #[cfg(unix)]
113    pub fn check(path: &Path, diag_ctx: DiagCtx) {
114        let mut check = diag_ctx.start_check("bins");
115
116        use std::ffi::OsStr;
117
118        const ALLOWED: &[&str] = &["configure", "x"];
119
120        for p in RI_EXCLUSION_LIST {
121            if !path.join(Path::new(p)).exists() {
122                check.error(format!("rust-installer test bins missed: {p}"));
123            }
124        }
125
126        // FIXME: we don't need to look at all binaries, only files that have been modified in this branch
127        // (e.g. using `git ls-files`).
128        walk_no_read(
129            &[path],
130            |path, _is_dir| {
131                filter_dirs(path)
132                    || path.ends_with("src/etc")
133                    || filter_rust_installer_no_so_bins(path)
134            },
135            &mut |entry| {
136                let file = entry.path();
137                let extension = file.extension();
138                let scripts = ["py", "sh", "ps1", "woff2"];
139                if scripts.into_iter().any(|e| extension == Some(OsStr::new(e))) {
140                    return;
141                }
142
143                if t!(is_executable(file), file) {
144                    let rel_path = file.strip_prefix(path).unwrap();
145                    let git_friendly_path = rel_path.to_str().unwrap().replace("\\", "/");
146
147                    if ALLOWED.contains(&git_friendly_path.as_str()) {
148                        return;
149                    }
150
151                    let output = Command::new("git")
152                        .arg("ls-files")
153                        .arg(&git_friendly_path)
154                        .current_dir(path)
155                        .stderr(Stdio::null())
156                        .output()
157                        .unwrap_or_else(|e| {
158                            panic!("could not run git ls-files: {e}");
159                        });
160                    let path_bytes = rel_path.as_os_str().as_bytes();
161                    if output.status.success() && output.stdout.starts_with(path_bytes) {
162                        check.error(format!("binary checked into source: {}", file.display()));
163                    }
164                }
165            },
166        )
167    }
168}