cargo_dev_install/
install.rs1use 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}