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/model/build.py ADDED
@@ -0,0 +1,304 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Build driver: SKU + source model -> .alpmodel package (compile-what's-available).
3
+
4
+ Resolves the SoM's targets, runs each *available* compiler adapter, and assembles
5
+ the package. A backend whose adapter is missing, or whose tool is not installed,
6
+ is recorded as a `coverage` skip; a source format no adapter accepts is
7
+ `incompatible`. If *no* blob is produced the build fails loudly -- an .alpmodel
8
+ with zero runnable blobs is broken.
9
+
10
+ PER-TARGET, NOT PER-PACKAGE (tan-cli#789 review BLOCKER 1). A SKU resolves to
11
+ several targets and they succeed or fail independently, so two of them are
12
+ scoped to ONE target's coverage entry instead of killing the package:
13
+
14
+ * `VelaFootprintRefused` -- vela compiled cleanly and tan refused the
15
+ footprint it reported (see `tan.model.adapters.ethos_u`). Measured, real
16
+ `ethos-u-vela` 5.1.0 over `tests/fixtures/models/tiny_int8.tflite`: with no
17
+ guard here, `E1M-AEN801`'s `ethos-u85-256` refusal aborted the whole build
18
+ -- `ethos-u55-256`, `ethos-u55-128` and `cpu` all compiled fine and NONE of
19
+ them shipped, and the same held for `E1M-AEN401` and `E1M-AEN601`.
20
+ * A ZERO-placement accelerator compile -- see `_placed_nothing_on_accelerator`.
21
+
22
+ Every OTHER `adapter.compile()` exception still fails the whole build loudly.
23
+ That is deliberate: a toolchain that crashed, timed out or produced no artifact
24
+ is a broken build, not a coverage line (`tests/model/test_build.py` states the
25
+ same contract from the other side).
26
+
27
+ If EVERY target ends up skipped there is no blob, and the existing zero-blob
28
+ guard at the bottom raises with the full coverage detail -- each refusal named.
29
+ A package with no runnable blob is never written: that is worse than an error,
30
+ because the customer only finds out on the device."""
31
+ from __future__ import annotations
32
+ import hashlib
33
+ import re
34
+ from pathlib import Path
35
+
36
+ from ..core.atomic_write import atomic_write_bytes
37
+ from .adapters import Blob, CompilerAdapter
38
+ from .adapters.cpu import CpuAdapter
39
+ from .adapters.ethos_u import VelaAdapter, VelaFootprintRefused
40
+ from .adapters.drpai import DrpaiAdapter
41
+ from .adapters.deepx import DeepxAdapter
42
+ from .adapters.executorch import ExecutorchAdapter
43
+ from .manifest import Manifest, Target, Coverage
44
+ from .package import write_package
45
+ from .sram_fit import SramFit, evaluate_sram_fit
46
+ from .targets import TargetSpec, resolve_targets
47
+ from .tensorio import extract_io
48
+
49
+ # Default adapter registry. Each is detect-and-skip (is_available() False when
50
+ # its tool is absent); vela (ethos_u) skips on hosts without the ethos-u-vela package.
51
+ # A backend may carry more than one adapter (cpu: CpuAdapter for .tflite,
52
+ # ExecutorchAdapter for .pte) -- see the by_backend grouping below, which
53
+ # selects among a backend's adapters by accepts(src_fmt), not by last-one-wins.
54
+ _ADAPTERS: list[CompilerAdapter] = [
55
+ CpuAdapter(), VelaAdapter(), DrpaiAdapter(), DeepxAdapter(), ExecutorchAdapter(),
56
+ ]
57
+
58
+ # #1125: mirrors metadata/schemas/board.schema.json's `models[].name` pattern.
59
+ # build_model() is called directly by non-CLI callers (tests, future tooling),
60
+ # not just alp_cli.model's schema-validated path -- an allowlist here is the
61
+ # root-cause guard, independent of whether the caller validated board.yaml.
62
+ _NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")
63
+
64
+
65
+ def _src_format(source: Path) -> str:
66
+ return source.suffix.lstrip(".").lower() # "tflite" | "onnx"
67
+
68
+
69
+ def _placed_nothing_on_accelerator(backend: str, blob: Blob) -> bool:
70
+ """True for an accelerator compile whose own compiler reports it placed
71
+ ZERO operators on the accelerator.
72
+
73
+ Such a target is not an accelerator target. It carries `arena=0` and
74
+ `requires={'sram_kib': 0}` -- the exact "fits any envelope" shape
75
+ alp-sdk's on-device selector reads unconditionally
76
+ (`return e->arena_sram_kib == 0u || t->req_sram_kib <= e->arena_sram_kib;`,
77
+ src/backends/inference/alp_model_select.c) -- so the board would happily
78
+ select an `ethos_u` blob that runs every operator on the CPU anyway, while
79
+ the honest `cpu` target sits beside it in the same package. Measured with
80
+ real `ethos-u-vela` 5.1.0 on `tests/fixtures/models/float32_fc.tflite`:
81
+ `E1M-AEN801` shipped THREE such `ethos_u` targets (`ethos-u85-256`,
82
+ `ethos-u55-256`, `ethos-u55-128`). Dropped to a coverage skip instead -- legibly absent,
83
+ with the reason, rather than silently present as a zero (tan-cli#789
84
+ review MINOR 7).
85
+
86
+ `cpu` is exempt: a CPU target legitimately places nothing on an
87
+ accelerator, and its 0 arena is a real figure, not a missing one.
88
+ `npu_op_count is None` (every adapter but vela today, and vela itself when
89
+ its placement summary could not be parsed) is exempt too -- unknown is not
90
+ zero, and inventing a verdict from an unreadable summary is exactly what
91
+ `_parse_vela_placement` returning None exists to prevent."""
92
+ return backend != "cpu" and blob.npu_op_count == 0
93
+
94
+
95
+ class SramNoFitRefused(Exception):
96
+ """A certain arena or SRAM0-residency NO-FIT under `Sram_Only`
97
+ (tan-cli#1288, `tan.model.sram_fit`) on EVERY ethos_u target the build
98
+ reached -- refuses the WHOLE per-model build (a single target's no-fit
99
+ is a coverage skip, like `VelaFootprintRefused`; tan-cli#1486): every
100
+ ethos_u variant this SKU would ship describes a placement that cannot
101
+ execute on it (`src/backends/inference/ethos_u_aen.cpp` pins every NPU
102
+ access to the SRAM AXI port under `Sram_Only`), so no partial package is
103
+ written -- `_run_build` (`tan.commands.model_cmd`) catches this ahead of
104
+ the generic per-model except-clause and reports `model.sram-no-fit` at
105
+ `ExitCode.VALIDATION_FAILURE`, not the generic `model.build-failed`."""
106
+
107
+
108
+ def _sram_no_fit_message(spec: TargetSpec, fit: SramFit) -> str:
109
+ parts = []
110
+ if fit.arena.verdict == "no-fit":
111
+ parts.append(f"arena needs {fit.arena.needed_kib} KiB, only "
112
+ f"{fit.arena.limit_kib} KiB available "
113
+ f"(cores.<id>.inference.default_arena_kib)")
114
+ if fit.sram0.verdict == "no-fit":
115
+ # NOT `fit.arena.limit_kib` (tan-cli#1288 review round 2, finding 4):
116
+ # the arena figure `sram0` actually summed against is whichever
117
+ # `resolve_arena_budget` shape decided it -- the SAME core's budget
118
+ # only when `arena.kind == "single"`; `sram0` uses the SMALLEST of a
119
+ # `"range"`'s several candidates (`arena.limit_kib` there is the
120
+ # LARGEST, a different number), and a `MIN_ARENA_KIB` lower bound
121
+ # when arena itself is unresolved (`arena.limit_kib` is `None`
122
+ # there -- this would have literally printed "arena None KiB").
123
+ # `sram0.needed_kib` is ALWAYS `blob_kib + <that figure>` by
124
+ # construction (`tan.model.sram_fit._evaluate_sram0_*`), so deriving
125
+ # it back out this way is correct regardless of which shape produced
126
+ # the verdict -- reading `fit.arena.limit_kib` instead is not.
127
+ arena_used = fit.sram0.needed_kib - fit.blob_kib
128
+ parts.append(f"SRAM0 needs {fit.sram0.needed_kib} KiB (blob "
129
+ f"{fit.blob_kib} KiB + arena {arena_used} KiB), "
130
+ f"only {fit.sram0.limit_kib} KiB available")
131
+ detail = "; ".join(parts)
132
+ return (f"{spec.accel_config or spec.backend}: {detail} -- refusing to ship a "
133
+ f"Sram_Only target whose weights/arena cannot be SRAM0-resident "
134
+ f"(src/backends/inference/ethos_u_aen.cpp pins every NPU access "
135
+ f"to the SRAM AXI port). No package written for this model.")
136
+
137
+
138
+ def _no_placement_reason(spec: TargetSpec, blob: Blob) -> str:
139
+ """The coverage reason for a dropped zero-placement accelerator target --
140
+ the compiler's own verdict, the target it applies to, and what the package
141
+ still offers instead. One line, same as every other coverage reason."""
142
+ total = (blob.npu_op_count or 0) + (blob.cpu_op_count or 0)
143
+ tool = blob.compiler_version or spec.backend
144
+ # "accepts ... against ANY arena size", not the retired "fits" vocabulary --
145
+ # same reason `ethos_u._refuse_zero_sram_footprint` words it that way.
146
+ return (f"{tool} placed 0 of {total} operators on {spec.accel_config or spec.backend}; "
147
+ f"an accelerator target with no accelerator placement would ship arena 0 / "
148
+ f"sram_kib 0, which alp-sdk's on-device selector accepts against ANY arena "
149
+ f"size (src/backends/inference/alp_model_select.c). The cpu target runs this "
150
+ f"model.")
151
+
152
+
153
+ def build_model(*, sku: str, name: str, source: Path, out_dir: Path,
154
+ metadata_root: Path,
155
+ adapters: list[CompilerAdapter] | None = None,
156
+ compile_opts: dict[str, dict] | None = None,
157
+ board_doc: dict | None = None) -> Path:
158
+ if not _NAME_RE.fullmatch(name):
159
+ raise ValueError(f"invalid model name {name!r}: must match {_NAME_RE.pattern!r}")
160
+ registry = list(_ADAPTERS if adapters is None else adapters)
161
+ by_backend: dict[str, list[CompilerAdapter]] = {}
162
+ for a in registry:
163
+ by_backend.setdefault(a.backend, []).append(a)
164
+ specs = resolve_targets(sku, metadata_root=metadata_root)
165
+ src_fmt = _src_format(source)
166
+ opts_by_backend = compile_opts or {}
167
+
168
+ out_dir.mkdir(parents=True, exist_ok=True)
169
+ targets: list[Target] = []
170
+ coverage: list[Coverage] = []
171
+ blobs: list[bytes] = []
172
+ no_fit_msgs: list[str] = []
173
+ ethos_u_fit = 0
174
+ for spec in specs:
175
+ candidates = by_backend.get(spec.backend, [])
176
+ if not candidates:
177
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped",
178
+ f"no compiler adapter for {spec.backend}"))
179
+ continue
180
+ if len(candidates) > 1:
181
+ # A backend with more than one adapter (cpu: CpuAdapter + ExecutorchAdapter)
182
+ # is disambiguated by source format up front -- accepts() decides identity,
183
+ # not registration order. A single-adapter backend keeps the original order
184
+ # below (requires_compile_opts / is_available reported before "incompatible"),
185
+ # so an unrelated format mismatch doesn't mask a "no compile config" skip.
186
+ adapter = next((a for a in candidates if a.accepts(src_fmt)), None)
187
+ if adapter is None:
188
+ coverage.append(Coverage(spec.backend, spec.accel_config, "incompatible",
189
+ f"{spec.backend} does not accept .{src_fmt}"))
190
+ continue
191
+ else:
192
+ adapter = candidates[0]
193
+ backend_opts = opts_by_backend.get(spec.backend)
194
+ if adapter.requires_compile_opts and not backend_opts:
195
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped",
196
+ f"no compile config for {spec.backend} "
197
+ f"(add models[].compile.{spec.backend} to board.yaml)"))
198
+ continue
199
+ if not adapter.is_available():
200
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped",
201
+ f"{spec.backend} compiler not installed"))
202
+ continue
203
+ if not adapter.accepts(src_fmt):
204
+ coverage.append(Coverage(spec.backend, spec.accel_config, "incompatible",
205
+ f"{spec.backend} does not accept .{src_fmt}"))
206
+ continue
207
+ try:
208
+ # `spec.vela_vendor_config_filename` / `spec.soc_declares_dram` are
209
+ # passed for the adapter's DIAGNOSTICS, not its output -- see
210
+ # `CompilerAdapter.compile`. They are what let a vela footprint
211
+ # refusal name the proprietary profile file a part's own SoC spec
212
+ # declares -- and stay silent for one that declares none
213
+ # (tan-cli#789 review (g)) -- and say that a DRAM
214
+ # placement went to memory this part declares no interface to.
215
+ #
216
+ # `spec.vela_memory_mode` / `.vela_system_config` /
217
+ # `.vela_vendor_system_config` are the opposite: the silicon's own
218
+ # vela memory profile (SoC spec `npu_toolchain.vela`, alp-sdk
219
+ # #1470), and it DOES change the artifact -- that is the point. It
220
+ # is what makes an `ethos-u85-256` target ship at all: measured,
221
+ # real `ethos-u-vela` 5.1.0 over `tests/fixtures/models/
222
+ # tiny_int8.tflite` reported `sram_memory_used = 0.0` for
223
+ # E1M-AEN801 without it (refused, a coverage row) and 0.03125 KiB
224
+ # with `--memory-mode Sram_Only` (a real target). Passed on the
225
+ # same call as everything else so a target can never be compiled
226
+ # with one target's accel-config and another's memory model; every
227
+ # vela field is None for a non-ethos_u backend by construction
228
+ # (`targets._soc_targets`).
229
+ blob = adapter.compile(source, accel_config=spec.accel_config,
230
+ out_dir=out_dir, opts=backend_opts,
231
+ vela_memory_mode=spec.vela_memory_mode,
232
+ vela_system_config=spec.vela_system_config,
233
+ vela_vendor_system_config=spec.vela_vendor_system_config,
234
+ vela_vendor_config_filename=spec.vela_vendor_config_filename,
235
+ soc_declares_dram=spec.soc_declares_dram)
236
+ except VelaFootprintRefused as err:
237
+ # ONE target's refusal, not the package's. See the module docstring.
238
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped", str(err)))
239
+ continue
240
+ if spec.backend == "ethos_u":
241
+ # tan-cli#1288: a certain arena/SRAM0 no-fit under `Sram_Only`
242
+ # skips THIS target as a coverage row; the model is refused
243
+ # (see `SramNoFitRefused`) only when no ethos_u target ships
244
+ # (tan-cli#1486) -- a no-op for every other memory mode
245
+ # (`evaluate_sram_fit` itself gates on it).
246
+ fit = evaluate_sram_fit(
247
+ memory_mode=spec.vela_memory_mode, req_sram_kib=blob.req_sram_kib,
248
+ blob_len_bytes=len(blob.payload), board_doc=board_doc,
249
+ paired_core=spec.paired_core, sku=sku, metadata_root=metadata_root,
250
+ )
251
+ if fit.no_fit:
252
+ # ONE target's no-fit skips that target only; the model is
253
+ # refused as a whole (below) only when NO ethos_u target
254
+ # survives (tan-cli#1486).
255
+ msg = _sram_no_fit_message(spec, fit)
256
+ no_fit_msgs.append(msg)
257
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped", msg))
258
+ continue
259
+ if _placed_nothing_on_accelerator(spec.backend, blob):
260
+ coverage.append(Coverage(spec.backend, spec.accel_config, "skipped",
261
+ _no_placement_reason(spec, blob)))
262
+ continue
263
+ if spec.backend == "ethos_u":
264
+ ethos_u_fit += 1 # fits AND ships; a fit that places nothing does not count
265
+ targets.append(Target(
266
+ backend=spec.backend, silicon_ref=spec.silicon_ref,
267
+ blob_format=blob.format, accel_config=spec.accel_config,
268
+ arena=blob.arena_bytes,
269
+ requires={"sram_kib": blob.req_sram_kib, "op_features": []},
270
+ blob=len(blobs), compiler_version=blob.compiler_version,
271
+ # The compiler's unresolved caveats travel WITH the blob into the
272
+ # package. Dropping them here was the gap: `tan model check
273
+ # --exact` reported them and shipped nothing, `tan model build`
274
+ # shipped bytes and reported nothing, so a blob compiled against
275
+ # vela's BUILT-IN default memory model could reach a board with
276
+ # the package silent about it -- while `arena`/`sram_kib` right
277
+ # beside it, figures that describe THAT default memory model, are
278
+ # what alp-sdk's on-device selector consumes
279
+ # (src/backends/inference/alp_model_select.c).
280
+ caveats=list(blob.caveats)))
281
+ blobs.append(blob.payload)
282
+
283
+ if no_fit_msgs and not ethos_u_fit:
284
+ raise SramNoFitRefused("; ".join(no_fit_msgs))
285
+
286
+ if not blobs:
287
+ detail = "; ".join(f"{c.backend}:{c.status} ({c.reason})" for c in coverage)
288
+ raise ValueError(f"no blob compiled for model '{name}' (.{src_fmt}); coverage: {detail}")
289
+
290
+ src_bytes = source.read_bytes() # read once: shared by the sha + tensor-I/O
291
+ inputs, outputs = extract_io(source, raw=src_bytes)
292
+ mft = Manifest(name=name, src_sha=hashlib.sha256(src_bytes).digest(),
293
+ inputs=inputs, outputs=outputs,
294
+ targets=targets, coverage=coverage)
295
+ out_path = out_dir / f"{name}.alpmodel"
296
+ # Belt-and-suspenders: the name allowlist above already makes escape
297
+ # impossible for a bare filename, but fail closed on containment too --
298
+ # a resolved-path check catches this even if the join expression above
299
+ # ever grows a second path segment.
300
+ resolved_out_dir = out_dir.resolve()
301
+ if not out_path.resolve().is_relative_to(resolved_out_dir):
302
+ raise ValueError(f"refusing to write outside out_dir: {out_path}")
303
+ atomic_write_bytes(str(out_path), write_package(mft, blobs))
304
+ return out_path