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,164 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan bootstrap`'s `zephyr/patches.yml` phase (tan-cli#1296).
|
|
3
|
+
|
|
4
|
+
alp-sdk carries patches against Zephyr and its modules in `zephyr/patches.yml`
|
|
5
|
+
(e.g. `ipm_arm_mhuv2.c`'s `poll_out`, hal_alif's `se_service_boot_cpu`).
|
|
6
|
+
`scripts/bootstrap.sh` applies them and documents them as required to build;
|
|
7
|
+
`tan bootstrap` did not, and a tan-made workspace failed to compile the
|
|
8
|
+
`peripheral-io/hello-world` example for E1M-AEN801 (m55_hp slice) with
|
|
9
|
+
`'struct ipm_driver_api' has no member named 'poll_out'`. This mirrors that
|
|
10
|
+
script's algorithm -- the SDK's own `scripts/verify_west_patches.py` is the
|
|
11
|
+
oracle for "is it applied"; tan only drives it (it must not re-implement the
|
|
12
|
+
SDK's patch knowledge).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from tan.core.west_patches import classify_verify, output_tail, parse_unapplied
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING: # bootstrap_cmd imports THIS module, so no runtime import back
|
|
22
|
+
from tan.commands.bootstrap_cmd import Log, Runner, VenvBin, Workspace
|
|
23
|
+
|
|
24
|
+
#: Issue-code suffixes (`bootstrap.<suffix>`); `FAILED` is a `WORKSPACE_BLOCKING`
|
|
25
|
+
#: member: a tree that lacks the patches cannot build what needs them.
|
|
26
|
+
FAILED = "west-patches-failed"
|
|
27
|
+
UNCHECKED = "west-patches-unchecked"
|
|
28
|
+
|
|
29
|
+
_VERIFIER = Path("scripts") / "verify_west_patches.py"
|
|
30
|
+
_PATCHES_YML = Path("zephyr") / "patches.yml"
|
|
31
|
+
|
|
32
|
+
_OPT_OUT = (
|
|
33
|
+
" Pass `--no-patches` to skip this step (zephyr/patches.yml is then NOT applied, "
|
|
34
|
+
"and builds that need those patches will not compile)."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _detail(*chunks: str) -> str:
|
|
39
|
+
tail = output_tail("\n".join(chunk for chunk in chunks if chunk))
|
|
40
|
+
return f"\n{tail}" if tail else ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fail(log: Log, message: str, *output: str) -> None:
|
|
44
|
+
log.warn(FAILED, f"{message}{_OPT_OUT}{_detail(*output)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _report(
|
|
48
|
+
ws: Workspace, log: Log, verdict: str, out: str, err: str, *, after_apply: bool
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""Act on a verifier verdict. True when the phase is finished; False only
|
|
51
|
+
for the one verdict that means "go apply" (`unapplied`, before applying)."""
|
|
52
|
+
if verdict == "applied":
|
|
53
|
+
when = "verified applied" if after_apply else "already applied"
|
|
54
|
+
log.line(f"zephyr/patches.yml {when} in {ws.workspace_dir}")
|
|
55
|
+
elif verdict == "unchecked":
|
|
56
|
+
# Everything inspectable is patched; a module the file names just is not
|
|
57
|
+
# in this workspace. Normal for a narrow one, so a warning.
|
|
58
|
+
log.warn(
|
|
59
|
+
UNCHECKED,
|
|
60
|
+
"some zephyr/patches.yml modules are not in this workspace, so their patches "
|
|
61
|
+
f"could not be checked.{_detail(out, err)}",
|
|
62
|
+
)
|
|
63
|
+
elif verdict == "unrunnable":
|
|
64
|
+
# Exit 2 / never launched: the patch state is UNKNOWN, so do not claim
|
|
65
|
+
# "not applied".
|
|
66
|
+
_fail(
|
|
67
|
+
log,
|
|
68
|
+
"could not run or inspect the zephyr/patches.yml verifier "
|
|
69
|
+
"(scripts/verify_west_patches.py) -- the patch state is unknown.",
|
|
70
|
+
out, err,
|
|
71
|
+
)
|
|
72
|
+
elif after_apply:
|
|
73
|
+
_fail(
|
|
74
|
+
log,
|
|
75
|
+
f"zephyr/patches.yml is still not applied in {ws.workspace_dir} after "
|
|
76
|
+
"`west patch apply`.",
|
|
77
|
+
out, err,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _apply_missing(
|
|
85
|
+
ws: Workspace, venv: VenvBin, log: Log, runner: Runner, modules: list[str]
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""`west patch apply` per module. False (after recording the error) on the
|
|
88
|
+
first failure."""
|
|
89
|
+
# Late import: `bootstrap_cmd` imports this module, so a top-level import
|
|
90
|
+
# of its `_west_argv` would be circular.
|
|
91
|
+
from tan.commands.bootstrap_cmd import _west_argv
|
|
92
|
+
|
|
93
|
+
for module in modules:
|
|
94
|
+
# PER MODULE, never a bare `west patch apply`: it is not idempotent, so
|
|
95
|
+
# on a partially patched workspace (zephyr patched, mcuboot fresh) it
|
|
96
|
+
# re-applies the patched module's patches and dies on the first.
|
|
97
|
+
# `--dst-module` is a flag of `west patch` ITSELF, so it precedes
|
|
98
|
+
# `apply`; the other order is a usage error.
|
|
99
|
+
log.line(f"Applying zephyr/patches.yml for module '{module}'")
|
|
100
|
+
failure = runner.run(
|
|
101
|
+
_west_argv(venv, ["patch", "--dst-module", module, "apply"]), cwd=ws.workspace_dir
|
|
102
|
+
)
|
|
103
|
+
if failure is not None:
|
|
104
|
+
# `clean` is the zephyr `west patch` default pair `git checkout .`
|
|
105
|
+
# + `git clean -d -f -x` run in the module: it is the way out of a
|
|
106
|
+
# half-applied patch, and it discards uncommitted work there.
|
|
107
|
+
_fail(
|
|
108
|
+
log,
|
|
109
|
+
f"`west patch --dst-module {module} apply` failed, so the module may be "
|
|
110
|
+
f"half-patched. To recover, run `west patch --dst-module {module} clean` "
|
|
111
|
+
f"from {ws.workspace_dir} (it runs `git checkout .` and `git clean -d -f "
|
|
112
|
+
f"-x` in that module, discarding uncommitted changes there), then re-run "
|
|
113
|
+
f"`tan bootstrap`.",
|
|
114
|
+
failure,
|
|
115
|
+
)
|
|
116
|
+
return False
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def patches_phase(
|
|
121
|
+
ws: Workspace, venv: VenvBin, log: Log, runner: Runner, sdk_root: str
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Verify, then apply only what is missing, then verify again.
|
|
124
|
+
|
|
125
|
+
Non-fatal like `pip_phase`/`toolchain_phase`: a failure is a recorded
|
|
126
|
+
`bootstrap.west-patches-failed` (which blocks `complete.` unless
|
|
127
|
+
`--allow-partial`), never an abort, so it cannot skip the toolchain phase.
|
|
128
|
+
"""
|
|
129
|
+
sdk = Path(sdk_root)
|
|
130
|
+
verifier = sdk / _VERIFIER
|
|
131
|
+
if not verifier.is_file() or not (sdk / _PATCHES_YML).is_file():
|
|
132
|
+
# An older SDK tag simply has no patch set to apply -- not an error.
|
|
133
|
+
log.line("Skipping zephyr/patches.yml: this alp-sdk checkout carries no patch verifier")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Run from the SDK root, like bootstrap.sh, so the verifier resolves its
|
|
137
|
+
# own `patches.yml` the way it was written to.
|
|
138
|
+
verify = [
|
|
139
|
+
str(venv.python), str(verifier),
|
|
140
|
+
"--topdir", str(ws.workspace_dir), "--west", str(venv.west),
|
|
141
|
+
]
|
|
142
|
+
code, out, err = runner.run_status(verify, cwd=sdk)
|
|
143
|
+
if runner.dry_run:
|
|
144
|
+
log.line("Would verify zephyr/patches.yml and apply any missing patches (--dry-run)")
|
|
145
|
+
return
|
|
146
|
+
if _report(ws, log, classify_verify(code), out, err, after_apply=False):
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
code, listing, list_err = runner.run_status([*verify, "--list-unapplied"], cwd=sdk)
|
|
150
|
+
modules = parse_unapplied(listing) if code == 0 else []
|
|
151
|
+
if not modules:
|
|
152
|
+
_fail(
|
|
153
|
+
log,
|
|
154
|
+
"zephyr/patches.yml is not applied but the verifier named no module to patch "
|
|
155
|
+
"-- run scripts/verify_west_patches.py directly to see why.",
|
|
156
|
+
out, err, list_err,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if _apply_missing(ws, venv, log, runner, modules):
|
|
161
|
+
# `west patch apply` exits 0 on three do-nothing paths, so its status is
|
|
162
|
+
# not evidence; the verifier is.
|
|
163
|
+
code, out, err = runner.run_status(verify, cwd=sdk)
|
|
164
|
+
_report(ws, log, classify_verify(code), out, err, after_apply=True)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""IO half of tan-cli#1496's toolchain-archive check: download the pinned
|
|
3
|
+
`arm-zephyr-eabi` archive ourselves, hash it against alp-sdk's pin while it
|
|
4
|
+
streams, and extract it into the layout the SDK's own `setup.sh` produces
|
|
5
|
+
(`<sdk>/gnu/arm-zephyr-eabi`). West verifies only the minimal bundle and its
|
|
6
|
+
`setup.sh` downloads this archive unchecked, so west is told
|
|
7
|
+
`--no-gnu-toolchains` and this module does the job (see `tan.core.toolchain_pin`)."""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tarfile
|
|
15
|
+
import tempfile
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from tan.core.proxy import select_https_proxy
|
|
20
|
+
from tan.core.subprocess_env import spawn_env
|
|
21
|
+
from tan.core.toolchain_provision import (
|
|
22
|
+
TMP_SUFFIX_PREFIX,
|
|
23
|
+
TOOLCHAIN_COMPONENT,
|
|
24
|
+
ToolchainArtifact,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
#: The artifact row whose archive west's `setup.sh` would have fetched.
|
|
28
|
+
TOOLCHAIN_ARTIFACT_COMPONENT = f"{TOOLCHAIN_COMPONENT}-toolchain"
|
|
29
|
+
SEVEN_ZIP_EXTRACTORS = ("7z", "7za", "7zr", "7zz", "7zzs")
|
|
30
|
+
_CHUNK = 1 << 20
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class FetchOutcome:
|
|
35
|
+
#: "ok" | "mismatch" (hash differs from the pin) | "unverified" (could not download)
|
|
36
|
+
#: | "install" (downloaded and matched, but could not be extracted/placed).
|
|
37
|
+
kind: str
|
|
38
|
+
message: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def toolchain_artifact(artifacts: tuple[ToolchainArtifact, ...]) -> ToolchainArtifact | None:
|
|
42
|
+
found = [a for a in artifacts if a.component == TOOLCHAIN_ARTIFACT_COMPONENT]
|
|
43
|
+
return found[0] if len(found) == 1 else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _download(url: str, dest: Path, art: ToolchainArtifact) -> FetchOutcome:
|
|
47
|
+
from tan.commands.sdk_cmd import ( # noqa: PLC0415
|
|
48
|
+
NETWORK_TIMEOUT_SECONDS,
|
|
49
|
+
_releases_opener,
|
|
50
|
+
_unroutable_proxy_refusal,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
proxy = select_https_proxy(url)
|
|
54
|
+
refusal = _unroutable_proxy_refusal(proxy, url)
|
|
55
|
+
if refusal is not None:
|
|
56
|
+
return FetchOutcome("unverified", refusal)
|
|
57
|
+
import urllib.request # noqa: PLC0415
|
|
58
|
+
|
|
59
|
+
request = urllib.request.Request(url, headers={"User-Agent": "tan-cli/0"}) # noqa: S310
|
|
60
|
+
digest = hashlib.sha256()
|
|
61
|
+
total = 0
|
|
62
|
+
try:
|
|
63
|
+
with _releases_opener(proxy).open(request, timeout=NETWORK_TIMEOUT_SECONDS) as resp:
|
|
64
|
+
with open(dest, "wb") as out:
|
|
65
|
+
while True:
|
|
66
|
+
chunk = resp.read(_CHUNK)
|
|
67
|
+
if not chunk:
|
|
68
|
+
break
|
|
69
|
+
total += len(chunk)
|
|
70
|
+
if total > art.size_bytes:
|
|
71
|
+
return FetchOutcome(
|
|
72
|
+
"mismatch",
|
|
73
|
+
f"{url} is larger than the {art.size_bytes} bytes alp-sdk pins "
|
|
74
|
+
f"for {art.filename}",
|
|
75
|
+
)
|
|
76
|
+
digest.update(chunk)
|
|
77
|
+
out.write(chunk)
|
|
78
|
+
except Exception as err: # noqa: BLE001 -- every transport failure is a message
|
|
79
|
+
return FetchOutcome("unverified", f"{type(err).__name__}: {err}")
|
|
80
|
+
if total < art.size_bytes:
|
|
81
|
+
return FetchOutcome(
|
|
82
|
+
"unverified",
|
|
83
|
+
f"incomplete download: {total} of the pinned {art.size_bytes} bytes of "
|
|
84
|
+
f"{art.filename} arrived before the stream ended",
|
|
85
|
+
)
|
|
86
|
+
got = digest.hexdigest()
|
|
87
|
+
if got != art.sha256.strip().lower():
|
|
88
|
+
return FetchOutcome(
|
|
89
|
+
"mismatch",
|
|
90
|
+
f"{art.filename} downloaded from {url} has sha256 {got}, but alp-sdk pins "
|
|
91
|
+
f"{art.sha256} (a proxy rewriting the download gives the same symptom)",
|
|
92
|
+
)
|
|
93
|
+
return FetchOutcome("ok")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _find_seven_zip() -> str | None:
|
|
97
|
+
"""Absolute path of the first 7-Zip on PATH that is not inside the current
|
|
98
|
+
directory. The bare name is never spawned: on Windows CreateProcess searches the
|
|
99
|
+
parent's cwd first, so a planted `7z.exe` in the project would run (cf. #1483)."""
|
|
100
|
+
from tan.core.tool_lookup import resolve_tool # noqa: PLC0415
|
|
101
|
+
|
|
102
|
+
cwd = Path(os.getcwd()).resolve()
|
|
103
|
+
for name in SEVEN_ZIP_EXTRACTORS:
|
|
104
|
+
found = resolve_tool(name, os.environ).resolved
|
|
105
|
+
if found is None or not Path(found).is_absolute():
|
|
106
|
+
continue
|
|
107
|
+
real = Path(found).resolve()
|
|
108
|
+
if real == cwd or cwd in real.parents:
|
|
109
|
+
continue
|
|
110
|
+
return str(real)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract(archive: Path, dest: Path) -> str | None:
|
|
115
|
+
"""Error text, or None. `.tar.xz` via tarfile's `data` filter (no absolute paths,
|
|
116
|
+
no escapes, no device nodes); `.7z` via a 7-Zip binary (Windows' archive format)."""
|
|
117
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
try:
|
|
119
|
+
if archive.name.endswith(".7z"):
|
|
120
|
+
program = _find_seven_zip()
|
|
121
|
+
if program is None:
|
|
122
|
+
return "no usable 7-Zip on PATH to extract the .7z toolchain archive"
|
|
123
|
+
proc = subprocess.run( # noqa: S603
|
|
124
|
+
[program, "x", "-y", f"-o{dest}", "--", str(archive)],
|
|
125
|
+
capture_output=True, text=True, check=False, env=spawn_env(),
|
|
126
|
+
)
|
|
127
|
+
return None if proc.returncode == 0 else f"{program} exited {proc.returncode}"
|
|
128
|
+
with tarfile.open(archive, "r:*") as tar:
|
|
129
|
+
tar.extractall(dest, filter="data")
|
|
130
|
+
except (OSError, tarfile.TarError, subprocess.SubprocessError) as err:
|
|
131
|
+
return f"{type(err).__name__}: {err}"
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def install_pinned_toolchain(
|
|
136
|
+
base_url: str, art: ToolchainArtifact, tmp_dir: Path, scratch_parent: Path, leaf: str
|
|
137
|
+
) -> FetchOutcome:
|
|
138
|
+
"""Download + hash + extract `art` into `<tmp_dir>/gnu/` (the layout `setup.sh`
|
|
139
|
+
gives). The scratch dir is a swept `<leaf>.tmp-*` sibling, deleted on every path."""
|
|
140
|
+
url = base_url.rstrip("/") + "/" + art.filename
|
|
141
|
+
with tempfile.TemporaryDirectory(
|
|
142
|
+
dir=scratch_parent, prefix=f"{leaf}{TMP_SUFFIX_PREFIX}dl-"
|
|
143
|
+
) as scratch:
|
|
144
|
+
archive = Path(scratch) / art.filename
|
|
145
|
+
got = _download(url, archive, art)
|
|
146
|
+
if got.kind != "ok":
|
|
147
|
+
return got
|
|
148
|
+
err = _extract(archive, tmp_dir / "gnu")
|
|
149
|
+
if err is not None:
|
|
150
|
+
return FetchOutcome("install", f"extracting {art.filename} failed: {err}")
|
|
151
|
+
if not (tmp_dir / "gnu" / TOOLCHAIN_COMPONENT).is_dir():
|
|
152
|
+
return FetchOutcome(
|
|
153
|
+
"install", f"{art.filename} did not extract a gnu/{TOOLCHAIN_COMPONENT} directory"
|
|
154
|
+
)
|
|
155
|
+
return FetchOutcome("ok")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""IO half of tan-cli#1496: fetch the release `sha256.sum` through the same
|
|
3
|
+
proxy decision + OS-trust-store TLS context `tan sdk list --online` uses, then
|
|
4
|
+
hand the text to `tan.core.toolchain_pin`. Only this small file is downloaded."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from tan.core.proxy import select_https_proxy
|
|
8
|
+
|
|
9
|
+
#: Cap on the sum file. The real one is ~100 KiB; anything bigger is not it.
|
|
10
|
+
MAX_SUM_BYTES = 4 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fetch_sum_text(url: str) -> tuple[str | None, str | None]:
|
|
14
|
+
"""`(text, None)` or `(None, why)`. Never raises: every transport failure is a
|
|
15
|
+
message, so the caller always refuses visibly instead of passing silently."""
|
|
16
|
+
from tan.commands.sdk_cmd import ( # noqa: PLC0415
|
|
17
|
+
NETWORK_TIMEOUT_SECONDS,
|
|
18
|
+
_releases_opener,
|
|
19
|
+
_unroutable_proxy_refusal,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
proxy = select_https_proxy(url)
|
|
23
|
+
refusal = _unroutable_proxy_refusal(proxy, url)
|
|
24
|
+
if refusal is not None:
|
|
25
|
+
return None, refusal
|
|
26
|
+
import urllib.request # noqa: PLC0415
|
|
27
|
+
|
|
28
|
+
request = urllib.request.Request(url, headers={"User-Agent": "tan-cli/0"}) # noqa: S310
|
|
29
|
+
try:
|
|
30
|
+
with _releases_opener(proxy).open(request, timeout=NETWORK_TIMEOUT_SECONDS) as resp:
|
|
31
|
+
raw = resp.read(MAX_SUM_BYTES + 1)
|
|
32
|
+
except Exception as err: # noqa: BLE001 -- see docstring
|
|
33
|
+
return None, f"{type(err).__name__}: {err}"
|
|
34
|
+
if len(raw) > MAX_SUM_BYTES:
|
|
35
|
+
return None, f"response exceeds {MAX_SUM_BYTES} bytes"
|
|
36
|
+
return raw.decode("utf-8", errors="replace"), None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""tan-cli#655: the file-existence side of Zephyr's own devicetree-overlay /
|
|
3
|
+
Kconfig-fragment auto-discovery, tracked across `tan build` invocations so
|
|
4
|
+
`execute.py` can tell when the discoverable SET changed (a new `app.overlay`
|
|
5
|
+
appeared, an existing one was removed, a new `boards/<board>.conf` showed up)
|
|
6
|
+
-- the one case Zephyr's own `CMAKE_CONFIGURE_DEPENDS` machinery does not
|
|
7
|
+
self-heal, because `configuration_files.cmake` caches its auto-discovery
|
|
8
|
+
result the first time it runs and only re-discovers `if(NOT DEFINED ...)`.
|
|
9
|
+
See `execute._maybe_reset_stale_configure_cache` for the fix this supports
|
|
10
|
+
and the measurement behind it.
|
|
11
|
+
|
|
12
|
+
Existence-only, not content: an EDIT to an ALREADY-discovered file is already
|
|
13
|
+
handled by Zephyr's own `CMAKE_CONFIGURE_DEPENDS` (the tracked file's mtime
|
|
14
|
+
bump alone triggers a reconfigure) -- this module exists only for the set-
|
|
15
|
+
membership change that mechanism cannot see."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path, PurePath
|
|
20
|
+
|
|
21
|
+
from tan.core.shapes import is_file, matches_glob_suffix
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"configure_inputs_stamp_path",
|
|
25
|
+
"discover_configure_inputs",
|
|
26
|
+
"read_configure_inputs_stamp",
|
|
27
|
+
"relative_key",
|
|
28
|
+
"resolve_zephyr_discovery_dir",
|
|
29
|
+
"write_configure_inputs_stamp",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
#: Lives inside west's own nested build dir (mirrors `manifest.py`'s
|
|
33
|
+
#: `_SDK_STAMP_FILE`/`sdk_stamp_path`), so a pristine wipe of that dir retires
|
|
34
|
+
#: this stamp atomically along with the CMake cache it describes -- it can
|
|
35
|
+
#: never outlive or misdescribe a build dir that no longer exists.
|
|
36
|
+
_CONFIGURE_INPUTS_STAMP_FILE = ".tan-configure-inputs"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_zephyr_discovery_dir(app_dir: str, build_root: Path) -> Path:
|
|
40
|
+
"""Anchor a slice's plan-supplied `appDir` on `build_root`, then apply
|
|
41
|
+
Zephyr's own `app: ./src`-vs-self-contained convention, so a caller
|
|
42
|
+
working from the CONFIGURED app dir (`appDir` -- see
|
|
43
|
+
`tan.commands.build_cmd`'s "Note `appDir` is NOT the substituted value"
|
|
44
|
+
docstring) lands on the directory Zephyr actually auto-discovers
|
|
45
|
+
overlays/Kconfig fragments from -- the same one `west build` is pointed
|
|
46
|
+
at (tan-cli#798).
|
|
47
|
+
|
|
48
|
+
Anchoring mirrors both existing `appDir` probes,
|
|
49
|
+
`build_cmd._missing_app_dirs` and `build_cmd._substituted_app_dirs`: a
|
|
50
|
+
relative `appDir` resolves against `build_root`, never the `tan`
|
|
51
|
+
process's own CWD, so the result does not depend on where `tan` was
|
|
52
|
+
invoked from. The fallback mirrors `tan.planner.orchestrator`'s
|
|
53
|
+
`_zephyr_app_dir` (hash-audited, tan-cli#655's relocation gate -- not
|
|
54
|
+
importable here without reproducing its `_resolve_app_path` machinery
|
|
55
|
+
for a value, `appDir`, that is already resolved): when `app_dir` itself
|
|
56
|
+
carries no `CMakeLists.txt` but its parent does, Zephyr auto-discovers
|
|
57
|
+
from the parent instead (the shipped `app: ./src` sources-only
|
|
58
|
+
convention -- 96 of 105 alp-sdk example core entries use it). Neither
|
|
59
|
+
directory existing is reported as-is; the caller's own existence checks
|
|
60
|
+
(`discover_configure_inputs` returns the empty set for a missing dir)
|
|
61
|
+
already handle that case correctly without this function raising.
|
|
62
|
+
|
|
63
|
+
Unlike `build_cmd._substituted_app_dirs`, this does NOT carry an
|
|
64
|
+
`app_dir_path.is_dir()` precondition before falling back to the parent --
|
|
65
|
+
a nonexistent `app_dir` whose parent happens to carry a `CMakeLists.txt`
|
|
66
|
+
globs the parent here. Deliberately left unguarded: this function's only
|
|
67
|
+
caller runs on slices that already survived `_missing_app_dirs`
|
|
68
|
+
(`build.app-dir-missing` already failed a nonexistent `appDir` before
|
|
69
|
+
`execute_slices` dispatches it), so the divergence is unreachable in
|
|
70
|
+
practice, not merely tolerated."""
|
|
71
|
+
app_dir_path = Path(app_dir)
|
|
72
|
+
if not app_dir_path.is_absolute():
|
|
73
|
+
app_dir_path = build_root / app_dir_path
|
|
74
|
+
if (app_dir_path / "CMakeLists.txt").is_file():
|
|
75
|
+
return app_dir_path
|
|
76
|
+
if (app_dir_path.parent / "CMakeLists.txt").is_file():
|
|
77
|
+
return app_dir_path.parent
|
|
78
|
+
return app_dir_path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
#: The two auto-discovery families `cmake/modules/configuration_files.cmake`
|
|
82
|
+
#: resolves `if(NOT DEFINED ...)`: Kconfig fragments (`CONF_FILE`, `NAMES
|
|
83
|
+
#: "prj.conf"` at the app root plus `boards/`/`socs/` qualifiers) and
|
|
84
|
+
#: devicetree overlays (`DTC_OVERLAY_FILE`, `NAMES "app.overlay"` plus the
|
|
85
|
+
#: same `boards/`/`socs/` qualifiers). Not an attempt to reproduce Zephyr's
|
|
86
|
+
#: exact per-qualifier candidate-name algorithm (board revision, `SUFFIX`,
|
|
87
|
+
#: sysbuild image scoping -- all of which drift across Zephyr versions);
|
|
88
|
+
#: tracking every `*.conf`/`*.overlay` at these locations is a superset that
|
|
89
|
+
#: stays correct even when it over-tracks a file Zephyr itself would not have
|
|
90
|
+
#: picked for THIS exact board target -- an extra reset is harmless, a missed
|
|
91
|
+
#: one is the bug this module exists to close.
|
|
92
|
+
_CANDIDATE_SUFFIXES = (".conf", ".overlay")
|
|
93
|
+
|
|
94
|
+
#: The two qualifier subtrees, walked to ANY depth below themselves -- the
|
|
95
|
+
#: `boards/**/*.conf` half of what this module used to express as six
|
|
96
|
+
#: `Path.glob` patterns. `**` matched zero or more directories, so a file
|
|
97
|
+
#: directly in `boards/` counted too; the walk below reproduces that by
|
|
98
|
+
#: starting at the subtree root itself, not at its children.
|
|
99
|
+
_CANDIDATE_SUBTREES = ("boards", "socs")
|
|
100
|
+
|
|
101
|
+
# WHY `os.scandir` AND NOT `Path.glob` (tan-cli#1132, following tan-cli#1127's
|
|
102
|
+
# identical swap in `new_som_cmd._known_board_names`).
|
|
103
|
+
#
|
|
104
|
+
# This module used to iterate `app_dir.glob(pattern)` OUTSIDE the `try` that
|
|
105
|
+
# was supposed to guard it:
|
|
106
|
+
#
|
|
107
|
+
# try:
|
|
108
|
+
# matches = app_dir.glob(pattern) # lazy: builds a generator
|
|
109
|
+
# except OSError:
|
|
110
|
+
# continue
|
|
111
|
+
# for path in matches: # the filesystem work happens HERE
|
|
112
|
+
#
|
|
113
|
+
# `Path.glob` is lazy, so the guarded statement could not fail and the
|
|
114
|
+
# `except OSError` was dead on every interpreter -- it wrapped the
|
|
115
|
+
# construction of a generator. Forcing iteration inside the `try` would have
|
|
116
|
+
# fixed the escape but not the contract, because `Path.glob` does not raise
|
|
117
|
+
# uniformly. Measured on this tree, non-root, for the two denied shapes
|
|
118
|
+
# (`app_dir` itself `chmod 000`, and `app_dir`'s PARENT `chmod 000`):
|
|
119
|
+
#
|
|
120
|
+
# primitive self-denied ancestor-denied
|
|
121
|
+
# Path.is_dir() True / True / True RAISE / RAISE / False
|
|
122
|
+
# list(Path.glob("*.conf")) [] / [] / [] RAISE / [] / []
|
|
123
|
+
# os.listdir() / os.scandir() RAISE / RAISE / RAISE RAISE / RAISE / RAISE
|
|
124
|
+
# (3.12.3 / 3.13.15 / 3.14.7)
|
|
125
|
+
#
|
|
126
|
+
# So `Path.glob` never raises at all for a self-denied directory, and raises
|
|
127
|
+
# for a denied ancestor only on 3.12.3 -- an `except OSError` around it is
|
|
128
|
+
# dead code on 3.13.15 and 3.14.7. `os.scandir` raises `PermissionError` for
|
|
129
|
+
# both shapes on all three, which is what makes the single `except OSError`
|
|
130
|
+
# in `discover_configure_inputs` load-bearing everywhere rather than on one
|
|
131
|
+
# interpreter by accident. The `if not app_dir.is_dir()` pre-flight is gone
|
|
132
|
+
# for the same reason (tan-cli#1127, and PR #1110/#1121 before it): it
|
|
133
|
+
# raised `PermissionError` on 3.12.3/3.13.15 for a denied ancestor and
|
|
134
|
+
# answered `False` on 3.14.7, so it could not be the thing that decides this
|
|
135
|
+
# function's contract. The real exception classifies it now.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def relative_key(path: PurePath, base: PurePath) -> str:
|
|
139
|
+
"""@path relative to @base, spelled with forward slashes -- THE stamp
|
|
140
|
+
file's key, and the one expression `_candidate_files` uses to build one.
|
|
141
|
+
|
|
142
|
+
A separate function so the Windows spelling can be DRIVEN rather than
|
|
143
|
+
restated (tan-cli#1132 review): `os.scandir` hands back a
|
|
144
|
+
backslash-separated `entry.path` on Windows, so the key must be proven to
|
|
145
|
+
normalise. Taking `PurePath` (not `Path`) is what makes that provable on
|
|
146
|
+
ANY host without touching a filesystem, which is the point -- CI does run
|
|
147
|
+
this suite on `windows-latest` (`parity.yml`'s `python-tests-shard`, whose
|
|
148
|
+
matrix `os:` carries all three platforms; tan-cli#1153 corrects the "no
|
|
149
|
+
Windows host" claim that stood here), but only after a push, whereas a
|
|
150
|
+
pure-string oracle answers while the code is being written. A test passes
|
|
151
|
+
real `PureWindowsPath` values through THIS function, so a future edit here
|
|
152
|
+
(say, to `os.path.relpath`, which would leave backslashes in) reds the
|
|
153
|
+
oracle instead of sailing past a restatement of the old expression. The
|
|
154
|
+
keys are compared across runs, so a changed spelling would report every
|
|
155
|
+
tracked file as new on the first build after an upgrade."""
|
|
156
|
+
return path.relative_to(base).as_posix()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _candidate_files(directory: Path, base: Path, *, recurse: bool) -> set[str]:
|
|
160
|
+
"""Relative-POSIX paths (to @base) of the regular files directly in
|
|
161
|
+
@directory -- and, when @recurse, at any depth below it -- whose name
|
|
162
|
+
matches `_CANDIDATE_SUFFIXES`.
|
|
163
|
+
|
|
164
|
+
Raises `OSError` (`PermissionError`, `FileNotFoundError`,
|
|
165
|
+
`NotADirectoryError`, ...) for any directory it cannot list; the single
|
|
166
|
+
caller classifies. See the block comment above for why that is
|
|
167
|
+
`os.scandir` and not `Path.glob`.
|
|
168
|
+
|
|
169
|
+
Three `Path.glob` behaviours this deliberately reproduces rather than
|
|
170
|
+
quietly improving on:
|
|
171
|
+
|
|
172
|
+
* A symlinked directory is NOT descended into -- `**` does not follow
|
|
173
|
+
symlinks on any supported interpreter (3.13's own
|
|
174
|
+
`recurse_symlinks=False` default), which is what
|
|
175
|
+
`entry.is_dir(follow_symlinks=False)` preserves.
|
|
176
|
+
* A name matching a candidate suffix that is a DIRECTORY is not a match,
|
|
177
|
+
the old code's `path.is_file()` filter.
|
|
178
|
+
* A per-entry stat failure SKIPS that entry rather than failing the
|
|
179
|
+
walk. That is why the file test is `shapes.is_file` and not
|
|
180
|
+
`entry.is_file()`: measured on 3.12.3, 3.13.15 and 3.14.7 alike, a
|
|
181
|
+
self-referential `loop.conf -> loop.conf` symlink makes
|
|
182
|
+
`os.DirEntry.is_file()` raise `OSError(ELOOP)` where `Path.is_file()`
|
|
183
|
+
-- what the `glob` version called -- returns `False` and keeps the
|
|
184
|
+
rest of the set. Only the DIRECTORY LISTING is allowed to fail the
|
|
185
|
+
walk, and `os.scandir` above is the call that does it.
|
|
186
|
+
|
|
187
|
+
Symlinks are otherwise followed for the file test, so a symlink TO a
|
|
188
|
+
fragment still counts, exactly as it did through `glob` + `is_file()`."""
|
|
189
|
+
found: set[str] = set()
|
|
190
|
+
pending = [directory]
|
|
191
|
+
while pending:
|
|
192
|
+
with os.scandir(pending.pop()) as entries:
|
|
193
|
+
for entry in entries:
|
|
194
|
+
if recurse and entry.is_dir(follow_symlinks=False):
|
|
195
|
+
pending.append(Path(entry.path))
|
|
196
|
+
elif matches_glob_suffix(entry.name, *_CANDIDATE_SUFFIXES) and is_file(entry.path):
|
|
197
|
+
found.add(relative_key(Path(entry.path), base))
|
|
198
|
+
return found
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def discover_configure_inputs(app_dir: Path) -> frozenset[str]:
|
|
202
|
+
"""The set of configure-time Kconfig-fragment/devicetree-overlay paths
|
|
203
|
+
(relative POSIX, to `app_dir`) currently present under `app_dir` at one
|
|
204
|
+
of Zephyr's own auto-discovery locations. Returns the empty set for a
|
|
205
|
+
missing or unreadable `app_dir`, and for an unreadable `boards/`/`socs/`
|
|
206
|
+
subtree under it -- "nothing discoverable" is the same honest answer a
|
|
207
|
+
real Zephyr configure would reach, not an error this best-effort tracker
|
|
208
|
+
should raise.
|
|
209
|
+
|
|
210
|
+
An ABSENT `boards/`/`socs/` is the ordinary case, not a failure: it
|
|
211
|
+
contributes nothing and the app-root fragments still come back. That is
|
|
212
|
+
the inner handler's whole job, and it is why the outer one cannot simply
|
|
213
|
+
swallow every `OSError` from the whole walk."""
|
|
214
|
+
found: set[str] = set()
|
|
215
|
+
try:
|
|
216
|
+
found |= _candidate_files(app_dir, app_dir, recurse=False)
|
|
217
|
+
for subtree in _CANDIDATE_SUBTREES:
|
|
218
|
+
try:
|
|
219
|
+
found |= _candidate_files(app_dir / subtree, app_dir, recurse=True)
|
|
220
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
221
|
+
continue
|
|
222
|
+
except OSError:
|
|
223
|
+
return frozenset()
|
|
224
|
+
return frozenset(found)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def configure_inputs_stamp_path(slice_cwd: Path) -> Path:
|
|
228
|
+
"""Path of the tan-owned configure-inputs stamp for one slice."""
|
|
229
|
+
return slice_cwd / "build" / _CONFIGURE_INPUTS_STAMP_FILE
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def read_configure_inputs_stamp(slice_cwd: Path) -> frozenset[str] | None:
|
|
233
|
+
"""The configure-input file set recorded at the last stamp write, or
|
|
234
|
+
`None` for "never stamped", "unreadable", or "unreadable as UTF-8" --
|
|
235
|
+
all three read as "no signal", which the caller treats conservatively
|
|
236
|
+
(as a change, forcing one reset) rather than trusting a cache it cannot
|
|
237
|
+
actually verify. An empty stamped file (no candidate files at all) reads
|
|
238
|
+
back as the real empty set, `frozenset()`, not `None`."""
|
|
239
|
+
try:
|
|
240
|
+
text = configure_inputs_stamp_path(slice_cwd).read_text(encoding="utf-8")
|
|
241
|
+
except (OSError, UnicodeDecodeError):
|
|
242
|
+
return None
|
|
243
|
+
return frozenset(line for line in text.splitlines() if line)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def write_configure_inputs_stamp(slice_cwd: Path, inputs: frozenset[str]) -> None:
|
|
247
|
+
"""Write the stamp BEFORE the tool is spawned (see the caller): a
|
|
248
|
+
mid-configure failure still stamped correctly, since the discoverable
|
|
249
|
+
set on disk really was what `inputs` records regardless of whether the
|
|
250
|
+
build finishes. One relative path per line, sorted for a stable diff.
|
|
251
|
+
Raises `OSError` on failure -- the caller treats the write as
|
|
252
|
+
best-effort (a write failure just means the next run may treat this dir
|
|
253
|
+
as unstamped: fails toward a spurious extra reset, never toward trusting
|
|
254
|
+
a stale cache)."""
|
|
255
|
+
path = configure_inputs_stamp_path(slice_cwd)
|
|
256
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
path.write_text("\n".join(sorted(inputs)) + ("\n" if inputs else ""), encoding="utf-8")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _user_defines_stamp_path(slice_cwd: Path) -> Path:
|
|
261
|
+
return slice_cwd / "build" / ".tan-user-defines"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def read_user_defines_stamp(slice_cwd: Path) -> list[str] | None:
|
|
265
|
+
"""The user `-D` key names of the last configure, or `None` if unstamped."""
|
|
266
|
+
try:
|
|
267
|
+
text = _user_defines_stamp_path(slice_cwd).read_text(encoding="utf-8")
|
|
268
|
+
except (OSError, UnicodeDecodeError):
|
|
269
|
+
return None
|
|
270
|
+
return [line for line in text.splitlines() if line]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def write_user_defines_stamp(slice_cwd: Path, keys: list[str]) -> None:
|
|
274
|
+
"""Best-effort for the caller (raises `OSError`), like the inputs stamp."""
|
|
275
|
+
path = _user_defines_stamp_path(slice_cwd)
|
|
276
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
path.write_text("\n".join(sorted(keys)) + ("\n" if keys else ""), encoding="utf-8")
|