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,677 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Curated third-party library layer (ADR 0018).
3
+
4
+ A project selects libraries with one top-level key::
5
+
6
+ libraries: [lvgl, cmsis-dsp, nanopb]
7
+
8
+ Each name resolves to a manifest at ``metadata/libraries/<name>.yaml`` (the
9
+ single source of truth, validated by ``scripts/validate_metadata.py``). This
10
+ module is the emit-side half: it loads the manifests, validates each selection
11
+ against the resolved SoM/core capabilities, and yields the per-OS wiring the
12
+ slice emitters (``kconfig.py``) splice into their fragments -- Zephyr Kconfig,
13
+ Yocto ``IMAGE_INSTALL``, baremetal cmake args -- through the ADR 0014 --emit
14
+ contract. No forking, no fetching: west/OE own the actual pin (ADR 0017).
15
+
16
+ Design invariants:
17
+
18
+ * **Zero-diff when unused.** A project that declares no ``libraries:`` gets
19
+ byte-identical emit output -- every helper returns ``[]`` for an empty
20
+ selection and the emitters guard on that.
21
+ * **Fail early, name the constraint.** An incompatible selection raises
22
+ ``OrchestratorError`` with the failing ``requires:`` constraint named, the
23
+ same clear-error contract as schema validation, so ``tan doctor`` /
24
+ alp-studio (which read the same manifests) can never disagree with emit.
25
+
26
+ ``_emit_library_hw_backends`` + ``_SOC_FAMILY_TOKEN`` are RELOCATED from
27
+ alp-sdk's ``scripts/alp_project_emit/west_libs.py``. They land here because
28
+ this is already the library layer and ``kconfig.py`` already imports it, so the
29
+ lazy cross-repo import it used at ``_slice_alp_conf`` time is gone entirely --
30
+ that import was the single edge keeping ``alp_project`` (and through its
31
+ ``_load_yaml``, the whole of ``alp_orchestrate``) loaded on the in-process path.
32
+ ``west_libs``' own ``--emit west-libraries`` half did NOT move: that mode is
33
+ still owned and spawned by alp-sdk.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from pathlib import Path
39
+ from typing import Any, Optional
40
+
41
+ from tan.core.document_guards import DocumentGuards
42
+
43
+ from .models import BoardProject, OrchestratorError, Slice
44
+ from .som_metadata import _sku_family, resolve_capabilities
45
+
46
+ #: The malformed-document register (`tan/core/document_guards.py`), bound to
47
+ #: THIS module's own curated class. See that module's docstring for why the
48
+ #: exception type is injected rather than fixed.
49
+ #:
50
+ #: There is NO module-scope `import yaml` here any more (tan-cli#1162):
51
+ #: `load_manifest` was its only user and now reads through
52
+ #: `read_yaml_mapping`, which defers the import into the function body --
53
+ #: the idiom `som_buildability.py:109` documents and seven `tan/core/**`
54
+ #: modules share.
55
+ _GUARDS = DocumentGuards(OrchestratorError)
56
+
57
+
58
+ def _libraries_dir(metadata_root: Path) -> Path:
59
+ return metadata_root / "libraries"
60
+
61
+
62
+ # §D.lib.loader: map _sku_family() return values to the soc_family
63
+ # tokens used in the manifest's integration.zephyr.hw_backends model.
64
+ _SOC_FAMILY_TOKEN: dict[str, str] = {
65
+ "aen": "alif_ensemble",
66
+ "v2n": "renesas_rzv2n",
67
+ "v2n-m1": "renesas_rzv2n", # DEEPX add-on; HW-acc tokens still resolve via host family.
68
+ }
69
+
70
+
71
+ def _emit_library_hw_backends(
72
+ libs: list[str], sku: str, metadata_root: Path) -> list[str]:
73
+ """Per-library HW-accelerator binding loader.
74
+
75
+ For each enabled library whose canonical manifest (resolved via the
76
+ metadata/library-aliases-v1.json token table) carries an
77
+ `integration.zephyr.hw_backends` block, pick the highest-priority
78
+ implemented matching backend per accelerator class given the active
79
+ SoM SKU and emit the matching `CONFIG_*=y` line.
80
+
81
+ Match rules (per priority entry; checked in order, all specified
82
+ keys must match):
83
+ - `silicon:` key, if present, must equal the SKU's `silicon:`
84
+ ref exactly (e.g. `alif:ensemble:e4`). Lets us pin a backend
85
+ to specific SoCs in a family -- Ethos-U85 on E4/E6/E8, but
86
+ not on E3/E5/E7 which carry only U55.
87
+ - `soc_family:` key, if present, must equal the SKU family
88
+ token (`alif_ensemble`, `renesas_rzv2n`, ...). Selects every
89
+ SKU in that family.
90
+ - `requires_cap:` key, if present, must name a capability flag
91
+ in the SKU's `metadata/e1m_modules/<sku>.yaml` `capabilities:`
92
+ block that resolves to a truthy value (`true` or a non-zero
93
+ count). Cleanest matcher when an accelerator is shared
94
+ across families.
95
+ - All three omitted = universal entry (e.g. plain FPU, generic
96
+ DMA), matches any SKU.
97
+ - `status: planned` / `status: stub` entries are retained as
98
+ metadata for the roadmap but are not emitted as active build
99
+ claims.
100
+
101
+ RELOCATED from `scripts/alp_project_emit/west_libs.py`. One mechanical
102
+ change, no behavioural one on the success path: a malformed metadata YAML
103
+ now raises `OrchestratorError` (the loader's own error) where alp-sdk's
104
+ delegating wrapper turned it into `sys.exit`. A refusal that raises is
105
+ reportable as a coded envelope; one that exits the interpreter is not, and
106
+ in-process that interpreter is `tan`'s.
107
+ """
108
+ # An unrecognised SKU pattern (a synthetic/test-only SKU, or a real SKU
109
+ # from a family this matcher doesn't know) resolves to "no HW backend
110
+ # applies" rather than raising -- this is an OPTIONAL accelerator-wiring
111
+ # enhancement layered on top of the required baseline Kconfig
112
+ # (`_slice_alp_conf` calls this unconditionally for every Zephyr slice,
113
+ # including test fixtures that intentionally use non-family SKUs), never a
114
+ # reason to fail the whole fragment emit.
115
+ try:
116
+ family = _sku_family(sku)
117
+ except ValueError:
118
+ return []
119
+ soc_token = _SOC_FAMILY_TOKEN.get(family)
120
+ if soc_token is None:
121
+ return []
122
+
123
+ # `_library_alias_table` is `loader.py`'s, not a second copy: this module
124
+ # had its own byte-identical twin until tan-cli#868, and the two had
125
+ # already drifted by a docstring clause. `loader` cannot be imported at
126
+ # module scope here -- `loader` -> `validate` -> `libraries` is a cycle --
127
+ # so it joins `_load_yaml` on the same deferred line the module docstring
128
+ # explains.
129
+ from .loader import _library_alias_table, _load_yaml # noqa: PLC0415
130
+
131
+ out: list[str] = []
132
+
133
+ # Resolve the SKU's `silicon:` ref and merged capabilities (SoC JSON
134
+ # defaults + SoM-level overrides) via resolve_capabilities(). This
135
+ # replaces the former inline YAML text-parser so that silicon-determined
136
+ # capabilities removed from SoM YAMLs continue to resolve from the SoC JSON.
137
+ silicon_ref: str | None = None
138
+ sku_path = metadata_root / "e1m_modules" / f"{sku}.yaml"
139
+ sku_preset: dict[str, Any] = {}
140
+ if sku_path.exists():
141
+ sku_preset = _load_yaml(sku_path) or {}
142
+ silicon_ref = sku_preset.get("silicon")
143
+
144
+ merged_caps: dict[str, Any] = resolve_capabilities(sku_preset, metadata_root)
145
+
146
+ def _cap_truthy(name: str) -> bool:
147
+ v = merged_caps.get(name)
148
+ if v is None:
149
+ return False
150
+ if isinstance(v, bool):
151
+ return v
152
+ if isinstance(v, int):
153
+ return v > 0
154
+ # String value from a YAML-loaded dict (should not occur after
155
+ # resolve_capabilities, but guard for safety).
156
+ sv = str(v).lower()
157
+ if sv in ("true", "yes"):
158
+ return True
159
+ if sv in ("false", "no", "null", "none", "0"):
160
+ return False
161
+ try:
162
+ return int(sv) > 0
163
+ except ValueError:
164
+ return False
165
+
166
+ alias = _library_alias_table(metadata_root)
167
+ # Canonical -> legacy token, so the annotation comment names the library
168
+ # by its declared token regardless of whether the caller passed the legacy
169
+ # spelling (schemaVersion 1 per-core lists) or the canonical name (the
170
+ # schemaVersion 2 unified `libraries:` list resolves to canonical). The
171
+ # annotation is cosmetic; keeping it stable makes the v1->v2 migration a
172
+ # byte-identical emit (WS6-c #610 §6).
173
+ to_token = {canon: legacy for legacy, canon in alias.items()}
174
+
175
+ for lib in libs:
176
+ # Resolve the legacy per-core token to its canonical manifest and
177
+ # read the folded integration.zephyr.hw_backends block (WS6-c #610
178
+ # §6 -- replaces the retired metadata/library-profiles/ tree).
179
+ canonical = alias.get(lib, lib)
180
+ label = to_token.get(lib, lib)
181
+ manifest_path = metadata_root / "libraries" / f"{canonical}.yaml"
182
+ if not manifest_path.exists():
183
+ continue
184
+ manifest = _load_yaml(manifest_path) or {}
185
+ hw = ((manifest.get("integration") or {}).get("zephyr") or {}).get("hw_backends")
186
+ if not isinstance(hw, dict):
187
+ continue
188
+
189
+ # Per-class first-match, walking accelerator classes and their
190
+ # `priority:` lists in declaration order (identical to the retired
191
+ # hw-backends.yaml top-down walk). `sw_fallback:` is NOT emitted
192
+ # here -- the SW floor rides the base library-enable line.
193
+ for cls in (hw.get("accelerators") or []):
194
+ if not isinstance(cls, dict):
195
+ continue
196
+ current_class = cls.get("class")
197
+ for entry in (cls.get("priority") or []):
198
+ if not isinstance(entry, dict):
199
+ continue
200
+ kcv = entry.get("kconfig")
201
+ if not kcv:
202
+ continue
203
+ status = str(entry.get("status", "implemented")).strip().lower()
204
+ if status in {"planned", "stub"}:
205
+ continue
206
+ sili = entry.get("silicon")
207
+ sf = entry.get("soc_family")
208
+ cap = entry.get("requires_cap")
209
+ # All specified matchers must succeed.
210
+ if sili is not None and sili != silicon_ref:
211
+ continue
212
+ if sf is not None and sf != soc_token:
213
+ continue
214
+ if cap is not None and not _cap_truthy(str(cap)):
215
+ continue
216
+ out.append(f"{kcv} # {label} / {current_class}")
217
+ break # per-class first-match
218
+
219
+ return out
220
+
221
+
222
+ def available_libraries(metadata_root: Path) -> list[str]:
223
+ """Sorted list of every curated library token (manifest filename stem).
224
+
225
+ RELOCATED divergence from alp-sdk's own `scripts/alp_orchestrate/
226
+ libraries.py` (tan-cli#1171 review of #1162): upstream still spells this
227
+ as an `if not d.is_dir(): return []` pre-flight in front of the `glob`.
228
+ That pre-flight was the SAME tan-cli#1127 shape the four sites in #1162
229
+ removed, one directory further out, and it survived that pass because
230
+ nothing drove it: `_PlannerDocumentCases.test_permission_denied_parent`
231
+ denies `metadata/libraries/`, and the shape that breaks is a denied
232
+ `metadata/` -- the manifest's GRANDPARENT. Measured there, before this
233
+ change: raw `PermissionError` out of `Path.is_dir()` on 3.12.3 and
234
+ 3.13.15, curated on 3.14.7.
235
+
236
+ Both halves had to go, not just the pre-flight: `Path.glob` is not
237
+ interpreter-uniform on this shape either. Measured on the same denied
238
+ `metadata/`, `sorted(d.glob("*.yaml"))` raises `PermissionError` on
239
+ 3.12.3 and returns `[]` on 3.13.15 and 3.14.7 -- so the `except OSError`
240
+ is what makes the three interpreters agree, and the pre-flight is
241
+ redundant once it is there (`glob` on a directory that is simply not
242
+ present yields nothing on all three).
243
+
244
+ An unreadable library directory answers `[]` rather than raising because
245
+ this function is not a document read: its whole output is the
246
+ typo-correcting `Available: ...` HINT inside another function's error
247
+ message, and it is reached only once that function's own guarded read has
248
+ already raised. There is no caller for whom an exception here would be
249
+ more useful than an empty option list, and one -- `load_manifest`'s
250
+ `absent=` -- for whom it would replace a true, curated message with a raw
251
+ traceback.
252
+ """
253
+ try:
254
+ return sorted(p.stem for p in _libraries_dir(metadata_root).glob("*.yaml"))
255
+ except OSError:
256
+ return []
257
+
258
+
259
+ def load_manifest(name: str, metadata_root: Path) -> dict[str, Any]:
260
+ """Load one library manifest, or raise OrchestratorError naming the options.
261
+
262
+ The error lists every available library so a typo in ``libraries: [...]``
263
+ is self-correcting.
264
+
265
+ RELOCATED divergence from alp-sdk's own `scripts/alp_orchestrate/
266
+ libraries.py` (tan-cli#1162): upstream still spells this as an
267
+ `is_file()` pre-flight followed by a wholly unguarded
268
+ `yaml.safe_load(path.read_text(...))`. Read + parse + shape-check now go
269
+ through `tan/core/document_guards.py`'s register, for the reasons
270
+ `read_yaml_mapping` documents: the pre-flight answered `False` to a
271
+ directory, a denied mode and an `ELOOP` alike, and on a `chmod 000`
272
+ PARENT it did not answer at all on 3.12.3/3.13.15 (`PermissionError`
273
+ straight out of `Path.is_file()`, tan-cli#1127). Measured before this
274
+ change, through the real `tan build` path: a non-UTF-8 manifest escaped
275
+ as a raw `UnicodeDecodeError`, a malformed one as a raw
276
+ `yaml.parser.ParserError`, a `chmod 000` one as a raw `PermissionError`
277
+ -- each absorbed two frames up by `build_cmd.py:505`'s broad `except
278
+ Exception` into a `build.plan-unavailable` envelope naming the EXCEPTION
279
+ TYPE rather than the file, which is the poor-message half of this
280
+ family rather than its bare-traceback half.
281
+
282
+ `available_libraries` is passed LAZILY, as the zero-argument callable
283
+ `absent=` also accepts (tan-cli#1171 review), so the directory listing
284
+ stays on the miss path exactly as the pre-flight-era code kept it. Built
285
+ eagerly it ran ahead of the guarded read on every call, which put its own
286
+ unguarded `is_dir()` first in line and reinstated the tan-cli#1127 split
287
+ the rest of this change removes -- raw `PermissionError` on 3.12.3 and
288
+ 3.13.15, curated on 3.14.7, with `metadata/` denied. The typo-correcting
289
+ `Available: ...` message is preserved byte for byte either way.
290
+ """
291
+ path = _libraries_dir(metadata_root) / f"{name}.yaml"
292
+ return _GUARDS.read_yaml_mapping(
293
+ path,
294
+ what=f"library manifest metadata/libraries/{name}.yaml",
295
+ absent=lambda: (
296
+ f"unknown library `{name}` in `libraries:` -- no manifest at "
297
+ f"metadata/libraries/{name}.yaml. Available: "
298
+ f"{', '.join(available_libraries(metadata_root)) or '<none>'}"))
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Target-capability resolution (the `requires:` right-hand side)
303
+ # ---------------------------------------------------------------------------
304
+
305
+ def _core_classes(project: BoardProject) -> set[str]:
306
+ """The set of application-core classes the SoC provides: {"m"}, {"a"}, ...
307
+
308
+ Derived from the SoC spec's ``cores[].type`` (``cortex-m*`` -> m,
309
+ ``cortex-a*`` -> a), matching how the loader picks a default OS per core.
310
+ """
311
+ classes: set[str] = set()
312
+ for core in project.soc_spec.get("cores", []) or []:
313
+ ctype = str(core.get("type", "")).lower()
314
+ if ctype.startswith("cortex-m"):
315
+ classes.add("m")
316
+ elif ctype.startswith("cortex-a"):
317
+ classes.add("a")
318
+ return classes
319
+
320
+
321
+ def _core_class_of(project: BoardProject, slice_: Slice) -> Optional[str]:
322
+ """The class (m|a) of a specific slice's core, from the SoC spec."""
323
+ for core in project.soc_spec.get("cores", []) or []:
324
+ if core.get("id") == slice_.core_id:
325
+ ctype = str(core.get("type", "")).lower()
326
+ if ctype.startswith("cortex-m"):
327
+ return "m"
328
+ if ctype.startswith("cortex-a"):
329
+ return "a"
330
+ return None
331
+
332
+
333
+ def _project_oses(project: BoardProject) -> set[str]:
334
+ """OSes actually running on some core (excludes `off`)."""
335
+ return {s.os for s in project.cores.values() if s.os and s.os != "off"}
336
+
337
+
338
+ def _check_requires(
339
+ name: str,
340
+ manifest: dict[str, Any],
341
+ project: BoardProject,
342
+ metadata_root: Path,
343
+ ) -> None:
344
+ """Raise OrchestratorError if the project fails the manifest's `requires:`.
345
+
346
+ Every failure names the exact constraint and the observed target value so
347
+ the message is actionable ("... needs core_class m but SoM E1M-... has {a}").
348
+ """
349
+ requires = manifest.get("requires") or {}
350
+ if not isinstance(requires, dict):
351
+ return
352
+
353
+ caps = resolve_capabilities(project.som_preset, metadata_root)
354
+
355
+ for cap in requires.get("capabilities") or []:
356
+ if not caps.get(cap):
357
+ raise OrchestratorError(
358
+ f"library `{name}` requires capability `{cap}`, which SoM "
359
+ f"`{project.sku}` does not provide")
360
+
361
+ min_ram = requires.get("min_ram_kib")
362
+ if min_ram is not None:
363
+ soc_ram = project.soc_spec.get("soc_ram_kb")
364
+ if soc_ram is not None and float(soc_ram) < float(min_ram):
365
+ raise OrchestratorError(
366
+ f"library `{name}` requires min_ram_kib {min_ram}, but SoC for "
367
+ f"SoM `{project.sku}` has {soc_ram} KB RAM")
368
+
369
+ min_flash = requires.get("min_flash_kib")
370
+ if min_flash is not None:
371
+ soc_flash_mb = project.soc_spec.get("soc_flash_mb")
372
+ if soc_flash_mb is not None and float(soc_flash_mb) * 1024 < float(min_flash):
373
+ raise OrchestratorError(
374
+ f"library `{name}` requires min_flash_kib {min_flash}, but SoC "
375
+ f"for SoM `{project.sku}` has {soc_flash_mb} MB flash")
376
+
377
+ core_class = requires.get("core_class")
378
+ if core_class is not None:
379
+ have = _core_classes(project)
380
+ if core_class not in have:
381
+ raise OrchestratorError(
382
+ f"library `{name}` requires core_class `{core_class}`, but SoM "
383
+ f"`{project.sku}` has core class(es) {sorted(have) or '<none>'}")
384
+
385
+ req_os = requires.get("os")
386
+ if req_os:
387
+ have_os = _project_oses(project)
388
+ if not (set(req_os) & have_os):
389
+ raise OrchestratorError(
390
+ f"library `{name}` requires os {sorted(req_os)}, but this "
391
+ f"project's cores run {sorted(have_os) or '<none>'}")
392
+
393
+
394
+ def scoped_names(
395
+ project: BoardProject,
396
+ slice_: Optional[Slice] = None,
397
+ ) -> list[str]:
398
+ """Every library name in scope, in declaration order, deduped.
399
+
400
+ board.yaml declares each curated library ONCE, in the top-level
401
+ `libraries:` list. `loader._normalize_libraries` then folds each entry
402
+ into one of two internal channels: an entry with no `cores:` lands in
403
+ `project.libraries`, an entry with `cores:` lands in each named
404
+ `Slice.libraries`. Both channels are the SAME ADR-0018 selection and
405
+ must pass through the SAME checks -- reading only `project.libraries`
406
+ here is what let a core-scoped entry skip `load_manifest`'s unknown-name
407
+ refusal, its `requires:` constraints and its `integration:` sections
408
+ entirely (alplabai/tan-cli#555).
409
+
410
+ ``slice_`` None -> project-wide + every core-scoped name declared on any
411
+ core: the whole-project view `tan validate` /
412
+ `tan doctor` check.
413
+ ``slice_`` given -> project-wide + that one slice's core-scoped names:
414
+ what the per-slice emitters wire.
415
+ """
416
+ names: list[str] = list(project.libraries or [])
417
+ slices = [slice_] if slice_ is not None else list(project.cores.values())
418
+ for s in slices:
419
+ for name in (s.libraries or []):
420
+ if name not in names:
421
+ names.append(name)
422
+ return names
423
+
424
+
425
+ def resolve_selection(
426
+ project: BoardProject,
427
+ metadata_root: Path,
428
+ *,
429
+ slice_: Optional[Slice] = None,
430
+ ) -> list[tuple[str, dict[str, Any]]]:
431
+ """Validate every selected library and return [(name, manifest), ...].
432
+
433
+ Raises OrchestratorError on an unknown name, a failed `requires:`
434
+ constraint, or a library whose manifest has no `integration:` section for
435
+ ANY OS this project's cores run (you asked for a library that cannot be
436
+ wired anywhere on this target). Returns [] for an empty selection.
437
+
438
+ The selection covers BOTH declaration channels -- see `scoped_names` for
439
+ what ``slice_`` narrows it to.
440
+
441
+ `requires:` is checked at TWO altitudes, because the two channels ask
442
+ different questions. `_check_requires` is project-wide: a project-wide
443
+ `libraries: [ros2]` on a Yocto-A55 + Zephyr-M33 board is legitimate and
444
+ simply wires on the A55 only. A CORE-SCOPED entry (`{name: ros2, cores:
445
+ [m33_sm]}`) is instead an explicit instruction to wire it THERE, so it
446
+ additionally goes through `_check_slice_requires` against that one slice
447
+ (alplabai/tan-cli#555: "ros2's `requires: {os: [yocto], core_class: a}`
448
+ is never checked, so ros2 scoped to a Cortex-M core is silently
449
+ no-op'd"). The project-wide check cannot catch it -- `_project_oses`
450
+ contains `yocto` from the A55, so it passes.
451
+ """
452
+ selected = scoped_names(project, slice_)
453
+ core_scoped = set(slice_.libraries or ()) if slice_ is not None else set()
454
+ resolved: list[tuple[str, dict[str, Any]]] = []
455
+ project_oses = _project_oses(project)
456
+ for name in selected:
457
+ manifest = load_manifest(name, metadata_root)
458
+ _check_requires(name, manifest, project, metadata_root)
459
+ integration = manifest.get("integration") or {}
460
+ wireable = set(integration.keys()) & project_oses
461
+ if project_oses and not wireable:
462
+ have_sections = ", ".join(sorted(integration.keys())) or "<none>"
463
+ raise OrchestratorError(
464
+ f"library `{name}` cannot be wired on this project: its manifest "
465
+ f"has integration section(s) [{have_sections}] but this project's "
466
+ f"cores run {sorted(project_oses)}")
467
+ if name in core_scoped:
468
+ assert slice_ is not None # core_scoped is empty when slice_ is None
469
+ _check_slice_requires(name, manifest, project, slice_)
470
+ resolved.append((name, manifest))
471
+ return resolved
472
+
473
+
474
+ def _check_slice_requires(
475
+ name: str,
476
+ manifest: dict[str, Any],
477
+ project: BoardProject,
478
+ slice_: Slice,
479
+ ) -> None:
480
+ """`requires:` for a library board.yaml explicitly scoped to THIS core.
481
+
482
+ `cores: [<id>]` is a positive instruction, not a hint, so a `requires:`
483
+ key that the named core violates has to fail early and name the
484
+ constraint rather than resolve to nothing. Checks the two keys that are
485
+ per-core facts:
486
+
487
+ * `os:` -- against this slice's own OS, not the project-wide
488
+ union `_check_requires` uses.
489
+ * `core_class:` -- delegated to `_check_core_class`, which used to be
490
+ reachable only from `zephyr_kconfig_lines` AFTER an
491
+ `if not zephyr: continue`, so a manifest with no
492
+ `integration.zephyr:` (ros2) never reached it.
493
+
494
+ Deliberately does NOT refuse a manifest that merely has no
495
+ `integration.<this os>:` section: mbedtls and nlohmann-json are scoped to
496
+ the Yocto cores of four shipped examples and have no `integration.yocto:`,
497
+ and the honest handling for those is the explanatory `yocto_unwireable`
498
+ comment, not a hard error.
499
+ """
500
+ requires = manifest.get("requires") or {}
501
+ req_os = requires.get("os")
502
+ if req_os and slice_.os not in set(req_os):
503
+ raise OrchestratorError(
504
+ f"library `{name}` is scoped to core `{slice_.core_id}` "
505
+ f"(os `{slice_.os}`) but requires os {sorted(req_os)}")
506
+ _check_core_class(name, manifest, project, slice_)
507
+
508
+
509
+ # ---------------------------------------------------------------------------
510
+ # Per-slice emit helpers (called from kconfig.py)
511
+ # ---------------------------------------------------------------------------
512
+
513
+ def zephyr_library_kconfig(manifest: dict[str, Any]) -> list[str]:
514
+ """Kconfig lines for ONE library manifest's Zephyr integration.
515
+
516
+ The upstream module-enable line(s) (`integration.zephyr.kconfig`)
517
+ followed by the SDK SW-fallback floor
518
+ (`integration.zephyr.hw_backends.sw_fallback.kconfig`), module-enable
519
+ first and the fallback marker last, deduped. Header-only libraries whose
520
+ SW floor is a doc comment surface that comment only when there is no
521
+ module-enable line to attach it to.
522
+
523
+ The ONE derivation both declaration channels call for the base+fallback
524
+ set -- `zephyr_kconfig_lines` (project-wide) and
525
+ `kconfig.py::_per_core_library_kconfig` (core-scoped) -- so the same
526
+ library manifest can never emit a different symbol set depending on
527
+ which `libraries:` channel declared it (#1359: the project-wide channel
528
+ used to skip the SW-fallback line entirely, e.g. cmsis-dsp's project-wide
529
+ form never emitted `CONFIG_ALP_CMSIS_DSP_SCALAR=y` while its core-scoped
530
+ form did).
531
+ """
532
+ zephyr = (manifest.get("integration") or {}).get("zephyr") or {}
533
+ out: list[str] = []
534
+ for kc in zephyr.get("kconfig") or []:
535
+ kc = str(kc)
536
+ if kc not in out:
537
+ out.append(kc)
538
+ swf = ((zephyr.get("hw_backends") or {}).get("sw_fallback") or {}).get("kconfig")
539
+ if swf:
540
+ swf = str(swf)
541
+ if swf.lstrip().startswith("#"):
542
+ # Header-only library: the SW floor is a documentation comment,
543
+ # not a real knob. Surface it only when there is no module enable.
544
+ if not out:
545
+ out.append(swf)
546
+ elif swf not in out:
547
+ out.append(swf)
548
+ return out
549
+
550
+
551
+ def zephyr_kconfig_lines(
552
+ project: BoardProject,
553
+ slice_: Slice,
554
+ metadata_root: Path,
555
+ ) -> list[str]:
556
+ """Kconfig lines for every project-wide library with a Zephyr integration.
557
+
558
+ Emitted into a Zephyr slice's alp.conf. Returns [] when the project
559
+ selects no libraries (keeps existing emit byte-identical).
560
+
561
+ Both channels are RESOLVED here (so a core-scoped entry gets the same
562
+ unknown-name / `requires:` / wireability / core-class refusals as a
563
+ project-wide one), but only the project-wide half is EMITTED: the Zephyr
564
+ Kconfig for a core-scoped entry rides `kconfig.py::_emit_libraries`'
565
+ per-core channel, which reads the very same manifest. Emitting it twice
566
+ would duplicate every CONFIG_ line. Both halves derive their per-library
567
+ CONFIG_ set from the same `zephyr_library_kconfig` helper (#1359).
568
+ """
569
+ if slice_.os != "zephyr":
570
+ return []
571
+ project_wide = set(project.libraries or ())
572
+ lines: list[str] = []
573
+ for name, manifest in resolve_selection(project, metadata_root, slice_=slice_):
574
+ zephyr = (manifest.get("integration") or {}).get("zephyr")
575
+ if not zephyr:
576
+ continue
577
+ _check_core_class(name, manifest, project, slice_)
578
+ if name not in project_wide:
579
+ continue
580
+ module = zephyr.get("module")
581
+ version = str(manifest.get("version", "?"))
582
+ display_version = version if version.startswith("v") else f"v{version}"
583
+ tag = f"{name} {display_version}"
584
+ if module:
585
+ tag += f" (west module `{module}`)"
586
+ lines.append(f"# library: {tag}")
587
+ for kc in zephyr_library_kconfig(manifest):
588
+ lines.append(kc)
589
+ return lines
590
+
591
+
592
+ def yocto_image_install(
593
+ project: BoardProject,
594
+ slice_: Slice,
595
+ metadata_root: Path,
596
+ ) -> list[str]:
597
+ """IMAGE_INSTALL recipe names for libraries with a Yocto integration.
598
+
599
+ Covers BOTH declaration channels: a `libraries:` entry scoped to this
600
+ Yocto core contributes its manifest's `integration.yocto.image_install`
601
+ recipes, exactly like a project-wide one. The recipe names come from the
602
+ manifest and nowhere else -- they are never derived from the library name
603
+ (alplabai/tan-cli#555: the old per-core path invented `lib-<name>`, which
604
+ RPROVIDES nothing in meta-alp-sdk or OE).
605
+ """
606
+ if slice_.os != "yocto":
607
+ return []
608
+ packages: list[str] = []
609
+ for name, manifest in resolve_selection(project, metadata_root, slice_=slice_):
610
+ yocto = (manifest.get("integration") or {}).get("yocto")
611
+ if not yocto:
612
+ continue
613
+ for pkg in yocto.get("image_install") or []:
614
+ if pkg not in packages:
615
+ packages.append(pkg)
616
+ return packages
617
+
618
+
619
+ def yocto_unwireable(
620
+ project: BoardProject,
621
+ slice_: Slice,
622
+ metadata_root: Path,
623
+ ) -> list[str]:
624
+ """In-scope library names whose manifest has NO `integration.yocto:`.
625
+
626
+ Such a library contributes nothing to this slice's `IMAGE_INSTALL` -- the
627
+ honest emit is nothing. The caller surfaces the names as a comment so the
628
+ no-op is visible in the generated local.conf instead of silent.
629
+ """
630
+ if slice_.os != "yocto":
631
+ return []
632
+ return [
633
+ name
634
+ for name, manifest in resolve_selection(project, metadata_root, slice_=slice_)
635
+ if not (manifest.get("integration") or {}).get("yocto")
636
+ ]
637
+
638
+
639
+ def baremetal_cmake_args(
640
+ project: BoardProject,
641
+ slice_: Slice,
642
+ metadata_root: Path,
643
+ ) -> list[str]:
644
+ """cmake hint lines for libraries with a baremetal integration.
645
+
646
+ Covers both declaration channels (see `scoped_names`)."""
647
+ if slice_.os != "baremetal":
648
+ return []
649
+ args: list[str] = []
650
+ for name, manifest in resolve_selection(project, metadata_root, slice_=slice_):
651
+ baremetal = (manifest.get("integration") or {}).get("baremetal")
652
+ if not baremetal:
653
+ continue
654
+ cmake = baremetal.get("cmake")
655
+ if cmake:
656
+ args.append(f"# library {name}: {cmake}")
657
+ return args
658
+
659
+
660
+ def _check_core_class(
661
+ name: str,
662
+ manifest: dict[str, Any],
663
+ project: BoardProject,
664
+ slice_: Slice,
665
+ ) -> None:
666
+ """A per-slice guard: a core_class-constrained library must not be wired
667
+ onto a core of the wrong class, even if some OTHER core satisfies the
668
+ project-wide `requires:` check.
669
+ """
670
+ core_class = (manifest.get("requires") or {}).get("core_class")
671
+ if core_class is None:
672
+ return
673
+ have = _core_class_of(project, slice_)
674
+ if have is not None and have != core_class:
675
+ raise OrchestratorError(
676
+ f"library `{name}` requires core_class `{core_class}` but is "
677
+ f"selected on core `{slice_.core_id}` (class {have})")