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,734 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan kconfig` -- the board-scoped Kconfig symbol menu for one core (the
3
+ vscode `prj.conf` LSP's live feed).
4
+
5
+ Port of `crates/tan-cli/src/commands/kconfig.rs`. **Not a second Kconfig
6
+ path**: the actual solve is `tan.planner.kconfig_symbols.emit_kconfig`, the
7
+ SAME in-process renderer `tan.planner_root.emit("kconfig", ...)` already
8
+ serves for `west alp-emit kconfig` parity (`tan.planner.cli.emit_artefact`'s
9
+ `mode == "kconfig"` branch) -- this module is composition only: resolve the
10
+ project/SDK/core, call that ONE renderer, and fold its JSON into the standard
11
+ envelope. Reimplementing the Kconfig solve here would be exactly the drift
12
+ the oracle's own module doc warns against (two copies that can disagree).
13
+
14
+ **`--core` defaulting is CLI-side**, mirroring `tan_core::kconfig::
15
+ resolve_default_kconfig_core`: when omitted, this reads board.yaml's own
16
+ `cores:` block DIRECTLY (a lenient, un-validated read -- `preset:`
17
+ cross-file cores are not expanded, matching the oracle's `parse_board_model`,
18
+ which does the same single-file read) and picks the one `os: zephyr` core;
19
+ zero or more than one is a named-candidates error, never a guess.
20
+ `emit_kconfig` itself always requires an explicit core.
21
+
22
+ **ZEPHYR_BASE resolution** (tan-cli#453) delegates to `tan.core.venv.
23
+ west_workspace_dir` -- the SAME shared resolver `build`/`flash`/
24
+ `west_forward_cmd` already consult, not a second, private copy. It used to be
25
+ one: a hand-rolled 3-tier ladder (upward `.west` walk, an already-exported
26
+ `ZEPHYR_BASE`, the SDK-derived layouts) that skipped the MANIFEST
27
+ verification (tan-cli#307) the shared resolver applies to tiers 1 and 2 --
28
+ so on a host with an unrelated ancestor `.west` checkout, or an ambient
29
+ `$ZEPHYR_BASE` naming a DIFFERENT Zephyr tree, this command picked that
30
+ wrong workspace instead of the one an ordinary `tan bootstrap` had already
31
+ built for THIS `--sdk-root`, or fell through to nothing. "Dead by default on
32
+ any host that has another Zephyr checkout around" -- verbatim the bug
33
+ report. `west_workspace_dir` returns the workspace TOPDIR; `zephyr_base` is
34
+ then `<topdir>/zephyr`, filtered to a real directory, mirroring
35
+ `build/execute.py`'s own tan-cli#308 derivation exactly (`workspace_dir /
36
+ "zephyr"`, `None` if it is not a directory yet) so the two can no longer
37
+ disagree about what counts as "bootstrapped".
38
+
39
+ The resolved base is INJECTED into `os.environ["ZEPHYR_BASE"]` around the
40
+ `_plan_emit` call (temporarily, restored after) rather than relied on
41
+ ambiently -- `tan.planner.kconfig_symbols` reads `os.environ["ZEPHYR_BASE"]`
42
+ directly (it renders in-process, so there is no child env to set it on the
43
+ way the Rust oracle sets it on a spawned command), so a tree- or
44
+ SDK-derived workspace would otherwise never reach the planner at all.
45
+
46
+ **The workspace `west` no longer depends on `PATH` at all** (tan-cli#453,
47
+ other half; tan-cli#459 review). The in-process renderer's own
48
+ workspace-dependent step (`kconfig_symbols._load_board_symbols`) used to
49
+ shell a BARE `west build` with no explicit `env=` -- unlike every spawn in
50
+ `build`/`flash`, it never resolved an absolute, venv-scoped `west` binary
51
+ before invoking it, so it depended entirely on whatever `west` PATH already
52
+ had, which is nothing: `tan bootstrap` installs `west` ONLY inside the
53
+ workspace-local venv, never system- or user-wide. An earlier version of this
54
+ fix prepended that venv's `bin`/`Scripts` dir onto `os.environ["PATH"]`
55
+ around the call here, in this module -- a mitigation scoped to `tan kconfig`
56
+ alone that left the same bare `"west"` in place one layer down. The real fix
57
+ is IN `kconfig_symbols._load_board_symbols`: it now resolves
58
+ `tan.core.venv.west_program` itself and spawns that absolute path, so
59
+ neither this module nor any other future caller needs to touch `PATH` for
60
+ it to work.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ import json as _json
66
+ import os
67
+ import sys
68
+ from pathlib import Path
69
+
70
+ import typer
71
+
72
+ from tan.commands.presets_cmd import resolve_project_paths, resolve_sdk
73
+ from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
74
+ from tan.core.sdk_discovery import with_sdk_search
75
+ from tan.core.global_flags import accept_global_flags
76
+ from tan.core.link_refusal import refusal_code
77
+ from tan.core.sdk_discovery import sdk_resolution_issues
78
+ from tan.core.venv import west_workspace_dir
79
+ from tan.envelope import Envelope, Issue, Project, SdkDisclosure, SdkInfo, emit
80
+ from tan.exit_codes import ExitCode
81
+ from tan.output_format import FORMAT_HELP, OutputFormat
82
+
83
+ #: `tan.planner.kconfig_symbols.SCHEMA_VERSION`, verbatim -- not imported
84
+ #: directly: every `tan.planner.*` submodule import runs the package's own
85
+ #: `__init__.py`, which needs `tan.planner_root.bind_sdk_root` to have
86
+ #: already happened (its `paths.py` binds `REPO = sdk_root()` at module
87
+ #: scope) -- pulling the constant in at THIS module's import time, before
88
+ #: any SDK is resolved, would be exactly that ordering trap.
89
+ KCONFIG_SCHEMA_VERSION = 1
90
+
91
+
92
+ class _CoreResolutionError(Exception):
93
+ """`--core` could not be resolved from board.yaml. Carries the envelope
94
+ issue code and message the caller reports verbatim."""
95
+
96
+ def __init__(self, code: str, message: str) -> None:
97
+ super().__init__(message)
98
+ self.code = code
99
+ self.message = message
100
+
101
+
102
+ def _yaml_type_name(value: object) -> str:
103
+ """serde_yaml-flavoured type name for a board-yaml-invalid message
104
+ (`invalid type: sequence, expected struct BoardModel`, verified against
105
+ `target/debug/tan.exe`)."""
106
+ if isinstance(value, list):
107
+ return "sequence"
108
+ if isinstance(value, bool):
109
+ return "boolean"
110
+ if isinstance(value, (int, float)):
111
+ return "number"
112
+ if isinstance(value, str):
113
+ return "string"
114
+ return type(value).__name__
115
+
116
+
117
+ def _resolve_core(core_arg: str | None, board_yaml: str) -> str:
118
+ """The explicit `--core`, or the board's one declared `os: zephyr` core.
119
+
120
+ Mirrors `tan_core::kconfig::resolve_core` (Rust `kconfig.rs`): a raw,
121
+ single-file read of board.yaml's `cores:` block -- no schema validation,
122
+ no preset expansion -- so this works even for a board.yaml the full
123
+ planner would refuse for an unrelated reason, exactly like the oracle.
124
+ """
125
+ if core_arg:
126
+ return core_arg
127
+ try:
128
+ text = Path(board_yaml).read_text(encoding="utf-8")
129
+ except (OSError, UnicodeDecodeError) as err:
130
+ # tan-cli#396: `UnicodeDecodeError` is a `ValueError`, NOT an `OSError`,
131
+ # so an `except OSError` alone could not fire for a board.yaml saved in
132
+ # cp1252/latin-1 (or with one stray byte pasted into a comment) -- it
133
+ # escaped as an unhandled traceback, rc 1 with ZERO bytes on stdout.
134
+ # That is the worst shape a machine interface has: the extension's two
135
+ # string matches against the envelope both fail open, so the `prj.conf`
136
+ # symbol menu renders empty or stale and never says why.
137
+ #
138
+ # Deliberately the SAME code and message an unreadable file gets,
139
+ # because the oracle reaches this one arm for both: Rust's
140
+ # `std::fs::read_to_string` returns an `io::Error` (kind `InvalidData`,
141
+ # "stream did not contain valid UTF-8") for non-UTF-8 bytes, and
142
+ # `crates/tan-cli/src/commands/kconfig.rs:156-161` maps every
143
+ # `read_to_string` error to `kconfig.board-yaml-missing`. Not
144
+ # `errors="replace"`: that would silently mangle the user's bytes and
145
+ # then resolve a core out of the mangled result.
146
+ #
147
+ # tan-cli#440: a SECOND `except UnicodeDecodeError:` used to follow
148
+ # this clause, raising `kconfig.board-yaml-invalid` for the same
149
+ # bytes. It was unreachable -- the tuple above already consumes every
150
+ # `UnicodeDecodeError` -- and it contradicted this one about which
151
+ # code the contract owes. Deleted rather than reordered: reordering
152
+ # would have made the WRONG code live. Re-measured against the frozen
153
+ # oracle before deleting, on a board.yaml carrying one 0xE9 byte:
154
+ #
155
+ # $ target/debug/tan kconfig --sdk-root <sdk> --format json # tan 0.4.1
156
+ # ... "issues":[{"code":"kconfig.board-yaml-missing","severity":"error",
157
+ # "message":"failed to read board.yaml at `<...>`: stream did not
158
+ # contain valid UTF-8"}] rc=2
159
+ #
160
+ # so `board-yaml-missing` is the contract and the deleted arm was the
161
+ # pre-#421 explanation left behind by that parity correction.
162
+ raise _CoreResolutionError(
163
+ "kconfig.board-yaml-missing",
164
+ f"failed to read board.yaml at `{board_yaml}`: {err}",
165
+ ) from err
166
+ import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see `new_som_cmd`'s
167
+ # `_yaml_scalar` for why this file must not
168
+ # import PyYAML at module scope.
169
+
170
+ try:
171
+ doc = yaml.safe_load(text)
172
+ except yaml.YAMLError as err:
173
+ raise _CoreResolutionError(
174
+ "kconfig.board-yaml-invalid",
175
+ f"failed to parse board.yaml at `{board_yaml}`: {err}",
176
+ ) from err
177
+ # `doc` not a mapping (a list-shaped board.yaml, e.g.) is a structural
178
+ # parse failure, not "no cores declared" -- the oracle's `parse_board_model`
179
+ # deserializes the WHOLE document into a typed struct, so a non-mapping
180
+ # top level (or a `cores:` block that isn't itself a mapping) fails there,
181
+ # never reaches core resolution at all. Falling through to the ambiguous
182
+ # branch here would tell the customer to pass `--core` for a file that
183
+ # will not parse, full stop -- same exit code, wrong issue code and wrong
184
+ # remedy. `doc is None` (an empty/`null` file) is legitimate -- the oracle's
185
+ # `Option<BoardModel>` parses that to the default model -- and keeps
186
+ # falling through to "no cores declared" below.
187
+ if doc is not None and not isinstance(doc, dict):
188
+ raise _CoreResolutionError(
189
+ "kconfig.board-yaml-invalid",
190
+ f"failed to parse board.yaml at `{board_yaml}`: board.yaml is not valid "
191
+ f"YAML: invalid type: {_yaml_type_name(doc)}, expected a mapping",
192
+ )
193
+ cores = doc.get("cores") if isinstance(doc, dict) else None
194
+ if cores is not None and not isinstance(cores, dict):
195
+ raise _CoreResolutionError(
196
+ "kconfig.board-yaml-invalid",
197
+ f"failed to parse board.yaml at `{board_yaml}`: board.yaml is not valid "
198
+ f"YAML: cores: invalid type: {_yaml_type_name(cores)}, expected a map",
199
+ )
200
+ cores = cores or {}
201
+ zephyr_ids = sorted(
202
+ cid
203
+ for cid, entry in cores.items()
204
+ if isinstance(entry, dict) and entry.get("os") == "zephyr"
205
+ )
206
+ if len(zephyr_ids) == 1:
207
+ return zephyr_ids[0]
208
+ hint = (
209
+ "board.yaml declares no cores"
210
+ if not cores
211
+ else f"declared cores: {', '.join(sorted(str(c) for c in cores))}"
212
+ )
213
+ raise _CoreResolutionError(
214
+ "kconfig.core-ambiguous",
215
+ f"--core <id> is required (board.yaml doesn't declare exactly one Zephyr "
216
+ f"core); {hint}",
217
+ )
218
+
219
+
220
+ def _resolve_zephyr_base(root: str, sdk_root: str | None) -> str | None:
221
+ """The bootstrapped `ZEPHYR_BASE`, or `None`. tan-cli#453: delegates to
222
+ `tan.core.venv.west_workspace_dir` -- the SAME manifest-verified resolver
223
+ `build`/`flash`/`west_forward_cmd` already share (see the module
224
+ docstring for why the private ladder this replaced was a real bug, not
225
+ just a duplicate) -- rather than a second, private tier ladder that could
226
+ drift from it again. `<topdir>/zephyr`, filtered to a real directory,
227
+ mirrors `build/execute.py`'s own tan-cli#308 derivation byte for byte.
228
+ """
229
+ workspace = west_workspace_dir(root, Path(sdk_root) if sdk_root else None)
230
+ if workspace is None:
231
+ return None
232
+ zephyr = workspace / "zephyr"
233
+ return str(zephyr) if zephyr.is_dir() else None
234
+
235
+
236
+ def _empty_data(core: str | None) -> dict:
237
+ return {
238
+ "schemaVersion": KCONFIG_SCHEMA_VERSION,
239
+ "board": "",
240
+ "core": core or "",
241
+ "symbols": [],
242
+ }
243
+
244
+
245
+ def _fail(
246
+ *,
247
+ root: str,
248
+ board_path: str,
249
+ exit_code: ExitCode,
250
+ code: str,
251
+ message: str,
252
+ core: str | None,
253
+ json_mode: bool,
254
+ sdk: SdkInfo | None = None,
255
+ sdk_issues: list[Issue] | None = None,
256
+ ) -> None:
257
+ """tan-cli#497 defect 2: this used to hardcode `[Issue(code, "error",
258
+ message)]`, so EVERY refusal dropped whatever `sdk.project-pin-unresolved`
259
+ / `sdk.global-default-foreign-project` the resolution had already
260
+ computed -- while `sdk current`, `presets`, `size` and `image` all
261
+ surfaced them from the identical workspace. `sdk_issues` is the pair from
262
+ the one shared `sdk_resolution_issues`, prepended (warnings first, the
263
+ order `flash`/`size`/`image` use), so no refusal path here can drop them
264
+ again. Printed in text mode too, `{severity}: {message}` -- the shape
265
+ `build_cmd`/`run` use -- because text is the DEFAULT mode and this
266
+ command's whole job is answering "which symbols does my board have",
267
+ solved out of a checkout the pin does not name."""
268
+ warnings = sdk_issues or []
269
+ if json_mode:
270
+ emit(
271
+ Envelope(
272
+ "kconfig",
273
+ Project.resolved(root, board_path),
274
+ _empty_data(core),
275
+ [*warnings, Issue(code, "error", message)],
276
+ exit_code,
277
+ sdk=sdk,
278
+ )
279
+ )
280
+ else:
281
+ for issue in warnings:
282
+ print(f"{issue.severity}: {issue.message}", file=sys.stderr)
283
+ print(f"kconfig: {message}", file=sys.stderr)
284
+ raise typer.Exit(int(exit_code))
285
+
286
+
287
+ class _KconfigShapeError(Exception):
288
+ """A `--emit kconfig` document decoded as JSON but does not match
289
+ `tan_core::KconfigData`/`KconfigSymbol` (`crates/tan-core/src/kconfig.rs`).
290
+
291
+ The oracle's `parse_kconfig` deserializes straight into those typed
292
+ structs, so a MISSING `board`/`core`/`symbols` key -- or a symbol missing
293
+ `name`/`type`/`prompt`/`depends`/`default`/`help` -- is a `serde_json`
294
+ deserialize error wrapped in the SAME `KconfigError::Json` variant a raw
295
+ JSON syntax error gets (`kconfig.rs`'s `deserialize_present_option` doc
296
+ comment: "a missing key is never legitimate input -- it means the SDK
297
+ renamed the field"). This carries only the `serde`-shaped detail text;
298
+ the caller wraps it in the identical `kconfig.parse-failed` code and
299
+ `"kconfig emit is not valid JSON: {0}"` message prefix a syntax error
300
+ gets, rather than inventing a second message shape the oracle has none
301
+ of.
302
+ """
303
+
304
+
305
+ def _validate_kconfig_data(data: dict) -> dict:
306
+ """Structurally validate an already-JSON-decoded emit body and rebuild
307
+ `data` from exactly the modelled fields (dropping any unmodelled key --
308
+ the oracle's typed deserialize does the same), so `data` is exactly the
309
+ shape `Envelope<KconfigData>` serializes. Raises `_KconfigShapeError` on
310
+ the first missing/mistyped key, mirroring the oracle's required (never
311
+ `#[serde(default)]`) `board`/`core`/`symbols`/per-symbol fields --
312
+ `default` is the one field that is REQUIRED-but-nullable.
313
+ """
314
+
315
+ def require(mapping: dict, key: str) -> object:
316
+ if key not in mapping:
317
+ raise _KconfigShapeError(f"missing field `{key}`")
318
+ return mapping[key]
319
+
320
+ def require_str(mapping: dict, key: str) -> str:
321
+ value = require(mapping, key)
322
+ if not isinstance(value, str):
323
+ raise _KconfigShapeError(
324
+ f"invalid type: {_yaml_type_name(value)} for `{key}`, expected a string"
325
+ )
326
+ return value
327
+
328
+ board = require_str(data, "board")
329
+ core = require_str(data, "core")
330
+ symbols = require(data, "symbols")
331
+ if not isinstance(symbols, list):
332
+ raise _KconfigShapeError(
333
+ f"invalid type: {_yaml_type_name(symbols)} for `symbols`, expected a list"
334
+ )
335
+
336
+ rebuilt_symbols = []
337
+ for index, symbol in enumerate(symbols):
338
+ if not isinstance(symbol, dict):
339
+ raise _KconfigShapeError(
340
+ f"invalid type: {_yaml_type_name(symbol)} for `symbols[{index}]`, "
341
+ f"expected an object"
342
+ )
343
+ name = require_str(symbol, "name")
344
+ sym_type = require_str(symbol, "type")
345
+ prompt = require_str(symbol, "prompt")
346
+ depends = require_str(symbol, "depends")
347
+ default = require(symbol, "default") # present, but nullable
348
+ if default is not None and not isinstance(default, str):
349
+ raise _KconfigShapeError(
350
+ f"invalid type: {_yaml_type_name(default)} for `symbols[{index}].default`, "
351
+ f"expected a string or null"
352
+ )
353
+ help_text = require_str(symbol, "help")
354
+ rebuilt_symbols.append(
355
+ {
356
+ "name": name,
357
+ "type": sym_type,
358
+ "prompt": prompt,
359
+ "depends": depends,
360
+ "default": default,
361
+ "help": help_text,
362
+ }
363
+ )
364
+
365
+ return {
366
+ "schemaVersion": data["schemaVersion"],
367
+ "board": board,
368
+ "core": core,
369
+ "symbols": rebuilt_symbols,
370
+ }
371
+
372
+
373
+ def _text_lines(data: dict, verbose: bool) -> list[str]:
374
+ symbols = data.get("symbols") or []
375
+ lines = [
376
+ f"kconfig: {len(symbols)} symbol(s) for core '{data.get('core', '')}' "
377
+ f"({data.get('board', '')})"
378
+ ]
379
+ if verbose:
380
+ for sym in symbols:
381
+ lines.append(f" {sym.get('name')} [{sym.get('type')}] — {sym.get('prompt')}")
382
+ return lines
383
+
384
+
385
+ def _run_kconfig(
386
+ *,
387
+ root: str,
388
+ board_path: str,
389
+ core: str | None,
390
+ sdk_root: str | None,
391
+ verbose: bool,
392
+ json_mode: bool,
393
+ disclosure: SdkDisclosure,
394
+ ) -> None:
395
+ """The whole setup-class ladder plus the emit, split out of `kconfig`
396
+ below so that command can wrap it in ONE catch-all (tan-cli#396) without
397
+ indenting 130 lines under a `try:`.
398
+
399
+ Every failure exits through `_fail`, which raises `typer.Exit` after
400
+ writing the envelope -- so this function's only control-flow exception is
401
+ `typer.Exit`, which is exactly what the caller re-raises untouched.
402
+
403
+ `disclosure` is the caller's, by reference: the resolution facts are
404
+ computed HERE and the caller's `kconfig.internal-failure` handler is the
405
+ tenth `_fail` site, so it needs a name to read them from after this
406
+ function has already raised. See `SdkDisclosure`.
407
+ """
408
+ # Setup-class check #1: no SDK checkout resolved -- checked before core
409
+ # resolution so every setup-class failure here is uniformly one shape,
410
+ # never a spawn attempt with half-resolved inputs (mirrors kconfig.rs).
411
+ sdk = resolve_sdk(sdk_root, root)
412
+ if sdk.path is None:
413
+ # tan-cli#497 defect 2, the branch #578 explicitly left open (its own
414
+ # comment here used to read "there is nothing resolved to report").
415
+ # tan-cli#468 closed the gap this branch used to work around:
416
+ # `presets_cmd.resolve_sdk` now always returns an `ActiveSdk` (never a
417
+ # bare `None`) and carries `broken_project_pin`/
418
+ # `foreign_global_default_for` through even when `.path` stays
419
+ # unresolved -- so a workspace whose own `.alp/sdk-path` pin is
420
+ # broken, with no OTHER tier resolving anything either, no longer has
421
+ # to re-run the ladder a second time to recover them; `sdk` already
422
+ # has them.
423
+ #
424
+ # Still no `sdk=` -- there is genuinely no root to report, matching
425
+ # the oracle -- but `sdk_issues` carries whatever the ladder found
426
+ # before giving up, in both JSON and text (`_fail` prepends and
427
+ # prints them), so the workspace's own diagnosis reaches the user
428
+ # instead of being computed and silently discarded.
429
+ sdk_issues = sdk_resolution_issues(
430
+ sdk.broken_project_pin, sdk.tier, sdk.foreign_global_default_for
431
+ )
432
+ _fail(
433
+ root=root,
434
+ board_path=board_path,
435
+ exit_code=ExitCode.VALIDATION_FAILURE,
436
+ code="kconfig.no-sdk-root",
437
+ # `tan sdk switch` refuses in this build (tan-cli#305). Dropped
438
+ # "run `tan bootstrap` first" too -- bootstrap resolves an SDK
439
+ # through this exact same ladder, so with none resolved it
440
+ # refuses right back with tan-cli#305's own fix text; it is not a
441
+ # remedy for THIS failure, just a second site that needs one.
442
+ message=with_sdk_search(
443
+ f"no alp-sdk checkout found — pass `--sdk-root <PATH>`, or "
444
+ f"{NO_SDK_NEXT_STEPS}.",
445
+ root,
446
+ sdk_root,
447
+ ),
448
+ core=None,
449
+ json_mode=json_mode,
450
+ sdk_issues=sdk_issues,
451
+ )
452
+ return
453
+ # tan-cli#504's blessed constructor: it carries `foreign_global_default_for`
454
+ # and `broken_project_pin` off the resolution onto the `SdkInfo`, which is
455
+ # what `Envelope._with_sdk_resolution_advisories` reads. A raw
456
+ # `SdkInfo(sdk.path, sdk.tier)` drops both and
457
+ # `tests/gates/test_sdk_info_is_built_from_a_resolution.py` refuses it.
458
+ sdk_info = SdkInfo.from_resolution(sdk.path, sdk)
459
+ # tan-cli#497 defect 2: `resolve_sdk` deliberately CARRIES both facts onto
460
+ # the `ActiveSdk` it returns, and this module read neither -- it imported
461
+ # `resolve_sdk` and none of the issue helpers, so a workspace whose
462
+ # `.alp/sdk-path` pin misses answered `ok: true, issues: []` with a full
463
+ # symbol menu solved out of a checkout the pin does not name. Computed
464
+ # ONCE here, threaded into all seven `_fail` sites below and into the
465
+ # success emit -- and, via `disclosure` just below, into the eighth site
466
+ # this function cannot reach: `kconfig`'s own `kconfig.internal-failure`
467
+ # catch-all. Ten `_fail` calls exist; the tenth is the `sdk.path is None`
468
+ # branch above, which reads the same two facts straight off `sdk` now
469
+ # (tan-cli#468 -- `resolve_sdk` carries them even when unresolved, so no
470
+ # second `resolve_sdk_tiered` call is needed any more).
471
+ #
472
+ # Kept alongside #504's envelope-seam advisory rather than deleted in
473
+ # favour of it: the seam appends its pair at the END and dedupes BY CODE
474
+ # ("a command that already emitted the pair keeps its own copy and
475
+ # position"), so this hand-call is what keeps the two issues in this
476
+ # command's own order, ahead of the command-specific issues below.
477
+ sdk_issues = sdk_resolution_issues(
478
+ sdk.broken_project_pin, sdk.tier, sdk.foreign_global_default_for
479
+ )
480
+ # Handed to the caller's `kconfig.internal-failure` handler the instant
481
+ # both facts exist -- that handler is the one `_fail` site this function
482
+ # cannot reach, and every line below it can raise something unenumerated.
483
+ disclosure.record(sdk_info, sdk_issues)
484
+
485
+ try:
486
+ resolved_core = _resolve_core(core, board_path)
487
+ except _CoreResolutionError as err:
488
+ _fail(
489
+ root=root,
490
+ board_path=board_path,
491
+ exit_code=ExitCode.VALIDATION_FAILURE,
492
+ code=err.code,
493
+ message=err.message,
494
+ core=None,
495
+ json_mode=json_mode,
496
+ sdk=sdk_info,
497
+ sdk_issues=sdk_issues,
498
+ )
499
+ return
500
+
501
+ # Setup-class check #2: no bootstrapped Zephyr workspace (needs
502
+ # ZEPHYR_BASE for the real Kconfig solver).
503
+ zephyr_base = _resolve_zephyr_base(root, sdk.path)
504
+ if zephyr_base is None:
505
+ _fail(
506
+ root=root,
507
+ board_path=board_path,
508
+ exit_code=ExitCode.VALIDATION_FAILURE,
509
+ code="kconfig.no-workspace",
510
+ message="no bootstrapped Zephyr workspace found for `--emit kconfig` "
511
+ "(needs ZEPHYR_BASE) — run `tan bootstrap` first.",
512
+ core=resolved_core,
513
+ json_mode=json_mode,
514
+ sdk=sdk_info,
515
+ sdk_issues=sdk_issues,
516
+ )
517
+ return
518
+
519
+ from tan.planner_root import emit as _plan_emit
520
+
521
+ # Inject the RESOLVED base, not the ambient one -- `kconfig_symbols`
522
+ # renders in-process and reads `os.environ["ZEPHYR_BASE"]` directly, so a
523
+ # tree- or SDK-derived workspace (tiers 1/3 above) would never reach the
524
+ # planner without this. Restored afterwards so this call has no lasting
525
+ # effect on the process env (other commands, and other tests in the same
526
+ # process, must not see it).
527
+ prev_zephyr_base = os.environ.get("ZEPHYR_BASE")
528
+ os.environ["ZEPHYR_BASE"] = zephyr_base
529
+ try:
530
+ try:
531
+ stdout_text = _plan_emit(
532
+ "kconfig", root=sdk.path, board_yaml=Path(board_path), core=resolved_core
533
+ )
534
+ except SystemExit as err:
535
+ _fail(
536
+ root=root,
537
+ board_path=board_path,
538
+ exit_code=ExitCode.RUNTIME_FAILURE,
539
+ code="kconfig.emit-failed",
540
+ message=f"the kconfig emit exited early (code {err.code}).",
541
+ core=resolved_core,
542
+ json_mode=json_mode,
543
+ sdk=sdk_info,
544
+ sdk_issues=sdk_issues,
545
+ )
546
+ return
547
+ except Exception as err: # noqa: BLE001 -- every planner failure is an
548
+ # envelope, never a traceback (mirrors build_cmd._emit_plan's own
549
+ # backstop); includes `OrchestratorError` for an unknown/non-Zephyr
550
+ # `--core`.
551
+ # tan-cli#1350: a `diagnostics.link: itcm` refusal keeps its own
552
+ # code and the validation exit, as in `tan build`.
553
+ link_code = refusal_code(err)
554
+ _fail(
555
+ root=root,
556
+ board_path=board_path,
557
+ exit_code=(ExitCode.VALIDATION_FAILURE if link_code
558
+ else ExitCode.RUNTIME_FAILURE),
559
+ code=link_code or "kconfig.emit-failed",
560
+ message=f"the kconfig emit failed: {type(err).__name__}: {err}",
561
+ core=resolved_core,
562
+ json_mode=json_mode,
563
+ sdk=sdk_info,
564
+ sdk_issues=sdk_issues,
565
+ )
566
+ return
567
+ finally:
568
+ if prev_zephyr_base is None:
569
+ os.environ.pop("ZEPHYR_BASE", None)
570
+ else:
571
+ os.environ["ZEPHYR_BASE"] = prev_zephyr_base
572
+
573
+ try:
574
+ data = _json.loads(stdout_text)
575
+ except _json.JSONDecodeError as err:
576
+ _fail(
577
+ root=root,
578
+ board_path=board_path,
579
+ exit_code=ExitCode.RUNTIME_FAILURE,
580
+ code="kconfig.parse-failed",
581
+ message=f"kconfig emit is not valid JSON: {err}",
582
+ core=resolved_core,
583
+ json_mode=json_mode,
584
+ sdk=sdk_info,
585
+ sdk_issues=sdk_issues,
586
+ )
587
+ return
588
+ if not isinstance(data, dict) or data.get("schemaVersion") != KCONFIG_SCHEMA_VERSION:
589
+ found = data.get("schemaVersion") if isinstance(data, dict) else None
590
+ _fail(
591
+ root=root,
592
+ board_path=board_path,
593
+ exit_code=ExitCode.RUNTIME_FAILURE,
594
+ code="kconfig.parse-failed",
595
+ message=f"unsupported kconfig schemaVersion {found!r} (this CLI "
596
+ f"consumes v{KCONFIG_SCHEMA_VERSION}); upgrade the CLI or the SDK so "
597
+ f"the versions match",
598
+ core=resolved_core,
599
+ json_mode=json_mode,
600
+ sdk=sdk_info,
601
+ sdk_issues=sdk_issues,
602
+ )
603
+ return
604
+
605
+ try:
606
+ data = _validate_kconfig_data(data)
607
+ except _KconfigShapeError as err:
608
+ _fail(
609
+ root=root,
610
+ board_path=board_path,
611
+ exit_code=ExitCode.RUNTIME_FAILURE,
612
+ code="kconfig.parse-failed",
613
+ message=f"kconfig emit is not valid JSON: {err}",
614
+ core=resolved_core,
615
+ json_mode=json_mode,
616
+ sdk=sdk_info,
617
+ sdk_issues=sdk_issues,
618
+ )
619
+ return
620
+
621
+ if json_mode:
622
+ emit(
623
+ Envelope(
624
+ "kconfig",
625
+ Project.resolved(root, board_path),
626
+ data,
627
+ # tan-cli#497 defect 2: a literal `[]` here was the WORST of
628
+ # the drops -- a full symbol menu, `ok: true`, solved out of a
629
+ # checkout the workspace's own pin does not name, with nothing
630
+ # in the envelope the extension could notice.
631
+ list(sdk_issues),
632
+ ExitCode.SUCCESS,
633
+ sdk=sdk_info,
634
+ )
635
+ )
636
+ else:
637
+ for issue in sdk_issues:
638
+ print(f"{issue.severity}: {issue.message}", file=sys.stderr)
639
+ for line in _text_lines(data, verbose):
640
+ print(line, file=sys.stderr)
641
+ raise typer.Exit(int(ExitCode.SUCCESS))
642
+
643
+
644
+ def kconfig(
645
+ core: str = typer.Option(
646
+ None,
647
+ "--core",
648
+ metavar="CORE_ID",
649
+ help="Core id to scope the Kconfig symbol menu to (default: the board's "
650
+ "one declared Zephyr core, when unambiguous).",
651
+ ),
652
+ project: str = typer.Option(
653
+ None, "--project", metavar="PATH", help="Project root (defaults to current directory)."
654
+ ),
655
+ board_yaml: str = typer.Option(
656
+ None,
657
+ "--board-yaml",
658
+ metavar="PATH",
659
+ help="Explicit board.yaml path (overrides project resolution).",
660
+ ),
661
+ sdk_root: str = typer.Option(None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."),
662
+ verbose: bool = typer.Option(False, "--verbose", help="Emit additional diagnostic detail."),
663
+ output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
664
+ ) -> None:
665
+ """Show the board-scoped Kconfig symbol menu for one core (the vscode
666
+ `prj.conf` LSP's live feed). Needs a bootstrapped Zephyr workspace."""
667
+ json_mode = output_format == "json"
668
+
669
+ # The same backstop `presets_cmd.py` has had all along and this command
670
+ # did not (tan-cli#396). Nothing enumerated reaches it -- every IO/parse
671
+ # failure below is already coded -- and that is precisely the point: the
672
+ # ONE nobody thought of must arrive as an envelope rather than as a
673
+ # traceback on stderr with zero bytes on stdout, which the vscode
674
+ # extension renders as an empty or stale `prj.conf` symbol menu with no
675
+ # error of any kind (`contract/README.md:33-37`).
676
+ #
677
+ # `typer.Exit` is re-raised untouched: it is a `RuntimeError` subclass, so
678
+ # a bare `except Exception` swallows it -- and every SUCCESSFUL run, plus
679
+ # every already-coded refusal, leaves through exactly that exception.
680
+ # Catching it here would have turned `ok: true` into an internal failure.
681
+ #
682
+ # The pre-resolution defaults mirror `presets_cmd`'s: `resolve_project_
683
+ # paths` is itself inside the guard, so the failing envelope needs a
684
+ # `project` block even when resolution is what blew up.
685
+ root, board_path = ".", "./board.yaml"
686
+ # tan-cli#497 defect 2, the tenth `_fail` site. The first pass threaded the
687
+ # resolution pair into the seven sites inside `_run_kconfig` and left THIS
688
+ # one -- the catch-all, which runs strictly AFTER `resolve_sdk` has already
689
+ # produced both facts -- hardcoding a single-element issue list. Measured
690
+ # against a broken-`.alp/sdk-path` workspace with a discoverable sibling and
691
+ # `_resolve_zephyr_base` (outside every `try` in `_run_kconfig`) raising
692
+ # `OSError: [Errno 24] Too many open files`: exit 5,
693
+ # `issues: [kconfig.internal-failure]`, `sdk.project-pin-unresolved`
694
+ # dropped -- the same drop the issue is about, on a path the fix was
695
+ # believed to cover.
696
+ #
697
+ # The facts cannot simply be recomputed here: `resolve_sdk` is itself one
698
+ # of the things that can raise, and re-running the ladder in an exception
699
+ # handler risks a second raise out of the one place that must not throw.
700
+ # They are RECORDED by `_run_kconfig` instead, into a carrier this function
701
+ # owns, so the handler only ever reads two fields.
702
+ disclosure = SdkDisclosure()
703
+ try:
704
+ root, board_path = resolve_project_paths(project, board_yaml)
705
+ _run_kconfig(
706
+ root=root,
707
+ board_path=board_path,
708
+ core=core,
709
+ sdk_root=sdk_root,
710
+ verbose=verbose,
711
+ json_mode=json_mode,
712
+ disclosure=disclosure,
713
+ )
714
+ except typer.Exit:
715
+ raise
716
+ except Exception as err: # noqa: BLE001 -- the envelope IS the error contract
717
+ _fail(
718
+ root=root,
719
+ board_path=board_path,
720
+ exit_code=ExitCode.INTERNAL_FAILURE,
721
+ code="kconfig.internal-failure",
722
+ message=f"kconfig failed unexpectedly: {type(err).__name__}: {err}",
723
+ core=core,
724
+ json_mode=json_mode,
725
+ sdk=disclosure.sdk,
726
+ sdk_issues=disclosure.issues,
727
+ )
728
+
729
+
730
+ # tan-cli#261: adds the six oracle `GlobalArgs` flags this command was still
731
+ # missing (`--all`/`--ci`/`--no-color`/`--non-interactive`/`--quiet`/
732
+ # `--target`) on top of `--verbose`, already declared and read above; see
733
+ # `tan.core.global_flags`.
734
+ kconfig = accept_global_flags(kconfig)