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,381 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The IO/glue side of build-plan token substitution (alp-sdk #865): resolves
3
+ the SDK root, project root and planner python, drives the pure
4
+ `tan.core.plan_tokens` pass, and runs the one guard that pass cannot do
5
+ itself -- comparing the plan's `sdkCommit` against the resolved SDK
6
+ checkout's actual git HEAD (a subprocess call, so it belongs here, not in the
7
+ pure module).
8
+
9
+ `board_yaml_path`, `sdk_root`, `python` and `toolchain_root` are each
10
+ resolved exactly ONCE by the caller and handed in -- never re-resolved here
11
+ per-slice or on retry, the same "one resolution for the whole plan" contract
12
+ `tan_core::plan_tokens::TokenValues` documents. `toolchain_advice` travels
13
+ WITH `toolchain_root` for that reason: it is the second half of one
14
+ resolution (`toolchain.ToolchainResolution`), not an independent knob, and
15
+ re-deriving it here would let the reason drift from the value it explains."""
16
+ import os
17
+ import subprocess
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ from tan.commands.build.toolchain import NO_TOOLCHAIN_ADVICE
22
+ from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
23
+ from tan.core.build_plan import BuildPlan
24
+ from tan.core.plan_tokens import (
25
+ DeferredPlaceholder,
26
+ DeferredPolicy,
27
+ LeftoverToken,
28
+ PLAN_TOKEN_NAMES,
29
+ PLAN_PATH_MODE_TOKENED,
30
+ TokenValues,
31
+ UnknownPlanPathMode,
32
+ UnresolvedToolchainRoot,
33
+ project_root_diverges_from_exec_base,
34
+ sdk_commit_mismatches,
35
+ substitute_plan_tokens,
36
+ )
37
+ from tan.envelope import Issue
38
+ from tan.core.subprocess_env import spawn_env
39
+ from tan.core.sysbuild_image import rescope_sysbuild_extra_conf
40
+ from tan.core.tool_lookup import resolve_tool
41
+
42
+
43
+ class TokenSubstitutionError(Exception):
44
+ def __init__(self, code: str, message: str) -> None:
45
+ super().__init__(message)
46
+ self.code = code
47
+ self.message = message
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class SliceDemotion:
52
+ """A slice demoted by `substitute_plan_tokens` (tan-cli #89): its fields
53
+ still name a literal `${TOOLCHAIN_ROOT}` because this host has none
54
+ resolved, but the demotion has an owning slice AND an owning dispatch
55
+ seam (`executionPolicy.missingTool`) to route to, so it did not fail the
56
+ whole plan. `reason` is the full sentence the executor surfaces verbatim
57
+ at dispatch."""
58
+
59
+ slice_index: int
60
+ core_id: str
61
+ reason: str
62
+
63
+
64
+ def _resolve_git_executable() -> str | None:
65
+ """The absolute path to `git`, resolved through the shared
66
+ `tool_lookup.resolve_tool` walk -- never `shutil.which`, which on Windows
67
+ inserts the current directory ahead of `%PATH%`. `None` when `git` is not
68
+ on PATH at all, which both callers below treat as "no git provenance",
69
+ never a crash (tan-cli#797, mirroring `doctor_cmd._resolve_git_executable`
70
+ -- the same fix, duplicated here rather than imported for the same reason
71
+ `_is_own_git_checkout`'s own docstring gives: this is the second of the
72
+ two sites the issue names, not a shared library call).
73
+ """
74
+ return resolve_tool("git", os.environ).resolved
75
+
76
+
77
+ def _is_own_git_checkout(sdk_root: Path, git_exe: str | None) -> bool:
78
+ """Whether `sdk_root` is itself the TOP of a git checkout, not merely
79
+ nested somewhere inside an unrelated ENCLOSING one (tan-cli#488 defect 4,
80
+ mirroring `doctor_cmd._is_own_git_checkout` -- the SAME defect, the SAME
81
+ fix, duplicated here rather than imported because this is the second of
82
+ the two sites the issue names, not a shared library call).
83
+
84
+ `git -C <root> ...` discovery walks UPWARD looking for a `.git`, so an SDK
85
+ with no `.git` of its own -- an extracted release archive vendored under a
86
+ customer's own application repository, a setup this port explicitly
87
+ supports (`sdk_cmd.check_sdk_readiness`'s own docstring names exactly this
88
+ shape) -- answers every git query with the ENCLOSING repo's state instead
89
+ of "not a checkout". `git_short_head` below calls this first and returns
90
+ no signal (`""`) rather than misattributing a foreign repository's HEAD as
91
+ the SDK's own -- which, left unguarded, both stamps a build plan's
92
+ `sdkCommit` with the wrong provenance and lets `sdk_commit_mismatches` fire
93
+ a false split-brain refusal (or miss a real one) keyed off the enclosing
94
+ app repo's commit instead of the SDK's.
95
+
96
+ Compares the RESOLVED `--show-toplevel` against the RESOLVED `sdk_root`:
97
+ lexical string comparison alone would false-negative on a symlinked or
98
+ differently-cased path to the same directory.
99
+
100
+ `git_exe` -- the PATH-RESOLVED absolute `git` from `_resolve_git_executable`
101
+ (tan-cli#797): this runs on the BUILD path with cwd set to the CUSTOMER's
102
+ project, the Windows-reachable strong form of the cwd-shadowing hazard.
103
+ `None` refuses outright rather than falling back to a bare spawn.
104
+ """
105
+ if git_exe is None:
106
+ return False
107
+ try:
108
+ out = subprocess.run(
109
+ ["git", "-C", str(sdk_root), "rev-parse", "--show-toplevel"],
110
+ executable=git_exe,
111
+ capture_output=True,
112
+ text=True,
113
+ timeout=10,
114
+ env=spawn_env(),
115
+ check=False,
116
+ )
117
+ except (OSError, subprocess.SubprocessError):
118
+ return False
119
+ if out.returncode != 0:
120
+ return False
121
+ top = out.stdout.strip()
122
+ if not top:
123
+ return False
124
+ try:
125
+ return Path(top).resolve() == Path(sdk_root).resolve()
126
+ except OSError:
127
+ return False
128
+
129
+
130
+ def git_short_head(sdk_root: Path) -> str:
131
+ """`git rev-parse --short HEAD`. Empty string when git is missing, the
132
+ checkout has no `.git` OF ITS OWN (see `_is_own_git_checkout` --
133
+ tan-cli#488 defect 4), or the command otherwise fails -- all NO SIGNAL,
134
+ never a hard failure: an SDK checkout with no `.git` (a release tarball,
135
+ including one vendored inside a customer's own git repository) is a
136
+ normal, supported setup.
137
+
138
+ `git` is resolved ONCE, through `_resolve_git_executable`, and threaded
139
+ into both the `_is_own_git_checkout` guard and the spawn below as
140
+ `executable=` (tan-cli#797) -- neither hands `subprocess` the bare
141
+ `"git"` identity.
142
+ """
143
+ git_exe = _resolve_git_executable()
144
+ if not _is_own_git_checkout(sdk_root, git_exe):
145
+ return ""
146
+ try:
147
+ out = subprocess.run(
148
+ ["git", "-C", str(sdk_root), "rev-parse", "--short", "HEAD"],
149
+ executable=git_exe,
150
+ capture_output=True,
151
+ text=True,
152
+ timeout=10,
153
+ env=spawn_env(),
154
+ check=False,
155
+ )
156
+ except (OSError, subprocess.SubprocessError):
157
+ return ""
158
+ return out.stdout.strip() if out.returncode == 0 else ""
159
+
160
+
161
+ def _deferred_extra_note(plan: BuildPlan, e: LeftoverToken, policy: DeferredPolicy) -> str:
162
+ """What to append to a `build.plan-token-unresolved` message so the
163
+ refusal also says what a malformed `deferredPlaceholders` cost, or how a
164
+ config-artefact placeholder could have been accepted (tan-cli#1302)."""
165
+ notes = ""
166
+ if plan.deferred_placeholders_problem is not None:
167
+ notes += (
168
+ f" Note: {plan.deferred_placeholders_problem} -- the field was ignored, so no "
169
+ f"`${{NAME}}` placeholder was exempt."
170
+ )
171
+ elif e.deferrable:
172
+ name = e.token[2:-1]
173
+ if policy.has_plan_list:
174
+ notes += (
175
+ f" If `{e.token}` is a placeholder meant for the build host or the device, "
176
+ f"the plan's `deferredPlaceholders` has to list `{name}` "
177
+ f"(alplabai/alp-sdk#2696); it does not."
178
+ )
179
+ else:
180
+ notes += (
181
+ f" If `{e.token}` is a placeholder meant for the build host or the device, "
182
+ f"it is accepted only when the plan's `deferredPlaceholders` lists `{name}` "
183
+ f"(alplabai/alp-sdk#2696); this plan carries no `deferredPlaceholders` at "
184
+ f"all -- re-emit it with a current planner."
185
+ )
186
+ return notes
187
+
188
+
189
+ def _deferred_text(d: DeferredPlaceholder) -> str:
190
+ if d.live_kconfig:
191
+ return (
192
+ f"placeholder `${{{d.name}}}` in `{d.field}` sits on a live line of a Kconfig "
193
+ f"fragment; Kconfig does not expand `${{{d.name}}}`, so the firmware will carry "
194
+ f"the literal text unless something substitutes it before the build"
195
+ )
196
+ return (
197
+ f"placeholder `${{{d.name}}}` in `{d.field}` is left as written for the build "
198
+ f"host or the device to supply; tan does not substitute it"
199
+ )
200
+
201
+
202
+ def deferred_placeholder_issues(
203
+ plan: BuildPlan, deferred: list[DeferredPlaceholder]
204
+ ) -> list[Issue]:
205
+ """The envelope issues for tan-cli#1302: one issue per placeholder NAME
206
+ left standing in a config artefact (`info`, or `warning` when any
207
+ occurrence is on a live Kconfig line), plus `build.plan-warning`s for a
208
+ malformed `deferredPlaceholders` and for a plan path token listed in it."""
209
+ issues: list[Issue] = []
210
+ if plan.deferred_placeholders_problem is not None:
211
+ text = f"[deferred-placeholders-malformed] {plan.deferred_placeholders_problem} -- ignoring the field"
212
+ if plan.plan_path_mode == PLAN_PATH_MODE_TOKENED:
213
+ text += (
214
+ ", so no `${NAME}` placeholder is exempt from the unresolved-token refusal"
215
+ )
216
+ issues.append(Issue("build.plan-warning", "warning", text))
217
+ bugs = [n for n in plan.deferred_placeholders or () if n in PLAN_TOKEN_NAMES]
218
+ if bugs:
219
+ listed = ", ".join(f"`{n}`" for n in bugs)
220
+ issues.append(
221
+ Issue(
222
+ "build.plan-warning",
223
+ "warning",
224
+ f"[deferred-placeholders-plan-token] `deferredPlaceholders` lists the plan "
225
+ f"path token(s) {listed} -- a planner bug: tan substitutes those and they are "
226
+ f"never deferred; ignoring the entries",
227
+ )
228
+ )
229
+ by_name: dict[str, list[DeferredPlaceholder]] = {}
230
+ for d in deferred:
231
+ by_name.setdefault(d.name, []).append(d)
232
+ for occurrences in by_name.values():
233
+ pick = next((d for d in occurrences if d.live_kconfig), occurrences[0])
234
+ severity = "warning" if pick.live_kconfig else "info"
235
+ issues.append(Issue("build.deferred-placeholder", severity, _deferred_text(pick)))
236
+ return issues
237
+
238
+
239
+ def apply_plan_token_substitution(
240
+ plan: BuildPlan,
241
+ *,
242
+ board_yaml_path: str | None,
243
+ exec_base: str,
244
+ sdk_root: str | None,
245
+ python: str,
246
+ toolchain_root: str | None,
247
+ toolchain_advice: str = NO_TOOLCHAIN_ADVICE,
248
+ deferred_out: list[DeferredPlaceholder] | None = None,
249
+ project_root: str | None = None,
250
+ ) -> tuple[BuildPlan, list[SliceDemotion]]:
251
+ """Apply the build-plan token-substitution pass to `plan` before
252
+ materialise writes anything or a slice command runs. A no-op unless
253
+ `plan.plan_path_mode` is present; a present-but-unrecognised mode is a
254
+ hard error, never a half-applied guess.
255
+
256
+ `toolchain_advice` is the reason an unresolved `${TOOLCHAIN_ROOT}` gets,
257
+ both in the plan-fatal refusal and in each demoted slice's `reason`.
258
+ It defaults to the no-toolchain wording this pass always used, so a
259
+ caller that has not resolved a toolchain at all -- and every existing
260
+ test -- keeps its exact previous message. `build_cmd` passes the
261
+ resolver's own reason instead, which distinguishes "this host has none"
262
+ from "this host has SEVERAL and nothing picking between them"
263
+ (tan-cli#547): two different fixes, and only the second one names the
264
+ installs the caller can choose from.
265
+
266
+ `deferred_out` (tan-cli#1302), when given, receives every `${NAME}` that
267
+ was deliberately left standing in a `configArtefacts[*].contents` value
268
+ (see `DeferredPolicy` for the exact rule). A caller that passes nothing
269
+ still gets the exemption -- it just cannot report it."""
270
+ if plan.plan_path_mode is None:
271
+ return plan, []
272
+ if plan.plan_path_mode != PLAN_PATH_MODE_TOKENED:
273
+ raise TokenSubstitutionError(
274
+ "build.plan-invalid",
275
+ f'unknown planPathMode `{plan.plan_path_mode}` (only "tokened" is defined) -- '
276
+ f"refusing rather than silently treating it as legacy or applying token "
277
+ f"substitution",
278
+ )
279
+
280
+ # Guard: PROJECT_ROOT (board.yaml's directory) vs the executor's actual
281
+ # base dir. They coincide only in the default config -- a tokened plan
282
+ # substituting ${PROJECT_ROOT} from one while slices actually run under
283
+ # the other would silently build against the wrong tree.
284
+ # `project_root` is the `board.yaml`-less route's own (tan-cli#1359: `tan
285
+ # build --board`): there is no board.yaml to derive it from, so the caller
286
+ # names it (the build root) instead.
287
+ if board_yaml_path is None and project_root is None:
288
+ raise TokenSubstitutionError(
289
+ "build.plan-invalid",
290
+ "a `planPathMode: tokened` plan needs a resolved board.yaml to derive "
291
+ "${PROJECT_ROOT} from -- pass `--board-yaml <PATH>` or run from a project.",
292
+ )
293
+ if project_root is None:
294
+ project_root = str(Path(board_yaml_path).parent).replace("\\", "/")
295
+
296
+ if project_root_diverges_from_exec_base(project_root, exec_base):
297
+ raise TokenSubstitutionError(
298
+ "build.project-root-mismatch",
299
+ f"plan is `planPathMode: tokened`, but ${{PROJECT_ROOT}} (`{project_root}`, the "
300
+ f"board.yaml directory) differs from where tan actually runs each slice's "
301
+ f"command (`{exec_base}`) -- refusing to build against the wrong tree. This only "
302
+ f"happens with a non-default `west_cwd`/`--project` split; align them, or drop "
303
+ f"`planPathMode` from the plan.",
304
+ )
305
+
306
+ # ONE resolved sdk_root for the whole substitution. A tokened plan NEEDS
307
+ # a real ${SDK_ROOT} value: degrading an unresolved sdk_root to "" would
308
+ # substitute ${SDK_ROOT}/scripts into the bare relative path /scripts,
309
+ # sailing straight past the leftover-token guard -- refuse instead.
310
+ if sdk_root is None:
311
+ # `tan sdk switch` refuses in this build (tan-cli#305) -- kept the
312
+ # two mechanisms `resolve_sdk_root_ladder` (this value's caller)
313
+ # actually walks (`--sdk-root`, the lateral discovery tail) and
314
+ # swapped the third for NO_SDK_NEXT_STEPS's honest "how to get a
315
+ # checkout at all".
316
+ raise TokenSubstitutionError(
317
+ "build.sdk-root-unresolved",
318
+ "plan is `planPathMode: tokened` (needs ${SDK_ROOT} substituted with a real "
319
+ "path), but no alp-sdk checkout resolved -- pass `--sdk-root <PATH>`, run from "
320
+ f"a project near an alp-sdk checkout, or {NO_SDK_NEXT_STEPS}.",
321
+ )
322
+
323
+ # The two-SDK split-brain guard: whenever the plan carries a sdkCommit,
324
+ # a mismatch against the resolved SDK checkout's actual HEAD means tan
325
+ # is about to build against a DIFFERENT SDK checkout/SHA than the plan
326
+ # was emitted from.
327
+ if plan.sdk_commit:
328
+ resolved = git_short_head(Path(sdk_root))
329
+ if sdk_commit_mismatches(plan.sdk_commit, resolved):
330
+ raise TokenSubstitutionError(
331
+ "build.sdk-commit-mismatch",
332
+ f"plan was emitted from alp-sdk commit `{plan.sdk_commit}`, but the resolved "
333
+ f"SDK checkout is at `{resolved}` -- building against a different SDK "
334
+ f"checkout than the plan was captured from can silently produce the wrong "
335
+ f"image. Re-emit the plan from the current SDK checkout, or point "
336
+ f"`--sdk-root` at the checkout the plan names.",
337
+ )
338
+
339
+ values = TokenValues(
340
+ sdk_root=sdk_root, project_root=project_root, python=python, toolchain_root=toolchain_root
341
+ )
342
+ policy = DeferredPolicy(plan.deferred_placeholders)
343
+ try:
344
+ out, demoted = substitute_plan_tokens(plan, values, policy)
345
+ except LeftoverToken as e:
346
+ raise TokenSubstitutionError(
347
+ "build.plan-token-unresolved",
348
+ f"plan is `planPathMode: tokened` but field `{e.field}` still names the literal "
349
+ f"token `{e.token}` after substitution -- an SDK-side token this CLI does not "
350
+ f"resolve (only ${{SDK_ROOT}}, ${{PROJECT_ROOT}}, ${{PYTHON}}, ${{TOOLCHAIN_ROOT}} "
351
+ f"are known). Upgrade tan, or check the plan for a bug."
352
+ + _deferred_extra_note(plan, e, policy),
353
+ ) from e
354
+ except UnresolvedToolchainRoot as e:
355
+ raise TokenSubstitutionError(
356
+ "build.toolchain-root-unresolved",
357
+ f"plan field `{e.field}` names ${{TOOLCHAIN_ROOT}}, but {toolchain_advice}. "
358
+ f"tan refuses rather than substituting an empty path, which would silently build "
359
+ f"against the host root.",
360
+ ) from e
361
+ except UnknownPlanPathMode as e:
362
+ raise TokenSubstitutionError("build.plan-invalid", str(e)) from e
363
+
364
+ if deferred_out is not None:
365
+ deferred_out.extend(policy.exempted)
366
+
367
+ # alp-sdk#866: a sysbuild slice's `-D<image>_EXTRA_CONF_FILE` prefix was
368
+ # derived from the project root the plan was EMITTED in; re-derive it from
369
+ # the substituted app dir so a relocated plan still configures its image
370
+ # (see `tan.core.sysbuild_image` for why this rewrites rather than refuses).
371
+ out = rescope_sysbuild_extra_conf(out)
372
+
373
+ slice_demotions = [
374
+ SliceDemotion(
375
+ slice_index=d.slice_index,
376
+ core_id=d.core_id,
377
+ reason=f"plan field `{d.field}` names ${{TOOLCHAIN_ROOT}}, but {toolchain_advice}",
378
+ )
379
+ for d in demoted
380
+ ]
381
+ return out, slice_demotions