Skip to main content

idt/installers/
cargo.rs

1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4
5use rootcause::bail;
6use rootcause::prelude::ResultExt;
7
8use crate::installers::Installer;
9use crate::installers::run_health_check;
10
11/// Install a declared Cargo or Git tool from Cargo's configured install root.
12///
13/// Cargo's `--root` always appends `bin`, so using a private root would impose a `bin_dir` naming constraint.
14/// Instead, install only the declared tool into Cargo's configured standard root and link that exact binary into
15/// `bin_dir`. Do not enumerate or update other binaries already present in Cargo's home.
16pub struct Cargo<'a> {
17    source_bin_dir: &'a Path,
18    bin_dir: &'a Path,
19    bin_name: &'static str,
20    source: Source,
21}
22
23#[derive(Clone, Copy)]
24enum Source {
25    Registry {
26        crate_name: &'static str,
27        features: Option<&'static str>,
28        all_features: bool,
29        locked: bool,
30    },
31    Git {
32        repository: &'static str,
33        branch: Option<&'static str>,
34        locked: bool,
35        toolchain: Option<&'static str>,
36        package_name: Option<&'static str>,
37    },
38}
39
40impl<'a> Cargo<'a> {
41    pub const fn registry(
42        source_bin_dir: &'a Path,
43        bin_dir: &'a Path,
44        bin_name: &'static str,
45        crate_name: &'static str,
46    ) -> Self {
47        Self {
48            source_bin_dir,
49            bin_dir,
50            bin_name,
51            source: Source::Registry {
52                crate_name,
53                features: None,
54                all_features: false,
55                locked: false,
56            },
57        }
58    }
59
60    pub const fn registry_with_features(
61        source_bin_dir: &'a Path,
62        bin_dir: &'a Path,
63        bin_name: &'static str,
64        crate_name: &'static str,
65        features: &'static str,
66    ) -> Self {
67        Self {
68            source_bin_dir,
69            bin_dir,
70            bin_name,
71            source: Source::Registry {
72                crate_name,
73                features: Some(features),
74                all_features: false,
75                locked: false,
76            },
77        }
78    }
79
80    pub const fn locked_registry(
81        source_bin_dir: &'a Path,
82        bin_dir: &'a Path,
83        bin_name: &'static str,
84        crate_name: &'static str,
85    ) -> Self {
86        Self {
87            source_bin_dir,
88            bin_dir,
89            bin_name,
90            source: Source::Registry {
91                crate_name,
92                features: None,
93                all_features: false,
94                locked: true,
95            },
96        }
97    }
98
99    pub const fn git(
100        source_bin_dir: &'a Path,
101        bin_dir: &'a Path,
102        bin_name: &'static str,
103        repository: &'static str,
104    ) -> Self {
105        Self {
106            source_bin_dir,
107            bin_dir,
108            bin_name,
109            source: Source::Git {
110                repository,
111                branch: None,
112                locked: false,
113                toolchain: None,
114                package_name: None,
115            },
116        }
117    }
118
119    pub const fn registry_with_all_features(
120        source_bin_dir: &'a Path,
121        bin_dir: &'a Path,
122        bin_name: &'static str,
123        crate_name: &'static str,
124    ) -> Self {
125        Self {
126            source_bin_dir,
127            bin_dir,
128            bin_name,
129            source: Source::Registry {
130                crate_name,
131                features: None,
132                all_features: true,
133                locked: false,
134            },
135        }
136    }
137
138    pub const fn nightly_git(
139        source_bin_dir: &'a Path,
140        bin_dir: &'a Path,
141        bin_name: &'static str,
142        repository: &'static str,
143        branch: &'static str,
144    ) -> Self {
145        Self {
146            source_bin_dir,
147            bin_dir,
148            bin_name,
149            source: Source::Git {
150                repository,
151                branch: Some(branch),
152                locked: true,
153                toolchain: Some("nightly"),
154                package_name: Some(bin_name),
155            },
156        }
157    }
158}
159
160impl Installer for Cargo<'_> {
161    fn bin_name(&self) -> &'static str {
162        self.bin_name
163    }
164
165    fn should_verify_checksum(&self) -> bool {
166        false
167    }
168
169    fn install(&self) -> rootcause::Result<()> {
170        let mut command = ytil_cmd::silent_cmd("cargo");
171        if let Source::Git {
172            toolchain: Some(toolchain),
173            ..
174        } = self.source
175        {
176            command.arg(format!("+{toolchain}"));
177        }
178        command.args(["install", "--force"]);
179
180        match self.source {
181            Source::Registry {
182                crate_name,
183                features,
184                all_features,
185                locked,
186            } => {
187                command.arg(crate_name);
188                if let Some(features) = features {
189                    command.args(["--features", features]);
190                }
191                if all_features {
192                    command.arg("--all-features");
193                }
194                if locked {
195                    command.arg("--locked");
196                }
197            }
198            Source::Git {
199                repository,
200                branch,
201                locked,
202                package_name,
203                ..
204            } => {
205                command.args(["--git", repository]);
206                if let Some(branch) = branch {
207                    command.args(["--branch", branch]);
208                }
209                if locked {
210                    command.arg("--locked");
211                }
212                if let Some(package_name) = package_name {
213                    command.arg(package_name);
214                }
215            }
216        }
217
218        command
219            .status()
220            .context("failed to spawn cargo install")?
221            .exit_ok()
222            .context("cargo install failed")
223            .attach_with(|| format!("tool={}", self.bin_name()))?;
224
225        let cargo_binary = self
226            .source_bin_dir
227            .join(self.bin_name())
228            .canonicalize()
229            .context("could not resolve Cargo-installed binary")?;
230        ytil_sys::file::ln_sf(&cargo_binary, &self.bin_dir.join(self.bin_name()))?;
231        ytil_sys::file::chmod_x(cargo_binary)?;
232
233        Ok(())
234    }
235
236    fn health_check(&self) -> Option<rootcause::Result<String>> {
237        let args = self.health_check_args()?;
238        let mut command = Command::new(self.bin_dir.join(self.bin_name()));
239        command.args(args);
240        Some(run_health_check(command))
241    }
242}
243
244/// Resolve Cargo's binary directory once before installers begin running.
245pub fn cargo_bin_dir() -> rootcause::Result<PathBuf> {
246    Ok(cargo_install_root()?.join("bin"))
247}
248
249/// Resolve Cargo's install-root precedence without scanning installed packages or invoking Cargo.
250fn cargo_install_root() -> rootcause::Result<PathBuf> {
251    if let Some(install_root) = std::env::var_os("CARGO_INSTALL_ROOT") {
252        return Ok(PathBuf::from(install_root));
253    }
254
255    let cargo_home = cargo_home()?;
256    if let Some(install_root) = cargo_config_install_root(&cargo_home)? {
257        return Ok(install_root);
258    }
259
260    Ok(cargo_home)
261}
262
263fn cargo_home() -> rootcause::Result<PathBuf> {
264    if let Some(cargo_home) = std::env::var_os("CARGO_HOME") {
265        return Ok(PathBuf::from(cargo_home));
266    }
267
268    ytil_sys::dir::build_home_path(&[".cargo"])
269}
270
271/// Read `[install].root` from Cargo's standard home configuration file.
272fn cargo_config_install_root(cargo_home: &Path) -> rootcause::Result<Option<PathBuf>> {
273    let config_path = cargo_home.join("config.toml");
274    if !config_path.is_file() {
275        return Ok(None);
276    }
277    let config = std::fs::read_to_string(&config_path)
278        .context("could not read Cargo configuration")
279        .attach_with(|| format!("config={}", config_path.display()))?;
280    let config: toml::Value = toml::from_str(&config)
281        .context("could not parse Cargo configuration")
282        .attach_with(|| format!("config={}", config_path.display()))?;
283    let Some(install_root) = config
284        .get("install")
285        .and_then(toml::Value::as_table)
286        .and_then(|install| install.get("root"))
287    else {
288        return Ok(None);
289    };
290    let Some(install_root) = install_root.as_str() else {
291        bail!("install.root in {} must be a path string", config_path.display());
292    };
293
294    resolve_config_path(&config_path, install_root).map(Some)
295}
296
297fn resolve_config_path(config_path: &Path, install_root: &str) -> rootcause::Result<PathBuf> {
298    let install_root = PathBuf::from(install_root);
299    if install_root.is_absolute() {
300        return Ok(install_root);
301    }
302
303    let Some(config_dir) = config_path.parent() else {
304        bail!("Cargo configuration path has no parent: {}", config_path.display());
305    };
306    Ok(config_dir.join(install_root))
307}