use std::collections::BTreeSet;
use serde::Serialize;
use crate::debug::{DoctorCheck, DoctorStatus, DoctorSummary};
use crate::model::BoardModel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BuildOs {
Zephyr,
Yocto,
Baremetal,
}
impl BuildOs {
fn parse(raw: &str) -> Option<BuildOs> {
match raw {
"zephyr" => Some(BuildOs::Zephyr),
"yocto" => Some(BuildOs::Yocto),
"baremetal" => Some(BuildOs::Baremetal),
_ => None,
}
}
}
pub fn board_os_set(board: &BoardModel) -> Vec<BuildOs> {
let mut set: BTreeSet<BuildOs> = BTreeSet::new();
if let Some(os) = board.os.as_deref().and_then(BuildOs::parse) {
set.insert(os);
}
if let Some(cores) = &board.cores {
for core in cores.values() {
if let Some(os) = core.os.as_deref().and_then(BuildOs::parse) {
set.insert(os);
}
}
}
if set.is_empty() {
return vec![BuildOs::Zephyr, BuildOs::Yocto, BuildOs::Baremetal];
}
set.into_iter().collect()
}
#[derive(Debug, Clone, Copy)]
pub struct BuildToolProbe {
pub west: bool,
pub cmake: bool,
pub ninja: bool,
pub bitbake: bool,
pub zephyr_sdk: bool,
pub bmaptool: bool,
pub dd: bool,
pub is_linux: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct BuildReadinessReport {
#[serde(rename = "schemaVersion")]
pub schema_version: String,
#[serde(rename = "generatedAt")]
pub generated_at: String,
#[serde(rename = "osSet")]
pub os_set: Vec<BuildOs>,
pub summary: DoctorSummary,
pub checks: Vec<DoctorCheck>,
#[serde(rename = "nextSteps")]
pub next_steps: Vec<String>,
}
pub fn build_readiness_report(
generated_at: String,
os_set: Vec<BuildOs>,
probe: &BuildToolProbe,
) -> BuildReadinessReport {
let mut checks: Vec<DoctorCheck> = Vec::new();
let mut seen: BTreeSet<&'static str> = BTreeSet::new();
if os_set.contains(&BuildOs::Zephyr) {
push_tool(
&mut checks,
&mut seen,
"west",
probe.west,
"Zephyr",
"Install west via `alp bootstrap`.",
);
push_tool(
&mut checks,
&mut seen,
"cmake",
probe.cmake,
"Zephyr/baremetal",
"Install CMake (>=3.20).",
);
push_tool(
&mut checks,
&mut seen,
"ninja",
probe.ninja,
"Zephyr",
"Install Ninja.",
);
checks.push(DoctorCheck {
name: "zephyrSdk".to_string(),
status: if probe.zephyr_sdk {
DoctorStatus::Pass
} else {
DoctorStatus::Warn
},
detail: if probe.zephyr_sdk {
"Zephyr SDK toolchain detected.".to_string()
} else {
"Zephyr SDK toolchain not detected (ZEPHYR_SDK_INSTALL_DIR unset).".to_string()
},
fix: if probe.zephyr_sdk {
None
} else {
Some(
"Install the Zephyr SDK: https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html"
.to_string(),
)
},
});
}
if os_set.contains(&BuildOs::Yocto) {
if probe.is_linux {
push_tool(
&mut checks,
&mut seen,
"bitbake",
probe.bitbake,
"Yocto",
"Install the Yocto host packages (see docs/getting-started.md).",
);
let (status, detail, fix) = if probe.bmaptool {
(
DoctorStatus::Pass,
"bmaptool is available — fast sparse Yocto .wic flashing.".to_string(),
None,
)
} else if probe.dd {
(
DoctorStatus::Warn,
"bmaptool not found; Yocto .wic flash falls back to dd (slower)."
.to_string(),
Some(
"Install bmaptool for sparse .wic flashing (e.g. `apt install bmap-tools`)."
.to_string(),
),
)
} else {
(
DoctorStatus::Warn,
"neither bmaptool nor dd on PATH — Yocto .wic flash (`alp flash`) will fail."
.to_string(),
Some(
"Install bmaptool (`apt install bmap-tools`) or dd (coreutils)."
.to_string(),
),
)
};
checks.push(DoctorCheck {
name: "bmaptool".to_string(),
status,
detail,
fix,
});
} else {
checks.push(DoctorCheck {
name: "yoctoHost".to_string(),
status: DoctorStatus::Warn,
detail: "Yocto builds are Linux-only; use WSL2 or a Linux host/container."
.to_string(),
fix: Some("Run Yocto builds on Linux (WSL2 / Docker).".to_string()),
});
}
}
if os_set.contains(&BuildOs::Baremetal) {
push_tool(
&mut checks,
&mut seen,
"cmake",
probe.cmake,
"baremetal",
"Install CMake (>=3.20).",
);
checks.push(DoctorCheck {
name: "vendorToolchain".to_string(),
status: DoctorStatus::Warn,
detail: "Baremetal needs a vendor toolchain (Alif/Renesas/NXP), per SoC family."
.to_string(),
fix: Some(
"Install the vendor toolchain for your SoC (see docs/getting-started.md §8)."
.to_string(),
),
});
}
let summary = DoctorSummary {
pass: count(&checks, DoctorStatus::Pass),
warn: count(&checks, DoctorStatus::Warn),
fail: count(&checks, DoctorStatus::Fail),
};
let next_steps = unique_next_steps(&checks);
BuildReadinessReport {
schema_version: "1".to_string(),
generated_at,
os_set,
summary,
checks,
next_steps,
}
}
fn push_tool(
checks: &mut Vec<DoctorCheck>,
seen: &mut BTreeSet<&'static str>,
name: &'static str,
present: bool,
need: &str,
fix: &str,
) {
if !seen.insert(name) {
return;
}
checks.push(DoctorCheck {
name: name.to_string(),
status: if present {
DoctorStatus::Pass
} else {
DoctorStatus::Warn
},
detail: if present {
format!("{name} is available.")
} else {
format!("{name} not found on PATH — needed for {need} builds.")
},
fix: if present { None } else { Some(fix.to_string()) },
});
}
fn count(checks: &[DoctorCheck], status: DoctorStatus) -> u32 {
checks.iter().filter(|c| c.status == status).count() as u32
}
fn unique_next_steps(checks: &[DoctorCheck]) -> Vec<String> {
let mut steps = Vec::new();
for check in checks {
if check.status == DoctorStatus::Pass {
continue;
}
if let Some(fix) = &check.fix {
if !steps.contains(fix) {
steps.push(fix.clone());
}
}
}
steps
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::parse_board_model;
fn probe_all_present() -> BuildToolProbe {
BuildToolProbe {
west: true,
cmake: true,
ninja: true,
bitbake: true,
zephyr_sdk: true,
bmaptool: true,
dd: true,
is_linux: true,
}
}
#[test]
fn os_set_from_explicit_core_os() {
let board = parse_board_model(
"schema_version: 2\ncores:\n m55_hp:\n os: zephyr\n app: ./src\n a32:\n os: yocto\n app: ./a\n",
)
.unwrap();
assert_eq!(board_os_set(&board), vec![BuildOs::Zephyr, BuildOs::Yocto]);
}
#[test]
fn os_set_falls_back_to_all_when_undeclared() {
let board =
parse_board_model("schema_version: 2\ncores:\n m55_hp:\n app: ./src\n").unwrap();
assert_eq!(
board_os_set(&board),
vec![BuildOs::Zephyr, BuildOs::Yocto, BuildOs::Baremetal]
);
}
#[test]
fn zephyr_checks_present_pass_clean() {
let report =
build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe_all_present());
assert_eq!(report.summary.fail, 0);
assert!(report.summary.warn == 0);
assert!(
report
.checks
.iter()
.any(|c| c.name == "west" && c.status == DoctorStatus::Pass)
);
assert!(report.checks.iter().any(|c| c.name == "zephyrSdk"));
}
#[test]
fn missing_tools_warn_with_next_steps() {
let probe = BuildToolProbe {
west: false,
cmake: false,
ninja: false,
bitbake: false,
zephyr_sdk: false,
bmaptool: false,
dd: false,
is_linux: true,
};
let report = build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe);
assert!(report.summary.warn >= 4); assert_eq!(report.summary.fail, 0);
assert!(!report.next_steps.is_empty());
}
#[test]
fn cmake_not_duplicated_across_zephyr_and_baremetal() {
let report = build_readiness_report(
"t".to_string(),
vec![BuildOs::Zephyr, BuildOs::Baremetal],
&probe_all_present(),
);
assert_eq!(
report.checks.iter().filter(|c| c.name == "cmake").count(),
1
);
}
#[test]
fn yocto_on_non_linux_warns() {
let probe = BuildToolProbe {
is_linux: false,
..probe_all_present()
};
let report = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe);
assert!(report.checks.iter().any(|c| c.name == "yoctoHost"));
}
#[test]
fn yocto_flash_checks_bmaptool() {
let pass = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe_all_present());
assert!(
pass.checks
.iter()
.any(|c| c.name == "bmaptool" && c.status == DoctorStatus::Pass)
);
let probe = BuildToolProbe {
bmaptool: false,
dd: false,
..probe_all_present()
};
let warn = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe);
assert!(
warn.checks
.iter()
.any(|c| c.name == "bmaptool" && c.status == DoctorStatus::Warn)
);
let zephyr_only =
build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe_all_present());
assert!(!zephyr_only.checks.iter().any(|c| c.name == "bmaptool"));
}
}