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,874 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Slice-command resolution -- the planner-side helpers `buildplan.py`'s
4
+ `emit_build_plan` reads to describe (never run) each slice's build command.
5
+
6
+ ADR-0020 Phase 4 (preview) retired the SDK-side executor -- the
7
+ `Orchestrator` class that fanned build sub-processes out and materialised
8
+ artefacts to disk. What remains here is pure, side-effect-free: resolving
9
+ what a slice's build command WOULD be, so `emit_build_plan` can describe it
10
+ to an external consumer (`tan`, alplabai/tan-cli, / alp-studio) that owns
11
+ execution.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Any, Optional
18
+
19
+ import yaml
20
+
21
+ from tan.core.link_refusal import RAM_RUN_ONLY_METHOD
22
+ from .link_target import (
23
+ CONF_NAME as ITCM_CONF_NAME,
24
+ OVERLAY_NAME as ITCM_OVERLAY_NAME,
25
+ UNSUPPORTED_CODE,
26
+ LinkTargetError,
27
+ applies_to as link_applies_to,
28
+ )
29
+ from . import cameras as _cameras
30
+ from .models import BoardProject, OrchestratorError, Slice
31
+ from .paths import REPO
32
+ from .secure import (emit_sysbuild_conf, emit_tfm_sysbuild_conf,
33
+ sysbuild_family_base_conf)
34
+
35
+
36
+ def iter_buildable_slices(project: BoardProject):
37
+ """Yield every non-`off` core's `Slice`, sorted by `core_id`.
38
+
39
+ ADR-0020 Phase 1: the SINGLE source of WHICH cores build and in WHAT
40
+ ORDER, so `emit_build_plan()` (the plan `tan` reads) always
41
+ enumerates slices the same way.
42
+ """
43
+ for core_id in sorted(project.cores):
44
+ slice_ = project.cores[core_id]
45
+ if slice_.os == "off":
46
+ continue
47
+ yield slice_
48
+
49
+
50
+ def _slice_flash_recipe(
51
+ slice_: Slice,
52
+ ) -> tuple[Optional[str], Optional[dict[str, Any]]]:
53
+ """Per-runtime default flash backend + args for a slice.
54
+
55
+ Used by `Slice.to_manifest_entry` to record how an external flash
56
+ step should program the slice's output artefact.
57
+
58
+ Returns ``(None, None)`` for `os: off` slices (skipped at flash
59
+ time) and unknown `os:` values; the manifest emitter drops keys
60
+ with None values, so off slices stay tidy.
61
+ """
62
+ if slice_.os == "yocto":
63
+ return ("yocto_wic_to_sd_or_emmc",
64
+ {"target": slice_.machine or ""})
65
+ if slice_.os == "zephyr" and slice_.link_target == "itcm":
66
+ # `diagnostics.link: itcm` (tan-cli#1350): this image is linked at 0x0
67
+ # for a J-Link RAM-run. The MRAM recipe below would sign it and write
68
+ # it to slot0 -- a broken, unbootable image -- so the slice carries NO
69
+ # MRAM flash recipe: `flash_method` is the unregistered
70
+ # `ram_run_only` (plain `tan flash` refuses it and points at
71
+ # `tan flash --ram`), and `flash_args` keeps ONLY the wrong-board
72
+ # identity pair `--ram`'s probe guard reads. No `jlink_flash_device`
73
+ # / `slot0_load_address`, so Flow D is never offered.
74
+ ram_args: dict[str, Any] = {}
75
+ if slice_.expect_dpidr and slice_.jlink_device:
76
+ ram_args["expect_dpidr"] = slice_.expect_dpidr
77
+ ram_args["jlink_device"] = slice_.jlink_device
78
+ return (RAM_RUN_ONLY_METHOD, ram_args)
79
+ if slice_.os == "zephyr":
80
+ # No runner is forced here: not every in-tree board registers
81
+ # an openocd runner (e.g. AEN's board.cmake sets
82
+ # flash-runner: alif_flash), so `west flash --runner openocd`
83
+ # FATAL-errors on those boards. Emit no runner and let `west
84
+ # flash` fall back to the board.cmake default; an explicit
85
+ # runner can still be set on flash_args downstream when one is
86
+ # actually known.
87
+ #
88
+ # `jlink_flash_device` is added only when this slice's SoC
89
+ # variant actually publishes one: it's the part-number J-Link
90
+ # profile that arms Flow D's built-in Alif MRAM loader, a fact a
91
+ # downstream consumer (tan) needs to pick that path over the
92
+ # SETOOLS/SE-UART fallback. Absent for every non-AEN slice today,
93
+ # so the args dict stays `{}` -- no shape change for them.
94
+ #
95
+ # `expect_dpidr` + `jlink_device` are the read-only SW-DP IDR
96
+ # wrong-board preflight (alp-sdk #1355) and are emitted as ONE
97
+ # inseparable pair: the expected debug-port ID, and the live-core
98
+ # attach profile the read is performed with. Emitting exactly one of
99
+ # the two is NOT a partial win: tan's own
100
+ # `validate_flow_d_preflight_args` refuses a half-armed pair at plan
101
+ # time, so it would hard-fail every AEN flash including a dry run.
102
+ # `loader._resolve_flow_d_preflight` guarantees the both-or-neither
103
+ # shape upstream; the `and` here makes that guarantee locally
104
+ # readable rather than assumed at a distance.
105
+ #
106
+ # `slot0_load_address` (tan-cli#353) is the AEN MRAM slot0-XIP
107
+ # address the slice's application blob is linked at -- the fact
108
+ # Flow D's auto-sign-via-SETOOLS path needs. Independent of the
109
+ # `expect_dpidr`/`jlink_device` pair above -- see
110
+ # `loader._resolve_slot0_load_address` for where it comes from.
111
+ args: dict[str, Any] = {}
112
+ # tan-cli#734: PRESENCE, not truthiness. A schema-declared
113
+ # `jlink_flash_device: null` means "this variant has no known J-Link
114
+ # flash profile -- refuse loudly", and a truthiness test dropped it,
115
+ # so `flash_plan.flow_d_available` -- which decides on key presence
116
+ # via `_fa_has_key`, deliberately -- never saw it and `tan flash`
117
+ # silently downgraded Flow D to Flow A over the SE-UART. On Windows
118
+ # that is a downgrade to a runner that cannot run there at all.
119
+ if slice_.jlink_flash_device_declared or slice_.jlink_flash_device is not None:
120
+ args["jlink_flash_device"] = slice_.jlink_flash_device
121
+ if slice_.expect_dpidr and slice_.jlink_device:
122
+ args["expect_dpidr"] = slice_.expect_dpidr
123
+ args["jlink_device"] = slice_.jlink_device
124
+ if slice_.slot0_load_address:
125
+ args["slot0_load_address"] = slice_.slot0_load_address
126
+ return ("zephyr_west_flash", args)
127
+ if slice_.os == "baremetal":
128
+ return ("baremetal_cmake_flash", {})
129
+ return (None, None)
130
+
131
+
132
+ # M-core "stock shim" app token (Zephyr side). Accepted by the SoM-preset
133
+ # schema and defaulted into M-core slots (AEN m55_hp/he, V2N m33_sm).
134
+ # The token resolves to the SDK-owned app below rather than a
135
+ # project-local path.
136
+ STOCK_SHIM_APP = "alp-stock-shim"
137
+ STOCK_SHIM_DIR = REPO / "firmware" / "alp-stock-shim"
138
+
139
+ # A-core "stock image" app token (Yocto side). Every shipped SoM preset's
140
+ # topology.<a-core-id> defaults `app:` to this value (see
141
+ # metadata/e1m_modules/*.yaml) -- unlike a customer's own `app:` (a
142
+ # filesystem path to their app source), this token IS already the real
143
+ # bitbake recipe name for the stock alp-image-edge image, so it is exempt
144
+ # from the `recipe:` requirement `_slice_command` enforces for a
145
+ # project-supplied app-only Yocto slice (issue #597). That exemption
146
+ # assumes `bitbake <STOCK_IMAGE_APP>` is actually a buildable target for
147
+ # the slice's `machine:` -- see YOCTO_MACHINE_UNBUILDABLE below for the
148
+ # machines where that assumption is currently false (issue #1982).
149
+ STOCK_IMAGE_APP = "alp-image-edge"
150
+
151
+ # Yocto MACHINEs that cannot build today, keyed to the issue(s) that
152
+ # establish why -- consulted by `_slice_command` so the planner refuses
153
+ # these rather than hand a consumer a `bitbake` command guaranteed to
154
+ # fail. This is the SAME dict `scripts/check_yocto_machine_tree_parity.py`
155
+ # consults (issue #1982 follow-up) -- do not fork a second list. Be
156
+ # precise about what that gate does and does not buy you: it is a
157
+ # ONE-WAY absence check. It fails the PR only for a `machine:` that
158
+ # has NO conf under `meta-alp-sdk/conf/machine/` AND no entry here, so
159
+ # a new SKU cannot fall through both unnoticed. It does NOT fire when
160
+ # a conf ships for a MACHINE listed here, and it does NOT fire when a
161
+ # listed MACHINE's conf disappears -- a `.conf` merely existing is not
162
+ # proof the MACHINE builds (`e1m-aen801-a32.conf` and
163
+ # `e1m-aen701-a32.conf` both exist and are both still unbuildable).
164
+ # So this dict is NOT self-maintaining: removing an entry once its
165
+ # MACHINE genuinely builds is a human call, and nothing in CI will
166
+ # remind you. Re-read the per-entry reasons below before trusting
167
+ # them; the gate's own docstring draws the same line.
168
+ #
169
+ # Five AEN A32-cluster carriers declare a `topology.a32_cluster.machine:`
170
+ # today (`metadata/e1m_modules/E1M-AEN{501,601,701,801,803}.yaml`); all
171
+ # five are unbuildable, split into two distinct failure classes -- do
172
+ # not conflate them:
173
+ #
174
+ # * `e1m-aen801-a32.conf` has an ACTIVE, uncommented `require
175
+ # conf/machine/devkit-e8.conf` -- and that file exists in NEITHER
176
+ # branch of the public meta-alif-ensemble upstream (issue #1968), so
177
+ # this MACHINE fails at BitBake's own parse step.
178
+ # * `e1m-aen701-a32.conf`'s `require conf/machine/devkit-e7.conf` is
179
+ # already commented out in-tree (its own header: "Until the layer is
180
+ # vendored this require has no target ... intentional") -- unlike
181
+ # AEN801, `devkit-e7.conf` DOES exist on meta-alif-ensemble's
182
+ # `devkit-ex-b0` branch, but nothing here references it yet, so this
183
+ # MACHINE parses with no DEFAULTTUNE / kernel provider / TF-A
184
+ # platform set at all.
185
+ # * `e1m-aen501-a32` / `e1m-aen601-a32` / `e1m-aen803-a32` ship NO
186
+ # `meta-alp-sdk/conf/machine/*.conf` at all -- strictly MORE
187
+ # unbuildable than the two above, since BitBake fails to find the
188
+ # MACHINE before it can parse a single `require`. E1M-AEN803 is the
189
+ # SoM issue #1982 names as the bench module.
190
+ #
191
+ # All five are unbuildable regardless of the above: meta-alif-ensemble
192
+ # declares `LAYERSERIES_COMPAT = "warrior zeus"` and is structurally
193
+ # incompatible with this repo's Scarthgap baseline (issue #1971:
194
+ # pre-honister override syntax, a stale 5.4 kernel pin, obsolete TF-A
195
+ # build knobs), so even a corrected/uncommented `require` (or a shipped
196
+ # conf, for the three missing ones) would not make any of them buildable
197
+ # on its own. Issue #264 is rebuilding this path on a real base; remove
198
+ # an entry here only once its MACHINE resolves against a
199
+ # Scarthgap-compatible layer.
200
+ YOCTO_MACHINE_UNBUILDABLE: dict[str, str] = {
201
+ "e1m-aen801-a32": (
202
+ "MACHINE 'e1m-aen801-a32' cannot build: its base `require "
203
+ "conf/machine/devkit-e8.conf` names a file that exists in no "
204
+ "branch of the public meta-alif-ensemble upstream (issue #1968), "
205
+ "and that upstream layer's LAYERSERIES_COMPAT (\"warrior zeus\") "
206
+ "is incompatible with this repo's Scarthgap baseline regardless "
207
+ "(issue #1971). Tracked by issue #264."
208
+ ),
209
+ "e1m-aen701-a32": (
210
+ "MACHINE 'e1m-aen701-a32' cannot build: its base `require "
211
+ "conf/machine/devkit-e7.conf` is commented out pending "
212
+ "meta-alif-ensemble being vendored (no DEFAULTTUNE / kernel "
213
+ "provider / TF-A platform is set), and even once wired up that "
214
+ "upstream layer's LAYERSERIES_COMPAT (\"warrior zeus\") is "
215
+ "incompatible with this repo's Scarthgap baseline regardless "
216
+ "(issue #1971). Tracked by issue #264."
217
+ ),
218
+ "e1m-aen501-a32": (
219
+ "MACHINE 'e1m-aen501-a32' cannot build: meta-alp-sdk/conf/machine/ "
220
+ "ships no conf for it at all, so BitBake fails before any `require` "
221
+ "is even parsed -- strictly more unbuildable than 'e1m-aen801-a32' / "
222
+ "'e1m-aen701-a32' above, and, like them, on a meta-alif-ensemble "
223
+ "base that is Yocto-series-incompatible with this repo's Scarthgap "
224
+ "baseline regardless (issue #1971). Tracked by issue #264."
225
+ ),
226
+ "e1m-aen601-a32": (
227
+ "MACHINE 'e1m-aen601-a32' cannot build: meta-alp-sdk/conf/machine/ "
228
+ "ships no conf for it at all, so BitBake fails before any `require` "
229
+ "is even parsed -- strictly more unbuildable than 'e1m-aen801-a32' / "
230
+ "'e1m-aen701-a32' above, and, like them, on a meta-alif-ensemble "
231
+ "base that is Yocto-series-incompatible with this repo's Scarthgap "
232
+ "baseline regardless (issue #1971). Tracked by issue #264."
233
+ ),
234
+ "e1m-aen803-a32": (
235
+ "MACHINE 'e1m-aen803-a32' cannot build: meta-alp-sdk/conf/machine/ "
236
+ "ships no conf for it at all, so BitBake fails before any `require` "
237
+ "is even parsed -- strictly more unbuildable than 'e1m-aen801-a32' / "
238
+ "'e1m-aen701-a32' above, and, like them, on a meta-alif-ensemble "
239
+ "base that is Yocto-series-incompatible with this repo's Scarthgap "
240
+ "baseline regardless (issue #1971). E1M-AEN803 is the bench module "
241
+ "issue #1982 names. Tracked by issue #264."
242
+ ),
243
+ }
244
+
245
+
246
+ class UnbuildableYoctoMachineError(ValueError):
247
+ """Raised by `_slice_command` when a yocto slice's `machine:` is a
248
+ known-non-buildable MACHINE (`YOCTO_MACHINE_UNBUILDABLE`, issue
249
+ #1982): the planner refuses to emit `bitbake` for it rather than
250
+ hand a consumer a command that cannot succeed -- see
251
+ `YOCTO_MACHINE_UNBUILDABLE`'s own comment for why each listed
252
+ MACHINE fails (not always the same proximate failure mode).
253
+ Carries the machine name + reason so the caller can render both,
254
+ same as `UnknownBoardTargetError` below.
255
+ """
256
+
257
+ def __init__(self, core_id: str, machine: str, reason: str) -> None:
258
+ self.core_id = core_id
259
+ self.machine = machine
260
+ self.reason = reason
261
+ super().__init__(f"core '{core_id}': {reason}")
262
+
263
+
264
+ class UnrootedPathError(ValueError):
265
+ """Raised by `_tokenize` when a path resolves under neither
266
+ `${PROJECT_ROOT}` nor `${SDK_ROOT}` (issue #865's split-brain guard).
267
+
268
+ A single exception type raised from the ONE place paths get tokenized
269
+ means every call site -- the five command-arg sites in `_slice_command`
270
+ plus `buildplan.py`'s `appDir` -- is guarded uniformly by construction;
271
+ nothing can add a sixth call site and forget the check. Callers catch
272
+ this and turn it into a `warnings` entry (never a hard crash of the
273
+ whole emit), matching the existing `no-command`/`yocto-recipe-missing`
274
+ "block the one broken slice, keep going" convention.
275
+ """
276
+
277
+
278
+ def _real_zephyr_board_names(repo: Path) -> set[str]:
279
+ """Every Zephyr board name that actually has a tree under
280
+ `zephyr/boards/alp/`, read from each tree's own `board.yml`
281
+ `board: name:` field -- existence, not naming shape, is what makes a
282
+ target buildable. Mirrors `check_board_target_tree_parity.py`'s
283
+ `_load_real_board_names` (issue #999's own list of what's real),
284
+ duplicated here rather than imported: that script is a CI gate over
285
+ declared SoM presets, this is a runtime planner check over one
286
+ resolved slice, and a ~10-line directory glob is cheaper than
287
+ cross-importing a top-level gate script into the package.
288
+ """
289
+ boards_dir = repo / "zephyr" / "boards" / "alp"
290
+ names: set[str] = set()
291
+ if not boards_dir.is_dir():
292
+ return names
293
+ for board_yml in sorted(boards_dir.glob("*/board.yml")):
294
+ doc = yaml.safe_load(board_yml.read_text(encoding="utf-8")) or {}
295
+ name = (doc.get("board") or {}).get("name")
296
+ if isinstance(name, str):
297
+ names.add(name)
298
+ return names
299
+
300
+
301
+ class UnknownBoardTargetError(ValueError):
302
+ """Raised by `_slice_command` when a zephyr slice's `board:` target
303
+ (resolved from the SoM preset's `topology.<core>.board:`) names no
304
+ tree under `zephyr/boards/alp/` (issue #999 one layer down: the
305
+ *declaration* gate `check_board_target_tree_parity.py` already
306
+ flags this at the metadata level; this is the same fact checked at
307
+ *emit* time, so the planner never hands a consumer -- `tan`, `west`
308
+ itself -- a `west build -b <board>` command that is guaranteed to
309
+ fail with Zephyr's own "No board named ... Invalid BOARD" error).
310
+
311
+ Carries the SKU/core/board/what-exists facts so the caller can
312
+ render them in the customer's own terms, same as `UnrootedPathError`
313
+ above.
314
+ """
315
+
316
+ def __init__(self, sku: str, core_id: str, board: str,
317
+ real_boards: set[str]) -> None:
318
+ self.sku = sku
319
+ self.core_id = core_id
320
+ self.board = board
321
+ self.real_boards = real_boards
322
+ super().__init__(
323
+ f"SoM '{sku}' core '{core_id}' wants Zephyr board '{board}', "
324
+ f"which has no tree under zephyr/boards/alp/ -- board bring-up "
325
+ f"for this target has not happened yet."
326
+ )
327
+
328
+
329
+ def _tokenize(path: Path, base_dir: Path, repo: Path) -> str:
330
+ """Render an absolute path as a portable `${PROJECT_ROOT}`/`${SDK_ROOT}`
331
+ token (issue #865) instead of baking in THIS checkout's absolute path.
332
+ tan-cli (PR #24) substitutes both tokens at materialise time, so a plan
333
+ emitted on one machine/checkout can be materialised faithfully on
334
+ another -- the split-brain risk a baked-in absolute path invites.
335
+
336
+ Prefers `${PROJECT_ROOT}` (the project's own board.yaml directory) since
337
+ most anchored paths are project-owned; falls back to `${SDK_ROOT}` for
338
+ paths the SDK itself owns (e.g. the family sysbuild base, the stock
339
+ M-core shim app). Raises `UnrootedPathError` for a path under neither
340
+ root -- never returns a bare absolute path, so a non-hermetic path can
341
+ never silently reach a `planPathMode: tokened` plan.
342
+ """
343
+ path = Path(path)
344
+ for root, token in ((base_dir, "${PROJECT_ROOT}"), (repo, "${SDK_ROOT}")):
345
+ try:
346
+ rel = path.relative_to(root)
347
+ except ValueError:
348
+ continue
349
+ return token if rel == Path(".") else f"{token}/{rel.as_posix()}"
350
+ raise UnrootedPathError(path.as_posix())
351
+
352
+
353
+ def _slice_command(
354
+ project: BoardProject,
355
+ slice_: Slice,
356
+ base_dir: Path,
357
+ ) -> Optional[list[str]]:
358
+ """Resolve the build command for a slice. Returns None when there is no
359
+ buildable command yet -- the caller carries the slice as `skipped` /
360
+ `no-command`, never dropped. Raises `UnrootedPathError` /
361
+ `UnknownBoardTargetError` / `UnbuildableYoctoMachineError` for a slice
362
+ the plan must block rather than mis-emit.
363
+
364
+ `base_dir` anchors every relative `app:` path -- the directory holding
365
+ the project's `board.yaml` (or an equivalent explicit root), NEVER the
366
+ caller's process CWD. A relative `app:` means "relative to the project
367
+ file that named it", so the same board.yaml must resolve identically no
368
+ matter where the emitting process happens to be invoked from
369
+ (issue #596).
370
+ """
371
+ # `cameras:` unbuildable for this core (none/ambiguous owner, missing
372
+ # shield or overlay): block the command, whatever the OS.
373
+ _cameras.check(project, slice_)
374
+ if slice_.os == "zephyr":
375
+ if not slice_.app or not slice_.board:
376
+ return None
377
+ real_boards = _real_zephyr_board_names(REPO)
378
+ # `board:` may carry Zephyr's extended `board/soc/variant`
379
+ # qualifier (e.g. AEN801's `alp_e1m_aen801_m55_hp/ae822fa0e5597ls0/
380
+ # rtss_hp`) -- a tree's board.yml `name:` is always the bare board
381
+ # only, so check existence on the bare form (same `bare =
382
+ # raw.split()[0].split("/")[0]` normalisation
383
+ # check_board_target_tree_parity.py uses); the full qualified
384
+ # string still goes to `west build -b` unchanged below.
385
+ bare_board = slice_.board.split()[0].split("/")[0]
386
+ if bare_board not in real_boards:
387
+ raise UnknownBoardTargetError(
388
+ project.sku, slice_.core_id, slice_.board, real_boards)
389
+ # NB: no explicit `-d`. west's default output is <cwd>/build (a
390
+ # subdirectory of the command's cwd = buildDir), so the tree lands
391
+ # at <buildDir>/build/. Adding `-d <buildDir>` here would
392
+ # double-nest (west resolves a relative -d against its cwd,
393
+ # already = buildDir) -- see finding M14.
394
+ #
395
+ # The consumer does NOT have to reconcile that nesting any more:
396
+ # `_slice_artifacts` reports the `<buildDir>/build/...` paths west
397
+ # actually writes (issue #1360). `-d .` was the alternative -- it
398
+ # would move west's tree to <buildDir> itself and make the old
399
+ # un-nested `artifacts` spelling true -- and was REJECTED: this
400
+ # slice's `alp.conf` is materialised at <buildDir>/alp.conf and
401
+ # handed to this very command via `-DEXTRA_CONF_FILE=`, so making
402
+ # <buildDir> west's own build dir puts that file inside the tree
403
+ # `west build -p` (or a `--pristine=auto` board/app change) wipes,
404
+ # deleting the fragment the command line points at. Today it sits
405
+ # one level ABOVE west's tree, where pristine cannot reach it.
406
+ # Tokenized (issue #865): `_zephyr_app_dir` resolves an absolute
407
+ # path (project-anchored for a customer app, SDK-anchored for the
408
+ # stock M-core shim) -- see `_tokenize`.
409
+ cmd = [
410
+ "west", "build",
411
+ "-b", slice_.board,
412
+ _tokenize(_zephyr_app_dir(slice_.app, base_dir), base_dir, REPO),
413
+ ]
414
+ # ADR 0014 Phase-3 conf->build: wire the generated sysbuild
415
+ # overlays into the build command itself. `_shared_artefacts`
416
+ # emits the top-level overlay at build_root/alp_sysbuild.conf and
417
+ # the TF-M child overlay at build_root/sysbuild/tfm/tfm.conf.
418
+ # Pass --sysbuild whenever a sysbuild child image is configured (a
419
+ # `boot:` or `security.psa:` block), and point sysbuild at the
420
+ # top-level overlay only when it is non-empty (the TF-M overlay is
421
+ # picked up by sysbuild convention from its sysbuild/tfm/ path).
422
+ # Absent both, the stock per-family sysbuild defaults apply.
423
+ #
424
+ # Zephyr normally derives Python3_EXECUTABLE from the interpreter
425
+ # that launched west (WEST_PYTHON). A pre-existing CMake cache can
426
+ # already define Python3_EXECUTABLE, however, which prevents that
427
+ # hand-off and can select a host Python without the west package.
428
+ # The orchestrator itself runs under the intended workspace Python,
429
+ # so pin it as an explicit CMake cache override (issue #787).
430
+ # ${PYTHON} token, not `sys.executable` (issue #865): this plan is
431
+ # emitted once by whichever Python ran the planner, but may be
432
+ # materialised on a different host/checkout than that -- baking in
433
+ # a concrete interpreter path here would pin THIS run's Python, not
434
+ # the consumer's. tan-cli (PR #24) substitutes ${PYTHON} with its
435
+ # own resolved interpreter (forward-slashed for the same CMake
436
+ # backslash-escape reason issue #787/#849 fixed: CMake parses
437
+ # `\U`/`\N` etc. in a Windows path as an invalid character escape)
438
+ # at materialise time; the SDK never emits a bare fallback here.
439
+ defines = ["-DPython3_EXECUTABLE=${PYTHON}"]
440
+ is_sysbuild = emit_sysbuild_conf(project) or emit_tfm_sysbuild_conf(project)
441
+ if is_sysbuild:
442
+ cmd.append("--sysbuild")
443
+ if emit_sysbuild_conf(project):
444
+ # SB_CONF_FILE is the only supported way to name a
445
+ # non-default top-level sysbuild overlay: `west build` is a
446
+ # ZEPHYR extension command, and no Zephyr has ever had a
447
+ # `--sysbuild-config` flag -- west forwarded the unknown
448
+ # argument to CMake, which failed the configure step with
449
+ # `Unknown argument --sysbuild-config` (issue #805).
450
+ #
451
+ # The path must be ABSOLUTE. sysbuild resolves a relative
452
+ # SB_CONF_FILE against APP_DIR, not the command's cwd
453
+ # (share/sysbuild/cmake/modules/sysbuild_kconfig.cmake),
454
+ # so the cwd-relative form this used to emit would silently
455
+ # look for the overlay under the application's source dir.
456
+ #
457
+ # Anchor on `base_dir` (the board.yaml directory), never the
458
+ # emitting process's CWD: `slice_.build_dir`'s parent is the
459
+ # build root, which may itself be a relative path, and
460
+ # resolving a relative path bare falls back to
461
+ # `Path.cwd()` -- the same #596 CWD-dependence bug class
462
+ # `_resolve_app_path` guards against for `app:`.
463
+ build_root_dir = Path(slice_.build_dir).parent
464
+ if not build_root_dir.is_absolute():
465
+ build_root_dir = base_dir / build_root_dir
466
+ sb_conf = (build_root_dir.resolve() / "alp_sysbuild.conf")
467
+ # LAYER, don't replace: SB_CONF_FILE accepts a `;`-joined
468
+ # list, and sysbuild merges every listed file in order
469
+ # (later files win on a repeated symbol). When the SoM
470
+ # family ships a curated zephyr/sysbuild/<family>/
471
+ # sysbuild.conf, put it FIRST so the customer's `boot:`
472
+ # overlay lands as deltas on top of the curated base
473
+ # instead of forking family boot policy into two
474
+ # divergent places (issue #807).
475
+ family_base = sysbuild_family_base_conf(project)
476
+ # Forward slashes: CMake's `cmake_path()` (which
477
+ # sysbuild_kconfig.cmake uses to split the `;`-joined
478
+ # SB_CONF_FILE list) only recognises `/` -- a native
479
+ # Windows backslash path here dies the configure step
480
+ # with "File ... not found", the same class of bug
481
+ # #849 fixed for -DPython3_EXECUTABLE. `_tokenize` (#865)
482
+ # already emits posix-form tokens, so this holds for both:
483
+ # `sb_conf` is project-anchored (`${PROJECT_ROOT}/...`),
484
+ # `family_base` is SDK-anchored (`${SDK_ROOT}/...`) -- the
485
+ # `;`-joined list correctly carries both roots.
486
+ sb_conf_files = (
487
+ [_tokenize(family_base, base_dir, REPO),
488
+ _tokenize(sb_conf, base_dir, REPO)]
489
+ if family_base else [_tokenize(sb_conf, base_dir, REPO)]
490
+ )
491
+ defines.append(f"-DSB_CONF_FILE={';'.join(sb_conf_files)}")
492
+ # Wire the slice's materialised per-core Kconfig fragment
493
+ # (`_slice_config_artefact` -> build_dir/alp.conf, carried in the
494
+ # plan's `configArtefacts`) into the build command via
495
+ # EXTRA_CONF_FILE -- Zephyr's supported extra-fragment merge point
496
+ # (layered on prj.conf). The path is ABSOLUTE and anchored on
497
+ # `base_dir` (issue #596), never Path.cwd(), so the plan is
498
+ # byte-identical wherever it is emitted.
499
+ #
500
+ # A --sysbuild build scopes per-image variables as
501
+ # -D<image>_VAR: a bare -DEXTRA_CONF_FILE would land on the
502
+ # SYSBUILD image, not the application, silently dropping the
503
+ # per-core alp.conf on boot:/OTA projects. The application image's
504
+ # name IS derivable: sysbuild names it after the basename of the
505
+ # app directory (`get_filename_component(app_name ${APP_DIR}
506
+ # NAME)` in share/sysbuild/CMakeLists.txt), i.e. the directory
507
+ # `west build` is handed above -- so emit the image-prefixed form
508
+ # (#866, the plan-native replacement for the per-example
509
+ # CMakeLists.txt bridge #870).
510
+ #
511
+ # CAVEAT: the prefix is the app directory's real basename, but the
512
+ # command's app dir is a `${PROJECT_ROOT}` token. When the app dir
513
+ # IS the project root (`app: ./src` falls back to the example
514
+ # root), the image name is the project root's directory name, so a
515
+ # tokened plan materialised under a differently-named root names a
516
+ # stale image and Zephyr silently ignores the arg. A consumer that
517
+ # relocates the project root must re-derive the prefix from the
518
+ # substituted app dir (documented in docs/heterogeneous-builds.md).
519
+ alp_conf = Path(slice_.build_dir) / "alp.conf"
520
+ if not alp_conf.is_absolute():
521
+ alp_conf = Path(base_dir) / alp_conf
522
+ alp_conf = alp_conf.resolve()
523
+ extra_var = "EXTRA_CONF_FILE"
524
+ if is_sysbuild:
525
+ image = _zephyr_app_dir(slice_.app, base_dir).name
526
+ extra_var = f"{image}_EXTRA_CONF_FILE"
527
+ conf_files = [_tokenize(alp_conf, base_dir, REPO)]
528
+ if link_applies_to(project.diagnostics, slice_):
529
+ # `diagnostics.link: itcm` (tan-cli#1350): layer the Flow C ITCM
530
+ # retarget AFTER alp.conf (a later fragment wins) and hand Zephyr
531
+ # the devicetree half. Both are `_slice_config_artefact`
532
+ # siblings in the same build dir, materialised from the plan.
533
+ if is_sysbuild:
534
+ raise LinkTargetError(
535
+ UNSUPPORTED_CODE,
536
+ "diagnostics.link: itcm cannot be combined with a "
537
+ "sysbuild project (`boot:` / `ota:` / TF-M).")
538
+ itcm_conf_path = alp_conf.with_name(ITCM_CONF_NAME)
539
+ itcm_overlay_path = alp_conf.with_name(ITCM_OVERLAY_NAME)
540
+ conf_files.append(_tokenize(itcm_conf_path, base_dir, REPO))
541
+ defines.append(
542
+ "-DEXTRA_DTC_OVERLAY_FILE="
543
+ f"{_tokenize(itcm_overlay_path, base_dir, REPO)}")
544
+ defines.append(f"-D{extra_var}={';'.join(conf_files)}")
545
+ # `cameras:` -> ONE -DSHIELD (carrier + module shields), shared with
546
+ # the cmake-args listing via cameras.zephyr_shield_define.
547
+ # Sysbuild: `-D<image>_SHIELD` so MCUboot does not get the shields.
548
+ shield = _cameras.shield_define_for_build(project, slice_, base_dir)
549
+ if shield:
550
+ defines.append(f"-D{shield}")
551
+ cmd += ["--", *defines]
552
+ return cmd
553
+ if slice_.os == "yocto":
554
+ # Refuse a known-non-buildable MACHINE before considering
555
+ # image/app/recipe at all (issue #1982): none of those fields
556
+ # matter if `bitbake`'s own MACHINE parse cannot succeed.
557
+ if slice_.machine in YOCTO_MACHINE_UNBUILDABLE:
558
+ raise UnbuildableYoctoMachineError(
559
+ slice_.core_id, slice_.machine,
560
+ YOCTO_MACHINE_UNBUILDABLE[slice_.machine])
561
+ # `image:` always names a real recipe (e.g. `alp-image-edge`) --
562
+ # safe to hand straight to bitbake. `app:` is a filesystem path to
563
+ # the app's source directory (mirrors the zephyr/baremetal `app:`
564
+ # convention), NOT a recipe name -- `bitbake <path>` is never a
565
+ # valid target (issue #597), so an app-only slice needs an explicit
566
+ # `recipe:` naming the bitbake recipe that packages that source.
567
+ if slice_.image:
568
+ return ["bitbake", str(slice_.image)]
569
+ if slice_.app == STOCK_IMAGE_APP:
570
+ return ["bitbake", slice_.app]
571
+ if slice_.app:
572
+ if not slice_.recipe:
573
+ return None
574
+ return ["bitbake", str(slice_.recipe)]
575
+ return None
576
+ if slice_.os == "baremetal":
577
+ if not slice_.app:
578
+ return None
579
+ # `-S` tokenized (issue #865) same as the zephyr app-dir arg above.
580
+ app_path = _tokenize(_resolve_app_path(slice_.app, base_dir),
581
+ base_dir, REPO)
582
+ # `-B .`, NOT `-B <slice_.build_dir>`: the plan pins this command's
583
+ # `cwd` to the slice's buildDir (schema: "always equal to this
584
+ # slice's buildDir"), and cmake resolves a relative `-B` against
585
+ # its own cwd -- so the historical `-B build/<core>-baremetal`
586
+ # double-nested the tree at `<buildDir>/build/<core>-baremetal/`,
587
+ # where nothing that reads `artifacts`/`buildDir` would ever find
588
+ # it (tan-cli#550). The zephyr branch avoids that SECOND level by
589
+ # emitting no `-d` at all (finding M14, above); the one level west
590
+ # still adds on its own is reported honestly by `_slice_artifacts`
591
+ # rather than left for the reader to add back (issue #1360).
592
+ return [
593
+ "cmake", "-S", app_path, "-B", ".",
594
+ # Every `-D` below is set BY THIS PLANNER, not by the customer,
595
+ # and an app is free to consume none of them -- CMake would then
596
+ # end each configure with `CMake Warning: Manually-specified
597
+ # variables were not used by the project: ALP_SOM_FAMILY,
598
+ # ALP_TOOLCHAIN, ...`, a warning about the planner's own
599
+ # behaviour that the customer can neither act on nor silence.
600
+ "--no-warn-unused-cli",
601
+ # A best-effort IDE convenience, NOT a promise: CMake implements
602
+ # `CMAKE_EXPORT_COMPILE_COMMANDS` "only by Makefile Generators
603
+ # and Ninja Generators. It is ignored on other generators"
604
+ # (CMake's own docs for the variable). This planner does not
605
+ # choose the generator, so `artifacts.compileCommands` stays
606
+ # null -- see `buildplan._slice_artifacts`.
607
+ "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
608
+ # Pins WHERE the slice's linked executables land
609
+ # (`artifacts.outputDir`). The app's own CMakeLists.txt picks
610
+ # the target NAMES -- not an SDK convention this planner may
611
+ # invent -- so the directory is the strongest honest claim
612
+ # available.
613
+ #
614
+ # ABSOLUTE (tokened, #865) on purpose: CMake resolves a RELATIVE
615
+ # CMAKE_RUNTIME_OUTPUT_DIRECTORY against each target's own
616
+ # CMAKE_CURRENT_BINARY_DIR, so a relative value would scatter a
617
+ # multi-subdirectory app's outputs instead of pinning them.
618
+ #
619
+ # Wrapped in the no-op generator expression `$<1:...>` on
620
+ # purpose too: "Multi-configuration generators (Visual Studio,
621
+ # Xcode, Ninja Multi-Config) append a per-configuration
622
+ # subdirectory to the specified directory UNLESS A GENERATOR
623
+ # EXPRESSION IS USED" (CMake's own RUNTIME_OUTPUT_DIRECTORY
624
+ # docs). Without it the plan says `<buildDir>/output` while the
625
+ # binary is at `<buildDir>/output/Debug/` on every
626
+ # multi-config generator -- measured on Ninja Multi-Config, and
627
+ # the default generator on Windows (Visual Studio) is one.
628
+ f"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=$<1:"
629
+ f"{_tokenize(_baremetal_output_dir(slice_, base_dir), base_dir, REPO)}>",
630
+ # tan-cli#551: the slice's `-DALP_SOM_SKU` / `-DALP_SOM_FAMILY`
631
+ # / `-DALP_CORE_ID` / `-DALP_TOOLCHAIN` / NPU-dispatch cache
632
+ # entries. They used to be rendered ONLY into a `cmake-args.txt`
633
+ # nothing ever read, then dropped outright with that artefact
634
+ # (#1278) -- the configure has never carried a single one of
635
+ # them. The bare `#if defined(...)` guards from the same source
636
+ # (`ALP_BOARD_<slug>`, `ALP_SOM_<SKU>`) CANNOT ride here (cmake
637
+ # rejects a `-D` with no `=value`); they arrive as real compiler
638
+ # definitions via -DCMAKE_PROJECT_INCLUDE below.
639
+ *_baremetal_cache_args(project, slice_),
640
+ *_baremetal_project_include_arg(project, slice_, base_dir),
641
+ ]
642
+ return None
643
+
644
+
645
+ def _baremetal_output_dir(slice_: Slice, base_dir: Path) -> Path:
646
+ """Absolute directory a baremetal slice's linked executables land in.
647
+
648
+ Anchored on `base_dir` (the board.yaml's own directory), never
649
+ `Path.cwd()`, so the emitted plan is byte-identical wherever it is
650
+ emitted from -- the same rule `_resolve_app_path` and the zephyr
651
+ branch's `-DEXTRA_CONF_FILE` follow (issue #596).
652
+ """
653
+ out = Path(slice_.build_dir)
654
+ if not out.is_absolute():
655
+ out = Path(base_dir) / out
656
+ return (out / "output").resolve()
657
+
658
+
659
+ #: Filename of the generated CMake include a baremetal slice's configure
660
+ #: pulls in via `-DCMAKE_PROJECT_INCLUDE`. Shared by `_slice_command`
661
+ #: (which references it) and `buildplan._slice_config_artefact` (which
662
+ #: renders its contents) so the two cannot name different files.
663
+ BAREMETAL_PROJECT_INCLUDE = "alp-baremetal.cmake"
664
+
665
+
666
+ def _baremetal_alp_lines(project: BoardProject, slice_: Slice) -> list[str]:
667
+ """The slice's `-D` lines, read from the SINGLE source that renders
668
+ them -- `kconfig.py::_slice_cmake_args`, the same text
669
+ `alp_project.py --emit cmake-args` prints -- so the plan's configure
670
+ and that emit can never drift.
671
+
672
+ That emit is a TEXT format, not an argv: it opens with a
673
+ `# Auto-generated ...` banner, and `libraries.baremetal_cmake_args`
674
+ contributes `# library <name>: <cmake hint>` lines that are prose FOR
675
+ A HUMAN (no `-D` in them at all). Only real flags survive here --
676
+ handing cmake a `#`-prefixed line would make it a stray source-dir
677
+ argument, not a define.
678
+ """
679
+ # Lazy: `kconfig` is a heavier sibling, and `buildplan` already imports
680
+ # this module lazily for the same cycle-avoidance reason.
681
+ from .kconfig import _slice_cmake_args
682
+
683
+ return [line for line in _slice_cmake_args(project, slice_).splitlines()
684
+ if line.startswith("-D")]
685
+
686
+
687
+ def _baremetal_cache_args(
688
+ project: BoardProject,
689
+ slice_: Slice,
690
+ ) -> list[str]:
691
+ """The `-DNAME=VALUE` subset of the slice's `-D` lines -- CMake CACHE
692
+ entries, safe to pass on the configure command line verbatim.
693
+
694
+ Split from the bare `-DNAME` guards on the one thing that actually
695
+ distinguishes them: `cmake -D` REQUIRES `VAR[:type]=value` and exits
696
+ 1 with `Parse error in command line argument: ALP_BOARD_E1M_EVK /
697
+ Should be: VAR:type=value` on anything else (measured, not assumed).
698
+ `docs/board-config-emit.md` documents that split from the other side.
699
+ """
700
+ return [line for line in _baremetal_alp_lines(project, slice_)
701
+ if "=" in line]
702
+
703
+
704
+ def _baremetal_compile_guards(
705
+ project: BoardProject,
706
+ slice_: Slice,
707
+ ) -> list[str]:
708
+ """The bare `-DNAME` subset of the slice's `-D` lines, `-D` stripped.
709
+
710
+ These are compile-time `#if defined(...)` guards, NOT cache entries:
711
+ `ALP_BOARD_<SLUG>` gates `include/alp/board.h`'s board facade and
712
+ `ALP_SOM_<SKU>` gates the per-SKU override block in the generated
713
+ `<alp/soc_caps.h>`. A CMake cache variable of the same name is
714
+ invisible to the preprocessor, so they only do their job as real
715
+ compiler definitions -- which is what `_baremetal_project_include`
716
+ turns them into. Zephyr slices get the identical guards through
717
+ `CONFIG_COMPILER_OPT="-D..."` in their alp.conf.
718
+ """
719
+ return [line[len("-D"):]
720
+ for line in _baremetal_alp_lines(project, slice_)
721
+ if "=" not in line]
722
+
723
+
724
+ def _baremetal_project_include(
725
+ project: BoardProject,
726
+ slice_: Slice,
727
+ ) -> Optional[str]:
728
+ """Contents of the slice's generated `alp-baremetal.cmake`, or None
729
+ when the slice needs no compile-time guards.
730
+
731
+ Wired into the configure as `-DCMAKE_PROJECT_INCLUDE=<abs path>`,
732
+ which CMake includes at the end of every `project()` call (CMake
733
+ 3.15+, comfortably below the repo-wide `cmake_minimum_required(VERSION
734
+ 3.20)`). Deliberately NOT `-DCMAKE_C_FLAGS=-DALP_BOARD_...`: setting
735
+ that variable from the command line seeds the cache entry itself, so a
736
+ firmware toolchain file's `CMAKE_C_FLAGS_INIT` (`-mcpu=cortex-m55
737
+ -mfloat-abi=hard`, ...) would never be applied -- silently building the
738
+ slice for the wrong core. `add_compile_definitions` only ADDS, and
739
+ cannot drop a flag.
740
+ """
741
+ guards = _baremetal_compile_guards(project, slice_)
742
+ if not guards:
743
+ return None
744
+ lines = [
745
+ "# Auto-generated by scripts/alp_orchestrate -- do not edit.",
746
+ "# Pulled in by the slice's configure via -DCMAKE_PROJECT_INCLUDE.",
747
+ "# Compile-time guards <alp/board.h> / <alp/soc_caps.h> test with",
748
+ "# #if defined(...); a CMake cache variable would not reach the",
749
+ "# preprocessor at all.",
750
+ f"add_compile_definitions({' '.join(guards)})",
751
+ ]
752
+ return "\n".join(lines) + "\n"
753
+
754
+
755
+ def _baremetal_project_include_arg(
756
+ project: BoardProject,
757
+ slice_: Slice,
758
+ base_dir: Path,
759
+ ) -> list[str]:
760
+ """`['-DCMAKE_PROJECT_INCLUDE=<abs tokened path>']`, or `[]` when the
761
+ slice has no guards to carry (absence-emits-nothing: no dangling
762
+ reference to a file `_slice_config_artefact` would not have written).
763
+
764
+ ABSOLUTE and `base_dir`-anchored, never cwd-relative: CMake resolves a
765
+ relative `CMAKE_PROJECT_INCLUDE` against the SOURCE dir of the
766
+ `project()` that pulls it in, which is the app's tree, not the slice's
767
+ build dir. Same anchoring rule as the zephyr branch's
768
+ `-DEXTRA_CONF_FILE` (issue #596) and tokened the same way (#865).
769
+ """
770
+ if _baremetal_project_include(project, slice_) is None:
771
+ return []
772
+ inc = Path(slice_.build_dir)
773
+ if not inc.is_absolute():
774
+ inc = Path(base_dir) / inc
775
+ inc = (inc / BAREMETAL_PROJECT_INCLUDE).resolve()
776
+ return [f"-DCMAKE_PROJECT_INCLUDE={_tokenize(inc, base_dir, REPO)}"]
777
+
778
+
779
+ def _slice_post_commands(slice_: Slice) -> list[list[str]]:
780
+ """Argv steps that MUST run, in order, AFTER the slice's `command`.
781
+
782
+ `west build` (zephyr) and `bitbake` (yocto) each configure AND build in
783
+ one invocation, so they need none. `cmake -S ... -B ...` only
784
+ CONFIGURES: without the `cmake --build` step below, a baremetal slice
785
+ exits 0 having produced a `CMakeCache.txt` and no object file, no
786
+ archive and no executable -- a green build over an empty output
787
+ directory (tan-cli#550).
788
+
789
+ `.` (not the build dir path) for the same cwd-relative reason
790
+ `_slice_command`'s baremetal `-B .` uses; the caller pairs every step
791
+ with the slice's buildDir as its `cwd`.
792
+
793
+ No `--parallel`: job-count is the consumer's scheduling policy, the
794
+ same reason the plan carries no `sequential` key.
795
+ """
796
+ if slice_.os == "baremetal":
797
+ return [["cmake", "--build", "."]]
798
+ return []
799
+
800
+
801
+ def _resolve_app_path(app: str, base_dir: Path) -> Path:
802
+ """Resolve `./linux` or absolute paths from a slice.app.
803
+
804
+ Relative paths resolve against `base_dir` (the project's board.yaml
805
+ directory) -- never the process's current working directory, so the
806
+ result is identical regardless of the caller's CWD (issue #596).
807
+
808
+ `cores.<id>.app:` is a raw caller-supplied filesystem path -- like
809
+ `--input` and `extra_libraries[].profile:` (#1961) -- and unlike
810
+ `preset:`/`som.sku`, board.schema.json types it a bare `"string"`
811
+ with no pattern, so it reaches this `.resolve()` unsanitised. A
812
+ symlink loop (ELOOP) surfaces differently per platform: on POSIX,
813
+ `Path.resolve()` detects the cycle itself and raises `RuntimeError`
814
+ before returning; on Windows it returns without raising at all (the
815
+ `.is_file()` calls in `_zephyr_app_dir` below are where the Windows
816
+ crash actually surfaces for a zephyr slice) -- confirmed against a
817
+ real symlink loop on both platforms, not a mock. Same `except`
818
+ shape as `validate.py`'s `profile:` guard and `loader.py`'s
819
+ `_load_yaml` (#1961/#1987): raise a clean `OrchestratorError`
820
+ instead of letting the raw `RuntimeError`/`OSError` crash the CLI.
821
+ """
822
+ if app == STOCK_SHIM_APP:
823
+ return STOCK_SHIM_DIR
824
+ p = Path(app)
825
+ if p.is_absolute():
826
+ return p
827
+ try:
828
+ return (Path(base_dir) / p).resolve()
829
+ except (OSError, RuntimeError) as e:
830
+ raise OrchestratorError(
831
+ f"app: '{app}' could not be resolved: {e}") from e
832
+
833
+
834
+ def _zephyr_app_dir(app: str, base_dir: Path) -> Path:
835
+ """Resolve a Zephyr slice's `app:` to the directory holding the
836
+ application `CMakeLists.txt` (what `west build` needs).
837
+
838
+ `base_dir` anchors relative paths -- see `_resolve_app_path`.
839
+
840
+ Two example conventions are supported:
841
+
842
+ * multicore examples point `app:` straight at a self-contained
843
+ Zephyr app directory (e.g. ``./m33_sm`` -- carries its own
844
+ CMakeLists.txt + prj.conf); used verbatim.
845
+ * single-core examples keep one CMakeLists.txt at the example
846
+ root and point `app:` at the sources subdir (e.g. ``./src`` with
847
+ ``target_sources(app PRIVATE src/main.c)``). The sources dir has
848
+ no CMakeLists.txt of its own, so fall back to its parent (the
849
+ example root) which does.
850
+
851
+ Same unguarded-path defect class as `_resolve_app_path` above
852
+ (#1961/#1987): a symlink-loop `app:` reaches `.resolve()` clean on
853
+ Windows (see there), then crashes HERE instead -- `Path.is_file()`
854
+ raising a plain `OSError` (`WinError 1920`, "The file cannot be
855
+ accessed by the system") -- confirmed by driving a real WSL-made
856
+ symlink loop through the real CLI on Windows CPython 3.11.3, not a
857
+ mock. Both `.is_file()` calls are wrapped the same way.
858
+ """
859
+ p = _resolve_app_path(app, base_dir)
860
+ try:
861
+ has_cmakelists = (p / "CMakeLists.txt").is_file()
862
+ except (OSError, RuntimeError) as e:
863
+ raise OrchestratorError(
864
+ f"app: '{app}' could not be resolved: {e}") from e
865
+ if has_cmakelists:
866
+ return p
867
+ try:
868
+ parent_has_cmakelists = (p.parent / "CMakeLists.txt").is_file()
869
+ except (OSError, RuntimeError) as e:
870
+ raise OrchestratorError(
871
+ f"app: '{app}' could not be resolved: {e}") from e
872
+ if parent_has_cmakelists:
873
+ return p.parent
874
+ return p