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,583 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan flash --raw <file>@<addr>` -- a byte-exact MRAM sector write for bench
3
+ backup/restore (tan-cli#1446).
4
+
5
+ A bench backs MRAM up with J-Link `savebin` and must put it back byte-exact:
6
+ `he_slot0` at 0x80010000, the ATOC sector at 0x8057C000. Flow D cannot (it writes a
7
+ built image plus a signed ATOC), so this is the same J-Link part-profile path with
8
+ `loadbin` + `verifybin` and nothing else -- no signing, no SETOOLS, no reset, no `go`.
9
+
10
+ **Guards, in order:** every `--raw` is parsed and its range validated BEFORE anything is
11
+ spawned (`tan.core.raw_write`: explicit hex address, 16 KiB sector alignment, whole
12
+ sectors, inside the SKU's MRAM, no overlap), under `--dry-run` too. A real write then
13
+ needs the CLI `--confirm` and a held bench reservation (see `_reservation_refusal`), and goes through
14
+ the Flow D probe-selection guard and DPIDR preflight. tan derives no address from a name
15
+ or a map, so an ATOC/STOC is only ever written where the user said.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hmac
20
+ import json
21
+ import os
22
+ import re
23
+ import socket
24
+ import stat
25
+ import subprocess
26
+ from typing import Any
27
+
28
+ from tan.commands import flash_cmd as fc
29
+ from tan.core.flash_plan import (
30
+ FLOW_D_METHOD,
31
+ FlashInputs,
32
+ FlashPlan,
33
+ FlashPlanError,
34
+ FlashTarget,
35
+ _DEFAULT_JLINK_SPEED,
36
+ confirm_gate_note,
37
+ dpidr_preflight_unarmed,
38
+ fa_int_checked,
39
+ fa_str_checked,
40
+ validate_identifier,
41
+ )
42
+ from tan.core.flow_d_report import (
43
+ VERIFICATION_READBACK,
44
+ dpidr_in,
45
+ reset_failures,
46
+ sha256_of,
47
+ )
48
+ from tan.core.jlink_binary import resolve_jlink
49
+ from tan.core.subprocess_env import spawn_env
50
+ from tan.core.raw_write import (
51
+ CODE_FAILED,
52
+ CODE_INVALID,
53
+ CODE_RESERVATION,
54
+ RawError,
55
+ RawSpec,
56
+ lease_changed,
57
+ lease_holder,
58
+ lease_swd_path,
59
+ parse_raw,
60
+ planned,
61
+ raw_script,
62
+ validate_ranges,
63
+ )
64
+
65
+ METHOD = "mram_raw"
66
+ #: The bench's reservation marker: the J-Link wrapper on a shared bench only serves a
67
+ #: caller that holds the named labgrid place. tan cannot verify the lease itself, so it
68
+ #: refuses a real write when the marker is absent and reports the place it ran under.
69
+ RESERVATION_ENV = "JLINK_RUN_PLACE"
70
+ #: The reservation-enforcing J-Link wrapper, as an EXPLICIT absolute path. The marker text
71
+ #: inside a program found on PATH or in the cwd is spoofable by anything that can put a file
72
+ #: there, so the wrapper is accepted only by identity with this configured path.
73
+ WRAPPER_ENV = "TAN_JLINK_WRAPPER"
74
+ #: Optional absolute path of `labgrid-client`; otherwise only these fixed directories are
75
+ #: searched -- never `$PATH`, which a shadowing binary could front-run.
76
+ LABGRID_ENV = "TAN_LABGRID_CLIENT"
77
+ #: Per-SESSION lease (tan-cli#1457). labgrid's holder name is `<host>/<user>`, identical for
78
+ #: every session of one user, so "the place is acquired by me" cannot tell this session from
79
+ #: another. `scripts/bench/tan-lease.sh acquire <place>` records a random nonce in
80
+ #: `LEASE_DIR/<place>.lease` (0600) and prints `export TAN_LEASE_NONCE=<nonce>` for ONLY the
81
+ #: acquiring shell; the gate needs the two to match. (labgrid reservation tokens would also
82
+ #: separate sessions but only exist for `reserve`-allocated places, not for `acquire`.)
83
+ NONCE_ENV = "TAN_LEASE_NONCE"
84
+ LEASE_DIR = "~/.cache/alplab-leases"
85
+ _PLACE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
86
+ _NONCE_RE = re.compile(r"[0-9a-f]{32,128}")
87
+ LABGRID_DIRS = ("~/.local/bin", "/usr/local/bin", "/usr/bin", "/bin")
88
+
89
+
90
+ def _read_blobs(specs: list[RawSpec]) -> tuple[list[RawSpec], list[str]]:
91
+ """Each spec with its size filled in, plus the sha256s. J-Link runs with cwd=<west topdir>,
92
+ not tan's cwd, so the path is made absolute once here: the file hashed is then the file
93
+ loadbin/verifybin open (tan-cli#1513)."""
94
+ sized: list[RawSpec] = []
95
+ digests: list[str] = []
96
+ for spec in specs:
97
+ path = os.path.abspath(spec.path)
98
+ try:
99
+ size, digest = os.path.getsize(path), sha256_of(path)
100
+ except OSError as err:
101
+ raise RawError(f"cannot read {path} ({err})") from err
102
+ sized.append(RawSpec(path, spec.address, size))
103
+ digests.append(digest)
104
+ return sized, digests
105
+
106
+
107
+ def run_raw_entry(target: FlashTarget, ctx: Any) -> tuple[int, Any, list[str]]:
108
+ """Raw-write one slice's MRAM. Returns `(rc, entry, text-lines)` like `_flash_entry`."""
109
+ kind, entry_id = target.kind, target.id
110
+ lines: list[str] = [f"flash: {kind} '{entry_id}' -> {METHOD}"]
111
+ report: dict[str, Any] = {}
112
+ probe_echo: dict[str, Any] | None = None
113
+
114
+ def entry(status: str, rc: int, message: str, **kw: Any) -> Any:
115
+ return fc._Entry(
116
+ kind=kind, id=entry_id, method=METHOD, status=status, rc=rc, message=message,
117
+ probe=probe_echo, extra=report, **kw,
118
+ )
119
+
120
+ def fail(message: str, code: str | None = CODE_FAILED, **kw: Any) -> tuple[int, Any, list[str]]:
121
+ text = f"{METHOD}[{entry_id}]: {message}"
122
+ lines.append(f" FAIL: {text}")
123
+ return 1, entry("failed", 1, text, issue_code=code, **kw), lines
124
+
125
+ if kind != "slice":
126
+ return fail("only a slice's MRAM can be raw-written, not a helper MCU", CODE_INVALID)
127
+ if not isinstance(target.flash_args, dict) and target.flash_args is not None:
128
+ return fail("flash_args is not a mapping", CODE_INVALID)
129
+ flash_args: Any = dict(target.flash_args or {})
130
+
131
+ # ── the blobs: parsed, read, hashed and range-checked before anything is spawned ──
132
+ try:
133
+ specs = [parse_raw(raw) for raw in ctx.raw]
134
+ sized, digests = _read_blobs(specs)
135
+ validate_ranges(sized, *reversed(_mram_window(ctx)))
136
+ device = fa_str_checked(flash_args, "jlink_flash_device", False)
137
+ if device is None:
138
+ raise RawError(
139
+ "flash_args.jlink_flash_device is required -- only the part-number J-Link device "
140
+ "profile unlocks the MRAM loader; tan does not guess one"
141
+ )
142
+ validate_identifier(device, "jlink_flash_device")
143
+ except (RawError, FlashPlanError) as err:
144
+ return fail(str(err), CODE_INVALID)
145
+ report["raw"] = {"writes": planned(sized, digests), "resetCommands": False, "signs": False}
146
+
147
+ # ── J-Link: trusted binary, probe selection, parameters ──
148
+ found = resolve_jlink(ctx.jlink_path, project_dir=ctx.project_dir)
149
+ exe = found.path if found is not None else None
150
+ report["jlink"] = {"binary": exe, "binarySource": found.source if found else None}
151
+ if exe is None and not ctx.dry_run:
152
+ return fail(fc._NO_TRUSTED_JLINK)
153
+ try:
154
+ flash_args, selection, snapshot = fc._flow_d_probe_selection(flash_args, ctx)
155
+ probe_echo = fc._probe_echo(selection, ctx)
156
+ if selection is not None and selection.refusal_code is not None:
157
+ lines.append(f" FAIL: {selection.refusal}")
158
+ return (
159
+ 1,
160
+ entry("failed", 1, selection.refusal or "", probe_refusal=selection.refusal_code),
161
+ lines,
162
+ )
163
+ guard = None
164
+ if selection is not None and (selection.serial is not None or selection.visible is None):
165
+ guard = fc._ProbeGuard(selection, snapshot, ctx.enumerate_probes, probe_echo)
166
+ serial = fa_str_checked(flash_args, "jlink_serial", False)
167
+ speed = fa_int_checked(flash_args, "jlink_speed") or _DEFAULT_JLINK_SPEED
168
+ if serial is not None:
169
+ validate_identifier(serial, "jlink_serial", destination=fc._JLINK_SERIAL_DESTINATION)
170
+ fc.validate_flow_d_preflight_args(flash_args)
171
+ except FlashPlanError as err:
172
+ return fail(str(err), CODE_INVALID)
173
+
174
+ preamble = "".join(
175
+ [f"SelectEmuBySN {serial}\n" if serial is not None else "",
176
+ f"si SWD\nspeed {speed}\ndevice {device}\nconnect\n"]
177
+ )
178
+ script = raw_script(preamble, sized)
179
+ argv = (
180
+ "JLinkExe", "-device", device, "-if", "SWD", "-speed", str(speed),
181
+ "-ExitOnError", "1", "-NoGui", "1", "-CommanderScript",
182
+ )
183
+ report["plan"] = {"argv": list(argv), "jlinkScript": (fc._DISABLE_FW_UPDATE + script).splitlines()}
184
+ summary = ", ".join(
185
+ f"{w['size']} B at {w['address']} ({w['sectorSpan']['count']} sector(s))" for w in report["raw"]["writes"]
186
+ )
187
+
188
+ if ctx.dry_run:
189
+ msg = f"{METHOD}[{entry_id}]: would write {summary}; no reset; nothing spawned"
190
+ lines.append(f" {msg}")
191
+ return 0, entry("ok", 0, msg), lines
192
+
193
+ # The CLI `--confirm` (or ALP_FLASH_FORCE=1) ONLY: a manifest's `flash_args.confirm`
194
+ # is project-controlled and must never arm a raw MRAM overwrite.
195
+ confirmed = bool(ctx.force_confirm)
196
+ if not confirmed:
197
+ msg = (
198
+ f"{METHOD}[{entry_id}]: would write {summary} -- NOT written: "
199
+ f"{confirm_gate_note('--confirm was not given (flash_args.confirm does not arm --raw)')}"
200
+ )
201
+ lines.append(f" {msg}")
202
+ return 0, entry("planned", 0, msg), lines
203
+
204
+ place = os.environ.get(RESERVATION_ENV, "").strip()
205
+ refusal, verified = _reservation_refusal(
206
+ place, exe, selection.usb_path if selection is not None else None
207
+ )
208
+ if refusal is not None or verified is None:
209
+ return fail(refusal or "reservation could not be verified", CODE_RESERVATION)
210
+ # Every later spawn (guard, DPIDR preflight, write, read-back) uses exactly the path that
211
+ # was verified here -- the env var is not read again.
212
+ exe = verified
213
+ report["jlink"]["binary"] = exe
214
+ report["jlink"]["reservation"] = {
215
+ "env": RESERVATION_ENV, "place": place, "verified": "labgrid+session-lease", "usbPath": selection.usb_path,
216
+ }
217
+
218
+ # ── the wrong-board guard, exactly as for a Flow D write ──
219
+ armed = not dpidr_preflight_unarmed(FLOW_D_METHOD, flash_args, None)
220
+ if ctx.require_dpidr and not armed:
221
+ return fail(
222
+ "ALP_FLASH_REQUIRE_DPIDR=1 is set and flash_args.expect_dpidr / jlink_device are "
223
+ "not both set -- refusing to write with no wrong-board guard", CODE_INVALID,
224
+ )
225
+ if guard is not None:
226
+ refusal = fc._probe_guard_refusal(
227
+ guard, fc._jlink_program(ctx.venv_bin, exe), None, ctx.workspace
228
+ )
229
+ if refusal is not None:
230
+ return fail(refusal, None, probe_refusal=guard.tripped_code)
231
+ guard.fresh = True
232
+ facts: dict[str, Any] = {}
233
+ refusal = fc._flow_d_preflight(
234
+ FlashInputs(
235
+ artefact="", flash_args=flash_args, core_id=entry_id, sku=ctx.sku,
236
+ dry_run=False, force_confirm=ctx.force_confirm,
237
+ ),
238
+ ctx.venv_bin, ctx.workspace, probe_guard=guard, facts=facts, jlink_exe=exe,
239
+ )
240
+ if refusal is not None:
241
+ tripped = guard is not None and guard.tripped == refusal
242
+ return fail(refusal, None, probe_refusal=guard.tripped_code if tripped else None)
243
+
244
+ # ── write + verify (J-Link's flash cache), no reset ──
245
+ plan = FlashPlan(argv=argv, ok_message="", jlink_script=script)
246
+ outcome = fc._execute(plan, ctx.capture, ctx.venv_bin, ctx.workspace, guard, jlink_exe=exe)
247
+ transcript = f"{outcome.stdout}\n{outcome.stderr}"
248
+ report["jlink"]["dpidr"] = facts.get("dpidr") or dpidr_in(transcript)
249
+ report["jlink"]["verification"] = fc.VERIFICATION_CACHE
250
+ report["jlink"]["verificationNote"] = fc.VERIFICATION_NOTE
251
+ report["jlink"]["transcriptTail"] = fc.transcript_tail(transcript)
252
+ if guard is not None and guard.tripped:
253
+ return fail(guard.tripped, None, probe_refusal=guard.tripped_code)
254
+ if not outcome.success:
255
+ return fail(f"the write session failed: {fc._capture_tail(outcome) or 'J-Link failed'}")
256
+ if reset_failures(transcript):
257
+ return fail(
258
+ "J-Link reported a halt/reset failure during a write that must not reset: "
259
+ + ", ".join(reset_failures(transcript))
260
+ )
261
+
262
+ message = f"{METHOD}[{entry_id}]: wrote {summary}; cache-verified; no reset command was sent"
263
+ if ctx.readback:
264
+ writes = [
265
+ {"address": w["address"], "size": w["size"], "path": w["path"], "sha256": w["sha256"]}
266
+ for w in report["raw"]["writes"]
267
+ ]
268
+ failure = fc._flow_d_readback(
269
+ plan, outcome, ctx, writes, report, guard, exe, entry_id=entry_id
270
+ )
271
+ if failure is not None:
272
+ code, text = failure
273
+ return fail(text, code if code.startswith("flash.readback") else None,
274
+ probe_refusal=code[len("flash.probe-"):] if code.startswith("flash.probe-") else None)
275
+ if report["jlink"].get("verification") == VERIFICATION_READBACK:
276
+ message += "; read back in a fresh J-Link session (sha256 match)"
277
+ message += (
278
+ " -- loadbin may have halted the core; power-cycle the board so the Secure Enclave boots "
279
+ "the restored contents"
280
+ )
281
+ lines.append(f" ok: {message}")
282
+ return 0, entry("ok", 0, message, preflight_unarmed=not armed), lines
283
+
284
+
285
+ def _platform_refusal() -> str | None:
286
+ """`pwd`/`grp`/`getuid` and `O_NOFOLLOW` are POSIX-only, imported lazily so that
287
+ `import tan.cli` works everywhere; the raw-write interlock needs them, so it refuses
288
+ where they are missing rather than guessing."""
289
+ try:
290
+ import grp # noqa: F401
291
+ import pwd # noqa: F401
292
+ except ImportError:
293
+ return "unsupported on this platform: the --raw reservation interlock needs POSIX pwd/grp"
294
+ if os.name == "nt" or not hasattr(os, "getuid") or not hasattr(os, "O_NOFOLLOW"):
295
+ return "unsupported on this platform: the --raw reservation interlock needs POSIX uid/O_NOFOLLOW"
296
+ return None
297
+
298
+
299
+ def _current_user() -> str:
300
+ """The account name of the real uid (never `USER`/`LOGNAME`, which the caller controls)."""
301
+ import pwd
302
+
303
+ return pwd.getpwuid(os.getuid()).pw_name
304
+
305
+
306
+ def _private_group(gid: int) -> bool:
307
+ """Whether `gid` is a group only the current user belongs to (a user-private group)."""
308
+ import grp
309
+ import pwd
310
+
311
+ me = _current_user()
312
+ try:
313
+ members = set(grp.getgrgid(gid).gr_mem)
314
+ except KeyError:
315
+ return False
316
+ return members <= {me} and all(u.pw_name == me for u in pwd.getpwall() if u.pw_gid == gid)
317
+
318
+
319
+ def _node_problem(node: str, st: os.stat_result) -> str | None:
320
+ mode = st.st_mode
321
+ sticky_dir = stat.S_ISDIR(mode) and bool(mode & stat.S_ISVTX)
322
+ if st.st_uid not in (0, os.getuid()):
323
+ return f"{node} is owned by uid {st.st_uid}, not root or you"
324
+ if stat.S_ISLNK(mode):
325
+ return None # a symlink's own mode bits are always 0777 and mean nothing
326
+ if mode & stat.S_IWOTH and not sticky_dir:
327
+ return f"{node} is world-writable"
328
+ if mode & stat.S_IWGRP and not sticky_dir and not (
329
+ st.st_gid == os.getgid() and _private_group(st.st_gid)
330
+ ):
331
+ return f"{node} is group-writable by a group others belong to"
332
+ return None
333
+
334
+
335
+ def _chain_problem(start: str) -> str | None:
336
+ node = start
337
+ while True:
338
+ try:
339
+ st = os.lstat(node)
340
+ except OSError:
341
+ return f"{node} cannot be inspected"
342
+ problem = _node_problem(node, st)
343
+ if problem:
344
+ return problem
345
+ parent = os.path.dirname(node)
346
+ if parent == node:
347
+ return None
348
+ node = parent
349
+
350
+
351
+ def _unsafe(path: str) -> str | None:
352
+ """Why `path` cannot be trusted as an interlock binary, or `None`. It must be absolute,
353
+ resolve (symlinks followed) to an executable regular file outside the cwd, and BOTH chains
354
+ -- the given path itself and its directories as written (where a symlink lives), and the
355
+ resolved target with its parents -- must be owned by root or the current user and not
356
+ writable by others or by a group anyone else belongs to (a sticky directory is allowed)."""
357
+ if not os.path.isabs(path):
358
+ return "is not an absolute path"
359
+ real = os.path.realpath(path)
360
+ cwd = os.path.realpath(os.getcwd())
361
+ if real == cwd or real.startswith(cwd + os.sep):
362
+ return "lives under the current directory"
363
+ if not (os.path.isfile(real) and os.access(real, os.X_OK)):
364
+ return "is not an executable file"
365
+ return _chain_problem(os.path.abspath(path)) or _chain_problem(real)
366
+
367
+
368
+ # What the shim defaults to so `labgrid-client` finds the coordinator (bin/JLinkExe, jlink-run.sh).
369
+ _DEFAULT_COORDINATOR = "100.64.0.1:20408"
370
+ #: Interpreter-steering variables a hostile environment could use to run its own code inside
371
+ #: a Python `labgrid-client`.
372
+ _PYTHON_ENV = ("PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP", "PYTHONINSPECT", "PYTHONEXECUTABLE")
373
+
374
+
375
+ def _labgrid_client() -> str | None:
376
+ """The absolute `labgrid-client` to run: `TAN_LABGRID_CLIENT`, else the first of the fixed
377
+ directories holding a trustworthy one. Never a `$PATH` search."""
378
+ configured = os.environ.get(LABGRID_ENV, "").strip()
379
+ candidates = (
380
+ [configured] if configured
381
+ else [os.path.join(os.path.expanduser(d), "labgrid-client") for d in LABGRID_DIRS]
382
+ )
383
+ for candidate in candidates:
384
+ if os.path.exists(candidate) and _unsafe(candidate) is None:
385
+ return os.path.realpath(candidate)
386
+ return None
387
+
388
+
389
+ def _lease_refusal(place: str) -> tuple[str | None, str | None]:
390
+ """Refusal text unless THIS session proves it acquired `place`: `TAN_LEASE_NONCE` is set,
391
+ well-formed, and equals the nonce in this user's own 0600 lease file for exactly that
392
+ place. Any missing, unreadable, mis-owned, mis-moded or mismatching piece refuses. Returns
393
+ `(refusal, changed)`: the `changed=` recorded at acquire time, to be compared with the
394
+ place's CURRENT labgrid `changed:` so a stale lease from an earlier acquisition is refused."""
395
+ how = (
396
+ f"Acquire the place from this shell with `eval \"$(scripts/bench/tan-lease.sh acquire "
397
+ f"{place})\"` so {NONCE_ENV} and {LEASE_DIR}/{place}.lease agree; a place held by another "
398
+ "session of the same labgrid user is not yours to write."
399
+ )
400
+ if not _PLACE_RE.fullmatch(place):
401
+ return f"the place name {place!r} is not a plain labgrid place name. {how}", None
402
+ nonce = os.environ.get(NONCE_ENV, "").strip()
403
+ if not _NONCE_RE.fullmatch(nonce):
404
+ return f"{NONCE_ENV} is not set to a lease nonce, so this session has not acquired {place}. {how}", None
405
+ directory = os.path.expanduser(LEASE_DIR)
406
+ path = os.path.join(directory, f"{place}.lease")
407
+ # Opened with O_NOFOLLOW and judged by fstat of the OPEN descriptors: no lstat-then-open race.
408
+ try:
409
+ dfd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
410
+ except OSError:
411
+ return f"there is no lease directory {directory}: this session never acquired {place} with the lease helper. {how}", None
412
+ try:
413
+ dst = os.fstat(dfd)
414
+ try:
415
+ ffd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
416
+ except OSError:
417
+ return (
418
+ f"there is no readable regular lease file {path} (symlinks are refused): this "
419
+ f"session never acquired {place} with the lease helper. {how}"
420
+ ), None
421
+ try:
422
+ fst = os.fstat(ffd)
423
+ if not stat.S_ISDIR(dst.st_mode) or dst.st_uid != os.getuid() or dst.st_mode & 0o077:
424
+ return f"the lease directory {directory} must be a directory owned by you with mode 0700. {how}", None
425
+ if not stat.S_ISREG(fst.st_mode) or fst.st_uid != os.getuid() or fst.st_mode & 0o077:
426
+ return f"the lease file {path} must be a regular file owned by you with mode 0600. {how}", None
427
+ with os.fdopen(ffd, encoding="utf-8", closefd=False) as fh:
428
+ text = fh.read(4096)
429
+ except (OSError, UnicodeDecodeError):
430
+ return f"the lease file {path} cannot be read. {how}", None
431
+ finally:
432
+ os.close(ffd)
433
+ finally:
434
+ os.close(dfd)
435
+ fields = dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
436
+ recorded = fields.get("nonce", "")
437
+ # Validate the file's nonce's shape too, then compare bytes in constant time.
438
+ if (
439
+ fields.get("place") != place
440
+ or not _NONCE_RE.fullmatch(recorded)
441
+ or not hmac.compare_digest(recorded.encode("ascii"), nonce.encode("ascii"))
442
+ ):
443
+ return (
444
+ f"{NONCE_ENV} does not match the lease on {place} -- another session holds that lease, "
445
+ f"not this one. {how}"
446
+ ), None
447
+ changed = fields.get("changed", "").strip()
448
+ if not changed:
449
+ return (
450
+ f"the lease on {place} records no labgrid `changed` value, so it cannot be bound to "
451
+ f"one acquisition. {how}"
452
+ ), None
453
+ return None, changed
454
+
455
+
456
+ def _reservation_refusal(
457
+ place: str, exe: str | None, usb_path: str | None
458
+ ) -> tuple[str | None, str | None]:
459
+ """`(refusal, verified_path)`: a refusal text, or `None` plus the REAL path of the wrapper
460
+ to spawn from here on. Fails closed on any ambiguity. Needs: the place named
461
+ (`JLINK_RUN_PLACE`); the J-Link program tan will run IS the wrapper configured in
462
+ `TAN_JLINK_WRAPPER` (absolute, symlink-resolved, outside the cwd, trustworthy ownership and
463
+ modes on both chains -- a marker string in a binary found on PATH proves nothing);
464
+ `labgrid-client` (absolute, fixed directories) reports THIS uid's host/user as the single
465
+ holder of the place; and the probe tan selected IS the leased place's `swd` USB path."""
466
+ need = (
467
+ f"Needs: {RESERVATION_ENV}=<labgrid place you hold>, {WRAPPER_ENV}=<absolute path of the "
468
+ "reservation-enforcing JLinkExe wrapper> and tan running THAT program (--jlink "
469
+ "<that path>, or it first on PATH), --probe-usb-path <the place's swd port>, and "
470
+ "`labgrid-client` able to reach the coordinator."
471
+ )
472
+ if not place:
473
+ return f"refusing to overwrite MRAM without a held bench reservation: {RESERVATION_ENV} is not set. {need}", None
474
+ unsupported = _platform_refusal()
475
+ if unsupported is not None:
476
+ return f"{unsupported}. {need}", None
477
+ lease, leased_changed = _lease_refusal(place)
478
+ if lease is not None:
479
+ return lease, None
480
+ configured = os.environ.get(WRAPPER_ENV, "").strip()
481
+ if not configured:
482
+ return f"{WRAPPER_ENV} is not set, so no J-Link program can be recognised as the reservation wrapper. {need}", None
483
+ why = _unsafe(configured)
484
+ if why is not None:
485
+ return f"{WRAPPER_ENV}={configured}: {why}. {need}", None
486
+ real = os.path.realpath(configured)
487
+ if not exe or not os.path.isabs(exe) or os.path.realpath(exe) != real:
488
+ return (
489
+ f"the J-Link program tan would run ({exe}) is not the configured wrapper "
490
+ f"({configured}); a raw or look-alike JLinkExe would not enforce the lease. {need}"
491
+ ), None
492
+ text, why_show = _labgrid_show(place)
493
+ me = f"{socket.gethostname()}/{_current_user()}"
494
+ holder = lease_holder(text or "")
495
+ if holder != me:
496
+ return (
497
+ f"you do not hold the labgrid place {place} (acquired by: "
498
+ f"{holder or 'nobody / not readable / ambiguous'}; you are {me}"
499
+ + (f"; labgrid-client: {why_show}" if why_show else "")
500
+ + f"). {need}"
501
+ ), None
502
+ current_changed = lease_changed(text or "")
503
+ if current_changed is None or current_changed != leased_changed:
504
+ return (
505
+ f"the lease on {place} was written for an earlier acquisition (labgrid `changed:` was "
506
+ f"{leased_changed!r}, is {current_changed or 'not reported'!r}): the place has been "
507
+ "released/re-acquired since, so this lease is stale. Release and acquire it again "
508
+ "with the lease helper. " + need
509
+ ), None
510
+ leased = lease_swd_path(text or "")
511
+ if leased is None or usb_path is None or leased != usb_path:
512
+ return (
513
+ f"the selected probe (--probe-usb-path {usb_path or 'not given'}) is not the leased "
514
+ f"place's swd port ({leased or 'not reported by labgrid'}) -- refusing to write to a "
515
+ f"probe the reservation does not cover. {need}"
516
+ ), None
517
+ return None, real
518
+
519
+
520
+ def _labgrid_show(place: str) -> tuple[str | None, str]:
521
+ """`(output, why)` of `labgrid-client -p <place> show`: the output, or `None` with a reason
522
+ it could not be had."""
523
+ client = _labgrid_client()
524
+ if client is None:
525
+ return None, (
526
+ f"no trustworthy labgrid-client found (set {LABGRID_ENV}=<absolute path>, or install "
527
+ f"it in {', '.join(LABGRID_DIRS)})"
528
+ )
529
+ env = spawn_env({"LG_COORDINATOR": os.environ.get("LG_COORDINATOR") or _DEFAULT_COORDINATOR})
530
+ for name in _PYTHON_ENV:
531
+ env.pop(name, None)
532
+ try:
533
+ done = subprocess.run(
534
+ [client, "-p", place, "show"], capture_output=True, text=True, timeout=30,
535
+ check=False, env=env,
536
+ )
537
+ except (OSError, subprocess.SubprocessError) as err:
538
+ return None, f"could not run {client}: {err}"
539
+ if done.returncode != 0:
540
+ tail = (done.stderr or done.stdout).strip().splitlines()[-1:] or ["no output"]
541
+ return None, f"{client} exited {done.returncode}: {tail[0]}"
542
+ return done.stdout, ""
543
+
544
+
545
+ def _mram_window(ctx: Any) -> tuple[int, int]:
546
+ """`(base, length)` of the SKU's MRAM from the SoC metadata of the SDK in use: the SoC
547
+ document's `soc_flash_base` and the SKU's OWN variant `mram_mb` (E3 ships 1.5 MB and
548
+ 5.5 MB parts, so there is no family fallback). Refuses -- never guesses -- when the SDK
549
+ metadata, the variant or either number cannot be read."""
550
+ from tan.commands.build_output import read_sdk_som_and_soc
551
+ from tan.core.size import resolve_variant
552
+
553
+ metadata_root = os.path.join(ctx.sdk_root, "metadata") if ctx.sdk_root else None
554
+ walked = (
555
+ read_sdk_som_and_soc(metadata_root, ctx.sku)
556
+ if metadata_root and ctx.sku and os.path.isdir(metadata_root)
557
+ else None
558
+ )
559
+ if walked is None:
560
+ raise RawError(
561
+ f"cannot bound the write: no readable SoM preset / SoC metadata for '{ctx.sku}' "
562
+ f"under {metadata_root} -- refusing to guess the MRAM window"
563
+ )
564
+ silicon, silicon_variant, variants, _soc_flash_mb, _cores = walked
565
+ variant = resolve_variant(silicon_variant, ctx.sku, variants)
566
+ mb = variant.get("mram_mb") if variant else None
567
+ if not isinstance(mb, (int, float)) or isinstance(mb, bool) or mb <= 0:
568
+ raise RawError(
569
+ f"cannot bound the write: '{ctx.sku}' does not resolve to a SoC variant that states "
570
+ "mram_mb (a family default would be wrong, e.g. E3 has 1.5 MB parts) -- refusing"
571
+ )
572
+ parts = silicon.split(":")
573
+ try:
574
+ with open(os.path.join(metadata_root, "socs", *parts[:2], f"{parts[2]}.json"), encoding="utf-8") as fh:
575
+ base = json.load(fh).get("soc_flash_base")
576
+ except (OSError, ValueError, IndexError):
577
+ base = None
578
+ if not isinstance(base, int) or isinstance(base, bool) or base <= 0:
579
+ raise RawError(
580
+ f"cannot bound the write: the SoC document for '{ctx.sku}' states no soc_flash_base "
581
+ "-- refusing to assume the MRAM base"
582
+ )
583
+ return base, int(mb * 1024 * 1024)