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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/planner/loader.py
ADDED
|
@@ -0,0 +1,1525 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""board.yaml loader -- file IO, preset/silicon resolution, and load_board_yaml.
|
|
4
|
+
|
|
5
|
+
Parses board.yaml into a BoardProject: YAML/JSON IO, schema validation, the
|
|
6
|
+
`preset:` shared-board + inline-board resolution, silicon-ref -> SoC JSON path,
|
|
7
|
+
per-core topology defaults, and the big `load_board_yaml` entry point (which
|
|
8
|
+
finishes by running the cross-field validators). Extracted as the #285 loader
|
|
9
|
+
seam.
|
|
10
|
+
|
|
11
|
+
`load_board_schema` / `iter_schema_errors` are RELOCATED from alp-sdk's
|
|
12
|
+
`scripts/alp_cli/validator.py`, the last module-scope import this file made
|
|
13
|
+
across the repo boundary. They stay one pair of functions rather than being
|
|
14
|
+
inlined into `_validate_board` for the reason alp-sdk gave them their own
|
|
15
|
+
names: the schema FILE, the draft dialect, and the error ORDERING are one
|
|
16
|
+
decision, so every consumer reports identical violations in identical order.
|
|
17
|
+
The schema itself is still read from the bound SDK checkout (`BOARD_SCHEMA`) --
|
|
18
|
+
`metadata/**` did not move (ADR-0017).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import yaml # type: ignore[import-untyped]
|
|
30
|
+
except ImportError:
|
|
31
|
+
sys.exit("alp_orchestrate: PyYAML is required. Install via `pip install pyyaml`.")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import jsonschema # type: ignore[import-untyped]
|
|
35
|
+
except ImportError:
|
|
36
|
+
sys.exit("alp_orchestrate: jsonschema is required. Install via `pip install jsonschema`.")
|
|
37
|
+
|
|
38
|
+
from tan.core.metadata_schema import (
|
|
39
|
+
missing_schema_note,
|
|
40
|
+
soc_spec_schema_path,
|
|
41
|
+
som_preset_schema_path,
|
|
42
|
+
validate_document,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from . import sdk_compat
|
|
46
|
+
from .models import (
|
|
47
|
+
BoardProject,
|
|
48
|
+
IpcEntry,
|
|
49
|
+
OrchestratorError,
|
|
50
|
+
SdkRevisionNotBuildable,
|
|
51
|
+
SdkRevisionUnknown,
|
|
52
|
+
SdkRevisionUnsupported,
|
|
53
|
+
Slice,
|
|
54
|
+
StorageEntry,
|
|
55
|
+
)
|
|
56
|
+
from .ownership import load_ownership_doc, resolve_ownership
|
|
57
|
+
from .partition import _is_ospi_key_unassembled, _known_flash_devices
|
|
58
|
+
from .paths import BOARD_SCHEMA, METADATA_ROOT, REPO
|
|
59
|
+
from .som_metadata import _sku_family, resolve_memory_map
|
|
60
|
+
from .strict_loaders import DuplicateKeyError, strict_json_loads, strict_yaml_load
|
|
61
|
+
from .topology import _default_os_from_core_type
|
|
62
|
+
from .validate import (
|
|
63
|
+
_enforce_loader_rules,
|
|
64
|
+
_enforce_os_matches_core_class,
|
|
65
|
+
_validate_consistency,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_board_schema(schema_path: Path | None = None) -> dict[str, Any]:
|
|
70
|
+
"""Load board.schema.json (the bound SDK's copy unless *schema_path* overrides).
|
|
71
|
+
|
|
72
|
+
The one place the schema file is resolved + parsed.
|
|
73
|
+
"""
|
|
74
|
+
return json.loads((schema_path or BOARD_SCHEMA).read_text(encoding="utf-8"))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def iter_schema_errors(
|
|
78
|
+
data: dict[str, Any], schema_path: Path | None = None
|
|
79
|
+
) -> list[jsonschema.ValidationError]:
|
|
80
|
+
"""Validate *data* against board.schema.json; errors sorted by path.
|
|
81
|
+
|
|
82
|
+
The shared JSON-Schema pass: one schema file, one draft dialect
|
|
83
|
+
(2020-12, matching the schema's own `$schema` declaration), one error
|
|
84
|
+
ordering -- so every consumer reports identical violations.
|
|
85
|
+
"""
|
|
86
|
+
validator = jsonschema.Draft202012Validator(load_board_schema(schema_path))
|
|
87
|
+
# Stringify path parts: absolute_path mixes ints (array indices) and
|
|
88
|
+
# strs (keys); a raw list comparison would TypeError across siblings.
|
|
89
|
+
return sorted(validator.iter_errors(data),
|
|
90
|
+
key=lambda e: [str(p) for p in e.absolute_path])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _silicon_to_soc_path(silicon: str, metadata_root: Path) -> Path:
|
|
94
|
+
"""`alif:ensemble:e7` -> metadata/socs/alif/ensemble/e7.json."""
|
|
95
|
+
parts = silicon.split(":")
|
|
96
|
+
if len(parts) != 3:
|
|
97
|
+
raise OrchestratorError(
|
|
98
|
+
f"silicon ref '{silicon}' is not a triple-colon string")
|
|
99
|
+
return (metadata_root / "socs" / parts[0] / parts[1] /
|
|
100
|
+
f"{parts[2]}.json")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------
|
|
104
|
+
# Loader
|
|
105
|
+
# ---------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
109
|
+
try:
|
|
110
|
+
path_is_file = path.is_file()
|
|
111
|
+
except (OSError, RuntimeError) as e:
|
|
112
|
+
# Same defect class as validate.py's `profile:` guard (#1961):
|
|
113
|
+
# a bad `--input` board.yaml path must not crash the loader
|
|
114
|
+
# with an unhandled exception. `Path.is_file()`'s own
|
|
115
|
+
# `_ignore_error` list swallows ENOENT/ENOTDIR/EBADF/ELOOP on
|
|
116
|
+
# POSIX -- a symlink loop returns `False` there, no raise --
|
|
117
|
+
# but re-raises `PermissionError` on EACCES (an `OSError`
|
|
118
|
+
# subclass) rather than swallowing it, same as the `profile:`
|
|
119
|
+
# gap. On Windows, a real WSL-made symlink loop driven through
|
|
120
|
+
# the real CLI with CPython 3.11.3 shows `.is_file()` raise a
|
|
121
|
+
# plain `OSError` (`WinError 1920`, "The file cannot be
|
|
122
|
+
# accessed by the system") instead -- confirmed, not a mock.
|
|
123
|
+
# `RuntimeError` is caught too for parity with `.resolve()`'s
|
|
124
|
+
# own ELOOP shape elsewhere in this module.
|
|
125
|
+
raise OrchestratorError(f"could not access {path}: {e}") from e
|
|
126
|
+
if not path_is_file:
|
|
127
|
+
raise OrchestratorError(f"file not found: {path}")
|
|
128
|
+
try:
|
|
129
|
+
data = strict_yaml_load(path.read_text(encoding="utf-8"), source=path)
|
|
130
|
+
except (yaml.YAMLError, DuplicateKeyError) as e:
|
|
131
|
+
raise OrchestratorError(f"failed to parse {path}: {e}") from e
|
|
132
|
+
if not isinstance(data, dict):
|
|
133
|
+
raise OrchestratorError(
|
|
134
|
+
f"{path} did not parse to a top-level mapping")
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
139
|
+
if not path.is_file():
|
|
140
|
+
raise OrchestratorError(f"file not found: {path}")
|
|
141
|
+
try:
|
|
142
|
+
return strict_json_loads(path.read_text(encoding="utf-8"), source=path)
|
|
143
|
+
except (json.JSONDecodeError, DuplicateKeyError) as e:
|
|
144
|
+
raise OrchestratorError(f"failed to parse {path}: {e}") from e
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _validate_board(project: dict[str, Any],
|
|
148
|
+
metadata_root: Path = METADATA_ROOT) -> None:
|
|
149
|
+
schema_path = metadata_root / "schemas" / "board.schema.json"
|
|
150
|
+
if not schema_path.is_file():
|
|
151
|
+
# Fall back to the global schema when the metadata_root doesn't
|
|
152
|
+
# carry its own copy (e.g. synthetic test roots without schemas/).
|
|
153
|
+
schema_path = BOARD_SCHEMA
|
|
154
|
+
errors = iter_schema_errors(project, schema_path)
|
|
155
|
+
if errors:
|
|
156
|
+
messages = []
|
|
157
|
+
for e in errors:
|
|
158
|
+
loc = "/".join(str(p) for p in e.path) or "<root>"
|
|
159
|
+
messages.append(f" - {loc}: {e.message}")
|
|
160
|
+
raise OrchestratorError(
|
|
161
|
+
"board.yaml schema validation failed:\n" +
|
|
162
|
+
"\n".join(messages))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _resolve_board_preset(
|
|
166
|
+
preset: str,
|
|
167
|
+
metadata_root: Path,
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
"""Load the shared board YAML referenced by `preset:`.
|
|
170
|
+
|
|
171
|
+
Shared boards live at metadata/boards/<preset>.yaml. Raises
|
|
172
|
+
OrchestratorError when the file is missing (`preset:` must resolve;
|
|
173
|
+
customers with a custom board define it inline instead).
|
|
174
|
+
"""
|
|
175
|
+
p = metadata_root / "boards" / f"{preset}.yaml"
|
|
176
|
+
if not p.is_file():
|
|
177
|
+
raise OrchestratorError(
|
|
178
|
+
f"`preset: {preset}` does not resolve: no shared board "
|
|
179
|
+
f"at {p.relative_to(REPO) if p.is_relative_to(REPO) else p}. "
|
|
180
|
+
f"Available presets: "
|
|
181
|
+
f"{sorted(_available_presets(metadata_root))}")
|
|
182
|
+
return _load_yaml(p)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _available_presets(metadata_root: Path) -> list[str]:
|
|
186
|
+
boards_dir = metadata_root / "boards"
|
|
187
|
+
if not boards_dir.is_dir():
|
|
188
|
+
return []
|
|
189
|
+
return [p.stem for p in boards_dir.glob("*.yaml")]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _check_board_hosts_som_family(
|
|
193
|
+
sku: str,
|
|
194
|
+
som_preset: dict[str, Any],
|
|
195
|
+
preset: str,
|
|
196
|
+
board_preset: dict[str, Any],
|
|
197
|
+
) -> None:
|
|
198
|
+
family = som_preset.get("family")
|
|
199
|
+
allowed_raw = board_preset.get("hosts_som_families")
|
|
200
|
+
if not isinstance(family, str) or not isinstance(allowed_raw, list):
|
|
201
|
+
return
|
|
202
|
+
allowed = [str(item) for item in allowed_raw]
|
|
203
|
+
if family in allowed:
|
|
204
|
+
return
|
|
205
|
+
raise OrchestratorError(
|
|
206
|
+
f"board preset '{preset}' hosts SoM families {allowed}, "
|
|
207
|
+
f"but {sku} is family '{family}'")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _synthesize_inline_board(project: dict[str, Any]) -> dict[str, Any]:
|
|
211
|
+
"""Build a board-shaped dict from a project's inline top-level fields.
|
|
212
|
+
|
|
213
|
+
Used when board.yaml has no `preset:` -- the project's own `name`,
|
|
214
|
+
`populated`, `e1m_routes` (and optional `hw_rev`) double as the
|
|
215
|
+
board definition. Returned dict has the same shape downstream
|
|
216
|
+
emitters expect from a preset-resolved board.
|
|
217
|
+
"""
|
|
218
|
+
return {
|
|
219
|
+
"name": project.get("name"),
|
|
220
|
+
"populated": dict(project.get("populated") or {}),
|
|
221
|
+
"e1m_routes": dict(project.get("e1m_routes") or {}),
|
|
222
|
+
# Inline mode has no per-board hw_revisions table; loader code
|
|
223
|
+
# that reads default_hw_rev / hw_revisions tolerates None.
|
|
224
|
+
"default_hw_rev": project.get("hw_rev"),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _resolve_topology_for_core(
|
|
229
|
+
core_id: str,
|
|
230
|
+
project_cores: dict[str, Any],
|
|
231
|
+
som_topology: dict[str, Any],
|
|
232
|
+
) -> Optional[dict[str, Any]]:
|
|
233
|
+
"""Per spec §4.5: project's `cores.<id>` overrides the SoM preset's
|
|
234
|
+
`topology.<id>`. Returns None when neither source has the key."""
|
|
235
|
+
if core_id in project_cores:
|
|
236
|
+
# Customer-supplied entry; merge missing keys from topology.
|
|
237
|
+
merged: dict[str, Any] = dict(som_topology.get(core_id, {}) or {})
|
|
238
|
+
merged.update(project_cores[core_id] or {})
|
|
239
|
+
return merged
|
|
240
|
+
if core_id in som_topology:
|
|
241
|
+
return dict(som_topology[core_id] or {})
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_variant_debug(
|
|
246
|
+
som_preset: dict[str, Any],
|
|
247
|
+
soc_spec: dict[str, Any],
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""Resolve the whole `variants[].debug` block for the SoC variant this
|
|
250
|
+
SoM preset declares -- `{}` when the variant can't be resolved or
|
|
251
|
+
publishes no `debug:` block at all.
|
|
252
|
+
|
|
253
|
+
Matches the SoM's `silicon_variant:` against `soc_spec["variants"][].
|
|
254
|
+
order_code`, falling back to `alp_module_skus` membership when
|
|
255
|
+
`silicon_variant:` is absent/"TBD" -- the same two-step match as the
|
|
256
|
+
canonical `project_loader._resolve_silicon_variant` (alp-sdk's
|
|
257
|
+
`alp_project_loader`, re-exported there as
|
|
258
|
+
`alp_project._resolve_silicon_variant`) and `gen_zephyr_board.
|
|
259
|
+
_resolve_variant`. NOT delegated to that canonical helper -- unlike
|
|
260
|
+
`resolve_memory_map` (imported from `som_metadata` at the top of this
|
|
261
|
+
same file, so a cross-module import is clearly not the obstacle), it
|
|
262
|
+
takes `(sku_preset, metadata_root)` and re-resolves the SoC-JSON path +
|
|
263
|
+
reads + re-parses it from disk itself. By the time this function runs,
|
|
264
|
+
`_resolve_board_impl` has already loaded that exact JSON into the
|
|
265
|
+
`soc_spec` dict this function receives as a parameter, and this
|
|
266
|
+
function's callers have no other use for `metadata_root`. Delegating
|
|
267
|
+
would mean a second disk read + parse of a file already in hand, purely
|
|
268
|
+
to save this six-line match -- kept independent instead, deliberately in
|
|
269
|
+
lockstep with both of the above; if either one grows a third fallback or
|
|
270
|
+
drops the "TBD" sentinel, update this one too.
|
|
271
|
+
|
|
272
|
+
ONE variant match shared by every `debug:` reader below
|
|
273
|
+
(`_resolve_jlink_flash_device`, `_resolve_flow_d_preflight`) so a future
|
|
274
|
+
third fact cannot drift onto a differently-resolved variant than its
|
|
275
|
+
siblings -- the keys are read as a set precisely because
|
|
276
|
+
`expect_dpidr`/`jlink_device` are validated downstream as a PAIR.
|
|
277
|
+
"""
|
|
278
|
+
variants = soc_spec.get("variants") or []
|
|
279
|
+
variant: Optional[dict[str, Any]] = None
|
|
280
|
+
declared = som_preset.get("silicon_variant")
|
|
281
|
+
# Case/whitespace-insensitive TBD match (alp-sdk #1048): "tbd"/"Tbd"/
|
|
282
|
+
# " TBD " are the same hand-typed placeholder as a bare "TBD".
|
|
283
|
+
declared_is_tbd = isinstance(declared, str) and declared.strip().upper() == "TBD"
|
|
284
|
+
if declared and not declared_is_tbd:
|
|
285
|
+
variant = next(
|
|
286
|
+
(v for v in variants if v.get("order_code") == declared), None)
|
|
287
|
+
if variant is None:
|
|
288
|
+
sku = som_preset.get("sku")
|
|
289
|
+
variant = next(
|
|
290
|
+
(v for v in variants if sku in (v.get("alp_module_skus") or [])),
|
|
291
|
+
None)
|
|
292
|
+
if variant is None:
|
|
293
|
+
return {}
|
|
294
|
+
return dict(variant.get("debug") or {})
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _resolve_jlink_flash_device(debug: dict[str, Any]) -> Optional[str]:
|
|
298
|
+
"""`debug.jlink_flash_device` out of an already-resolved `debug:` block
|
|
299
|
+
(`_resolve_variant_debug`).
|
|
300
|
+
|
|
301
|
+
`jlink_flash_device` is a single per-variant string (per
|
|
302
|
+
soc-spec-v1.schema.json's own description: it "is not per-core... a
|
|
303
|
+
single per-variant string, not a jlink_device-shaped map"), unlike its
|
|
304
|
+
sibling `jlink_device` which IS keyed by core id -- so this returns one
|
|
305
|
+
value reused for every core's slice, not a per-core lookup.
|
|
306
|
+
|
|
307
|
+
None when the variant couldn't be resolved (`debug` is then `{}`), or
|
|
308
|
+
the resolved variant's `debug:` block carries no `jlink_flash_device`
|
|
309
|
+
key -- per that same schema description, an absent key is the correct
|
|
310
|
+
published "unknown", never a value to invent from a naming convention.
|
|
311
|
+
"""
|
|
312
|
+
return debug.get("jlink_flash_device")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _jlink_flash_device_declared(debug: dict[str, Any]) -> bool:
|
|
316
|
+
"""Whether the resolved `debug:` block carried the `jlink_flash_device`
|
|
317
|
+
KEY, independent of its value (tan-cli#734).
|
|
318
|
+
|
|
319
|
+
`_resolve_jlink_flash_device` above cannot answer this: `dict.get`
|
|
320
|
+
returns `None` both for a schema-declared `jlink_flash_device: null` and
|
|
321
|
+
for an absent key, and those two mean opposite things. Declared-null is
|
|
322
|
+
a published "no known J-Link flash profile, refuse loudly"; absent is
|
|
323
|
+
"this variant says nothing", which keeps the Flow A default. The
|
|
324
|
+
downstream contract is `flash_plan._fa_has_key`, which is presence-based
|
|
325
|
+
for exactly this reason -- this is its counterpart on the emitter side.
|
|
326
|
+
"""
|
|
327
|
+
return "jlink_flash_device" in debug
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _resolve_slot0_load_address(
|
|
331
|
+
som_preset: dict[str, Any], core_id: str,
|
|
332
|
+
) -> Optional[str]:
|
|
333
|
+
"""This core's AEN MRAM slot0-XIP load address, as a `0x`-prefixed hex
|
|
334
|
+
string for `flash_args.slot0_load_address` (tan-cli#353): the Flow D
|
|
335
|
+
built-in MRAM loader (`alif_mram_jlink`) needs to know where the
|
|
336
|
+
slot0-linked application blob itself belongs, distinct from
|
|
337
|
+
`jlink_flash_device` (which only picks the loader's *device profile*).
|
|
338
|
+
|
|
339
|
+
Deliberately sourced from the SoM preset's `memory_map:`, NOT from the
|
|
340
|
+
SoC JSON `debug:` block `jlink_flash_device` lives in: this address is
|
|
341
|
+
SDK/module build POLICY, not a silicon fact -- two SoMs built on the
|
|
342
|
+
same silicon part can freely choose different slot0 windows (alp-sdk
|
|
343
|
+
#1069, the fix for a stock symmetric layout that put HE's and HP's
|
|
344
|
+
slot0 at the SAME address and let flashing one silently clobber the
|
|
345
|
+
other).
|
|
346
|
+
|
|
347
|
+
Reuses `zephyr_board.py`'s own `_aen_role_slot0_map` (lazy import, to
|
|
348
|
+
dodge the circular import -- `zephyr_board.py` imports `_load_yaml`
|
|
349
|
+
from this module at module scope) rather than re-deriving its per-role
|
|
350
|
+
override lookup, so this and board generation read the identical
|
|
351
|
+
override/no-override decision and genuinely cannot disagree on THAT
|
|
352
|
+
part. A declared-but-invalid override (a half-authored map naming only
|
|
353
|
+
one role's `<role>_slot0`, or a wrong `accessible_from`) makes
|
|
354
|
+
`_aen_role_slot0_map` raise `ZephyrBoardEmitError` -- board generation
|
|
355
|
+
refuses to build that core's board file for the same reason, so this
|
|
356
|
+
re-raises as `OrchestratorError` instead of silently publishing an
|
|
357
|
+
address no board was actually built for.
|
|
358
|
+
|
|
359
|
+
Only `m55_he`/`m55_hp` roles are AEN slot0-XIP cores; every other
|
|
360
|
+
core_id (a32_cluster, non-AEN SoC families) returns None -- a
|
|
361
|
+
published "unknown", never a value to invent (same convention as
|
|
362
|
+
`jlink_flash_device`'s absent key).
|
|
363
|
+
|
|
364
|
+
No-override default: when `_aen_role_slot0_map` finds no per-role
|
|
365
|
+
override, this returns the stock symmetric layout's slot0 window --
|
|
366
|
+
right after the mcuboot region, at the MRAM base -- computed as
|
|
367
|
+
`zephyr_board._AEN_MRAM_BASE + zephyr_board._AEN_MCUBOOT_KIB * 1024`
|
|
368
|
+
directly against those two imported constants, not a locally pinned
|
|
369
|
+
literal, so a change to either constant moves this default too.
|
|
370
|
+
Applies uniformly to EVERY role in the no-override case (not just
|
|
371
|
+
`he`): the stock layout has exactly one slot0 window, and whichever
|
|
372
|
+
M55 core a SoM boots lands on it -- including an `m55_hp` that boots
|
|
373
|
+
alone, the case a role-keyed (`he`-only) fallback would still refuse.
|
|
374
|
+
(This sentence used to call E1M-AEN401/E1M-AEN601 "single-M55"; both
|
|
375
|
+
are DUAL-M55 -- see the paragraph below -- and alp-sdk#1446 cites
|
|
376
|
+
that wrong clause as the justification for having put two cores'
|
|
377
|
+
slot0 at one address until alp-sdk#1445.)
|
|
378
|
+
|
|
379
|
+
E1M-AEN401/E1M-AEN601 declare BOTH `m55_hp` and `m55_he` in
|
|
380
|
+
`topology:` (only the HP board's Zephyr tree is generated today --
|
|
381
|
+
alp-sdk#999, a separate, tracked gap), and since alp-sdk#1445 both
|
|
382
|
+
also declare per-role `he_slot0`/`hp_slot0` overrides in
|
|
383
|
+
`memory_map:` (metadata/e1m_modules/E1M-AEN401.yaml,
|
|
384
|
+
E1M-AEN601.yaml), so this resolves to DISJOINT addresses for the two
|
|
385
|
+
roles, not the no-override default above. The no-override branch is
|
|
386
|
+
reachable only by a FUTURE AEN variant that publishes
|
|
387
|
+
`jlink_flash_device` without also declaring a disjoint-slot0
|
|
388
|
+
override -- `_validate_topology_cores` only calls this resolver once
|
|
389
|
+
the SoC variant publishes `jlink_flash_device`, and every AEN preset
|
|
390
|
+
already declares the override. (Do not read that as a list of the
|
|
391
|
+
variants which publish `jlink_flash_device`: E4 does not --
|
|
392
|
+
`metadata/socs/alif/ensemble/e4.json` declares
|
|
393
|
+
`"jlink_flash_device": null`, so AEN401 never reaches this resolver
|
|
394
|
+
-- while E3/E5/E7 do, for AEN301/AEN501/AEN701.) That residual risk
|
|
395
|
+
is tracked, not silently accepted: alp-sdk#1384.
|
|
396
|
+
"""
|
|
397
|
+
if not core_id.startswith("m55_"):
|
|
398
|
+
return None
|
|
399
|
+
role = core_id[len("m55_"):]
|
|
400
|
+
if role not in ("he", "hp"):
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
from .zephyr_board import (
|
|
404
|
+
_AEN_MCUBOOT_KIB, _AEN_MRAM_BASE, ZephyrBoardEmitError,
|
|
405
|
+
_aen_role_slot0_map)
|
|
406
|
+
|
|
407
|
+
memory_map = som_preset.get("memory_map") or []
|
|
408
|
+
try:
|
|
409
|
+
slot0_region = _aen_role_slot0_map(memory_map, role)
|
|
410
|
+
except ZephyrBoardEmitError as exc:
|
|
411
|
+
raise OrchestratorError(
|
|
412
|
+
f"cannot resolve flash_args.slot0_load_address for "
|
|
413
|
+
f"{core_id!r}: {exc}") from exc
|
|
414
|
+
|
|
415
|
+
if slot0_region is not None:
|
|
416
|
+
return f"0x{slot0_region['base']:08x}"
|
|
417
|
+
return f"0x{_AEN_MRAM_BASE + _AEN_MCUBOOT_KIB * 1024:08x}"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _resolve_flow_d_preflight(
|
|
421
|
+
debug: dict[str, Any],
|
|
422
|
+
core_id: str,
|
|
423
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
424
|
+
"""The read-only SW-DP IDR preflight PAIR for one core, out of an
|
|
425
|
+
already-resolved `debug:` block: `(expect_dpidr, jlink_device)`.
|
|
426
|
+
|
|
427
|
+
Two facts, one guard (alp-sdk #1355). `expect_dpidr` is the DPIDR the
|
|
428
|
+
board's debug port must answer before a host flasher writes anything;
|
|
429
|
+
`jlink_device` is the LIVE-CORE attach profile the read is performed
|
|
430
|
+
with -- distinct from `jlink_flash_device`, which is the part-number
|
|
431
|
+
flash-algorithm profile and cannot attach to a running core on an older
|
|
432
|
+
J-Link DLL. A consumer needs both or the check cannot run, and tan
|
|
433
|
+
(`tan.core.flash_plan.validate_flow_d_preflight_args`) hard-refuses a
|
|
434
|
+
half-armed pair -- so emitting one without the other does not merely
|
|
435
|
+
leave the guard unarmed, it bricks the flash path outright.
|
|
436
|
+
|
|
437
|
+
Hence both-or-neither, unconditionally: either key missing yields
|
|
438
|
+
`(None, None)`. Note `debug.jlink_device` is legitimately sparse across
|
|
439
|
+
`cores[]` -- an AEN variant publishes an attach profile for
|
|
440
|
+
`m55_hp`/`m55_he` and none for `a32_cluster`, which is a Cortex-A
|
|
441
|
+
cluster running Yocto, not a J-Link flash target -- so "no attach
|
|
442
|
+
profile for THIS core" is a normal answer, not an error, and this
|
|
443
|
+
function must not treat it as one.
|
|
444
|
+
"""
|
|
445
|
+
expect_dpidr = debug.get("expect_dpidr")
|
|
446
|
+
jlink_device = (debug.get("jlink_device") or {}).get(core_id)
|
|
447
|
+
if not expect_dpidr or not jlink_device:
|
|
448
|
+
return (None, None)
|
|
449
|
+
return (expect_dpidr, jlink_device)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _enforce_flow_d_preflight_pair(
|
|
453
|
+
slice_: Slice,
|
|
454
|
+
debug: dict[str, Any],
|
|
455
|
+
sku: str,
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Refuse a Flow-D-armed Zephyr slice that silently dropped the
|
|
458
|
+
wrong-board preflight because its core has no attach profile (alp-sdk
|
|
459
|
+
#1355).
|
|
460
|
+
|
|
461
|
+
`_resolve_flow_d_preflight`'s both-or-neither rule is what keeps the
|
|
462
|
+
emitted `flash_args` legal, but on its own it would also let a REAL gap
|
|
463
|
+
-- a variant that publishes `expect_dpidr` and forgets (or loses, to a
|
|
464
|
+
core rename) the `jlink_device` entry for a core that genuinely flashes
|
|
465
|
+
-- degrade quietly back to an unguarded write. That is the exact
|
|
466
|
+
failure this issue exists to remove, so it must be loud.
|
|
467
|
+
|
|
468
|
+
Scoped to the slices where the guard actually applies: `os: zephyr`
|
|
469
|
+
(Flow D is a Zephyr-on-M path) whose SoC variant publishes
|
|
470
|
+
`jlink_flash_device` (that key's presence IS what promotes a
|
|
471
|
+
`zephyr_west_flash` entry to Flow D). An A-core, an `os: off` core, or
|
|
472
|
+
a part with no J-Link MRAM loader at all is not a Flow D target and is
|
|
473
|
+
correctly silent here.
|
|
474
|
+
|
|
475
|
+
The converse -- `jlink_device` with no `expect_dpidr` -- is deliberately
|
|
476
|
+
NOT an error: that is the normal, correct state of every variant whose
|
|
477
|
+
DPIDR nobody has measured yet, and it simply leaves the guard unarmed.
|
|
478
|
+
An unarmed guard is recoverable; a guard armed at a guessed ID is not,
|
|
479
|
+
because a wrong ID that happens to match another board on the same
|
|
480
|
+
bench passes on exactly the board it exists to exclude.
|
|
481
|
+
"""
|
|
482
|
+
if slice_.os != "zephyr" or not (
|
|
483
|
+
slice_.jlink_flash_device_declared or slice_.jlink_flash_device is not None
|
|
484
|
+
):
|
|
485
|
+
return
|
|
486
|
+
# Bound once and read from the local: interpolating `debug['expect_dpidr']`
|
|
487
|
+
# into the message would make a mis-edited condition fail with a KeyError
|
|
488
|
+
# raised from inside its own diagnostic, instead of with the diagnostic.
|
|
489
|
+
expect_dpidr = debug.get("expect_dpidr")
|
|
490
|
+
if expect_dpidr and not slice_.jlink_device:
|
|
491
|
+
raise OrchestratorError(
|
|
492
|
+
f"{sku}: SoC variant publishes debug.expect_dpidr "
|
|
493
|
+
f"({expect_dpidr}) but no debug.jlink_device entry for "
|
|
494
|
+
f"core '{slice_.core_id}', which flashes over Flow D -- the "
|
|
495
|
+
f"wrong-board SW-DP IDR preflight needs BOTH (the expected ID "
|
|
496
|
+
f"and the live-core attach profile it is read with), and a "
|
|
497
|
+
f"downstream flasher refuses a half-armed pair outright rather "
|
|
498
|
+
f"than skipping the check. Add "
|
|
499
|
+
f"debug.jlink_device['{slice_.core_id}'] to this variant in "
|
|
500
|
+
f"metadata/socs/, or remove debug.expect_dpidr.")
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _enforce_slot0_disjoint_across_roles(
|
|
504
|
+
cores: dict[str, Slice],
|
|
505
|
+
sku: str,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Refuse a dual-M55 AEN SoM whose `m55_he` and `m55_hp` slices publish
|
|
508
|
+
the SAME `flash_args.slot0_load_address` (alp-sdk #1384).
|
|
509
|
+
|
|
510
|
+
alp-sdk#1295 made this reachable for the first time: it populated
|
|
511
|
+
`debug.jlink_flash_device` for the E3/E5/E6/E7 variants too, not only
|
|
512
|
+
E1M-AEN801's, so the no-override default -- deliberately the SAME
|
|
513
|
+
address for both roles (see `_resolve_slot0_load_address`'s docstring)
|
|
514
|
+
-- can now meet a live `jlink_flash_device`.
|
|
515
|
+
|
|
516
|
+
Scoped to `os == "zephyr"` on BOTH roles (alp-sdk#1445, ported here as
|
|
517
|
+
tan-cli#744): a parked (`os: "off"`) or non-Zephyr core produces no
|
|
518
|
+
flashable artifact, so its resolved `slot0_load_address` is moot and
|
|
519
|
+
nothing would ever write to it. Comparing it anyway would refuse
|
|
520
|
+
`examples/power-timing/power-managed-sensor` -- a real, working app
|
|
521
|
+
that parks `m55_hp` -- for a collision that cannot physically happen.
|
|
522
|
+
Mirrors `_enforce_flow_d_preflight_pair`'s own `slice_.os != "zephyr"`
|
|
523
|
+
guard, this file's established convention for "only a live Zephyr slice
|
|
524
|
+
is a Flow D target".
|
|
525
|
+
|
|
526
|
+
Historical note, kept because it is why the guard long looked dead:
|
|
527
|
+
before alp-sdk#1295 the only AEN variant publishing
|
|
528
|
+
`jlink_flash_device` was E1M-AEN801's, and that SoM also declares a
|
|
529
|
+
disjoint `he_slot0`/`hp_slot0` `memory_map:` override -- so the shared
|
|
530
|
+
default address never met a live `jlink_flash_device` at all.
|
|
531
|
+
|
|
532
|
+
Still a real guard: a future AEN
|
|
533
|
+
variant that publishes `jlink_flash_device` without also declaring a
|
|
534
|
+
disjoint-slot0 override would otherwise silently reintroduce #1069's
|
|
535
|
+
HE/HP MRAM collision in `flash_args` -- flashing one core would corrupt
|
|
536
|
+
the other's slot0 window with no signal at all, the exact silent-wrong-
|
|
537
|
+
address class this file's other guards (`_enforce_flow_d_preflight_pair`,
|
|
538
|
+
`_resolve_slot0_load_address`'s own `OrchestratorError` on a half-
|
|
539
|
+
authored override) all exist to remove.
|
|
540
|
+
"""
|
|
541
|
+
he = cores.get("m55_he")
|
|
542
|
+
hp = cores.get("m55_hp")
|
|
543
|
+
if (he is None or hp is None
|
|
544
|
+
or he.os != "zephyr" or hp.os != "zephyr"
|
|
545
|
+
or he.slot0_load_address is None
|
|
546
|
+
or hp.slot0_load_address is None):
|
|
547
|
+
return
|
|
548
|
+
if he.slot0_load_address == hp.slot0_load_address:
|
|
549
|
+
raise OrchestratorError(
|
|
550
|
+
f"{sku}: m55_he and m55_hp both resolve "
|
|
551
|
+
f"flash_args.slot0_load_address to the same address "
|
|
552
|
+
f"({he.slot0_load_address}) -- this is the #1069 HE/HP MRAM "
|
|
553
|
+
f"slot0 collision (flashing one core would silently corrupt "
|
|
554
|
+
f"the other's slot0 window). Declare disjoint `he_slot0`/"
|
|
555
|
+
f"`hp_slot0` regions in this SoM preset's `memory_map:` before "
|
|
556
|
+
f"this SoM can publish debug.jlink_flash_device.")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _slice_from_resolved(
|
|
560
|
+
core_id: str,
|
|
561
|
+
entry: dict[str, Any],
|
|
562
|
+
soc_core_type: str = "",
|
|
563
|
+
jlink_flash_device: Optional[str] = None,
|
|
564
|
+
jlink_flash_device_declared: bool = False,
|
|
565
|
+
expect_dpidr: Optional[str] = None,
|
|
566
|
+
jlink_device: Optional[str] = None,
|
|
567
|
+
slot0_load_address: Optional[str] = None,
|
|
568
|
+
) -> Slice:
|
|
569
|
+
"""Build a Slice dataclass from the resolved per-core entry.
|
|
570
|
+
|
|
571
|
+
When `entry["os"]` is missing/empty, the OS is inferred from
|
|
572
|
+
`soc_core_type` via `_default_os_from_core_type()` (cortex-m* ->
|
|
573
|
+
zephyr, cortex-a* -> yocto, else "off"). Passing the empty
|
|
574
|
+
string for `soc_core_type` preserves the historical default of
|
|
575
|
+
"off".
|
|
576
|
+
|
|
577
|
+
`jlink_flash_device` is the caller's already-resolved
|
|
578
|
+
`_resolve_jlink_flash_device()` result (see that docstring) -- None
|
|
579
|
+
when this SoC variant publishes no J-Link flash-device profile.
|
|
580
|
+
|
|
581
|
+
`expect_dpidr` / `jlink_device` are the caller's already-resolved
|
|
582
|
+
`_resolve_flow_d_preflight()` PAIR for this core -- both None (the
|
|
583
|
+
preflight is not armed) or both set; never one of the two, which is
|
|
584
|
+
the half-armed shape a downstream flasher refuses (alp-sdk #1355).
|
|
585
|
+
|
|
586
|
+
`slot0_load_address` is the caller's already-resolved
|
|
587
|
+
`_resolve_slot0_load_address()` result for THIS core_id -- None when
|
|
588
|
+
this core has no AEN MRAM slot0-XIP window to publish.
|
|
589
|
+
"""
|
|
590
|
+
return Slice(
|
|
591
|
+
core_id=core_id,
|
|
592
|
+
os=str(entry.get("os") or _default_os_from_core_type(soc_core_type)),
|
|
593
|
+
app=entry.get("app"),
|
|
594
|
+
image=entry.get("image"),
|
|
595
|
+
recipe=entry.get("recipe"),
|
|
596
|
+
machine=entry.get("machine"),
|
|
597
|
+
board=entry.get("board"),
|
|
598
|
+
toolchain=entry.get("toolchain"),
|
|
599
|
+
peripherals=list(entry.get("peripherals") or []),
|
|
600
|
+
libraries=list(entry.get("libraries") or []),
|
|
601
|
+
extra_libraries=[dict(e) for e in (entry.get("extra_libraries") or [])],
|
|
602
|
+
inference=dict(entry.get("inference") or {}),
|
|
603
|
+
iot=dict(entry.get("iot") or {}),
|
|
604
|
+
memory=dict(entry.get("memory") or {}),
|
|
605
|
+
power=dict(entry.get("power") or {}),
|
|
606
|
+
# Absent -> True (the core has a HW console); only a SoM preset's
|
|
607
|
+
# `topology.<id>.hw_console: false` marks a headless core.
|
|
608
|
+
hw_console=bool(entry.get("hw_console", True)),
|
|
609
|
+
jlink_flash_device=jlink_flash_device,
|
|
610
|
+
jlink_flash_device_declared=jlink_flash_device_declared,
|
|
611
|
+
expect_dpidr=expect_dpidr,
|
|
612
|
+
jlink_device=jlink_device,
|
|
613
|
+
slot0_load_address=slot0_load_address,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _load_and_validate_yaml(path: Path,
|
|
618
|
+
metadata_root: Path) -> dict[str, Any]:
|
|
619
|
+
"""Stage 1 of the #673 Phase-1 `load_board_yaml` split: read
|
|
620
|
+
board.yaml and run schema validation.
|
|
621
|
+
|
|
622
|
+
Pass metadata_root so test stubs using non-production SKU patterns
|
|
623
|
+
(E1M-TST001 etc.) validate against their own copy of the schema
|
|
624
|
+
rather than the repo's strict pattern.
|
|
625
|
+
"""
|
|
626
|
+
path = Path(path)
|
|
627
|
+
project = _load_yaml(path)
|
|
628
|
+
_validate_board(project, metadata_root=metadata_root)
|
|
629
|
+
return project
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _resolve_board(
|
|
633
|
+
project: dict[str, Any],
|
|
634
|
+
metadata_root: Path,
|
|
635
|
+
*, skip_advisories: list[str] | None = None,
|
|
636
|
+
) -> tuple[str, Optional[str], dict[str, Any], str, dict[str, Any],
|
|
637
|
+
dict[str, Any], Optional[str], Optional[str]]:
|
|
638
|
+
"""Stage 2 of the #673 Phase-1 `load_board_yaml` split: SoM SKU
|
|
639
|
+
preset, SoC spec, and board (preset or inline) resolution.
|
|
640
|
+
|
|
641
|
+
Returns (sku, hw_rev, som_preset, silicon, soc_spec, board_preset,
|
|
642
|
+
board_name, board_hw_rev).
|
|
643
|
+
|
|
644
|
+
*skip_advisories*: threaded straight to `_resolve_board_impl`'s own
|
|
645
|
+
`_refuse_on_schema_errors` calls (tan-cli#964 review, major 6).
|
|
646
|
+
"""
|
|
647
|
+
return _resolve_board_impl(project, metadata_root, skip_advisories=skip_advisories)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _sku_family_dir(sku: str) -> Optional[str]:
|
|
651
|
+
"""The SoM-family directory name for `sku`, or None when unrecognised.
|
|
652
|
+
|
|
653
|
+
Shared by `_check_hw_rev_exists` and `_check_sdk_supports_hw_rev`: an
|
|
654
|
+
unrecognised SKU is `_resolve_board`'s error to raise, not either
|
|
655
|
+
gate's to pre-empt with a worse message, so both stay quiet on
|
|
656
|
+
`ValueError` rather than failing here.
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
return _sku_family(sku)
|
|
660
|
+
except ValueError:
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _check_hw_rev_exists(
|
|
665
|
+
metadata_root: Path,
|
|
666
|
+
*,
|
|
667
|
+
sku: str,
|
|
668
|
+
som_hw_rev: Optional[str],
|
|
669
|
+
board_name: Optional[str],
|
|
670
|
+
board_hw_rev: Optional[str],
|
|
671
|
+
board_preset: Optional[dict[str, Any]],
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Refuse an hw_rev that isn't a key in its resolved `hw_revisions:` table.
|
|
674
|
+
|
|
675
|
+
An hw_rev absent from the table used to silently resolve to empty
|
|
676
|
+
overrides -- base-revision pad routing with a clean exit code, i.e. a
|
|
677
|
+
wrong-hardware emit (alp-sdk #1025, the safe half). Runs BEFORE
|
|
678
|
+
`_check_sdk_supports_hw_rev`'s min/max comparison: an unknown revision
|
|
679
|
+
has no declared range to compare against, so reporting it as
|
|
680
|
+
out-of-range would name the wrong cause.
|
|
681
|
+
|
|
682
|
+
Existence-only, deliberately blind to `status:` -- a revision that
|
|
683
|
+
EXISTS passes regardless of being `status: reserved`, `status: tbd`,
|
|
684
|
+
or carrying no `status` key at all. Skips a side entirely when that
|
|
685
|
+
side's table doesn't exist to check against (inline boards carry no
|
|
686
|
+
per-board `hw_revisions:` table).
|
|
687
|
+
"""
|
|
688
|
+
family_dir = _sku_family_dir(sku)
|
|
689
|
+
|
|
690
|
+
known = sdk_compat.family_revision_known(metadata_root, family_dir, som_hw_rev)
|
|
691
|
+
if known is False:
|
|
692
|
+
available = sdk_compat.family_available_revisions(metadata_root, family_dir)
|
|
693
|
+
raise SdkRevisionUnknown(
|
|
694
|
+
f"SoM {sku} hw_rev {som_hw_rev!r} is not a known hardware "
|
|
695
|
+
f"revision. Available hw_rev(s) for {sku}: {available}.")
|
|
696
|
+
|
|
697
|
+
known = sdk_compat.revision_known(board_preset, board_hw_rev)
|
|
698
|
+
if known is False:
|
|
699
|
+
available = sorted((board_preset or {}).get("hw_revisions", {}).keys())
|
|
700
|
+
raise SdkRevisionUnknown(
|
|
701
|
+
f"board {board_name} hw_rev {board_hw_rev!r} is not a known "
|
|
702
|
+
f"hardware revision. Available hw_rev(s) for {board_name}: "
|
|
703
|
+
f"{available}.")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _status_repr(status: Optional[str]) -> str:
|
|
707
|
+
"""Render a `status:` value for an error message.
|
|
708
|
+
|
|
709
|
+
`status: None` reads as if the key were literally set to the word
|
|
710
|
+
"None" -- indistinguishable from a typo'd value. A missing key gets
|
|
711
|
+
its own wording instead.
|
|
712
|
+
"""
|
|
713
|
+
if status is None:
|
|
714
|
+
return "carries no `status:` key"
|
|
715
|
+
return f"status: {status!r}"
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _check_hw_rev_buildable(
|
|
719
|
+
metadata_root: Path,
|
|
720
|
+
*,
|
|
721
|
+
sku: str,
|
|
722
|
+
som_hw_rev: Optional[str],
|
|
723
|
+
board_name: Optional[str],
|
|
724
|
+
board_hw_rev: Optional[str],
|
|
725
|
+
board_preset: Optional[dict[str, Any]],
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Refuse an hw_rev that EXISTS but whose declared `status:` refuses a
|
|
728
|
+
build (alp-sdk #1025, the maintainer's broad-reading decision on the
|
|
729
|
+
status half).
|
|
730
|
+
|
|
731
|
+
Runs AFTER `_check_hw_rev_exists`: a revision absent from the table is
|
|
732
|
+
that gate's failure to report, not this one's -- there is no status to
|
|
733
|
+
have read. `status: reserved`, `status: tbd`, and a revision carrying
|
|
734
|
+
no `status` key at all are all refused; every other declared status
|
|
735
|
+
(`production`, `preview`, `preliminary`, `deprecated`) passes.
|
|
736
|
+
"""
|
|
737
|
+
family_dir = _sku_family_dir(sku)
|
|
738
|
+
|
|
739
|
+
buildable = sdk_compat.family_revision_buildable(metadata_root, family_dir, som_hw_rev)
|
|
740
|
+
if buildable is False:
|
|
741
|
+
status = sdk_compat.family_revision(metadata_root, family_dir, som_hw_rev).get("status")
|
|
742
|
+
raise SdkRevisionNotBuildable(
|
|
743
|
+
f"SoM {sku} hw_rev {som_hw_rev!r} exists but is not buildable "
|
|
744
|
+
f"({_status_repr(status)}).")
|
|
745
|
+
|
|
746
|
+
buildable = sdk_compat.revision_buildable(board_preset, board_hw_rev)
|
|
747
|
+
if buildable is False:
|
|
748
|
+
status = sdk_compat.board_revision(board_preset, board_hw_rev).get("status")
|
|
749
|
+
raise SdkRevisionNotBuildable(
|
|
750
|
+
f"board {board_name} hw_rev {board_hw_rev!r} exists but is not "
|
|
751
|
+
f"buildable ({_status_repr(status)}).")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _check_sdk_supports_hw_rev(
|
|
755
|
+
metadata_root: Path,
|
|
756
|
+
*,
|
|
757
|
+
sku: str,
|
|
758
|
+
som_hw_rev: Optional[str],
|
|
759
|
+
board_name: Optional[str],
|
|
760
|
+
board_hw_rev: Optional[str],
|
|
761
|
+
board_preset: Optional[dict[str, Any]],
|
|
762
|
+
) -> None:
|
|
763
|
+
"""Refuse when the running SDK is outside a requested revision's range.
|
|
764
|
+
|
|
765
|
+
`metadata/sdk_version.yaml` has always documented this refusal -- and
|
|
766
|
+
the matching `exit code 3` in `scripts/validate_board_yaml.py` -- but
|
|
767
|
+
nothing implemented it, so an upgraded SDK silently emitted for a
|
|
768
|
+
revision it no longer supported (alp-sdk #1019).
|
|
769
|
+
|
|
770
|
+
Callers run `_check_hw_rev_exists` first: an unknown revision is that
|
|
771
|
+
gate's failure, not this one's -- it has no bounds to compare.
|
|
772
|
+
"""
|
|
773
|
+
sdk_version = sdk_compat.read_sdk_version(metadata_root)
|
|
774
|
+
if sdk_version is None:
|
|
775
|
+
return
|
|
776
|
+
|
|
777
|
+
family_dir = _sku_family_dir(sku)
|
|
778
|
+
|
|
779
|
+
reason = sdk_compat.check(
|
|
780
|
+
sdk_version,
|
|
781
|
+
som_revision=sdk_compat.family_revision(
|
|
782
|
+
metadata_root, family_dir, som_hw_rev),
|
|
783
|
+
som_label=f"SoM {sku} hw_rev {som_hw_rev}",
|
|
784
|
+
board_revision_entry=sdk_compat.board_revision(
|
|
785
|
+
board_preset, board_hw_rev),
|
|
786
|
+
board_label=f"board {board_name} hw_rev {board_hw_rev}",
|
|
787
|
+
)
|
|
788
|
+
if reason:
|
|
789
|
+
raise SdkRevisionUnsupported(
|
|
790
|
+
f"SDK {sdk_version} does not support this hardware revision: "
|
|
791
|
+
f"{reason}. Pin an SDK inside the declared range, or set a "
|
|
792
|
+
f"hw_rev this SDK supports.")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _refuse_on_schema_errors(
|
|
796
|
+
doc: dict[str, Any], schema_path: Path, source: Path, schema_name: str, subject: str,
|
|
797
|
+
*, skip_advisories: list[str] | None = None,
|
|
798
|
+
) -> None:
|
|
799
|
+
"""tan-cli#964: refuse a `board.yaml`-bound `tan build`/`tan generate` run
|
|
800
|
+
when the SoM preset or SoC spec it just read does not validate against
|
|
801
|
+
its own schema.
|
|
802
|
+
|
|
803
|
+
This is the REFUSE half of #964's decided rule: `load_board_yaml` is what
|
|
804
|
+
`tan build` and `tan generate` (via `planner_root.emit` /
|
|
805
|
+
`planner_emit.render`) call to turn a `board.yaml` into a plan or an
|
|
806
|
+
emitted file, and every `OrchestratorError` raised anywhere in that walk
|
|
807
|
+
already becomes a coded refusal at the command layer (`build.plan-
|
|
808
|
+
unavailable` / `generate.emit-failed`) rather than a traceback -- so
|
|
809
|
+
raising here, at the point the document is READ, closes the read-path gap
|
|
810
|
+
#964 is about with the SAME refusal machinery every other loader check in
|
|
811
|
+
this file already uses (`_check_hw_rev_exists` and friends), not a new
|
|
812
|
+
one. `tan.core.metadata_schema.validate_document` never raises itself --
|
|
813
|
+
a CORRUPT schema file degrades to one synthetic message (which DOES
|
|
814
|
+
refuse, below, exactly like a real violation); an ABSENT schema file
|
|
815
|
+
degrades to `[]`, no message at all, which must never refuse (see
|
|
816
|
+
`validate_document`'s own docstring) -- the refusal, and its exit
|
|
817
|
+
code/issue code, are entirely this function's unconditional-if-non-empty
|
|
818
|
+
read of the returned list.
|
|
819
|
+
|
|
820
|
+
*skip_advisories* (tan-cli#964 review, major 6): when given, collects
|
|
821
|
+
`tan.core.metadata_schema.missing_schema_note`'s disclosure for the
|
|
822
|
+
ABSENT-schema case -- "skip-but-disclose", not a silent skip. Checked
|
|
823
|
+
unconditionally, before the refuse-worthy violations: a missing schema
|
|
824
|
+
means `validate_document` returns `[]` and this function would otherwise
|
|
825
|
+
return having said nothing at all, which is the exact vacuous-gate shape
|
|
826
|
+
the review measured (`tan generate --target os-topology` writing an
|
|
827
|
+
unvalidated SoC spec to disk at `ok: true`, `issues: []`).
|
|
828
|
+
"""
|
|
829
|
+
if skip_advisories is not None:
|
|
830
|
+
note = missing_schema_note(schema_path, source=source)
|
|
831
|
+
if note is not None:
|
|
832
|
+
skip_advisories.append(note)
|
|
833
|
+
errors = validate_document(doc, schema_path, source)
|
|
834
|
+
if not errors:
|
|
835
|
+
return
|
|
836
|
+
raise OrchestratorError(
|
|
837
|
+
f"{subject} does not validate against {schema_name}:\n" +
|
|
838
|
+
"\n".join(f" - {e}" for e in errors))
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _resolve_board_impl(
|
|
842
|
+
project: dict[str, Any],
|
|
843
|
+
metadata_root: Path,
|
|
844
|
+
*, skip_advisories: list[str] | None = None,
|
|
845
|
+
) -> tuple[str, Optional[str], dict[str, Any], str, dict[str, Any],
|
|
846
|
+
dict[str, Any], Optional[str], Optional[str]]:
|
|
847
|
+
sku = project["som"]["sku"]
|
|
848
|
+
hw_rev = project["som"].get("hw_rev")
|
|
849
|
+
|
|
850
|
+
# Resolve SKU preset.
|
|
851
|
+
sku_preset_path = metadata_root / "e1m_modules" / f"{sku}.yaml"
|
|
852
|
+
if not sku_preset_path.is_file():
|
|
853
|
+
raise OrchestratorError(
|
|
854
|
+
f"no preset for SoM SKU {sku} at "
|
|
855
|
+
f"{sku_preset_path.relative_to(REPO) if sku_preset_path.is_relative_to(REPO) else sku_preset_path}")
|
|
856
|
+
som_preset = _load_yaml(sku_preset_path)
|
|
857
|
+
_refuse_on_schema_errors(
|
|
858
|
+
som_preset, som_preset_schema_path(metadata_root), sku_preset_path,
|
|
859
|
+
"som-preset-v2", f"SoM preset {sku}", skip_advisories=skip_advisories)
|
|
860
|
+
|
|
861
|
+
# Resolve SoC spec via the preset's `silicon:` ref.
|
|
862
|
+
silicon = som_preset.get("silicon")
|
|
863
|
+
if not silicon:
|
|
864
|
+
raise OrchestratorError(
|
|
865
|
+
f"SoM preset {sku} has no `silicon:` field")
|
|
866
|
+
soc_path = _silicon_to_soc_path(silicon, metadata_root)
|
|
867
|
+
if not soc_path.is_file():
|
|
868
|
+
raise OrchestratorError(
|
|
869
|
+
f"no SoC spec at {soc_path.relative_to(REPO) if soc_path.is_relative_to(REPO) else soc_path} for ref '{silicon}'")
|
|
870
|
+
soc_spec = _load_json(soc_path)
|
|
871
|
+
_refuse_on_schema_errors(
|
|
872
|
+
soc_spec, soc_spec_schema_path(metadata_root), soc_path,
|
|
873
|
+
"soc-spec-v1", f"SoC spec for {silicon}", skip_advisories=skip_advisories)
|
|
874
|
+
|
|
875
|
+
# Board definition. Two mutually-exclusive sources (the
|
|
876
|
+
# schema's `oneOf` rule enforces this):
|
|
877
|
+
# - `preset: <name>` -> load metadata/boards/<name>.yaml
|
|
878
|
+
# - inline `name:` + `populated:` + `e1m_routes:` at top level
|
|
879
|
+
# Either way the rest of the loader sees a single board_preset
|
|
880
|
+
# dict with `name`, `populated`, `e1m_routes`.
|
|
881
|
+
if "preset" in project:
|
|
882
|
+
board_preset = _resolve_board_preset(project["preset"], metadata_root)
|
|
883
|
+
_check_board_hosts_som_family(
|
|
884
|
+
sku, som_preset, project["preset"], board_preset)
|
|
885
|
+
else:
|
|
886
|
+
board_preset = _synthesize_inline_board(project)
|
|
887
|
+
board_name = board_preset.get("name")
|
|
888
|
+
board_hw_rev = (project.get("hw_rev")
|
|
889
|
+
or board_preset.get("default_hw_rev"))
|
|
890
|
+
|
|
891
|
+
return (sku, hw_rev, som_preset, silicon, soc_spec, board_preset,
|
|
892
|
+
board_name, board_hw_rev)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _validate_topology_cores(
|
|
896
|
+
project: dict[str, Any],
|
|
897
|
+
som_preset: dict[str, Any],
|
|
898
|
+
soc_spec: dict[str, Any],
|
|
899
|
+
sku: str,
|
|
900
|
+
silicon: str,
|
|
901
|
+
board_preset: dict[str, Any],
|
|
902
|
+
board_name: Optional[str],
|
|
903
|
+
metadata_root: Path,
|
|
904
|
+
) -> tuple[dict[str, Slice], list[IpcEntry]]:
|
|
905
|
+
"""Stage 3 of the #673 Phase-1 `load_board_yaml` split: per-core
|
|
906
|
+
topology resolution + OS/class enforcement, IPC endpoint
|
|
907
|
+
cross-checks, and the optional top-level `pins:` cross-check
|
|
908
|
+
against the resolved board's `e1m_routes:`.
|
|
909
|
+
|
|
910
|
+
Returns (cores, ipc_entries).
|
|
911
|
+
"""
|
|
912
|
+
# Compute per-core effective mapping.
|
|
913
|
+
project_cores = project.get("cores") or {}
|
|
914
|
+
som_topology = som_preset.get("topology") or {}
|
|
915
|
+
soc_core_ids = [c["id"] for c in (soc_spec.get("cores") or []) if "id" in c]
|
|
916
|
+
|
|
917
|
+
# Reject SoM topology keys that don't exist in the SoC spec --
|
|
918
|
+
# surfaces preset bugs early.
|
|
919
|
+
bad_topology = [k for k in som_topology.keys() if k not in soc_core_ids]
|
|
920
|
+
if bad_topology:
|
|
921
|
+
raise OrchestratorError(
|
|
922
|
+
f"SoM preset {sku} `topology:` references core IDs "
|
|
923
|
+
f"{bad_topology} that aren't in SoC {silicon}'s "
|
|
924
|
+
f"cores[] (known: {soc_core_ids})")
|
|
925
|
+
|
|
926
|
+
# Phase B gap fix G-4 (hardened by #603): catch the cross-class
|
|
927
|
+
# `som.sku:` swap where `cores.<key>` doesn't match this SoM preset's
|
|
928
|
+
# `topology:`. Example: customer has `cores.m55_hp:` and swaps
|
|
929
|
+
# som.sku from E1M-AEN801 (topology: m55_hp + m55_he + a32_cluster)
|
|
930
|
+
# to E1M-V2N101 (topology: m33_sm + a55_cluster). Pre-fix the slice-
|
|
931
|
+
# build loop iterated topology keys, NOT project_cores keys, so
|
|
932
|
+
# `cores.m55_hp:` was silently dropped and the customer got an
|
|
933
|
+
# empty slice with no diagnostic.
|
|
934
|
+
#
|
|
935
|
+
# #603: EVERY unmatched key under `cores:` is a hard error, not just
|
|
936
|
+
# the all-unmatched case -- a `cores:` mapping with one valid core
|
|
937
|
+
# and one typo used to only warn-and-drop the typo, so a misspelled
|
|
938
|
+
# core silently vanished from the build while the file still
|
|
939
|
+
# validated "clean". There is no compatibility policy that
|
|
940
|
+
# tolerates an unknown core key, so this is unconditional. The
|
|
941
|
+
# SoC-level mismatch check (topology is a subset of SoC core IDs
|
|
942
|
+
# per the bad_topology guard above) is subsumed by this topology-
|
|
943
|
+
# level check: anything not in topology is wrong from the
|
|
944
|
+
# customer's POV, whether it's SoC-absent or merely SoM-preset-
|
|
945
|
+
# absent.
|
|
946
|
+
topology_keys = set(som_topology.keys())
|
|
947
|
+
project_keys = set(project_cores.keys())
|
|
948
|
+
unmatched = sorted(project_keys - topology_keys)
|
|
949
|
+
if unmatched:
|
|
950
|
+
sku_topology = sorted(topology_keys)
|
|
951
|
+
raise OrchestratorError(
|
|
952
|
+
f"board.yaml `cores:` declares unknown core id(s) {unmatched} "
|
|
953
|
+
f"that {sku}'s `topology:` does not expose. "
|
|
954
|
+
f"Did you mean one of: {sku_topology}?")
|
|
955
|
+
|
|
956
|
+
# Index SoC cores[] by id so we can look up `type` for the OS
|
|
957
|
+
# default inference (Finding A: pre-2026-05-18 every SoM YAML's
|
|
958
|
+
# `topology.<core>.os` followed cortex-m* -> zephyr, cortex-a* ->
|
|
959
|
+
# yocto; the loader now infers that fallback when `os:` is absent).
|
|
960
|
+
soc_core_type_by_id: dict[str, str] = {
|
|
961
|
+
str(c["id"]): str(c.get("type") or "")
|
|
962
|
+
for c in (soc_spec.get("cores") or []) if "id" in c
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
# SoC-variant `debug:` facts, resolved from ONE variant match (see
|
|
966
|
+
# `_resolve_variant_debug`). `jlink_flash_device` is not per-core, so
|
|
967
|
+
# it is resolved once and reused for every slice below; the
|
|
968
|
+
# `expect_dpidr`/`jlink_device` preflight pair IS per-core (the read
|
|
969
|
+
# attach profile is keyed by `cores[].id`) and is resolved inside the
|
|
970
|
+
# loop.
|
|
971
|
+
variant_debug = _resolve_variant_debug(som_preset, soc_spec)
|
|
972
|
+
jlink_flash_device = _resolve_jlink_flash_device(variant_debug)
|
|
973
|
+
# tan-cli#734: carried alongside the value, not derived from it -- the
|
|
974
|
+
# two differ exactly in the declared-null case this exists for.
|
|
975
|
+
jlink_flash_device_declared = _jlink_flash_device_declared(variant_debug)
|
|
976
|
+
|
|
977
|
+
cores: dict[str, Slice] = {}
|
|
978
|
+
for core_id in soc_core_ids:
|
|
979
|
+
resolved = _resolve_topology_for_core(
|
|
980
|
+
core_id, project_cores, som_topology)
|
|
981
|
+
if resolved is None:
|
|
982
|
+
# Multi-core SoCs require either project cores: or topology
|
|
983
|
+
# preset coverage; single-core SoCs default the one core to
|
|
984
|
+
# the preset (which we already checked above -- if we got
|
|
985
|
+
# here for a single-core SoC, the preset has no topology
|
|
986
|
+
# entry for the only core, which is a real error).
|
|
987
|
+
raise OrchestratorError(
|
|
988
|
+
f"core '{core_id}' has no runtime assigned (neither "
|
|
989
|
+
f"board.yaml `cores.{core_id}` nor SoM preset "
|
|
990
|
+
f"`topology.{core_id}` is set)")
|
|
991
|
+
expect_dpidr, jlink_device = _resolve_flow_d_preflight(
|
|
992
|
+
variant_debug, core_id)
|
|
993
|
+
# PRESENCE, not value -- the same promotion the `flash_args` emit
|
|
994
|
+
# uses, because alp-sdk#1444/#1446 (`86260edc`, "complete the Flow D
|
|
995
|
+
# truthiness fix") moved this gate to presence upstream.
|
|
996
|
+
#
|
|
997
|
+
# It used to be VALUE here, deliberately: tan-cli#744 matched
|
|
998
|
+
# alp-sdk's own gate of the day, and tan-cli#737's presence
|
|
999
|
+
# promotion had to be reverted because it emitted a
|
|
1000
|
+
# `slot0_load_address` alp-sdk did not. That rationale expired when
|
|
1001
|
+
# upstream flipped -- the byte-parity suite then caught the SAME
|
|
1002
|
+
# example from the other side: `usb-host-storage` (E1M-AEN401)
|
|
1003
|
+
# `--emit system-manifest` line 19, sdk emitting
|
|
1004
|
+
# `slot0_load_address: '0x802b0000'` where tan emitted nothing
|
|
1005
|
+
# (tan-cli#756, measured against alp-sdk 88318e75 on all three OSes).
|
|
1006
|
+
#
|
|
1007
|
+
# Presence is also the coherent half of the pair: a declared
|
|
1008
|
+
# `jlink_flash_device: null` (e4.json is the live case) reaches
|
|
1009
|
+
# `flash_args` AS a present null so the consumer's presence check
|
|
1010
|
+
# (`flash_plan._fa_has_key`) refuses loudly, and the slot0 window a
|
|
1011
|
+
# Flow D write would target travels with it instead of vanishing.
|
|
1012
|
+
# An ABSENT key still resolves nothing, on both gates.
|
|
1013
|
+
slot0_load_address = (
|
|
1014
|
+
_resolve_slot0_load_address(som_preset, core_id)
|
|
1015
|
+
if (jlink_flash_device_declared or jlink_flash_device is not None)
|
|
1016
|
+
else None)
|
|
1017
|
+
slice_ = _slice_from_resolved(
|
|
1018
|
+
core_id, resolved,
|
|
1019
|
+
soc_core_type=soc_core_type_by_id.get(core_id, ""),
|
|
1020
|
+
jlink_flash_device=jlink_flash_device,
|
|
1021
|
+
jlink_flash_device_declared=jlink_flash_device_declared,
|
|
1022
|
+
expect_dpidr=expect_dpidr,
|
|
1023
|
+
jlink_device=jlink_device,
|
|
1024
|
+
slot0_load_address=slot0_load_address,
|
|
1025
|
+
)
|
|
1026
|
+
_enforce_flow_d_preflight_pair(slice_, variant_debug, sku)
|
|
1027
|
+
_enforce_loader_rules(slice_, metadata_root)
|
|
1028
|
+
_enforce_os_matches_core_class(
|
|
1029
|
+
slice_, soc_core_type_by_id.get(core_id, ""))
|
|
1030
|
+
cores[core_id] = slice_
|
|
1031
|
+
|
|
1032
|
+
_enforce_slot0_disjoint_across_roles(cores, sku)
|
|
1033
|
+
|
|
1034
|
+
# IPC entries.
|
|
1035
|
+
ipc_raw = project.get("ipc") or []
|
|
1036
|
+
ipc_entries: list[IpcEntry] = []
|
|
1037
|
+
for entry in ipc_raw:
|
|
1038
|
+
ipc_entries.append(IpcEntry(
|
|
1039
|
+
name=entry["name"],
|
|
1040
|
+
kind=entry["kind"],
|
|
1041
|
+
endpoints=list(entry["endpoints"]),
|
|
1042
|
+
carve_out_kb=int(entry["carve_out_kb"]),
|
|
1043
|
+
cacheable=entry.get("cacheable"),
|
|
1044
|
+
address=entry.get("address"),
|
|
1045
|
+
))
|
|
1046
|
+
|
|
1047
|
+
# alp-sdk #1088: `kind: rpmsg` has no cache-maintenance layer.
|
|
1048
|
+
# `cfg->cacheable` is stored on the backend struct
|
|
1049
|
+
# (`src/backends/rpc/{zephyr,yocto}_drv.c`) and never read again -- no
|
|
1050
|
+
# `sys_cache_*` call exists anywhere under `src/` or `include/`.
|
|
1051
|
+
# `cacheable: true` on a rpmsg entry would therefore select a code path
|
|
1052
|
+
# that promises coherency it can't deliver, which is worse than no flag
|
|
1053
|
+
# at all -- reject it here rather than silently honouring it. Real fix
|
|
1054
|
+
# (sys_cache_data_flush_range / sys_cache_data_invd_range in
|
|
1055
|
+
# <alp/rpc.h>) remains open; see alp-sdk #1088.
|
|
1056
|
+
for e in ipc_entries:
|
|
1057
|
+
if e.kind == "rpmsg" and e.cacheable:
|
|
1058
|
+
raise OrchestratorError(
|
|
1059
|
+
f"ipc entry '{e.name}': kind: rpmsg does not support "
|
|
1060
|
+
f"cacheable: true -- <alp/rpc.h> has no cache-maintenance "
|
|
1061
|
+
f"implementation yet (#1088). The D-cache is forced off "
|
|
1062
|
+
f"for this carve-out's endpoints instead; remove "
|
|
1063
|
+
f"`cacheable: true` (or set it to false).")
|
|
1064
|
+
|
|
1065
|
+
# Loader rule §4.5.6: every ipc endpoint must be a core with
|
|
1066
|
+
# os != off.
|
|
1067
|
+
for e in ipc_entries:
|
|
1068
|
+
for ep in e.endpoints:
|
|
1069
|
+
if ep not in cores:
|
|
1070
|
+
raise OrchestratorError(
|
|
1071
|
+
f"ipc entry '{e.name}' references core '{ep}' "
|
|
1072
|
+
f"that isn't in this project")
|
|
1073
|
+
if cores[ep].os == "off":
|
|
1074
|
+
raise OrchestratorError(
|
|
1075
|
+
f"ipc entry '{e.name}' references core '{ep}' "
|
|
1076
|
+
f"which is os: off")
|
|
1077
|
+
|
|
1078
|
+
# Optional top-level `pins:` cross-check. When the project
|
|
1079
|
+
# lists which E1M pads it actively uses, every entry must exist
|
|
1080
|
+
# in the resolved board's `e1m_routes:` block; entries that
|
|
1081
|
+
# supply a `macro:` must also match the board's macro for that
|
|
1082
|
+
# pad. Catches typos + demos drifting from the EVK preset's
|
|
1083
|
+
# wiring. Each entry is either a bare string (just the pad
|
|
1084
|
+
# name) or a `{e1m, macro?, doc?}` mapping.
|
|
1085
|
+
used_pins = list(project.get("pins") or [])
|
|
1086
|
+
if used_pins:
|
|
1087
|
+
routes = (board_preset or {}).get("e1m_routes") or {}
|
|
1088
|
+
# Build a {pad -> [macros]} index; one pad can have several
|
|
1089
|
+
# macros aliasing it (e.g. E1M_PWM1 maps to EVK_PWM_LED_BLUE
|
|
1090
|
+
# AND EVK_ARD_PWM1 on the EVK).
|
|
1091
|
+
macros_by_pad: dict[str, set[str]] = {}
|
|
1092
|
+
for section in ("gpio", "buses", "pwm", "adc", "dac", "i2s", "can", "qenc"):
|
|
1093
|
+
for entry in (routes.get(section) or []):
|
|
1094
|
+
e1m = entry.get("e1m")
|
|
1095
|
+
macro = entry.get("macro")
|
|
1096
|
+
if isinstance(e1m, str) and isinstance(macro, str):
|
|
1097
|
+
macros_by_pad.setdefault(e1m, set()).add(macro)
|
|
1098
|
+
board_label = board_name or "<inline>"
|
|
1099
|
+
for idx, item in enumerate(used_pins):
|
|
1100
|
+
if isinstance(item, str):
|
|
1101
|
+
e1m_pad, macro_decl = item, None
|
|
1102
|
+
elif isinstance(item, dict):
|
|
1103
|
+
e1m_pad = item.get("e1m")
|
|
1104
|
+
macro_decl = item.get("macro")
|
|
1105
|
+
else:
|
|
1106
|
+
raise OrchestratorError(
|
|
1107
|
+
f"board.yaml `pins[{idx}]` is neither a string nor a mapping")
|
|
1108
|
+
if e1m_pad not in macros_by_pad:
|
|
1109
|
+
raise OrchestratorError(
|
|
1110
|
+
f"board.yaml `pins[{idx}].e1m: {e1m_pad}` is not in the "
|
|
1111
|
+
f"resolved board '{board_label}'s `e1m_routes:` block. "
|
|
1112
|
+
f"Known pads: {sorted(macros_by_pad.keys())}")
|
|
1113
|
+
if macro_decl is not None and macro_decl not in macros_by_pad[e1m_pad]:
|
|
1114
|
+
raise OrchestratorError(
|
|
1115
|
+
f"board.yaml `pins[{idx}].macro: {macro_decl}` does not "
|
|
1116
|
+
f"match the resolved board '{board_label}'s macros for "
|
|
1117
|
+
f"pad {e1m_pad}: {sorted(macros_by_pad[e1m_pad])}")
|
|
1118
|
+
|
|
1119
|
+
return cores, ipc_entries
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _resolve_storage(
|
|
1123
|
+
project: dict[str, Any],
|
|
1124
|
+
som_preset: dict[str, Any],
|
|
1125
|
+
sku: str,
|
|
1126
|
+
metadata_root: Path,
|
|
1127
|
+
) -> list[StorageEntry]:
|
|
1128
|
+
"""Stage 4 of the #673 Phase-1 `load_board_yaml` split: storage
|
|
1129
|
+
partitions (board.yaml `storage:` block). Parse into StorageEntry
|
|
1130
|
+
dataclasses + cross-field check: every `flash_device:` must resolve
|
|
1131
|
+
to either a memory_map region name or an `on_module.ospi_memories:`
|
|
1132
|
+
key. Resolution of base addresses / overlap detection happens in
|
|
1133
|
+
`resolve_storage_partitions()`; the loader catches typos eagerly
|
|
1134
|
+
because they are cheap to surface before the build kicks off.
|
|
1135
|
+
"""
|
|
1136
|
+
storage_raw = project.get("storage") or []
|
|
1137
|
+
storage_entries: list[StorageEntry] = []
|
|
1138
|
+
for idx, item in enumerate(storage_raw):
|
|
1139
|
+
# `fs` defaults to `raw` when omitted. The legacy `raw: true` alias is
|
|
1140
|
+
# gone: `board.schema.json` no longer declares the property and sets
|
|
1141
|
+
# `additionalProperties: false` on storage items, so a board carrying it
|
|
1142
|
+
# is rejected at validation rather than normalised here. Measured before
|
|
1143
|
+
# removal: zero tracked `board.yaml` files used it.
|
|
1144
|
+
fs = item.get("fs")
|
|
1145
|
+
if fs is None:
|
|
1146
|
+
fs = "raw"
|
|
1147
|
+
storage_entries.append(StorageEntry(
|
|
1148
|
+
name=item["name"],
|
|
1149
|
+
size_kib=int(item["size_kib"]),
|
|
1150
|
+
fs=fs,
|
|
1151
|
+
mount=item.get("mount"),
|
|
1152
|
+
flash_device=item.get("flash_device"),
|
|
1153
|
+
offset_kib=(int(item["offset_kib"])
|
|
1154
|
+
if item.get("offset_kib") is not None else None),
|
|
1155
|
+
))
|
|
1156
|
+
|
|
1157
|
+
# Cross-field: known flash device set is memory_map names + ospi keys.
|
|
1158
|
+
if storage_entries:
|
|
1159
|
+
known_devices = set(_known_flash_devices(som_preset, metadata_root))
|
|
1160
|
+
for entry in storage_entries:
|
|
1161
|
+
if entry.flash_device is None:
|
|
1162
|
+
continue # resolver will block it with a clear reason
|
|
1163
|
+
if _is_ospi_key_unassembled(som_preset, entry.flash_device):
|
|
1164
|
+
# Same #2311 guard as the PSA ITS/PS backing-store check
|
|
1165
|
+
# below: an `ospi_memories` key with `assembled: false`
|
|
1166
|
+
# (e.g. E1M-AEN801's `ospi0`/`ospi1`) is a designed-in
|
|
1167
|
+
# footprint, not a part this SKU actually carries -- refuse
|
|
1168
|
+
# it as a `storage[].flash_device` with the specific reason
|
|
1169
|
+
# rather than the generic "does not resolve" message.
|
|
1170
|
+
where = f"SoM {sku}" if sku else "this SoM"
|
|
1171
|
+
yaml_ref = (f"metadata/e1m_modules/{sku}.yaml" if sku
|
|
1172
|
+
else "its SoM preset YAML")
|
|
1173
|
+
raise OrchestratorError(
|
|
1174
|
+
f"board.yaml `storage[{entry.name}].flash_device: "
|
|
1175
|
+
f"{entry.flash_device}` names on-module OSPI part "
|
|
1176
|
+
f"'{entry.flash_device}', which is not assembled on "
|
|
1177
|
+
f"{where} (assembled: false in {yaml_ref}); pick a "
|
|
1178
|
+
f"flash device backed by a part this SKU actually "
|
|
1179
|
+
f"carries")
|
|
1180
|
+
if entry.flash_device not in known_devices:
|
|
1181
|
+
raise OrchestratorError(
|
|
1182
|
+
f"board.yaml `storage[{entry.name}].flash_device: "
|
|
1183
|
+
f"{entry.flash_device}` does not resolve to any "
|
|
1184
|
+
f"flash device on SoM {sku}. Known devices: "
|
|
1185
|
+
f"{sorted(known_devices)}")
|
|
1186
|
+
# Name uniqueness within storage[].
|
|
1187
|
+
names_seen: set[str] = set()
|
|
1188
|
+
for entry in storage_entries:
|
|
1189
|
+
if entry.name in names_seen:
|
|
1190
|
+
raise OrchestratorError(
|
|
1191
|
+
f"board.yaml `storage:` declares partition "
|
|
1192
|
+
f"`{entry.name}` more than once; names must be "
|
|
1193
|
+
f"unique within the project")
|
|
1194
|
+
names_seen.add(entry.name)
|
|
1195
|
+
|
|
1196
|
+
return storage_entries
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _validate_cross_fields(
|
|
1200
|
+
project: dict[str, Any],
|
|
1201
|
+
som_preset: dict[str, Any],
|
|
1202
|
+
sku: str,
|
|
1203
|
+
storage_entries: list[StorageEntry],
|
|
1204
|
+
metadata_root: Path,
|
|
1205
|
+
) -> dict[str, Any]:
|
|
1206
|
+
"""Stage 5 of the #673 Phase-1 `load_board_yaml` split:
|
|
1207
|
+
`security.psa:` cross-field validation. The schema is
|
|
1208
|
+
authoritative on field types; this block enforces the references:
|
|
1209
|
+
ITS/PS storage names must resolve to a `storage[].name`, a SoM
|
|
1210
|
+
memory_map region name, OR an `on_module.ospi_memories:` key
|
|
1211
|
+
(PS-class storage often lives on an on-module OSPI part rather
|
|
1212
|
+
than in MRAM); `attestation_root: optiga_trust_m` requires the
|
|
1213
|
+
SoM to physically ship OPTIGA Trust M. Errors point at the
|
|
1214
|
+
offending board.yaml path so the customer can fix it.
|
|
1215
|
+
|
|
1216
|
+
Returns the raw `security:` block for BoardProject assembly.
|
|
1217
|
+
"""
|
|
1218
|
+
security_block = dict(project.get("security") or {})
|
|
1219
|
+
psa = dict(security_block.get("psa") or {})
|
|
1220
|
+
if psa:
|
|
1221
|
+
storage_name_set = {e.name for e in storage_entries}
|
|
1222
|
+
try:
|
|
1223
|
+
mem_map = resolve_memory_map(som_preset, metadata_root)
|
|
1224
|
+
except Exception: # noqa: BLE001
|
|
1225
|
+
mem_map = []
|
|
1226
|
+
region_names = {
|
|
1227
|
+
str(r.get("name")) for r in mem_map
|
|
1228
|
+
if isinstance(r, dict) and r.get("name")
|
|
1229
|
+
}
|
|
1230
|
+
ospi_keys = {
|
|
1231
|
+
str(k) for k in
|
|
1232
|
+
((som_preset.get("on_module") or {}).get("ospi_memories") or {}).keys()
|
|
1233
|
+
if isinstance(k, str)
|
|
1234
|
+
}
|
|
1235
|
+
valid_refs = storage_name_set | region_names | ospi_keys
|
|
1236
|
+
|
|
1237
|
+
def _check_backing_store(field: str) -> None:
|
|
1238
|
+
ref = psa.get(field)
|
|
1239
|
+
if ref is None:
|
|
1240
|
+
return
|
|
1241
|
+
ref_str = str(ref)
|
|
1242
|
+
if ref_str in ospi_keys and _is_ospi_key_unassembled(
|
|
1243
|
+
som_preset, ref_str):
|
|
1244
|
+
# Same #2311 guard as `_known_flash_devices()` /
|
|
1245
|
+
# `_resolve_flash_device()` in partition.py: an
|
|
1246
|
+
# `ospi_memories` key with `assembled: false` (e.g.
|
|
1247
|
+
# E1M-AEN801's `ospi0`/`ospi1`) is a designed-in footprint,
|
|
1248
|
+
# not a part this SKU actually carries -- refuse it as a
|
|
1249
|
+
# PSA ITS/PS backing store with the specific reason rather
|
|
1250
|
+
# than the generic "does not resolve" message below.
|
|
1251
|
+
sku = som_preset.get("sku")
|
|
1252
|
+
where = f"SoM {sku}" if sku else "this SoM"
|
|
1253
|
+
yaml_ref = (f"metadata/e1m_modules/{sku}.yaml" if sku
|
|
1254
|
+
else "its SoM preset YAML")
|
|
1255
|
+
raise OrchestratorError(
|
|
1256
|
+
f"board.yaml `security.psa.{field}: {ref}` names "
|
|
1257
|
+
f"on-module OSPI part '{ref_str}', which is not "
|
|
1258
|
+
f"assembled on {where} (assembled: false in "
|
|
1259
|
+
f"{yaml_ref}); pick a storage partition or memory "
|
|
1260
|
+
f"region backed by a part this SKU actually carries")
|
|
1261
|
+
if ref_str in valid_refs:
|
|
1262
|
+
return
|
|
1263
|
+
raise OrchestratorError(
|
|
1264
|
+
f"board.yaml `security.psa.{field}: {ref}` does not "
|
|
1265
|
+
f"resolve to any `storage[].name`, SoM "
|
|
1266
|
+
f"`memory_map[].name`, or `on_module.ospi_memories:` "
|
|
1267
|
+
f"key. Known storage partitions: "
|
|
1268
|
+
f"{sorted(storage_name_set) or '[]'}; "
|
|
1269
|
+
f"known SoM memory regions: "
|
|
1270
|
+
f"{sorted(region_names) or '[]'}; "
|
|
1271
|
+
f"known on-module OSPI parts: "
|
|
1272
|
+
f"{sorted(ospi_keys) or '[]'}.")
|
|
1273
|
+
|
|
1274
|
+
_check_backing_store("its_storage")
|
|
1275
|
+
_check_backing_store("ps_storage")
|
|
1276
|
+
|
|
1277
|
+
att_root = psa.get("attestation_root")
|
|
1278
|
+
if att_root == "optiga_trust_m":
|
|
1279
|
+
if _is_i2c_chip_unassembled(som_preset, "optiga_trust_m"):
|
|
1280
|
+
# alp-sdk#2316 (tan-cli#1295), same class as alp-sdk#2311's
|
|
1281
|
+
# OSPI guard: E1M-AEN801/803 carry
|
|
1282
|
+
# `capabilities.optiga_trust_m: true` and name the part
|
|
1283
|
+
# under `on_module:`, but the part itself is DNP
|
|
1284
|
+
# (`assembled: false` on its i2c_devices entry). The
|
|
1285
|
+
# population fact wins -- an attestation root on a part
|
|
1286
|
+
# this SKU does not carry would build and then fail on
|
|
1287
|
+
# silicon.
|
|
1288
|
+
where = f"SoM {sku}" if sku else "this SoM"
|
|
1289
|
+
yaml_ref = (f"metadata/e1m_modules/{sku}.yaml" if sku
|
|
1290
|
+
else "its SoM preset YAML")
|
|
1291
|
+
raise OrchestratorError(
|
|
1292
|
+
f"board.yaml `security.psa.attestation_root: "
|
|
1293
|
+
f"optiga_trust_m` names on-module part "
|
|
1294
|
+
f"'optiga_trust_m', which is not assembled on "
|
|
1295
|
+
f"{where} (assembled: false in {yaml_ref}); pick "
|
|
1296
|
+
f"`tfm_internal` or `none`, or switch to a SKU that "
|
|
1297
|
+
f"carries OPTIGA Trust M")
|
|
1298
|
+
on_module = som_preset.get("on_module") or {}
|
|
1299
|
+
chip_set: set[str] = set()
|
|
1300
|
+
for key, val in on_module.items():
|
|
1301
|
+
if key == "ospi_memories":
|
|
1302
|
+
continue
|
|
1303
|
+
if isinstance(val, str):
|
|
1304
|
+
chip_set.add(val)
|
|
1305
|
+
capabilities = som_preset.get("capabilities") or {}
|
|
1306
|
+
has_optiga = (
|
|
1307
|
+
"optiga_trust_m" in chip_set
|
|
1308
|
+
or bool(capabilities.get("optiga_trust_m"))
|
|
1309
|
+
)
|
|
1310
|
+
if not has_optiga:
|
|
1311
|
+
raise OrchestratorError(
|
|
1312
|
+
f"board.yaml `security.psa.attestation_root: "
|
|
1313
|
+
f"optiga_trust_m` requires the SoM preset to "
|
|
1314
|
+
f"ship OPTIGA Trust M on-module, but SoM SKU "
|
|
1315
|
+
f"{sku} does not list it under `on_module:` or "
|
|
1316
|
+
f"`capabilities:`. Pick `tfm_internal` or "
|
|
1317
|
+
f"`none`, or switch to a SoM that carries OPTIGA "
|
|
1318
|
+
f"(AEN family).")
|
|
1319
|
+
|
|
1320
|
+
return security_block
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _is_i2c_chip_unassembled(som_preset: dict, chip: str) -> bool:
|
|
1324
|
+
"""True when the preset declares @p chip under `on_module.i2c_devices`
|
|
1325
|
+
and every such entry is `assembled: false` (alp-sdk#2316).
|
|
1326
|
+
|
|
1327
|
+
A chip the preset never lists there is not "unassembled" -- the other
|
|
1328
|
+
presence checks (on_module:/capabilities:) decide that case -- so this
|
|
1329
|
+
only refuses what the preset explicitly marks DNP.
|
|
1330
|
+
"""
|
|
1331
|
+
buses = ((som_preset.get("on_module") or {}).get("i2c_devices")) or {}
|
|
1332
|
+
entries = [dev for bus in buses.values() if isinstance(bus, dict)
|
|
1333
|
+
for dev in (bus.get("devices") or [])
|
|
1334
|
+
if isinstance(dev, dict) and dev.get("chip") == chip]
|
|
1335
|
+
return bool(entries) and all(dev.get("assembled") is False
|
|
1336
|
+
for dev in entries)
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _library_alias_table(metadata_root: Path) -> dict[str, str]:
|
|
1340
|
+
"""Legacy per-core `libraries:` token -> canonical manifest name
|
|
1341
|
+
(metadata/library-aliases-v1.json). Empty dict if the table is absent."""
|
|
1342
|
+
path = metadata_root / "library-aliases-v1.json"
|
|
1343
|
+
if not path.is_file():
|
|
1344
|
+
return {}
|
|
1345
|
+
doc = strict_json_loads(path.read_text(encoding="utf-8"), source=path)
|
|
1346
|
+
aliases = doc.get("aliases")
|
|
1347
|
+
return dict(aliases) if isinstance(aliases, dict) else {}
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _normalize_libraries(project: dict[str, Any],
|
|
1351
|
+
metadata_root: Path) -> None:
|
|
1352
|
+
"""Fold the unified top-level `libraries:` list into the internal channels
|
|
1353
|
+
the emitters consume (WS6-c #610 §6).
|
|
1354
|
+
|
|
1355
|
+
board.yaml declares every curated library once, at the top level, as a
|
|
1356
|
+
`{name, cores?}` object: project-wide when `cores:` is omitted, core-scoped
|
|
1357
|
+
otherwise (a bare string is accepted as shorthand for a project-wide
|
|
1358
|
+
`{name}`). This rewrites the parsed dict so the rest of the loader stays
|
|
1359
|
+
library-shape-agnostic: project-wide names land in `project['libraries']`
|
|
1360
|
+
and core-scoped names are injected into each `cores[<id>]['libraries']`.
|
|
1361
|
+
|
|
1362
|
+
This is the ONLY library read path -- there is no separate per-core
|
|
1363
|
+
`cores.<id>.libraries:` list to read. A `cores:` entry scoping a library
|
|
1364
|
+
to a core the topology doesn't declare is a hard error: silently dropping
|
|
1365
|
+
it would emit nothing for a library the app author explicitly asked for.
|
|
1366
|
+
"""
|
|
1367
|
+
unified = project.get("libraries") or []
|
|
1368
|
+
alias = _library_alias_table(metadata_root)
|
|
1369
|
+
cores_map = project.get("cores") or {}
|
|
1370
|
+
project_wide: list[str] = []
|
|
1371
|
+
per_core: dict[str, list[str]] = {}
|
|
1372
|
+
for entry in unified:
|
|
1373
|
+
if isinstance(entry, str):
|
|
1374
|
+
project_wide.append(alias.get(entry, entry))
|
|
1375
|
+
continue
|
|
1376
|
+
if not isinstance(entry, dict):
|
|
1377
|
+
continue
|
|
1378
|
+
name = entry.get("name")
|
|
1379
|
+
if not name:
|
|
1380
|
+
continue
|
|
1381
|
+
canonical = alias.get(name, name)
|
|
1382
|
+
cores = entry.get("cores")
|
|
1383
|
+
if cores:
|
|
1384
|
+
for cid in cores:
|
|
1385
|
+
if str(cid) not in cores_map:
|
|
1386
|
+
raise OrchestratorError(
|
|
1387
|
+
f"libraries: entry '{canonical}' is scoped to core "
|
|
1388
|
+
f"'{cid}', which is not declared under `cores:`")
|
|
1389
|
+
per_core.setdefault(str(cid), []).append(canonical)
|
|
1390
|
+
else:
|
|
1391
|
+
project_wide.append(canonical)
|
|
1392
|
+
project["libraries"] = project_wide
|
|
1393
|
+
for cid, names in per_core.items():
|
|
1394
|
+
centry = cores_map.get(cid)
|
|
1395
|
+
if not isinstance(centry, dict):
|
|
1396
|
+
continue
|
|
1397
|
+
existing = list(centry.get("libraries") or [])
|
|
1398
|
+
for n in names:
|
|
1399
|
+
if n not in existing:
|
|
1400
|
+
existing.append(n)
|
|
1401
|
+
centry["libraries"] = existing
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def load_board_yaml(path: Path, *,
|
|
1405
|
+
metadata_root: Path = METADATA_ROOT,
|
|
1406
|
+
sku: Optional[str] = None,
|
|
1407
|
+
skip_advisories: list[str] | None = None) -> BoardProject:
|
|
1408
|
+
"""Load + validate a board.yaml.
|
|
1409
|
+
|
|
1410
|
+
Raises OrchestratorError on any schema / preset / topology error.
|
|
1411
|
+
|
|
1412
|
+
#673 Phase 1: staged into a resolve pipeline. YAML/schema load,
|
|
1413
|
+
board/SKU resolution, topology/core validation, storage resolution,
|
|
1414
|
+
and final cross-field validation each run as their own private
|
|
1415
|
+
helper (`_load_and_validate_yaml`, `_resolve_board`,
|
|
1416
|
+
`_validate_topology_cores`, `_resolve_storage`,
|
|
1417
|
+
`_validate_cross_fields`), invoked in the exact same order as the
|
|
1418
|
+
original monolithic function so error precedence and messages are
|
|
1419
|
+
unchanged.
|
|
1420
|
+
|
|
1421
|
+
*skip_advisories* (tan-cli#964 review, major 6): optional, caller-owned,
|
|
1422
|
+
threaded to `_resolve_board`'s `_refuse_on_schema_errors` calls. `None`
|
|
1423
|
+
(the default) costs nothing extra -- every existing call site is
|
|
1424
|
+
unaffected. A caller that wants "skip-but-disclose" (rather than the bare
|
|
1425
|
+
silent skip an absent schema file used to be) passes its own list and
|
|
1426
|
+
folds the collected notes into its own issues after this returns; see
|
|
1427
|
+
`planner_emit._render_route_table`/`generate_cmd._emit_one_in_process`
|
|
1428
|
+
for the two commands that do.
|
|
1429
|
+
"""
|
|
1430
|
+
project = _load_and_validate_yaml(path, metadata_root)
|
|
1431
|
+
if sku:
|
|
1432
|
+
# Per-target SKU (alp-sdk#2597): the SoM facts follow the board
|
|
1433
|
+
# being built, not the single static `som.sku` an example declares.
|
|
1434
|
+
# Safe after schema validation: the override only changes the SKU
|
|
1435
|
+
# string, and every SKU-derived value (_resolve_board's preset + SoC
|
|
1436
|
+
# variant lookup, hw_rev checks, topology, storage) is computed
|
|
1437
|
+
# below from it. Callers pass a SKU taken from
|
|
1438
|
+
# metadata/e1m_modules/.
|
|
1439
|
+
project["som"]["sku"] = sku
|
|
1440
|
+
|
|
1441
|
+
# Fold the unified top-level `libraries:` list into the per-core /
|
|
1442
|
+
# project-wide channels the downstream resolution expects, so topology +
|
|
1443
|
+
# slice building stay library-shape-agnostic.
|
|
1444
|
+
_normalize_libraries(project, metadata_root)
|
|
1445
|
+
|
|
1446
|
+
(sku, hw_rev, som_preset, silicon, soc_spec, board_preset,
|
|
1447
|
+
board_name, board_hw_rev) = _resolve_board(
|
|
1448
|
+
project, metadata_root, skip_advisories=skip_advisories)
|
|
1449
|
+
|
|
1450
|
+
_check_hw_rev_exists(
|
|
1451
|
+
metadata_root,
|
|
1452
|
+
sku=sku,
|
|
1453
|
+
som_hw_rev=hw_rev or som_preset.get("default_hw_rev"),
|
|
1454
|
+
board_name=board_name,
|
|
1455
|
+
board_hw_rev=board_hw_rev,
|
|
1456
|
+
board_preset=board_preset)
|
|
1457
|
+
|
|
1458
|
+
_check_hw_rev_buildable(
|
|
1459
|
+
metadata_root,
|
|
1460
|
+
sku=sku,
|
|
1461
|
+
som_hw_rev=hw_rev or som_preset.get("default_hw_rev"),
|
|
1462
|
+
board_name=board_name,
|
|
1463
|
+
board_hw_rev=board_hw_rev,
|
|
1464
|
+
board_preset=board_preset)
|
|
1465
|
+
|
|
1466
|
+
_check_sdk_supports_hw_rev(
|
|
1467
|
+
metadata_root,
|
|
1468
|
+
sku=sku,
|
|
1469
|
+
som_hw_rev=hw_rev or som_preset.get("default_hw_rev"),
|
|
1470
|
+
board_name=board_name,
|
|
1471
|
+
board_hw_rev=board_hw_rev,
|
|
1472
|
+
board_preset=board_preset)
|
|
1473
|
+
|
|
1474
|
+
cores, ipc_entries = _validate_topology_cores(
|
|
1475
|
+
project, som_preset, soc_spec, sku, silicon, board_preset,
|
|
1476
|
+
board_name, metadata_root)
|
|
1477
|
+
|
|
1478
|
+
storage_entries = _resolve_storage(project, som_preset, sku, metadata_root)
|
|
1479
|
+
|
|
1480
|
+
security_block = _validate_cross_fields(
|
|
1481
|
+
project, som_preset, sku, storage_entries, metadata_root)
|
|
1482
|
+
|
|
1483
|
+
ownership = resolve_ownership(
|
|
1484
|
+
load_ownership_doc(metadata_root, _sku_family_dir(sku)),
|
|
1485
|
+
project.get("ownership"),
|
|
1486
|
+
declared_core_types=(
|
|
1487
|
+
{str(c.get("type") or "") for c in (soc_spec.get("cores") or [])
|
|
1488
|
+
if c.get("id") in project["cores"]}
|
|
1489
|
+
if project.get("cores") else None))
|
|
1490
|
+
|
|
1491
|
+
out = BoardProject(
|
|
1492
|
+
sku=sku,
|
|
1493
|
+
hw_rev=hw_rev or som_preset.get("default_hw_rev"),
|
|
1494
|
+
board_name=board_name,
|
|
1495
|
+
board_hw_rev=board_hw_rev,
|
|
1496
|
+
cores=cores,
|
|
1497
|
+
ipc=ipc_entries,
|
|
1498
|
+
soc_spec=soc_spec,
|
|
1499
|
+
som_preset=som_preset,
|
|
1500
|
+
board_preset=board_preset,
|
|
1501
|
+
diagnostics=dict(project.get("diagnostics") or {}),
|
|
1502
|
+
chips=list(project.get("chips") or []),
|
|
1503
|
+
libraries=list(project.get("libraries") or []),
|
|
1504
|
+
features=dict(project.get("features") or {}),
|
|
1505
|
+
boot=dict(project.get("boot") or {}),
|
|
1506
|
+
ota=dict(project.get("ota") or {}),
|
|
1507
|
+
storage=storage_entries,
|
|
1508
|
+
security=security_block,
|
|
1509
|
+
ownership=ownership,
|
|
1510
|
+
raw=project,
|
|
1511
|
+
metadata_root=metadata_root, # tan-cli#573: the tree THIS load read
|
|
1512
|
+
source_dir=Path(path).resolve().parent,
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
# Cross-field consistency pass (v0.6 P2.3). Runs last so it can
|
|
1516
|
+
# inspect the fully-assembled project + every per-core
|
|
1517
|
+
# extra_libraries: entry the schema couldn't validate cleanly.
|
|
1518
|
+
_validate_consistency(out)
|
|
1519
|
+
|
|
1520
|
+
# `diagnostics.link: itcm` is an AEN M55-HE-only knob (tan-cli#1350):
|
|
1521
|
+
# refuse it here so every planner consumer sees the coded error.
|
|
1522
|
+
from .link_target import apply_link_target # noqa: PLC0415
|
|
1523
|
+
apply_link_target(out)
|
|
1524
|
+
|
|
1525
|
+
return out
|