use std::path::{Path, PathBuf};
use std::process::Command;
use crate::targets::host_os;
struct Probe {
name: &'static str,
detail: Option<String>,
fix: String,
soft: bool,
}
impl Probe {
fn new(name: &'static str, detail: Option<String>, fix: impl Into<String>) -> Self {
Probe {
name,
detail,
fix: fix.into(),
soft: false,
}
}
fn soft(mut self) -> Self {
self.soft = true;
self
}
}
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 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`",
)],
setup: "macOS desktop (AppKit) builds as a plain cargo binary and needs Apple's clang\n\
toolchain: `xcode-select --install` (or a full Xcode). No extra Rust target — the\n\
host toolchain builds it.",
}
}
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)",
)
.soft(),
],
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`)",
)
.soft(),
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`)",
)
.soft(),
],
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\
`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` · aqtinstall on Windows)",
),
Probe::new(
"rcc",
find_rcc().map(|p| p.display().to_string()),
"install Qt 6 (rcc, the resource compiler, ships in Qt's libexec)",
)
.soft(),
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`)",
)
.soft(),
],
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— install Qt (aqtinstall or the online installer) and put its bin/ on PATH\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)",
)
.soft(),
Probe::new(
"makensis",
day_toolchain::makensis().map(|p| p.display().to_string()),
"choco install nsis (for `day pack` setup.exe)",
)
.soft(),
],
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\
• the Windows App SDK runtime (for XAML Islands at launch)\n\
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",
)
.soft(),
],
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",
)
.soft(),
],
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",
)],
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 only toolchain requirement is the Rust\n\
target: `rustup target add wasm32-unknown-unknown`. `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(),
]
}
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]) -> i32 {
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()) {
eprintln!(
"error: unknown toolkit {f:?} — choose from {}",
known
.iter()
.filter(|k| **k != "core")
.cloned()
.collect::<Vec<_>>()
.join(", ")
);
return 2;
}
}
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
);
3
} else if total.warnings > 0 {
eprintln!(
"{WARN}⚠ {} warning(s){WARN:#} — optional toolkits not fully set up. Fine unless you build them.",
total.warnings
);
0
} else {
eprintln!("{SUCCESS_BOLD}✓ all good{SUCCESS_BOLD:#}");
0
}
}