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,208 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Per-core OS-class taxonomy + the topology view (issue #95).
4
+
5
+ A core's runtime is fixed by its Cortex class -- Cortex-A -> Yocto (Linux),
6
+ Cortex-M -> Zephyr (RTOS) -- not chosen freely. This module owns the
7
+ allowed-OS set read from the board schema, plus `core_os_topology` /
8
+ `emit_os_topology`, the per-core OS facts an IDE/tool renders. Extracted from
9
+ alp_orchestrate as the #285 topology seam and re-exported from the package
10
+ __init__, so callers + alp_project.py keep importing the same names unchanged.
11
+
12
+ The default-OS rule itself (`_default_os_from_core_type` / `CLASS_RUNTIMES` /
13
+ `_cross_class_os` below) is IMPORTED, not defined here, as of tan-cli#870:
14
+ `tan.core.os_class` now owns it, re-exported under these same names so every
15
+ caller and test that already imports them from this module is unaffected. See
16
+ that module's docstring for why the rule moved somewhere `tan.planner`'s
17
+ process-global SDK-root binding cannot reach -- `tan presets`' `allowedOs`
18
+ field needs the identical convention without paying this package's bind-first
19
+ import cost.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import functools
25
+ import json
26
+ from pathlib import Path
27
+ from typing import TYPE_CHECKING, Any
28
+
29
+ from tan.core.document_guards import DocumentGuards
30
+ from tan.core.os_class import CLASS_RUNTIMES # noqa: F401 (re-export: unchanged public name)
31
+ from tan.core.os_class import allowed_os_for_core as _allowed_os_for_core_shared
32
+ from tan.core.os_class import cross_class_os as _cross_class_os # noqa: F401 (re-export: validate.py's `from .topology import ... _cross_class_os`)
33
+ from tan.core.os_class import default_os_from_core_type as _default_os_from_core_type
34
+
35
+ from .models import OrchestratorError
36
+ from .paths import BOARD_SCHEMA
37
+
38
+ #: The malformed-document register, bound to THIS module's curated class.
39
+ _GUARDS = DocumentGuards(OrchestratorError)
40
+
41
+ if TYPE_CHECKING:
42
+ from .models import BoardProject
43
+
44
+
45
+ @functools.lru_cache(maxsize=None)
46
+ def _core_os_choices(metadata_root: Path) -> tuple[str, ...]:
47
+ """The runtimes a core's `os:` may resolve to, read from *metadata_root*'s
48
+ board schema's `$defs/core_entry/properties/os` enum.
49
+
50
+ Derived (not re-typed) so the value-set has exactly one source of truth
51
+ and cannot drift between the schema and the code. `off` skips the core
52
+ (no slice is built). *metadata_root* is REQUIRED and the cache is keyed
53
+ on it -- a fixed in-tree default here silently ignored a project's
54
+ `--metadata-root` override (#1485).
55
+
56
+ Falls back to the in-tree `BOARD_SCHEMA` when *metadata_root* has no
57
+ `schemas/` of its own (e.g. a synthetic test root) -- the same fallback
58
+ `loader._validate_board` applies, so a scratch root without a schema copy
59
+ still resolves instead of raising a raw `FileNotFoundError`.
60
+
61
+ RELOCATED divergence from alp-sdk's own `scripts/alp_orchestrate/
62
+ topology.py` (tan-cli#1162), which still spells the lines below as two
63
+ `is_file()` pre-flights and a bare `json.loads(read_text(...))`. Two
64
+ defects lived in that, and only one is the bare read the issue names:
65
+
66
+ * The fallback pre-flight is not a guard but a SELECTOR, and
67
+ `is_file()` answered `False` to "denied" and "is a directory"
68
+ exactly as to "not there" -- so an unreadable project schema fell
69
+ back silently onto a DIFFERENT document and the caller was told an
70
+ `os:` set the project never declared. `read_optional_text`
71
+ classifies on the real exception: `ENOENT`/`ENOTDIR` is the legal
72
+ no-schema-of-its-own branch, everything else is a refusal.
73
+ * On a `chmod 000` PARENT that pre-flight does not answer at all on
74
+ 3.12.3/3.13.15 (`Path.is_file()` raises `PermissionError`; 3.14.7
75
+ returns `False` -- tan-cli#1127), so one unreadable schema was a raw
76
+ traceback on two interpreters and a silent wrong answer on the third.
77
+
78
+ CALLERS: `validate._enforce_loader_rules:343` via `loader.py:1009`, so
79
+ every `load_board_yaml` -- `tan build`, `tan validate`, `tan generate`,
80
+ `tan kconfig` -- plus `_allowed_os_for_core` below. On `tan build` the
81
+ raw exception was absorbed by `build_cmd.py:505` into a
82
+ `build.plan-unavailable` envelope naming the type, not the file.
83
+
84
+ The `$defs` chain uses `require_key` rather than bare subscript for the
85
+ same reason: a legal-JSON schema missing `core_entry`, or with a
86
+ non-list `enum`, reached `tuple()` as a raw `KeyError`/`TypeError`.
87
+ """
88
+ schema_path = Path(metadata_root) / "schemas" / "board.schema.json"
89
+ text = _GUARDS.read_optional_text(schema_path, what="board schema")
90
+ if text is None:
91
+ schema_path = BOARD_SCHEMA
92
+ text = _GUARDS.require_readable_text(
93
+ schema_path, what="board schema",
94
+ absent=f"board schema not found: {schema_path}")
95
+ schema = _GUARDS.require_json_mapping_doc(
96
+ text, path=schema_path, what="board schema")
97
+ defs = _GUARDS.require_key(
98
+ schema, "$defs", dict, doc=schema_path, field="board schema")
99
+ entry = _GUARDS.require_key(
100
+ defs, "core_entry", dict, doc=schema_path, field="$defs")
101
+ props = _GUARDS.require_key(
102
+ entry, "properties", dict, doc=schema_path, field="$defs.core_entry")
103
+ os_prop = _GUARDS.require_key(
104
+ props, "os", dict, doc=schema_path,
105
+ field="$defs.core_entry.properties")
106
+ return tuple(_GUARDS.require_key(
107
+ os_prop, "enum", list, doc=schema_path,
108
+ field="$defs.core_entry.properties.os"))
109
+
110
+
111
+ def _runtime_class(core_type: str) -> str:
112
+ """`linux` for Cortex-A, `rtos` for Cortex-M, else `other`.
113
+
114
+ `core_type` is typed `str`, and today's one caller (`core_os_topology`)
115
+ already normalises a non-string `type` to `""` before this is reached
116
+ (`soc_types`'s `isinstance` guard, tan-cli#957/#962) -- so the
117
+ `isinstance` check below is defense-in-depth, not a live bug fix: it is
118
+ currently unreachable, not currently wrong. It is added anyway so this
119
+ shared-shaped helper does not repeat the exact assumption ("my caller
120
+ surely guarded this") that let the bare idiom survive unnoticed at both
121
+ `presets_cmd.core_type_lookup` and `kconfig._emit_inference` (tan-cli#962)
122
+ -- a future caller of this function gets the same backstop those two
123
+ now have, instead of inheriting the crash-or-leak choice again.
124
+ """
125
+ t = core_type.lower() if isinstance(core_type, str) else ""
126
+ if t.startswith("cortex-a"):
127
+ return "linux"
128
+ if t.startswith("cortex-m"):
129
+ return "rtos"
130
+ return "other"
131
+
132
+
133
+ def _allowed_os_for_core(core_type: str, metadata_root: Path) -> list[str]:
134
+ """The os: values valid for this core: every runtime minus the other
135
+ class's OS -- e.g. Cortex-A -> [yocto, baremetal, off], Cortex-M ->
136
+ [zephyr, baremetal, off].
137
+
138
+ Delegates to `tan.core.os_class.allowed_os_for_core` (tan-cli#914) rather
139
+ than open-coding the same subtraction `_cross_class_os` performs: an
140
+ unresolved `core_type` (`""` -- `core_os_topology`'s own
141
+ `soc_types.get(core_id, "")`) degrades to `[]` there rather than the
142
+ plausible-but-wrong cross-class subtraction, the SAME degrade
143
+ `presets_cmd.allowed_os_lookup` needs -- see that function's docstring for
144
+ why the two must never drift apart on this again.
145
+
146
+ Upstream agrees: alp-sdk's `scripts/alp_orchestrate/topology.py` carries
147
+ the same `[]` guard for an unresolved `core_type` since alp-sdk#1852
148
+ (porting tan-cli#914 / tan-cli#957), so there is no divergence to record.
149
+ Do NOT reintroduce a `["baremetal", "off"]` guess here -- that is the
150
+ plausible-but-wrong answer #870/#914 exist to remove. This reaches
151
+ `core_os_topology`'s `allowed_os` field, which `test_planner_emit_parity`
152
+ pins byte-for-byte against the oracle.
153
+ """
154
+ return _allowed_os_for_core_shared(core_type, _core_os_choices(metadata_root))
155
+
156
+
157
+ def core_os_topology(project: "BoardProject") -> dict[str, Any]:
158
+ """Per-core OS facts for an IDE / tool (issue #95).
159
+
160
+ The runtime is *determined by the core class* -- Cortex-A -> Yocto (Linux),
161
+ Cortex-M -> Zephyr (RTOS) -- and is not user-selectable. For each resolved
162
+ core this reports its `runtime_class`, the class `default_os`, the
163
+ `effective_os` (after a `baremetal`/`off` override -- the only board.yaml
164
+ knobs), whether it is `enabled`, and the per-core `allowed_os` set (the
165
+ valid dropdown). Lets the Board Configurator show the SDK's selection +
166
+ the legal overrides instead of guessing or offering a cross-class OS.
167
+ """
168
+ # `c.get("type", "")` is UNVALIDATED against `soc-spec-v1.schema.json`'s
169
+ # own `"type": {"type": "string"}` -- the identical gap `presets_cmd.
170
+ # core_type_lookup` closed for tan-cli#957, same class, this call site
171
+ # pre-dating it. `core_type` below feeds `_runtime_class`/
172
+ # `_default_os_from_core_type`'s `(core_type or "").lower()` AND is
173
+ # written verbatim to the emitted `core_type` field, so a non-string
174
+ # here is an `AttributeError` (build/`--emit os-topology` abort) or a
175
+ # wire leak, same two failure modes. Normalises to the same `""`
176
+ # unresolved sentinel a missing `type`/entry already produces.
177
+ soc_types = {
178
+ c["id"]: c["type"] if isinstance(c.get("type"), str) else ""
179
+ for c in (project.soc_spec.get("cores") or []) if "id" in c
180
+ }
181
+ rows: list[dict[str, Any]] = []
182
+ for core_id, sl in sorted(project.cores.items()):
183
+ core_type = soc_types.get(core_id, "")
184
+ default_os = _default_os_from_core_type(core_type)
185
+ rows.append({
186
+ "core_id": core_id,
187
+ "core_type": core_type,
188
+ "runtime_class": _runtime_class(core_type),
189
+ "default_os": default_os,
190
+ "effective_os": sl.os,
191
+ "enabled": sl.os != "off",
192
+ "overridden": sl.os != default_os,
193
+ "allowed_os": _allowed_os_for_core(
194
+ core_type, project.effective_metadata_root()),
195
+ })
196
+ return {
197
+ "schema_version": 1,
198
+ "sku": project.sku,
199
+ "cores": rows,
200
+ }
201
+
202
+
203
+ def emit_os_topology(project: "BoardProject") -> str:
204
+ """JSON for `alp_project.py --emit os-topology` (see core_os_topology).
205
+
206
+ Sorted keys + a trailing newline so the output is byte-deterministic.
207
+ """
208
+ return json.dumps(core_os_topology(project), indent=2) + "\n"
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Cross-field validators (P2.1 + P2.3 of v0.6).
4
+
5
+ The post-parse consistency rules over a resolved BoardProject / its slices: the
6
+ curated-library + TLS-provider sets, boot-signing family support,
7
+ `_validate_consistency` (the big cross-field pass), and the per-slice OS-class /
8
+ loader-rule enforcement. Pure checks -- they read the model and raise
9
+ OrchestratorError; nothing here mutates or emits. Extracted as the #285 validate
10
+ seam. The OS-class taxonomy comes from topology.py.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING, Optional
19
+
20
+ from . import libraries as _library_layer
21
+ from .models import OrchestratorError, Slice
22
+ from .orchestrator import STOCK_IMAGE_APP, STOCK_SHIM_APP
23
+ from .paths import METADATA_ROOT, REPO
24
+ from .topology import _core_os_choices, _cross_class_os
25
+
26
+ if TYPE_CHECKING:
27
+ from .models import BoardProject # noqa: F401
28
+
29
+
30
+ def _curated_library_names(metadata_root: Path) -> frozenset[str]:
31
+ """The curated `libraries:` set, derived from the real registry instead
32
+ of hand-listed (alp-sdk #1197 WS6-c #610 §6): every
33
+ metadata/libraries/<name>.yaml manifest is reachable either via its
34
+ legacy token in metadata/library-aliases-v1.json
35
+ (`aliases: {token: canonical}`) or, absent an alias entry, via its own
36
+ name with hyphens folded to underscores. This is the same reachability
37
+ rule alp-sdk's scripts/check_library_registry.py `_registry_vs_curated`
38
+ cross-checks this set against, so the two can no longer drift the way the
39
+ old hand-maintained frozenset did (9 curated libraries silently missing).
40
+ Used by `_validate_consistency()` to reject `extra_libraries:` slugs
41
+ that collide with the curated set (the curated library MUST be
42
+ declared via `libraries:` -- the escape hatch is for non-curated
43
+ entries only).
44
+
45
+ *metadata_root* is the BOUND alp-sdk checkout's `metadata/` -- the
46
+ registry is a fact and facts did not relocate (ADR-0017)."""
47
+ libdir = metadata_root / "libraries"
48
+ if not libdir.is_dir():
49
+ return frozenset()
50
+ manifests = {p.stem for p in libdir.glob("*.yaml")}
51
+ alias_p = metadata_root / "library-aliases-v1.json"
52
+ aliases: dict[str, str] = {}
53
+ if alias_p.is_file():
54
+ doc = json.loads(alias_p.read_text(encoding="utf-8"))
55
+ aliases = doc.get("aliases") or {}
56
+ reverse_alias = {canonical: token for token, canonical in aliases.items()}
57
+ return frozenset(reverse_alias.get(m, m.replace("-", "_")) for m in manifests)
58
+
59
+
60
+ _CURATED_LIBRARIES: frozenset[str] = _curated_library_names(METADATA_ROOT)
61
+
62
+
63
+ def _boot_signing_supported_for_family(
64
+ family: str,
65
+ ) -> Optional[frozenset[str]]:
66
+ """Return the allowed `boot.signing.algorithm:` values for a SoM
67
+ family, or None when the family is unknown (caller accepts any
68
+ schema-enum value).
69
+
70
+ Per the v0.6 P2.3 cross-field-validator pass:
71
+ - Alif Ensemble (with OPTIGA Trust M as attestation root):
72
+ `ecdsa_p256`, `ed25519` only. RSA is excluded -- the OPTIGA
73
+ slot type paired with the AEN secure-boot flow is ECC-only.
74
+ - Renesas RZ/V2N (+ V2N + DEEPX): `ecdsa_p256`, `rsa2048`,
75
+ `rsa3072`. ed25519 is excluded (not in the U-Boot
76
+ fit-signature stack used on V2N today).
77
+ - Unknown families: None (validator falls through to the schema
78
+ enum -- don't block on missing capability data for
79
+ in-development presets).
80
+ """
81
+ f = (family or "").lower()
82
+ if f.startswith("alif-ensemble"):
83
+ return frozenset({"ecdsa_p256", "ed25519"})
84
+ if f.startswith("renesas-rzv2n"):
85
+ return frozenset({"ecdsa_p256", "rsa2048", "rsa3072"})
86
+ return None
87
+
88
+
89
+ # TLS providers acceptable for `cores.<id>.iot.tls: true` (rule 3).
90
+ # A library named `mbedtls` or `bearssl` in either `libraries:` or
91
+ # `extra_libraries:` satisfies the requirement.
92
+ _TLS_PROVIDERS: frozenset[str] = frozenset({"mbedtls", "bearssl"})
93
+
94
+
95
+ def _validate_consistency(project: "BoardProject") -> None:
96
+ """Cross-field validation that the schema can't express cleanly.
97
+
98
+ Runs after the project is fully assembled by `load_board_yaml()`.
99
+ Raises `OrchestratorError` for hard violations; emits stderr
100
+ `WARN: ...` lines for soft signals (rules 4 + 5).
101
+
102
+ Rules (v0.6 P2.3):
103
+
104
+ 1. `ota.provider: mender` requires at least one
105
+ `cores.<id>.os: yocto` slice. Reason: server-mode Mender is a
106
+ Yocto-only flow today; the Zephyr-side dispatch
107
+ (Mender-MCU-client) lands separately per ADR 0009.
108
+ 2. `boot.signing.algorithm:` must be in the SoM family's supported
109
+ set (see `_boot_signing_supported_for_family`). Unknown
110
+ families pass through (don't block on missing capability data).
111
+ 3. `cores.<id>.iot.tls: true` requires `libraries:` (curated,
112
+ project-wide OR `cores:`-scoped -- both channels checked via
113
+ `libraries.scoped_names`, #1359 follow-up) OR `extra_libraries:`
114
+ (open-set) to include `mbedtls` or `bearssl`.
115
+ 4. WARNING (not error): `cores.<id>.inference.default_arena_kib`
116
+ larger than `cores.<id>.memory.heap_kib`; inference may OOM.
117
+ 5. WARNING (not error): `cores.<id>.power.sleep_mode != disabled`
118
+ with no `wakeup_sources:` declared; device will sleep but can't
119
+ wake.
120
+
121
+ Also enforces the `extra_libraries:` invariants the schema can't
122
+ cleanly express (P2.1):
123
+
124
+ - Exactly one of `kconfig:` / `profile:` per entry.
125
+ - Names globally unique across every core's `extra_libraries:`.
126
+ - Names don't collide with the curated `libraries:` enum.
127
+ - `profile:` paths resolve to a real file (repo-relative).
128
+
129
+ The curated set is recomputed against `project.effective_metadata_root()`
130
+ (NOT the module-level `_CURATED_LIBRARIES`, which stays pinned to the
131
+ SDK's own in-tree metadata/ for `check_library_registry.py`'s
132
+ self-consistency gate) so an `--metadata-root` override's own curated
133
+ libraries collide correctly instead of the SDK's in-tree set (#1485).
134
+ """
135
+ curated_libraries = _curated_library_names(
136
+ project.effective_metadata_root())
137
+
138
+ # ---- extra_libraries invariants (P2.1) -----------------------
139
+ seen_names: dict[str, str] = {} # name -> first core_id seen on
140
+ for core_id, slice_ in project.cores.items():
141
+ for idx, entry in enumerate(slice_.extra_libraries):
142
+ name = entry.get("name")
143
+ if not isinstance(name, str) or not name:
144
+ raise OrchestratorError(
145
+ f"core '{core_id}' extra_libraries[{idx}]: missing "
146
+ f"or non-string `name:`")
147
+ has_kc = "kconfig" in entry and entry.get("kconfig") is not None
148
+ has_pf = "profile" in entry and entry.get("profile") is not None
149
+ if has_kc == has_pf:
150
+ # Both set OR both unset -- reject.
151
+ raise OrchestratorError(
152
+ f"core '{core_id}' extra_libraries entry '{name}' "
153
+ f"must declare exactly one of `kconfig:` or "
154
+ f"`profile:` (got "
155
+ f"{'both' if has_kc and has_pf else 'neither'}). "
156
+ f"Inline `kconfig:` is the fast path; `profile:` "
157
+ f"points at a hw-backends.yaml-style file.")
158
+ if name in curated_libraries:
159
+ raise OrchestratorError(
160
+ f"core '{core_id}' extra_libraries entry '{name}' "
161
+ f"collides with the curated `libraries:` enum; "
162
+ f"declare curated libraries via "
163
+ f"`cores.{core_id}.libraries:` instead.")
164
+ if name in seen_names:
165
+ raise OrchestratorError(
166
+ f"core '{core_id}' extra_libraries entry '{name}' "
167
+ f"collides with the same name on core "
168
+ f"'{seen_names[name]}'; extra_libraries names "
169
+ f"must be globally unique across all cores.")
170
+ seen_names[name] = core_id
171
+ if has_pf:
172
+ prof = entry.get("profile")
173
+ if not isinstance(prof, str) or not prof:
174
+ raise OrchestratorError(
175
+ f"core '{core_id}' extra_libraries entry "
176
+ f"'{name}' has non-string `profile:`")
177
+ try:
178
+ prof_path = (REPO / prof).resolve()
179
+ prof_is_file = prof_path.is_file()
180
+ except (OSError, RuntimeError) as e:
181
+ # A symlink loop (ELOOP) surfaces differently per
182
+ # platform: on POSIX, `Path.resolve()` detects the
183
+ # cycle itself and raises `RuntimeError` before
184
+ # `Path.is_file()` is ever reached; on Windows,
185
+ # `.resolve()` returns without raising and it's
186
+ # `Path.is_file()` that raises a plain `OSError`
187
+ # (`WinError 1920`, "The file cannot be accessed
188
+ # by the system") -- confirmed on both platforms
189
+ # against a real symlink loop, not a mock. `.is_file()`
190
+ # also re-raises `PermissionError` (an `OSError`
191
+ # subclass) on EACCES rather than swallowing it
192
+ # like ENOENT/ENOTDIR/EBADF/ELOOP -- alp-sdk#1961's
193
+ # "same defect class" note. Same treatment as every
194
+ # other extra_libraries invariant in this loop: a
195
+ # clean OrchestratorError a board.yaml load can
196
+ # report, not an unhandled crash out of the loader.
197
+ raise OrchestratorError(
198
+ f"core '{core_id}' extra_libraries entry "
199
+ f"'{name}' `profile: {prof}` could not be "
200
+ f"resolved: {e}") from e
201
+ if not prof_is_file:
202
+ raise OrchestratorError(
203
+ f"core '{core_id}' extra_libraries entry "
204
+ f"'{name}' `profile: {prof}` does not resolve "
205
+ f"to a file at {prof_path}")
206
+
207
+ # ---- Rule 1: ota.provider requires a compatible slice -------
208
+ # Provider-driven dispatch (ADR 0009 follow-up resolved):
209
+ # mender -> Yocto (server-mode) OR Zephyr (Mender-MCU-client)
210
+ # hawkbit -> Zephyr (upstream Hawkbit DDI client)
211
+ # mcumgr -> Zephyr (upstream MCUmgr / SMP)
212
+ ota = project.ota or {}
213
+ provider = (ota.get("provider") or "").lower() if isinstance(ota, dict) else ""
214
+ if provider == "mender":
215
+ has_target = any(s.os in ("yocto", "zephyr")
216
+ for s in project.cores.values())
217
+ if not has_target:
218
+ raise OrchestratorError(
219
+ "ota.provider: mender requires at least one "
220
+ "cores.<id>.os: yocto (server-mode Mender) OR "
221
+ "cores.<id>.os: zephyr (Mender-MCU-client); none "
222
+ "found. See ADR 0009 for the provider-driven "
223
+ "dispatch.")
224
+ elif provider == "hawkbit":
225
+ if not any(s.os == "zephyr" for s in project.cores.values()):
226
+ raise OrchestratorError(
227
+ "ota.provider: hawkbit requires at least one "
228
+ "cores.<id>.os: zephyr (Hawkbit DDI client is in "
229
+ "Zephyr upstream); none found.")
230
+ elif provider == "mcumgr":
231
+ if not any(s.os == "zephyr" for s in project.cores.values()):
232
+ raise OrchestratorError(
233
+ "ota.provider: mcumgr requires at least one "
234
+ "cores.<id>.os: zephyr (MCUmgr is Zephyr's SMP "
235
+ "transport for OTA); none found.")
236
+
237
+ # ---- Rule 2: boot.signing.algorithm supported by SoM family --
238
+ boot = project.boot or {}
239
+ sign = (boot.get("signing") or {}) if isinstance(boot, dict) else {}
240
+ algo = ((sign.get("algorithm") or "").lower()
241
+ if isinstance(sign, dict) else "")
242
+ if algo:
243
+ family = (project.som_preset.get("family") or "")
244
+ supported = _boot_signing_supported_for_family(family)
245
+ if supported is not None and algo not in supported:
246
+ raise OrchestratorError(
247
+ f"boot.signing.algorithm: {algo} is not supported by "
248
+ f"SoM family '{family}'; supported algorithms for "
249
+ f"this family: {sorted(supported)}. Edit "
250
+ f"boot.signing.algorithm or pick a SoM family that "
251
+ f"supports {algo}.")
252
+
253
+ # ---- Rule 3: iot.tls: true requires mbedtls/bearssl ----------
254
+ for core_id, slice_ in project.cores.items():
255
+ if not (slice_.iot or {}).get("tls"):
256
+ continue
257
+ libs = set(_library_layer.scoped_names(project, slice_=slice_))
258
+ extras = {e.get("name") for e in slice_.extra_libraries
259
+ if isinstance(e.get("name"), str)}
260
+ if not (libs & _TLS_PROVIDERS) and not (extras & _TLS_PROVIDERS):
261
+ raise OrchestratorError(
262
+ f"core '{core_id}' has iot.tls: true but neither "
263
+ f"`libraries:` nor `extra_libraries:` declares a TLS "
264
+ f"provider (mbedtls or bearssl). Add one of these "
265
+ f"to cores.{core_id}.libraries: (curated) or "
266
+ f"cores.{core_id}.extra_libraries: (open-set).")
267
+
268
+ # ---- Rule 4: inference arena > heap is a WARN ----------------
269
+ for core_id, slice_ in project.cores.items():
270
+ arena_kib = (slice_.inference or {}).get("default_arena_kib")
271
+ heap_kib = (slice_.memory or {}).get("heap_kib")
272
+ if (isinstance(arena_kib, int) and isinstance(heap_kib, int)
273
+ and arena_kib > heap_kib):
274
+ print(
275
+ f"WARN: cores.{core_id}.inference.default_arena_kib="
276
+ f"{arena_kib} > cores.{core_id}.memory.heap_kib="
277
+ f"{heap_kib}; inference may OOM",
278
+ file=sys.stderr,
279
+ )
280
+
281
+ # ---- Rule 5: sleep_mode != disabled with no wakeup_sources --
282
+ for core_id, slice_ in project.cores.items():
283
+ pwr = slice_.power or {}
284
+ sleep_mode = (pwr.get("sleep_mode") or "disabled").lower()
285
+ wake = pwr.get("wakeup_sources") or []
286
+ if sleep_mode != "disabled" and not wake:
287
+ print(
288
+ f"WARN: cores.{core_id}.power.sleep_mode={sleep_mode} "
289
+ f"but no wakeup_sources: declared; device will sleep "
290
+ f"but cannot wake",
291
+ file=sys.stderr,
292
+ )
293
+
294
+
295
+ def _enforce_os_matches_core_class(slice_: Slice, core_type: str) -> None:
296
+ """The runtime follows the core class and is not user-selectable
297
+ (issue #95): a Cortex-A core runs Yocto/Linux, a Cortex-M core runs
298
+ Zephyr/RTOS. A board.yaml may only disable a core (`off`) or drop it to
299
+ no-OS (`baremetal`); selecting the *other* class's OS is rejected.
300
+ """
301
+ if slice_.os in _cross_class_os(core_type):
302
+ raise OrchestratorError(
303
+ f"core '{slice_.core_id}' ({core_type or 'unclassified'}): its runtime "
304
+ f"is determined by the core class (Cortex-A -> Yocto/Linux, "
305
+ f"Cortex-M -> Zephyr/RTOS) and is not selectable. Set os: 'off' to "
306
+ f"disable it or 'baremetal' for no-OS firmware -- got os: '{slice_.os}'.")
307
+
308
+
309
+ def _enforce_baremetal_app_rule(slice_: Slice) -> None:
310
+ """`os: baremetal`'s own `app:` rule -- split out of
311
+ `_enforce_loader_rules` to keep that function under the 50-line
312
+ function-count budget (tan-cli#1103); same single-purpose-sibling shape
313
+ as `manifest.py`'s `_check_blob_formats`/`_check_schema_version`
314
+ (tan-cli#1074).
315
+
316
+ alp-sdk#1889: a baremetal core with no `app:` of its own still resolves
317
+ one -- `_resolve_topology_for_core` (loader.py) merges the SoM topology
318
+ default OVER a project entry that omits `app:`, and every topology
319
+ default is one of the two stock tokens checked below (`alp-stock-shim`
320
+ for Cortex-M, `alp-image-edge` for Cortex-A; see every
321
+ metadata/e1m_modules/<SKU>.yaml `topology:` block). Neither is a
322
+ bare-metal app, and there is no bare-metal-flavoured stock default
323
+ anywhere in the tree, so `not slice_.app` alone never caught this -- the
324
+ inherited token is truthy.
325
+
326
+ Before this refusal existed the slice was NOT silently dropped: it
327
+ reached the executor with a real, wrong-target build command (`cmake -S
328
+ .../alp-stock-shim -B .` for the Cortex-M case, a nonexistent
329
+ `alp-image-edge` source dir for the Cortex-A one) that failed
330
+ downstream with a confusing, off-target CMake error instead of this
331
+ explicit one."""
332
+ if slice_.app in (STOCK_SHIM_APP, STOCK_IMAGE_APP):
333
+ other_os = "zephyr" if slice_.app == STOCK_SHIM_APP else "yocto"
334
+ raise OrchestratorError(
335
+ f"core '{slice_.core_id}': os: baremetal requires `app:` "
336
+ f"pointing at a CMakeLists.txt directory -- `{slice_.app}` is "
337
+ f"the {other_os} stock default (whether inherited from the SoM "
338
+ f"topology preset when no `app:` was given, or set explicitly), "
339
+ f"and there is no bare-metal stock default to fall back to")
340
+ if not slice_.app:
341
+ raise OrchestratorError(
342
+ f"core '{slice_.core_id}': os: baremetal requires `app:` "
343
+ f"pointing at a CMakeLists.txt directory")
344
+
345
+
346
+ def _enforce_loader_rules(slice_: Slice, metadata_root: Path) -> None:
347
+ """Loader rules from spec §4.5: every non-off slice must declare
348
+ enough to actually build."""
349
+ if slice_.os == "off":
350
+ return
351
+ if slice_.os == "zephyr":
352
+ if not slice_.app:
353
+ raise OrchestratorError(
354
+ f"core '{slice_.core_id}': os: zephyr requires `app:` "
355
+ f"pointing at a prj.conf / CMakeLists.txt directory")
356
+ elif slice_.os == "baremetal":
357
+ _enforce_baremetal_app_rule(slice_)
358
+ elif slice_.os == "yocto":
359
+ if not slice_.app and not slice_.image:
360
+ raise OrchestratorError(
361
+ f"core '{slice_.core_id}': os: yocto requires either "
362
+ f"`app:` (custom recipe) or `image:` (stock recipe)")
363
+ elif slice_.os not in _core_os_choices(metadata_root):
364
+ raise OrchestratorError(
365
+ f"core '{slice_.core_id}': unknown os '{slice_.os}'")
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Shared "extent == aperture exactly" whole-device-alias predicate (alp-sdk#2073).
4
+
5
+ A `memory_map:` region whose resolved `[base, base + size)` extent equals
6
+ a SoC's declared on-die MRAM aperture exactly (e.g. `mram_main`, once its
7
+ `base` stops being the `"TBD"` sentinel) is the device itself, not a
8
+ partition inside it -- `atoc`/`mcuboot`/`he_slot0`/`hp_slot0`/`reserved`/
9
+ `storage` all subdivide the SAME window `mram_main` aliases.
10
+
11
+ Two independent callers need this exact predicate and must never drift on
12
+ it: `aperture.py`'s `classify_region()` / `is_partition_inside_aperture()`
13
+ (the aperture-tiling / flash-class checks, alp-sdk#1365 split B), and
14
+ `zephyr_board.py`'s `_aen_check_map_overlaps()` (the disjoint-slot0
15
+ overlap check, alp-sdk#2073).
16
+
17
+ RELOCATION NOTE -- this module keeps its own file for a DIFFERENT reason
18
+ than upstream's. Upstream `scripts/whole_device_alias.py` is flat and
19
+ package-free because `gen_zephyr_board.py` lives OUTSIDE the
20
+ `alp_orchestrate` package, so importing the predicate from
21
+ `alp_orchestrate.aperture` would drag in the whole orchestrator
22
+ (jsonschema, alp_project, alp_cli) and add a package-to-generator import
23
+ loop with `loader.py` / `secure.py`'s existing deferred import going the
24
+ other way. NEITHER hazard exists here: `aperture.py` and `zephyr_board.py`
25
+ are both inside `tan.planner`, so a plain `from .whole_device_alias import
26
+ ...` costs nothing and closes no loop. What DOES carry over is the reason
27
+ the predicate is shared at all -- one spelling, so the callers cannot
28
+ drift, which is the entire point of alp-sdk#2073.
29
+
30
+ That is also why this is NOT inlined the way `sentinels.is_tbd` was
31
+ relocated into `zephyr_board.py::_is_tbd`. That inline stays within ONE
32
+ module (six call sites, all inside `zephyr_board.py`); this predicate is
33
+ needed by TWO, so inlining it would mean two copies in two files.
34
+ And `is_tbd` is the warning here, not the model: tan already carries a
35
+ SECOND, independent copy of that same predicate at `slugs.py:108`
36
+ (imported by `kconfig.py`), so the relocated spelling did not stay single
37
+ even inside tan. Keeping exactly one spelling of THIS predicate is the
38
+ whole point of alp-sdk#2073.
39
+
40
+ Upstream's docstring also records that `check_atoc_reservation.py` still
41
+ hand-writes this same comparison instead of importing it. That gap is
42
+ alp-sdk's alone -- tan relocates no counterpart of that gate, so the
43
+ three call sites named above (`aperture.py`'s two, `zephyr_board.py`'s
44
+ one) are the whole surface here.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+
50
+ def is_whole_device_alias(
51
+ ext: tuple[int, int], aperture: tuple[int, int],
52
+ ) -> bool:
53
+ """True when *ext* equals *aperture* exactly -- the whole-device alias
54
+ case, not a partition inside the device.
55
+
56
+ `classify_region()` and `is_partition_inside_aperture()` both test
57
+ this FIRST, before their subset-containment check, because the
58
+ alias's own extent also satisfies `lo >= full_lo and hi <= full_hi`
59
+ -- order matters, not just the predicate. Comparing BOTH edges is
60
+ the whole point: a region merely flush with the aperture's low edge
61
+ (e.g. `mcuboot`, same `base` as `mram_main`, far smaller `size_kib`)
62
+ must NOT match here, or a genuine overlap at that edge would be
63
+ silently excluded from the overlap comparison instead of refused.
64
+ """
65
+ lo, hi = ext
66
+ full_lo, full_hi = aperture
67
+ return lo == full_lo and hi == full_hi