1use std::process::Command;
11
12use serde::Serialize;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "kebab-case")]
17pub enum ProbeClass {
18 Hard,
20 Soft,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum ProbeStatus {
28 Ok,
30 Failed,
32}
33
34#[derive(Debug, Serialize)]
36pub struct ProbeResult {
37 pub id: &'static str,
39 pub class: ProbeClass,
41 pub status: ProbeStatus,
43 pub message: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub remediation: Option<String>,
48}
49
50impl ProbeResult {
51 fn ok(id: &'static str, class: ProbeClass, message: impl Into<String>) -> Self {
52 Self {
53 id,
54 class,
55 status: ProbeStatus::Ok,
56 message: message.into(),
57 remediation: None,
58 }
59 }
60
61 fn failed(
62 id: &'static str,
63 class: ProbeClass,
64 message: impl Into<String>,
65 remediation: impl Into<String>,
66 ) -> Self {
67 Self {
68 id,
69 class,
70 status: ProbeStatus::Failed,
71 message: message.into(),
72 remediation: Some(remediation.into()),
73 }
74 }
75}
76
77#[must_use]
79pub fn run_all() -> Vec<ProbeResult> {
80 vec![
81 shell(),
82 state_root(),
83 git_remote(),
84 forge_cli(
85 "gh-auth",
86 "RK_GH_BIN",
87 "gh",
88 "the GitHub CLI",
89 "gh auth login",
90 &[&["auth", "status", "--active"], &["auth", "status"]],
95 ),
96 forge_cli(
97 "glab-auth",
98 "RK_GLAB_BIN",
99 "glab",
100 "the GitLab CLI",
101 "glab auth login",
102 &[&["auth", "status"]],
103 ),
104 tool(
105 "openssl",
106 "RK_OPENSSL_BIN",
107 "openssl",
108 "OpenSSL; install-bot signs the App JWT with it",
109 &["version"],
110 ),
111 tool(
112 "curl",
113 "RK_CURL_BIN",
114 "curl",
115 "curl; install-bot reads the installation and rk versions --check fetches with it",
116 &["--version"],
117 ),
118 ]
119}
120
121fn tool(
125 id: &'static str,
126 env_override: &str,
127 default_bin: &str,
128 label: &str,
129 args: &[&str],
130) -> ProbeResult {
131 let bin = std::env::var(env_override).unwrap_or_else(|_| default_bin.to_owned());
132 match Command::new(&bin).args(args).output() {
133 Ok(out) if out.status.success() => {
134 ProbeResult::ok(id, ProbeClass::Soft, format!("{default_bin} runs"))
135 }
136 Ok(_) => ProbeResult::failed(
137 id,
138 ProbeClass::Soft,
139 format!("{default_bin} does not answer {}", args.join(" ")),
140 format!("repair {label}"),
141 ),
142 Err(_) => ProbeResult::failed(
143 id,
144 ProbeClass::Soft,
145 format!("{default_bin} is not on PATH"),
146 format!("install {label}"),
147 ),
148 }
149}
150
151fn shell() -> ProbeResult {
153 let id = "sh";
154 match Command::new("sh").args(["-c", "exit 0"]).status() {
155 Ok(status) if status.success() => ProbeResult::ok(id, ProbeClass::Hard, "sh runs"),
156 Ok(status) => ProbeResult::failed(
157 id,
158 ProbeClass::Hard,
159 format!("sh exited {status}"),
160 "repair the POSIX shell on PATH",
161 ),
162 Err(source) => ProbeResult::failed(
163 id,
164 ProbeClass::Hard,
165 format!("sh does not spawn: {source}"),
166 "install a POSIX shell on PATH",
167 ),
168 }
169}
170
171fn state_root() -> ProbeResult {
174 let id = "state-root";
175 let Some(root) = crate::applog::state_root() else {
176 return ProbeResult::failed(
177 id,
178 ProbeClass::Hard,
179 "neither XDG_STATE_HOME nor HOME is set",
180 "export HOME, or XDG_STATE_HOME",
181 );
182 };
183 let display = root.display().to_string();
184 let probe = root.join(format!(".probe-{}", std::process::id()));
185 let written = std::fs::create_dir_all(&root).and_then(|()| std::fs::write(&probe, b"probe"));
186 let _ = std::fs::remove_file(&probe);
187 match written {
188 Ok(()) => ProbeResult::ok(id, ProbeClass::Hard, format!("{display} is writable")),
189 Err(source) => ProbeResult::failed(
190 id,
191 ProbeClass::Hard,
192 format!("{display} is not writable: {source}"),
193 format!("make {display} writable"),
194 ),
195 }
196}
197
198fn git_remote() -> ProbeResult {
201 let id = "git-remote";
202 let out = Command::new("git")
203 .args(["remote", "get-url", "origin"])
204 .output();
205 let url = match out {
206 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(),
207 _ => {
208 return ProbeResult::failed(
209 id,
210 ProbeClass::Soft,
211 "the working directory has no origin remote",
212 "pass --repo <owner/name> where a command needs the slug",
213 );
214 }
215 };
216 remote_host(&url).map_or_else(
220 || {
221 ProbeResult::failed(
222 id,
223 ProbeClass::Soft,
224 "the origin remote does not parse to a host",
225 "pass --repo <owner/name> where a command needs the slug",
226 )
227 },
228 |host| ProbeResult::ok(id, ProbeClass::Soft, format!("origin resolves to {host}")),
229 )
230}
231
232fn remote_host(url: &str) -> Option<String> {
234 if let Some(rest) = url.split_once("://").map(|(_, rest)| rest) {
235 let authority = rest.split('/').next()?;
236 let host = authority
237 .rsplit_once('@')
238 .map_or(authority, |(_, host)| host);
239 let host = host.split(':').next()?;
240 return (!host.is_empty()).then(|| host.to_owned());
241 }
242 let (authority, path) = url.split_once(':')?;
243 let host = authority
244 .rsplit_once('@')
245 .map_or(authority, |(_, host)| host);
246 (!host.is_empty() && !path.is_empty()).then(|| host.to_owned())
247}
248
249fn forge_cli(
255 id: &'static str,
256 env_override: &str,
257 default_bin: &str,
258 label: &str,
259 login: &str,
260 attempts: &[&[&str]],
261) -> ProbeResult {
262 let bin = std::env::var(env_override).unwrap_or_else(|_| default_bin.to_owned());
263 let mut spawned = false;
264 for args in attempts {
265 match Command::new(&bin).args(*args).output() {
266 Ok(out) if out.status.success() => {
267 return ProbeResult::ok(
268 id,
269 ProbeClass::Soft,
270 format!("{default_bin} is authenticated"),
271 );
272 }
273 Ok(_) => spawned = true,
274 Err(_) => {}
275 }
276 }
277 if spawned {
278 ProbeResult::failed(
279 id,
280 ProbeClass::Soft,
281 format!("{default_bin} is not authenticated"),
282 format!("run {login}"),
283 )
284 } else {
285 ProbeResult::failed(
286 id,
287 ProbeClass::Soft,
288 format!("{default_bin} is not on PATH"),
289 format!("install {label}"),
290 )
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::remote_host;
297
298 #[test]
299 fn a_remote_host_parses_from_both_url_forms() {
300 assert_eq!(
301 remote_host("https://github.com/owner/name.git").as_deref(),
302 Some("github.com")
303 );
304 assert_eq!(
305 remote_host("git@gitlab.com:group/sub/name.git").as_deref(),
306 Some("gitlab.com")
307 );
308 assert_eq!(
309 remote_host("ssh://git@github.com:22/owner/name.git").as_deref(),
310 Some("github.com")
311 );
312 assert_eq!(remote_host("not a url"), None);
313 }
314}