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,264 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """SKU / board-preset / pad-route resolution for the `alp_project.py` emit modes.
3
+
4
+ RELOCATED (was alp-sdk `scripts/alp_project_loader.py`). Only the part the six
5
+ relocated `alp_project.py`-owned emit modes actually reach came across --
6
+ `dts-overlay`, `native-sim-overlay`, `hw-info-h`, `west-libraries`,
7
+ `carrier-netlist` and `zephyr-board`. Everything else that file held was already
8
+ here: `_sku_family`, `silicon_to_kconfig`, `resolve_soc_path`,
9
+ `_resolve_silicon_variant`, `resolve_memory_map`, `resolve_capabilities` and
10
+ `som_unpopulated_capabilities` live in `som_metadata.py`, and `_load_yaml` in
11
+ `loader.py`. Copying them again would be a second source of truth for facts the
12
+ planner already reads once.
13
+
14
+ Three mechanical changes, no behavioural one on the success path -- which is the
15
+ path byte-identity is measured on
16
+ (`tests/parity/test_planner_emit_parity.py`):
17
+
18
+ * **`sys.exit` became `raise OrchestratorError`.** alp-sdk's copy ran inside a
19
+ spawned interpreter, where exiting was a non-zero rc the caller reported. In
20
+ `tan`'s own process a `sys.exit` is a `SystemExit` that tears down the CLI past
21
+ every envelope guarantee -- an unwritten stdout is rendered as nothing at all
22
+ by the extension. Same message text, minus the `alp_project: ` prefix the
23
+ spawned CLI added.
24
+ * **`REPO` is the BOUND SDK checkout** (`paths.REPO`), not a `__file__` walk. The
25
+ walk landed on the SDK because the file lived there; inside `tan` it would land
26
+ on the `tan` package and every `metadata/**` path would be wrong.
27
+ * **`_validate_and_load` did NOT come across.** It ran alp-sdk's rich diagnostic
28
+ validator (`alp_cli.validator`) before handing the emitters a plain dict --
29
+ importing `scripts/alp_cli` is exactly the edge this relocation removes. What
30
+ the emitters consume is the plain dict, and alp-sdk's own code already has a
31
+ branch for producing it without `alp_cli` (that function's `ImportError`
32
+ fallback is `_load_yaml`). `load_board_yaml` -- which every caller here runs
33
+ first -- validates the same file against `metadata/schemas/board.schema.json`,
34
+ so nothing goes unvalidated; what is lost is only the rendered-diagnostic
35
+ presentation, which `tan validate` owns on this side.
36
+
37
+ One accidental improvement rides along: alp-sdk's `_validate_and_load` re-read
38
+ the file with a bare `path.read_text()`, i.e. in the host's locale encoding, so
39
+ a non-ASCII board.yaml decoded as cp1252 on Windows. `loader._load_yaml` reads
40
+ UTF-8 explicitly.
41
+
42
+ A fourth change landed later and IS behavioural on a refusal path: alp-sdk
43
+ #1025 fixed `_hwrev_pad_route_overrides` to raise on an `hw_rev` absent from
44
+ the resolved `hw_revisions:` table -- ported here in the same shape (see that
45
+ function's own docstring). Before it, an unrecognised `hw_rev` silently
46
+ resolved to base-revision pad routing with a clean exit code on this file's
47
+ two emit modes (`carrier-netlist` / `composed-route-table`), which never run
48
+ `loader.load_board_yaml` and so never saw that fix's other half.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ from pathlib import Path
54
+ from typing import Any
55
+
56
+ from . import sdk_compat
57
+ from .loader import _load_yaml
58
+ from .models import OrchestratorError, SdkRevisionNotBuildable, SdkRevisionUnknown
59
+ from .paths import METADATA_ROOT, REPO
60
+ from .som_metadata import _sku_family
61
+
62
+ __all__ = [
63
+ "_compose_route",
64
+ "_hwrev_pad_route_overrides",
65
+ "_resolve_board",
66
+ "_resolve_inline_or_preset_board",
67
+ "_resolve_pad_routes",
68
+ "_resolve_sku",
69
+ "_sku_form_factor",
70
+ ]
71
+
72
+
73
+ def _sku_form_factor(sku: str) -> str:
74
+ """Return the E1M connector family for a SKU string."""
75
+ return "e1m-x" if _sku_family(sku) in {"v2n", "v2n-m1"} else "e1m"
76
+
77
+
78
+ def _resolve_sku(sku: str, metadata_root: Path = METADATA_ROOT) -> dict[str, Any]:
79
+ # Per-SKU preset lives at metadata/e1m_modules/<SKU>.yaml.
80
+ preset_path = metadata_root / "e1m_modules" / f"{sku}.yaml"
81
+ if not preset_path.is_file():
82
+ raise OrchestratorError(
83
+ f"no preset for SKU {sku} at "
84
+ f"{preset_path.relative_to(REPO) if preset_path.is_relative_to(REPO) else preset_path} "
85
+ f"(remaining SKUs land alongside the user-supplied HW config writeup)"
86
+ )
87
+ return _load_yaml(preset_path)
88
+
89
+
90
+ def _resolve_board(preset: str, metadata_root: Path = METADATA_ROOT) -> dict[str, Any]:
91
+ # Shared board definition lives at metadata/boards/<preset>.yaml.
92
+ preset_path = metadata_root / "boards" / f"{preset}.yaml"
93
+ if not preset_path.is_file():
94
+ raise OrchestratorError(
95
+ f"`preset: {preset}` does not resolve at "
96
+ f"{preset_path.relative_to(REPO) if preset_path.is_relative_to(REPO) else preset_path}")
97
+ return _load_yaml(preset_path)
98
+
99
+
100
+ def _resolve_inline_or_preset_board(
101
+ project: dict[str, Any], metadata_root: Path = METADATA_ROOT,
102
+ ) -> dict[str, Any]:
103
+ """Return the resolved board dict for a project.
104
+
105
+ Mirrors the orchestrator's resolution: `preset: <name>` loads
106
+ metadata/boards/<name>.yaml; otherwise the project's own
107
+ top-level `name`/`populated`/`e1m_routes` are wrapped into the
108
+ same shape the downstream emitters consume.
109
+ """
110
+ if "preset" in project:
111
+ return _resolve_board(project["preset"], metadata_root)
112
+ return {
113
+ "name": project.get("name"),
114
+ "populated": dict(project.get("populated") or {}),
115
+ "e1m_routes": dict(project.get("e1m_routes") or {}),
116
+ "default_hw_rev": project.get("hw_rev"),
117
+ }
118
+
119
+
120
+ def _resolve_pad_routes(
121
+ sku_preset: dict[str, Any],
122
+ ) -> dict[str, dict[str, Any]]:
123
+ """Index a SoM preset's `pad_routes:` block by E1M pad/instance.
124
+
125
+ Returns a dict keyed by `E1M_*` identifier (e.g., `E1M_GPIO_IO15`,
126
+ `E1M_SPI1`) with the full route entry as value:
127
+
128
+ { "dispatch": "...", "dispatch_pin": 14, "doc": "..." }
129
+
130
+ Pads NOT present in the preset's `pad_routes:` (or absent block
131
+ entirely) are implicitly `direct` -- they route to the main
132
+ silicon's GPIO / peripheral with no mediator.
133
+
134
+ The SDK's codegen layer composes this with the board preset's
135
+ `e1m_routes:` block: when an E1M pad appears in both, the board
136
+ supplies the role and the SoM supplies the dispatch path. The two
137
+ blocks together let a customer swap SoMs without touching the board
138
+ YAML or any app source -- the som-swappable-without-board-changes
139
+ promise.
140
+ """
141
+ routes = sku_preset.get("pad_routes") or []
142
+ if not isinstance(routes, list):
143
+ return {}
144
+ indexed: dict[str, dict[str, Any]] = {}
145
+ for entry in routes:
146
+ if not isinstance(entry, dict):
147
+ continue
148
+ e1m = entry.get("e1m")
149
+ if not isinstance(e1m, str):
150
+ continue
151
+ # Last-write-wins on duplicates -- the schema doesn't forbid
152
+ # them but real SoMs shouldn't have any; if the YAML carries
153
+ # duplicates, surface the later entry (most-recent author wins).
154
+ indexed[e1m] = entry
155
+ return indexed
156
+
157
+
158
+ def _hwrev_pad_route_overrides(
159
+ sku: str,
160
+ hw_rev: str | None,
161
+ metadata_root: Path = METADATA_ROOT,
162
+ ) -> list[dict[str, Any]]:
163
+ """Per-rev pad-route overrides for the selected ``hw_rev``.
164
+
165
+ The base SoM ``pad_routes:`` tracks the *production* revision. A board
166
+ revision whose routing differs declares the deviating pads as data in
167
+ the family ``hw-revisions.yaml`` ``pad_route_overrides:`` block; this
168
+ returns that rev's list (empty when it declares none, so the base
169
+ ``pad_routes:`` then applies verbatim). Applying these is what makes
170
+ the composed route table differ between revisions of one SKU.
171
+
172
+ Raises `SdkRevisionUnknown` when `hw_rev` isn't a key in the family
173
+ table, and `SdkRevisionNotBuildable` when it IS a key but its `status:`
174
+ refuses a build (`reserved`/`tbd`/status-less -- alp-sdk #1025's
175
+ existence + status halves; the status half by `1a91a232`, ported here
176
+ tan-cli#485). This emit path resolves its own SoM/board data
177
+ independently of `loader.load_board_yaml`, so both gates need their own
178
+ copy here -- before tan-cli#485, `--target carrier-netlist` /
179
+ `composed-route-table` emitted a full route table for a `reserved`
180
+ hw_rev while `tan build` refused the same input. Same predicates
181
+ (`sdk_compat.revision_known`/`revision_buildable`), same error TYPE and
182
+ MESSAGE SHAPE as `loader.load_board_yaml`'s SoM-side refusals -- the
183
+ `status_repr` construction below is `loader._status_repr` INLINED, not
184
+ imported, so a caller does not see two different shapes of the same
185
+ problem depending on which path resolved it; keep the two in sync by
186
+ hand if either one's message wording changes.
187
+
188
+ Reads the table through ``sdk_compat.load_family_table()`` rather than
189
+ its own ``yaml.safe_load``, so both independent readers of this file
190
+ agree about what a damaged one means (#563). This site used to do
191
+ ``yaml.safe_load(...) or {}``, and that ``or {}`` FAILED OPEN on the
192
+ two shapes that parse without raising: an EMPTY file and a file
193
+ TRUNCATED above its ``hw_revisions:`` block both yielded ``{}``, both
194
+ gates above then read "nothing to judge", and ``--emit
195
+ composed-route-table`` shipped a wrong-hardware artefact at exit 0 for
196
+ an hw_rev that does not exist -- the exact outcome the two gates below
197
+ exist to prevent. An unparseable table escaped as a raw
198
+ ``yaml.ScannerError`` traceback: a refusal, but not a diagnosable one.
199
+ The shared reader turns all of those into one coded
200
+ ``OrchestratorError`` naming the file. An ABSENT table still returns
201
+ ``{}`` there and stays benign here.
202
+ """
203
+ if not hw_rev:
204
+ return []
205
+ try:
206
+ family = _sku_family(sku)
207
+ except ValueError:
208
+ return []
209
+ data = sdk_compat.load_family_table(metadata_root, family)
210
+ if not data:
211
+ # Absent table only: `load_family_table` raises on every
212
+ # present-but-unusable shape, so reaching here with a falsy
213
+ # `data` means the family genuinely ships no table.
214
+ return []
215
+ if sdk_compat.revision_known(data, hw_rev) is False:
216
+ available = sorted((data.get("hw_revisions") or {}).keys())
217
+ raise SdkRevisionUnknown(
218
+ f"SoM {sku} hw_rev {hw_rev!r} is not a known hardware "
219
+ f"revision. Available hw_rev(s) for {sku}: {available}.")
220
+ if sdk_compat.revision_buildable(data, hw_rev) is False:
221
+ status = (data.get("hw_revisions") or {}).get(hw_rev, {}).get("status")
222
+ status_repr = f"status: {status!r}" if status is not None else \
223
+ "carries no `status:` key"
224
+ raise SdkRevisionNotBuildable(
225
+ f"SoM {sku} hw_rev {hw_rev!r} exists but is not buildable "
226
+ f"({status_repr}).")
227
+ rev = (data.get("hw_revisions") or {}).get(hw_rev) or {}
228
+ overrides = rev.get("pad_route_overrides") or []
229
+ return [e for e in overrides
230
+ if isinstance(e, dict) and isinstance(e.get("e1m"), str)]
231
+
232
+
233
+ def _compose_route(
234
+ e1m_pad: str,
235
+ board_route: dict[str, Any] | None,
236
+ pad_routes: dict[str, dict[str, Any]],
237
+ ) -> dict[str, Any]:
238
+ """Compose a board `e1m_routes:` entry (role on the board
239
+ side) with the SoM's `pad_routes:` entry (dispatch path on the
240
+ SoM side) for a single E1M pad / instance.
241
+
242
+ Returns a dict with: `board_role`, `board_macro`, `dispatch`
243
+ (defaults to `direct` when the SoM declares no proxy),
244
+ `dispatch_pin`, plus any docs.
245
+
246
+ Callers use the composed dict to drive codegen (Zephyr DTS
247
+ overlays, dispatch shims, capability validation).
248
+ """
249
+ out: dict[str, Any] = {"e1m": e1m_pad}
250
+ if board_route:
251
+ out["board_role"] = board_route.get("role")
252
+ out["board_macro"] = board_route.get("macro")
253
+ if board_route.get("doc"):
254
+ out["board_doc"] = board_route["doc"]
255
+ som_route = pad_routes.get(e1m_pad)
256
+ if som_route:
257
+ out["dispatch"] = som_route.get("dispatch", "direct")
258
+ if som_route.get("dispatch_pin") is not None:
259
+ out["dispatch_pin"] = som_route["dispatch_pin"]
260
+ if som_route.get("doc"):
261
+ out["som_doc"] = som_route["doc"]
262
+ else:
263
+ out["dispatch"] = "direct"
264
+ return out
@@ -0,0 +1,203 @@
1
+ # Copyright (c) 2026 Alp Lab AB
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ A general "does the BOUND alp-sdk checkout carry capability X" floor
5
+ (tan-cli#591).
6
+
7
+ `tan/planner/zephyr_board.py` hand-ports alp-sdk's `scripts/gen_zephyr_board.py`
8
+ generator, which grows a REQUIRED fact from time to time (the SE-owned `atoc`
9
+ memory-map region, alp-sdk#1289; the SoC JSON's `zephyr_peripherals_dtsi` key,
10
+ alp-sdk#1352). When such a fact is absent, there are two entirely different
11
+ causes that used to produce ONE message blaming the customer's own metadata:
12
+
13
+ 1. the bound alp-sdk checkout PREDATES the requirement -- no released
14
+ alp-sdk has it yet, and no SoM/SoC authoring fixes it;
15
+ 2. the checkout is new enough, but THIS SoM/SoC/board's own metadata is
16
+ genuinely incomplete.
17
+
18
+ This module answers (1) directly, from the checkout's own facts, rather than
19
+ inferring it from the shape of one board's metadata (the imprecise heuristic
20
+ `_aen_missing_region_message` used to apply to `atoc` alone, before this
21
+ module existed -- see its removal in the same change that added this file).
22
+ A caller still owns its OWN case-(2) message; `require_capability` only rules
23
+ case (1) out or in.
24
+
25
+ Deliberately keyed on a CAPABILITY, never a bare alp-sdk version number: the
26
+ planner mirror (`tan/planner/`) moves per-capability as alp-sdk gains ported
27
+ requirements one at a time, not on a release cadence, so a version-number
28
+ floor would need renegotiating on every unrelated alp-sdk release and would
29
+ say nothing true about a checkout built from an arbitrary commit. A
30
+ capability's *probe* instead reads a fact that ships with the checkout itself
31
+ -- a JSON-Schema property, or an entry in alp-sdk's own
32
+ `metadata/quality-tasks-v1.json` gate registry (the same registry
33
+ `adding-a-ci-gate`'s four-site lockstep keeps in sync with `scripts/check_*.py`
34
+ on the alp-sdk side) -- so the answer is correct for ANY checkout, release or
35
+ `dev`, without tan knowing which alp-sdk version introduced it.
36
+
37
+ Every probe reads ONLY under `metadata_root` (never `<sdk_root>/scripts/`):
38
+ `metadata/` is the one directory `tan/planner/` already has a resolved root
39
+ for (ADR-0017 -- the generators relocated into tan, the facts did not), and
40
+ restricting probes to it means a probe can be exercised against a plain copy
41
+ of `metadata/`, the same fixture shape
42
+ `tests/planner/test_zephyr_board_metadata_facts.py`'s `_MutatedMetadata`
43
+ already builds for every other refusal in this generator.
44
+
45
+ Adding the NEXT ported requirement's floor is: one `SdkCapability` entry
46
+ below (an id, the alp-sdk issue, a description, and a probe), plus one
47
+ `require_capability(metadata_root, THE_CAPABILITY)` call at the site that
48
+ already raises the authoring-gap message for that fact -- not a second
49
+ bespoke heuristic.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import json
55
+ from collections.abc import Callable
56
+ from dataclasses import dataclass
57
+ from pathlib import Path
58
+ from typing import Any
59
+
60
+
61
+ class SdkTooOldError(Exception):
62
+ """The BOUND alp-sdk checkout itself predates a required capability --
63
+ distinct from a `ZephyrBoardEmitError` authoring gap, which means the
64
+ checkout knows the fact but THIS SoM/SoC/board's own metadata omits it.
65
+
66
+ Not a `ZephyrBoardEmitError` subclass on purpose: `zephyr_board.py`'s own
67
+ exception funnels (both `tan generate`'s `_emit_one_in_process` and every
68
+ `pytest.raises` in its test module) catch broadly enough that the
69
+ distinction survives in the message via `type(err).__name__` -- exactly
70
+ how `tan generate`'s in-process engine already reports every planner
71
+ exception (`generate_cmd.py::_emit_one_in_process`) -- without requiring
72
+ a new top-level issue code in `generate_cmd.py`, which is out of scope
73
+ for this fix (tan-cli#591 stays inside `tan/planner/`).
74
+ """
75
+
76
+ def __init__(self, capability: "SdkCapability") -> None:
77
+ self.capability = capability
78
+ super().__init__(
79
+ f"this alp-sdk predates {capability.description} "
80
+ f"({capability.issue}); {capability.emit_description} needs a "
81
+ "checkout containing it -- upgrade alp-sdk, or pin tan to a "
82
+ "release that predates the requirement"
83
+ )
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class SdkCapability:
88
+ """One ported requirement's checkable floor.
89
+
90
+ *probe* answers True iff *metadata_root* shows the bound alp-sdk
91
+ checkout knows the concept AT ALL -- independent of whether any
92
+ particular SoM/SoC/board actually used it. `id` and `issue` are for
93
+ diagnostics and tests, not compared against anything; `description` and
94
+ `emit_description` compose directly into `SdkTooOldError`'s message.
95
+ """
96
+
97
+ id: str
98
+ issue: str
99
+ description: str
100
+ probe: Callable[[Path], bool]
101
+ emit_description: str = "the AEN board emit"
102
+
103
+
104
+ def _read_json(path: Path) -> Any | None:
105
+ """`None` on anything short of a parsed JSON value -- absent, unreadable,
106
+ or malformed are all "this probe can't confirm the capability", not a
107
+ crash. A probe answering `False` when it can't tell is NOT neutral:
108
+ `require_capability` treats every `False` as the SDK-floor refusal, so
109
+ an unreadable or malformed probe file surfaces as `SdkTooOldError`
110
+ (the "upgrade alp-sdk" message), not as the caller's authoring-gap
111
+ message."""
112
+ if not path.is_file():
113
+ return None
114
+ try:
115
+ return json.loads(path.read_text(encoding="utf-8"))
116
+ except (OSError, ValueError):
117
+ return None
118
+
119
+
120
+ def schema_declares_property(schema_relpath: str, prop: str) -> Callable[[Path], bool]:
121
+ """Probe factory: True iff `metadata/schemas/<schema_relpath>` declares
122
+ *prop* as a top-level JSON-Schema property.
123
+
124
+ A property that exists only from some alp-sdk commit onward is a
125
+ reliable, checkout-only marker for "this checkout is new enough to know
126
+ the concept": schemas ship with every checkout, release or `dev` alike,
127
+ so the probe needs nothing beyond the already-resolved `metadata_root`.
128
+ """
129
+
130
+ def _probe(metadata_root: Path) -> bool:
131
+ spec = _read_json(metadata_root / "schemas" / schema_relpath)
132
+ if not isinstance(spec, dict):
133
+ return False
134
+ return prop in (spec.get("properties") or {})
135
+
136
+ return _probe
137
+
138
+
139
+ def quality_task_declared(task_id: str) -> Callable[[Path], bool]:
140
+ """Probe factory: True iff `metadata/quality-tasks-v1.json` lists a task
141
+ with id *task_id* -- alp-sdk's own registry of which `check_*.py` gates
142
+ exist (the same registry `adding-a-ci-gate`'s four-site lockstep keeps
143
+ current on the alp-sdk side). A requirement that shipped its own gate is
144
+ checkable through the registry it already updated, rather than inventing
145
+ a second marker file for tan to track.
146
+ """
147
+
148
+ def _probe(metadata_root: Path) -> bool:
149
+ doc = _read_json(metadata_root / "quality-tasks-v1.json")
150
+ if not isinstance(doc, dict):
151
+ return False
152
+ tasks = doc.get("tasks")
153
+ if not isinstance(tasks, list):
154
+ return False
155
+ return any(isinstance(t, dict) and t.get("id") == task_id for t in tasks)
156
+
157
+ return _probe
158
+
159
+
160
+ # ---------------------------------------------------------------------
161
+ # Registry -- one entry per ported requirement.
162
+ # ---------------------------------------------------------------------
163
+
164
+ AEN_ATOC_RESERVATION = SdkCapability(
165
+ id="aen.atoc_reservation",
166
+ issue="alp-sdk#1289",
167
+ description="the SE-owned ATOC reservation",
168
+ probe=quality_task_declared("atoc-reservation"),
169
+ )
170
+
171
+ AEN_ZEPHYR_PERIPHERALS_DTSI = SdkCapability(
172
+ id="aen.zephyr_peripherals_dtsi",
173
+ issue="alp-sdk#1352",
174
+ description="the per-SoC `zephyr_peripherals_dtsi` overlay reference",
175
+ probe=schema_declares_property(
176
+ "soc-spec-v1.schema.json", "zephyr_peripherals_dtsi"
177
+ ),
178
+ )
179
+
180
+ AEN_SOM_POWER_DOMAINS = SdkCapability(
181
+ id="aen.som_power_domains",
182
+ issue="alp-sdk#2784",
183
+ description="the AEN SoM `power_domains:` block (on-module-links-v2)",
184
+ probe=schema_declares_property(
185
+ "on-module-links-v2.schema.json", "power_domains"
186
+ ),
187
+ )
188
+
189
+
190
+ def require_capability(metadata_root: Path, capability: SdkCapability) -> None:
191
+ """No-op when the bound checkout carries *capability*; raises
192
+ `SdkTooOldError` when it doesn't.
193
+
194
+ Answers ONLY "does the checkout know the concept" -- a caller still owns
195
+ its own authoring-gap message for "the checkout knows it, but this
196
+ SoM/SoC/board's metadata doesn't declare it". A probe that cannot read
197
+ its metadata file (absent, unreadable, malformed -- e.g. a partial
198
+ `--metadata-root` copy) answers False and so also raises
199
+ `SdkTooOldError`; the message cannot tell that apart from a genuinely
200
+ old checkout.
201
+ """
202
+ if not capability.probe(metadata_root):
203
+ raise SdkTooOldError(capability)