use crate::sdk::{Sdk, host_abi};
use crate::store::Store;
use crate::style::Style;
use anyhow::Result;
use std::process::Command;
#[derive(Clone, Copy, PartialEq)]
enum Level {
Ok,
Warn,
Fail,
}
struct Check {
name: &'static str,
level: Level,
detail: String,
fix: Option<String>,
}
fn out(cmd: &mut Command) -> Option<String> {
let o = cmd.output().ok()?;
o.status
.success()
.then(|| String::from_utf8_lossy(&o.stdout).trim().to_string())
}
fn first_line(s: &str) -> String {
s.lines().next().unwrap_or("").to_string()
}
fn collect() -> Vec<Check> {
let mut v = Vec::new();
let mut push = |name, level, detail: String, fix: Option<&str>| {
v.push(Check {
name,
level,
detail,
fix: fix.map(String::from),
})
};
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let ver = if os == "macos" {
out(Command::new("sw_vers").arg("-productVersion"))
} else {
None
};
let supported = (os == "macos" && arch == "aarch64") || os == "linux";
push(
"platform",
if supported { Level::Ok } else { Level::Warn },
format!("{os} {} {arch}", ver.unwrap_or_default())
.trim()
.to_string(),
(!supported)
.then_some("dorker targets macOS Apple Silicon first; other hosts are untested"),
);
let sdk = Sdk::locate();
match &sdk {
Ok(s) => push("android sdk", Level::Ok, s.root.display().to_string(), None),
Err(e) => push(
"android sdk",
Level::Fail,
first_line(&e.to_string()),
Some("brew install --cask android-commandlinetools # then set ANDROID_HOME"),
),
}
if let Ok(s) = &sdk {
if s.emulator().is_file() {
let v = out(Command::new(s.emulator()).arg("-version")).unwrap_or_default();
let line = v
.lines()
.find(|l| l.contains("emulator version"))
.unwrap_or("")
.to_string();
push("emulator", Level::Ok, line, None);
} else {
push(
"emulator",
Level::Fail,
format!("not found at {}", s.emulator().display()),
Some("android sdk install emulator"),
);
}
if s.adb().is_file() {
let v = out(Command::new(s.adb()).arg("version")).unwrap_or_default();
push("adb", Level::Ok, first_line(&v), None);
} else {
push(
"adb",
Level::Fail,
format!("not found at {}", s.adb().display()),
Some("android sdk install platform-tools"),
);
}
let images = s.system_images();
let host: Vec<_> = images.iter().filter(|i| i.abi == host_abi()).collect();
if host.is_empty() {
push(
"system images",
Level::Fail,
format!("no {} images installed", host_abi()),
Some(&format!(
"dorker pull android:35 (or: android sdk install \"system-images;android-35;google_apis;{}\")",
host_abi()
)),
);
} else {
let list: Vec<String> = host
.iter()
.map(|i| {
format!(
"android:{}-{}",
crate::sdk::version_label(i.api, i.minor),
i.flavour
)
})
.collect();
push("system images", Level::Ok, list.join(", "), None);
}
}
if os == "macos" {
let hv = out(Command::new("sysctl").args(["-n", "kern.hv_support"]));
match hv.as_deref() {
Some("1") => push(
"acceleration",
Level::Ok,
"Hypervisor.framework available".into(),
None,
),
_ => push(
"acceleration",
Level::Fail,
"Hypervisor.framework unavailable".into(),
Some("Run on bare-metal macOS; nested virtualisation is not supported"),
),
}
} else if std::path::Path::new("/dev/kvm").exists() {
push("acceleration", Level::Ok, "/dev/kvm present".into(), None);
} else {
push(
"acceleration",
Level::Fail,
"/dev/kvm missing".into(),
Some("Enable KVM and add your user to the kvm group"),
);
}
let home = crate::sdk::dirs_home().unwrap_or_else(|| "/".into());
if let Some(df) = out(Command::new("df").arg("-k").arg(&home)) {
let avail_kb: u64 = df
.lines()
.nth(1)
.and_then(|l| l.split_whitespace().nth(3))
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let gb = avail_kb / 1024 / 1024;
push(
"disk space",
if gb >= 10 { Level::Ok } else { Level::Warn },
format!("{gb} GB free"),
(gb < 10).then_some("each instance needs roughly 4-8 GB; free up space"),
);
}
match Store::open() {
Ok(_) => push(
"state dir",
Level::Ok,
crate::store::default_root()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "~/.dorker".into()),
None,
),
Err(e) => push(
"state dir",
Level::Fail,
e.to_string(),
Some("check permissions on ~/.dorker"),
),
}
v
}
fn render(checks: &[Check], failed: bool, st: &Style) -> String {
let mut out = String::new();
for c in checks {
let mark = match c.level {
Level::Ok => st.green("ok "),
Level::Warn => st.yellow("warn"),
Level::Fail => st.red_bold("FAIL"),
};
let name = st.bold(&format!("{:<14}", c.name));
let detail = match c.level {
Level::Ok => c.detail.clone(),
Level::Warn => st.yellow(&c.detail),
Level::Fail => st.red_bold(&c.detail),
};
out.push_str(&format!("[{mark}] {name} {detail}\n"));
if let (Some(fix), true) = (&c.fix, c.level != Level::Ok) {
out.push_str(&format!(" {} {}\n", st.dim("fix:"), st.cyan(fix)));
}
}
let verdict = if failed {
st.red_bold("Some checks failed.")
} else {
st.green_bold("All required checks passed.")
};
out.push_str(&format!("\n{verdict}\n"));
out
}
pub fn run(json: bool) -> Result<i32> {
let checks = collect();
let failed = checks.iter().any(|c| c.level == Level::Fail);
if json {
let rows: Vec<_> = checks
.iter()
.map(|c| {
serde_json::json!({
"check": c.name,
"status": match c.level { Level::Ok => "ok", Level::Warn => "warn", Level::Fail => "fail" },
"detail": c.detail, "fix": c.fix,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({ "ok": !failed, "checks": rows }))?
);
} else {
print!("{}", render(&checks, failed, &Style::stdout()));
}
Ok(if failed { crate::errors::DEPENDENCY } else { 0 })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::strip_ansi;
fn sample() -> Vec<Check> {
vec![
Check {
name: "platform",
level: Level::Ok,
detail: "macos 26 aarch64".into(),
fix: None,
},
Check {
name: "disk space",
level: Level::Warn,
detail: "3 GB free".into(),
fix: Some("free up space".into()),
},
Check {
name: "emulator",
level: Level::Fail,
detail: "not found".into(),
fix: Some("android sdk install emulator".into()),
},
]
}
#[test]
fn plain_output_is_unchanged_and_has_no_escapes() {
let plain = render(&sample(), true, &Style { on: false });
assert!(!plain.contains('\x1b'));
assert_eq!(
plain,
"[ok ] platform macos 26 aarch64\n\
[warn] disk space 3 GB free\n fix: free up space\n\
[FAIL] emulator not found\n fix: android sdk install emulator\n\
\nSome checks failed.\n"
);
}
#[test]
fn colour_adds_escapes_without_changing_the_text_or_columns() {
let plain = render(&sample(), true, &Style { on: false });
let styled = render(&sample(), true, &Style { on: true });
assert!(styled.contains("\x1b[32mok \x1b[0m"), "ok is green");
assert!(styled.contains("\x1b[33mwarn\x1b[0m"), "warn is yellow");
assert!(styled.contains("\x1b[1;31mFAIL\x1b[0m"), "FAIL is bold red");
assert_eq!(
strip_ansi(&styled),
plain,
"same text and alignment once the colour is removed"
);
let ok = render(&sample()[..1], false, &Style { on: true });
assert!(ok.contains("\x1b[1;32mAll required checks passed.\x1b[0m"));
}
}