tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/core/image_bundle.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure model + string builders for `tan image` -- port of
|
|
3
|
+
`crates/tan-core/src/image_bundle.rs` plus the `path_guard::is_plain_relative`
|
|
4
|
+
seam it depends on.
|
|
5
|
+
|
|
6
|
+
The IO (tar/gzip/sha256/copy/mkdir) lives in `tan.commands.image_cmd`; every
|
|
7
|
+
*decision* that does not touch the filesystem is here so it stays unit-testable:
|
|
8
|
+
the bundle-manifest shape + key order, the OS-independent forward-slash artefact
|
|
9
|
+
paths, and the ok-slice gate.
|
|
10
|
+
|
|
11
|
+
Two deliberate contract fixes carried over from the Rust:
|
|
12
|
+
- the helper entry carries `chip` (the real schema field) instead of the
|
|
13
|
+
perpetually-`null` `role` the retired Python emitted;
|
|
14
|
+
- artefact paths are built as forward-slash STRINGS, never from a `Path`, so
|
|
15
|
+
the machine contract never leaks a Windows `\\` separator.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ntpath
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
#: Bundle-manifest `schema_version` (matches the system-manifest v1 line).
|
|
26
|
+
BUNDLE_SCHEMA_VERSION = 1
|
|
27
|
+
#: `generated_by` value -- tan-branded (the retired Python emitted `west alp-image`).
|
|
28
|
+
GENERATED_BY = "tan image"
|
|
29
|
+
#: Bundle root directory name, under the build root.
|
|
30
|
+
BUNDLE_DIR = "image-bundle"
|
|
31
|
+
#: Per-slice archive subdirectory name.
|
|
32
|
+
SLICES_DIR = "slices"
|
|
33
|
+
#: Helper-MCU firmware subdirectory name.
|
|
34
|
+
HELPERS_DIR = "helper-mcus"
|
|
35
|
+
#: Output bundle-manifest filename.
|
|
36
|
+
BUNDLE_MANIFEST = "bundle-manifest.json"
|
|
37
|
+
|
|
38
|
+
#: The not-yet-built sentinel a SoM preset uses for a helper firmware that has no
|
|
39
|
+
#: release yet. A legitimate state, so it is a warning, never a failure.
|
|
40
|
+
PENDING_SENTINEL = "TBD"
|
|
41
|
+
|
|
42
|
+
_SEPARATORS = re.compile(r"[/\\]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_plain_relative(path: str) -> bool:
|
|
46
|
+
"""True when `path` is safe to join onto a trusted base: relative, and built
|
|
47
|
+
only from ordinary names. Port of `tan_core::path_guard::is_plain_relative`.
|
|
48
|
+
|
|
49
|
+
`os.path.isabs()` is NOT this check, for the same reason `Path::is_absolute()`
|
|
50
|
+
is not: on Windows it is false for `/x`, `\\x` and `C:x`, none of which
|
|
51
|
+
contains a `..` either, yet every one of them makes `base / p` discard part
|
|
52
|
+
or all of `base`. An empty path and a bare `.` are rejected too: both make
|
|
53
|
+
`base / p` resolve to `base` itself, so a caller meaning "a file under base"
|
|
54
|
+
would act on base.
|
|
55
|
+
|
|
56
|
+
Platform-dependent exactly as `Path::components()` is: `C:foo` is a drive
|
|
57
|
+
prefix on Windows and an ordinary filename on POSIX.
|
|
58
|
+
"""
|
|
59
|
+
if os.name == "nt":
|
|
60
|
+
drive, rest = os.path.splitdrive(path)
|
|
61
|
+
if drive:
|
|
62
|
+
return False # Component::Prefix -- `C:`, `C:/`, `\\\\server\\share`
|
|
63
|
+
if rest[:1] in ("/", "\\"):
|
|
64
|
+
return False # Component::RootDir -- `\\x` and `/x` both
|
|
65
|
+
parts = _SEPARATORS.split(rest)
|
|
66
|
+
else:
|
|
67
|
+
if path.startswith("/"):
|
|
68
|
+
return False # Component::RootDir
|
|
69
|
+
parts = path.split("/")
|
|
70
|
+
parts = [p for p in parts if p != ""] # repeated separators collapse
|
|
71
|
+
if not parts:
|
|
72
|
+
return False # "" and "." and "/" all yield nothing usable
|
|
73
|
+
# `.` is normalized away EXCEPT at the start of the path, where it is a real
|
|
74
|
+
# `Component::CurDir` -- so `a/./b` passes and `./a` does not.
|
|
75
|
+
for index, part in enumerate(parts):
|
|
76
|
+
if part == "." and index > 0:
|
|
77
|
+
continue
|
|
78
|
+
if part in (".", ".."):
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_plain_filename(name: str) -> bool:
|
|
84
|
+
"""True when `name` is safe to use as ONE filename component -- the check
|
|
85
|
+
[`slice_archive_name`] needs, which is strictly narrower than
|
|
86
|
+
[`is_plain_relative`].
|
|
87
|
+
|
|
88
|
+
`is_plain_relative` answers "is this a safe RELATIVE PATH", so it correctly
|
|
89
|
+
accepts nested shapes: `a/b` and `a/./b` are both plain relative paths. A
|
|
90
|
+
value that gets interpolated into a single filename may not contain a
|
|
91
|
+
separator at all -- `a/b` there means a `slices/a/` subdirectory that was
|
|
92
|
+
never created, not a filename.
|
|
93
|
+
|
|
94
|
+
Both separators AND a Windows drive prefix are rejected on BOTH platforms,
|
|
95
|
+
unlike `is_plain_relative`'s deliberately platform-dependent component scan.
|
|
96
|
+
The resulting string is not only a local filename: [`slice_artefact_rel`]
|
|
97
|
+
folds it into the machine contract in `bundle-manifest.json`, which is read
|
|
98
|
+
on whatever OS consumes the bundle, and a `\\` a Linux `tan image` treated
|
|
99
|
+
as an ordinary character is a separator to the Windows consumer that reads
|
|
100
|
+
it back.
|
|
101
|
+
|
|
102
|
+
The drive check is `ntpath.splitdrive`, run unconditionally, NOT
|
|
103
|
+
`os.path.splitdrive` behind an `os.name == "nt"` guard (tan-cli#499 defect 9
|
|
104
|
+
REVIEW). The guarded version applied the cross-OS rule above to the
|
|
105
|
+
separators and then abandoned it one line later: on POSIX,
|
|
106
|
+
`os.path.splitdrive` is `posixpath`'s, which never finds a drive, so a
|
|
107
|
+
`core_id` of `C:foo` was accepted and written to `bundle-manifest.json` as
|
|
108
|
+
`C:foo-zephyr.tar.gz` -- a drive-RELATIVE path to the Windows consumer that
|
|
109
|
+
reads the bundle back, which is precisely the hazard the separator rule is
|
|
110
|
+
stated to prevent. One rule, applied to every component of it.
|
|
111
|
+
"""
|
|
112
|
+
if not name or name in (".", ".."):
|
|
113
|
+
return False
|
|
114
|
+
if _SEPARATORS.search(name):
|
|
115
|
+
return False
|
|
116
|
+
if ntpath.splitdrive(name)[0]:
|
|
117
|
+
return False # `C:` / `C:foo` -- a drive prefix, not a name
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def slice_archive_name(core_id: str, os_name: str) -> str | None:
|
|
122
|
+
"""Archive filename for a slice: `<core_id>-<os>.tar.gz`.
|
|
123
|
+
|
|
124
|
+
`None` when `core_id`/`os` are not safe to fold into a filename that will be
|
|
125
|
+
joined onto the bundle's `slices/` dir. Both come straight off
|
|
126
|
+
system-manifest.yaml -- a deliberately tolerant reader, not a validator --
|
|
127
|
+
and this used to interpolate them unchecked: a `core_id` of
|
|
128
|
+
`../../../../home/u/x` or (Windows) `C:/x` escaped `image-bundle/slices/`
|
|
129
|
+
entirely, so the caller's unlink + create could clobber an arbitrary
|
|
130
|
+
`*.tar.gz` outside the build tree. The guard lives HERE, not at the call
|
|
131
|
+
site, because [`slice_artefact_rel`] needs the identical check for the
|
|
132
|
+
manifest's `artefact` string -- a per-call-site guard is how this class of
|
|
133
|
+
bug keeps recurring.
|
|
134
|
+
|
|
135
|
+
The check is [`is_plain_filename`], NOT `is_plain_relative` (tan-cli#499
|
|
136
|
+
defect 9): the path guard accepts a nested `a/b`, which produced
|
|
137
|
+
`slices/a/b-zephyr.tar.gz` in a directory nothing had created, so
|
|
138
|
+
`image_cmd._tar_gzip_dir` raised and the WHOLE `tan image` run aborted at
|
|
139
|
+
exit 3 with no `bundle-manifest.json` and already-tarred slices orphaned --
|
|
140
|
+
where the equally-invalid `../x` took the intended `image.slice-unsafe-name`
|
|
141
|
+
warning-skip and still produced a bundle. This is a deliberate divergence
|
|
142
|
+
from the frozen oracle, which shares the defect; `is_plain_relative` itself
|
|
143
|
+
is unchanged, so its own nested-path contract (`a/./b` is plain) still holds.
|
|
144
|
+
"""
|
|
145
|
+
if not is_plain_filename(core_id) or not is_plain_filename(os_name):
|
|
146
|
+
return None
|
|
147
|
+
return f"{core_id}-{os_name}.tar.gz"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def slice_artefact_rel(core_id: str, os_name: str) -> str | None:
|
|
151
|
+
"""Forward-slash relative artefact path: `slices/<core_id>-<os>.tar.gz`.
|
|
152
|
+
`None` for the same unsafe shapes [`slice_archive_name`] rejects."""
|
|
153
|
+
name = slice_archive_name(core_id, os_name)
|
|
154
|
+
return None if name is None else f"{SLICES_DIR}/{name}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def helper_artefact_rel(basename: str) -> str:
|
|
158
|
+
"""Forward-slash relative artefact path: `helper-mcus/<basename>`."""
|
|
159
|
+
return f"{HELPERS_DIR}/{basename}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def helper_firmware_candidates(
|
|
163
|
+
raw: str, build_root: str, sdk_root: str | None
|
|
164
|
+
) -> list[tuple[str, str]]:
|
|
165
|
+
"""Labeled `(root, absolute_candidate)` pairs for a helper's declared
|
|
166
|
+
`firmware_path`, in resolution order (alp-sdk#330).
|
|
167
|
+
|
|
168
|
+
alp-sdk's `som-preset-v2.schema.json` defines a relative `firmware_path` as
|
|
169
|
+
repository-relative -- relative to the SDK checkout, not to this project's
|
|
170
|
+
build tree. `build_root` is still tried FIRST, ahead of `sdk_root`,
|
|
171
|
+
mirroring the precedence `flash_plan.resolve_artefact_path` already
|
|
172
|
+
established for the identical two-roots ambiguity (tan-cli#301, #322): a
|
|
173
|
+
helper artefact the CURRENT build actually produced under `build/` is the
|
|
174
|
+
one this run just made, and must win over a same-named file the SDK
|
|
175
|
+
happens to ship. `sdk_root` is the fallback that lets a genuinely
|
|
176
|
+
SDK-shipped firmware (the common case) resolve at all -- build-root-only
|
|
177
|
+
resolution never could, which is the whole bug.
|
|
178
|
+
|
|
179
|
+
An absolute `raw` yields itself alone, unchanged: `sdk_root` never applies
|
|
180
|
+
to it (matches `system_manifest.anchor_under`'s own absolute passthrough).
|
|
181
|
+
|
|
182
|
+
Returned in full even when nothing resolves, so the caller can name every
|
|
183
|
+
root it tried and the absolute path each one produced (issue #330: the old
|
|
184
|
+
message named only the raw relative string, so the reporter had to go find
|
|
185
|
+
the file by hand to prove it existed).
|
|
186
|
+
"""
|
|
187
|
+
if os.path.isabs(raw):
|
|
188
|
+
return [("absolute path", raw)]
|
|
189
|
+
candidates = [("build root", os.path.join(build_root, raw))]
|
|
190
|
+
if sdk_root is not None:
|
|
191
|
+
candidates.append(("sdk root", os.path.join(sdk_root, raw)))
|
|
192
|
+
return candidates
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def slice_should_bundle(status: Any) -> bool:
|
|
196
|
+
"""A slice is bundled iff it built successfully (`status == "ok"`)."""
|
|
197
|
+
return status == "ok"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def slice_entry(
|
|
201
|
+
core_id: str, os_name: str, artefact: str, sha256: str, size: int
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
"""One bundled slice's manifest entry; key order IS the on-disk order."""
|
|
204
|
+
return {
|
|
205
|
+
"core_id": core_id,
|
|
206
|
+
"os": os_name,
|
|
207
|
+
"artefact": artefact,
|
|
208
|
+
"sha256": sha256,
|
|
209
|
+
"size": size,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def helper_entry(
|
|
214
|
+
name: str | None, chip: str | None, artefact: str, sha256: str, size: int
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""One bundled helper firmware's manifest entry. `name`/`chip` are `null`
|
|
217
|
+
only if the manifest omitted them -- and `chip`, not the retired Python's
|
|
218
|
+
always-`null` `role`, is the field."""
|
|
219
|
+
return {
|
|
220
|
+
"name": name,
|
|
221
|
+
"chip": chip,
|
|
222
|
+
"artefact": artefact,
|
|
223
|
+
"sha256": sha256,
|
|
224
|
+
"size": size,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def assemble_bundle_manifest(
|
|
229
|
+
hw_info: dict,
|
|
230
|
+
boot_order: list,
|
|
231
|
+
slices: list[dict[str, Any]],
|
|
232
|
+
helper_mcus: list[dict[str, Any]],
|
|
233
|
+
) -> dict[str, Any]:
|
|
234
|
+
"""The written `bundle-manifest.json`, and the envelope's `data`. Insertion
|
|
235
|
+
order IS the key order: schema_version, generated_by, hw_info, slices,
|
|
236
|
+
helper_mcus, boot_order. Pure: no IO."""
|
|
237
|
+
return {
|
|
238
|
+
"schema_version": BUNDLE_SCHEMA_VERSION,
|
|
239
|
+
"generated_by": GENERATED_BY,
|
|
240
|
+
# Carried verbatim off the YAML so additive keys (`eeprom`, anything a
|
|
241
|
+
# newer SDK adds) survive into the bundle -- see
|
|
242
|
+
# `system_manifest.raw_passthrough`.
|
|
243
|
+
"hw_info": hw_info,
|
|
244
|
+
"slices": slices,
|
|
245
|
+
"helper_mcus": helper_mcus,
|
|
246
|
+
"boot_order": boot_order,
|
|
247
|
+
}
|
tan/core/inert.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The one spelling of "tan accepts this option and does nothing with it"
|
|
3
|
+
(tan-cli#886).
|
|
4
|
+
|
|
5
|
+
`--help` marks an option inert in four structurally different situations, and
|
|
6
|
+
until this module existed the difference lived only in free prose that varied
|
|
7
|
+
per site: "Accepted by other commands; not implemented for `build` yet
|
|
8
|
+
(tan-cli#427)", "Accepted for compatibility (tan-cli#290)", "(unused:
|
|
9
|
+
faultdecode is HW-free)", "(unused; see below)". Four meanings, six spellings,
|
|
10
|
+
two of which said nothing at all once the option was read out of its
|
|
11
|
+
surrounding help block.
|
|
12
|
+
|
|
13
|
+
**Why that is a defect and not a style nit.** `alp-sdk-vscode` records tan's
|
|
14
|
+
whole option surface into a golden (`test/golden/tan-surface/surface.json`)
|
|
15
|
+
and renders the inert ones for the customer. Only ONE of the four kinds will
|
|
16
|
+
ever start acting; the other three are permanent by design. Telling someone
|
|
17
|
+
"not implemented yet, see tan-cli#427" about `doctor --build` invites them to
|
|
18
|
+
wait for a flag that is never going to act. With nothing structured to read,
|
|
19
|
+
the consumer's only options were to pattern-match tan's prose -- fragile, and
|
|
20
|
+
a condition pinned to one spelling is blind to the other five -- or to
|
|
21
|
+
hand-maintain its own copy of a table tan already knows
|
|
22
|
+
(alp-sdk-vscode#577 did the latter).
|
|
23
|
+
|
|
24
|
+
**The marker.** Every inert option's help ends with a single whitespace-free
|
|
25
|
+
token, `(inert:KIND)` or `(inert:KIND:REF)`:
|
|
26
|
+
|
|
27
|
+
--build Accepted for compatibility: ... (inert:compatibility:tan-cli#290)
|
|
28
|
+
--project Project root. Not read: ... (inert:not-applicable)
|
|
29
|
+
|
|
30
|
+
`deferred` has no live instance to show, so this block cannot illustrate it.
|
|
31
|
+
`--plan` filled the slot until tan-cli#427 RETIRED that flag, and
|
|
32
|
+
`--no-auto-bootstrap` took it over until the same issue retired that one too.
|
|
33
|
+
Measured on this tree with the census gate's own walk: 458 options, 120 marked
|
|
34
|
+
-- parity 117, compatibility 1, not-applicable 2, deferred 0. (A hand-rolled
|
|
35
|
+
walk reports 2: typer vendors click, so `isinstance(cmd, click.Group)` is
|
|
36
|
+
False and the recursion never starts. Use `hasattr(cmd, "list_commands")`.)
|
|
37
|
+
|
|
38
|
+
The kind stays in the closed vocabulary because it is the right marker the
|
|
39
|
+
next time tan declares a flag it has not built yet. A RETIRED flag is NOT it
|
|
40
|
+
and carries no marker at all: its value selects a real, distinct refusal
|
|
41
|
+
rather than being accepted and dropped.
|
|
42
|
+
|
|
43
|
+
Three properties of that shape are deliberate, and each rules out a rendering
|
|
44
|
+
defect measured on the real `--help`:
|
|
45
|
+
|
|
46
|
+
1. **Parentheses, not square brackets.** Typer runs this app with
|
|
47
|
+
`rich_markup_mode="rich"`, so help text is rich MARKUP: a `[inert:deferred]`
|
|
48
|
+
marker parses as a style tag and renders as *nothing at all* -- measured, the
|
|
49
|
+
whole token vanishes from `tan build --help`. `\\[` escapes it, but only
|
|
50
|
+
while the markup mode stays `rich`, so the escape is a second thing to keep
|
|
51
|
+
true. Parentheses need neither.
|
|
52
|
+
2. **No whitespace inside the token.** Rich wraps help text at spaces. A
|
|
53
|
+
`(inert: deferred, ref: tan-cli#427)` marker splits across two lines in a
|
|
54
|
+
narrow terminal and a consumer's regex has to re-join them first; this one
|
|
55
|
+
cannot split.
|
|
56
|
+
3. **The ref lives in the marker, not a second time in the prose.** The prose
|
|
57
|
+
the twelve `build` flags shared already ended `(tan-cli#427).`; keeping both
|
|
58
|
+
would be two places to keep true for one fact.
|
|
59
|
+
|
|
60
|
+
**`deferred` is the only kind that requires a ref**, and `inert_help` refuses
|
|
61
|
+
to render one without it. That requirement is the whole point of publishing
|
|
62
|
+
the kind: `deferred` means "an upstream issue tracks its arrival", so a
|
|
63
|
+
`deferred` marker naming no issue is a promise with nothing behind it. The
|
|
64
|
+
permanent kinds MAY carry a ref (`compatibility` and `parity` both name the
|
|
65
|
+
issue that explains the history) and a consumer must not read the presence of
|
|
66
|
+
a ref as "this will arrive" -- the KIND is what says that, never the ref.
|
|
67
|
+
"""
|
|
68
|
+
from __future__ import annotations
|
|
69
|
+
|
|
70
|
+
import re
|
|
71
|
+
|
|
72
|
+
#: An upstream issue tracks this option's arrival: it is accepted now, it does
|
|
73
|
+
#: nothing now, and it is expected to start acting. The ONLY kind that is not
|
|
74
|
+
#: permanent, and the only one that requires a ref.
|
|
75
|
+
DEFERRED = "deferred"
|
|
76
|
+
|
|
77
|
+
#: Kept so an existing caller's command line keeps parsing, after the
|
|
78
|
+
#: behaviour it used to select stopped being conditional. Permanent.
|
|
79
|
+
COMPATIBILITY = "compatibility"
|
|
80
|
+
|
|
81
|
+
#: Accepted only because a sibling surface accepts it -- the v0.4.1 oracle's
|
|
82
|
+
#: clap `GlobalArgs` are `global = true`, so every verb parses all ten
|
|
83
|
+
#: (tan-cli#261). Permanent; see `tan.core.global_flags`.
|
|
84
|
+
PARITY = "parity"
|
|
85
|
+
|
|
86
|
+
#: Structurally meaningless for THIS command -- there is no behaviour for it
|
|
87
|
+
#: to select, no matter what tan implements later. Permanent.
|
|
88
|
+
NOT_APPLICABLE = "not-applicable"
|
|
89
|
+
|
|
90
|
+
#: The closed vocabulary. A consumer switches on exactly these; anything else
|
|
91
|
+
#: is a tan bug, not a value to fall back on.
|
|
92
|
+
INERT_KINDS: tuple[str, ...] = (DEFERRED, COMPATIBILITY, PARITY, NOT_APPLICABLE)
|
|
93
|
+
|
|
94
|
+
#: The kinds that will never start acting. Stated as its own tuple rather than
|
|
95
|
+
#: as "everything except DEFERRED" so adding a fifth kind forces a decision
|
|
96
|
+
#: about which side it lands on instead of inheriting one.
|
|
97
|
+
PERMANENT_KINDS: tuple[str, ...] = (COMPATIBILITY, PARITY, NOT_APPLICABLE)
|
|
98
|
+
|
|
99
|
+
#: How the marker is read back OUT of rendered `--help` text -- published in
|
|
100
|
+
#: `contract/README.md` for the extension, and used by this repo's own gate
|
|
101
|
+
#: (`tests/gates/test_inert_option_markers.py`) so both sides read one regex.
|
|
102
|
+
MARKER_RE = re.compile(r"\(inert:(?P<kind>[a-z-]+)(?::(?P<ref>[^)\s]+))?\)")
|
|
103
|
+
|
|
104
|
+
#: The only ref spelling tan emits. `tan-cli#N` rather than a full URL: it is
|
|
105
|
+
#: what every other cross-reference in this CLI's prose already uses, and it
|
|
106
|
+
#: survives the line-wrapping rule above (a URL would not be shorter).
|
|
107
|
+
REF_RE = re.compile(r"^tan-cli#[1-9][0-9]*$")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def inert_help(prose: str, kind: str, ref: str | None = None) -> str:
|
|
111
|
+
"""Render `prose` with the inert marker appended.
|
|
112
|
+
|
|
113
|
+
Raises `ValueError` -- at IMPORT time, since every call site is a
|
|
114
|
+
`typer.Option(...)` default evaluated when its module loads -- rather than
|
|
115
|
+
emitting a marker a consumer cannot switch on. A malformed marker that
|
|
116
|
+
only fails a test run would still have shipped in someone's branch;
|
|
117
|
+
failing the import fails the CLI itself.
|
|
118
|
+
"""
|
|
119
|
+
if kind not in INERT_KINDS:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"unknown inert kind {kind!r}; expected one of {', '.join(INERT_KINDS)}. "
|
|
122
|
+
"The vocabulary is closed on purpose -- a consumer switches on it "
|
|
123
|
+
"(tan-cli#886). Adding a kind means adding it to INERT_KINDS, to "
|
|
124
|
+
"PERMANENT_KINDS or not, and to contract/README.md."
|
|
125
|
+
)
|
|
126
|
+
if kind == DEFERRED and ref is None:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"a deferred option must name the issue tracking its arrival: "
|
|
129
|
+
"`deferred` is the one kind that promises the flag will start "
|
|
130
|
+
"acting, and a promise with no issue behind it is exactly what "
|
|
131
|
+
"tan-cli#886 asked to make impossible. Use a permanent kind "
|
|
132
|
+
f"({', '.join(PERMANENT_KINDS)}) if nothing tracks it."
|
|
133
|
+
)
|
|
134
|
+
if ref is not None and not REF_RE.match(ref):
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"inert ref {ref!r} is not of the form 'tan-cli#123'. One spelling "
|
|
137
|
+
"keeps the consumer's extraction regex (MARKER_RE) exact."
|
|
138
|
+
)
|
|
139
|
+
stripped = prose.strip()
|
|
140
|
+
if not stripped:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"inert help needs prose of its own: the marker says WHICH KIND of "
|
|
143
|
+
"inert, never what the option would have meant. An option whose "
|
|
144
|
+
"help is only a marker reads as nameless in `--help`."
|
|
145
|
+
)
|
|
146
|
+
marker = f"(inert:{kind})" if ref is None else f"(inert:{kind}:{ref})"
|
|
147
|
+
return f"{stripped} {marker}"
|
tan/core/jlink_binary.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Where tan may take the J-Link Commander binary from (tan-cli#1336).
|
|
3
|
+
|
|
4
|
+
Flow D spawns the J-Link tool with the user's privileges while a probe is
|
|
5
|
+
attached, for the probe listing, the DPIDR preflight, the write and the
|
|
6
|
+
read-back. The workspace `.venv` is part of the PROJECT -- a checkout can ship
|
|
7
|
+
its own `.venv/bin/JLinkExe` -- so tan's "a west-capable venv is trusted"
|
|
8
|
+
rule (right for `west`) must not reach this program. It is resolved ONLY from:
|
|
9
|
+
|
|
10
|
+
1. an explicit `--jlink <path>` (a CLI input);
|
|
11
|
+
2. the `TAN_JLINK` environment variable (an environment input);
|
|
12
|
+
3. `PATH` as the user's own environment has it -- never with the project venv
|
|
13
|
+
prepended, and skipping any PATH entry that is a virtualenv bin directory inside the project
|
|
14
|
+
directory (an activated project `.venv/bin` is still the project's);
|
|
15
|
+
4. a known SEGGER install root: `/opt/SEGGER/*`, `/Applications/SEGGER/*`,
|
|
16
|
+
`%ProgramFiles%` / `%ProgramFiles(x86)%` `\\SEGGER\\*`.
|
|
17
|
+
|
|
18
|
+
Never a manifest field: `build/system-manifest.yaml` is regenerated from the
|
|
19
|
+
checkout and is exactly the surface a hostile project controls.
|
|
20
|
+
|
|
21
|
+
The ONE resolution is used by every J-Link spawn in a run, so the listing that
|
|
22
|
+
verifies the probe, the preflight that verifies the board, the write and the
|
|
23
|
+
read-back all execute the same binary, and the envelope reports it as
|
|
24
|
+
`jlink.binary`.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import glob
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
|
|
34
|
+
from tan.core.tool_lookup import resolve_tool
|
|
35
|
+
|
|
36
|
+
#: The binary identities, in preference order (same as `flash_plan._JLINK_BINARIES`).
|
|
37
|
+
JLINK_NAMES = ("JLinkExe", "JLink")
|
|
38
|
+
|
|
39
|
+
#: The environment override.
|
|
40
|
+
ENV_OVERRIDE = "TAN_JLINK"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class JlinkBinary:
|
|
45
|
+
"""A resolved J-Link binary: absolute `path` and where it came from."""
|
|
46
|
+
|
|
47
|
+
path: str
|
|
48
|
+
source: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _install_roots(env: Mapping[str, str], platform: str) -> list[str]:
|
|
52
|
+
roots: list[str] = []
|
|
53
|
+
if platform.startswith("win"):
|
|
54
|
+
for var in ("ProgramFiles", "ProgramFiles(x86)"):
|
|
55
|
+
base = env.get(var)
|
|
56
|
+
if base:
|
|
57
|
+
roots += sorted(glob.glob(os.path.join(base, "SEGGER", "JLink*")), reverse=True)
|
|
58
|
+
elif platform == "darwin":
|
|
59
|
+
roots += sorted(glob.glob("/Applications/SEGGER/JLink*"), reverse=True)
|
|
60
|
+
else:
|
|
61
|
+
roots += sorted(glob.glob("/opt/SEGGER/JLink*"), reverse=True)
|
|
62
|
+
return roots
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _in_dir(directory: str, platform: str) -> str | None:
|
|
66
|
+
suffixes = ("", ".exe") if platform.startswith("win") else ("",)
|
|
67
|
+
for name in JLINK_NAMES:
|
|
68
|
+
for suffix in suffixes:
|
|
69
|
+
candidate = os.path.join(directory, name + suffix)
|
|
70
|
+
if os.path.isfile(candidate):
|
|
71
|
+
return candidate
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _inside(path: str, root: str) -> bool:
|
|
76
|
+
try:
|
|
77
|
+
real_path, real_root = os.path.realpath(path), os.path.realpath(root)
|
|
78
|
+
return os.path.commonpath([real_path, real_root]) == real_root
|
|
79
|
+
except (ValueError, OSError):
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_project_venv_bin(entry: str, project_dir: str) -> bool:
|
|
84
|
+
"""A PATH entry that is a virtualenv `bin`/`Scripts` directory (its parent holds
|
|
85
|
+
`pyvenv.cfg`) living INSIDE the project directory."""
|
|
86
|
+
return _inside(entry, project_dir) and os.path.isfile(
|
|
87
|
+
os.path.join(os.path.dirname(os.path.realpath(entry)), "pyvenv.cfg")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _path_without_project(env: Mapping[str, str], project_dir: str | None) -> dict[str, str]:
|
|
92
|
+
"""`env` with every PATH entry that is a project virtualenv's bin directory removed
|
|
93
|
+
-- an ACTIVATED project `.venv` on the user's PATH is still the project's, and the
|
|
94
|
+
whole point of tan-cli#1336 is that the project's venv never supplies this binary."""
|
|
95
|
+
path = env.get("PATH")
|
|
96
|
+
if not project_dir or not path:
|
|
97
|
+
return dict(env)
|
|
98
|
+
kept = [d for d in path.split(os.pathsep) if d and not _is_project_venv_bin(d, project_dir)]
|
|
99
|
+
return {**env, "PATH": os.pathsep.join(kept)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_jlink(
|
|
103
|
+
cli_path: str | None,
|
|
104
|
+
env: Mapping[str, str] | None = None,
|
|
105
|
+
*,
|
|
106
|
+
platform: str | None = None,
|
|
107
|
+
project_dir: str | None = None,
|
|
108
|
+
) -> JlinkBinary | None:
|
|
109
|
+
"""The J-Link binary tan may spawn, or `None` when none is found. An
|
|
110
|
+
explicit `cli_path` / `TAN_JLINK` that does not name an existing file
|
|
111
|
+
resolves to `None` rather than falling through: the operator named a
|
|
112
|
+
binary, and quietly using a different one is the thing this refuses."""
|
|
113
|
+
env = os.environ if env is None else env
|
|
114
|
+
platform = sys.platform if platform is None else platform
|
|
115
|
+
for value, source in ((cli_path, "the --jlink flag"), (env.get(ENV_OVERRIDE), f"{ENV_OVERRIDE}")):
|
|
116
|
+
if value:
|
|
117
|
+
path = os.path.abspath(value)
|
|
118
|
+
return JlinkBinary(path, source) if os.path.isfile(path) else None
|
|
119
|
+
path_env = _path_without_project(env, project_dir)
|
|
120
|
+
for name in JLINK_NAMES:
|
|
121
|
+
found = resolve_tool(name, path_env).resolved
|
|
122
|
+
if found:
|
|
123
|
+
return JlinkBinary(os.path.abspath(found), "PATH")
|
|
124
|
+
for root in _install_roots(env, platform):
|
|
125
|
+
found = _in_dir(root, platform)
|
|
126
|
+
if found:
|
|
127
|
+
return JlinkBinary(found, f"SEGGER install {root}")
|
|
128
|
+
return None
|