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,230 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The single place a command opts into the oracle's global-argument surface
3
+ (tan-cli#261).
4
+
5
+ The v0.4.1 oracle's clap `GlobalArgs` (`crates/tan-cli/src/cli.rs` lines
6
+ 24-73) marks every field `#[arg(long, global = true, ...)]`: clap attaches
7
+ the WHOLE struct to every subcommand, so `tan <any-command> --verbose` parses
8
+ on the oracle even for a command whose own Rust handler never reads
9
+ `args.verbose`. Measured against this port (tan-cli#261, re-measured before
10
+ this file existed): 99 registration sites across 17 already-ported commands
11
+ raised Click's own "No such option" instead -- not because the flag did the
12
+ wrong thing, but because nobody had declared it there at all, an
13
+ eighteenth-command-repeats-the-mistake defect this module exists to remove
14
+ structurally rather than patch site by site.
15
+
16
+ `accept_global_flags` closes exactly that gap and nothing else: it adds a
17
+ `typer.Option` for whichever of `_GLOBAL_FLAG_SPECS` a command's own
18
+ signature does not ALREADY declare, detected by the CLI flag STRING itself
19
+ (each parameter's `typer.Option(...).param_decls`) rather than by Python
20
+ parameter name, which already varies command to command for the identical
21
+ flag (`all_cores` in `clean_cmd.clean`, `all_targets` in `build_cmd.build`,
22
+ both `--all`). A flag a command already reads for real keeps doing exactly
23
+ that -- this module never touches an existing parameter.
24
+
25
+ Every flag this module ADDS -- arity-0 or value-carrying alike -- is accepted
26
+ and then DROPPED before the wrapped command runs. That was always true for a
27
+ boolean (an accepted-and-ignored `--verbose` changes nothing a caller can
28
+ observe); tan-cli#398 tried to narrow it for a value-carrying flag instead,
29
+ refusing the flag outright the moment a value was actually supplied
30
+ (`typer.BadParameter`, exit 2), reasoning that dropping the value silently
31
+ serves the command's own default in its place -- measured harmful on exactly
32
+ one flag, `model`'s injected `--board-yaml` (see `model_cmd.py`'s own
33
+ `--board`, which now declares `--board-yaml` as a second spelling of the SAME
34
+ option instead, so it is never one of the flags this module injects for
35
+ `model` at all; `_declared_flags` below already skips anything a command
36
+ declares for real).
37
+
38
+ Measured against the oracle (tan-cli#403 postmortem), the refusal was wrong
39
+ everywhere else it fired: 17 other (command, flag) pairs -- `--board-yaml` on
40
+ `examples`/`explain`/`sdk`, `--target` on 14 more -- accept the SAME flag on
41
+ `target/debug/tan.exe` and ignore it (it is `global = true` there too, but
42
+ each command's own Rust handler simply never reads it), so refusing it here
43
+ was a NEW divergence: `tan doctor --target zephyr` went from matching the
44
+ oracle's exit 4 to a `typer.BadParameter` exit 2 with the WRONG command in the
45
+ JSON envelope (`main`'s generic `command: "cli"` fallback, since the refusal
46
+ happens before the wrapped command -- and therefore before `emit()` -- ever
47
+ runs). A command that genuinely computes a wrong answer from a dropped value
48
+ needs the SAME fix `model` got -- read the real flag, whatever its
49
+ oracle-parity alias is called -- not a blanket refusal that punishes every
50
+ command the oracle itself treats as accept-and-ignore.
51
+
52
+ This is the same "declared once, accepted everywhere, read only where a
53
+ command already reads it" shape `clean_cmd.clean` hand-wrote for six flags
54
+ before this module existed -- see its own comment there: "the shared fix
55
+ (one decorator for every command) belongs with whoever owns the global-flag
56
+ surface." This is that decorator, generalised and applied to the rest of the
57
+ surface.
58
+
59
+ `--format` is deliberately NOT one of `_GLOBAL_FLAG_SPECS`: unlike the
60
+ arity-0 flags here, a command that merely ACCEPTED `--format json` without
61
+ reading it would silently run in text mode for a caller who asked for JSON --
62
+ exit 0, human text on stderr, nothing on stdout, an envelope-less
63
+ `--format json` run.
64
+ `--format` is instead made global by RELOCATION (`RELOCATABLE_FLAG_ARITY`
65
+ below, tan-cli#378) -- every registered command already declares and READS its
66
+ own `--format`, so moving the token past the subcommand name hands it to a
67
+ parameter that acts on it rather than to one that drops it.
68
+ """
69
+ from __future__ import annotations
70
+
71
+ import inspect
72
+ import typing
73
+ from collections.abc import Callable
74
+
75
+ import typer
76
+
77
+ from tan.core.inert import PARITY, inert_help
78
+
79
+ #: One entry per oracle `GlobalArgs` field this port must be able to PARSE on
80
+ #: every command (`crates/tan-cli/src/cli.rs:24-73`), as
81
+ #: `(flag, python_name, is_bool, metavar)`. `metavar` is `None` for a bool
82
+ #: flag (arity 0). `--project`/`--sdk-root` are included even though every
83
+ #: command touched by tan-cli#261 already declares them (measured) -- the
84
+ #: NEXT command to be added is exactly who this guards.
85
+ _GLOBAL_FLAG_SPECS: tuple[tuple[str, str, bool, str | None], ...] = (
86
+ ("--project", "project", False, "PATH"),
87
+ ("--board-yaml", "board_yaml", False, "PATH"),
88
+ ("--sdk-root", "sdk_root", False, "PATH"),
89
+ ("--target", "target", False, "EMIT"),
90
+ ("--all", "all_", True, None),
91
+ ("--verbose", "verbose", True, None),
92
+ ("--quiet", "quiet", True, None),
93
+ ("--no-color", "no_color", True, None),
94
+ ("--non-interactive", "non_interactive", True, None),
95
+ ("--ci", "ci", True, None),
96
+ )
97
+
98
+ #: `{flag: arity}` -- `cli.py`'s `_reorder_global_flags` reads this (arity 1
99
+ #: = takes a value, 0 = boolean) to relocate a leading global flag across the
100
+ #: subcommand boundary. Derived from `_GLOBAL_FLAG_SPECS` so the reorder
101
+ #: table and this module's own injection list cannot drift apart the way
102
+ #: tan-cli#261's own second comment flagged `_GLOBAL_FLAG_ARITY` could.
103
+ GLOBAL_FLAG_ARITY: dict[str, int] = {
104
+ flag: (0 if is_bool else 1) for flag, _name, is_bool, _metavar in _GLOBAL_FLAG_SPECS
105
+ }
106
+
107
+ #: Every flag this module knows how to inject -- what the port-wide gate
108
+ #: (`tests/gates/test_global_flags_gate.py`) walks to assert the whole
109
+ #: registered command surface accepts it.
110
+ GLOBAL_FLAGS: tuple[str, ...] = tuple(flag for flag, *_rest in _GLOBAL_FLAG_SPECS)
111
+
112
+ #: What `cli.py`'s `_reorder_global_flags` actually relocates across the
113
+ #: subcommand boundary: the injectable set above PLUS `--format` (tan-cli#378).
114
+ #:
115
+ #: `--format` is `#[arg(long, global = true, ...)]` on the oracle exactly like
116
+ #: the ten above, so `tan --format json <anything>` parses there; the port used
117
+ #: to instead declare `--format` on the ROOT callback and hand-list which
118
+ #: commands were allowed to precede it, which shipped as global on 12 of 32
119
+ #: commands and a `cli.parse-error` (exit 2, `command: "cli"` -- the WRONG
120
+ #: command in the envelope) on the other 20. Relocating is what makes it
121
+ #: uniform with no list to forget: the flag lands on the command's own
122
+ #: `--format` parameter, the position every golden and every command's
123
+ #: validation already uses.
124
+ #:
125
+ #: Separate from `_GLOBAL_FLAG_SPECS` on purpose, NOT merged into it: that
126
+ #: tuple is the INJECTION list, and injecting `--format` would give a future
127
+ #: command that does not declare one an accepted-and-dropped `--format json`
128
+ #: -- the silent-text-mode defect the module docstring rules out. Keeping the
129
+ #: two apart also leaves `GLOBAL_FLAGS`/`GLOBAL_FLAG_ARITY` (and the gate that
130
+ #: walks them) meaning exactly what they meant before.
131
+ RELOCATABLE_FLAG_ARITY: dict[str, int] = {**GLOBAL_FLAG_ARITY, "--format": 1}
132
+
133
+ #: Shown nowhere (every injected option is `hidden=True`, matching the
134
+ #: precedent `clean_cmd.clean` already set for its six) -- kept as a real
135
+ #: string anyway so a `--help -v` or future un-hiding does not surface a bare
136
+ #: `None`.
137
+ _ACCEPTED_NOT_READ_HELP = inert_help(
138
+ "Accepted for oracle parity; not read by this command.", PARITY, "tan-cli#261"
139
+ )
140
+
141
+
142
+ def _declared_flags(func: Callable[..., object]) -> set[str]:
143
+ """Every CLI flag string `func` already declares, keyed by the flag
144
+ string itself rather than by Python parameter name -- the same fact
145
+ under a different name (`all_cores` vs `all_targets` for `--all`) must
146
+ still count as "already declared", or this would hand a command a
147
+ second, silently-ignored `--all` behind its own real one."""
148
+ declared: set[str] = set()
149
+ for param in inspect.signature(func).parameters.values():
150
+ decls = getattr(param.default, "param_decls", None)
151
+ if decls:
152
+ declared.update(decls)
153
+ return declared
154
+
155
+
156
+ def accept_global_flags(func: Callable[..., object]) -> Callable[..., object]:
157
+ """Return a callable Typer can register whose declared options are
158
+ `func`'s own plus whichever of `_GLOBAL_FLAG_SPECS` it was missing, each
159
+ of the added ones accepted and then dropped before `func` ever runs.
160
+
161
+ A command that already declares the full set is returned UNCHANGED
162
+ (`func` itself, not a wrapper) -- the common case once every command in
163
+ tan-cli#261 has been swept once.
164
+
165
+ Call this right where the command function is defined, in its own
166
+ `*_cmd.py` module (`validate = accept_global_flags(validate)`), not as a
167
+ decorator on the `app.command(...)` call in `cli.py`: by the time
168
+ `cli.py` runs, the click `Command` Typer builds from the function is
169
+ already final, and wrapping there would scatter the fix away from the
170
+ command it changes.
171
+ """
172
+ sig = inspect.signature(func)
173
+ existing = _declared_flags(func)
174
+ to_add = [spec for spec in _GLOBAL_FLAG_SPECS if spec[0] not in existing]
175
+ if not to_add:
176
+ return func
177
+
178
+ # Resolve every EXISTING parameter's annotation to the real type object,
179
+ # not whatever bare STRING `from __future__ import annotations` leaves it
180
+ # as: `inspect.signature` alone never evaluates PEP 563 postponed
181
+ # annotations, and every one of these 17 `*_cmd.py` modules has that
182
+ # import at the top. Typer normally resolves the string itself via
183
+ # `typing.get_type_hints(callback)`, using `callback.__globals__` --
184
+ # `func`'s module, where `str`/`bool`/`typer.Context`/... are in scope.
185
+ # Once `func` is wrapped below, Typer would instead resolve hints
186
+ # against `wrapper.__globals__` -- THIS module's globals, not `func`'s --
187
+ # and get nothing back for a name it cannot see; the fallback path then
188
+ # feeds `get_click_type` the literal string `'str'` instead of the type
189
+ # `str`, which fails with `RuntimeError: Type not yet supported: str`
190
+ # (measured: `explain --target zephyr-board` after a first version of
191
+ # this function skipped this step). Resolving here, once, against the
192
+ # ORIGINAL `func` -- exactly what Typer would have done directly -- is
193
+ # what keeps a wrapped command's pre-existing options working at all.
194
+ resolved_hints = typing.get_type_hints(func)
195
+ params = [
196
+ param.replace(annotation=resolved_hints[name]) if name in resolved_hints else param
197
+ for name, param in sig.parameters.items()
198
+ ]
199
+
200
+ #: Every injected flag's python param name -- arity-0 or value-carrying
201
+ #: alike, all dropped before `func` runs (see the module docstring for why
202
+ #: refusing a value-carrying one, tried in tan-cli#398, is not this
203
+ #: module's job any more).
204
+ dropped: list[str] = []
205
+ for flag, name, is_bool, metavar in to_add:
206
+ if is_bool:
207
+ option = typer.Option(False, flag, hidden=True, help=_ACCEPTED_NOT_READ_HELP)
208
+ annotation: type = bool
209
+ else:
210
+ option = typer.Option(
211
+ None, flag, metavar=metavar, hidden=True, help=_ACCEPTED_NOT_READ_HELP
212
+ )
213
+ annotation = str
214
+ dropped.append(name)
215
+ params.append(
216
+ inspect.Parameter(
217
+ name, inspect.Parameter.KEYWORD_ONLY, default=option, annotation=annotation
218
+ )
219
+ )
220
+
221
+ def wrapper(*args: object, **kwargs: object) -> object:
222
+ for name in dropped:
223
+ kwargs.pop(name, None)
224
+ return func(*args, **kwargs)
225
+
226
+ wrapper.__doc__ = func.__doc__
227
+ wrapper.__name__ = getattr(func, "__name__", "wrapper")
228
+ return_annotation = resolved_hints.get("return", sig.return_annotation)
229
+ wrapper.__signature__ = inspect.Signature(params, return_annotation=return_annotation)
230
+ return wrapper
@@ -0,0 +1,210 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """ONE host-interpreter resolver, shared by `tan doctor`'s `hostPython` check
3
+ and `tan build`'s `-DPython3_EXECUTABLE`.
4
+
5
+ tan-cli#1317. Doctor probed `python3`/`python` off PATH and passed on a 3.14
6
+ interpreter while `tan build` (no workspace venv) baked the BARE name
7
+ `python3` into the Zephyr slice, so CMake ran its own `FindPython3` search
8
+ and picked a shadowing `~/.local/bin/python3.10`. Two resolvers meant the two
9
+ commands could disagree about which Python the build uses. This module is the
10
+ single answer; both call it.
11
+
12
+ `resolve_tool` finds each candidate, the candidate is RUN, and the absolute
13
+ `sys.executable` it reports is what a build bakes in -- never a bare name.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import re
19
+ from dataclasses import dataclass
20
+
21
+ from tan.core.probe import probe
22
+ from tan.core.tool_lookup import resolve_tool
23
+
24
+ # Security (code execution via module hijack): `-c` puts the CWD on `sys.path`,
25
+ # so `core.probe` runs every `python -c` probe from a fresh EMPTY directory, and
26
+ # the code below also strips '' / the CWD from `sys.path` before LOCATING
27
+ # `west` (`find_spec('west.version')` imports the parent `west` package from
28
+ # the interpreter's own site dirs, never from the CWD). `-I` is deliberately
29
+ # NOT used: it would hide a legitimately installed west in the user site
30
+ # (`~/.local/lib/pythonX.Y/site-packages`, where `pip install --user west`
31
+ # lands). `-P` (3.11+) is not used either: the probe must run under 3.10
32
+ # candidates too, and the explicit strip does the same job there.
33
+ _PROBE_CODE = (
34
+ "import sys,os;sys.path[:]=[p for p in sys.path if p not in ('',os.getcwd(),os.curdir)];"
35
+ "import importlib.util as u\n"
36
+ "def f(n):\n"
37
+ " try:\n"
38
+ " s=u.find_spec(n)\n"
39
+ " return s is not None and s.origin is not None\n"
40
+ " except Exception:\n"
41
+ " return False\n"
42
+ "print('%d.%d' % sys.version_info[:2]);print(sys.executable);"
43
+ "print('west=%d' % (f('west') and f('west.version')))"
44
+ )
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class HostPython:
49
+ display: str #: how the candidate is spelled (`python3`, `py -3`)
50
+ version: tuple[int, int]
51
+ resolved: str #: PATH-resolved path of the candidate's first argv word
52
+ interpreter: str #: absolute `sys.executable` the interpreter reported
53
+ has_west: bool = False #: `import west.version` succeeds under it
54
+
55
+
56
+ def python_candidates() -> list[list[str]]:
57
+ """Windows leads with the `py` launcher (a machine can have a good 3.12
58
+ with no bare `python`, and bare `python.exe` is often the Store alias)."""
59
+ if os.name == "nt":
60
+ return [["py", "-3"], ["python"], ["python3"]]
61
+ return [["python3"], ["python"]]
62
+
63
+
64
+ def _posix_path_pythons(environ) -> list[str]:
65
+ """Every `python3`/`python`/`python3.N` file along PATH, in PATH order (a
66
+ venv's `bin/` first on PATH must not hide a better interpreter behind it).
67
+ An unreadable PATH entry is skipped, never raised."""
68
+ out: list[str] = []
69
+ seen: set[tuple[str, str]] = set()
70
+ for d in (environ.get("PATH") or "").split(os.pathsep):
71
+ if not d or not os.path.isabs(d):
72
+ continue
73
+ try:
74
+ names = os.listdir(d)
75
+ except OSError:
76
+ continue
77
+ minors = sorted(
78
+ (n for n in names if re.fullmatch(r"python3\.\d+", n)),
79
+ key=lambda n: -int(n.split(".")[1]),
80
+ )
81
+ for name in ["python3", "python", *minors]:
82
+ full = os.path.join(d, name)
83
+ if not (os.path.isfile(full) and os.access(full, os.X_OK)):
84
+ continue
85
+ # Dedupe on (realpath(dir), realpath(file)): folds `/bin` vs
86
+ # `/usr/bin` under usrmerge, but keeps a venv's `bin/python3`
87
+ # (a symlink to the system python, in a DIFFERENT dir and a
88
+ # different environment) separate.
89
+ key = (os.path.realpath(d), os.path.realpath(full))
90
+ if key in seen:
91
+ continue
92
+ seen.add(key)
93
+ out.append(full)
94
+ return out
95
+
96
+
97
+ def _parse(raw: str) -> tuple[tuple[int, int], str | None, bool] | None:
98
+ """Version from the LAST version-shaped line (noise may precede it); the
99
+ next lines are `sys.executable` and `west=0|1`."""
100
+ lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
101
+ for i in range(len(lines) - 1, -1, -1):
102
+ m = re.fullmatch(r"(\d+)\.(\d+)", lines[i])
103
+ if m is not None:
104
+ exe = lines[i + 1] if i + 1 < len(lines) else None
105
+ west = i + 2 < len(lines) and lines[i + 2] == "west=1"
106
+ return (int(m.group(1)), int(m.group(2))), exe, west
107
+ if lines:
108
+ m = re.search(r"(\d+)\.(\d+)", lines[-1])
109
+ if m is not None:
110
+ return (int(m.group(1)), int(m.group(2))), None, False
111
+ return None
112
+
113
+
114
+ def probe_all_host_pythons(env=None) -> list[HostPython]:
115
+ """Every distinct interpreter that RUNS, in discovery order."""
116
+ environ = os.environ if env is None else env
117
+ if os.name == "nt":
118
+ cands = [(c, resolve_tool(c[0], environ).resolved) for c in python_candidates()]
119
+ else:
120
+ # argv[0] is the ABSOLUTE path: a bare name makes Python re-search PATH for
121
+ # `sys.executable`, misreporting a venv's python as whatever is later on PATH.
122
+ cands = [([p], p) for p in _posix_path_pythons(environ)]
123
+ seen: set[str] = set()
124
+ found: list[HostPython] = []
125
+ for candidate, resolved in cands:
126
+ if resolved is None:
127
+ continue
128
+ out = probe([*candidate, "-c", _PROBE_CODE], executable=resolved)
129
+ parsed = _parse(out) if out is not None else None
130
+ if parsed is None:
131
+ continue
132
+ version, exe, west = parsed
133
+ interpreter = exe if exe and os.path.isabs(exe) else resolved
134
+ if interpreter in seen:
135
+ continue
136
+ seen.add(interpreter)
137
+ found.append(HostPython(" ".join(candidate) if os.name == "nt" else os.path.basename(candidate[0]), version, resolved, interpreter, west))
138
+ return found
139
+
140
+
141
+ def select_host_python(
142
+ found: list[HostPython], floor: tuple[int, int], *, need_west: bool = False
143
+ ) -> HostPython | None:
144
+ """Best of `found`: >=floor AND west-capable, then >=floor (unless
145
+ `need_west`), then the first that merely ran (so a too-old message can name
146
+ a real version)."""
147
+ for pred in (
148
+ lambda h: h.version >= floor and h.has_west,
149
+ lambda h: h.version >= floor and not need_west,
150
+ lambda h: True,
151
+ ):
152
+ for h in found:
153
+ if pred(h):
154
+ return h
155
+ return None
156
+
157
+
158
+ def probe_host_python(
159
+ floor: tuple[int, int], env=None, *, need_west: bool = False
160
+ ) -> HostPython | None:
161
+ return select_host_python(probe_all_host_pythons(env), floor, need_west=need_west)
162
+
163
+
164
+ def describe_unsuitable(found: list[HostPython], floor: tuple[int, int]) -> str:
165
+ """Name every candidate and why it does not qualify."""
166
+ want = f"{floor[0]}.{floor[1]}"
167
+ if not found:
168
+ return (
169
+ "no runnable Python interpreter was found on PATH; "
170
+ f"Zephyr needs Python {want}+ with `west` importable."
171
+ )
172
+ parts = []
173
+ for h in found:
174
+ why = []
175
+ if h.version < floor:
176
+ why.append(f"too old (below {want})")
177
+ if not h.has_west:
178
+ why.append("no `west` module")
179
+ parts.append(f"`{h.interpreter}` {h.version[0]}.{h.version[1]}: " + ", ".join(why or ["ok"]))
180
+ return (
181
+ f"no host Python is usable for the Zephyr build -- Zephyr needs Python {want}+ "
182
+ "that can import `west`. Found: " + "; ".join(parts) + "."
183
+ )
184
+
185
+
186
+ def build_interpreter(
187
+ venv_python: str | None,
188
+ fallback: str,
189
+ plan_uses_python_token: bool,
190
+ floor: tuple[int, int],
191
+ *,
192
+ probe_all=probe_all_host_pythons,
193
+ ) -> tuple[str, str | None]:
194
+ """The `${PYTHON}` a build bakes in, as `(interpreter, refusal)`.
195
+
196
+ A workspace venv wins unchanged. Otherwise, when the plan uses `${PYTHON}`
197
+ (every Zephyr slice's `-DPython3_EXECUTABLE`), the interpreter is the
198
+ absolute one the shared resolver picks: >= `floor` (the doctor's EFFECTIVE
199
+ floor) and able to import `west` -- never the bare `python3` (tan-cli#1317).
200
+ Forward-slashed, because a Windows backslash in a CMake `-D` is an escape
201
+ (alp-sdk#849, `tan.core.venv.venv_python`). `refusal` names each candidate
202
+ when none qualifies.
203
+ """
204
+ if venv_python is not None or not plan_uses_python_token:
205
+ return (venv_python or fallback), None
206
+ found = probe_all()
207
+ best = select_host_python(found, floor, need_west=True)
208
+ if best is not None and best.version >= floor and best.has_west:
209
+ return best.interpreter.replace("\\", "/"), None
210
+ return fallback, describe_unsuitable(found, floor)