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-wasip1, return WasmTargetNotInstalled
41    // - if the list does contain wasm32-wasip1, 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-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/// 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-wasip1 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-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    // We currently distribute Windows builds only for x64, so hopefully
202    // this won't be an issue.
203    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}