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
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The ALP **system manifest** (`build/system-manifest.yaml`) -- the derived
|
|
3
|
+
projection of a `board.yaml` that `--emit system-manifest` writes
|
|
4
|
+
(`tan/planner/manifest.py`, relocated from alp-sdk's `alp_orchestrate`), and the
|
|
5
|
+
tool CONTRACT for a (possibly multi-image) project. `tan image` and `tan size` both read THIS instead of re-deriving
|
|
6
|
+
anything from `board.yaml` + the SoM presets.
|
|
7
|
+
|
|
8
|
+
Port of `crates/tan-core/src/system_manifest.rs` (read half) plus the two
|
|
9
|
+
path rules its consumers share.
|
|
10
|
+
|
|
11
|
+
TOLERANT READER (the stability policy negotiated in alp-sdk#106):
|
|
12
|
+
`schema_version` 1 is additive-only, so an unknown key is IGNORED, never
|
|
13
|
+
rejected -- a newer SDK must not break an older `tan`. What IS rejected is a
|
|
14
|
+
document the Rust `serde` model could not deserialise at all: a non-mapping
|
|
15
|
+
root, a missing `schema_version`, or a `slices[]`/`ipc[]`/`helper_mcus[]` entry
|
|
16
|
+
missing a field that is non-`Option` there. Those are hard errors in the
|
|
17
|
+
oracle, and a partial read here would let `tan` act on a manifest the shipped
|
|
18
|
+
binary refuses.
|
|
19
|
+
|
|
20
|
+
**Nothing here shells the SDK.** Every input is a file this project's own build
|
|
21
|
+
wrote under its build root; the alp-sdk checkout is never invoked. That is
|
|
22
|
+
invariant I-32 and its anti-pattern #22 in
|
|
23
|
+
`docs/superpowers/specs/2026-07-29-tan-port-invariants.md`: giving a command an
|
|
24
|
+
alp-sdk-checkout dependency it deliberately does not have is a regression no
|
|
25
|
+
gate catches. `size` does READ `<sdk>/metadata/**` for the memory budget (the
|
|
26
|
+
oracle does too, and a missing SDK is non-fatal there -- the budget resolves
|
|
27
|
+
`unknown`), but it never executes anything in it, and no hardware fact -- SKU,
|
|
28
|
+
address, pin name -- is spelled in this port. Those live in alp-sdk
|
|
29
|
+
`metadata/` (I-26).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
#: The schema major this CLI consumes. A different value is REFUSED rather than
|
|
42
|
+
#: silently mis-applied -- `SYSTEM_MANIFEST_SCHEMA_VERSION` in the Rust.
|
|
43
|
+
SYSTEM_MANIFEST_SCHEMA_VERSION = 1
|
|
44
|
+
|
|
45
|
+
#: The single input document, read from the build root.
|
|
46
|
+
MANIFEST_FILE = "system-manifest.yaml"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def manifest_candidates(build_root: str) -> list[str]:
|
|
50
|
+
"""Where a consumer looks for the manifest given `--build-root X`:
|
|
51
|
+
`X/system-manifest.yaml` first (a root that IS the build dir), then
|
|
52
|
+
`X/build/system-manifest.yaml` -- where `tan build --build-root X` writes
|
|
53
|
+
it (tan-cli#1405). The same `X` therefore works for build and every
|
|
54
|
+
consumer."""
|
|
55
|
+
return [
|
|
56
|
+
os.path.join(build_root, MANIFEST_FILE),
|
|
57
|
+
os.path.join(build_root, "build", MANIFEST_FILE),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_manifest(build_root: str, is_file=os.path.isfile) -> str:
|
|
62
|
+
"""The first existing candidate, else the first (so the caller's
|
|
63
|
+
not-found/read error names the primary path)."""
|
|
64
|
+
candidates = manifest_candidates(build_root)
|
|
65
|
+
for candidate in candidates:
|
|
66
|
+
if is_file(candidate):
|
|
67
|
+
return candidate
|
|
68
|
+
return candidates[0]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def effective_build_root(build_root: str, is_file=os.path.isfile) -> str:
|
|
72
|
+
"""The directory the manifest was actually found in. Every relative
|
|
73
|
+
artefact path in the manifest (and the slice build-dir fallbacks) is
|
|
74
|
+
relative to THIS, so for `--build-root X` with the nested spelling it is
|
|
75
|
+
`X/build`, not `X` (tan-cli#1405). With no manifest present it is `X`."""
|
|
76
|
+
return os.path.dirname(find_manifest(build_root, is_file))
|
|
77
|
+
|
|
78
|
+
#: Subdirectory `west build` writes into when it is emitted with NO `-d` --
|
|
79
|
+
#: which is exactly how the SDK's build plan emits it (I-18). See
|
|
80
|
+
#: [`slice_elf_candidates`].
|
|
81
|
+
WEST_NESTED_DIR = "build"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SystemManifestError(Exception):
|
|
85
|
+
"""Why a system-manifest could not be consumed. `message` is the text the
|
|
86
|
+
oracle puts after `<path>: ` in its `*.manifest-invalid` issue."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, message: str) -> None:
|
|
89
|
+
super().__init__(message)
|
|
90
|
+
self.message = message
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_error(detail: str) -> SystemManifestError:
|
|
94
|
+
# Verbatim `SystemManifestError::Parse`'s `#[error(...)]` prefix. The
|
|
95
|
+
# `detail` tail comes from a different YAML library than the oracle's
|
|
96
|
+
# `serde_yaml`, so a syntax-error tail differs in wording; the shapes that
|
|
97
|
+
# matter to a reader (`missing field \`x\``, `invalid type: ...`) are
|
|
98
|
+
# reproduced below.
|
|
99
|
+
return SystemManifestError(f"system-manifest is not valid YAML: {detail}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _unsupported_version_error(found: Any) -> SystemManifestError:
|
|
103
|
+
# Verbatim `SystemManifestError::UnsupportedSchemaVersion`'s `#[error(...)]`.
|
|
104
|
+
return SystemManifestError(
|
|
105
|
+
f"unsupported system-manifest schema_version {found} "
|
|
106
|
+
f"(this CLI consumes v{SYSTEM_MANIFEST_SCHEMA_VERSION}); "
|
|
107
|
+
"upgrade the CLI or the SDK so the versions match"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _serde_type_name(value: Any) -> str:
|
|
112
|
+
"""serde's own spelling of an unexpected value's type, for the
|
|
113
|
+
`invalid type: <this>, expected struct SystemManifest` message."""
|
|
114
|
+
if value is None:
|
|
115
|
+
return "unit value"
|
|
116
|
+
if isinstance(value, bool):
|
|
117
|
+
return f"boolean `{str(value).lower()}`"
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
return f'string "{value}"'
|
|
120
|
+
if isinstance(value, int):
|
|
121
|
+
return f"integer `{value}`"
|
|
122
|
+
if isinstance(value, float):
|
|
123
|
+
return f"floating point `{value}`"
|
|
124
|
+
if isinstance(value, (list, tuple)):
|
|
125
|
+
return "sequence"
|
|
126
|
+
return "map"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
#: YAML **1.2 core-schema** implicit resolvers -- what serde_yaml applies, and
|
|
130
|
+
#: NOT what PyYAML's `SafeLoader` applies (that is YAML 1.1). Verified case by
|
|
131
|
+
#: case against the compiled binary; every row below is a document where the two
|
|
132
|
+
#: disagree and the difference reaches `data.hw_info` verbatim:
|
|
133
|
+
#: `yes`/`no`/`on`/`off` 1.1 boolean -> 1.2 plain STRING
|
|
134
|
+
#: `007` 1.1 int 7 -> 1.2 plain STRING
|
|
135
|
+
#: `1:30` 1.1 int 90 -> 1.2 plain STRING
|
|
136
|
+
#: `2024-01-01` 1.1 date -> 1.2 plain STRING (and a `date` is not
|
|
137
|
+
#: JSON-serializable, so PyYAML's answer
|
|
138
|
+
#: made `tan image` exit 3 where the
|
|
139
|
+
#: oracle exits 0)
|
|
140
|
+
#: `0o17` 1.1 string -> 1.2 int 15
|
|
141
|
+
#: `0x…` is an int in both.
|
|
142
|
+
#:
|
|
143
|
+
#: The INT row is serde_yaml's `parse_signed_int`, NOT the YAML 1.2 spec's core
|
|
144
|
+
#: schema regex, which is what it used to transcribe (tan-cli#499 defect 10).
|
|
145
|
+
#: serde_yaml strips a leading `+`/`-` BEFORE it tests the radix prefixes, and it
|
|
146
|
+
#: also accepts `0b`, so two more shapes are integers on the oracle and were
|
|
147
|
+
#: staying strings here -- measured against `tan 0.4.1`:
|
|
148
|
+
#: `0b1010` -> 10 `0b11` -> 3 `-0b11` -> -3 `+0b11` -> 3
|
|
149
|
+
#: `-0x1E` -> -30 `+0x1E` -> 30 `-0o17` -> -15 `+0o17` -> 15
|
|
150
|
+
#: and, unchanged, these stay STRINGS (also measured, and the reason the regex
|
|
151
|
+
#: has to stay the gate rather than deferring to PyYAML's `construct_yaml_int`,
|
|
152
|
+
#: which strips `_` and would resolve most of them):
|
|
153
|
+
#: `0B11` `0XA5` `0O17` `-0B11` `0x1_F` `0b1_0` `0o1_7` `1_000` `0_1`
|
|
154
|
+
#: `0x` `0b` `0o` `-0x` `0o8` `0xG` `0b2` `007` `+007` `++5`
|
|
155
|
+
#: The float regex is still the core schema's own.
|
|
156
|
+
_CORE_RESOLVERS = (
|
|
157
|
+
("tag:yaml.org,2002:null", r"^(?:~|null|Null|NULL|)$", ["~", "n", "N", ""]),
|
|
158
|
+
(
|
|
159
|
+
"tag:yaml.org,2002:bool",
|
|
160
|
+
r"^(?:true|True|TRUE|false|False|FALSE)$",
|
|
161
|
+
list("tTfF"),
|
|
162
|
+
),
|
|
163
|
+
(
|
|
164
|
+
"tag:yaml.org,2002:int",
|
|
165
|
+
r"^[-+]?(?:0|[1-9][0-9]*|0b[01]+|0o[0-7]+|0x[0-9a-fA-F]+)$",
|
|
166
|
+
list("-+0123456789"),
|
|
167
|
+
),
|
|
168
|
+
(
|
|
169
|
+
"tag:yaml.org,2002:float",
|
|
170
|
+
r"^(?:[-+]?(?:\.[0-9]+|(?:0|[1-9][0-9]*)(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?"
|
|
171
|
+
r"|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$",
|
|
172
|
+
list("-+.0123456789"),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
#: The infinity/not-a-number spellings serde_yaml's `parse_f64` special-cases
|
|
178
|
+
#: BEFORE it ever tries a numeric parse, matched against the scalar with any
|
|
179
|
+
#: leading sign stripped. Only these become non-finite floats; see
|
|
180
|
+
#: [`_construct_core_float`].
|
|
181
|
+
_YAML_INF_SPELLINGS = (".inf", ".Inf", ".INF")
|
|
182
|
+
_YAML_NAN_SPELLINGS = (".nan", ".NaN", ".NAN")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _construct_core_float(loader, node):
|
|
186
|
+
"""A float-tagged plain scalar as serde_yaml's `parse_f64` resolves it --
|
|
187
|
+
which is NOT what `float()` does for an overflowing exponent (tan-cli#387).
|
|
188
|
+
|
|
189
|
+
serde_yaml accepts a parsed number only `if float.is_finite()`, and returns
|
|
190
|
+
`None` otherwise; a `serde_yaml::Value` then falls through to the next
|
|
191
|
+
candidate type and the scalar stays a STRING. So `soc_flash_mb: 1e400` is
|
|
192
|
+
the string `"1e400"` on the oracle, while PyYAML's own float constructor
|
|
193
|
+
resolves it to `inf`. That diverged twice over: the wrong TYPE in
|
|
194
|
+
`data.hw_info`, and then a value `json.dumps` wrote as the non-JSON literal
|
|
195
|
+
`Infinity` into both stdout and the persisted `bundle-manifest.json`.
|
|
196
|
+
|
|
197
|
+
The three EXPLICIT spellings are the deliberate exception -- `.inf`, `-.inf`
|
|
198
|
+
and `.nan` really do resolve to non-finite `f64` on the oracle. Turning them
|
|
199
|
+
into `null` is `serde_json`'s job at serialise time
|
|
200
|
+
(`tan.envelope.json_safe_floats`), not this loader's: doing it here would
|
|
201
|
+
emit `null` where the oracle emits the string `"1e400"`, and would lose the
|
|
202
|
+
distinction between the two cases entirely.
|
|
203
|
+
"""
|
|
204
|
+
text = loader.construct_scalar(node)
|
|
205
|
+
unsigned = text[1:] if text[:1] in ("-", "+") else text
|
|
206
|
+
if unsigned in _YAML_INF_SPELLINGS:
|
|
207
|
+
return float("-inf") if text.startswith("-") else float("inf")
|
|
208
|
+
if unsigned in _YAML_NAN_SPELLINGS:
|
|
209
|
+
return float("nan")
|
|
210
|
+
try:
|
|
211
|
+
value = float(text)
|
|
212
|
+
except ValueError:
|
|
213
|
+
# Only reachable through an EXPLICIT `!!float` tag -- the implicit
|
|
214
|
+
# resolver's regex admits nothing `float()` refuses. The raw text is
|
|
215
|
+
# what serde_yaml is left holding too.
|
|
216
|
+
return text
|
|
217
|
+
return value if math.isfinite(value) else text
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _core_schema_loader(yaml_module):
|
|
221
|
+
"""A `SafeLoader` resolving plain scalars by the YAML 1.2 CORE schema, so a
|
|
222
|
+
value carried verbatim into the bundle manifest matches what serde_yaml's
|
|
223
|
+
`Value` would have produced. See [`_CORE_RESOLVERS`]."""
|
|
224
|
+
|
|
225
|
+
class CoreSchemaLoader(yaml_module.SafeLoader):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# A fresh dict on the subclass -- never a mutation of SafeLoader's own, which
|
|
229
|
+
# would silently change every other `safe_load` in the process.
|
|
230
|
+
CoreSchemaLoader.yaml_implicit_resolvers = {}
|
|
231
|
+
for tag, pattern, first in _CORE_RESOLVERS:
|
|
232
|
+
CoreSchemaLoader.add_implicit_resolver(tag, re.compile(pattern), first)
|
|
233
|
+
# `add_constructor` copies `yaml_constructors` onto the subclass first, so
|
|
234
|
+
# this is scoped the same way the resolvers above are.
|
|
235
|
+
CoreSchemaLoader.add_constructor("tag:yaml.org,2002:float", _construct_core_float)
|
|
236
|
+
return CoreSchemaLoader
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _literal_scalar_loader(yaml_module):
|
|
240
|
+
"""A `SafeLoader` whose implicit tag resolution is switched OFF entirely, so
|
|
241
|
+
every plain scalar comes back as its RAW TEXT.
|
|
242
|
+
|
|
243
|
+
This is not a nicety. serde_yaml resolves a plain scalar against the TARGET
|
|
244
|
+
type, so a `String` field receives the scalar's raw text -- `os: ~` is the
|
|
245
|
+
string `"~"`, `build_dir: 007` is the path `007`, `sku: 0x10` is `"0x10"`.
|
|
246
|
+
Resolving first and stringifying after gives `"None"`, `"7"` and `"16"`: three
|
|
247
|
+
different values than the oracle for the same document, and `build_dir` /
|
|
248
|
+
`output_artefact` / `firmware_path` are PATHS.
|
|
249
|
+
|
|
250
|
+
An explicit tag (`!!int 5`) still resolves, which is why `_scalar_str` below
|
|
251
|
+
keeps its coercion arm.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
class LiteralScalarLoader(yaml_module.SafeLoader):
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
LiteralScalarLoader.yaml_implicit_resolvers = {}
|
|
258
|
+
return LiteralScalarLoader
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _import_yaml():
|
|
262
|
+
"""PyYAML, or a [`SystemManifestError`] naming why it isn't available --
|
|
263
|
+
the one import guard [`load_yaml_document`] and [`serialize_system_manifest_raw`]
|
|
264
|
+
both funnel through, so a frozen `tan` built from a stale venv reports the
|
|
265
|
+
SAME coded envelope on either the read or the write path rather than one
|
|
266
|
+
of them tracebacking."""
|
|
267
|
+
try:
|
|
268
|
+
import yaml # noqa: PLC0415 (declared base dep; imported where needed)
|
|
269
|
+
except ImportError as err:
|
|
270
|
+
raise _parse_error(
|
|
271
|
+
f"no YAML parser available ({err}); install PyYAML "
|
|
272
|
+
"(`pip install pyyaml`) so tan can read system-manifest.yaml"
|
|
273
|
+
) from err
|
|
274
|
+
return yaml
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def load_yaml_document(text: str, *, literal_scalars: bool = False) -> Any:
|
|
278
|
+
"""Parse a manifest document, with PyYAML's absence and every parse failure
|
|
279
|
+
funnelled into [`SystemManifestError`].
|
|
280
|
+
|
|
281
|
+
Two scalar policies, both required and neither PyYAML's default:
|
|
282
|
+
`literal_scalars` keeps every plain scalar as its raw text
|
|
283
|
+
([`_literal_scalar_loader`]); otherwise the YAML 1.2 core schema applies
|
|
284
|
+
([`_core_schema_loader`]). `yaml.safe_load` is used NOWHERE -- its YAML 1.1
|
|
285
|
+
resolution disagrees with serde_yaml on five shapes that reach the wire.
|
|
286
|
+
|
|
287
|
+
PyYAML IS a declared runtime dependency (`pyproject.toml` records why: a
|
|
288
|
+
frozen `tan` built without it has a `size`/`image` that can never read a
|
|
289
|
+
manifest). The import is still guarded, because a declared dependency is not
|
|
290
|
+
a present one -- a frozen artifact built from a stale venv, or a broken
|
|
291
|
+
source install, must still emit an envelope. The oracle cannot reach this
|
|
292
|
+
branch at all (serde_yaml is compiled in), so the code reused is
|
|
293
|
+
`*.manifest-invalid`: an existing spelling with an honest message beats
|
|
294
|
+
inventing one the extension has never heard of.
|
|
295
|
+
"""
|
|
296
|
+
yaml = _import_yaml()
|
|
297
|
+
try:
|
|
298
|
+
loader = (
|
|
299
|
+
_literal_scalar_loader(yaml)
|
|
300
|
+
if literal_scalars
|
|
301
|
+
else _core_schema_loader(yaml)
|
|
302
|
+
)
|
|
303
|
+
return yaml.load(text, Loader=loader) # noqa: S506 -- SafeLoader-derived
|
|
304
|
+
except Exception as err: # noqa: BLE001 -- any PyYAML failure is bad input
|
|
305
|
+
# Includes ConstructorError for an unhashable YAML key (a sequence or
|
|
306
|
+
# mapping used as a key), which the oracle tolerates on read and only
|
|
307
|
+
# fails on later, at serialize time. Both ends emit a coded envelope;
|
|
308
|
+
# only the code differs, and neither ever tracebacks.
|
|
309
|
+
raise _parse_error(str(err).replace("\n", " ")) from err
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# The string-typed fields of each modeled struct, split by whether the Rust models
|
|
313
|
+
# them `String` or `Option<String>`. Enumerated rather than sniffed because the
|
|
314
|
+
# CHECK is what makes this reader agree with serde on which documents are legal --
|
|
315
|
+
# and the SPLIT decides what a null means, which is not the same on both sides:
|
|
316
|
+
#
|
|
317
|
+
# `os: ~` `String` -> the string "~" (serde_yaml hands a
|
|
318
|
+
# `String` the scalar's RAW TEXT)
|
|
319
|
+
# `build_dir: ~` `Option<String>` -> ABSENT (`deserialize_option`
|
|
320
|
+
# sees the null first and yields `None`)
|
|
321
|
+
#
|
|
322
|
+
# Getting that backwards puts a slice's build dir at the literal path `~`, or
|
|
323
|
+
# loses a real `os` value. Both verified against the compiled binary.
|
|
324
|
+
#
|
|
325
|
+
# `flash_args` is a `serde_yaml::Value` in both structs and is deliberately absent
|
|
326
|
+
# from every tuple: it is opaque, and any shape is legal.
|
|
327
|
+
_SLICE_REQUIRED_STRINGS = ("core_id", "os", "status")
|
|
328
|
+
_SLICE_OPTIONAL_STRINGS = (
|
|
329
|
+
"app", "image", "machine", "board", "toolchain", "build_dir",
|
|
330
|
+
"output_artefact", "log_path", "reason", "flash_method",
|
|
331
|
+
)
|
|
332
|
+
_HELPER_REQUIRED_STRINGS = ("name", "chip")
|
|
333
|
+
_HELPER_OPTIONAL_STRINGS = ("firmware_path", "flash_method", "update_channel")
|
|
334
|
+
_IPC_REQUIRED_STRINGS = ("name", "kind")
|
|
335
|
+
_IPC_OPTIONAL_STRINGS = ("status", "reason")
|
|
336
|
+
_HW_INFO_REQUIRED_STRINGS = ("sku",)
|
|
337
|
+
_HW_INFO_OPTIONAL_STRINGS = ("som_hw_rev", "board_name", "board_hw_rev", "silicon")
|
|
338
|
+
|
|
339
|
+
#: Every root key the Rust models as a `Vec<_>`. Present-but-not-a-sequence is a
|
|
340
|
+
#: hard error, including `~`: unlike a string field, serde_yaml does NOT accept a
|
|
341
|
+
#: null there (verified against the binary -- `slices: ~` is refused while
|
|
342
|
+
#: `hw_info.sku: ~` is accepted).
|
|
343
|
+
_SEQUENCE_KEYS = ("slices", "ipc", "helper_mcus", "boot_order", "storage")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _scalar_str(value: Any, field: str, where: str) -> str:
|
|
347
|
+
"""A YAML scalar as the string serde_yaml produces for a `String` field.
|
|
348
|
+
|
|
349
|
+
Reached with the LITERAL tree (see [`_literal_scalar_loader`]), so a plain
|
|
350
|
+
scalar is already its raw text and this is a pass-through: `build_dir: 7` is
|
|
351
|
+
the path `7`, `os: ~` is the string `"~"`, `sku: 0x10` is `"0x10"` -- all
|
|
352
|
+
verified against the compiled binary, and all wrong if the resolved value is
|
|
353
|
+
stringified instead.
|
|
354
|
+
|
|
355
|
+
The coercion arm covers an EXPLICIT tag (`!!int 5`), which resolves even with
|
|
356
|
+
implicit resolution off. A sequence or mapping is refused, as serde does.
|
|
357
|
+
"""
|
|
358
|
+
if isinstance(value, str):
|
|
359
|
+
return value
|
|
360
|
+
if value is None:
|
|
361
|
+
return ""
|
|
362
|
+
if isinstance(value, bool):
|
|
363
|
+
return "true" if value else "false"
|
|
364
|
+
if isinstance(value, (int, float)):
|
|
365
|
+
return str(value)
|
|
366
|
+
raise _parse_error(
|
|
367
|
+
f"{where}.{field}: invalid type: {_serde_type_name(value)}, expected a string"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _coerce_strings(
|
|
372
|
+
entry: dict,
|
|
373
|
+
core: Any,
|
|
374
|
+
required: tuple[str, ...],
|
|
375
|
+
optional: tuple[str, ...],
|
|
376
|
+
where: str,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Validate + normalise every present string-typed field of `entry` in place.
|
|
379
|
+
|
|
380
|
+
`core` is the same mapping out of the CORE-schema tree, consulted for one
|
|
381
|
+
thing only: whether the scalar was a YAML null. An `Option<String>` field that
|
|
382
|
+
was is DELETED (serde yields `None`, i.e. absent); a plain `String` keeps its
|
|
383
|
+
raw text.
|
|
384
|
+
"""
|
|
385
|
+
core = core if isinstance(core, dict) else {}
|
|
386
|
+
for field in required:
|
|
387
|
+
if field in entry:
|
|
388
|
+
entry[field] = _scalar_str(entry[field], field, where)
|
|
389
|
+
for field in optional:
|
|
390
|
+
if field not in entry:
|
|
391
|
+
continue
|
|
392
|
+
if core.get(field, "") is None:
|
|
393
|
+
del entry[field]
|
|
394
|
+
continue
|
|
395
|
+
entry[field] = _scalar_str(entry[field], field, where)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _sequence(root: dict, key: str) -> list:
|
|
399
|
+
"""A root `Vec<_>` field: absent is `[]`, present-but-not-a-sequence is a hard
|
|
400
|
+
error (`~` included)."""
|
|
401
|
+
if key not in root:
|
|
402
|
+
return []
|
|
403
|
+
raw = root[key]
|
|
404
|
+
if not isinstance(raw, list):
|
|
405
|
+
raise _parse_error(
|
|
406
|
+
f"{key}: invalid type: {_serde_type_name(raw)}, expected a sequence"
|
|
407
|
+
)
|
|
408
|
+
return raw
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _entries(
|
|
412
|
+
root: dict,
|
|
413
|
+
core_root: Any,
|
|
414
|
+
key: str,
|
|
415
|
+
required_present: tuple[str, ...],
|
|
416
|
+
required_strings: tuple[str, ...],
|
|
417
|
+
optional_strings: tuple[str, ...],
|
|
418
|
+
) -> list[dict]:
|
|
419
|
+
"""One of the manifest's three lists of mappings, with every field that is
|
|
420
|
+
non-`Option` in the Rust struct enforced and every string-typed field
|
|
421
|
+
normalised. A non-list value, a non-mapping entry, or a missing required field
|
|
422
|
+
fails the WHOLE document -- serde does too, and reading half of it would make
|
|
423
|
+
`tan` act on a manifest the shipped binary rejects."""
|
|
424
|
+
core_entries = core_root.get(key) if isinstance(core_root, dict) else None
|
|
425
|
+
core_entries = core_entries if isinstance(core_entries, list) else []
|
|
426
|
+
out: list[dict] = []
|
|
427
|
+
for index, entry in enumerate(_sequence(root, key)):
|
|
428
|
+
where = f"{key}[{index}]"
|
|
429
|
+
if not isinstance(entry, dict):
|
|
430
|
+
raise _parse_error(
|
|
431
|
+
f"{where}: invalid type: {_serde_type_name(entry)}, expected a map"
|
|
432
|
+
)
|
|
433
|
+
for field in required_present:
|
|
434
|
+
if field not in entry:
|
|
435
|
+
raise _parse_error(f"{where}: missing field `{field}`")
|
|
436
|
+
core = core_entries[index] if index < len(core_entries) else {}
|
|
437
|
+
_coerce_strings(entry, core, required_strings, optional_strings, where)
|
|
438
|
+
out.append(entry)
|
|
439
|
+
return out
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class SystemManifest:
|
|
443
|
+
"""A parsed, version-guarded manifest over the LITERAL-scalar tree (see
|
|
444
|
+
[`_literal_scalar_loader`]). Entries stay raw `dict`s -- with their
|
|
445
|
+
string-typed fields validated in place -- because the fields this family reads
|
|
446
|
+
are few, and a typed projection is exactly what makes the Rust reader lossy on
|
|
447
|
+
write (`serialize_system_manifest`'s own warning)."""
|
|
448
|
+
|
|
449
|
+
__slots__ = ("root", "schema_version", "sku", "slices", "helper_mcus")
|
|
450
|
+
|
|
451
|
+
def __init__(self, root: dict, core_root: Any) -> None:
|
|
452
|
+
self.root = root
|
|
453
|
+
# From the CORE tree: it is a `u32` in the Rust, and the literal tree holds
|
|
454
|
+
# the scalar's text (`"1"`, not `1`).
|
|
455
|
+
self.schema_version = core_root["schema_version"]
|
|
456
|
+
if "hw_info" in root:
|
|
457
|
+
hw_info = root["hw_info"]
|
|
458
|
+
if not isinstance(hw_info, dict):
|
|
459
|
+
raise _parse_error(
|
|
460
|
+
f"hw_info: invalid type: {_serde_type_name(hw_info)}, "
|
|
461
|
+
"expected struct HwInfo"
|
|
462
|
+
)
|
|
463
|
+
_coerce_strings(
|
|
464
|
+
hw_info,
|
|
465
|
+
core_root.get("hw_info") if isinstance(core_root, dict) else None,
|
|
466
|
+
_HW_INFO_REQUIRED_STRINGS,
|
|
467
|
+
_HW_INFO_OPTIONAL_STRINGS,
|
|
468
|
+
"hw_info",
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
hw_info = {}
|
|
472
|
+
if "generated_by" in root:
|
|
473
|
+
root["generated_by"] = _scalar_str(
|
|
474
|
+
root["generated_by"], "generated_by", "SystemManifest"
|
|
475
|
+
)
|
|
476
|
+
self.sku: str = hw_info.get("sku") or ""
|
|
477
|
+
self.slices = _entries(
|
|
478
|
+
root,
|
|
479
|
+
core_root,
|
|
480
|
+
"slices",
|
|
481
|
+
("core_id", "os"),
|
|
482
|
+
_SLICE_REQUIRED_STRINGS,
|
|
483
|
+
_SLICE_OPTIONAL_STRINGS,
|
|
484
|
+
)
|
|
485
|
+
self.helper_mcus = _entries(
|
|
486
|
+
root,
|
|
487
|
+
core_root,
|
|
488
|
+
"helper_mcus",
|
|
489
|
+
("name", "chip"),
|
|
490
|
+
_HELPER_REQUIRED_STRINGS,
|
|
491
|
+
_HELPER_OPTIONAL_STRINGS,
|
|
492
|
+
)
|
|
493
|
+
# `ipc[]` and the remaining sequence fields are validated but not otherwise
|
|
494
|
+
# read here: a document the oracle rejects must fail this parse too, or
|
|
495
|
+
# `tan image` would bundle a manifest `tan flash` then refuses.
|
|
496
|
+
ipc = _entries(
|
|
497
|
+
root,
|
|
498
|
+
core_root,
|
|
499
|
+
"ipc",
|
|
500
|
+
("name", "kind"),
|
|
501
|
+
_IPC_REQUIRED_STRINGS,
|
|
502
|
+
_IPC_OPTIONAL_STRINGS,
|
|
503
|
+
)
|
|
504
|
+
for entry in ipc:
|
|
505
|
+
if "endpoints" in entry:
|
|
506
|
+
raw = entry["endpoints"]
|
|
507
|
+
if not isinstance(raw, list):
|
|
508
|
+
raise _parse_error(
|
|
509
|
+
f"ipc.endpoints: invalid type: {_serde_type_name(raw)}, "
|
|
510
|
+
"expected a sequence"
|
|
511
|
+
)
|
|
512
|
+
entry["endpoints"] = [
|
|
513
|
+
_scalar_str(e, "endpoints", "ipc") for e in raw
|
|
514
|
+
]
|
|
515
|
+
for key in _SEQUENCE_KEYS:
|
|
516
|
+
_sequence(root, key)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def parse_system_manifest(text: str) -> SystemManifest:
|
|
520
|
+
"""Parse + version-guard a `system-manifest.yaml` document. No IO.
|
|
521
|
+
|
|
522
|
+
Order matters and mirrors serde: the document's SHAPE is checked before its
|
|
523
|
+
`schema_version` value, because serde fails deserialization outright on a
|
|
524
|
+
malformed `slices[]` and never reaches the version comparison.
|
|
525
|
+
|
|
526
|
+
TWO parses of the same text, deliberately, because serde resolves a scalar
|
|
527
|
+
against its TARGET type and no single Python tree can stand in for all of them:
|
|
528
|
+
* `schema_version` is a `u32`, so its check needs real type resolution --
|
|
529
|
+
`schema_version: '1'` is a hard error and `schema_version: 1` is not, and a
|
|
530
|
+
literal-scalar tree cannot tell those apart;
|
|
531
|
+
* a `String` field takes the scalar's RAW TEXT ([`_literal_scalar_loader`]);
|
|
532
|
+
* an `Option<String>` field takes `None` for a null, which only the resolving
|
|
533
|
+
tree can identify.
|
|
534
|
+
Both trees come from the same document, so they have the same structure and the
|
|
535
|
+
shape checks are identical whichever one runs them.
|
|
536
|
+
"""
|
|
537
|
+
core = load_yaml_document(text)
|
|
538
|
+
root = load_yaml_document(text, literal_scalars=True)
|
|
539
|
+
if not isinstance(root, dict):
|
|
540
|
+
raise _parse_error(
|
|
541
|
+
f"invalid type: {_serde_type_name(core)}, expected struct SystemManifest"
|
|
542
|
+
)
|
|
543
|
+
if "schema_version" not in root:
|
|
544
|
+
raise _parse_error("missing field `schema_version`")
|
|
545
|
+
version = core["schema_version"] if isinstance(core, dict) else None
|
|
546
|
+
if isinstance(version, bool) or not isinstance(version, int) or version < 0:
|
|
547
|
+
raise _parse_error(
|
|
548
|
+
f"invalid type: {_serde_type_name(version)}, expected u32"
|
|
549
|
+
)
|
|
550
|
+
manifest = SystemManifest(root, core)
|
|
551
|
+
# AFTER the shape check, and comparing the full value -- a `schema_version`
|
|
552
|
+
# of 2**32 + 1 is congruent to 1 mod 2**32, so a truncating guard would
|
|
553
|
+
# wrongly accept it (`parse_system_manifest_raw_rejects_schema_version_
|
|
554
|
+
# beyond_u32` in the oracle).
|
|
555
|
+
if version != SYSTEM_MANIFEST_SCHEMA_VERSION:
|
|
556
|
+
raise _unsupported_version_error(version)
|
|
557
|
+
return manifest
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# --------------------------------------------------------------------------
|
|
561
|
+
# The write path: overlay this run's per-slice outcomes onto a freshly
|
|
562
|
+
# emitted system-manifest and serialize it back out. Port of the RAW half of
|
|
563
|
+
# `tan_core::system_manifest` (`parse_system_manifest_raw` /
|
|
564
|
+
# `overlay_run_results_raw` / `serialize_system_manifest_raw`) -- the half
|
|
565
|
+
# `crates/tan-cli/src/commands/build/execute/manifest.rs` uses for the
|
|
566
|
+
# post-build rewrite, never the typed `SystemManifest`/`serialize_system_
|
|
567
|
+
# manifest` pair (the Rust module doc explains why: the typed struct there
|
|
568
|
+
# only models the fields it reads, so re-serializing it silently drops any
|
|
569
|
+
# field it doesn't model -- an rpmsg `IpcLink` carve-out, `hw_info.eeprom`,
|
|
570
|
+
# anything a newer SDK adds).
|
|
571
|
+
#
|
|
572
|
+
# That specific defect does not reproduce here: [`SystemManifest`]'s own
|
|
573
|
+
# `slices`/`helper_mcus` entries above are already plain `dict`s carrying
|
|
574
|
+
# every key the document had (`_entries` normalises known string fields IN
|
|
575
|
+
# PLACE and never drops the rest) -- so a plain re-dump of `SystemManifest.root`
|
|
576
|
+
# would not lose an additive field either. This module still keeps a distinct
|
|
577
|
+
# `parse_system_manifest_raw` entry point rather than reusing `parse_system_
|
|
578
|
+
# manifest`, for a different reason: the typed reader does TWO parses of the
|
|
579
|
+
# same text (a literal-scalar tree for `String` fields, a core-schema tree for
|
|
580
|
+
# everything else -- see `parse_system_manifest`'s own docstring) specifically
|
|
581
|
+
# to interpret an ARBITRARY, possibly hand-authored document the way serde_yaml
|
|
582
|
+
# would. The write path's input is never that: it is the SDK's own YAML,
|
|
583
|
+
# freshly emitted and immediately rewritten in the same step, so the single
|
|
584
|
+
# core-schema parse `load_yaml_document` already performs is sufficient and
|
|
585
|
+
# this avoids reasoning about which of the two trees a write should mutate.
|
|
586
|
+
# --------------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@dataclass(frozen=True)
|
|
590
|
+
class SliceRunResult:
|
|
591
|
+
"""One slice's post-build outcome, as the executor reports it -- the
|
|
592
|
+
write-side counterpart of a manifest `slices[]` entry. Port of
|
|
593
|
+
`tan_core::system_manifest::SliceRunResult` (a 5-tuple there; a dataclass
|
|
594
|
+
here for readability -- same fields, same order, same "`None` leaves the
|
|
595
|
+
plan-time value untouched" meaning for every optional one). Pass an
|
|
596
|
+
EXPLICIT empty string (never `None`) to actively CLEAR a stale
|
|
597
|
+
`output_artefact`/`build_dir` for a slice this run knows never dispatched
|
|
598
|
+
(mirrors the Rust oracle's `Some(String::new())` convention for a demoted
|
|
599
|
+
slice)."""
|
|
600
|
+
|
|
601
|
+
core_id: str
|
|
602
|
+
status: str
|
|
603
|
+
output_artefact: str | None = None
|
|
604
|
+
build_dir: str | None = None
|
|
605
|
+
reason: str | None = None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def parse_system_manifest_raw(text: str) -> dict:
|
|
609
|
+
"""Parse a `system-manifest.yaml` document into a raw mapping (str-in /
|
|
610
|
+
dict-out) -- the entry point the write path uses instead of
|
|
611
|
+
[`parse_system_manifest`]; see the section docstring above for why a
|
|
612
|
+
single core-schema parse is enough here. Still version-guarded
|
|
613
|
+
identically to the typed reader -- but with a DIFFERENT, collapsed error
|
|
614
|
+
message on a missing/malformed `schema_version`: the oracle's raw parser
|
|
615
|
+
(`tan_core::system_manifest::parse_system_manifest_raw`,
|
|
616
|
+
system_manifest.rs:294-310) extracts it with a single
|
|
617
|
+
`.get("schema_version").and_then(as_u64)` and reports every failure --
|
|
618
|
+
a missing key, a non-mapping root, a non-numeric, negative, or
|
|
619
|
+
out-of-`u64`-range value -- as the one string "missing or non-numeric
|
|
620
|
+
schema_version", not the typed reader's two-branch "missing field" /
|
|
621
|
+
"invalid type" pair. This string reaches a build's `issues[].message`
|
|
622
|
+
verbatim (a failed post-build manifest write folds it in), so the exact
|
|
623
|
+
wording is part of the wire contract, not cosmetic."""
|
|
624
|
+
root = load_yaml_document(text)
|
|
625
|
+
version = root.get("schema_version") if isinstance(root, dict) else None
|
|
626
|
+
if (
|
|
627
|
+
isinstance(version, bool)
|
|
628
|
+
or not isinstance(version, int)
|
|
629
|
+
or not (0 <= version <= _SERDE_INT_MAX)
|
|
630
|
+
):
|
|
631
|
+
raise _parse_error("missing or non-numeric schema_version")
|
|
632
|
+
if version != SYSTEM_MANIFEST_SCHEMA_VERSION:
|
|
633
|
+
raise _unsupported_version_error(version)
|
|
634
|
+
return root
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def overlay_run_results_raw(raw: dict, results: Sequence[SliceRunResult]) -> None:
|
|
638
|
+
"""Overlay this run's per-slice outcomes onto the RAW manifest mapping (as
|
|
639
|
+
returned by [`parse_system_manifest_raw`]) so the written `system-
|
|
640
|
+
manifest.yaml` reflects what actually built. Slices are matched by
|
|
641
|
+
`core_id`; a slice with no matching result (e.g. an `off` core) is left
|
|
642
|
+
untouched. `output_artefact`/`build_dir`/`reason` are each written only
|
|
643
|
+
when the result carries one -- a `None` never wipes a plan-time value.
|
|
644
|
+
Port of `tan_core::system_manifest::overlay_run_results_raw`. Pure: no IO,
|
|
645
|
+
mutates `raw` in place."""
|
|
646
|
+
slices = raw.get("slices")
|
|
647
|
+
if not isinstance(slices, list):
|
|
648
|
+
return
|
|
649
|
+
# FIRST match wins, not last -- `results.iter().find(...)` in the oracle
|
|
650
|
+
# (system_manifest.rs:332-333). Unreachable with today's callers (one
|
|
651
|
+
# result per slice), but `setdefault` keeps a silent semantic flip out of
|
|
652
|
+
# a ported pure function rather than relying on "no caller ever hits it".
|
|
653
|
+
by_core: dict[str, SliceRunResult] = {}
|
|
654
|
+
for r in results:
|
|
655
|
+
by_core.setdefault(r.core_id, r)
|
|
656
|
+
for entry in slices:
|
|
657
|
+
if not isinstance(entry, dict):
|
|
658
|
+
continue
|
|
659
|
+
result = by_core.get(entry.get("core_id"))
|
|
660
|
+
if result is None:
|
|
661
|
+
continue
|
|
662
|
+
entry["status"] = result.status
|
|
663
|
+
if result.output_artefact is not None:
|
|
664
|
+
entry["output_artefact"] = result.output_artefact
|
|
665
|
+
if result.build_dir is not None:
|
|
666
|
+
entry["build_dir"] = result.build_dir
|
|
667
|
+
if result.reason is not None:
|
|
668
|
+
entry["reason"] = result.reason
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def serialize_system_manifest_raw(raw: dict) -> str:
|
|
672
|
+
"""Serialize the RAW manifest mapping (as returned by
|
|
673
|
+
[`parse_system_manifest_raw`], possibly overlaid by
|
|
674
|
+
[`overlay_run_results_raw`]) back to YAML. Port of `tan_core::
|
|
675
|
+
system_manifest::serialize_system_manifest_raw`."""
|
|
676
|
+
yaml = _import_yaml()
|
|
677
|
+
return yaml.safe_dump(raw, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
#: `serde_yaml::Value`'s integer range: `i64::MIN ..= u64::MAX`. A YAML integer
|
|
681
|
+
#: outside it cannot be represented, so the WHOLE `Value` parse fails and
|
|
682
|
+
#: `raw_passthrough` yields its defaults -- silently dropping `hw_info` and
|
|
683
|
+
#: `boot_order`. Reproduced rather than "improved": Python's unbounded int would
|
|
684
|
+
#: otherwise put a number in `bundle-manifest.json` that no `JSON.parse` consumer
|
|
685
|
+
#: can hold without losing digits, which is a worse answer than the oracle's.
|
|
686
|
+
_SERDE_INT_MIN = -(2**63)
|
|
687
|
+
_SERDE_INT_MAX = 2**64 - 1
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _representable_by_serde_yaml(node: Any) -> bool:
|
|
691
|
+
"""Whether serde_yaml could hold this whole tree in a `Value` -- an integer
|
|
692
|
+
bound only.
|
|
693
|
+
|
|
694
|
+
A non-finite FLOAT is deliberately representable here and must stay that
|
|
695
|
+
way (tan-cli#387). `.inf` / `-.inf` / `.nan` are perfectly good
|
|
696
|
+
`serde_yaml::Value::Number`s; returning False for them would make
|
|
697
|
+
`raw_passthrough` yield its empty defaults and drop the WHOLE `hw_info` and
|
|
698
|
+
`boot_order` on the floor, where the oracle emits every key with the
|
|
699
|
+
non-finite ones as `null`. The JSON side of that problem is fixed where
|
|
700
|
+
JSON is written (`tan.envelope.json_safe_floats`), not by refusing the
|
|
701
|
+
document.
|
|
702
|
+
"""
|
|
703
|
+
if isinstance(node, bool):
|
|
704
|
+
return True
|
|
705
|
+
if isinstance(node, int):
|
|
706
|
+
return _SERDE_INT_MIN <= node <= _SERDE_INT_MAX
|
|
707
|
+
if isinstance(node, dict):
|
|
708
|
+
return all(
|
|
709
|
+
_representable_by_serde_yaml(k) and _representable_by_serde_yaml(v)
|
|
710
|
+
for k, v in node.items()
|
|
711
|
+
)
|
|
712
|
+
if isinstance(node, (list, tuple)):
|
|
713
|
+
return all(_representable_by_serde_yaml(v) for v in node)
|
|
714
|
+
return True
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def raw_passthrough(text: str) -> tuple[dict, list]:
|
|
718
|
+
"""The manifest's `hw_info` (default `{}`) + `boot_order` (default `[]`),
|
|
719
|
+
straight off the YAML -- bypassing any typed projection so additive schema
|
|
720
|
+
keys (`hw_info.eeprom`, an rpmsg carve-out, anything a newer SDK adds) pass
|
|
721
|
+
through unchanged into the bundle manifest.
|
|
722
|
+
|
|
723
|
+
Never fails: unparseable or non-mapping input yields the empty defaults,
|
|
724
|
+
matching `image_bundle::raw_passthrough`. That is what makes it safe to call
|
|
725
|
+
on the error path with an empty string.
|
|
726
|
+
"""
|
|
727
|
+
try:
|
|
728
|
+
root = load_yaml_document(text)
|
|
729
|
+
except SystemManifestError:
|
|
730
|
+
root = None
|
|
731
|
+
if not isinstance(root, dict) or not _representable_by_serde_yaml(root):
|
|
732
|
+
return {}, []
|
|
733
|
+
hw_info = root.get("hw_info")
|
|
734
|
+
boot_order = root.get("boot_order")
|
|
735
|
+
return (
|
|
736
|
+
hw_info if isinstance(hw_info, dict) else {},
|
|
737
|
+
list(boot_order) if isinstance(boot_order, list) else [],
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# --------------------------------------------------------------------------
|
|
742
|
+
# The two path rules `tan image` and `tan size` share.
|
|
743
|
+
# --------------------------------------------------------------------------
|
|
744
|
+
#
|
|
745
|
+
# Paths here are STRINGS joined with `os.path.join`, not `pathlib.Path`.
|
|
746
|
+
# `Path("C:/a/b") / "c"` re-renders the whole path in the platform separator,
|
|
747
|
+
# and Rust's `PathBuf::join` does not: it appends and leaves what was already
|
|
748
|
+
# there alone. Both values end up in `data.slices[].notes[]` and in issue
|
|
749
|
+
# messages, so the difference is visible in the machine contract -- a
|
|
750
|
+
# `/`-authored workspace root keeps its slashes with a `\` tail, exactly as the
|
|
751
|
+
# shipped binary reports it.
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def anchor_under(path: str, build_root: str) -> str:
|
|
755
|
+
"""A manifest path string as an absolute path: absolute as-is, relative
|
|
756
|
+
under `build_root` (where the manifest itself lives). Verbatim
|
|
757
|
+
`size.rs::anchor_under` / `image.rs`'s inline twin."""
|
|
758
|
+
return path if os.path.isabs(path) else os.path.join(build_root, path)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def slice_build_dir(slice_: dict, build_root: str) -> str | None:
|
|
762
|
+
"""A slice's recorded build directory, or `None` when the manifest declares
|
|
763
|
+
none (a plan-time manifest, before any build ran).
|
|
764
|
+
|
|
765
|
+
`tan image` treats `None` as "skip this slice"; `tan size` falls back to the
|
|
766
|
+
canonical name via [`slice_build_dir_or_default`]. Both anchor the same way,
|
|
767
|
+
which is why the rule lives here once.
|
|
768
|
+
"""
|
|
769
|
+
raw = slice_.get("build_dir")
|
|
770
|
+
if not isinstance(raw, str) or raw == "":
|
|
771
|
+
return None
|
|
772
|
+
return anchor_under(raw, build_root)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def slice_build_dir_or_default(slice_: dict, build_root: str) -> str:
|
|
776
|
+
"""[`slice_build_dir`], falling back to the canonical `<core_id>-<os>`."""
|
|
777
|
+
recorded = slice_build_dir(slice_, build_root)
|
|
778
|
+
if recorded is not None:
|
|
779
|
+
return recorded
|
|
780
|
+
return os.path.join(build_root, f"{slice_.get('core_id')}-{slice_.get('os')}")
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _nested_variants(base: str) -> list[str]:
|
|
784
|
+
"""`base`, then `base/build` -- the I-18 pair. `west build` is emitted with
|
|
785
|
+
NO `-d`, so its tree lands at `<slice-cwd>/build/`.
|
|
786
|
+
|
|
787
|
+
Before tan-cli#560 (alp-sdk d00dbdc1), the plan's `artifacts` block still
|
|
788
|
+
named the un-nested `<slice-cwd>/...`, so this reconciled the off-by-one
|
|
789
|
+
on every read. As of that pin the planner's own in-process output already
|
|
790
|
+
carries the nested path; both are still probed, un-nested first, because
|
|
791
|
+
an older cached plan, the alp-sdk subprocess fallback pinned to a stale
|
|
792
|
+
SDK, or a hand-authored manifest can still carry the un-nested spelling.
|
|
793
|
+
|
|
794
|
+
The un-nested path is tried FIRST, always: for every input where the oracle
|
|
795
|
+
finds something, this finds the identical thing, and the nested probe only
|
|
796
|
+
ever converts an oracle `not-built` into a real measurement. See
|
|
797
|
+
`resolve_zephyr_artefact` (`crates/tan-cli/src/commands/build/execute/
|
|
798
|
+
manifest.rs`), which is where the shipped binary resolves this -- at BUILD
|
|
799
|
+
time, by recording the nested paths into the manifest it rewrites. This
|
|
800
|
+
port's `tan build` does not yet write that manifest, so the reconciliation
|
|
801
|
+
has to happen on the read side or every slice of a real nested build reports
|
|
802
|
+
`not-built`.
|
|
803
|
+
"""
|
|
804
|
+
return [base, os.path.join(base, WEST_NESTED_DIR)]
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def slice_elf_candidates(slice_: dict, build_root: str) -> list[str]:
|
|
808
|
+
"""Where this slice's `zephyr.elf` may be, best first.
|
|
809
|
+
|
|
810
|
+
A recorded `output_artefact` is authoritative and used ALONE -- `tan build`
|
|
811
|
+
only ever writes it already-resolved, so second-guessing it would ignore a
|
|
812
|
+
deliberate `-d`/`--build-dir` override.
|
|
813
|
+
"""
|
|
814
|
+
artefact = slice_.get("output_artefact")
|
|
815
|
+
if isinstance(artefact, str) and artefact != "":
|
|
816
|
+
return [anchor_under(artefact, build_root)]
|
|
817
|
+
build_dir = slice_build_dir_or_default(slice_, build_root)
|
|
818
|
+
return [
|
|
819
|
+
os.path.join(base, "zephyr", "zephyr.elf")
|
|
820
|
+
for base in _nested_variants(build_dir)
|
|
821
|
+
]
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def slice_footprint_dirs(slice_: dict, build_root: str) -> list[str]:
|
|
825
|
+
"""Where this slice's `rom.json` + `ram.json` may be, best first (same I-18
|
|
826
|
+
pair as [`slice_elf_candidates`])."""
|
|
827
|
+
return _nested_variants(slice_build_dir_or_default(slice_, build_root))
|