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,482 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Host toolchain-root resolution for build-plan `${TOOLCHAIN_ROOT}`
3
+ substitution -- the Python port of `crate::toolchain::resolve_toolchain_root`
4
+ (tan-cli#547).
5
+
6
+ `${TOOLCHAIN_ROOT}` was a RECOGNISED token on the Python line with no
7
+ resolver behind it: `build_cmd.py` passed `toolchain_root=None`
8
+ unconditionally, so every slice naming the token demoted on EVERY host --
9
+ including hosts that have a perfectly good toolchain -- rather than only
10
+ where the host genuinely has none. That is what this module closes.
11
+
12
+ ## The contract, MEASURED against the oracle, never read out of `crates/`
13
+
14
+ Established by running `target/debug/tan` (`tan 0.4.1`) with a
15
+ `planPathMode: tokened` plan whose one `configArtefacts[].contents` is
16
+ `TOOLCHAIN_ROOT=${TOOLCHAIN_ROOT}`, under `--materialise`, and reading back
17
+ the file it wrote (or the `issues[]` entry when it wrote nothing):
18
+
19
+ 1. `ZEPHYR_SDK_INSTALL_DIR`, when set, non-empty, and naming a path that
20
+ EXISTS, wins outright and is returned VERBATIM. Measured: a trailing
21
+ slash survives into the substituted value (`/home/x/zephyr-sdk-1.0.1/`),
22
+ and a directory that exists but contains no toolchain at all is still
23
+ accepted -- the oracle probes existence, not contents.
24
+ 2. Set but naming a path that does NOT exist -- and the empty string --
25
+ fall THROUGH to the scan rather than failing. Measured: with a real
26
+ install under `$HOME` and `ZEPHYR_SDK_INSTALL_DIR` pointing at a
27
+ nonexistent directory, the oracle resolved the scanned install.
28
+ 3. Otherwise every entry named `zephyr-sdk*` DIRECTLY under a scan root is
29
+ a candidate. Prefix match, not a `zephyr-sdk-<version>` shape:
30
+ `zephyr-sdkXYZ` was accepted. Direct children only: an install one
31
+ level deeper (`$HOME/tools/zephyr-sdk-1.0.1`) was NOT found.
32
+ 4. Exactly one candidate resolves. ZERO is unresolved with
33
+ `NO_TOOLCHAIN_ADVICE`. TWO OR MORE is ALSO unresolved -- with a
34
+ different, sharper reason (`_several_installs_advice`) that names every
35
+ candidate, SORTED (measured: created in `zzz, mmm, aaa` readdir order,
36
+ reported `aaa, mmm, zzz`). That second reason had no Python equivalent
37
+ at all before this module; the port collapsed both cases onto the
38
+ "no toolchain" wording.
39
+
40
+ ## The two DELIBERATE divergences from the oracle, and why
41
+
42
+ * **A candidate must be a DIRECTORY.** The oracle accepts a plain FILE
43
+ named `zephyr-sdk-*` as a toolchain root (measured: a `touch`ed
44
+ `$HOME/zephyr-sdk-1.0.1` resolved). That is not a wart with no
45
+ consequence: `"zephyr-sdk-0.16.5_linux-x86_64.tar.xz".startswith(
46
+ "zephyr-sdk")` is true, so the downloaded ARCHIVE sitting in `$HOME`
47
+ beside the install it was extracted from makes the host look like it has
48
+ TWO installs -- and rule 4 above then demotes every slice on a host that
49
+ has exactly one working toolchain. Requiring a directory can only ever
50
+ REMOVE a false candidate, never add one, so it cannot cause the
51
+ demotion this issue is about.
52
+ * **The scan roots come from `HOME`, `USERPROFILE` and `Path.home()`, not
53
+ `HOME` alone.** Measured, the oracle reads `$HOME` only: with `HOME`
54
+ pointed at an SDK-less directory and `USERPROFILE` at the real one, it
55
+ resolved nothing. Reproducing that exactly would re-open the defect
56
+ `doctor_cmd._zephyr_sdk_scan_roots` was written for -- under Git
57
+ Bash/MSYS on Windows `HOME` is a POSIX-translated path (`/c/Users/dev`)
58
+ while the SDK sits under the native `%USERPROFILE%`, and a real host was
59
+ measured reporting "no SDK" with the SDK installed. On any ordinary
60
+ POSIX host the two sets are identical, so the measured Linux parity
61
+ above still holds byte-for-byte.
62
+ * **The ADR 0021 artifact-keyed store (`tan.core.toolchain_provision.
63
+ resolve_toolchain_root`, `~/.alp/toolchains` or `$ALP_TOOLCHAIN_ROOT`) is
64
+ ALSO scanned, one level in, for its own `zephyr-sdk*`-prefixed leaf
65
+ directories.** The v0.4.1 oracle predates issue #474 entirely and has no
66
+ opinion on this store -- there is nothing to diverge FROM here, only a
67
+ gap `tan-cli#990` review closed: `tan bootstrap`'s own toolchain phase
68
+ installs into `<store-root>/zephyr-sdk-<version>-arm-zephyr-eabi/`, two
69
+ directory levels below every root this function used to look at, so a
70
+ customer who ran nothing but `tan bootstrap` (the whole point of issue
71
+ #474) still could not get `${TOOLCHAIN_ROOT}` to resolve to what it just
72
+ downloaded without ALSO hand-exporting `ZEPHYR_SDK_INSTALL_DIR` --
73
+ keeping the very manual step ADR 0021 Lane 1 P1 exists to remove. Per
74
+ alp-sdk#1286's own closing decision ("never a location; resolving it to
75
+ a concrete path is the executor's job, not the plan's"), tan is that
76
+ executor, and this is that resolution. A `.tmp-<pid>` wreckage sibling
77
+ (`toolchain_provision.TMP_SUFFIX_PREFIX`) from an interrupted attempt is
78
+ excluded by name in `_candidates()` below -- it also starts with
79
+ `zephyr-sdk`, and would otherwise fake a "several installs, ambiguous"
80
+ host next to the one real, verified store.
81
+
82
+ `doctor_cmd`'s scan is DUPLICATED here rather than imported: `doctor_cmd`
83
+ imports from `build_cmd`, and `build_cmd` imports this module, so the import
84
+ would be circular. (`token_substitution._is_own_git_checkout` is duplicated
85
+ from the same module for its own reason -- this is the second instance of
86
+ the same shape, not a new one.)
87
+
88
+ That circularity is about `_scan_roots`/`_candidates`/`resolve_toolchain_root`
89
+ specifically, not about every name in this module: `doctor_cmd` DOES import
90
+ [`_is_toolchain_wreckage`] and [`_toolchain_store_scan_root`] directly from
91
+ here (tan-cli#1186), since neither of those two touches `build_cmd` and the
92
+ cycle above never applies to them.
93
+
94
+ `doctor_cmd._zephyr_sdk_root_valid`'s stricter probe -- "does
95
+ `gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc` exist" -- is deliberately NOT
96
+ reused. Doctor answers a yes/no health question where a false PASS is the
97
+ harm (tan-cli#286). Here the harm runs the other way: a Zephyr SDK installed
98
+ with a non-ARM toolchain subset is a real, supported install, and refusing
99
+ it would demote every slice on that host -- which is precisely the defect
100
+ tan-cli#547 reports.
101
+ """
102
+ import os
103
+ from dataclasses import dataclass
104
+ from pathlib import Path
105
+
106
+ from tan.core import toolchain_provision as _tp
107
+
108
+ #: What every candidate directory's name must start with. A PREFIX, not a
109
+ #: `zephyr-sdk-<version>` pattern -- measured against the oracle, which
110
+ #: accepted a directory named `zephyr-sdkXYZ`. Matches BOTH a manual
111
+ #: `zephyr-sdk-1.0.1` install AND `tan.core.toolchain_provision.
112
+ #: store_dir_name`'s `zephyr-sdk-<version>-arm-zephyr-eabi` leaf -- no
113
+ #: change needed here for the artifact-keyed store below, only to WHERE it
114
+ #: is looked for.
115
+ ZEPHYR_SDK_DIR_PREFIX = "zephyr-sdk"
116
+
117
+ #: The env var that overrides the scan outright, and the one every reason
118
+ #: string below tells the caller to set.
119
+ ZEPHYR_SDK_INSTALL_DIR = "ZEPHYR_SDK_INSTALL_DIR"
120
+
121
+ #: The reason a `${TOOLCHAIN_ROOT}` demotion (or the plan-fatal
122
+ #: `boardYaml`/`sharedArtefacts` refusal) carries when this host has NO
123
+ #: toolchain at all. Verbatim the oracle's wording, in this port's ASCII
124
+ #: dash convention. Lives here rather than in `token_substitution.py` so the
125
+ #: resolver owns both of its own reasons; that module imports it.
126
+ NO_TOOLCHAIN_ADVICE = (
127
+ "no toolchain install is detectable on this host -- install the Zephyr SDK "
128
+ "(`west sdk install`) or set `ZEPHYR_SDK_INSTALL_DIR` to an existing install"
129
+ )
130
+
131
+
132
+ def _several_installs_advice(candidates: list[str]) -> str:
133
+ """The reason for the AMBIGUOUS host -- several installs and nothing
134
+ picking between them. Sharper than `NO_TOOLCHAIN_ADVICE` because the fix
135
+ is different: nothing needs installing, one of these needs choosing.
136
+ Names every candidate so the caller can paste one straight into
137
+ `ZEPHYR_SDK_INSTALL_DIR`."""
138
+ return (
139
+ f"this host has several toolchain installs and no `{ZEPHYR_SDK_INSTALL_DIR}` to "
140
+ f"choose between them ({', '.join(candidates)}) -- set `{ZEPHYR_SDK_INSTALL_DIR}` "
141
+ f"to the one this build should use"
142
+ )
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class ToolchainResolution:
147
+ """`root` is the host toolchain root, or `None` when this host has no
148
+ single unambiguous one. `advice` is the reason for that `None`, threaded
149
+ into the demotion message so the two unresolved cases -- none installed
150
+ vs several installed -- read differently. `advice` is still populated
151
+ when `root` resolved (nothing reads it then); it is not a nullable
152
+ second signal for the same fact."""
153
+
154
+ root: str | None
155
+ advice: str = NO_TOOLCHAIN_ADVICE
156
+
157
+
158
+ def _scan_roots() -> list[Path]:
159
+ """Every directory scanned for a `zephyr-sdk*` install: `/opt`, plus
160
+ `$HOME`, `%USERPROFILE%` AND `Path.home()` -- ALL of them, never
161
+ `HOME or USERPROFILE`. See this module's docstring for the measured Git
162
+ Bash/MSYS host that makes the union load-bearing rather than defensive.
163
+
164
+ Deduplicated on the raw string only; identical directories reached by
165
+ different spellings are collapsed later, on the CANDIDATES, where a
166
+ duplicate would otherwise fake an ambiguous host."""
167
+ roots = [Path("/opt")]
168
+ seen: set[str] = set()
169
+ for raw in (os.environ.get("HOME"), os.environ.get("USERPROFILE")):
170
+ if raw and raw not in seen:
171
+ seen.add(raw)
172
+ roots.append(Path(raw))
173
+ try:
174
+ home = Path.home()
175
+ except (OSError, RuntimeError):
176
+ home = None
177
+ if home is not None and str(home) not in seen:
178
+ roots.append(home)
179
+ return roots
180
+
181
+
182
+ def _toolchain_store_scan_root() -> Path:
183
+ """`~/.alp/toolchains` (or `$ALP_TOOLCHAIN_ROOT`) -- the ADR 0021
184
+ artifact-keyed store `tan bootstrap`'s own toolchain phase installs
185
+ into. Delegates to `toolchain_provision.resolve_toolchain_root`, the
186
+ SAME function `bootstrap_cmd._toolchain_root_and_leaf` calls, so this
187
+ can never compute a different root than the one bootstrap actually used
188
+ -- the two-levels-deep discovery gap tan-cli#990 review found was a
189
+ location bug, not a fact this module should ever re-derive its own
190
+ opinion of.
191
+
192
+ `home_alp_dir` is duplicated inline (`~/.alp`, `USERPROFILE` on Windows
193
+ else `HOME`) rather than importing `tan.core.sdk_discovery._home_alp_dir`
194
+ -- this module already duplicates `doctor_cmd`'s scan for the identical
195
+ reason (see the module docstring): one two-line duplication is cheaper
196
+ than auditing this module's own import graph for a cycle every time the
197
+ canonical helper's home changes.
198
+ """
199
+ home = os.environ.get("USERPROFILE" if os.name == "nt" else "HOME")
200
+ home_alp_dir = str(Path(home or ".") / ".alp")
201
+ root = _tp.resolve_toolchain_root(os.environ.get("ALP_TOOLCHAIN_ROOT"), home_alp_dir)
202
+ return Path(root.path_str)
203
+
204
+
205
+ def _read_text_or_none(path: Path) -> str | None:
206
+ """`None` on ANY read failure -- `OSError` (missing/unreadable/a
207
+ directory) AND `UnicodeDecodeError` (a subclass of `ValueError`) alike.
208
+
209
+ tan-cli#1209 review MAJOR: `verified_store_dir`'s two callers of this
210
+ used to write `path.read_text(encoding="utf-8")` under a bare
211
+ `except OSError`, so one non-UTF-8 byte in either
212
+ `<sdk_root>/metadata/toolchains.json` or the store's own
213
+ `.alp-toolchain-stamp.json` raised `UnicodeDecodeError` straight past
214
+ this function's own "never raises" contract and out of `execute_slices`
215
+ before any slice dispatched -- with no envelope at all under
216
+ `--format json`. `errors="replace"` plus `except (OSError, ValueError)`
217
+ mirrors `doctor_cmd._read_text` exactly (same two files, read from the
218
+ same two call sites in that module), so `tan build` and `tan doctor`
219
+ can no longer disagree on a corrupt manifest or stamp: neither raises,
220
+ and a mangled byte fails the JSON/shape parse that follows rather than
221
+ the read itself."""
222
+ try:
223
+ return path.read_text(encoding="utf-8", errors="replace")
224
+ except (OSError, ValueError):
225
+ return None
226
+
227
+
228
+ def _read_manifest(sdk_root: str | None) -> _tp.ToolchainManifest | None:
229
+ """@sdk_root's `metadata/toolchains.json`, parsed -- `None` on no
230
+ `sdk_root`, an unreadable file, or a malformed manifest. Shared by
231
+ [`verified_store_dir`] and [`host_scan_has_toolchain`] so the two can
232
+ never independently disagree on what THIS checkout's pin says; never
233
+ raises, matching every other reader in this module."""
234
+ if sdk_root is None:
235
+ return None
236
+ manifest_path = Path(sdk_root) / "metadata" / "toolchains.json"
237
+ manifest_text = _read_text_or_none(manifest_path)
238
+ if manifest_text is None:
239
+ return None
240
+ try:
241
+ return _tp.parse_toolchain_manifest(manifest_text)
242
+ except _tp.ToolchainManifestError:
243
+ return None
244
+
245
+
246
+ def _toolchain_store_dir(manifest: _tp.ToolchainManifest) -> Path:
247
+ """`<store-root>/<per-version leaf>` for @manifest -- the ONE directory
248
+ tan's own installs use for this exact pin. Mirrors
249
+ `doctor_cmd._toolchain_store_dir` byte-for-byte (same formula: `_
250
+ toolchain_store_scan_root() / store_dir_name(manifest.version)`) so the
251
+ two modules can never independently compute a different answer for
252
+ "which directory is tan's own install for this pin".
253
+
254
+ [`verified_store_dir`] and [`host_scan_has_toolchain`] both key their
255
+ store exclusion off THIS leaf, never the whole `_toolchain_store_
256
+ scan_root()` -- tan-cli#1209 review BLOCKER. See `doctor_cmd.
257
+ _host_toolchain_matching_pin`'s docstring for why the narrower leaf,
258
+ not the whole configured root, is the correct exclusion under the ADR
259
+ 0021 `$ALP_TOOLCHAIN_ROOT`-pointed-at-an-ancestor escape hatch: a
260
+ stamped leaf for a DIFFERENT, no-longer-pinned version can sit right
261
+ next to the current one inside the same store, and is a real,
262
+ independent host toolchain that must not be hidden by excluding the
263
+ whole root."""
264
+ return _toolchain_store_scan_root() / _tp.store_dir_name(manifest.version)
265
+
266
+
267
+ def verified_store_dir(sdk_root: str | None) -> Path | None:
268
+ """tan's own ADR 0021 store for the checkout's PINNED cross-toolchain
269
+ version, iff `.alp-toolchain-stamp.json` there matches that pin -- the
270
+ identical pin+stamp predicate `bootstrap_cmd.toolchain_phase`
271
+ (skip-if-already-installed) and `doctor_cmd.toolchain_check` (the
272
+ `toolchain` check) already apply via `stamp_matches_pin`, reused here
273
+ rather than re-derived a third time (tan-cli#1209: `ZEPHYR_SDK_INSTALL_DIR`
274
+ handoff into a spawned build's env).
275
+
276
+ `None` on ANY of: no `sdk_root`, an unreadable/malformed
277
+ `metadata/toolchains.json`, no stamp file, or a stamp that does not
278
+ match -- never raises, matching every other reader in this module.
279
+
280
+ Deliberately NOT [`resolve_toolchain_root`] (this module's
281
+ `${TOOLCHAIN_ROOT}` resolver): that scans every `zephyr-sdk*` under
282
+ `_scan_roots()` PLUS this store and refuses outright on ambiguity
283
+ ("several installs"). A caller wiring `ZEPHYR_SDK_INSTALL_DIR` for a
284
+ spawned child wants tan's own stamped, digest-verified store for
285
+ EXACTLY this pin -- never a scan that can be defeated by an unrelated
286
+ hand-installed SDK sitting elsewhere on the same host.
287
+ """
288
+ manifest = _read_manifest(sdk_root)
289
+ if manifest is None:
290
+ return None
291
+ store_dir = _toolchain_store_dir(manifest)
292
+ stamp_text = _read_text_or_none(store_dir / _tp.STAMP_FILENAME)
293
+ if stamp_text is None:
294
+ return None
295
+ stamp = _tp.parse_stamp(stamp_text)
296
+ if not _tp.stamp_matches_pin(stamp, manifest):
297
+ return None
298
+ return store_dir
299
+
300
+
301
+ def _is_toolchain_wreckage(name: str) -> bool:
302
+ """`True` for a `.tmp-<pid>` sibling of an interrupted acquisition
303
+ (`toolchain_provision.TMP_SUFFIX_PREFIX`) -- it starts with
304
+ `zephyr-sdk` too (it is `<leaf><TMP_SUFFIX_PREFIX><pid>` and `leaf`
305
+ itself starts with `zephyr-sdk`), so without this it would fake a
306
+ SECOND, ambiguous candidate next to the one real, verified store entry
307
+ every time `tan bootstrap` was interrupted mid-acquisition and its own
308
+ startup reclaim (`bootstrap_cmd._reclaim_toolchain_wreckage`) has not
309
+ yet run again."""
310
+ return _tp.TMP_SUFFIX_PREFIX in name
311
+
312
+
313
+ def _candidates(roots: list[Path] | None = None) -> list[str]:
314
+ """Every distinct `zephyr-sdk*` install directory found under @roots, as
315
+ POSIX-separator strings, SORTED. @roots defaults to `_scan_roots()` PLUS
316
+ `_toolchain_store_scan_root()` -- the full set `resolve_toolchain_root`
317
+ needs. [`host_scan_has_toolchain`] passes `_scan_roots()` alone, to ask
318
+ the narrower "does the HOST carry one, never mind tan's own store"
319
+ question.
320
+
321
+ Deduplicated on `Path.resolve()`, keeping the FIRST literal spelling
322
+ seen. This is the guard that stops `_scan_roots()`'s deliberate overlap
323
+ from breaking the caller: `$HOME` and `Path.home()` routinely name the
324
+ same directory (and a symlinked or trailing-slash `$HOME` slips past
325
+ `_scan_roots`'s own string-level dedup), which would list one install
326
+ twice and demote every slice on a host with exactly one toolchain --
327
+ the very failure tan-cli#547 is about.
328
+
329
+ `as_posix()`, not `str()`: identical on POSIX (so the measured oracle
330
+ parity holds byte-for-byte) and the separator convention
331
+ `token_substitution.py` already normalises `${PROJECT_ROOT}` to, so a
332
+ substituted `${TOOLCHAIN_ROOT}` does not arrive at CMake in the one
333
+ spelling `${PROJECT_ROOT}` never uses.
334
+
335
+ Never raises: an unreadable or missing scan root is "nothing found
336
+ there", not a failed build.
337
+ """
338
+ if roots is None:
339
+ roots = [*_scan_roots(), _toolchain_store_scan_root()]
340
+ found: dict[str, str] = {}
341
+ for root in roots:
342
+ try:
343
+ entries = sorted(root.iterdir())
344
+ except OSError:
345
+ continue
346
+ for entry in entries:
347
+ if not entry.name.startswith(ZEPHYR_SDK_DIR_PREFIX):
348
+ continue
349
+ if _is_toolchain_wreckage(entry.name):
350
+ continue
351
+ try:
352
+ if not entry.is_dir():
353
+ continue
354
+ key = str(entry.resolve())
355
+ except OSError:
356
+ continue
357
+ found.setdefault(key, entry.as_posix())
358
+ return sorted(found.values())
359
+
360
+
361
+ #: Duplicated from `doctor_cmd.ZEPHYR_SDK_TOOLCHAIN_DIR` -- see this
362
+ #: module's own docstring on why `doctor_cmd` is not imported here.
363
+ _ZEPHYR_SDK_TOOLCHAIN_DIR = ("gnu", "arm-zephyr-eabi", "bin")
364
+
365
+
366
+ def _host_toolchain_is_usable(root: Path) -> bool:
367
+ """`True` when @root actually contains the `arm-zephyr-eabi` cross
368
+ compiler, not merely a directory named right. Duplicated from
369
+ `doctor_cmd._zephyr_sdk_root_valid` (import cycle, see module
370
+ docstring) for [`host_scan_has_toolchain`] ALONE -- never for
371
+ `_candidates`/`resolve_toolchain_root` above, which deliberately accept
372
+ a name-only or non-ARM directory as a real, substitutable toolchain
373
+ (see this module's docstring on the two deliberate divergences).
374
+
375
+ `host_scan_has_toolchain` asks a different question -- "does the host
376
+ already have ITS OWN working toolchain, independent of tan's store" --
377
+ and a directory that merely STARTS WITH `zephyr-sdk`, with no compiler
378
+ inside it, answers "no", not "yes" (tan-cli#1209 review MAJOR: an empty
379
+ `~/zephyr-sdk-leftover/` -- a stale download, an interrupted manual
380
+ extraction -- was silently disabling tan's own verified store on any
381
+ host carrying one, because `_candidates` applies no validity probe by
382
+ design)."""
383
+ exe = "arm-zephyr-eabi-gcc.exe" if os.name == "nt" else "arm-zephyr-eabi-gcc"
384
+ try:
385
+ return root.joinpath(*_ZEPHYR_SDK_TOOLCHAIN_DIR, exe).is_file()
386
+ except OSError:
387
+ return False
388
+
389
+
390
+ def host_scan_has_toolchain(sdk_root: str | None) -> bool:
391
+ """`True` when a USABLE `zephyr-sdk*` directory is visible under
392
+ `_scan_roots()` -- CMake's own `FindZephyr-sdk.cmake` prefix-scan
393
+ territory -- that is NOT itself tan's own ADR 0021 store leaf for
394
+ @sdk_root's pinned version.
395
+
396
+ tan-cli#1209 review MINOR: `verified_store_dir`'s caller
397
+ (`zephyr_env_overrides`, wired from `execute.py`) used to fill
398
+ `ZEPHYR_SDK_INSTALL_DIR` from tan's own store whenever it verified,
399
+ full stop -- outranking CMake's own prefix scan and the CMake user
400
+ package registry for EVERY slice, on EVERY host. `doctor_cmd.
401
+ _zephyr_sdk_scan_roots` never does that: it lists `_toolchain_store_
402
+ scan_root()` LAST among its scan roots, so a full, pre-existing host
403
+ SDK always wins there. tan's own store is installed `-t
404
+ arm-zephyr-eabi` ONLY, so forcing it into a spawned child's env ahead
405
+ of a fuller host SDK can fail a non-ARM slice (e.g.
406
+ `aarch64-zephyr-elf`) that configured fine without any
407
+ `ZEPHYR_SDK_INSTALL_DIR` override at all, and makes `tan build` use a
408
+ different toolchain than `tan doctor` reports. `execute_slices` calls
409
+ this before deciding whether to pass `verified_store_dir`'s result
410
+ through at all, so tan's own store fills the gap only when the host
411
+ scan finds nothing else -- the same "last resort" precedence doctor
412
+ already gives it.
413
+
414
+ The store exclusion is keyed on `_toolchain_store_dir(manifest)` -- the
415
+ one PER-VERSION leaf tan's own install for THIS pin uses -- never the
416
+ whole `_toolchain_store_scan_root()` (tan-cli#1209 review BLOCKER: this
417
+ used to key on the whole root, the exact form `doctor_cmd.
418
+ _host_toolchain_matching_pin`'s own docstring documents as wrong, and the
419
+ same mistake tan-cli#1186 already shipped and had to fix once).
420
+ Excluding the whole root hides every OTHER leaf living in the same
421
+ store too -- e.g. a stamped leaf for a version this checkout no longer
422
+ pins, still a real, independent, usable toolchain -- as well as
423
+ misreading path containment under the ADR 0021
424
+ `$ALP_TOOLCHAIN_ROOT`-pointed-at-an-ancestor escape hatch (`$HOME` or
425
+ `/opt`, ADR 0021's own documented bench/CI case), where the whole store
426
+ root coincides with a `_scan_roots()` root and would otherwise
427
+ swallow a genuinely independent, adjacent hand-install along with tan's
428
+ own leaf. `sdk_root=None` (no checkout resolved, no pin to protect)
429
+ excludes nothing -- matching `verified_store_dir(None)`, which returns
430
+ `None` regardless of this function's answer in that case.
431
+
432
+ Never raises: an unresolvable path reads as itself, matching
433
+ `_candidates`'s own `except OSError` fallbacks."""
434
+ manifest = _read_manifest(sdk_root)
435
+ store_leaf: Path | None = None
436
+ if manifest is not None:
437
+ try:
438
+ store_leaf = _toolchain_store_dir(manifest).resolve()
439
+ except OSError:
440
+ store_leaf = _toolchain_store_dir(manifest)
441
+ for candidate in _candidates(_scan_roots()):
442
+ entry = Path(candidate)
443
+ try:
444
+ resolved = entry.resolve()
445
+ except OSError:
446
+ resolved = entry
447
+ if store_leaf is not None and resolved.is_relative_to(store_leaf):
448
+ continue
449
+ if _host_toolchain_is_usable(resolved):
450
+ return True
451
+ return False
452
+
453
+
454
+ def resolve_toolchain_root() -> ToolchainResolution:
455
+ """This host's `${TOOLCHAIN_ROOT}`, or the reason there is not exactly
456
+ one. See the module docstring for the measured oracle contract each
457
+ branch below reproduces.
458
+
459
+ Called LAZILY by `build_cmd` -- only for a plan that actually names the
460
+ token -- so the filesystem scan never runs for the plans every SDK emits
461
+ today, which name none. The laziness is a property of the CALL SITE, not
462
+ of this function: it does the work it is asked for, every time.
463
+ """
464
+ env_dir = os.environ.get(ZEPHYR_SDK_INSTALL_DIR)
465
+ if env_dir:
466
+ try:
467
+ exists = Path(env_dir).exists()
468
+ except OSError:
469
+ exists = False
470
+ if exists:
471
+ # VERBATIM, not normalised: measured, the oracle substitutes the
472
+ # env value exactly as given, trailing slash and all. An operator
473
+ # who pinned this variable pinned a literal path, and rewriting
474
+ # it would make the substituted value disagree with what they set.
475
+ return ToolchainResolution(env_dir)
476
+
477
+ candidates = _candidates()
478
+ if len(candidates) == 1:
479
+ return ToolchainResolution(candidates[0])
480
+ if not candidates:
481
+ return ToolchainResolution(None, NO_TOOLCHAIN_ADVICE)
482
+ return ToolchainResolution(None, _several_installs_advice(candidates))
@@ -0,0 +1,115 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Per-slice reaction to a changed user `-D` set (tan-cli#1382).
3
+
4
+ Zephyr caches SHIELD / SNIPPET and friends (`zephyr_check_cache(... WATCH)`
5
+ restores them from `CACHED_*` and refuses a value change short of a pristine
6
+ build; sysbuild images keep their own caches), and `-U` fixes none of that. So a
7
+ changed `-D` set on an already-configured slice wipes that slice's build dir --
8
+ behind the same two structural guards as tan's other wipe (no `-d`/`--build-dir`
9
+ override, cwd under `build/`).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import shutil
15
+ from collections.abc import Callable
16
+ from pathlib import Path
17
+
18
+ from tan.commands.build.configure_inputs import read_user_defines_stamp, write_user_defines_stamp
19
+ from tan.commands.build.manifest import cmake_cache_configured
20
+ from tan.core.user_defines import changed_defines
21
+ from tan.envelope import Issue
22
+
23
+ #: A non-empty cached SHIELD / SNIPPET: set by SOME earlier configure.
24
+ _CACHED_SELECTION = re.compile(r"^CACHED_(?:SHIELD|SNIPPET)(?::\w+)?=.+$", re.MULTILINE)
25
+
26
+
27
+ def _cache_holds_selection(cwd: Path) -> bool:
28
+ try:
29
+ text = (cwd / "build" / "CMakeCache.txt").read_text(encoding="utf-8", errors="replace")
30
+ except OSError:
31
+ return False
32
+ return _CACHED_SELECTION.search(text) is not None
33
+
34
+
35
+ def _unsafe_wipe_reason(cwd: Path, build_root: Path) -> str | None:
36
+ """Why `cwd/build` must not be removed, or None when it is safe: the slice
37
+ dir must resolve strictly inside `<build_root>/build`, and neither it nor
38
+ its `build` may be a symlink (rmtree would follow or refuse)."""
39
+ try:
40
+ root = (build_root / "build").resolve()
41
+ resolved = cwd.resolve()
42
+ except OSError as err:
43
+ return f"could not resolve the build dir ({err.strerror or type(err).__name__})"
44
+ if resolved == root or not resolved.is_relative_to(root):
45
+ return f"build dir resolves outside {build_root / 'build'}"
46
+ if cwd.is_symlink() or (cwd / "build").is_symlink():
47
+ return "the build dir is a symlink; not wiping"
48
+ return None
49
+
50
+
51
+ def reconcile_user_defines(
52
+ core_id: str,
53
+ cwd: Path,
54
+ now: list[str],
55
+ *,
56
+ build_root: Path,
57
+ guards_ok: bool,
58
+ on_output: Callable[[str], None],
59
+ ) -> tuple[list[Issue], bool]:
60
+ """Wipe `cwd/build` when the user `-D` set differs from the stamp, and
61
+ return `(issues, stampable)`. `stampable` is False only when a change could
62
+ not be applied (a guard suppressed the wipe, or the wipe failed): the stamp
63
+ is then left alone so the next build still sees the change.
64
+
65
+ An UNSTAMPED configured dir (built before this stamp existed) is unknown,
66
+ not "no -D": it is wiped once only if its cache holds a SHIELD/SNIPPET or
67
+ the build now passes `-D`."""
68
+ if not cmake_cache_configured(cwd):
69
+ return [], True
70
+ before = read_user_defines_stamp(cwd)
71
+ changed = changed_defines(before or [], now)
72
+ if before is None and not changed and _cache_holds_selection(cwd):
73
+ changed = ["SHIELD/SNIPPET (cached by an earlier build)"]
74
+ if not changed:
75
+ return [], True
76
+ names = ", ".join(changed)
77
+ if not guards_ok:
78
+ message = (
79
+ f"{core_id}: user -D changed ({names}) but this slice's build dir is "
80
+ "redirected (`-d`/`--build-dir`, or a cwd outside `build/`), so tan did "
81
+ "not wipe it: the cache may still hold stale values -- use a fresh build dir"
82
+ )
83
+ on_output(f"note: {message}")
84
+ return [Issue("build.configure-cache-stale", "warning", message)], False
85
+ refusal = _unsafe_wipe_reason(cwd, build_root)
86
+ if refusal is not None:
87
+ message = (
88
+ f"{core_id}: user -D changed ({names}) but tan did not wipe the build dir: "
89
+ f"{refusal}. The cache may still hold stale values -- use a fresh build dir"
90
+ )
91
+ on_output(f"note: {message}")
92
+ return [Issue("build.configure-cache-stale", "warning", message)], False
93
+ try:
94
+ shutil.rmtree(cwd / "build")
95
+ except OSError as err:
96
+ detail = err.strerror or str(err.args[0] if err.args else type(err).__name__)
97
+ message = (
98
+ f"{core_id}: user -D changed ({names}) but the build dir could not be wiped: {detail}"
99
+ )
100
+ on_output(f"note: {message}")
101
+ return [Issue("build.configure-cache-stale", "warning", message)], False
102
+ message = (
103
+ f"{core_id}: user -D changed ({names}) -- wiped this slice's build dir so Zephyr "
104
+ "re-configures from scratch instead of keeping the cached value (tan-cli#1382)"
105
+ )
106
+ on_output(f"note: {message}")
107
+ return [Issue("build.configure-cache-reset", "info", message)], True
108
+
109
+
110
+ def stamp_user_defines(cwd: Path, now: list[str]) -> None:
111
+ """Record what the configure about to run is given (best-effort)."""
112
+ try:
113
+ write_user_defines_stamp(cwd, now)
114
+ except OSError:
115
+ pass
@@ -0,0 +1,65 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan build`'s workspace-patch warning and `$ZEPHYR_BASE` note (tan-cli#1376).
3
+
4
+ Warning-only by design: a missing patch never changes `ok` or the exit code
5
+ (a workspace can be deliberately unpatched, and the check can be wrong about a
6
+ narrow one). Promoting `build.workspace-patches-missing` to an error is a
7
+ one-line severity change here, left for a later decision.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+ from tan.commands.workspace_patch_check import MISSING, check_workspace_patches
15
+ from tan.core.venv import west_workspace_dir
16
+ from tan.core.west_patches import describe_unapplied, patch_fix_text, zephyr_base_note
17
+ from tan.envelope import Issue
18
+
19
+ #: The consumer build directory (`build_cmd.CONSUMER_BUILD_ROOT`; not imported
20
+ #: because `build_cmd` imports this module). `build_root` here is the project
21
+ #: root, so the cache belongs under `<project>/build/`, not beside board.yaml.
22
+ _BUILD_DIR = "build"
23
+
24
+
25
+ def workspace_patch_issues(
26
+ build_root: Path, sdk_root: str | None, *, has_zephyr_slice: bool
27
+ ) -> list[Issue]:
28
+ """Issues for the resolved workspace of a plan with a Zephyr slice; never
29
+ raises, empty when there is nothing to say (including "could not check")."""
30
+ if not has_zephyr_slice:
31
+ return []
32
+ try:
33
+ workspace = west_workspace_dir(str(build_root), Path(sdk_root) if sdk_root else None)
34
+ if workspace is None:
35
+ return []
36
+ issues: list[Issue] = []
37
+ result = check_workspace_patches(
38
+ workspace, sdk_root, cache_dir=build_root / _BUILD_DIR
39
+ )
40
+ if result.state == MISSING:
41
+ issues.append(
42
+ Issue(
43
+ "build.workspace-patches-missing",
44
+ "warning",
45
+ f"alp-sdk's zephyr/patches.yml is not applied in {workspace}: "
46
+ f"{describe_unapplied(result.patches, result.modules)}. The build can "
47
+ "succeed yet fail at runtime (for example `alp_camera_open` returns "
48
+ f"ALP_ERR_NOSUPPORT). Fix: {patch_fix_text(result.modules, str(workspace))}.",
49
+ )
50
+ )
51
+ if result.cache_note:
52
+ issues.append(
53
+ Issue(
54
+ "build.workspace-patches-uncached",
55
+ "info",
56
+ "the workspace patch check is not cached, so every build "
57
+ f"re-verifies it: {result.cache_note}.",
58
+ )
59
+ )
60
+ note = zephyr_base_note(os.environ.get("ZEPHYR_BASE"), str(workspace / "zephyr"))
61
+ if note is not None:
62
+ issues.append(Issue("build.zephyr-base-ignored", "info", note))
63
+ return issues
64
+ except Exception: # noqa: BLE001 -- an advisory check must never break a build
65
+ return []