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,243 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Per-run scratch overlay of an Alif SETOOLS install, the DEVICE-entry source
3
+ and the ATOC report parser -- the side-effect-free half of the Flow D sign step
4
+ (tan-cli#1325, tan-cli#1322, tan-cli#1318).
5
+
6
+ **Why a scratch tree (tan-cli#1325).** `app-gen-toc` writes its inputs and
7
+ outputs into the install it runs from: `build/images/`, `build/config/`,
8
+ `build/AppTocPackage.bin`, an APPEND-mode `build/app-package-map.txt` and
9
+ `build/logs/SBContent.log`. Running it inside the customer's shared install
10
+ overwrote a package another user of that install (a raw bench recipe, another
11
+ tan run, another slice) had left there. So every sign now runs in a private
12
+ overlay and the shared install is only ever READ.
13
+
14
+ **What the overlay is, measured against SETOOLS SE_FW_1.110.00.** The tool
15
+ `chdir`s into one of its own subdirectories and then addresses the output tree
16
+ as `../build/...` (its log line reads `Logging to ../build/logs/SBContent.log`),
17
+ so a symlinked SUBDIRECTORY would resolve `..` back into the shared install and
18
+ write there. Therefore:
19
+
20
+ * every top-level FILE (the `app-gen-*`/`app-write-mram` executables,
21
+ `version.txt`, ...) is symlinked -- read-only and cwd-independent;
22
+ * every top-level DIRECTORY is COPIED (`bin/`, `cert/`, `utils/`: ~2.5 MiB
23
+ measured -- the tool writes there, e.g. its logs), so its `..` is the scratch
24
+ root. The ONLY exceptions are an explicit read-only allowlist,
25
+ [`_LINK_DIRS`] (`alif/`: ~74 MiB of System Package images the signing step
26
+ does not read, so copying it per run would cost more than it isolates), and
27
+ `utils/key`, the signing keys, which are symlinked INTO the copied `utils/`
28
+ rather than duplicated into the temp directory (measured: `app-gen-toc` signs
29
+ correctly through the link and the shared install stays byte-identical);
30
+ * `build/` is a FRESH real directory holding only what this one sign needs
31
+ (`config/`, `images/`, `logs/`), so `app-package-map.txt` is never shared
32
+ history and the post-sign report belongs to exactly this run.
33
+
34
+ Where a symlink cannot be created (Windows without the privilege) the file is
35
+ copied instead.
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import os
41
+ import re
42
+ import shutil
43
+ import tempfile
44
+ from dataclasses import dataclass
45
+
46
+ from tan.core.flash_plan import FlashPlanError
47
+
48
+ #: Top-level directories that are symlinked, not copied: read-only to the sign
49
+ #: step (see the module docstring). Everything else is copied.
50
+ _LINK_DIRS = frozenset({"alif"})
51
+
52
+ #: Directory, relative to the install root, whose contents are key material:
53
+ #: linked into the copy instead of duplicated.
54
+ _KEY_DIR = os.path.join("utils", "key")
55
+
56
+ #: The stock device configuration SETOOLS ships, relative to the install root.
57
+ STOCK_DEVICE_CONFIG_REL = os.path.join("build", "config", "app-device-config.json")
58
+
59
+ #: The DEVICE entry's `version` in the bench recipe
60
+ #: (alp-sdk `scripts/bench/aen/flash-run.sh`).
61
+ DEVICE_ENTRY_VERSION = "0.5.00"
62
+
63
+ #: Registered issue code (`contract/issue-codes.json`) for the refusal below.
64
+ DEVICE_CONFIG_MISSING_CODE = "flash.device-config-missing"
65
+
66
+ _DEVICE_BASENAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
67
+
68
+
69
+ class DeviceConfigMissingError(FlashPlanError):
70
+ """No device configuration could be found for the DEVICE ATOC entry.
71
+ `code` is the registered issue code `tan flash` reports for it."""
72
+
73
+ code = DEVICE_CONFIG_MISSING_CODE
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class DeviceConfig:
78
+ """The resolved DEVICE-entry source: an absolute `path` (read-only), a human
79
+ `source` naming where it came from, and the `metadata.device` the file itself
80
+ declares (`None` when it does not parse)."""
81
+
82
+ path: str
83
+ source: str
84
+ metadata_device: str | None = None
85
+
86
+ @property
87
+ def name(self) -> str:
88
+ return os.path.basename(self.path)
89
+
90
+
91
+ def read_metadata_device(path: str) -> str | None:
92
+ """`metadata.device` out of a device-config JSON, or `None`."""
93
+ try:
94
+ with open(path, encoding="utf-8") as fh:
95
+ doc = json.load(fh)
96
+ device = doc["metadata"]["device"]
97
+ except (OSError, ValueError, KeyError, TypeError):
98
+ return None
99
+ return device if isinstance(device, str) else None
100
+
101
+
102
+ def family_mismatch(metadata_device: str | None, flash_device: str | None) -> str | None:
103
+ """A warning when the device configuration was written for a different Alif
104
+ family than `flash_device` (the J-Link part profile): the part numbers share
105
+ their first three characters within a family (`AE8...` Ensemble E8). `None`
106
+ when they agree or either is unknown."""
107
+ if not metadata_device or not flash_device:
108
+ return None
109
+ if metadata_device[:3].upper() == flash_device[:3].upper():
110
+ return None
111
+ return (
112
+ f"the device configuration declares device '{metadata_device}' but this slice's J-Link "
113
+ f"part profile is '{flash_device}' -- a different Alif family. The DEVICE entry carries "
114
+ "firewall, clock and boot settings. This is a warning, not a refusal: the metadata may be "
115
+ "stale (the stock SETOOLS file declares an E7 part, yet its compiled blob is byte-identical "
116
+ "to the E8 board's original DEVICE entry). Confirm it is the right file (it is the stock "
117
+ "SETOOLS config unless flash_args.setools_device_config names another)."
118
+ )
119
+
120
+
121
+ def resolve_device_config(
122
+ explicit: str | None, setools_dir: str, *, entry_id: str
123
+ ) -> DeviceConfig:
124
+ """`flash_args.setools_device_config` (already resolved to an absolute path
125
+ by the caller) when set -- refusing, never falling through to the stock
126
+ file, if it does not exist -- else SETOOLS' own stock
127
+ `build/config/app-device-config.json`. Refuses with
128
+ [`DeviceConfigMissingError`] when neither exists: the DEVICE entry carries the
129
+ firewall/clock/boot configuration a table-replacing ATOC write would
130
+ otherwise delete (tan-cli#1322), so dropping it silently is not an option.
131
+ `--no-device-config` never reaches here."""
132
+ if explicit:
133
+ if not os.path.isfile(explicit):
134
+ raise DeviceConfigMissingError(
135
+ f"alif_mram_jlink[{entry_id}]: flash_args.setools_device_config is "
136
+ f"'{explicit}' but no such file exists -- the DEVICE entry's source "
137
+ "is never silently swapped for the stock config. Fix the path, or "
138
+ "pass --no-device-config to sign an app-only ATOC (a table-replacing "
139
+ "write then deletes the resident DEVICE entry)."
140
+ )
141
+ config = DeviceConfig(explicit, "flash_args.setools_device_config")
142
+ else:
143
+ stock = os.path.join(setools_dir, STOCK_DEVICE_CONFIG_REL)
144
+ if not os.path.isfile(stock):
145
+ raise DeviceConfigMissingError(
146
+ f"alif_mram_jlink[{entry_id}]: no device configuration for the DEVICE "
147
+ f"ATOC entry -- flash_args.setools_device_config is unset and SETOOLS "
148
+ f"ships no '{stock}'. The ATOC write REPLACES the whole table, so a "
149
+ "missing DEVICE entry deletes the resident one (firewall regions, HFXO "
150
+ "trims, SE_BOOT_INFO). Point flash_args.setools_device_config at an "
151
+ "Alif device-config JSON, or pass --no-device-config to knowingly sign "
152
+ "an app-only ATOC."
153
+ )
154
+ config = DeviceConfig(stock, "the stock SETOOLS build/config/app-device-config.json")
155
+ config = DeviceConfig(config.path, config.source, read_metadata_device(config.path))
156
+ if _DEVICE_BASENAME_RE.match(config.name) is None:
157
+ raise DeviceConfigMissingError(
158
+ f"alif_mram_jlink[{entry_id}]: the device configuration file name "
159
+ f"'{config.name}' is not a plain file name (letters, digits, '.', '_', '-')"
160
+ )
161
+ return config
162
+
163
+
164
+ def _link_or_copy(src: str, dst: str) -> None:
165
+ try:
166
+ os.symlink(src, dst, target_is_directory=os.path.isdir(src))
167
+ except (OSError, NotImplementedError):
168
+ if os.path.isdir(src):
169
+ shutil.copytree(src, dst, symlinks=True)
170
+ else:
171
+ shutil.copy2(src, dst)
172
+
173
+
174
+ def make_scratch(setools_dir: str, parent: str | None = None) -> str:
175
+ """Create a private overlay of `setools_dir` (see the module docstring) and
176
+ return its root. The shared install is only read. Raises `OSError` on a
177
+ filesystem failure; the half-built directory is removed first."""
178
+ root = tempfile.mkdtemp(prefix="tan-setools-", dir=parent)
179
+ try:
180
+ shared_key = os.path.join(setools_dir, _KEY_DIR)
181
+ key_abs = os.path.abspath(shared_key)
182
+
183
+ def _skip_keys(directory: str, names: list[str]) -> list[str]:
184
+ # The signing keys are never copied: a kill between a copy and its removal would
185
+ # leave them in the temp directory (tan-cli#1485). They are linked in below.
186
+ return [n for n in names if os.path.abspath(os.path.join(directory, n)) == key_abs]
187
+
188
+ for entry in os.scandir(setools_dir):
189
+ if entry.name == "build":
190
+ continue
191
+ dst = os.path.join(root, entry.name)
192
+ if entry.is_dir(follow_symlinks=False) and entry.name not in _LINK_DIRS:
193
+ shutil.copytree(entry.path, dst, symlinks=True, ignore=_skip_keys)
194
+ else:
195
+ _link_or_copy(entry.path, dst)
196
+ scratch_utils = os.path.dirname(os.path.join(root, _KEY_DIR))
197
+ if os.path.isdir(shared_key) and os.path.isdir(scratch_utils):
198
+ _link_or_copy(shared_key, os.path.join(root, _KEY_DIR))
199
+ for sub in ("config", "images", "logs"):
200
+ os.makedirs(os.path.join(root, "build", sub))
201
+ except BaseException:
202
+ shutil.rmtree(root, ignore_errors=True)
203
+ raise
204
+ return root
205
+
206
+
207
+ def cleanup_scratch(path: str | None) -> bool:
208
+ """Remove a scratch overlay made by [`make_scratch`] -- ONLY the overlay:
209
+ symlinks are unlinked, never followed. `True` when nothing is left."""
210
+ if not path:
211
+ return True
212
+ shutil.rmtree(path, ignore_errors=True)
213
+ return not os.path.exists(path)
214
+
215
+
216
+ # ── the ATOC report ─────────────────────────────────────────────────────────
217
+
218
+ _TOC_ENTRY_RE = re.compile(
219
+ r"APP TOC entry for\s+(\S+)\s+obj_address\s+(0x[0-9A-Fa-f]+)"
220
+ )
221
+ _PACKAGE_SIZE_RE = re.compile(r"APP Package total size:\s*(\d+)\s*bytes")
222
+
223
+
224
+ @dataclass(frozen=True)
225
+ class AtocReport:
226
+ """What `app-package-map.txt` says about the package this run built:
227
+ `entries` are `(name, obj_address)` per `APP TOC entry for` line (empty for
228
+ a report that carries none), `package_size` the reported total in bytes."""
229
+
230
+ entries: tuple[tuple[str, str], ...] = ()
231
+ package_size: int | None = None
232
+
233
+
234
+ def parse_atoc_report(text: str) -> AtocReport:
235
+ """Parse the entry list and total size out of an `app-package-map.txt`. The
236
+ scratch report is fresh (one block), so every match belongs to this run."""
237
+ # `app-gen-toc` pads the name field with NULs ("DEVICE\x00\x00"); they are not
238
+ # part of the name (bench round 6).
239
+ entries = tuple(
240
+ (m.group(1).replace("\x00", ""), m.group(2)) for m in _TOC_ENTRY_RE.finditer(text)
241
+ )
242
+ sizes = _PACKAGE_SIZE_RE.findall(text)
243
+ return AtocReport(entries=entries, package_size=int(sizes[-1]) if sizes else None)
tan/core/shapes.py ADDED
@@ -0,0 +1,194 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Two questions four command modules each answered with their own private
3
+ copy: "is this directory an alp-sdk checkout?" and "what shape is this YAML
4
+ value?" (tan-cli#408).
5
+
6
+ Neither is domain logic worth four implementations. `_is_sdk_root` had two
7
+ (`build_cmd.py`, `flash_cmd.py`) and `_yaml_kind` two
8
+ (`diff_cmd.py`, `pinmux_cmd.py`), and the copies had already drifted in TYPE
9
+ -- one took `str`, one took `Path` -- which is exactly how a "same" helper
10
+ stops being the same one.
11
+
12
+ Lives under `tan.core` rather than beside any one caller because
13
+ `tan/commands/*` import each other freely and `build_cmd` already imports
14
+ `sdk_cmd` at module level; a shared helper hosted in either would be a new
15
+ edge in that graph. `tan.core` imports no command module, so this direction
16
+ cannot cycle.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ #: THE marker for an alp-sdk checkout (I-31), as path segments so callers can
25
+ #: join it either way. THE single spelling: `build_cmd` and, since
26
+ #: tan-cli#815, `sdk_cmd` both re-export this one rather than holding a
27
+ #: literal of their own. Relocating `scripts/alp_project.py` is a one-line
28
+ #: change here, which is the whole point -- a second literal meant missing it
29
+ #: silently broke SDK resolution in five commands.
30
+ SDK_MARKER = ("scripts", "alp_project.py")
31
+
32
+
33
+ def is_sdk_root(path: Path | str) -> bool:
34
+ """Whether `path` is an alp-sdk checkout -- port of `util.rs::
35
+ has_loader_script`, and INCAPABLE OF RAISING.
36
+
37
+ Accepts `Path` or `str` because the copies this replaces disagreed:
38
+ `build_cmd`'s took a `Path`, `flash_cmd`'s took a `str`. Callers keep
39
+ whichever they already hold rather than converting at
40
+ every site.
41
+
42
+ tan-cli#408 asks for a deliberate decision on the `except (OSError,
43
+ ValueError)` guard the string-based copy carried, so: **it stays**,
44
+ and the reason is that this is a PRE-FLIGHT guard. Every caller is asking
45
+ "may I use this?" in a command whose whole job is to answer with an
46
+ envelope; a path with an embedded NUL or an unreadable parent must read
47
+ as "not an SDK root", never as a traceback where a refusal belongs.
48
+
49
+ Measured on this tree's interpreter (CPython 3.14.6), both
50
+ `Path.is_file()` and `os.path.isfile()` already return `False` rather
51
+ than raising on an embedded NUL, so the guard catches nothing there
52
+ today. It is kept anyway: `requires-python = ">=3.12"`, the floor was not
53
+ measured here, and a guard that costs one `try` is not worth trading for
54
+ an assumption about an interpreter nobody ran.
55
+ """
56
+ try:
57
+ return os.path.isfile(os.path.join(str(path), *SDK_MARKER))
58
+ except (OSError, ValueError):
59
+ return False
60
+
61
+
62
+ def is_file(path: Path | str) -> bool:
63
+ """`os.path.isfile`, INCAPABLE OF RAISING -- the same pre-flight contract
64
+ `is_sdk_root` above documents at length.
65
+
66
+ Four private copies preceded this one (tan-cli#815): `size_cmd`,
67
+ `image_cmd` and `flash_cmd` each held a byte-identical `str` version, and
68
+ `bootstrap_cmd`'s took a `Path` -- the TYPE drift this module's docstring
69
+ was written about, so the signature is `Path | str` for the same reason
70
+ `is_sdk_root`'s is: callers keep whichever they already hold.
71
+
72
+ Its narrower `except OSError` (the others caught `(OSError, ValueError)`)
73
+ was INERT: `pathlib` catches `ValueError` inside `Path.is_file()`, so
74
+ there was nothing to miss. Measured, not assumed -- an earlier draft of
75
+ tan-cli#815 called that half a behaviour change and it was not. The type
76
+ change does move one input: `Path('')` normalises to `Path('.')`, so the
77
+ old `_is_dir("")` answered `True` where this answers `False`. Unreachable
78
+ from its call sites, every one of which passes a join.
79
+
80
+ All three `str` callers read `build/system-manifest.yaml`, whose values
81
+ are manifest-supplied strings that may carry an embedded NUL or an
82
+ overlong component. Such a path is "not a file", never an exception
83
+ escaping the envelope.
84
+ """
85
+ try:
86
+ return os.path.isfile(path)
87
+ except (OSError, ValueError):
88
+ return False
89
+
90
+
91
+ def is_dir(path: Path | str) -> bool:
92
+ """`os.path.isdir`, INCAPABLE OF RAISING -- `is_file`'s sibling, same
93
+ contract and same two prior copies (`image_cmd` took `str` and caught
94
+ `(OSError, ValueError)`, `bootstrap_cmd` took `Path` and caught `OSError`).
95
+
96
+ NOT the same question as `presets_cmd._is_dir` or
97
+ `examples_cmd._is_dir_no_follow`, which take an `os.DirEntry` and differ
98
+ on whether symlinks are followed. Those are a different predicate with a
99
+ deliberate divergence recorded at each site; they are not copies of this
100
+ and must not be folded into it.
101
+ """
102
+ try:
103
+ return os.path.isdir(path)
104
+ except (OSError, ValueError):
105
+ return False
106
+
107
+
108
+ def matches_glob_suffix(name: str, *suffixes: str) -> bool:
109
+ """True if @name ends in any of @suffixes -- case-SENSITIVELY on POSIX,
110
+ case-INSENSITIVELY on Windows, matching `Path.glob`'s own
111
+ `case_sensitive=None` default (platform casing rules) exactly.
112
+
113
+ THE one spelling of the casing rule every `Path.glob("*.<ext>")` ->
114
+ `os.listdir`/`os.scandir` swap in this tree needs (tan-cli#1127 review
115
+ round 2, then tan-cli#1132). Swapping a glob for a listing plus a plain
116
+ `name.endswith(suffix)` silently NARROWS the match on Windows: a
117
+ `Foo.YAML` board file, or a `prj.CONF` Kconfig fragment, that `glob`
118
+ used to enumerate there becomes invisible, and nothing at the call site
119
+ announces the change. Three call sites share this one rule rather than
120
+ re-deriving it per swap -- `new_som_cmd._is_yaml_board_file`,
121
+ `commands/build/configure_inputs`'s fragment/overlay walk, and
122
+ `model/analyze._resolve_table`'s support-table listing.
123
+
124
+ @suffixes are case-folded here, so a caller may spell them either way;
125
+ what varies by platform is only whether @name is folded to match.
126
+ """
127
+ if os.name == "nt":
128
+ return name.lower().endswith(tuple(s.lower() for s in suffixes))
129
+ return name.endswith(suffixes)
130
+
131
+
132
+ def rejected_sdk_root_message(sdk_root: str, consequence: str) -> str:
133
+ """The `<command>.sdk-root-unresolved` message for a `--sdk-root` the
134
+ loader-marker check REJECTED, naming the value the caller typed.
135
+
136
+ `--sdk-root` is TERMINAL (I-31), so a path without `scripts/alp_project.py`
137
+ resolves to nothing rather than falling through to a lower tier. Five
138
+ commands (`generate`, `model build`, `new-som`, `pinmux`, `validate`) still
139
+ dropped the rejected path on the floor, and four of those (all but
140
+ `pinmux`) answered with the SAME string they use when no flag was given at
141
+ all -- "Use --sdk-root, place the project near an alp-sdk checkout, ...",
142
+ i.e. recommending the flag the caller had just passed -- with the failing
143
+ value nowhere in the envelope and nowhere in the stderr text either.
144
+ `pinmux` used a message of its own with the same gap (tan-cli#497 defect
145
+ 7). A user who typos `--sdk-root ~/alp-sdk-typo` was told to pass
146
+ `--sdk-root`, and could not see WHICH path had been rejected or why, on the
147
+ one surface that knew both.
148
+
149
+ `consequence` is the caller's own "and so this is what you got instead"
150
+ clause, because it differs per command (no pinmux table read, nothing
151
+ generated, nothing validated) and is the half a reader acts on. The
152
+ remediation clause is deliberately NOT carried into it: naming the rejected
153
+ path IS the remediation here.
154
+
155
+ The no-flag branch is deliberately left alone at every call site. There is
156
+ no typed value to name on it, and for `presets` its exact string is
157
+ byte-pinned by the `presets-no-sdk` golden envelope.
158
+
159
+ tan-cli#620 landed a same-named helper in `tan/commands/sdk_cmd.py` with
160
+ a byte-identical body, and the note here asking for the two to be
161
+ collapsed sat unexecuted for four releases (tan-cli#815). They are one
162
+ function now: `sdk_cmd` imports this one. The direction is the only one
163
+ that cannot cycle -- `tan.core` imports no command module (see the module
164
+ docstring).
165
+ """
166
+ marker = "/".join(SDK_MARKER)
167
+ return (
168
+ f'alp-sdk root is unresolved: --sdk-root "{sdk_root}" is not an alp-sdk '
169
+ f"checkout ({marker} not found under it). {consequence}"
170
+ )
171
+
172
+
173
+ def yaml_kind(value: Any) -> str:
174
+ """A short YAML-ish type name for an error message -- not a claim of
175
+ matching serde's exact wording (see `diff_cmd`'s module docstring for the
176
+ scope note this inherits).
177
+
178
+ `bool` is tested BEFORE `int`, and that order is load-bearing rather than
179
+ stylistic: `bool` is a subclass of `int` in Python, so the reverse order
180
+ reports every `true`/`false` in a board.yaml as "a number".
181
+ """
182
+ if value is None:
183
+ return "null"
184
+ if isinstance(value, bool):
185
+ return "a boolean"
186
+ if isinstance(value, (int, float)):
187
+ return "a number"
188
+ if isinstance(value, str):
189
+ return "a string"
190
+ if isinstance(value, list):
191
+ return "a sequence"
192
+ if isinstance(value, dict):
193
+ return "a mapping"
194
+ return type(value).__name__