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/setools.py ADDED
@@ -0,0 +1,608 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """SETOOLS integration for the Flow D (`alif_mram_jlink`) slot0 sign step --
3
+ tan-cli#353's remaining half.
4
+
5
+ Both host paths that put a signed image into an Alif Ensemble part's MRAM
6
+ (`tan.core.flash_plan`'s Flow A/Flow D) need Alif's SETOOLS `app-gen-toc` step
7
+ to sign the ATOC first; alp-sdk's own manifest never carries a signed blob --
8
+ measured on a fresh AEN801 emit, `flash_args` holds only
9
+ `jlink_flash_device`. Before this module, that meant a customer signed
10
+ OUTSIDE tan (alp-sdk's `docs/aen-provisioning.md` §3-4 -- that path is not in
11
+ THIS repo; alp-sdk is where it lives) and hand-edited the manifest
12
+ with the resulting `atoc`/`atoc_address` before `tan flash` would do anything
13
+ -- `flash_plan.plan_alif_mram_jlink`'s "both required" refusal names the
14
+ missing fields, not the vendor tool that produces them.
15
+
16
+ **SETOOLS is license-gated and Alp Lab does not redistribute it** -- the same
17
+ stance `tan doctor`'s own `setools` check already takes. What this module
18
+ adds is: given a SETOOLS install the customer already has on disk, drive its
19
+ `app-gen-toc` step for them -- copy the build's raw `.bin`, write the JSON
20
+ config it wants, run it, and read back the ATOC placement it prints -- so
21
+ `tan flash` can complete end to end. RESOLVING that install is also this
22
+ module's job ([`resolve_setools_dir`]): the `--setools-dir` flag, then
23
+ `SETOOLS_DIR`, then `flash_args.setools_dir`, in that order, and NOTHING
24
+ ELSE -- no filesystem search -- because a WRONG SETOOLS silently signing
25
+ against the wrong part is worse than tan refusing outright.
26
+
27
+ That ranking is deliberate and is the tan-cli#368 re-ranking; read
28
+ [`resolve_setools_dir`]'s own docstring before changing it. `flash_args` is
29
+ GENERATED -- every `tan build` overwrites it -- so a stale hand-edited
30
+ `setools_dir` must never outrank the `SETOOLS_DIR` an operator exported or a
31
+ flag they passed for this one invocation. This header previously stated the
32
+ reverse order (tan-cli#572), leaving two docstrings in ONE file disagreeing
33
+ about which SETOOLS signs the image.
34
+
35
+ **Not `tan.core.flash_plan`.** That module is pure/no-IO by its own
36
+ docstring; this one is not -- it copies a file, writes a config, and spawns
37
+ `app-gen-toc`, the same real-filesystem-work exception
38
+ `tan.core.venv`/`tan.core.bootstrap` already carry. Every DECISION about
39
+ *when* to call this module (never off the `alif_mram_jlink` path, never once
40
+ `atoc`/`atoc_address` are already resolved) stays in `tan.commands.flash_cmd`,
41
+ which is also the only caller.
42
+
43
+ **The sign never writes the customer's install (tan-cli#1325).** It runs in a
44
+ private scratch overlay of it (`tan.core.setools_scratch`), so it is
45
+ side-effect-free and `--dry-run` runs it too. The caller owns the scratch tree
46
+ and removes it when the entry ends.
47
+
48
+ **No new hardware fact (ADR-0017 / I-26).** `mramAddress` is
49
+ `flash_args.slot0_load_address` verbatim -- already a documented Flow D key
50
+ (`flash_plan.plan_alif_mram_jlink`) -- and `cpu_id` is the manifest's own
51
+ `core_id` upper-cased (`m55_he` -> `M55_HE`); neither is invented here.
52
+
53
+ ponytail: `cpu_id = core_id.upper()` is a naming-convention bet, not a
54
+ metadata fact -- correct for every AEN `core_id` measured so far (`m55_he` /
55
+ `m55_hp`). Upgrade path if a future `core_id` spelling ever diverges from
56
+ SETOOLS' own `cpu_id` vocabulary: a `flash_args.setools_cpu_id` override,
57
+ added once a real manifest needs one -- not added speculatively here.
58
+ """
59
+ from __future__ import annotations
60
+
61
+ import dataclasses
62
+ import json
63
+ import os
64
+ import shutil
65
+ import subprocess
66
+ import time
67
+ from collections.abc import Callable, Sequence
68
+ from dataclasses import dataclass
69
+ from typing import Any
70
+
71
+ from tan.core.flash_plan import (
72
+ FLOW_D_METHOD,
73
+ FlashPlanError,
74
+ fa_str,
75
+ parse_atoc_start_address,
76
+ validate_identifier,
77
+ )
78
+ from tan.core.setools_scratch import (
79
+ DEVICE_ENTRY_VERSION,
80
+ AtocReport,
81
+ DeviceConfig,
82
+ cleanup_scratch,
83
+ make_scratch,
84
+ parse_atoc_report,
85
+ )
86
+ from tan.core.subprocess_env import spawn_env
87
+
88
+ #: The one SETOOLS executable this module drives. Bare name, no extension --
89
+ #: the Alif Security Toolkit bundle (`app-release-exec-linux-SE_FW_x.y.z`) is
90
+ #: a Linux tool; nothing here guesses a `.exe`/`.bat` variant, matching `tan
91
+ #: doctor`'s own `setools_check` (`tan/commands/doctor_cmd.py`).
92
+ APP_GEN_TOC = "app-gen-toc"
93
+
94
+ #: Seconds `app-gen-toc` may run before it is killed -- a local sign step over
95
+ #: one small binary, generous mainly against a hung/misconfigured SETOOLS
96
+ #: install (e.g. waiting on an interactive prompt an app-only ATOC should
97
+ #: never need).
98
+ APP_GEN_TOC_TIMEOUT_S = 120.0
99
+
100
+ #: SETOOLS' own fixed output locations, always relative to `$SETOOLS_DIR` and
101
+ #: never configurable -- reading these back is not "searching the
102
+ #: filesystem": they are the ONE place `app-gen-toc` itself writes, per
103
+ #: alp-sdk's `docs/aen-provisioning.md` and every bench script under alp-sdk's
104
+ #: `scripts/bench/aen/` -- NEITHER path exists in this repo (tan-cli); both
105
+ #: are alp-sdk paths, cited here only as the authority for the fixed shape.
106
+ _ATOC_BLOB_REL = os.path.join("build", "AppTocPackage.bin")
107
+ _ATOC_MAP_REL = os.path.join("build", "app-package-map.txt")
108
+
109
+ @dataclass(frozen=True)
110
+ class SetoolsSource:
111
+ """A resolved `$SETOOLS_DIR`, plus WHERE it came from -- every refusal
112
+ downstream names `source`, so a customer juggling both an explicit
113
+ manifest value and a shell export knows which one tan actually read."""
114
+
115
+ path: str
116
+ source: str
117
+
118
+ @property
119
+ def operator_supplied(self) -> bool:
120
+ """The path came from the operator -- `--setools-dir` or `$SETOOLS_DIR` --
121
+ not from `flash_args.setools_dir`, which the PROJECT controls (a checkout's
122
+ manifest). Only an operator-supplied install is one tan may EXECUTE for a
123
+ preview (tan-cli#1343 review)."""
124
+ return self.source != _MANIFEST_SOURCE
125
+
126
+
127
+ _MANIFEST_SOURCE = "flash_args.setools_dir"
128
+
129
+ #: Registered issue code (`contract/issue-codes.json`) for a SETOOLS install or device
130
+ #: config that only the PROJECT named.
131
+ UNTRUSTED_SOURCE_CODE = "flash.setools-untrusted-source"
132
+
133
+
134
+ class SetoolsUntrustedSourceError(FlashPlanError):
135
+ """The SETOOLS install (or the device config signed into MRAM) was named only
136
+ by the project's manifest. `code` is the registered issue code."""
137
+
138
+ code = UNTRUSTED_SOURCE_CODE
139
+
140
+
141
+ def untrusted_install_message(setools: "SetoolsSource") -> str:
142
+ return (
143
+ f"{FLOW_D_METHOD}: the SETOOLS install '{setools.path}' is named only by "
144
+ "flash_args.setools_dir in the manifest, which the project controls -- tan will not "
145
+ "EXECUTE an app-gen-toc a checkout picked for a write to a board. Name the install "
146
+ "yourself: --setools-dir <path> on the command line or SETOOLS_DIR=<path> in the "
147
+ "environment."
148
+ )
149
+
150
+
151
+ def _inside(path: str, root: str) -> bool:
152
+ try:
153
+ real_path, real_root = os.path.realpath(path), os.path.realpath(root)
154
+ return os.path.commonpath([real_path, real_root]) == real_root
155
+ except (ValueError, OSError):
156
+ return False
157
+
158
+
159
+ def check_device_config_location(path: str, setools_dir: str, project_dir: str | None) -> None:
160
+ """A manifest-named device configuration is accepted only when it resolves (symlinks
161
+ followed) inside the operator-named SETOOLS install or the project directory: the
162
+ file is signed into MRAM, so an arbitrary path (`/etc/...`, another user's home) is
163
+ refused. Raises [`SetoolsUntrustedSourceError`]."""
164
+ roots = [setools_dir] + ([project_dir] if project_dir else [])
165
+ if any(_inside(path, root) for root in roots):
166
+ return
167
+ raise SetoolsUntrustedSourceError(
168
+ f"{FLOW_D_METHOD}: flash_args.setools_device_config resolves to '{path}', outside the "
169
+ f"SETOOLS install ('{setools_dir}') and the project directory"
170
+ + (f" ('{project_dir}')" if project_dir else "")
171
+ + " -- it would be signed into MRAM, so tan only accepts a device config from "
172
+ "those places. Move it into the project, or drop the key to use the stock config."
173
+ )
174
+
175
+
176
+ def resolve_setools_dir(
177
+ flash_args: Any, env: dict[str, str], flag: str | None = None
178
+ ) -> SetoolsSource | None:
179
+ """Most-explicit-first (tan-cli#368): the `--setools-dir` CLI flag, then
180
+ `$SETOOLS_DIR`, then `flash_args.setools_dir`. `None` when none of the
181
+ three is set. Never a filesystem search and never a guess -- a wrong
182
+ SETOOLS signing against the wrong part is worse than refusing.
183
+
184
+ The flag outranks the environment, which outranks the manifest --
185
+ DELIBERATELY the opposite of most `flash_args` accessors in this codebase
186
+ (which read the manifest as authoritative). `build/system-manifest.yaml`
187
+ is regenerated by every `tan build` and alp-sdk's own emit carries no
188
+ `setools_dir` key at all, so a hand-edit there is silently destroyed by
189
+ the customer's next build (#368) -- it is the LEAST durable of the three,
190
+ not the most, and is ranked accordingly. `SETOOLS_DIR` survives a build
191
+ but is shell/session-scoped; `--setools-dir` is the one source pinnable
192
+ per invocation regardless of either, so it wins outright.
193
+ """
194
+ if flag:
195
+ return SetoolsSource(flag, "the --setools-dir flag")
196
+ from_env = env.get("SETOOLS_DIR")
197
+ if from_env:
198
+ return SetoolsSource(from_env, "the SETOOLS_DIR environment variable")
199
+ explicit = fa_str(flash_args, "setools_dir")
200
+ if explicit:
201
+ return SetoolsSource(explicit, _MANIFEST_SOURCE)
202
+ return None
203
+
204
+
205
+ def _app_gen_toc_candidates(setools_dir: str) -> list[str]:
206
+ """Every filename [`find_app_gen_toc`] tries, in order -- shared with
207
+ [`missing_tool_message`] (tan-cli#369) so the diagnosis names EXACTLY
208
+ what was checked, never a conclusion beyond it. Bare `APP_GEN_TOC`
209
+ everywhere; also `APP_GEN_TOC + ".exe"` on Windows, since a genuine
210
+ Windows SETOOLS install ships the executable with an extension and the
211
+ bare name alone is never found there."""
212
+ candidates = [os.path.join(setools_dir, APP_GEN_TOC)]
213
+ if os.name == "nt":
214
+ candidates.append(os.path.join(setools_dir, APP_GEN_TOC + ".exe"))
215
+ return candidates
216
+
217
+
218
+ def find_app_gen_toc(setools_dir: str) -> str | None:
219
+ """The first of [`_app_gen_toc_candidates`] that exists inside
220
+ `setools_dir`, or `None`. Incapable of raising -- `setools_dir` is a
221
+ customer-supplied path (`--setools-dir`, an env var, or
222
+ `flash_args.setools_dir`) that may hold anything."""
223
+ try:
224
+ return next(
225
+ (c for c in _app_gen_toc_candidates(setools_dir) if os.path.isfile(c)), None
226
+ )
227
+ except (OSError, ValueError):
228
+ return None
229
+
230
+
231
+ def unresolved_message(sku: str | None = None, flash_device: str | None = None) -> str:
232
+ """The guidance for `resolve_setools_dir` answering `None` -- names EVERY
233
+ accepted source, in PRECEDENCE ORDER, flag first (tan-cli#368): the flag
234
+ is the one source visible in `tan flash --help` and pinnable per
235
+ invocation, so it leads; the manifest field is named last and flagged as
236
+ build-owned, since `tan build` silently overwrites a hand-edit there on
237
+ the customer's next build. Modeled on `sdk_cmd.NO_SDK_NEXT_STEPS`/
238
+ `doctor_cmd.setools_check`'s own tone: remedy first, blame never.
239
+
240
+ tan-cli#1319: the subject is NAMED from the manifest -- `sku` (its
241
+ `hw_info.sku`) and `flash_device` (`flash_args.jlink_flash_device`, the
242
+ SoC variant's J-Link part profile) -- never a hardcoded SKU. Both absent
243
+ (a hand-written manifest) falls back to a SKU-free noun."""
244
+ subject = _slot0_subject(sku, flash_device)
245
+ return (
246
+ f"{FLOW_D_METHOD}: {subject} needs a SIGNED ATOC, which only "
247
+ f"Alif's SETOOLS `{APP_GEN_TOC}` step can produce. SETOOLS is license-gated "
248
+ "and alp-sdk does not redistribute it -- install it from Alif, then point "
249
+ "tan at it, most-specific first: --setools-dir <path> on the command line, "
250
+ "SETOOLS_DIR=<path> in the environment, or flash_args.setools_dir in the "
251
+ "manifest (lowest precedence, and OVERWRITTEN by the next `tan build` -- "
252
+ "prefer the flag or the environment variable for a durable setting)."
253
+ )
254
+
255
+
256
+ def _slot0_subject(sku: str | None, flash_device: str | None) -> str:
257
+ """`the E1M-AEN803 slot0 image (AE822FA0E5597LS0_M55_HE)` -- whatever of
258
+ the manifest's SKU / J-Link device profile is known, never invented."""
259
+ parts = [p.strip() for p in (sku, flash_device) if isinstance(p, str) and p.strip()]
260
+ if not parts:
261
+ return "an Alif Ensemble MRAM slot0 image"
262
+ if len(parts) == 1:
263
+ return f"the {parts[0]} slot0 image"
264
+ return f"the {parts[0]} slot0 image ({parts[1]})"
265
+
266
+
267
+ def missing_tool_message(setools: SetoolsSource) -> str:
268
+ """The guidance when `setools.path` resolved (from `setools.source`) but
269
+ [`find_app_gen_toc`] found nothing there -- distinct from
270
+ [`unresolved_message`] because the customer already told tan where to
271
+ look; the problem is what tan found there, not that nothing was named.
272
+
273
+ **tan-cli#369.** Used to assert a CONCLUSION ("this does not look like an
274
+ Alif Security Toolkit install") identically for a directory that does not
275
+ exist at all, a path pointed at the `app-gen-toc` BINARY itself instead
276
+ of its parent directory, and a genuine Windows install
277
+ (`find_app_gen_toc` did not try `app-gen-toc.exe` -- fixed alongside
278
+ this). Names only what was actually checked: every candidate filename
279
+ [`_app_gen_toc_candidates`] tries, and whether `setools.path` is even a
280
+ real directory -- never a verdict the check did not make.
281
+ """
282
+ candidates = _app_gen_toc_candidates(setools.path)
283
+ tried = " or ".join(f"'{c}'" for c in candidates)
284
+ try:
285
+ is_dir = os.path.isdir(setools.path)
286
+ except (OSError, ValueError):
287
+ is_dir = False
288
+ if is_dir:
289
+ where = f"SETOOLS not found at {tried} -- the directory exists but holds none of them."
290
+ else:
291
+ where = (
292
+ f"SETOOLS not found at {tried} -- '{setools.path}' is not a directory at "
293
+ f"all. If it names the {APP_GEN_TOC} binary itself, point tan at its "
294
+ "PARENT directory instead."
295
+ )
296
+ return (
297
+ f"{FLOW_D_METHOD}: SETOOLS_DIR resolved to '{setools.path}' (via "
298
+ f"{setools.source}), but {where} Check the path, or re-download SETOOLS from "
299
+ "Alif."
300
+ )
301
+
302
+
303
+ def slot0_config(
304
+ name: str,
305
+ binary: str,
306
+ mram_address: str,
307
+ cpu_id: str,
308
+ device_binary: str | None = None,
309
+ ) -> dict[str, Any]:
310
+ """The `app-gen-toc` JSON config for one slot0 ATOC. With `device_binary`
311
+ (the file name of the device configuration, tan-cli#1322) the table leads
312
+ with a `DEVICE` entry in the exact shape alp-sdk's bench recipe signs
313
+ (`scripts/bench/aen/flash-run.sh`: `binary`, version `0.5.00`, `signed`,
314
+ `disabled: false`); without it the config is the app-only shape tan-cli#353
315
+ first measured. The app entry itself is identical either way."""
316
+ config: dict[str, Any] = {}
317
+ if device_binary is not None:
318
+ config["DEVICE"] = {
319
+ "disabled": False,
320
+ "binary": device_binary,
321
+ "version": DEVICE_ENTRY_VERSION,
322
+ "signed": True,
323
+ }
324
+ config[name] = {
325
+ "binary": binary,
326
+ "version": "1.0.0",
327
+ "mramAddress": mram_address,
328
+ "cpu_id": cpu_id,
329
+ "flags": ["boot"],
330
+ "signed": True,
331
+ }
332
+ return config
333
+
334
+
335
+ def combined_slot0_config(
336
+ slices: Sequence[tuple[str, str, str]],
337
+ device_binary: str | None = None,
338
+ ) -> dict[str, Any]:
339
+ """The `app-gen-toc` config for ONE ATOC naming every slice (tan-cli#1509):
340
+ the optional `DEVICE` entry, then one entry per `(name, binary, mram_address)`
341
+ -- each exactly what [`slot0_config`] writes for a lone slice (`cpu_id` is the
342
+ upper-cased name). One slice gives `slot0_config`'s output verbatim."""
343
+ config: dict[str, Any] = {}
344
+ for n, (name, binary, address) in enumerate(slices):
345
+ config.update(
346
+ slot0_config(
347
+ name, binary, address, name.upper(),
348
+ device_binary=device_binary if n == 0 else None,
349
+ )
350
+ )
351
+ return config
352
+
353
+
354
+ def read_atoc_address(setools_dir: str) -> str | None:
355
+ """The ATOC placement `app-gen-toc` just wrote, out of its own
356
+ `build/app-package-map.txt` report. Reuses `flash_plan
357
+ .parse_atoc_start_address`'s parse (byte-identical to every bench
358
+ script's own `awk .../app-package-map.txt | tail -1`) -- the read half
359
+ lives here, not there, since `flash_plan` stays no-IO. `None` when the
360
+ report is not there; a caller decides what that means."""
361
+ map_path = os.path.join(setools_dir, _ATOC_MAP_REL)
362
+ try:
363
+ with open(map_path, encoding="utf-8", errors="replace", newline="") as fh:
364
+ text = fh.read()
365
+ except OSError:
366
+ return None
367
+ return parse_atoc_start_address(text)
368
+
369
+
370
+ def _tail(stdout: str, stderr: str) -> str:
371
+ """The last 4 non-empty lines of whichever stream carries something --
372
+ mirrors `flash_cmd._capture_tail`'s shape (not imported: that helper
373
+ reads a `flash_cmd._Outcome`, a shape this module has no reason to
374
+ depend on)."""
375
+ text = stderr if stderr.strip() else stdout
376
+ lines = [line for line in text.splitlines() if line.strip()][-4:]
377
+ return " | ".join(lines) if lines else "no output"
378
+
379
+
380
+ @dataclass(frozen=True)
381
+ class SignedSlot0:
382
+ """One completed scratch sign (tan-cli#1325). The ATOC blob lives in
383
+ `scratch_dir`, which the CALLER removes ([`cleanup_scratch`]) once J-Link has
384
+ read it; the shared SETOOLS install was never written."""
385
+
386
+ atoc_path: str
387
+ atoc_address: str
388
+ atoc_size: int
389
+ scratch_dir: str
390
+ report: AtocReport
391
+ device_config: DeviceConfig | None
392
+ #: Paths in the SHARED install newer than the start of this sign (at most 5).
393
+ #: Expected empty; non-empty means another process wrote there meanwhile -- or
394
+ #: that the overlay leaked. Reported, never a refusal (a concurrent raw bench
395
+ #: recipe is legitimate).
396
+ shared_touched: tuple[str, ...] = ()
397
+
398
+
399
+ def sign_slot0(
400
+ setools_dir: str,
401
+ app_gen_toc: str,
402
+ artefact_bin: str,
403
+ entry_id: str,
404
+ mram_address: str,
405
+ *,
406
+ device_config: DeviceConfig | None = None,
407
+ scratch_parent: str | None = None,
408
+ on_scratch: Callable[[str], None] | None = None,
409
+ extra_slices: Sequence[tuple[str, str, str]] = (),
410
+ ) -> SignedSlot0:
411
+ """Run one `app-gen-toc` sign step in a PRIVATE scratch overlay of
412
+ `setools_dir` ([`make_scratch`]): copy `artefact_bin` into the scratch
413
+ `build/images/`, write `build/config/<entry_id>-slot0.json` (with a leading
414
+ `DEVICE` entry when `device_config` is given, its file copied into the
415
+ scratch `build/config/`), spawn the scratch copy of `app_gen_toc` with
416
+ `cwd=<scratch>` (its config path is relative to it, matching the bench's own
417
+ `cd $SETOOLS_DIR && ./app-gen-toc -f build/config/...`), then read back the
418
+ ATOC placement and entry list from the scratch report.
419
+
420
+ The shared install is READ ONLY here (tan-cli#1325): the previous design
421
+ wrote `build/AppTocPackage.bin`, `build/images/`, `build/config/`, appended
422
+ to `build/app-package-map.txt` and created `build/tan-atoc/` plus a lock file
423
+ in it, so one `tan flash` changed the package every other user of that
424
+ install found there. Because nothing shared is written, the cross-process
425
+ lock tan-cli#380 needed for that shared output is gone too: two runs get two
426
+ scratch trees and cannot cross-pair. A stale package or map cannot leak in
427
+ either -- the scratch `build/` starts empty, so a tool that exits 0 without
428
+ writing is caught by the plain "no report / no blob" checks below.
429
+
430
+ `extra_slices` (`(entry_id, artefact_bin, mram_address)` each, tan-cli#1509)
431
+ adds further entries to the SAME config, so one ATOC names every slice; the
432
+ first slice is the one named by the positional arguments.
433
+
434
+ Returns [`SignedSlot0`]; the caller owns `scratch_dir`. Raises
435
+ `FlashPlanError` -- naming `app-gen-toc`'s own captured output where there is
436
+ any -- on: a filesystem failure preparing the scratch tree, a spawn failure
437
+ or timeout, a non-zero exit, a report with no `'APP Package Start Address:'`
438
+ line, or a successful exit that did not produce the ATOC blob. SETOOLS' own
439
+ diagnostic is the authoritative one; this only surfaces it. On any raise the
440
+ scratch tree is already removed.
441
+ """
442
+ validate_identifier(entry_id, "the flash target id")
443
+ for extra_id, _bin, _addr in extra_slices:
444
+ validate_identifier(extra_id, "the flash target id")
445
+ setools_dir = os.path.abspath(setools_dir)
446
+ app_gen_toc = os.path.abspath(app_gen_toc)
447
+ try:
448
+ scratch = make_scratch(setools_dir, scratch_parent)
449
+ except OSError as err:
450
+ raise FlashPlanError(
451
+ f"{FLOW_D_METHOD}: could not prepare a scratch copy of the SETOOLS install "
452
+ f"'{setools_dir}' for the sign step: {err}"
453
+ ) from err
454
+ started, before = time.time_ns(), _snapshot(setools_dir)
455
+ try:
456
+ # BEFORE anything can be interrupted: the caller registers the scratch's
457
+ # removal here, so a KeyboardInterrupt mid-sign cannot leak the tree.
458
+ if on_scratch is not None:
459
+ on_scratch(scratch)
460
+ signed = _sign_in_scratch(
461
+ scratch, setools_dir, app_gen_toc, artefact_bin, entry_id, mram_address,
462
+ device_config, extra_slices,
463
+ )
464
+ return dataclasses.replace(signed, shared_touched=_newer_than(setools_dir, started, before))
465
+ except BaseException:
466
+ cleanup_scratch(scratch)
467
+ raise
468
+
469
+
470
+ def _snapshot(root: str) -> dict[str, tuple[int, int]]:
471
+ """`{path: (mtime_ns, size)}` for every file under `root`. Never raises."""
472
+ snap: dict[str, tuple[int, int]] = {}
473
+ try:
474
+ for base, _dirs, files in os.walk(root):
475
+ for name in files:
476
+ path = os.path.join(base, name)
477
+ try:
478
+ st = os.lstat(path)
479
+ except OSError:
480
+ continue
481
+ snap[path] = (st.st_mtime_ns, st.st_size)
482
+ except OSError:
483
+ pass
484
+ return snap
485
+
486
+
487
+ def _newer_than(
488
+ root: str,
489
+ since_ns: int,
490
+ before: dict[str, tuple[int, int]] | None = None,
491
+ limit: int = 5,
492
+ ) -> tuple[str, ...]:
493
+ """Files under `root` changed since the sign began (cheap post-check that
494
+ the overlay kept the shared install untouched). A file counts when it is
495
+ absent from the `before` snapshot, its (mtime, size) differs from it, or its
496
+ mtime is newer than `since_ns`. The snapshot is what makes this robust: on
497
+ Windows file mtimes come from a coarse (~15 ms) clock that can lag
498
+ `time.time_ns()`, so a write right after `since_ns` was taken can carry an
499
+ mtime <= it and a bare timestamp compare misses it. Never raises."""
500
+ found: list[str] = []
501
+ for path, (mtime_ns, size) in _snapshot(root).items():
502
+ prior = None if before is None else before.get(path)
503
+ if mtime_ns > since_ns or (before is not None and prior != (mtime_ns, size)):
504
+ found.append(path)
505
+ if len(found) >= limit:
506
+ break
507
+ return tuple(found)
508
+
509
+
510
+ def _sign_in_scratch(
511
+ scratch: str,
512
+ setools_dir: str,
513
+ app_gen_toc: str,
514
+ artefact_bin: str,
515
+ entry_id: str,
516
+ mram_address: str,
517
+ device_config: DeviceConfig | None,
518
+ extra_slices: Sequence[tuple[str, str, str]] = (),
519
+ ) -> SignedSlot0:
520
+ binary_name = f"{entry_id}.bin"
521
+ config_rel = os.path.join("build", "config", f"{entry_id}-slot0.json")
522
+ atoc_map_path = os.path.join(scratch, _ATOC_MAP_REL)
523
+ atoc_blob_path = os.path.join(scratch, _ATOC_BLOB_REL)
524
+ try:
525
+ shutil.copyfile(artefact_bin, os.path.join(scratch, "build", "images", binary_name))
526
+ for extra_id, extra_bin, _addr in extra_slices:
527
+ shutil.copyfile(extra_bin, os.path.join(scratch, "build", "images", f"{extra_id}.bin"))
528
+ if device_config is not None:
529
+ shutil.copyfile(
530
+ device_config.path, os.path.join(scratch, "build", "config", device_config.name)
531
+ )
532
+ with open(os.path.join(scratch, config_rel), "w", encoding="utf-8", newline="\n") as fh:
533
+ json.dump(
534
+ combined_slot0_config(
535
+ [(entry_id, binary_name, mram_address)]
536
+ + [(i, f"{i}.bin", a) for i, _b, a in extra_slices],
537
+ device_binary=device_config.name if device_config is not None else None,
538
+ ),
539
+ fh,
540
+ indent=2,
541
+ )
542
+ fh.write("\n")
543
+ except OSError as err:
544
+ raise FlashPlanError(
545
+ f"{FLOW_D_METHOD}: could not prepare the SETOOLS sign step under the scratch "
546
+ f"copy '{scratch}' of '{setools_dir}': {err}"
547
+ ) from err
548
+
549
+ # The scratch copy of the tool, not the shared one: its own directory (and the
550
+ # `../build` it addresses) is then the scratch root.
551
+ inside = os.path.relpath(app_gen_toc, setools_dir)
552
+ scratch_tool = os.path.join(scratch, inside) if not inside.startswith("..") else app_gen_toc
553
+ try:
554
+ # tan-cli#992: this signs the ATOC that gets written to real silicon --
555
+ # the same reasoning `flash_cmd._child_env` documents applies here
556
+ # verbatim, so `env=` is never left to inherit this process's (possibly
557
+ # bundle-poisoned) environment.
558
+ proc = subprocess.run(
559
+ [scratch_tool, "-f", config_rel],
560
+ cwd=scratch,
561
+ capture_output=True,
562
+ text=True,
563
+ encoding="utf-8",
564
+ errors="replace",
565
+ env=spawn_env(),
566
+ timeout=APP_GEN_TOC_TIMEOUT_S,
567
+ )
568
+ except subprocess.TimeoutExpired as err:
569
+ raise FlashPlanError(
570
+ f"{FLOW_D_METHOD}: {APP_GEN_TOC} timed out after "
571
+ f"{APP_GEN_TOC_TIMEOUT_S:.0f}s signing {config_rel}"
572
+ ) from err
573
+ except OSError as err:
574
+ raise FlashPlanError(f"{FLOW_D_METHOD}: could not run {app_gen_toc}: {err}") from err
575
+ if proc.returncode != 0:
576
+ raise FlashPlanError(
577
+ f"{FLOW_D_METHOD}: {APP_GEN_TOC} -f {config_rel} exited {proc.returncode}: "
578
+ f"{_tail(proc.stdout, proc.stderr)}"
579
+ )
580
+
581
+ try:
582
+ with open(atoc_map_path, encoding="utf-8", errors="replace", newline="") as fh:
583
+ report_text = fh.read()
584
+ except OSError:
585
+ report_text = ""
586
+ address = parse_atoc_start_address(report_text)
587
+ if address is None:
588
+ raise FlashPlanError(
589
+ f"{FLOW_D_METHOD}: {APP_GEN_TOC} exited 0 but its report "
590
+ f"({_ATOC_MAP_REL}, in the scratch copy) carries no 'APP Package Start "
591
+ "Address:' line -- check the SETOOLS config, or sign by hand."
592
+ )
593
+ try:
594
+ atoc_size = os.path.getsize(atoc_blob_path)
595
+ except OSError:
596
+ raise FlashPlanError(
597
+ f"{FLOW_D_METHOD}: {APP_GEN_TOC} exited 0 and reported an address, but "
598
+ f"{_ATOC_BLOB_REL} was not produced in the scratch copy -- check the SETOOLS "
599
+ "output."
600
+ ) from None
601
+ return SignedSlot0(
602
+ atoc_path=atoc_blob_path,
603
+ atoc_address=address,
604
+ atoc_size=atoc_size,
605
+ scratch_dir=scratch,
606
+ report=parse_atoc_report(report_text),
607
+ device_config=device_config,
608
+ )