tan-cli 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,395 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The hw_rev <-> SDK-version compatibility gate `metadata/sdk_version.yaml`
3
+ advertises.
4
+
5
+ That file has stated since it was written that `scripts/alp_project.py`
6
+ "refuses to emit when the requested hw_rev is outside
7
+ [min_sdk_version, max_sdk_version]", and that
8
+ `scripts/validate_board_yaml.py` runs "the same check, exit code 3 on
9
+ mismatch". Neither existed: a grep for `min_sdk_version` across
10
+ `scripts/` returned nothing, so a customer whose upgraded SDK no longer
11
+ supported their board revision got a normal, successful emit and firmware
12
+ built against the wrong hardware assumptions (alp-sdk issue #1019).
13
+
14
+ This module is that check, relocated verbatim from alp-sdk's
15
+ `scripts/alp_orchestrate/sdk_compat.py`. It is deliberately pure and
16
+ data-only so the comparison can be tested without a board.yaml, a
17
+ metadata tree, or a loader.
18
+
19
+ Also here (alp-sdk issue #1025's SAFE half only): `revision_known()`, the
20
+ existence predicate -- given a resolved `hw_revisions:` table, is the
21
+ requested hw_rev even a declared key. A revision absent from the table used
22
+ to silently resolve to base-revision routing with a clean exit code; that is
23
+ a wrong-hardware emit, not a version mismatch, so it is a distinct check
24
+ with a distinct error (`SdkRevisionUnknown`, not `SdkRevisionUnsupported`)
25
+ run BEFORE the range comparison below -- an unknown revision has no bounds
26
+ to compare against.
27
+
28
+ Also here: `revision_buildable()`, the maintainer's broad-reading decision
29
+ on alp-sdk #1025's other half -- a revision that EXISTS but is
30
+ `status: reserved`, `status: tbd`, or carries no `status` key at all is
31
+ refused, distinctly from not existing at all (`SdkRevisionNotBuildable`,
32
+ not `SdkRevisionUnknown`). `revision_known()` itself stays deliberately
33
+ blind to `status:` -- existence and buildability are two different
34
+ questions, answered by two different predicates, so a later status change
35
+ can't slip into the wrong one silently.
36
+
37
+ All three of those gates read ONE file per SoM family,
38
+ `metadata/e1m_modules/<family>/hw-revisions.yaml`, through
39
+ `load_family_table()` below. That reader used to swallow `OSError` and
40
+ `yaml.YAMLError` and answer `{}`, which every predicate here reads as
41
+ "nothing to judge" -- so a single tab-indented line in that file disabled
42
+ the unknown-revision gate, the not-buildable gate AND the SDK-range gate
43
+ at once and emitted a wrong-hardware artefact at exit 0. It now refuses
44
+ instead; see `load_family_table`'s own docstring for why an ABSENT table
45
+ is still benign while a present-but-unusable one is not.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import re
51
+ from pathlib import Path
52
+ from typing import Any, Optional
53
+
54
+ import yaml
55
+
56
+ from .models import OrchestratorError
57
+ from .strict_loaders import fast_safe_load
58
+
59
+ # A revision may bound one side, both, or neither. `max_sdk_version: ~`
60
+ # (YAML null) is the common case in tree today and means "no upper bound
61
+ # declared yet" -- every one of the 27 ranges currently shipped is
62
+ # open-ended on the high side. An ABSENT bound is unbounded, never zero:
63
+ # reading it as a bound would refuse every `status: reserved` revision,
64
+ # which declares no range at all.
65
+ _VERSION_RE = re.compile(r"^\s*v?(\d+)\.(\d+)\.(\d+)")
66
+
67
+
68
+ def parse_version(value: Optional[str]) -> Optional[tuple[int, int, int]]:
69
+ """Parse `X.Y.Z` (or `vX.Y.Z`) into a comparable tuple.
70
+
71
+ Returns None for anything unparseable, including None itself. A
72
+ malformed bound is treated as absent rather than as a failure: this
73
+ gate exists to catch a real hw_rev/SDK mismatch, and turning a typo in
74
+ metadata into a refused build would be a worse failure than the one it
75
+ prevents. `check_metadata` in the validator is where malformed
76
+ metadata belongs.
77
+ """
78
+ if not isinstance(value, str):
79
+ return None
80
+ m = _VERSION_RE.match(value)
81
+ if m is None:
82
+ return None
83
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
84
+
85
+
86
+ def incompatibility(
87
+ sdk_version: Optional[str],
88
+ minimum: Optional[str],
89
+ maximum: Optional[str],
90
+ ) -> Optional[str]:
91
+ """Return a human reason the SDK is outside the range, or None if it fits.
92
+
93
+ Both bounds are INCLUSIVE: a revision declaring `min_sdk_version: 0.3.0`
94
+ is supported *by* 0.3.0, not merely after it.
95
+ """
96
+ running = parse_version(sdk_version)
97
+ if running is None:
98
+ # No readable version (a wheel with no adjacent metadata/ tree, the
99
+ # same case `buildplan._sdk_version` guards). Provenance is
100
+ # best-effort there and a missing version is not evidence of a
101
+ # mismatch, so this stays quiet rather than refusing.
102
+ return None
103
+
104
+ lo = parse_version(minimum)
105
+ hi = parse_version(maximum)
106
+
107
+ if lo is not None and running < lo:
108
+ return (f"needs SDK >= {minimum}, but this SDK is "
109
+ f"{sdk_version}")
110
+ if hi is not None and running > hi:
111
+ return (f"is supported up to SDK {maximum}, but this SDK is "
112
+ f"{sdk_version}")
113
+ return None
114
+
115
+
116
+ def read_sdk_version(metadata_root: Path) -> Optional[str]:
117
+ """The `version:` field out of `metadata/sdk_version.yaml`.
118
+
119
+ Same read-and-strip idiom as `buildplan._sdk_version`, taking the
120
+ metadata root as an argument so a test (and `--metadata-root`) can
121
+ point it somewhere else.
122
+ """
123
+ try:
124
+ text = (metadata_root / "sdk_version.yaml").read_text(encoding="utf-8")
125
+ except OSError:
126
+ return None
127
+ for line in text.splitlines():
128
+ stripped = line.strip()
129
+ if stripped.startswith("version:"):
130
+ return stripped.split(":", 1)[1].split("#", 1)[0].strip()
131
+ return None
132
+
133
+
134
+ def _revision(table: Any, hw_rev: Optional[str]) -> dict[str, Any]:
135
+ """The entry for `hw_rev` out of a `hw_revisions:` mapping, or {}."""
136
+ if not hw_rev or not isinstance(table, dict):
137
+ return {}
138
+ entry = (table.get("hw_revisions") or {}).get(hw_rev)
139
+ return entry if isinstance(entry, dict) else {}
140
+
141
+
142
+ def _hw_revisions(table: Any) -> Optional[dict[str, Any]]:
143
+ """The raw `hw_revisions:` mapping out of `table`, or None if absent.
144
+
145
+ None -- not {} -- distinguishes "this table declares no `hw_revisions:`
146
+ section at all" (an inline board has no per-board table; there is
147
+ nothing to assert existence against) from "a table exists and the key
148
+ simply isn't in it" (a genuine unknown-revision refusal).
149
+ """
150
+ if not isinstance(table, dict):
151
+ return None
152
+ revisions = table.get("hw_revisions")
153
+ return revisions if isinstance(revisions, dict) else None
154
+
155
+
156
+ def revision_known(table: Any, hw_rev: Optional[str]) -> Optional[bool]:
157
+ """Whether `hw_rev` is a declared key in `table`'s `hw_revisions:` map.
158
+
159
+ Pure existence check (alp-sdk #1025's safe half) -- deliberately blind
160
+ to `status:`. A `status: reserved` revision, or one carrying no
161
+ `status` key at all, is still "known" here; whether it is buildable is
162
+ a different question this predicate does not answer.
163
+
164
+ Returns None -- not False -- when `table` has no `hw_revisions:`
165
+ mapping at all, so a caller can tell "nothing to check existence
166
+ against" (e.g. an inline board with no per-board table -- skip) apart
167
+ from "checked, and it's missing" (True/False -- refuse on False).
168
+ """
169
+ revisions = _hw_revisions(table)
170
+ if revisions is None:
171
+ return None
172
+ return bool(hw_rev) and hw_rev in revisions
173
+
174
+
175
+ # Statuses a build is permitted to proceed against. Everything else --
176
+ # `reserved`, `tbd`, and (the broad reading) a revision that carries no
177
+ # `status` key at all -- is refused. alp-sdk's schema requires `status` on
178
+ # every entry (issue #1025), so a missing key means a table hand-edited or
179
+ # written before that requirement, not a deliberate declaration.
180
+ _NOT_BUILDABLE_STATUSES = frozenset({"reserved", "tbd"})
181
+
182
+
183
+ def revision_buildable(table: Any, hw_rev: Optional[str]) -> Optional[bool]:
184
+ """Whether `hw_rev`'s declared `status:` permits a build.
185
+
186
+ Tri-state like `revision_known()`, and answers a DIFFERENT question:
187
+ None when there is nothing to judge -- no `hw_revisions:` table at all,
188
+ or `hw_rev` isn't a key in it (that is `revision_known()`'s failure to
189
+ report, not this one's -- an unknown revision has no status to have
190
+ read). Otherwise False for `status: reserved`, `status: tbd`, or an
191
+ entry with no `status` key; True for every other declared status.
192
+ """
193
+ revisions = _hw_revisions(table)
194
+ if revisions is None:
195
+ return None
196
+ entry = revisions.get(hw_rev) if hw_rev else None
197
+ if not isinstance(entry, dict):
198
+ # A key present but not a dict (None, a bare string, ...) is a
199
+ # malformed entry, not an absent one -- it has no status, so it
200
+ # is not buildable. Only an absent key stays None (unknown).
201
+ return False if hw_rev in revisions else None
202
+ status = entry.get("status")
203
+ if not isinstance(status, str) or status in _NOT_BUILDABLE_STATUSES:
204
+ return False
205
+ return True
206
+
207
+
208
+ def _table_unreadable(path: Path, why: str) -> OrchestratorError:
209
+ """The refusal every present-but-unusable family table raises.
210
+
211
+ One message shape so the three gates that read the table cannot
212
+ disagree about what an unreadable one means.
213
+ """
214
+ return OrchestratorError(
215
+ f"{path}: the SoM-family hardware-revision table is present but "
216
+ f"unusable ({why}). This table is the ONLY thing the hw_rev "
217
+ "existence, buildable-status and SDK-version-range gates check "
218
+ "against, so an unusable one is refused rather than read as "
219
+ "\"this family declares no revisions\" -- treating it as empty "
220
+ "would let every one of those gates pass and emit firmware for "
221
+ "an unverified hardware revision. Restore the file (a partial "
222
+ "checkout, a truncated write or a hand-edit that broke the YAML "
223
+ "are the usual causes) and re-run.")
224
+
225
+
226
+ def load_family_table(metadata_root: Path, family_dir: str) -> Any:
227
+ """The raw parsed `hw-revisions.yaml` for a SoM family, or {} when the
228
+ family ships no table at all.
229
+
230
+ Public, not `_`-private, because it has a SECOND consumer outside this
231
+ module: `alp_project_loader._hwrev_pad_route_overrides()` carries its
232
+ own copies of the #1025 existence and buildable gates for the
233
+ `--emit composed-route-table` / `--emit carrier-netlist` path, which
234
+ resolves its SoM data independently of `alp_orchestrate.loader`. That
235
+ reader had its own tolerant `yaml.safe_load(...) or {}` and fell open
236
+ on exactly the shapes guarded below; both readers now share this one,
237
+ so they cannot disagree about whether a damaged table is fatal or
238
+ about what the refusal says.
239
+
240
+ ABSENT and UNUSABLE are two different situations and only one of them
241
+ is benign (#563). A family directory with no `hw-revisions.yaml`
242
+ legitimately has nothing to check against -- the tri-state predicates
243
+ below answer None ("nothing to judge") and the gates skip, which is
244
+ what an in-development family and the test-isolation fixture in
245
+ `tests/scripts/_orchestrate_support.py` both rely on. A table that
246
+ EXISTS but cannot be read or parsed is the opposite: there IS
247
+ something to check against and we failed to read it, so answering
248
+ None would silently disable all three gates on one typo and produce
249
+ a wrong-hardware artefact at exit 0. That is a refusal.
250
+
251
+ Raises `OrchestratorError` (not one of the `SdkRevision*` subclasses):
252
+ those name a defect in the customer's `board.yaml`, and
253
+ `scripts/validate_board_yaml.py` maps each to its own exit code. An
254
+ unreadable table is a defect in the SDK's own metadata tree, so it
255
+ takes the generic refusal + exit 1 instead.
256
+ """
257
+ path = metadata_root / "e1m_modules" / family_dir / "hw-revisions.yaml"
258
+ if not path.is_file():
259
+ return {}
260
+ try:
261
+ text = path.read_text(encoding="utf-8")
262
+ except OSError as exc:
263
+ raise _table_unreadable(path, f"cannot be read: {exc}") from exc
264
+ try:
265
+ # libyaml-backed yaml.safe_load; this runs once per hw-rev check
266
+ # and was the third-hottest YAML read in tests/scripts (#2328).
267
+ table = fast_safe_load(text)
268
+ except yaml.YAMLError as exc:
269
+ detail = str(exc)
270
+ first = detail.splitlines()[0] if detail else type(exc).__name__
271
+ raise _table_unreadable(path, f"is not valid YAML: {first}") from exc
272
+ if not isinstance(table, dict):
273
+ # An empty file parses to None and a stray scalar to a str/int;
274
+ # both are "present but not a table", the same fail-open hazard
275
+ # as a parse error. `metadata/schemas/hw-revisions-v1.schema.json`
276
+ # requires a mapping with `family`, `display_name` and
277
+ # `hw_revisions`, so no legitimate in-tree file lands here.
278
+ kind = "an empty file" if table is None else type(table).__name__
279
+ raise _table_unreadable(
280
+ path, f"does not parse to a mapping ({kind})")
281
+ if not isinstance(table.get("hw_revisions"), dict):
282
+ # A file truncated above its `hw_revisions:` block still parses
283
+ # as valid YAML, so the parse guards above do not catch it --
284
+ # and a family table with no `hw_revisions:` mapping disables
285
+ # the same three gates just as completely. The schema requires
286
+ # the key, so this is only ever a damaged file.
287
+ raise _table_unreadable(
288
+ path, "declares no `hw_revisions:` mapping")
289
+ return table
290
+
291
+
292
+ def family_revision(metadata_root: Path,
293
+ family_dir: Optional[str],
294
+ hw_rev: Optional[str]) -> dict[str, Any]:
295
+ """The SoM-family `hw_revisions:` entry for `hw_rev`, or {}.
296
+
297
+ `family_dir` is the directory name (`aen`, `v2n`, `v2n-m1`),
298
+ not the SoM preset's `family:` string (`alif-ensemble`, ...) -- the two
299
+ differ and only the former names a path.
300
+ """
301
+ if not family_dir:
302
+ return {}
303
+ return _revision(load_family_table(metadata_root, family_dir), hw_rev)
304
+
305
+
306
+ def family_revision_known(metadata_root: Path,
307
+ family_dir: Optional[str],
308
+ hw_rev: Optional[str]) -> Optional[bool]:
309
+ """`revision_known()` against the SoM-family hw-revisions.yaml table."""
310
+ if not family_dir:
311
+ return None
312
+ return revision_known(load_family_table(metadata_root, family_dir), hw_rev)
313
+
314
+
315
+ def family_revision_buildable(metadata_root: Path,
316
+ family_dir: Optional[str],
317
+ hw_rev: Optional[str]) -> Optional[bool]:
318
+ """`revision_buildable()` against the SoM-family hw-revisions.yaml table."""
319
+ if not family_dir:
320
+ return None
321
+ return revision_buildable(load_family_table(metadata_root, family_dir), hw_rev)
322
+
323
+
324
+ def family_available_revisions(metadata_root: Path,
325
+ family_dir: Optional[str]) -> list[str]:
326
+ """Sorted `hw_revisions:` keys for a SoM family, or [] if none/missing.
327
+
328
+ For an error message naming what a customer's SDK actually knows about
329
+ (`family_revision_known` returning False needs somewhere to point).
330
+ """
331
+ if not family_dir:
332
+ return []
333
+ revisions = _hw_revisions(load_family_table(metadata_root, family_dir))
334
+ return sorted(revisions.keys()) if revisions else []
335
+
336
+
337
+ def board_revision(board_preset: Any, hw_rev: Optional[str]) -> dict[str, Any]:
338
+ """The board `hw_revisions:` entry for `hw_rev`, or {}."""
339
+ return _revision(board_preset, hw_rev)
340
+
341
+
342
+ def check(
343
+ sdk_version: Optional[str],
344
+ *,
345
+ som_revision: dict[str, Any],
346
+ som_label: str,
347
+ board_revision_entry: dict[str, Any],
348
+ board_label: str,
349
+ ) -> Optional[str]:
350
+ """Both halves of the claim -- "the family / board hw_revisions tables".
351
+
352
+ Returns a single assembled message naming every side that refuses, or
353
+ None when the SDK sits inside every declared range. The SoM is checked
354
+ first because its range is the one a customer changes by swapping
355
+ modules.
356
+ """
357
+ reasons: list[str] = []
358
+ for entry, label in ((som_revision, som_label),
359
+ (board_revision_entry, board_label)):
360
+ if not entry:
361
+ continue
362
+ why = incompatibility(sdk_version,
363
+ entry.get("min_sdk_version"),
364
+ entry.get("max_sdk_version"))
365
+ if why:
366
+ reasons.append(f"{label} {why}")
367
+ if not reasons:
368
+ return None
369
+ return "; ".join(reasons)
370
+
371
+
372
+ def board_designator(table: Any, hw_rev: Optional[str]) -> Optional[str]:
373
+ """Compose the full board designator for a revision, e.g. `2626-r2`.
374
+
375
+ The physical board is `E1M-AEN-2626-R2`: `2626` is a YYWW datecode carried by
376
+ the Altium board number and declared once, per family, as `board_datecode:`
377
+ in `hw-revisions.yaml`. A module whose identity says only `r2` cannot be tied
378
+ back to its board number, so the identity written into the EEPROM -- and the
379
+ build-side value the boot banner compares it against -- both carry the
380
+ composed form.
381
+
382
+ IMPORTANT: this is NOT a replacement for the bare revision key. `hw_rev`
383
+ stays `r2` everywhere it is used as a LOOKUP KEY (board.yaml, the loader's
384
+ `family_revision_known()` check, `pad_route_overrides`). Composing there
385
+ would break the lookup. Only the identity/compare surfaces use this.
386
+
387
+ Returns the bare `hw_rev` unchanged when the family declares no datecode, so
388
+ families that have not adopted one are untouched.
389
+ """
390
+ if not hw_rev:
391
+ return hw_rev
392
+ if not isinstance(table, dict):
393
+ return hw_rev
394
+ datecode = table.get("board_datecode")
395
+ return f"{datecode}-{hw_rev}" if datecode else hw_rev