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/lazy_typer.py ADDED
@@ -0,0 +1,185 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """A Typer app / Click group that builds each subcommand on first use.
3
+
4
+ ``tan.cli`` used to import all ~31 command modules at module scope, so every
5
+ invocation -- ``tan --version`` included -- paid for the heaviest of them
6
+ (tan-cli#1505). Here the Click tree is assembled from ``COMMAND_TABLE`` and a
7
+ command's module is imported only when Click asks for that command by name
8
+ (dispatch, ``tan <cmd> --help``) or when something walks the whole tree
9
+ (``tan --help``, a ``--format`` domain check): ``LazyCommands`` is a mapping, so
10
+ every ``group.commands[...]`` / ``sorted(group.commands)`` / ``.items()`` caller
11
+ keeps working unchanged; only ``.items()``/``.values()`` pay for everything.
12
+
13
+ The loaders in ``tan.command_table`` stay plain static ``from ... import``
14
+ statements, which is what keeps PyInstaller able to see every command module.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable, Iterator, MutableMapping
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ import typer
22
+ from typer.core import TyperGroup
23
+ from typer.main import get_command_from_info
24
+ from typer.models import CommandInfo
25
+
26
+ from tan.command_table import COMMAND_TABLE, CommandSpec
27
+
28
+ if TYPE_CHECKING:
29
+ import click
30
+
31
+ #: Captured at import so a swap of `typer.main.get_command` (see
32
+ #: `LazyTyper.__call__`) can never make this module call itself.
33
+ _REAL_GET_COMMAND = typer.main.get_command
34
+
35
+ COMMAND_NAMES: tuple[str, ...] = tuple(spec.name for spec in COMMAND_TABLE)
36
+
37
+
38
+ _REGISTER_MSG = (
39
+ "tan commands are not registered with app.command(); add a loader and a "
40
+ "CommandSpec row to tan.command_table.COMMAND_TABLE"
41
+ )
42
+
43
+
44
+ class LazyCommands(MutableMapping): # type: ignore[type-arg]
45
+ """name -> Click command, built on first lookup, in ``COMMAND_TABLE`` order."""
46
+
47
+ def __init__(self, build: Callable[[CommandSpec], 'click.Command']) -> None:
48
+ self._build = build
49
+ self._specs = {spec.name: spec for spec in COMMAND_TABLE}
50
+ self._built: dict[str, 'click.Command'] = {}
51
+ self._removed: set[str] = set()
52
+ self._extra: dict[str, 'click.Command'] = {}
53
+
54
+ def __getitem__(self, name: str) -> 'click.Command':
55
+ if name in self._extra:
56
+ return self._extra[name]
57
+ if name in self._built:
58
+ return self._built[name]
59
+ if name in self._removed or name not in self._specs:
60
+ raise KeyError(name)
61
+ if name not in self._built:
62
+ self._built[name] = self._build(self._specs[name])
63
+ return self._built[name]
64
+
65
+ def __setitem__(self, name: str, command: 'click.Command') -> None:
66
+ if name in self._specs:
67
+ self._removed.discard(name)
68
+ self._built[name] = command
69
+ return
70
+ self._extra[name] = command
71
+
72
+ def __delitem__(self, name: str) -> None:
73
+ if name not in self:
74
+ raise KeyError(name)
75
+ self._extra.pop(name, None)
76
+ self._built.pop(name, None)
77
+ if name in self._specs:
78
+ self._removed.add(name)
79
+
80
+ def __iter__(self) -> Iterator[str]:
81
+ for name in self._specs:
82
+ if name not in self._removed:
83
+ yield name
84
+ yield from self._extra
85
+
86
+ def __len__(self) -> int:
87
+ return sum(1 for _ in self)
88
+
89
+ def __contains__(self, name: object) -> bool:
90
+ return name in self._extra or (name in self._specs and name not in self._removed)
91
+
92
+
93
+ class LazyGroup(TyperGroup):
94
+ """A ``TyperGroup`` whose ``commands`` is a ``LazyCommands`` when built lazily."""
95
+
96
+ def install_lazy_commands(self, build: Callable[[CommandSpec], 'click.Command']) -> None:
97
+ self.commands = LazyCommands(build)
98
+
99
+
100
+ class LazyTyper(typer.Typer):
101
+ """``typer.Typer`` whose subcommands come from ``COMMAND_TABLE``.
102
+
103
+ ``registered_commands`` still answers with the full, ordered list of
104
+ ``CommandInfo`` (built on first read) so table-derived tests and tools keep
105
+ their view, and ``typer.main.get_command(app)`` still yields the complete
106
+ eager tree. Only ``__call__`` -- the real ``tan`` entry -- and
107
+ ``lazy_group()`` take the lazy path.
108
+ """
109
+
110
+ def __init__(self, *args: Any, command_cls: type | None = None, **kwargs: Any) -> None:
111
+ self._command_cls = command_cls
112
+ self._lazy_build = False
113
+ self._lazy_group: LazyGroup | None = None
114
+ self._infos: dict[str, CommandInfo] = {}
115
+ super().__init__(*args, cls=LazyGroup, **kwargs)
116
+
117
+ # `Typer.__init__` assigns `self.registered_commands = []`; absorb it.
118
+ @property
119
+ def registered_commands(self) -> list[CommandInfo]: # type: ignore[override]
120
+ if self._lazy_build:
121
+ return []
122
+ return [self.command_info(spec) for spec in COMMAND_TABLE]
123
+
124
+ @registered_commands.setter
125
+ def registered_commands(self, value: list[CommandInfo]) -> None:
126
+ if value:
127
+ raise TypeError(_REGISTER_MSG)
128
+
129
+ def command(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[override]
130
+ raise TypeError(_REGISTER_MSG)
131
+
132
+ def command_info(self, spec: CommandSpec) -> CommandInfo:
133
+ info = self._infos.get(spec.name)
134
+ if info is None:
135
+ callback, context_settings = spec.load()
136
+ info = CommandInfo(
137
+ name=spec.name,
138
+ cls=self._command_cls,
139
+ context_settings=context_settings,
140
+ callback=callback,
141
+ rich_help_panel=spec.rich_help_panel,
142
+ )
143
+ self._infos[spec.name] = info
144
+ return info
145
+
146
+ def _build_command(self, spec: CommandSpec) -> 'click.Command':
147
+ return get_command_from_info(
148
+ self.command_info(spec),
149
+ pretty_exceptions_short=self.pretty_exceptions_short,
150
+ rich_markup_mode=self.rich_markup_mode,
151
+ )
152
+
153
+ def lazy_group(self) -> 'click.Command':
154
+ """The Click tree with subcommand modules deferred until looked up.
155
+
156
+ Built once per instance; every caller (dispatch, help envelope, the
157
+ `--format` domain walk) shares it.
158
+ """
159
+ if self._lazy_group is None:
160
+ self._lazy_build = True
161
+ try:
162
+ group = _REAL_GET_COMMAND(self)
163
+ finally:
164
+ self._lazy_build = False
165
+ assert isinstance(group, LazyGroup)
166
+ group.install_lazy_commands(self._build_command)
167
+ self._lazy_group = group
168
+ return self._lazy_group
169
+
170
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
171
+ group = self.lazy_group()
172
+
173
+ def _get_command(typer_instance: typer.Typer) -> 'click.Command':
174
+ # `Typer.__call__` rebuilds the tree via `typer.main.get_command(self)`;
175
+ # hand it the lazy group for THIS app and delegate for anything else.
176
+ if typer_instance is self:
177
+ return group
178
+ return _REAL_GET_COMMAND(typer_instance)
179
+
180
+ original = typer.main.get_command
181
+ typer.main.get_command = _get_command # type: ignore[assignment]
182
+ try:
183
+ return super().__call__(*args, **kwargs)
184
+ finally:
185
+ typer.main.get_command = original
tan/model/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """tan model packaging: .alpmodel writer + manifest model."""
@@ -0,0 +1,132 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Generate the canonical test fixtures: a minimal .alpmodel + its C byte
3
+ array, and the issue #1254 onnx_cpu regression fixture (tests/yocto/).
4
+
5
+ These three committed files (`tests/fixtures/alpmodel/minimal.alpmodel`,
6
+ `tests/unit/alpmodel_reader/src/fixture.h`, `tests/yocto/onnx_cpu_fixture.h`)
7
+ live in alp-sdk, not tan-cli -- `tests/unit/` and `tests/yocto/` are
8
+ Zephyr/Yocto C-test trees with no tan-cli equivalent (ADR-0028 Decision-3:
9
+ the on-device `.alpmodel` C reader, and the tests that exercise it, stay in
10
+ alp-sdk). `python/tests/model/test_package.py` compares the (now relocated)
11
+ Python encoder's output against these files read out of a bound alp-sdk
12
+ checkout, the same way `tests.conftest.sdk_root()` binds every other
13
+ cross-repo test in this suite.
14
+
15
+ Run as: python -m tan.model._gen_fixture --root <path-to-alp-sdk-checkout>
16
+ (or set ALP_SDK_ROOT; --root wins when both are given)"""
17
+ from __future__ import annotations
18
+ import argparse
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+ from ..core.shapes import SDK_MARKER as _SDK_MARKER
23
+ from .manifest import Manifest, Tensor, Target, Coverage
24
+ from .package import write_package
25
+
26
+
27
+ def _manifest() -> Manifest:
28
+ return Manifest(
29
+ name="minimal",
30
+ src_sha=bytes(range(32)),
31
+ inputs=[Tensor("int8", 4, [1, 96, 96, 1], 0.0078, -1)],
32
+ outputs=[Tensor("int8", 2, [1, 2], 0.004, 0)],
33
+ targets=[
34
+ Target("ethos_u", "alif:ensemble:e8", "vela_tflite", "ethos-u85-256",
35
+ 65536, {"sram_kib": 256, "op_features": []}, 0, compiler_version="vela 4.1.0"),
36
+ Target("cpu", "*", "tflite", "", 131072,
37
+ {"sram_kib": 0, "op_features": []}, 1, compiler_version="passthrough"),
38
+ ],
39
+ coverage=[Coverage("deepx_dxm1", "", "skipped", "dx-compiler not found")],
40
+ )
41
+
42
+
43
+ def build_fixture_bytes() -> bytes:
44
+ return write_package(_manifest(), [b"VELA\x00\x01\x02\x03", b"TFLITE\x00"])
45
+
46
+
47
+ def _onnx_cpu_manifest() -> Manifest:
48
+ return Manifest(
49
+ name="onnx_cpu_smoke",
50
+ src_sha=bytes(range(32)),
51
+ targets=[
52
+ Target("cpu", "*", "onnx", "", 65536, {"sram_kib": 0, "op_features": []}, 0,
53
+ compiler_version="passthrough"),
54
+ ],
55
+ )
56
+
57
+
58
+ def build_onnx_cpu_fixture_bytes() -> bytes:
59
+ """Issue #1254 end-to-end regression fixture (tests/yocto/alpmodel_onnx_cpu.c):
60
+ an "onnx" blob_format .alpmodel container, decoded by the real
61
+ zcbor-backed alp_model_parse()."""
62
+ return write_package(_onnx_cpu_manifest(), [b"\x08\x01\x12\x04test-onnx-blob-placeholder"])
63
+
64
+
65
+ def to_c_header(raw: bytes, *, array_name: str = "alp_model_fixture",
66
+ guard: str = "ALP_MODEL_FIXTURE_H") -> str:
67
+ # clang-format guards: the array wrap width is the generator's,
68
+ # not the formatter's -- without them a repo-wide clang-format
69
+ # sweep rewraps the rows and the committed-matches-generator
70
+ # pytest fails.
71
+ lines = ["/* GENERATED by `python -m tan.model._gen_fixture` -- do not edit. */",
72
+ "/* clang-format off */",
73
+ f"#ifndef {guard}", f"#define {guard}",
74
+ "#include <stddef.h>", "#include <stdint.h>", "",
75
+ f"static const uint8_t {array_name}[] = {{"]
76
+ for i in range(0, len(raw), 12):
77
+ chunk = ", ".join(f"0x{b:02x}" for b in raw[i:i + 12])
78
+ lines.append(f" {chunk},")
79
+ lines += ["};",
80
+ f"static const size_t {array_name}_len = {len(raw)};",
81
+ "", f"#endif /* {guard} */",
82
+ "/* clang-format on */", ""]
83
+ return "\n".join(lines)
84
+
85
+
86
+ def _resolve_sdk_root(explicit: str | None) -> Path:
87
+ """`--root` wins over `ALP_SDK_ROOT`; either must name a real alp-sdk
88
+ checkout (the same `scripts/alp_project.py` marker `tan`'s own SDK
89
+ discovery uses), not just an existing directory -- a typo'd path should
90
+ fail loudly here, not by writing into the wrong tree."""
91
+ root = explicit or os.environ.get("ALP_SDK_ROOT")
92
+ if not root:
93
+ print("tan.model._gen_fixture: no alp-sdk checkout given -- pass "
94
+ "--root <path> or set ALP_SDK_ROOT.", file=sys.stderr)
95
+ raise SystemExit(1)
96
+ resolved = Path(root).resolve()
97
+ if not resolved.joinpath(*_SDK_MARKER).is_file():
98
+ print(f"tan.model._gen_fixture: {resolved} does not look like an "
99
+ f"alp-sdk checkout (missing {'/'.join(_SDK_MARKER)}).",
100
+ file=sys.stderr)
101
+ raise SystemExit(1)
102
+ return resolved
103
+
104
+
105
+ def main(argv: list[str] | None = None) -> None:
106
+ parser = argparse.ArgumentParser(description=__doc__)
107
+ parser.add_argument("--root", help="alp-sdk checkout to write the fixtures into "
108
+ "(defaults to $ALP_SDK_ROOT)")
109
+ args = parser.parse_args(argv)
110
+ sdk_root = _resolve_sdk_root(args.root)
111
+
112
+ alpmodel = sdk_root / "tests/fixtures/alpmodel/minimal.alpmodel"
113
+ header = sdk_root / "tests/unit/alpmodel_reader/src/fixture.h"
114
+ onnx_cpu_header_path = sdk_root / "tests/yocto/onnx_cpu_fixture.h"
115
+
116
+ raw = build_fixture_bytes()
117
+ alpmodel.parent.mkdir(parents=True, exist_ok=True)
118
+ header.parent.mkdir(parents=True, exist_ok=True)
119
+ alpmodel.write_bytes(raw)
120
+ header.write_text(to_c_header(raw), encoding="utf-8", newline="")
121
+
122
+ onnx_cpu_raw = build_onnx_cpu_fixture_bytes()
123
+ onnx_cpu_header = to_c_header(onnx_cpu_raw, array_name="k_onnx_cpu_alpmodel",
124
+ guard="ALP_MODEL_ONNX_CPU_FIXTURE_H")
125
+ onnx_cpu_header_path.parent.mkdir(parents=True, exist_ok=True)
126
+ onnx_cpu_header_path.write_text(onnx_cpu_header, encoding="utf-8", newline="")
127
+
128
+ print(f"wrote {alpmodel} ({len(raw)} bytes), {header} and {onnx_cpu_header_path}")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
@@ -0,0 +1,116 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Compiler-adapter interface: one adapter per backend toolchain."""
3
+ from __future__ import annotations
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass
10
+ class Blob:
11
+ """One compiled artifact + the manifest metadata the writer needs."""
12
+ format: str # conventionally one of manifest.VALID_BLOB_FORMATS
13
+ # (vela_tflite | tflite | drpai_dir | dxnn |
14
+ # onnx | executorch); this is a WRITE-side
15
+ # field and is never itself validated -- an
16
+ # unlisted format here is rejected only when
17
+ # the package is later DECODED
18
+ # (Manifest.from_dict), and
19
+ # tests/gates/test_blob_format_producers_stay_in_valid_blob_formats.py
20
+ # is what actually keeps a producer's own
21
+ # literal in sync with the set.
22
+ payload: bytes
23
+ arena_bytes: int = 0
24
+ compiler_version: str = ""
25
+ req_sram_kib: int = 0
26
+ # Real per-operator NPU-vs-CPU placement, as the compiler itself reports
27
+ # it -- today only `VelaAdapter` populates these (parsed from vela's own
28
+ # "CPU/NPU operators = N (P%)" summary line, `ethos_u._parse_vela_
29
+ # placement`). None for every other adapter, and None when the compiler
30
+ # ran but didn't print a placement summary this could parse: a compiler
31
+ # exiting 0 is NOT proof every op landed on the NPU (vela exits 0 on a
32
+ # full CPU fallback by design), so a caller that wants "did this compile
33
+ # actually place anything on the accelerator" must read these, never
34
+ # infer it from the mere absence of an exception.
35
+ npu_op_count: int | None = None
36
+ cpu_op_count: int | None = None
37
+ # Adapter-authored caveats about THIS compile that a report must not drop
38
+ # -- each already a complete, customer-readable sentence. Today only
39
+ # `VelaAdapter` populates them, for the profile flags vela had to default:
40
+ # `--memory-mode` now comes from the SoC spec (alp-sdk #1470), but
41
+ # `--system-config` still does not, because the SoM-authoritative names
42
+ # live only in a proprietary .ini alp-sdk does not redistribute. Surfaced
43
+ # rather than swallowed so a compile tuned to vela's own bandwidth model
44
+ # -- or, under a memory mode whose const area sits on the arena's OTHER
45
+ # AXI port (`Shared_Sram`), one whose WEIGHTS vela's default system config
46
+ # placed as well, or, when no memory mode was resolvable at all, one sent
47
+ # to vela's DRAM-backed default on a module that has no DRAM -- cannot be
48
+ # mistaken for a module-authoritative one; `tan.model.check`'s
49
+ # `_report_from_vela_compile` carries them into `BackendReport.notes` and
50
+ # so into the JSON envelope. Never a substitute for failing: a figure that
51
+ # cannot be derived is refused (`ethos_u._refuse_zero_sram_footprint`),
52
+ # not caveated.
53
+ caveats: tuple[str, ...] = ()
54
+
55
+
56
+ class CompilerAdapter(ABC):
57
+ backend: str # cpu | ethos_u | drpai | deepx_dxm1
58
+ # True for backends that need a per-model compile config the SDK can't
59
+ # derive (DEEPX JSON+calibration, DRP-AI spec). build_model records a
60
+ # "no compile config" coverage skip for these when no opts block is given.
61
+ requires_compile_opts: bool = False
62
+
63
+ @abstractmethod
64
+ def is_available(self) -> bool:
65
+ """True if this backend's compiler is installed/usable on this host."""
66
+
67
+ @abstractmethod
68
+ def accepts(self, src_format: str) -> bool:
69
+ """True if this adapter can consume the given source format (onnx|tflite)."""
70
+
71
+ @abstractmethod
72
+ def compile(self, source: Path, *, accel_config: str, out_dir: Path,
73
+ opts: dict | None = None,
74
+ vela_memory_mode: str | None = None,
75
+ vela_system_config: str | None = None,
76
+ vela_vendor_system_config: str | None = None,
77
+ vela_vendor_config_filename: str | None = None,
78
+ soc_declares_dram: bool | None = None) -> Blob:
79
+ """Compile @source for @accel_config; return the Blob.
80
+
81
+ @opts is the per-model compile config for this backend
82
+ (board.yaml `models[].compile.<backend>`), with any path values already
83
+ resolved to absolute paths by the caller; None when the backend needs
84
+ no per-model config (cpu, ethos_u).
85
+
86
+ @vela_memory_mode / @vela_system_config / @vela_vendor_system_config
87
+ ARE compile inputs, and the only backend-specific ones on this
88
+ interface: `TargetSpec.vela_*`, the silicon's own vela invocation
89
+ profile out of the SoC spec's `npu_toolchain.vela` (alp-sdk #1470).
90
+ Only `VelaAdapter` acts on them -- every other adapter accepts and
91
+ ignores them, exactly as `cpu` and `ethos_u` accept and ignore `opts`
92
+ -- and they are named for vela on purpose, so a future DRP-AI or DEEPX
93
+ profile cannot be mistaken for being carried by them. `None` means the
94
+ caller resolved no profile, and an adapter must then invoke its
95
+ compiler exactly as it did before the profile existed rather than
96
+ substituting a default of its own: a guessed memory model compiles a
97
+ command stream for hardware the module does not have. The vendor
98
+ System_Config is a SEPARATE field from the built-in one because it is
99
+ legal only alongside a `--config` the customer supplies through
100
+ `ALP_VELA_CONFIG`; an adapter handed both in one field could put a
101
+ section name on a command line vela cannot resolve, which is rc=1.
102
+
103
+ @vela_vendor_config_filename / @soc_declares_dram are the opposite:
104
+ DIAGNOSTIC facts about this part, out of the same SoC spec
105
+ (`npu_toolchain.vela.vendor_config_filename` and
106
+ `external_memory_interfaces[]`), for an adapter whose ERROR TEXT is
107
+ part-specific even though its compile is not. Today only `VelaAdapter`
108
+ reads them -- to name the vendor `.ini` a part actually declares, and
109
+ to say that a DRAM placement describes memory this part has no
110
+ interface to. They replaced a `silicon_ref` vendor-prefix match that
111
+ stood in for both questions (tan-cli#789 review (g)); metadata answers
112
+ them per part, so no adapter has to know who builds what. `None` means
113
+ the caller resolved no such fact, and an adapter must then say nothing
114
+ about it rather than infer one from an accelerator config or a
115
+ compiler-reported profile name. Neither is a compile input: no adapter
116
+ may change the artifact it emits based on these."""
@@ -0,0 +1,26 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """CPU/TFLM passthrough adapter: the model runs as-is on reference kernels."""
3
+ from __future__ import annotations
4
+ from pathlib import Path
5
+ from . import CompilerAdapter, Blob
6
+
7
+
8
+ class CpuAdapter(CompilerAdapter):
9
+ backend = "cpu"
10
+
11
+ def is_available(self) -> bool:
12
+ return True # always available; no external tool
13
+
14
+ def accepts(self, src_format: str) -> bool:
15
+ return src_format == "tflite"
16
+
17
+ def compile(self, source: Path, *, accel_config: str, out_dir: Path,
18
+ opts: dict | None = None,
19
+ vela_memory_mode: str | None = None,
20
+ vela_system_config: str | None = None,
21
+ vela_vendor_system_config: str | None = None,
22
+ vela_vendor_config_filename: str | None = None,
23
+ soc_declares_dram: bool | None = None) -> Blob:
24
+ payload = source.read_bytes()
25
+ return Blob(format="tflite", payload=payload, arena_bytes=0,
26
+ compiler_version="passthrough")
@@ -0,0 +1,94 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """DEEPX DX-M1 compiler adapter (host model compiler).
3
+
4
+ DEEPX's compiler is the proprietary, license-gated `dx-com` Python wheel
5
+ (console script `dxcom`; `dx-com` 2.3.0 verified -- Linux x86_64, CPython 3.12,
6
+ ONNX frontend, Target Hardware: M1). It compiles an ONNX model into a DEEPX NPU
7
+ model binary:
8
+ dxcom -m <model.onnx> -c <config.json> -o <output_dir>
9
+ with a per-model JSON config (the calibration dataset is referenced from inside
10
+ that config -- `dxcom` has no separate calibration CLI flag). `-o` is a
11
+ *directory*, but a successful compile writes exactly one canonical artifact into
12
+ it: ``<model_stem>.dxnn`` -- a self-describing flatbuffer (magic ``DXNN`` + a
13
+ JSON header). That single ``.dxnn`` is what the on-device `dx_rt` runtime loads,
14
+ so the adapter returns its raw bytes with `blob_format` ``dxnn`` (matching the
15
+ device-side ALP_INFERENCE_MODEL_DXNN that `alp_model_select` decodes), NOT a tar
16
+ of the directory. (Confirmed against a real `dxcom` 2.3.0 run, 2026-05-27: a
17
+ tiny CNN and a real yolo11n both emit a single ``<stem>.dxnn``; `compiler.log`
18
+ only appears with `--gen_log`. dxcom also requires >15 GB host RAM.)
19
+
20
+ The wheel is not redistributable, so it is NOT bundled; is_available() is True
21
+ only when `dxcom` is on PATH or ALP_DEEPX_SDK_HOME points at an install. The
22
+ per-model config + calibration come from board.yaml `models[].compile.deepx_dxm1`
23
+ (threaded in as `opts`). The dx_rt A55/PCIe *runtime* backend
24
+ (`src/yocto/inference_deepx.cpp`) is Stage 2 step 4 (bench-gated)."""
25
+ from __future__ import annotations
26
+ import os
27
+ import re
28
+ import shutil
29
+ import subprocess
30
+ from pathlib import Path
31
+ from tan.core.subprocess_env import spawn_env
32
+ from . import CompilerAdapter, Blob
33
+
34
+ # dxcom does post-training quantization + compilation (torch/onnx under the
35
+ # hood); minutes for a real model, but never unbounded in CI.
36
+ _DXCOM_TIMEOUT_S = 1800
37
+
38
+
39
+ def _dxcom_version() -> str:
40
+ """Best-effort compiler version, e.g. 'DX-COM 2.3.0'; 'dxcom' on failure."""
41
+ try:
42
+ proc = subprocess.run(
43
+ ["dxcom", "-v"], capture_output=True, text=True, timeout=60, env=spawn_env()
44
+ )
45
+ except (OSError, subprocess.SubprocessError):
46
+ return "dxcom"
47
+ m = re.search(r"DX-COM[^\d]*(\d+\.\d+\.\d+)", proc.stdout + proc.stderr)
48
+ return f"DX-COM {m.group(1)}" if m else "dxcom"
49
+
50
+
51
+ class DeepxAdapter(CompilerAdapter):
52
+ backend = "deepx_dxm1"
53
+ requires_compile_opts = True # needs a per-model dxcom JSON config
54
+
55
+ def is_available(self) -> bool:
56
+ if shutil.which("dxcom"): # the dx-com wheel's console script
57
+ return True
58
+ root = os.environ.get("ALP_DEEPX_SDK_HOME")
59
+ return bool(root) and Path(root).is_dir()
60
+
61
+ def accepts(self, src_format: str) -> bool:
62
+ return src_format == "onnx" # dxcom is an ONNX frontend
63
+
64
+ def compile(self, source: Path, *, accel_config: str, out_dir: Path,
65
+ opts: dict | None = None,
66
+ vela_memory_mode: str | None = None,
67
+ vela_system_config: str | None = None,
68
+ vela_vendor_system_config: str | None = None,
69
+ vela_vendor_config_filename: str | None = None,
70
+ soc_declares_dram: bool | None = None) -> Blob:
71
+ config = (opts or {}).get("config")
72
+ if not config:
73
+ raise RuntimeError(
74
+ "DEEPX compile needs models[].compile.deepx_dxm1.config "
75
+ "(a dxcom JSON config; the calibration set is referenced from it)")
76
+ dst = out_dir / f"{source.stem}_dxnn"
77
+ dst.mkdir(parents=True, exist_ok=True)
78
+ cmd = ["dxcom", "-m", str(source), "-c", str(config), "-o", str(dst)]
79
+ try:
80
+ proc = subprocess.run(
81
+ cmd, capture_output=True, text=True, timeout=_DXCOM_TIMEOUT_S, env=spawn_env()
82
+ )
83
+ except subprocess.TimeoutExpired as exc:
84
+ raise RuntimeError(f"dxcom timed out after {exc.timeout}s") from exc
85
+ if proc.returncode != 0:
86
+ raise RuntimeError(f"dxcom failed: {proc.stderr.strip() or proc.stdout.strip()}")
87
+ artifacts = sorted(dst.glob("*.dxnn"))
88
+ if not artifacts:
89
+ raise RuntimeError(f"dxcom produced no .dxnn in {dst}")
90
+ # A single-input compile emits exactly one <stem>.dxnn; prefer the
91
+ # model-stem-named artifact if dxcom ever emits more than one.
92
+ canonical = next((p for p in artifacts if p.stem == source.stem), artifacts[0])
93
+ return Blob(format="dxnn", payload=canonical.read_bytes(),
94
+ compiler_version=_dxcom_version())