Skip to main content

ic_testkit/artifacts/
tool.rs

1use std::{
2    ffi::OsStr,
3    fs, io,
4    path::{Path, PathBuf},
5};
6
7#[cfg(windows)]
8use std::ffi::OsString;
9
10/// Resolve one executable exactly as an artifact-cache tool input.
11///
12/// Paths containing more than one component are resolved directly. Bare
13/// program names are searched through the current `PATH`. The returned path is
14/// canonical, points to a regular file, and can be passed to
15/// [`super::ArtifactCacheSpec::with_tool`].
16pub fn resolve_executable(program: impl AsRef<OsStr>) -> io::Result<PathBuf> {
17    let program = program.as_ref();
18    if program.is_empty() {
19        return Err(io::Error::new(
20            io::ErrorKind::InvalidInput,
21            "executable name must not be empty",
22        ));
23    }
24    let current_dir = std::env::current_dir()?;
25    let path = Path::new(program);
26    if path.is_absolute() || path.components().count() > 1 {
27        return canonical_executable(&current_dir.join(path));
28    }
29
30    let search_path = std::env::var_os("PATH").ok_or_else(|| {
31        io::Error::new(
32            io::ErrorKind::NotFound,
33            format!(
34                "cannot resolve executable `{}` because PATH is unset",
35                program.to_string_lossy()
36            ),
37        )
38    })?;
39    resolve_executable_in(program, &search_path, &current_dir)
40}
41
42fn resolve_executable_in(
43    program: &OsStr,
44    search_path: &OsStr,
45    current_dir: &Path,
46) -> io::Result<PathBuf> {
47    for directory in std::env::split_paths(search_path) {
48        let directory = if directory.as_os_str().is_empty() {
49            current_dir.to_owned()
50        } else if directory.is_absolute() {
51            directory
52        } else {
53            current_dir.join(directory)
54        };
55        for candidate in executable_candidates(&directory, program) {
56            match canonical_executable(&candidate) {
57                Ok(path) => return Ok(path),
58                Err(error)
59                    if matches!(
60                        error.kind(),
61                        io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
62                    ) => {}
63                Err(error) => return Err(error),
64            }
65        }
66    }
67    Err(io::Error::new(
68        io::ErrorKind::NotFound,
69        format!(
70            "executable `{}` was not found in PATH",
71            program.to_string_lossy()
72        ),
73    ))
74}
75
76fn canonical_executable(path: &Path) -> io::Result<PathBuf> {
77    let canonical = path.canonicalize()?;
78    let metadata = fs::metadata(&canonical)?;
79    if !metadata.is_file() {
80        return Err(io::Error::new(
81            io::ErrorKind::NotFound,
82            format!("executable path is not a regular file: {}", path.display()),
83        ));
84    }
85    if !is_executable(&metadata) {
86        return Err(io::Error::new(
87            io::ErrorKind::PermissionDenied,
88            format!("file is not executable: {}", path.display()),
89        ));
90    }
91    Ok(canonical)
92}
93
94#[cfg(unix)]
95fn is_executable(metadata: &fs::Metadata) -> bool {
96    use std::os::unix::fs::PermissionsExt as _;
97    metadata.permissions().mode() & 0o111 != 0
98}
99
100#[cfg(not(unix))]
101fn is_executable(_metadata: &fs::Metadata) -> bool {
102    true
103}
104
105#[cfg(windows)]
106fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
107    let program_path = Path::new(program);
108    if program_path.extension().is_some() {
109        return vec![directory.join(program_path)];
110    }
111    let extensions =
112        std::env::var_os("PATHEXT").unwrap_or_else(|| OsString::from(".COM;.EXE;.BAT;.CMD"));
113    extensions
114        .to_string_lossy()
115        .split(';')
116        .filter(|extension| !extension.is_empty())
117        .map(|extension| {
118            let mut name = program.to_os_string();
119            name.push(extension);
120            directory.join(name)
121        })
122        .collect()
123}
124
125#[cfg(not(windows))]
126fn executable_candidates(directory: &Path, program: &OsStr) -> Vec<PathBuf> {
127    vec![directory.join(program)]
128}
129
130#[cfg(test)]
131mod tests {
132    use super::resolve_executable_in;
133    use crate::artifacts::test_support::unique_temp_directory;
134    use std::{ffi::OsStr, fs};
135
136    #[cfg(unix)]
137    use crate::artifacts::test_support::write_executable_script;
138
139    #[test]
140    #[cfg(unix)]
141    fn path_resolution_returns_one_canonical_executable_file() {
142        let root = unique_temp_directory("resolve-executable");
143        let bin = root.join("bin");
144        fs::create_dir_all(&bin).expect("create executable search directory");
145        let tool = bin.join("optimizer");
146        write_executable_script(&tool, b"#!/bin/sh\nexit 0\n");
147
148        let resolved = resolve_executable_in(OsStr::new("optimizer"), bin.as_os_str(), &root)
149            .expect("resolve executable from supplied PATH");
150
151        assert_eq!(
152            resolved,
153            tool.canonicalize()
154                .expect("canonicalize executable fixture")
155        );
156        fs::remove_dir_all(root).expect("remove executable-resolution fixture");
157    }
158}