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,481 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """J-Link probe selection for `tan flash` (tan-cli#1312). Pure decision logic
3
+ plus one read-only sysfs enumerator.
4
+
5
+ WHY THIS EXISTS. J-Link Commander selects an emulator by SERIAL only
6
+ (`SelectEmuBySN`). On the Alp Lab bench every probe carries the SAME OEM-cloned
7
+ serial, so a serial cannot tell two boards apart, and a Flow D MRAM write
8
+ issued with a shared serial lands on whichever probe the J-Link DLL opens
9
+ first. What DOES identify a probe on Linux is its USB port path (`3-4.2`, the
10
+ sysfs device name), and that path is not something J-Link can be told.
11
+
12
+ THE DESIGN (the honest one), in three layers:
13
+
14
+ 1. SELECTION (`resolve_probe_selection`, from a sysfs enumeration). A
15
+ `--probe-usb-path` is resolved to the serial sysfs reports for that port. A
16
+ serial (CLI or `flash_args.jlink_serial`) is matched against the visible
17
+ probes by its CANONICAL form (`canon_serial`: `603000869` and
18
+ `000603000869` are the same serial -- YAML turns the unquoted one into an
19
+ int). A serial no visible probe carries is refused; one that several visible
20
+ probes carry is refused unless a USB path names the target.
21
+ 2. VERIFICATION at spawn time (`verify_emulator_list`). Selection alone cannot
22
+ make J-Link open the right probe when serials collide -- only isolation
23
+ OUTSIDE tan can (a wrapper masking every other probe's `/dev/bus/usb` node
24
+ AND sysfs directory in a private mount namespace). So immediately before
25
+ EACH `JLinkExe` spawn tan runs the same binary read-only (`ShowEmuList`) and
26
+ requires exactly one emulator whose canonical serial matches the selection
27
+ (and that the listing run itself succeeded: rc 0, no timeout). When the
28
+ serial is SHARED, one emulator in the list still proves nothing about WHICH
29
+ port it is -- a wrapper masking the wrong place passes that -- so the
30
+ wrapper must also print `TAN_PROBE_ISOLATED_USB_PATH=<its target port>`
31
+ (`HANDSHAKE_PREFIX`) and tan requires it to equal the selected path; the
32
+ envelope then says `wrapper-attested:<path>`, never "verified". A masking
33
+ wrapper on PATH passes; plain `JLinkExe` with cloned serials refuses.
34
+ `TAN_PROBE_USB_PATH=<resolved path>` is exported in that spawn's environment
35
+ so a wrapper can cross-check its own mask against it.
36
+ 3. TOCTOU (`snapshot_drift`). The enumeration is repeated before each spawn
37
+ and the run refuses if the set of (path, canonical serial) changed since
38
+ selection.
39
+
40
+ Enumeration is strictly read-only (directory listing + reading two sysfs
41
+ attributes). It never opens a probe.
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import os
46
+ import re
47
+ import sys
48
+ from dataclasses import dataclass
49
+ from typing import Any
50
+
51
+ #: SEGGER's USB vendor id as sysfs spells it.
52
+ SEGGER_VENDOR_ID = "1366"
53
+
54
+ SYSFS_USB_DEVICES = "/sys/bus/usb/devices"
55
+
56
+ #: A sysfs USB device name for a device on a hub port: `<bus>-<port>[.<port>...]`.
57
+ _USB_PATH_RE = re.compile(r"^[0-9]+-[0-9]+(\.[0-9]+)*$")
58
+
59
+ #: Exported into every `JLinkExe` spawn of a selected probe: the resolved USB
60
+ #: path, for a wrapper to cross-check its mask against.
61
+ USB_PATH_ENV = "TAN_PROBE_USB_PATH"
62
+
63
+ ISOLATION_VERIFIED = "verified-single-emulator"
64
+
65
+ #: A line a wrapper prints (from its actual target port) into `ShowEmuList`
66
+ #: output; the only way tan can bind a single listed emulator to a USB path.
67
+ HANDSHAKE_PREFIX = "TAN_PROBE_ISOLATED_USB_PATH="
68
+ _HANDSHAKE_RE = re.compile(r"^" + HANDSHAKE_PREFIX + r"(\S+)\s*$", re.MULTILINE)
69
+
70
+ CODE_AMBIGUOUS = "ambiguous"
71
+ CODE_NOT_FOUND = "not-found"
72
+ CODE_CONFLICT = "selector-conflict"
73
+ #: A spawn-time guard failure that is NOT an ambiguity: the check itself could
74
+ #: not be completed or found nothing (`flash.probe-verify-failed`).
75
+ CODE_VERIFY_FAILED = "verify-failed"
76
+
77
+ #: One `ShowEmuList` line: `J-Link[0]: Connection: USB, Serial number: 000603000869, ...`.
78
+ _EMU_SERIAL_RE = re.compile(r"Serial number:\s*([0-9A-Za-z]+)")
79
+
80
+
81
+ class ProbeEnumerationError(Exception):
82
+ """A device directory that is populated but cannot be read (unreadable
83
+ `idVendor`, or a SEGGER device with no readable `serial`). NOT the same as
84
+ an empty directory, which is what a mount-namespace mask leaves behind."""
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class JLinkProbe:
89
+ """One visible J-Link: its USB port path and the serial sysfs reports."""
90
+
91
+ usb_path: str
92
+ serial: str
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class ProbeSelection:
97
+ """The outcome of resolving which probe a run will use.
98
+
99
+ `refusal_code` is one of the `CODE_*` suffixes (the issue code is
100
+ `flash.probe-<suffix>`) and is set iff the run must refuse before any write.
101
+ `serial` is what Flow D must put in `SelectEmuBySN` (`None`: pin nothing).
102
+ `candidates` lists every visible probe sharing `serial` when more than one
103
+ does -- the case only spawn-time verification can settle.
104
+ """
105
+
106
+ serial: str | None
107
+ usb_path: str | None
108
+ source: str
109
+ visible: int | None
110
+ candidates: tuple[str, ...] = ()
111
+ overrides_manifest_serial: str | None = None
112
+ refusal_code: str | None = None
113
+ refusal: str | None = None
114
+
115
+ def as_dict(self) -> dict[str, Any]:
116
+ out: dict[str, Any] = {
117
+ "source": self.source,
118
+ "serial": self.serial,
119
+ "usbPath": self.usb_path,
120
+ "visibleProbes": self.visible,
121
+ }
122
+ if self.overrides_manifest_serial is not None:
123
+ out["overridesFlashArgsSerial"] = self.overrides_manifest_serial
124
+ if self.candidates:
125
+ out["candidates"] = list(self.candidates)
126
+ return out
127
+
128
+ def describe(self) -> str:
129
+ who = self.serial if self.serial is not None else "(none pinned)"
130
+ where = f" at USB path {self.usb_path}" if self.usb_path else ""
131
+ shared = ""
132
+ if len(self.candidates) > 1 and self.usb_path:
133
+ shared = (
134
+ f"; serial shared by {', '.join(self.candidates)} -- a real run needs the "
135
+ "J-Link on PATH to show exactly one emulator with it (masking wrapper)"
136
+ )
137
+ return f"probe selection: serial {who}{where} via {self.source}{shared}"
138
+
139
+
140
+ def canon_serial(text: str) -> str:
141
+ """The comparison form of a J-Link serial: stripped, and when all digits,
142
+ without leading zeros (`000603000869` == `603000869`)."""
143
+ text = text.strip()
144
+ return str(int(text)) if text.isascii() and text.isdigit() else text
145
+
146
+
147
+ def is_valid_usb_path(text: str) -> bool:
148
+ return bool(_USB_PATH_RE.match(text))
149
+
150
+
151
+ def enumerate_jlinks(root: str | None = None) -> list[JLinkProbe] | None:
152
+ """Every SEGGER device visible in sysfs, sorted by path. READ-ONLY.
153
+
154
+ `None` means "this host cannot enumerate" (no sysfs USB tree: macOS,
155
+ Windows, a container without sysfs) -- distinct from `[]`, "enumerated and
156
+ found none". Raises `ProbeEnumerationError` for a populated device
157
+ directory it cannot read -- never read as "no probe there"."""
158
+ if root is None and not sys.platform.startswith("linux"):
159
+ return None # no sysfs USB tree to read: cannot enumerate
160
+ root = root or SYSFS_USB_DEVICES
161
+ try:
162
+ names = sorted(os.listdir(root))
163
+ except (FileNotFoundError, NotADirectoryError):
164
+ return None
165
+ except OSError as err:
166
+ raise ProbeEnumerationError(f"{root}: {err.strerror or err}") from err
167
+ probes: list[JLinkProbe] = []
168
+ for name in names:
169
+ # Interfaces (`3-4.2:1.0`) and root hubs (`usb3`) are not devices on a
170
+ # port; only `<bus>-<port>` names are.
171
+ if ":" in name or not _USB_PATH_RE.match(name):
172
+ continue
173
+ probe = _read_device(os.path.join(root, name), name)
174
+ if probe is not None:
175
+ probes.append(probe)
176
+ return probes
177
+
178
+
179
+ def _read_device(base: str, name: str) -> JLinkProbe | None:
180
+ try:
181
+ populated = bool(os.listdir(base))
182
+ except (FileNotFoundError, NotADirectoryError):
183
+ populated = False # unplugged between the two listings, or not a device dir
184
+ except OSError as err:
185
+ raise ProbeEnumerationError(f"{name}: {err.strerror or err}") from err
186
+ if not populated:
187
+ return None # an empty directory is what a namespace mask leaves
188
+ vendor = _read_attr(os.path.join(base, "idVendor"))
189
+ if vendor is None:
190
+ raise ProbeEnumerationError(f"{name}: idVendor is unreadable")
191
+ if vendor != SEGGER_VENDOR_ID:
192
+ return None
193
+ serial = _read_attr(os.path.join(base, "serial"))
194
+ if not serial:
195
+ raise ProbeEnumerationError(f"{name}: a SEGGER device whose serial is unreadable")
196
+ return JLinkProbe(usb_path=name, serial=serial)
197
+
198
+
199
+ def _read_attr(path: str) -> str | None:
200
+ try:
201
+ with open(path, encoding="utf-8", errors="replace") as handle:
202
+ return handle.read().strip()
203
+ except OSError:
204
+ return None
205
+
206
+
207
+ def probe_set(probes: list[JLinkProbe] | None) -> frozenset[tuple[str, str]] | None:
208
+ """The identity of an enumeration for the TOCTOU comparison."""
209
+ if probes is None:
210
+ return None
211
+ return frozenset((p.usb_path, canon_serial(p.serial)) for p in probes)
212
+
213
+
214
+ def snapshot_drift(
215
+ snapshot: frozenset[tuple[str, str]] | None, now: list[JLinkProbe] | None
216
+ ) -> str | None:
217
+ """A refusal when the visible probes changed between selection and spawn."""
218
+ if snapshot is None:
219
+ return None
220
+ current = probe_set(now)
221
+ if current == snapshot:
222
+ return None
223
+ return (
224
+ "the visible J-Links changed between probe selection and the spawn "
225
+ f"(was {_fmt_set(snapshot)}, now {_fmt_set(current)}); refusing rather than "
226
+ "writing through a probe set that was not the one selected."
227
+ )
228
+
229
+
230
+ def _fmt_set(items: frozenset[tuple[str, str]] | None) -> str:
231
+ if items is None:
232
+ return "unenumerable"
233
+ return ", ".join(f"{p} ({s})" for p, s in sorted(items)) or "none"
234
+
235
+
236
+ def parse_show_emu_list(text: str) -> list[str]:
237
+ """The serials a `ShowEmuList` output names, in order."""
238
+ return _EMU_SERIAL_RE.findall(text)
239
+
240
+
241
+ def parse_handshakes(text: str) -> list[str]:
242
+ """The distinct `TAN_PROBE_ISOLATED_USB_PATH=` values a listing carries."""
243
+ return sorted(set(_HANDSHAKE_RE.findall(text)))
244
+
245
+
246
+ def verify_listing(
247
+ serial: str | None,
248
+ usb_path: str | None,
249
+ candidates: tuple[str, ...],
250
+ listing: str,
251
+ ) -> tuple[str | None, str | None, str | None]:
252
+ """Spawn-time verification of a `ShowEmuList` listing. Returns
253
+ `(code, message, isolation)`: `code is None` means proceed and `isolation`
254
+ is what the envelope reports; else `code` is `CODE_AMBIGUOUS` or
255
+ `CODE_VERIFY_FAILED`.
256
+
257
+ `serial=None` is the unpinned run on a host that cannot enumerate: exactly
258
+ one emulator in total is required."""
259
+ emus = parse_show_emu_list(listing)
260
+ want = canon_serial(serial) if serial is not None else None
261
+ matching = [e for e in emus if want is None or canon_serial(e) == want]
262
+ if not matching:
263
+ return (CODE_VERIFY_FAILED,
264
+ f"J-Link's own ShowEmuList shows no emulator"
265
+ f"{' with serial ' + serial if serial else ''} "
266
+ f"(saw: {', '.join(emus) or 'none'}); refusing to write.", None)
267
+ if len(matching) > 1:
268
+ return (CODE_AMBIGUOUS, _several_emulators(len(matching), serial), None)
269
+ return _bind_to_path(usb_path, candidates, parse_handshakes(listing))
270
+
271
+
272
+ def _several_emulators(n: int, serial: str | None) -> str:
273
+ return (
274
+ f"J-Link's own ShowEmuList shows {n} emulators"
275
+ f"{' with serial ' + serial if serial else ''}, so SelectEmuBySN would open "
276
+ "whichever enumerates first -- possibly the wrong board. J-Link selects by "
277
+ "serial only; run it where only the target probe is visible (a wrapper masking "
278
+ "the other probes' /dev/bus/usb nodes AND sysfs directories in a private mount "
279
+ "namespace) or give the probes distinct serials. flash_args.expect_dpidr stays "
280
+ "mandatory on a shared-serial bench: it is the only check that the board "
281
+ "answering is the one intended."
282
+ )
283
+
284
+
285
+ def _bind_to_path(
286
+ usb_path: str | None, candidates: tuple[str, ...], handshakes: list[str]
287
+ ) -> tuple[str | None, str | None, str | None]:
288
+ if len(handshakes) > 1:
289
+ return (CODE_AMBIGUOUS,
290
+ f"ShowEmuList carries conflicting {HANDSHAKE_PREFIX} lines "
291
+ f"({', '.join(handshakes)}); refusing.", None)
292
+ if handshakes and usb_path is not None and handshakes[0] != usb_path:
293
+ return (CODE_AMBIGUOUS,
294
+ f"the J-Link wrapper is isolated to USB path {handshakes[0]} but "
295
+ f"{usb_path} was selected; refusing -- this is the wrong place.", None)
296
+ if handshakes:
297
+ return None, None, f"wrapper-attested:{handshakes[0]}"
298
+ if len(candidates) > 1:
299
+ return (CODE_AMBIGUOUS,
300
+ f"serial is shared by {', '.join(candidates)}: one emulator in "
301
+ "ShowEmuList cannot be bound to the selected USB path by itself (a "
302
+ "wrapper masking the WRONG place would look identical). The J-Link on "
303
+ f"PATH must print {HANDSHAKE_PREFIX}<its target port> and it must equal "
304
+ f"{usb_path}.", None)
305
+ return None, None, ISOLATION_VERIFIED
306
+
307
+
308
+ def _paths(probes: list[JLinkProbe]) -> tuple[str, ...]:
309
+ return tuple(p.usb_path for p in probes)
310
+
311
+
312
+ def _refuse(
313
+ code: str,
314
+ message: str,
315
+ *,
316
+ serial: str | None,
317
+ usb_path: str | None,
318
+ source: str,
319
+ visible: int | None,
320
+ candidates: tuple[str, ...] = (),
321
+ ) -> ProbeSelection:
322
+ return ProbeSelection(
323
+ serial=serial,
324
+ usb_path=usb_path,
325
+ source=source,
326
+ visible=visible,
327
+ candidates=candidates,
328
+ refusal_code=code,
329
+ refusal=message,
330
+ )
331
+
332
+
333
+ _HINT = (
334
+ "Pass --probe-usb-path <bus-port> to name one probe. Note J-Link Commander selects "
335
+ "by serial only, so with shared serials the write is additionally refused unless "
336
+ "the J-Link on PATH is isolated to that one probe (a masking wrapper). "
337
+ "flash_args.expect_dpidr stays mandatory on a shared-serial bench."
338
+ )
339
+
340
+
341
+ def resolve_probe_selection(
342
+ probes: list[JLinkProbe] | None,
343
+ *,
344
+ cli_serial: str | None,
345
+ cli_usb_path: str | None,
346
+ manifest_serial: str | None,
347
+ ) -> ProbeSelection:
348
+ """Decide which J-Link a Flow D run will use, or that it must refuse.
349
+
350
+ `probes` is `enumerate_jlinks()`'s answer (`None`: cannot enumerate).
351
+ Precedence: `--probe-usb-path`, then `--probe-serial`, then
352
+ `flash_args.jlink_serial`; the CLI selectors override the manifest for this
353
+ run. Pure."""
354
+ if cli_usb_path is not None:
355
+ return _by_usb_path(probes, cli_usb_path, cli_serial, manifest_serial)
356
+ serial = cli_serial if cli_serial is not None else manifest_serial
357
+ if serial is not None:
358
+ return _by_serial(probes, serial, cli_serial, manifest_serial)
359
+ return _unselected(probes)
360
+
361
+
362
+ def _usb_path_refusal(
363
+ probes: list[JLinkProbe] | None, path: str, cli_serial: str | None
364
+ ) -> ProbeSelection | None:
365
+ visible = None if probes is None else len(probes)
366
+ if probes is None:
367
+ return _refuse(
368
+ CODE_NOT_FOUND,
369
+ f"--probe-usb-path {path}: this host cannot enumerate USB devices (no "
370
+ "sysfs), so tan cannot verify which probe that is. Refusing rather than "
371
+ "writing through an unverified probe.",
372
+ serial=None, usb_path=path, source="cli-usb-path", visible=None,
373
+ )
374
+ hit = next((p for p in probes if p.usb_path == path), None)
375
+ if hit is None:
376
+ return _refuse(
377
+ CODE_NOT_FOUND,
378
+ f"--probe-usb-path {path}: no J-Link is visible at that USB path. "
379
+ f"Visible J-Links: {', '.join(_paths(probes)) or 'none'}.",
380
+ serial=None, usb_path=path, source="cli-usb-path",
381
+ visible=visible, candidates=_paths(probes),
382
+ )
383
+ if cli_serial is not None and canon_serial(cli_serial) != canon_serial(hit.serial):
384
+ return _refuse(
385
+ CODE_CONFLICT,
386
+ f"--probe-usb-path {path} is a probe with serial {hit.serial}, but "
387
+ f"--probe-serial {cli_serial} was also given. The two selectors name "
388
+ "different probes; refusing.",
389
+ serial=hit.serial, usb_path=path, source="cli-usb-path", visible=visible,
390
+ candidates=_paths(
391
+ [p for p in probes if canon_serial(p.serial) == canon_serial(hit.serial)]
392
+ ),
393
+ )
394
+ return None
395
+
396
+
397
+ def _by_usb_path(
398
+ probes: list[JLinkProbe] | None,
399
+ path: str,
400
+ cli_serial: str | None,
401
+ manifest_serial: str | None,
402
+ ) -> ProbeSelection:
403
+ refusal = _usb_path_refusal(probes, path, cli_serial)
404
+ if refusal is not None:
405
+ return refusal
406
+ assert probes is not None # `_usb_path_refusal` refused the None case
407
+ hit = next(p for p in probes if p.usb_path == path)
408
+ sharing = [p for p in probes if canon_serial(p.serial) == canon_serial(hit.serial)]
409
+ overrides = (
410
+ manifest_serial
411
+ if manifest_serial is not None
412
+ and canon_serial(manifest_serial) != canon_serial(hit.serial)
413
+ else None
414
+ )
415
+ return ProbeSelection(
416
+ serial=hit.serial,
417
+ usb_path=path,
418
+ source="cli-usb-path",
419
+ visible=len(probes),
420
+ candidates=_paths(sharing) if len(sharing) > 1 else (),
421
+ overrides_manifest_serial=overrides,
422
+ )
423
+
424
+
425
+ def _by_serial(
426
+ probes: list[JLinkProbe] | None,
427
+ serial: str,
428
+ cli_serial: str | None,
429
+ manifest_serial: str | None,
430
+ ) -> ProbeSelection:
431
+ source = "cli-serial" if cli_serial is not None else "flash_args.jlink_serial"
432
+ overrides = (
433
+ manifest_serial
434
+ if cli_serial is not None
435
+ and manifest_serial is not None
436
+ and canon_serial(manifest_serial) != canon_serial(cli_serial)
437
+ else None
438
+ )
439
+ if probes is None:
440
+ # Unverifiable host: the spawn-time ShowEmuList check is the only guard.
441
+ return ProbeSelection(serial, None, source, None, overrides_manifest_serial=overrides)
442
+ matches = [p for p in probes if canon_serial(p.serial) == canon_serial(serial)]
443
+ if not matches:
444
+ return _refuse(
445
+ CODE_NOT_FOUND,
446
+ f"serial {serial} ({source}): no visible J-Link carries that serial. "
447
+ "Visible J-Links: "
448
+ + (", ".join(f"{p.usb_path} ({p.serial})" for p in probes) or "none")
449
+ + ".",
450
+ serial=serial, usb_path=None, source=source, visible=len(probes),
451
+ candidates=_paths(probes),
452
+ )
453
+ if len(matches) > 1:
454
+ return _refuse(
455
+ CODE_AMBIGUOUS,
456
+ f"serial {serial} ({source}) is shared by {len(matches)} visible probes "
457
+ f"({', '.join(_paths(matches))}), so J-Link would open whichever "
458
+ "enumerates first -- possibly the wrong board. " + _HINT,
459
+ serial=serial, usb_path=None, source=source, visible=len(probes),
460
+ candidates=_paths(matches),
461
+ )
462
+ return ProbeSelection(
463
+ matches[0].serial, matches[0].usb_path, source, len(probes),
464
+ overrides_manifest_serial=overrides,
465
+ )
466
+
467
+
468
+ def _unselected(probes: list[JLinkProbe] | None) -> ProbeSelection:
469
+ if probes is None or not probes:
470
+ return ProbeSelection(None, None, "unpinned", None if probes is None else 0)
471
+ if len(probes) > 1:
472
+ return _refuse(
473
+ CODE_AMBIGUOUS,
474
+ f"{len(probes)} J-Links are visible ({', '.join(_paths(probes))}) and nothing "
475
+ "selects one: no --probe-usb-path, no --probe-serial, no "
476
+ "flash_args.jlink_serial. Refusing before any write. " + _HINT,
477
+ serial=None, usb_path=None, source="unpinned", visible=len(probes),
478
+ candidates=_paths(probes),
479
+ )
480
+ only = probes[0]
481
+ return ProbeSelection(only.serial, only.usb_path, "sole-visible", 1)