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,237 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Method-aware inputs for `tan doctor`'s `setools` check (tan-cli#1323).
3
+
4
+ The check used to assert, for every host, that "AEN MRAM flashing (`west
5
+ flash`, the alif_flash runner) will fail" without `$SE_UART`. That is Flow A
6
+ only. A planner-emitted AEN manifest dispatches Flow D (`alif_mram_jlink`):
7
+ J-Link over SWD, SETOOLS needed solely to sign the ATOC (`app-gen-toc`), no
8
+ SE-UART at all. So the check must know which method `tan flash` would pick.
9
+
10
+ This module answers that question by calling `tan.core.flash_plan.
11
+ select_flash_method` -- the SAME resolver `tan flash` dispatches with -- on
12
+ each slice of the project's `build/system-manifest.yaml`, never by
13
+ re-deriving the Flow D rule. Pure/IO split: `flash_methods_for_manifest_text`
14
+ is pure; `project_flash_methods` does the one read. Neither raises.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+
23
+ from tan.core.setools import find_app_gen_toc, resolve_setools_dir
24
+
25
+ from tan.core.flash_plan import FLOW_D_METHOD, FlashTarget, parse_system_manifest, select_flash_method
26
+
27
+ #: The method `select_flash_method` leaves a Flow A entry on: no `FLOW_D_KEYS`,
28
+ #: so `west flash` picks the board's default runner (`alif_flash`, SE-UART).
29
+ FLOW_A_METHOD = "zephyr_west_flash"
30
+
31
+ __all__ = [
32
+ "FLOW_A_METHOD",
33
+ "FLOW_D_METHOD",
34
+ "flash_methods_for_manifest_text",
35
+ "ProjectFlash",
36
+ "project_flash",
37
+ "project_flash_methods",
38
+ "verdict",
39
+ "signing_problems",
40
+ ]
41
+
42
+
43
+ def flash_methods_for_manifest_text(text: str) -> frozenset[str] | None:
44
+ """The set of `flash_method`s `tan flash` would dispatch for the slices of
45
+ this `system-manifest.yaml`, or `None` when the text cannot be parsed.
46
+
47
+ Slices only: a helper MCU's method is never a SETOOLS path."""
48
+ try:
49
+ manifest = parse_system_manifest(text)
50
+ except Exception: # noqa: BLE001 -- a doctor probe must not raise
51
+ return None
52
+ methods = set()
53
+ for s in manifest.slices:
54
+ method = select_flash_method(
55
+ FlashTarget(
56
+ kind="slice", id=s.core_id, flash_method=s.flash_method, flash_args=s.flash_args
57
+ )
58
+ )
59
+ if method:
60
+ methods.add(method)
61
+ return frozenset(methods)
62
+
63
+
64
+ def project_flash_methods(board_yaml: str | None) -> frozenset[str] | None:
65
+ """Methods for the project owning `board_yaml`, read from its
66
+ `build/system-manifest.yaml`. `None` when there is no project, no built
67
+ manifest yet, or it is unreadable/invalid -- the caller then phrases the
68
+ verdict per method instead of guessing one."""
69
+ if board_yaml is None:
70
+ return None
71
+ try:
72
+ text = (Path(board_yaml).parent / "build" / "system-manifest.yaml").read_text(
73
+ encoding="utf-8"
74
+ )
75
+ except (OSError, ValueError):
76
+ return None
77
+ return flash_methods_for_manifest_text(text)
78
+
79
+
80
+ def signing_problems(
81
+ setools_dir: str | None,
82
+ executables: tuple[str, ...],
83
+ find_app_gen_toc,
84
+ require_exec: bool = False,
85
+ ) -> list[str]:
86
+ """What stops SETOOLS from signing/writing: `$SETOOLS_DIR` unset, or a
87
+ directory lacking any of `executables`. `app-gen-toc` is located through
88
+ `find_app_gen_toc` (the `.exe`-aware lookup `tan flash` uses); with
89
+ `require_exec` it must also be executable on POSIX (Flow D spawns it)."""
90
+ if not setools_dir:
91
+ return [
92
+ "$SETOOLS_DIR is unset (the Alif Security Toolkit is license-gated and "
93
+ "NOT redistributed by alp-sdk)"
94
+ ]
95
+ absent: list[str] = []
96
+ for exe in executables:
97
+ try:
98
+ if exe == "app-gen-toc":
99
+ found = find_app_gen_toc(setools_dir)
100
+ ok = found is not None and (
101
+ not require_exec or os.name == "nt" or os.access(found, os.X_OK)
102
+ )
103
+ else:
104
+ ok = (Path(setools_dir) / exe).is_file()
105
+ except OSError:
106
+ ok = False
107
+ if not ok:
108
+ absent.append(exe)
109
+ if absent:
110
+ return [
111
+ f"$SETOOLS_DIR=`{setools_dir}` does not look like an "
112
+ f"app-release-exec-linux directory (no {', '.join(absent)}{' executable' if require_exec else ''})"
113
+ ]
114
+ return []
115
+
116
+
117
+ #: Said wherever doctor reports a SETOOLS verdict: doctor cannot see per-run
118
+ #: arguments, so a flag given only to `tan flash` is outside its view.
119
+ FLAG_NOTE = (
120
+ "doctor reads $SETOOLS_DIR and the built manifest's flash_args.setools_dir; "
121
+ "a --setools-dir flag given to `tan flash` is not visible to it"
122
+ )
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class ProjectFlash:
127
+ """What the project's built manifest says about flashing: the dispatched
128
+ `methods` and the first `flash_args.setools_dir` a slice carries."""
129
+
130
+ methods: frozenset[str]
131
+ setools_dir: str | None = None
132
+
133
+
134
+ def project_flash(board_yaml: str | None) -> ProjectFlash | None:
135
+ """[`project_flash_methods`] plus the manifest's `setools_dir`, resolved
136
+ with `tan.core.setools.resolve_setools_dir` (empty env, no flag) so it
137
+ reads the key exactly as `tan flash` does.
138
+
139
+ **Assumption:** the manifest lives at `<board.yaml dir>/build/
140
+ system-manifest.yaml`, the default `tan flash`/`tan build` use. A custom
141
+ `--build-root` is invisible here, and doctor then falls back to per-method
142
+ wording. `None` when no project or no readable manifest."""
143
+ methods = project_flash_methods(board_yaml)
144
+ if methods is None or board_yaml is None:
145
+ return None
146
+ setools_dir = None
147
+ try:
148
+ text = (Path(board_yaml).parent / "build" / "system-manifest.yaml").read_text(
149
+ encoding="utf-8"
150
+ )
151
+ for s in parse_system_manifest(text).slices:
152
+ found = resolve_setools_dir(s.flash_args, {}, None)
153
+ if found is not None:
154
+ setools_dir = found.path
155
+ break
156
+ except Exception: # noqa: BLE001 -- a doctor probe must not raise
157
+ setools_dir = None
158
+ return ProjectFlash(methods, setools_dir)
159
+
160
+
161
+ def verdict(
162
+ setools_dir: str | None,
163
+ setools_source: str,
164
+ se_uart: str | None,
165
+ flash_methods: frozenset[str] | None,
166
+ jlink_found: bool | None,
167
+ bundle: str,
168
+ executables: tuple[str, ...],
169
+ is_linux: bool = True,
170
+ ) -> tuple[str, str, str | None]:
171
+ """`(status, detail, fix)` for the `setools` check, per flash method."""
172
+ flow_d = flash_methods is not None and FLOW_D_METHOD in flash_methods
173
+ flow_a = flash_methods is not None and FLOW_A_METHOD in flash_methods
174
+ only_gen_toc = not flow_a
175
+ # Flow A's `alif_flash` runner hard-codes `app-release-exec-linux`; Flow D
176
+ # (J-Link + SETOOLS signing, `app-gen-toc.exe` on Windows) is not Linux-only.
177
+ linux_note = (
178
+ ""
179
+ if is_linux or not (flow_a or flash_methods is None)
180
+ else " Flow A (`west flash`, alif_flash) is Linux-only in this tree -- run it "
181
+ "from WSL2/Linux."
182
+ )
183
+ signing = signing_problems(
184
+ setools_dir,
185
+ ("app-gen-toc",) if only_gen_toc else executables,
186
+ find_app_gen_toc,
187
+ require_exec=flow_d,
188
+ )
189
+ d_problems = list(signing)
190
+ if flow_d and jlink_found is False:
191
+ d_problems.append("no J-Link tool (JLinkExe/JLink) on PATH or in the workspace venv")
192
+ a_problems = list(signing)
193
+ if flow_a and not se_uart:
194
+ a_problems.append(
195
+ "$SE_UART is unset (the SE-UART device: Linux /dev/ttyUSB*, macOS "
196
+ "/dev/cu.usbserial-*, a passed-through COM under WSL)"
197
+ )
198
+ if flash_methods is None:
199
+ problems = signing
200
+ lead = (
201
+ "AEN MRAM flashing (Flow D `alif_mram_jlink`, the planner default, and "
202
+ "Flow A `west flash`) cannot sign an ATOC: "
203
+ )
204
+ joined = "; ".join(problems)
205
+ else:
206
+ problems = []
207
+ if flow_d and d_problems:
208
+ problems.append("Flow D (`alif_mram_jlink`): " + "; ".join(d_problems))
209
+ if flow_a and a_problems:
210
+ problems.append("Flow A (`west flash`, the alif_flash runner): " + "; ".join(a_problems))
211
+ lead = "AEN MRAM flashing will fail: "
212
+ joined = " | ".join(problems)
213
+ if not problems:
214
+ tools = "app-gen-toc" if only_gen_toc else "/".join(executables)
215
+ ready = f"SETOOLS ready: SETOOLS dir `{setools_dir}` (from {setools_source}) has {tools}"
216
+ if flash_methods is None:
217
+ ready += (
218
+ ". Flow D (`alif_mram_jlink`) needs only this plus a J-Link (see the "
219
+ "`jlink` check); Flow A (`west flash`) also needs $SE_UART "
220
+ + (f"(`{se_uart}`, set)." if se_uart else "(currently unset).")
221
+ )
222
+ elif flow_a:
223
+ ready += f", $SE_UART=`{se_uart}`."
224
+ else:
225
+ ready += " and a J-Link is available (Flow D needs no SE-UART)."
226
+ return "pass", ready + linux_note + " (" + FLAG_NOTE + ".)", None
227
+ fix = (
228
+ f"Download the Alif Security Toolkit (`{bundle}`) from the Alif "
229
+ "developer portal -- it is license-gated and alp-sdk does not "
230
+ "redistribute it -- then `export SETOOLS_DIR=<...>/app-release-exec-linux`. "
231
+ "Flow A (`west flash`) additionally needs `export SE_UART=/dev/ttyUSB0` "
232
+ "(your SE-UART device); Flow D does not. See docs/aen-bench-bringup.md. "
233
+ + FLAG_NOTE[0].upper()
234
+ + FLAG_NOTE[1:]
235
+ + "."
236
+ )
237
+ return "warn", lead + joined + "." + linux_note, fix
@@ -0,0 +1,180 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan doctor`'s stale-install verdict: is the `tan` that is RUNNING behind
3
+ the source it was installed from?
4
+
5
+ Motivating case: a `pipx install git+https://github.com/alplabai/tan-cli.git@dev#subdirectory=python`
6
+ taken on one day keeps answering `tan 0.7.0` after `dev` has moved on, and
7
+ nothing says so -- agents kept using it unknowingly.
8
+
9
+ The question is answered from the install's own provenance record, PEP 610's
10
+ `direct_url.json` in the `tan-cli` dist-info, in order of cost:
11
+
12
+ * **path / editable install** (`file://` url): offline. Read the source
13
+ tree's `tan/version.py` `TAN_VERSION` and compare with the running one.
14
+ * **VCS install** (`vcs_info`): ONE `git ls-remote <url> <branch>` with a short
15
+ timeout, compared with the installed `commit_id`. Pinned installs (a tag or
16
+ a sha, which `refs/heads/<rev>` does not resolve) are intentional and pass.
17
+ * anything else (wheel/PyPI/frozen binary): no provenance, no verdict.
18
+
19
+ Never raises and never fails doctor: offline/timeout/no-git degrades to
20
+ `None` ("unable to check"), which the caller renders as a pass.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import re
26
+ import urllib.parse
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from tan.core.probe import probe
31
+
32
+ #: Network ceiling for the one `ls-remote`. Doctor must stay snappy offline.
33
+ LS_REMOTE_TIMEOUT_S = 4
34
+
35
+ _VERSION_RE = re.compile(r'^TAN_VERSION\s*=\s*"([^"]+)"', re.MULTILINE)
36
+ _SHA_RE = re.compile(r"^[0-9a-f]{40}$")
37
+ DIST_NAMES = ("tan-cli", "tan_cli", "tan")
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class InstallOrigin:
42
+ kind: str # "path" | "vcs"
43
+ url: str
44
+ commit: str | None = None # vcs: installed commit
45
+ revision: str | None = None # vcs: requested branch/tag/sha
46
+ subdirectory: str | None = None
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class StaleVerdict:
51
+ detail: str
52
+ fix: str
53
+
54
+
55
+ def read_install_origin(direct_url_text: str | None) -> InstallOrigin | None:
56
+ """Parse a PEP 610 `direct_url.json` body; `None` if absent/unusable."""
57
+ if not direct_url_text:
58
+ return None
59
+ try:
60
+ data = json.loads(direct_url_text)
61
+ except ValueError:
62
+ return None
63
+ if not isinstance(data, dict) or not isinstance(data.get("url"), str):
64
+ return None
65
+ url = data["url"]
66
+ sub = data.get("subdirectory")
67
+ sub = sub if isinstance(sub, str) else None
68
+ vcs = data.get("vcs_info")
69
+ if isinstance(vcs, dict) and vcs.get("vcs") == "git":
70
+ commit = vcs.get("commit_id")
71
+ rev = vcs.get("requested_revision")
72
+ return InstallOrigin(
73
+ "vcs",
74
+ url,
75
+ commit if isinstance(commit, str) else None,
76
+ rev if isinstance(rev, str) else None,
77
+ sub,
78
+ )
79
+ if url.startswith("file:"):
80
+ return InstallOrigin("path", url, subdirectory=sub)
81
+ return None
82
+
83
+
84
+ def installed_direct_url_text() -> str | None:
85
+ """The `direct_url.json` of the running tan's dist, or `None`."""
86
+ from importlib import metadata
87
+
88
+ for name in DIST_NAMES:
89
+ try:
90
+ return metadata.distribution(name).read_text("direct_url.json")
91
+ except metadata.PackageNotFoundError:
92
+ continue
93
+ except OSError:
94
+ return None
95
+ return None
96
+
97
+
98
+ def parse_tan_version(version_py_text: str) -> str | None:
99
+ m = _VERSION_RE.search(version_py_text)
100
+ return m.group(1) if m else None
101
+
102
+
103
+ def source_version(origin: InstallOrigin) -> str | None:
104
+ """`TAN_VERSION` of a `file://` source tree (offline); `None` if unreadable."""
105
+ from urllib.request import url2pathname
106
+
107
+ root = Path(url2pathname(urllib.parse.urlparse(origin.url).path))
108
+ for rel in (origin.subdirectory or "", "python", ""):
109
+ try:
110
+ text = (root / rel / "tan" / "version.py").read_text(encoding="utf-8")
111
+ except OSError:
112
+ continue
113
+ return parse_tan_version(text)
114
+ return None
115
+
116
+
117
+ def remote_head(origin: InstallOrigin, git_exe: str | None) -> str | None:
118
+ """The commit `origin.revision` (default `HEAD`) points at on the remote;
119
+ `None` when it cannot be learned or the revision is not a branch (a tag/sha
120
+ pin) -- either way there is no "newer" to report."""
121
+ if git_exe is None or (origin.revision and _SHA_RE.match(origin.revision)):
122
+ return None
123
+ ref = f"refs/heads/{origin.revision}" if origin.revision else "HEAD"
124
+ out = probe([git_exe, "ls-remote", origin.url, ref], timeout=LS_REMOTE_TIMEOUT_S)
125
+ if not out:
126
+ return None
127
+ sha = out.split(None, 1)[0].strip().lower()
128
+ return sha if _SHA_RE.match(sha) else None
129
+
130
+
131
+ def reinstall_command(origin: InstallOrigin) -> str:
132
+ spec = origin.url if origin.url.startswith("git+") else f"git+{origin.url}"
133
+ if origin.revision:
134
+ spec += f"@{origin.revision}"
135
+ if origin.subdirectory:
136
+ spec += f"#subdirectory={origin.subdirectory}"
137
+ return f'pipx install --force "{spec}"'
138
+
139
+
140
+ def stale_verdict(
141
+ running_version: str,
142
+ origin: InstallOrigin | None,
143
+ *,
144
+ source_ver: str | None = None,
145
+ remote_commit: str | None = None,
146
+ ) -> StaleVerdict | None:
147
+ """Pure verdict. `None` = not behind, or unable to tell."""
148
+ if origin is None:
149
+ return None
150
+ if origin.kind == "path":
151
+ if source_ver is None or source_ver == running_version:
152
+ return None
153
+ return StaleVerdict(
154
+ f"installed tan is {running_version} but the source it was installed from "
155
+ f"({origin.url}) is {source_ver} -- reinstall to pick it up",
156
+ "reinstall from that source (pipx install --force <source>/python)",
157
+ )
158
+ if origin.commit and remote_commit and origin.commit != remote_commit:
159
+ rev = origin.revision or "HEAD"
160
+ return StaleVerdict(
161
+ f"installed tan {running_version} was built from {origin.commit[:8]} but "
162
+ f"{rev} is now {remote_commit[:8]} -- this install is behind",
163
+ reinstall_command(origin),
164
+ )
165
+ return None
166
+
167
+
168
+ def running_tan_verdict(version: str, git_exe: str | None, offline: bool) -> StaleVerdict | None:
169
+ """IO half: read the running tan's provenance and judge it. `offline`
170
+ skips the one `git ls-remote`. Cannot raise."""
171
+ try:
172
+ origin = read_install_origin(installed_direct_url_text())
173
+ if origin is None:
174
+ return None
175
+ if origin.kind == "path":
176
+ return stale_verdict(version, origin, source_ver=source_version(origin))
177
+ remote = None if offline else remote_head(origin, git_exe)
178
+ return stale_verdict(version, origin, remote_commit=remote)
179
+ except Exception: # noqa: BLE001 -- a doctor probe must never traceback
180
+ return None