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
tan/core/mram_link.py ADDED
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure logic for Flow D's link-address guard (tan-cli#1371).
3
+
4
+ An image destined for the app's MRAM slot must be LOADed at or above that slot. An
5
+ ITCM-linked image (`p_paddr` 0x0) written there is the mistake this catches, and the
6
+ manifest's `flash_method: ram_run_only` only stops it when the manifest is current --
7
+ a hand-edited manifest, a build whose manifest write failed, or a direct backend call
8
+ bypass it. It is the mirror of `flash.ram-image-not-ram-linked` (`tan.core.ram_run`).
9
+
10
+ Only the program headers are read, with no IO: the symbol table (and its limits) is
11
+ irrelevant to a load address and must neither skip nor refuse the check. `p_paddr` is
12
+ the LOAD address; `p_vaddr` is where the image runs, and an image loaded into MRAM may
13
+ legitimately run from ITCM.
14
+
15
+ **Lower bound only.** An image is refused when it is loaded BELOW its slot, never for
16
+ being above it. An image linked above its own slot -- say an M55-HP image at 0x802B0000
17
+ written to the M55-HE slot 0x80010000 -- is NOT caught: that needs per-core slot windows
18
+ this guard does not have. Equality with the slot is not required either, because an image
19
+ header may offset the first segment."""
20
+ from __future__ import annotations
21
+
22
+ import struct
23
+
24
+ #: Registered issue code (`contract/issue-codes.json`).
25
+ CODE_NOT_MRAM_LINKED = "flash.mram-image-not-mram-linked"
26
+
27
+ _ELF_HEADER_SIZE = 52
28
+ _PHDR_SIZE = 32
29
+ _PT_LOAD = 1
30
+
31
+
32
+ class UnreadableElf(Exception):
33
+ """The program headers cannot be read; the message is the reason."""
34
+
35
+
36
+ def lowest_load_paddr(data: bytes) -> int:
37
+ """The lowest `p_paddr` among LOAD segments with a nonzero `p_filesz` (never "the
38
+ first LOAD": a zero-FileSiz `.bss` in DTCM is often listed first). Raises
39
+ [`UnreadableElf`] for a 64-bit or big-endian file, a truncated header, a program
40
+ header table past the end of the file, or no such segment."""
41
+ if len(data) < _ELF_HEADER_SIZE:
42
+ raise UnreadableElf(f"truncated: {len(data)} B is shorter than an ELF32 header")
43
+ if data[4] != 1:
44
+ raise UnreadableElf("it is not an ELF32 file (only ELF32 images are supported)")
45
+ if data[5] != 1:
46
+ raise UnreadableElf("it is not little-endian (only little-endian images are supported)")
47
+ phoff = struct.unpack_from("<I", data, 28)[0]
48
+ phentsize, phnum = struct.unpack_from("<HH", data, 42)
49
+ if phnum and phentsize < _PHDR_SIZE:
50
+ raise UnreadableElf(f"its program header entry size is {phentsize} B (an Elf32_Phdr is 32)")
51
+ if phoff + phnum * phentsize > len(data):
52
+ raise UnreadableElf(
53
+ f"its {phnum} program headers at offset {phoff} run past the end of the file "
54
+ f"({len(data)} B)"
55
+ )
56
+ loadable = []
57
+ for i in range(phnum):
58
+ p_type, _off, _vaddr, p_paddr, p_filesz, _memsz = struct.unpack_from(
59
+ "<IIIIII", data, phoff + i * phentsize
60
+ )
61
+ if p_type == _PT_LOAD and p_filesz:
62
+ loadable.append(p_paddr)
63
+ if not loadable:
64
+ raise UnreadableElf("it has no LOAD segment with file content")
65
+ return min(loadable)
66
+
67
+
68
+ def mram_link_refusal(data: bytes, floor: int, floor_name: str) -> str | None:
69
+ """A refusal message when the ELF in `data` is not loaded at or above `floor`, else
70
+ `None`. `floor` is the entry's `slot0_load_address`;
71
+ `floor_name` names it for the message. An ELF whose program headers cannot be
72
+ read is REFUSED, never waved through: the guard exists for the image nothing else
73
+ caught. Equality is not required -- an image header may offset the first segment."""
74
+ try:
75
+ lowest = lowest_load_paddr(data)
76
+ except UnreadableElf as err:
77
+ return (
78
+ f"the artefact's ELF could not be parsed ({err}), so its load address cannot be "
79
+ "verified against the MRAM slot -- refusing to write an unchecked image."
80
+ )
81
+ if lowest >= floor:
82
+ return None
83
+ return (
84
+ f"the image's lowest LOAD segment (p_paddr) is at 0x{lowest:X}, below {floor_name} "
85
+ f"0x{floor:X}: it is not linked for that MRAM slot (an ITCM/RAM-linked build). "
86
+ "Rebuild it MRAM-linked, or RAM-run it with `tan flash --ram` (Flow C)."
87
+ )
@@ -0,0 +1,94 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`ospi_memories:`/`hyperram:` `on_module:` scaffold lines for `tan new-som`
3
+ (tan-cli#1220).
4
+
5
+ Split into its own module rather than grown inside `new_som_cmd.py`: that
6
+ file already sits at its `tests/gates/module_size_budget.d/` ceiling
7
+ (`MODULE_CAP`), and PR #1222 set the precedent for this exact situation --
8
+ `model_cmd.py` at its own ceiling got a new `tan/core/model_list.py` for the
9
+ actual engine, keeping the command file's own growth to dispatch/glue. This
10
+ module is the `_render_preset` sibling: pure string rendering, no IO, no
11
+ click/typer.
12
+
13
+ Scoped to Alif Ensemble parts only. The caller (`_render_preset`) already
14
+ splits every `soc_ref` into `vendor_slug, family_slug, part_slug` for its own
15
+ `metadata/socs/...` path -- `is_alif_ensemble` reads that SAME triple rather
16
+ than adding a second vendor/family discrimination mechanism (tan-cli#1220's
17
+ own instruction: reuse how `new_som_cmd.py` already tells vendors/families
18
+ apart, don't invent a new one). The scope itself is not arbitrary: the
19
+ schema's own `on_module.ospi_memories` description reads "(AEN family)"
20
+ (`metadata/schemas/som-preset-v2.schema.json`), and every shipped
21
+ `E1M-AEN*.yaml` preset that populates these fields (301/401/501/601/701/801)
22
+ uses the identical CS0=NOR/CS1=HyperRAM split on one shared octal OSPI0
23
+ controller.
24
+
25
+ Every value below is either that shared Ensemble-silicon convention
26
+ (`chip_select`, `interface`) or an explicit placeholder -- never an invented
27
+ MPN or capacity (ADR-0017, invariant I-26). Two of the schema's fields have
28
+ no `"TBD"` string escape hatch the way `chip`/`capacity_mbit` do:
29
+ `chip_select` is a bare `"type": "integer"` (no `oneOf` alternative), and
30
+ `role`/`interface` must match a lowercase-slug pattern (`^[a-z][a-z0-9_]*$`,
31
+ which an uppercase `TBD` fails). The convention value fills `chip_select`/
32
+ `interface`; the schema-legal lowercase literal `tbd` fills `role`, matching
33
+ `inference.auto_order`'s own `[tbd]` placeholder convention next door in
34
+ `_render_preset`.
35
+
36
+ `chip_select`'s only bound in `metadata/schemas/som-preset-v2.schema.json` is
37
+ `"minimum": 0` -- there is no schema maximum, and no `scripts/check_*.py`
38
+ gate in the pinned alp-sdk checkout mentions `chip_select` at all (measured:
39
+ `grep -rn chip_select scripts/` finds nothing). "Bounded to 0 or 1" is a
40
+ description of the real OSPI0 controller's two physical chip-selects, not an
41
+ enforced ceiling; nothing stops a preset from declaring `chip_select: 2` and
42
+ still passing every automated check in this checkout.
43
+
44
+ `assembled:` (optional on both `$defs`, and also has no `"TBD"` variant) is
45
+ left out of the rendered block entirely -- the schema's own default when
46
+ omitted is `true` (populated), which this scaffold has no basis to assert OR
47
+ deny for a module whose BOM does not exist yet. Matches how `_render_preset`
48
+ already treats every other schema-optional block it cannot safely guess
49
+ (`capabilities:`, `pad_routes:`, `helper_firmware:`): a comment, not a
50
+ guessed value.
51
+ """
52
+ from __future__ import annotations
53
+
54
+ #: The one (vendor, family) pair `ospi_memories:`/`hyperram:` are scoped to
55
+ #: -- see the module docstring for why.
56
+ _ALIF_ENSEMBLE = ("alif", "ensemble")
57
+
58
+
59
+ def is_alif_ensemble(vendor_slug: str, family_slug: str) -> bool:
60
+ """True when a `soc_ref`'s parsed `vendor_slug`/`family_slug` name an
61
+ Alif Ensemble part -- the one scope `render_ospi_memories_and_hyperram`
62
+ is meant to be called under."""
63
+ return (vendor_slug, family_slug) == _ALIF_ENSEMBLE
64
+
65
+
66
+ def render_ospi_memories_and_hyperram() -> list[str]:
67
+ """The `ospi_memories:`/`hyperram:` `on_module:` sub-block, as 2-space-
68
+ indented lines ready to append inside `_render_preset`'s `on_module:`
69
+ section (append after `eeprom:`, before the `i2c_devices:` comment
70
+ block -- the same relative order every shipped `E1M-AEN*.yaml` preset
71
+ that carries both keys uses). See the module docstring for what each
72
+ field's placeholder is and why.
73
+ """
74
+ lines: list[str] = []
75
+ a = lines.append
76
+ a(" # OSPI0 dual-chip-select external memory (Ensemble-family")
77
+ a(" # convention): NOR flash on CS0, HyperRAM on CS1, sharing one")
78
+ a(" # shared octal bus. Delete the whole ospi_memories:/hyperram:")
79
+ a(" # pair if this module populates neither; add an ospi1: row")
80
+ a(" # alongside ospi0: for a second, independent OSPI controller.")
81
+ a(" ospi_memories:")
82
+ a(" ospi0:")
83
+ a(" chip: TBD # storage-part MPN (datasheet/BOM), or TBD")
84
+ a(" capacity_mbit: TBD # Mbit, from the storage-part datasheet")
85
+ a(" chip_select: 0 # OSPI0 CS0 (Ensemble-family convention)")
86
+ a(" role: tbd # what it's for, e.g. app_storage")
87
+ a(" # HyperRAM sharing the OSPI0 octal bus with the flash above,")
88
+ a(" # separated only by chip-select -- delete if not populated.")
89
+ a(" hyperram:")
90
+ a(" chip: TBD # HyperRAM MPN (datasheet/BOM), or TBD")
91
+ a(" capacity_mbit: TBD # Mbit, from the HyperRAM datasheet")
92
+ a(" interface: ospi0 # controller instance (Ensemble convention)")
93
+ a(" chip_select: 1 # OSPI0 CS1 (Ensemble-family convention)")
94
+ return lines
tan/core/os_class.py ADDED
@@ -0,0 +1,159 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The Cortex-A/Cortex-M -> OS convention (issue #95), free of `tan.planner`'s
3
+ binding requirement.
4
+
5
+ `tan.planner.topology` owns the AUTHORITATIVE version of this rule --
6
+ `_default_os_from_core_type` there is what `validate._enforce_os_matches_core_class`
7
+ gates a real build on, and it re-exports the same names this module defines so
8
+ every existing planner caller/test is unaffected by this file's existence
9
+ (`tan.planner.topology._default_os_from_core_type` and `.CLASS_RUNTIMES` are
10
+ now this module's `default_os_from_core_type`/`CLASS_RUNTIMES`, imported not
11
+ redefined).
12
+
13
+ **Why this lives OUTSIDE `tan.planner` (tan-cli#870).** `tan.planner` is a
14
+ process-global-bound package: importing ANY of its submodules requires
15
+ `tan.planner_root.bind_sdk_root(<checkout>)` to have already run, because
16
+ `tan/planner/__init__.py` -> `tan/planner/paths.py` reads `sdk_root()` at
17
+ IMPORT time and the binding is refuse-to-rebind for the rest of the process
18
+ (`tan/planner_root.py`'s own docstring). That is the right contract for `tan
19
+ build`'s single big planning pass per process. It is the WRONG contract for
20
+ `tan presets`, which is exercised by dozens of small, independent unit tests
21
+ in the SAME pytest process, each against its own disposable synthetic SDK
22
+ root: the first such test to reach a `tan.planner` import permanently binds
23
+ the whole worker process to ITS throwaway root, and every later test in that
24
+ worker that needs a DIFFERENT (often the real, `ALP_SDK_ROOT`-derived) root
25
+ then fails outright -- measured while building tan-cli#870: routing
26
+ `presets_cmd`'s SoC-type/`allowed_os` lookups through `tan.planner.topology`
27
+ turned a clean local bound suite run (5 failed, 0 errors, the 5 pre-existing
28
+ terminal-width/breadth-layer artefacts) into 292 `PlannerRootError` cascades
29
+ across `tests/parity/test_planner_emit_parity.py`, in a completely unrelated
30
+ file, purely from `tests/commands/test_presets_command.py`'s own throwaway
31
+ fixtures winning the race to bind first in their xdist worker.
32
+
33
+ So the RULE (this file) lives somewhere binding-free, and both consumers
34
+ import it: `tan.planner.topology` for the planner's own enforcement (still
35
+ combined there with `_core_os_choices`'s bound-tree schema fallback, which a
36
+ real build-time metadata root may legitimately need), and
37
+ `tan.commands.presets_cmd` for the wizard's `allowedOs` (combined there with a
38
+ plain, no-fallback read of the GIVEN checkout's own `board.schema.json` --
39
+ `presets` never has a "different bound tree" to fall back to; it is reading
40
+ exactly one checkout throughout). Neither consumer re-derives the convention;
41
+ both call this module.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ from collections.abc import Iterable
47
+
48
+ #: The two class-determined OS runtimes (issue #95): Cortex-A -> Yocto
49
+ #: (Linux), Cortex-M -> Zephyr (RTOS). These follow the core class and are NOT
50
+ #: user-selectable (see `default_os_from_core_type`); `baremetal` (no-OS) and
51
+ #: `off` (disabled) are the only values a board.yaml may set explicitly.
52
+ CLASS_RUNTIMES = ("yocto", "zephyr")
53
+
54
+
55
+ def default_os_from_core_type(core_type: str) -> str:
56
+ """Infer default OS from a SoC's `cores[].type`.
57
+
58
+ Convention (codified across the SoM presets pre-2026-05-18):
59
+ cortex-a* -> yocto
60
+ cortex-m* -> zephyr
61
+ anything else -> off
62
+
63
+ Used as the fallback when a SoM preset's `topology.<core>.os` is
64
+ omitted (the field is optional in som-preset-v2.schema.json -- M-class
65
+ cores default to Zephyr, A-class to Yocto).
66
+
67
+ *core_type* is typed `str` but this is the SHARED entry point both
68
+ binding-free callers (`presets_cmd.allowed_os_lookup`) and the planner
69
+ (`topology._default_os_from_core_type`) reach with a value read off a
70
+ SoC JSON that a schema-invalid `--sdk-root`/`--metadata-root` tree makes
71
+ no promise about (tan-cli#957) -- `isinstance` here, not `str`-only trust
72
+ in the annotation, is the backstop for every caller, present or future,
73
+ that forwards a `cores[].type` value without its OWN guard. A non-string
74
+ normalises to the same `""` unresolved sentinel a genuinely absent
75
+ `type` already produces, rather than raising `AttributeError` on
76
+ `.lower()`.
77
+ """
78
+ t = core_type.lower() if isinstance(core_type, str) else ""
79
+ if t.startswith("cortex-a"):
80
+ return "yocto"
81
+ if t.startswith("cortex-m"):
82
+ return "zephyr"
83
+ return "off"
84
+
85
+
86
+ def cross_class_os(core_type: str) -> set[str]:
87
+ """The class runtime a core may NOT be set to -- the OS of the *other*
88
+ class. A Cortex-A can't run Zephyr; a Cortex-M can't run Yocto."""
89
+ return set(CLASS_RUNTIMES) - {default_os_from_core_type(core_type)}
90
+
91
+
92
+ def allowed_os_for_core(core_type: str, choices: Iterable[str]) -> list[str]:
93
+ """The `os:` values valid for a core: *choices* (the checkout's own `os:`
94
+ enum) minus the cross-class OS -- e.g. Cortex-A -> `[yocto, baremetal,
95
+ off]`, Cortex-M -> `[zephyr, baremetal, off]`.
96
+
97
+ `core_type == ""` is the shared UNRESOLVED sentinel both callers of this
98
+ function use when a core's `type` could not be determined (no matching
99
+ SoC JSON on disk, no entry for this core id in it, a synthetic/partial
100
+ checkout, ...) -- `tan.planner.topology.core_os_topology`'s
101
+ `soc_types.get(core_id, "")` and `tan.commands.presets_cmd.read_soms`'s
102
+ `core_types.get(c.id, "")` both feed it here unchanged. Degrades to `[]`
103
+ rather than the plausible-but-wrong cross-class subtraction `cross_class_os("")`
104
+ would otherwise produce (tan-cli#914): `default_os_from_core_type("")`
105
+ falls through to `"off"`, so an UNRESOLVED type and a genuinely
106
+ off-class core type were indistinguishable at this call, and the
107
+ unresolved case silently offered Bare-metal for what may be a Cortex-M
108
+ core -- the exact defect #870 exists to close, reproduced by the very
109
+ field meant to prevent it. An unresolved type gets an unresolved (empty)
110
+ answer instead, for both the wizard's `presets_cmd.allowed_os_lookup` and
111
+ the build-time `topology._allowed_os_for_core` -- the one call this
112
+ module exists to share, so the two can never drift back apart on this.
113
+
114
+ A non-string *core_type* is ALSO treated as unresolved (tan-cli#957),
115
+ not just falsy ones: `default_os_from_core_type`'s own `isinstance`
116
+ guard already stops a truthy non-string (`7`, a populated list, `True`,
117
+ ...) from raising here, but without this function's OWN `isinstance`
118
+ check that value would still reach `cross_class_os` truthy, fall through
119
+ `default_os_from_core_type`'s `"off"` default the same way `""` does,
120
+ and hand back the identical `["baremetal", "off"]` plausible-but-wrong
121
+ guess this docstring already rejects for the empty-string case --
122
+ the crash-vs-leak choice tan-cli#957 found in `core_type_lookup` shows
123
+ up here too, one level in, if this guard is skipped: a caller's own
124
+ `isinstance` at its read site is not something this shared function may
125
+ assume every present or future caller remembered to add.
126
+ """
127
+ if not isinstance(core_type, str) or not core_type:
128
+ return []
129
+ cross = cross_class_os(core_type)
130
+ return [o for o in choices if o not in cross]
131
+
132
+
133
+ def infer_runtime_for_core_id(core_id: str) -> str:
134
+ """Best-effort runtime for a core id with no OS given: an `a` followed by
135
+ a digit at a WORD start (`a55_cluster`, `cluster_a32`, `A55`) runs
136
+ `yocto`; everything else defaults to `zephyr`.
137
+
138
+ Word starts are the string start and any position after `_`/`-`, so
139
+ `data55` is NOT Cortex-A. Mirrors
140
+ `tan_core::wizard::infer_runtime_for_core_id`, and KEEP IN SYNC with
141
+ alp-sdk-vscode's ConfiguratorView `coreSiliconClass` (same word-start
142
+ test; the one intentional difference is the fallback, since a CLI must
143
+ pick a concrete runtime where the IDE can offer "unknown").
144
+
145
+ A last resort: a topology that declares `board:`/`machine:` never reaches
146
+ it. Lives here rather than beside either caller because it had TWO
147
+ definitions -- `tan/core/scaffold.py` and `tan/commands/presets_cmd.py` --
148
+ and `tan/core/bootstrap.py` reached OUTWARD past the core one to import
149
+ the command's copy (tan-cli#408). One owner, in the leaf module that
150
+ already answers "what OS class is this core", is what the duplicated
151
+ docstrings each said they wanted.
152
+ """
153
+ lowered = core_id.lower()
154
+ word_start = True
155
+ for i, ch in enumerate(lowered):
156
+ if word_start and ch == "a" and i + 1 < len(lowered) and lowered[i + 1].isdigit():
157
+ return "yocto"
158
+ word_start = ch in "_-"
159
+ return "zephyr"
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Repo-relative paths a unified diff / `git format-patch` file touches.
3
+
4
+ Pure text in, paths out (tan-cli#1384). Used to fingerprint the working-tree
5
+ files alp-sdk's `zephyr/patches/` write, so the set must include every path a
6
+ patch can change on disk, not only the ones with a `+++ b/` line:
7
+
8
+ * `diff --git a/<old> b/<new>` names BOTH sides, which is the only place a pure
9
+ rename, a mode-only change or a binary patch is visible (none of them carry
10
+ `---`/`+++` lines);
11
+ * `--- a/<path>` names a file the patch deletes (`+++ /dev/null`);
12
+ * `+++ b/<path>` names a file it writes (`--- /dev/null` when it creates it).
13
+
14
+ `---`/`+++` count only as headers, never as hunk content: after a
15
+ `diff --git` line and before the first `@@`, or (for a plain `diff -u` with no
16
+ `diff --git` line) as a `---`, `+++`, `@@` triple.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ _TOKEN = re.compile(r'"(?:[^"\\]|\\.)*"|\S+')
23
+
24
+
25
+ def _unquote(raw: str) -> str:
26
+ """Undo git's C-style quoting (`"a/caf\\303\\251"`); plain paths pass through."""
27
+ raw = raw.strip()
28
+ if len(raw) >= 2 and raw[0] == raw[-1] == '"':
29
+ try:
30
+ return (
31
+ raw[1:-1].encode("utf-8").decode("unicode_escape")
32
+ .encode("latin-1").decode("utf-8", "replace")
33
+ )
34
+ except (UnicodeError, ValueError):
35
+ return raw[1:-1]
36
+ return raw
37
+
38
+
39
+ def _side(raw: str, prefix: str) -> str | None:
40
+ """The path on one side of a header, or `None` (`/dev/null`, no prefix)."""
41
+ path = _unquote(raw.split("\t", 1)[0]).strip()
42
+ if path == "/dev/null" or not path.startswith(prefix):
43
+ return None
44
+ return path[len(prefix):] or None
45
+
46
+
47
+ def _git_header(rest: str) -> list[str]:
48
+ """Both paths of `diff --git a/<old> b/<new>` (the text after `diff --git `)."""
49
+ if '"' in rest:
50
+ tokens = _TOKEN.findall(rest)
51
+ if len(tokens) == 2:
52
+ return [p for p in (_side(tokens[0], "a/"), _side(tokens[1], "b/")) if p]
53
+ half = (len(rest) - 1) // 2
54
+ if len(rest) % 2 == 1 and rest[:half].startswith("a/") and rest[half] == " " \
55
+ and rest[half + 1:] == "b/" + rest[:half][2:]:
56
+ return [rest[:half][2:]] # unrenamed: both sides are the same path
57
+ old, sep, new = rest.partition(" b/")
58
+ return [p for p in (_side(old, "a/"), new if sep else None) if p]
59
+
60
+
61
+ def patch_paths(text: str) -> set[str]:
62
+ """Every repo-relative path `text` (one patch file) touches."""
63
+ lines = text.splitlines()
64
+ paths: set[str] = set()
65
+ in_header = False
66
+ for i, ln in enumerate(lines):
67
+ if ln.startswith("diff --git "):
68
+ in_header = True
69
+ paths.update(_git_header(ln[len("diff --git "):]))
70
+ elif ln.startswith("@@"):
71
+ in_header = False
72
+ elif ln.startswith(("--- ", "+++ ")):
73
+ plain_triple = (
74
+ ln.startswith("--- ")
75
+ and i + 2 < len(lines)
76
+ and lines[i + 1].startswith("+++ ")
77
+ and lines[i + 2].startswith("@@")
78
+ )
79
+ if in_header or plain_triple:
80
+ side = _side(ln[4:], "a/" if ln[0] == "-" else "b/")
81
+ if side:
82
+ paths.add(side)
83
+ if plain_triple:
84
+ plus = _side(lines[i + 1][4:], "b/")
85
+ if plus:
86
+ paths.add(plus)
87
+ return paths
tan/core/pending.py ADDED
@@ -0,0 +1,40 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The one definition of alp-sdk's `TBD` pending-placeholder sentinel (#276).
3
+
4
+ alp-sdk writes the literal string `TBD` into a manifest/preset field it has
5
+ not filled in yet -- `flash_args: TBD`, `silicon_variant: TBD`, an unfilled
6
+ `e1m_pad` -- a deliberate "don't invent a value" convention, not an error.
7
+ Every reader of that convention has to agree on what "unfilled" means, so the
8
+ rule lives here ONCE rather than being re-typed at each read site.
9
+
10
+ This module is deliberately the neutral one: it has no flash-planning
11
+ machinery (`tan.core.flash_plan`) and no image-bundle machinery
12
+ (`tan.core.image_bundle`) behind it, so a consumer that is neither -- `tan.core.size`,
13
+ and `pinmux` once it is ported -- can test for the sentinel without pulling
14
+ either domain in. `flash_plan` still owns the flash-specific decisions built
15
+ on top (`flash_args_has_tbd`'s mapping/sequence recursion, the skip-vs-fail
16
+ split between `flash_args` and an artefact path); only the base comparison
17
+ moved here.
18
+
19
+ Trimmed before comparing -- a YAML `device: " TBD "` is the same unfilled
20
+ field -- but deliberately NOT case-folded and NOT a substring test:
21
+ `TBD-1234-XYZ` is a plausible part number and `flash_args.build_dir:
22
+ /opt/TBDtool/x` a plausible path, and refusing either would block a
23
+ legitimate value. `tbd` lowercase is not the sentinel alp-sdk emits; widening
24
+ to it means widening the SDK's convention first, in one place, not here.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ #: alp-sdk's unfilled-field sentinel.
31
+ PENDING_PLACEHOLDER = "TBD"
32
+
33
+
34
+ def is_pending_placeholder(value: Any) -> bool:
35
+ """Whether a manifest/preset SCALAR is the SDK's unfilled-field sentinel.
36
+
37
+ `None` and every non-string value are never pending; a string is pending
38
+ only when it is exactly `PENDING_PLACEHOLDER` after trimming surrounding
39
+ whitespace."""
40
+ return isinstance(value, str) and value.strip() == PENDING_PLACEHOLDER
@@ -0,0 +1,138 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """A single-slice build plan for a `board.yaml`-less Zephyr example (tan-cli#1359).
3
+
4
+ alp-sdk's bench-style examples (`examples/aen/aen-inference-latency`, ...) are
5
+ plain Zephyr apps built per board target and ship no `board.yaml`, so the
6
+ planner has nothing to read. `tan build --project <dir> --board <target>`
7
+ synthesises the plan the planner WOULD have emitted for one Zephyr slice and
8
+ feeds it through the ordinary `tan build` pipeline -- the same token
9
+ substitution, host-Python resolver (#1326), toolchain / west-workspace
10
+ resolution, dispatch, pristine policy and envelope as a planned build. Nothing
11
+ here spawns or resolves anything; it only shapes data.
12
+
13
+ The slice mirrors a planner Zephyr slice: `west build -b <board> <app> --`
14
+ run with cwd `build/<core>-zephyr` (so west's own `build/` lands under it and
15
+ `--pristine`'s wipe guards still apply -- an explicit `-d` would disable them).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import re
21
+
22
+ from tan.core.plan_tokens import TOKEN_PYTHON, TOKEN_SDK_ROOT
23
+
24
+ #: A Zephyr board target: `<board>[@rev][/<soc>[/<cpucluster>[/<variant>]]]`.
25
+ _BOARD_TARGET = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.@-]*(/[A-Za-z0-9_.-]+){0,3}")
26
+ #: `-DNAME=value` / `-DNAME:TYPE=value` / `-DNAME`.
27
+ _DEFINE = re.compile(r"-D[A-Za-z_][A-Za-z0-9_]*(:[A-Za-z]+)?(=.*)?", re.DOTALL)
28
+
29
+
30
+ def board_target_problem(board: str) -> str | None:
31
+ if not _BOARD_TARGET.fullmatch(board):
32
+ return (
33
+ f"`--board {board}` is not a Zephyr board target "
34
+ "(e.g. `alp_e1m_aen803_m55_he/ae822fa0e5597ls0/rtss_he`)"
35
+ )
36
+ return None
37
+
38
+
39
+ def normalise_defines(defines: list[str]) -> tuple[list[str], str | None]:
40
+ """`-D` values as CMake args: `FOO=1` and `-DFOO=1` both become `-DFOO=1`."""
41
+ out: list[str] = []
42
+ for raw in defines:
43
+ item = raw if raw.startswith("-D") else f"-D{raw}"
44
+ if "${" in item:
45
+ # Plan token substitution would expand (or choke on) it.
46
+ return [], f"`-D {raw}` contains `${{`, which tan reserves for build-plan tokens"
47
+ if not _DEFINE.fullmatch(item):
48
+ return [], f"`-D {raw}` is not a CMake definition (`NAME=VALUE`)"
49
+ out.append(item)
50
+ return out, None
51
+
52
+
53
+ def core_id_for(board: str) -> str:
54
+ """The slice's id: the board target's core part, else its board name."""
55
+ parts = board.split("/")
56
+ name = parts[2] if len(parts) >= 3 else parts[0]
57
+ return re.sub(r"[^A-Za-z0-9_]+", "_", name).strip("_").lower() or "app"
58
+
59
+
60
+ #: `generatedBy` of a synthesised plan: how the post-build manifest write
61
+ #: recognises the plain route (tan-cli#1370).
62
+ PLAIN_GENERATED_BY = "tan build --board"
63
+
64
+
65
+ def plain_route_target(plan) -> tuple[str, str] | None:
66
+ """`(board target, core id)` of a plan [`plain_zephyr_plan`] synthesised."""
67
+ if plan.generated_by != PLAIN_GENERATED_BY or len(plan.slices) != 1:
68
+ return None
69
+ (sl,) = plan.slices
70
+ args = sl.command.args if sl.command else []
71
+ if "-b" not in args or args.index("-b") + 1 >= len(args):
72
+ return None
73
+ return args[args.index("-b") + 1], sl.core_id
74
+
75
+
76
+ def plain_zephyr_plan(
77
+ app_dir: str, board: str, defines: list[str], core_id: str | None = None
78
+ ) -> str:
79
+ """The build-plan JSON text for one Zephyr slice building `app_dir`.
80
+
81
+ `core_id` is the planner core id the board target maps to (`m55_he` for
82
+ `.../rtss_he`); without it the slice is named for the board qualifier."""
83
+ core = core_id or core_id_for(board)
84
+ slice_dir = f"build/{core}-zephyr"
85
+ out = f"{slice_dir}/build"
86
+ plan = {
87
+ "schemaVersion": 1,
88
+ "planPathMode": "tokened",
89
+ "generatedBy": PLAIN_GENERATED_BY,
90
+ "boardYaml": "",
91
+ "sku": "",
92
+ "buildRoot": "build",
93
+ "executionPolicy": {"unknownBackend": "fail", "missingTool": "skip", "nullCommand": "skip"},
94
+ "slices": [
95
+ {
96
+ "coreId": core,
97
+ "backend": "zephyr",
98
+ "buildDir": slice_dir,
99
+ "appDir": app_dir,
100
+ "configArtefacts": [],
101
+ # `toolchain` / `debug` below are required plan fields; no consumer
102
+ # of this synthesised plan reads their values.
103
+ "toolchain": {
104
+ "targetTriple": "arm-zephyr-eabi",
105
+ "compiler": "arm-zephyr-eabi-gcc",
106
+ "sysroot": None,
107
+ "id": "arm-zephyr-eabi",
108
+ },
109
+ "artifacts": {
110
+ "elf": f"{out}/zephyr/zephyr.elf",
111
+ "map": f"{out}/zephyr/zephyr.map",
112
+ "bin": f"{out}/zephyr/zephyr.bin",
113
+ "sizeReport": f"{out}/zephyr/zephyr.stat",
114
+ "symbols": f"{out}/zephyr/zephyr.symbols",
115
+ "compileCommands": f"{out}/compile_commands.json",
116
+ "outputDir": None,
117
+ },
118
+ "debug": {"console": "uart", "probe": None},
119
+ "command": {
120
+ "tool": "west",
121
+ "args": [
122
+ "build", "-b", board, app_dir, "--",
123
+ f"-DPython3_EXECUTABLE={TOKEN_PYTHON}", *defines,
124
+ ],
125
+ "cwd": slice_dir,
126
+ },
127
+ "postCommands": [],
128
+ "env": {"ALP_SDK_ROOT": TOKEN_SDK_ROOT},
129
+ "envAppendPath": {
130
+ "EXTRA_ZEPHYR_MODULES": [TOKEN_SDK_ROOT],
131
+ "PYTHONPATH": [f"{TOKEN_SDK_ROOT}/scripts"],
132
+ },
133
+ }
134
+ ],
135
+ "sharedArtefacts": [],
136
+ "warnings": [],
137
+ }
138
+ return json.dumps(plan, indent=2)