Skip to main content

ordinary_doctor/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5// Copyright (C) 2026 Ordinary Labs, LLC.
6//
7// SPDX-License-Identifier: AGPL-3.0-only
8
9use std::env::home_dir;
10use std::process::{Command, Output};
11
12const RUST_SH: &str = include_str!("../scripts/rust.sh");
13
14#[cfg(all(target_os = "macos", any(target_arch = "aarch64", target_arch = "arm")))]
15const BINARYEN_ARM64_MACOS_SH: &str = include_str!("../scripts/binaryen-arm64-macos.sh");
16#[cfg(all(target_os = "macos", any(target_arch = "x86_64", target_arch = "x86")))]
17const BINARYEN_X86_64_MACOS_SH: &str = include_str!("../scripts/binaryen-x86_64-macos.sh");
18#[cfg(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "arm")))]
19const BINARYEN_AARCH64_LINUX_SH: &str = include_str!("../scripts/binaryen-aarch64-linux.sh");
20#[cfg(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "x86")))]
21const BINARYEN_X86_64_LINUX_SH: &str = include_str!("../scripts/binaryen-x86_64-linux.sh");
22
23use clap::builder::PossibleValue;
24
25#[derive(Clone, Debug, PartialOrd, PartialEq)]
26pub enum Fix {
27    All,
28    Rust,
29    Wasm,
30    WasmOpt,
31}
32
33impl clap::ValueEnum for Fix {
34    fn value_variants<'a>() -> &'a [Self] {
35        &[Self::All, Self::Rust, Self::Wasm, Self::WasmOpt]
36    }
37
38    fn to_possible_value(&self) -> Option<PossibleValue> {
39        match self {
40            Self::All => Some(PossibleValue::new("all")),
41            Self::Rust => Some(PossibleValue::new("rust")),
42            Self::Wasm => Some(PossibleValue::new("wasm")),
43            Self::WasmOpt => Some(PossibleValue::new("wasm-opt")),
44        }
45    }
46}
47
48pub fn doctor(fix: &[Fix]) -> anyhow::Result<()> {
49    let fix_all = fix.contains(&Fix::All);
50
51    let installed_components = installed_components()?;
52
53    let mut rustup_installed = false;
54
55    for component in &installed_components {
56        if component.contains("rustup") {
57            rustup_installed = true;
58        }
59    }
60
61    if !rustup_installed {
62        install_rust()?;
63    }
64
65    if installed_components.contains(&"rustup-1.29.0".into()) {
66        tracing::info!("rustup up to date!");
67    } else if fix.contains(&Fix::Rust) || fix_all {
68        update_rustup()?;
69    } else {
70        tracing::error!("rustup-1.29.0 is not installed");
71    }
72
73    if installed_components.contains(&"cargo-1.94.0".into()) {
74        tracing::info!("cargo up to date!");
75    } else if fix.contains(&Fix::Rust) || fix_all {
76        update_cargo()?;
77    } else {
78        tracing::error!("cargo-1.94.0 is not installed");
79    }
80
81    if fix.contains(&Fix::Wasm) || fix_all {
82        add_wasm_targets()?;
83    }
84
85    if installed_components.contains(&"wasm-opt-128".into()) {
86        tracing::info!("wasm-opt up to date!");
87    } else if fix.contains(&Fix::WasmOpt) || fix_all {
88        install_wasm_opt()?;
89    } else {
90        tracing::error!("wasm-opt-128 is not installed");
91    }
92
93    Ok(())
94}
95
96#[allow(clippy::missing_panics_doc)]
97pub fn installed_components() -> anyhow::Result<Vec<String>> {
98    tracing::info!("checking host for installed components...");
99
100    let mut list = Vec::new();
101
102    if let Ok(output) = Command::new("rustup").args(["--version"]).output()
103        && output.status.success()
104        && let Ok(stdout) = std::str::from_utf8(&output.stdout)
105        && let Some(version) = stdout.split(' ').nth(1)
106    {
107        list.push(format!("rustup-{version}"));
108    }
109
110    if let Ok(output) = Command::new("cargo").args(["--version"]).output()
111        && output.status.success()
112        && let Ok(stdout) = std::str::from_utf8(&output.stdout)
113        && let Some(version) = stdout.split(' ').nth(1)
114    {
115        list.push(format!("cargo-{version}"));
116    }
117
118    let wasm_opt_path = home_dir()
119        .expect("home dir doesn't exist")
120        .join(".ordinary")
121        .join("bin")
122        .join("wasm-opt");
123
124    if let Ok(output) = Command::new(wasm_opt_path).args(["--version"]).output()
125        && output.status.success()
126        && let Ok(stdout) = std::str::from_utf8(&output.stdout)
127        && let Some(version) = stdout.split(' ').nth(2)
128    {
129        list.push(format!("wasm-opt-{version}",));
130    }
131
132    if let Ok(output) = Command::new("pnpm").args(["--version"]).output()
133        && output.status.success()
134        && let Ok(version) = std::str::from_utf8(&output.stdout)
135    {
136        let mut out = format!("pnpm-{version}");
137
138        out.pop();
139        list.push(out);
140    }
141
142    for item in &list {
143        tracing::info!("lang '{}' components installed!", item);
144    }
145
146    Ok(list)
147}
148
149pub fn install_rust() -> anyhow::Result<()> {
150    tracing::info!("installing rust...");
151
152    match Command::new("sh").args(["-c", RUST_SH]).output() {
153        Ok(output) => {
154            trace_stdio(&output)?;
155        }
156        Err(err) => {
157            tracing::error!("failed to install rust: {}", err);
158        }
159    }
160
161    Ok(())
162}
163
164pub fn update_rustup() -> anyhow::Result<()> {
165    tracing::info!("updating rustup...");
166
167    match Command::new("rustup").args(["self", "update"]).output() {
168        Ok(output) => {
169            trace_stdio(&output)?;
170        }
171        Err(err) => {
172            tracing::error!("failed to update rustup: {}", err);
173        }
174    }
175
176    Ok(())
177}
178
179pub fn update_cargo() -> anyhow::Result<()> {
180    tracing::info!("updating cargo...");
181
182    match Command::new("rustup").arg("update").output() {
183        Ok(output) => {
184            trace_stdio(&output)?;
185        }
186        Err(err) => {
187            tracing::error!("failed to update cargo: {}", err);
188        }
189    }
190
191    Ok(())
192}
193
194pub fn add_wasm_targets() -> anyhow::Result<()> {
195    tracing::info!("adding rustup targets 'wasm32-wasip1' and 'wasm32-unknown-unknown'...");
196
197    match Command::new("rustup")
198        .args(["target", "add", "wasm32-wasip1", "wasm32-unknown-unknown"])
199        .output()
200    {
201        Ok(output) => {
202            trace_stdio(&output)?;
203        }
204        Err(err) => {
205            tracing::error!("failed to add wasm targets: {}", err);
206        }
207    }
208
209    Ok(())
210}
211
212pub fn install_wasm_opt() -> anyhow::Result<()> {
213    tracing::info!("installing wasm-opt...");
214
215    #[cfg(all(target_os = "macos", any(target_arch = "aarch64", target_arch = "arm")))]
216    let script = BINARYEN_ARM64_MACOS_SH;
217    #[cfg(all(target_os = "macos", any(target_arch = "x86_64", target_arch = "x86")))]
218    let script = BINARYEN_X86_64_MACOS_SH;
219    #[cfg(all(target_os = "linux", any(target_arch = "aarch64", target_arch = "arm")))]
220    let script = BINARYEN_AARCH64_LINUX_SH;
221    #[cfg(all(target_os = "linux", any(target_arch = "x86_64", target_arch = "x86")))]
222    let script = BINARYEN_X86_64_LINUX_SH;
223
224    match Command::new("sh").args(["-c", script]).output() {
225        Ok(output) => {
226            trace_stdio(&output)?;
227        }
228        Err(err) => {
229            tracing::error!("failed to install rust: {}", err);
230        }
231    }
232
233    Ok(())
234}
235
236fn trace_stdio(output: &Output) -> anyhow::Result<()> {
237    for line in std::str::from_utf8(&output.stdout)?.split('\n') {
238        tracing::info!(stdout = true, "{line}");
239    }
240
241    for line in std::str::from_utf8(&output.stderr)?.split('\n') {
242        tracing::info!(stderr = true, "{line}");
243    }
244
245    Ok(())
246}