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,168 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Will Zephyr's devicetree LINT pass run for a build on this host?
|
|
3
|
+
|
|
4
|
+
Zephyr compiles the devicetree with its own Python `gen_defines.py`; `dtc`
|
|
5
|
+
is not needed for that and a build with no `dtc` anywhere still links a real
|
|
6
|
+
`zephyr.elf`. What `dtc` is used for is a SECOND, diagnostics-only pass --
|
|
7
|
+
`zephyr/cmake/modules/dts.cmake` runs the compiler over the assembled `.dts`
|
|
8
|
+
under `if(DTC)`, above the verbatim comment *"This is just to generate
|
|
9
|
+
warnings and errors; we discard the output"*, with `-E unit_address_vs_reg`
|
|
10
|
+
and (when the binary accepts it) `-Wunique_unit_address_if_enabled`. No
|
|
11
|
+
`dtc`, no `if(DTC)` body, no diagnostics -- and nothing on either side says
|
|
12
|
+
so, because the block is skipped rather than failed.
|
|
13
|
+
|
|
14
|
+
**Why this module exists at all: tan-cli#1176/#1178 changed the premise.**
|
|
15
|
+
`tan bootstrap` acquires the Zephyr SDK with `--no-hosttools`
|
|
16
|
+
(`tan.core.toolchain_provision.NO_HOSTTOOLS_FLAG`), so the install carries no
|
|
17
|
+
`hosttools/` tree -- and `hosttools/` is where the SDK's own `dtc` lives
|
|
18
|
+
(measured on a real `zephyr-sdk-1.0.1`:
|
|
19
|
+
`hosttools/sysroots/x86_64-pokysdk-linux/usr/bin/dtc`, `Version: DTC v1.7.0+`,
|
|
20
|
+
beside `openocd`/`fdtdump`/`fdtget`/`fdtoverlay`/`fdtput`). Before #1178 the
|
|
21
|
+
SDK put a `dtc` in CMake's reach on every install; now it does not, so the
|
|
22
|
+
lint pass silently stopped running on the default path. That is the whole of
|
|
23
|
+
tan-cli#1192.
|
|
24
|
+
|
|
25
|
+
**This partially reverses a decision `tan.commands.doctor_cmd`'s own module
|
|
26
|
+
docstring records as deliberate** -- `dtc`/`gperf` are named there as the
|
|
27
|
+
frozen Rust oracle's `--build` check vocabulary, NOT ported. That decision was
|
|
28
|
+
correct when it was made and its premise is gone: it was written when `dtc`
|
|
29
|
+
arrived with the SDK, so a `dtc` probe could only ever restate what
|
|
30
|
+
`zephyrSdk` already said. `gperf` stays unported; only `dtc` is reopened here,
|
|
31
|
+
and only as the devicetree-lint question, never as a bare is-it-on-PATH probe.
|
|
32
|
+
|
|
33
|
+
## The two CMake mechanisms this mirrors, and why BOTH are needed
|
|
34
|
+
|
|
35
|
+
`find_program(DTC dtc)` (`zephyr/cmake/modules/FindDtc.cmake`) searches
|
|
36
|
+
`CMAKE_PREFIX_PATH` BEFORE the ambient `PATH`, and the Zephyr SDK's own
|
|
37
|
+
`cmake/zephyr/host-tools.cmake` is what puts its `hosttools/` on that prefix
|
|
38
|
+
list -- `list(APPEND CMAKE_PREFIX_PATH ${HOST_TOOLS_HOME}/usr)`, where
|
|
39
|
+
`HOST_TOOLS_HOME` is the poky sysroot on Linux, `<sdk>/hosttools` on macOS,
|
|
40
|
+
and on WINDOWS is never given a `dtc`-bearing prefix at all (that branch
|
|
41
|
+
appends only `qemu`/`qemu-arc`/`openocd`, and the Windows hosttools archive
|
|
42
|
+
ships neither `dtc` nor `gperf` -- `tan.core.bootstrap`'s
|
|
43
|
+
`manual_install_windows` prose records the `7z l` measurement). So a probe
|
|
44
|
+
that looked only at `PATH` would report "no lint" on a host whose SDK carries
|
|
45
|
+
one, which is a report that lies; [`hosttools_bin_dir`] is the other half.
|
|
46
|
+
|
|
47
|
+
`find_package(Dtc 1.4.6)` (`dts.cmake`, line 10 of the same file) is the
|
|
48
|
+
second mechanism and the one that makes this check worth having a `warn` arm
|
|
49
|
+
at all. `FindDtc.cmake` runs `dtc --version`, scrapes `Version: DTC v?X.Y.Z`
|
|
50
|
+
out of it, hands the result to `find_package_handle_standard_args`, and then:
|
|
51
|
+
|
|
52
|
+
if(NOT Dtc_FOUND)
|
|
53
|
+
# DTC was found but version requirement is not met, or dtc was not working.
|
|
54
|
+
# Treat it as DTC was never found by resetting the result from `find_program()`
|
|
55
|
+
set(DTC DTC-NOTFOUND CACHE FILEPATH "Path to a program" FORCE)
|
|
56
|
+
endif()
|
|
57
|
+
|
|
58
|
+
A `dtc` that IS installed but is older than [`DTC_MIN_VERSION`], or whose
|
|
59
|
+
`--version` does not run or does not parse, is therefore reset to
|
|
60
|
+
`DTC-NOTFOUND` and the lint is skipped -- with no message. That host is the
|
|
61
|
+
one this module has something to say to: somebody installed `dtc` on purpose,
|
|
62
|
+
believes the lint runs, and it does not. A host with NO `dtc` is the ordinary,
|
|
63
|
+
supported, documented post-#1178 state and gets no warning at all; see
|
|
64
|
+
`doctor_cmd.devicetree_lint_check` for the severity argument.
|
|
65
|
+
|
|
66
|
+
Pure: no subprocess, no filesystem, no environment. `doctor_cmd` owns the IO
|
|
67
|
+
(`_resolve_dtc`) and builds the `Check`.
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import re
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
from typing import NamedTuple
|
|
74
|
+
|
|
75
|
+
#: `zephyr/cmake/modules/dts.cmake`'s own `find_package(Dtc 1.4.6)`. A `dtc`
|
|
76
|
+
#: below this is reset to `DTC-NOTFOUND` by `FindDtc.cmake` -- see the module
|
|
77
|
+
#: docstring. Compared component-wise, `>=`, non-EXACT, exactly as CMake's
|
|
78
|
+
#: `find_package_handle_standard_args` compares a `VERSION_VAR`.
|
|
79
|
+
DTC_MIN_VERSION: tuple[int, int, int] = (1, 4, 6)
|
|
80
|
+
|
|
81
|
+
#: `FindDtc.cmake`'s own scrape, transcribed: `string(REGEX MATCH "Version:
|
|
82
|
+
#: DTC v?([0-9]+[.][0-9]+[.][0-9]+).*" ...)`. Deliberately NOT anchored and
|
|
83
|
+
#: deliberately tolerant of a trailing suffix -- the SDK's own binary answers
|
|
84
|
+
#: `Version: DTC v1.7.0+` (measured), whose `+` CMake drops the same way this
|
|
85
|
+
#: does.
|
|
86
|
+
_DTC_VERSION_RE = re.compile(r"Version: DTC v?([0-9]+)[.]([0-9]+)[.]([0-9]+)")
|
|
87
|
+
|
|
88
|
+
#: Where the resolved `dtc` came from. `hosttools` means the Zephyr SDK's own
|
|
89
|
+
#: copy, reached through the `CMAKE_PREFIX_PATH` append `host-tools.cmake`
|
|
90
|
+
#: does; `path` means the ambient `PATH`, which CMake searches second;
|
|
91
|
+
#: `absent` means neither had one, which is what a `--no-hosttools` install
|
|
92
|
+
#: with no distro `dtc` looks like.
|
|
93
|
+
ORIGIN_HOSTTOOLS = "hosttools"
|
|
94
|
+
ORIGIN_PATH = "path"
|
|
95
|
+
ORIGIN_ABSENT = "absent"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DtcResolution(NamedTuple):
|
|
99
|
+
"""What a host's `dtc` situation is, in the terms CMake would resolve it.
|
|
100
|
+
|
|
101
|
+
`version` is `None` both when there is no binary to ask and when the
|
|
102
|
+
binary answered something `FindDtc.cmake`'s own regex would not match --
|
|
103
|
+
the two are distinguished by `origin`, not by this field, because CMake
|
|
104
|
+
does not distinguish them either: both end at `DTC-NOTFOUND`.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
path: str | None
|
|
108
|
+
origin: str
|
|
109
|
+
version: tuple[int, int, int] | None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_dtc_version(text: str | None) -> tuple[int, int, int] | None:
|
|
113
|
+
"""`dtc --version` output -> `(major, minor, patch)`, or `None` when
|
|
114
|
+
`FindDtc.cmake`'s regex would not have matched it either."""
|
|
115
|
+
if not text:
|
|
116
|
+
return None
|
|
117
|
+
found = _DTC_VERSION_RE.search(text)
|
|
118
|
+
if found is None:
|
|
119
|
+
return None
|
|
120
|
+
return (int(found.group(1)), int(found.group(2)), int(found.group(3)))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def format_version(version: tuple[int, int, int]) -> str:
|
|
124
|
+
return ".".join(str(part) for part in version)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def hosttools_bin_dir(sdk_install_dir: Path, host_os: str, host_arch: str) -> Path | None:
|
|
128
|
+
"""The directory `find_program(DTC dtc)` would find the SDK's own `dtc`
|
|
129
|
+
in, given an install at `sdk_install_dir` -- or `None` on a host whose
|
|
130
|
+
SDK never puts one on `CMAKE_PREFIX_PATH`.
|
|
131
|
+
|
|
132
|
+
One branch per branch of `<sdk>/cmake/zephyr/host-tools.cmake`, whose
|
|
133
|
+
`list(APPEND CMAKE_PREFIX_PATH ${HOST_TOOLS_HOME}/usr)` is what makes the
|
|
134
|
+
bundle findable; `find_program` appends `bin` to a prefix itself.
|
|
135
|
+
|
|
136
|
+
* `linux` -- `hosttools/sysroots/<arch>-pokysdk-linux/usr/bin`, the arch
|
|
137
|
+
token coming from `cmake_host_system_information(... OS_PLATFORM)`, i.e.
|
|
138
|
+
`uname -m`, which is the same token `doctor_cmd._host_os_arch_tags`
|
|
139
|
+
already normalises (`x86_64`/`aarch64`).
|
|
140
|
+
* `macos` -- `hosttools/usr/bin`; the per-component `opt/` trees are
|
|
141
|
+
symlinked into that one directory by the SDK itself.
|
|
142
|
+
* `windows` -- `None`, and that is a fact about the SDK, not a gap here:
|
|
143
|
+
the Windows branch appends only `qemu`, `qemu-arc` and `openocd`
|
|
144
|
+
prefixes, and the Windows hosttools archive ships no `dtc` to append a
|
|
145
|
+
prefix for. On Windows `dtc` can only ever come from `PATH`.
|
|
146
|
+
"""
|
|
147
|
+
if host_os == "linux":
|
|
148
|
+
return (
|
|
149
|
+
sdk_install_dir
|
|
150
|
+
/ "hosttools"
|
|
151
|
+
/ "sysroots"
|
|
152
|
+
/ f"{host_arch}-pokysdk-linux"
|
|
153
|
+
/ "usr"
|
|
154
|
+
/ "bin"
|
|
155
|
+
)
|
|
156
|
+
if host_os == "macos":
|
|
157
|
+
return sdk_install_dir / "hosttools" / "usr" / "bin"
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def lint_will_run(resolution: DtcResolution) -> bool:
|
|
162
|
+
"""`Dtc_FOUND`, computed the way `FindDtc.cmake` computes it: a binary was
|
|
163
|
+
resolved AND its version parsed AND that version is at least
|
|
164
|
+
[`DTC_MIN_VERSION`]. Anything else is the `set(DTC DTC-NOTFOUND ... FORCE)`
|
|
165
|
+
reset, and `dts.cmake`'s `if(DTC)` block does not run."""
|
|
166
|
+
if resolution.path is None or resolution.version is None:
|
|
167
|
+
return False
|
|
168
|
+
return resolution.version >= DTC_MIN_VERSION
|
tan/core/dir_removal.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Shared, hardened directory-removal primitives (tan-cli#790).
|
|
3
|
+
|
|
4
|
+
**Extracted from `tan.commands.clean_cmd`, verbatim logic**, where these six
|
|
5
|
+
names (`is_link`, `os_error_text`, `_reraise_removal_failure`,
|
|
6
|
+
`_retry_after_clearing_readonly`, `_RMTREE_HOOK`, `remove_dir`) first landed
|
|
7
|
+
for `tan clean` and were battle-tested against the frozen v0.4.1 oracle on
|
|
8
|
+
real read-only build outputs, real junctions, and real symlinked build roots
|
|
9
|
+
(see each function's own docstring for the specific oracle-diff that shaped
|
|
10
|
+
it). `tan sdk remove` needs the identical hardening -- a real `tan bootstrap`
|
|
11
|
+
tree can carry a read-only `.git/objects/**` entry from a shallow clone, and
|
|
12
|
+
neither command's job is to reinvent "how does this host actually let me
|
|
13
|
+
delete a stubborn directory" independently.
|
|
14
|
+
|
|
15
|
+
**Lives in `tan.core`, not a second copy in `tan.commands.sdk_cmd`, and not
|
|
16
|
+
left where it was**, for the same reason `sdk_discovery.py`'s own module
|
|
17
|
+
docstring gives for why IT moved out of a command module: `tan.core` may
|
|
18
|
+
import no `tan.commands.*` module (the invariant `tan.core.shapes` and
|
|
19
|
+
`tan.core.sdk_discovery` already state and rely on), so a `tan/core/`
|
|
20
|
+
caller — this module has none yet, but `tan.core.sdk_removal` (tan-cli#790)
|
|
21
|
+
is the first — could not reach these primitives while they lived inside
|
|
22
|
+
`clean_cmd.py` without creating exactly the `tan.core` -> `tan.commands`
|
|
23
|
+
inversion that rule exists to forbid.
|
|
24
|
+
|
|
25
|
+
`clean_cmd.py` re-imports every one of these six names at its own module
|
|
26
|
+
level (`from tan.core.dir_removal import ...`) rather than losing them: its
|
|
27
|
+
own internal call sites still say the bare `_remove_dir(...)` /
|
|
28
|
+
`_retry_after_clearing_readonly(...)`, resolved through `clean_cmd`'s own
|
|
29
|
+
module globals at call time, so `monkeypatch.setattr(clean_cmd, "_remove_dir",
|
|
30
|
+
...)` in the existing test suite keeps working unchanged -- the monkeypatch
|
|
31
|
+
only cares which module attribute it is overwriting, never which module
|
|
32
|
+
originally defined the value that attribute pointed at.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
import shutil
|
|
38
|
+
import stat
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_link(path: str) -> bool:
|
|
43
|
+
"""Whether `path` is a link that must not be followed -- a POSIX symlink, a
|
|
44
|
+
Windows directory symlink, OR a Windows JUNCTION.
|
|
45
|
+
|
|
46
|
+
**`os.path.islink` is not this test.** On Windows `ntpath.islink` returns
|
|
47
|
+
True only for `IO_REPARSE_TAG_SYMLINK`; a junction is
|
|
48
|
+
`IO_REPARSE_TAG_MOUNT_POINT`, and `stat.S_ISLNK` is False for it as well.
|
|
49
|
+
Measured on this host: for `build/` junctioned at an out-of-tree directory,
|
|
50
|
+
`os.path.islink` and `S_ISLNK` both report False while
|
|
51
|
+
`st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT`. A guard written on
|
|
52
|
+
`os.path.islink` therefore lets a junction reach `shutil.rmtree` -- which
|
|
53
|
+
has its OWN, correct check (`shutil._rmtree_islink`, mirrored here) and
|
|
54
|
+
refuses, so nothing outside the tree is destroyed, but the junction is then
|
|
55
|
+
never cleaned and the run reports a spurious `remove-failed`. This was a
|
|
56
|
+
live defect in the first cut of `tan clean`'s port, caught only by diffing
|
|
57
|
+
against the Rust binary.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
st = os.lstat(path)
|
|
61
|
+
except (OSError, ValueError):
|
|
62
|
+
return False
|
|
63
|
+
if stat.S_ISLNK(st.st_mode):
|
|
64
|
+
return True
|
|
65
|
+
attributes = getattr(st, "st_file_attributes", 0)
|
|
66
|
+
return bool(
|
|
67
|
+
attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
|
|
68
|
+
and getattr(st, "st_reparse_tag", 0)
|
|
69
|
+
== getattr(stat, "IO_REPARSE_TAG_MOUNT_POINT", -1)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def os_error_text(err: BaseException) -> str:
|
|
74
|
+
"""An `OSError` rendered the way Rust's `io::Error` Display renders it:
|
|
75
|
+
`<system message> (os error <code>)`.
|
|
76
|
+
|
|
77
|
+
Python's own `str(OSError)` is `[WinError 32] <message>: '<path>'`, which
|
|
78
|
+
both differs from the oracle and repeats a path the message already names.
|
|
79
|
+
The Windows error code (`winerror`) is preferred over the translated
|
|
80
|
+
`errno`, matching Rust, which reports the raw OS code.
|
|
81
|
+
|
|
82
|
+
One character still differs on Windows: `FormatMessageW` ends its sentences
|
|
83
|
+
with a period and Rust keeps it, while Python's `strerror` strips it. Not
|
|
84
|
+
synthesized here -- guessing at punctuation inside a system message is worse
|
|
85
|
+
than a documented one-character divergence in a warning string.
|
|
86
|
+
"""
|
|
87
|
+
if not isinstance(err, OSError):
|
|
88
|
+
return str(err)
|
|
89
|
+
code = getattr(err, "winerror", None) or err.errno
|
|
90
|
+
if err.strerror is None or code is None:
|
|
91
|
+
return str(err)
|
|
92
|
+
return f"{err.strerror} (os error {code})"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#: The two functions `shutil.rmtree` hands its error hook that CANNOT be called
|
|
96
|
+
#: with one positional argument. On POSIX `rmtree` runs the fd-based
|
|
97
|
+
#: `_rmtree_safe_fd` walk, which reports failures of `os.open` (shutil 3.12
|
|
98
|
+
#: lines 682 and 781) and `os.close` (692/712/791/808) through the same hook as
|
|
99
|
+
#: the one-argument `os.scandir`/`os.unlink`/`os.rmdir`/`os.lstat`. `os.open`
|
|
100
|
+
#: needs `flags` and `os.close` takes an fd, not the path the hook is handed --
|
|
101
|
+
#: so retrying either is a `TypeError`, not a repair. Windows' `_rmtree_unsafe`
|
|
102
|
+
#: never passes these, which is why the crash class this guards is POSIX-only.
|
|
103
|
+
_NOT_RETRYABLE_WITH_PATH_ALONE = frozenset({os.open, os.close})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _reraise_removal_failure(func, path, exc) -> None:
|
|
107
|
+
"""Re-raise the failure `shutil.rmtree` reported, so the caller's
|
|
108
|
+
`except (OSError, ValueError)` sees it and can report it as a removal
|
|
109
|
+
failure rather than a silent no-op.
|
|
110
|
+
|
|
111
|
+
`exc` is the exception under `onexc` and an `exc_info` TUPLE under the
|
|
112
|
+
deprecated `onerror` (see `_RMTREE_HOOK`), so both shapes are unwrapped
|
|
113
|
+
here. A hook that has nothing to re-raise still must not return quietly --
|
|
114
|
+
`rmtree` would then report the tree as removed -- so the fallback states the
|
|
115
|
+
operation that failed.
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(exc, tuple) and len(exc) == 3:
|
|
118
|
+
exc = exc[1]
|
|
119
|
+
if isinstance(exc, BaseException):
|
|
120
|
+
raise exc
|
|
121
|
+
raise OSError(f"{getattr(func, '__name__', func)} failed on {path}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _retry_after_clearing_readonly(func, path, exc=None) -> None:
|
|
125
|
+
"""`shutil.rmtree` error hook: clear the read-only bit and retry once.
|
|
126
|
+
|
|
127
|
+
Rust's `remove_dir_all` deletes a read-only file on Windows outright (it
|
|
128
|
+
passes `FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE`), where `shutil.rmtree`
|
|
129
|
+
fails the WHOLE tree with `[WinError 5] Access is denied`. Measured against
|
|
130
|
+
the Rust binary, from `tan clean`'s own port: one read-only file inside
|
|
131
|
+
`build/` had Rust remove the build dir and exit 0 while a naive port left
|
|
132
|
+
every artefact in place and warned. Read-only build outputs -- and, for
|
|
133
|
+
`tan sdk remove`'s own caller, read-only `.git/objects/**` entries left by
|
|
134
|
+
a shallow `git clone` -- are ORDINARY, not exotic, so this is the primary
|
|
135
|
+
path here, not a fallback.
|
|
136
|
+
|
|
137
|
+
`st_mode | S_IWUSR` rather than a bare `S_IWRITE`: on POSIX the latter would
|
|
138
|
+
replace the whole mode with `0o200` and strip the owner's read/execute bits
|
|
139
|
+
from a directory mid-walk. A failure here propagates out of `rmtree` and is
|
|
140
|
+
the caller's to report.
|
|
141
|
+
|
|
142
|
+
The retry is only attempted for a `func` that a path alone can drive
|
|
143
|
+
([`_NOT_RETRYABLE_WITH_PATH_ALONE`]). It used to end in a bare `func(path)`,
|
|
144
|
+
so a build directory the invoking user OWNS but cannot open -- a
|
|
145
|
+
`chmod -R a-r` or tar-preserved tree, where `os.chmod` SUCCEEDS and
|
|
146
|
+
`os.open` is what failed -- raised `TypeError: open() missing required
|
|
147
|
+
argument 'flags' (pos 2)`. That is neither `OSError` nor `ValueError`, so it
|
|
148
|
+
used to sail past `tan clean`'s best-effort guard, abort the target loop and
|
|
149
|
+
hit its outer catch-all: exit 5, `clean.internal-failure`, and every
|
|
150
|
+
remaining target silently skipped. Measured on the oracle for the same
|
|
151
|
+
tree: exit 0, a warning, and the rest of the run completing -- which is
|
|
152
|
+
what re-raising the original failure (rather than a `TypeError`) restores.
|
|
153
|
+
|
|
154
|
+
**Also clears the PARENT directory's own write bit** (tan-cli#790,
|
|
155
|
+
`sdk.remove`'s own maintainer follow-up correction). On POSIX, `unlink`
|
|
156
|
+
and `rmdir` consult only the CONTAINING directory's write+execute
|
|
157
|
+
permission -- a file or directory's OWN mode bits are irrelevant to
|
|
158
|
+
whether it can be removed. So the chmod above, alone, fixes nothing on
|
|
159
|
+
POSIX for the one case that is genuinely common there: a read-only
|
|
160
|
+
directory (`chmod 555 somedir`, or exactly what a bootstrapped SDK tree's
|
|
161
|
+
`.git/objects/xx/` subdirectories carry after certain git versions'
|
|
162
|
+
packing) defeats the unlink of every child inside it, and `path` here is
|
|
163
|
+
the CHILD's path, whose own mode was never the problem. Measured: with
|
|
164
|
+
only `path` chmod'd, `shutil.rmtree` over a tree containing one such
|
|
165
|
+
directory still raised `PermissionError` on the first child inside it.
|
|
166
|
+
Chmod'ing `os.path.dirname(path)` too -- best-effort, a failure here is
|
|
167
|
+
swallowed rather than propagated, because the ORIGINAL failure (or
|
|
168
|
+
whatever `func(path)` raises next) is the honest one to report if this
|
|
169
|
+
does not fix it -- closes that gap. This is additive to the Windows
|
|
170
|
+
read-only-ATTRIBUTE fix above, not a replacement for it: the two failure
|
|
171
|
+
shapes (an NTFS attribute on the file itself; a POSIX permission bit on
|
|
172
|
+
its parent) are independent and this function now clears both.
|
|
173
|
+
"""
|
|
174
|
+
if func in _NOT_RETRYABLE_WITH_PATH_ALONE:
|
|
175
|
+
_reraise_removal_failure(func, path, exc)
|
|
176
|
+
# tan-cli#1482: never follow a link -- chmod'ing through it would change the
|
|
177
|
+
# permissions of a file OUTSIDE the tree being removed, and `os.stat` on a
|
|
178
|
+
# dangling link raised before the parent fix below could run. A link needs
|
|
179
|
+
# only its parent's write bit (POSIX unlink consults the parent alone).
|
|
180
|
+
try:
|
|
181
|
+
if not stat.S_ISLNK(os.lstat(path).st_mode):
|
|
182
|
+
os.chmod(path, os.lstat(path).st_mode | stat.S_IWUSR)
|
|
183
|
+
except OSError:
|
|
184
|
+
pass
|
|
185
|
+
parent = os.path.dirname(path)
|
|
186
|
+
if parent and parent != path:
|
|
187
|
+
try:
|
|
188
|
+
os.chmod(parent, os.stat(parent).st_mode | stat.S_IWUSR | stat.S_IXUSR)
|
|
189
|
+
except OSError:
|
|
190
|
+
pass
|
|
191
|
+
try:
|
|
192
|
+
func(path)
|
|
193
|
+
except TypeError:
|
|
194
|
+
# Belt and braces for a future `shutil` that routes one more
|
|
195
|
+
# many-argument callable through this hook: the removal still failed,
|
|
196
|
+
# and it must be reported as such rather than escaping as a `TypeError`.
|
|
197
|
+
_reraise_removal_failure(func, path, exc)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
#: `shutil.rmtree`'s error-hook keyword. `onerror` is deprecated from 3.12 and
|
|
201
|
+
#: scheduled for removal; `onexc` does not exist before it. Selected once here so
|
|
202
|
+
#: the call site stays a single expression on either interpreter -- the handler
|
|
203
|
+
#: signature is compatible because it ignores its third argument, which is the
|
|
204
|
+
#: only thing the two hooks disagree about (`exc_info` tuple vs exception).
|
|
205
|
+
_RMTREE_HOOK = "onexc" if sys.version_info >= (3, 12) else "onerror"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def remove_dir(path: str) -> None:
|
|
209
|
+
"""Remove a directory target recursively, never following a link out of the
|
|
210
|
+
tree, and clearing a read-only attribute/bit in the way instead of failing
|
|
211
|
+
the whole removal on it.
|
|
212
|
+
|
|
213
|
+
A link ([`is_link`]) is unlinked ITSELF, exactly as the oracle's
|
|
214
|
+
`remove_dir_all` does on Windows: verified against the Rust binary with
|
|
215
|
+
`build/` junctioned at an out-of-tree directory -- the junction goes, the
|
|
216
|
+
target's contents stay. `shutil.rmtree` handles the ordinary case and never
|
|
217
|
+
recurses through a link INSIDE the tree, so both arms are contained.
|
|
218
|
+
|
|
219
|
+
`os.rmdir` before `os.unlink`: on Windows a junction or directory symlink is
|
|
220
|
+
removed by `RemoveDirectory`, and `unlink` fails on it; on POSIX `rmdir`
|
|
221
|
+
fails on a symlink and `unlink` is what removes it.
|
|
222
|
+
"""
|
|
223
|
+
if is_link(path):
|
|
224
|
+
try:
|
|
225
|
+
os.rmdir(path)
|
|
226
|
+
except OSError:
|
|
227
|
+
os.unlink(path)
|
|
228
|
+
return
|
|
229
|
+
shutil.rmtree(path, **{_RMTREE_HOOK: _retry_after_clearing_readonly})
|
tan/core/doctor_git.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""SDK-provenance git helpers, extracted out of `tan.commands.doctor_cmd`
|
|
3
|
+
(tan-cli#797 module-size budget) rather than left to grow that file further.
|
|
4
|
+
|
|
5
|
+
Every spawn here resolves `git` through `tan.core.tool_lookup.resolve_tool`
|
|
6
|
+
FIRST and threads the result through as `executable=` on the actual
|
|
7
|
+
`subprocess`/`probe` call -- never a bare `"git"` argv[0], which on Windows
|
|
8
|
+
`CreateProcess` searches the current directory for ahead of `%PATH%`
|
|
9
|
+
(tan-cli#797, the same hazard class tan-cli#567/#488 closed for
|
|
10
|
+
flash/size/west). `doctor_cmd` re-exports every name here (a plain
|
|
11
|
+
`from tan.core.doctor_git import ...`) so `doctor_cmd._resolve_git_executable`
|
|
12
|
+
etc. keep resolving for every existing caller and test.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from tan.core.probe import PROBE_TIMEOUT_S, isolated_cwd, probe
|
|
21
|
+
from tan.core.subprocess_env import spawn_env
|
|
22
|
+
from tan.core.tool_lookup import resolve_tool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_git_executable() -> str | None:
|
|
26
|
+
"""The absolute path to `git`, resolved through the shared
|
|
27
|
+
`tool_lookup.resolve_tool` walk -- never `shutil.which`, which on Windows
|
|
28
|
+
inserts the current directory ahead of `%PATH%` (see `tool_lookup`'s own
|
|
29
|
+
docstring). `None` when `git` is not on PATH at all, which every caller
|
|
30
|
+
below must treat as "no git provenance to report", not a crash
|
|
31
|
+
(tan-cli#797): a doctor that raises because git is absent is worse than
|
|
32
|
+
the bare-argv0 spawn it replaces.
|
|
33
|
+
"""
|
|
34
|
+
return resolve_tool("git", os.environ).resolved
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_own_git_checkout(root: str, git_exe: str | None) -> bool:
|
|
38
|
+
"""Whether `root` is itself the TOP of a git checkout, not merely nested
|
|
39
|
+
somewhere inside an unrelated ENCLOSING one (tan-cli#488 defect 4).
|
|
40
|
+
|
|
41
|
+
`git -C <root> ...` discovery walks UPWARD looking for a `.git`, so an SDK
|
|
42
|
+
with no `.git` of its own -- an extracted release archive, vendored under
|
|
43
|
+
a customer's own application repository, which this port explicitly
|
|
44
|
+
supports (`tan.commands.sdk_cmd.check_sdk_readiness`'s own docstring names
|
|
45
|
+
exactly this shape) -- answers every git query with the ENCLOSING repo's
|
|
46
|
+
state instead of "not a checkout". `_git_short_commit`/`_git_behind_
|
|
47
|
+
upstream` below both call this FIRST and refuse (`None`) rather than
|
|
48
|
+
misattribute a foreign repository's commit or upstream-skew as the SDK's
|
|
49
|
+
own.
|
|
50
|
+
|
|
51
|
+
Compares the RESOLVED `--show-toplevel` against the RESOLVED `root`:
|
|
52
|
+
lexical string comparison alone would false-negative on a symlinked or
|
|
53
|
+
differently-cased path to the same directory.
|
|
54
|
+
|
|
55
|
+
`git_exe` -- the PATH-RESOLVED absolute `git`, from `_resolve_git_executable`
|
|
56
|
+
(tan-cli#797). `None` when `git` is not on PATH at all: refuses outright
|
|
57
|
+
rather than falling back to a bare `probe(["git", ...])`, which would
|
|
58
|
+
re-open the exact cwd-shadowing hazard this resolution exists to close.
|
|
59
|
+
"""
|
|
60
|
+
if git_exe is None:
|
|
61
|
+
return False
|
|
62
|
+
top = probe(["git", "-C", root, "rev-parse", "--show-toplevel"], executable=git_exe)
|
|
63
|
+
if top is None:
|
|
64
|
+
return False
|
|
65
|
+
try:
|
|
66
|
+
return Path(top.strip()).resolve() == Path(root).resolve()
|
|
67
|
+
except OSError:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _git_short_commit(root: str, git_exe: str | None) -> str | None:
|
|
72
|
+
"""`git -C <root> rev-parse --short HEAD`, or `None` when `root` is not a
|
|
73
|
+
git checkout of its own (e.g. an extracted SDK release archive -- including
|
|
74
|
+
one vendored inside a customer's own git repository, see
|
|
75
|
+
`_is_own_git_checkout`) or `git_exe` is `None` (git not on PATH)."""
|
|
76
|
+
if not _is_own_git_checkout(root, git_exe):
|
|
77
|
+
return None
|
|
78
|
+
out = probe(["git", "-C", root, "rev-parse", "--short", "HEAD"], executable=git_exe)
|
|
79
|
+
if out is None:
|
|
80
|
+
return None
|
|
81
|
+
commit = out.strip()
|
|
82
|
+
return commit or None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _git_behind_upstream(root: str, git_exe: str | None) -> int | None:
|
|
86
|
+
"""Commit count `HEAD` is behind its upstream tracking ref, without
|
|
87
|
+
fetching. `None` when there is no upstream, `root` is not a git checkout
|
|
88
|
+
of its own (see `_is_own_git_checkout`), or `git_exe` is `None` (git not
|
|
89
|
+
on PATH)."""
|
|
90
|
+
if not _is_own_git_checkout(root, git_exe):
|
|
91
|
+
return None
|
|
92
|
+
out = probe(
|
|
93
|
+
["git", "-C", root, "rev-list", "--count", "HEAD..@{upstream}"], executable=git_exe
|
|
94
|
+
)
|
|
95
|
+
if out is None:
|
|
96
|
+
return None
|
|
97
|
+
try:
|
|
98
|
+
return int(out.strip())
|
|
99
|
+
except ValueError:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def classify_git_core_longpaths(exit_code: int | None, stdout: str) -> bool | None:
|
|
104
|
+
"""The three-way verdict for a `git config --get core.longpaths`
|
|
105
|
+
invocation -- the git-side counterpart to `doctor_cmd._long_paths_enabled`'s
|
|
106
|
+
registry read, split out as its own pure function (mirroring
|
|
107
|
+
`tan_core::host_env::classify_git_core_longpaths`) so the exact mapping
|
|
108
|
+
tan-cli#306 argues hardest about is unit-tested without needing a real
|
|
109
|
+
`git` invocation for every case.
|
|
110
|
+
|
|
111
|
+
* exit 0 -> the stdout value, parsed with git's own boolean grammar.
|
|
112
|
+
* exit 1 -> `False`. `git config --get` documents this code as "the key
|
|
113
|
+
is not set in any scope (system/global/local)" -- git's own default,
|
|
114
|
+
and the state a fresh `HOME` is in (tan-cli#306's exact repro).
|
|
115
|
+
* anything else (`git` not on PATH, a malformed config file, a
|
|
116
|
+
permissions error) -> `None`: uncertain, not guessed.
|
|
117
|
+
"""
|
|
118
|
+
if exit_code == 0:
|
|
119
|
+
value = stdout.strip().lower()
|
|
120
|
+
return value not in ("false", "no", "off", "0")
|
|
121
|
+
if exit_code == 1:
|
|
122
|
+
return False
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _git_core_longpaths(git_exe: str | None) -> bool | None:
|
|
127
|
+
"""Read git's own EFFECTIVE `core.longpaths` (system -> global precedence
|
|
128
|
+
-- NOT the project repo's local config, see below -- resolved by `git config --get` itself rather than tan
|
|
129
|
+
re-implementing that precedence by hand) via a real `git` subprocess.
|
|
130
|
+
|
|
131
|
+
A SEPARATE axis from `doctor_cmd._long_paths_enabled` on purpose
|
|
132
|
+
(tan-cli#306): the registry governs manifested Win32 API calls; it does
|
|
133
|
+
nothing for git, which `west update` uses for every project
|
|
134
|
+
clone/checkout and which refuses a long path unless ITS OWN setting says
|
|
135
|
+
so -- the registry read alone reported `pass` on a fresh `HOME` while
|
|
136
|
+
`west update` died on `hal_nxp`'s `tf-psa-crypto` tree.
|
|
137
|
+
|
|
138
|
+
Not built on `tan.core.probe.probe`: `probe()` collapses "ran and exited
|
|
139
|
+
non-zero" (exit 1, meaning "unset") and "could not run at all" (meaning
|
|
140
|
+
"unknown") to the same `None`, and `classify_git_core_longpaths` needs to
|
|
141
|
+
tell those apart.
|
|
142
|
+
|
|
143
|
+
`git_exe` -- the PATH-RESOLVED absolute `git` from `_resolve_git_executable`
|
|
144
|
+
(tan-cli#797). `None` when git is not on PATH: answers "unknown" rather
|
|
145
|
+
than spawning the bare `"git"` this used to hand straight to
|
|
146
|
+
`subprocess.run`, which on Windows searches the CURRENT DIRECTORY (the
|
|
147
|
+
customer project's, since doctor never `chdir`s) ahead of `%PATH%`.
|
|
148
|
+
"""
|
|
149
|
+
if git_exe is None:
|
|
150
|
+
return None
|
|
151
|
+
try:
|
|
152
|
+
# Run from a fresh EMPTY directory (not inside any repository): from the
|
|
153
|
+
# project's cwd `git config --get` also merges that repo's LOCAL config,
|
|
154
|
+
# but `west update` clones fresh module repos that never inherit it, so
|
|
155
|
+
# only the system/global scopes predict whether those clones succeed.
|
|
156
|
+
with isolated_cwd() as empty:
|
|
157
|
+
out = subprocess.run(
|
|
158
|
+
["git", "config", "--get", "core.longpaths"],
|
|
159
|
+
cwd=empty,
|
|
160
|
+
executable=git_exe,
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
encoding="utf-8",
|
|
164
|
+
errors="replace",
|
|
165
|
+
stdin=subprocess.DEVNULL,
|
|
166
|
+
timeout=PROBE_TIMEOUT_S,
|
|
167
|
+
env=spawn_env(),
|
|
168
|
+
check=False,
|
|
169
|
+
)
|
|
170
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
171
|
+
return None
|
|
172
|
+
return classify_git_core_longpaths(out.returncode, out.stdout)
|