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
11pub 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
244pub fn cargo_bin_dir() -> rootcause::Result<PathBuf> {
246 Ok(cargo_install_root()?.join("bin"))
247}
248
249fn 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
271fn 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}