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/ram_watch.py ADDED
@@ -0,0 +1,231 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan flash --ram --watch` -- read-only memory sampling while a RAM-run image runs
3
+ (tan-cli#1436). Pure logic, no IO:
4
+
5
+ * [`parse_watch`]: one `--watch <addr>[:<words>][@<period-ms>]` spec -> [`WatchSpec`],
6
+ refusing an unaligned address, a zero or excessive word count, a period outside the
7
+ supported range, and any address the probe cannot safely read.
8
+ * [`schedule`]: the specs + the `--wait` window -> the sample times, merged in order.
9
+ * [`watch_lines`]: the J-Link Commander lines appended after `go` -- `mem32` reads
10
+ separated by `Sleep <ms>`. `mem32` only: no write command is ever produced.
11
+ * [`parse_samples`]: the load transcript -> the envelope's `data.watch[]`.
12
+
13
+ **Time basis.** J-Link Commander prints no timestamps, so a sample's `elapsedMs` is its
14
+ SCHEDULED offset after `go` (the `Sleep` sum), not a measurement; every `mem32` adds SWD
15
+ latency on top, so the real offset is later and drifts upward over a long window.
16
+
17
+ **Unsafe addresses.** The denylist is keyed by the attached core. For the M55 HE (the
18
+ only core a RAM-run attaches to) the HP TCM windows `[0x50000000, 0x58000000)` (ITCM and
19
+ DTCM globals) are refused: reading them from an HE attach is the bench's known-bad read,
20
+ it leaves the core unhaltable until a PIN reset (bench round 8, see `tan.core.ram_run`).
21
+
22
+ **Side effects.** `mem32` is read-only on memory, but a register read (a FIFO, a
23
+ clear-on-read status, a clock-gated block) may have side effects; tan cannot tell them
24
+ apart, so watch RAM or plain data registers.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from collections.abc import Sequence
30
+ from dataclasses import dataclass
31
+
32
+ from tan.core.ram_run import HE_ALIAS, _ITCM_GLOBAL
33
+
34
+ CODE_INVALID = "flash.ram-watch-invalid"
35
+ CODE_UNSAFE = "flash.ram-watch-unsafe-address"
36
+ CODE_INCOMPLETE = "flash.ram-watch-incomplete"
37
+
38
+ DEFAULT_WORDS = 1
39
+ MAX_WORDS = 64
40
+ DEFAULT_PERIOD_MS = 100
41
+ MIN_PERIOD_MS = 10
42
+ MAX_PERIOD_MS = 60_000
43
+ MAX_WATCHES = 8
44
+ #: Upper bound on the `mem32` reads in one session, so a long `--wait` with a short
45
+ #: period cannot build a script of unbounded size.
46
+ MAX_SAMPLES = 2000
47
+ #: Per attached core, the spans its attach must never read. HE: the HP TCM windows
48
+ #: [HP ITCM global, HE ITCM global) -- ITCM and DTCM.
49
+ DENIED_SPANS = {"M55_HE": ((_ITCM_GLOBAL["M55_HP"], HE_ALIAS),)}
50
+ #: Wall-clock budget per `mem32` (SWD read + J-Link command overhead), generous.
51
+ PER_READ_BUDGET_S = 2.0
52
+ TIMEOUT_MARGIN_S = 60.0
53
+
54
+
55
+ def session_timeout_s(specs: "Sequence[WatchSpec]", duration_ms: int) -> float:
56
+ """The load session's spawn timeout: the window, plus a budget per scheduled read,
57
+ plus a margin."""
58
+ return duration_ms / 1000 + len(schedule(specs, duration_ms)) * PER_READ_BUDGET_S + TIMEOUT_MARGIN_S
59
+
60
+ _SPEC = re.compile(
61
+ r"^(?P<addr>0[xX][0-9A-Fa-f]{1,8}|[0-9]{1,10})(?::(?P<words>[0-9]+))?(?:@(?P<period>[0-9]+))?$"
62
+ )
63
+
64
+
65
+ class WatchError(Exception):
66
+ """A `--watch` refusal. `code` is the registered issue code."""
67
+
68
+ def __init__(self, message: str, code: str = CODE_INVALID):
69
+ super().__init__(message)
70
+ self.code = code
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class WatchSpec:
75
+ address: int
76
+ words: int
77
+ period_ms: int
78
+ text: str
79
+
80
+
81
+ def parse_watch(text: str, core: str = "M55_HE") -> WatchSpec:
82
+ match = _SPEC.match(text.strip()) if isinstance(text, str) else None
83
+ if match is None:
84
+ raise WatchError(
85
+ f"--watch {text!r} is not <addr>[:<words>][@<period-ms>] (addr is 0x-hex or decimal)"
86
+ )
87
+ raw = match["addr"]
88
+ address = int(raw, 16) if raw[:2].lower() == "0x" else int(raw)
89
+ words = int(match["words"]) if match["words"] is not None else DEFAULT_WORDS
90
+ period = int(match["period"]) if match["period"] is not None else DEFAULT_PERIOD_MS
91
+ if address > 0xFFFFFFFF:
92
+ raise WatchError(f"--watch {text!r}: address 0x{address:X} is beyond 32 bits")
93
+ if address % 4:
94
+ raise WatchError(f"--watch {text!r}: address 0x{address:08X} is not 4-byte aligned (mem32)")
95
+ if not 1 <= words <= MAX_WORDS:
96
+ raise WatchError(f"--watch {text!r}: {words} words is outside 1..{MAX_WORDS}")
97
+ if not MIN_PERIOD_MS <= period <= MAX_PERIOD_MS:
98
+ raise WatchError(
99
+ f"--watch {text!r}: period {period} ms is outside {MIN_PERIOD_MS}..{MAX_PERIOD_MS}"
100
+ )
101
+ last = address + 4 * words - 1
102
+ if last > 0xFFFFFFFF:
103
+ raise WatchError(f"--watch {text!r}: the read runs past the end of the address space")
104
+ for lo, hi in DENIED_SPANS.get(core, ()):
105
+ if address < hi and last >= lo:
106
+ raise WatchError(
107
+ f"--watch {text!r}: 0x{address:08X}..0x{last:08X} overlaps the HP TCM "
108
+ f"windows 0x{lo:08X}..0x{hi - 1:08X}; reading it from the HE attach "
109
+ "leaves the core unhaltable until a PIN reset",
110
+ CODE_UNSAFE,
111
+ )
112
+ return WatchSpec(address, words, period, text.strip())
113
+
114
+
115
+ def parse_watches(texts: Sequence[str], core: str = "M55_HE") -> list[WatchSpec]:
116
+ if len(texts) > MAX_WATCHES:
117
+ raise WatchError(f"{len(texts)} --watch options; at most {MAX_WATCHES}")
118
+ return [parse_watch(t, core) for t in texts]
119
+
120
+
121
+ def schedule(specs: Sequence[WatchSpec], duration_ms: int) -> list[tuple[int, int]]:
122
+ """`(offset_ms, spec_index)` for every sample in `[0, duration_ms]`, time-ordered
123
+ (ties in spec order). Refuses more than [`MAX_SAMPLES`]."""
124
+ out = [
125
+ (t, i)
126
+ for i, s in enumerate(specs)
127
+ for t in range(0, max(duration_ms, 0) + 1, s.period_ms)
128
+ ]
129
+ if len(out) > MAX_SAMPLES:
130
+ raise WatchError(
131
+ f"{len(out)} samples over {duration_ms} ms exceeds {MAX_SAMPLES}; "
132
+ "raise the period or shorten --wait"
133
+ )
134
+ return sorted(out)
135
+
136
+
137
+ def watch_lines(specs: Sequence[WatchSpec], duration_ms: int) -> list[str]:
138
+ """Commander lines for after `go`: `mem32` reads with `Sleep` between, padded so the
139
+ session lasts the whole `--wait` window. Addresses and counts are ints rendered `0x%X`."""
140
+ lines: list[str] = []
141
+ now = 0
142
+ for offset, i in schedule(specs, duration_ms):
143
+ if offset > now:
144
+ lines.append(f"Sleep {offset - now}")
145
+ now = offset
146
+ lines.append(f"mem32 0x{specs[i].address:X}, 0x{specs[i].words:X}")
147
+ if duration_ms > now:
148
+ lines.append(f"Sleep {duration_ms - now}")
149
+ return lines
150
+
151
+
152
+ _MEM32_LINE = re.compile(r"^([0-9A-Fa-f]{8}) = ((?:[0-9A-Fa-f]{8}(?:\s+|$))+)$")
153
+
154
+
155
+ _ECHO = re.compile(r"^J-Link>\s*mem32\s+0[xX]([0-9A-Fa-f]+)\s*,\s*0[xX]([0-9A-Fa-f]+)\s*$")
156
+
157
+
158
+ def _blocks(transcript: str) -> list[tuple[int, list[tuple[int, list[int]]]]] | None:
159
+ """Each echoed `J-Link>mem32 0xADDR, 0xN` with the dump lines that follow it (up to the
160
+ next `J-Link>` echo). `None` when the transcript echoes no `mem32` at all."""
161
+ blocks: list[tuple[int, list[tuple[int, list[int]]]]] = []
162
+ for raw in transcript.splitlines():
163
+ line = raw.strip()
164
+ echo = _ECHO.match(line)
165
+ if echo:
166
+ blocks.append((int(echo[1], 16), []))
167
+ elif line.startswith("J-Link>"):
168
+ blocks.append((-1, [])) # another command: closes the open block
169
+ else:
170
+ m = _MEM32_LINE.match(line)
171
+ if m and blocks:
172
+ blocks[-1][1].append((int(m[1], 16), [int(w, 16) for w in m[2].split()]))
173
+ reads = [b for b in blocks if b[0] >= 0]
174
+ return reads if reads else None
175
+
176
+
177
+ def _collect(address: int, words: int, dumps: list[tuple[int, list[int]]]) -> list[int] | None:
178
+ values: list[int] = []
179
+ for base, got in dumps:
180
+ if base != address + 4 * len(values):
181
+ break
182
+ values.extend(got)
183
+ return values if len(values) == words else None
184
+
185
+
186
+ def parse_samples(transcript: str, specs: Sequence[WatchSpec], duration_ms: int) -> list[dict]:
187
+ """The envelope's `data.watch[]`: one entry per scheduled sample, in order, each
188
+ `{address, words, index, elapsedMs, values}`; `values` is `None` when its dump is
189
+ missing. Strict: only `ADDR = WWWWWWWW ...` lines count. When J-Link echoes the
190
+ commands, each echoed `mem32` is paired with the dumps that follow IT, so a missing
191
+ dump is `None` at its own index and shifts nothing; a transcript with no echo falls
192
+ back to matching dump lines in order against each sample's expected address."""
193
+ sched = schedule(specs, duration_ms)
194
+ blocks = _blocks(transcript)
195
+ flat = []
196
+ for raw in transcript.splitlines():
197
+ m = _MEM32_LINE.match(raw.strip())
198
+ if m:
199
+ flat.append((int(m[1], 16), [int(w, 16) for w in m[2].split()]))
200
+ out: list[dict] = []
201
+ cursor = 0
202
+ counts: dict[int, int] = {}
203
+ for n, (offset, i) in enumerate(sched):
204
+ spec = specs[i]
205
+ if blocks is not None:
206
+ got = None
207
+ if n < len(blocks) and blocks[n][0] == spec.address:
208
+ got = _collect(spec.address, spec.words, blocks[n][1])
209
+ else:
210
+ got = None
211
+ pos, values = cursor, []
212
+ while len(values) < spec.words and pos < len(flat):
213
+ base, words = flat[pos]
214
+ if base != spec.address + 4 * len(values):
215
+ break
216
+ values.extend(words)
217
+ pos += 1
218
+ if len(values) == spec.words:
219
+ got, cursor = values, pos
220
+ idx = counts.get(i, 0)
221
+ counts[i] = idx + 1
222
+ out.append(
223
+ {
224
+ "address": f"0x{spec.address:08X}",
225
+ "words": spec.words,
226
+ "index": idx,
227
+ "elapsedMs": offset,
228
+ "values": [f"0x{v:08X}" for v in got] if got is not None else None,
229
+ }
230
+ )
231
+ return out
tan/core/raw_write.py ADDED
@@ -0,0 +1,160 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure facts for `tan flash --raw <file>@<addr>` (tan-cli#1446): a byte-exact,
3
+ sector-granular MRAM write for bench backup/restore.
4
+
5
+ Flow D writes a built image plus a signed ATOC. A bench that backed MRAM up with
6
+ `savebin` needs the saved sectors put back EXACTLY, and nothing else: no signing, no
7
+ address derivation, no reset. Everything here is IO-free; the J-Link session lives in
8
+ `tan.commands.flash_raw`.
9
+
10
+ **tan never invents an address.** Each `--raw` names its own address on the command
11
+ line; nothing is read from an `app-package-map.txt`, a manifest or a file name. That is
12
+ the whole ATOC/STOC rule: the Secure Enclave table lives in the last MRAM sectors, and a
13
+ write there is only ever one the user asked for by address.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Sequence
18
+ from dataclasses import dataclass
19
+
20
+ from tan.core.flash_plan import commander_path, validate_commander_path
21
+ from tan.core.flow_d_report import SECTOR_BYTES, hex_addr, sector_span
22
+
23
+ CODE_INVALID = "flash.raw-invalid"
24
+ CODE_RESERVATION = "flash.raw-reservation-required"
25
+ CODE_FAILED = "flash.raw-failed"
26
+
27
+
28
+ class RawError(ValueError):
29
+ """A `--raw` input tan refuses before any spawn. `code` is the registered issue code."""
30
+
31
+ code = CODE_INVALID
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RawSpec:
36
+ path: str
37
+ address: int
38
+ size: int = 0
39
+
40
+
41
+ def parse_raw(spec: str) -> RawSpec:
42
+ """`<file>@<addr>` -> `RawSpec`. The LAST `@` splits, so a path may contain one; the
43
+ address must be an explicit hex literal (`0x80010000`)."""
44
+ path, sep, addr = spec.rpartition("@")
45
+ if not sep or not path or not addr:
46
+ raise RawError(f"--raw {spec!r} is not <file>@<address> (e.g. he_slot0.bin@0x80010000)")
47
+ if not addr.lower().startswith("0x"):
48
+ raise RawError(f"--raw {spec!r}: the address must be an explicit hex literal like 0x80010000")
49
+ try:
50
+ value = int(addr, 16)
51
+ except ValueError:
52
+ raise RawError(f"--raw {spec!r}: {addr!r} is not a hex address") from None
53
+ validate_commander_path(path, "the --raw file path")
54
+ return RawSpec(path=path, address=value)
55
+
56
+
57
+ def validate_ranges(
58
+ specs: Sequence[RawSpec], mram_bytes: int, base: int
59
+ ) -> None:
60
+ """Refuse every unaligned, empty, out-of-MRAM or overlapping range. The loader rewrites
61
+ whole 16 KiB sectors and fills a short tail with 0xFF, so a blob whose address or size
62
+ is not sector-aligned would NOT be a byte-exact restore of what it names."""
63
+ if not specs:
64
+ raise RawError("--raw needs at least one <file>@<address>")
65
+ end_of_mram = base + mram_bytes
66
+ problems: list[str] = []
67
+ for s in specs:
68
+ where = f"{s.path}@{hex_addr(s.address)} ({s.size} B)"
69
+ if s.size <= 0:
70
+ problems.append(f"{where}: the file is empty")
71
+ continue
72
+ if s.address % SECTOR_BYTES:
73
+ problems.append(f"{where}: the address is not {SECTOR_BYTES // 1024} KiB sector-aligned")
74
+ if s.size % SECTOR_BYTES:
75
+ problems.append(
76
+ f"{where}: the size is not a whole number of {SECTOR_BYTES // 1024} KiB sectors -- "
77
+ "the loader would fill the rest of the last sector with 0xFF, so this is not "
78
+ "a byte-exact write; pad the saved blob to the sector boundary yourself"
79
+ )
80
+ if s.address < base or s.address + s.size > end_of_mram:
81
+ problems.append(
82
+ f"{where}: outside MRAM [{hex_addr(base)}, {hex_addr(end_of_mram)})"
83
+ )
84
+ ordered = sorted(specs, key=lambda s: s.address)
85
+ for a, b in zip(ordered, ordered[1:]):
86
+ if a.address + a.size > b.address:
87
+ problems.append(
88
+ f"{a.path}@{hex_addr(a.address)} and {b.path}@{hex_addr(b.address)} overlap"
89
+ )
90
+ if problems:
91
+ raise RawError("; ".join(problems))
92
+
93
+
94
+ def raw_script(preamble: str, specs: Sequence[RawSpec]) -> str:
95
+ """The write session: `preamble` (everything up to and including `connect`), a
96
+ `loadbin` then `verifybin` per blob, `exit`. NO reset and NO `go`: the write must not
97
+ boot, halt-resume or reset anything."""
98
+ lines = [preamble.rstrip("\n")]
99
+ for s in specs:
100
+ lines.append(f"loadbin {commander_path(s.path)} {hex_addr(s.address)}")
101
+ for s in specs:
102
+ lines.append(f"verifybin {commander_path(s.path)} {hex_addr(s.address)}")
103
+ lines.append("exit")
104
+ return "\n".join(lines) + "\n"
105
+
106
+
107
+ def planned(specs: Sequence[RawSpec], sha256s: Sequence[str]) -> list[dict]:
108
+ """Each blob as the envelope reports it: path, address, size, sha256, sector span."""
109
+ return [
110
+ {
111
+ "path": s.path, "address": hex_addr(s.address), "size": s.size, "sha256": digest,
112
+ "sectorSpan": sector_span(s.address, s.size),
113
+ }
114
+ for s, digest in zip(specs, sha256s)
115
+ ]
116
+
117
+
118
+ def lease_holder(show_text: str) -> str | None:
119
+ """The `acquired:` holder (`host/user`) in `labgrid-client -p <place> show` output,
120
+ strictly: exactly one distinct, non-empty value across the ` acquired:` lines, else
121
+ `None` (ambiguity is never a holder)."""
122
+ holders = {
123
+ line.split(":", 1)[1].strip()
124
+ for line in show_text.splitlines()
125
+ if line.startswith(" acquired:")
126
+ }
127
+ return holders.pop() if len(holders) == 1 and next(iter(holders), "x") else None
128
+
129
+
130
+ def lease_swd_path(show_text: str) -> str | None:
131
+ """The USB path of the `swd` resource in `labgrid-client -p <place> show` output (the
132
+ `'path': '3-4.2'` entry of the `Acquired|Matching resource 'swd'` block), or `None`.
133
+ The `matches:` list at the top names the same resource without a path, so only the
134
+ resource block counts."""
135
+ import re
136
+
137
+ inside = False
138
+ for line in show_text.splitlines():
139
+ if re.match(r"^(Acquired|Matching) resource 'swd'", line):
140
+ inside = True
141
+ continue
142
+ if inside:
143
+ if line and not line[0].isspace():
144
+ return None
145
+ found = re.search(r"'path': '([^']+)'", line)
146
+ if found:
147
+ return found.group(1)
148
+ return None
149
+
150
+
151
+ def lease_changed(show_text: str) -> str | None:
152
+ """The place's `changed:` timestamp in `labgrid-client show` output (labgrid 26.0 updates it
153
+ on every acquire/release), strictly: exactly one distinct, non-empty value, else `None`.
154
+ The lease file records it at acquire time so a lease cannot outlive its acquisition."""
155
+ values = {
156
+ line.split(":", 1)[1].strip()
157
+ for line in show_text.splitlines()
158
+ if line.startswith(" changed:")
159
+ }
160
+ return next(iter(values)) if len(values) == 1 and next(iter(values)) else None
tan/core/reset_plan.py ADDED
@@ -0,0 +1,56 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """tan-cli#1452: the pure half of `tan reset` -- the one-pulse nRESET script.
3
+
4
+ `tan reset` is ONE clean pin pulse through J-Link Commander: `r0` (drive nRESET
5
+ low), a `sleep` of the pulse width, `r1` (release), `q`. It deliberately has none of
6
+ the machinery `tan flash`'s pin reset carries: no `connect`, no halt, no `RSetType`, no
7
+ connect-under-reset, no `g`, no retry. Each of those either wakes a target out
8
+ of STOP, loses the VBAT/BKRAM evidence of a non-waking STOP, or both.
9
+ `FORBIDDEN_VERBS` is what the tests hold the generated script to.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ DEFAULT_PULSE_MS = 100
16
+ MIN_PULSE_MS = 1
17
+ MAX_PULSE_MS = 10000
18
+ #: Commander has to open the probe, never attach to the target: a `connect`
19
+ #: against an Alif target in STOP fails (debug domain gated) or disturbs the
20
+ #: evidence this verb exists to preserve. Bench-proven script: r0 / sleep 100 / r1 / q.
21
+ _SAFE_TOKEN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
22
+
23
+ #: Commander verbs a reset script must never contain.
24
+ FORBIDDEN_VERBS = (
25
+ "connect", "h", "halt", "g", "go", "rsettype", "resettype", "reset", "r", "rx", "w1", "w2", "w4",
26
+ "loadbin", "loadfile", "erase", "unlock", "setpc", "step", "s",
27
+ )
28
+
29
+
30
+ class ResetArgError(ValueError):
31
+ """A bad `--pulse-ms`."""
32
+
33
+
34
+ def check_pulse_ms(pulse_ms: int) -> int:
35
+ if isinstance(pulse_ms, bool) or not isinstance(pulse_ms, int):
36
+ raise ResetArgError(f"--pulse-ms {pulse_ms!r} is not an integer")
37
+ if not MIN_PULSE_MS <= pulse_ms <= MAX_PULSE_MS:
38
+ raise ResetArgError(
39
+ f"--pulse-ms {pulse_ms} is outside {MIN_PULSE_MS}..{MAX_PULSE_MS} milliseconds"
40
+ )
41
+ return pulse_ms
42
+
43
+
44
+ def pulse_script(serial: str | None, pulse_ms: int) -> str:
45
+ """`SelectEmuBySN <serial>` (only when a probe is selected), then
46
+ `r0`, `sleep <ms>`, `r1`, `q`. No `connect`."""
47
+ check_pulse_ms(pulse_ms)
48
+ if serial is not None and not _SAFE_TOKEN.match(serial):
49
+ raise ResetArgError(f"the J-Link serial {serial!r} is not a plain token")
50
+ pre = [f"SelectEmuBySN {serial}"] if serial else []
51
+ return "\n".join([*pre, "r0", f"sleep {pulse_ms}", "r1", "q"]) + "\n"
52
+
53
+
54
+ def script_verbs(script: str) -> list[str]:
55
+ """The first token of every non-empty line, lower-cased."""
56
+ return [ln.split()[0].lower() for ln in script.splitlines() if ln.split()]
tan/core/run.py ADDED
@@ -0,0 +1,161 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure post-build decision logic for `tan run` -- port of
3
+ `crates/tan-core/src/run.rs`. No IO: the subprocess spawn and the filesystem
4
+ probe live in `tan.commands.run_cmd`.
5
+
6
+ `tan run` is a THIN ORCHESTRATOR over `tan build`'s engine and (on a hardware
7
+ target, with `--flash`) `tan flash`'s engine -- it never re-derives either.
8
+ The only thing this module owns is the decision of WHICH of those two to
9
+ reach, from signals the build step that just ran produced: whether the build
10
+ succeeded, whether it could establish a host (`native_sim`) vs. hardware
11
+ target, whether `--flash` was requested, and whether the post-build
12
+ `system-manifest.yaml` write is confirmed to have succeeded. Flashing real
13
+ silicon is the dangerous path, so it is never the default action -- a
14
+ hardware target flashes only on an explicit `--flash`.
15
+
16
+ **`native_sim_target`/`manifest_written` must come from the SAME build run
17
+ `tan run` just performed, in memory -- never from re-reading
18
+ `system-manifest.yaml` off disk afterward.** The Rust oracle's own module doc
19
+ records why (three attempts, the last two of which re-read a stale on-disk
20
+ manifest and let a `--flash` slip through against the WRONG target). `tan build` writes the post-build `system-manifest.yaml` and
21
+ `execute.last_manifest_write()` supplies the real signal, so
22
+ `tan.commands.run_cmd` passes the genuine `native_sim_target` / `manifest_written`
23
+ pair and the FLASH and EXECUTE_NATIVE arms are reachable. `native_sim_target=None`
24
+ still means "could not be established", never "not native_sim".
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from enum import StrEnum
29
+ from typing import Any
30
+
31
+ #: The Zephyr board target of a `native_sim` slice -- the robust
32
+ #: discriminator for the host build (an actual `board` value from the
33
+ #: manifest, not a build-dir-name guess). Verbatim from `tan_core::run`.
34
+ NATIVE_SIM_BOARD = "native_sim"
35
+
36
+ #: The literal binary Zephyr's `native_sim` target produces -- a fixed name on
37
+ #: every host OS. It sits beside `zephyr.elf` in the build's `zephyr/` output
38
+ #: dir.
39
+ #:
40
+ #: Module-private (the oracle's equivalent is `pub(crate)`, run.rs:166-169 --
41
+ #: the #83 fix): [`native_sim_exe_beside`] is the ONLY sanctioned way to spell
42
+ #: the swap. A third caller importing this constant to hand-roll
43
+ #: `parent().join(NATIVE_SIM_EXE)` again would silently reopen the door #83
44
+ #: closed -- see that function's docstring.
45
+ _NATIVE_SIM_EXE = "zephyr.exe"
46
+
47
+
48
+ class RunAction(StrEnum):
49
+ """What `tan run` does after its build step -- the pure decision,
50
+ decoupled from the subprocess/filesystem work so it is unit-testable
51
+ without a real build."""
52
+
53
+ #: The build failed -- short-circuit: never run or flash, report the build.
54
+ BUILD_FAILED = "build_failed"
55
+ #: A host target -- execute the produced `native_sim` binary.
56
+ EXECUTE_NATIVE = "execute_native"
57
+ #: A hardware target with an explicit `--flash`, and THIS run's manifest
58
+ #: write is confirmed to have succeeded -- flash the built image.
59
+ FLASH = "flash"
60
+ #: A hardware target WITHOUT `--flash` -- build succeeded, but programming
61
+ #: the board needs explicit consent: report + stop, leave hardware
62
+ #: untouched.
63
+ BUILD_ONLY = "build_only"
64
+ #: `--flash` was requested, but this run's OWN build outcome does not
65
+ #: confirm it is safe to flash: either the target could not be
66
+ #: established at all (`native_sim_target is None`), or it is hardware
67
+ #: but THIS run's manifest file write failed (`manifest_written` is
68
+ #: `False`). Refuse rather than flash from a build that can't vouch for
69
+ #: itself. Board untouched, same as `BUILD_ONLY`, but reported as an
70
+ #: error: the caller explicitly asked to flash and that request could not
71
+ #: be honored safely.
72
+ MANIFEST_STALE = "manifest_stale"
73
+
74
+
75
+ def decide_run_action(
76
+ build_ok: bool,
77
+ native_sim_target: bool | None,
78
+ flash_requested: bool,
79
+ manifest_written: bool,
80
+ ) -> RunAction:
81
+ """Decide what `tan run` does once the build step finished -- entirely
82
+ from signals THIS build produced, never from a post-hoc disk read (see
83
+ the module doc for why that used to be the actual defect).
84
+
85
+ - `native_sim_target` is `True`/`False` when this run's build established
86
+ a target (from the SDK's freshly-emitted system-manifest projection,
87
+ independent of whether the on-disk file write then succeeded), or
88
+ `None` when it could not (the emit itself failed/didn't parse) -- an
89
+ unconfirmed target, not "not native_sim".
90
+ - `manifest_written` is whether THIS run's `system-manifest.yaml` file
91
+ write actually succeeded -- the "write definitely succeeded" signal
92
+ that gates `--flash` for a confirmed hardware target (a failed write
93
+ means `flash` would next read a stale/wrong file).
94
+
95
+ Rules (verbatim from `tan_core::run::decide_run_action`):
96
+ - build failed -> BUILD_FAILED
97
+ - host target (`True`) -> EXECUTE_NATIVE
98
+ (never FLASH: native_sim is not flashable, regardless of `--flash`/
99
+ `manifest_written`)
100
+ - hardware (`False`) + `--flash` + write ok -> FLASH
101
+ - hardware (`False`) + `--flash`, write failed -> MANIFEST_STALE
102
+ - hardware (`False`), no `--flash` -> BUILD_ONLY
103
+ - unknown (`None`) + `--flash` -> MANIFEST_STALE (refuse)
104
+ - unknown (`None`), no `--flash` -> BUILD_ONLY
105
+ """
106
+ if not build_ok:
107
+ return RunAction.BUILD_FAILED
108
+ if native_sim_target is True:
109
+ # A host target is never flashable -- settled by THIS run's own build
110
+ # outcome, so a failed/absent manifest FILE write can't flip a host
111
+ # build into FLASH.
112
+ return RunAction.EXECUTE_NATIVE
113
+ if native_sim_target is False:
114
+ if not flash_requested:
115
+ return RunAction.BUILD_ONLY
116
+ return RunAction.FLASH if manifest_written else RunAction.MANIFEST_STALE
117
+ # native_sim_target is None: this run's build could not confirm host vs.
118
+ # hardware at all. Flashing blind is never acceptable; a bare run still
119
+ # gets to report + stop like any other hardware-shaped build.
120
+ return RunAction.MANIFEST_STALE if flash_requested else RunAction.BUILD_ONLY
121
+
122
+
123
+ def is_native_sim_board(board: Any) -> bool:
124
+ """True for the bare `native_sim` board AND Zephyr's qualified board form
125
+ (`native_sim/native/64`, SoC/variant-qualified). Exact-matching only the
126
+ bare name let a qualified board name defeat the host-vs-hardware
127
+ discriminator on a perfectly fresh manifest. `"native_simulated_foo"`
128
+ deliberately does NOT match -- the required `/` after the prefix anchors
129
+ it to Zephyr's actual board-qualifier syntax instead of an arbitrary
130
+ substring."""
131
+ return isinstance(board, str) and (
132
+ board == NATIVE_SIM_BOARD or board.startswith(f"{NATIVE_SIM_BOARD}/")
133
+ )
134
+
135
+
136
+ def native_sim_slice(manifest: Any) -> dict[str, Any] | None:
137
+ """The `native_sim` slice in a post-build manifest, if any -- identified
138
+ by its Zephyr board target ([`is_native_sim_board`]). `manifest` is a
139
+ `tan.core.system_manifest.SystemManifest`; its `slices` are raw dicts
140
+ (the manifest reader's own lossless-round-trip convention). Pure: no IO.
141
+ """
142
+ return next(
143
+ (s for s in manifest.slices if is_native_sim_board(s.get("board"))), None
144
+ )
145
+
146
+
147
+ def native_sim_exe_beside(elf: str) -> str:
148
+ """Swap a native_sim slice's `output_artefact` for the runnable
149
+ [`_NATIVE_SIM_EXE`] that sits beside it. A manifest NEVER records the
150
+ `.exe`: `output_artefact` always names `zephyr.elf`, for every zephyr
151
+ slice including native_sim, so every consumer that wants the host
152
+ runnable has to make this swap -- and must all make the SAME one. This is
153
+ THE one spelling of the rule: `tan.commands.debug_config_cmd` calls this
154
+ function rather than carrying its own copy (the #83 fix this module's
155
+ `_NATIVE_SIM_EXE` docstring records).
156
+
157
+ Splits on the separator the path itself uses rather than normalising via
158
+ a path library, which on Windows would rejoin with `\\` and turn a
159
+ `/`-authored manifest path into a mixed `a/b\\zephyr.exe`."""
160
+ cut = max(elf.rfind("/"), elf.rfind("\\"))
161
+ return _NATIVE_SIM_EXE if cut < 0 else elf[: cut + 1] + _NATIVE_SIM_EXE