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/buildplan.py
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Build-plan emission -- the Wave C consumer contract.
|
|
4
|
+
|
|
5
|
+
`emit_build_plan` renders the machine-readable JSON build plan `tan`
|
|
6
|
+
(alplabai/tan-cli) materialises; the shared helpers `_slice_build_dir` /
|
|
7
|
+
`_slice_config_artefact` / `_shared_artefacts` are the single source the
|
|
8
|
+
Orchestrator's materialise path and the plan MUST agree on byte-for-byte
|
|
9
|
+
(tan reads what the Orchestrator writes).
|
|
10
|
+
Extracted as the #285 build-plan emit seam. The per-slice config emitters come
|
|
11
|
+
from kconfig.py, the header/secure artefacts from headers.py / secure.py; the
|
|
12
|
+
orchestrator-side slice-command bits (_slice_command, STOCK_SHIM_APP) are
|
|
13
|
+
lazy-imported from the package (they stay inline until orchestrator.py).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
from dataclasses import replace
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Optional
|
|
25
|
+
|
|
26
|
+
from tan.core.subprocess_env import spawn_env
|
|
27
|
+
from tan.core.tool_lookup import resolve_tool
|
|
28
|
+
|
|
29
|
+
from .headers import (
|
|
30
|
+
emit_dts_partitions,
|
|
31
|
+
emit_dts_reservations,
|
|
32
|
+
emit_ipc_contract_h,
|
|
33
|
+
emit_storage_mounts_c,
|
|
34
|
+
has_storage_mounts,
|
|
35
|
+
)
|
|
36
|
+
from .kconfig import (
|
|
37
|
+
_resolve_console,
|
|
38
|
+
_slice_alp_conf,
|
|
39
|
+
_slice_cmake_args,
|
|
40
|
+
_slice_local_conf,
|
|
41
|
+
)
|
|
42
|
+
from .link_target import extra_config_artefacts
|
|
43
|
+
from .models import BoardProject, OrchestratorError, Slice
|
|
44
|
+
from .ownership import project_m33_overlay
|
|
45
|
+
from .paths import REPO
|
|
46
|
+
from .project_emit.dts import DtsOverlayUnavailable # noqa: F401 (tan adaptation: defined beside the emitter that raises it)
|
|
47
|
+
from .secure import emit_sysbuild_conf, emit_tfm_sysbuild_conf
|
|
48
|
+
|
|
49
|
+
# The skip-vs-fail policy a slice dispatcher MUST apply, published verbatim
|
|
50
|
+
# in the plan envelope so a consumer (tan-cli) stops hand-porting it: an
|
|
51
|
+
# unknown `backend` fails the slice, a missing tool on PATH or a null
|
|
52
|
+
# `command` skip it.
|
|
53
|
+
_EXECUTION_POLICY = {
|
|
54
|
+
"unknownBackend": "fail",
|
|
55
|
+
"missingTool": "skip",
|
|
56
|
+
"nullCommand": "skip",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# The `${NAME}` tokens a plan consumer substitutes in a `planPathMode:
|
|
60
|
+
# tokened` plan. This emitter writes the first three; tan-cli also resolves
|
|
61
|
+
# TOOLCHAIN_ROOT, so a board.yaml placeholder must not reuse that name either.
|
|
62
|
+
PLAN_PATH_TOKENS = frozenset({"SDK_ROOT", "PROJECT_ROOT", "PYTHON", "TOOLCHAIN_ROOT"})
|
|
63
|
+
|
|
64
|
+
# Any `${...}` in a plan field, and the names `deferredPlaceholders` may carry
|
|
65
|
+
# (the schema's pattern; tan-cli matches the same).
|
|
66
|
+
_PLAN_REF_RE = re.compile(r"\$\{([^}]*)\}")
|
|
67
|
+
_DEFERRED_NAME_RE = re.compile(r"[A-Z][A-Z0-9_]*")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _deferred_placeholders(
|
|
71
|
+
slices_out: list[dict[str, Any]],
|
|
72
|
+
shared: list[dict[str, str]],
|
|
73
|
+
board_yaml: Path,
|
|
74
|
+
) -> list[str]:
|
|
75
|
+
"""Names of the `${NAME}` placeholders this plan leaves unresolved on
|
|
76
|
+
purpose (issue #2696), sorted.
|
|
77
|
+
|
|
78
|
+
A deferred placeholder is a value the user wrote in board.yaml as
|
|
79
|
+
`${NAME}` and the planner copied verbatim into a slice config artefact,
|
|
80
|
+
for the build host or the device to fill
|
|
81
|
+
(`examples/connectivity/iot-fleet-ota`'s `ota.server.tenant`). Who fills
|
|
82
|
+
it, and how, is outside the plan; the list only says the consumer must
|
|
83
|
+
leave it alone. Listing
|
|
84
|
+
them lets a consumer tell them from a plan path token it must substitute,
|
|
85
|
+
instead of refusing the whole plan as an unresolved token.
|
|
86
|
+
|
|
87
|
+
Every other `${...}` is refused here, because a consumer cannot handle it
|
|
88
|
+
correctly:
|
|
89
|
+
* a plan path token in a config artefact -- a user value named like
|
|
90
|
+
`${SDK_ROOT}` would be silently replaced by a checkout path;
|
|
91
|
+
* a name not written as `${NAME}` in the project's board.yaml -- the
|
|
92
|
+
planner invented it, and tan would refuse it anyway;
|
|
93
|
+
* a name outside `[A-Z][A-Z0-9_]*`, the contract's name pattern;
|
|
94
|
+
* a placeholder in an artefact that is not a `.conf` file -- CMake
|
|
95
|
+
would expand it itself (the same rule tan-cli#1306 applies);
|
|
96
|
+
* any non-token `${...}` outside `configArtefacts` (command, env,
|
|
97
|
+
envAppendPath, appDir, postCommands, sharedArtefacts).
|
|
98
|
+
"""
|
|
99
|
+
board_text = Path(board_yaml).read_text(encoding="utf-8")
|
|
100
|
+
deferred: set[str] = set()
|
|
101
|
+
|
|
102
|
+
def refuse(where: str, ref: str, why: str) -> None:
|
|
103
|
+
raise OrchestratorError(
|
|
104
|
+
f"build plan: `${{{ref}}}` in {where} {why} (issue #2696)")
|
|
105
|
+
|
|
106
|
+
for slice_ in slices_out:
|
|
107
|
+
core = slice_["coreId"]
|
|
108
|
+
for artefact in slice_["configArtefacts"]:
|
|
109
|
+
where = f"core '{core}' config artefact `{artefact['path']}`"
|
|
110
|
+
for ref in _PLAN_REF_RE.findall(artefact["contents"]):
|
|
111
|
+
if not artefact["path"].endswith(".conf"):
|
|
112
|
+
refuse(where, ref, "is in a non-`.conf` artefact, which "
|
|
113
|
+
"its reader (e.g. CMake) would expand "
|
|
114
|
+
"itself -- a placeholder may only "
|
|
115
|
+
"reach a Kconfig fragment or local.conf")
|
|
116
|
+
if ref in PLAN_PATH_TOKENS:
|
|
117
|
+
refuse(where, ref, "is a plan path token: a consumer would "
|
|
118
|
+
"replace it with a checkout path")
|
|
119
|
+
if not _DEFERRED_NAME_RE.fullmatch(ref):
|
|
120
|
+
refuse(where, ref, "is not a valid placeholder name -- "
|
|
121
|
+
"use upper-case letters, digits and "
|
|
122
|
+
"`_`, starting with a letter")
|
|
123
|
+
if f"${{{ref}}}" not in board_text:
|
|
124
|
+
refuse(where, ref, "does not come from board.yaml, so the "
|
|
125
|
+
"planner produced it -- an unresolved "
|
|
126
|
+
"token")
|
|
127
|
+
deferred.add(ref)
|
|
128
|
+
rest = {k: slice_[k] for k in ("command", "env", "envAppendPath",
|
|
129
|
+
"appDir", "postCommands")}
|
|
130
|
+
for ref in _PLAN_REF_RE.findall(json.dumps(rest)):
|
|
131
|
+
if ref not in PLAN_PATH_TOKENS:
|
|
132
|
+
refuse(f"core '{core}' command/env/appDir", ref,
|
|
133
|
+
"is not a plan path token -- a placeholder may only "
|
|
134
|
+
"appear in a config artefact")
|
|
135
|
+
for artefact in shared:
|
|
136
|
+
for ref in _PLAN_REF_RE.findall(artefact["contents"]):
|
|
137
|
+
refuse(f"shared artefact `{artefact['path']}`", ref,
|
|
138
|
+
"cannot be resolved -- a placeholder may only appear in a "
|
|
139
|
+
"slice config artefact")
|
|
140
|
+
return sorted(deferred)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _slice_build_dir(build_root: Path, slice_: Slice) -> Path:
|
|
144
|
+
"""Per-slice build directory: build/<core>-<os>/."""
|
|
145
|
+
return Path(build_root) / f"{slice_.core_id}-{slice_.os}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _slice_config_artefact(
|
|
149
|
+
project: BoardProject,
|
|
150
|
+
slice_: Slice,
|
|
151
|
+
) -> Optional[tuple[str, str]]:
|
|
152
|
+
"""(filename, contents) of the slice's config artefact, or None
|
|
153
|
+
when the os has none.
|
|
154
|
+
|
|
155
|
+
Single source for both a consumer's materialise step and
|
|
156
|
+
`emit_build_plan` -- the two MUST agree byte-for-byte (the CLI
|
|
157
|
+
consumer byte-writes the plan's contents and trusts them to match
|
|
158
|
+
what we'd write ourselves).
|
|
159
|
+
|
|
160
|
+
`baremetal` carries `alp-baremetal.cmake` -- and ONLY when the slice
|
|
161
|
+
actually has compile-time guards to carry (absence-emits-nothing).
|
|
162
|
+
This is NOT the old `cmake-args.txt` coming back: that file was
|
|
163
|
+
removed in 2026-08 (#1278) precisely because no build command ever
|
|
164
|
+
read it, and the test below is exactly the condition it failed --
|
|
165
|
+
`_slice_command`'s baremetal branch pulls this file in with
|
|
166
|
+
`-DCMAKE_PROJECT_INCLUDE=<abs path>`, so a slice that stops writing
|
|
167
|
+
it stops compiling with its `ALP_BOARD_<SLUG>` / `ALP_SOM_<SKU>`
|
|
168
|
+
guards, loudly (alplabai/tan-cli#551). The `=`-bearing cache entries
|
|
169
|
+
from the same source ride the configure command line directly and are
|
|
170
|
+
NOT duplicated here. The full human-readable `-D` listing remains
|
|
171
|
+
also published as the plan's `cmake-args.txt` artefact
|
|
172
|
+
(`_slice_cmake_args_artefact`) -- see docs/board-config-emit.md.
|
|
173
|
+
"""
|
|
174
|
+
if slice_.os == "zephyr":
|
|
175
|
+
return ("alp.conf", _slice_alp_conf(project, slice_))
|
|
176
|
+
if slice_.os == "yocto":
|
|
177
|
+
return ("local.conf", _slice_local_conf(project, slice_))
|
|
178
|
+
if slice_.os == "baremetal":
|
|
179
|
+
# Lazy, same buildplan<->orchestrator cycle avoidance
|
|
180
|
+
# `emit_build_plan` uses for the slice-command helpers.
|
|
181
|
+
from .orchestrator import (
|
|
182
|
+
BAREMETAL_PROJECT_INCLUDE,
|
|
183
|
+
_baremetal_project_include,
|
|
184
|
+
)
|
|
185
|
+
contents = _baremetal_project_include(project, slice_)
|
|
186
|
+
if contents is None:
|
|
187
|
+
return None
|
|
188
|
+
return (BAREMETAL_PROJECT_INCLUDE, contents)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#: The os classes a slice's `alp.overlay` artefact is emitted for: the
|
|
193
|
+
#: classes `alp_project.py --emit dts-overlay` unions over. A yocto slice has
|
|
194
|
+
#: none. `cmake-args.txt` is emitted for the same two (`--emit cmake-args`).
|
|
195
|
+
_DTS_OVERLAY_OS = ("zephyr", "baremetal")
|
|
196
|
+
|
|
197
|
+
#: Filename of the rendered DTS overlay config artefact (under `buildDir`).
|
|
198
|
+
DTS_OVERLAY_ARTEFACT = "alp.overlay"
|
|
199
|
+
|
|
200
|
+
#: Filename of the rendered full `-D` listing config artefact.
|
|
201
|
+
CMAKE_ARGS_ARTEFACT = "cmake-args.txt"
|
|
202
|
+
|
|
203
|
+
#: Filename of the rendered build-identifier header config artefact
|
|
204
|
+
#: (`--emit hw-info-h --core <id>`). Same name `<alp/hw_info.h>` documents.
|
|
205
|
+
HW_INFO_ARTEFACT = "alp_hw_info_build.h"
|
|
206
|
+
|
|
207
|
+
#: Filename of the rendered west manifest fragment config artefact
|
|
208
|
+
#: (`--emit west-libraries --core <id>`).
|
|
209
|
+
WEST_LIBS_ARTEFACT = "alp-west-libs.yml"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class HwInfoUnavailable(OrchestratorError):
|
|
213
|
+
"""The slice's SKU is outside the production families, so there is no
|
|
214
|
+
family to put in `ALP_HW_BUILD_SOM_FAMILY`. Downgraded by
|
|
215
|
+
`emit_build_plan` to a `hw-info-unavailable` warning, like
|
|
216
|
+
`DtsOverlayUnavailable`; any other failure still fails the plan."""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _v1_shaped_project(project: BoardProject) -> dict[str, Any]:
|
|
220
|
+
"""The legacy `board:`-wrapper dict the project-wide emitters read.
|
|
221
|
+
|
|
222
|
+
The public board.yaml schema no longer uses this wrapper, but the
|
|
223
|
+
in-file emitters (dts-overlay, hw-info-h, west-libraries) still consume
|
|
224
|
+
it. Shared by `alp_project.py` and `_slice_dts_overlay`.
|
|
225
|
+
"""
|
|
226
|
+
return {
|
|
227
|
+
"som": {
|
|
228
|
+
"sku": project.sku,
|
|
229
|
+
"hw_rev": project.hw_rev,
|
|
230
|
+
},
|
|
231
|
+
"pins": list(project.raw.get("pins") or []),
|
|
232
|
+
"board": ({
|
|
233
|
+
"name": project.board_name,
|
|
234
|
+
"hw_rev": project.board_hw_rev,
|
|
235
|
+
} if project.board_name else None),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _slice_dts_overlay(project: BoardProject, slice_: Slice) -> str:
|
|
240
|
+
"""The slice's DTS overlay text -- exactly what
|
|
241
|
+
`alp_project.py --emit dts-overlay --core <id>` prints.
|
|
242
|
+
|
|
243
|
+
Single source for that standalone emit and the build plan's
|
|
244
|
+
`alp.overlay` config artefact (ADR-0026 §D: `tan` consumes these bytes
|
|
245
|
+
instead of re-rendering them). The overlay is shaped by the board
|
|
246
|
+
header (bus aliases + `alp,pin-array`), a SoM-mounting fact; the slice
|
|
247
|
+
contributes its peripheral list and, per-product, the assignable nodes
|
|
248
|
+
board.yaml `ownership:` gave the M33.
|
|
249
|
+
"""
|
|
250
|
+
# tan adaptation: the emitter is a sibling subpackage here, not the
|
|
251
|
+
# `alp_project_emit` package beside `alp_orchestrate/` upstream.
|
|
252
|
+
from .project_emit.dts import _emit_dts_overlay
|
|
253
|
+
from .som_metadata import _sku_family
|
|
254
|
+
|
|
255
|
+
# Only the two facts the emitter itself cannot render are "unavailable"
|
|
256
|
+
# (the plan degrades to a warning); everything else propagates.
|
|
257
|
+
try:
|
|
258
|
+
_sku_family(project.sku)
|
|
259
|
+
except ValueError as exc:
|
|
260
|
+
raise DtsOverlayUnavailable(str(exc)) from None
|
|
261
|
+
out = _emit_dts_overlay(
|
|
262
|
+
_v1_shaped_project(project), project.som_preset,
|
|
263
|
+
project.board_preset,
|
|
264
|
+
v2_peripherals=sorted(set(slice_.peripherals)),
|
|
265
|
+
v2_core_id=slice_.core_id,
|
|
266
|
+
v2_core_os=slice_.os,
|
|
267
|
+
v2_core_ids=[slice_.core_id],
|
|
268
|
+
)
|
|
269
|
+
# (tan adaptation: the missing-header case raises `DtsOverlayUnavailable`
|
|
270
|
+
# from the emitter itself rather than `sys.exit`ing.)
|
|
271
|
+
# Outside the degrade path above: an M33 ownership defect must still
|
|
272
|
+
# fail the plan, not turn into a missing artefact.
|
|
273
|
+
own_dts, _ = project_m33_overlay(project, slice_.core_id)
|
|
274
|
+
if own_dts:
|
|
275
|
+
out += ("\n/* Assignable peripherals owned by the M33 "
|
|
276
|
+
"(board.yaml `ownership:`). */\n" + "\n".join(own_dts) + "\n")
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _slice_dts_overlay_artefact(
|
|
281
|
+
project: BoardProject,
|
|
282
|
+
slice_: Slice,
|
|
283
|
+
) -> Optional[tuple[str, str]]:
|
|
284
|
+
"""(filename, contents) of the slice's rendered DTS overlay, or None for
|
|
285
|
+
an os that has none (see `_DTS_OVERLAY_OS`)."""
|
|
286
|
+
if slice_.os not in _DTS_OVERLAY_OS:
|
|
287
|
+
return None
|
|
288
|
+
return (DTS_OVERLAY_ARTEFACT, _slice_dts_overlay(project, slice_))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _slice_cmake_args_artefact(
|
|
292
|
+
project: BoardProject,
|
|
293
|
+
slice_: Slice,
|
|
294
|
+
) -> Optional[tuple[str, str]]:
|
|
295
|
+
"""(filename, contents) of the slice's full `-D` listing -- exactly what
|
|
296
|
+
`alp_project.py --emit cmake-args --core <id>` prints after its
|
|
297
|
+
`# --- core ---` marker -- or None for an os that has none.
|
|
298
|
+
|
|
299
|
+
A rendered REFERENCE listing for consumers (ADR-0026 §D). The
|
|
300
|
+
`=`-bearing cache entries already ride the baremetal configure line and
|
|
301
|
+
the bare guards arrive via `alp-baremetal.cmake`; this file is not read
|
|
302
|
+
by any build command and so does not resurrect the dead `cmake-args.txt`
|
|
303
|
+
#1278 removed -- it is the same text, now published in the plan so `tan`
|
|
304
|
+
stops re-rendering it.
|
|
305
|
+
"""
|
|
306
|
+
if slice_.os not in _DTS_OVERLAY_OS:
|
|
307
|
+
return None
|
|
308
|
+
return (CMAKE_ARGS_ARTEFACT, _slice_cmake_args(project, slice_))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _slice_hw_info_h(project: BoardProject, slice_: Slice) -> str:
|
|
312
|
+
"""The slice's build-identifier header -- exactly what
|
|
313
|
+
`alp_project.py --emit hw-info-h --core <id>` prints.
|
|
314
|
+
|
|
315
|
+
`ALP_HW_BUILD_CORES` / `HAS_<id>` range over EVERY core of the project
|
|
316
|
+
(`v2_cores`), and `ALP_HW_BUILD_OS` / `PRIMARY_CORE` track the slice
|
|
317
|
+
(`v2_selected_core`). Single source for the standalone emit and the
|
|
318
|
+
build plan's `alp_hw_info_build.h` config artefact.
|
|
319
|
+
"""
|
|
320
|
+
# tan adaptation: the emitter is a sibling subpackage here.
|
|
321
|
+
from .project_emit.hw_info import _emit_hw_info_h
|
|
322
|
+
|
|
323
|
+
return _emit_hw_info_h(
|
|
324
|
+
_v1_shaped_project(project), project.som_preset,
|
|
325
|
+
project.board_preset,
|
|
326
|
+
v2_cores={cid: s.os for cid, s in project.cores.items()},
|
|
327
|
+
v2_selected_core=slice_.core_id,
|
|
328
|
+
# Same tree alp.conf's `CONFIG_ALP_SDK_SOM_HW_REV` reads, so the two
|
|
329
|
+
# composed hw_rev designators cannot disagree under --metadata-root.
|
|
330
|
+
metadata_root=project.effective_metadata_root(),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _slice_west_libraries(project: BoardProject, slice_: Slice) -> str:
|
|
335
|
+
"""The slice's west manifest fragment -- exactly what
|
|
336
|
+
`alp_project.py --emit west-libraries --core <id>` prints.
|
|
337
|
+
|
|
338
|
+
Single source for that standalone emit and the build plan's
|
|
339
|
+
`alp-west-libs.yml` config artefact.
|
|
340
|
+
"""
|
|
341
|
+
from .project_emit.west_libs import _emit_west_libraries
|
|
342
|
+
|
|
343
|
+
return _emit_west_libraries(
|
|
344
|
+
_v1_shaped_project(project), project.som_preset,
|
|
345
|
+
project.board_preset,
|
|
346
|
+
v2_libraries=sorted(set(slice_.libraries)),
|
|
347
|
+
v2_project_libraries=sorted(project.libraries),
|
|
348
|
+
metadata_root=project.effective_metadata_root(),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _slice_hw_info_artefact(
|
|
353
|
+
project: BoardProject,
|
|
354
|
+
slice_: Slice,
|
|
355
|
+
) -> Optional[tuple[str, str]]:
|
|
356
|
+
"""(filename, contents) of the slice's rendered `alp_hw_info_build.h`, or
|
|
357
|
+
None for an os that has none (see `_DTS_OVERLAY_OS`).
|
|
358
|
+
|
|
359
|
+
A REFERENCE artefact (ADR-0026 §D), like `cmake-args.txt`: no build
|
|
360
|
+
command, CMake file or Kconfig in this SDK reads it. The firmware's
|
|
361
|
+
own `ALP_HW_BUILD_*` values still come from the build, not this file.
|
|
362
|
+
"""
|
|
363
|
+
if slice_.os not in _DTS_OVERLAY_OS:
|
|
364
|
+
return None
|
|
365
|
+
# Only the SKU->family lookup is "unavailable" (same narrow pattern as
|
|
366
|
+
# `_slice_dts_overlay`); a damaged hw-revisions table etc. must still fail
|
|
367
|
+
# the plan, so the render itself runs outside any except.
|
|
368
|
+
from .som_metadata import _sku_family
|
|
369
|
+
try:
|
|
370
|
+
_sku_family(project.sku)
|
|
371
|
+
except ValueError as exc:
|
|
372
|
+
raise HwInfoUnavailable(str(exc)) from None
|
|
373
|
+
return (HW_INFO_ARTEFACT, _slice_hw_info_h(project, slice_))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _slice_west_libs_artefact(
|
|
377
|
+
project: BoardProject,
|
|
378
|
+
slice_: Slice,
|
|
379
|
+
) -> Optional[tuple[str, str]]:
|
|
380
|
+
"""(filename, contents) of the slice's rendered west fragment, or None
|
|
381
|
+
for an os that has none. Emitted even when the slice selects no
|
|
382
|
+
libraries: the emitter renders a well-formed empty allowlist, and
|
|
383
|
+
byte-identity with `--emit west-libraries --core` is the contract.
|
|
384
|
+
Reference only, like `cmake-args.txt`."""
|
|
385
|
+
if slice_.os not in _DTS_OVERLAY_OS:
|
|
386
|
+
return None
|
|
387
|
+
return (WEST_LIBS_ARTEFACT, _slice_west_libraries(project, slice_))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _emit_storage_mount_table(project: BoardProject) -> str:
|
|
391
|
+
"""The `storage_mount_table.c` contents, or "" when board.yaml `storage:`
|
|
392
|
+
declares no mountable partition. The standalone emit prints an empty
|
|
393
|
+
table for that case, which the plan has no reason to carry, so the
|
|
394
|
+
conditional-artefact rule (empty -> absent) keys off `has_storage_mounts`
|
|
395
|
+
rather than off the text, which is never empty (alp-sdk#2820)."""
|
|
396
|
+
if not has_storage_mounts(project):
|
|
397
|
+
return ""
|
|
398
|
+
return emit_storage_mounts_c(project)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
#: Every shared generated artefact: `(path under the build root, emitter,
|
|
402
|
+
#: conditional)`. ONE call site per artefact: `_shared_artefacts` (the plan)
|
|
403
|
+
#: and `_shared_artefact` (the standalone `--emit`) both run the emitter from
|
|
404
|
+
#: THIS table, and the latter runs only the one it was asked for, so one
|
|
405
|
+
#: emitter's refusal (e.g. `emit_sysbuild_conf` on an unsupported `boot:`)
|
|
406
|
+
#: cannot leak into another mode (tan-cli#1216, ADR-0026 §D).
|
|
407
|
+
#:
|
|
408
|
+
#: `<alp/system_ipc.h>` is the canonical include path consumers use (see
|
|
409
|
+
#: include/alp/rpc.h §usage and the per-slice main.c references) -- it sits in
|
|
410
|
+
#: an `alp/` subdir so slice CMakeLists add `generated/` straight to the
|
|
411
|
+
#: include path. Apps that don't declare storage[] still get a stub
|
|
412
|
+
#: `dts-partitions.dtsi` so downstream #include resolves. Conditional
|
|
413
|
+
#: artefacts (sysbuild / TF-M) follow absence-emits-nothing: they only appear
|
|
414
|
+
#: when their emit is non-empty.
|
|
415
|
+
_SHARED_EMITTERS: tuple[tuple[str, Callable[[BoardProject], str], bool], ...] = (
|
|
416
|
+
("generated/alp/system_ipc.h", emit_ipc_contract_h, False),
|
|
417
|
+
("generated/dts-reservations.dtsi", emit_dts_reservations, False),
|
|
418
|
+
("generated/dts-partitions.dtsi", emit_dts_partitions, False),
|
|
419
|
+
("alp_sysbuild.conf", emit_sysbuild_conf, True),
|
|
420
|
+
("sysbuild/tfm/tfm.conf", emit_tfm_sysbuild_conf, True),
|
|
421
|
+
("generated/storage_mount_table.c", _emit_storage_mount_table, True),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _shared_artefacts(
|
|
426
|
+
project: BoardProject,
|
|
427
|
+
build_root: Path,
|
|
428
|
+
) -> list[tuple[Path, str]]:
|
|
429
|
+
"""(path, contents) of every shared generated artefact.
|
|
430
|
+
|
|
431
|
+
Single source for `_materialise_shared` and `emit_build_plan`
|
|
432
|
+
(same byte-parity contract as `_slice_config_artefact`).
|
|
433
|
+
"""
|
|
434
|
+
build_root = Path(build_root)
|
|
435
|
+
out: list[tuple[Path, str]] = []
|
|
436
|
+
for rel, emitter, conditional in _SHARED_EMITTERS:
|
|
437
|
+
contents = emitter(project)
|
|
438
|
+
if conditional and not contents:
|
|
439
|
+
continue
|
|
440
|
+
out.append((build_root.joinpath(*rel.split("/")), contents))
|
|
441
|
+
return out
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _shared_artefact(project: BoardProject, name: str) -> str:
|
|
445
|
+
"""Contents of the shared artefact at `name` (its full path under the build
|
|
446
|
+
root, e.g. `generated/dts-partitions.dtsi`), running ONLY that artefact's
|
|
447
|
+
emitter. The standalone `--emit` modes render through this, so they and
|
|
448
|
+
the plan's `sharedArtefacts[].contents` share one call site per artefact.
|
|
449
|
+
|
|
450
|
+
The unconditional artefacts always exist; an unknown `name` raises. A
|
|
451
|
+
conditional one (sysbuild / TF-M) yields "" when its emit is empty.
|
|
452
|
+
"""
|
|
453
|
+
matches = [(emitter, conditional) for rel, emitter, conditional
|
|
454
|
+
in _SHARED_EMITTERS if rel == name]
|
|
455
|
+
if len(matches) != 1:
|
|
456
|
+
raise OrchestratorError(
|
|
457
|
+
f"no unique shared artefact '{name}' ({len(matches)} matches)")
|
|
458
|
+
emitter, _conditional = matches[0]
|
|
459
|
+
return emitter(project)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _shared_tfm_conf(project: BoardProject) -> str:
|
|
463
|
+
"""The `sysbuild/tfm/tfm.conf` contents, or "" when the project has none
|
|
464
|
+
(absence-emits-nothing). Runs only the TF-M emitter (tan-cli#1216)."""
|
|
465
|
+
return _shared_artefact(project, "sysbuild/tfm/tfm.conf")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _slice_toolchain(slice_: Slice) -> dict[str, Optional[str]]:
|
|
469
|
+
"""This slice's compiler identity: `{targetTriple, compiler, sysroot, id}`
|
|
470
|
+
(#610 §4 per-slice tooling index).
|
|
471
|
+
|
|
472
|
+
Grounded in the SoM preset's `topology.<core>.toolchain` default, or a
|
|
473
|
+
project's `cores.<core>.toolchain` override (issue #964) -- the same
|
|
474
|
+
field `Slice.to_manifest_entry` already surfaces in
|
|
475
|
+
`system-manifest.yaml` -- never invented. For a Zephyr slice this
|
|
476
|
+
value (e.g. `arm-zephyr-eabi`) IS the real Zephyr SDK toolchain
|
|
477
|
+
directory name AND its GCC target triple, so `targetTriple` /
|
|
478
|
+
`compiler` derive straight from it. A Yocto slice's toolchain tag
|
|
479
|
+
(`poky-glibc`) is a *category* (C-library flavour), not a literal
|
|
480
|
+
GCC triple -- the real triple depends on the Yocto build's own
|
|
481
|
+
TUNE_FEATURES/TCLIBC (outside board.yaml / SoM metadata), so
|
|
482
|
+
`targetTriple` / `compiler` / `sysroot` stay null rather than
|
|
483
|
+
guess an ABI suffix (e.g. `gnueabi` vs `gnueabihf`). Zephyr has no
|
|
484
|
+
conventional cross-compile sysroot either (the SDK bundles its own
|
|
485
|
+
libc per architecture) -- `sysroot` is null for every runtime today.
|
|
486
|
+
"""
|
|
487
|
+
target_triple: Optional[str] = None
|
|
488
|
+
compiler: Optional[str] = None
|
|
489
|
+
if slice_.os == "zephyr" and slice_.toolchain:
|
|
490
|
+
target_triple = slice_.toolchain
|
|
491
|
+
compiler = f"{slice_.toolchain}-gcc"
|
|
492
|
+
return {
|
|
493
|
+
"targetTriple": target_triple,
|
|
494
|
+
"compiler": compiler,
|
|
495
|
+
"sysroot": None,
|
|
496
|
+
"id": slice_.toolchain,
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
#: An artifact block that claims nothing. Shared by the runtimes with
|
|
501
|
+
#: no honest path to report (yocto) and by any slice whose `command` was
|
|
502
|
+
#: blocked, so the two can never drift apart.
|
|
503
|
+
_NULL_ARTIFACTS: dict[str, Optional[str]] = {
|
|
504
|
+
"elf": None,
|
|
505
|
+
"map": None,
|
|
506
|
+
"bin": None,
|
|
507
|
+
"sizeReport": None,
|
|
508
|
+
"symbols": None,
|
|
509
|
+
"compileCommands": None,
|
|
510
|
+
"outputDir": None,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _slice_artifacts(build_dir: Path, slice_: Slice,
|
|
515
|
+
has_command: bool = True) -> dict[str, Optional[str]]:
|
|
516
|
+
"""Deterministic OUTPUT paths under `build_dir` (#610 §4) -- the
|
|
517
|
+
WHERE, not a promise the files already exist (they don't until the
|
|
518
|
+
slice is actually built).
|
|
519
|
+
|
|
520
|
+
Zephyr's own CMake layout fixes these names: `cmake/modules/
|
|
521
|
+
kernel.cmake` (`PROJECT_BINARY_DIR = CMAKE_CURRENT_BINARY_DIR/
|
|
522
|
+
zephyr`, `KERNEL_ELF_NAME`/`_BIN_NAME`/`_MAP_NAME`/`_SYMBOLS_NAME`/
|
|
523
|
+
`_STAT_NAME`) always lands `zephyr.elf` / `.bin` / `.map` /
|
|
524
|
+
`.symbols` / `.stat` in a `zephyr/` subdirectory of the build dir
|
|
525
|
+
(`.symbols` / `.stat` are gated behind the opt-in `CONFIG_
|
|
526
|
+
OUTPUT_SYMBOLS` / `CONFIG_OUTPUT_STAT`, same "doesn't exist until
|
|
527
|
+
built/enabled" caveat as the rest); the top-level `CMakeLists.txt`
|
|
528
|
+
forces `CMAKE_EXPORT_COMPILE_COMMANDS` unconditionally, always to
|
|
529
|
+
the build dir root, not the `zephyr/` subdirectory.
|
|
530
|
+
|
|
531
|
+
That build dir root is `<buildDir>/build/`, NOT `<buildDir>` --
|
|
532
|
+
`_slice_command` deliberately emits no `-d` (finding M14: west
|
|
533
|
+
resolves a relative `-d` against its own cwd, already `<buildDir>`,
|
|
534
|
+
so `-d <buildDir>` would double-nest), and west's default output is
|
|
535
|
+
`<cwd>/build`. Every zephyr path below therefore carries that
|
|
536
|
+
`build/` level. It used to be missing (issue #1360): the plan named
|
|
537
|
+
`<buildDir>/zephyr/zephyr.elf`, a path west never creates, and every
|
|
538
|
+
consumer had to re-derive the real one from `buildDir` instead of
|
|
539
|
+
reading the block it was given -- tan's since-retired `renode`
|
|
540
|
+
verb (ADR 0022) looked for `build/m55_he-zephyr/zephyr/zephyr.elf`
|
|
541
|
+
and found nothing. The
|
|
542
|
+
`build/` level is west's, not this planner's, which is exactly why
|
|
543
|
+
it belongs in the reported path and not in each reader's head.
|
|
544
|
+
|
|
545
|
+
A Yocto slice's real output (the wic/ext4 image) lands under the
|
|
546
|
+
*Yocto build tree's* own deploy dir -- outside this slice's
|
|
547
|
+
`build_dir`, which only ever carries the `local.conf` fragment --
|
|
548
|
+
so there is no honest path to report.
|
|
549
|
+
|
|
550
|
+
`baremetal` reports the ONE path its configure line GUARANTEES
|
|
551
|
+
(tan-cli#550 -- the whole block used to be null, so a slice that
|
|
552
|
+
produced no binary at all was indistinguishable from one that built
|
|
553
|
+
fine): `outputDir`. `_slice_command` passes
|
|
554
|
+
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=$<1:<buildDir>/output>`, so every
|
|
555
|
+
RUNTIME artifact the app links -- i.e. every `add_executable()`
|
|
556
|
+
target -- lands there, on single- and multi-config generators alike
|
|
557
|
+
(the `$<1:...>` generator expression is what suppresses the
|
|
558
|
+
per-config subdirectory a multi-config generator would otherwise
|
|
559
|
+
append).
|
|
560
|
+
|
|
561
|
+
What `outputDir` does NOT license is the inverse reading. An empty
|
|
562
|
+
or absent `output/` after the build step does NOT prove the slice
|
|
563
|
+
produced nothing: `CMAKE_RUNTIME_OUTPUT_DIRECTORY` governs
|
|
564
|
+
`add_executable` targets ONLY, so a firmware app built as
|
|
565
|
+
`add_library(fwcore STATIC ...)` plus a custom link/objcopy target
|
|
566
|
+
builds cleanly (rc 0, `libfwcore.a` + `main.c.o` + `fw.elf`
|
|
567
|
+
produced) and never creates `output/` at all -- measured, not
|
|
568
|
+
assumed. It is a deterministic PLACE TO LOOK, not a
|
|
569
|
+
build-succeeded oracle.
|
|
570
|
+
|
|
571
|
+
`compileCommands` stays null for baremetal even though the
|
|
572
|
+
configure passes `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`: CMake
|
|
573
|
+
implements that variable "only by Makefile Generators and Ninja
|
|
574
|
+
Generators. It is ignored on other generators" (CMake's own docs
|
|
575
|
+
for the variable), and this planner does not choose the generator.
|
|
576
|
+
On Windows, whose default generator is Visual Studio, the file is
|
|
577
|
+
never written -- so naming the path here would be exactly the
|
|
578
|
+
artifacts-lie tan-cli#550 is about, pointed the other way. A zephyr
|
|
579
|
+
slice's `compileCommands` is a different case and stays: `west
|
|
580
|
+
build` drives Ninja and Zephyr's own top-level `CMakeLists.txt`
|
|
581
|
+
forces the variable on.
|
|
582
|
+
|
|
583
|
+
`elf` / `bin` / `map` / `sizeReport` / `symbols` stay null for
|
|
584
|
+
baremetal: the executable's NAME is the app's own `CMakeLists.txt`
|
|
585
|
+
to pick, not an SDK convention this emitter may invent -- pinning
|
|
586
|
+
the DIRECTORY is the strongest honest claim, which is exactly what
|
|
587
|
+
`outputDir` carries.
|
|
588
|
+
|
|
589
|
+
`has_command` is False for a slice whose `command` was BLOCKED
|
|
590
|
+
(`command-unrooted` / `board-tree-missing` / `no-command`): nothing
|
|
591
|
+
will ever configure that build dir, so every path below would be a
|
|
592
|
+
dangling promise pinned by a configure that never runs. Such a
|
|
593
|
+
slice reports an all-null block.
|
|
594
|
+
"""
|
|
595
|
+
if not has_command:
|
|
596
|
+
return dict(_NULL_ARTIFACTS)
|
|
597
|
+
if slice_.os == "zephyr":
|
|
598
|
+
# west's own build tree: `<buildDir>/build/`, because the command
|
|
599
|
+
# runs with cwd=<buildDir> and no `-d` (see the docstring above
|
|
600
|
+
# and `_slice_command`'s M14 note).
|
|
601
|
+
wdir = build_dir / "build"
|
|
602
|
+
zdir = wdir / "zephyr"
|
|
603
|
+
return {
|
|
604
|
+
"elf": (zdir / "zephyr.elf").as_posix(),
|
|
605
|
+
"map": (zdir / "zephyr.map").as_posix(),
|
|
606
|
+
"bin": (zdir / "zephyr.bin").as_posix(),
|
|
607
|
+
"sizeReport": (zdir / "zephyr.stat").as_posix(),
|
|
608
|
+
"symbols": (zdir / "zephyr.symbols").as_posix(),
|
|
609
|
+
"compileCommands": (wdir / "compile_commands.json").as_posix(),
|
|
610
|
+
# Zephyr's own tree lands at `<buildDir>/build/` and the six
|
|
611
|
+
# named paths above all index it -- no separate output
|
|
612
|
+
# directory to report.
|
|
613
|
+
"outputDir": None,
|
|
614
|
+
}
|
|
615
|
+
if slice_.os == "baremetal":
|
|
616
|
+
return dict(_NULL_ARTIFACTS,
|
|
617
|
+
outputDir=(build_dir / "output").as_posix())
|
|
618
|
+
return dict(_NULL_ARTIFACTS)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _slice_debug(
|
|
622
|
+
project: BoardProject,
|
|
623
|
+
slice_: Slice,
|
|
624
|
+
flash_method: Optional[str],
|
|
625
|
+
flash_args: Optional[dict[str, Any]],
|
|
626
|
+
) -> dict[str, Optional[str]]:
|
|
627
|
+
"""Headless monitor/debug selectors for this slice: `{console, probe}`
|
|
628
|
+
(#610 §4).
|
|
629
|
+
|
|
630
|
+
`console` reuses `_resolve_console` -- the same OS-derived /
|
|
631
|
+
`diagnostics.console:`-overridable selector `_slice_alp_conf` /
|
|
632
|
+
`_slice_local_conf` already emit Kconfig from. `"none"` means
|
|
633
|
+
"inherit whatever the board provides" -- not a concrete selector a
|
|
634
|
+
headless consumer can act on -- so it maps to null here. `probe`
|
|
635
|
+
reuses `_slice_flash_recipe`'s already-resolved runner (the same
|
|
636
|
+
fact `tan flash` dispatches on, computed once per slice by the
|
|
637
|
+
caller and passed in): `_slice_flash_recipe` no longer forces a
|
|
638
|
+
runner for `zephyr_west_flash` slices (not every in-tree board
|
|
639
|
+
registers `openocd`), so `probe` is the explicitly-configured
|
|
640
|
+
runner when one is set, else null -- meaning "board.cmake default,
|
|
641
|
+
not independently knowable here". The Yocto image-flash recipe and
|
|
642
|
+
the baremetal cmake recipe don't identify a live debug probe
|
|
643
|
+
either, so `probe` stays null there too.
|
|
644
|
+
"""
|
|
645
|
+
console_sel = _resolve_console(
|
|
646
|
+
project.diagnostics.get("console"), slice_.os, slice_.hw_console)
|
|
647
|
+
console = None if console_sel == "none" else console_sel
|
|
648
|
+
probe: Optional[str] = None
|
|
649
|
+
if flash_method == "zephyr_west_flash":
|
|
650
|
+
probe = (flash_args or {}).get("runner")
|
|
651
|
+
return {"console": console, "probe": probe}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _sdk_version() -> Optional[str]:
|
|
655
|
+
"""The `version:` field out of `metadata/sdk_version.yaml` -- the single
|
|
656
|
+
source `scripts/bump_version.py` bumps and `check_version_doc_sync.py`
|
|
657
|
+
pins every other copy against. Same read-and-strip idiom as
|
|
658
|
+
`alp_cli._version` / `check_version_doc_sync.declared_version`, kept
|
|
659
|
+
inline here rather than imported: neither of those lives on an import
|
|
660
|
+
path this package can reach without a sys.path hack (`scripts/` itself
|
|
661
|
+
carries no `__init__.py`).
|
|
662
|
+
"""
|
|
663
|
+
sdk_version_yaml = REPO / "metadata" / "sdk_version.yaml"
|
|
664
|
+
try:
|
|
665
|
+
text = sdk_version_yaml.read_text(encoding="utf-8")
|
|
666
|
+
except OSError:
|
|
667
|
+
# No adjacent metadata/ tree (e.g. packaged as a wheel) -- provenance
|
|
668
|
+
# is best-effort, never a reason to fail the emit. Mirrors the
|
|
669
|
+
# OSError guard in `alp_cli._version` and `_sdk_commit` below.
|
|
670
|
+
return None
|
|
671
|
+
for line in text.splitlines():
|
|
672
|
+
stripped = line.strip()
|
|
673
|
+
if stripped.startswith("version:"):
|
|
674
|
+
return stripped.split(":", 1)[1].split("#", 1)[0].strip()
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _sdk_commit() -> Optional[str]:
|
|
679
|
+
"""Short git commit of this checkout (`git rev-parse --short HEAD`), or
|
|
680
|
+
`None` when git -- or a `.git` dir -- isn't available. Mirrors the
|
|
681
|
+
robustness of `scripts/build_receipt.py::_git_rev` (try/except over both
|
|
682
|
+
`CalledProcessError` and a missing `git` binary) rather than importing
|
|
683
|
+
it: that module isn't reachable from this package without the same
|
|
684
|
+
sys.path workaround noted on `_sdk_version` above, and receipt's variant
|
|
685
|
+
also resolves the FULL rev plus a dirty-tree flag this envelope doesn't
|
|
686
|
+
need.
|
|
687
|
+
|
|
688
|
+
`git` is resolved through `tool_lookup.resolve_tool` and spawned with
|
|
689
|
+
`executable=` pinned, the same treatment `token_substitution.py`'s
|
|
690
|
+
build-side git-provenance pair got (tan-cli#797): `emit_build_plan` runs
|
|
691
|
+
with the process cwd sitting in the CUSTOMER's project directory (`tan
|
|
692
|
+
build` is invoked from there, not from `REPO`), so a bare `"git"` argv[0]
|
|
693
|
+
here is the same Windows `CreateProcess`-searches-cwd-first hazard as
|
|
694
|
+
those two -- a checked-in or unpacked `git.exe` at the project root would
|
|
695
|
+
have run in `git`'s place and silently corrupted the emitted plan's
|
|
696
|
+
`sdkCommit`. `resolve_tool` returning `None` (git not on PATH at all)
|
|
697
|
+
degrades to "no commit", the same no-signal answer a `CalledProcessError`
|
|
698
|
+
already gave -- never a crash of the emit.
|
|
699
|
+
"""
|
|
700
|
+
git_exe = resolve_tool("git", os.environ).resolved
|
|
701
|
+
if git_exe is None:
|
|
702
|
+
return None
|
|
703
|
+
try:
|
|
704
|
+
result = subprocess.run(
|
|
705
|
+
["git", "-C", str(REPO), "rev-parse", "--short", "HEAD"],
|
|
706
|
+
executable=git_exe,
|
|
707
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
708
|
+
env=spawn_env(), check=True)
|
|
709
|
+
except (subprocess.CalledProcessError, OSError):
|
|
710
|
+
return None
|
|
711
|
+
commit = result.stdout.strip()
|
|
712
|
+
return commit or None
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def emit_build_plan(
|
|
716
|
+
project: BoardProject,
|
|
717
|
+
*,
|
|
718
|
+
board_yaml: Path,
|
|
719
|
+
build_root: Path,
|
|
720
|
+
) -> str:
|
|
721
|
+
"""Emit the machine-readable build plan as JSON (Wave C contract).
|
|
722
|
+
|
|
723
|
+
Consumed by `tan` (alplabai/tan-cli), which materialises the plan's
|
|
724
|
+
files, runs each slice's command, and owns scheduling / caching /
|
|
725
|
+
progress UX on top -- instead of re-implementing this planner. The
|
|
726
|
+
Wave C contract was settled 2026-06-04 with the alp-sdk-vscode team
|
|
727
|
+
(docs/PROPOSAL-alp-build-core.md records that settlement); the real
|
|
728
|
+
parser today is tan-cli, not the alp-sdk-vscode extension itself.
|
|
729
|
+
|
|
730
|
+
Contract notes (locked with the consumer -- bump `schemaVersion`
|
|
731
|
+
and flag in the CHANGELOG before changing the shape):
|
|
732
|
+
|
|
733
|
+
* camelCase keys; `schemaVersion` is independent of board.yaml's
|
|
734
|
+
schema version.
|
|
735
|
+
* Every artefact carries its `contents` so the consumer's
|
|
736
|
+
materialise step stays pure IO. `_shared_artefacts` /
|
|
737
|
+
`_slice_config_artefact` are the single sources both this emit
|
|
738
|
+
and the Orchestrator's own materialise step read, so the two
|
|
739
|
+
cannot drift.
|
|
740
|
+
* No `inputHash` (the consumer computes its own cache key over
|
|
741
|
+
the plan) and no `sequential` (parallelism policy belongs to
|
|
742
|
+
the consumer's scheduler).
|
|
743
|
+
* One slice per non-`off` core, sorted by coreId. A slice this
|
|
744
|
+
script cannot build yet (e.g. no `app:`) is carried with
|
|
745
|
+
`command: null` plus a `no-command` warning -- never dropped,
|
|
746
|
+
so the consumer can still report the core. Same treatment for
|
|
747
|
+
a zephyr slice whose `board:` target has no tree under
|
|
748
|
+
`zephyr/boards/alp/` (`board-tree-missing`, issue #999): the
|
|
749
|
+
plan never carries a `west build -b <board>` command that is
|
|
750
|
+
guaranteed to fail Zephyr's own board lookup.
|
|
751
|
+
* Write-free: nothing is created on disk. (Command resolution
|
|
752
|
+
stats the app dir to pick the CMakeLists.txt convention --
|
|
753
|
+
read-only, same as the build itself.)
|
|
754
|
+
* Per-slice tooling index (#610 §4, additive to schemaVersion 1 --
|
|
755
|
+
never renamed/removed, see `metadata/schemas/build-plan-v1.
|
|
756
|
+
schema.json`): `toolchain` (compiler identity --
|
|
757
|
+
`_slice_toolchain`, keys `targetTriple`/`compiler`/`sysroot`/`id`),
|
|
758
|
+
`artifacts` (deterministic OUTPUT paths under `buildDir`, not a
|
|
759
|
+
promise they exist yet -- `_slice_artifacts`, keys `elf`/`map`/
|
|
760
|
+
`bin`/`sizeReport`/`symbols`/`compileCommands`), and `debug`
|
|
761
|
+
(headless console/probe selectors -- `_slice_debug`). A field
|
|
762
|
+
genuinely not derivable for a runtime (e.g. a Yocto slice's exact
|
|
763
|
+
GCC triple) is null, never guessed. (These three sub-objects'
|
|
764
|
+
keys were corrected from an accidental snake_case to camelCase
|
|
765
|
+
to match the rest of this contract -- see the CHANGELOG.)
|
|
766
|
+
* Envelope provenance, additive to schemaVersion 1: `sdkVersion`
|
|
767
|
+
(`metadata/sdk_version.yaml`'s `version:`, via `_sdk_version`)
|
|
768
|
+
and `sdkCommit` (`git rev-parse --short HEAD` of this checkout,
|
|
769
|
+
via `_sdk_commit`; `null` when git/`.git` isn't available -- never
|
|
770
|
+
raises) so a cached/materialised plan can be traced back to the
|
|
771
|
+
planner that produced it.
|
|
772
|
+
"""
|
|
773
|
+
# Orchestrator-side (stay inline until orchestrator.py); lazy to avoid
|
|
774
|
+
# a buildplan<->package import cycle.
|
|
775
|
+
from .cameras import CameraSelectError
|
|
776
|
+
from .orchestrator import (
|
|
777
|
+
STOCK_IMAGE_APP,
|
|
778
|
+
UnbuildableYoctoMachineError,
|
|
779
|
+
UnknownBoardTargetError,
|
|
780
|
+
UnrootedPathError,
|
|
781
|
+
_resolve_app_path,
|
|
782
|
+
_slice_command,
|
|
783
|
+
_slice_flash_recipe,
|
|
784
|
+
_slice_post_commands,
|
|
785
|
+
_tokenize,
|
|
786
|
+
iter_buildable_slices,
|
|
787
|
+
)
|
|
788
|
+
build_root = Path(build_root)
|
|
789
|
+
# Anchor every slice's relative `app:` on the board.yaml's own
|
|
790
|
+
# directory, never the emitting process's CWD -- the plan must be
|
|
791
|
+
# byte-identical no matter where `--emit build-plan` is invoked from
|
|
792
|
+
# (issue #596).
|
|
793
|
+
base_dir = Path(board_yaml).resolve().parent
|
|
794
|
+
slices_out: list[dict[str, Any]] = []
|
|
795
|
+
warnings: list[dict[str, Any]] = []
|
|
796
|
+
|
|
797
|
+
for slice_ in iter_buildable_slices(project):
|
|
798
|
+
build_dir = _slice_build_dir(build_root, slice_)
|
|
799
|
+
# `replace` keeps this emit side-effect free: _slice_command
|
|
800
|
+
# reads `build_dir` off the slice (baremetal -B), and the
|
|
801
|
+
# project's own Slice objects must stay untouched.
|
|
802
|
+
try:
|
|
803
|
+
cmd = _slice_command(
|
|
804
|
+
project, replace(slice_, build_dir=build_dir),
|
|
805
|
+
base_dir=base_dir)
|
|
806
|
+
except UnrootedPathError as e:
|
|
807
|
+
# One of _slice_command's five `_tokenize` call sites (west
|
|
808
|
+
# build's app-dir arg, -DSB_CONF_FILE's two paths,
|
|
809
|
+
# -DEXTRA_CONF_FILE, cmake -S) hit a path outside both
|
|
810
|
+
# ${PROJECT_ROOT} and ${SDK_ROOT} (issue #865). Block the
|
|
811
|
+
# command rather than ever let a bare absolute path reach
|
|
812
|
+
# `command.args` on a plan tagged `planPathMode: tokened` --
|
|
813
|
+
# same "carry the slice, never emit a broken/non-hermetic
|
|
814
|
+
# command" convention as `no-command` below.
|
|
815
|
+
cmd = None
|
|
816
|
+
warnings.append({
|
|
817
|
+
"code": "command-unrooted",
|
|
818
|
+
"coreId": slice_.core_id,
|
|
819
|
+
"message": (f"core '{slice_.core_id}' build command would "
|
|
820
|
+
f"embed a path outside both the project "
|
|
821
|
+
f"({base_dir}) and the SDK checkout ({REPO}): "
|
|
822
|
+
f"'{e}' -- blocked rather than emit a "
|
|
823
|
+
f"non-hermetic command"),
|
|
824
|
+
})
|
|
825
|
+
except UnknownBoardTargetError as e:
|
|
826
|
+
# The SoM preset's topology named a Zephyr board with no
|
|
827
|
+
# tree under zephyr/boards/alp/ (issue #999 one layer down
|
|
828
|
+
# from the declaration gate, at emit time): block the
|
|
829
|
+
# command rather than ever hand a consumer (tan, or a
|
|
830
|
+
# customer's own `west build`) a `-b <board>` argument that
|
|
831
|
+
# is guaranteed to die with Zephyr's own "No board named
|
|
832
|
+
# ... Invalid BOARD" error -- same "carry the slice, never
|
|
833
|
+
# emit a broken command" convention as `no-command` below.
|
|
834
|
+
cmd = None
|
|
835
|
+
warnings.append({
|
|
836
|
+
"code": "board-tree-missing",
|
|
837
|
+
"coreId": slice_.core_id,
|
|
838
|
+
"message": str(e),
|
|
839
|
+
})
|
|
840
|
+
except CameraSelectError as e:
|
|
841
|
+
# A `cameras:` entry that cannot become a -DSHIELD for this
|
|
842
|
+
# slice (module without a zephyr_shield, or a carrier shield
|
|
843
|
+
# with no overlay for the board target): block, never emit a
|
|
844
|
+
# command that silently drops the camera.
|
|
845
|
+
cmd = None
|
|
846
|
+
warnings.append({
|
|
847
|
+
"code": "camera-select-failed",
|
|
848
|
+
"coreId": slice_.core_id,
|
|
849
|
+
"message": str(e),
|
|
850
|
+
})
|
|
851
|
+
except UnbuildableYoctoMachineError as e:
|
|
852
|
+
# The slice's `machine:` is a known-non-buildable Yocto MACHINE
|
|
853
|
+
# (issue #1982 -- the two AEN A32-cluster carriers, unbuildable
|
|
854
|
+
# for related but distinct reasons: see
|
|
855
|
+
# `orchestrator.YOCTO_MACHINE_UNBUILDABLE`'s own comment,
|
|
856
|
+
# issues #1968 / #1971): block the command rather than ever
|
|
857
|
+
# hand a consumer a `bitbake` target that cannot succeed --
|
|
858
|
+
# same "carry the slice, never emit a broken command"
|
|
859
|
+
# convention as `no-command` below.
|
|
860
|
+
cmd = None
|
|
861
|
+
warnings.append({
|
|
862
|
+
"code": "yocto-machine-unbuildable",
|
|
863
|
+
"coreId": slice_.core_id,
|
|
864
|
+
"message": str(e),
|
|
865
|
+
})
|
|
866
|
+
else:
|
|
867
|
+
if cmd is None:
|
|
868
|
+
if (slice_.os == "yocto" and slice_.app
|
|
869
|
+
and not slice_.image and not slice_.recipe):
|
|
870
|
+
# An app-only Yocto slice with no `recipe:` has no valid
|
|
871
|
+
# bitbake target -- `app:` is a source directory, not a
|
|
872
|
+
# recipe name (issue #597). Block the plan explicitly
|
|
873
|
+
# instead of ever emitting `bitbake <path>`.
|
|
874
|
+
warnings.append({
|
|
875
|
+
"code": "yocto-recipe-missing",
|
|
876
|
+
"coreId": slice_.core_id,
|
|
877
|
+
"message": (f"core '{slice_.core_id}' has app: "
|
|
878
|
+
f"'{slice_.app}' but no recipe: -- add "
|
|
879
|
+
f"the bitbake recipe name that packages "
|
|
880
|
+
f"this app source, or set image: to "
|
|
881
|
+
f"build a stock image instead"),
|
|
882
|
+
})
|
|
883
|
+
else:
|
|
884
|
+
warnings.append({
|
|
885
|
+
"code": "no-command",
|
|
886
|
+
"coreId": slice_.core_id,
|
|
887
|
+
"message": (f"no build command for core "
|
|
888
|
+
f"'{slice_.core_id}' (os: {slice_.os}) "
|
|
889
|
+
f"-- missing app/board/image"),
|
|
890
|
+
})
|
|
891
|
+
config_artefacts: list[dict[str, str]] = []
|
|
892
|
+
# A baremetal slice's `alp-baremetal.cmake` has exactly ONE reader:
|
|
893
|
+
# the `-DCMAKE_PROJECT_INCLUDE=` arg on this slice's own configure.
|
|
894
|
+
# If the command was blocked there is no reader, so materialising
|
|
895
|
+
# the file would leave a consumer holding a path nothing will ever
|
|
896
|
+
# open. (A zephyr/yocto artefact is a plain config fragment a human
|
|
897
|
+
# can still read, so those are emitted either way.)
|
|
898
|
+
artefact = (None if (cmd is None and slice_.os == "baremetal")
|
|
899
|
+
else _slice_config_artefact(project, slice_))
|
|
900
|
+
if artefact is not None:
|
|
901
|
+
name, contents = artefact
|
|
902
|
+
config_artefacts.append({
|
|
903
|
+
"path": (build_dir / name).as_posix(),
|
|
904
|
+
"contents": contents,
|
|
905
|
+
})
|
|
906
|
+
# `diagnostics.link: itcm` (tan-cli#1350): the ITCM retarget
|
|
907
|
+
# conf + overlay ride beside `alp.conf`; `_slice_command` wires
|
|
908
|
+
# them in. Only when the command exists to read them.
|
|
909
|
+
if cmd is not None:
|
|
910
|
+
for extra_name, extra_contents in extra_config_artefacts(
|
|
911
|
+
project, slice_):
|
|
912
|
+
config_artefacts.append({
|
|
913
|
+
"path": (build_dir / extra_name).as_posix(),
|
|
914
|
+
"contents": extra_contents,
|
|
915
|
+
})
|
|
916
|
+
# Additive rendered-text artefacts (ADR-0026 §D, tan-cli#1216): the
|
|
917
|
+
# DTS overlay, the full `-D` listing, the build-identifier header and
|
|
918
|
+
# the west fragment, byte-identical to the standalone
|
|
919
|
+
# `--emit dts-overlay|cmake-args|hw-info-h|west-libraries --core`
|
|
920
|
+
# renders. Always AFTER the slice's primary config artefact, so a
|
|
921
|
+
# consumer that reads `configArtefacts[0]` is unaffected.
|
|
922
|
+
extras: list[Optional[tuple[str, str]]] = []
|
|
923
|
+
# Same dangling-path rule as `alp-baremetal.cmake` above: a baremetal
|
|
924
|
+
# slice whose command was blocked will never configure its build dir,
|
|
925
|
+
# so promise it nothing.
|
|
926
|
+
if not (cmd is None and slice_.os == "baremetal"):
|
|
927
|
+
try:
|
|
928
|
+
extras.append(_slice_dts_overlay_artefact(project, slice_))
|
|
929
|
+
except DtsOverlayUnavailable as exc:
|
|
930
|
+
# Additive artefact: a board with no header (or an
|
|
931
|
+
# unrecognised SKU) must not stop a plan that emitted fine
|
|
932
|
+
# before. The consumer sees the warning and falls back.
|
|
933
|
+
# Any other OrchestratorError still fails the plan.
|
|
934
|
+
warnings.append({
|
|
935
|
+
"code": "dts-overlay-unavailable",
|
|
936
|
+
"coreId": slice_.core_id,
|
|
937
|
+
"message": (f"core '{slice_.core_id}': no `alp.overlay` "
|
|
938
|
+
f"artefact -- {exc}"),
|
|
939
|
+
})
|
|
940
|
+
try:
|
|
941
|
+
extras.append(_slice_cmake_args_artefact(project, slice_))
|
|
942
|
+
except CameraSelectError as exc:
|
|
943
|
+
# The listing is not emitted; make sure the plan still says
|
|
944
|
+
# why (the command path usually already did).
|
|
945
|
+
if not any(w["code"] == "camera-select-failed"
|
|
946
|
+
and w["coreId"] == slice_.core_id for w in warnings):
|
|
947
|
+
warnings.append({
|
|
948
|
+
"code": "camera-select-failed",
|
|
949
|
+
"coreId": slice_.core_id,
|
|
950
|
+
"message": str(exc),
|
|
951
|
+
})
|
|
952
|
+
try:
|
|
953
|
+
extras.append(_slice_hw_info_artefact(project, slice_))
|
|
954
|
+
except HwInfoUnavailable as exc:
|
|
955
|
+
warnings.append({
|
|
956
|
+
"code": "hw-info-unavailable",
|
|
957
|
+
"coreId": slice_.core_id,
|
|
958
|
+
"message": (f"core '{slice_.core_id}': no "
|
|
959
|
+
f"`alp_hw_info_build.h` artefact -- {exc}"),
|
|
960
|
+
})
|
|
961
|
+
extras.append(_slice_west_libs_artefact(project, slice_))
|
|
962
|
+
for extra in extras:
|
|
963
|
+
if extra is not None:
|
|
964
|
+
config_artefacts.append({
|
|
965
|
+
"path": (build_dir / extra[0]).as_posix(),
|
|
966
|
+
"contents": extra[1],
|
|
967
|
+
})
|
|
968
|
+
# `appDir` retains the resolved source directory independent of
|
|
969
|
+
# `command` -- tooling that wants the app source (e.g. to watch
|
|
970
|
+
# it for incremental rebuilds) doesn't have to reverse-engineer
|
|
971
|
+
# it out of a yocto/zephyr/baremetal-shaped command (issue #597).
|
|
972
|
+
# `alp-image-edge` is the A-core stock-image token, not a source
|
|
973
|
+
# path -- there is no app dir to report for it.
|
|
974
|
+
# Tokenized (issue #865): almost every `app:` resolves under the
|
|
975
|
+
# project (`${PROJECT_ROOT}/...`); the SDK-owned stock M-core shim
|
|
976
|
+
# (STOCK_SHIM_APP) resolves under the SDK checkout instead
|
|
977
|
+
# (`${SDK_ROOT}/...`). `_tokenize` raises `UnrootedPathError` when
|
|
978
|
+
# it is under neither -- fall back to the absolute path (still
|
|
979
|
+
# useful for tooling) but flag it rather than let it silently
|
|
980
|
+
# mis-root on the consumer side.
|
|
981
|
+
app_dir = None
|
|
982
|
+
if slice_.app and slice_.app != STOCK_IMAGE_APP:
|
|
983
|
+
resolved_app = _resolve_app_path(slice_.app, base_dir)
|
|
984
|
+
try:
|
|
985
|
+
app_dir = _tokenize(resolved_app, base_dir, REPO)
|
|
986
|
+
except UnrootedPathError:
|
|
987
|
+
app_dir = resolved_app.as_posix()
|
|
988
|
+
warnings.append({
|
|
989
|
+
"code": "appdir-unrooted",
|
|
990
|
+
"coreId": slice_.core_id,
|
|
991
|
+
"message": (f"core '{slice_.core_id}' app path "
|
|
992
|
+
f"'{app_dir}' is outside both the project "
|
|
993
|
+
f"({base_dir}) and the SDK checkout "
|
|
994
|
+
f"({REPO}) -- emitted absolute; a consumer "
|
|
995
|
+
f"on a different checkout cannot re-root "
|
|
996
|
+
f"it"),
|
|
997
|
+
})
|
|
998
|
+
# Same flash-recipe fact `Slice.to_manifest_entry` surfaces to
|
|
999
|
+
# `tan flash` -- reused here (not re-derived) so `debug.probe`
|
|
1000
|
+
# can never drift from the manifest's own `flash_method`/`flash_args`.
|
|
1001
|
+
flash_method, flash_args = _slice_flash_recipe(slice_)
|
|
1002
|
+
slices_out.append({
|
|
1003
|
+
"coreId": slice_.core_id,
|
|
1004
|
+
"backend": slice_.os,
|
|
1005
|
+
"buildDir": build_dir.as_posix(),
|
|
1006
|
+
"appDir": app_dir,
|
|
1007
|
+
"configArtefacts": config_artefacts,
|
|
1008
|
+
"toolchain": _slice_toolchain(slice_),
|
|
1009
|
+
"artifacts": _slice_artifacts(build_dir, slice_,
|
|
1010
|
+
has_command=cmd is not None),
|
|
1011
|
+
"debug": _slice_debug(
|
|
1012
|
+
project, slice_, flash_method, flash_args),
|
|
1013
|
+
"command": None if cmd is None else {
|
|
1014
|
+
"tool": cmd[0],
|
|
1015
|
+
"args": cmd[1:],
|
|
1016
|
+
"cwd": build_dir.as_posix(),
|
|
1017
|
+
},
|
|
1018
|
+
# Steps the executor MUST run, in order, after `command`
|
|
1019
|
+
# succeeds -- empty for the runtimes whose single tool
|
|
1020
|
+
# invocation already configures AND builds (`west build`,
|
|
1021
|
+
# `bitbake`). A baremetal slice's `cmake -S ... -B .` only
|
|
1022
|
+
# CONFIGURES, so its `cmake --build .` lives here; without it
|
|
1023
|
+
# the slice exited 0 having produced no object file, archive
|
|
1024
|
+
# or executable at all (tan-cli#550). Empty whenever
|
|
1025
|
+
# `command` is null: there is nothing to build on top of a
|
|
1026
|
+
# slice that was never configured.
|
|
1027
|
+
"postCommands": [] if cmd is None else [
|
|
1028
|
+
{
|
|
1029
|
+
"tool": step[0],
|
|
1030
|
+
"args": step[1:],
|
|
1031
|
+
"cwd": build_dir.as_posix(),
|
|
1032
|
+
}
|
|
1033
|
+
for step in _slice_post_commands(slice_)
|
|
1034
|
+
],
|
|
1035
|
+
# Tokened (issue #865), not the native host-path form: tan-cli
|
|
1036
|
+
# (PR #24) substitutes ${SDK_ROOT} with its own checkout root
|
|
1037
|
+
# before handing this to the slice subprocess environment, so
|
|
1038
|
+
# a cached/materialised plan never carries THIS run's absolute
|
|
1039
|
+
# path onto a different machine/checkout.
|
|
1040
|
+
"env": {"ALP_SDK_ROOT": "${SDK_ROOT}"},
|
|
1041
|
+
# SDK-owned values the consumer APPENDS to its own env, distinct
|
|
1042
|
+
# from `env` above (set-verbatim). The join separator is
|
|
1043
|
+
# PER-KEY, not uniformly os.pathsep: EXTRA_ZEPHYR_MODULES is a
|
|
1044
|
+
# CMake list Zephyr's zephyr_module.py splits on `;` on every
|
|
1045
|
+
# platform (not an OS path list), while PYTHONPATH is a real
|
|
1046
|
+
# OS-native path list (os.pathsep). Mirrors the append
|
|
1047
|
+
# `_alp_common.env_with_sdk` / `_workspace.subprocess_env` do
|
|
1048
|
+
# for a real `west build` (ADR-0020 item 3). Tokened same as
|
|
1049
|
+
# `env` above (issue #865).
|
|
1050
|
+
"envAppendPath": {
|
|
1051
|
+
"EXTRA_ZEPHYR_MODULES": ["${SDK_ROOT}"],
|
|
1052
|
+
"PYTHONPATH": ["${SDK_ROOT}/scripts"],
|
|
1053
|
+
},
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
shared_out = [
|
|
1057
|
+
{"path": p.as_posix(), "contents": c}
|
|
1058
|
+
for p, c in _shared_artefacts(project, build_root)
|
|
1059
|
+
]
|
|
1060
|
+
plan: dict[str, Any] = {
|
|
1061
|
+
"schemaVersion": 1,
|
|
1062
|
+
# Additive to schemaVersion 1 (issue #865): every path in this plan
|
|
1063
|
+
# that anchors on THIS checkout or THIS project is now emitted as
|
|
1064
|
+
# a `${SDK_ROOT}`/`${PROJECT_ROOT}`/`${PYTHON}` token rather than a
|
|
1065
|
+
# baked-in absolute path -- tan-cli (PR #24) requires this literal
|
|
1066
|
+
# value before it will substitute them. `boardYaml` is deliberately
|
|
1067
|
+
# NOT tokenized (kept repo-relative as-passed) -- it is the anchor
|
|
1068
|
+
# both this plan's own comparator and tan use to locate
|
|
1069
|
+
# PROJECT_ROOT in the first place.
|
|
1070
|
+
"planPathMode": "tokened",
|
|
1071
|
+
# Additive (issue #2696): the `${NAME}` values left unresolved on
|
|
1072
|
+
# purpose, so a consumer never mistakes one for a plan path token
|
|
1073
|
+
# above. Always present, `[]` when there are none, so a consumer can
|
|
1074
|
+
# tell "none" from a pre-#2696 plan that does not carry the field.
|
|
1075
|
+
"deferredPlaceholders": _deferred_placeholders(
|
|
1076
|
+
slices_out, shared_out, board_yaml),
|
|
1077
|
+
"generatedBy": "scripts/alp_orchestrate.py",
|
|
1078
|
+
# Additive provenance (ADR 0014's additive rule -- no schemaVersion
|
|
1079
|
+
# bump): traces a cached/materialised plan back to the planner that
|
|
1080
|
+
# produced it. `sdkCommit` is null, never a crash, when git/`.git`
|
|
1081
|
+
# isn't available (e.g. a wheel-installed CLI with no checkout).
|
|
1082
|
+
"sdkVersion": _sdk_version(),
|
|
1083
|
+
"sdkCommit": _sdk_commit(),
|
|
1084
|
+
"boardYaml": Path(board_yaml).as_posix(),
|
|
1085
|
+
"sku": project.sku,
|
|
1086
|
+
"buildRoot": build_root.as_posix(),
|
|
1087
|
+
"executionPolicy": _EXECUTION_POLICY,
|
|
1088
|
+
"slices": slices_out,
|
|
1089
|
+
"sharedArtefacts": shared_out,
|
|
1090
|
+
"warnings": warnings,
|
|
1091
|
+
}
|
|
1092
|
+
return json.dumps(plan, indent=2) + "\n"
|