1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4
5use rootcause::bail;
6use rootcause::prelude::ResultExt;
7use ytil_sys::rustup::RequestedRustToolchain;
8
9use crate::installers::Installer;
10use crate::installers::run_health_check;
11
12pub struct Cargo<'a> {
18 source_bin_dir: &'a Path,
19 bin_dir: &'a Path,
20 bin_name: &'static str,
21 source: Source,
22 health_check: HealthCheck,
23}
24
25enum Source {
26 Registry {
27 crate_name: &'static str,
28 features: Option<&'static str>,
29 all_features: bool,
30 locked: bool,
31 },
32 Git {
33 repository: &'static str,
34 branch: Option<&'static str>,
35 locked: bool,
36 requested_toolchain: Option<RequestedRustToolchain>,
37 package_name: Option<&'static str>,
38 },
39}
40
41#[derive(Clone, Copy)]
42enum HealthCheck {
43 Binary(&'static [&'static str]),
45 CargoSubcommand(&'static str),
47}
48
49impl<'a> Cargo<'a> {
50 pub const fn registry(
51 source_bin_dir: &'a Path,
52 bin_dir: &'a Path,
53 bin_name: &'static str,
54 crate_name: &'static str,
55 ) -> Self {
56 Self {
57 source_bin_dir,
58 bin_dir,
59 bin_name,
60 source: Source::Registry {
61 crate_name,
62 features: None,
63 all_features: false,
64 locked: false,
65 },
66 health_check: HealthCheck::Binary(&["--version"]),
67 }
68 }
69
70 pub const fn registry_with_features(
71 source_bin_dir: &'a Path,
72 bin_dir: &'a Path,
73 bin_name: &'static str,
74 crate_name: &'static str,
75 features: &'static str,
76 ) -> Self {
77 Self {
78 source_bin_dir,
79 bin_dir,
80 bin_name,
81 source: Source::Registry {
82 crate_name,
83 features: Some(features),
84 all_features: false,
85 locked: false,
86 },
87 health_check: HealthCheck::Binary(&["--version"]),
88 }
89 }
90
91 pub const fn locked_registry(
92 source_bin_dir: &'a Path,
93 bin_dir: &'a Path,
94 bin_name: &'static str,
95 crate_name: &'static str,
96 ) -> Self {
97 Self {
98 source_bin_dir,
99 bin_dir,
100 bin_name,
101 source: Source::Registry {
102 crate_name,
103 features: None,
104 all_features: false,
105 locked: true,
106 },
107 health_check: HealthCheck::Binary(&["--version"]),
108 }
109 }
110
111 pub const fn git(
112 source_bin_dir: &'a Path,
113 bin_dir: &'a Path,
114 bin_name: &'static str,
115 repository: &'static str,
116 ) -> Self {
117 Self {
118 source_bin_dir,
119 bin_dir,
120 bin_name,
121 source: Source::Git {
122 repository,
123 branch: None,
124 locked: false,
125 requested_toolchain: None,
126 package_name: None,
127 },
128 health_check: HealthCheck::Binary(&["--version"]),
129 }
130 }
131
132 pub const fn registry_with_all_features(
133 source_bin_dir: &'a Path,
134 bin_dir: &'a Path,
135 bin_name: &'static str,
136 crate_name: &'static str,
137 ) -> Self {
138 Self {
139 source_bin_dir,
140 bin_dir,
141 bin_name,
142 source: Source::Registry {
143 crate_name,
144 features: None,
145 all_features: true,
146 locked: false,
147 },
148 health_check: HealthCheck::Binary(&["--version"]),
149 }
150 }
151
152 pub const fn registry_with_cargo_subcommand(
153 source_bin_dir: &'a Path,
154 bin_dir: &'a Path,
155 bin_name: &'static str,
156 crate_name: &'static str,
157 cargo_subcommand: &'static str,
158 ) -> Self {
159 let mut installer = Self::registry(source_bin_dir, bin_dir, bin_name, crate_name);
160 installer.health_check = HealthCheck::CargoSubcommand(cargo_subcommand);
161 installer
162 }
163
164 pub const fn registry_with_health_check_args(
165 source_bin_dir: &'a Path,
166 bin_dir: &'a Path,
167 bin_name: &'static str,
168 crate_name: &'static str,
169 health_check_args: &'static [&'static str],
170 ) -> Self {
171 let mut installer = Self::registry(source_bin_dir, bin_dir, bin_name, crate_name);
172 installer.health_check = HealthCheck::Binary(health_check_args);
173 installer
174 }
175
176 pub const fn nightly_git(
177 source_bin_dir: &'a Path,
178 bin_dir: &'a Path,
179 bin_name: &'static str,
180 repository: &'static str,
181 branch: &'static str,
182 ) -> Self {
183 Self {
184 source_bin_dir,
185 bin_dir,
186 bin_name,
187 source: Source::Git {
188 repository,
189 branch: Some(branch),
190 locked: true,
191 requested_toolchain: Some(RequestedRustToolchain::Nightly(None)),
192 package_name: Some(bin_name),
193 },
194 health_check: HealthCheck::Binary(&["--version"]),
195 }
196 }
197}
198
199impl Installer for Cargo<'_> {
200 fn bin_name(&self) -> &'static str {
201 self.bin_name
202 }
203
204 fn should_verify_checksum(&self) -> bool {
205 false
206 }
207
208 fn install(&self) -> rootcause::Result<()> {
209 let toolchain = match &self.source {
210 Source::Git {
211 requested_toolchain: Some(requested_toolchain),
212 ..
213 } => Some(ytil_sys::rustup::find_latest_installed_rust_toolchain(
214 requested_toolchain,
215 )?),
216 Source::Registry { .. }
217 | Source::Git {
218 requested_toolchain: None,
219 ..
220 } => None,
221 };
222 let mut command = ytil_cmd::silent_cmd("cargo");
223 if let Some(toolchain) = toolchain {
224 command.arg(format!("+{toolchain}"));
225 }
226 command.args(["install", "--force"]);
227
228 match &self.source {
229 Source::Registry {
230 crate_name,
231 features,
232 all_features,
233 locked,
234 } => {
235 command.arg(*crate_name);
236 if let Some(features) = features {
237 command.args(["--features", *features]);
238 }
239 if *all_features {
240 command.arg("--all-features");
241 }
242 if *locked {
243 command.arg("--locked");
244 }
245 }
246 Source::Git {
247 repository,
248 branch,
249 locked,
250 package_name,
251 ..
252 } => {
253 command.args(["--git", *repository]);
254 if let Some(branch) = branch {
255 command.args(["--branch", *branch]);
256 }
257 if *locked {
258 command.arg("--locked");
259 }
260 if let Some(package_name) = package_name {
261 command.arg(*package_name);
262 }
263 }
264 }
265
266 command
267 .status()
268 .context("failed to spawn cargo install")?
269 .exit_ok()
270 .context("cargo install failed")
271 .attach_with(|| format!("tool={}", self.bin_name()))
272 .attach_with(|| format!("command={command:?}"))?;
273
274 let cargo_binary = self
275 .source_bin_dir
276 .join(self.bin_name())
277 .canonicalize()
278 .context("could not resolve Cargo-installed binary")?;
279 ytil_sys::file::ln_sf(&cargo_binary, &self.bin_dir.join(self.bin_name()))?;
280 ytil_sys::file::chmod_x(cargo_binary)?;
281
282 Ok(())
283 }
284
285 fn health_check(&self) -> Option<rootcause::Result<String>> {
286 let command = self.health_check_command();
287 Some(run_health_check(command))
288 }
289}
290
291impl Cargo<'_> {
292 fn health_check_command(&self) -> Command {
293 match self.health_check {
294 HealthCheck::Binary(args) => {
295 let mut command = Command::new(self.bin_dir.join(self.bin_name()));
296 command.args(args);
297 command
298 }
299 HealthCheck::CargoSubcommand(subcommand) => {
300 let mut command = Command::new("cargo");
301 command.args([subcommand, "--version"]);
302 command
303 }
304 }
305 }
306}
307
308pub fn bin_dir() -> rootcause::Result<PathBuf> {
310 Ok(cargo_install_root()?.join("bin"))
311}
312
313fn cargo_install_root() -> rootcause::Result<PathBuf> {
315 if let Some(install_root) = std::env::var_os("CARGO_INSTALL_ROOT") {
316 return Ok(PathBuf::from(install_root));
317 }
318
319 let cargo_home = cargo_home()?;
320 if let Some(install_root) = cargo_config_install_root(&cargo_home)? {
321 return Ok(install_root);
322 }
323
324 Ok(cargo_home)
325}
326
327fn cargo_home() -> rootcause::Result<PathBuf> {
328 if let Some(cargo_home) = std::env::var_os("CARGO_HOME") {
329 return Ok(PathBuf::from(cargo_home));
330 }
331
332 ytil_sys::dir::build_home_path(&[".cargo"])
333}
334
335fn cargo_config_install_root(cargo_home: &Path) -> rootcause::Result<Option<PathBuf>> {
337 let config_path = cargo_home.join("config.toml");
338 if !config_path.is_file() {
339 return Ok(None);
340 }
341 let config = std::fs::read_to_string(&config_path)
342 .context("could not read Cargo configuration")
343 .attach_with(|| format!("config={}", config_path.display()))?;
344 let config: toml::Value = toml::from_str(&config)
345 .context("could not parse Cargo configuration")
346 .attach_with(|| format!("config={}", config_path.display()))?;
347 let Some(install_root) = config
348 .get("install")
349 .and_then(toml::Value::as_table)
350 .and_then(|install| install.get("root"))
351 else {
352 return Ok(None);
353 };
354 let Some(install_root) = install_root.as_str() else {
355 bail!("install.root in {} must be a path string", config_path.display());
356 };
357
358 resolve_config_path(&config_path, install_root).map(Some)
359}
360
361fn resolve_config_path(config_path: &Path, install_root: &str) -> rootcause::Result<PathBuf> {
362 let install_root = PathBuf::from(install_root);
363 if install_root.is_absolute() {
364 return Ok(install_root);
365 }
366
367 let Some(config_dir) = config_path.parent() else {
368 bail!("Cargo configuration path has no parent: {}", config_path.display());
369 };
370 Ok(config_dir.join(install_root))
371}
372
373#[cfg(test)]
374mod tests {
375 use std::ffi::OsStr;
376
377 use super::*;
378
379 #[test]
380 fn test_cargo_health_check_command_when_cargo_subcommand_is_selected_uses_cargo_dispatch() {
381 let source_bin_dir = Path::new("/source/bin");
382 let bin_dir = Path::new("/target/bin");
383 let installer = Cargo::registry_with_cargo_subcommand(
384 source_bin_dir,
385 bin_dir,
386 "cargo-llvm-cov",
387 "cargo-llvm-cov",
388 "llvm-cov",
389 );
390
391 let command = installer.health_check_command();
392
393 assert_eq!(command.get_program(), OsStr::new("cargo"));
394 assert_eq!(
395 command.get_args().collect::<Vec<_>>(),
396 vec![OsStr::new("llvm-cov"), OsStr::new("--version")]
397 );
398 }
399
400 #[test]
401 fn test_cargo_health_check_command_when_custom_args_are_selected_uses_direct_binary() {
402 let source_bin_dir = Path::new("/source/bin");
403 let bin_dir = Path::new("/target/bin");
404 let installer = Cargo::registry_with_health_check_args(source_bin_dir, bin_dir, "pv", "pv", &["--help"]);
405
406 let command = installer.health_check_command();
407
408 assert_eq!(command.get_program(), bin_dir.join("pv"));
409 assert_eq!(command.get_args().collect::<Vec<_>>(), vec![OsStr::new("--help")]);
410 }
411}