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/core/build_plan.py
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The build-plan CONSUMER model (alp-sdk metadata/schemas/build-plan-v1.schema.json).
|
|
3
|
+
|
|
4
|
+
Strict producer / tolerant consumer: the required keys are enforced, the
|
|
5
|
+
optional-but-always-emitted ones default cleanly, and an unsupported
|
|
6
|
+
schemaVersion is REFUSED rather than silently hand-ported around."""
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from tan.core.plan_exec import ExecutionPolicy, PolicyAction
|
|
14
|
+
|
|
15
|
+
SUPPORTED_SCHEMA_VERSION = 1
|
|
16
|
+
|
|
17
|
+
#: The oracle types `schemaVersion` as a Rust `u32`, so the accepted domain is
|
|
18
|
+
#: exactly `[0, 2**32 - 1]` -- established by RUNNING it: `-1` and
|
|
19
|
+
#: `4294967296` are refused as `invalid value: integer ..., expected u32`,
|
|
20
|
+
#: while `4294967295` gets through the type check and lands on the
|
|
21
|
+
#: version-skew message instead.
|
|
22
|
+
_U32_MAX = 0xFFFFFFFF
|
|
23
|
+
|
|
24
|
+
_REQUIRED_TOP = (
|
|
25
|
+
"schemaVersion", "generatedBy", "boardYaml", "sku",
|
|
26
|
+
"buildRoot", "slices", "sharedArtefacts", "warnings",
|
|
27
|
+
)
|
|
28
|
+
_REQUIRED_SLICE = (
|
|
29
|
+
"coreId", "backend", "buildDir", "appDir", "configArtefacts",
|
|
30
|
+
"toolchain", "artifacts", "debug", "command", "env", "envAppendPath",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# The scalar fields Rust models as `String` / `Option<String>`. Typed here for
|
|
34
|
+
# the same reason serde types them there: unvalidated, a non-string escapes
|
|
35
|
+
# parse entirely and only detonates much later and far away -- `boardYaml` as
|
|
36
|
+
# an int reaches `str.replace` in the token pass, `sdkCommit` reaches
|
|
37
|
+
# `.strip()`, an unhashable `backend` reaches `in KNOWN_BACKENDS` -- as a bare
|
|
38
|
+
# AttributeError/TypeError in the executor's catch-all, i.e. reported as a tan
|
|
39
|
+
# bug (exit 5) when the truth is a malformed plan (`build.plan-invalid`,
|
|
40
|
+
# exit 1).
|
|
41
|
+
_REQUIRED_STR_TOP = ("generatedBy", "boardYaml", "sku", "buildRoot")
|
|
42
|
+
_OPTIONAL_STR_TOP = ("sdkVersion", "sdkCommit", "planPathMode")
|
|
43
|
+
_REQUIRED_STR_SLICE = ("coreId", "backend", "buildDir")
|
|
44
|
+
_OPTIONAL_STR_SLICE = ("appDir",)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PlanParseError(Exception):
|
|
48
|
+
def __init__(self, code: str, message: str) -> None:
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
self.code = code
|
|
51
|
+
self.message = message
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SliceCommand:
|
|
56
|
+
tool: str
|
|
57
|
+
args: list[str]
|
|
58
|
+
cwd: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Slice:
|
|
63
|
+
core_id: str
|
|
64
|
+
backend: str
|
|
65
|
+
build_dir: str
|
|
66
|
+
app_dir: str | None
|
|
67
|
+
config_artefacts: list[dict[str, Any]]
|
|
68
|
+
toolchain: Any
|
|
69
|
+
artifacts: dict[str, Any]
|
|
70
|
+
debug: dict[str, Any]
|
|
71
|
+
command: SliceCommand | None
|
|
72
|
+
env: dict[str, str]
|
|
73
|
+
env_append_path: dict[str, list[str]]
|
|
74
|
+
#: Steps that MUST run, in order, after `command` succeeds (alp-sdk
|
|
75
|
+
#: #1344, tan-cli#550). `west build` and `bitbake` configure AND build in
|
|
76
|
+
#: one invocation and carry none; a baremetal slice's `cmake -S ... -B .`
|
|
77
|
+
#: only CONFIGURES, so its `cmake --build .` arrives here.
|
|
78
|
+
#:
|
|
79
|
+
#: Defaulted rather than required -- the strict-producer/tolerant-consumer
|
|
80
|
+
#: rule this module's own docstring states. A plan emitted by an alp-sdk
|
|
81
|
+
#: predating #1344 has no `postCommands` key at all, and refusing it would
|
|
82
|
+
#: turn "your SDK is older than your tan" into `build.plan-invalid` for
|
|
83
|
+
#: every slice, including the zephyr/yocto ones whose correct value is
|
|
84
|
+
#: `[]` anyway.
|
|
85
|
+
#:
|
|
86
|
+
#: A tuple, not a list: this dataclass is `frozen=True`, and a mutable
|
|
87
|
+
#: default on a frozen dataclass is a `ValueError` at class-definition
|
|
88
|
+
#: time. Every consumer only iterates it.
|
|
89
|
+
post_commands: tuple[SliceCommand, ...] = ()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class BuildPlan:
|
|
94
|
+
schema_version: int
|
|
95
|
+
generated_by: str
|
|
96
|
+
board_yaml: str
|
|
97
|
+
sku: str
|
|
98
|
+
build_root: str
|
|
99
|
+
slices: list[Slice]
|
|
100
|
+
shared_artefacts: list[dict[str, Any]]
|
|
101
|
+
warnings: list[dict[str, Any]]
|
|
102
|
+
sdk_version: str | None = None
|
|
103
|
+
sdk_commit: str | None = None
|
|
104
|
+
plan_path_mode: str | None = None
|
|
105
|
+
execution_policy: ExecutionPolicy | None = None
|
|
106
|
+
#: Top-level `deferredPlaceholders` (alp-sdk#2696): bare names of `${NAME}`
|
|
107
|
+
#: values the planner left unresolved on purpose. `None` means the plan
|
|
108
|
+
#: carries NO such key (older planner); a tuple (possibly empty) means it
|
|
109
|
+
#: does, and is then authoritative. A malformed value parses as `()` with
|
|
110
|
+
#: the reason in `deferred_placeholders_problem` -- never a licence to
|
|
111
|
+
#: exempt anything.
|
|
112
|
+
deferred_placeholders: tuple[str, ...] | None = None
|
|
113
|
+
deferred_placeholders_problem: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
#: A deferred-placeholder name, as alp-sdk#2696 spells it.
|
|
117
|
+
DEFERRED_PLACEHOLDER_NAME = re.compile(r"[A-Z][A-Z0-9_]*")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _deferred_placeholders(raw: dict[str, Any]) -> tuple[tuple[str, ...] | None, str | None]:
|
|
121
|
+
"""Parse the optional `deferredPlaceholders` list. Absent or `null` -> `(None, None)`.
|
|
122
|
+
Present but malformed -> `((), reason)`: the key IS there, so the plan is
|
|
123
|
+
authoritative and lists nothing, which exempts nothing."""
|
|
124
|
+
# `null` is ABSENT, like `sdkVersion`/`sdkCommit`: a tolerant consumer.
|
|
125
|
+
if raw.get("deferredPlaceholders") is None:
|
|
126
|
+
return None, None
|
|
127
|
+
value = raw["deferredPlaceholders"]
|
|
128
|
+
if not isinstance(value, list):
|
|
129
|
+
return (), "`deferredPlaceholders` must be a list of placeholder names"
|
|
130
|
+
for i, name in enumerate(value):
|
|
131
|
+
if not isinstance(name, str) or not DEFERRED_PLACEHOLDER_NAME.fullmatch(name):
|
|
132
|
+
return (), (
|
|
133
|
+
f"`deferredPlaceholders[{i}]` must be a string matching "
|
|
134
|
+
f"`^[A-Z][A-Z0-9_]*$`"
|
|
135
|
+
)
|
|
136
|
+
# The schema says unique; a duplicate is harmless, so it is folded, not refused.
|
|
137
|
+
return tuple(dict.fromkeys(value)), None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _require_strings(
|
|
141
|
+
raw: dict[str, Any], required: tuple[str, ...], optional: tuple[str, ...], context: str
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Type the scalar fields at parse time, where a mistyped one is still a
|
|
144
|
+
plan defect rather than a crash somewhere downstream. `context` is `""` at
|
|
145
|
+
the plan root and `slices[N]` inside a slice, so the message names the
|
|
146
|
+
field the way the plan spells it."""
|
|
147
|
+
prefix = f"{context}." if context else ""
|
|
148
|
+
for key in required:
|
|
149
|
+
if not isinstance(raw[key], str):
|
|
150
|
+
raise PlanParseError("build.plan-invalid", f"`{prefix}{key}` must be a string")
|
|
151
|
+
for key in optional:
|
|
152
|
+
if raw.get(key) is not None and not isinstance(raw[key], str):
|
|
153
|
+
raise PlanParseError("build.plan-invalid", f"`{prefix}{key}` must be a string or null")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _action(raw: Any, context: str) -> PolicyAction | None:
|
|
157
|
+
"""One `executionPolicy` entry, refused as a coded parse error rather than
|
|
158
|
+
a bare `ValueError` escaping the enum constructor.
|
|
159
|
+
|
|
160
|
+
This is a FORWARD-COMPATIBILITY trap, not a typo guard: the day the SDK
|
|
161
|
+
adds a fourth `executionPolicy` action, every plan carrying it hit
|
|
162
|
+
`PolicyAction(raw)` -> `ValueError` -> the caller's catch-all, so every
|
|
163
|
+
`tan build` against that SDK reported `build.internal-failure` at exit 5
|
|
164
|
+
-- a tan bug -- for a plan this binary merely does not understand yet.
|
|
165
|
+
Rust's serde refuses it as `build.plan-invalid` at exit 1; so does this."""
|
|
166
|
+
if raw is None:
|
|
167
|
+
return None
|
|
168
|
+
try:
|
|
169
|
+
return PolicyAction(raw)
|
|
170
|
+
except ValueError as err:
|
|
171
|
+
known = ", ".join(f"`{a.value}`" for a in PolicyAction)
|
|
172
|
+
raise PlanParseError(
|
|
173
|
+
"build.plan-invalid",
|
|
174
|
+
f"`{context}` must be one of {known} -- got `{raw}`. Upgrade tan if the SDK "
|
|
175
|
+
f"has added a newer execution policy.",
|
|
176
|
+
) from err
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _policy(raw: Any) -> ExecutionPolicy | None:
|
|
180
|
+
if raw is None:
|
|
181
|
+
return None
|
|
182
|
+
if not isinstance(raw, dict):
|
|
183
|
+
raise PlanParseError("build.plan-invalid", "`executionPolicy` must be an object or null")
|
|
184
|
+
return ExecutionPolicy(
|
|
185
|
+
unknown_backend=_action(raw.get("unknownBackend"), "executionPolicy.unknownBackend"),
|
|
186
|
+
missing_tool=_action(raw.get("missingTool"), "executionPolicy.missingTool"),
|
|
187
|
+
null_command=_action(raw.get("nullCommand"), "executionPolicy.nullCommand"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _artefacts(raw: Any, context: str) -> list[dict[str, Any]]:
|
|
192
|
+
"""Validate a `configArtefacts`/`sharedArtefacts` list at parse time
|
|
193
|
+
(Rust's serde rejects a malformed `GeneratedFile` here too) so a
|
|
194
|
+
malformed or null-valued entry fails as a coded `PlanParseError` instead
|
|
195
|
+
of an uncaught `KeyError`/`AttributeError` reaching `plan_tokens`, which
|
|
196
|
+
indexes `path`/`contents` unguarded."""
|
|
197
|
+
if not isinstance(raw, list):
|
|
198
|
+
raise PlanParseError("build.plan-invalid", f"`{context}` must be a list")
|
|
199
|
+
for i, art in enumerate(raw):
|
|
200
|
+
if (
|
|
201
|
+
not isinstance(art, dict)
|
|
202
|
+
or not isinstance(art.get("path"), str)
|
|
203
|
+
or not isinstance(art.get("contents"), str)
|
|
204
|
+
):
|
|
205
|
+
raise PlanParseError(
|
|
206
|
+
"build.plan-invalid",
|
|
207
|
+
f"`{context}[{i}]` must be an object with string `path` and `contents` fields",
|
|
208
|
+
)
|
|
209
|
+
return raw
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _warnings(raw: Any) -> list[dict[str, Any]]:
|
|
213
|
+
"""Validate `warnings` -- the LIST and now also every ENTRY.
|
|
214
|
+
|
|
215
|
+
`warnings` is a CONTRACT SURFACE, not merely an internal field: `tan
|
|
216
|
+
build` copies it verbatim into the envelope's `data.warnings`, and a
|
|
217
|
+
consumer doing `data.warnings ?? []` then `.map(w => w.code)` breaks on a
|
|
218
|
+
string. An earlier version checked only that the value was a list and
|
|
219
|
+
left the ENTRIES untyped "on purpose", reasoning that new warning codes
|
|
220
|
+
may appear without a schemaVersion bump. Running the oracle refutes the
|
|
221
|
+
second half: `{"warnings": ["oops"]}` is refused as `invalid type: string
|
|
222
|
+
"oops", expected struct PlanWarning`. The forward compatibility is in the
|
|
223
|
+
CODES being open, not the entry SHAPE -- measured, `PlanWarning` is a
|
|
224
|
+
3-field struct: `code` and `message` are required strings, `coreId` is an
|
|
225
|
+
`Option<String>` (null and absent both fine), and an unknown key such as
|
|
226
|
+
`zzz` is accepted AND came back untouched in `data.warnings`. `code` is
|
|
227
|
+
checked before `message`, the order serde reports them in.
|
|
228
|
+
|
|
229
|
+
Deliberately NOT replicated: the oracle also accepts a 3-element ARRAY per
|
|
230
|
+
entry (serde's tuple form for the same struct -- hence its "expected
|
|
231
|
+
struct PlanWarning with 3 elements" wording). No producer emits that, the
|
|
232
|
+
schema says objects, and honouring it would mean carrying a second entry
|
|
233
|
+
shape through every consumer of `data.warnings`."""
|
|
234
|
+
if not isinstance(raw, list):
|
|
235
|
+
raise PlanParseError("build.plan-invalid", "`warnings` must be a list")
|
|
236
|
+
for i, entry in enumerate(raw):
|
|
237
|
+
if not isinstance(entry, dict):
|
|
238
|
+
raise PlanParseError(
|
|
239
|
+
"build.plan-invalid",
|
|
240
|
+
f"`warnings[{i}]` must be an object with string `code` and `message` fields",
|
|
241
|
+
)
|
|
242
|
+
for key in ("code", "message"):
|
|
243
|
+
if key not in entry:
|
|
244
|
+
raise PlanParseError(
|
|
245
|
+
"build.plan-invalid", f"`warnings[{i}]` is missing required key `{key}`"
|
|
246
|
+
)
|
|
247
|
+
if not isinstance(entry[key], str):
|
|
248
|
+
raise PlanParseError(
|
|
249
|
+
"build.plan-invalid", f"`warnings[{i}].{key}` must be a string"
|
|
250
|
+
)
|
|
251
|
+
if entry.get("coreId") is not None and not isinstance(entry["coreId"], str):
|
|
252
|
+
raise PlanParseError(
|
|
253
|
+
"build.plan-invalid", f"`warnings[{i}].coreId` must be a string or null"
|
|
254
|
+
)
|
|
255
|
+
return raw
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _foreign_os_absolute(tool: str) -> str | None:
|
|
259
|
+
"""Return a short name for the OTHER OS's path convention if `tool`
|
|
260
|
+
looks absolute under it, else `None`.
|
|
261
|
+
|
|
262
|
+
`Path(tool).is_absolute()` (below) answers relative to THIS host's own
|
|
263
|
+
`pathlib` flavour: `/usr/bin/west` is absolute under `PurePosixPath` but
|
|
264
|
+
NOT under `PureWindowsPath` (no drive), and `C:\\tools\\west.exe` is
|
|
265
|
+
absolute under `PureWindowsPath` but NOT under `PurePosixPath` (no
|
|
266
|
+
leading `/`, and `pathlib` never special-cases a drive-letter prefix on
|
|
267
|
+
POSIX). Used only to name the reason precisely in the refusal message
|
|
268
|
+
below -- a bare "relative path" reading is misleading for a path that IS
|
|
269
|
+
absolute, just under the wrong OS's rules."""
|
|
270
|
+
if tool.startswith("/"):
|
|
271
|
+
return "POSIX"
|
|
272
|
+
if len(tool) >= 3 and tool[1] == ":" and tool[2] in "\\/" and tool[0].isalpha():
|
|
273
|
+
return "Windows"
|
|
274
|
+
if tool.startswith("\\\\"):
|
|
275
|
+
return "Windows"
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _is_legal_env_name(name: str) -> bool:
|
|
280
|
+
"""`os.environ`/`subprocess` reject an empty name, one containing `=`
|
|
281
|
+
(`ValueError: illegal environment variable name`) or one containing an
|
|
282
|
+
embedded NUL (`ValueError: embedded null byte`) -- catch that shape here,
|
|
283
|
+
at parse time, instead of at spawn time deep inside `execute.py`."""
|
|
284
|
+
return name != "" and "=" not in name and "\0" not in name
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _reject_nul(value: str, context: str) -> None:
|
|
288
|
+
"""Refuse an embedded NUL in a string that is headed for `subprocess`.
|
|
289
|
+
|
|
290
|
+
Every C-level exec/environ interface takes NUL-terminated strings, so
|
|
291
|
+
CPython refuses the whole call with a bare `ValueError: embedded null
|
|
292
|
+
byte`. Measured before this fix, on `tan build --plan-from <f>
|
|
293
|
+
--execute`, a NUL in `env` (name OR value), `envAppendPath`, `command.args`
|
|
294
|
+
or `command.cwd` each produced `exit 5 build.internal-failure:
|
|
295
|
+
ValueError: embedded null byte` -- tan reporting ITSELF as broken for
|
|
296
|
+
what is a malformed plan. That is the same failure mode, and the same
|
|
297
|
+
remedy, as the `=`/empty env names `_is_legal_env_name` already refuses
|
|
298
|
+
two lines up.
|
|
299
|
+
|
|
300
|
+
ORACLE NOTE -- a MEASURED DIVERGENCE, stated plainly. v0.4.1 has no
|
|
301
|
+
`--execute` flag, so the oracle has no observable behaviour at all for a
|
|
302
|
+
NUL-bearing plan on the spawn path; the only path it can reach is plan
|
|
303
|
+
DISPLAY, where it echoes the NUL back at exit 0. This parse runs on that
|
|
304
|
+
path too, so `tan build --plan-from <nul-plan>` -- which implies display
|
|
305
|
+
in this port, exactly as it does in the oracle -- now exits 1 here where
|
|
306
|
+
the oracle exits 0.
|
|
307
|
+
|
|
308
|
+
**That surface is `--plan-from`, NOT `--plan`.** An earlier version of
|
|
309
|
+
this note named `tan build --plan --plan-from <nul-plan>` as the diverging
|
|
310
|
+
invocation; measured, `--plan` on `tan build` is now a RETIRED flag in
|
|
311
|
+
this port and answers `exit 2 build.flag-retired` for EVERY invocation,
|
|
312
|
+
valid or not (tan-cli#427), so nothing about a NUL is observable through
|
|
313
|
+
it.
|
|
314
|
+
|
|
315
|
+
The trade is taken deliberately, for the same reason and in the same shape
|
|
316
|
+
as the `=`/empty env names above, which already diverge from the oracle in
|
|
317
|
+
exactly this way (measured: `{"F=OO": "bar"}` is `exit 0 ok:true` on the
|
|
318
|
+
oracle and `exit 1 build.plan-invalid` here, and has been since before this
|
|
319
|
+
change). The alternative is keeping a `build.internal-failure` exit 5 on
|
|
320
|
+
the only path where the value is ever USED.
|
|
321
|
+
|
|
322
|
+
The divergence is deliberately NARROW, and three NUL-bearing fields are
|
|
323
|
+
still accepted here. What each one ACTUALLY does was re-measured; an
|
|
324
|
+
earlier version of this list described two of them wrongly:
|
|
325
|
+
|
|
326
|
+
* `configArtefacts[].path` and `sharedArtefacts[].path` -- the oracle
|
|
327
|
+
exits 3 `build.materialise-failed`, "file name contained an unexpected
|
|
328
|
+
NUL byte", so this parser must not pre-empt it with a vaguer refusal.
|
|
329
|
+
But this port does NOT reach that answer: measured on
|
|
330
|
+
`tan build --plan-from <f> --materialise`, both fields give
|
|
331
|
+
`exit 5 build.internal-failure: ValueError: embedded null byte`. They
|
|
332
|
+
are NOT "left to the materialiser" in any working sense -- they are a
|
|
333
|
+
LIVE defect of exactly the class this function exists to close, left
|
|
334
|
+
open because the fix belongs in `tan.commands.build.materialise`
|
|
335
|
+
(`confine_to_build_root`/`_write`), out of this lane's file.
|
|
336
|
+
* `command.tool` -- answered downstream: the slice is `skipped` with
|
|
337
|
+
`tool `<name>` not found`, the envelope carries a `build.missing-tool`
|
|
338
|
+
warning and a `build.nothing-built` error, exit 1. A NUL-bearing path
|
|
339
|
+
can never exist on disk, so the miss is guaranteed.
|
|
340
|
+
* `buildDir` -- no crash, and no failure either: measured, a slice with
|
|
341
|
+
a NUL `buildDir` RUNS and SUCCEEDS -- `exit 0`, `ok:true`, slice
|
|
342
|
+
status `ok`. Nothing on the execute path opens it (`plan_tokens.
|
|
343
|
+
_substitute_slice` substitutes it; `SliceOutcome.build_dir` comes from
|
|
344
|
+
`resolve_zephyr_artefact(cwd, ...)`, not from the plan), so the NUL is
|
|
345
|
+
carried into `data` and dropped. Left accepted rather than refused
|
|
346
|
+
because the value never reaches an OS call -- but "the slice fails on
|
|
347
|
+
its own terms", which this note used to claim, is simply not what
|
|
348
|
+
happens.
|
|
349
|
+
"""
|
|
350
|
+
if "\0" in value:
|
|
351
|
+
raise PlanParseError(
|
|
352
|
+
"build.plan-invalid",
|
|
353
|
+
f"`{context}` contains an embedded NUL byte -- it cannot be handed to a "
|
|
354
|
+
f"process, its environment, or the filesystem. This is a malformed plan; "
|
|
355
|
+
f"re-emit it.",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _validate_tool_shape(tool: str, context: str) -> None:
|
|
360
|
+
"""Refuse a `command.tool` that is neither a bare identity nor an
|
|
361
|
+
already-resolved absolute path -- split out of `_command` so that
|
|
362
|
+
function stays under the repo's long-function budget.
|
|
363
|
+
|
|
364
|
+
tan-cli#510 review, MAJOR 4: `command.tool` is an IDENTITY per ADR-0020
|
|
365
|
+
(a bare name to look up, e.g. `west`) or an ALREADY-RESOLVED absolute
|
|
366
|
+
path a producer computed -- never a relative path. A relative path
|
|
367
|
+
carrying a separator (`bin/sh`) is neither: `_resolve_tool`
|
|
368
|
+
(`build/execute.py`) answers `Path(tool).is_absolute()` False for it, so
|
|
369
|
+
it falls into the bare-identity PATH search, which checks it against
|
|
370
|
+
THIS process's cwd -- but the spawn below hands it to
|
|
371
|
+
`subprocess.Popen(cwd=spawn_cwd)`, resolved against the CHILD's cwd
|
|
372
|
+
instead. Two different directories deciding what "the tool" even means
|
|
373
|
+
is the exact defect this issue exists to close; refused here, at parse
|
|
374
|
+
time, rather than reaching that check/spawn split at all. A bare `west`
|
|
375
|
+
(no separator) is untouched -- it is exactly the identity shape this
|
|
376
|
+
refusal is not about.
|
|
377
|
+
|
|
378
|
+
tan-cli#530, decision (a): "absolute" is deliberately THIS HOST's own
|
|
379
|
+
notion (`Path(tool).is_absolute()`) -- an already-absolute
|
|
380
|
+
`command.tool` is inherently host-specific even though the plan around
|
|
381
|
+
it is portable (see `_foreign_os_absolute`'s docstring for the full
|
|
382
|
+
reasoning). No emitting flow produces this shape today (the SDK planner
|
|
383
|
+
only emits bare `west`/`bitbake`/`cmake`); this guards a hand-authored
|
|
384
|
+
or foreign-host `--plan-from` file."""
|
|
385
|
+
if Path(tool).is_absolute() or ("/" not in tool and "\\" not in tool):
|
|
386
|
+
return
|
|
387
|
+
foreign = _foreign_os_absolute(tool)
|
|
388
|
+
if foreign is not None:
|
|
389
|
+
raise PlanParseError(
|
|
390
|
+
"build.plan-invalid",
|
|
391
|
+
f"`{context}.tool` (`{tool}`) is a {foreign}-absolute path, not "
|
|
392
|
+
f"executable on this host -- a build plan is a portable artefact, "
|
|
393
|
+
f"but an absolute `command.tool` is inherently host-specific: "
|
|
394
|
+
f"`{tool}` is only absolute under {foreign} path rules, and this "
|
|
395
|
+
f"host cannot resolve or spawn it. `command.tool` must be either "
|
|
396
|
+
f"a bare identity to look up on PATH (e.g. `west`) or a path this "
|
|
397
|
+
f"host itself recognises as absolute.",
|
|
398
|
+
)
|
|
399
|
+
raise PlanParseError(
|
|
400
|
+
"build.plan-invalid",
|
|
401
|
+
f"`{context}.tool` (`{tool}`) is a relative path, not an identity -- "
|
|
402
|
+
f"`command.tool` must be either a bare identity to look up on PATH "
|
|
403
|
+
f"(e.g. `west`) or an already-resolved absolute path; a relative path "
|
|
404
|
+
f"carrying a separator is neither.",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _command(raw: Any, context: str) -> SliceCommand | None:
|
|
409
|
+
"""Validate a slice's `command` (or `null`) at parse time. Rust's serde
|
|
410
|
+
rejects a malformed `ToolStep` the same way -- an unguarded `cmd["tool"]`
|
|
411
|
+
let a missing key escape as a bare `KeyError`, and a non-string
|
|
412
|
+
`tool`/`args` element/`cwd` reached `subprocess.Popen` as an uncaught
|
|
413
|
+
`TypeError`."""
|
|
414
|
+
if raw is None:
|
|
415
|
+
return None
|
|
416
|
+
if not isinstance(raw, dict):
|
|
417
|
+
raise PlanParseError("build.plan-invalid", f"`{context}` must be an object or null")
|
|
418
|
+
tool = raw.get("tool")
|
|
419
|
+
if not isinstance(tool, str):
|
|
420
|
+
raise PlanParseError("build.plan-invalid", f"`{context}.tool` must be a string")
|
|
421
|
+
_validate_tool_shape(tool, context)
|
|
422
|
+
args = raw.get("args", [])
|
|
423
|
+
if not isinstance(args, list) or not all(isinstance(a, str) for a in args):
|
|
424
|
+
raise PlanParseError("build.plan-invalid", f"`{context}.args` must be a list of strings")
|
|
425
|
+
for i, arg in enumerate(args):
|
|
426
|
+
_reject_nul(arg, f"{context}.args[{i}]")
|
|
427
|
+
cwd = raw.get("cwd")
|
|
428
|
+
if cwd is not None and not isinstance(cwd, str):
|
|
429
|
+
raise PlanParseError("build.plan-invalid", f"`{context}.cwd` must be a string or null")
|
|
430
|
+
if cwd is not None:
|
|
431
|
+
_reject_nul(cwd, f"{context}.cwd")
|
|
432
|
+
return SliceCommand(tool=tool, args=list(args), cwd=cwd)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _post_commands(raw: Any, context: str) -> tuple[SliceCommand, ...]:
|
|
436
|
+
"""Validate a slice's `postCommands` (alp-sdk #1344, tan-cli#550).
|
|
437
|
+
|
|
438
|
+
Absent or `null` is `()`, not a refusal -- see [`Slice.post_commands`] for
|
|
439
|
+
why an older SDK's plan must still parse. Anything else must be a LIST,
|
|
440
|
+
and every entry a real command object: a `null` entry is refused here
|
|
441
|
+
rather than silently dropped, because `_command` answers `None` for a null
|
|
442
|
+
`command` (the plan's own "this slice has nothing to run" signal) and that
|
|
443
|
+
meaning does not exist inside a step list -- a dropped step is a build
|
|
444
|
+
step that never ran with nothing on the wire to say so, which is the exact
|
|
445
|
+
class of silence tan-cli#550 is about.
|
|
446
|
+
|
|
447
|
+
Each entry goes through the SAME `_command` validation as the slice's own
|
|
448
|
+
`command`: identical `tool` shape rules (bare identity or host-absolute,
|
|
449
|
+
never a relative path with a separator), identical `args`/`cwd` typing and
|
|
450
|
+
identical embedded-NUL refusal. They are handed to `subprocess` by the
|
|
451
|
+
same executor, so a shape this parser accepts here but not there would be
|
|
452
|
+
a hole, not a tolerance."""
|
|
453
|
+
if raw is None:
|
|
454
|
+
return ()
|
|
455
|
+
if not isinstance(raw, list):
|
|
456
|
+
raise PlanParseError(
|
|
457
|
+
"build.plan-invalid", f"`{context}` must be a list of command objects or null"
|
|
458
|
+
)
|
|
459
|
+
steps: list[SliceCommand] = []
|
|
460
|
+
for i, entry in enumerate(raw):
|
|
461
|
+
if entry is None:
|
|
462
|
+
raise PlanParseError(
|
|
463
|
+
"build.plan-invalid",
|
|
464
|
+
f"`{context}[{i}]` must be an object -- a null step is not a command, and "
|
|
465
|
+
f"a step that cannot run must be absent from the list, not present as null",
|
|
466
|
+
)
|
|
467
|
+
step = _command(entry, f"{context}[{i}]")
|
|
468
|
+
if step is not None:
|
|
469
|
+
steps.append(step)
|
|
470
|
+
return tuple(steps)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _env(raw: Any, context: str) -> dict[str, str]:
|
|
474
|
+
if not isinstance(raw, dict) or not all(
|
|
475
|
+
isinstance(k, str) and _is_legal_env_name(k) and isinstance(v, str) for k, v in raw.items()
|
|
476
|
+
):
|
|
477
|
+
raise PlanParseError(
|
|
478
|
+
"build.plan-invalid",
|
|
479
|
+
f"`{context}` must be an object mapping a legal env-var name to a string value",
|
|
480
|
+
)
|
|
481
|
+
for key, value in raw.items():
|
|
482
|
+
_reject_nul(value, f"{context}.{key}")
|
|
483
|
+
return dict(raw)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _env_append_path(raw: Any, context: str) -> dict[str, list[str]]:
|
|
487
|
+
if not isinstance(raw, dict) or not all(
|
|
488
|
+
isinstance(k, str)
|
|
489
|
+
and _is_legal_env_name(k)
|
|
490
|
+
and isinstance(v, list)
|
|
491
|
+
and all(isinstance(x, str) for x in v)
|
|
492
|
+
for k, v in raw.items()
|
|
493
|
+
):
|
|
494
|
+
raise PlanParseError(
|
|
495
|
+
"build.plan-invalid",
|
|
496
|
+
f"`{context}` must be an object mapping a legal env-var name to a list of strings",
|
|
497
|
+
)
|
|
498
|
+
for key, values in raw.items():
|
|
499
|
+
for i, value in enumerate(values):
|
|
500
|
+
_reject_nul(value, f"{context}.{key}[{i}]")
|
|
501
|
+
return {k: list(v) for k, v in raw.items()}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _slice(raw: Any, i: int) -> Slice:
|
|
505
|
+
if not isinstance(raw, dict):
|
|
506
|
+
raise PlanParseError("build.plan-invalid", f"`slices[{i}]` must be an object")
|
|
507
|
+
missing = [k for k in _REQUIRED_SLICE if k not in raw]
|
|
508
|
+
if missing:
|
|
509
|
+
raise PlanParseError(
|
|
510
|
+
"build.plan-invalid",
|
|
511
|
+
f"slice is missing required key(s): {', '.join(missing)}",
|
|
512
|
+
)
|
|
513
|
+
_require_strings(raw, _REQUIRED_STR_SLICE, _OPTIONAL_STR_SLICE, f"slices[{i}]")
|
|
514
|
+
return Slice(
|
|
515
|
+
core_id=raw["coreId"], backend=raw["backend"], build_dir=raw["buildDir"],
|
|
516
|
+
app_dir=raw["appDir"],
|
|
517
|
+
config_artefacts=_artefacts(raw["configArtefacts"], f"slices[{i}].configArtefacts"),
|
|
518
|
+
toolchain=raw["toolchain"], artifacts=raw["artifacts"], debug=raw["debug"],
|
|
519
|
+
command=_command(raw["command"], f"slices[{i}].command"),
|
|
520
|
+
env=_env(raw["env"], f"slices[{i}].env"),
|
|
521
|
+
env_append_path=_env_append_path(raw["envAppendPath"], f"slices[{i}].envAppendPath"),
|
|
522
|
+
post_commands=_post_commands(raw.get("postCommands"), f"slices[{i}].postCommands"),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _schema_version(raw: dict[str, Any]) -> int:
|
|
527
|
+
"""Read and TYPE `schemaVersion`, then apply the version-skew guard.
|
|
528
|
+
|
|
529
|
+
The type check is not pedantry, it is the whole defect (tan-cli#491,
|
|
530
|
+
defect 9). This was a bare `version != SUPPORTED_SCHEMA_VERSION`, and in
|
|
531
|
+
Python `True == 1` and `1.0 == 1` are both true -- so a plan declaring
|
|
532
|
+
`"schemaVersion": true` or `"schemaVersion": 1.0` compared EQUAL to 1 and
|
|
533
|
+
was accepted as a v1 plan. Measured, the oracle refuses both outright:
|
|
534
|
+
`invalid type: boolean `true`, expected u32` and `invalid type: floating
|
|
535
|
+
point `1.0`, expected u32`.
|
|
536
|
+
|
|
537
|
+
`isinstance(True, int)` is True (bool subclasses int), so the bool test
|
|
538
|
+
must come first and cannot be folded into the int test.
|
|
539
|
+
|
|
540
|
+
The two codes are a deliberate split the oracle does not make -- it
|
|
541
|
+
answers every case below with `build.plan-invalid`. A value that is not a
|
|
542
|
+
u32 AT ALL is a malformed plan and keeps that code; a well-typed u32 that
|
|
543
|
+
simply is not 1 is a version SKEW, which this port reports as
|
|
544
|
+
`build.plan-unsupported-schema` so a consumer can tell "your SDK and your
|
|
545
|
+
tan disagree" from "this file is garbage". That split predates this fix
|
|
546
|
+
and is preserved exactly; what changes is which values reach it."""
|
|
547
|
+
if "schemaVersion" not in raw:
|
|
548
|
+
raise PlanParseError(
|
|
549
|
+
"build.plan-invalid",
|
|
550
|
+
"plan is missing required key(s): schemaVersion",
|
|
551
|
+
)
|
|
552
|
+
version = raw["schemaVersion"]
|
|
553
|
+
if isinstance(version, bool) or not isinstance(version, int):
|
|
554
|
+
raise PlanParseError(
|
|
555
|
+
"build.plan-invalid",
|
|
556
|
+
f"`schemaVersion` must be an integer -- got `{version!r}`. A build plan "
|
|
557
|
+
f"declares its schema as a whole number (the SDK emits "
|
|
558
|
+
f"{SUPPORTED_SCHEMA_VERSION}); re-emit the plan.",
|
|
559
|
+
)
|
|
560
|
+
if not 0 <= version <= _U32_MAX:
|
|
561
|
+
raise PlanParseError(
|
|
562
|
+
"build.plan-invalid",
|
|
563
|
+
f"`schemaVersion` ({version}) is outside the representable range "
|
|
564
|
+
f"0..{_U32_MAX}; re-emit the plan.",
|
|
565
|
+
)
|
|
566
|
+
if version != SUPPORTED_SCHEMA_VERSION:
|
|
567
|
+
raise PlanParseError(
|
|
568
|
+
"build.plan-unsupported-schema",
|
|
569
|
+
f"unsupported build-plan schemaVersion `{version}` (this tan supports "
|
|
570
|
+
f"{SUPPORTED_SCHEMA_VERSION}) -- refusing rather than falling back to "
|
|
571
|
+
f"hand-ported behaviour. Upgrade tan, or re-emit the plan.",
|
|
572
|
+
)
|
|
573
|
+
return version
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def parse_build_plan(text: str) -> BuildPlan:
|
|
577
|
+
try:
|
|
578
|
+
raw = json.loads(text)
|
|
579
|
+
except ValueError as err:
|
|
580
|
+
raise PlanParseError("build.plan-invalid", f"plan is not valid JSON: {err}") from err
|
|
581
|
+
|
|
582
|
+
if not isinstance(raw, dict):
|
|
583
|
+
raise PlanParseError("build.plan-invalid", "plan is not a JSON object")
|
|
584
|
+
|
|
585
|
+
version = _schema_version(raw)
|
|
586
|
+
|
|
587
|
+
missing = [k for k in _REQUIRED_TOP if k not in raw]
|
|
588
|
+
if missing:
|
|
589
|
+
raise PlanParseError(
|
|
590
|
+
"build.plan-invalid",
|
|
591
|
+
f"plan is missing required key(s): {', '.join(missing)}",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
_require_strings(raw, _REQUIRED_STR_TOP, _OPTIONAL_STR_TOP, "")
|
|
595
|
+
# tan-cli#491 defect 8, REVIEW round: a fourth live NUL vector of the same
|
|
596
|
+
# class as `env`/`envAppendPath`/`command.args`/`command.cwd`, missed by
|
|
597
|
+
# the first pass. `buildRoot` reaches `manifest.write_post_build_manifest`
|
|
598
|
+
# -> `confine_to_build_root(Path(base), plan_build_root)` -> `Path.resolve`
|
|
599
|
+
# after every dispatch, so measured on `tan build --plan-from <f>
|
|
600
|
+
# --execute` a NUL here gave `exit 5 build.internal-failure: ValueError:
|
|
601
|
+
# embedded null byte` -- tan reporting itself as broken for a malformed
|
|
602
|
+
# plan, the exact shape this defect is about. The oracle has no answer to
|
|
603
|
+
# pre-empt: it has no `--execute`, and on `--materialise` it never reads
|
|
604
|
+
# the plan's `buildRoot` at all (measured: `exit 0 ok:true`).
|
|
605
|
+
_reject_nul(raw["buildRoot"], "buildRoot")
|
|
606
|
+
|
|
607
|
+
if not isinstance(raw["slices"], list):
|
|
608
|
+
raise PlanParseError("build.plan-invalid", "`slices` must be a list")
|
|
609
|
+
# Validated HERE, ahead of the slices, purely to keep the pre-existing
|
|
610
|
+
# report order: a plan with both a bad warning and a bad slice named the
|
|
611
|
+
# warning first before this change, and still does.
|
|
612
|
+
warnings = _warnings(raw["warnings"])
|
|
613
|
+
|
|
614
|
+
deferred, deferred_problem = _deferred_placeholders(raw)
|
|
615
|
+
|
|
616
|
+
return BuildPlan(
|
|
617
|
+
schema_version=version, generated_by=raw["generatedBy"], board_yaml=raw["boardYaml"],
|
|
618
|
+
sku=raw["sku"], build_root=raw["buildRoot"],
|
|
619
|
+
slices=[_slice(s, i) for i, s in enumerate(raw["slices"])],
|
|
620
|
+
shared_artefacts=_artefacts(raw["sharedArtefacts"], "sharedArtefacts"),
|
|
621
|
+
warnings=warnings,
|
|
622
|
+
sdk_version=raw.get("sdkVersion"), sdk_commit=raw.get("sdkCommit"),
|
|
623
|
+
plan_path_mode=raw.get("planPathMode"), execution_policy=_policy(raw.get("executionPolicy")),
|
|
624
|
+
deferred_placeholders=deferred, deferred_placeholders_problem=deferred_problem,
|
|
625
|
+
)
|