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,1160 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Storage-partition resolver -- board.yaml `storage:` entries -> resolved partitions.
4
+
5
+ Resolves each StorageEntry to a ResolvedPartition: picks the flash device (the
6
+ SoM memory_map region names + on_module.ospi_memories keys), computes base/size
7
+ with page alignment, and flags overlaps. An entry that can't resolve (unknown
8
+ device / no size) becomes a blocked ResolvedPartition. Extracted from
9
+ alp_orchestrate as the #285 partition seam.
10
+
11
+ Depends only downward -- models (which carries the metadata root the project
12
+ was resolved against, tan-cli#573), memregion (_PAGE /
13
+ _region_size_bytes), and som_metadata.resolve_memory_map; nothing calls back into
14
+ the package __init__.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+ from typing import Any, Optional
22
+
23
+ from .aperture import is_partition_inside_aperture, resolve_aperture
24
+ from .memregion import _PAGE, _region_size_bytes
25
+ from .models import BoardProject, ResolvedPartition, StorageEntry
26
+ from .som_metadata import resolve_memory_map
27
+
28
+ # Sentinel distinguishing "caller has no hoisted aperture yet, resolve it"
29
+ # from a genuinely-resolved `aperture is None` ("this SoC declares none").
30
+ # `resolve_storage_partitions()` resolves the aperture ONCE and threads it
31
+ # through every helper below (the way `carveout.py` hoists it once per
32
+ # `resolve_carve_outs()` call) instead of each helper -- and each region a
33
+ # helper loops over -- re-parsing the SoC JSON and re-resolving the silicon
34
+ # variant on its own. Callers outside this module (`loader.py`'s eager
35
+ # cross-check, and the direct-call pytest suite) don't pass `aperture` at
36
+ # all, so they keep resolving it themselves via this sentinel's default --
37
+ # unchanged behaviour, just no longer redundant inside one resolve.
38
+ _APERTURE_UNSET: Any = object()
39
+
40
+ # `write_authority` value that means "writable by the application at
41
+ # runtime" -- the ONLY value a `memory_map` row may itself claim for a
42
+ # runtime mount (#2088). Same value, same rationale, as
43
+ # `check_atoc_reservation._RUNTIME_WRITABLE_AUTHORITY` (#2086); duplicated
44
+ # rather than imported since the two live in separate top-level scripts.
45
+ _RUNTIME_WRITABLE_AUTHORITY = "customer_runtime"
46
+
47
+ # `write_authority` value meaning "whole-device alias spanning contained
48
+ # rows of DIFFERENT authority -- consult the contained rows instead" (the
49
+ # schema's own words for `memory_region.write_authority`, `mram_main`'s tag
50
+ # on every AEN preset today). A composite row does NOT claim
51
+ # customer-runtime-writable for itself, but it is also not an unconditional
52
+ # pass: `_composite_alias_coverage_gap()` below is what actually "consults
53
+ # the contained rows" this value promises -- refusing the alias outright
54
+ # whenever a contained row can't be verified (unresolved base/size, absent
55
+ # write_authority, or a gap none of them cover). Treating `composite` the
56
+ # same as `customer_image`/`vendor_image`/`secure_enclave`/`none` --
57
+ # refusing it unconditionally -- would refuse `mram_main` on every AEN
58
+ # preset, the only whole-device alias any of them declares, even though it
59
+ # is the intended `flash_device:` target
60
+ # (docs/adr/0027-storage-regions-are-declared-by-role.md); this value
61
+ # exists so that shape can still be verified rather than rubber-stamped.
62
+ _DEFERRING_ALIAS_AUTHORITY = "composite"
63
+
64
+
65
+ def _is_ospi_key_unassembled(som_preset: dict[str, Any], key: str) -> bool:
66
+ """Does `som_preset.on_module.ospi_memories[key]` carry `assembled:
67
+ false` -- the schema's tag for an on-module OSPI part this SKU's PCB
68
+ doesn't actually populate (e.g. E1M-AEN801's `ospi0`/`ospi1`, #2311)?
69
+
70
+ Shared by every caller that must refuse an unfitted OSPI part rather
71
+ than treat its `capacity_mbit` (a property of the shared PCB footprint,
72
+ not evidence of assembly) as usable: `_known_flash_devices()` and
73
+ `_resolve_flash_device()` below, and `loader.py`'s
74
+ `storage[].flash_device` cross-field check and its
75
+ `security.psa.{its,ps}_storage` cross-check. Line-for-line port of
76
+ alp-sdk's `scripts/alp_orchestrate/partition.py::_is_ospi_key_unassembled`;
77
+ both sides move together (#2311).
78
+ """
79
+ om = som_preset.get("on_module") or {}
80
+ ospi = om.get("ospi_memories") or {}
81
+ if not isinstance(ospi, dict) or key not in ospi:
82
+ return False
83
+ entry = ospi[key] or {}
84
+ return isinstance(entry, dict) and entry.get("assembled") is False
85
+
86
+
87
+ def _resolve_aperture_arg(
88
+ som_preset: dict[str, Any],
89
+ metadata_root: Path,
90
+ aperture: Any,
91
+ ) -> Optional[tuple[int, int]]:
92
+ """Return `aperture` verbatim when a caller already hoisted it;
93
+ resolve it fresh only when `aperture` is the `_APERTURE_UNSET`
94
+ sentinel (no caller has resolved it yet for this call)."""
95
+ if aperture is _APERTURE_UNSET:
96
+ return resolve_aperture(som_preset, metadata_root)
97
+ return aperture
98
+
99
+
100
+ def _is_flash_sub_partition(
101
+ region: dict[str, Any],
102
+ som_preset: dict[str, Any],
103
+ metadata_root: Path,
104
+ aperture: Any = _APERTURE_UNSET,
105
+ ) -> bool:
106
+ """alp-sdk#1365 split B (P2): is `region` a partition INSIDE a flash
107
+ device, not a device of its own?
108
+
109
+ Derives the verdict against the SoC's declared on-die MRAM aperture
110
+ (`aperture.is_partition_inside_aperture`) instead of trusting the
111
+ legacy `carveout:` flag outright. Falls back to the legacy flag
112
+ verbatim whenever the derivation can't resolve a definite verdict --
113
+ no aperture declared for this SoC (every non-Alif SoM), this region's
114
+ own `base` still unresolved (e.g. AEN's `mram_main`), OR this region's
115
+ resolved extent lying OUTSIDE the declared aperture (containment is
116
+ one-sided: outside proves nothing, so it does not by itself mean
117
+ "this is a device", alp-sdk#1365 split B review MAJOR 3): this is what
118
+ keeps split B a no-op everywhere it can't prove a better answer.
119
+
120
+ `aperture` defaults to the `_APERTURE_UNSET` sentinel, resolving it
121
+ fresh per call; pass an already-resolved aperture to avoid re-parsing
122
+ the SoC JSON when a caller is looping over many regions/devices in one
123
+ resolve.
124
+ """
125
+ aperture = _resolve_aperture_arg(som_preset, metadata_root, aperture)
126
+ inside = is_partition_inside_aperture(region, aperture)
127
+ if inside is None:
128
+ return region.get("carveout") is False
129
+ return inside
130
+
131
+
132
+ def _known_flash_devices(
133
+ som_preset: dict[str, Any],
134
+ metadata_root: Path,
135
+ aperture: Any = _APERTURE_UNSET,
136
+ ) -> list[str]:
137
+ """Enumerate every flash-device name a `storage[].flash_device:` may
138
+ reference for the given SoM preset.
139
+
140
+ Today this is the union of:
141
+ - `memory_map:` region names (explicit override or derived from
142
+ the SoC variant), excluding any region `_is_flash_sub_partition()`
143
+ calls a partition INSIDE a flash-class node rather than a device
144
+ of its own (#1484) -- derived against the SoC's declared on-die
145
+ MRAM aperture, falling back to the legacy `carveout: false` flag
146
+ wherever containment can't prove a verdict (alp-sdk#1365 split B),
147
+ AND
148
+ - `on_module.ospi_memories:` keys (when declared on the SoM).
149
+
150
+ Kept as a list so the loader's "did you mean..." message can sort
151
+ it deterministically.
152
+
153
+ `aperture` defaults to `_APERTURE_UNSET`, resolving it once for this
154
+ call (rather than once per region, as a call embedded in the loop
155
+ used to); pass an already-resolved aperture to share it across many
156
+ calls in one resolve (see `_APERTURE_UNSET`'s module comment).
157
+ """
158
+ aperture = _resolve_aperture_arg(som_preset, metadata_root, aperture)
159
+ names: set[str] = set()
160
+ for region in resolve_memory_map(som_preset, metadata_root):
161
+ n = region.get("name")
162
+ if not isinstance(n, str):
163
+ continue
164
+ if _is_flash_sub_partition(region, som_preset, metadata_root, aperture):
165
+ # A flash-class SUB-region -- an MRAM partition living inside a
166
+ # `mram_storage`-class flash node, not a flash device of its own
167
+ # (see the schema's `memory_region.carveout` description). On
168
+ # AEN this is `mcuboot` / `he_slot0` / `hp_slot0` / `reserved` /
169
+ # `storage` / `atoc`: none of them carries a Devicetree label,
170
+ # and decorating one with a `partitions { }` child targets a
171
+ # node the board tree never defines (#1484). Keep it out of the
172
+ # advertised set and out of `_resolve_flash_device()` below.
173
+ # alp-sdk#1365 split B: derived against the SoC's declared
174
+ # on-die MRAM aperture (containment), not the legacy
175
+ # `carveout:` flag -- see `_is_flash_sub_partition()`.
176
+ continue
177
+ names.add(n)
178
+ om = som_preset.get("on_module") or {}
179
+ ospi = om.get("ospi_memories") or {}
180
+ if isinstance(ospi, dict):
181
+ for k in ospi:
182
+ if not isinstance(k, str):
183
+ continue
184
+ if _is_ospi_key_unassembled(som_preset, k):
185
+ # Not fitted on this SKU (e.g. E1M-AEN801's ospi0/ospi1) --
186
+ # don't advertise a device `_resolve_flash_device()` below
187
+ # will refuse anyway (alp-sdk#2311).
188
+ continue
189
+ names.add(k)
190
+ return sorted(names)
191
+
192
+
193
+ def _reserved_spans(
194
+ device_name: str,
195
+ capacity_bytes: int,
196
+ som_preset: dict[str, Any],
197
+ metadata_root: Path,
198
+ ) -> "tuple[list[tuple[int, int, str]], Optional[str]]":
199
+ """Return the SoM's own regions as DEVICE-RELATIVE [lo, hi) spans.
200
+
201
+ Returns `(spans, None)` on success, `([], reason)` when the spans can't
202
+ be computed safely -- the caller then falls back to sibling-only overlap
203
+ checking rather than trusting a guessed conversion (alp-sdk#1331).
204
+
205
+ Why this is needed at all: `storage[].offset_kib:` (and the bump
206
+ allocator) work in offsets WITHIN a flash device, while `memory_map:`
207
+ regions declare ABSOLUTE bases. Nothing previously bridged the two, so
208
+ every check was blind to the SoM's own layout -- on E1M-AEN801 that let a
209
+ littlefs mount resolve to offset 0 of `mram_main`, which is MCUboot, with
210
+ no `offset_kib:` involved and no `status: blocked`.
211
+
212
+ The device base is DERIVED, then VERIFIED, never assumed:
213
+
214
+ - If the device is itself a `memory_map:` region with an integer
215
+ `base`, that base is the origin -- authoritative, no further check
216
+ needed. On every AEN preset today this is the branch `mram_main`
217
+ takes: #2053 resolved its `base` to `0x80000000`, an ordinary
218
+ integer like any other region's.
219
+ - Otherwise (a device with no `base` of its own, e.g. a whole-window
220
+ alias whose `base:` is still an unresolved `"TBD"` placeholder --
221
+ no shipped AEN preset has this shape today; `mram_main` was the
222
+ standing example before #2053) take the lowest integer base among
223
+ the sibling regions and require `lowest + capacity == highest
224
+ region top`. That identity is what proves the alias really spans
225
+ the same window the fine-grained regions tile; if it does not
226
+ hold, refuse to convert. This is the ONLY leg that ever ran the
227
+ identity check -- it exists to substitute for a missing origin,
228
+ so it is unreachable (and its absence is safe, not a gap) once
229
+ the device carries a real `base` and takes the branch above
230
+ instead.
231
+
232
+ `_resolve_flash_device`'s descriptor deliberately carries no physical
233
+ base (Zephyr's flash-mapping layer derives it from the DT controller
234
+ node), which is why the origin is reconstructed here instead of being
235
+ read off the descriptor.
236
+ """
237
+ regions = [r for r in resolve_memory_map(som_preset, metadata_root)
238
+ if isinstance(r, dict)]
239
+ sized = [(r["base"], r["base"] + (_region_size_bytes(r) or 0), str(r.get("name")))
240
+ for r in regions
241
+ if isinstance(r.get("base"), int) and _region_size_bytes(r)]
242
+ if not sized:
243
+ return [], (f"SoM declares no addressed memory_map region, so "
244
+ f"'{device_name}' offsets cannot be checked against it")
245
+
246
+ self_region = next((r for r in regions if r.get("name") == device_name), None)
247
+ if self_region is not None and isinstance(self_region.get("base"), int):
248
+ origin = self_region["base"]
249
+ else:
250
+ origin = min(lo for lo, _, _ in sized)
251
+ window_top = max(hi for _, hi, _ in sized)
252
+ if origin + capacity_bytes != window_top:
253
+ return [], (
254
+ f"flash device '{device_name}' has no declared base and its "
255
+ f"capacity ({capacity_bytes} B) does not span the addressed "
256
+ f"memory_map window (0x{origin:x}..0x{window_top:x}), so its "
257
+ f"offsets cannot be mapped onto the SoM's regions")
258
+
259
+ spans: "list[tuple[int, int, str]]" = []
260
+ for lo, hi, name in sized:
261
+ # The device itself is not an obstacle inside itself; nor is a region
262
+ # that spans the whole window (a coarse alias) -- otherwise every
263
+ # partition would "overlap" it and nothing could ever be placed.
264
+ if name == device_name or (lo <= origin and hi - lo >= capacity_bytes):
265
+ continue
266
+ rel_lo, rel_hi = lo - origin, hi - origin
267
+ # Clip to the device; a region outside it is simply not our problem.
268
+ rel_lo, rel_hi = max(rel_lo, 0), min(rel_hi, capacity_bytes)
269
+ if rel_hi > rel_lo:
270
+ spans.append((rel_lo, rel_hi, name))
271
+ return sorted(spans), None
272
+
273
+
274
+ def _first_free(
275
+ base_bytes: int,
276
+ size_aligned: int,
277
+ allocated: "list[tuple[int, int, str]]",
278
+ capacity_bytes: int,
279
+ ) -> int:
280
+ """Advance `base_bytes` past any span it collides with, page-aligned.
281
+
282
+ The bump allocator must allocate AROUND the SoM's reserved regions, not
283
+ merely notice a collision: refusing instead of skipping would break the
284
+ ordinary case (a project that names no addresses at all) rather than fix
285
+ it. Returns the first non-colliding page-aligned base, or a value past
286
+ `capacity_bytes` when the device has no room left -- the caller reports
287
+ that as a blocked entry.
288
+ """
289
+ moved = True
290
+ while moved:
291
+ moved = False
292
+ for lo, hi, _ in sorted(allocated):
293
+ if base_bytes < hi and lo < base_bytes + size_aligned:
294
+ base_bytes = ((hi + _PAGE - 1) // _PAGE) * _PAGE
295
+ moved = True
296
+ if base_bytes >= capacity_bytes:
297
+ return base_bytes
298
+ return base_bytes
299
+
300
+
301
+ def _composite_alias_coverage_gap(
302
+ alias_name: str,
303
+ alias_base: Any,
304
+ capacity_bytes: int,
305
+ som_preset: dict[str, Any],
306
+ metadata_root: Path,
307
+ aperture: Optional[tuple[int, int]],
308
+ ) -> Optional[str]:
309
+ """#2088: what "`write_authority: composite` means consult the
310
+ contained rows" actually has to DO, not just claim.
311
+
312
+ Returns `None` when the `memory_map:` rows CONTAINED in
313
+ `alias_name`'s own `[origin, origin+capacity_bytes)` window --
314
+ resolved, `write_authority`-bearing, non-overlapping -- CONTIGUOUSLY
315
+ tile that window with no gap (review round 2, Major 1: an earlier
316
+ version of this function summed sizes rather than walking spans, so
317
+ an overlap of N bytes plus a hole of N bytes passed the sum check
318
+ while the hole itself stayed unprotected -- reproduced by deleting
319
+ `atoc` and widening `he_slot0` by exactly `atoc`'s size). Returns a
320
+ refusal reason otherwise, naming the gap or the overlapping pair.
321
+
322
+ A row whose resolved extent lies OUTSIDE the window (a genuinely
323
+ unrelated device that merely happens to share this SoM's
324
+ `memory_map:` list) is ignored -- containment, not mere co-listing,
325
+ is what makes a row this alias's business.
326
+
327
+ A row whose base or size DOESN'T resolve is not immediately fatal on
328
+ its own (review round 2, Major 3: an earlier version refused for
329
+ ANY such row unconditionally, which also refused a genuinely
330
+ unrelated device parked at a TBD-but-clearly-different address
331
+ purely because its address wasn't resolved yet -- indistinguishable
332
+ from `atoc`'s real pre-HW-mapped shape without more information).
333
+ Instead: walk only the rows that DO resolve; if they alone already
334
+ tile the window with no gap, an unresolved-base sibling can only be
335
+ something else entirely (resolving it into an overlap would be a
336
+ SEPARATE metadata bug for a different gate to catch, not evidence
337
+ this alias is unsafe today) -- but if a gap remains, an unresolved
338
+ sibling is named as a candidate that might explain it (never ruled
339
+ out, per ADR-0034 clause 4), while an alias with NO unresolved
340
+ sibling AND a gap has nothing left to blame but a genuinely missing
341
+ row.
342
+
343
+ `origin` is `alias_base` when the alias's own base has resolved;
344
+ else `aperture[0]` when an aperture resolves for this SoM (`mram_main`
345
+ itself carries `base: "TBD"` on every AEN preset today, so this is
346
+ the common case, not the exception -- using the SoC's own declared
347
+ aperture start here, rather than the lowest resolved sibling base,
348
+ avoids anchoring the window at a fabricated address when a resolved
349
+ but genuinely-unrelated sibling happens to sit below every real
350
+ contained row, review round 2 Minor); else the lowest resolved base
351
+ among sibling rows, for a non-Alif SoM with no aperture to anchor to.
352
+
353
+ Without this whole function, a sibling with an unresolved `base` or
354
+ a sibling missing from `memory_map:` altogether contributes NO span
355
+ to `_reserved_spans()`, which reserves only what it can see -- so
356
+ the bump allocator treats the unaccounted band as free. On
357
+ `mram_main`, that band can be the Secure-Enclave-owned `atoc` window
358
+ (bench-observed live at 0x80578000..0x80580000, `carveout.py:100-104`).
359
+ `aperture` gates the same absent-write_authority leniency
360
+ `_resolve_flash_device()`'s own guard applies just above this call --
361
+ a non-Alif sibling with no `write_authority` at all is not itself
362
+ the hazard.
363
+ """
364
+ siblings = [r for r in resolve_memory_map(som_preset, metadata_root)
365
+ if isinstance(r, dict) and r.get("name") != alias_name]
366
+
367
+ def _resolved_int(value: Any) -> bool:
368
+ return isinstance(value, int) and not isinstance(value, bool)
369
+
370
+ if _resolved_int(alias_base):
371
+ origin = alias_base
372
+ elif aperture is not None:
373
+ origin = aperture[0]
374
+ else:
375
+ resolved_bases = [r["base"] for r in siblings if _resolved_int(r.get("base"))]
376
+ if not resolved_bases:
377
+ return (
378
+ f"flash device '{alias_name}' declares "
379
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on SoM "
380
+ f"{som_preset.get('sku', '<unknown>')}, which defers "
381
+ f"eligibility to its contained memory_map rows -- but "
382
+ f"neither '{alias_name}' itself nor any sibling row has a "
383
+ f"resolved base, and no SoC aperture resolves either, so "
384
+ f"its window cannot be anchored to verify anything inside "
385
+ f"it. Resolve at least one memory_map row's base before "
386
+ f"targeting '{alias_name}' with a storage[].flash_device: "
387
+ f"(#2088)")
388
+ origin = min(resolved_bases)
389
+ top = origin + capacity_bytes
390
+
391
+ contained: "list[tuple[int, int, str]]" = []
392
+ ambiguous_names: "list[str]" = []
393
+ for region in siblings:
394
+ name = region.get("name")
395
+ base = region.get("base")
396
+ if not _resolved_int(base):
397
+ ambiguous_names.append(str(name))
398
+ continue
399
+ size_bytes = _region_size_bytes(region)
400
+ if size_bytes is None:
401
+ ambiguous_names.append(str(name))
402
+ continue
403
+ hi = base + size_bytes
404
+ if base >= top or hi <= origin:
405
+ continue # Disjoint -- a different device, not this alias's business.
406
+ if not (base >= origin and hi <= top):
407
+ # Partial overlap with the WINDOW boundary -- neither cleanly
408
+ # inside nor cleanly outside. Rare/malformed; refuse rather
409
+ # than guess which side of the boundary the bytes belong to.
410
+ return (
411
+ f"flash device '{alias_name}' declares "
412
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on SoM "
413
+ f"{som_preset.get('sku', '<unknown>')}, which defers "
414
+ f"eligibility to its contained memory_map rows -- but "
415
+ f"region {name!r} [0x{base:x}, 0x{hi:x}) only partially "
416
+ f"overlaps '{alias_name}'s 0x{origin:x}..0x{top:x} "
417
+ f"window, so it can't be cleanly attributed as contained "
418
+ f"or disjoint. Fix the addresses before targeting "
419
+ f"'{alias_name}' with a storage[].flash_device: (#2088)")
420
+ wa = region.get("write_authority")
421
+ if wa is None and aperture is not None:
422
+ return (
423
+ f"flash device '{alias_name}' declares "
424
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on SoM "
425
+ f"{som_preset.get('sku', '<unknown>')}, which defers "
426
+ f"eligibility to its contained memory_map rows -- but "
427
+ f"region {name!r}, contained in '{alias_name}'s window, "
428
+ f"carries no write_authority, so whether it may host a "
429
+ f"runtime mount cannot be verified. Author "
430
+ f"write_authority: on {name!r} before targeting "
431
+ f"'{alias_name}' with a storage[].flash_device: (#2088)")
432
+ contained.append((base, hi, str(name)))
433
+
434
+ # Walk the CONTAINED, resolved spans in address order and require
435
+ # exact contiguity -- an overlap of N bytes plus a hole of N bytes
436
+ # must not cancel out in a sum (review round 2, Major 1).
437
+ contained.sort()
438
+ cursor = origin
439
+ prev_name = None
440
+ for lo, hi, name in contained:
441
+ if lo < cursor:
442
+ return (
443
+ f"flash device '{alias_name}' declares "
444
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on SoM "
445
+ f"{som_preset.get('sku', '<unknown>')}, which defers "
446
+ f"eligibility to its contained memory_map rows -- but "
447
+ f"region {name!r} [0x{lo:x}, 0x{hi:x}) overlaps "
448
+ f"{prev_name!r}, which ends at 0x{cursor:x}, by "
449
+ f"{(cursor - lo) // 1024} KiB. Two rows claiming the same "
450
+ f"bytes means at most one of their `write_authority` "
451
+ f"values actually applies there; fix the addresses before "
452
+ f"targeting '{alias_name}' with a storage[].flash_device: "
453
+ f"(#2088)")
454
+ if lo > cursor:
455
+ break # Gap -- reported below, naming any candidate that might fill it.
456
+ cursor = hi
457
+ prev_name = name
458
+
459
+ if cursor < top:
460
+ gap_lo, gap_hi = cursor, top
461
+ candidate_note = (
462
+ f" -- {', '.join(sorted(ambiguous_names))} carries an "
463
+ f"unresolved base or size and cannot be ruled out as the "
464
+ f"row that belongs there"
465
+ if ambiguous_names else
466
+ f" and no memory_map row with an unresolved base or size "
467
+ f"exists either, so this is a genuinely undeclared row")
468
+ return (
469
+ f"flash device '{alias_name}' declares "
470
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on SoM "
471
+ f"{som_preset.get('sku', '<unknown>')} "
472
+ f"({capacity_bytes // 1024} KiB, window 0x{origin:x}.."
473
+ f"0x{top:x}), but its contained memory_map rows leave a gap "
474
+ f"at 0x{gap_lo:x}..0x{gap_hi:x} ({(gap_hi - gap_lo) // 1024} "
475
+ f"KiB) unaccounted for{candidate_note}. Declare the missing "
476
+ f"row(s) before targeting '{alias_name}' with a "
477
+ f"storage[].flash_device: (#2088)")
478
+ return None
479
+
480
+
481
+ def _resolve_flash_device(
482
+ flash_device: str,
483
+ som_preset: dict[str, Any],
484
+ metadata_root: Path,
485
+ aperture: Any = _APERTURE_UNSET,
486
+ ) -> tuple[Optional[dict[str, Any]], Optional[str]]:
487
+ """Resolve a board.yaml `flash_device:` reference to a device descriptor.
488
+
489
+ Returns `(descriptor, None)` on success; `(None, reason)` when the
490
+ device is known but cannot be allocated against (TBD capacity).
491
+ The descriptor carries:
492
+ name -- the SDK device name (verbatim from board.yaml)
493
+ dt_label -- the Zephyr DT label to decorate
494
+ size_bytes -- capacity in bytes (None when unresolvable)
495
+
496
+ Address allocation is offset-within-device; the descriptor does
497
+ NOT carry a physical base because Zephyr's flash-mapping layer
498
+ derives that from the DT controller node. The emitted overlay
499
+ references `&<dt_label>` and lets Zephyr handle the physical mapping.
500
+
501
+ `aperture` defaults to `_APERTURE_UNSET` (resolve fresh); pass an
502
+ already-resolved aperture to share it across many calls in one
503
+ resolve (see `_APERTURE_UNSET`'s module comment).
504
+ """
505
+ aperture = _resolve_aperture_arg(som_preset, metadata_root, aperture)
506
+ # memory_map: region match first (covers the auto-derived MRAM /
507
+ # SRAM region table from resolve_memory_map()).
508
+ for region in resolve_memory_map(som_preset, metadata_root):
509
+ if region.get("name") != flash_device:
510
+ continue
511
+ if _is_flash_sub_partition(region, som_preset, metadata_root, aperture):
512
+ # Defense in depth for a hand-built project that skips the
513
+ # loader's `_known_flash_devices()` check: refuse rather than
514
+ # fabricate a `dt_label` from the region name. This region is
515
+ # a partition INSIDE a flash-class node (e.g. AEN's
516
+ # `mram_storage`), not a flash device -- it has no Devicetree
517
+ # label of its own and a `partitions { }` child on it targets
518
+ # a node the board tree never defines (#1484). alp-sdk#1365
519
+ # split B: derived against the declared MRAM aperture, not the
520
+ # legacy `carveout:` flag -- see `_is_flash_sub_partition()`.
521
+ return None, (
522
+ f"flash device '{flash_device}' is a partition inside a "
523
+ f"flash-class region on SoM "
524
+ f"{som_preset.get('sku', '<unknown>')}, not a flash device "
525
+ f"of its own -- it has no Devicetree label and cannot take "
526
+ f"a `partitions {{ }}` child")
527
+ # #2088: a resolved memory_map region is not automatically
528
+ # customer-writable at runtime -- that includes a whole-device
529
+ # alias (e.g. `mram_main`), which `_is_flash_sub_partition()`
530
+ # above correctly does NOT treat as a sub-partition, so it would
531
+ # otherwise fall straight through to a resolved descriptor. Per
532
+ # the schema (`memory_region.write_authority`), a row may take a
533
+ # runtime mount only when it claims `customer_runtime` directly,
534
+ # OR defers as a `composite` alias (see `_DEFERRING_ALIAS_AUTHORITY`
535
+ # above) AND every contained row it defers to is independently
536
+ # verified safe below -- every other AUTHORED value
537
+ # (customer_image/vendor_image/secure_enclave/none) means
538
+ # something else owns writes to this region and refuses outright.
539
+ wa = region.get("write_authority")
540
+ if wa is not None and wa not in (
541
+ _RUNTIME_WRITABLE_AUTHORITY, _DEFERRING_ALIAS_AUTHORITY):
542
+ return None, (
543
+ f"flash device '{flash_device}' declares "
544
+ f"write_authority: {wa!r} on SoM "
545
+ f"{som_preset.get('sku', '<unknown>')}, which is not "
546
+ f"customer-writable at runtime -- only a region declaring "
547
+ f"write_authority: {_RUNTIME_WRITABLE_AUTHORITY!r} (or "
548
+ f"{_DEFERRING_ALIAS_AUTHORITY!r} for a whole-device alias "
549
+ f"that defers to contained rows) may take a runtime mount "
550
+ f"(storage[].flash_device:). Either give '{flash_device}' "
551
+ f"write_authority: {_RUNTIME_WRITABLE_AUTHORITY!r} if it "
552
+ f"truly is the customer's runtime storage, or point "
553
+ f"flash_device: at the region that is (#2088)")
554
+ # An ABSENT value is different: the schema and ADR-0034 clause 4
555
+ # both say absent means UNRESOLVED, never `customer_runtime` -- "a
556
+ # consumer must treat an authored region carrying no
557
+ # write_authority as ineligible for ... runtime write ... rather
558
+ # than defaulting permissively" (`docs/porting-new-som.md`
559
+ # repeats this verbatim). `carveout.py`'s equivalent guard
560
+ # (`_region_ipc_eligibility`'s "unclassified"/"unresolved" legs)
561
+ # enforces this the same way -- refuse -- but ONLY once an
562
+ # on-die MRAM aperture actually resolves for this SoM (every
563
+ # non-Alif SoM, and any Alif SoC/variant that hasn't declared
564
+ # one, short-circuits before this rule even runs -- see
565
+ # `_candidate_regions()`). Mirrored here: refusing
566
+ # unconditionally would fire on every V2N/V2M preset's
567
+ # `memory_map:` rows, none of which author `write_authority` at
568
+ # all and none of which a customer can fix by authoring a field
569
+ # this guard doesn't even apply to.
570
+ if wa is None and aperture is not None:
571
+ return None, (
572
+ f"flash device '{flash_device}' on SoM "
573
+ f"{som_preset.get('sku', '<unknown>')} carries no "
574
+ f"write_authority -- ADR-0034 clause 4 and the schema "
575
+ f"both say an absent value means unresolved, not "
576
+ f"'customer_runtime': treat it as ineligible for a "
577
+ f"runtime write rather than defaulting permissively. "
578
+ f"Author write_authority: {_RUNTIME_WRITABLE_AUTHORITY!r} "
579
+ f"on '{flash_device}' if it is meant to take a runtime "
580
+ f"mount, or point flash_device: at a region that already "
581
+ f"declares it (#2088)")
582
+ size_bytes = _region_size_bytes(region)
583
+ if size_bytes is None:
584
+ return None, (
585
+ f"flash device '{flash_device}' resolves to memory_map "
586
+ f"region but size_mib / size_kib is unset or TBD on "
587
+ f"SoM {som_preset.get('sku', '<unknown>')}; the SoM "
588
+ f"hasn't been HW-mapped yet")
589
+ if wa == _DEFERRING_ALIAS_AUTHORITY:
590
+ # `composite` means "consult the contained rows instead" --
591
+ # so actually consult them, rather than trusting the alias's
592
+ # own tag as a pass. Every OTHER row in this SoM's memory_map
593
+ # must resolve to a concrete [base, base+size) span AND
594
+ # declare its own write_authority, and together they must
595
+ # fully tile this alias's capacity, before the alias can be
596
+ # called safe: a row this can't account for (unresolved base
597
+ # or size, absent authority, or a gap none of them cover) is
598
+ # exactly the #2088 hazard (a storage[] entry silently
599
+ # landing in the unaccounted-for band, e.g. the SE-owned
600
+ # `atoc` window when its base is still TBD) -- refuse the
601
+ # whole alias rather than resolve permissively around a hole
602
+ # this function can't see the shape of.
603
+ gap_reason = _composite_alias_coverage_gap(
604
+ flash_device, region.get("base"), size_bytes, som_preset,
605
+ metadata_root, aperture)
606
+ if gap_reason is not None:
607
+ return None, gap_reason
608
+ # #2088 review round 2, Major 2: the coverage-gap walk above
609
+ # proves the CONTAINED rows are individually sound, but
610
+ # placement itself runs through `_reserved_spans()` -- a
611
+ # SEPARATE, older function with its own (weaker) origin/
612
+ # window derivation. An out-of-window sibling (correctly
613
+ # ignored as disjoint above) can still push
614
+ # `_reserved_spans()`'s own `window_top = max(hi for sized)`
615
+ # past this alias's real top, failing its
616
+ # `origin + capacity == window_top` identity check and
617
+ # degrading it to `([], reason)` -- silently reserving
618
+ # NOTHING, including `mcuboot`, for any caller that placed a
619
+ # storage[] entry here. An alias whose contained rows can't
620
+ # actually be RESERVED is exactly as unconsultable as one
621
+ # whose rows can't be VERIFIED (the same principle already
622
+ # applied to an unresolved base above) -- refuse rather than
623
+ # let a caller believe the window this function just proved
624
+ # safe stays protected at placement time.
625
+ _reserved_probe, reserved_degraded_reason = _reserved_spans(
626
+ flash_device, size_bytes, som_preset, metadata_root)
627
+ if reserved_degraded_reason is not None:
628
+ return None, (
629
+ f"flash device '{flash_device}' declares "
630
+ f"write_authority: {_DEFERRING_ALIAS_AUTHORITY!r} on "
631
+ f"SoM {som_preset.get('sku', '<unknown>')} -- its "
632
+ f"contained memory_map rows verify safe, but "
633
+ f"{reserved_degraded_reason}, so placement cannot "
634
+ f"actually reserve them: an alias whose contained "
635
+ f"rows can't be reserved can't be consulted either "
636
+ f"(#2088)")
637
+ dt_label = (region.get("dt_label")
638
+ if isinstance(region.get("dt_label"), str)
639
+ else flash_device)
640
+ return {
641
+ "name": flash_device,
642
+ "dt_label": dt_label,
643
+ "size_bytes": size_bytes,
644
+ }, None
645
+
646
+ # on_module.ospi_memories: key match.
647
+ #
648
+ # alp-sdk#2311: refuse a device this SKU's preset declares
649
+ # `assembled: false` (e.g. `E1M-AEN801`'s `ospi0`/`ospi1`) BEFORE
650
+ # looking at `capacity_mbit` -- `capacity_mbit` is the DESIGNED-IN
651
+ # part's capacity (present on the shared PCB footprint), not evidence
652
+ # the module actually carries the part. `_known_flash_devices()` above
653
+ # already keeps an unassembled device out of the advertised set; this
654
+ # is the same guard applied where a hand-built project (or a stale
655
+ # caller) skips that check and calls this resolver directly (defense
656
+ # in depth). `optional` and an absent key (schema default: true) both
657
+ # still resolve -- `assembled` must be the literal `False` the schema
658
+ # uses for "not fitted".
659
+ #
660
+ # Ported line-for-line from alp-sdk's
661
+ # scripts/alp_orchestrate/partition.py::_resolve_flash_device; both
662
+ # sides move together (alp-sdk#2311).
663
+ om = som_preset.get("on_module") or {}
664
+ ospi = om.get("ospi_memories") or {}
665
+ if isinstance(ospi, dict) and flash_device in ospi:
666
+ entry = ospi[flash_device] or {}
667
+ if _is_ospi_key_unassembled(som_preset, flash_device):
668
+ sku = som_preset.get("sku")
669
+ where = f"SoM {sku}" if sku else "this SoM"
670
+ yaml_ref = (f"metadata/e1m_modules/{sku}.yaml" if sku
671
+ else "its SoM preset YAML")
672
+ return None, (
673
+ f"flash device '{flash_device}' is not assembled on "
674
+ f"{where} (assembled: false in {yaml_ref}); it cannot "
675
+ f"take a runtime storage mount")
676
+ cap = entry.get("capacity_mbit")
677
+ if isinstance(cap, str) and cap.strip().upper() == "TBD":
678
+ return None, (
679
+ f"on_module.ospi_memories.{flash_device}.capacity_mbit "
680
+ f"is TBD on SoM {som_preset.get('sku', '<unknown>')}; "
681
+ f"fill the value or move the storage entry to a sized "
682
+ f"flash device")
683
+ if not isinstance(cap, int):
684
+ return None, (
685
+ f"on_module.ospi_memories.{flash_device}.capacity_mbit "
686
+ f"is missing on SoM {som_preset.get('sku', '<unknown>')}; "
687
+ f"the storage allocator needs an authoritative capacity")
688
+ dt_label = (entry.get("dt_label")
689
+ if isinstance(entry.get("dt_label"), str)
690
+ else flash_device)
691
+ # capacity_mbit is megabits; convert to bytes.
692
+ size_bytes = int(cap) * 1024 * 1024 // 8
693
+ return {
694
+ "name": flash_device,
695
+ "dt_label": dt_label,
696
+ "size_bytes": size_bytes,
697
+ }, None
698
+
699
+ # Unknown device — the loader catches this earlier, but defend
700
+ # in depth in case a resolver is called with a hand-built project.
701
+ return None, (
702
+ f"flash device '{flash_device}' is neither a memory_map region "
703
+ f"nor an on_module.ospi_memories key on SoM "
704
+ f"{som_preset.get('sku', '<unknown>')}")
705
+
706
+
707
+ def _has_real_dt_label(
708
+ flash_device: str,
709
+ som_preset: dict[str, Any],
710
+ metadata_root: Path,
711
+ ) -> bool:
712
+ """True when `flash_device`'s Devicetree label is KNOWN, not fabricated.
713
+
714
+ `_resolve_flash_device()` succeeding is NOT enough to recommend a device
715
+ to a customer: its `dt_label` DEFAULTS to the device name whenever the
716
+ metadata omits an explicit override, and that default is unverified
717
+ against the board `.dts` (see the schema's `memory_region.dt_label`
718
+ description -- AEN's `mram_main` region, for example, names its flash
719
+ node `mram_storage`, not `mram_main`). Emitting `&<name>` for one of
720
+ those is the exact defect #1484 closes.
721
+
722
+ An `on_module.ospi_memories:` key does NOT qualify, despite its name
723
+ matching a controller node's label 1:1 on paper. Measured against the
724
+ generated Zephyr board trees (#1484 re-review): `ospi0` is the ONLY
725
+ `ospi<n>` label that exists anywhere under `zephyr/`
726
+ (`zephyr/dts/alif/ensemble_e8_peripherals.dtsi:688`), it is included by
727
+ only the two E1M-AEN801 board `.dts` files, and there it is the OSPI
728
+ CONTROLLER node (`compatible = "snps,designware-ospi"`,
729
+ `status = "disabled"`) -- neither AEN801 board tree enables it or gives
730
+ it a flash-chip child, so a `fixed-partitions` overlay under `&ospi0`
731
+ would not be a working flash area even on the one SKU where the label
732
+ exists. E1M-AEN401/601 have a board tree with no `ospi0` node at all;
733
+ E1M-AEN301/501/701 have no board tree in `zephyr/boards/alp/` at all.
734
+ `scripts/gen_zephyr_board.py` never generates an `ospi<n>` node either.
735
+ So until a real per-instance DT label is wired up and verified against
736
+ an ENABLED node, no `ospi_memories:` key is trustworthy here.
737
+
738
+ Only trust a `memory_map:` region that carries an EXPLICIT `dt_label:`
739
+ override -- nothing does yet, so this currently returns `False` for
740
+ every device on every SoM. That is the correct interim answer: it means
741
+ `alt_devices` never recommends a target this resolver cannot verify,
742
+ rather than recommending one that merely LOOKS verified.
743
+
744
+ alp-sdk#1556: `resolve_storage_partitions()` now gates the PRIMARY
745
+ resolution on this same predicate for a `memory_map:`-sourced device
746
+ (not just the alt-device remedy) -- naming `flash_device: mram_main`
747
+ (or any other unverified `memory_map:` region) can no longer reach
748
+ `status: ok`; it blocks with a reason instead of fabricating a DT
749
+ label the board tree never defines. `on_module.ospi_memories:`
750
+ devices are NOT gated by that change (`is_ospi_device` in the
751
+ resolver still short-circuits this predicate to `True` for them) --
752
+ this function already returns `False` for every `ospi_memories:` key
753
+ (the loop below only ever matches a `memory_map:` region name), so
754
+ gating the primary path on it unconditionally would also block every
755
+ `ospi0`-targeting entry, including the ones the existing storage test
756
+ suite (`tests/scripts/test_orchestrate_storage*.py`) deliberately uses
757
+ as its "device-independent allocator mechanics" fixture. Verifying
758
+ `ospi_memories:` against its own real, per-instance DT label is a
759
+ separate gap this issue's repro does not cover (see this function's
760
+ own paragraph above) -- left as tracked follow-up, not silently
761
+ widened here.
762
+ """
763
+ for region in resolve_memory_map(som_preset, metadata_root):
764
+ if region.get("name") == flash_device:
765
+ return isinstance(region.get("dt_label"), str)
766
+ return False
767
+
768
+
769
+ def _verified_alt_devices(
770
+ exclude: str,
771
+ som_preset: dict[str, Any],
772
+ metadata_root: Path,
773
+ aperture: Any = _APERTURE_UNSET,
774
+ ) -> list[str]:
775
+ """Every known flash device other than `exclude` that both resolves
776
+ (`_resolve_flash_device()`) and carries a verified Devicetree label
777
+ (`_has_real_dt_label()`) -- the only kind of device safe to name back
778
+ to a customer as a `flash_device:` alternative (#1484 / #1556: naming
779
+ one that merely LOOKS verified reproduces the exact defect either
780
+ issue closes). Used by `resolve_storage_partitions()`'s #1556
781
+ unverified-dt_label block; kept separate from the near-identical
782
+ filter already inlined in the overlap-block remedy below (that one
783
+ also excludes the SoM's own reserved region names, which don't apply
784
+ here) rather than risk a shared-helper refactor rippling into that
785
+ already-reviewed code path.
786
+
787
+ `aperture` defaults to `_APERTURE_UNSET` (resolve fresh); pass an
788
+ already-resolved aperture to share it across many calls in one
789
+ resolve (see `_APERTURE_UNSET`'s module comment).
790
+ """
791
+ aperture = _resolve_aperture_arg(som_preset, metadata_root, aperture)
792
+ return sorted(
793
+ d for d in _known_flash_devices(som_preset, metadata_root, aperture)
794
+ if d != exclude
795
+ and _has_real_dt_label(d, som_preset, metadata_root)
796
+ and _resolve_flash_device(d, som_preset, metadata_root, aperture)[0] is not None)
797
+
798
+
799
+ def _blocked_partition(
800
+ entry: StorageEntry,
801
+ reason: str,
802
+ ) -> ResolvedPartition:
803
+ """Project a StorageEntry into a blocked ResolvedPartition."""
804
+ return ResolvedPartition(
805
+ name=entry.name,
806
+ fs=entry.fs,
807
+ flash_device=entry.flash_device or "",
808
+ dt_label="",
809
+ base_kib=0,
810
+ size_kib=entry.size_kib,
811
+ mount=entry.mount,
812
+ status="blocked",
813
+ reason=reason,
814
+ )
815
+
816
+
817
+ def resolve_storage_partitions(
818
+ project: BoardProject,
819
+ ) -> list[ResolvedPartition]:
820
+ """Allocate physical offsets for every storage[] entry.
821
+
822
+ Algorithm (mirrors `resolve_carve_outs()`):
823
+ 1. Group entries by `flash_device:` (entries without one block
824
+ immediately; the loader normally catches that, but this is
825
+ the resolver's failure mode for hand-built projects).
826
+ 2. Within each group, sort by name for determinism (an explicit
827
+ `offset_kib:` doesn't change sort order — it's an override
828
+ the allocator simply respects).
829
+ 3. Place every `offset_kib:`-pinned entry FIRST (page-aligned
830
+ check; overlap check against the SoM's own regions and prior
831
+ pins), then bump-allocate the rest bottom-up from the current
832
+ high-water mark, stepping over the pins. Pinning first is
833
+ what keeps a pin that sorts LATE alphabetically from being
834
+ dropped by a sibling the allocator had already placed on top
835
+ of it (#554).
836
+ 4. Block on capacity overflow, TBD device, page-misaligned
837
+ offset, or sibling overlap. Blocked entries land in the
838
+ manifest with `status: blocked` + `reason:` so reviewers see
839
+ the gap; the slice build trips when the DTS overlay is
840
+ consumed.
841
+
842
+ Spec note: byte-stable OTA images require the orchestrator to pin
843
+ addresses (resolved in design Q1 for v0.6 -- option "Pin in
844
+ orchestrator"). Customers get reproducible per-rebuild addresses
845
+ by declaring stable `name:` slugs; partitions stay at the same
846
+ offset across rebuilds as long as their relative order doesn't
847
+ change. Explicit `offset_kib:` is the escape hatch.
848
+ """
849
+ if not project.storage:
850
+ return []
851
+
852
+ # Group by flash device, preserving original order for the
853
+ # downstream resolver (we re-sort for allocation determinism below).
854
+ by_device: dict[str, list[StorageEntry]] = {}
855
+ no_device: list[StorageEntry] = []
856
+ for entry in project.storage:
857
+ if not entry.flash_device:
858
+ no_device.append(entry)
859
+ else:
860
+ by_device.setdefault(entry.flash_device, []).append(entry)
861
+
862
+ resolved: list[ResolvedPartition] = []
863
+
864
+ for entry in no_device:
865
+ resolved.append(_blocked_partition(entry, (
866
+ f"storage entry '{entry.name}' has no flash_device: "
867
+ f"declared; add one referencing a SoM memory_map region "
868
+ f"or an on_module.ospi_memories key")))
869
+
870
+ # alp-sdk#1365 split B (Also fix): hoist the aperture ONCE for this
871
+ # whole resolve, the way `carveout.py` hoists it once per
872
+ # `resolve_carve_outs()` call -- every downstream helper below
873
+ # (`_resolve_flash_device`, `_known_flash_devices` via
874
+ # `_verified_alt_devices`, the alt-device scan inlined in `_place()`)
875
+ # otherwise re-parses the SoC JSON and re-resolves the silicon variant
876
+ # once per region AND once per storage entry / alt-device enumeration.
877
+ aperture = resolve_aperture(
878
+ project.som_preset, project.effective_metadata_root())
879
+
880
+ # Iterate flash devices in alphabetical order; within each device,
881
+ # entries are name-sorted for byte-stable allocation.
882
+ for device_name in sorted(by_device.keys()):
883
+ descriptor, block_reason = _resolve_flash_device(
884
+ device_name, project.som_preset,
885
+ project.effective_metadata_root(), aperture)
886
+ if descriptor is None:
887
+ for entry in by_device[device_name]:
888
+ resolved.append(_blocked_partition(
889
+ entry, block_reason or "flash device unresolvable"))
890
+ continue
891
+
892
+ dt_label = descriptor["dt_label"]
893
+ capacity_bytes = descriptor["size_bytes"]
894
+ # True when `device_name` is an `on_module.ospi_memories:` key --
895
+ # an off-chip OSPI part that legitimately has no `memory_map:`
896
+ # presence at all, vs. an on-chip `memory_map:` device (`ddr_main`
897
+ # etc.) where the SoM's own regions really do live on the same
898
+ # address window. Used below to keep the reserved-spans warning
899
+ # for the case it exists to catch.
900
+ is_ospi_device = device_name in (
901
+ (project.som_preset.get("on_module") or {}).get(
902
+ "ospi_memories") or {})
903
+ # #1556: `descriptor["dt_label"]` DEFAULTS to the device name
904
+ # whenever the SoM preset sets no explicit `dt_label:` override,
905
+ # and that default was never checked against the generated
906
+ # Zephyr board tree -- `flash_device: mram_main` resolved
907
+ # `status: ok` with a fabricated `dt_label` of `mram_main` even
908
+ # though the generated tree only defines `mram_storage`. Gate on
909
+ # the exact predicate #1484 already built for this
910
+ # (`_has_real_dt_label()`) so an entry can never reach
911
+ # `status: ok` on a `memory_map:` device whose label is
912
+ # unverified -- applied in `_place()` below, AFTER the
913
+ # overlap/overflow checks, so an entry that was already going to
914
+ # block for one of THOSE reasons keeps that reason (the SoM's
915
+ # own regions are the more actionable fact to report).
916
+ # `on_module.ospi_memories:` devices are deliberately exempted
917
+ # (see `_has_real_dt_label()`'s docstring) -- that key's
918
+ # verification gap is separate follow-up.
919
+ verified_dt_label = is_ospi_device or _has_real_dt_label(
920
+ device_name, project.som_preset,
921
+ project.effective_metadata_root())
922
+ # Page-aligned high-water mark; sibling partitions allocate
923
+ # bottom-up from offset 0. Page = 4 KiB matches the IPC
924
+ # carve-out allocator; storage erase blocks on every silicon
925
+ # the SDK targets are 4 KiB-or-larger multiples.
926
+ high_water_bytes = 0
927
+ # Track allocated [lo, hi) ranges so explicit `offset_kib:`
928
+ # overrides can be checked against sibling allocations even
929
+ # when the bump allocator would have placed them differently.
930
+ allocated: list[tuple[int, int, str]] = [] # (lo, hi, name)
931
+
932
+ # Seed with the SoM's OWN regions (alp-sdk#1331). Without this the
933
+ # checks below only ever saw sibling storage[] entries, so mcuboot, a
934
+ # core's slot0 and the SE-owned atoc band were all invisible: on
935
+ # E1M-AEN801 a littlefs mount resolved to offset 0 of `mram_main` --
936
+ # MCUboot -- with no offset_kib: declared and no status: blocked.
937
+ reserved_spans, reserved_reason = _reserved_spans(
938
+ device_name, capacity_bytes, project.som_preset,
939
+ project.effective_metadata_root())
940
+ reserved_names = {name for _, _, name in reserved_spans}
941
+ allocated.extend(reserved_spans)
942
+ if reserved_reason is not None and not is_ospi_device:
943
+ # Degrading to sibling-only checking is exactly the silent
944
+ # false-PASS this fix exists to remove, so SAY so rather than
945
+ # letting the caller believe the SoM's regions were honoured.
946
+ # Suppressed for an `ospi_memories:` device: an off-chip OSPI
947
+ # part has no `memory_map:` presence to begin with, so sibling-
948
+ # only checking isn't a degradation there -- warning on it
949
+ # would fire on every AEN storage build using `ospi0` (#1484
950
+ # review).
951
+ print(f"alp_orchestrate.partition: WARNING: {reserved_reason}; "
952
+ f"storage offsets on '{device_name}' are checked against "
953
+ f"sibling partitions only", file=sys.stderr)
954
+
955
+ entries_sorted = sorted(
956
+ by_device[device_name], key=lambda e: e.name)
957
+
958
+ # (base_bytes, size_aligned, reason) per entry INDEX. Placement runs
959
+ # in TWO passes over the name-sorted list -- every `offset_kib:` pin
960
+ # first, then the bump allocator around them (#554). In one pass a
961
+ # pin only ever became an obstacle for the siblings that happened to
962
+ # sort BEFORE it, so `pinned_low` (offset_kib: 0, the shape
963
+ # docs/board-config-features.md itself documents) collided with an
964
+ # already-auto-allocated `app_data` and was silently dropped from
965
+ # dts-partitions.dtsi -- a validate-clean layout the allocator could
966
+ # have satisfied. Emission below still walks `entries_sorted`.
967
+ # Keyed by index rather than name because board.schema.json does not
968
+ # require `storage[].name` to be unique.
969
+ placement: dict[int, tuple[int, int, Optional[str]]] = {}
970
+
971
+ def _place(entry: StorageEntry) -> tuple[int, int, Optional[str]]:
972
+ size_bytes = entry.size_kib * 1024
973
+ size_aligned = ((size_bytes + _PAGE - 1) // _PAGE) * _PAGE
974
+
975
+ if entry.offset_kib is not None:
976
+ base_bytes = entry.offset_kib * 1024
977
+ if base_bytes % _PAGE != 0:
978
+ return 0, size_aligned, (
979
+ f"storage entry '{entry.name}' explicit "
980
+ f"offset_kib={entry.offset_kib} is not page-"
981
+ f"aligned (4 KiB)")
982
+ else:
983
+ # Allocate AROUND the reserved regions and the pins, not
984
+ # merely detect a collision with them (alp-sdk#1331 / #554):
985
+ # refusing here instead of skipping would break every
986
+ # project that names no addresses -- which is the common
987
+ # case -- rather than fix it.
988
+ base_bytes = _first_free(
989
+ high_water_bytes, size_aligned, allocated, capacity_bytes)
990
+
991
+ top_bytes = base_bytes + size_aligned
992
+ if top_bytes > capacity_bytes:
993
+ free_note = ""
994
+ if entry.offset_kib is None and reserved_spans:
995
+ free_note = (
996
+ f"; the SoM's own regions ("
997
+ f"{', '.join(sorted(reserved_names))}) already occupy "
998
+ f"{sum(hi - lo for lo, hi, _ in reserved_spans) // 1024} "
999
+ f"KiB of it")
1000
+ return 0, size_aligned, (
1001
+ f"storage entry '{entry.name}' ({entry.size_kib} "
1002
+ f"KiB at offset {base_bytes // 1024} KiB) overruns "
1003
+ f"flash device '{device_name}' "
1004
+ f"(capacity {capacity_bytes // 1024} KiB){free_note}")
1005
+
1006
+ # Overlap check -- against sibling partitions AND the SoM's own
1007
+ # regions seeded above.
1008
+ overlap_with: Optional[str] = None
1009
+ for lo, hi, peer_name in allocated:
1010
+ if not (top_bytes <= lo or base_bytes >= hi):
1011
+ overlap_with = peer_name
1012
+ break
1013
+ if overlap_with is not None:
1014
+ where = (f"offset_kib={entry.offset_kib}"
1015
+ if entry.offset_kib is not None
1016
+ else f"auto-allocated offset {base_bytes // 1024} KiB")
1017
+ if overlap_with in reserved_names:
1018
+ # `overlap_with` is one of the SoM's OWN regions (any
1019
+ # `memory_map:` region with an addressed `base:` is
1020
+ # seeded into `reserved_spans` regardless of its
1021
+ # `carveout:` value -- see `_reserved_spans()`) -- it
1022
+ # must NOT be named here as something to target
1023
+ # directly (#1484), and neither may any other reserved
1024
+ # region: they are exactly what this entry just failed
1025
+ # to overlap. A name in `_known_flash_devices()` is
1026
+ # not enough either -- e.g. an `ospi_memories:` entry
1027
+ # with a TBD capacity (E1M-AEN801's `ospi1`) is known
1028
+ # but does not actually resolve, and a device that DOES
1029
+ # resolve may still be recommending an undefined DT
1030
+ # node (`_resolve_flash_device()` defaults `dt_label`
1031
+ # to the device name when unset -- see
1032
+ # `_has_real_dt_label()`). Only recommend a device
1033
+ # that both resolves AND has a verified DT label.
1034
+ alt_devices = [
1035
+ d for d in _known_flash_devices(
1036
+ project.som_preset,
1037
+ project.effective_metadata_root(), aperture)
1038
+ if d != device_name
1039
+ and d not in reserved_names
1040
+ and _has_real_dt_label(
1041
+ d, project.som_preset,
1042
+ project.effective_metadata_root())
1043
+ and _resolve_flash_device(
1044
+ d, project.som_preset,
1045
+ project.effective_metadata_root(), aperture)[0]
1046
+ is not None]
1047
+ # Only lead with "pick an offset on <device>" when the
1048
+ # device actually has free room outside its reserved
1049
+ # spans. Reserved spans come from ANY addressed
1050
+ # memory_map region, `carveout: false` or not; AEN's
1051
+ # `mram_main` is the fully-tiled case (0 KiB free,
1052
+ # tiled exactly by its `carveout: false` sub-regions),
1053
+ # not the only case this branch can fire for.
1054
+ reserved_bytes = sum(
1055
+ hi - lo for lo, hi, _ in reserved_spans)
1056
+ if reserved_bytes < capacity_bytes:
1057
+ remedy = (
1058
+ f"pick an offset on '{device_name}' outside "
1059
+ f"the SoM's declared regions")
1060
+ if alt_devices:
1061
+ remedy += (
1062
+ f", or use a different flash_device: "
1063
+ f"({', '.join(alt_devices)})")
1064
+ elif alt_devices:
1065
+ remedy = (
1066
+ f"'{device_name}' is fully tiled by the SoM's "
1067
+ f"own regions ({capacity_bytes // 1024} KiB, "
1068
+ f"no free room) -- use a different "
1069
+ f"flash_device: ({', '.join(alt_devices)})")
1070
+ else:
1071
+ # NOT "no other flash_device: resolves" -- on an
1072
+ # AEN SoM, ospi0 typically DOES resolve
1073
+ # (`_resolve_flash_device()`), it just has no
1074
+ # verified Devicetree label yet (see
1075
+ # `_has_real_dt_label()`). Naming it here would
1076
+ # recommend the very label #1484 exists to stop
1077
+ # fabricating, so the message must stay true for
1078
+ # BOTH cases (nothing resolves / something
1079
+ # resolves but is unverified) without naming a
1080
+ # device either way.
1081
+ remedy = (
1082
+ f"'{device_name}' is fully tiled by the SoM's "
1083
+ f"own regions ({capacity_bytes // 1024} KiB, "
1084
+ f"no free room) and no other flash_device: on "
1085
+ f"this SoM both resolves and has a verified "
1086
+ f"Devicetree label (alp-sdk#1556)")
1087
+ return 0, size_aligned, (
1088
+ f"storage entry '{entry.name}' {where} overlaps SoM "
1089
+ f"region '{overlap_with}' on device '{device_name}' "
1090
+ f"-- that region is not customer-writable. "
1091
+ f"{remedy}.")
1092
+ return 0, size_aligned, (
1093
+ f"storage entry '{entry.name}' {where} overlaps "
1094
+ f"sibling partition '{overlap_with}' on device "
1095
+ f"'{device_name}'")
1096
+
1097
+ if not verified_dt_label:
1098
+ # #1556: this entry cleared every other check (page
1099
+ # alignment, capacity, sibling/reserved overlap) and was
1100
+ # about to resolve `status: ok` -- but `dt_label` is the
1101
+ # device NAME, defaulted because no SoM preset declares
1102
+ # an explicit `dt_label:` override for '{device_name}',
1103
+ # and that default has never been checked against the
1104
+ # generated board tree. Refuse rather than emit a
1105
+ # `partitions { }` overlay under a label the board `.dts`
1106
+ # may not define (`grep -rn '{dt_label}' zephyr/` is the
1107
+ # check to run). Checked HERE, not earlier, so an entry
1108
+ # that would ALSO overlap a reserved/sibling region still
1109
+ # gets that more actionable reason instead.
1110
+ alt = _verified_alt_devices(
1111
+ device_name, project.som_preset,
1112
+ project.effective_metadata_root(), aperture)
1113
+ remedy = (
1114
+ f"use a different flash_device: ({', '.join(alt)})"
1115
+ if alt else
1116
+ "no other flash_device: on this SoM both resolves "
1117
+ "and has a verified Devicetree label")
1118
+ return 0, size_aligned, (
1119
+ f"storage entry '{entry.name}' would resolve on "
1120
+ f"flash device '{device_name}', but its Devicetree "
1121
+ f"label defaults to '{dt_label}' (the device name) "
1122
+ f"-- no SoM preset declares an explicit dt_label: "
1123
+ f"override for '{device_name}', so this default is "
1124
+ f"unverified against the generated board tree and "
1125
+ f"may decorate a `partitions {{ }}` child under a "
1126
+ f"node the board `.dts` never defines; {remedy}.")
1127
+
1128
+ allocated.append((base_bytes, top_bytes, entry.name))
1129
+ return base_bytes, size_aligned, None
1130
+
1131
+ for index, entry in enumerate(entries_sorted):
1132
+ if entry.offset_kib is not None:
1133
+ placement[index] = _place(entry)
1134
+ for index, entry in enumerate(entries_sorted):
1135
+ if entry.offset_kib is None:
1136
+ base_bytes, size_aligned, reason = _place(entry)
1137
+ placement[index] = (base_bytes, size_aligned, reason)
1138
+ if reason is None:
1139
+ # Only bump the allocator when we used it; explicit
1140
+ # offsets don't shift the high-water mark (they may be
1141
+ # below it deliberately, e.g. to reserve a low slot) --
1142
+ # `allocated` is what makes the allocator step over them.
1143
+ high_water_bytes = base_bytes + size_aligned
1144
+
1145
+ for index, entry in enumerate(entries_sorted):
1146
+ base_bytes, size_aligned, reason = placement[index]
1147
+ if reason is not None:
1148
+ resolved.append(_blocked_partition(entry, reason))
1149
+ continue
1150
+ resolved.append(ResolvedPartition(
1151
+ name=entry.name,
1152
+ fs=entry.fs,
1153
+ flash_device=device_name,
1154
+ dt_label=dt_label,
1155
+ base_kib=base_bytes // 1024,
1156
+ size_kib=size_aligned // 1024,
1157
+ mount=entry.mount,
1158
+ ))
1159
+
1160
+ return resolved