waterui_cli/toolchain/
cargo_helpers.rs1use crate::{
10 toolchain::{Host, Installation, Toolchain, ToolchainError},
11 utils::CommandError,
12};
13
14#[derive(Debug, Clone)]
16pub struct CargoHelpers {
17 required: Vec<String>,
18}
19
20impl CargoHelpers {
21 #[must_use]
23 pub fn new(required: impl IntoIterator<Item = impl Into<String>>) -> Self {
24 Self {
25 required: required.into_iter().map(Into::into).collect(),
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct CargoHelpersInstallation {
33 crates: Vec<String>,
34}
35
36impl CargoHelpersInstallation {
37 pub(crate) const fn new(crates: Vec<String>) -> Self {
39 Self { crates }
40 }
41
42 #[must_use]
45 pub fn describe(&self) -> String {
46 format!(
47 "cargo helpers not on PATH: {} (installed with `cargo install {}`)",
48 self.crates.join(", "),
49 self.crates.join(" ")
50 )
51 }
52}
53
54#[derive(Debug, thiserror::Error)]
56pub enum FailToInstallCargoHelpers {
57 #[error(
59 "cargo is required to install cargo helpers but is not on PATH; fix the `rust` doctor item first."
60 )]
61 CargoNotFound,
62 #[error("Failed to install `{krate}` with cargo: {source}")]
64 Install {
65 krate: String,
67 source: CommandError,
69 },
70}
71
72impl Toolchain for CargoHelpers {
73 type Installation = CargoHelpersInstallation;
74
75 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
76 let mut missing = Vec::new();
77 for binary in &self.required {
78 if host.which(binary).await.is_err() {
79 missing.push(binary.clone());
80 }
81 }
82 if missing.is_empty() {
83 return Ok(());
84 }
85 if host.which("cargo").await.is_err() {
86 return Err(ToolchainError::unfixable(
87 format!("cargo helpers not on PATH: {}", missing.join(", ")),
88 format!(
89 "Install Rust via rustup (fix the `rust` doctor item first), then run `cargo install {}`.",
90 missing.join(" ")
91 ),
92 ));
93 }
94 Err(ToolchainError::fixable(CargoHelpersInstallation::new(
95 missing,
96 )))
97 }
98}
99
100impl Installation for CargoHelpersInstallation {
101 type Error = FailToInstallCargoHelpers;
102
103 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
104 if host.which("cargo").await.is_err() {
105 return Err(FailToInstallCargoHelpers::CargoNotFound);
106 }
107 let binstall = host.which("cargo-binstall").await.is_ok();
110 for krate in &self.crates {
111 if binstall {
112 host.run("cargo", ["binstall", "--no-confirm", krate.as_str()])
113 .await
114 } else {
115 host.run("cargo", ["install", "--locked", krate.as_str()])
116 .await
117 }
118 .map_err(|source| FailToInstallCargoHelpers::Install {
119 krate: krate.clone(),
120 source,
121 })?;
122 }
123 Ok(())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::CargoHelpers;
130 use crate::toolchain::testing::TestMachine;
131 use crate::toolchain::{Toolchain, ToolchainError};
132
133 #[test]
134 fn helpers_ok_when_on_path() {
135 let machine = TestMachine::new();
136 machine.install("cargo-nextest");
137 let host = machine.host(Vec::<(String, String)>::new());
138 smol::block_on(CargoHelpers::new(["cargo-nextest"]).check(&host))
139 .expect("helper on PATH must be ok");
140 }
141
142 #[test]
143 fn helpers_fixable_when_cargo_present() {
144 let machine = TestMachine::new();
145 machine.install("cargo");
146 let host = machine.host(Vec::<(String, String)>::new());
147 let result = smol::block_on(CargoHelpers::new(["cargo-nextest"]).check(&host));
148 assert!(
149 matches!(result, Err(ToolchainError::Fixable(_))),
150 "missing helper with cargo present must be fixable: {result:?}"
151 );
152 }
153
154 #[test]
155 fn helpers_unfixable_without_cargo() {
156 let machine = TestMachine::new();
157 let host = machine.host(Vec::<(String, String)>::new());
158 let result = smol::block_on(CargoHelpers::new(["cargo-nextest"]).check(&host));
159 match &result {
160 Err(ToolchainError::Unfixable(error)) => {
161 assert!(
162 error.suggestion().contains("cargo install"),
163 "the manual repair must name cargo install: {}",
164 error.suggestion()
165 );
166 }
167 other => panic!("missing helper without cargo must be manual: {other:?}"),
168 }
169 }
170}