1#![doc = include_str!("../README.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5use 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}