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,59 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The `${PYTHON}` a `tan build` bakes into its slices (tan-cli#1317).
3
+
4
+ Split out of `build_cmd._build`. The resolver itself is `tan.core.host_python`,
5
+ shared with `tan doctor`'s `hostPython`; the FLOOR is doctor's own EFFECTIVE
6
+ floor (`tan.core.python_floor.effective_python_floor`), never a second constant.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from tan.core import host_python
15
+ from tan.core.plan_tokens import TOKEN_PYTHON
16
+ from tan.core.python_floor import effective_python_floor, read_manifest_floors
17
+ from tan.core.sdk_discovery import _planner_python_resolution
18
+ from tan.core.venv import west_workspace_dir
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class BuildPython:
23
+ python: str
24
+ #: Why no interpreter qualified (`None` when one did / none was needed).
25
+ refusal: str | None
26
+ #: Cores whose pre-substitution command/env uses `${PYTHON}`.
27
+ users: frozenset[str]
28
+
29
+
30
+ def _effective_floor(build_root: str, sdk_root: str | None) -> tuple[int, int]:
31
+ """Doctor's EFFECTIVE floor, via the shared pure `tan.core.python_floor`
32
+ (same inputs `tan doctor` feeds it: the resolved workspace's `zephyr/`,
33
+ else `$ZEPHYR_BASE`; the SDK manifest's two floors)."""
34
+ ws = west_workspace_dir(build_root, Path(sdk_root) if sdk_root else None)
35
+ zephyr_base = str(ws / "zephyr") if ws is not None else os.environ.get("ZEPHYR_BASE")
36
+ manifest_floor, zephyr_manifest_floor = read_manifest_floors(sdk_root)
37
+ return effective_python_floor(manifest_floor, zephyr_base, zephyr_manifest_floor)[0]
38
+
39
+
40
+ def resolve_build_python(plan, plan_text: str, build_root: str, sdk_root: str | None) -> BuildPython:
41
+ fallback, used_venv = _planner_python_resolution(build_root, sdk_root)
42
+ users = frozenset(
43
+ sl.core_id
44
+ for sl in plan.slices
45
+ if TOKEN_PYTHON in f"{sl.command!r}{sl.env!r}{sl.env_append_path!r}"
46
+ )
47
+ needs = TOKEN_PYTHON in plan_text and not used_venv
48
+ floor = _effective_floor(build_root, sdk_root) if needs else (0, 0)
49
+ python, refusal = host_python.build_interpreter(
50
+ fallback if used_venv else None, fallback, needs, floor
51
+ )
52
+ return BuildPython(python, refusal, users)
53
+
54
+
55
+ def refusal_message(refusal: str) -> str:
56
+ return (
57
+ refusal + " Install it and put it first on PATH, or run `tan bootstrap` "
58
+ "to create a workspace venv; `tan doctor` reports the same interpreter."
59
+ )
@@ -0,0 +1,78 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """tan-cli#1350: switching board.yaml `diagnostics.link: itcm` back to `auto`
3
+ in an already-configured build dir must not keep linking at the ITCM.
4
+
5
+ Zephyr resolves `EXTRA_DTC_OVERLAY_FILE` with `zephyr_get(... CACHE ...)`
6
+ (`cmake/modules/configuration_files.cmake`): a `-DEXTRA_DTC_OVERLAY_FILE=...`
7
+ from an earlier configure is stored in `CMakeCache.txt` and read back on every
8
+ later configure that does not pass the variable itself. With the knob off the
9
+ plan carries no `-DEXTRA_DTC_OVERLAY_FILE`, so the cached
10
+ `<build>/alp-link-itcm.overlay` is still applied -- and the overlay file itself
11
+ is still on disk from the earlier materialise -- and the image silently links
12
+ at 0x0 with `ok: true`. (`EXTRA_CONF_FILE` is not affected: every plan re-passes
13
+ it.)
14
+
15
+ [`stale_itcm_overlay_reset`] is the fix, run before each Zephyr configure:
16
+ when the slice's command does NOT carry the ITCM overlay, delete any leftover
17
+ `alp-link-itcm.*` artefact and, if the cache still names the overlay, return
18
+ `-UEXTRA_DTC_OVERLAY_FILE`. The WHOLE `EXTRA_DTC_OVERLAY_FILE` cache entry is
19
+ unset when our overlay appears anywhere in it -- harmless, because tan never
20
+ passes any other `EXTRA_DTC_OVERLAY_FILE`; an entry that never names our
21
+ overlay is left alone.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from pathlib import Path
27
+
28
+ from tan.envelope import Issue
29
+
30
+ _OVERLAY = "alp-link-itcm.overlay"
31
+ _ARTEFACT_GLOB = "alp-link-itcm.*"
32
+
33
+
34
+ def stale_itcm_overlay_reset(cwd: Path, args: list[str]) -> tuple[list[str], list[Issue]]:
35
+ """`(extra_cmake_args, issues)` -- empty when nothing is stale or when the
36
+ slice IS itcm-targeted (its command names the overlay itself)."""
37
+ if any(_OVERLAY in a for a in args):
38
+ return [], []
39
+ removed: list[str] = []
40
+ for stale in sorted(Path(cwd).glob(_ARTEFACT_GLOB)):
41
+ try:
42
+ stale.unlink()
43
+ removed.append(stale.name)
44
+ except OSError:
45
+ pass # best-effort; the -U below is what protects the link
46
+ try:
47
+ cache = (Path(cwd) / "build" / "CMakeCache.txt").read_text(
48
+ encoding="utf-8", errors="replace")
49
+ except OSError:
50
+ cache = ""
51
+ cached = any(
52
+ line.startswith("EXTRA_DTC_OVERLAY_FILE") and _OVERLAY in line
53
+ for line in cache.splitlines()
54
+ )
55
+ if not cached:
56
+ return [], []
57
+ note = (
58
+ "this slice is no longer linked for the ITCM (`diagnostics.link` is "
59
+ f"not `itcm`) but its cached configure still names {_OVERLAY}"
60
+ + (f"; removed stale {', '.join(removed)}" if removed else "")
61
+ + " -- unsetting EXTRA_DTC_OVERLAY_FILE so it links for MRAM again"
62
+ )
63
+ return ["-UEXTRA_DTC_OVERLAY_FILE"], [Issue("build.configure-cache-reset", "info", note)]
64
+
65
+
66
+ def insert_after_separator(args: list[str], extra: list[str]) -> list[str]:
67
+ """`args` with `extra` placed immediately after the first `--` (before any
68
+ `-D`), or appended when there is no `--`. A `-U` placed before a `-D` of the
69
+ same cache key is applied first, so the later `-D` always survives."""
70
+ if not extra:
71
+ return list(args)
72
+ out = list(args)
73
+ try:
74
+ at = out.index("--") + 1
75
+ except ValueError:
76
+ at = len(out)
77
+ out[at:at] = extra
78
+ return out
@@ -0,0 +1,428 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Post-build output seam for `execute_slices`: write the `system-
3
+ manifest.yaml` the downstream `tan run`/`tan flash`/`tan size`/`tan image`
4
+ contract reads, and resolve the real on-disk `zephyr.elf` a slice produced.
5
+
6
+ Port of `crates/tan-cli/src/commands/build/execute/manifest.rs`. What's
7
+ ported: the post-build `write_post_build_manifest` seam and its two in-memory
8
+ signals (`write_failed_reason` / `native_sim_target`), `resolve_zephyr_artefact`'s
9
+ default-nested-build-dir artefact resolution, the SDK-identity stamp
10
+ (`sdk_stamp_path`/`read_sdk_stamp`/`write_sdk_stamp`/`cmake_cache_configured`)
11
+ the sdk-switch-pristine guard in `execute.py` reads and writes (issue #52),
12
+ and (tan-cli#309) the Zephyr-boilerplate-loaded guard (tan-cli #97 upstream)
13
+ [`zephyr_boilerplate_loaded`] -- `execute.py`'s status assembly applies it.
14
+
15
+ **Why `sdk_root`/`board_yaml` stay ACCEPTED overrides here rather than always
16
+ required.** The Rust oracle's `write_post_build_manifest` takes a
17
+ `ProjectContext` its caller (`crates/tan-cli/src/commands/build/execute/mod.rs`)
18
+ already resolved. This port's real caller, `tan.commands.build_cmd._dispatch` /
19
+ `_build`, now threads its own already-resolved `--sdk-root` straight through
20
+ (`execute_slices(..., sdk_root=sdk_root)` -> [`write_post_build_manifest`]) --
21
+ mirroring the oracle's `ProjectContext` exactly. The fallback below (the plan's
22
+ own `boardYaml` field plus [`tan.core.sdk_discovery.resolve_sdk_root_ladder`]'s
23
+ full precedence chain) stays for any OTHER caller of `execute_slices` that
24
+ passes no `sdk_root` at all (every test in this port's own suite does exactly
25
+ that), so the write is never permanently unreachable just because a caller
26
+ omitted the override.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from collections.abc import Sequence
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+
35
+ from tan.commands.build.materialise import MaterialiseError, confine_to_build_root
36
+ from tan.core.sdk_discovery import resolve_sdk_root_ladder
37
+ from tan.core.system_manifest import (
38
+ SliceRunResult,
39
+ SystemManifestError,
40
+ overlay_run_results_raw,
41
+ parse_system_manifest,
42
+ parse_system_manifest_raw,
43
+ serialize_system_manifest_raw,
44
+ )
45
+
46
+ __all__ = [
47
+ "PostBuildManifest",
48
+ "build_dir_overridden",
49
+ "cmake_cache_configured",
50
+ "read_sdk_stamp",
51
+ "resolve_zephyr_artefact",
52
+ "sdk_stamp_path",
53
+ "write_post_build_manifest",
54
+ "write_sdk_stamp",
55
+ "zephyr_boilerplate_loaded",
56
+ ]
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class PostBuildManifest:
61
+ """Outcome of [`write_post_build_manifest`] -- the two independent,
62
+ IN-MEMORY signals `tan run` decides host-vs-hardware and flash-safety
63
+ from (see `tan.core.run`'s module doc). Neither field is ever derived by
64
+ re-reading the on-disk file afterward. Port of `crates/tan-cli/.../
65
+ execute/manifest.rs::PostBuildManifest`."""
66
+
67
+ #: `None` when the on-disk file write succeeded; otherwise why it didn't.
68
+ #: Never flips the build's own exit code -- only gates `tan run --flash`.
69
+ write_failed_reason: str | None
70
+ #: Whether THIS run's freshly emitted SDK system-manifest projection
71
+ #: declares a `native_sim` slice -- independent of `write_failed_reason`
72
+ #: (the emit and the file write are separate steps; a write failure must
73
+ #: not erase a target the emit already established). `None` only when the
74
+ #: emit itself failed or didn't parse -- no signal at all, not "not
75
+ #: native_sim".
76
+ native_sim_target: bool | None
77
+
78
+
79
+ def _native_sim_target_from_yaml(yaml_text: str) -> bool | None:
80
+ """Whether THIS run's freshly emitted system-manifest YAML declares a
81
+ `native_sim` slice. `None` only when `yaml_text` doesn't parse (no signal
82
+ at all); a manifest with no `native_sim` slice is `False`, not `None`.
83
+ Port of `crates/tan-cli/.../execute/manifest.rs::native_sim_target_from_yaml`."""
84
+ from tan.core.run import native_sim_slice # local: avoids a core<->core cycle at import time
85
+
86
+ try:
87
+ manifest = parse_system_manifest(yaml_text)
88
+ except SystemManifestError:
89
+ return None
90
+ return native_sim_slice(manifest) is not None
91
+
92
+
93
+ def write_post_build_manifest(
94
+ *,
95
+ sdk_root: str | None,
96
+ board_yaml: str | None,
97
+ base: str,
98
+ plan_build_root: str,
99
+ results: Sequence[SliceRunResult],
100
+ plain_route: tuple[str, str] | None = None,
101
+ ) -> PostBuildManifest:
102
+ """Write `<base>/<plan_build_root>/system-manifest.yaml` after a build:
103
+ fetch the plan-time projection from the SDK (`--emit system-manifest`),
104
+ overlay this run's per-slice status, serialize, and write. Best-effort --
105
+ a failure here never flips the build's own exit code (the slices already
106
+ ran; this is a post-build convenience write) -- but every failure branch
107
+ reports `write_failed_reason` rather than failing silently.
108
+
109
+ `native_sim_target` is derived from the SDK emit BEFORE the file write is
110
+ even attempted, so it survives a write failure intact -- this is what lets
111
+ `tan run` decide from THIS build's own outcome instead of a later disk
112
+ read. Port of `crates/tan-cli/.../execute/manifest.rs::write_post_build_manifest`.
113
+ """
114
+ try:
115
+ dest_dir = confine_to_build_root(Path(base), plan_build_root)
116
+ except MaterialiseError as err:
117
+ # KNOWN STRING DIVERGENCE (deliberate, not fixed): the oracle
118
+ # (`manifest.rs::write_post_build_manifest`) guards `plan.build_root`
119
+ # with the narrower `tan_core::is_plain_relative` shape check and
120
+ # reports `"unsafe build_root `{plan.build_root}`"` on failure. This
121
+ # port reuses `confine_to_build_root` -- the SAME resolve-and-contain
122
+ # guard `materialise_plan` and this module's own `execute.py` slice-
123
+ # cwd confinement already share -- which is a STRONGER check (it
124
+ # also catches drive-relative/rooted Windows shapes a syntactic
125
+ # `is_plain_relative` component scan would reject differently) but
126
+ # reports a different message: `"path `{rel}` would escape the build
127
+ # root `{root}`"`. No caller asserts the exact wording (verified:
128
+ # no test in this port's suite matches either string), so the
129
+ # divergence is recorded here rather than forking a third
130
+ # `plan_build_root`-only guard duplicating `confine_to_build_root`'s
131
+ # logic for a Minor, message-only difference.
132
+ return PostBuildManifest(write_failed_reason=err.message, native_sim_target=None)
133
+
134
+ effective_sdk_root = sdk_root
135
+ effective_board_yaml = board_yaml
136
+ if effective_sdk_root is None and effective_board_yaml:
137
+ # The FULL ladder (`--sdk-root` > project pin > global default >
138
+ # positional walk), not the positional walk alone: this fallback only
139
+ # fires for a caller of `execute_slices` that passed no `sdk_root`
140
+ # (every test in this port's own suite), and the positional walk
141
+ # alone would let the post-build manifest record a DIFFERENT SDK root
142
+ # than `resolve_sdk_root_ladder` resolved for the build itself the
143
+ # moment a project pin or machine-global default was in play.
144
+ # Imported at module level (tan-cli#408): `resolve_sdk_root_ladder`
145
+ # moved to `tan.core.sdk_discovery`, which `build_cmd.py` (and this
146
+ # module's own `tan.commands.build.execute` ancestor) no longer sit
147
+ # upstream of, so the import that used to be circular here is not
148
+ # any more.
149
+
150
+ # `_broken_pin` unused: this fallback only fires for a caller that
151
+ # passed no `sdk_root` at all (test-only, per the docstring above) --
152
+ # the real `tan build` path already resolved (and, since tan-cli#263
153
+ # review, already reported) the project pin at its own call site
154
+ # before ever reaching here, and `PostBuildManifest` has no issues
155
+ # channel to carry a second copy through.
156
+ discovered = resolve_sdk_root_ladder(None, Path(effective_board_yaml).parent).path
157
+ effective_sdk_root = str(discovered) if discovered else None
158
+
159
+ if effective_sdk_root is not None and not effective_board_yaml and plain_route is None:
160
+ # A build with no board.yaml has nothing to project the manifest from
161
+ # (the plain `tan build --board` route has its own writer, below).
162
+ return PostBuildManifest(
163
+ write_failed_reason="the plan has no board.yaml to project it from",
164
+ native_sim_target=None,
165
+ )
166
+
167
+ if effective_sdk_root is None or (not effective_board_yaml and plain_route is None):
168
+ return PostBuildManifest(
169
+ write_failed_reason=(
170
+ "no alp-sdk checkout resolved for the post-build system-manifest emit"
171
+ ),
172
+ native_sim_target=None,
173
+ )
174
+
175
+ # Refuse to BIND a root that is not an alp-sdk checkout. `_planner_emit`
176
+ # calls `bind_sdk_root(root)` and then imports `tan.planner`, and that
177
+ # binding is process-global, first-bind-wins by design: `tan/planner/
178
+ # paths.py` freezes `REPO = sdk_root()` at module scope and a dozen planner
179
+ # functions take `metadata_root: Path = METADATA_ROOT` as a default
180
+ # argument, so both freeze at import time.
181
+ #
182
+ # Binding a non-SDK path can never produce a manifest, but it is far from
183
+ # harmless. `tan/planner/slugs.py` reads
184
+ # `metadata/registries/peripheral-kconfig.json` at module scope, so the
185
+ # import dies PARTWAY: Python evicts `tan.planner` and `tan.planner.slugs`
186
+ # but leaves `tan.planner.paths` cached with `REPO` frozen to the bogus
187
+ # root. The `except Exception` below then swallows that failure -- this
188
+ # write is best-effort by design and must never escape -- so the caller
189
+ # sees success while the process is left holding a planner frozen against
190
+ # a tree that is not an SDK at all.
191
+ #
192
+ # Checking first is cheaper than unwinding after, and uses the same
193
+ # `scripts/alp_project.py` marker `resolve_sdk_root_ladder` already treats
194
+ # as the definition of "this is an alp-sdk checkout".
195
+ if not (Path(effective_sdk_root) / "scripts" / "alp_project.py").is_file():
196
+ return PostBuildManifest(
197
+ write_failed_reason=(
198
+ f"resolved alp-sdk root {effective_sdk_root} has no "
199
+ f"scripts/alp_project.py, so it is not an alp-sdk checkout; "
200
+ f"skipping the post-build system-manifest emit rather than "
201
+ f"binding the planner to it"
202
+ ),
203
+ native_sim_target=None,
204
+ )
205
+
206
+ try:
207
+ from tan.planner_root import emit as _planner_emit
208
+ except ImportError as err: # pragma: no cover -- planner absent from this build
209
+ return PostBuildManifest(
210
+ write_failed_reason=f"planner unavailable: {err}", native_sim_target=None
211
+ )
212
+ try:
213
+ if plain_route is not None:
214
+ # tan-cli#1370: no board.yaml to project from -- a one-slice
215
+ # manifest from the board target's SoM preset instead.
216
+ from tan.planner_root import bind_sdk_root
217
+
218
+ bind_sdk_root(effective_sdk_root)
219
+ from tan.planner.plain_slice import plain_system_manifest
220
+
221
+ yaml_text = plain_system_manifest(
222
+ plain_route[0],
223
+ Path(effective_sdk_root) / "metadata",
224
+ fallback_core_id=plain_route[1],
225
+ elf_path=next((r.output_artefact for r in results if r.output_artefact), None),
226
+ )
227
+ else:
228
+ yaml_text = _planner_emit(
229
+ "system-manifest", root=effective_sdk_root, board_yaml=Path(effective_board_yaml)
230
+ )
231
+ except Exception as err: # noqa: BLE001 -- best-effort write, never escapes
232
+ return PostBuildManifest(
233
+ write_failed_reason=f"{type(err).__name__}: {err}", native_sim_target=None
234
+ )
235
+
236
+ # THIS run's own target signal -- computed from the emit, independent of
237
+ # whether the file write below then succeeds.
238
+ native_sim_target = _native_sim_target_from_yaml(yaml_text)
239
+
240
+ try:
241
+ raw = parse_system_manifest_raw(yaml_text)
242
+ overlay_run_results_raw(raw, results)
243
+ out = serialize_system_manifest_raw(raw)
244
+ except SystemManifestError as err:
245
+ return PostBuildManifest(
246
+ write_failed_reason=err.message, native_sim_target=native_sim_target
247
+ )
248
+ except Exception as err: # noqa: BLE001 -- best-effort write, never escapes
249
+ # `serialize_system_manifest_raw` (`yaml.safe_dump`) is called OUTSIDE
250
+ # any `try` in the oracle's own terms too -- `rewrite_manifest_yaml`
251
+ # returns `Result<String, String>` for every branch, including the
252
+ # dumper's own failure (e.g. a `!!binary` node PyYAML's SafeDumper
253
+ # cannot represent). Uncaught here, that would escape
254
+ # `write_post_build_manifest`, escape `execute_slices`, and abort a
255
+ # build whose slices already ran -- breaking the documented
256
+ # best-effort contract this function's own docstring states.
257
+ return PostBuildManifest(
258
+ write_failed_reason=f"{type(err).__name__}: {err}",
259
+ native_sim_target=native_sim_target,
260
+ )
261
+
262
+ dest = dest_dir / "system-manifest.yaml"
263
+ try:
264
+ dest_dir.mkdir(parents=True, exist_ok=True)
265
+ # newline="" -- PRESERVE the LF `yaml.safe_dump` always emits, rather
266
+ # than Python's default universal-newline translation (CRLF on
267
+ # Windows), which made this contract file diverge byte-for-byte from
268
+ # the oracle's `std::fs::write` (raw bytes, no translation). Same
269
+ # convention already documented at materialise.py:63 and
270
+ # image_cmd.py:319-330.
271
+ dest.write_text(out, encoding="utf-8", newline="")
272
+ except OSError as err:
273
+ return PostBuildManifest(
274
+ write_failed_reason=str(err), native_sim_target=native_sim_target
275
+ )
276
+
277
+ return PostBuildManifest(write_failed_reason=None, native_sim_target=native_sim_target)
278
+
279
+
280
+ def build_dir_overridden(cmd_args: Sequence[str]) -> bool:
281
+ """True when the slice's argv redirects west's build dir away from the
282
+ default nested `<cwd>/build` (`-d <dir>`, `--build-dir <dir>`, or
283
+ `--build-dir=<dir>`) -- the one case [`resolve_zephyr_artefact`]'s default
284
+ path can't follow. Port of `crates/tan-cli/.../execute/manifest.rs::
285
+ build_dir_overridden`."""
286
+ return any(
287
+ a == "-d" or a == "--build-dir" or a.startswith("--build-dir=") for a in cmd_args
288
+ )
289
+
290
+
291
+ def resolve_zephyr_artefact(
292
+ slice_cwd: Path, cmd_args: Sequence[str]
293
+ ) -> tuple[str | None, str | None]:
294
+ """After a slice builds `ok`, resolve the real `zephyr.elf` west produced
295
+ and its build dir. West's default output is a nested `build/` under the
296
+ slice's run cwd, so the elf lands at `<cwd>/build/zephyr/zephyr.elf`.
297
+ Returned ABSOLUTE (lexically, via `os.path.abspath` -- matching
298
+ `_abs_posix`'s convention elsewhere in this port -- never resolved through
299
+ a symlink) so every downstream consumer uses the path verbatim. `(None,
300
+ None)` when the slice's own command redirects the build dir
301
+ (`-d`/`--build-dir`) -- west then wrote somewhere other than the default
302
+ nested path, so the default location can't be trusted.
303
+
304
+ Deliberately NOT mtime-gated -- see the Rust oracle's own docstring on
305
+ why: a repeat `west build` with nothing to rebuild leaves `zephyr.elf`'s
306
+ mtime unchanged, so an mtime gate would drop a perfectly valid artefact on
307
+ every no-op/incremental rebuild. `is_file()` at the default path is
308
+ sufficient on its own. Port of `crates/tan-cli/.../execute/manifest.rs::
309
+ resolve_zephyr_artefact`."""
310
+ if build_dir_overridden(cmd_args):
311
+ return None, None
312
+ west_build = slice_cwd / "build"
313
+ elf = west_build / "zephyr" / "zephyr.elf"
314
+ if elf.is_file():
315
+ return os.path.abspath(str(elf)), os.path.abspath(str(west_build))
316
+ return None, None
317
+
318
+
319
+ #: Filename of the tan-owned SDK-identity stamp (issue #52). Lives INSIDE
320
+ #: west's own nested build dir (`<cwd>/build`, not `<cwd>` itself), so wiping
321
+ #: that dir on a mismatch retires the stamp atomically -- it can never outlive
322
+ #: or misdescribe a build dir that no longer exists. Port of
323
+ #: `crates/tan-cli/.../execute/manifest.rs::sdk_stamp_path`'s hard-coded name.
324
+ _SDK_STAMP_FILE = ".tan-sdk-root"
325
+
326
+
327
+ def sdk_stamp_path(slice_cwd: Path) -> Path:
328
+ """Path of the tan-owned SDK-identity stamp for one slice."""
329
+ return slice_cwd / "build" / _SDK_STAMP_FILE
330
+
331
+
332
+ def read_sdk_stamp(slice_cwd: Path) -> str | None:
333
+ """The SDK identity key recorded in the stamp file, if any. `None` covers
334
+ "never stamped", "unreadable", AND "unreadable as UTF-8" -- all three
335
+ read as "no signal", which `sdk_stamp_action` treats as a mismatch on an
336
+ already-configured dir. The value is whatever [`write_sdk_stamp`] wrote
337
+ -- the bare `--sdk-root` path on an older stamp, or
338
+ `tan.core.plan_exec.sdk_stamp_key`'s `<root>@<sdkCommit>` form once the
339
+ plan carries a commit -- this function does not itself parse or care
340
+ which. Port of `manifest.rs::read_sdk_stamp`, whose `std::fs::
341
+ read_to_string` folds invalid-UTF-8 into the SAME `io::Error` a missing
342
+ file raises, one `.ok()`; `UnicodeDecodeError` is a `ValueError`, not an
343
+ `OSError`, so `except OSError` alone let a corrupt (non-UTF-8)
344
+ `.tan-sdk-root` escape this function as an uncaught exception -- verified
345
+ to reach past `execute_slices`, whose own module docstring promises no
346
+ escaping exception."""
347
+ try:
348
+ return sdk_stamp_path(slice_cwd).read_text(encoding="utf-8").strip()
349
+ except (OSError, UnicodeDecodeError):
350
+ return None
351
+
352
+
353
+ def write_sdk_stamp(slice_cwd: Path, key: str) -> None:
354
+ """Write the stamp BEFORE the tool is spawned (see the executor call
355
+ site): a mid-configure failure still stamped correctly, since the dir
356
+ really was configured against `key` regardless of whether the build
357
+ finishes. `key` is `tan.core.plan_exec.sdk_stamp_key`'s output. Raises
358
+ `OSError` on failure -- the caller treats the write as best-effort (a
359
+ write failure just means the next run may treat this dir as unstamped:
360
+ fails toward a spurious rebuild, not toward trusting a stale one). Port
361
+ of `manifest.rs::write_sdk_stamp`."""
362
+ path = sdk_stamp_path(slice_cwd)
363
+ path.parent.mkdir(parents=True, exist_ok=True)
364
+ path.write_text(key, encoding="utf-8")
365
+
366
+
367
+ def cmake_cache_configured(slice_cwd: Path) -> bool:
368
+ """Whether west has ever configured this slice's build dir (`<cwd>/build/
369
+ CMakeCache.txt`) -- the "was this dir ever configured" signal
370
+ `sdk_stamp_action` needs before it treats a missing stamp as stale. Port
371
+ of `manifest.rs::cmake_cache_configured`."""
372
+ return (slice_cwd / "build" / "CMakeCache.txt").is_file()
373
+
374
+
375
+ def _dir_shows_zephyr(directory: Path) -> bool:
376
+ """The two per-directory signals behind [`zephyr_boilerplate_loaded`]:
377
+ a `ZEPHYR_BASE:` entry in `directory`'s own `CMakeCache.txt` (the primary
378
+ signal -- what `find_package(Zephyr)` caches, verified against a real
379
+ `<slice>/build/CMakeCache.txt`; a plain host configure never writes one),
380
+ OR a `zephyr/` subdirectory (Zephyr's boilerplate binary dir, kept as an
381
+ OR fallback so the guard can only ever fail a build it is SURE about).
382
+ Port of `manifest.rs::dir_shows_zephyr`, whose `std::fs::read_to_string`
383
+ folds invalid-UTF-8 into the SAME `io::Error` a missing file raises
384
+ (`.is_ok_and(...)` then just falls through to the `zephyr/` fallback);
385
+ `UnicodeDecodeError` is a `ValueError`, not an `OSError`, so `except
386
+ OSError` alone let a non-UTF-8 `CMakeCache.txt` escape this function as
387
+ an uncaught exception -- the same lesson `read_sdk_stamp` above already
388
+ records for the sibling `.tan-sdk-root` read."""
389
+ try:
390
+ cache = (directory / "CMakeCache.txt").read_text(encoding="utf-8")
391
+ except (OSError, UnicodeDecodeError):
392
+ cache = ""
393
+ if any(line.startswith("ZEPHYR_BASE:") for line in cache.splitlines()):
394
+ return True
395
+ return (directory / "zephyr").is_dir()
396
+
397
+
398
+ def zephyr_boilerplate_loaded(slice_cwd: Path) -> bool:
399
+ """tan-cli#309 (upstream tan-cli #97): whether this slice's build dir
400
+ shows that Zephyr's CMake boilerplate actually ran -- the signal behind
401
+ the `os: zephyr` guard `execute.py`'s status assembly applies.
402
+
403
+ The reported defect: a project whose `CMakeLists.txt` never calls
404
+ `find_package(Zephyr ...)` still configures and links fine under `west
405
+ build -b <board>` (CMake only emits a *dev* warning about the missing
406
+ `project()` call), so a core declared `os: zephyr` produced a host
407
+ binary and the executor reported it `[+] ok`. The board name is never
408
+ even validated, because nothing loaded the code that would validate it.
409
+
410
+ Checked one level down too (not just `<slice_cwd>/build` itself),
411
+ because `--sysbuild` nests the real per-image Zephyr builds one
412
+ directory deeper under its own superbuild -- Zephyr's own
413
+ `share/sysbuild/CMakeLists.txt` calls `find_package(Sysbuild ...)`, not
414
+ `find_package(Zephyr)`, so a sysbuild top level carries neither signal
415
+ and only the nested per-image build does. One level is enough (sysbuild
416
+ nests per-image, not recursively).
417
+
418
+ Callers must skip this check when [`build_dir_overridden`] -- west then
419
+ wrote somewhere this can't see, the same refusal [`resolve_zephyr_artefact`]
420
+ already makes. Port of `manifest.rs::zephyr_boilerplate_loaded`."""
421
+ build = slice_cwd / "build"
422
+ if _dir_shows_zephyr(build):
423
+ return True
424
+ try:
425
+ children = [p for p in build.iterdir() if p.is_dir()]
426
+ except OSError:
427
+ return False
428
+ return any(_dir_shows_zephyr(child) for child in children)
@@ -0,0 +1,81 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Write every artefact the plan carries under `build_root` -- BEFORE any
3
+ slice runs. That ordering is the stated precondition of the plan's
4
+ slice-independence invariant: `sharedArtefacts` and every slice's
5
+ `configArtefacts` must all exist on disk before dispatch starts, or
6
+ concurrent slice execution (planned later) would race a slice reading a
7
+ config file its own materialise step hasn't written yet.
8
+
9
+ Mirrors `tan-cli/src/commands/build/materialise.rs`'s `MaterialiseError`
10
+ (one type, two failure shapes -- an unsafe path, or an I/O error) rather than
11
+ letting either escape as a bare exception."""
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from tan.core.build_plan import BuildPlan
16
+
17
+
18
+ class MaterialiseError(ValueError):
19
+ """A structured, coded materialise failure. Subclasses `ValueError` (not
20
+ a plain `Exception`) so `except ValueError` -- the shape a caller reaches
21
+ for around "a plan-supplied path was bad" -- still catches it; `.code`/
22
+ `.message` are there for a caller that wants to fold it into the
23
+ `(code, message)` envelope contract instead (mirrors `PlanParseError`,
24
+ `TokenSubstitutionError`)."""
25
+
26
+ def __init__(self, code: str, message: str) -> None:
27
+ super().__init__(message)
28
+ self.code = code
29
+ self.message = message
30
+
31
+
32
+ def confine_to_build_root(build_root: Path, rel: str) -> Path:
33
+ """Resolve `rel` under `build_root`, refusing anything that escapes it.
34
+
35
+ Plans are trusted input, but a plan-supplied relative path (an
36
+ artefact's `path`, or -- reused by `execute.py` -- a slice command's
37
+ `cwd`) must never let `build_root / rel` land outside `build_root`.
38
+ `Path.is_absolute()` alone is not enough to guard against this on
39
+ Windows: a drive-relative `C:foo` or a rooted `\\foo` is NOT
40
+ `is_absolute()`, yet joining either onto `build_root` discards part or
41
+ all of it (`PurePath.__truediv__` replaces the base outright once the
42
+ right-hand side supplies its own root/drive) -- exactly the shape that
43
+ let a path escape silently. Resolving both sides and requiring
44
+ containment catches every one of these shapes (verified empirically:
45
+ `..`, a bare posix `/x`, `C:x`, and `\\x` all resolve outside
46
+ `build_root.resolve()`), not just the lexical `..` case.
47
+ """
48
+ root = build_root.resolve()
49
+ target = (build_root / rel).resolve()
50
+ if target != root and root not in target.parents:
51
+ raise MaterialiseError(
52
+ "build.path-escape",
53
+ f"path `{rel}` would escape the build root `{root}`",
54
+ )
55
+ return target
56
+
57
+
58
+ def _write(build_root: Path, artefact: dict[str, Any]) -> Path:
59
+ rel = artefact["path"]
60
+ target = confine_to_build_root(build_root, rel)
61
+ try:
62
+ target.parent.mkdir(parents=True, exist_ok=True)
63
+ target.write_text(artefact.get("contents", ""), encoding="utf-8", newline="")
64
+ except OSError as err:
65
+ # Permission denied, a Windows path over MAX_PATH, a parent that's
66
+ # actually a file, target itself being a directory (an empty/`.`
67
+ # artefact path) -- every write-time OSError, never a bare traceback.
68
+ raise MaterialiseError(
69
+ "build.artefact-write-failed", f"failed to write `{rel}`: {err}"
70
+ ) from err
71
+ return target
72
+
73
+
74
+ def materialise_plan(plan: BuildPlan, build_root: Path) -> list[Path]:
75
+ """Write ALL `sharedArtefacts`, then every slice's `configArtefacts`, and
76
+ only then return. Callers must not start any slice before this returns --
77
+ that ordering is the whole point (see module docstring)."""
78
+ written = [_write(build_root, a) for a in plan.shared_artefacts]
79
+ for sl in plan.slices:
80
+ written.extend(_write(build_root, a) for a in sl.config_artefacts)
81
+ return written