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