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/probe.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The two subprocess primitives every host/tool probe in `tan doctor` (and
|
|
3
|
+
its git-provenance/build-side counterparts, `tan.core.doctor_git`) is built
|
|
4
|
+
on. Extracted out of `tan.commands.doctor_cmd` (tan-cli#797 module-size
|
|
5
|
+
budget) so `tan.core.doctor_git` can depend on them without an import cycle
|
|
6
|
+
back into `doctor_cmd` -- `tan/core` is the layer BELOW `tan/commands`, never
|
|
7
|
+
the reverse.
|
|
8
|
+
|
|
9
|
+
Neither function can raise: every subprocess failure mode a fresh, possibly
|
|
10
|
+
broken host can produce (missing binary, permission error, a probe that hangs
|
|
11
|
+
forever, garbage output) is swallowed and turned into "no answer", never a
|
|
12
|
+
traceback that would misrepresent a fixable environment problem as a tan bug.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import subprocess
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
from tan.core.subprocess_env import spawn_env
|
|
23
|
+
|
|
24
|
+
#: Every probe in this module gets the same ceiling: long enough for a slow
|
|
25
|
+
#: but genuinely-answering tool, short enough that a hung one does not stall
|
|
26
|
+
#: /a build's provenance read indefinitely.
|
|
27
|
+
PROBE_TIMEOUT_S = 15
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_PYTHON_NAME = re.compile(r"^(py|python[\d.]*[tw]?)(\.exe)?$", re.IGNORECASE)
|
|
31
|
+
#: `-c`, or a bundle ending in it (`-Ic`, `-Sc`, `-Bsc`).
|
|
32
|
+
_DASH_C = re.compile(r"^-[A-Za-z]*c$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def isolated_cwd() -> "tempfile.TemporaryDirectory[str]":
|
|
36
|
+
"""A fresh EMPTY directory to use as a child's cwd. `python -m <mod>` and
|
|
37
|
+
`python -c` put the cwd on `sys.path`, so a project directory can shadow
|
|
38
|
+
`pip`/`venv`/a stdlib module; every spawn that must not be hijackable runs
|
|
39
|
+
from here, with absolute path arguments (tan-cli#1317, #1331). The one
|
|
40
|
+
helper both the probes and `bootstrap_cmd.Runner.run(isolated=True)` use."""
|
|
41
|
+
return tempfile.TemporaryDirectory(prefix="tan-spawn-", ignore_cleanup_errors=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_python_dash_c(argv: list[str]) -> bool:
|
|
45
|
+
"""`<python> ... -c <code>`. With `-c`, `sys.path[0]` is the CWD, so a
|
|
46
|
+
`west.py`/`json.py` planted in the user's project or SDK dir would run
|
|
47
|
+
during a probe. Such probes run from a fresh EMPTY directory instead."""
|
|
48
|
+
return (
|
|
49
|
+
bool(argv)
|
|
50
|
+
and any(_DASH_C.match(a) for a in argv[1:])
|
|
51
|
+
and bool(_PYTHON_NAME.match(os.path.basename(argv[0])))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _absolute_for_new_cwd(argv: list[str], executable: str | None):
|
|
56
|
+
"""The probe runs from a different cwd, so a RELATIVE program path (one with
|
|
57
|
+
a separator) must be made absolute first or it would stop resolving."""
|
|
58
|
+
def fix(p: str) -> str:
|
|
59
|
+
return os.path.abspath(p) if (os.sep in p or (os.altsep and os.altsep in p)) else p
|
|
60
|
+
|
|
61
|
+
return [fix(argv[0]), *argv[1:]], (fix(executable) if executable else executable)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def probe_status(
|
|
65
|
+
argv: list[str], timeout: int = PROBE_TIMEOUT_S, executable: str | None = None
|
|
66
|
+
) -> tuple[bool, str | None]:
|
|
67
|
+
"""Run `argv` and return `(ran, stdout)`.
|
|
68
|
+
|
|
69
|
+
`executable`, when given, is `lpApplicationName`/POSIX `execve`'s own
|
|
70
|
+
program path -- the PATH-RESOLVED absolute location `tool_lookup.resolve_tool`
|
|
71
|
+
found, threaded through by a caller that needs `argv[0]` to stay the bare
|
|
72
|
+
identity (so a tool's own diagnostics keep printing the short name) while
|
|
73
|
+
the OS loads the hardened path instead of re-searching for the bare one
|
|
74
|
+
itself (tan-cli#797, the same `executable=` shape tan-cli#567 established
|
|
75
|
+
for the flash/size spawns).
|
|
76
|
+
|
|
77
|
+
`ran` is `True` exactly when the process was actually spawned AND exited
|
|
78
|
+
zero -- the one predicate that answers "could a build slice run this
|
|
79
|
+
binary at all". `stdout` is the captured text only when `ran` is `True`,
|
|
80
|
+
else always `None`. `probe()` below collapses both into a single `None`,
|
|
81
|
+
which is right for most callers (an unparseable version banner and a
|
|
82
|
+
binary that could not be spawned are both just "no answer") but wrong for
|
|
83
|
+
a caller that needs to tell "spawned fine, printed garbage" apart from
|
|
84
|
+
"never ran at all" -- `doctor_cmd.west_resolved_check` is exactly that
|
|
85
|
+
caller (tan-cli#488 defect 1): a `west` launcher whose file survives but
|
|
86
|
+
whose interpreter does not (a relocated workspace, a deleted
|
|
87
|
+
`.venv/bin/python`) is `is_file()`-true and therefore reaches this probe,
|
|
88
|
+
but every real invocation of it fails with `OSError`/a non-zero exit --
|
|
89
|
+
`probe()` alone cannot distinguish that from a `west` that ran and printed
|
|
90
|
+
something this command's regex could not parse, and reporting the former
|
|
91
|
+
as `pass` is the false green this function exists to close.
|
|
92
|
+
|
|
93
|
+
See `probe()` for why each failure mode below is swallowed rather than
|
|
94
|
+
raised.
|
|
95
|
+
|
|
96
|
+
`env=spawn_env()` (tan-cli#992): every probe in this module IS the
|
|
97
|
+
"does tan think this tool works" verdict, so a probe that leaked tan's
|
|
98
|
+
own bundled `LD_LIBRARY_PATH` into the child would report a perfectly
|
|
99
|
+
fine host tool as broken the moment its own bundled copy of some shared
|
|
100
|
+
library disagreed with the host's -- exactly the failure mode
|
|
101
|
+
`spawn_env` exists to close.
|
|
102
|
+
"""
|
|
103
|
+
isolate = is_python_dash_c(argv)
|
|
104
|
+
if isolate:
|
|
105
|
+
argv, executable = _absolute_for_new_cwd(argv, executable)
|
|
106
|
+
scratch = (
|
|
107
|
+
isolated_cwd()
|
|
108
|
+
if isolate
|
|
109
|
+
else contextlib.nullcontext(None)
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
with scratch as cwd:
|
|
113
|
+
out = subprocess.run(
|
|
114
|
+
argv,
|
|
115
|
+
executable=executable,
|
|
116
|
+
cwd=cwd,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
encoding="utf-8",
|
|
120
|
+
errors="replace",
|
|
121
|
+
stdin=subprocess.DEVNULL,
|
|
122
|
+
timeout=timeout,
|
|
123
|
+
env=spawn_env(),
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
127
|
+
# SubprocessError covers TimeoutExpired (the child is already killed by
|
|
128
|
+
# `run`); ValueError catches an empty/garbage argv rather than letting
|
|
129
|
+
# it escape as a traceback.
|
|
130
|
+
return False, None
|
|
131
|
+
return (out.returncode == 0), (out.stdout if out.returncode == 0 else None)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def probe(
|
|
135
|
+
argv: list[str], timeout: int = PROBE_TIMEOUT_S, executable: str | None = None
|
|
136
|
+
) -> str | None:
|
|
137
|
+
"""Run `argv` and return its stdout, or `None` for every way that can fail.
|
|
138
|
+
|
|
139
|
+
`None` means "no answer", never "the answer is bad" -- callers must not read
|
|
140
|
+
it as a verdict. The failure modes this swallows are all real on a fresh
|
|
141
|
+
host: the binary is absent (`FileNotFoundError`), it is a directory or not
|
|
142
|
+
executable (`OSError`/`PermissionError`), it waits forever on a probe that is
|
|
143
|
+
not plugged in (`TimeoutExpired`), or it exits non-zero.
|
|
144
|
+
|
|
145
|
+
`stdin` is closed, not inherited: a tool that decides to prompt then reads
|
|
146
|
+
EOF and dies instead of blocking until the timeout. `errors="replace"` is
|
|
147
|
+
the same reason `tests/conformance` uses it -- a tool answering in the
|
|
148
|
+
platform code page must not turn into a `UnicodeDecodeError` crash that
|
|
149
|
+
masquerades as a host problem.
|
|
150
|
+
|
|
151
|
+
A thin wrapper over `probe_status` (above) for every caller that only ever
|
|
152
|
+
wanted "the answer, or nothing" and has no use for the ran/did-not-run
|
|
153
|
+
split.
|
|
154
|
+
"""
|
|
155
|
+
return probe_status(argv, timeout, executable=executable)[1]
|
tan/core/probe_plan.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""tan-cli#1406: the pure half of `tan probe` -- argument parsing, the region
|
|
3
|
+
safety rule and the read-only J-Link Commander scripts.
|
|
4
|
+
|
|
5
|
+
`tan probe` exists so the bench never needs raw `JLinkExe` for two READ-ONLY
|
|
6
|
+
questions: "which SW-DP / core is attached?" and "what do these words read?".
|
|
7
|
+
Every script built here is `connect` plus `mem32` reads and `exit`; nothing in
|
|
8
|
+
this module can emit a write, erase, `loadbin`, `setpc`, `go`, reset or
|
|
9
|
+
`w1`/`w2`/`w4` command, and `FORBIDDEN_VERBS` is what the tests hold the
|
|
10
|
+
generated scripts to.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from typing import Sequence
|
|
16
|
+
|
|
17
|
+
from tan.core.ram_run import core_check_script, preamble
|
|
18
|
+
|
|
19
|
+
CODE_BAD_ARGUMENT = "probe.bad-argument"
|
|
20
|
+
CODE_READ_TOO_LARGE = "probe.read-too-large"
|
|
21
|
+
CODE_READ_UNSAFE_REGION = "probe.read-unsafe-region"
|
|
22
|
+
CODE_READ_FAILED = "probe.read-failed"
|
|
23
|
+
CODE_FAILED = "probe.failed"
|
|
24
|
+
|
|
25
|
+
#: Most words one `probe read` returns.
|
|
26
|
+
MAX_WORDS = 256
|
|
27
|
+
DEFAULT_WORDS = 4
|
|
28
|
+
#: The generic Cortex-M55 attach profile (the --ram attach check's own).
|
|
29
|
+
DEFAULT_DEVICE = "Cortex-M55"
|
|
30
|
+
CORES = ("m55_he", "m55_hp")
|
|
31
|
+
|
|
32
|
+
#: The window a session attached to the M55-HE must never touch: reading the HP
|
|
33
|
+
#: ITCM global alias from an HE attach leaves the core unhaltable until a PIN reset
|
|
34
|
+
#: (bench round 8, tan-cli#1354).
|
|
35
|
+
UNSAFE_WINDOW = (0x50000000, 0x5FFFFFFF)
|
|
36
|
+
|
|
37
|
+
#: Commander verbs a `probe` script must never contain (the tests scan for them).
|
|
38
|
+
FORBIDDEN_VERBS = (
|
|
39
|
+
"w1", "w2", "w4", "w8", "write", "erase", "loadbin", "loadfile", "setpc", "go", "g",
|
|
40
|
+
"reset", "r", "rx", "halt", "h", "unlock", "wreg", "savebin", "setbp", "step", "s",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_HEX = re.compile(r"^0[xX][0-9A-Fa-f]{1,8}$")
|
|
44
|
+
_DEC = re.compile(r"^[0-9]{1,10}$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProbeArgError(ValueError):
|
|
48
|
+
def __init__(self, code: str, message: str) -> None:
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
self.code = code
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_number(text: str, what: str) -> int:
|
|
54
|
+
"""Plain hex (`0x...`) or decimal only -- no signs, underscores, expressions."""
|
|
55
|
+
text = text.strip() if isinstance(text, str) else ""
|
|
56
|
+
if _HEX.match(text):
|
|
57
|
+
return int(text, 16)
|
|
58
|
+
if _DEC.match(text):
|
|
59
|
+
return int(text, 10)
|
|
60
|
+
raise ProbeArgError(
|
|
61
|
+
CODE_BAD_ARGUMENT, f"{what} {text!r} is not a plain hex (0x...) or decimal number"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_read(address: str, count: str | int | None) -> tuple[int, int]:
|
|
66
|
+
"""`(address, words)` for a `probe read`, or `ProbeArgError`."""
|
|
67
|
+
addr = parse_number(address, "the address")
|
|
68
|
+
if addr % 4:
|
|
69
|
+
raise ProbeArgError(CODE_BAD_ARGUMENT, f"address 0x{addr:X} is not 4-byte aligned")
|
|
70
|
+
if count is None:
|
|
71
|
+
words = DEFAULT_WORDS
|
|
72
|
+
else:
|
|
73
|
+
words = parse_number(str(count), "the word count")
|
|
74
|
+
if words < 1:
|
|
75
|
+
raise ProbeArgError(CODE_BAD_ARGUMENT, "the word count must be at least 1")
|
|
76
|
+
if words > MAX_WORDS:
|
|
77
|
+
raise ProbeArgError(
|
|
78
|
+
CODE_READ_TOO_LARGE,
|
|
79
|
+
f"{words} words requested; `tan probe read` returns at most {MAX_WORDS} "
|
|
80
|
+
"(1 KiB) per call",
|
|
81
|
+
)
|
|
82
|
+
if addr + 4 * words > 0x100000000:
|
|
83
|
+
raise ProbeArgError(CODE_BAD_ARGUMENT, "the read would run past the 32-bit address space")
|
|
84
|
+
return addr, words
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def normalise_core(core: str | None) -> str | None:
|
|
88
|
+
if core is None:
|
|
89
|
+
return None
|
|
90
|
+
value = core.strip().lower().replace("-", "_")
|
|
91
|
+
if value not in CORES:
|
|
92
|
+
raise ProbeArgError(CODE_BAD_ARGUMENT, f"--core {core!r} is not one of {', '.join(CORES)}")
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def touches_unsafe_window(address: int, words: int) -> bool:
|
|
97
|
+
lo, hi = UNSAFE_WINDOW
|
|
98
|
+
return address <= hi and address + 4 * words - 1 >= lo
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def window_refusal(address: int, words: int) -> str | None:
|
|
102
|
+
"""The refusal for ANY overlap with 0x50000000-0x5FFFFFFF, on every core -- there is
|
|
103
|
+
no HP exception: a bench-proven-safe HP read is not something an attach check can
|
|
104
|
+
establish (a multiple-AP banner or `conflict-hp` still looks HP-ish), so the window
|
|
105
|
+
is simply not readable through `tan probe`."""
|
|
106
|
+
if not touches_unsafe_window(address, words):
|
|
107
|
+
return None
|
|
108
|
+
return (
|
|
109
|
+
f"refusing to read 0x{address:08X}..0x{address + 4 * words - 1:08X}: `tan probe read` "
|
|
110
|
+
"refuses 0x50000000-0x5FFFFFFF on every core. From the HE that window is the HP ITCM "
|
|
111
|
+
"alias (reading it leaves the core unhaltable until a PIN reset), and tan does not "
|
|
112
|
+
"read it from any attach."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _slice_ambiguity(distinct: dict[str, set[str]]) -> str | None:
|
|
117
|
+
for key, values in distinct.items():
|
|
118
|
+
if len(values) > 1:
|
|
119
|
+
return (
|
|
120
|
+
f"the manifest's slices pin different {key} values ({', '.join(sorted(values))}); "
|
|
121
|
+
"pass --core to pick one, as `tan flash` requires"
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def select_slice(
|
|
127
|
+
slices: Sequence[tuple[str, dict]], core: str | None
|
|
128
|
+
) -> tuple[dict, str | None, str | None, str | None]:
|
|
129
|
+
"""`(flash_args, selected core id, ambiguity message, note)` from `(core_id, flash_args)` pairs.
|
|
130
|
+
|
|
131
|
+
The args are taken whether or not `expect_dpidr` is armed (`jlink_serial`,
|
|
132
|
+
`jlink_speed`, `jlink_device` apply either way). With `--core` the matching slice
|
|
133
|
+
wins (no match: `{}` plus a `note`); without it one slice is used as is, and several
|
|
134
|
+
are acceptable only when they do not disagree on `jlink_serial` / `expect_dpidr`
|
|
135
|
+
(then the selected id is unknown). A `jlink_serial` pinned by exactly one of them is
|
|
136
|
+
carried onto the chosen args."""
|
|
137
|
+
pool = [(c, a) for c, a in slices if isinstance(a, dict)]
|
|
138
|
+
if core is not None:
|
|
139
|
+
matched = [(c, a) for c, a in pool if c.lower() == core]
|
|
140
|
+
if pool and not matched:
|
|
141
|
+
return {}, None, None, f"the manifest has no slice for --core {core}; its settings were not used"
|
|
142
|
+
pool = matched
|
|
143
|
+
if not pool:
|
|
144
|
+
return {}, None, None, None
|
|
145
|
+
if len(pool) == 1:
|
|
146
|
+
return dict(pool[0][1]), pool[0][0].lower(), None, None
|
|
147
|
+
distinct = {
|
|
148
|
+
key: {str(a[key]) for _c, a in pool if a.get(key) is not None}
|
|
149
|
+
for key in ("jlink_serial", "expect_dpidr")
|
|
150
|
+
}
|
|
151
|
+
problem = _slice_ambiguity(distinct)
|
|
152
|
+
if problem:
|
|
153
|
+
return {}, None, problem, None
|
|
154
|
+
armed = [a for _c, a in pool if a.get("expect_dpidr")]
|
|
155
|
+
chosen = dict((armed or [a for _c, a in pool])[0])
|
|
156
|
+
if chosen.get("jlink_serial") is None and distinct["jlink_serial"]:
|
|
157
|
+
chosen["jlink_serial"] = next(iter(distinct["jlink_serial"]))
|
|
158
|
+
return chosen, None, None, None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_he_target(core: str | None, selected_id: str | None) -> bool:
|
|
162
|
+
"""The target is the M55-HE: `--core m55_he`, or no `--core` and the manifest's
|
|
163
|
+
selected slice is the HE."""
|
|
164
|
+
return core == "m55_he" or (core is None and selected_id == "m55_he")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def itcm_check_allowed(target_he: bool, attach_verdict: str) -> bool:
|
|
168
|
+
"""The `mem32 0x0` / `mem32 0x58000000` session is bench-proven only from an HE attach:
|
|
169
|
+
send it only when the target is the HE AND session 1's banner placed the attach on
|
|
170
|
+
the HE access port (exactly `he`), never on the claim alone."""
|
|
171
|
+
return target_he and attach_verdict == "he"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def itcm_skip_reason(core: str | None, selected_id: str | None, verdict: str) -> str:
|
|
175
|
+
"""Why the ITCM corroboration was not run, and the fix, for the info issue."""
|
|
176
|
+
if core is None and selected_id is None:
|
|
177
|
+
return ("the target core is ambiguous (several manifest slices, or none, and no --core); "
|
|
178
|
+
"pass --core m55_he to corroborate an HE attach")
|
|
179
|
+
if not is_he_target(core, selected_id):
|
|
180
|
+
return (f"the target is {core or selected_id}, and the ITCM read is bench-proven only "
|
|
181
|
+
"from an HE attach; pass --core m55_he to corroborate an HE attach")
|
|
182
|
+
return (f"the connect banner placed the attach on '{verdict}', not the M55-HE access port, "
|
|
183
|
+
"so the HE-window read was not sent")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def claimed_core(core: str | None, selected_id: str | None) -> str | None:
|
|
187
|
+
"""`--core`, else the manifest's selected slice when that is one of the known cores."""
|
|
188
|
+
return core or (selected_id if selected_id in CORES else None)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def core_contradiction(core: str | None, verdict: str) -> str | None:
|
|
192
|
+
"""A message when the attach `verdict` (`ram_run` vocabulary, or the bare AP verdict)
|
|
193
|
+
contradicts the claimed `--core`; `None` when it agrees or says nothing."""
|
|
194
|
+
if core == "m55_he" and verdict == "conflict":
|
|
195
|
+
return f"--core m55_he, but the attach verdict is contradictory ({verdict})"
|
|
196
|
+
if core == "m55_he" and verdict in ("hp", "conflict-hp"):
|
|
197
|
+
return f"--core m55_he, but the probe attached to the M55-HP ({verdict})"
|
|
198
|
+
if core == "m55_hp" and verdict in ("he", "conflict"):
|
|
199
|
+
return f"--core m55_hp, but the probe attached to the M55-HE ({verdict})"
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def dpidr_state(expected: str | None, actual: str | None) -> str | None:
|
|
204
|
+
"""`None` when no `expect_dpidr` is armed or it matches; `"unread"` / `"mismatch"` else."""
|
|
205
|
+
if not expected:
|
|
206
|
+
return None
|
|
207
|
+
if actual is None:
|
|
208
|
+
return "unread"
|
|
209
|
+
try:
|
|
210
|
+
return None if int(actual, 16) == int(expected, 16) else "mismatch"
|
|
211
|
+
except ValueError:
|
|
212
|
+
return "mismatch"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def identity_script(pre: Sequence[str]) -> str:
|
|
216
|
+
"""The DP IDR read: exactly the Flow D / --ram DPIDR preflight --
|
|
217
|
+
`[SelectEmuBySN], si SWD, speed, device, connect, exit`."""
|
|
218
|
+
return "\n".join([*pre, "exit"]) + "\n"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def core_script(pre: Sequence[str]) -> str:
|
|
222
|
+
"""The --ram attach check, verbatim (`connect` + two read-only `mem32`)."""
|
|
223
|
+
return core_check_script(pre)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def read_script(pre: Sequence[str], address: int, words: int) -> str:
|
|
227
|
+
"""`connect`, ONE `mem32`, `exit`. Address and count are ints rendered with `0x%X`."""
|
|
228
|
+
return "\n".join([*pre, f"mem32 0x{address:X}, 0x{words:X}", "exit"]) + "\n"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def make_preamble(serial: str | None, speed: int, device: str = DEFAULT_DEVICE) -> list[str]:
|
|
232
|
+
return preamble(serial, speed, device)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def script_verbs(script: str) -> list[str]:
|
|
236
|
+
"""The first token of every non-empty script line, lower-cased."""
|
|
237
|
+
return [ln.split()[0].lower() for ln in script.splitlines() if ln.split()]
|
tan/core/proxy.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Which proxy environment variable applies to an `https://` request, whether
|
|
3
|
+
`NO_PROXY` exempts the target from it, and whether this interpreter can actually
|
|
4
|
+
route through the proxy that wins.
|
|
5
|
+
|
|
6
|
+
Port of `crates/tan-core/src/proxy.rs`, which is the frozen oracle's rule and
|
|
7
|
+
therefore the contract. Pure on purpose, for the same reason the Rust is:
|
|
8
|
+
reading the environment is IO and belongs at the call site (`sdk_cmd`), while
|
|
9
|
+
the *precedence rule* is a decision, and a decision with a scheme trap in it
|
|
10
|
+
deserves tests that never mutate the process environment.
|
|
11
|
+
|
|
12
|
+
**Why this module exists at all.** The port's one network call site used a bare
|
|
13
|
+
`urllib.request.urlopen`, which builds its proxy handler from
|
|
14
|
+
`getproxies_environment()`. That maps `ALL_PROXY`/`all_proxy` to the key `all`,
|
|
15
|
+
and `ProxyHandler` then installs an `all_open` method that `OpenerDirector._open`
|
|
16
|
+
-- which dispatches on `req.type`, i.e. `"https"` -- never calls. Measured
|
|
17
|
+
against the live GitHub Releases API: with `HTTPS_PROXY` pointing at a closed
|
|
18
|
+
port `tan sdk list --online` failed as it should (rc 1, `sdk.fetch-failed`,
|
|
19
|
+
`[Errno 111] Connection refused`); with `ALL_PROXY` pointing at that SAME closed
|
|
20
|
+
port and nothing else set, tan connected DIRECTLY and returned rc 0 with 13
|
|
21
|
+
releases. So a host whose only egress is `ALL_PROXY` -- the spelling curl, git
|
|
22
|
+
and the shipped Rust `tan` all honour -- was hard-broken, and a host with direct
|
|
23
|
+
egress silently circumvented the proxy it was told to use (tan-cli#497 defect 8).
|
|
24
|
+
|
|
25
|
+
**The precedence, MEASURED on the oracle rather than read off `proxy.rs`.** Two
|
|
26
|
+
listening sockets, `ALL_PROXY=http://127.0.0.1:45001` and
|
|
27
|
+
`HTTPS_PROXY=http://127.0.0.1:45002`, one `target/debug/tan sdk list`: the
|
|
28
|
+
connection landed on **45001**. `ALL_PROXY` outranks `HTTPS_PROXY`, and the
|
|
29
|
+
uppercase spelling outranks its lowercase alias. `HTTP_PROXY`/`http_proxy` are
|
|
30
|
+
deliberately ABSENT: curl, git and urllib all treat those as http-only, and
|
|
31
|
+
pushing an `https://` request through a plain-HTTP proxy that may refuse
|
|
32
|
+
`CONNECT` would break a corporate host that worked fine going direct.
|
|
33
|
+
|
|
34
|
+
**`NO_PROXY` is the other half of the same rule**, also measured: with
|
|
35
|
+
`ALL_PROXY` at a listening socket and `NO_PROXY=api.github.com`, the oracle went
|
|
36
|
+
direct and returned the release list (rc 0), and nothing ever reached the
|
|
37
|
+
socket. Honouring the proxy without the bypass list would proxy a host that was
|
|
38
|
+
deliberately exempted.
|
|
39
|
+
|
|
40
|
+
**SOCKS is where this port cannot follow the oracle.** `crates/tan-cli/src/http.rs`
|
|
41
|
+
compiles ureq's `socks-proxy` feature, and the oracle really does dial a
|
|
42
|
+
`socks5://` `ALL_PROXY` (measured: the listener on port 45012 was hit).
|
|
43
|
+
`urllib` has no SOCKS transport at all -- `ProxyHandler` would hand the request
|
|
44
|
+
back to `OpenerDirector` under the type `socks5` and produce `unknown url type`.
|
|
45
|
+
[`unsupported_proxy_scheme`] names that case so the call site can REFUSE with a
|
|
46
|
+
message that says what happened, rather than falling back to a direct
|
|
47
|
+
connection: silently bypassing a proxy the host mandated is the failure mode
|
|
48
|
+
this whole module exists to remove, and doing it as the *fix* would be worse
|
|
49
|
+
than the bug.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import os
|
|
55
|
+
from collections.abc import Callable
|
|
56
|
+
|
|
57
|
+
#: Proxy env vars that apply to an `https://` URL, in the oracle's own
|
|
58
|
+
#: precedence order (`ALL_PROXY` first, each lowercase alias immediately after
|
|
59
|
+
#: its uppercase form). `HTTP_PROXY`/`http_proxy` are deliberately absent -- see
|
|
60
|
+
#: the module docstring.
|
|
61
|
+
HTTPS_PROXY_ENV_VARS = ("ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy")
|
|
62
|
+
|
|
63
|
+
#: Bypass-list env vars, uppercase first for the same reason. curl and git read
|
|
64
|
+
#: exactly these two.
|
|
65
|
+
NO_PROXY_ENV_VARS = ("NO_PROXY", "no_proxy")
|
|
66
|
+
|
|
67
|
+
#: URL schemes `urllib` can actually carry a proxied `https://` request over.
|
|
68
|
+
#: Anything else (`socks5`, `socks5h`, `socks4`, `socks4a`) needs a transport
|
|
69
|
+
#: this interpreter does not ship -- see [`unsupported_proxy_scheme`].
|
|
70
|
+
SUPPORTED_PROXY_SCHEMES = ("http", "https")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _first_set(keys: tuple[str, ...], lookup: Callable[[str], str | None]) -> str | None:
|
|
74
|
+
"""The first key with a non-blank value, or `None`.
|
|
75
|
+
|
|
76
|
+
An empty value counts as UNSET: exporting `HTTPS_PROXY=` is the conventional
|
|
77
|
+
way to disable an inherited proxy, and treating it as a proxy URL would make
|
|
78
|
+
every request fail.
|
|
79
|
+
"""
|
|
80
|
+
for key in keys:
|
|
81
|
+
value = lookup(key)
|
|
82
|
+
if value is not None and value.strip():
|
|
83
|
+
return value
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def host_of(url: str) -> str:
|
|
88
|
+
"""The host of an absolute URL -- no scheme, userinfo, port, path or
|
|
89
|
+
trailing root dot -- lowercased for comparison.
|
|
90
|
+
|
|
91
|
+
Deliberately not a URL parser: the one URL this path ever requests is one we
|
|
92
|
+
wrote ourselves (`sdk_cmd.GITHUB_RELEASES_URL`), so a parser would be more
|
|
93
|
+
moving parts than the two splits it replaces.
|
|
94
|
+
"""
|
|
95
|
+
after_scheme = url.split("://", 1)[-1] if "://" in url else url
|
|
96
|
+
authority = after_scheme
|
|
97
|
+
for delimiter in ("/", "?", "#"):
|
|
98
|
+
authority = authority.split(delimiter, 1)[0]
|
|
99
|
+
host = authority.rsplit("@", 1)[-1]
|
|
100
|
+
return host.split(":", 1)[0].rstrip(".").lower()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def no_proxy_covers(list_: str, host: str) -> bool:
|
|
104
|
+
"""Does a `NO_PROXY` list cover `host` (already lowercased by [`host_of`])?
|
|
105
|
+
|
|
106
|
+
The rules are curl's, git's and Python's, because those are what the user's
|
|
107
|
+
existing `NO_PROXY` was written against:
|
|
108
|
+
|
|
109
|
+
* `*` as an entry bypasses the proxy for everything.
|
|
110
|
+
* Comma-separated; surrounding whitespace and empty entries are ignored, so
|
|
111
|
+
`"a, ,b"` is two entries and a trailing comma is harmless.
|
|
112
|
+
* Case-insensitive on both sides -- DNS is.
|
|
113
|
+
* An entry matches the host exactly, or as a suffix **on a label boundary**:
|
|
114
|
+
`github.com` and `.github.com` both cover `api.github.com`, while
|
|
115
|
+
`hub.com` covers neither. A bare `endswith` is the classic bug here -- it
|
|
116
|
+
would let `hub.com` silently disable the proxy for `github.com`.
|
|
117
|
+
* A `:port` on an entry is ignored rather than honoured: every request on
|
|
118
|
+
this path is `https://` on 443, so a port-qualified entry can only mean
|
|
119
|
+
443 anyway.
|
|
120
|
+
"""
|
|
121
|
+
for raw in list_.split(","):
|
|
122
|
+
entry = raw.strip()
|
|
123
|
+
if entry == "*":
|
|
124
|
+
return True
|
|
125
|
+
entry = entry.split(":", 1)[0].strip(".").lower()
|
|
126
|
+
if not entry:
|
|
127
|
+
continue
|
|
128
|
+
if host == entry:
|
|
129
|
+
return True
|
|
130
|
+
if host.endswith(entry) and host[: -len(entry)].endswith("."):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def select_https_proxy(
|
|
136
|
+
target_url: str, lookup: Callable[[str], str | None] | None = None
|
|
137
|
+
) -> str | None:
|
|
138
|
+
"""The proxy URL an `https://` request to `target_url` should go through, or
|
|
139
|
+
`None` for direct.
|
|
140
|
+
|
|
141
|
+
`lookup` is the injected environment read, defaulting to `os.environ.get`.
|
|
142
|
+
Takes the whole URL rather than a pre-extracted host so that deriving the
|
|
143
|
+
host is covered by this module's tests too.
|
|
144
|
+
"""
|
|
145
|
+
read = os.environ.get if lookup is None else lookup
|
|
146
|
+
proxy = _first_set(HTTPS_PROXY_ENV_VARS, read)
|
|
147
|
+
if proxy is None:
|
|
148
|
+
return None
|
|
149
|
+
bypass = _first_set(NO_PROXY_ENV_VARS, read)
|
|
150
|
+
if bypass is not None and no_proxy_covers(bypass, host_of(target_url)):
|
|
151
|
+
return None
|
|
152
|
+
return proxy.strip()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def unsupported_proxy_scheme(proxy_url: str) -> str | None:
|
|
156
|
+
"""The scheme of `proxy_url` when `urllib` cannot route through it, else
|
|
157
|
+
`None`.
|
|
158
|
+
|
|
159
|
+
A bare `host:port` with no scheme is supported: `urllib._parse_proxy` treats
|
|
160
|
+
it as the request's own scheme, which is exactly the http(s) CONNECT path.
|
|
161
|
+
"""
|
|
162
|
+
scheme, separator, _ = proxy_url.partition("://")
|
|
163
|
+
if not separator:
|
|
164
|
+
return None
|
|
165
|
+
scheme = scheme.lower()
|
|
166
|
+
if scheme in SUPPORTED_PROXY_SCHEMES:
|
|
167
|
+
return None
|
|
168
|
+
return scheme
|
tan/core/publish.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Publish a finished temp file to its final name without ever overwriting or
|
|
3
|
+
following a link -- shared by `tan model add` and `tan model prep`."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def publish_exclusive(tmp: Path, dest: Path) -> None:
|
|
13
|
+
"""Move the verified temp file to `dest` without ever overwriting or
|
|
14
|
+
following a link: a hard link fails if `dest` exists (even dangling); on a
|
|
15
|
+
filesystem without hard links, `O_EXCL|O_NOFOLLOW` creates it instead.
|
|
16
|
+
Raises `FileExistsError` / `OSError`."""
|
|
17
|
+
try:
|
|
18
|
+
os.link(tmp, dest)
|
|
19
|
+
return
|
|
20
|
+
except FileExistsError:
|
|
21
|
+
raise
|
|
22
|
+
except (OSError, NotImplementedError):
|
|
23
|
+
pass
|
|
24
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0)
|
|
25
|
+
fd = os.open(dest, flags, 0o644)
|
|
26
|
+
try:
|
|
27
|
+
with open(tmp, "rb") as src, os.fdopen(fd, "wb") as out:
|
|
28
|
+
shutil.copyfileobj(src, out)
|
|
29
|
+
except BaseException:
|
|
30
|
+
dest.unlink(missing_ok=True)
|
|
31
|
+
raise
|