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/planner_emit.py ADDED
@@ -0,0 +1,617 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Render `alp_project.py`'s emit modes in-process, from `tan.planner`.
3
+
4
+ `tan.planner_root.emit` covers the eight modes alp-sdk's `alp_orchestrate` CLI
5
+ exposed. This module covers the OTHER front door: the modes whose renderer moved
6
+ into `tan/planner/` but whose `--emit` flag lived on `scripts/alp_project.py`.
7
+ `tan generate` is the only caller, and until it routed through here `tan` shelled
8
+ that script -- so nothing had actually moved off alp-sdk's Python, whatever the
9
+ relocation commit said.
10
+
11
+ **All twelve modes `tan generate` can reach now render here.** The last six --
12
+ `dts-overlay`, `native-sim-overlay`, `hw-info-h`, `west-libraries`,
13
+ `carrier-netlist` and `zephyr-board` -- relocated in the same MOVE-not-rewrite
14
+ shape as the first five, plus `composed-route-table` (the older debug view of
15
+ `carrier-netlist`'s own route rows), so `IN_PROCESS_MODES` is the whole
16
+ reachable set and `tan generate` spawns nothing:
17
+
18
+ zephyr-conf kconfig.py _slice_alp_conf
19
+ cmake-args kconfig.py _slice_cmake_args
20
+ yocto-conf kconfig.py _slice_local_conf
21
+ os-topology topology.py emit_os_topology
22
+ ipc-contract-h headers.py emit_ipc_contract_h
23
+ dts-overlay project_emit/dts.py _emit_dts_overlay
24
+ native-sim-overlay project_emit/native_sim.py _emit_native_sim_overlay
25
+ hw-info-h project_emit/hw_info.py _emit_hw_info_h
26
+ west-libraries project_emit/west_libs.py _emit_west_libraries
27
+ carrier-netlist project_emit/bom_netlist.py _emit_carrier_netlist
28
+ composed-route-table project_emit/bom_netlist.py _emit_composed_route_table
29
+ zephyr-board zephyr_board.py emit_zephyr_board
30
+
31
+ The mapping is not guessed: `metadata/emit-registry-v1.json` names each mode's
32
+ `owner.module`, and every module above is one of those owners, relocated.
33
+
34
+ **Byte-identity is the whole contract**, so the dispatch below mirrors
35
+ `_run_v2_per_core_emit` and `main()` in `scripts/alp_project.py` line for line --
36
+ including the details a rewrite loses:
37
+
38
+ * `os: off` cores are skipped before the OS-class check;
39
+ * `zephyr-conf --core <id>` emits the slice with NO `# --- core: ... ---` marker
40
+ while every other per-core combination emits one;
41
+ * the trailing newline is added only when the last part lacks it;
42
+ * `dts-overlay` and `west-libraries` union across ZEPHYR/BAREMETAL cores when
43
+ unscoped, while `hw-info-h` unions across ALL cores and picks a primary;
44
+ * `dts-overlay` needs the Zephyr/baremetal core-id LIST, not just the union, so
45
+ the AEN i3c wiring can ask whether `m55_he` is in scope at all;
46
+ * `native-sim-overlay` takes no `--core` scoping of any kind;
47
+ * `carrier-netlist` and `composed-route-table` never build a per-core slice
48
+ model -- both read the raw board.yaml dict, the SoM preset and the
49
+ inline-or-preset board, exactly as `main()` does before the per-core
50
+ machinery is ever reached.
51
+
52
+ **`write` uses `newline=""`, matching the SDK's own `_write_or_print`.** Windows
53
+ text mode would translate every `\\n` to `\\r\\n`; the SDK's emits are LF on every
54
+ host, and a CRLF `alp.conf` flips every emit-snapshot golden in alp-sdk.
55
+
56
+ **Which OS classes a per-core mode accepts is metadata, not a table here.**
57
+ `compatible.os` in the emit registry carries it (`zephyr-conf` -> zephyr,
58
+ `cmake-args` -> baremetal+zephyr, `yocto-conf` -> yocto) and alp-sdk's
59
+ `scripts/check_emit_registry.py` keeps it honest against the real code. Copying
60
+ it into `tan` would be a second source of truth for a fact tan must not learn
61
+ (ADR-0017 / I-26).
62
+
63
+ **`metadata/**` never relocated.** Every emitter here reads it out of the
64
+ RESOLVED `sdk_root` via `tan.planner.paths`, which is why `bind_sdk_root` runs
65
+ before the planner is imported at all.
66
+ """
67
+
68
+ from __future__ import annotations
69
+
70
+ import json
71
+ from pathlib import Path
72
+ from typing import Any
73
+
74
+ from tan.planner_root import bind_sdk_root
75
+
76
+ __all__ = [
77
+ "IN_PROCESS_MODES",
78
+ "TREE_MODES",
79
+ "PlannerEmitError",
80
+ "render",
81
+ "render_tree",
82
+ "unavailable",
83
+ "write",
84
+ "write_tree",
85
+ ]
86
+
87
+ #: Every mode `tan generate` can reach, all of them served here (see the module
88
+ #: docstring for how the registry decides the set).
89
+ IN_PROCESS_MODES = frozenset({
90
+ "zephyr-conf",
91
+ "cmake-args",
92
+ "yocto-conf",
93
+ "os-topology",
94
+ "ipc-contract-h",
95
+ "dts-overlay",
96
+ "native-sim-overlay",
97
+ "hw-info-h",
98
+ "west-libraries",
99
+ "carrier-netlist",
100
+ "composed-route-table",
101
+ "zephyr-board",
102
+ })
103
+
104
+ #: The modes that write a DIRECTORY of files rather than one stream, and so go
105
+ #: through `render_tree`/`write_tree` instead of `render`/`write`. One today:
106
+ #: `--emit zephyr-board` emits a whole Zephyr board tree named per SKU+core.
107
+ TREE_MODES = frozenset({"zephyr-board"})
108
+
109
+ #: The three per-core config slices this mode set names -- the membership test
110
+ #: `render()` uses to route into `_render_per_core` at all
111
+ #: (`if mode in _SLICE_RENDERER`). All three render through a `buildplan`
112
+ #: artefact helper instead (tan-cli#1216, `_CONFIG_ARTEFACT_HELPER`) -- see
113
+ #: `_render_per_core` -- so the values below only document the leaf renderer
114
+ #: that helper still calls; nothing resolves them by name.
115
+ _SLICE_RENDERER = {
116
+ "zephyr-conf": "_slice_alp_conf",
117
+ "yocto-conf": "_slice_local_conf",
118
+ "cmake-args": "_slice_cmake_args",
119
+ }
120
+
121
+ #: The modes whose bytes ARE a build-plan `configArtefacts[]` entry, and the
122
+ #: file name that entry carries (`metadata/schemas/build-plan-v1.schema.json`:
123
+ #: "byte-identical to what a consumer's own materialise step writes to
124
+ #: buildDir"). These render through the `buildplan` helper `emit_build_plan` also
125
+ #: calls for the same artefact (tan-cli#1216, ADR-0026 §D).
126
+ _CONFIG_ARTEFACT_FILE = {
127
+ "zephyr-conf": "alp.conf",
128
+ "yocto-conf": "local.conf",
129
+ "cmake-args": "cmake-args.txt",
130
+ }
131
+
132
+ #: The `buildplan` helper that returns each of those modes' `(file, contents)`.
133
+ #: `cmake-args.txt` is an ADDITIVE plan artefact (tan-cli#1216), so it has its
134
+ #: own helper; the other two are a slice's primary config artefact.
135
+ _CONFIG_ARTEFACT_HELPER = {
136
+ "zephyr-conf": "_slice_config_artefact",
137
+ "yocto-conf": "_slice_config_artefact",
138
+ "cmake-args": "_slice_cmake_args_artefact",
139
+ }
140
+
141
+ #: `carrier-netlist` / `composed-route-table` share one resolution shape (see
142
+ #: `_render_route_table`); this maps each to the emitter function that owns it.
143
+ _ROUTE_TABLE_EMITTER = {
144
+ "carrier-netlist": "_emit_carrier_netlist",
145
+ "composed-route-table": "_emit_composed_route_table",
146
+ }
147
+
148
+ #: The emit registry, relative to the SDK checkout root.
149
+ _REGISTRY = ("metadata", "emit-registry-v1.json")
150
+
151
+ #: The one file whose absence means "this is not a usable alp-sdk checkout".
152
+ #: The same probe `build_cmd._emit_plan` makes, deliberately: the planner is
153
+ #: tan's now, so requiring the SDK to still ship `alp_orchestrate` would refuse
154
+ #: precisely the releases this relocation exists to enable.
155
+ _SDK_PROBE = ("metadata", "schemas", "board.schema.json")
156
+
157
+
158
+ class PlannerEmitError(Exception):
159
+ """The planner refused this emit. Carries the message a user should read."""
160
+
161
+
162
+ def _joined(root: Path, parts: tuple[str, ...]) -> Path:
163
+ for part in parts:
164
+ root = root / part
165
+ return root
166
+
167
+
168
+ def _os_classes(sdk_root: Path) -> dict[str, tuple[str, ...]]:
169
+ """`{mode: (os_class, ...)}` from the emit registry, for the modes that
170
+ restrict one. `compatible.os == ["any"]` means no restriction, so it is
171
+ absent from the result -- mirroring `_EMIT_OS_CLASSES.get(mode) is None`.
172
+ """
173
+ try:
174
+ doc = json.loads(_joined(sdk_root, _REGISTRY).read_text(encoding="utf-8"))
175
+ modes = doc["modes"]
176
+ except (OSError, UnicodeDecodeError, ValueError, KeyError, TypeError) as err:
177
+ raise PlannerEmitError(
178
+ f"`{_joined(sdk_root, _REGISTRY)}` is missing or unreadable ({err}); "
179
+ "it declares which OS classes each per-core emit mode accepts."
180
+ ) from err
181
+ classes: dict[str, tuple[str, ...]] = {}
182
+ for entry in modes:
183
+ allowed = ((entry.get("compatible") or {}).get("os")) or []
184
+ if allowed and "any" not in allowed:
185
+ classes[entry.get("mode")] = tuple(allowed)
186
+ return classes
187
+
188
+
189
+ def unavailable(sdk_root: Path) -> str | None:
190
+ """Why the in-process path cannot serve `sdk_root`, or `None` when it can.
191
+
192
+ A REASON, never a bool: `tan generate` reports it, because a fallback nobody
193
+ can see is how a port gets believed before it happened.
194
+
195
+ Three ways it comes back non-`None`, and all three are a customer's layout
196
+ rather than a bug: the path is not a usable checkout, its emit registry is
197
+ unreadable, or this build of `tan` cannot import the planner at all (a
198
+ source checkout or frozen binary without `PyYAML`/`jsonschema` -- which
199
+ `tan.planner.loader` reports by calling `sys.exit`, not by raising).
200
+ """
201
+ probe = _joined(sdk_root, _SDK_PROBE)
202
+ if not probe.is_file():
203
+ return f"`{probe}` is absent, so `{sdk_root}` is not a usable alp-sdk checkout"
204
+ try:
205
+ _os_classes(sdk_root)
206
+ bind_sdk_root(sdk_root)
207
+ import tan.planner # noqa: F401,PLC0415 -- the availability probe IS the import
208
+ except SystemExit as err:
209
+ return f"the relocated planner exited on import (code {err.code})"
210
+ except PlannerEmitError as err:
211
+ return str(err)
212
+ except Exception as err: # noqa: BLE001 -- any import failure is a fallback, not a crash
213
+ return f"the relocated planner could not be imported: {type(err).__name__}: {err}"
214
+ return None
215
+
216
+
217
+ def render(
218
+ mode: str,
219
+ *,
220
+ sdk_root: Path,
221
+ board_yaml: Path,
222
+ core: str | None = None,
223
+ skip_advisories: list[str] | None = None,
224
+ ) -> str:
225
+ """One relocated emit mode as text. Raises on any refusal.
226
+
227
+ Every exception is the CALLER's to turn into an envelope: in a subprocess a
228
+ planner blow-up arrived as a non-zero exit code, and in-process it lands
229
+ here instead.
230
+
231
+ `TREE_MODES` are refused here rather than silently mis-served: they write a
232
+ directory, so a caller that asked for text would get a path it never wrote.
233
+
234
+ *skip_advisories* (tan-cli#964 review, major 6): optional, caller-owned;
235
+ threaded to `load_board_yaml`/`_render_route_table`'s own schema checks so
236
+ a caller that writes the rendered text to disk (`generate_cmd.py`) can
237
+ disclose "validated against nothing -- the schema itself is absent" rather
238
+ than silently writing an unvalidated artefact at `ok: true`.
239
+ """
240
+ if mode in TREE_MODES:
241
+ raise PlannerEmitError(
242
+ f"--emit {mode} writes a directory of files, not a stream; "
243
+ "render_tree() serves it.")
244
+
245
+ bind_sdk_root(sdk_root)
246
+ import tan.planner as planner # noqa: PLC0415 -- must follow the bind
247
+
248
+ if mode in _ROUTE_TABLE_EMITTER:
249
+ # NOT via `load_board_yaml`: `alp_project.main()` dispatches these two
250
+ # modes before the per-core slice machinery is reached, off the raw
251
+ # board.yaml dict alone. Routing them through the v2 loader would
252
+ # resolve a topology the emitter never looks at -- and could refuse a
253
+ # board the SDK's own front door serves.
254
+ return _render_route_table(mode, board_yaml, skip_advisories=skip_advisories)
255
+
256
+ project = planner.load_board_yaml(Path(board_yaml), skip_advisories=skip_advisories)
257
+ if mode in _SLICE_RENDERER:
258
+ return _render_per_core(planner, project, mode, core=core, sdk_root=sdk_root)
259
+ if mode == "os-topology":
260
+ # Not in `emit_artefact`: alp-sdk's `alp_orchestrate` CLI never exposed
261
+ # it, only `alp_project.py` did.
262
+ return planner.emit_os_topology(project)
263
+ if mode in _V1_SHAPED_MODES:
264
+ return _render_v1_shaped(project, mode, core=core)
265
+ # `ipc-contract-h` (and anything else relocated later that the planner CLI
266
+ # already knows) goes through the ONE dispatch, so a mode cannot render
267
+ # differently depending on which front door reached it.
268
+ from tan.planner.cli import emit_artefact # noqa: PLC0415
269
+
270
+ return emit_artefact(project, mode, board_yaml=Path(board_yaml), core=core)
271
+
272
+
273
+ def render_tree(
274
+ mode: str,
275
+ *,
276
+ sdk_root: Path,
277
+ board_yaml: Path,
278
+ core: str | None = None,
279
+ ) -> dict[str, str]:
280
+ """`{filename: content}` for a mode that writes a DIRECTORY.
281
+
282
+ Keys are the FILE names, with the generator's own `<board_dir>/` prefix
283
+ already stripped -- exactly the split `alp_project.py` does before joining
284
+ each name onto `--output`, because `--output` names the board directory
285
+ itself (`docs/porting-new-som.md`'s `--output build/boards/<board>/`, then
286
+ `west build --board-root build/boards`).
287
+ """
288
+ if mode not in TREE_MODES:
289
+ raise PlannerEmitError(
290
+ f"--emit {mode} writes a single stream, not a directory; "
291
+ "render() serves it.")
292
+
293
+ bind_sdk_root(sdk_root)
294
+ import tan.planner as planner # noqa: PLC0415 -- must follow the bind
295
+
296
+ if core is None:
297
+ # `alp_project.py` refuses the same way: one board tree is one core's.
298
+ raise PlannerEmitError(
299
+ f"--emit {mode} requires --core (it generates one core's board tree)")
300
+
301
+ project = planner.load_board_yaml(Path(board_yaml))
302
+ if core not in project.cores:
303
+ raise PlannerEmitError(
304
+ f"--core {core} not present in board.yaml "
305
+ f"(known: {sorted(project.cores.keys())})")
306
+
307
+ from tan.planner.paths import METADATA_ROOT # noqa: PLC0415
308
+ from tan.planner.zephyr_board import emit_zephyr_board # noqa: PLC0415
309
+
310
+ files = emit_zephyr_board(project.sku, core, METADATA_ROOT)
311
+ return {relpath.split("/", 1)[1]: content for relpath, content in files.items()}
312
+
313
+
314
+ def _render_route_table(
315
+ mode: str, board_yaml: Path, *, skip_advisories: list[str] | None = None
316
+ ) -> str:
317
+ """`--emit carrier-netlist` / `--emit composed-route-table`, mirroring
318
+ `alp_project.main()`'s own branch (the two share one dispatch there too)."""
319
+ from tan.core.metadata_schema import ( # noqa: PLC0415
320
+ missing_schema_note,
321
+ som_preset_schema_path,
322
+ validate_document,
323
+ )
324
+ from tan.planner.loader import _load_yaml # noqa: PLC0415
325
+ from tan.planner.paths import METADATA_ROOT # noqa: PLC0415
326
+ from tan.planner.project_loader import ( # noqa: PLC0415
327
+ _resolve_inline_or_preset_board,
328
+ _resolve_sku,
329
+ )
330
+
331
+ project = _load_yaml(Path(board_yaml))
332
+ # PHRASED, not a bare KeyError. alp-sdk reached this point only after its
333
+ # rich diagnostic validator had run; here the schema check happens in
334
+ # `load_board_yaml`, which this mode deliberately does not call, so the one
335
+ # field the emitter dereferences is checked by hand.
336
+ sku = (project.get("som") or {}).get("sku")
337
+ if not isinstance(sku, str) or not sku:
338
+ raise PlannerEmitError(
339
+ f"{board_yaml}: `som.sku` is missing or is not a string; "
340
+ f"--emit {mode} renders the SoM's pad routes against it.")
341
+ sku_preset = _resolve_sku(sku, METADATA_ROOT)
342
+ # tan-cli#964 review (major 4): both route-table modes write a real file
343
+ # to disk (`generate_cmd.py`'s `build/generated/{carrier,composed-route
344
+ # -table}-netlist.json`) WITHOUT going through `load_board_yaml` -- the one
345
+ # place #964's REFUSE gate otherwise lives -- so the gate is repeated here,
346
+ # against the raw preset `_resolve_sku` just returned, before it reaches
347
+ # either emitter. Same wording `tan.planner.loader._refuse_on_schema_errors`
348
+ # uses for `build`/`generate`'s other modes.
349
+ som_preset_path = METADATA_ROOT / "e1m_modules" / f"{sku}.yaml"
350
+ # major 6: "skip-but-disclose", checked BEFORE the refuse-worthy
351
+ # violations, same ordering `_refuse_on_schema_errors` uses -- an absent
352
+ # schema means `validate_document` below returns `[]` and this function
353
+ # would otherwise write the file having said nothing at all.
354
+ if skip_advisories is not None:
355
+ note = missing_schema_note(som_preset_schema_path(METADATA_ROOT), source=som_preset_path)
356
+ if note is not None:
357
+ skip_advisories.append(note)
358
+ som_schema_errors = validate_document(
359
+ sku_preset, som_preset_schema_path(METADATA_ROOT), som_preset_path,
360
+ )
361
+ if som_schema_errors:
362
+ raise PlannerEmitError(
363
+ f"SoM preset {sku} does not validate against som-preset-v2:\n"
364
+ + "\n".join(f" - {e}" for e in som_schema_errors))
365
+ board_preset = _resolve_inline_or_preset_board(project, METADATA_ROOT)
366
+ from tan.planner.project_emit import bom_netlist # noqa: PLC0415
367
+
368
+ emitter = getattr(bom_netlist, _ROUTE_TABLE_EMITTER[mode])
369
+ return emitter(project, sku_preset, board_preset, METADATA_ROOT)
370
+
371
+
372
+ #: The four modes `alp_project.py` re-fitted onto the v2 schema by projecting the
373
+ #: resolved project back into the v1 `board:`-wrapper dict its emitters read.
374
+ #: That projection is `_run_v2_per_core_emit`'s `project_v1_shaped`, reproduced
375
+ #: verbatim in `_v1_shaped_project` -- these emitters take a plain dict, not a
376
+ #: `BoardProject`, and changing that would be the rewrite this move is not.
377
+ _V1_SHAPED_MODES = frozenset({
378
+ "dts-overlay", "native-sim-overlay", "hw-info-h", "west-libraries",
379
+ })
380
+
381
+
382
+ def _v1_shaped_project(project) -> dict[str, Any]:
383
+ """The legacy `board:`-wrapper dict the four relocated emitters consume --
384
+ the one `buildplan._v1_shaped_project` (alp-sdk's `alp_project.py` shares
385
+ it with the plan emit, #2771)."""
386
+ from tan.planner.buildplan import _v1_shaped_project as shaped # noqa: PLC0415
387
+
388
+ return shaped(project)
389
+
390
+
391
+ def _render_dts_overlay(project, shaped, core: str | None) -> str:
392
+ """`--emit dts-overlay`, mirroring `alp_project._run_v2_per_core_emit`:
393
+ the board overlay, then the nodes board.yaml `ownership:` hands the M33."""
394
+ from tan.planner.models import OrchestratorError # noqa: PLC0415
395
+ from tan.planner.ownership import project_m33_overlay # noqa: PLC0415
396
+ from tan.planner.project_emit.dts import _emit_dts_overlay # noqa: PLC0415
397
+
398
+ # The DTS overlay is shaped by the board header (bus aliases +
399
+ # alp,pin-array) which is a SoM-mounting fact, not a per-core fact.
400
+ # v2 contributes only the peripherals list: union across
401
+ # Zephyr/baremetal cores (or one core when --core is set).
402
+ if core is not None:
403
+ # The slice's own overlay IS the build plan's `alp.overlay`
404
+ # configArtefact (tan-cli#1216, ADR-0026 §D): one function, so
405
+ # `tan generate --core` and `tan build`'s plan cannot disagree. It
406
+ # also appends the M33-ownership nodes, so this path returns early.
407
+ from tan.planner.buildplan import _slice_dts_overlay # noqa: PLC0415
408
+
409
+ try:
410
+ return _slice_dts_overlay(project, project.cores[core])
411
+ except OrchestratorError as err:
412
+ raise PlannerEmitError(str(err)) from err
413
+ else:
414
+ union: set[str] = set()
415
+ zephyr_core_ids: list[str] = []
416
+ for core_id, slice_ in project.cores.items():
417
+ if slice_.os in ("zephyr", "baremetal"):
418
+ union.update(slice_.peripherals)
419
+ zephyr_core_ids.append(core_id)
420
+ out = _emit_dts_overlay(
421
+ shaped, project.som_preset, project.board_preset,
422
+ v2_peripherals=sorted(union),
423
+ v2_core_ids=zephyr_core_ids,
424
+ )
425
+ # Per-product core ownership: enable the assignable nodes this project
426
+ # assigned to the M33 (the board tree carries them disabled).
427
+ try:
428
+ own_dts, _ = project_m33_overlay(project, core)
429
+ except OrchestratorError as err:
430
+ raise PlannerEmitError(str(err)) from err
431
+ if own_dts:
432
+ out += ("\n/* Assignable peripherals owned by the M33 "
433
+ "(board.yaml `ownership:`). */\n" + "\n".join(own_dts) + "\n")
434
+ return out
435
+
436
+
437
+ def _render_v1_shaped(project, mode: str, *, core: str | None) -> str:
438
+ """`dts-overlay` / `native-sim-overlay` / `hw-info-h` / `west-libraries`,
439
+ mirroring `alp_project._run_v2_per_core_emit`'s project-wide section.
440
+
441
+ The `--core` validity check runs first for all four, as it does there: an
442
+ unknown core id is a refusal, never a `KeyError` from inside an emitter.
443
+ """
444
+ if core is not None and core not in project.cores:
445
+ raise PlannerEmitError(
446
+ f"--core {core} not present in board.yaml "
447
+ f"(known: {sorted(project.cores.keys())})")
448
+
449
+ from tan.planner.buildplan import ( # noqa: PLC0415
450
+ _slice_hw_info_h,
451
+ _slice_west_libraries,
452
+ )
453
+ from tan.planner.project_emit.hw_info import _emit_hw_info_h # noqa: PLC0415
454
+ from tan.planner.project_emit.native_sim import ( # noqa: PLC0415
455
+ _emit_native_sim_overlay,
456
+ )
457
+ from tan.planner.project_emit.west_libs import ( # noqa: PLC0415
458
+ _emit_west_libraries,
459
+ )
460
+
461
+ shaped = _v1_shaped_project(project)
462
+
463
+ if mode == "dts-overlay":
464
+ return _render_dts_overlay(project, shaped, core)
465
+
466
+ if mode == "native-sim-overlay":
467
+ # native_sim GPIO emulation -- board-agnostic (the E1M pad map is a
468
+ # SoM-mounting fact), so no --core / peripheral scoping applies.
469
+ return _emit_native_sim_overlay(shaped)
470
+
471
+ if mode == "hw-info-h":
472
+ # hw-info-h is a project-level emit even under v2 -- consumers
473
+ # `#include` it from any slice. --core picks which slice's OS lands in
474
+ # ALP_HW_BUILD_OS; absent --core, primary-core rules apply.
475
+ if core is not None:
476
+ # Single source shared with the build plan's
477
+ # `alp_hw_info_build.h` configArtefact (ADR-0026 §D).
478
+ return _slice_hw_info_h(project, project.cores[core])
479
+ return _emit_hw_info_h(
480
+ shaped, project.som_preset, project.board_preset,
481
+ v2_cores={cid: s.os for cid, s in project.cores.items()},
482
+ v2_selected_core=None,
483
+ metadata_root=project.effective_metadata_root(),
484
+ )
485
+
486
+ # west-libraries
487
+ if core is not None:
488
+ # Single source shared with the build plan's `alp-west-libs.yml`
489
+ # configArtefact (ADR-0026 §D).
490
+ return _slice_west_libraries(project, project.cores[core])
491
+ else:
492
+ union_l: set[str] = set()
493
+ for slice_ in project.cores.values():
494
+ if slice_.os in ("zephyr", "baremetal"):
495
+ union_l.update(slice_.libraries)
496
+ v2_libraries = sorted(union_l)
497
+ return _emit_west_libraries(
498
+ shaped, project.som_preset, project.board_preset,
499
+ v2_libraries=v2_libraries,
500
+ v2_project_libraries=sorted(project.libraries),
501
+ metadata_root=project.effective_metadata_root(),
502
+ )
503
+
504
+
505
+ def _render_per_core(planner, project, mode: str, *, core: str | None,
506
+ sdk_root: Path) -> str:
507
+ """`zephyr-conf` / `cmake-args` / `yocto-conf`, mirroring
508
+ `alp_project._run_v2_per_core_emit`'s per-core section exactly.
509
+
510
+ `zephyr-conf` and `yocto-conf` render through `buildplan._slice_config_artefact` -- the
511
+ SAME helper `emit_build_plan` calls to fill a slice's
512
+ `configArtefacts[].contents` (tan-cli#1216, ADR-0026 §D) -- rather than a
513
+ second, independent dispatch straight to the leaf renderer. The bytes are
514
+ unchanged (`_slice_config_artefact`'s zephyr branch IS `_slice_alp_conf`, its yocto
515
+ branch `_slice_local_conf`);
516
+ what changes is that `tan generate --target zephyr-conf` and `tan build`'s
517
+ plan are now structurally pinned to the one call site the schema's own
518
+ words describe ("byte-identical to what a consumer's own materialise step
519
+ writes to buildDir") instead of merely agreeing today by coincidence of
520
+ two dispatch tables that happen to name the same function.
521
+ """
522
+ if core is not None and core not in project.cores:
523
+ raise PlannerEmitError(
524
+ f"--core {core} not present in board.yaml "
525
+ f"(known: {sorted(project.cores.keys())})"
526
+ )
527
+ if project.libraries:
528
+ # Resolved up front purely to VALIDATE: an unknown library name or a
529
+ # failed `requires:` constraint must be one clean line (ADR 0018), not a
530
+ # traceback from the middle of a slice render.
531
+ from tan.planner.libraries import resolve_selection # noqa: PLC0415
532
+
533
+ resolve_selection(project, project.effective_metadata_root())
534
+
535
+ allowed_os = _os_classes(sdk_root).get(mode)
536
+ # Already imported (`tan.planner.__init__` imports `.buildplan` at
537
+ # module scope) by the time `render()` reaches here -- this is a
538
+ # `sys.modules` cache hit, not a fresh load, and it must stay lazy
539
+ # like every other `tan.planner*` import in this file: importing it
540
+ # before `bind_sdk_root` has run would raise.
541
+ from tan.planner import buildplan # noqa: PLC0415
542
+
543
+ wanted = _CONFIG_ARTEFACT_FILE[mode]
544
+ artefact_of = getattr(buildplan, _CONFIG_ARTEFACT_HELPER[mode])
545
+
546
+ from tan.planner.models import OrchestratorError # noqa: PLC0415
547
+
548
+ def slice_renderer(proj, sl):
549
+ try:
550
+ artefact = artefact_of(proj, sl)
551
+ except OrchestratorError as err:
552
+ if mode != "cmake-args":
553
+ raise
554
+ # alp-sdk ed57bd025 (#2791): `alp_project._run_v2_per_core_emit`
555
+ # turns a cmake-args OrchestratorError -- e.g. a `cameras:` entry
556
+ # unbuildable for this core -- into one clean refusal line.
557
+ raise PlannerEmitError(str(err)) from err
558
+ if artefact is None or artefact[0] != wanted:
559
+ # Unreachable in practice: `allowed_os` above already confines
560
+ # each mode to the one OS whose artefact IS `wanted`. A coded
561
+ # refusal beats a bare `NoneType` subscript -- or silently
562
+ # emitting a different file's bytes -- if that invariant breaks.
563
+ raise PlannerEmitError(
564
+ f"no {wanted} config artefact for core `{sl.core_id}` "
565
+ f"(os: {sl.os})")
566
+ return artefact[1]
567
+
568
+ core_ids = [core] if core is not None else sorted(project.cores.keys())
569
+
570
+ parts: list[str] = []
571
+ for core_id in core_ids:
572
+ slice_ = project.cores[core_id]
573
+ if slice_.os == "off":
574
+ continue
575
+ if allowed_os is not None and slice_.os not in allowed_os:
576
+ if core is None:
577
+ # The unscoped sum-across-cores form is explicitly a "give me
578
+ # everything applicable" query, so it skips rather than refuses.
579
+ continue
580
+ raise PlannerEmitError(
581
+ f"--core {core_id} has os: {slice_.os}, which --emit {mode} does "
582
+ f"not support (supported os: {', '.join(allowed_os)})"
583
+ )
584
+ if mode == "zephyr-conf" and core is not None:
585
+ # The canonical per-core path emits the slice VERBATIM -- no section
586
+ # marker -- so a `west build` driven by alp-sdk's CMakeLists.txt and
587
+ # one driven by `tan` off the build plan cannot diverge on Kconfig.
588
+ parts.append(slice_renderer(project, slice_))
589
+ else:
590
+ parts.append(f"# --- core: {core_id} ({slice_.os}) ---")
591
+ parts.append(slice_renderer(project, slice_))
592
+ return "\n".join(parts) + ("\n" if parts and not parts[-1].endswith("\n") else "")
593
+
594
+
595
+ def write(text: str, destination: Path) -> None:
596
+ """Write `text` to `destination`, byte for byte, creating its parent.
597
+
598
+ `newline=""` is load-bearing -- see the module docstring. The SDK's
599
+ `_write_or_print` writes exactly this way, and the whole point of the
600
+ in-process path is that the bytes on disk do not change.
601
+ """
602
+ destination.parent.mkdir(parents=True, exist_ok=True)
603
+ destination.write_text(text, encoding="utf-8", newline="")
604
+
605
+
606
+ def write_tree(files: dict[str, str], destination: Path) -> None:
607
+ """Write a `render_tree` result into `destination`, which IS the board
608
+ directory. Same `newline=""` contract as `write`, per file.
609
+
610
+ Sorted, so the order files appear on disk (and in any log that follows the
611
+ writes) does not depend on dict iteration order across interpreters.
612
+ """
613
+ destination.mkdir(parents=True, exist_ok=True)
614
+ for name in sorted(files):
615
+ target = destination / name
616
+ target.parent.mkdir(parents=True, exist_ok=True)
617
+ target.write_text(files[name], encoding="utf-8", newline="")