Skip to main content

spin_doctor/rustlang/
target.rs

1use anyhow::Result;
2use async_trait::async_trait;
3
4use crate::{Diagnosis, Diagnostic, PatientApp, StopDiagnosing, Treatment};
5
6/// VersionDiagnostic detects problems with the app manifest version field.
7#[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        // TODO: this, down to the "does the app use Rust" check, probably ought to move up to the Rust level
16        // but we can defer this until we have more Rust diagnoses
17        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    // does any component contain a build command with `cargo` as the program?
36    // if so, run rustup target list --installed and:
37    // - if rustup is not present, check if cargo is present
38    //   - if not, return RustNotInstalled
39    //   - if so, warn but return empty list (Rust is installed but not via rustup, so we can't perform a diagnosis - bit of an edge case this one, and the user probably knows what they're doing...?)
40    // - if rustup is present but the list does not contain wasm32-wasip2, return WasmTargetNotInstalled
41    // - if the list does contain wasm32-wasip2, return an empty list
42    // NOTE: this does not currently check against the Rust SDK MSRV - that could
43    // be a future enhancement or separate diagnosis, but at least the Rust compiler
44    // should give a clear error for that!
45
46    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/// TargetDiagnosis represents a problem with the Rust target.
118#[derive(Debug)]
119pub enum TargetDiagnosis {
120    /// Rust is not installed: neither cargo nor rustup is present
121    RustNotInstalled,
122    /// The Rust wasm32-wasip2 target is not installed: rustup is present but the target isn't
123    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    // We currently distribute Windows builds only for x64, so hopefully
208    // this won't be an issue.
209    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}