spin_doctor/rustlang/
target.rs1use anyhow::Result;
2use async_trait::async_trait;
3
4use crate::{Diagnosis, Diagnostic, PatientApp, StopDiagnosing, Treatment};
5
6#[derive(Default)]
8pub struct TargetDiagnostic;
9
10#[async_trait]
11impl Diagnostic for TargetDiagnostic {
12 type Diagnosis = TargetDiagnosis;
13
14 async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>> {
15 let manifest_str = patient.manifest_doc.to_string();
18 let manifest = spin_manifest::manifest_from_str(&manifest_str)?;
19 let uses_rust = manifest.components.values().any(|c| {
20 c.build
21 .as_ref()
22 .map(|b| b.commands().any(|c| c.starts_with("cargo")))
23 .unwrap_or_default()
24 });
25
26 if uses_rust {
27 diagnose_rust_wasi_target().await
28 } else {
29 Ok(vec![])
30 }
31 }
32}
33
34async fn diagnose_rust_wasi_target() -> Result<Vec<TargetDiagnosis>> {
35 let diagnosis = match get_rustup_target_status().await? {
47 RustupStatus::AllInstalled => vec![],
48 RustupStatus::WasiNotInstalled => vec![TargetDiagnosis::WasmTargetNotInstalled],
49 RustupStatus::RustupNotInstalled => match get_cargo_status().await? {
50 CargoStatus::Installed => {
51 terminal::warn!(
52 "Spin Doctor can't determine if the Rust wasm32-wasip2 target is installed."
53 );
54 vec![]
55 }
56 CargoStatus::NotInstalled => vec![TargetDiagnosis::RustNotInstalled],
57 },
58 };
59 Ok(diagnosis)
60}
61
62#[allow(clippy::enum_variant_names)]
63enum RustupStatus {
64 RustupNotInstalled,
65 WasiNotInstalled,
66 AllInstalled,
67}
68
69async fn get_rustup_target_status() -> Result<RustupStatus> {
70 let target_list_output = tokio::process::Command::new("rustup")
71 .args(["target", "list", "--installed"])
72 .output()
73 .await;
74 let status = match target_list_output {
75 Err(e) => {
76 if e.kind() == std::io::ErrorKind::NotFound {
77 RustupStatus::RustupNotInstalled
78 } else {
79 anyhow::bail!("Failed to run `rustup target list --installed`: {e:#}")
80 }
81 }
82 Ok(output) => {
83 let stdout = String::from_utf8_lossy(&output.stdout);
84 if stdout.lines().any(|line| line == "wasm32-wasip2") {
85 RustupStatus::AllInstalled
86 } else {
87 RustupStatus::WasiNotInstalled
88 }
89 }
90 };
91 Ok(status)
92}
93
94enum CargoStatus {
95 Installed,
96 NotInstalled,
97}
98
99async fn get_cargo_status() -> Result<CargoStatus> {
100 let cmd_output = tokio::process::Command::new("cargo")
101 .arg("--version")
102 .output()
103 .await;
104 let status = match cmd_output {
105 Err(e) => {
106 if e.kind() == std::io::ErrorKind::NotFound {
107 CargoStatus::NotInstalled
108 } else {
109 anyhow::bail!("Failed to run `cargo --version`: {e:#}")
110 }
111 }
112 Ok(_) => CargoStatus::Installed,
113 };
114 Ok(status)
115}
116
117#[derive(Debug)]
119pub enum TargetDiagnosis {
120 RustNotInstalled,
122 WasmTargetNotInstalled,
124}
125
126impl Diagnosis for TargetDiagnosis {
127 fn description(&self) -> String {
128 match self {
129 Self::RustNotInstalled => "The Rust compiler isn't installed".into(),
130 Self::WasmTargetNotInstalled => {
131 "The required Rust target 'wasm32-wasip2' isn't installed".into()
132 }
133 }
134 }
135
136 fn treatment(&self) -> Option<&dyn Treatment> {
137 Some(self)
138 }
139}
140
141#[async_trait]
142impl Treatment for TargetDiagnosis {
143 fn summary(&self) -> String {
144 match self {
145 Self::RustNotInstalled => "Install the Rust compiler and the wasm32-wasip2 target",
146 Self::WasmTargetNotInstalled => "Install the Rust wasm32-wasip2 target",
147 }
148 .into()
149 }
150
151 async fn dry_run(&self, _patient: &PatientApp) -> Result<String> {
152 let message = match self {
153 Self::RustNotInstalled => {
154 "Download and run the Rust installer from https://rustup.rs, with the `--target wasm32-wasip2` option"
155 }
156 Self::WasmTargetNotInstalled => {
157 "Run the following command:\n `rustup target add wasm32-wasip2`"
158 }
159 };
160 Ok(message.into())
161 }
162
163 async fn treat(&self, _patient: &mut PatientApp) -> Result<()> {
164 match self {
165 Self::RustNotInstalled => {
166 install_rust_with_wasi_target().await?;
167 }
168 Self::WasmTargetNotInstalled => {
169 install_wasi_target()?;
170 }
171 }
172 Ok(())
173 }
174}
175
176async fn install_rust_with_wasi_target() -> Result<()> {
177 let status = run_rust_installer().await?;
178 anyhow::ensure!(status.success(), "Rust installation failed: {status:?}");
179 let stop = StopDiagnosing::new(
180 "Because Rust was just installed, you may need to run a script or restart your command shell to add Rust to your PATH. Please follow the instructions at the end of the installer output above before re-running `spin doctor`.",
181 );
182 Err(anyhow::anyhow!(stop))
183}
184
185#[cfg(not(windows))]
186async fn run_rust_installer() -> Result<std::process::ExitStatus> {
187 use std::io::Write;
188
189 let resp = reqwest::get("https://sh.rustup.rs").await?;
190 let script = resp.bytes().await?;
191
192 let mut cmd = std::process::Command::new("sh");
193 cmd.args(["-s", "--", "--target", "wasm32-wasip2"]);
194 cmd.stdin(std::process::Stdio::piped());
195 let mut shell = cmd.spawn()?;
196 let mut stdin = shell.stdin.take().unwrap();
197 std::thread::spawn(move || {
198 stdin.write_all(&script).unwrap();
199 });
200
201 let output = shell.wait_with_output()?;
202 Ok(output.status)
203}
204
205#[cfg(windows)]
206async fn run_rust_installer() -> Result<std::process::ExitStatus> {
207 if std::env::consts::ARCH != "x86_64" {
210 anyhow::bail!("Spin Doctor can only install Rust for Windows on x64 processors");
211 }
212
213 let temp_dir = tempfile::TempDir::new()?;
214 let installer_path = temp_dir.path().join("rustup-init.exe");
215
216 let resp = reqwest::get("https://win.rustup.rs/x86_64").await?;
217 let installer_bin = resp.bytes().await?;
218
219 std::fs::write(&installer_path, &installer_bin)?;
220
221 let mut cmd = std::process::Command::new(installer_path);
222 cmd.args(["--target", "wasm32-wasip2"]);
223 let status = cmd.status()?;
224 Ok(status)
225}
226
227fn install_wasi_target() -> Result<()> {
228 let mut cmd = std::process::Command::new("rustup");
229 cmd.args(["target", "add", "wasm32-wasip2"]);
230 let status = cmd.status()?;
231 anyhow::ensure!(
232 status.success(),
233 "Installation command {cmd:?} failed: {status:?}"
234 );
235 Ok(())
236}