tan-cli 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/core/plan_exec.py ADDED
@@ -0,0 +1,373 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure build-plan execution decisions (ADR-0020): turn the plan's
3
+ envAppendPath and executionPolicy into concrete env values and skip/fail
4
+ dispositions. No IO, no spawning -- the executor calls these and owns the IO."""
5
+ import ntpath
6
+ import os
7
+ import re
8
+ from collections.abc import Callable, Sequence
9
+ from dataclasses import dataclass
10
+ from enum import StrEnum
11
+ from pathlib import Path
12
+
13
+
14
+ def normalize_path(path: Path) -> Path:
15
+ """Lexical `.`/`..` collapse, no filesystem access -- port of
16
+ `tan_core::path_guard::normalize` (crates/tan-core/src/path_guard.rs).
17
+ Used (joined onto a resolved workspace root first) so a relative and an
18
+ absolute spelling of the SAME `--sdk-root` normalize to the identical
19
+ string before [`sdk_stamp_key`] compares them -- otherwise a relative
20
+ `--sdk-root ../alp-sdk` byte-mismatches the absolute pointer `tan sdk
21
+ switch` already pinned for the SAME checkout, thrashing a full wipe on
22
+ every alternating invocation between the two forms (tan-cli#163).
23
+
24
+ A `..` past the anchor (the drive/root, or nothing left on a relative
25
+ path) is dropped rather than kept literal, matching `PathBuf::pop`'s own
26
+ "false, does nothing" behaviour at the top."""
27
+ anchor = path.anchor
28
+ parts: list[str] = []
29
+ for part in (path.parts[1:] if anchor else path.parts):
30
+ if part == ".":
31
+ continue
32
+ if part == "..":
33
+ if parts:
34
+ parts.pop()
35
+ continue
36
+ parts.append(part)
37
+ if anchor:
38
+ return Path(anchor).joinpath(*parts)
39
+ return Path(*parts) if parts else Path(".")
40
+
41
+
42
+ class PolicyAction(StrEnum):
43
+ SKIP = "skip"
44
+ FAIL = "fail"
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ExecutionPolicy:
49
+ unknown_backend: PolicyAction | None = None
50
+ missing_tool: PolicyAction | None = None
51
+ null_command: PolicyAction | None = None
52
+
53
+
54
+ def sep_for_key(var: str) -> str:
55
+ """The join separator for ONE envAppendPath var -- per-key, not uniformly
56
+ os.pathsep. EXTRA_ZEPHYR_MODULES is a CMake list that Zephyr's
57
+ zephyr_module.py splits on ';' on EVERY platform (never an OS path list);
58
+ joining it with ':' on Linux/WSL fails `west build` configure with
59
+ "is not a valid zephyr module"."""
60
+ return ";" if var == "EXTRA_ZEPHYR_MODULES" else os.pathsep
61
+
62
+
63
+ def apply_env_append(base: list[tuple[str, str]], append: dict[str, list[str]]) -> None:
64
+ """Append each value to its var using that var's separator, skipping a value
65
+ already present segment-wise. A var absent from `base` is seeded from the
66
+ appended values (the plan owns it). Mutates `base` in place."""
67
+ for var, values in append.items():
68
+ sep = sep_for_key(var)
69
+ current = next((v for k, v in base if k == var), None)
70
+ segments = current.split(sep) if current else []
71
+ for val in values:
72
+ if val not in segments:
73
+ segments.append(val)
74
+ joined = sep.join(segments)
75
+ for i, (k, _) in enumerate(base):
76
+ if k == var:
77
+ base[i] = (var, joined)
78
+ break
79
+ else:
80
+ base.append((var, joined))
81
+
82
+
83
+ def assemble_slice_env(
84
+ slice_env: dict[str, str],
85
+ env_append_path: dict[str, list[str]],
86
+ inherited: Callable[[str], str | None],
87
+ gap_fillers: Sequence[tuple[str, str]],
88
+ ) -> list[tuple[str, str]]:
89
+ """Start from the slice's verbatim env, seed any envAppendPath var the slice
90
+ doesn't pin from `inherited` (so an append EXTENDS rather than replaces an
91
+ inherited PYTHONPATH), apply the appends, then merge the CLI's
92
+ consumer-mechanism gap fillers -- "plan wins / CLI fills gaps"."""
93
+ env: list[tuple[str, str]] = list(slice_env.items())
94
+ for key in env_append_path:
95
+ if not any(k == key for k, _ in env):
96
+ value = inherited(key)
97
+ if value is not None:
98
+ env.append((key, value))
99
+ apply_env_append(env, env_append_path)
100
+ for key, value in gap_fillers:
101
+ for i, (k, _) in enumerate(env):
102
+ if k == key:
103
+ env[i] = (key, value)
104
+ break
105
+ else:
106
+ env.append((key, value))
107
+ return env
108
+
109
+
110
+ def resolve_action(
111
+ policy: ExecutionPolicy | None, key: str, default: PolicyAction
112
+ ) -> PolicyAction:
113
+ """Honour executionPolicy's entry when present, else the CLI's built-in
114
+ behaviour for an older plan that omits it."""
115
+ if policy is None:
116
+ return default
117
+ picked = getattr(policy, key, None)
118
+ return picked if picked is not None else default
119
+
120
+
121
+ class SdkStampAction(StrEnum):
122
+ KEEP = "keep"
123
+ PRISTINE = "pristine"
124
+
125
+
126
+ def sdk_stamp_action(
127
+ cached: str | None,
128
+ current: str | None,
129
+ cache_configured: bool,
130
+ build_dir_overridden: bool,
131
+ cwd_under_build_root: bool,
132
+ ) -> SdkStampAction:
133
+ """Whether to wipe a slice's build dir because it was configured against a
134
+ different SDK root. West refuses to reconfigure a build dir whose CMake cache
135
+ is bound to another source tree ("FATAL ERROR: refusing to proceed without
136
+ --force"), which reaches the user as a bare "terminated with exit code: 1".
137
+
138
+ A missing stamp on an already-configured dir reads as stale DELIBERATELY --
139
+ it is the only way a build dir predating this feature ever self-heals."""
140
+ if build_dir_overridden or not cwd_under_build_root or not cache_configured:
141
+ return SdkStampAction.KEEP
142
+ if current is None:
143
+ return SdkStampAction.KEEP
144
+ return SdkStampAction.KEEP if cached == current else SdkStampAction.PRISTINE
145
+
146
+
147
+ class PristineSkipped(StrEnum):
148
+ """Why an explicit `tan build --pristine` did NOT wipe a slice's build
149
+ dir -- port of the Rust oracle's `PristineSkipped`
150
+ (`crates/tan-core/src/plan_exec.rs`, pre-tan-cli#269 deletion). Every
151
+ member has a fixed `.reason()` sentence below rather than a message
152
+ assembled at each call site, so `build.pristine-skipped` always names the
153
+ SAME fact for the SAME cause.
154
+
155
+ `NEVER_CONFIGURED` is not one of the two structural safety guards
156
+ (`BUILD_DIR_OVERRIDDEN`/`CWD_OUTSIDE_BUILD_ROOT`) -- it is the ordinary
157
+ "nothing to wipe" case, and (per the oracle's own comment) the only one
158
+ of the three a stock plan reaches today: `write_sdk_stamp` itself
159
+ creates `<cwd>/build`, so after any prior slice the directory exists
160
+ holding nothing but tan's own stamp file."""
161
+
162
+ BUILD_DIR_OVERRIDDEN = "build-dir-overridden"
163
+ CWD_OUTSIDE_BUILD_ROOT = "cwd-outside-build-root"
164
+ NEVER_CONFIGURED = "never-configured"
165
+
166
+ def reason(self) -> str:
167
+ """One clause naming what stopped the wipe -- phrased as the reason,
168
+ not as an apology, verbatim from the oracle's own `reason()`."""
169
+ return {
170
+ PristineSkipped.BUILD_DIR_OVERRIDDEN: (
171
+ "the slice redirects west's build dir (`-d`/`--build-dir`), so tan "
172
+ "cannot know which directory to wipe"
173
+ ),
174
+ PristineSkipped.CWD_OUTSIDE_BUILD_ROOT: (
175
+ "the plan cwd is not under `build/`, so the wipe target could hold "
176
+ "files the build never created"
177
+ ),
178
+ PristineSkipped.NEVER_CONFIGURED: (
179
+ "that build dir has no CMakeCache.txt — it was never configured, "
180
+ "so there was nothing to wipe"
181
+ ),
182
+ }[self]
183
+
184
+
185
+ def pristine_suppression(
186
+ force_pristine: bool,
187
+ build_dir_overridden: bool,
188
+ cwd_under_build_root: bool,
189
+ cache_configured: bool,
190
+ ) -> PristineSkipped | None:
191
+ """Whether an explicit `--pristine` was suppressed, and why -- `None` when
192
+ the wipe actually happened or none was asked for (tan-cli#183, tan-cli#427).
193
+
194
+ ONE decision for all THREE suppression paths, rather than a message
195
+ bolted onto whichever branch the caller happens to be in. Order is
196
+ most-specific-first: an overridden build dir is reported even when the
197
+ cwd would also have failed, because that is the fact the user must
198
+ change first."""
199
+ if not force_pristine:
200
+ return None
201
+ if build_dir_overridden:
202
+ return PristineSkipped.BUILD_DIR_OVERRIDDEN
203
+ if not cwd_under_build_root:
204
+ return PristineSkipped.CWD_OUTSIDE_BUILD_ROOT
205
+ if not cache_configured:
206
+ return PristineSkipped.NEVER_CONFIGURED
207
+ return None
208
+
209
+
210
+ def sdk_stamp_key(sdk_root: str | None, sdk_commit: str | None) -> str | None:
211
+ """The identity [`sdk_stamp_action`] actually compares, and the executor
212
+ writes to `.tan-sdk-root`: the resolved SDK root path alone is not enough,
213
+ because a standalone checkout that changes CONTENT at the SAME path (a
214
+ `git pull`/`git checkout <tag>` in a `--sdk-root ../alp-sdk` checkout)
215
+ leaves a path-only stamp matching while the build dir is actually stale.
216
+ Folding the plan's own `sdkCommit` in closes it: a freshly emitted plan
217
+ always reflects the SDK checkout's CURRENT git HEAD, so a content change
218
+ at the same root changes the key.
219
+
220
+ `sdk_commit` blank or absent (an older plan, or an SDK checkout with no
221
+ resolvable git HEAD) degrades the key to the root alone -- the historical,
222
+ path-only stamp. `None` when `sdk_root` itself is unresolved -- nothing to
223
+ key on at all."""
224
+ if sdk_root is None:
225
+ return None
226
+ commit = sdk_commit.strip() if sdk_commit is not None else ""
227
+ return f"{sdk_root}@{commit}" if commit else sdk_root
228
+
229
+
230
+ def west_build_source_dir(args: Sequence[str]) -> str | None:
231
+ """The SOURCE_DIR positional `west build`'s own `_sanity_check_source_dir`
232
+ resolves -- `None` when `args` carries no `-b`/`--board` flag (a shape
233
+ `orchestrator.py` never emits for a `west build` slice).
234
+
235
+ `orchestrator.py` always emits the source dir as the token immediately
236
+ following `-b`'s own value: `cmd = ["west", "build", "-b", slice_.board,
237
+ app_dir]` (`tan.planner.orchestrator`, the `zephyr` backend's command
238
+ builder). That is NOT the same as `args[-1]`, the guess this replaces --
239
+ a real zephyr slice's command tail is never just that four-element
240
+ prefix. `cmd += ["--", *defines]` always appends at least one CMake
241
+ define (`defines` starts life as `["-DPython3_EXECUTABLE=${PYTHON}"]`
242
+ and is never emptied), and a sysbuild slice additionally inserts
243
+ `--sysbuild` between the source dir and that `--` separator -- so
244
+ neither "the last token" nor "the last token before `--`" is safe.
245
+ Anchoring on `-b`'s own position instead matches exactly how the
246
+ emitter places the source dir, regardless of what is appended after
247
+ it."""
248
+ for i, token in enumerate(args):
249
+ if token in ("-b", "--board") and i + 2 < len(args):
250
+ return args[i + 2]
251
+ return None
252
+
253
+
254
+ def _build_dir_overridden(args: Sequence[str]) -> bool:
255
+ """Verbatim mirror of `tan.commands.build.manifest.build_dir_overridden`
256
+ -- duplicated, not imported: `tan/core` is pure/dependency-free and must
257
+ not import from `tan/commands` (the reverse of the one real dependency
258
+ direction in this codebase). Keep the two definitions in sync by hand if
259
+ `west build`'s own build-dir flag set ever changes."""
260
+ return any(a == "-d" or a == "--build-dir" or a.startswith("--build-dir=") for a in args)
261
+
262
+
263
+ #: tan-cli#697. `_pin_west_workspace` (`tan.commands.build.execute`) redirects
264
+ #: `west build`'s own spawned cwd to the resolved Zephyr workspace so west's
265
+ #: ancestor-`.west` walk lands on the right one -- but `west build`'s own
266
+ #: source-directory sanity check (`_sanity_check_source_dir`, upstream
267
+ #: `scripts/west_commands/build.py:534`, frozen third-party code this repo
268
+ #: does not own) then calls `os.path.relpath(self.source_dir)` with NO
269
+ #: explicit `start`, which defaults to `os.getcwd()` -- the pinned
270
+ #: WORKSPACE, not the slice's own project. On Windows, `os.path.relpath`
271
+ #: RAISES rather than returning a path when its two arguments live on
272
+ #: different drive letters (`ValueError: path is on mount 'E:', start on
273
+ #: mount 'C:'`, reproduced verbatim in the issue). There is no fix reachable
274
+ #: from tan's own side of the seam -- `west` is an upstream pip dependency,
275
+ #: not `crates/`/`tan/planner` -- so the only correct move is to REFUSE,
276
+ #: with a coded reason naming both mounts, before spawning a process
277
+ #: already known to crash.
278
+ CROSS_DRIVE_MSG = "west build cannot span two Windows drives"
279
+
280
+
281
+ #: tan-cli#801: the structural shape both of `tan.commands.build.execute`'s
282
+ #: missing-tool refusals emit -- the slice's own `command` precheck
283
+ #: (`` tool `{tool}` not found -- searched ... ``, at the START of the
284
+ #: message) and `_missing_post_tool`'s post-build-step refusal (`` slice
285
+ #: `{core_id}` post-build step N of M (`{label}`) cannot run: tool `{tool}`
286
+ #: not found -- ... ``, where the phrase sits mid-string, AFTER "cannot
287
+ #: run: "). `build_cmd._missing_tool_issues` used to key on
288
+ #: `message.startswith("tool `")`, which only ever matched the first shape
289
+ #: -- the #550 post-build-step skip's reason never promoted into
290
+ #: `issues[]`, silently, because the phrase is not at position 0 there.
291
+ #: Matched with `.search`, never `.match`/`startswith`, for exactly that
292
+ #: reason: the marker's POSITION in the message is not part of the
293
+ #: contract, only its shape is. Anchored on the backtick-delimited tool
294
+ #: name (not two independent "tool `" / "` not found" substring checks
295
+ #: with no adjacency requirement between them) so an unrelated message that
296
+ #: happens to contain both fragments separately cannot false-positive.
297
+ _MISSING_TOOL_PREFIX = "tool `"
298
+ _MISSING_TOOL_SUFFIX = "` not found"
299
+ MISSING_TOOL_RE = re.compile(
300
+ re.escape(_MISSING_TOOL_PREFIX) + r"[^`]*" + re.escape(_MISSING_TOOL_SUFFIX)
301
+ )
302
+
303
+
304
+ def missing_tool_message(tool: str) -> str:
305
+ """The one place `` tool `{tool}` not found `` is spelled. Every producer
306
+ in `tan.commands.build.execute` (the slice's own `command` precheck AND
307
+ `_missing_post_tool`'s post-build-step refusal) MUST build its message
308
+ through this function rather than hand-writing the literal -- MISSING_TOOL_RE
309
+ above is built from the exact same `_MISSING_TOOL_PREFIX`/`_MISSING_TOOL_SUFFIX`
310
+ pair, so a wording edit made only at a call site (not here) silently stops
311
+ `build_cmd._missing_tool_issues` from promoting that producer's refusals
312
+ into `issues[]` again -- the exact #801 regression this coupling exists to
313
+ prevent."""
314
+ return f"{_MISSING_TOOL_PREFIX}{tool}{_MISSING_TOOL_SUFFIX}"
315
+
316
+
317
+ @dataclass(frozen=True)
318
+ class CrossDriveRefusal:
319
+ #: Full explanation, naming both mounts and both paths -- this run's
320
+ #: stdout and the envelope's `data.slices[].reason`.
321
+ message: str
322
+ #: Short form for `system-manifest.yaml` `slices[].reason` -- names both
323
+ #: mounts, drops the absolute paths and the upstream-mechanism
324
+ #: explanation. Same split, for the same reason, as the missing-tool
325
+ #: refusal's own `manifest_message` (`tan.commands.build.execute`): a
326
+ #: build ARTEFACT that outlives the run and gets forwarded should not
327
+ #: carry a customer's private directory layout.
328
+ manifest_message: str
329
+
330
+
331
+ def cross_drive_source_refusal(
332
+ workspace_dir: Path | None, args: Sequence[str]
333
+ ) -> CrossDriveRefusal | None:
334
+ """`None` when `_pin_west_workspace` is safe to redirect `west build`'s
335
+ cwd to `workspace_dir`; else the tan-cli#697 refusal, naming both
336
+ mounts, for a slice that pin would otherwise send straight into the
337
+ `ValueError` [`CROSS_DRIVE_MSG`] documents.
338
+
339
+ The no-op conditions mirror `_pin_west_workspace`'s own guard verbatim:
340
+ a slice that pin never rewrites can never hit the crash the pin
341
+ introduces, so it must never be refused here either.
342
+
343
+ `ntpath.splitdrive`, never `os.path.splitdrive`: the two-drive-letter
344
+ shape this exists to catch is a WINDOWS filesystem fact, independent of
345
+ whatever platform is running this check right now. `ntpath` parses
346
+ `C:\\...`/`C:/...` identically on every host -- which is what makes
347
+ this function exercisable (and unit-tested) on a POSIX box with no
348
+ two-drive Windows filesystem to reproduce against, unlike the crash it
349
+ heads off, which only ever fires on Windows itself. Either side missing
350
+ a drive letter (a POSIX path, or a Windows UNC share) returns `None`,
351
+ not a refusal -- that combination is not the shape the issue reproduced,
352
+ and a false "no refusal" there only leaves west to run exactly as it
353
+ does today, never a NEW failure this function introduces. The
354
+ comparison itself is case-insensitive (`C:` and `c:` name the same
355
+ mount)."""
356
+ if workspace_dir is None or not args or args[0] != "build" or _build_dir_overridden(args):
357
+ return None
358
+ source = west_build_source_dir(args)
359
+ if source is None:
360
+ return None
361
+ ws_drive, _ = ntpath.splitdrive(str(workspace_dir))
362
+ src_drive, _ = ntpath.splitdrive(source)
363
+ if not ws_drive or not src_drive or ws_drive.lower() == src_drive.lower():
364
+ return None
365
+ message = (
366
+ f"{CROSS_DRIVE_MSG} -- the resolved west workspace `{workspace_dir}` is on "
367
+ f"mount `{ws_drive}`, but this slice's own project source directory `{source}` "
368
+ f"is on mount `{src_drive}`. west build's own source-directory check "
369
+ f"(`os.path.relpath`, upstream `scripts/west_commands/build.py`) raises rather "
370
+ f"than resolving a path across two Windows drives -- move the project or the "
371
+ f"`tan bootstrap`-created workspace so both live on the same drive."
372
+ )
373
+ return CrossDriveRefusal(message, f"{CROSS_DRIVE_MSG} ({ws_drive} vs {src_drive})")