1use crate::{
4 toolchain::{Host, Installation, Toolchain, ToolchainError},
5 utils::CommandError,
6 web::PackageManager,
7};
8
9#[derive(Debug, Clone, Copy, Default)]
11pub struct Wasm32UnknownUnknownTarget;
12
13#[derive(Debug, Clone, Copy, Default)]
15pub struct Wasm32UnknownUnknownTargetInstallation;
16
17impl Toolchain for Wasm32UnknownUnknownTarget {
18 type Installation = Wasm32UnknownUnknownTargetInstallation;
19
20 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
21 if host.which("rustup").await.is_err() {
22 return Err(ToolchainError::unfixable(
23 "rustup is not installed or not found in PATH",
24 "Install Rust with rustup from https://rustup.rs/ and re-run the command.",
25 ));
26 }
27
28 let output = host
29 .output("rustup", ["target", "list", "--installed"])
30 .await;
31 let output = match output {
32 Ok(output) => output,
33 Err(error) => {
34 return Err(ToolchainError::unfixable(
35 format!("failed to query installed rustup targets: {error}"),
36 "Ensure rustup is healthy, then run `rustup target add wasm32-unknown-unknown` manually.",
37 ));
38 }
39 };
40
41 if !output.status.success() {
42 return Err(ToolchainError::unfixable(
43 format!(
44 "rustup target query failed: {}",
45 String::from_utf8_lossy(&output.stderr).trim()
46 ),
47 "Ensure rustup is healthy, then run `rustup target add wasm32-unknown-unknown` manually.",
48 ));
49 }
50
51 let installed = String::from_utf8_lossy(&output.stdout);
52 if installed
53 .lines()
54 .any(|line| line.trim() == "wasm32-unknown-unknown")
55 {
56 Ok(())
57 } else {
58 Err(ToolchainError::Fixable(
59 Wasm32UnknownUnknownTargetInstallation,
60 ))
61 }
62 }
63}
64
65impl Installation for Wasm32UnknownUnknownTargetInstallation {
66 type Error = CommandError;
67
68 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
69 host.run("rustup", ["target", "add", "wasm32-unknown-unknown"])
70 .await
71 .map(|_| ())
72 }
73}
74
75#[derive(Debug, Clone, Copy, Default)]
77pub struct WasmPack;
78
79#[derive(Debug, Clone, Copy, Default)]
81pub struct WasmPackInstallation;
82
83impl Toolchain for WasmPack {
84 type Installation = WasmPackInstallation;
85
86 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
87 if host.which("wasm-pack").await.is_ok() {
88 Ok(())
89 } else {
90 Err(ToolchainError::Fixable(WasmPackInstallation))
91 }
92 }
93}
94
95impl Installation for WasmPackInstallation {
96 type Error = CommandError;
97
98 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
99 host.run("cargo", ["install", "wasm-pack"])
100 .await
101 .map(|_| ())
102 }
103}
104
105#[derive(Debug, Clone, Copy)]
109pub struct PackageManagerToolchain(pub PackageManager);
110
111#[derive(Debug, Clone, Copy)]
114pub struct PackageManagerInstallation(pub PackageManager);
115
116impl Toolchain for PackageManagerToolchain {
117 type Installation = PackageManagerInstallation;
118
119 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
120 let package_manager = self.0;
121 if host.which(package_manager.binary()).await.is_ok() {
122 return Ok(());
123 }
124 match package_manager {
125 PackageManager::Npm => Err(ToolchainError::unfixable(
128 "npm is not installed",
129 package_manager.install_hint(),
130 )),
131 _ => Err(ToolchainError::fixable(PackageManagerInstallation(
132 package_manager,
133 ))),
134 }
135 }
136}
137
138impl Installation for PackageManagerInstallation {
139 type Error = eyre::Report;
140
141 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
142 match self.0 {
143 PackageManager::Yarn => host
145 .run("corepack", ["enable"])
146 .await
147 .map(|_| ())
148 .map_err(Into::into),
149 PackageManager::Npm => Err(eyre::eyre!(
150 "npm ships with Node.js; install Node.js from https://nodejs.org/"
151 )),
152 package_manager => {
153 #[cfg(unix)]
154 {
155 let script = match package_manager {
156 PackageManager::Bun => "curl -fsSL https://bun.sh/install | bash",
157 PackageManager::Pnpm => "curl -fsSL https://get.pnpm.io/install.sh | sh -",
158 _ => unreachable!(),
159 };
160 host.run("sh", ["-c", script])
161 .await
162 .map(|_| ())
163 .map_err(Into::into)
164 }
165 #[cfg(windows)]
166 {
167 let script = match package_manager {
168 PackageManager::Bun => "irm bun.sh/install.ps1 | iex",
169 PackageManager::Pnpm => "iwr https://get.pnpm.io/install.ps1 -useb | iex",
170 _ => unreachable!(),
171 };
172 host.run("powershell", ["-c", script])
173 .await
174 .map(|_| ())
175 .map_err(Into::into)
176 }
177 #[cfg(not(any(unix, windows)))]
178 {
179 Err(eyre::eyre!(
180 "no automatic installer for {} on this platform; run: {}",
181 package_manager.binary(),
182 package_manager.install_hint()
183 ))
184 }
185 }
186 }
187 }
188}
189
190pub type WebToolchain = (Wasm32UnknownUnknownTarget, WasmPack);
192
193#[cfg(test)]
194mod tests {
195 use super::{Wasm32UnknownUnknownTarget, WasmPack};
196 use crate::toolchain::testing::TestMachine;
197 use crate::toolchain::{Toolchain, ToolchainError};
198
199 fn machine_with_rustup() -> TestMachine {
200 let machine = TestMachine::new();
201 machine.install("rustup");
202 machine
203 }
204
205 #[test]
206 fn wasm32_target_unfixable_without_rustup() {
207 let machine = TestMachine::new();
208 let host = machine.host(Vec::<(String, String)>::new());
209 let result = smol::block_on(Wasm32UnknownUnknownTarget.check(&host));
210 assert!(
211 matches!(result, Err(ToolchainError::Unfixable(_))),
212 "missing rustup must be unfixable: {result:?}"
213 );
214 }
215
216 #[test]
217 fn wasm32_target_fixable_when_not_installed() {
218 let machine = machine_with_rustup();
219 let host = machine.host([(
220 String::from("WATERUI_FAKE_RUSTUP_INSTALLED_TARGETS"),
221 String::from("aarch64-apple-darwin"),
222 )]);
223 let result = smol::block_on(Wasm32UnknownUnknownTarget.check(&host));
224 assert!(
225 matches!(result, Err(ToolchainError::Fixable(_))),
226 "absent wasm32 target must be fixable: {result:?}"
227 );
228 }
229
230 #[test]
231 fn wasm32_target_ok_when_installed() {
232 let machine = machine_with_rustup();
233 machine.respond(
235 "RUSTUP_INSTALLED_TARGETS",
236 &["aarch64-apple-darwin", "wasm32-unknown-unknown"].join("\n"),
237 );
238 let host = machine.host(Vec::<(String, String)>::new());
239 smol::block_on(Wasm32UnknownUnknownTarget.check(&host))
240 .expect("installed wasm32 target must be ok");
241 }
242
243 #[test]
244 fn wasm_pack_ok_when_on_path() {
245 let machine = TestMachine::new();
246 machine.install("wasm-pack");
247 let host = machine.host(Vec::<(String, String)>::new());
248 smol::block_on(WasmPack.check(&host)).expect("wasm-pack on PATH must be ok");
249 }
250
251 #[test]
252 fn wasm_pack_fixable_when_missing() {
253 let machine = TestMachine::new();
254 let host = machine.host(Vec::<(String, String)>::new());
255 let result = smol::block_on(WasmPack.check(&host));
256 assert!(
257 matches!(result, Err(ToolchainError::Fixable(_))),
258 "missing wasm-pack must be fixable via cargo install: {result:?}"
259 );
260 }
261}