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,482 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Host toolchain-root resolution for build-plan `${TOOLCHAIN_ROOT}`
|
|
3
|
+
substitution -- the Python port of `crate::toolchain::resolve_toolchain_root`
|
|
4
|
+
(tan-cli#547).
|
|
5
|
+
|
|
6
|
+
`${TOOLCHAIN_ROOT}` was a RECOGNISED token on the Python line with no
|
|
7
|
+
resolver behind it: `build_cmd.py` passed `toolchain_root=None`
|
|
8
|
+
unconditionally, so every slice naming the token demoted on EVERY host --
|
|
9
|
+
including hosts that have a perfectly good toolchain -- rather than only
|
|
10
|
+
where the host genuinely has none. That is what this module closes.
|
|
11
|
+
|
|
12
|
+
## The contract, MEASURED against the oracle, never read out of `crates/`
|
|
13
|
+
|
|
14
|
+
Established by running `target/debug/tan` (`tan 0.4.1`) with a
|
|
15
|
+
`planPathMode: tokened` plan whose one `configArtefacts[].contents` is
|
|
16
|
+
`TOOLCHAIN_ROOT=${TOOLCHAIN_ROOT}`, under `--materialise`, and reading back
|
|
17
|
+
the file it wrote (or the `issues[]` entry when it wrote nothing):
|
|
18
|
+
|
|
19
|
+
1. `ZEPHYR_SDK_INSTALL_DIR`, when set, non-empty, and naming a path that
|
|
20
|
+
EXISTS, wins outright and is returned VERBATIM. Measured: a trailing
|
|
21
|
+
slash survives into the substituted value (`/home/x/zephyr-sdk-1.0.1/`),
|
|
22
|
+
and a directory that exists but contains no toolchain at all is still
|
|
23
|
+
accepted -- the oracle probes existence, not contents.
|
|
24
|
+
2. Set but naming a path that does NOT exist -- and the empty string --
|
|
25
|
+
fall THROUGH to the scan rather than failing. Measured: with a real
|
|
26
|
+
install under `$HOME` and `ZEPHYR_SDK_INSTALL_DIR` pointing at a
|
|
27
|
+
nonexistent directory, the oracle resolved the scanned install.
|
|
28
|
+
3. Otherwise every entry named `zephyr-sdk*` DIRECTLY under a scan root is
|
|
29
|
+
a candidate. Prefix match, not a `zephyr-sdk-<version>` shape:
|
|
30
|
+
`zephyr-sdkXYZ` was accepted. Direct children only: an install one
|
|
31
|
+
level deeper (`$HOME/tools/zephyr-sdk-1.0.1`) was NOT found.
|
|
32
|
+
4. Exactly one candidate resolves. ZERO is unresolved with
|
|
33
|
+
`NO_TOOLCHAIN_ADVICE`. TWO OR MORE is ALSO unresolved -- with a
|
|
34
|
+
different, sharper reason (`_several_installs_advice`) that names every
|
|
35
|
+
candidate, SORTED (measured: created in `zzz, mmm, aaa` readdir order,
|
|
36
|
+
reported `aaa, mmm, zzz`). That second reason had no Python equivalent
|
|
37
|
+
at all before this module; the port collapsed both cases onto the
|
|
38
|
+
"no toolchain" wording.
|
|
39
|
+
|
|
40
|
+
## The two DELIBERATE divergences from the oracle, and why
|
|
41
|
+
|
|
42
|
+
* **A candidate must be a DIRECTORY.** The oracle accepts a plain FILE
|
|
43
|
+
named `zephyr-sdk-*` as a toolchain root (measured: a `touch`ed
|
|
44
|
+
`$HOME/zephyr-sdk-1.0.1` resolved). That is not a wart with no
|
|
45
|
+
consequence: `"zephyr-sdk-0.16.5_linux-x86_64.tar.xz".startswith(
|
|
46
|
+
"zephyr-sdk")` is true, so the downloaded ARCHIVE sitting in `$HOME`
|
|
47
|
+
beside the install it was extracted from makes the host look like it has
|
|
48
|
+
TWO installs -- and rule 4 above then demotes every slice on a host that
|
|
49
|
+
has exactly one working toolchain. Requiring a directory can only ever
|
|
50
|
+
REMOVE a false candidate, never add one, so it cannot cause the
|
|
51
|
+
demotion this issue is about.
|
|
52
|
+
* **The scan roots come from `HOME`, `USERPROFILE` and `Path.home()`, not
|
|
53
|
+
`HOME` alone.** Measured, the oracle reads `$HOME` only: with `HOME`
|
|
54
|
+
pointed at an SDK-less directory and `USERPROFILE` at the real one, it
|
|
55
|
+
resolved nothing. Reproducing that exactly would re-open the defect
|
|
56
|
+
`doctor_cmd._zephyr_sdk_scan_roots` was written for -- under Git
|
|
57
|
+
Bash/MSYS on Windows `HOME` is a POSIX-translated path (`/c/Users/dev`)
|
|
58
|
+
while the SDK sits under the native `%USERPROFILE%`, and a real host was
|
|
59
|
+
measured reporting "no SDK" with the SDK installed. On any ordinary
|
|
60
|
+
POSIX host the two sets are identical, so the measured Linux parity
|
|
61
|
+
above still holds byte-for-byte.
|
|
62
|
+
* **The ADR 0021 artifact-keyed store (`tan.core.toolchain_provision.
|
|
63
|
+
resolve_toolchain_root`, `~/.alp/toolchains` or `$ALP_TOOLCHAIN_ROOT`) is
|
|
64
|
+
ALSO scanned, one level in, for its own `zephyr-sdk*`-prefixed leaf
|
|
65
|
+
directories.** The v0.4.1 oracle predates issue #474 entirely and has no
|
|
66
|
+
opinion on this store -- there is nothing to diverge FROM here, only a
|
|
67
|
+
gap `tan-cli#990` review closed: `tan bootstrap`'s own toolchain phase
|
|
68
|
+
installs into `<store-root>/zephyr-sdk-<version>-arm-zephyr-eabi/`, two
|
|
69
|
+
directory levels below every root this function used to look at, so a
|
|
70
|
+
customer who ran nothing but `tan bootstrap` (the whole point of issue
|
|
71
|
+
#474) still could not get `${TOOLCHAIN_ROOT}` to resolve to what it just
|
|
72
|
+
downloaded without ALSO hand-exporting `ZEPHYR_SDK_INSTALL_DIR` --
|
|
73
|
+
keeping the very manual step ADR 0021 Lane 1 P1 exists to remove. Per
|
|
74
|
+
alp-sdk#1286's own closing decision ("never a location; resolving it to
|
|
75
|
+
a concrete path is the executor's job, not the plan's"), tan is that
|
|
76
|
+
executor, and this is that resolution. A `.tmp-<pid>` wreckage sibling
|
|
77
|
+
(`toolchain_provision.TMP_SUFFIX_PREFIX`) from an interrupted attempt is
|
|
78
|
+
excluded by name in `_candidates()` below -- it also starts with
|
|
79
|
+
`zephyr-sdk`, and would otherwise fake a "several installs, ambiguous"
|
|
80
|
+
host next to the one real, verified store.
|
|
81
|
+
|
|
82
|
+
`doctor_cmd`'s scan is DUPLICATED here rather than imported: `doctor_cmd`
|
|
83
|
+
imports from `build_cmd`, and `build_cmd` imports this module, so the import
|
|
84
|
+
would be circular. (`token_substitution._is_own_git_checkout` is duplicated
|
|
85
|
+
from the same module for its own reason -- this is the second instance of
|
|
86
|
+
the same shape, not a new one.)
|
|
87
|
+
|
|
88
|
+
That circularity is about `_scan_roots`/`_candidates`/`resolve_toolchain_root`
|
|
89
|
+
specifically, not about every name in this module: `doctor_cmd` DOES import
|
|
90
|
+
[`_is_toolchain_wreckage`] and [`_toolchain_store_scan_root`] directly from
|
|
91
|
+
here (tan-cli#1186), since neither of those two touches `build_cmd` and the
|
|
92
|
+
cycle above never applies to them.
|
|
93
|
+
|
|
94
|
+
`doctor_cmd._zephyr_sdk_root_valid`'s stricter probe -- "does
|
|
95
|
+
`gnu/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc` exist" -- is deliberately NOT
|
|
96
|
+
reused. Doctor answers a yes/no health question where a false PASS is the
|
|
97
|
+
harm (tan-cli#286). Here the harm runs the other way: a Zephyr SDK installed
|
|
98
|
+
with a non-ARM toolchain subset is a real, supported install, and refusing
|
|
99
|
+
it would demote every slice on that host -- which is precisely the defect
|
|
100
|
+
tan-cli#547 reports.
|
|
101
|
+
"""
|
|
102
|
+
import os
|
|
103
|
+
from dataclasses import dataclass
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
from tan.core import toolchain_provision as _tp
|
|
107
|
+
|
|
108
|
+
#: What every candidate directory's name must start with. A PREFIX, not a
|
|
109
|
+
#: `zephyr-sdk-<version>` pattern -- measured against the oracle, which
|
|
110
|
+
#: accepted a directory named `zephyr-sdkXYZ`. Matches BOTH a manual
|
|
111
|
+
#: `zephyr-sdk-1.0.1` install AND `tan.core.toolchain_provision.
|
|
112
|
+
#: store_dir_name`'s `zephyr-sdk-<version>-arm-zephyr-eabi` leaf -- no
|
|
113
|
+
#: change needed here for the artifact-keyed store below, only to WHERE it
|
|
114
|
+
#: is looked for.
|
|
115
|
+
ZEPHYR_SDK_DIR_PREFIX = "zephyr-sdk"
|
|
116
|
+
|
|
117
|
+
#: The env var that overrides the scan outright, and the one every reason
|
|
118
|
+
#: string below tells the caller to set.
|
|
119
|
+
ZEPHYR_SDK_INSTALL_DIR = "ZEPHYR_SDK_INSTALL_DIR"
|
|
120
|
+
|
|
121
|
+
#: The reason a `${TOOLCHAIN_ROOT}` demotion (or the plan-fatal
|
|
122
|
+
#: `boardYaml`/`sharedArtefacts` refusal) carries when this host has NO
|
|
123
|
+
#: toolchain at all. Verbatim the oracle's wording, in this port's ASCII
|
|
124
|
+
#: dash convention. Lives here rather than in `token_substitution.py` so the
|
|
125
|
+
#: resolver owns both of its own reasons; that module imports it.
|
|
126
|
+
NO_TOOLCHAIN_ADVICE = (
|
|
127
|
+
"no toolchain install is detectable on this host -- install the Zephyr SDK "
|
|
128
|
+
"(`west sdk install`) or set `ZEPHYR_SDK_INSTALL_DIR` to an existing install"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _several_installs_advice(candidates: list[str]) -> str:
|
|
133
|
+
"""The reason for the AMBIGUOUS host -- several installs and nothing
|
|
134
|
+
picking between them. Sharper than `NO_TOOLCHAIN_ADVICE` because the fix
|
|
135
|
+
is different: nothing needs installing, one of these needs choosing.
|
|
136
|
+
Names every candidate so the caller can paste one straight into
|
|
137
|
+
`ZEPHYR_SDK_INSTALL_DIR`."""
|
|
138
|
+
return (
|
|
139
|
+
f"this host has several toolchain installs and no `{ZEPHYR_SDK_INSTALL_DIR}` to "
|
|
140
|
+
f"choose between them ({', '.join(candidates)}) -- set `{ZEPHYR_SDK_INSTALL_DIR}` "
|
|
141
|
+
f"to the one this build should use"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class ToolchainResolution:
|
|
147
|
+
"""`root` is the host toolchain root, or `None` when this host has no
|
|
148
|
+
single unambiguous one. `advice` is the reason for that `None`, threaded
|
|
149
|
+
into the demotion message so the two unresolved cases -- none installed
|
|
150
|
+
vs several installed -- read differently. `advice` is still populated
|
|
151
|
+
when `root` resolved (nothing reads it then); it is not a nullable
|
|
152
|
+
second signal for the same fact."""
|
|
153
|
+
|
|
154
|
+
root: str | None
|
|
155
|
+
advice: str = NO_TOOLCHAIN_ADVICE
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _scan_roots() -> list[Path]:
|
|
159
|
+
"""Every directory scanned for a `zephyr-sdk*` install: `/opt`, plus
|
|
160
|
+
`$HOME`, `%USERPROFILE%` AND `Path.home()` -- ALL of them, never
|
|
161
|
+
`HOME or USERPROFILE`. See this module's docstring for the measured Git
|
|
162
|
+
Bash/MSYS host that makes the union load-bearing rather than defensive.
|
|
163
|
+
|
|
164
|
+
Deduplicated on the raw string only; identical directories reached by
|
|
165
|
+
different spellings are collapsed later, on the CANDIDATES, where a
|
|
166
|
+
duplicate would otherwise fake an ambiguous host."""
|
|
167
|
+
roots = [Path("/opt")]
|
|
168
|
+
seen: set[str] = set()
|
|
169
|
+
for raw in (os.environ.get("HOME"), os.environ.get("USERPROFILE")):
|
|
170
|
+
if raw and raw not in seen:
|
|
171
|
+
seen.add(raw)
|
|
172
|
+
roots.append(Path(raw))
|
|
173
|
+
try:
|
|
174
|
+
home = Path.home()
|
|
175
|
+
except (OSError, RuntimeError):
|
|
176
|
+
home = None
|
|
177
|
+
if home is not None and str(home) not in seen:
|
|
178
|
+
roots.append(home)
|
|
179
|
+
return roots
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _toolchain_store_scan_root() -> Path:
|
|
183
|
+
"""`~/.alp/toolchains` (or `$ALP_TOOLCHAIN_ROOT`) -- the ADR 0021
|
|
184
|
+
artifact-keyed store `tan bootstrap`'s own toolchain phase installs
|
|
185
|
+
into. Delegates to `toolchain_provision.resolve_toolchain_root`, the
|
|
186
|
+
SAME function `bootstrap_cmd._toolchain_root_and_leaf` calls, so this
|
|
187
|
+
can never compute a different root than the one bootstrap actually used
|
|
188
|
+
-- the two-levels-deep discovery gap tan-cli#990 review found was a
|
|
189
|
+
location bug, not a fact this module should ever re-derive its own
|
|
190
|
+
opinion of.
|
|
191
|
+
|
|
192
|
+
`home_alp_dir` is duplicated inline (`~/.alp`, `USERPROFILE` on Windows
|
|
193
|
+
else `HOME`) rather than importing `tan.core.sdk_discovery._home_alp_dir`
|
|
194
|
+
-- this module already duplicates `doctor_cmd`'s scan for the identical
|
|
195
|
+
reason (see the module docstring): one two-line duplication is cheaper
|
|
196
|
+
than auditing this module's own import graph for a cycle every time the
|
|
197
|
+
canonical helper's home changes.
|
|
198
|
+
"""
|
|
199
|
+
home = os.environ.get("USERPROFILE" if os.name == "nt" else "HOME")
|
|
200
|
+
home_alp_dir = str(Path(home or ".") / ".alp")
|
|
201
|
+
root = _tp.resolve_toolchain_root(os.environ.get("ALP_TOOLCHAIN_ROOT"), home_alp_dir)
|
|
202
|
+
return Path(root.path_str)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_text_or_none(path: Path) -> str | None:
|
|
206
|
+
"""`None` on ANY read failure -- `OSError` (missing/unreadable/a
|
|
207
|
+
directory) AND `UnicodeDecodeError` (a subclass of `ValueError`) alike.
|
|
208
|
+
|
|
209
|
+
tan-cli#1209 review MAJOR: `verified_store_dir`'s two callers of this
|
|
210
|
+
used to write `path.read_text(encoding="utf-8")` under a bare
|
|
211
|
+
`except OSError`, so one non-UTF-8 byte in either
|
|
212
|
+
`<sdk_root>/metadata/toolchains.json` or the store's own
|
|
213
|
+
`.alp-toolchain-stamp.json` raised `UnicodeDecodeError` straight past
|
|
214
|
+
this function's own "never raises" contract and out of `execute_slices`
|
|
215
|
+
before any slice dispatched -- with no envelope at all under
|
|
216
|
+
`--format json`. `errors="replace"` plus `except (OSError, ValueError)`
|
|
217
|
+
mirrors `doctor_cmd._read_text` exactly (same two files, read from the
|
|
218
|
+
same two call sites in that module), so `tan build` and `tan doctor`
|
|
219
|
+
can no longer disagree on a corrupt manifest or stamp: neither raises,
|
|
220
|
+
and a mangled byte fails the JSON/shape parse that follows rather than
|
|
221
|
+
the read itself."""
|
|
222
|
+
try:
|
|
223
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
224
|
+
except (OSError, ValueError):
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _read_manifest(sdk_root: str | None) -> _tp.ToolchainManifest | None:
|
|
229
|
+
"""@sdk_root's `metadata/toolchains.json`, parsed -- `None` on no
|
|
230
|
+
`sdk_root`, an unreadable file, or a malformed manifest. Shared by
|
|
231
|
+
[`verified_store_dir`] and [`host_scan_has_toolchain`] so the two can
|
|
232
|
+
never independently disagree on what THIS checkout's pin says; never
|
|
233
|
+
raises, matching every other reader in this module."""
|
|
234
|
+
if sdk_root is None:
|
|
235
|
+
return None
|
|
236
|
+
manifest_path = Path(sdk_root) / "metadata" / "toolchains.json"
|
|
237
|
+
manifest_text = _read_text_or_none(manifest_path)
|
|
238
|
+
if manifest_text is None:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
return _tp.parse_toolchain_manifest(manifest_text)
|
|
242
|
+
except _tp.ToolchainManifestError:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _toolchain_store_dir(manifest: _tp.ToolchainManifest) -> Path:
|
|
247
|
+
"""`<store-root>/<per-version leaf>` for @manifest -- the ONE directory
|
|
248
|
+
tan's own installs use for this exact pin. Mirrors
|
|
249
|
+
`doctor_cmd._toolchain_store_dir` byte-for-byte (same formula: `_
|
|
250
|
+
toolchain_store_scan_root() / store_dir_name(manifest.version)`) so the
|
|
251
|
+
two modules can never independently compute a different answer for
|
|
252
|
+
"which directory is tan's own install for this pin".
|
|
253
|
+
|
|
254
|
+
[`verified_store_dir`] and [`host_scan_has_toolchain`] both key their
|
|
255
|
+
store exclusion off THIS leaf, never the whole `_toolchain_store_
|
|
256
|
+
scan_root()` -- tan-cli#1209 review BLOCKER. See `doctor_cmd.
|
|
257
|
+
_host_toolchain_matching_pin`'s docstring for why the narrower leaf,
|
|
258
|
+
not the whole configured root, is the correct exclusion under the ADR
|
|
259
|
+
0021 `$ALP_TOOLCHAIN_ROOT`-pointed-at-an-ancestor escape hatch: a
|
|
260
|
+
stamped leaf for a DIFFERENT, no-longer-pinned version can sit right
|
|
261
|
+
next to the current one inside the same store, and is a real,
|
|
262
|
+
independent host toolchain that must not be hidden by excluding the
|
|
263
|
+
whole root."""
|
|
264
|
+
return _toolchain_store_scan_root() / _tp.store_dir_name(manifest.version)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def verified_store_dir(sdk_root: str | None) -> Path | None:
|
|
268
|
+
"""tan's own ADR 0021 store for the checkout's PINNED cross-toolchain
|
|
269
|
+
version, iff `.alp-toolchain-stamp.json` there matches that pin -- the
|
|
270
|
+
identical pin+stamp predicate `bootstrap_cmd.toolchain_phase`
|
|
271
|
+
(skip-if-already-installed) and `doctor_cmd.toolchain_check` (the
|
|
272
|
+
`toolchain` check) already apply via `stamp_matches_pin`, reused here
|
|
273
|
+
rather than re-derived a third time (tan-cli#1209: `ZEPHYR_SDK_INSTALL_DIR`
|
|
274
|
+
handoff into a spawned build's env).
|
|
275
|
+
|
|
276
|
+
`None` on ANY of: no `sdk_root`, an unreadable/malformed
|
|
277
|
+
`metadata/toolchains.json`, no stamp file, or a stamp that does not
|
|
278
|
+
match -- never raises, matching every other reader in this module.
|
|
279
|
+
|
|
280
|
+
Deliberately NOT [`resolve_toolchain_root`] (this module's
|
|
281
|
+
`${TOOLCHAIN_ROOT}` resolver): that scans every `zephyr-sdk*` under
|
|
282
|
+
`_scan_roots()` PLUS this store and refuses outright on ambiguity
|
|
283
|
+
("several installs"). A caller wiring `ZEPHYR_SDK_INSTALL_DIR` for a
|
|
284
|
+
spawned child wants tan's own stamped, digest-verified store for
|
|
285
|
+
EXACTLY this pin -- never a scan that can be defeated by an unrelated
|
|
286
|
+
hand-installed SDK sitting elsewhere on the same host.
|
|
287
|
+
"""
|
|
288
|
+
manifest = _read_manifest(sdk_root)
|
|
289
|
+
if manifest is None:
|
|
290
|
+
return None
|
|
291
|
+
store_dir = _toolchain_store_dir(manifest)
|
|
292
|
+
stamp_text = _read_text_or_none(store_dir / _tp.STAMP_FILENAME)
|
|
293
|
+
if stamp_text is None:
|
|
294
|
+
return None
|
|
295
|
+
stamp = _tp.parse_stamp(stamp_text)
|
|
296
|
+
if not _tp.stamp_matches_pin(stamp, manifest):
|
|
297
|
+
return None
|
|
298
|
+
return store_dir
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _is_toolchain_wreckage(name: str) -> bool:
|
|
302
|
+
"""`True` for a `.tmp-<pid>` sibling of an interrupted acquisition
|
|
303
|
+
(`toolchain_provision.TMP_SUFFIX_PREFIX`) -- it starts with
|
|
304
|
+
`zephyr-sdk` too (it is `<leaf><TMP_SUFFIX_PREFIX><pid>` and `leaf`
|
|
305
|
+
itself starts with `zephyr-sdk`), so without this it would fake a
|
|
306
|
+
SECOND, ambiguous candidate next to the one real, verified store entry
|
|
307
|
+
every time `tan bootstrap` was interrupted mid-acquisition and its own
|
|
308
|
+
startup reclaim (`bootstrap_cmd._reclaim_toolchain_wreckage`) has not
|
|
309
|
+
yet run again."""
|
|
310
|
+
return _tp.TMP_SUFFIX_PREFIX in name
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _candidates(roots: list[Path] | None = None) -> list[str]:
|
|
314
|
+
"""Every distinct `zephyr-sdk*` install directory found under @roots, as
|
|
315
|
+
POSIX-separator strings, SORTED. @roots defaults to `_scan_roots()` PLUS
|
|
316
|
+
`_toolchain_store_scan_root()` -- the full set `resolve_toolchain_root`
|
|
317
|
+
needs. [`host_scan_has_toolchain`] passes `_scan_roots()` alone, to ask
|
|
318
|
+
the narrower "does the HOST carry one, never mind tan's own store"
|
|
319
|
+
question.
|
|
320
|
+
|
|
321
|
+
Deduplicated on `Path.resolve()`, keeping the FIRST literal spelling
|
|
322
|
+
seen. This is the guard that stops `_scan_roots()`'s deliberate overlap
|
|
323
|
+
from breaking the caller: `$HOME` and `Path.home()` routinely name the
|
|
324
|
+
same directory (and a symlinked or trailing-slash `$HOME` slips past
|
|
325
|
+
`_scan_roots`'s own string-level dedup), which would list one install
|
|
326
|
+
twice and demote every slice on a host with exactly one toolchain --
|
|
327
|
+
the very failure tan-cli#547 is about.
|
|
328
|
+
|
|
329
|
+
`as_posix()`, not `str()`: identical on POSIX (so the measured oracle
|
|
330
|
+
parity holds byte-for-byte) and the separator convention
|
|
331
|
+
`token_substitution.py` already normalises `${PROJECT_ROOT}` to, so a
|
|
332
|
+
substituted `${TOOLCHAIN_ROOT}` does not arrive at CMake in the one
|
|
333
|
+
spelling `${PROJECT_ROOT}` never uses.
|
|
334
|
+
|
|
335
|
+
Never raises: an unreadable or missing scan root is "nothing found
|
|
336
|
+
there", not a failed build.
|
|
337
|
+
"""
|
|
338
|
+
if roots is None:
|
|
339
|
+
roots = [*_scan_roots(), _toolchain_store_scan_root()]
|
|
340
|
+
found: dict[str, str] = {}
|
|
341
|
+
for root in roots:
|
|
342
|
+
try:
|
|
343
|
+
entries = sorted(root.iterdir())
|
|
344
|
+
except OSError:
|
|
345
|
+
continue
|
|
346
|
+
for entry in entries:
|
|
347
|
+
if not entry.name.startswith(ZEPHYR_SDK_DIR_PREFIX):
|
|
348
|
+
continue
|
|
349
|
+
if _is_toolchain_wreckage(entry.name):
|
|
350
|
+
continue
|
|
351
|
+
try:
|
|
352
|
+
if not entry.is_dir():
|
|
353
|
+
continue
|
|
354
|
+
key = str(entry.resolve())
|
|
355
|
+
except OSError:
|
|
356
|
+
continue
|
|
357
|
+
found.setdefault(key, entry.as_posix())
|
|
358
|
+
return sorted(found.values())
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
#: Duplicated from `doctor_cmd.ZEPHYR_SDK_TOOLCHAIN_DIR` -- see this
|
|
362
|
+
#: module's own docstring on why `doctor_cmd` is not imported here.
|
|
363
|
+
_ZEPHYR_SDK_TOOLCHAIN_DIR = ("gnu", "arm-zephyr-eabi", "bin")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _host_toolchain_is_usable(root: Path) -> bool:
|
|
367
|
+
"""`True` when @root actually contains the `arm-zephyr-eabi` cross
|
|
368
|
+
compiler, not merely a directory named right. Duplicated from
|
|
369
|
+
`doctor_cmd._zephyr_sdk_root_valid` (import cycle, see module
|
|
370
|
+
docstring) for [`host_scan_has_toolchain`] ALONE -- never for
|
|
371
|
+
`_candidates`/`resolve_toolchain_root` above, which deliberately accept
|
|
372
|
+
a name-only or non-ARM directory as a real, substitutable toolchain
|
|
373
|
+
(see this module's docstring on the two deliberate divergences).
|
|
374
|
+
|
|
375
|
+
`host_scan_has_toolchain` asks a different question -- "does the host
|
|
376
|
+
already have ITS OWN working toolchain, independent of tan's store" --
|
|
377
|
+
and a directory that merely STARTS WITH `zephyr-sdk`, with no compiler
|
|
378
|
+
inside it, answers "no", not "yes" (tan-cli#1209 review MAJOR: an empty
|
|
379
|
+
`~/zephyr-sdk-leftover/` -- a stale download, an interrupted manual
|
|
380
|
+
extraction -- was silently disabling tan's own verified store on any
|
|
381
|
+
host carrying one, because `_candidates` applies no validity probe by
|
|
382
|
+
design)."""
|
|
383
|
+
exe = "arm-zephyr-eabi-gcc.exe" if os.name == "nt" else "arm-zephyr-eabi-gcc"
|
|
384
|
+
try:
|
|
385
|
+
return root.joinpath(*_ZEPHYR_SDK_TOOLCHAIN_DIR, exe).is_file()
|
|
386
|
+
except OSError:
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def host_scan_has_toolchain(sdk_root: str | None) -> bool:
|
|
391
|
+
"""`True` when a USABLE `zephyr-sdk*` directory is visible under
|
|
392
|
+
`_scan_roots()` -- CMake's own `FindZephyr-sdk.cmake` prefix-scan
|
|
393
|
+
territory -- that is NOT itself tan's own ADR 0021 store leaf for
|
|
394
|
+
@sdk_root's pinned version.
|
|
395
|
+
|
|
396
|
+
tan-cli#1209 review MINOR: `verified_store_dir`'s caller
|
|
397
|
+
(`zephyr_env_overrides`, wired from `execute.py`) used to fill
|
|
398
|
+
`ZEPHYR_SDK_INSTALL_DIR` from tan's own store whenever it verified,
|
|
399
|
+
full stop -- outranking CMake's own prefix scan and the CMake user
|
|
400
|
+
package registry for EVERY slice, on EVERY host. `doctor_cmd.
|
|
401
|
+
_zephyr_sdk_scan_roots` never does that: it lists `_toolchain_store_
|
|
402
|
+
scan_root()` LAST among its scan roots, so a full, pre-existing host
|
|
403
|
+
SDK always wins there. tan's own store is installed `-t
|
|
404
|
+
arm-zephyr-eabi` ONLY, so forcing it into a spawned child's env ahead
|
|
405
|
+
of a fuller host SDK can fail a non-ARM slice (e.g.
|
|
406
|
+
`aarch64-zephyr-elf`) that configured fine without any
|
|
407
|
+
`ZEPHYR_SDK_INSTALL_DIR` override at all, and makes `tan build` use a
|
|
408
|
+
different toolchain than `tan doctor` reports. `execute_slices` calls
|
|
409
|
+
this before deciding whether to pass `verified_store_dir`'s result
|
|
410
|
+
through at all, so tan's own store fills the gap only when the host
|
|
411
|
+
scan finds nothing else -- the same "last resort" precedence doctor
|
|
412
|
+
already gives it.
|
|
413
|
+
|
|
414
|
+
The store exclusion is keyed on `_toolchain_store_dir(manifest)` -- the
|
|
415
|
+
one PER-VERSION leaf tan's own install for THIS pin uses -- never the
|
|
416
|
+
whole `_toolchain_store_scan_root()` (tan-cli#1209 review BLOCKER: this
|
|
417
|
+
used to key on the whole root, the exact form `doctor_cmd.
|
|
418
|
+
_host_toolchain_matching_pin`'s own docstring documents as wrong, and the
|
|
419
|
+
same mistake tan-cli#1186 already shipped and had to fix once).
|
|
420
|
+
Excluding the whole root hides every OTHER leaf living in the same
|
|
421
|
+
store too -- e.g. a stamped leaf for a version this checkout no longer
|
|
422
|
+
pins, still a real, independent, usable toolchain -- as well as
|
|
423
|
+
misreading path containment under the ADR 0021
|
|
424
|
+
`$ALP_TOOLCHAIN_ROOT`-pointed-at-an-ancestor escape hatch (`$HOME` or
|
|
425
|
+
`/opt`, ADR 0021's own documented bench/CI case), where the whole store
|
|
426
|
+
root coincides with a `_scan_roots()` root and would otherwise
|
|
427
|
+
swallow a genuinely independent, adjacent hand-install along with tan's
|
|
428
|
+
own leaf. `sdk_root=None` (no checkout resolved, no pin to protect)
|
|
429
|
+
excludes nothing -- matching `verified_store_dir(None)`, which returns
|
|
430
|
+
`None` regardless of this function's answer in that case.
|
|
431
|
+
|
|
432
|
+
Never raises: an unresolvable path reads as itself, matching
|
|
433
|
+
`_candidates`'s own `except OSError` fallbacks."""
|
|
434
|
+
manifest = _read_manifest(sdk_root)
|
|
435
|
+
store_leaf: Path | None = None
|
|
436
|
+
if manifest is not None:
|
|
437
|
+
try:
|
|
438
|
+
store_leaf = _toolchain_store_dir(manifest).resolve()
|
|
439
|
+
except OSError:
|
|
440
|
+
store_leaf = _toolchain_store_dir(manifest)
|
|
441
|
+
for candidate in _candidates(_scan_roots()):
|
|
442
|
+
entry = Path(candidate)
|
|
443
|
+
try:
|
|
444
|
+
resolved = entry.resolve()
|
|
445
|
+
except OSError:
|
|
446
|
+
resolved = entry
|
|
447
|
+
if store_leaf is not None and resolved.is_relative_to(store_leaf):
|
|
448
|
+
continue
|
|
449
|
+
if _host_toolchain_is_usable(resolved):
|
|
450
|
+
return True
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def resolve_toolchain_root() -> ToolchainResolution:
|
|
455
|
+
"""This host's `${TOOLCHAIN_ROOT}`, or the reason there is not exactly
|
|
456
|
+
one. See the module docstring for the measured oracle contract each
|
|
457
|
+
branch below reproduces.
|
|
458
|
+
|
|
459
|
+
Called LAZILY by `build_cmd` -- only for a plan that actually names the
|
|
460
|
+
token -- so the filesystem scan never runs for the plans every SDK emits
|
|
461
|
+
today, which name none. The laziness is a property of the CALL SITE, not
|
|
462
|
+
of this function: it does the work it is asked for, every time.
|
|
463
|
+
"""
|
|
464
|
+
env_dir = os.environ.get(ZEPHYR_SDK_INSTALL_DIR)
|
|
465
|
+
if env_dir:
|
|
466
|
+
try:
|
|
467
|
+
exists = Path(env_dir).exists()
|
|
468
|
+
except OSError:
|
|
469
|
+
exists = False
|
|
470
|
+
if exists:
|
|
471
|
+
# VERBATIM, not normalised: measured, the oracle substitutes the
|
|
472
|
+
# env value exactly as given, trailing slash and all. An operator
|
|
473
|
+
# who pinned this variable pinned a literal path, and rewriting
|
|
474
|
+
# it would make the substituted value disagree with what they set.
|
|
475
|
+
return ToolchainResolution(env_dir)
|
|
476
|
+
|
|
477
|
+
candidates = _candidates()
|
|
478
|
+
if len(candidates) == 1:
|
|
479
|
+
return ToolchainResolution(candidates[0])
|
|
480
|
+
if not candidates:
|
|
481
|
+
return ToolchainResolution(None, NO_TOOLCHAIN_ADVICE)
|
|
482
|
+
return ToolchainResolution(None, _several_installs_advice(candidates))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Per-slice reaction to a changed user `-D` set (tan-cli#1382).
|
|
3
|
+
|
|
4
|
+
Zephyr caches SHIELD / SNIPPET and friends (`zephyr_check_cache(... WATCH)`
|
|
5
|
+
restores them from `CACHED_*` and refuses a value change short of a pristine
|
|
6
|
+
build; sysbuild images keep their own caches), and `-U` fixes none of that. So a
|
|
7
|
+
changed `-D` set on an already-configured slice wipes that slice's build dir --
|
|
8
|
+
behind the same two structural guards as tan's other wipe (no `-d`/`--build-dir`
|
|
9
|
+
override, cwd under `build/`).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from tan.commands.build.configure_inputs import read_user_defines_stamp, write_user_defines_stamp
|
|
19
|
+
from tan.commands.build.manifest import cmake_cache_configured
|
|
20
|
+
from tan.core.user_defines import changed_defines
|
|
21
|
+
from tan.envelope import Issue
|
|
22
|
+
|
|
23
|
+
#: A non-empty cached SHIELD / SNIPPET: set by SOME earlier configure.
|
|
24
|
+
_CACHED_SELECTION = re.compile(r"^CACHED_(?:SHIELD|SNIPPET)(?::\w+)?=.+$", re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cache_holds_selection(cwd: Path) -> bool:
|
|
28
|
+
try:
|
|
29
|
+
text = (cwd / "build" / "CMakeCache.txt").read_text(encoding="utf-8", errors="replace")
|
|
30
|
+
except OSError:
|
|
31
|
+
return False
|
|
32
|
+
return _CACHED_SELECTION.search(text) is not None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _unsafe_wipe_reason(cwd: Path, build_root: Path) -> str | None:
|
|
36
|
+
"""Why `cwd/build` must not be removed, or None when it is safe: the slice
|
|
37
|
+
dir must resolve strictly inside `<build_root>/build`, and neither it nor
|
|
38
|
+
its `build` may be a symlink (rmtree would follow or refuse)."""
|
|
39
|
+
try:
|
|
40
|
+
root = (build_root / "build").resolve()
|
|
41
|
+
resolved = cwd.resolve()
|
|
42
|
+
except OSError as err:
|
|
43
|
+
return f"could not resolve the build dir ({err.strerror or type(err).__name__})"
|
|
44
|
+
if resolved == root or not resolved.is_relative_to(root):
|
|
45
|
+
return f"build dir resolves outside {build_root / 'build'}"
|
|
46
|
+
if cwd.is_symlink() or (cwd / "build").is_symlink():
|
|
47
|
+
return "the build dir is a symlink; not wiping"
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reconcile_user_defines(
|
|
52
|
+
core_id: str,
|
|
53
|
+
cwd: Path,
|
|
54
|
+
now: list[str],
|
|
55
|
+
*,
|
|
56
|
+
build_root: Path,
|
|
57
|
+
guards_ok: bool,
|
|
58
|
+
on_output: Callable[[str], None],
|
|
59
|
+
) -> tuple[list[Issue], bool]:
|
|
60
|
+
"""Wipe `cwd/build` when the user `-D` set differs from the stamp, and
|
|
61
|
+
return `(issues, stampable)`. `stampable` is False only when a change could
|
|
62
|
+
not be applied (a guard suppressed the wipe, or the wipe failed): the stamp
|
|
63
|
+
is then left alone so the next build still sees the change.
|
|
64
|
+
|
|
65
|
+
An UNSTAMPED configured dir (built before this stamp existed) is unknown,
|
|
66
|
+
not "no -D": it is wiped once only if its cache holds a SHIELD/SNIPPET or
|
|
67
|
+
the build now passes `-D`."""
|
|
68
|
+
if not cmake_cache_configured(cwd):
|
|
69
|
+
return [], True
|
|
70
|
+
before = read_user_defines_stamp(cwd)
|
|
71
|
+
changed = changed_defines(before or [], now)
|
|
72
|
+
if before is None and not changed and _cache_holds_selection(cwd):
|
|
73
|
+
changed = ["SHIELD/SNIPPET (cached by an earlier build)"]
|
|
74
|
+
if not changed:
|
|
75
|
+
return [], True
|
|
76
|
+
names = ", ".join(changed)
|
|
77
|
+
if not guards_ok:
|
|
78
|
+
message = (
|
|
79
|
+
f"{core_id}: user -D changed ({names}) but this slice's build dir is "
|
|
80
|
+
"redirected (`-d`/`--build-dir`, or a cwd outside `build/`), so tan did "
|
|
81
|
+
"not wipe it: the cache may still hold stale values -- use a fresh build dir"
|
|
82
|
+
)
|
|
83
|
+
on_output(f"note: {message}")
|
|
84
|
+
return [Issue("build.configure-cache-stale", "warning", message)], False
|
|
85
|
+
refusal = _unsafe_wipe_reason(cwd, build_root)
|
|
86
|
+
if refusal is not None:
|
|
87
|
+
message = (
|
|
88
|
+
f"{core_id}: user -D changed ({names}) but tan did not wipe the build dir: "
|
|
89
|
+
f"{refusal}. The cache may still hold stale values -- use a fresh build dir"
|
|
90
|
+
)
|
|
91
|
+
on_output(f"note: {message}")
|
|
92
|
+
return [Issue("build.configure-cache-stale", "warning", message)], False
|
|
93
|
+
try:
|
|
94
|
+
shutil.rmtree(cwd / "build")
|
|
95
|
+
except OSError as err:
|
|
96
|
+
detail = err.strerror or str(err.args[0] if err.args else type(err).__name__)
|
|
97
|
+
message = (
|
|
98
|
+
f"{core_id}: user -D changed ({names}) but the build dir could not be wiped: {detail}"
|
|
99
|
+
)
|
|
100
|
+
on_output(f"note: {message}")
|
|
101
|
+
return [Issue("build.configure-cache-stale", "warning", message)], False
|
|
102
|
+
message = (
|
|
103
|
+
f"{core_id}: user -D changed ({names}) -- wiped this slice's build dir so Zephyr "
|
|
104
|
+
"re-configures from scratch instead of keeping the cached value (tan-cli#1382)"
|
|
105
|
+
)
|
|
106
|
+
on_output(f"note: {message}")
|
|
107
|
+
return [Issue("build.configure-cache-reset", "info", message)], True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def stamp_user_defines(cwd: Path, now: list[str]) -> None:
|
|
111
|
+
"""Record what the configure about to run is given (best-effort)."""
|
|
112
|
+
try:
|
|
113
|
+
write_user_defines_stamp(cwd, now)
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan build`'s workspace-patch warning and `$ZEPHYR_BASE` note (tan-cli#1376).
|
|
3
|
+
|
|
4
|
+
Warning-only by design: a missing patch never changes `ok` or the exit code
|
|
5
|
+
(a workspace can be deliberately unpatched, and the check can be wrong about a
|
|
6
|
+
narrow one). Promoting `build.workspace-patches-missing` to an error is a
|
|
7
|
+
one-line severity change here, left for a later decision.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from tan.commands.workspace_patch_check import MISSING, check_workspace_patches
|
|
15
|
+
from tan.core.venv import west_workspace_dir
|
|
16
|
+
from tan.core.west_patches import describe_unapplied, patch_fix_text, zephyr_base_note
|
|
17
|
+
from tan.envelope import Issue
|
|
18
|
+
|
|
19
|
+
#: The consumer build directory (`build_cmd.CONSUMER_BUILD_ROOT`; not imported
|
|
20
|
+
#: because `build_cmd` imports this module). `build_root` here is the project
|
|
21
|
+
#: root, so the cache belongs under `<project>/build/`, not beside board.yaml.
|
|
22
|
+
_BUILD_DIR = "build"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def workspace_patch_issues(
|
|
26
|
+
build_root: Path, sdk_root: str | None, *, has_zephyr_slice: bool
|
|
27
|
+
) -> list[Issue]:
|
|
28
|
+
"""Issues for the resolved workspace of a plan with a Zephyr slice; never
|
|
29
|
+
raises, empty when there is nothing to say (including "could not check")."""
|
|
30
|
+
if not has_zephyr_slice:
|
|
31
|
+
return []
|
|
32
|
+
try:
|
|
33
|
+
workspace = west_workspace_dir(str(build_root), Path(sdk_root) if sdk_root else None)
|
|
34
|
+
if workspace is None:
|
|
35
|
+
return []
|
|
36
|
+
issues: list[Issue] = []
|
|
37
|
+
result = check_workspace_patches(
|
|
38
|
+
workspace, sdk_root, cache_dir=build_root / _BUILD_DIR
|
|
39
|
+
)
|
|
40
|
+
if result.state == MISSING:
|
|
41
|
+
issues.append(
|
|
42
|
+
Issue(
|
|
43
|
+
"build.workspace-patches-missing",
|
|
44
|
+
"warning",
|
|
45
|
+
f"alp-sdk's zephyr/patches.yml is not applied in {workspace}: "
|
|
46
|
+
f"{describe_unapplied(result.patches, result.modules)}. The build can "
|
|
47
|
+
"succeed yet fail at runtime (for example `alp_camera_open` returns "
|
|
48
|
+
f"ALP_ERR_NOSUPPORT). Fix: {patch_fix_text(result.modules, str(workspace))}.",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
if result.cache_note:
|
|
52
|
+
issues.append(
|
|
53
|
+
Issue(
|
|
54
|
+
"build.workspace-patches-uncached",
|
|
55
|
+
"info",
|
|
56
|
+
"the workspace patch check is not cached, so every build "
|
|
57
|
+
f"re-verifies it: {result.cache_note}.",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
note = zephyr_base_note(os.environ.get("ZEPHYR_BASE"), str(workspace / "zephyr"))
|
|
61
|
+
if note is not None:
|
|
62
|
+
issues.append(Issue("build.zephyr-base-ignored", "info", note))
|
|
63
|
+
return issues
|
|
64
|
+
except Exception: # noqa: BLE001 -- an advisory check must never break a build
|
|
65
|
+
return []
|