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,76 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`diagnostics.link: itcm` (tan-cli#1350) -- the pieces every planner consumer
3
+ and the flash path share without importing the planner.
4
+
5
+ The planner raises `LinkTargetError` (`tan/planner/link_target.py`), which
6
+ carries a `build.link-itcm-*` issue code. The planner needs a bound SDK root to
7
+ import, so a consumer that catches a planner failure recognises the refusal by
8
+ that code (duck-typed), never by class: `refusal_code(err)`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ #: Both refusal codes start with this; see `contract/issue-codes.json`.
14
+ CODE_PREFIX = "build.link-itcm-"
15
+
16
+ #: The `flash_method` the manifest carries for an ITCM-linked slice. NOT a
17
+ #: registered backend on purpose: the image is linked at 0x0 and must never be
18
+ #: signed or written to MRAM; `tan flash` refuses it and points at
19
+ #: `tan flash --ram`.
20
+ RAM_RUN_ONLY_METHOD = "ram_run_only"
21
+
22
+
23
+ def refusal_code(err: BaseException) -> str | None:
24
+ """The `build.link-itcm-*` code `err` carries, else None."""
25
+ code = getattr(err, "code", None)
26
+ if isinstance(code, str) and code.startswith(CODE_PREFIX):
27
+ return code
28
+ return None
29
+
30
+
31
+ def split_coded_message(message: str) -> tuple[str | None, str]:
32
+ """Inverse of the `"<code>: <text>"` form a string-returning consumer uses."""
33
+ if message.startswith(CODE_PREFIX):
34
+ code, _, text = message.partition(": ")
35
+ return code, text
36
+ return None, message
37
+
38
+
39
+ RAM_ONLY_PROJECT_CODE = "flash.ram-run-only-project"
40
+
41
+
42
+ def ram_run_only_project_refusal(
43
+ slices: "list[tuple[str, str | None]]",
44
+ core: str | None,
45
+ helper: str | None,
46
+ *,
47
+ ram: bool = False,
48
+ ) -> str | None:
49
+ """Fail-closed rule for a project marked RAM-only (`diagnostics.link: itcm`).
50
+
51
+ `slices` is `(core_id, flash_method)` for every manifest slice. A plain
52
+ `tan flash` (not `--ram`) on a manifest holding any `ram_run_only` slice is
53
+ refused as a WHOLE run -- the project's other slices (e.g. the default
54
+ `m55_hp` stock shim, still planned for an MRAM write) must not be flashed
55
+ in a partial run -- UNLESS the operator explicitly selected only non-ram
56
+ slices (`--core <non-ram core>`) or only a helper (`--helper`). Returns the
57
+ refusal message, else None. `--ram` is never affected.
58
+ """
59
+ if ram:
60
+ return None
61
+ ram_cores = sorted(c for c, m in slices if m == RAM_RUN_ONLY_METHOD)
62
+ if not ram_cores:
63
+ return None
64
+ if helper is not None:
65
+ return None
66
+ if core is not None and core not in ram_cores:
67
+ return None
68
+ first = ram_cores[0]
69
+ return (
70
+ f"flash: this project is marked RAM-only (board.yaml `diagnostics.link: "
71
+ f"itcm`; slice {', '.join(ram_cores)} is `ram_run_only`) -- refusing the "
72
+ f"whole run before any write, so a RAM-only project is never partly "
73
+ f"written to MRAM. RAM-run it with `tan flash --ram --core {first}`; to "
74
+ f"flash only another slice, name it explicitly with `--core <id>`; or "
75
+ f"remove `diagnostics.link` and rebuild."
76
+ )
@@ -0,0 +1,376 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The ONE place `soc-spec-v1.schema.json` / `som-preset-v2.schema.json`
3
+ validation runs -- tan-cli#964.
4
+
5
+ Before this module, `_schema_errors` (`tan.commands.new_som_cmd`) ran
6
+ `jsonschema.Draft202012Validator` against a generated SoC-spec/SoM-preset
7
+ skeleton on the `tan new-som` WRITE path only. Every READ consumer --
8
+ `tan build`, `tan generate`, `tan presets`, `tan size`, `tan debug-config`,
9
+ `tan bootstrap` -- trusted `metadata/socs/**` / `metadata/e1m_modules/**`
10
+ unvalidated, so a schema-invalid field (`cores[].type` a number instead of a
11
+ string, most often) reached an `isinstance` guard downstream instead of a
12
+ coded, readable message. Five rounds of guards across #957/#962/#965/#969
13
+ are the measured cost of validating at the point of USE instead of the point
14
+ of READ: `presets_cmd.core_type_lookup`, `core/os_class.py`,
15
+ `planner/topology.py`, and twice in `planner/kconfig.py` each independently
16
+ rediscovered the same missing gate.
17
+
18
+ tan-cli#964's decided rule (posted as a comment on the issue, not re-derived
19
+ here): **validate on every read, always report, refuse only when the invalid
20
+ field feeds something the run puts on disk or on silicon.** This module does
21
+ the FIRST two verbs -- it validates and it always returns every violation as
22
+ a message that names the file. It does NOT decide refuse vs. warn: that is
23
+ each caller's call, made by what it DOES with the returned list (raise an
24
+ `OrchestratorError` and refuse, or fold the messages into the envelope's
25
+ `issues[]` as warnings and continue). Putting the validator in one place and
26
+ leaving the policy to the caller is what a "per-consumer guard" is not --
27
+ the five guards above were each written at the point a bad value was ABOUT
28
+ TO BE USED, independently rediscovering the same missing check; this module
29
+ is written at the point the document is READ, once, so nothing downstream of
30
+ a successful validation call needs to guard the field's TYPE again (the
31
+ `isinstance` guards stay anyway -- tan-cli#965 proved schema-valid can still
32
+ crash a consumer whose contract is narrower than the schema; see
33
+ `targets.py`'s `npu['mac_per_cycle']`, filed separately).
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import functools
39
+ import json
40
+ from pathlib import Path
41
+ from typing import Any
42
+
43
+
44
+ def soc_spec_schema_path(metadata_root: Path | str) -> Path:
45
+ """`<sdk_root>/metadata/schemas/soc-spec-v1.schema.json`.
46
+
47
+ Takes the METADATA directory (`<sdk_root>/metadata`), not the SDK root --
48
+ every read-path caller (`planner.loader`, `presets_cmd`, `size_cmd`)
49
+ already has that path bound under a name like `metadata_root` or `root`;
50
+ `new_som_cmd`'s own `_soc_schema_path`/`_som_schema_path` take the SDK
51
+ root instead (their write-path callers have THAT bound) and are left as
52
+ they are rather than reconciled onto this convention -- two path helpers
53
+ that each match their own caller's already-bound variable is simpler
54
+ than a `sdk_root` vs. `metadata_root` footgun on one shared name.
55
+ """
56
+ return Path(metadata_root) / "schemas" / "soc-spec-v1.schema.json"
57
+
58
+
59
+ def som_preset_schema_path(metadata_root: Path | str) -> Path:
60
+ """`<sdk_root>/metadata/schemas/som-preset-v2.schema.json`. See
61
+ `soc_spec_schema_path` for the `metadata_root` convention."""
62
+ return Path(metadata_root) / "schemas" / "som-preset-v2.schema.json"
63
+
64
+
65
+ def npu_ops_schema_path(metadata_root: Path | str) -> Path:
66
+ """`<sdk_root>/metadata/schemas/npu-ops-v1.schema.json` (tan-cli#1298).
67
+ See `soc_spec_schema_path` for the `metadata_root` convention. Present on
68
+ alp-sdk `dev`; absent from SDK releases that predate it."""
69
+ return Path(metadata_root) / "schemas" / "npu-ops-v1.schema.json"
70
+
71
+
72
+ def _posix(value: object) -> str:
73
+ """`to_posix`: backslashes to forward slashes, nothing else.
74
+
75
+ Mirrors `build_output.to_posix` / `presets_cmd._posix` / `sdk_discovery._to_posix`
76
+ -- every other module that puts a filesystem path into an envelope field
77
+ or message keeps its own copy of this one-liner rather than importing
78
+ `tan.commands.build_output` from `tan.core` (which would be the pure-logic
79
+ layer reaching back into the command/IO layer). A schema-violation message
80
+ is exactly such a field: `f"{source}: ..."` on Windows renders
81
+ `sdk\\metadata\\socs\\...` unless the value is normalised first, and the
82
+ envelope convention (`build_output.py:34-37`) is forward slashes, always.
83
+ """
84
+ return str(value).replace("\\", "/")
85
+
86
+
87
+ def schema_errors(doc: object, schema_path: Path, *, source: Path | str | None = None) -> list[str]:
88
+ """Validate *doc* against the schema at *schema_path*; return one
89
+ formatted string per violation, sorted by JSON-pointer path, or `[]`
90
+ when it validates clean.
91
+
92
+ Moved here from `new_som_cmd._schema_errors` verbatim (tan-cli#964) --
93
+ that function is now a thin alias for this one, so there is exactly one
94
+ `jsonschema.Draft202012Validator(...)` call for SoC/SoM documents left in
95
+ the package (tan-cli#964 review, minor 7: `planner/loader.py` carries a
96
+ SEPARATE one for `board.yaml` against `BOARD_SCHEMA`, a different
97
+ document against a different schema this module was never the gap for --
98
+ "exactly one call in the package" overstated it), not two that could
99
+ drift on the SoC/SoM message format. `new_som_cmd`'s own two call
100
+ sites (the generated-skeleton self-check, run before anything is written)
101
+ pass `source=None` and get the EXACT original message shape back
102
+ (`f"{pointer}: {message}"`) -- unchanged, because that self-check
103
+ describes an in-memory document with no file of its own yet.
104
+
105
+ *source*, when given, is the file *doc* was read from, and PREFIXES every
106
+ message (`f"{source}: {pointer}: {message}"`) -- tan-cli#964's CX
107
+ requirement is that a violation name the file, the JSON pointer, and what
108
+ was found, all in one line, with no second lookup to learn which file
109
+ produced it. Every read-path caller (there is more than one file open at
110
+ once in `tan build`'s topology walk) passes a real `source`; only the
111
+ write-path self-check above passes `None`.
112
+
113
+ Raises whatever reading/parsing *schema_path* raises (`OSError`,
114
+ `UnicodeDecodeError`, `json.JSONDecodeError`) -- unchanged from the
115
+ original `_schema_errors`, and exactly what `new_som_cmd`'s two call
116
+ sites already catch to report a *schema* problem (`could not read
117
+ <path>`) distinctly from a *document* problem (`_internal_error`).
118
+ Read-path callers that want a schema-file failure folded into the same
119
+ message list instead of an exception use `validate_document` below.
120
+ """
121
+ # tan-cli#810: the only `jsonschema` call in the package (well -- the only
122
+ # one outside `planner/loader.py`'s SEPARATE `board.yaml`-schema pass,
123
+ # which validates a different document against a different schema and
124
+ # was never the gap #964 closes), and it drags in `attr`, `referencing`
125
+ # and `jsonschema_specifications` behind it -- all of which every `tan`
126
+ # invocation used to load for this one line. Deferred here, same as the
127
+ # original. Validating on every READ (not just `new-som`'s write) makes
128
+ # this import land on more invocations than before -- `tan presets`/`tan
129
+ # size`/`tan build`/`tan generate` now pay it too -- but it stays deferred
130
+ # to the point of use rather than promoted to module scope, so a `tan`
131
+ # invocation that never reaches a SoC/SoM read (`tan --version`, `tan
132
+ # doctor` before any metadata walk, ...) still does not pay for it.
133
+ validator = _cached_validator(Path(schema_path))
134
+ prefix = f"{_posix(source)}: " if source is not None else ""
135
+ return [
136
+ f"{prefix}{'/'.join(str(p) for p in err.absolute_path) or '<root>'}: {err.message}"
137
+ for err in sorted(validator.iter_errors(doc), key=lambda e: list(e.absolute_path))
138
+ ]
139
+
140
+
141
+ @functools.lru_cache(maxsize=64)
142
+ def _cached_validator_by_stat(schema_path: str, mtime_ns: int, size: int) -> Any:
143
+ """Build + cache one `jsonschema.Draft202012Validator` for *schema_path*,
144
+ keyed on its own `(mtime_ns, size)` -- tan-cli#964 review, minor 8.
145
+
146
+ Every `validate_document`/`schema_errors` call used to re-read and
147
+ re-`json.loads` the schema file and re-construct its validator from
148
+ scratch, EVERY time -- measured, this makes revalidation scale linearly
149
+ with the number of documents read against one schema: +0.08s at 11 SoMs,
150
+ +0.46s at 111 (the PR's own "within run-to-run noise" claim did not
151
+ reproduce). `(mtime_ns, size)` -- not just the path -- is the cache key so
152
+ a schema file edited mid-process (a `tan new-som` run immediately
153
+ followed by a `tan presets` in the same interpreter, or a test that
154
+ overwrites a fixture file between two calls in the SAME process) still
155
+ gets a fresh validator rather than a stale cached one; `lru_cache` does
156
+ not cache the exception a corrupt/unreadable file raises, so
157
+ `schema_errors`'s own "raises on unreadable schema" contract is
158
+ unaffected -- a failing call is retried, never permanently poisoned.
159
+ """
160
+ import jsonschema # noqa: PLC0415
161
+
162
+ schema = json.loads(Path(schema_path).read_text(encoding="utf-8"))
163
+ return jsonschema.Draft202012Validator(schema)
164
+
165
+
166
+ def _cached_validator(schema_path: Path) -> Any:
167
+ """`_cached_validator_by_stat`, keyed off *schema_path*'s CURRENT
168
+ `os.stat()` -- the indirection `schema_errors` calls, so the `stat()`
169
+ itself (cheap, but a real syscall) still runs every call while the
170
+ expensive `read_text` + `json.loads` + `Draft202012Validator(...)`
171
+ construction only runs once per distinct `(path, mtime, size)`."""
172
+ stat = Path(schema_path).stat()
173
+ return _cached_validator_by_stat(str(schema_path), stat.st_mtime_ns, stat.st_size)
174
+
175
+
176
+ @functools.lru_cache(maxsize=64)
177
+ def _cached_schema_problem_by_stat(schema_path: str, mtime_ns: int, size: int) -> str | None:
178
+ """What is wrong with the schema itself, or `None` when it is a valid
179
+ JSON Schema -- cached on the same `(mtime_ns, size)` key as the validator,
180
+ so the metaschema check runs once per schema file, not once per document.
181
+
182
+ `check_schema` is what turns `{"type": 5}` into a `SchemaError` with a
183
+ readable message; without it the same file reaches `iter_errors` and
184
+ escapes as a raw `TypeError` (tan-cli#1303)."""
185
+ import jsonschema # noqa: PLC0415
186
+
187
+ validator = _cached_validator_by_stat(schema_path, mtime_ns, size)
188
+ try:
189
+ jsonschema.Draft202012Validator.check_schema(validator.schema)
190
+ except jsonschema.exceptions.SchemaError as exc:
191
+ return f"not a valid JSON Schema: {exc.message}"
192
+ return None
193
+
194
+
195
+ def _schema_problem(schema_path: Path) -> str | None:
196
+ """`_cached_schema_problem_by_stat` keyed off *schema_path*'s current
197
+ `stat()`. Raises exactly what `_cached_validator` raises for a file that
198
+ is absent or unreadable, so `validate_document` keeps classifying those
199
+ the way it always has."""
200
+ stat = Path(schema_path).stat()
201
+ return _cached_schema_problem_by_stat(str(schema_path), stat.st_mtime_ns, stat.st_size)
202
+
203
+
204
+ def validate_document(doc: object, schema_path: Path, source: Path | str) -> list[str]:
205
+ """Read-path `schema_errors`: never raises.
206
+
207
+ Two different "I could not check this" shapes, given two different
208
+ answers, and this is the one place that split lives (tan-cli#964's own
209
+ "must not fire on a checkout that is simply older ... in a way that
210
+ blocks a legitimate customer" AND "a missing or unreadable schema file
211
+ must not become 'everything is valid'" both apply here, to two different
212
+ causes):
213
+
214
+ * The schema file is simply ABSENT (`schema_path` does not exist). This
215
+ is the ORDINARY shape of an SDK checkout that predates this schema, or
216
+ a synthetic/partial metadata root a test built -- `presets_cmd.py`'s
217
+ own `_os_choices()` already treats a missing `board.schema.json` the
218
+ same way ("a checkout with no schema of its own answers `None`
219
+ (degrade), not ... a DIFFERENT checkout's enum"), silently, with no
220
+ warning. Matching that existing precedent: `[]`, no message. Measured
221
+ against this file's own tests: without this half, EVERY synthetic
222
+ metadata root the test suite builds (routinely no `metadata/schemas/`
223
+ at all) would carry a `*.metadata-schema-invalid` warning on every
224
+ single read, which is not "the document is wrong", it is "this test
225
+ fixture is deliberately minimal" -- exactly the false-positive noise
226
+ that would erode the signal a REAL violation is supposed to carry.
227
+ * The schema file EXISTS but cannot be read or parsed (permissions,
228
+ truncated, invalid JSON). This is a genuine anomaly on an otherwise-real
229
+ checkout -- unlike the absent case, there is no legitimate reason for
230
+ it, so it becomes ONE synthetic message rather than silently degrading
231
+ to "nothing to report". This is the half that keeps the promise: a
232
+ customer whose checkout's schema file itself is corrupt still gets
233
+ told their document could not be validated, not a false "clean".
234
+ A file that parses as JSON but is not a valid JSON Schema
235
+ (`{"type": 5}`) belongs to this half too: it is checked against the
236
+ metaschema first and reported as `not a valid JSON Schema: ...`,
237
+ where it used to reach `iter_errors` and escape as a raw `TypeError`
238
+ (tan-cli#1303).
239
+
240
+ `source` is required (unlike `schema_errors`'s optional one): every
241
+ caller of this function has a real file on disk to name; the one caller
242
+ that does not (`new_som_cmd`'s in-memory self-check) calls `schema_errors`
243
+ directly instead, precisely because it has no `source` to give.
244
+
245
+ tan-cli#1116 round 2: the ABSENT check is NOT `is_file()`/`exists()` --
246
+ a first pass here used a pre-flight `is_file()` guarded by `except
247
+ OSError: exists = True`, reasoning that `Path.is_file()` swallows only
248
+ `ENOENT`/`ENOTDIR`/`EBADF`/`ELOOP` (`pathlib`'s own `_IGNORED_ERRNOS`
249
+ list), not `EACCES`. That RAISING BEHAVIOUR stopped holding in Python
250
+ 3.14, so `is_file()`/`is_dir()`/`exists()` swallow EVERY `OSError` --
251
+ including `EACCES` -- and return `False` on every one of them there,
252
+ version-dependent behaviour a version-independent contract cannot be
253
+ built on (measured: seam1's `parity.yml` job resolves
254
+ `python-version: "3.x"` to CPython 3.14.7, where the old guard was a
255
+ silent no-op and a permission-denied schema directory took the ABSENT
256
+ arm -- `[]`, "everything is valid," the exact outcome this docstring
257
+ forbids two paragraphs up).
258
+
259
+ Round 2's OWN first fix got the boundary wrong -- worth recording
260
+ precisely, since it explains why three independent readings (this
261
+ module's own first fix included) landed on the wrong version.
262
+ `_IGNORED_ERRNOS` the CONSTANT vanishes from `pathlib` a release
263
+ EARLIER, in 3.13 (`AttributeError` probing `pathlib._IGNORED_ERRNOS`
264
+ there) -- but the BEHAVIOUR it used to name survives past that removal:
265
+ `is_file()` on a permission-denied ancestor still raises
266
+ `PermissionError` on 3.13.15, exactly as on 3.12.3, and only starts
267
+ returning `False` in 3.14.7. Measured directly against all three real
268
+ interpreters, `errno=13` throughout:
269
+
270
+ 3.12.3 is_file() -> raises PermissionError _IGNORED_ERRNOS: (2, 20, 9, 40)
271
+ 3.13.15 is_file() -> raises PermissionError _IGNORED_ERRNOS: gone (AttributeError)
272
+ 3.14.7 is_file() -> False _IGNORED_ERRNOS: gone (AttributeError)
273
+
274
+ So the constant and the behaviour it once named parted ways for one
275
+ full release: a guard that reasons from "the constant still exists" is
276
+ already wrong reading `pathlib` in 3.13, and a guard that reasons from
277
+ "I measured `is_file()` raise on the interpreters I have" is wrong the
278
+ moment 3.14 ships, whichever question it asked. Neither question is
279
+ safe to ask; the fix below asks neither.
280
+
281
+ The fix is to not stat at all: call `schema_errors` directly and let
282
+ the read itself answer, the same shape `_resolve_hw_rev`
283
+ (`perf_apply.py`) and `example_catalog.catalog_unreadable` already use.
284
+ `_cached_validator`'s own `Path(schema_path).stat()` raises
285
+ `FileNotFoundError` for a genuinely absent path and ONLY for that --
286
+ a directory raises `IsADirectoryError` once the read past `stat()`
287
+ reaches it, a file parent raises `NotADirectoryError`, an ELOOP symlink
288
+ loop and an EACCES ancestor both raise their own `OSError` -- so
289
+ `except FileNotFoundError` alone is the ABSENT half, precisely, on every
290
+ supported interpreter, because it never depends on what `is_file()`
291
+ happens to swallow this Python version.
292
+ """
293
+ try:
294
+ problem = _schema_problem(Path(schema_path))
295
+ if problem is not None:
296
+ return [f"{_posix(source)}: could not validate against {_posix(schema_path)}: {problem}"]
297
+ return schema_errors(doc, schema_path, source=source)
298
+ except FileNotFoundError:
299
+ return []
300
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
301
+ return [f"{_posix(source)}: could not validate against {_posix(schema_path)}: {exc}"]
302
+
303
+
304
+ def schema_unusable_reason(schema_path: Path | str) -> str | None:
305
+ """Why the schema at *schema_path* cannot be used to validate anything, or
306
+ `None` when it can -- or when it is simply ABSENT (`FileNotFoundError`;
307
+ `missing_schema_note` is the disclosure for that, and absence is not
308
+ "broken").
309
+
310
+ `validate_document` only turns an unreadable/unparseable schema FILE into
311
+ its one synthetic message. A file that is valid JSON but not a valid JSON
312
+ Schema (`{"type": 5}`) escapes it as a raw `TypeError` out of
313
+ `iter_errors` (measured), and an `OSError` other than `FileNotFoundError`
314
+ (a permission-denied ancestor) is indistinguishable from a document
315
+ violation by message alone. A caller that wants "schema unusable" kept
316
+ apart from "document violates schema" -- so a broken schema is never
317
+ blamed on the document -- asks this first (tan-cli#1298). `validate_document`
318
+ and its other callers are unchanged.
319
+
320
+ The bare `except Exception` is deliberate: this probes a third-party
321
+ library with an untrusted schema, whose failure types (`SchemaError`,
322
+ `TypeError`, unresolvable `$ref`) are not a closed set.
323
+ """
324
+ try:
325
+ import jsonschema # noqa: PLC0415
326
+
327
+ validator = _cached_validator(Path(schema_path))
328
+ jsonschema.Draft202012Validator.check_schema(validator.schema)
329
+ except FileNotFoundError:
330
+ return None
331
+ except OSError as exc:
332
+ # `str(exc)` embeds the offending path (`[Errno 13] Permission denied:
333
+ # '/abs/...'`); the OS's own wording alone is the part worth showing,
334
+ # and keeps absolute host paths out of anything built from this.
335
+ return f"{type(exc).__name__}: {exc.strerror or 'OS error'}"
336
+ except Exception as exc: # noqa: BLE001 -- see docstring
337
+ return f"{type(exc).__name__}: {exc}".splitlines()[0]
338
+ return None
339
+
340
+
341
+ def missing_schema_note(schema_path: Path | str, *, source: Path | str) -> str | None:
342
+ """`None` when *schema_path* exists (nothing to disclose); otherwise a
343
+ one-line note that *source* was not validated because the schema itself
344
+ is absent.
345
+
346
+ tan-cli#964 review, major 6 ("ship skip-but-disclose"): before this
347
+ function existed, the ABSENT-schema half of `validate_document` was a
348
+ SILENT skip -- `[]`, indistinguishable on the wire from "validated
349
+ clean" (`{ok: true, exitCode: 0, issues: []}` either way). The review
350
+ measured that the customer this protected -- "an SDK checkout that
351
+ predates this schema" -- does not exist in the released fleet (every
352
+ tagged alp-sdk `v0.6.0`..`v0.16.0` ships both schemas), while the cost of
353
+ silence is real: `tan generate --target os-topology` against a metadata
354
+ root with `soc-spec-v1.schema.json` deleted writes
355
+ `build/generated/os-topology.json` from a wholly unvalidated SoC spec, at
356
+ `ok: true`, with `issues: []`.
357
+
358
+ Deliberately a SEPARATE function from `validate_document`, not a second
359
+ return value on it: `validate_document`'s own contract (never raises,
360
+ "the document is not known to be invalid") is unchanged by this --
361
+ `[]` in the absent-schema case is still correct, because nothing was
362
+ found wrong with the DOCUMENT, only with tan's ability to CHECK it. A
363
+ caller that wants the disclosure calls this too, alongside its existing
364
+ `validate_document` call, and decides its own severity/code for the
365
+ result -- `presets`/`size`/`debug-config` fold it in at `info` beside
366
+ their `*.metadata-schema-invalid` `warning`; `load_board_yaml`'s REFUSE
367
+ callers thread it into an advisory list instead of raising, since an
368
+ absent schema must never become a refusal (see `validate_document`'s own
369
+ docstring on why silently skipping is not the same failure this closes).
370
+ """
371
+ if Path(schema_path).is_file():
372
+ return None
373
+ return (
374
+ f"{_posix(source)}: not validated -- no schema at {_posix(schema_path)} "
375
+ "in this checkout"
376
+ )