tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/core/venv.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Workspace-venv resolution, shared by every command that spawns a tool the
|
|
3
|
+
`tan bootstrap` venv owns.
|
|
4
|
+
|
|
5
|
+
Port of `crates/tan-cli/src/venv.rs`. `west` normally lives ONLY inside the
|
|
6
|
+
bootstrapped venv -- nothing activates it for a GUI-launched editor, so the
|
|
7
|
+
ambient PATH has no `west` at all. This module is what lets `tan` find it
|
|
8
|
+
anyway.
|
|
9
|
+
|
|
10
|
+
Moved here from `tan.commands.build_cmd` (tan-cli#289): `_find_workspace_venv`
|
|
11
|
+
was private to that module but already imported across two others
|
|
12
|
+
(`west_forward_cmd.py`, and indirectly `generate_cmd.py`/`model_cmd.py`/
|
|
13
|
+
`monitor_cmd.py`/`new_som_cmd.py` via `_planner_python`) -- and three more
|
|
14
|
+
consumers (`build.execute`'s slice dispatch, `flash_cmd`'s tool gate and
|
|
15
|
+
spawn) needed the SAME search wired in rather than a second copy of it. This
|
|
16
|
+
is the one shared module `flash_cmd.py`'s own docstring asked for.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from tan.core.bootstrap import VenvLayout, manifest_points_at, venv_layout
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_layout(venv: Path) -> VenvLayout | None:
|
|
27
|
+
"""Which bin directory actually has `west`, POSIX first -- the SAME
|
|
28
|
+
directory-wins rule creation already applies
|
|
29
|
+
(`bootstrap_cmd.Workspace.venv_bin`/`venv_present`), so a
|
|
30
|
+
Git-Bash/MSYS-created `Scripts/` venv (or a `bin/` venv found on a
|
|
31
|
+
Windows host) that bootstrap accepted is not invisible here. `None` when
|
|
32
|
+
`west` is present under neither layout.
|
|
33
|
+
|
|
34
|
+
Deliberately NOT `venv_layout(os.name == "nt")` (host-only): that was
|
|
35
|
+
tan-cli#291 -- bootstrap already derives the executable NAMES from the
|
|
36
|
+
winning directory (`core.bootstrap.venv_exe_names`), not the host, and
|
|
37
|
+
resolution disagreeing let a venv bootstrap just populated be invisible
|
|
38
|
+
to `build`/`flash`. The Rust oracle (`crates/tan-cli/src/venv.rs`) is
|
|
39
|
+
still host-only; tan-cli#291 identifies that as a latent oracle bug this
|
|
40
|
+
port inherited, not a divergence to keep copying now that creation has
|
|
41
|
+
already fixed it on its side (`3879cae`, tan-cli#289).
|
|
42
|
+
"""
|
|
43
|
+
for is_windows in (False, True):
|
|
44
|
+
layout = venv_layout(is_windows)
|
|
45
|
+
if (venv / layout.bin_dir / layout.west).is_file():
|
|
46
|
+
return layout
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_workspace_venv(start: str, sdk_root: str | None) -> Path | None:
|
|
51
|
+
"""Locate the west-capable workspace `.venv`, mirroring Rust's
|
|
52
|
+
`find_workspace_venv` but resolving the bin-dir layout directory-wins
|
|
53
|
+
(see `_resolve_layout`) rather than host-only: gated on `west` actually
|
|
54
|
+
being present under EITHER layout (not just the directory existing), in
|
|
55
|
+
this order:
|
|
56
|
+
|
|
57
|
+
1. a `.venv` in the project tree, searched from `start` upward;
|
|
58
|
+
2. the workspace venv derived from `$ZEPHYR_BASE`
|
|
59
|
+
(`<ZEPHYR_BASE>/../.venv`), manifest-guarded against `sdk_root` (see
|
|
60
|
+
`_zephyr_base_venv`, tan-cli#292 consequence 2);
|
|
61
|
+
3. the SDK's canonical `<sdk-parent>/.venv` (post-alp-sdk#782) or the
|
|
62
|
+
legacy `<sdk-parent>/zephyrproject/.venv`.
|
|
63
|
+
|
|
64
|
+
`None` when none resolve (CI, an activated venv, the contract harness).
|
|
65
|
+
|
|
66
|
+
**tan-cli#495 defect 1: step 1 skips an ancestor that IS a west topdir
|
|
67
|
+
whose manifest is not alp-sdk's.** Step 1 used to accept any `.venv` with
|
|
68
|
+
a `west` in it, with no `sdk_root` involvement at all, while the sibling
|
|
69
|
+
resolver `west_workspace_dir` (below) rejects that same ancestor via its
|
|
70
|
+
tan-cli#307 `manifest_ok` guard -- so on a dirty host the two DISAGREED:
|
|
71
|
+
`tan build` spawned the ancestor venv's `west` with `cwd` pinned to the
|
|
72
|
+
correct workspace, and baked the ancestor venv's `python` into every
|
|
73
|
+
Zephyr slice as `-DPython3_EXECUTABLE`. That interpreter has none of
|
|
74
|
+
`zephyr/scripts/requirements.txt`, so `west build` died inside Zephyr's
|
|
75
|
+
own CMake configure naming Zephyr rather than tan -- the exact class
|
|
76
|
+
tan-cli#307 closed for the topdir, mirrored onto the binary. Worse when
|
|
77
|
+
the ancestor is a FULL Zephyr venv: no failure at all, just a silent
|
|
78
|
+
build against a different Zephyr's pinned requirements.
|
|
79
|
+
|
|
80
|
+
Deliberately NOT a naive mirror of #307's guard: `manifest_points_at` is
|
|
81
|
+
False for a project directory with no `.west` at all, so requiring it
|
|
82
|
+
outright would reject the SUPPORTED project-local `.venv` -- the exact
|
|
83
|
+
shape #307's own e2e test plants at `build_root/.venv`. The guard is
|
|
84
|
+
therefore conditional on the candidate's directory actually HOLDING a
|
|
85
|
+
`.west`: a directory that claims to be a west topdir must be the right
|
|
86
|
+
one, and a directory that claims nothing is judged on its `.venv` alone,
|
|
87
|
+
exactly as before.
|
|
88
|
+
"""
|
|
89
|
+
directory: Path | None = Path(start)
|
|
90
|
+
while directory is not None:
|
|
91
|
+
candidate = directory / ".venv"
|
|
92
|
+
if _resolve_layout(candidate) is not None and _not_a_foreign_topdir(directory, sdk_root):
|
|
93
|
+
return candidate
|
|
94
|
+
parent = directory.parent
|
|
95
|
+
directory = parent if parent != directory else None
|
|
96
|
+
|
|
97
|
+
zephyr_base = os.environ.get("ZEPHYR_BASE")
|
|
98
|
+
if zephyr_base:
|
|
99
|
+
candidate = _zephyr_base_venv(Path(zephyr_base), sdk_root)
|
|
100
|
+
if candidate is not None:
|
|
101
|
+
return candidate
|
|
102
|
+
|
|
103
|
+
if sdk_root is not None:
|
|
104
|
+
parent = Path(sdk_root).parent
|
|
105
|
+
for workspace in (parent, parent / "zephyrproject"):
|
|
106
|
+
candidate = workspace / ".venv"
|
|
107
|
+
if _resolve_layout(candidate) is not None:
|
|
108
|
+
return candidate
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _not_a_foreign_topdir(directory: Path, sdk_root: str | None) -> bool:
|
|
114
|
+
"""Whether `directory` may supply the workspace venv (tan-cli#495 defect 1).
|
|
115
|
+
|
|
116
|
+
True in the two cases that carry no claim to check -- `sdk_root` did not
|
|
117
|
+
resolve, so there is nothing to verify against (the same "nothing to check"
|
|
118
|
+
fallback `_zephyr_base_venv` and `west_workspace_dir.manifest_ok` already
|
|
119
|
+
apply), or `directory` is not a west topdir, which is the ordinary
|
|
120
|
+
project-local `.venv`. False ONLY for a directory that really is a west
|
|
121
|
+
topdir whose manifest does not point at `sdk_root`: that one is a different
|
|
122
|
+
workspace, and its venv is a different Zephyr's.
|
|
123
|
+
|
|
124
|
+
**Topdir-ness is `.west/config`, not the `.west` DIRECTORY.** An earlier
|
|
125
|
+
cut tested `(directory / ".west").is_dir()` and broke `tan doctor`'s
|
|
126
|
+
`venvProvenance` check outright: tan writes its OWN provenance record to
|
|
127
|
+
`<workspace>/.west/tan-workspace-sdk` (`bootstrap_cmd.
|
|
128
|
+
record_workspace_sdk`), so a workspace carrying that record but no west
|
|
129
|
+
config -- every workspace between `record_workspace_sdk` and a completed
|
|
130
|
+
`west init` -- has a `.west` directory that is not a topdir. It was judged
|
|
131
|
+
foreign, its venv was refused, and the check vanished from the report
|
|
132
|
+
instead of passing. `manifest_points_at` reads `.west/config`, so keying on
|
|
133
|
+
that same file is what makes the guard and its verdict agree.
|
|
134
|
+
"""
|
|
135
|
+
if sdk_root is None or not (directory / ".west" / "config").is_file():
|
|
136
|
+
return True
|
|
137
|
+
return manifest_points_at(directory, Path(sdk_root))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _zephyr_base_venv(zephyr_base: Path, sdk_root: str | None) -> Path | None:
|
|
141
|
+
"""Step 2 of `find_workspace_venv`: `zephyr_base`'s PARENT's `.venv`, but
|
|
142
|
+
only when it is both west-capable AND (when `sdk_root` resolved) its
|
|
143
|
+
workspace manifest is verifiably alp-sdk's -- port of the SAME guard
|
|
144
|
+
`_zephyr_base_workspace` already applies to the topdir search
|
|
145
|
+
(tan-cli#61), which this venv search lacked (tan-cli#292 consequence 2,
|
|
146
|
+
kept consistent with the tan-cli#301 fix one layer up: both now source
|
|
147
|
+
"which Zephyr/venv" from the RESOLVED workspace rather than a bare
|
|
148
|
+
`$ZEPHYR_BASE` read).
|
|
149
|
+
|
|
150
|
+
Without this, a stale exported `$ZEPHYR_BASE` naming an unrelated Zephyr
|
|
151
|
+
workspace shadowed the canonical `<sdk-parent>/.venv` (step 3) even when
|
|
152
|
+
the caller passed an explicit `--sdk-root` -- reproduced against the
|
|
153
|
+
published v0.5.0-rc2 asset on a clean host with no alp-sdk checkout
|
|
154
|
+
anywhere (tan-cli#278). `sdk_root` absent means there is nothing to
|
|
155
|
+
verify against, so the old unconditional accept stands, exactly as
|
|
156
|
+
`_zephyr_base_workspace` does.
|
|
157
|
+
"""
|
|
158
|
+
workspace = zephyr_base.parent
|
|
159
|
+
candidate = workspace / ".venv"
|
|
160
|
+
if _resolve_layout(candidate) is None:
|
|
161
|
+
return None
|
|
162
|
+
if sdk_root is not None:
|
|
163
|
+
if not manifest_points_at(workspace, Path(sdk_root)):
|
|
164
|
+
return None
|
|
165
|
+
return candidate
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def venv_bin_dir(start: str, sdk_root: str | None) -> Path | None:
|
|
169
|
+
"""The west-capable workspace venv's executable directory (`bin` or
|
|
170
|
+
`Scripts`, chosen directory-wins -- see `_resolve_layout`, tan-cli#291).
|
|
171
|
+
This is the handle a spawning command needs: the directory to look tool
|
|
172
|
+
names up in, and the directory to put on the child's PATH. `None` when no
|
|
173
|
+
west-capable venv resolves."""
|
|
174
|
+
venv = find_workspace_venv(start, sdk_root)
|
|
175
|
+
if venv is None:
|
|
176
|
+
return None
|
|
177
|
+
layout = _resolve_layout(venv)
|
|
178
|
+
assert layout is not None # find_workspace_venv only returns what _resolve_layout accepted
|
|
179
|
+
return venv / layout.bin_dir
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def west_workspace_dir(start: str, sdk_root: Path | None) -> Path | None:
|
|
183
|
+
"""Port of `workspace.rs::west_workspace_dir`: the directory holding
|
|
184
|
+
`.west/` that a `west alp-*` extension command -- and `tan flash`'s
|
|
185
|
+
spawned children (tan-cli#289/#61), and (tan-cli#307) `tan build`'s own
|
|
186
|
+
`west build` children -- must run from. Discovered only from a workspace
|
|
187
|
+
manifest, never the app dir alone. Checked in order: the project tree
|
|
188
|
+
upward (manifest-verified against `sdk_root` when one resolved --
|
|
189
|
+
tan-cli#307, see `_manifest_ok` below), `$ZEPHYR_BASE/..`
|
|
190
|
+
(manifest-verified the same way), then the SDK-derived layouts
|
|
191
|
+
(`<sdk-parent>` and the legacy `<sdk-parent>/zephyrproject`, never
|
|
192
|
+
manifest-checked -- they are already anchored on `sdk_root` itself, with
|
|
193
|
+
nothing independent left to verify). `None` when nothing resolves -- the
|
|
194
|
+
caller then keeps the old app-dir cwd, matching the oracle exactly.
|
|
195
|
+
|
|
196
|
+
**tan-cli#307: the upward walk is manifest-guarded too, not just
|
|
197
|
+
`$ZEPHYR_BASE`.** A project sitting inside a directory that ALSO holds an
|
|
198
|
+
unrelated `.west` (a second Zephyr checkout, a vendor SDK, an old
|
|
199
|
+
workspace) used to have that ancestor win here purely because it sits
|
|
200
|
+
closer to `start` than the workspace `tan bootstrap` actually created
|
|
201
|
+
elsewhere -- the same class of bug tan-cli#61/#292 already fixed for the
|
|
202
|
+
`$ZEPHYR_BASE` candidate, now closed for the plain upward walk too.
|
|
203
|
+
|
|
204
|
+
**Moved here from `tan.commands.west_forward_cmd` (tan-cli#289 review).**
|
|
205
|
+
`flash_cmd.py` needed the SAME resolver `west_forward_cmd.py` already
|
|
206
|
+
had -- a second copy risked the two disagreeing the moment one changed,
|
|
207
|
+
the exact class of bug `find_workspace_venv` (above) already exists to
|
|
208
|
+
avoid. `west_forward_cmd` keeps a `_west_workspace_dir` alias bound to
|
|
209
|
+
this function for its own call site and its existing tests; a THIRD,
|
|
210
|
+
partial copy still lives in `tan.commands.doctor_cmd._resolve_
|
|
211
|
+
west_workspace_dir` and is a follow-up, not retired here.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def is_workspace(d: Path) -> bool:
|
|
215
|
+
return (d / ".west").is_dir()
|
|
216
|
+
|
|
217
|
+
def manifest_ok(d: Path) -> bool:
|
|
218
|
+
"""`sdk_root` absent means there is nothing to verify against, so
|
|
219
|
+
`d` is accepted unconditionally -- the same "nothing to check"
|
|
220
|
+
fallback every other candidate in this function already applies."""
|
|
221
|
+
if sdk_root is None:
|
|
222
|
+
return True
|
|
223
|
+
return manifest_points_at(d, sdk_root)
|
|
224
|
+
|
|
225
|
+
directory: Path | None = Path(start)
|
|
226
|
+
while directory is not None:
|
|
227
|
+
if is_workspace(directory) and manifest_ok(directory):
|
|
228
|
+
return directory
|
|
229
|
+
parent = directory.parent
|
|
230
|
+
directory = parent if parent != directory else None
|
|
231
|
+
|
|
232
|
+
zephyr_base = os.environ.get("ZEPHYR_BASE")
|
|
233
|
+
if zephyr_base:
|
|
234
|
+
found = _zephyr_base_workspace(Path(zephyr_base), sdk_root)
|
|
235
|
+
if found is not None:
|
|
236
|
+
return found
|
|
237
|
+
|
|
238
|
+
if sdk_root is not None:
|
|
239
|
+
parent = sdk_root.parent
|
|
240
|
+
for workspace in (parent, parent / "zephyrproject"):
|
|
241
|
+
if is_workspace(workspace):
|
|
242
|
+
return workspace
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _zephyr_base_workspace(zephyr_base: Path, sdk_root: Path | None) -> Path | None:
|
|
248
|
+
"""Step 2 of `west_workspace_dir`: port of `workspace.rs::zephyr_base_workspace`.
|
|
249
|
+
`zephyr_base`'s PARENT, but only when it is both a west workspace AND (when
|
|
250
|
+
`sdk_root` resolved) its manifest is verifiably alp-sdk's -- a stock/unrelated
|
|
251
|
+
Zephyr checkout's parent is a west workspace by the bare `.west`-dir test
|
|
252
|
+
alone, yet carries no alp-sdk extension commands at all. `sdk_root` absent
|
|
253
|
+
means there is nothing to verify against, so the old unconditional accept
|
|
254
|
+
stands rather than refusing a workspace this call can't check.
|
|
255
|
+
"""
|
|
256
|
+
workspace = zephyr_base.parent
|
|
257
|
+
if not (workspace / ".west").is_dir():
|
|
258
|
+
return None
|
|
259
|
+
if sdk_root is not None:
|
|
260
|
+
if not manifest_points_at(workspace, sdk_root):
|
|
261
|
+
return None
|
|
262
|
+
return workspace
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def tool_in_venv(bin_dir: Path, tool: str) -> str | None:
|
|
266
|
+
"""Resolve `tool` INSIDE an already-located venv bin dir: returns its
|
|
267
|
+
absolute path when it is really a file there. `.exe` is appended when
|
|
268
|
+
`bin_dir` IS the Windows-layout dir (its name matches
|
|
269
|
+
`venv_layout(True).bin_dir`, i.e. `Scripts`) unless the caller already
|
|
270
|
+
spelled it -- keyed on the directory that won (tan-cli#291), not
|
|
271
|
+
`os.name`: `bin_dir` is whatever `_resolve_layout`'s directory-wins probe
|
|
272
|
+
picked, which can be `Scripts/` on a POSIX-reporting host or `bin/` on
|
|
273
|
+
Windows when that is the layout actually on disk. `None` means "this venv
|
|
274
|
+
does not provide that tool" -- the caller then keeps its PATH behaviour
|
|
275
|
+
instead of spawning a path that doesn't exist."""
|
|
276
|
+
name = tool
|
|
277
|
+
if bin_dir.name == venv_layout(True).bin_dir and not tool.lower().endswith(".exe"):
|
|
278
|
+
name = f"{tool}.exe"
|
|
279
|
+
candidate = bin_dir / name
|
|
280
|
+
return str(candidate) if candidate.is_file() else None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def west_program(start: str, sdk_root: str | None) -> str:
|
|
284
|
+
"""Resolve the `west` program to launch: the west-capable workspace
|
|
285
|
+
venv's `west` binary (see [`find_workspace_venv`]), falling back to
|
|
286
|
+
`"west"` on PATH when none resolve (CI, an activated venv, the contract
|
|
287
|
+
harness) -- behaving exactly as before in those environments. Mirrors
|
|
288
|
+
Rust's `west_program`."""
|
|
289
|
+
bin_dir = venv_bin_dir(start, sdk_root)
|
|
290
|
+
if bin_dir is None:
|
|
291
|
+
return "west"
|
|
292
|
+
return tool_in_venv(bin_dir, "west") or "west"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def venv_python(start: str, sdk_root: str | None) -> str | None:
|
|
296
|
+
"""The west-capable workspace venv's `python` (see
|
|
297
|
+
[`find_workspace_venv`]), mirroring Rust's `venv_python`. `None` when no
|
|
298
|
+
workspace venv resolves, or the one that does has no `python` binary --
|
|
299
|
+
the caller then keeps its own PATH-name fallback rather than spawning a
|
|
300
|
+
path that doesn't exist.
|
|
301
|
+
|
|
302
|
+
Forward-slashed: a caller substitutes this verbatim into `${PYTHON}`,
|
|
303
|
+
which lands in a `-DPython3_EXECUTABLE=<...>` CMake `-D` argument, and a
|
|
304
|
+
Windows backslash there is an escape character (alp-sdk#849).
|
|
305
|
+
"""
|
|
306
|
+
bin_dir = venv_bin_dir(start, sdk_root)
|
|
307
|
+
if bin_dir is None:
|
|
308
|
+
return None
|
|
309
|
+
resolved = tool_in_venv(bin_dir, "python")
|
|
310
|
+
return resolved.replace("\\", "/") if resolved is not None else None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def prepend_path(env: dict[str, str], directory: Path) -> dict[str, str]:
|
|
314
|
+
"""Put `directory` at the FRONT of `env`'s `PATH`, mirroring Rust's
|
|
315
|
+
`prepend_path`. Returns a NEW dict -- `env` itself is never mutated, so a
|
|
316
|
+
caller holding onto the original (e.g. `dict(os.environ)`) is unaffected."""
|
|
317
|
+
existing = env.get("PATH", "")
|
|
318
|
+
joined = os.pathsep.join([str(directory), existing]) if existing else str(directory)
|
|
319
|
+
return {**env, "PATH": joined}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def with_venv_on_path(env: dict[str, str], tool: str) -> dict[str, str]:
|
|
323
|
+
"""When `tool` resolved to an absolute (venv) path, prepend its
|
|
324
|
+
directory to `env`'s `PATH` -- mirroring Rust's `with_venv_on_path`. The
|
|
325
|
+
`alp-*`/`west` program spawned at that path may itself shell a nested
|
|
326
|
+
`west`/`bitbake`, which resolves purely via PATH. A bare tool name (the
|
|
327
|
+
PATH fallback) is left untouched, matching the oracle's early return for
|
|
328
|
+
a non-absolute `tool`."""
|
|
329
|
+
bin_path = Path(tool)
|
|
330
|
+
if not bin_path.is_absolute():
|
|
331
|
+
return env
|
|
332
|
+
return prepend_path(env, bin_path.parent)
|
tan/core/west_patches.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure decisions for `tan bootstrap`'s `zephyr/patches.yml` phase (tan-cli#1296).
|
|
3
|
+
|
|
4
|
+
No IO here: `tan.commands.bootstrap_patches` owns every spawn and feeds the
|
|
5
|
+
results through these functions, so the exit-code contract of alp-sdk's
|
|
6
|
+
`scripts/verify_west_patches.py` lives in exactly one testable place.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
#: The verifier's exit codes (its module docstring is the authority).
|
|
16
|
+
VERIFY_APPLIED = 0
|
|
17
|
+
#: Everything this workspace carries is patched, but a module `patches.yml`
|
|
18
|
+
#: names is not checked out here, so it could not be inspected. Normal for a
|
|
19
|
+
#: narrow workspace -- a warning, never a failure.
|
|
20
|
+
VERIFY_UNCHECKED = 3
|
|
21
|
+
#: Nothing could be inspected (no workspace, unreadable/empty `patches.yml`).
|
|
22
|
+
VERIFY_UNRUNNABLE = 2
|
|
23
|
+
|
|
24
|
+
#: Bounds for the output excerpt put into an issue message.
|
|
25
|
+
_TAIL_LINES = 12
|
|
26
|
+
_TAIL_CHARS = 1500
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def classify_verify(returncode: int | None) -> str:
|
|
30
|
+
"""`"applied"` / `"unchecked"` / `"unapplied"` / `"unrunnable"` for one run
|
|
31
|
+
of the verifier, before OR after the per-module applies.
|
|
32
|
+
|
|
33
|
+
`"unrunnable"` is exit 2 ("nothing could be inspected": no workspace,
|
|
34
|
+
unreadable `patches.yml`) or `None` (the verifier never launched). It is NOT
|
|
35
|
+
"unapplied": the patch state is unknown, and saying "not applied" would send
|
|
36
|
+
the reader to the wrong place. Re-verifying after the applies matters
|
|
37
|
+
because `west patch apply` exiting 0 is not evidence it applied anything
|
|
38
|
+
(it exits 0 on a missing `patches.yml`, an empty patch list, and an
|
|
39
|
+
unresolvable `module:`).
|
|
40
|
+
"""
|
|
41
|
+
if returncode == VERIFY_APPLIED:
|
|
42
|
+
return "applied"
|
|
43
|
+
if returncode == VERIFY_UNCHECKED:
|
|
44
|
+
return "unchecked"
|
|
45
|
+
if returncode == VERIFY_UNRUNNABLE or returncode is None:
|
|
46
|
+
return "unrunnable"
|
|
47
|
+
return "unapplied"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_unapplied(stdout: str) -> list[str]:
|
|
51
|
+
"""Module names from `--list-unapplied` (one per line), de-duplicated in
|
|
52
|
+
first-seen order. Blank lines are ignored."""
|
|
53
|
+
seen: dict[str, None] = {}
|
|
54
|
+
for line in stdout.splitlines():
|
|
55
|
+
name = line.strip()
|
|
56
|
+
if name:
|
|
57
|
+
seen.setdefault(name, None)
|
|
58
|
+
return list(seen)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def output_tail(text: str) -> str:
|
|
62
|
+
"""The last few lines of child output, bounded, for an issue message --
|
|
63
|
+
the cause (`patch does not apply`, a `pykwalify` ImportError) is at the
|
|
64
|
+
end, so the head is what gets dropped."""
|
|
65
|
+
lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()]
|
|
66
|
+
tail = "\n".join(lines[-_TAIL_LINES:])
|
|
67
|
+
return tail[-_TAIL_CHARS:]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Workspace patch CHECK (tan-cli#1376): `tan doctor` / `tan build` ask the SAME
|
|
72
|
+
# verifier "is `zephyr/patches.yml` applied" without applying anything.
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
#: One failing line of the verifier's stderr: ` ABSENT <patch rel path>`.
|
|
76
|
+
_FAILURE_LINE = re.compile(r"^\s+(ABSENT|DRIFTED|UNRESOLVED)\s+(\S+)\s*$")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class UnappliedPatch:
|
|
81
|
+
"""One patch the verifier reports as not applied. `verdict` is the
|
|
82
|
+
verifier's own word (`ABSENT`/`DRIFTED`/`UNRESOLVED`); `patch` is the path
|
|
83
|
+
under `zephyr/patches/` it prints (e.g. `hal_alif/0001-....patch`)."""
|
|
84
|
+
|
|
85
|
+
verdict: str
|
|
86
|
+
patch: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_unapplied_patches(stderr: str) -> list[UnappliedPatch]:
|
|
90
|
+
"""The patches named by the verifier's failure report, in report order."""
|
|
91
|
+
found: list[UnappliedPatch] = []
|
|
92
|
+
for line in stderr.splitlines():
|
|
93
|
+
m = _FAILURE_LINE.match(line)
|
|
94
|
+
if m is not None:
|
|
95
|
+
found.append(UnappliedPatch(m.group(1), m.group(2)))
|
|
96
|
+
return found
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def describe_unapplied(patches: list[UnappliedPatch], modules: list[str]) -> str:
|
|
100
|
+
"""`hal_alif/0001-x.patch (ABSENT), ...` plus the modules to patch."""
|
|
101
|
+
names = ", ".join(f"{p.patch} ({p.verdict})" for p in patches) or "unnamed patches"
|
|
102
|
+
where = f" in module(s) {', '.join(modules)}" if modules else ""
|
|
103
|
+
return f"{names}{where}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def patch_fix_text(modules: list[str], workspace_dir: str) -> str:
|
|
107
|
+
"""The exact remedy: `tan bootstrap` applies exactly the unapplied modules
|
|
108
|
+
(verify, `west patch --dst-module <m> apply`, re-verify). The per-module
|
|
109
|
+
west form is the manual equivalent; a bare `west patch apply` is NOT
|
|
110
|
+
offered because it re-applies already-patched modules and fails."""
|
|
111
|
+
manual = (
|
|
112
|
+
"; ".join(f"west patch --dst-module {m} apply" for m in modules)
|
|
113
|
+
or "west patch --dst-module <module> apply"
|
|
114
|
+
)
|
|
115
|
+
return f"run `tan bootstrap` (or, from {workspace_dir}: {manual})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cache_key(patches_yml: bytes, heads: dict[str, str]) -> str:
|
|
119
|
+
"""Stable digest of (`patches.yml` bytes, every workspace module's HEAD)."""
|
|
120
|
+
h = hashlib.sha256()
|
|
121
|
+
h.update(hashlib.sha256(patches_yml).digest())
|
|
122
|
+
for path in sorted(heads):
|
|
123
|
+
h.update(f"\0{path}\0{heads[path]}".encode())
|
|
124
|
+
return h.hexdigest()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def zephyr_base_note(env_value: str | None, workspace_zephyr: str | None) -> str | None:
|
|
128
|
+
"""Message when `$ZEPHYR_BASE` is set and names a different tree than the
|
|
129
|
+
resolved workspace's `zephyr/`; `None` otherwise. tan never honours the
|
|
130
|
+
override, so the user must hear it is being ignored."""
|
|
131
|
+
if not env_value or workspace_zephyr is None:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def norm(p: str) -> str:
|
|
135
|
+
return os.path.normcase(os.path.realpath(p))
|
|
136
|
+
|
|
137
|
+
if norm(env_value) == norm(workspace_zephyr):
|
|
138
|
+
return None
|
|
139
|
+
return (
|
|
140
|
+
f"$ZEPHYR_BASE={env_value} is ignored: tan always builds with the resolved west "
|
|
141
|
+
f"workspace's zephyr ({workspace_zephyr}). To build against another tree, run "
|
|
142
|
+
"tan from that workspace or bootstrap it."
|
|
143
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""tan-cli#1429: refuse a `west build` slice up front when nothing will find a
|
|
2
|
+
west workspace for it, instead of letting west fail with its own
|
|
3
|
+
`west: unknown command "build"; do you need to run this inside a workspace?`.
|
|
4
|
+
|
|
5
|
+
The shape the issue reproduced: `--sdk-root` names an alp-sdk checkout that is
|
|
6
|
+
NOT the manifest project of any west workspace tan can see (a sibling git
|
|
7
|
+
worktree such as `~/.cache/sdk-dev-2784`), and the app lives outside every
|
|
8
|
+
workspace tree (`/tmp/...`). `tan.core.venv.west_workspace_dir` then returns
|
|
9
|
+
`None`, so `execute_slices` keeps the slice's own cwd (tan-cli#307's fallback),
|
|
10
|
+
and west -- finding no `.west` above that cwd -- never loads the `build`
|
|
11
|
+
extension at all. The envelope carried only the generic `build.slice-failed`.
|
|
12
|
+
|
|
13
|
+
**Keyed on what WEST will do, not on what tan resolved.** `workspace_dir is
|
|
14
|
+
None` alone is not a failure: an app inside some workspace tree still builds,
|
|
15
|
+
because west's own ancestor walk finds that `.west` without tan's manifest
|
|
16
|
+
guard (the "move it under the workspace and it builds" observation in the
|
|
17
|
+
issue), and an ambient `ZEPHYR_BASE` gives west a second place to look. The
|
|
18
|
+
refusal therefore fires only when neither the spawn cwd nor the spawn env's
|
|
19
|
+
`ZEPHYR_BASE` has a `.west` on any ancestor -- the case where west cannot
|
|
20
|
+
succeed. Anything less certain is left to west exactly as before.
|
|
21
|
+
|
|
22
|
+
**Reusing a workspace is deliberately not attempted.** tan has no record that
|
|
23
|
+
maps an SDK checkout to a workspace (`.west/tan-workspace-sdk` maps the other
|
|
24
|
+
way and sits inside the workspace), so picking one would be a guess -- the
|
|
25
|
+
thing tan-cli#307/#61/#292's manifest guard exists to refuse.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from tan.core.bootstrap import get_manifest_path
|
|
34
|
+
|
|
35
|
+
#: The marker `build_cmd._workspace_unresolved_issues` matches to promote the
|
|
36
|
+
#: refused slice's reason into the coded `build.workspace-unresolved` issue,
|
|
37
|
+
#: the same idiom as `tan.core.plan_exec.CROSS_DRIVE_MSG`.
|
|
38
|
+
WORKSPACE_UNRESOLVED_MSG = "no west workspace resolves for this build"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class WorkspaceRefusal:
|
|
43
|
+
"""`message` carries absolute paths for this run's stdout and envelope;
|
|
44
|
+
`manifest_message` is the short form `system-manifest.yaml` keeps, without
|
|
45
|
+
the host's directory layout (same split as `CrossDriveRefusal`)."""
|
|
46
|
+
|
|
47
|
+
message: str
|
|
48
|
+
manifest_message: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def west_ancestor(start: Path) -> Path | None:
|
|
52
|
+
"""The nearest directory at or above `start` holding a `.west` directory,
|
|
53
|
+
or `None`. Mirrors west's own `west_topdir` walk: unguarded, because west
|
|
54
|
+
does not check which manifest a `.west` belongs to. `start` is resolved
|
|
55
|
+
(symlinks followed) because west walks from `os.getcwd()`, which POSIX
|
|
56
|
+
reports as the real path."""
|
|
57
|
+
start = Path(start).resolve()
|
|
58
|
+
for directory in (start, *start.parents):
|
|
59
|
+
if (directory / ".west").is_dir():
|
|
60
|
+
return directory
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def workspace_unresolved_refusal(
|
|
65
|
+
workspace_dir: Path | None,
|
|
66
|
+
args: list[str],
|
|
67
|
+
spawn_cwd: Path,
|
|
68
|
+
zephyr_base: str | None,
|
|
69
|
+
sdk_root: Path | None,
|
|
70
|
+
) -> WorkspaceRefusal | None:
|
|
71
|
+
"""`None` unless this `west build` slice cannot find a workspace.
|
|
72
|
+
|
|
73
|
+
`zephyr_base` is the value the SPAWN's env carries, not `os.environ`'s.
|
|
74
|
+
`sdk_root` absent is never refused: without it there is no checkout to
|
|
75
|
+
name, and no `tan bootstrap --sdk-root` remedy to give.
|
|
76
|
+
"""
|
|
77
|
+
if workspace_dir is not None or sdk_root is None or not args or args[0] != "build":
|
|
78
|
+
return None
|
|
79
|
+
if west_ancestor(spawn_cwd) is not None:
|
|
80
|
+
return None
|
|
81
|
+
# A relative ZEPHYR_BASE is resolved by west against the spawn cwd.
|
|
82
|
+
if zephyr_base and west_ancestor(Path(spawn_cwd) / zephyr_base) is not None:
|
|
83
|
+
return None
|
|
84
|
+
message = (
|
|
85
|
+
f"{WORKSPACE_UNRESOLVED_MSG} -- `west build` would run from `{spawn_cwd}`, "
|
|
86
|
+
f"which has no `.west` on any ancestor; {_unresolved_facts(zephyr_base, sdk_root)}. "
|
|
87
|
+
f"west would stop with `unknown command \"build\"`. {_remedies(sdk_root)}"
|
|
88
|
+
)
|
|
89
|
+
return WorkspaceRefusal(message, f"{WORKSPACE_UNRESOLVED_MSG} (run `tan bootstrap`)")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _unresolved_facts(zephyr_base: str | None, sdk_root: Path) -> str:
|
|
93
|
+
zephyr_base_fact = (
|
|
94
|
+
f"ZEPHYR_BASE `{zephyr_base}` has no `.west` above it either"
|
|
95
|
+
if zephyr_base
|
|
96
|
+
else "ZEPHYR_BASE is unset"
|
|
97
|
+
)
|
|
98
|
+
sdk_parent = sdk_root.parent
|
|
99
|
+
return (
|
|
100
|
+
f"{zephyr_base_fact}; and neither `{sdk_parent}` nor "
|
|
101
|
+
f"`{sdk_parent / 'zephyrproject'}` (next to the SDK root `{sdk_root}`) holds a `.west`"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _remedies(sdk_root: Path) -> str:
|
|
106
|
+
return (
|
|
107
|
+
f"Run `tan bootstrap --sdk-root {sdk_root}` to create a workspace for this "
|
|
108
|
+
f"checkout, or set ZEPHYR_BASE to an existing workspace's `zephyr/` -- that "
|
|
109
|
+
f"builds against that workspace's Zephyr revision and patches, which need "
|
|
110
|
+
f"not match this checkout's `west.yml`."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def manifest_project(topdir: Path) -> Path | None:
|
|
115
|
+
"""The manifest project `<topdir>/.west/config` names, or `None` when the
|
|
116
|
+
config is missing or names none."""
|
|
117
|
+
try:
|
|
118
|
+
config = (topdir / ".west" / "config").read_text(encoding="utf-8", errors="replace")
|
|
119
|
+
except OSError:
|
|
120
|
+
return None
|
|
121
|
+
rel = get_manifest_path(config)
|
|
122
|
+
return topdir / rel.strip() if rel else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def unresolved_workspace_verdict(
|
|
126
|
+
start: Path, zephyr_base: str | None, sdk_root: Path
|
|
127
|
+
) -> tuple[str, str]:
|
|
128
|
+
"""tan-cli#1432: `tan doctor`'s `workspace` verdict when tan resolved no
|
|
129
|
+
workspace for `sdk_root`, as `(status, detail)`, agreeing with what
|
|
130
|
+
`tan build` will then do from `start`.
|
|
131
|
+
|
|
132
|
+
`warn` when west's own unguarded walk still lands on a `.west` -- above
|
|
133
|
+
`start` (west uses it; the build succeeds against THAT workspace's Zephyr),
|
|
134
|
+
or above `zephyr_base` (west's fallback, which is not verified to reach
|
|
135
|
+
the `build` extension, hence "may"). `fail` otherwise, naming the same
|
|
136
|
+
facts and remedies as `build.workspace-unresolved`.
|
|
137
|
+
"""
|
|
138
|
+
sdk_root = Path(sdk_root)
|
|
139
|
+
found = west_ancestor(start)
|
|
140
|
+
lead = f"west will use `{found}`, a `.west` above `{Path(start).absolute()}`"
|
|
141
|
+
if found is None and zephyr_base:
|
|
142
|
+
# As `workspace_unresolved_refusal`: west resolves a relative
|
|
143
|
+
# ZEPHYR_BASE against the cwd it runs in, which is `start`.
|
|
144
|
+
found = west_ancestor(Path(start) / zephyr_base)
|
|
145
|
+
lead = f"west may fall back to `{found}`, a `.west` above ZEPHYR_BASE `{zephyr_base}`"
|
|
146
|
+
if found is None:
|
|
147
|
+
return "fail", (
|
|
148
|
+
f"no Zephyr workspace for the SDK root `{sdk_root}`: `{Path(start).absolute()}` has "
|
|
149
|
+
f"no `.west` on any ancestor; {_unresolved_facts(zephyr_base, sdk_root)}, so "
|
|
150
|
+
f"a `west build` slice is refused as `build.workspace-unresolved`. {_remedies(sdk_root)}"
|
|
151
|
+
)
|
|
152
|
+
names = manifest_project(found)
|
|
153
|
+
manifest = f"names `{names}` as its manifest" if names else "names no manifest project"
|
|
154
|
+
return "warn", (
|
|
155
|
+
f"no workspace has the SDK root `{sdk_root}` as its manifest, but {lead}, which "
|
|
156
|
+
f"{manifest} -- a build uses that workspace's Zephyr revision and patches, which "
|
|
157
|
+
f"need not match this checkout's `west.yml`. Run `tan bootstrap --sdk-root "
|
|
158
|
+
f"{sdk_root}` for a workspace of its own."
|
|
159
|
+
)
|