Convert one backend's ``build_info_ .json`` into NormalizedFlags. Both backends key the per-board record by ``board`` (e.g. ``"teensy41"``). The schemas differ slightly: - fbuild emits ``cc_flags`` that already contains ``-D``/``-I`` tokens alongside the real cc flags, pl
(raw: dict, board: str, backend: str)
| 275 | |
| 276 | |
| 277 | def normalize_build_info(raw: dict, board: str, backend: str) -> NormalizedFlags: |
| 278 | """Convert one backend's ``build_info_<example>.json`` into NormalizedFlags. |
| 279 | |
| 280 | Both backends key the per-board record by ``board`` (e.g. |
| 281 | ``"teensy41"``). The schemas differ slightly: |
| 282 | |
| 283 | - fbuild emits ``cc_flags`` that already contains ``-D``/``-I`` tokens |
| 284 | alongside the real cc flags, plus separate ``defines`` and ``includes`` |
| 285 | mirrors. We prefer the separate mirrors when present. |
| 286 | - PIO emits ``cc_flags`` / ``cxx_flags`` as pure compiler flags and keeps |
| 287 | ``defines`` (no ``-D``) and ``includes.build`` / ``includes.compatlib`` |
| 288 | as the source of truth. |
| 289 | """ |
| 290 | # Fail fast if the requested board key is missing. A silent fallback to |
| 291 | # "the only top-level entry" hides real schema mismatches and can flip a |
| 292 | # genuine drift into a false clean (or vice versa) by normalizing the |
| 293 | # wrong snapshot. |
| 294 | if board not in raw: |
| 295 | raise ValueError( |
| 296 | f"{backend} build_info is missing board key {board!r}; " |
| 297 | f"found top-level keys: {sorted(raw)}" |
| 298 | ) |
| 299 | board_record = raw[board] |
| 300 | |
| 301 | raw_cc = list(board_record.get("cc_flags", [])) |
| 302 | raw_cxx = list(board_record.get("cxx_flags", [])) |
| 303 | raw_defines = list(board_record.get("defines", [])) |
| 304 | raw_includes_field = board_record.get("includes", []) |
| 305 | |
| 306 | # Split fbuild's combined cc_flags back into clean buckets. |
| 307 | if backend == "fbuild": |
| 308 | split = _split_fbuild_combined_flags(raw_cc) |
| 309 | raw_cc = split.pure |
| 310 | if not raw_defines: |
| 311 | raw_defines = split.defines |
| 312 | if not raw_includes_field: |
| 313 | raw_includes_field = split.includes |
| 314 | # fbuild often duplicates the same cc set into cxx_flags or doesn't |
| 315 | # populate cxx_flags separately. If empty, fall back to cc. |
| 316 | if raw_cxx: |
| 317 | raw_cxx = _split_fbuild_combined_flags(raw_cxx).pure |
| 318 | else: |
| 319 | raw_cxx = list(raw_cc) |
| 320 | |
| 321 | # PIO's includes is a dict-of-lists keyed by scope (build/compatlib/...). |
| 322 | # Flatten to a single list for the comparison. |
| 323 | if isinstance(raw_includes_field, dict): |
| 324 | flat_includes: list[str] = [] |
| 325 | for scope in ("build", "compatlib", "toolchain"): |
| 326 | flat_includes.extend(raw_includes_field.get(scope, [])) |
| 327 | else: |
| 328 | flat_includes = list(raw_includes_field) |
| 329 | |
| 330 | cc_clean = _clean_flags(raw_cc) |
| 331 | cxx_clean = _clean_flags(raw_cxx) |
| 332 | defines_clean = { |
| 333 | norm for d in raw_defines if (norm := _normalize_define(d)) is not None |
| 334 | } |
no test coverage detected