use std::path::{Path, PathBuf};
use std::process::Command;
use crate::targets::host_os;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Need {
Build,
BuildOptional,
Pack,
PackOptional,
Launch,
}
struct Probe {
name: &'static str,
detail: Option<String>,
fix: String,
need: Need,
}
impl Probe {
fn new(name: &'static str, detail: Option<String>, fix: impl Into<String>) -> Self {
Probe {
name,
detail,
fix: fix.into(),
need: Need::Build,
}
}
fn need(mut self, need: Need) -> Self {
self.need = need;
self
}
fn soft(&self) -> bool {
self.need != Need::Build
}
}
struct Group {
id: &'static str,
label: &'static str,
hosts: &'static [&'static str],
probes: Vec<Probe>,
setup: &'static str,
}
impl Group {
fn builds_on(&self, host: &str) -> bool {
self.hosts == ["any"] || self.hosts.contains(&host)
}
}
fn run_line(cmd: &str, args: &[&str]) -> Option<String> {
Command::new(cmd).args(args).output().ok().and_then(|o| {
o.status.success().then(|| {
String::from_utf8_lossy(&o.stdout)
.lines()
.next()
.unwrap_or("")
.trim()
.to_string()
})
})
}
fn run_out(cmd: &str, args: &[&str]) -> Option<String> {
Command::new(cmd).args(args).output().ok().and_then(|o| {
o.status
.success()
.then(|| String::from_utf8_lossy(&o.stdout).into_owned())
})
}
fn existing_dir(dir: &Path) -> Option<String> {
dir.is_dir().then(|| dir.display().to_string())
}
fn have_rust_target(triple: &str) -> Option<String> {
run_line("rustc", &["--print", "target-list"])?; let out = Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.ok()?;
out.status.success().then_some(())?;
String::from_utf8_lossy(&out.stdout)
.lines()
.any(|l| l.trim() == triple)
.then(|| triple.to_string())
}
fn have_any_rust_target(triples: &[&str]) -> Option<String> {
triples.iter().find_map(|t| have_rust_target(t))
}
fn have_jdk() -> Option<String> {
let java = day_toolchain::jdk_home()?.join("bin").join("java");
let out = Command::new(&java).arg("-version").output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stderr);
let major = text
.split(|c: char| c.is_whitespace() || c == '"')
.filter(|t| !t.is_empty())
.find_map(|t| t.split(['.', '-', '_']).next()?.parse::<u32>().ok())?;
(major >= 17).then(|| text.lines().next().unwrap_or("").trim().to_string())
}
fn have_wasm_cc() -> Option<String> {
match day_toolchain::wasm_cc() {
day_toolchain::WasmCc::Env(program) => day_toolchain::emits_wasm32(Path::new(&program))
.then(|| format!("{program} (from a CC variable; wasm32 backend)")),
day_toolchain::WasmCc::PathClang => Some("clang (wasm32 backend)".to_string()),
day_toolchain::WasmCc::Fallback(cc) => Some(format!("{} (auto-selected)", cc.display())),
day_toolchain::WasmCc::Missing => None,
}
}
fn which(bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
let names: Vec<String> = if cfg!(windows) && !bin.ends_with(".exe") {
vec![bin.to_string(), format!("{bin}.exe")]
} else {
vec![bin.to_string()]
};
std::env::split_paths(&path).find_map(|dir| {
names.iter().find_map(|name| {
let p = dir.join(name);
p.is_file().then_some(p)
})
})
}
fn find_rcc() -> Option<PathBuf> {
let names: &[&str] = if cfg!(windows) {
&["rcc.exe", "rcc"]
} else {
&["rcc"]
};
for qmake in ["qmake6", "qmake"] {
for var in ["QT_INSTALL_LIBEXECS", "QT_HOST_BINS"] {
if let Some(dir) = run_line(qmake, &["-query", var]) {
for name in names {
let p = Path::new(&dir).join(name);
if p.is_file() {
return Some(p);
}
}
}
}
}
names.iter().find_map(|n| which(n))
}
fn core_group() -> Group {
Group {
id: "core",
label: "Core toolchain",
hosts: &["any"],
probes: vec![Probe::new(
"rust",
run_line("cargo", &["--version"]),
"install Rust via https://rustup.rs (rustup) or `brew install rust`",
)],
setup: "Install the Rust toolchain from https://rustup.rs, or `brew install rust`. Cross-\n\
compiled targets (iOS/Android/OpenHarmony) additionally need the rustup-managed\n\
toolchain — Homebrew's rustc ships no cross std.",
}
}
fn appkit_group() -> Group {
Group {
id: "appkit",
label: "macOS · AppKit",
hosts: &["macos"],
probes: vec![
Probe::new(
"xcode-clang",
run_line("xcrun", &["--find", "clang"]),
"install the Xcode command-line tools: `xcode-select --install`",
),
Probe::new(
"swift",
run_line("swift", &["--version"]),
"install Xcode or the command-line tools — needed only when a dependency embeds \
Swift/SwiftUI (docs/swiftui.md)",
)
.need(Need::BuildOptional),
],
setup: "macOS desktop (AppKit) needs Apple's clang toolchain: `xcode-select --install`\n\
(or a full Xcode). No extra Rust target — the host toolchain builds it. An app\n\
carrying `platform/macos/DayApp.xcodeproj` builds through xcodebuild, which needs\n\
a full Xcode; `DAY_MACOS_XCODE=0` (or no scaffold) falls back to the bare cargo\n\
build, where the command-line tools are enough. When a dependency contributes\n\
macOS Swift (SwiftUI embedding, docs/swiftui.md), that path adds a `swift build`\n\
prepass and links the result statically — it also needs the `swift` compiler.",
}
}
fn uikit_group() -> Group {
Group {
id: "uikit",
label: "iOS · UIKit",
hosts: &["macos"],
probes: vec![
Probe::new(
"xcode",
run_line("xcodebuild", &["-version"]),
"install Xcode from the App Store (the iOS build drives xcodebuild)",
),
Probe::new(
"rust-ios-sim",
have_rust_target("aarch64-apple-ios-sim"),
"rustup target add aarch64-apple-ios-sim",
),
Probe::new(
"simulator",
run_line(
"bash",
&["-c", "xcrun simctl list devices booted | grep -m1 Booted"],
),
"boot a simulator: `xcrun simctl boot <device>` (or open Simulator.app)",
)
.need(Need::Launch),
],
setup: "iOS (UIKit) cross-compiles via an Xcode script phase and runs on the Simulator.\n\
Needs: full Xcode (`xcode-select -s /Applications/Xcode.app`), the simulator Rust\n\
target `rustup target add aarch64-apple-ios-sim`, and a booted simulator to launch\n\
(`xcrun simctl boot <device>`). iOS builds only on a macOS host.",
}
}
fn gtk_group() -> Group {
Group {
id: "gtk",
label: "GTK 4 · libadwaita",
hosts: &["macos", "linux", "windows"],
probes: vec![
Probe::new(
"gtk4",
run_line("pkg-config", &["--modversion", "gtk4"]),
"install GTK 4 (`brew install gtk4` · `apt install libgtk-4-dev` · MSYS2 mingw-w64-gtk4)",
),
Probe::new(
"libadwaita",
run_line("pkg-config", &["--modversion", "libadwaita-1"]),
"install libadwaita (`brew install libadwaita` · `apt install libadwaita-1-dev`)",
),
Probe::new(
"glib-compile-resources",
which("glib-compile-resources").map(|p| p.display().to_string()),
"install glib tools (bundled with glib/GTK; ships `glib-compile-resources`)",
)
.need(Need::BuildOptional),
Probe::new(
"flatpak-builder",
which("flatpak-builder").map(|p| p.display().to_string()),
"install flatpak + flatpak-builder and add the flathub remote (for `day pack`)",
)
.need(Need::Pack),
Probe::new(
"linuxdeploy",
crate::pack::appimage_tool_probe("linuxdeploy"),
"download linuxdeploy from github.com/linuxdeploy/linuxdeploy/releases (for `day pack` → .appimage)",
)
.need(Need::Pack),
Probe::new(
"linuxdeploy-plugin-gtk",
crate::pack::appimage_tool_probe("linuxdeploy-plugin-gtk"),
"download linuxdeploy-plugin-gtk — without it the AppImage needs a machine that already has GTK",
)
.need(Need::PackOptional),
],
setup: "GTK 4 builds on macOS, Linux, and Windows via pkg-config. Install the dev libraries:\n\
• macOS — `brew install gtk4 libadwaita pkg-config`\n\
• Linux — `apt install libgtk-4-dev libadwaita-1-dev pkg-config`\n\
• Windows— MSYS2: `pacman -S mingw-w64-x86_64-gtk4 mingw-w64-x86_64-libadwaita`\n\
(ARM64 hosts: the CLANGARM64 environment's `mingw-w64-clang-aarch64-` packages),\n\
plus a GNU Rust toolchain — MSVC cannot link MSYS2's import libraries:\n\
`rustup toolchain install stable-x86_64-pc-windows-gnu` (ARM64:\n\
`stable-aarch64-pc-windows-gnullvm`), then build with MSYS2's bin on PATH and\n\
RUSTUP_TOOLCHAIN set to it.\n\
`glib-compile-resources` (ships with glib) compiles bundled resources (§18.3); without\n\
it images fall back to loose files.",
}
}
fn qt_group() -> Group {
Group {
id: "qt",
label: "Qt 6 Widgets",
hosts: &["macos", "linux", "windows"],
probes: vec![
Probe::new(
"qt6-widgets",
run_line("pkg-config", &["--modversion", "Qt6Widgets"])
.or_else(|| run_line("qmake6", &["-query", "QT_VERSION"]))
.or_else(|| run_line("qmake", &["-query", "QT_VERSION"])),
"install Qt 6 (`brew install qt` · `apt install qt6-base-dev` · MSYS2 mingw-w64-qt6-base)",
),
Probe::new(
"rcc",
find_rcc().map(|p| p.display().to_string()),
"install Qt 6 (rcc, the resource compiler, ships in Qt's libexec)",
)
.need(Need::BuildOptional),
Probe::new(
"flatpak-builder",
which("flatpak-builder").map(|p| p.display().to_string()),
"install flatpak + flatpak-builder and add the flathub remote (for `day pack`)",
)
.need(Need::Pack),
Probe::new(
"linuxdeploy",
crate::pack::appimage_tool_probe("linuxdeploy"),
"download linuxdeploy from github.com/linuxdeploy/linuxdeploy/releases (for `day pack` → .appimage)",
)
.need(Need::Pack),
Probe::new(
"linuxdeploy-plugin-qt",
crate::pack::appimage_tool_probe("linuxdeploy-plugin-qt"),
"download linuxdeploy-plugin-qt — without it the AppImage needs a machine that already has Qt",
)
.need(Need::PackOptional),
],
setup: "Qt 6 Widgets builds on macOS, Linux, and Windows. Install Qt 6 and pkg-config:\n\
• macOS — `brew install qt pkg-config`\n\
• Linux — `apt install qt6-base-dev qt6-webengine-dev pkg-config`\n\
• Windows— MSYS2: `pacman -S mingw-w64-x86_64-qt6-base` (ARM64 hosts: the\n\
CLANGARM64 environment's `mingw-w64-clang-aarch64-qt6-base`), plus a GNU Rust\n\
toolchain — MSVC cannot link MSYS2's import libraries, and the C++ shim is built\n\
from pkg-config's flags, which an aqtinstall/online-installer Qt does not ship:\n\
`rustup toolchain install stable-x86_64-pc-windows-gnu` (ARM64:\n\
`stable-aarch64-pc-windows-gnullvm`), then build with MSYS2's bin on PATH and\n\
RUSTUP_TOOLCHAIN set to it.\n\
`rcc` (Qt's resource compiler, §18.3) is resolved from qmake's libexec; a missing Qt\n\
means both the build and bundled-resource staging fail.",
}
}
fn xaml_group() -> Group {
Group {
id: "xaml",
label: "Windows · XAML",
hosts: &["windows"],
probes: vec![
Probe::new(
"msvc-toolchain",
run_out("rustc", &["-vV"]).and_then(|s| {
s.lines()
.find_map(|l| l.strip_prefix("host: "))
.filter(|h| h.contains("windows-msvc"))
.map(str::to_string)
}),
"rustup default stable-msvc + install the VS 2022 C++ Build Tools",
),
Probe::new(
"makeappx (Windows SDK)",
crate::pack::windows_kit_tool_probe("makeappx.exe"),
"install the Windows 10/11 SDK (for `day pack` msix)",
)
.need(Need::Pack),
Probe::new(
"makensis",
day_toolchain::makensis().map(|p| p.display().to_string()),
"choco install nsis (for `day pack` setup.exe)",
)
.need(Need::Pack),
],
setup: "XAML builds on a Windows host with the MSVC toolchain. Install:\n\
• the Visual Studio 2022 C++ Build Tools (MSVC + Windows SDK)\n\
• the MSVC Rust toolchain: `rustup default stable-msvc`\n\
No runtime installer is needed: Day uses system XAML (in Windows 10/11), not\n\
the Windows App SDK. XAML cannot build off a Windows host.",
}
}
fn android_group() -> Group {
let sdk = crate::mobile::android_sdk_dir();
let ndk = crate::mobile::find_ndk().ok();
let adb = sdk.join("platform-tools/adb");
Group {
id: "android",
label: "Android · Material",
hosts: &["any"],
probes: vec![
Probe::new(
"android-sdk",
existing_dir(&sdk),
"install the Android SDK and set ANDROID_HOME (Android Studio, or cmdline-tools)",
),
Probe::new(
"android-ndk",
ndk.as_ref().and_then(|p| existing_dir(p)),
"install an NDK via sdkmanager and/or set ANDROID_NDK_HOME",
),
Probe::new(
"rust-android",
have_any_rust_target(&["aarch64-linux-android", "x86_64-linux-android"]),
"rustup target add aarch64-linux-android (arm64 device/emulator) or x86_64-linux-android (x86_64 emulator)",
),
Probe::new(
"cargo-ndk",
run_line("cargo", &["ndk", "--version"]),
"cargo install cargo-ndk",
),
Probe::new(
"jdk",
have_jdk(),
"install JDK 17 or newer and point JAVA_HOME at it (`brew install openjdk@21`); the Gradle build uses $JAVA_HOME",
),
Probe::new(
"device",
which("adb")
.or_else(|| adb.is_file().then_some(adb.clone()))
.and_then(|adb| {
run_line(&adb.display().to_string(), &["devices"]).and_then(|_| {
run_line(
"bash",
&[
"-c",
&format!("{} devices | grep -m1 -w device", adb.display()),
],
)
})
}),
"start an emulator (`emulator -avd <name>`, or Android Studio's Device Manager) or attach a device",
)
.need(Need::Launch),
],
setup: "Android (Material Components) cross-compiles the app to a JNI .so and runs it in a\n\
Gradle app. Install:\n\
• the Android SDK — set ANDROID_HOME (or ANDROID_SDK_ROOT); Android Studio installs it\n\
at the platform default (~/Library/Android/sdk on macOS; docs/environment.md) otherwise\n\
• an NDK — via `sdkmanager --install 'ndk;<ver>'`; set ANDROID_NDK_HOME to override\n\
• the Android Rust target — `rustup target add aarch64-linux-android`\n\
• `cargo install cargo-ndk`\n\
• JDK 17 or newer — `brew install openjdk@21` (AGP 9's minimum is 17; the Gradle\n\
build uses $JAVA_HOME, so set it if `java` on PATH is older)\n\
A booted emulator or attached device is needed only to launch, not to build. Create\n\
an AVD in Android Studio's Device Manager (or `avdmanager create avd`) and start it\n\
with `emulator -avd <name>` — `day` has no Android-emulator command of its own.",
}
}
fn harmonyos_group() -> Group {
let ndk = crate::ohos::find_ohos_ndk().ok();
let hdc = which("hdc").or_else(|| {
ndk.as_ref().and_then(|n| {
let c = Path::new(n).parent()?.join("toolchains/hdc");
c.is_file().then_some(c)
})
});
Group {
id: "harmonyos",
label: "HarmonyOS · ArkUI",
hosts: &["any"],
probes: vec![
Probe::new(
"ohos-ndk",
ndk.as_ref()
.and_then(|p| existing_dir(&Path::new(p).join("llvm/bin")).map(|_| p.clone())),
"set OHOS_NDK_HOME to the OpenHarmony SDK's `native` dir (see docs/harmonyos.md)",
),
Probe::new(
"rust-ohos",
have_rust_target("aarch64-unknown-linux-ohos")
.or_else(|| have_rust_target("x86_64-unknown-linux-ohos")),
"rustup target add aarch64-unknown-linux-ohos x86_64-unknown-linux-ohos",
),
Probe::new(
"hvigorw",
which("hvigorw").map(|p| p.display().to_string()),
"install the OpenHarmony command-line-tools (hvigor); put its bin/ on PATH",
),
Probe::new(
"ohpm",
which("ohpm").map(|p| p.display().to_string()),
"install the OpenHarmony command-line-tools (ohpm); put its bin/ on PATH",
),
Probe::new(
"hdc",
hdc.map(|p| p.display().to_string()),
"hdc ships with the SDK toolchains/ dir — put it on PATH to install/launch",
)
.need(Need::Launch),
],
setup: "HarmonyOS (ArkUI) cross-compiles a Rust cdylib (libentry.so), packages a .hap with\n\
hvigor, signs it, and installs over hdc. Install:\n\
• the OpenHarmony SDK `native` component — set OHOS_NDK_HOME to it (login-free: extract\n\
the public SDK, see docs/harmonyos.md). `hdc` lives in the sibling toolchains/ dir\n\
• the OpenHarmony Rust targets — `rustup target add aarch64-unknown-linux-ohos\n\
x86_64-unknown-linux-ohos`\n\
• hvigor + ohpm — from the OpenHarmony command-line-tools (bundled with DevEco Studio);\n\
put their bin/ on PATH. These package the .hap and are not part of the public SDK.\n\
An OpenHarmony emulator (Oniro) or device is needed only to launch, not to build —\n\
start the bundled Oniro emulator with `day ohos emulator launch`.",
}
}
fn dom_group() -> Group {
Group {
id: "dom",
label: "Web · DOM",
hosts: &["any"],
probes: vec![
Probe::new(
"rust-wasm",
have_rust_target("wasm32-unknown-unknown"),
"rustup target add wasm32-unknown-unknown",
),
Probe::new(
"wasm-cc",
have_wasm_cc(),
"install a clang with the wasm32 backend (`brew install llvm`, or a swift.org \
toolchain — `day build` finds either), or point CC_wasm32_unknown_unknown at \
one; needed only when the app enables `persistence` (docs/web.md)",
)
.need(Need::BuildOptional),
],
setup: "web-dom (docs/web.md) compiles the app's lib crate to WebAssembly and pairs it with\n\
the host page embedded in the CLI. The Rust target is the whole toolchain for a\n\
UI-only app: `rustup target add wasm32-unknown-unknown`. The `persistence` feature\n\
also compiles the bundled SQLite to wasm, which needs a clang with the wasm32\n\
backend — Apple's has none. `day build` probes plain `clang`, then Homebrew LLVM\n\
and swift.org toolchains, exporting what it finds; a set CC_wasm32_unknown_unknown\n\
(or CC) picks the compiler yourself. `day build -p web-dom` writes a\n\
self-contained static dist/; `day launch -p web-dom` serves it and opens a browser.",
}
}
fn all_groups() -> Vec<Group> {
vec![
core_group(),
appkit_group(),
uikit_group(),
gtk_group(),
qt_group(),
xaml_group(),
android_group(),
harmonyos_group(),
dom_group(),
]
}
pub fn group_id(toolkit: &str) -> &str {
match toolkit {
"mdc" => "android",
"arkui" => "harmonyos",
other => other,
}
}
#[derive(Clone)]
pub struct Missing {
pub name: &'static str,
pub fix: String,
}
#[derive(Clone, Default)]
pub struct Readiness {
pub missing_build: Vec<Missing>,
pub missing_pack: Vec<Missing>,
}
impl Readiness {
pub fn can_build(&self) -> bool {
self.missing_build.is_empty()
}
}
pub fn readiness(group: &str) -> Option<Readiness> {
let g = all_groups().into_iter().find(|g| g.id == group)?;
let mut out = Readiness::default();
for p in g.probes {
if p.detail.is_some() {
continue;
}
let missing = Missing {
name: p.name,
fix: p.fix,
};
match p.need {
Need::Build => out.missing_build.push(missing),
Need::Pack => out.missing_pack.push(missing),
Need::BuildOptional | Need::PackOptional | Need::Launch => {}
}
}
Some(out)
}
use crate::term::{BOLD, DIM, ERROR, ERROR_BOLD, SUCCESS, SUCCESS_BOLD, WARN};
use anstream::eprintln;
#[derive(Default)]
struct Tally {
errors: u32,
warnings: u32,
}
fn report_group(g: &Group, host: &str, hard: bool, show_setup: bool) -> Tally {
eprintln!("{BOLD}{}{BOLD:#}", g.label);
let mut t = Tally::default();
if hard && !g.builds_on(host) {
eprintln!(
" {ERROR}✗{ERROR:#} {:<14} builds on {:?}, not this {host} host",
"host", g.hosts
);
t.errors += 1;
}
for p in &g.probes {
match &p.detail {
Some(d) => eprintln!(" {SUCCESS}✓{SUCCESS:#} {:<14} {d}", p.name),
None if hard && !p.soft() => {
eprintln!(" {ERROR}✗{ERROR:#} {:<14} {}", p.name, p.fix);
t.errors += 1;
}
None => {
eprintln!(" {WARN}⚠{WARN:#} {:<14} {}", p.name, p.fix);
t.warnings += 1;
}
}
}
if show_setup {
eprint_setup(g);
}
t
}
fn eprint_setup(g: &Group) {
eprintln!(" {DIM}── setup ──{DIM:#}");
for line in g.setup.lines() {
eprintln!(" {DIM}{line}{DIM:#}");
}
eprintln!();
}
pub fn run(
focus: &[String],
external: &[crate::external::ExternalToolkit],
) -> Result<i32, crate::cli::CliError> {
let host = host_os();
let groups = all_groups();
let mut known: Vec<&str> = groups.iter().map(|g| g.id).collect();
for e in external {
known.push(e.target.name);
known.push(e.target.toolkit);
}
for f in focus {
if !known.contains(&f.as_str()) {
return Err(crate::cli::CliError::usage(format!(
"unknown toolkit {f:?} — choose from {}",
known
.iter()
.filter(|k| **k != "core")
.cloned()
.collect::<Vec<_>>()
.join(", ")
)));
}
}
if focus.is_empty() {
eprintln!(
"{DIM}Scanning all toolkits buildable on this {host} host. Missing OPTIONAL toolkit\n\
dependencies are warnings; run `day doctor --toolkit <id>` for hard checks + setup help.{DIM:#}\n"
);
} else {
eprintln!(
"{DIM}Focused check: {} (missing pieces are errors).{DIM:#}\n",
focus.join(", ")
);
}
let mut total = Tally::default();
for g in &groups {
let focused = focus.iter().any(|f| f == g.id);
let hard = focused || g.id == "core";
if focus.is_empty() {
if g.id != "core" && !g.builds_on(host) {
eprintln!(
"{BOLD}{}{BOLD:#} {DIM}n/a — builds on {:?}{DIM:#}",
g.label, g.hosts
);
continue;
}
} else if g.id != "core" && !focused {
continue;
}
let t = report_group(g, host, hard, focused);
total.errors += t.errors;
total.warnings += t.warnings;
}
for e in external {
let focused = focus
.iter()
.any(|f| f == e.target.name || f == e.target.toolkit);
if !focus.is_empty() && !focused {
continue;
}
eprintln!(
"{BOLD}{}{BOLD:#} {DIM}external — declared by {}{DIM:#}",
e.target.label, e.crate_name
);
match &e.doctor {
None => eprintln!(" {DIM}– no doctor probe declared{DIM:#}"),
Some(cmd) => {
let mut parts = cmd.split_whitespace();
let bin = parts.next().unwrap_or_default();
let args: Vec<&str> = parts.collect();
match run_line(bin, &args) {
Some(d) => eprintln!(" {SUCCESS}✓{SUCCESS:#} {:<14} {d}", e.target.toolkit),
None => {
eprintln!(
" {ERROR}✗{ERROR:#} {:<14} `{cmd}` failed — see {}'s setup docs",
e.target.toolkit, e.crate_name
);
if focused {
total.errors += 1;
} else {
total.warnings += 1;
}
}
}
}
}
}
eprintln!();
if total.errors > 0 {
eprintln!(
"{ERROR_BOLD}✗ {} error(s){ERROR_BOLD:#}, {} warning(s).",
total.errors, total.warnings
);
Ok(crate::cli::ErrKind::Env.exit_code())
} else if total.warnings > 0 {
eprintln!(
"{WARN}⚠ {} warning(s){WARN:#} — optional toolkits not fully set up. Fine unless you build them.",
total.warnings
);
Ok(0)
} else {
eprintln!("{SUCCESS_BOLD}✓ all good{SUCCESS_BOLD:#}");
Ok(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::targets::TARGETS;
#[test]
fn every_target_toolkit_has_a_doctor_group() {
let groups: Vec<&str> = all_groups().iter().map(|g| g.id).collect();
for t in TARGETS {
let id = group_id(t.toolkit);
assert!(
groups.contains(&id),
"{}: toolkit {:?} maps to {id:?}, which is not a doctor group ({groups:?})",
t.name,
t.toolkit
);
assert!(readiness(id).is_some(), "{id} has no readiness report");
}
}
#[test]
fn every_toolkit_group_states_a_build_prerequisite() {
for g in all_groups() {
if g.id == "core" {
continue;
}
assert!(
g.probes.iter().any(|p| p.need == Need::Build),
"{} has no Need::Build probe",
g.id
);
}
}
#[test]
fn unknown_group_has_no_readiness() {
assert!(readiness("not-a-toolkit").is_none());
}
}