summaryrefslogtreecommitdiffstats
path: root/src/tools/tidy/src/bins.rs
blob: 9615c4db6b4b51c4fc09fcdaa8f250539ee71415 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Tidy check to ensure that there are no binaries checked into the source tree
//! by accident.
//!
//! In the past we've accidentally checked in test binaries and such which add a
//! huge amount of bloat to the Git history, so it's good to just ensure we
//! don't do that again.

pub use os_impl::*;

// All files are executable on Windows, so just check on Unix.
#[cfg(windows)]
mod os_impl {
    use std::path::Path;

    pub fn check_filesystem_support(_sources: &[&Path], _output: &Path) -> bool {
        return false;
    }

    pub fn check(_path: &Path, _bad: &mut bool) {}
}

#[cfg(unix)]
mod os_impl {
    use std::fs;
    use std::os::unix::prelude::*;
    use std::path::Path;
    use std::process::{Command, Stdio};

    enum FilesystemSupport {
        Supported,
        Unsupported,
        ReadOnlyFs,
    }

    use FilesystemSupport::*;

    fn is_executable(path: &Path) -> std::io::Result<bool> {
        Ok(path.metadata()?.mode() & 0o111 != 0)
    }

    pub fn check_filesystem_support(sources: &[&Path], output: &Path) -> bool {
        // We want to avoid false positives on filesystems that do not support the
        // executable bit. This occurs on some versions of Window's linux subsystem,
        // for example.
        //
        // We try to create the temporary file first in the src directory, which is
        // the preferred location as it's most likely to be on the same filesystem,
        // and then in the output (`build`) directory if that fails. Sometimes we
        // see the source directory mounted as read-only which means we can't
        // readily create a file there to test.
        //
        // See #36706 and #74753 for context.

        fn check_dir(dir: &Path) -> FilesystemSupport {
            let path = dir.join("tidy-test-file");
            match fs::File::create(&path) {
                Ok(file) => {
                    let exec = is_executable(&path).unwrap_or(false);
                    std::mem::drop(file);
                    std::fs::remove_file(&path).expect("Deleted temp file");
                    // If the file is executable, then we assume that this
                    // filesystem does not track executability, so skip this check.
                    return if exec { Unsupported } else { Supported };
                }
                Err(e) => {
                    // If the directory is read-only or we otherwise don't have rights,
                    // just don't run this check.
                    //
                    // 30 is the "Read-only filesystem" code at least in one CI
                    //    environment.
                    if e.raw_os_error() == Some(30) {
                        eprintln!("tidy: Skipping binary file check, read-only filesystem");
                        return ReadOnlyFs;
                    }

                    panic!("unable to create temporary file `{:?}`: {:?}", path, e);
                }
            };
        }

        for &source_dir in sources {
            match check_dir(source_dir) {
                Unsupported => return false,
                ReadOnlyFs => {
                    return match check_dir(output) {
                        Supported => true,
                        _ => false,
                    };
                }
                _ => {}
            }
        }

        return true;
    }

    #[cfg(unix)]
    pub fn check(path: &Path, bad: &mut bool) {
        const ALLOWED: &[&str] = &["configure"];

        crate::walk_no_read(
            path,
            &mut |path| {
                crate::filter_dirs(path)
                    || path.ends_with("src/etc")
                    // This is a list of directories that we almost certainly
                    // don't need to walk. A future PR will likely want to
                    // remove these in favor of crate::walk_no_read using git
                    // ls-files to discover the paths we should check, which
                    // would naturally ignore all of these directories. It's
                    // also likely faster than walking the directory tree
                    // directly (since git is just reading from a couple files
                    // to produce the results).
                    || path.ends_with("target")
                    || path.ends_with("build")
                    || path.ends_with(".git")
            },
            &mut |entry| {
                let file = entry.path();
                let filename = file.file_name().unwrap().to_string_lossy();
                let extensions = [".py", ".sh"];
                if extensions.iter().any(|e| filename.ends_with(e)) {
                    return;
                }

                if t!(is_executable(&file), file) {
                    let rel_path = file.strip_prefix(path).unwrap();
                    let git_friendly_path = rel_path.to_str().unwrap().replace("\\", "/");

                    if ALLOWED.contains(&git_friendly_path.as_str()) {
                        return;
                    }

                    let output = Command::new("git")
                        .arg("ls-files")
                        .arg(&git_friendly_path)
                        .current_dir(path)
                        .stderr(Stdio::null())
                        .output()
                        .unwrap_or_else(|e| {
                            panic!("could not run git ls-files: {e}");
                        });
                    let path_bytes = rel_path.as_os_str().as_bytes();
                    if output.status.success() && output.stdout.starts_with(path_bytes) {
                        tidy_error!(bad, "binary checked into source: {}", file.display());
                    }
                }
            },
        )
    }
}