1use std::collections::BTreeSet;
14
15use serde::Serialize;
16
17use crate::debug::{DoctorCheck, DoctorStatus, DoctorSummary};
18use crate::model::BoardModel;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
22#[serde(rename_all = "lowercase")]
23pub enum BuildOs {
24 Zephyr,
26 Yocto,
28 Baremetal,
30}
31
32impl BuildOs {
33 fn parse(raw: &str) -> Option<BuildOs> {
34 match raw {
35 "zephyr" => Some(BuildOs::Zephyr),
36 "yocto" => Some(BuildOs::Yocto),
37 "baremetal" => Some(BuildOs::Baremetal),
38 _ => None,
39 }
40 }
41}
42
43pub fn board_os_set(board: &BoardModel) -> Vec<BuildOs> {
47 let mut set: BTreeSet<BuildOs> = BTreeSet::new();
48 if let Some(os) = board.os.as_deref().and_then(BuildOs::parse) {
49 set.insert(os);
50 }
51 if let Some(cores) = &board.cores {
52 for core in cores.values() {
53 if let Some(os) = core.os.as_deref().and_then(BuildOs::parse) {
54 set.insert(os);
55 }
56 }
57 }
58 if set.is_empty() {
59 return vec![BuildOs::Zephyr, BuildOs::Yocto, BuildOs::Baremetal];
60 }
61 set.into_iter().collect()
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct BuildToolProbe {
67 pub west: bool,
69 pub cmake: bool,
71 pub ninja: bool,
73 pub bitbake: bool,
75 pub zephyr_sdk: bool,
77 pub bmaptool: bool,
79 pub dd: bool,
81 pub is_linux: bool,
83}
84
85#[derive(Debug, Clone, Serialize)]
88pub struct BuildReadinessReport {
89 #[serde(rename = "schemaVersion")]
91 pub schema_version: String,
92 #[serde(rename = "generatedAt")]
94 pub generated_at: String,
95 #[serde(rename = "osSet")]
97 pub os_set: Vec<BuildOs>,
98 pub summary: DoctorSummary,
100 pub checks: Vec<DoctorCheck>,
102 #[serde(rename = "nextSteps")]
104 pub next_steps: Vec<String>,
105}
106
107pub fn build_readiness_report(
109 generated_at: String,
110 os_set: Vec<BuildOs>,
111 probe: &BuildToolProbe,
112) -> BuildReadinessReport {
113 let mut checks: Vec<DoctorCheck> = Vec::new();
114 let mut seen: BTreeSet<&'static str> = BTreeSet::new();
115
116 if os_set.contains(&BuildOs::Zephyr) {
117 push_tool(
118 &mut checks,
119 &mut seen,
120 "west",
121 probe.west,
122 "Zephyr",
123 "Install west via `alp bootstrap`.",
124 );
125 push_tool(
126 &mut checks,
127 &mut seen,
128 "cmake",
129 probe.cmake,
130 "Zephyr/baremetal",
131 "Install CMake (>=3.20).",
132 );
133 push_tool(
134 &mut checks,
135 &mut seen,
136 "ninja",
137 probe.ninja,
138 "Zephyr",
139 "Install Ninja.",
140 );
141 checks.push(DoctorCheck {
143 name: "zephyrSdk".to_string(),
144 status: if probe.zephyr_sdk {
145 DoctorStatus::Pass
146 } else {
147 DoctorStatus::Warn
148 },
149 detail: if probe.zephyr_sdk {
150 "Zephyr SDK toolchain detected.".to_string()
151 } else {
152 "Zephyr SDK toolchain not detected (ZEPHYR_SDK_INSTALL_DIR unset).".to_string()
153 },
154 fix: if probe.zephyr_sdk {
155 None
156 } else {
157 Some(
158 "Install the Zephyr SDK: https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html"
159 .to_string(),
160 )
161 },
162 });
163 }
164
165 if os_set.contains(&BuildOs::Yocto) {
166 if probe.is_linux {
167 push_tool(
168 &mut checks,
169 &mut seen,
170 "bitbake",
171 probe.bitbake,
172 "Yocto",
173 "Install the Yocto host packages (see docs/getting-started.md).",
174 );
175 let (status, detail, fix) = if probe.bmaptool {
179 (
180 DoctorStatus::Pass,
181 "bmaptool is available — fast sparse Yocto .wic flashing.".to_string(),
182 None,
183 )
184 } else if probe.dd {
185 (
186 DoctorStatus::Warn,
187 "bmaptool not found; Yocto .wic flash falls back to dd (slower)."
188 .to_string(),
189 Some(
190 "Install bmaptool for sparse .wic flashing (e.g. `apt install bmap-tools`)."
191 .to_string(),
192 ),
193 )
194 } else {
195 (
196 DoctorStatus::Warn,
197 "neither bmaptool nor dd on PATH — Yocto .wic flash (`alp flash`) will fail."
198 .to_string(),
199 Some(
200 "Install bmaptool (`apt install bmap-tools`) or dd (coreutils)."
201 .to_string(),
202 ),
203 )
204 };
205 checks.push(DoctorCheck {
206 name: "bmaptool".to_string(),
207 status,
208 detail,
209 fix,
210 });
211 } else {
212 checks.push(DoctorCheck {
213 name: "yoctoHost".to_string(),
214 status: DoctorStatus::Warn,
215 detail: "Yocto builds are Linux-only; use WSL2 or a Linux host/container."
216 .to_string(),
217 fix: Some("Run Yocto builds on Linux (WSL2 / Docker).".to_string()),
218 });
219 }
220 }
221
222 if os_set.contains(&BuildOs::Baremetal) {
223 push_tool(
224 &mut checks,
225 &mut seen,
226 "cmake",
227 probe.cmake,
228 "baremetal",
229 "Install CMake (>=3.20).",
230 );
231 checks.push(DoctorCheck {
232 name: "vendorToolchain".to_string(),
233 status: DoctorStatus::Warn,
234 detail: "Baremetal needs a vendor toolchain (Alif/Renesas/NXP), per SoC family."
235 .to_string(),
236 fix: Some(
237 "Install the vendor toolchain for your SoC (see docs/getting-started.md §8)."
238 .to_string(),
239 ),
240 });
241 }
242
243 let summary = DoctorSummary {
244 pass: count(&checks, DoctorStatus::Pass),
245 warn: count(&checks, DoctorStatus::Warn),
246 fail: count(&checks, DoctorStatus::Fail),
247 };
248 let next_steps = unique_next_steps(&checks);
249
250 BuildReadinessReport {
251 schema_version: "1".to_string(),
252 generated_at,
253 os_set,
254 summary,
255 checks,
256 next_steps,
257 }
258}
259
260fn push_tool(
263 checks: &mut Vec<DoctorCheck>,
264 seen: &mut BTreeSet<&'static str>,
265 name: &'static str,
266 present: bool,
267 need: &str,
268 fix: &str,
269) {
270 if !seen.insert(name) {
271 return;
272 }
273 checks.push(DoctorCheck {
274 name: name.to_string(),
275 status: if present {
276 DoctorStatus::Pass
277 } else {
278 DoctorStatus::Warn
279 },
280 detail: if present {
281 format!("{name} is available.")
282 } else {
283 format!("{name} not found on PATH — needed for {need} builds.")
284 },
285 fix: if present { None } else { Some(fix.to_string()) },
286 });
287}
288
289fn count(checks: &[DoctorCheck], status: DoctorStatus) -> u32 {
290 checks.iter().filter(|c| c.status == status).count() as u32
291}
292
293fn unique_next_steps(checks: &[DoctorCheck]) -> Vec<String> {
294 let mut steps = Vec::new();
295 for check in checks {
296 if check.status == DoctorStatus::Pass {
297 continue;
298 }
299 if let Some(fix) = &check.fix {
300 if !steps.contains(fix) {
301 steps.push(fix.clone());
302 }
303 }
304 }
305 steps
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::validate::parse_board_model;
312
313 fn probe_all_present() -> BuildToolProbe {
314 BuildToolProbe {
315 west: true,
316 cmake: true,
317 ninja: true,
318 bitbake: true,
319 zephyr_sdk: true,
320 bmaptool: true,
321 dd: true,
322 is_linux: true,
323 }
324 }
325
326 #[test]
327 fn os_set_from_explicit_core_os() {
328 let board = parse_board_model(
329 "schema_version: 2\ncores:\n m55_hp:\n os: zephyr\n app: ./src\n a32:\n os: yocto\n app: ./a\n",
330 )
331 .unwrap();
332 assert_eq!(board_os_set(&board), vec![BuildOs::Zephyr, BuildOs::Yocto]);
333 }
334
335 #[test]
336 fn os_set_falls_back_to_all_when_undeclared() {
337 let board =
338 parse_board_model("schema_version: 2\ncores:\n m55_hp:\n app: ./src\n").unwrap();
339 assert_eq!(
340 board_os_set(&board),
341 vec![BuildOs::Zephyr, BuildOs::Yocto, BuildOs::Baremetal]
342 );
343 }
344
345 #[test]
346 fn zephyr_checks_present_pass_clean() {
347 let report =
348 build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe_all_present());
349 assert_eq!(report.summary.fail, 0);
350 assert!(report.summary.warn == 0);
351 assert!(
352 report
353 .checks
354 .iter()
355 .any(|c| c.name == "west" && c.status == DoctorStatus::Pass)
356 );
357 assert!(report.checks.iter().any(|c| c.name == "zephyrSdk"));
358 }
359
360 #[test]
361 fn missing_tools_warn_with_next_steps() {
362 let probe = BuildToolProbe {
363 west: false,
364 cmake: false,
365 ninja: false,
366 bitbake: false,
367 zephyr_sdk: false,
368 bmaptool: false,
369 dd: false,
370 is_linux: true,
371 };
372 let report = build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe);
373 assert!(report.summary.warn >= 4); assert_eq!(report.summary.fail, 0);
375 assert!(!report.next_steps.is_empty());
376 }
377
378 #[test]
379 fn cmake_not_duplicated_across_zephyr_and_baremetal() {
380 let report = build_readiness_report(
381 "t".to_string(),
382 vec![BuildOs::Zephyr, BuildOs::Baremetal],
383 &probe_all_present(),
384 );
385 assert_eq!(
386 report.checks.iter().filter(|c| c.name == "cmake").count(),
387 1
388 );
389 }
390
391 #[test]
392 fn yocto_on_non_linux_warns() {
393 let probe = BuildToolProbe {
394 is_linux: false,
395 ..probe_all_present()
396 };
397 let report = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe);
398 assert!(report.checks.iter().any(|c| c.name == "yoctoHost"));
399 }
400
401 #[test]
402 fn yocto_flash_checks_bmaptool() {
403 let pass = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe_all_present());
405 assert!(
406 pass.checks
407 .iter()
408 .any(|c| c.name == "bmaptool" && c.status == DoctorStatus::Pass)
409 );
410
411 let probe = BuildToolProbe {
414 bmaptool: false,
415 dd: false,
416 ..probe_all_present()
417 };
418 let warn = build_readiness_report("t".to_string(), vec![BuildOs::Yocto], &probe);
419 assert!(
420 warn.checks
421 .iter()
422 .any(|c| c.name == "bmaptool" && c.status == DoctorStatus::Warn)
423 );
424 let zephyr_only =
425 build_readiness_report("t".to_string(), vec![BuildOs::Zephyr], &probe_all_present());
426 assert!(!zephyr_only.checks.iter().any(|c| c.name == "bmaptool"));
427 }
428}