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-wasip1 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-wasip1") {
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-wasip1' 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-wasip1 target",
146 Self::WasmTargetNotInstalled => "Install the Rust wasm32-wasip1 target",
147 }
148 .into()
149 }
150
151 async fn dry_run(&self, _patient: &PatientApp) -> Result<String> {
152 let message = match self {
153 Self::RustNotInstalled => "Download and run the Rust installer from https://rustup.rs, with the `--target wasm32-wasip1` option",
154 Self::WasmTargetNotInstalled => "Run the following command:\n `rustup target add wasm32-wasip1`",
155 };
156 Ok(message.into())
157 }
158
159 async fn treat(&self, _patient: &mut PatientApp) -> Result<()> {
160 match self {
161 Self::RustNotInstalled => {
162 install_rust_with_wasi_target().await?;
163 }
164 Self::WasmTargetNotInstalled => {
165 install_wasi_target()?;
166 }
167 }
168 Ok(())
169 }
170}
171
172async fn install_rust_with_wasi_target() -> Result<()> {
173 let status = run_rust_installer().await?;
174 anyhow::ensure!(status.success(), "Rust installation failed: {status:?}");
175 let stop = StopDiagnosing::new("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`.");
176 Err(anyhow::anyhow!(stop))
177}
178
179#[cfg(not(windows))]
180async fn run_rust_installer() -> Result<std::process::ExitStatus> {
181 use std::io::Write;
182
183 let resp = reqwest::get("https://sh.rustup.rs").await?;
184 let script = resp.bytes().await?;
185
186 let mut cmd = std::process::Command::new("sh");
187 cmd.args(["-s", "--", "--target", "wasm32-wasip1"]);
188 cmd.stdin(std::process::Stdio::piped());
189 let mut shell = cmd.spawn()?;
190 let mut stdin = shell.stdin.take().unwrap();
191 std::thread::spawn(move || {
192 stdin.write_all(&script).unwrap();
193 });
194
195 let output = shell.wait_with_output()?;
196 Ok(output.status)
197}
198
199#[cfg(windows)]
200async fn run_rust_installer() -> Result<std::process::ExitStatus> {
201 if std::env::consts::ARCH != "x86_64" {
204 anyhow::bail!("Spin Doctor can only install Rust for Windows on x64 processors");
205 }
206
207 let temp_dir = tempfile::TempDir::new()?;
208 let installer_path = temp_dir.path().join("rustup-init.exe");
209
210 let resp = reqwest::get("https://win.rustup.rs/x86_64").await?;
211 let installer_bin = resp.bytes().await?;
212
213 std::fs::write(&installer_path, &installer_bin)?;
214
215 let mut cmd = std::process::Command::new(installer_path);
216 cmd.args(["--target", "wasm32-wasip1"]);
217 let status = cmd.status()?;
218 Ok(status)
219}
220
221fn install_wasi_target() -> Result<()> {
222 let mut cmd = std::process::Command::new("rustup");
223 cmd.args(["target", "add", "wasm32-wasip1"]);
224 let status = cmd.status()?;
225 anyhow::ensure!(
226 status.success(),
227 "Installation command {cmd:?} failed: {status:?}"
228 );
229 Ok(())
230}