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,363 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan model zoo` and `tan model add <id>` (tan-cli#1286) -- the runners and
3
+ text renderers, kept out of `model_cmd.py` (module-size budget); that file
4
+ still owns the typer surface, the SDK-root/board.yaml preamble and `finish()`.
5
+
6
+ **`zoo [--sku SKU]`** lists the entries in the bound SDK's
7
+ `metadata/model_zoo/`, optionally only those bench-validated on `SKU` (an empty
8
+ `validated_soms` matches nothing -- `tan.core.model_zoo`). Read-only.
9
+
10
+ **`add <id>`** copies one entry into the project: it fetches the model (a
11
+ bundled starter, or a download verified against the manifest's sha256), writes
12
+ it to `models/<id><ext>` beside `board.yaml`, and appends
13
+ `{name: <id>, source: models/<id><ext>[, compile: ...]}` to `models:` with
14
+ `tan.core.board_yaml_edit` -- a text splice that keeps every other byte of the
15
+ file. Everything is computed before anything is written (so a refused edit
16
+ leaves no stray model file), the model lands first and `board.yaml` last, and
17
+ a failed `board.yaml` write removes the model file it just created. A name
18
+ already in `models:` and an existing destination file are refused, never
19
+ overwritten.
20
+
21
+ Refusals are plain `Issue(...)` returns, not exceptions, so the codes stay
22
+ visible to the issue-code registry gate.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import os
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from tan.commands.build_output import ProjectContext
33
+ from tan.core.atomic_write import atomic_write_bytes
34
+ from tan.core.publish import publish_exclusive as _publish
35
+ from tan.core.board_yaml_edit import BoardEditRefused, append_models_entry
36
+ from tan.core.model_zoo import (
37
+ MIN_SDK_COMMIT,
38
+ ZooEntry,
39
+ StagedModel,
40
+ bundled_chunks,
41
+ ZooFetchError,
42
+ ZooUnavailable,
43
+ entry_as_dict,
44
+ fetch_source,
45
+ filter_by_sku,
46
+ load_zoo,
47
+ )
48
+ from tan.envelope import Issue, Project, SdkInfo
49
+ from tan.exit_codes import ExitCode
50
+
51
+ ZOO_DATA_SCHEMA_VERSION = "1"
52
+ ADD_DATA_SCHEMA_VERSION = "1"
53
+
54
+ #: Directory (beside `board.yaml`) `add` puts the fetched model in.
55
+ MODELS_DIR = "models"
56
+
57
+ Result = tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]
58
+
59
+
60
+ def zoo_empty_data() -> dict[str, Any]:
61
+ return {"schemaVersion": ZOO_DATA_SCHEMA_VERSION, "sku": None, "boardSku": None, "entries": []}
62
+
63
+
64
+ def add_empty_data() -> dict[str, Any]:
65
+ return {"schemaVersion": ADD_DATA_SCHEMA_VERSION, "sku": None, "added": None, "addedEntry": None}
66
+
67
+
68
+ def _load(zoo_dir: Path) -> tuple[list[ZooEntry], list[Issue]] | Issue:
69
+ """The loaded entries plus a warning per skipped manifest, or the
70
+ `model.zoo-unavailable` refusal when the SDK predates the zoo."""
71
+ try:
72
+ entries, problems = load_zoo(zoo_dir)
73
+ except ZooUnavailable:
74
+ return Issue(
75
+ "model.zoo-unavailable",
76
+ "error",
77
+ f"The bound alp-sdk has no metadata/model_zoo/ ({zoo_dir}). The model zoo needs "
78
+ f"alp-sdk commit {MIN_SDK_COMMIT} (alp-sdk#2542) or newer; point --sdk-root at "
79
+ "such a checkout.",
80
+ )
81
+ return entries, [
82
+ Issue("model.zoo-entry-invalid", "warning", f"Skipped zoo entry {name}: {why}.")
83
+ for name, why in problems
84
+ ]
85
+
86
+
87
+ def tolerant_board_sku(board_path: Path) -> str | None:
88
+ """`som.sku` of `board_path`, or `None` for ANY problem (absent, unreadable,
89
+ not YAML): `zoo` is a browse command and never refuses over the board."""
90
+ import yaml # noqa: PLC0415 (declared dependency)
91
+
92
+ try:
93
+ doc = yaml.safe_load(board_path.read_bytes().decode("utf-8"))
94
+ except (OSError, UnicodeDecodeError, yaml.YAMLError, RecursionError):
95
+ return None
96
+ som = doc.get("som") if isinstance(doc, dict) else None
97
+ sku = som.get("sku") if isinstance(som, dict) else None
98
+ return sku if isinstance(sku, str) and sku else None
99
+
100
+
101
+ def run_zoo(*, context: ProjectContext, zoo_dir: Path, sku: str | None) -> Result:
102
+ """Each row carries `runsHere`: whether the entry is bench-validated on the
103
+ effective SKU (`--sku`, else the project board's `som.sku`); `null` when
104
+ neither names one."""
105
+ project, sdk = context.project(), context.sdk
106
+ board_sku = tolerant_board_sku(Path(context.board_yaml))
107
+ data = zoo_empty_data()
108
+ data["sku"] = sku
109
+ data["boardSku"] = board_sku
110
+ loaded = _load(zoo_dir)
111
+ if isinstance(loaded, Issue):
112
+ return project, sdk, data, [loaded], ExitCode.VALIDATION_FAILURE
113
+ entries, issues = loaded
114
+ if sku:
115
+ entries = filter_by_sku(entries, sku)
116
+ effective = sku or board_sku
117
+ rows = []
118
+ for e in entries:
119
+ row = entry_as_dict(e)
120
+ row["runsHere"] = None if effective is None else effective in e.validated_soms
121
+ rows.append(row)
122
+ data["entries"] = rows
123
+ return project, sdk, data, issues, ExitCode.SUCCESS
124
+
125
+
126
+ def _is_orphan_of(entry: ZooEntry, zoo_dir: Path, dest: Path) -> StagedModel | None:
127
+ """`dest` as a `StagedModel` when it is a plain file already holding exactly
128
+ this entry's bytes -- what a run interrupted between publishing the model
129
+ and writing `board.yaml` leaves behind -- so a re-run can finish the job
130
+ instead of refusing. A symlink, or any other content, is never adopted."""
131
+ if dest.is_symlink() or not dest.is_file():
132
+ return None
133
+ try:
134
+ got = hashlib.sha256()
135
+ with dest.open("rb") as handle:
136
+ while chunk := handle.read(1 << 20):
137
+ got.update(chunk)
138
+ want = entry.source.get("sha256")
139
+ if want is None:
140
+ bundled = hashlib.sha256()
141
+ for chunk in bundled_chunks(zoo_dir, entry.source["bundled"]):
142
+ bundled.update(chunk)
143
+ want = bundled.hexdigest()
144
+ return StagedModel(dest, got.hexdigest(), dest.stat().st_size) if got.hexdigest() == want else None
145
+ except (OSError, ZooFetchError):
146
+ return None
147
+
148
+
149
+ def _precheck(
150
+ model_id: str,
151
+ entry: ZooEntry | None,
152
+ entries: list[ZooEntry],
153
+ existing_names: set[str],
154
+ dest: Path,
155
+ adopt: bool = False,
156
+ ) -> Issue | None:
157
+ """The refusal `add` must make before fetching anything, or `None`."""
158
+ if entry is None:
159
+ known = ", ".join(e.id for e in entries) or "(none)"
160
+ return Issue(
161
+ "model.zoo-entry-not-found",
162
+ "error",
163
+ f"No model-zoo entry named {model_id}. Available: {known}.",
164
+ )
165
+ if entry.id in existing_names:
166
+ return Issue(
167
+ "model.add-name-exists",
168
+ "error",
169
+ f"board.yaml already declares a model named {entry.id}; nothing was changed.",
170
+ )
171
+ if os.path.lexists(dest) and not adopt:
172
+ return Issue(
173
+ "model.add-destination-exists",
174
+ "error",
175
+ f"{dest} already exists; move it aside first. Nothing was changed.",
176
+ )
177
+ return _unsafe_models_dir(dest.parent)
178
+
179
+
180
+ def _unsafe_models_dir(models_dir: Path) -> Issue | None:
181
+ """Refuse a `models/` that is a symlink or resolves outside the project
182
+ (a write through it would land elsewhere)."""
183
+ if not os.path.lexists(models_dir):
184
+ return None
185
+ board_dir = models_dir.parent.resolve()
186
+ if models_dir.is_symlink() or not models_dir.is_dir() or models_dir.resolve().parent != board_dir:
187
+ return Issue(
188
+ "model.add-destination-unsafe",
189
+ "error",
190
+ f"{models_dir} is not a plain directory inside the project; nothing was changed.",
191
+ )
192
+ return None
193
+
194
+
195
+ def _sku_warning(entry: ZooEntry, sku: str | None) -> Issue | None:
196
+ """The hardware-claim caveat for a `kind: model` entry not validated on `sku`."""
197
+ if entry.kind != "model" or not sku or sku in entry.validated_soms:
198
+ return None
199
+ return Issue(
200
+ "model.add-sku-not-validated",
201
+ "warning",
202
+ f"{entry.id} has not been bench-validated on {sku} "
203
+ f"(validated: {', '.join(entry.validated_soms) or 'none'}).",
204
+ )
205
+
206
+
207
+ def _added_row(entry: ZooEntry, dest: Path, rel_source: str, staged: StagedModel) -> dict[str, Any]:
208
+ return {
209
+ "id": entry.id,
210
+ "name": entry.id,
211
+ "source": rel_source,
212
+ "path": dest.as_posix(),
213
+ "sha256": staged.sha256,
214
+ "bytes": staged.size,
215
+ "kind": entry.kind,
216
+ "license": entry.license,
217
+ }
218
+
219
+
220
+ def _plan_edit(entry: ZooEntry, board_path: Path) -> tuple[str, str] | Issue:
221
+ """`(new board.yaml text, relative source)` -- computed before any fetch so
222
+ a refused edit costs nothing."""
223
+ rel_source = f"{MODELS_DIR}/{entry.id}{entry.suffix}"
224
+ new_entry: dict[str, Any] = {"name": entry.id, "source": rel_source}
225
+ if entry.compile:
226
+ new_entry["compile"] = entry.compile
227
+ try:
228
+ return append_models_entry(board_path.read_bytes().decode("utf-8"), new_entry), rel_source
229
+ except (OSError, UnicodeDecodeError) as err:
230
+ return Issue("model.board-yaml-edit-failed", "error", f"{board_path}: {err}")
231
+ except BoardEditRefused as err:
232
+ return Issue(
233
+ "model.board-yaml-edit-refused",
234
+ "error",
235
+ f"Not editing {board_path}: {err}. Nothing was changed; add this entry by hand: "
236
+ f"name: {entry.id}, source: {rel_source}.",
237
+ )
238
+
239
+
240
+ def _commit_add(
241
+ entry: ZooEntry,
242
+ zoo_dir: Path,
243
+ dest: Path,
244
+ board_path: Path,
245
+ new_text: str,
246
+ reader: Any,
247
+ adopted: StagedModel | None = None,
248
+ ) -> StagedModel | Issue:
249
+ """Stage (stream + verify) into a temp file beside `dest`, publish it
250
+ without overwriting, then write `board.yaml` last. Any failure removes
251
+ everything this run created, including a `models/` it made."""
252
+ models_dir = dest.parent
253
+ created_dir = not os.path.lexists(models_dir)
254
+ staged: StagedModel | None = None
255
+ published = False
256
+ try:
257
+ if adopted is not None:
258
+ atomic_write_bytes(str(board_path), new_text.encode("utf-8"))
259
+ return adopted
260
+ if created_dir:
261
+ models_dir.mkdir()
262
+ staged = fetch_source(entry, zoo_dir, tmp_dir=models_dir, **({"reader": reader} if reader else {}))
263
+ _publish(staged.path, dest)
264
+ published = True
265
+ try:
266
+ staged.path.unlink(missing_ok=True)
267
+ except OSError:
268
+ pass # the model is published; a leftover temp name is harmless
269
+ atomic_write_bytes(str(board_path), new_text.encode("utf-8"))
270
+ return staged
271
+ except ZooFetchError as err:
272
+ if err.mismatch:
273
+ failure = Issue("model.zoo-integrity-failed", "error", str(err))
274
+ else:
275
+ failure = Issue("model.zoo-fetch-failed", "error", str(err))
276
+ except FileExistsError:
277
+ failure = Issue("model.add-destination-exists", "error", f"{dest} already exists; nothing was changed.")
278
+ except OSError as err:
279
+ if published or adopted is not None:
280
+ failure = Issue("model.board-yaml-edit-failed", "error", f"{board_path}: {err}")
281
+ else:
282
+ failure = Issue("model.add-write-failed", "error", f"{dest}: {err}")
283
+ if published:
284
+ dest.unlink(missing_ok=True)
285
+ if staged is not None:
286
+ staged.path.unlink(missing_ok=True)
287
+ if created_dir:
288
+ try:
289
+ models_dir.rmdir()
290
+ except OSError:
291
+ pass
292
+ return failure
293
+
294
+
295
+ def run_add(
296
+ *,
297
+ context: ProjectContext,
298
+ zoo_dir: Path,
299
+ model_id: str | None,
300
+ sku: str | None,
301
+ existing_names: set[str],
302
+ reader: Any = None,
303
+ ) -> Result:
304
+ project, sdk = context.project(), context.sdk
305
+ data = add_empty_data()
306
+ data["sku"] = sku
307
+
308
+ def refuse(issue: Issue, exit_code: ExitCode, issues: list[Issue]) -> Result:
309
+ return project, sdk, data, [*issues, issue], exit_code
310
+
311
+ if not model_id:
312
+ missing = Issue(
313
+ "model.add-id-missing",
314
+ "error",
315
+ "`tan model add` needs a zoo entry id; list them with `tan model zoo`.",
316
+ )
317
+ return refuse(missing, ExitCode.VALIDATION_FAILURE, [])
318
+ loaded = _load(zoo_dir)
319
+ if isinstance(loaded, Issue):
320
+ return project, sdk, data, [loaded], ExitCode.VALIDATION_FAILURE
321
+ entries, issues = loaded
322
+ entry = next((e for e in entries if e.id == model_id), None)
323
+ board_path = Path(context.board_yaml)
324
+ dest = board_path.parent / MODELS_DIR / f"{model_id}{entry.suffix if entry else ''}"
325
+ adopted = _is_orphan_of(entry, zoo_dir, dest) if entry and entry.id not in existing_names else None
326
+ refusal = _precheck(model_id, entry, entries, existing_names, dest, adopt=adopted is not None)
327
+ if refusal is not None:
328
+ return refuse(refusal, ExitCode.VALIDATION_FAILURE, issues)
329
+ assert entry is not None
330
+ issues.extend(w for w in [_sku_warning(entry, sku)] if w is not None)
331
+
332
+ plan = _plan_edit(entry, board_path)
333
+ if isinstance(plan, Issue):
334
+ refused = plan.code == "model.board-yaml-edit-refused"
335
+ return refuse(plan, ExitCode.VALIDATION_FAILURE if refused else ExitCode.RUNTIME_FAILURE, issues)
336
+ new_text, rel_source = plan
337
+ staged = _commit_add(entry, zoo_dir, dest, board_path, new_text, reader, adopted)
338
+ if isinstance(staged, Issue):
339
+ exit_code = ExitCode.VALIDATION_FAILURE if staged.code == "model.add-destination-exists" else ExitCode.RUNTIME_FAILURE
340
+ return refuse(staged, exit_code, issues)
341
+ data["added"] = entry.id
342
+ data["addedEntry"] = _added_row(entry, dest, rel_source, staged)
343
+ return project, sdk, data, issues, ExitCode.SUCCESS
344
+
345
+
346
+ def render_zoo_text(data: dict) -> list[str]:
347
+ lines = []
348
+ for e in data.get("entries", []):
349
+ soms = ",".join(e["validatedSoms"]) or "none"
350
+ here = {True: " (runs on this board)", False: "", None: ""}[e.get("runsHere")]
351
+ lines.append(f"{e['id']} [{e['kind']}] {e['task']} {e['license']} validated: {soms}{here}")
352
+ lines.append(f" {e['description']}")
353
+ return lines
354
+
355
+
356
+ def render_add_text(data: dict) -> list[str]:
357
+ a = data.get("addedEntry")
358
+ if not a:
359
+ return []
360
+ return [
361
+ f"added {a['name']}: {a['source']} ({a['bytes']} bytes, sha256 {a['sha256']})",
362
+ f"board.yaml: appended `{a['name']}` to models:",
363
+ ]
@@ -0,0 +1,125 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan monitor --capture` input and baud-switch options (tan-cli#1451).
3
+
4
+ Validation of `--send`/`--send-after`/`--send-on`/`--send-gap`/`--reopen-at`/
5
+ `--on` into a `serial_actions.ActionSpec`, and the glue that gives the capture
6
+ loop a `CaptureActions` bound to the open port (reopening through
7
+ `monitor_session.open_port`, so it behaves the same over a local tty and over
8
+ `rfc2217://`). The logic itself is `tan.core.serial_actions`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from tan.commands.monitor_cmd import DATA_SCHEMA_VERSION, MonitorError
16
+ from tan.core import serial_actions, uboot_breakin
17
+ from tan.exit_codes import ExitCode
18
+
19
+
20
+ def _bad(message: str) -> MonitorError:
21
+ return MonitorError(
22
+ "monitor.capture-bad-option",
23
+ message,
24
+ ExitCode.VALIDATION_FAILURE,
25
+ {"schemaVersion": DATA_SCHEMA_VERSION},
26
+ )
27
+
28
+
29
+ def _regex(text: str, flag: str) -> re.Pattern[str]:
30
+ if not text:
31
+ raise ValueError(f"{flag} must not be empty")
32
+ try:
33
+ return re.compile(text)
34
+ except re.error as err:
35
+ raise ValueError(f"{flag} is not a valid regex: {err}") from err
36
+
37
+
38
+ def _check(capture, sends, send_after, send_on, send_gap, reopen_at, on) -> None:
39
+ if not capture:
40
+ raise ValueError("--send/--send-after/--send-on/--send-gap/--reopen-at/--on require --capture")
41
+ if not sends and any(v is not None for v in (send_after, send_on, send_gap)):
42
+ raise ValueError("--send-after/--send-on/--send-gap require --send")
43
+ if send_after is not None and send_on is not None:
44
+ raise ValueError("--send-after and --send-on are mutually exclusive")
45
+ if (reopen_at is None) != (on is None):
46
+ raise ValueError("--reopen-at and --on must be given together")
47
+ if send_after is not None and send_after < 0:
48
+ raise ValueError("--send-after must not be negative")
49
+ if send_gap is not None and send_gap < 0:
50
+ raise ValueError("--send-gap must not be negative")
51
+ if reopen_at is not None and reopen_at <= 0:
52
+ raise ValueError("--reopen-at must be greater than 0")
53
+
54
+
55
+ def action_opts(
56
+ capture: bool,
57
+ send: list[str] | None,
58
+ send_after: float | None,
59
+ send_on: str | None,
60
+ send_gap: float | None,
61
+ reopen_at: int | None,
62
+ on: str | None,
63
+ ) -> serial_actions.ActionSpec | None:
64
+ """Validate the input / baud-switch options. `None` when none was given.
65
+
66
+ All of them need `--capture`; `--send-after`/`--send-on`/`--send-gap` need
67
+ `--send` (and the two triggers exclude each other); `--reopen-at` and
68
+ `--on` need each other. Without a trigger the `--send` items go out at the
69
+ start of the capture."""
70
+ sends = list(send or [])
71
+ values = (send_after, send_on, send_gap, reopen_at, on)
72
+ if not sends and all(v is None for v in values):
73
+ return None
74
+ try:
75
+ _check(capture, sends, send_after, send_on, send_gap, reopen_at, on)
76
+ return serial_actions.ActionSpec(
77
+ sends=tuple(uboot_breakin.parse_escaped(item, "--send") for item in sends),
78
+ send_after_s=send_after,
79
+ send_on=_regex(send_on, "--send-on") if send_on is not None else None,
80
+ send_gap_s=serial_actions.DEFAULT_SEND_GAP_S if send_gap is None else send_gap,
81
+ reopen_baud=reopen_at,
82
+ reopen_on=_regex(on, "--on") if on is not None else None,
83
+ )
84
+ except ValueError as err:
85
+ raise _bad(str(err)) from err
86
+
87
+
88
+ def build(spec: serial_actions.ActionSpec, ser, port: str):
89
+ """The `CaptureActions` for the open `ser`: sends honour the rfc2217 write
90
+ guard; a reopen goes through `monitor_session.open_port`."""
91
+ from tan.commands import monitor_session # noqa: PLC0415 (it imports this module's peers)
92
+
93
+ def write(current, data: bytes) -> None:
94
+ target = (
95
+ monitor_session._GuardedWrites(current)
96
+ if getattr(current, "_tan_guard_writes", False)
97
+ else current
98
+ )
99
+ target.write(data)
100
+
101
+ def reopen(baud: int):
102
+ return monitor_session.open_port(port, baud, capture=True)
103
+
104
+ return serial_actions.CaptureActions(spec, ser, reopen, write=write)
105
+
106
+
107
+ def action_error(err: serial_actions.ActionError, port: str, data: dict) -> MonitorError:
108
+ code = "monitor.capture-send-failed" if err.kind == "send" else "monitor.capture-reopen-failed"
109
+ return MonitorError(
110
+ code,
111
+ f"{err} (port '{port}')",
112
+ ExitCode.RUNTIME_FAILURE,
113
+ data,
114
+ )
115
+
116
+
117
+ def report(actions: serial_actions.CaptureActions) -> dict:
118
+ """`data.capture.actions`: what fired, and what is still pending."""
119
+ spec = actions.spec
120
+ reopened = any(e["action"] == "reopen" for e in actions.events)
121
+ return {
122
+ "events": actions.events,
123
+ "sendsPending": actions.sends_pending,
124
+ "reopenPending": spec.reopen_baud is not None and not reopened,
125
+ }