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,59 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The `${PYTHON}` a `tan build` bakes into its slices (tan-cli#1317).
|
|
3
|
+
|
|
4
|
+
Split out of `build_cmd._build`. The resolver itself is `tan.core.host_python`,
|
|
5
|
+
shared with `tan doctor`'s `hostPython`; the FLOOR is doctor's own EFFECTIVE
|
|
6
|
+
floor (`tan.core.python_floor.effective_python_floor`), never a second constant.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from tan.core import host_python
|
|
15
|
+
from tan.core.plan_tokens import TOKEN_PYTHON
|
|
16
|
+
from tan.core.python_floor import effective_python_floor, read_manifest_floors
|
|
17
|
+
from tan.core.sdk_discovery import _planner_python_resolution
|
|
18
|
+
from tan.core.venv import west_workspace_dir
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class BuildPython:
|
|
23
|
+
python: str
|
|
24
|
+
#: Why no interpreter qualified (`None` when one did / none was needed).
|
|
25
|
+
refusal: str | None
|
|
26
|
+
#: Cores whose pre-substitution command/env uses `${PYTHON}`.
|
|
27
|
+
users: frozenset[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _effective_floor(build_root: str, sdk_root: str | None) -> tuple[int, int]:
|
|
31
|
+
"""Doctor's EFFECTIVE floor, via the shared pure `tan.core.python_floor`
|
|
32
|
+
(same inputs `tan doctor` feeds it: the resolved workspace's `zephyr/`,
|
|
33
|
+
else `$ZEPHYR_BASE`; the SDK manifest's two floors)."""
|
|
34
|
+
ws = west_workspace_dir(build_root, Path(sdk_root) if sdk_root else None)
|
|
35
|
+
zephyr_base = str(ws / "zephyr") if ws is not None else os.environ.get("ZEPHYR_BASE")
|
|
36
|
+
manifest_floor, zephyr_manifest_floor = read_manifest_floors(sdk_root)
|
|
37
|
+
return effective_python_floor(manifest_floor, zephyr_base, zephyr_manifest_floor)[0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_build_python(plan, plan_text: str, build_root: str, sdk_root: str | None) -> BuildPython:
|
|
41
|
+
fallback, used_venv = _planner_python_resolution(build_root, sdk_root)
|
|
42
|
+
users = frozenset(
|
|
43
|
+
sl.core_id
|
|
44
|
+
for sl in plan.slices
|
|
45
|
+
if TOKEN_PYTHON in f"{sl.command!r}{sl.env!r}{sl.env_append_path!r}"
|
|
46
|
+
)
|
|
47
|
+
needs = TOKEN_PYTHON in plan_text and not used_venv
|
|
48
|
+
floor = _effective_floor(build_root, sdk_root) if needs else (0, 0)
|
|
49
|
+
python, refusal = host_python.build_interpreter(
|
|
50
|
+
fallback if used_venv else None, fallback, needs, floor
|
|
51
|
+
)
|
|
52
|
+
return BuildPython(python, refusal, users)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def refusal_message(refusal: str) -> str:
|
|
56
|
+
return (
|
|
57
|
+
refusal + " Install it and put it first on PATH, or run `tan bootstrap` "
|
|
58
|
+
"to create a workspace venv; `tan doctor` reports the same interpreter."
|
|
59
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""tan-cli#1350: switching board.yaml `diagnostics.link: itcm` back to `auto`
|
|
3
|
+
in an already-configured build dir must not keep linking at the ITCM.
|
|
4
|
+
|
|
5
|
+
Zephyr resolves `EXTRA_DTC_OVERLAY_FILE` with `zephyr_get(... CACHE ...)`
|
|
6
|
+
(`cmake/modules/configuration_files.cmake`): a `-DEXTRA_DTC_OVERLAY_FILE=...`
|
|
7
|
+
from an earlier configure is stored in `CMakeCache.txt` and read back on every
|
|
8
|
+
later configure that does not pass the variable itself. With the knob off the
|
|
9
|
+
plan carries no `-DEXTRA_DTC_OVERLAY_FILE`, so the cached
|
|
10
|
+
`<build>/alp-link-itcm.overlay` is still applied -- and the overlay file itself
|
|
11
|
+
is still on disk from the earlier materialise -- and the image silently links
|
|
12
|
+
at 0x0 with `ok: true`. (`EXTRA_CONF_FILE` is not affected: every plan re-passes
|
|
13
|
+
it.)
|
|
14
|
+
|
|
15
|
+
[`stale_itcm_overlay_reset`] is the fix, run before each Zephyr configure:
|
|
16
|
+
when the slice's command does NOT carry the ITCM overlay, delete any leftover
|
|
17
|
+
`alp-link-itcm.*` artefact and, if the cache still names the overlay, return
|
|
18
|
+
`-UEXTRA_DTC_OVERLAY_FILE`. The WHOLE `EXTRA_DTC_OVERLAY_FILE` cache entry is
|
|
19
|
+
unset when our overlay appears anywhere in it -- harmless, because tan never
|
|
20
|
+
passes any other `EXTRA_DTC_OVERLAY_FILE`; an entry that never names our
|
|
21
|
+
overlay is left alone.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from tan.envelope import Issue
|
|
29
|
+
|
|
30
|
+
_OVERLAY = "alp-link-itcm.overlay"
|
|
31
|
+
_ARTEFACT_GLOB = "alp-link-itcm.*"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def stale_itcm_overlay_reset(cwd: Path, args: list[str]) -> tuple[list[str], list[Issue]]:
|
|
35
|
+
"""`(extra_cmake_args, issues)` -- empty when nothing is stale or when the
|
|
36
|
+
slice IS itcm-targeted (its command names the overlay itself)."""
|
|
37
|
+
if any(_OVERLAY in a for a in args):
|
|
38
|
+
return [], []
|
|
39
|
+
removed: list[str] = []
|
|
40
|
+
for stale in sorted(Path(cwd).glob(_ARTEFACT_GLOB)):
|
|
41
|
+
try:
|
|
42
|
+
stale.unlink()
|
|
43
|
+
removed.append(stale.name)
|
|
44
|
+
except OSError:
|
|
45
|
+
pass # best-effort; the -U below is what protects the link
|
|
46
|
+
try:
|
|
47
|
+
cache = (Path(cwd) / "build" / "CMakeCache.txt").read_text(
|
|
48
|
+
encoding="utf-8", errors="replace")
|
|
49
|
+
except OSError:
|
|
50
|
+
cache = ""
|
|
51
|
+
cached = any(
|
|
52
|
+
line.startswith("EXTRA_DTC_OVERLAY_FILE") and _OVERLAY in line
|
|
53
|
+
for line in cache.splitlines()
|
|
54
|
+
)
|
|
55
|
+
if not cached:
|
|
56
|
+
return [], []
|
|
57
|
+
note = (
|
|
58
|
+
"this slice is no longer linked for the ITCM (`diagnostics.link` is "
|
|
59
|
+
f"not `itcm`) but its cached configure still names {_OVERLAY}"
|
|
60
|
+
+ (f"; removed stale {', '.join(removed)}" if removed else "")
|
|
61
|
+
+ " -- unsetting EXTRA_DTC_OVERLAY_FILE so it links for MRAM again"
|
|
62
|
+
)
|
|
63
|
+
return ["-UEXTRA_DTC_OVERLAY_FILE"], [Issue("build.configure-cache-reset", "info", note)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def insert_after_separator(args: list[str], extra: list[str]) -> list[str]:
|
|
67
|
+
"""`args` with `extra` placed immediately after the first `--` (before any
|
|
68
|
+
`-D`), or appended when there is no `--`. A `-U` placed before a `-D` of the
|
|
69
|
+
same cache key is applied first, so the later `-D` always survives."""
|
|
70
|
+
if not extra:
|
|
71
|
+
return list(args)
|
|
72
|
+
out = list(args)
|
|
73
|
+
try:
|
|
74
|
+
at = out.index("--") + 1
|
|
75
|
+
except ValueError:
|
|
76
|
+
at = len(out)
|
|
77
|
+
out[at:at] = extra
|
|
78
|
+
return out
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Post-build output seam for `execute_slices`: write the `system-
|
|
3
|
+
manifest.yaml` the downstream `tan run`/`tan flash`/`tan size`/`tan image`
|
|
4
|
+
contract reads, and resolve the real on-disk `zephyr.elf` a slice produced.
|
|
5
|
+
|
|
6
|
+
Port of `crates/tan-cli/src/commands/build/execute/manifest.rs`. What's
|
|
7
|
+
ported: the post-build `write_post_build_manifest` seam and its two in-memory
|
|
8
|
+
signals (`write_failed_reason` / `native_sim_target`), `resolve_zephyr_artefact`'s
|
|
9
|
+
default-nested-build-dir artefact resolution, the SDK-identity stamp
|
|
10
|
+
(`sdk_stamp_path`/`read_sdk_stamp`/`write_sdk_stamp`/`cmake_cache_configured`)
|
|
11
|
+
the sdk-switch-pristine guard in `execute.py` reads and writes (issue #52),
|
|
12
|
+
and (tan-cli#309) the Zephyr-boilerplate-loaded guard (tan-cli #97 upstream)
|
|
13
|
+
[`zephyr_boilerplate_loaded`] -- `execute.py`'s status assembly applies it.
|
|
14
|
+
|
|
15
|
+
**Why `sdk_root`/`board_yaml` stay ACCEPTED overrides here rather than always
|
|
16
|
+
required.** The Rust oracle's `write_post_build_manifest` takes a
|
|
17
|
+
`ProjectContext` its caller (`crates/tan-cli/src/commands/build/execute/mod.rs`)
|
|
18
|
+
already resolved. This port's real caller, `tan.commands.build_cmd._dispatch` /
|
|
19
|
+
`_build`, now threads its own already-resolved `--sdk-root` straight through
|
|
20
|
+
(`execute_slices(..., sdk_root=sdk_root)` -> [`write_post_build_manifest`]) --
|
|
21
|
+
mirroring the oracle's `ProjectContext` exactly. The fallback below (the plan's
|
|
22
|
+
own `boardYaml` field plus [`tan.core.sdk_discovery.resolve_sdk_root_ladder`]'s
|
|
23
|
+
full precedence chain) stays for any OTHER caller of `execute_slices` that
|
|
24
|
+
passes no `sdk_root` at all (every test in this port's own suite does exactly
|
|
25
|
+
that), so the write is never permanently unreachable just because a caller
|
|
26
|
+
omitted the override.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from tan.commands.build.materialise import MaterialiseError, confine_to_build_root
|
|
36
|
+
from tan.core.sdk_discovery import resolve_sdk_root_ladder
|
|
37
|
+
from tan.core.system_manifest import (
|
|
38
|
+
SliceRunResult,
|
|
39
|
+
SystemManifestError,
|
|
40
|
+
overlay_run_results_raw,
|
|
41
|
+
parse_system_manifest,
|
|
42
|
+
parse_system_manifest_raw,
|
|
43
|
+
serialize_system_manifest_raw,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"PostBuildManifest",
|
|
48
|
+
"build_dir_overridden",
|
|
49
|
+
"cmake_cache_configured",
|
|
50
|
+
"read_sdk_stamp",
|
|
51
|
+
"resolve_zephyr_artefact",
|
|
52
|
+
"sdk_stamp_path",
|
|
53
|
+
"write_post_build_manifest",
|
|
54
|
+
"write_sdk_stamp",
|
|
55
|
+
"zephyr_boilerplate_loaded",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class PostBuildManifest:
|
|
61
|
+
"""Outcome of [`write_post_build_manifest`] -- the two independent,
|
|
62
|
+
IN-MEMORY signals `tan run` decides host-vs-hardware and flash-safety
|
|
63
|
+
from (see `tan.core.run`'s module doc). Neither field is ever derived by
|
|
64
|
+
re-reading the on-disk file afterward. Port of `crates/tan-cli/.../
|
|
65
|
+
execute/manifest.rs::PostBuildManifest`."""
|
|
66
|
+
|
|
67
|
+
#: `None` when the on-disk file write succeeded; otherwise why it didn't.
|
|
68
|
+
#: Never flips the build's own exit code -- only gates `tan run --flash`.
|
|
69
|
+
write_failed_reason: str | None
|
|
70
|
+
#: Whether THIS run's freshly emitted SDK system-manifest projection
|
|
71
|
+
#: declares a `native_sim` slice -- independent of `write_failed_reason`
|
|
72
|
+
#: (the emit and the file write are separate steps; a write failure must
|
|
73
|
+
#: not erase a target the emit already established). `None` only when the
|
|
74
|
+
#: emit itself failed or didn't parse -- no signal at all, not "not
|
|
75
|
+
#: native_sim".
|
|
76
|
+
native_sim_target: bool | None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _native_sim_target_from_yaml(yaml_text: str) -> bool | None:
|
|
80
|
+
"""Whether THIS run's freshly emitted system-manifest YAML declares a
|
|
81
|
+
`native_sim` slice. `None` only when `yaml_text` doesn't parse (no signal
|
|
82
|
+
at all); a manifest with no `native_sim` slice is `False`, not `None`.
|
|
83
|
+
Port of `crates/tan-cli/.../execute/manifest.rs::native_sim_target_from_yaml`."""
|
|
84
|
+
from tan.core.run import native_sim_slice # local: avoids a core<->core cycle at import time
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
manifest = parse_system_manifest(yaml_text)
|
|
88
|
+
except SystemManifestError:
|
|
89
|
+
return None
|
|
90
|
+
return native_sim_slice(manifest) is not None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_post_build_manifest(
|
|
94
|
+
*,
|
|
95
|
+
sdk_root: str | None,
|
|
96
|
+
board_yaml: str | None,
|
|
97
|
+
base: str,
|
|
98
|
+
plan_build_root: str,
|
|
99
|
+
results: Sequence[SliceRunResult],
|
|
100
|
+
plain_route: tuple[str, str] | None = None,
|
|
101
|
+
) -> PostBuildManifest:
|
|
102
|
+
"""Write `<base>/<plan_build_root>/system-manifest.yaml` after a build:
|
|
103
|
+
fetch the plan-time projection from the SDK (`--emit system-manifest`),
|
|
104
|
+
overlay this run's per-slice status, serialize, and write. Best-effort --
|
|
105
|
+
a failure here never flips the build's own exit code (the slices already
|
|
106
|
+
ran; this is a post-build convenience write) -- but every failure branch
|
|
107
|
+
reports `write_failed_reason` rather than failing silently.
|
|
108
|
+
|
|
109
|
+
`native_sim_target` is derived from the SDK emit BEFORE the file write is
|
|
110
|
+
even attempted, so it survives a write failure intact -- this is what lets
|
|
111
|
+
`tan run` decide from THIS build's own outcome instead of a later disk
|
|
112
|
+
read. Port of `crates/tan-cli/.../execute/manifest.rs::write_post_build_manifest`.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
dest_dir = confine_to_build_root(Path(base), plan_build_root)
|
|
116
|
+
except MaterialiseError as err:
|
|
117
|
+
# KNOWN STRING DIVERGENCE (deliberate, not fixed): the oracle
|
|
118
|
+
# (`manifest.rs::write_post_build_manifest`) guards `plan.build_root`
|
|
119
|
+
# with the narrower `tan_core::is_plain_relative` shape check and
|
|
120
|
+
# reports `"unsafe build_root `{plan.build_root}`"` on failure. This
|
|
121
|
+
# port reuses `confine_to_build_root` -- the SAME resolve-and-contain
|
|
122
|
+
# guard `materialise_plan` and this module's own `execute.py` slice-
|
|
123
|
+
# cwd confinement already share -- which is a STRONGER check (it
|
|
124
|
+
# also catches drive-relative/rooted Windows shapes a syntactic
|
|
125
|
+
# `is_plain_relative` component scan would reject differently) but
|
|
126
|
+
# reports a different message: `"path `{rel}` would escape the build
|
|
127
|
+
# root `{root}`"`. No caller asserts the exact wording (verified:
|
|
128
|
+
# no test in this port's suite matches either string), so the
|
|
129
|
+
# divergence is recorded here rather than forking a third
|
|
130
|
+
# `plan_build_root`-only guard duplicating `confine_to_build_root`'s
|
|
131
|
+
# logic for a Minor, message-only difference.
|
|
132
|
+
return PostBuildManifest(write_failed_reason=err.message, native_sim_target=None)
|
|
133
|
+
|
|
134
|
+
effective_sdk_root = sdk_root
|
|
135
|
+
effective_board_yaml = board_yaml
|
|
136
|
+
if effective_sdk_root is None and effective_board_yaml:
|
|
137
|
+
# The FULL ladder (`--sdk-root` > project pin > global default >
|
|
138
|
+
# positional walk), not the positional walk alone: this fallback only
|
|
139
|
+
# fires for a caller of `execute_slices` that passed no `sdk_root`
|
|
140
|
+
# (every test in this port's own suite), and the positional walk
|
|
141
|
+
# alone would let the post-build manifest record a DIFFERENT SDK root
|
|
142
|
+
# than `resolve_sdk_root_ladder` resolved for the build itself the
|
|
143
|
+
# moment a project pin or machine-global default was in play.
|
|
144
|
+
# Imported at module level (tan-cli#408): `resolve_sdk_root_ladder`
|
|
145
|
+
# moved to `tan.core.sdk_discovery`, which `build_cmd.py` (and this
|
|
146
|
+
# module's own `tan.commands.build.execute` ancestor) no longer sit
|
|
147
|
+
# upstream of, so the import that used to be circular here is not
|
|
148
|
+
# any more.
|
|
149
|
+
|
|
150
|
+
# `_broken_pin` unused: this fallback only fires for a caller that
|
|
151
|
+
# passed no `sdk_root` at all (test-only, per the docstring above) --
|
|
152
|
+
# the real `tan build` path already resolved (and, since tan-cli#263
|
|
153
|
+
# review, already reported) the project pin at its own call site
|
|
154
|
+
# before ever reaching here, and `PostBuildManifest` has no issues
|
|
155
|
+
# channel to carry a second copy through.
|
|
156
|
+
discovered = resolve_sdk_root_ladder(None, Path(effective_board_yaml).parent).path
|
|
157
|
+
effective_sdk_root = str(discovered) if discovered else None
|
|
158
|
+
|
|
159
|
+
if effective_sdk_root is not None and not effective_board_yaml and plain_route is None:
|
|
160
|
+
# A build with no board.yaml has nothing to project the manifest from
|
|
161
|
+
# (the plain `tan build --board` route has its own writer, below).
|
|
162
|
+
return PostBuildManifest(
|
|
163
|
+
write_failed_reason="the plan has no board.yaml to project it from",
|
|
164
|
+
native_sim_target=None,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if effective_sdk_root is None or (not effective_board_yaml and plain_route is None):
|
|
168
|
+
return PostBuildManifest(
|
|
169
|
+
write_failed_reason=(
|
|
170
|
+
"no alp-sdk checkout resolved for the post-build system-manifest emit"
|
|
171
|
+
),
|
|
172
|
+
native_sim_target=None,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Refuse to BIND a root that is not an alp-sdk checkout. `_planner_emit`
|
|
176
|
+
# calls `bind_sdk_root(root)` and then imports `tan.planner`, and that
|
|
177
|
+
# binding is process-global, first-bind-wins by design: `tan/planner/
|
|
178
|
+
# paths.py` freezes `REPO = sdk_root()` at module scope and a dozen planner
|
|
179
|
+
# functions take `metadata_root: Path = METADATA_ROOT` as a default
|
|
180
|
+
# argument, so both freeze at import time.
|
|
181
|
+
#
|
|
182
|
+
# Binding a non-SDK path can never produce a manifest, but it is far from
|
|
183
|
+
# harmless. `tan/planner/slugs.py` reads
|
|
184
|
+
# `metadata/registries/peripheral-kconfig.json` at module scope, so the
|
|
185
|
+
# import dies PARTWAY: Python evicts `tan.planner` and `tan.planner.slugs`
|
|
186
|
+
# but leaves `tan.planner.paths` cached with `REPO` frozen to the bogus
|
|
187
|
+
# root. The `except Exception` below then swallows that failure -- this
|
|
188
|
+
# write is best-effort by design and must never escape -- so the caller
|
|
189
|
+
# sees success while the process is left holding a planner frozen against
|
|
190
|
+
# a tree that is not an SDK at all.
|
|
191
|
+
#
|
|
192
|
+
# Checking first is cheaper than unwinding after, and uses the same
|
|
193
|
+
# `scripts/alp_project.py` marker `resolve_sdk_root_ladder` already treats
|
|
194
|
+
# as the definition of "this is an alp-sdk checkout".
|
|
195
|
+
if not (Path(effective_sdk_root) / "scripts" / "alp_project.py").is_file():
|
|
196
|
+
return PostBuildManifest(
|
|
197
|
+
write_failed_reason=(
|
|
198
|
+
f"resolved alp-sdk root {effective_sdk_root} has no "
|
|
199
|
+
f"scripts/alp_project.py, so it is not an alp-sdk checkout; "
|
|
200
|
+
f"skipping the post-build system-manifest emit rather than "
|
|
201
|
+
f"binding the planner to it"
|
|
202
|
+
),
|
|
203
|
+
native_sim_target=None,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
from tan.planner_root import emit as _planner_emit
|
|
208
|
+
except ImportError as err: # pragma: no cover -- planner absent from this build
|
|
209
|
+
return PostBuildManifest(
|
|
210
|
+
write_failed_reason=f"planner unavailable: {err}", native_sim_target=None
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
if plain_route is not None:
|
|
214
|
+
# tan-cli#1370: no board.yaml to project from -- a one-slice
|
|
215
|
+
# manifest from the board target's SoM preset instead.
|
|
216
|
+
from tan.planner_root import bind_sdk_root
|
|
217
|
+
|
|
218
|
+
bind_sdk_root(effective_sdk_root)
|
|
219
|
+
from tan.planner.plain_slice import plain_system_manifest
|
|
220
|
+
|
|
221
|
+
yaml_text = plain_system_manifest(
|
|
222
|
+
plain_route[0],
|
|
223
|
+
Path(effective_sdk_root) / "metadata",
|
|
224
|
+
fallback_core_id=plain_route[1],
|
|
225
|
+
elf_path=next((r.output_artefact for r in results if r.output_artefact), None),
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
yaml_text = _planner_emit(
|
|
229
|
+
"system-manifest", root=effective_sdk_root, board_yaml=Path(effective_board_yaml)
|
|
230
|
+
)
|
|
231
|
+
except Exception as err: # noqa: BLE001 -- best-effort write, never escapes
|
|
232
|
+
return PostBuildManifest(
|
|
233
|
+
write_failed_reason=f"{type(err).__name__}: {err}", native_sim_target=None
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# THIS run's own target signal -- computed from the emit, independent of
|
|
237
|
+
# whether the file write below then succeeds.
|
|
238
|
+
native_sim_target = _native_sim_target_from_yaml(yaml_text)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
raw = parse_system_manifest_raw(yaml_text)
|
|
242
|
+
overlay_run_results_raw(raw, results)
|
|
243
|
+
out = serialize_system_manifest_raw(raw)
|
|
244
|
+
except SystemManifestError as err:
|
|
245
|
+
return PostBuildManifest(
|
|
246
|
+
write_failed_reason=err.message, native_sim_target=native_sim_target
|
|
247
|
+
)
|
|
248
|
+
except Exception as err: # noqa: BLE001 -- best-effort write, never escapes
|
|
249
|
+
# `serialize_system_manifest_raw` (`yaml.safe_dump`) is called OUTSIDE
|
|
250
|
+
# any `try` in the oracle's own terms too -- `rewrite_manifest_yaml`
|
|
251
|
+
# returns `Result<String, String>` for every branch, including the
|
|
252
|
+
# dumper's own failure (e.g. a `!!binary` node PyYAML's SafeDumper
|
|
253
|
+
# cannot represent). Uncaught here, that would escape
|
|
254
|
+
# `write_post_build_manifest`, escape `execute_slices`, and abort a
|
|
255
|
+
# build whose slices already ran -- breaking the documented
|
|
256
|
+
# best-effort contract this function's own docstring states.
|
|
257
|
+
return PostBuildManifest(
|
|
258
|
+
write_failed_reason=f"{type(err).__name__}: {err}",
|
|
259
|
+
native_sim_target=native_sim_target,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
dest = dest_dir / "system-manifest.yaml"
|
|
263
|
+
try:
|
|
264
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
# newline="" -- PRESERVE the LF `yaml.safe_dump` always emits, rather
|
|
266
|
+
# than Python's default universal-newline translation (CRLF on
|
|
267
|
+
# Windows), which made this contract file diverge byte-for-byte from
|
|
268
|
+
# the oracle's `std::fs::write` (raw bytes, no translation). Same
|
|
269
|
+
# convention already documented at materialise.py:63 and
|
|
270
|
+
# image_cmd.py:319-330.
|
|
271
|
+
dest.write_text(out, encoding="utf-8", newline="")
|
|
272
|
+
except OSError as err:
|
|
273
|
+
return PostBuildManifest(
|
|
274
|
+
write_failed_reason=str(err), native_sim_target=native_sim_target
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return PostBuildManifest(write_failed_reason=None, native_sim_target=native_sim_target)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_dir_overridden(cmd_args: Sequence[str]) -> bool:
|
|
281
|
+
"""True when the slice's argv redirects west's build dir away from the
|
|
282
|
+
default nested `<cwd>/build` (`-d <dir>`, `--build-dir <dir>`, or
|
|
283
|
+
`--build-dir=<dir>`) -- the one case [`resolve_zephyr_artefact`]'s default
|
|
284
|
+
path can't follow. Port of `crates/tan-cli/.../execute/manifest.rs::
|
|
285
|
+
build_dir_overridden`."""
|
|
286
|
+
return any(
|
|
287
|
+
a == "-d" or a == "--build-dir" or a.startswith("--build-dir=") for a in cmd_args
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def resolve_zephyr_artefact(
|
|
292
|
+
slice_cwd: Path, cmd_args: Sequence[str]
|
|
293
|
+
) -> tuple[str | None, str | None]:
|
|
294
|
+
"""After a slice builds `ok`, resolve the real `zephyr.elf` west produced
|
|
295
|
+
and its build dir. West's default output is a nested `build/` under the
|
|
296
|
+
slice's run cwd, so the elf lands at `<cwd>/build/zephyr/zephyr.elf`.
|
|
297
|
+
Returned ABSOLUTE (lexically, via `os.path.abspath` -- matching
|
|
298
|
+
`_abs_posix`'s convention elsewhere in this port -- never resolved through
|
|
299
|
+
a symlink) so every downstream consumer uses the path verbatim. `(None,
|
|
300
|
+
None)` when the slice's own command redirects the build dir
|
|
301
|
+
(`-d`/`--build-dir`) -- west then wrote somewhere other than the default
|
|
302
|
+
nested path, so the default location can't be trusted.
|
|
303
|
+
|
|
304
|
+
Deliberately NOT mtime-gated -- see the Rust oracle's own docstring on
|
|
305
|
+
why: a repeat `west build` with nothing to rebuild leaves `zephyr.elf`'s
|
|
306
|
+
mtime unchanged, so an mtime gate would drop a perfectly valid artefact on
|
|
307
|
+
every no-op/incremental rebuild. `is_file()` at the default path is
|
|
308
|
+
sufficient on its own. Port of `crates/tan-cli/.../execute/manifest.rs::
|
|
309
|
+
resolve_zephyr_artefact`."""
|
|
310
|
+
if build_dir_overridden(cmd_args):
|
|
311
|
+
return None, None
|
|
312
|
+
west_build = slice_cwd / "build"
|
|
313
|
+
elf = west_build / "zephyr" / "zephyr.elf"
|
|
314
|
+
if elf.is_file():
|
|
315
|
+
return os.path.abspath(str(elf)), os.path.abspath(str(west_build))
|
|
316
|
+
return None, None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
#: Filename of the tan-owned SDK-identity stamp (issue #52). Lives INSIDE
|
|
320
|
+
#: west's own nested build dir (`<cwd>/build`, not `<cwd>` itself), so wiping
|
|
321
|
+
#: that dir on a mismatch retires the stamp atomically -- it can never outlive
|
|
322
|
+
#: or misdescribe a build dir that no longer exists. Port of
|
|
323
|
+
#: `crates/tan-cli/.../execute/manifest.rs::sdk_stamp_path`'s hard-coded name.
|
|
324
|
+
_SDK_STAMP_FILE = ".tan-sdk-root"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def sdk_stamp_path(slice_cwd: Path) -> Path:
|
|
328
|
+
"""Path of the tan-owned SDK-identity stamp for one slice."""
|
|
329
|
+
return slice_cwd / "build" / _SDK_STAMP_FILE
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def read_sdk_stamp(slice_cwd: Path) -> str | None:
|
|
333
|
+
"""The SDK identity key recorded in the stamp file, if any. `None` covers
|
|
334
|
+
"never stamped", "unreadable", AND "unreadable as UTF-8" -- all three
|
|
335
|
+
read as "no signal", which `sdk_stamp_action` treats as a mismatch on an
|
|
336
|
+
already-configured dir. The value is whatever [`write_sdk_stamp`] wrote
|
|
337
|
+
-- the bare `--sdk-root` path on an older stamp, or
|
|
338
|
+
`tan.core.plan_exec.sdk_stamp_key`'s `<root>@<sdkCommit>` form once the
|
|
339
|
+
plan carries a commit -- this function does not itself parse or care
|
|
340
|
+
which. Port of `manifest.rs::read_sdk_stamp`, whose `std::fs::
|
|
341
|
+
read_to_string` folds invalid-UTF-8 into the SAME `io::Error` a missing
|
|
342
|
+
file raises, one `.ok()`; `UnicodeDecodeError` is a `ValueError`, not an
|
|
343
|
+
`OSError`, so `except OSError` alone let a corrupt (non-UTF-8)
|
|
344
|
+
`.tan-sdk-root` escape this function as an uncaught exception -- verified
|
|
345
|
+
to reach past `execute_slices`, whose own module docstring promises no
|
|
346
|
+
escaping exception."""
|
|
347
|
+
try:
|
|
348
|
+
return sdk_stamp_path(slice_cwd).read_text(encoding="utf-8").strip()
|
|
349
|
+
except (OSError, UnicodeDecodeError):
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def write_sdk_stamp(slice_cwd: Path, key: str) -> None:
|
|
354
|
+
"""Write the stamp BEFORE the tool is spawned (see the executor call
|
|
355
|
+
site): a mid-configure failure still stamped correctly, since the dir
|
|
356
|
+
really was configured against `key` regardless of whether the build
|
|
357
|
+
finishes. `key` is `tan.core.plan_exec.sdk_stamp_key`'s output. Raises
|
|
358
|
+
`OSError` on failure -- the caller treats the write as best-effort (a
|
|
359
|
+
write failure just means the next run may treat this dir as unstamped:
|
|
360
|
+
fails toward a spurious rebuild, not toward trusting a stale one). Port
|
|
361
|
+
of `manifest.rs::write_sdk_stamp`."""
|
|
362
|
+
path = sdk_stamp_path(slice_cwd)
|
|
363
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
364
|
+
path.write_text(key, encoding="utf-8")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def cmake_cache_configured(slice_cwd: Path) -> bool:
|
|
368
|
+
"""Whether west has ever configured this slice's build dir (`<cwd>/build/
|
|
369
|
+
CMakeCache.txt`) -- the "was this dir ever configured" signal
|
|
370
|
+
`sdk_stamp_action` needs before it treats a missing stamp as stale. Port
|
|
371
|
+
of `manifest.rs::cmake_cache_configured`."""
|
|
372
|
+
return (slice_cwd / "build" / "CMakeCache.txt").is_file()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _dir_shows_zephyr(directory: Path) -> bool:
|
|
376
|
+
"""The two per-directory signals behind [`zephyr_boilerplate_loaded`]:
|
|
377
|
+
a `ZEPHYR_BASE:` entry in `directory`'s own `CMakeCache.txt` (the primary
|
|
378
|
+
signal -- what `find_package(Zephyr)` caches, verified against a real
|
|
379
|
+
`<slice>/build/CMakeCache.txt`; a plain host configure never writes one),
|
|
380
|
+
OR a `zephyr/` subdirectory (Zephyr's boilerplate binary dir, kept as an
|
|
381
|
+
OR fallback so the guard can only ever fail a build it is SURE about).
|
|
382
|
+
Port of `manifest.rs::dir_shows_zephyr`, whose `std::fs::read_to_string`
|
|
383
|
+
folds invalid-UTF-8 into the SAME `io::Error` a missing file raises
|
|
384
|
+
(`.is_ok_and(...)` then just falls through to the `zephyr/` fallback);
|
|
385
|
+
`UnicodeDecodeError` is a `ValueError`, not an `OSError`, so `except
|
|
386
|
+
OSError` alone let a non-UTF-8 `CMakeCache.txt` escape this function as
|
|
387
|
+
an uncaught exception -- the same lesson `read_sdk_stamp` above already
|
|
388
|
+
records for the sibling `.tan-sdk-root` read."""
|
|
389
|
+
try:
|
|
390
|
+
cache = (directory / "CMakeCache.txt").read_text(encoding="utf-8")
|
|
391
|
+
except (OSError, UnicodeDecodeError):
|
|
392
|
+
cache = ""
|
|
393
|
+
if any(line.startswith("ZEPHYR_BASE:") for line in cache.splitlines()):
|
|
394
|
+
return True
|
|
395
|
+
return (directory / "zephyr").is_dir()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def zephyr_boilerplate_loaded(slice_cwd: Path) -> bool:
|
|
399
|
+
"""tan-cli#309 (upstream tan-cli #97): whether this slice's build dir
|
|
400
|
+
shows that Zephyr's CMake boilerplate actually ran -- the signal behind
|
|
401
|
+
the `os: zephyr` guard `execute.py`'s status assembly applies.
|
|
402
|
+
|
|
403
|
+
The reported defect: a project whose `CMakeLists.txt` never calls
|
|
404
|
+
`find_package(Zephyr ...)` still configures and links fine under `west
|
|
405
|
+
build -b <board>` (CMake only emits a *dev* warning about the missing
|
|
406
|
+
`project()` call), so a core declared `os: zephyr` produced a host
|
|
407
|
+
binary and the executor reported it `[+] ok`. The board name is never
|
|
408
|
+
even validated, because nothing loaded the code that would validate it.
|
|
409
|
+
|
|
410
|
+
Checked one level down too (not just `<slice_cwd>/build` itself),
|
|
411
|
+
because `--sysbuild` nests the real per-image Zephyr builds one
|
|
412
|
+
directory deeper under its own superbuild -- Zephyr's own
|
|
413
|
+
`share/sysbuild/CMakeLists.txt` calls `find_package(Sysbuild ...)`, not
|
|
414
|
+
`find_package(Zephyr)`, so a sysbuild top level carries neither signal
|
|
415
|
+
and only the nested per-image build does. One level is enough (sysbuild
|
|
416
|
+
nests per-image, not recursively).
|
|
417
|
+
|
|
418
|
+
Callers must skip this check when [`build_dir_overridden`] -- west then
|
|
419
|
+
wrote somewhere this can't see, the same refusal [`resolve_zephyr_artefact`]
|
|
420
|
+
already makes. Port of `manifest.rs::zephyr_boilerplate_loaded`."""
|
|
421
|
+
build = slice_cwd / "build"
|
|
422
|
+
if _dir_shows_zephyr(build):
|
|
423
|
+
return True
|
|
424
|
+
try:
|
|
425
|
+
children = [p for p in build.iterdir() if p.is_dir()]
|
|
426
|
+
except OSError:
|
|
427
|
+
return False
|
|
428
|
+
return any(_dir_shows_zephyr(child) for child in children)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Write every artefact the plan carries under `build_root` -- BEFORE any
|
|
3
|
+
slice runs. That ordering is the stated precondition of the plan's
|
|
4
|
+
slice-independence invariant: `sharedArtefacts` and every slice's
|
|
5
|
+
`configArtefacts` must all exist on disk before dispatch starts, or
|
|
6
|
+
concurrent slice execution (planned later) would race a slice reading a
|
|
7
|
+
config file its own materialise step hasn't written yet.
|
|
8
|
+
|
|
9
|
+
Mirrors `tan-cli/src/commands/build/materialise.rs`'s `MaterialiseError`
|
|
10
|
+
(one type, two failure shapes -- an unsafe path, or an I/O error) rather than
|
|
11
|
+
letting either escape as a bare exception."""
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from tan.core.build_plan import BuildPlan
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MaterialiseError(ValueError):
|
|
19
|
+
"""A structured, coded materialise failure. Subclasses `ValueError` (not
|
|
20
|
+
a plain `Exception`) so `except ValueError` -- the shape a caller reaches
|
|
21
|
+
for around "a plan-supplied path was bad" -- still catches it; `.code`/
|
|
22
|
+
`.message` are there for a caller that wants to fold it into the
|
|
23
|
+
`(code, message)` envelope contract instead (mirrors `PlanParseError`,
|
|
24
|
+
`TokenSubstitutionError`)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, code: str, message: str) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.code = code
|
|
29
|
+
self.message = message
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def confine_to_build_root(build_root: Path, rel: str) -> Path:
|
|
33
|
+
"""Resolve `rel` under `build_root`, refusing anything that escapes it.
|
|
34
|
+
|
|
35
|
+
Plans are trusted input, but a plan-supplied relative path (an
|
|
36
|
+
artefact's `path`, or -- reused by `execute.py` -- a slice command's
|
|
37
|
+
`cwd`) must never let `build_root / rel` land outside `build_root`.
|
|
38
|
+
`Path.is_absolute()` alone is not enough to guard against this on
|
|
39
|
+
Windows: a drive-relative `C:foo` or a rooted `\\foo` is NOT
|
|
40
|
+
`is_absolute()`, yet joining either onto `build_root` discards part or
|
|
41
|
+
all of it (`PurePath.__truediv__` replaces the base outright once the
|
|
42
|
+
right-hand side supplies its own root/drive) -- exactly the shape that
|
|
43
|
+
let a path escape silently. Resolving both sides and requiring
|
|
44
|
+
containment catches every one of these shapes (verified empirically:
|
|
45
|
+
`..`, a bare posix `/x`, `C:x`, and `\\x` all resolve outside
|
|
46
|
+
`build_root.resolve()`), not just the lexical `..` case.
|
|
47
|
+
"""
|
|
48
|
+
root = build_root.resolve()
|
|
49
|
+
target = (build_root / rel).resolve()
|
|
50
|
+
if target != root and root not in target.parents:
|
|
51
|
+
raise MaterialiseError(
|
|
52
|
+
"build.path-escape",
|
|
53
|
+
f"path `{rel}` would escape the build root `{root}`",
|
|
54
|
+
)
|
|
55
|
+
return target
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write(build_root: Path, artefact: dict[str, Any]) -> Path:
|
|
59
|
+
rel = artefact["path"]
|
|
60
|
+
target = confine_to_build_root(build_root, rel)
|
|
61
|
+
try:
|
|
62
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
target.write_text(artefact.get("contents", ""), encoding="utf-8", newline="")
|
|
64
|
+
except OSError as err:
|
|
65
|
+
# Permission denied, a Windows path over MAX_PATH, a parent that's
|
|
66
|
+
# actually a file, target itself being a directory (an empty/`.`
|
|
67
|
+
# artefact path) -- every write-time OSError, never a bare traceback.
|
|
68
|
+
raise MaterialiseError(
|
|
69
|
+
"build.artefact-write-failed", f"failed to write `{rel}`: {err}"
|
|
70
|
+
) from err
|
|
71
|
+
return target
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def materialise_plan(plan: BuildPlan, build_root: Path) -> list[Path]:
|
|
75
|
+
"""Write ALL `sharedArtefacts`, then every slice's `configArtefacts`, and
|
|
76
|
+
only then return. Callers must not start any slice before this returns --
|
|
77
|
+
that ordering is the whole point (see module docstring)."""
|
|
78
|
+
written = [_write(build_root, a) for a in plan.shared_artefacts]
|
|
79
|
+
for sl in plan.slices:
|
|
80
|
+
written.extend(_write(build_root, a) for a in sl.config_artefacts)
|
|
81
|
+
return written
|