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
tan/core/dp_id.py ADDED
@@ -0,0 +1,97 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure SW-DP ID banner parsing for the read-only DPIDR preflight (tan-cli#520
3
+ generalised this from Flow D-only; tan-cli#795 split it out of `tan.core.
4
+ flash_plan` on its own, since these functions are pure string/bool logic that
5
+ nothing in `flash_plan.py` itself calls -- only `tan.commands.flash_cmd.
6
+ _flow_d_preflight`, the one caller that spawns `JLinkExe` and reads its
7
+ banner, does. `flash_plan.py` keeps `_validate_expect_dpidr_width` (it raises
8
+ `FlashPlanError`, defined there); this module has no IO and raises nothing.
9
+
10
+ Extracted verbatim (names unchanged, including the leading underscore --
11
+ matches this codebase's existing cross-module convention for a name that is
12
+ implementation-private to the *feature*, not to the *file*, e.g. `flash_plan.
13
+ _DEV_ROOT` imported straight into `flash_cmd.py`), no behaviour change -- see
14
+ tan-cli#312/#369/#373/#795 in the docstrings below for the history of each
15
+ refusal shape this parsing feeds.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import re
20
+
21
+
22
+ def _dp_id_matches(expected: str, banner: str) -> bool:
23
+ """Whether the banner's reported SW-DP ID equals `expected`, compared as
24
+ PARSED 32-bit values (tan-cli#795) rather than the prior unanchored
25
+ substring match (`_hex_in`, since removed): that match accepted any
26
+ truncation of `expected` that happened to appear in the banner --
27
+ `0x2477` matched `Found SW-DP with ID 0x0BE12477`, and `0x477` (ARM's own
28
+ JEP106 designer field, shared by every ARM SW-DP) matched every ARM
29
+ board's banner. The caller already refuses a short `expected` via
30
+ `flash_plan.validate_flow_d_preflight_args`'s own width check
31
+ (`flash_plan._validate_expect_dpidr_width`, run at plan time, before this
32
+ ever executes -- see `tan.commands.flash_cmd._flow_d_preflight`);
33
+ `_dp_id_value` extracts the banner's own reported ID the same way the
34
+ mismatch-reporting branch below already does."""
35
+ actual = _dp_id_value(banner)
36
+ if actual is None:
37
+ return False
38
+ return int(actual, 16) == int(expected, 16)
39
+
40
+
41
+ #: SEGGER's own wording for a successful SWD connect that read AN id, whatever
42
+ #: it turned out to be -- "Found SW-DP with ID 0x........" / "DPIDR: 0x........".
43
+ #: Matched loosely on purpose: what this distinguishes is "a real board
44
+ #: answered with a different identity" from "nothing answered", not the exact
45
+ #: firmware/DLL version's phrasing. The hex value is its own capture group
46
+ #: (tan-cli#512) so `_dp_id_value` can report what was ACTUALLY read, not only
47
+ #: whether something was -- `_dp_id_reported` still just asks whether this
48
+ #: matches at all.
49
+ _DP_ID_RE = re.compile(r"(?:with\s+ID|DPIDR)\s*:?\s*(0x[0-9A-Fa-f]+)", re.IGNORECASE)
50
+
51
+ #: SEGGER's own wording for the PROBE itself refusing the connection outright
52
+ #: -- measured verbatim on the rc3 bench run: "Connecting to J-Link ...FAILED:
53
+ #: Cannot connect to the probe/programmer." (tan-cli#312). Deliberately NOT a
54
+ #: bare `FAILED`/`Cannot connect` match (that was the tan-cli#312 review
55
+ #: finding): JLinkExe prints "FAILED" in many contexts, and "Cannot connect"
56
+ #: alone also fires on a TARGET-level refusal -- see `_CONNECT_FAILED_TARGET_RE`
57
+ #: below -- which is a real wiring/probe-selection problem, not a re-enumerating
58
+ #: probe.
59
+ _CONNECT_FAILED_RE = re.compile(r"Cannot connect to the probe/programmer", re.IGNORECASE)
60
+
61
+ #: SEGGER's own wording for a TARGET-level connect refusal -- "Cannot connect
62
+ #: to target." (unplugged SWD ribbon, unpowered board) or "Cannot connect to
63
+ #: J-Link." (a probe that IS reachable via USB but refuses the requested
64
+ #: `jlink_serial`). Both are genuine wiring/probe-selection problems, so their
65
+ #: presence forces `_connect_failed_outright` to False even alongside the
66
+ #: probe-level phrase above -- asserting "wiring is fine" here would be the
67
+ #: false negative tan-cli#312's review flagged (measured against a real
68
+ #: unplugged-ribbon and a real unpowered-target banner).
69
+ _CONNECT_FAILED_TARGET_RE = re.compile(r"Cannot connect to (?:target|J-Link)\b", re.IGNORECASE)
70
+
71
+
72
+ def _dp_id_reported(banner: str) -> bool:
73
+ """Whether the banner names ANY SW-DP ID -- not whether it matches
74
+ `expected` (the caller already ruled that out via `_dp_id_matches`), only
75
+ whether a connect got far enough to read one at all."""
76
+ return _DP_ID_RE.search(banner) is not None
77
+
78
+
79
+ def _dp_id_value(banner: str) -> str | None:
80
+ """The actual SW-DP ID text the banner reported, verbatim (whatever hex
81
+ casing/`0x` spelling SEGGER printed), or `None` if `_DP_ID_RE` does not
82
+ match at all. tan-cli#512: a caller that already knows `_dp_id_reported(
83
+ banner)` is `True` gets a non-`None` value here by construction -- both
84
+ read the same regex against the same banner."""
85
+ match = _DP_ID_RE.search(banner)
86
+ return match.group(1) if match else None
87
+
88
+
89
+ def _connect_failed_outright(banner: str) -> bool:
90
+ """Whether the banner carries SEGGER's own wording for the PROBE itself
91
+ refusing the connection (still re-enumerating, no board reachable at all),
92
+ as opposed to a TARGET-level refusal -- a real wiring/probe-selection
93
+ problem that must keep the original remediation, not the re-enumeration
94
+ one."""
95
+ if _CONNECT_FAILED_TARGET_RE.search(banner) is not None:
96
+ return False
97
+ return _CONNECT_FAILED_RE.search(banner) is not None
tan/core/elf_load.py ADDED
@@ -0,0 +1,63 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The lowest load address of an ELF image -- pure, stdlib-only.
3
+
4
+ `tan build --board` (tan-cli#1370) needs one fact about a Zephyr ELF to decide
5
+ how the image may be run: whether it is linked into the TCM (its lowest
6
+ `PT_LOAD` sits below the SoC's flash base) or into flash. This reads the program
7
+ headers and nothing else; it never raises on a file that is not an ELF.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import struct
12
+ from pathlib import Path
13
+
14
+ _PT_LOAD = 1
15
+
16
+
17
+ def lowest_load_address(path: str | Path) -> int | None:
18
+ """Lowest `p_paddr` among `PT_LOAD` segments that carry bytes (`p_filesz > 0`).
19
+
20
+ A NOBITS `.bss` segment (filesz 0) sits in RAM whatever the image is linked
21
+ to, so counting it would call every flash image a RAM image. `None` means
22
+ UNCLASSIFIABLE -- unreadable, not a little-endian ELF32/ELF64, a malformed
23
+ header table, or no loadable segment with bytes -- and never raises."""
24
+ try:
25
+ data = Path(path).read_bytes()
26
+ return _lowest(data)
27
+ except (OSError, struct.error, ValueError):
28
+ return None
29
+
30
+
31
+ def _lowest(data: bytes) -> int | None:
32
+ if len(data) < 52 or data[:4] != b"\x7fELF" or data[5] != 1:
33
+ return None
34
+ if data[4] == 1: # ELF32
35
+ min_ent, word = 32, "<I"
36
+ phoff, = struct.unpack_from("<I", data, 28)
37
+ phentsize, phnum = struct.unpack_from("<HH", data, 42)
38
+ paddr_at, filesz_at = 12, 16
39
+ elif data[4] == 2: # ELF64
40
+ min_ent, word = 56, "<Q"
41
+ if len(data) < 64:
42
+ return None
43
+ phoff, = struct.unpack_from("<Q", data, 32)
44
+ phentsize, phnum = struct.unpack_from("<HH", data, 54)
45
+ paddr_at, filesz_at = 24, 32
46
+ else:
47
+ return None
48
+ if phentsize < min_ent or phnum == 0:
49
+ return None
50
+ lowest: int | None = None
51
+ for i in range(phnum):
52
+ off = phoff + i * phentsize
53
+ if off + phentsize > len(data):
54
+ return None
55
+ p_type, = struct.unpack_from("<I", data, off)
56
+ if p_type != _PT_LOAD:
57
+ continue
58
+ filesz, = struct.unpack_from(word, data, off + filesz_at)
59
+ if filesz == 0:
60
+ continue
61
+ paddr, = struct.unpack_from(word, data, off + paddr_at)
62
+ lowest = paddr if lowest is None else min(lowest, paddr)
63
+ return lowest
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The alp-sdk error/diagnostic catalogue, as `tan explain --code` reads it.
3
+
4
+ Port of the lookup half of alp-sdk's `scripts/alp_cli/explain.py` (ADR-0020
5
+ end-state B: the verb moves to `tan`, it does not disappear). That command is
6
+ the only reader of `<sdk>/metadata/error-catalog.json` today, and retiring it
7
+ without this port would leave every `ALP-Bxxx` landing page and every
8
+ `ALP_ERR_*` enum comment unreachable from any CLI.
9
+
10
+ **The source is the generated catalogue, not the markdown.** The task this
11
+ was written for described `explain.py` as reading `docs/diagnostics/ALP-B*.md`
12
+ and the `ALP_ERR_*` enum directly. Measured against alp-sdk `origin/dev`, it
13
+ does not: it reads `metadata/error-catalog.json` and nothing else
14
+ (`explain.py:24-25,39-42`). Those two sources are what
15
+ `scripts/gen_error_catalog.py` reads, and that generator stays in alp-sdk --
16
+ it is a `pr-generated-files.yml` drift gate over alp-sdk's own headers and
17
+ docs, not a user command, so ADR-0020 does not move it. tan reading the
18
+ generated JSON is also the only shape compatible with I-26/ADR-0017: tan
19
+ learns no hardware fact and parses no C header, it reads one JSON file out of
20
+ the bound checkout the same way `presets` reads `metadata/e1m_modules/`.
21
+
22
+ Pure apart from the one file read, so the lookup, the suggestion list and the
23
+ rendered field order are unit-testable without a checkout on disk.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import difflib
29
+ import json
30
+ import os
31
+ from pathlib import Path
32
+
33
+ #: `<sdk_root>/metadata/error-catalog.json`, as path segments -- the same
34
+ #: shape `tan.core.shapes.SDK_MARKER` uses so a caller may join it either way.
35
+ CATALOG_RELATIVE = ("metadata", "error-catalog.json")
36
+
37
+ #: Rendered field order and label, verbatim from `explain.py`'s `_FIELD_ORDER`.
38
+ #: A field the catalogue omits (an `api-error` carries no `cause`/`fix`) is
39
+ #: SKIPPED, never rendered empty -- the generator's own "never fabricate a
40
+ #: field that has no source" rule, preserved on the reading side.
41
+ FIELD_ORDER: tuple[tuple[str, str], ...] = (
42
+ ("summary", "summary"),
43
+ ("cause", "cause"),
44
+ ("fix", "fix"),
45
+ ("doc", "doc"),
46
+ )
47
+
48
+ #: `difflib.get_close_matches` tuning, verbatim from `explain.py:87-88`. Kept
49
+ #: as named constants because the SUGGESTION LIST is user-visible output: a
50
+ #: different cutoff silently changes what a typo is answered with.
51
+ SUGGESTION_LIMIT = 5
52
+ SUGGESTION_CUTOFF = 0.4
53
+
54
+
55
+ class CatalogUnreadable(Exception):
56
+ """The bound checkout has no readable `metadata/error-catalog.json`.
57
+
58
+ Raised for an absent file, an unreadable one, malformed JSON, and a
59
+ document whose top level is not an object -- one exception for all four
60
+ because the caller's answer is the same in every case: name the path it
61
+ looked at and say the checkout cannot answer, rather than guessing which
62
+ of alp-sdk's several possible states produced it.
63
+ """
64
+
65
+ def __init__(self, path: Path, detail: str) -> None:
66
+ super().__init__(f"{path}: {detail}")
67
+ self.path = path
68
+ self.detail = detail
69
+
70
+
71
+ def catalog_path(sdk_root: Path | str) -> Path:
72
+ """`<sdk_root>/metadata/error-catalog.json`."""
73
+ return Path(os.path.join(str(sdk_root), *CATALOG_RELATIVE))
74
+
75
+
76
+ def load_codes(sdk_root: Path | str) -> dict[str, dict]:
77
+ """The `{code: entry}` map from the bound checkout's catalogue.
78
+
79
+ Raises `CatalogUnreadable`; never returns a partial map. A document that
80
+ parses but carries no `codes` key yields `{}` rather than raising, exactly
81
+ as `explain.py`'s `data.get("codes", {})` does -- an empty catalogue is a
82
+ catalogue in which every code is unknown, which the miss path already
83
+ reports honestly.
84
+
85
+ Every VALUE in `codes` is also checked here, not just `codes` itself: a
86
+ catalogue shaped `{"codes": {"ALP-B003": "not an object"}}` (also a list,
87
+ also `null`) otherwise reaches `lookup`, matches on `ALP-B003`, and
88
+ `resolve_code`'s `entry = codes[key]` then hands a non-dict to
89
+ `summary_line`/`detail_lines`, both of which call `entry.get(...)`.
90
+ Measured with this guard removed: `AttributeError: 'str' object has no
91
+ attribute 'get'`, surfacing as `explain.internal-failure` at exit 5 for a
92
+ checkout problem the reader could fix -- the same exit-1-not-exit-5
93
+ concern `codes` itself is guarded for, just one dereference deeper.
94
+ """
95
+ path = catalog_path(sdk_root)
96
+ try:
97
+ raw = path.read_text(encoding="utf-8")
98
+ except FileNotFoundError:
99
+ raise CatalogUnreadable(
100
+ path,
101
+ "error catalog not found -- run `python3 scripts/gen_error_catalog.py` "
102
+ "in the alp-sdk checkout",
103
+ ) from None
104
+ except (OSError, UnicodeDecodeError) as err:
105
+ raise CatalogUnreadable(path, f"error catalog is unreadable: {err}") from None
106
+ try:
107
+ data = json.loads(raw)
108
+ except ValueError as err:
109
+ raise CatalogUnreadable(path, f"error catalog is not valid JSON: {err}") from None
110
+ if not isinstance(data, dict):
111
+ raise CatalogUnreadable(path, "error catalog is not a JSON object")
112
+ codes = data.get("codes", {})
113
+ if not isinstance(codes, dict):
114
+ raise CatalogUnreadable(path, "error catalog's `codes` is not a JSON object")
115
+ for code, entry in codes.items():
116
+ if not isinstance(entry, dict):
117
+ raise CatalogUnreadable(
118
+ path, f"error catalog entry '{code}' is not a JSON object"
119
+ )
120
+ return codes
121
+
122
+
123
+ def normalise(code: str) -> str:
124
+ """Canonicalise user input for matching -- `explain.py`'s `_normalise`.
125
+
126
+ Case-insensitive and whitespace-trimmed, so `alp-b003`, ` ALP-B003 ` and
127
+ `alp_err_no_backend` all resolve.
128
+ """
129
+ return code.strip().upper()
130
+
131
+
132
+ def lookup(codes: dict[str, dict], query: str) -> tuple[str | None, list[str]]:
133
+ """`(matched_key, suggestions)` for `query` against `codes`.
134
+
135
+ On a hit the suggestion list is empty; on a miss the key is `None` and the
136
+ suggestions are `explain.py`'s own `difflib` shortlist, mapped back to the
137
+ catalogue's real spelling. Both halves are returned together because the
138
+ caller needs exactly one of them and deciding which is this function's job.
139
+ """
140
+ by_upper = {normalise(key): key for key in codes}
141
+ wanted = normalise(query)
142
+ key = by_upper.get(wanted)
143
+ if key is not None:
144
+ return key, []
145
+ close = difflib.get_close_matches(
146
+ wanted, list(by_upper), n=SUGGESTION_LIMIT, cutoff=SUGGESTION_CUTOFF
147
+ )
148
+ return None, [by_upper[match] for match in close]
149
+
150
+
151
+ def summary_line(entry: dict) -> str:
152
+ """`<CODE> (<kind>)` -- the header `explain.py`'s `_render` paints cyan.
153
+
154
+ ONE space, where `explain.py` writes two (`f"{code} ({kind})"`): tan's
155
+ `explain` summary is `"<label> (<id>)"` for every other selector kind and a
156
+ per-mode spacing rule inside one command is a worse surface than a
157
+ one-character divergence. The CODE and the KIND are verbatim.
158
+ """
159
+ return f"{entry.get('code', '')} ({entry.get('kind', '')})".strip()
160
+
161
+
162
+ def detail_lines(entry: dict) -> list[str]:
163
+ """`<label>: <value>` per populated field, in `FIELD_ORDER`.
164
+
165
+ The values are verbatim catalogue text -- no re-wrapping, no markdown
166
+ stripping, no truncation. `explain_cmd` prefixes each with `- ` and wraps
167
+ it exactly as it wraps a template's detail lines.
168
+ """
169
+ return [
170
+ f"{label}: {entry[key]}" for key, label in FIELD_ORDER if entry.get(key)
171
+ ]