Skip to main content

cargo_dev_install/
install.rs

1use std::os::unix::fs::PermissionsExt;
2use std::path::{Path, PathBuf};
3use std::{fs, io};
4
5pub fn install_dir(env: &crate::EnvSnapshot) -> Option<PathBuf> {
6    if let Some(xdg) = env.xdg_bin_home.as_deref() {
7        return Some(xdg.to_path_buf());
8    }
9
10    let home = env.home.as_deref()?;
11    Some(home.join(".local").join("bin"))
12}
13
14pub fn is_on_path(dir: &Path, path_var: Option<&str>) -> bool {
15    let path_var = match path_var {
16        Some(path_var) => path_var,
17        None => return false,
18    };
19
20    std::env::split_paths(path_var).any(|entry| entry == dir)
21}
22
23pub fn render_wrapper(crate_root: &Path) -> String {
24    format!(
25        "#!/usr/bin/env bash\nset -euo pipefail\n\nREPO=\"{}\"\nexec cargo run --quiet --release --manifest-path \"$REPO/Cargo.toml\" -- \"$@\"\n",
26        crate_root.display()
27    )
28}
29
30pub fn write_wrapper(wrapper_path: &Path, contents: &str, force: bool) -> io::Result<()> {
31    if wrapper_path.exists() && !force {
32        return Err(io::Error::new(
33            io::ErrorKind::AlreadyExists,
34            "wrapper already exists",
35        ));
36    }
37
38    if let Some(parent) = wrapper_path.parent() {
39        fs::create_dir_all(parent)?;
40    }
41
42    let temp_path = wrapper_path.with_extension("tmp");
43    fs::write(&temp_path, contents)?;
44
45    let mut perms = fs::metadata(&temp_path)?.permissions();
46    perms.set_mode(0o755);
47    fs::set_permissions(&temp_path, perms)?;
48
49    fs::rename(&temp_path, wrapper_path)?;
50
51    Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use std::fs::OpenOptions;
58
59    #[test]
60    fn install_dir_prefers_xdg_bin_home() {
61        let env = crate::EnvSnapshot {
62            home: Some(PathBuf::from("/home/demo")),
63            xdg_bin_home: Some(PathBuf::from("/custom/bin")),
64            path: None,
65        };
66        assert_eq!(install_dir(&env), Some(PathBuf::from("/custom/bin")));
67    }
68
69    #[test]
70    fn install_dir_falls_back_to_home_local_bin() {
71        let env = crate::EnvSnapshot {
72            home: Some(PathBuf::from("/home/demo")),
73            xdg_bin_home: None,
74            path: None,
75        };
76        assert_eq!(
77            install_dir(&env),
78            Some(PathBuf::from("/home/demo/.local/bin"))
79        );
80    }
81
82    #[test]
83    fn install_dir_none_when_no_home() {
84        let env = crate::EnvSnapshot {
85            home: None,
86            xdg_bin_home: None,
87            path: None,
88        };
89        assert_eq!(install_dir(&env), None);
90    }
91
92    #[test]
93    fn is_on_path_detects_match() {
94        let dir = Path::new("/home/demo/.local/bin");
95        let path_var = "/usr/bin:/home/demo/.local/bin:/bin";
96        assert!(is_on_path(dir, Some(path_var)));
97    }
98
99    #[test]
100    fn is_on_path_handles_missing_path() {
101        let dir = Path::new("/home/demo/.local/bin");
102        assert!(!is_on_path(dir, None));
103    }
104
105    #[test]
106    fn is_on_path_returns_false_for_absent_dir() {
107        let dir = Path::new("/opt/bin");
108        let path_var = "/usr/bin:/home/demo/.local/bin:/bin";
109        assert!(!is_on_path(dir, Some(path_var)));
110    }
111
112    #[test]
113    fn render_wrapper_contains_expected_lines() {
114        let wrapper = render_wrapper(Path::new("/repo/root"));
115        assert!(wrapper.starts_with("#!/usr/bin/env bash\n"));
116        assert!(wrapper.contains("set -euo pipefail\n"));
117        assert!(wrapper.contains("REPO=\"/repo/root\"\n"));
118        assert!(wrapper.contains(
119            "exec cargo run --quiet --release --manifest-path \"$REPO/Cargo.toml\" -- \"$@\"\n"
120        ));
121    }
122
123    #[test]
124    fn render_wrapper_quotes_repo_paths_with_spaces() {
125        let wrapper = render_wrapper(Path::new("/path with spaces/repo"));
126        assert!(wrapper.contains("REPO=\"/path with spaces/repo\"\n"));
127    }
128
129    #[test]
130    fn write_wrapper_creates_parent_and_sets_executable() {
131        let temp_dir = tempfile::tempdir().expect("temp dir");
132        let wrapper_path = temp_dir.path().join("bin").join("demo");
133
134        write_wrapper(&wrapper_path, "echo demo\n", false).expect("write wrapper");
135
136        let metadata = fs::metadata(&wrapper_path).expect("wrapper metadata");
137        let mode = metadata.permissions().mode();
138        assert_eq!(mode & 0o111, 0o111);
139    }
140
141    #[test]
142    fn write_wrapper_refuses_overwrite_without_force() {
143        let temp_dir = tempfile::tempdir().expect("temp dir");
144        let wrapper_path = temp_dir.path().join("demo");
145        write_wrapper(&wrapper_path, "echo demo\n", false).expect("write wrapper");
146
147        let err = write_wrapper(&wrapper_path, "echo other\n", false)
148            .expect_err("expected already exists");
149        assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
150    }
151
152    #[test]
153    fn write_wrapper_overwrites_with_force() {
154        let temp_dir = tempfile::tempdir().expect("temp dir");
155        let wrapper_path = temp_dir.path().join("demo");
156        write_wrapper(&wrapper_path, "echo demo\n", false).expect("write wrapper");
157
158        write_wrapper(&wrapper_path, "echo other\n", true).expect("overwrite");
159        let contents = fs::read_to_string(&wrapper_path).expect("read wrapper");
160        assert_eq!(contents, "echo other\n");
161    }
162
163    #[test]
164    fn write_wrapper_overwrites_existing_regular_file_with_force() {
165        let temp_dir = tempfile::tempdir().expect("temp dir");
166        let wrapper_path = temp_dir.path().join("demo");
167        OpenOptions::new()
168            .create(true)
169            .write(true)
170            .open(&wrapper_path)
171            .expect("create file");
172
173        write_wrapper(&wrapper_path, "echo demo\n", true).expect("overwrite");
174        let contents = fs::read_to_string(&wrapper_path).expect("read wrapper");
175        assert_eq!(contents, "echo demo\n");
176    }
177}