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