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,583 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan flash --raw <file>@<addr>` -- a byte-exact MRAM sector write for bench
|
|
3
|
+
backup/restore (tan-cli#1446).
|
|
4
|
+
|
|
5
|
+
A bench backs MRAM up with J-Link `savebin` and must put it back byte-exact:
|
|
6
|
+
`he_slot0` at 0x80010000, the ATOC sector at 0x8057C000. Flow D cannot (it writes a
|
|
7
|
+
built image plus a signed ATOC), so this is the same J-Link part-profile path with
|
|
8
|
+
`loadbin` + `verifybin` and nothing else -- no signing, no SETOOLS, no reset, no `go`.
|
|
9
|
+
|
|
10
|
+
**Guards, in order:** every `--raw` is parsed and its range validated BEFORE anything is
|
|
11
|
+
spawned (`tan.core.raw_write`: explicit hex address, 16 KiB sector alignment, whole
|
|
12
|
+
sectors, inside the SKU's MRAM, no overlap), under `--dry-run` too. A real write then
|
|
13
|
+
needs the CLI `--confirm` and a held bench reservation (see `_reservation_refusal`), and goes through
|
|
14
|
+
the Flow D probe-selection guard and DPIDR preflight. tan derives no address from a name
|
|
15
|
+
or a map, so an ATOC/STOC is only ever written where the user said.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hmac
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import socket
|
|
24
|
+
import stat
|
|
25
|
+
import subprocess
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from tan.commands import flash_cmd as fc
|
|
29
|
+
from tan.core.flash_plan import (
|
|
30
|
+
FLOW_D_METHOD,
|
|
31
|
+
FlashInputs,
|
|
32
|
+
FlashPlan,
|
|
33
|
+
FlashPlanError,
|
|
34
|
+
FlashTarget,
|
|
35
|
+
_DEFAULT_JLINK_SPEED,
|
|
36
|
+
confirm_gate_note,
|
|
37
|
+
dpidr_preflight_unarmed,
|
|
38
|
+
fa_int_checked,
|
|
39
|
+
fa_str_checked,
|
|
40
|
+
validate_identifier,
|
|
41
|
+
)
|
|
42
|
+
from tan.core.flow_d_report import (
|
|
43
|
+
VERIFICATION_READBACK,
|
|
44
|
+
dpidr_in,
|
|
45
|
+
reset_failures,
|
|
46
|
+
sha256_of,
|
|
47
|
+
)
|
|
48
|
+
from tan.core.jlink_binary import resolve_jlink
|
|
49
|
+
from tan.core.subprocess_env import spawn_env
|
|
50
|
+
from tan.core.raw_write import (
|
|
51
|
+
CODE_FAILED,
|
|
52
|
+
CODE_INVALID,
|
|
53
|
+
CODE_RESERVATION,
|
|
54
|
+
RawError,
|
|
55
|
+
RawSpec,
|
|
56
|
+
lease_changed,
|
|
57
|
+
lease_holder,
|
|
58
|
+
lease_swd_path,
|
|
59
|
+
parse_raw,
|
|
60
|
+
planned,
|
|
61
|
+
raw_script,
|
|
62
|
+
validate_ranges,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
METHOD = "mram_raw"
|
|
66
|
+
#: The bench's reservation marker: the J-Link wrapper on a shared bench only serves a
|
|
67
|
+
#: caller that holds the named labgrid place. tan cannot verify the lease itself, so it
|
|
68
|
+
#: refuses a real write when the marker is absent and reports the place it ran under.
|
|
69
|
+
RESERVATION_ENV = "JLINK_RUN_PLACE"
|
|
70
|
+
#: The reservation-enforcing J-Link wrapper, as an EXPLICIT absolute path. The marker text
|
|
71
|
+
#: inside a program found on PATH or in the cwd is spoofable by anything that can put a file
|
|
72
|
+
#: there, so the wrapper is accepted only by identity with this configured path.
|
|
73
|
+
WRAPPER_ENV = "TAN_JLINK_WRAPPER"
|
|
74
|
+
#: Optional absolute path of `labgrid-client`; otherwise only these fixed directories are
|
|
75
|
+
#: searched -- never `$PATH`, which a shadowing binary could front-run.
|
|
76
|
+
LABGRID_ENV = "TAN_LABGRID_CLIENT"
|
|
77
|
+
#: Per-SESSION lease (tan-cli#1457). labgrid's holder name is `<host>/<user>`, identical for
|
|
78
|
+
#: every session of one user, so "the place is acquired by me" cannot tell this session from
|
|
79
|
+
#: another. `scripts/bench/tan-lease.sh acquire <place>` records a random nonce in
|
|
80
|
+
#: `LEASE_DIR/<place>.lease` (0600) and prints `export TAN_LEASE_NONCE=<nonce>` for ONLY the
|
|
81
|
+
#: acquiring shell; the gate needs the two to match. (labgrid reservation tokens would also
|
|
82
|
+
#: separate sessions but only exist for `reserve`-allocated places, not for `acquire`.)
|
|
83
|
+
NONCE_ENV = "TAN_LEASE_NONCE"
|
|
84
|
+
LEASE_DIR = "~/.cache/alplab-leases"
|
|
85
|
+
_PLACE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]*")
|
|
86
|
+
_NONCE_RE = re.compile(r"[0-9a-f]{32,128}")
|
|
87
|
+
LABGRID_DIRS = ("~/.local/bin", "/usr/local/bin", "/usr/bin", "/bin")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_blobs(specs: list[RawSpec]) -> tuple[list[RawSpec], list[str]]:
|
|
91
|
+
"""Each spec with its size filled in, plus the sha256s. J-Link runs with cwd=<west topdir>,
|
|
92
|
+
not tan's cwd, so the path is made absolute once here: the file hashed is then the file
|
|
93
|
+
loadbin/verifybin open (tan-cli#1513)."""
|
|
94
|
+
sized: list[RawSpec] = []
|
|
95
|
+
digests: list[str] = []
|
|
96
|
+
for spec in specs:
|
|
97
|
+
path = os.path.abspath(spec.path)
|
|
98
|
+
try:
|
|
99
|
+
size, digest = os.path.getsize(path), sha256_of(path)
|
|
100
|
+
except OSError as err:
|
|
101
|
+
raise RawError(f"cannot read {path} ({err})") from err
|
|
102
|
+
sized.append(RawSpec(path, spec.address, size))
|
|
103
|
+
digests.append(digest)
|
|
104
|
+
return sized, digests
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_raw_entry(target: FlashTarget, ctx: Any) -> tuple[int, Any, list[str]]:
|
|
108
|
+
"""Raw-write one slice's MRAM. Returns `(rc, entry, text-lines)` like `_flash_entry`."""
|
|
109
|
+
kind, entry_id = target.kind, target.id
|
|
110
|
+
lines: list[str] = [f"flash: {kind} '{entry_id}' -> {METHOD}"]
|
|
111
|
+
report: dict[str, Any] = {}
|
|
112
|
+
probe_echo: dict[str, Any] | None = None
|
|
113
|
+
|
|
114
|
+
def entry(status: str, rc: int, message: str, **kw: Any) -> Any:
|
|
115
|
+
return fc._Entry(
|
|
116
|
+
kind=kind, id=entry_id, method=METHOD, status=status, rc=rc, message=message,
|
|
117
|
+
probe=probe_echo, extra=report, **kw,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def fail(message: str, code: str | None = CODE_FAILED, **kw: Any) -> tuple[int, Any, list[str]]:
|
|
121
|
+
text = f"{METHOD}[{entry_id}]: {message}"
|
|
122
|
+
lines.append(f" FAIL: {text}")
|
|
123
|
+
return 1, entry("failed", 1, text, issue_code=code, **kw), lines
|
|
124
|
+
|
|
125
|
+
if kind != "slice":
|
|
126
|
+
return fail("only a slice's MRAM can be raw-written, not a helper MCU", CODE_INVALID)
|
|
127
|
+
if not isinstance(target.flash_args, dict) and target.flash_args is not None:
|
|
128
|
+
return fail("flash_args is not a mapping", CODE_INVALID)
|
|
129
|
+
flash_args: Any = dict(target.flash_args or {})
|
|
130
|
+
|
|
131
|
+
# ── the blobs: parsed, read, hashed and range-checked before anything is spawned ──
|
|
132
|
+
try:
|
|
133
|
+
specs = [parse_raw(raw) for raw in ctx.raw]
|
|
134
|
+
sized, digests = _read_blobs(specs)
|
|
135
|
+
validate_ranges(sized, *reversed(_mram_window(ctx)))
|
|
136
|
+
device = fa_str_checked(flash_args, "jlink_flash_device", False)
|
|
137
|
+
if device is None:
|
|
138
|
+
raise RawError(
|
|
139
|
+
"flash_args.jlink_flash_device is required -- only the part-number J-Link device "
|
|
140
|
+
"profile unlocks the MRAM loader; tan does not guess one"
|
|
141
|
+
)
|
|
142
|
+
validate_identifier(device, "jlink_flash_device")
|
|
143
|
+
except (RawError, FlashPlanError) as err:
|
|
144
|
+
return fail(str(err), CODE_INVALID)
|
|
145
|
+
report["raw"] = {"writes": planned(sized, digests), "resetCommands": False, "signs": False}
|
|
146
|
+
|
|
147
|
+
# ── J-Link: trusted binary, probe selection, parameters ──
|
|
148
|
+
found = resolve_jlink(ctx.jlink_path, project_dir=ctx.project_dir)
|
|
149
|
+
exe = found.path if found is not None else None
|
|
150
|
+
report["jlink"] = {"binary": exe, "binarySource": found.source if found else None}
|
|
151
|
+
if exe is None and not ctx.dry_run:
|
|
152
|
+
return fail(fc._NO_TRUSTED_JLINK)
|
|
153
|
+
try:
|
|
154
|
+
flash_args, selection, snapshot = fc._flow_d_probe_selection(flash_args, ctx)
|
|
155
|
+
probe_echo = fc._probe_echo(selection, ctx)
|
|
156
|
+
if selection is not None and selection.refusal_code is not None:
|
|
157
|
+
lines.append(f" FAIL: {selection.refusal}")
|
|
158
|
+
return (
|
|
159
|
+
1,
|
|
160
|
+
entry("failed", 1, selection.refusal or "", probe_refusal=selection.refusal_code),
|
|
161
|
+
lines,
|
|
162
|
+
)
|
|
163
|
+
guard = None
|
|
164
|
+
if selection is not None and (selection.serial is not None or selection.visible is None):
|
|
165
|
+
guard = fc._ProbeGuard(selection, snapshot, ctx.enumerate_probes, probe_echo)
|
|
166
|
+
serial = fa_str_checked(flash_args, "jlink_serial", False)
|
|
167
|
+
speed = fa_int_checked(flash_args, "jlink_speed") or _DEFAULT_JLINK_SPEED
|
|
168
|
+
if serial is not None:
|
|
169
|
+
validate_identifier(serial, "jlink_serial", destination=fc._JLINK_SERIAL_DESTINATION)
|
|
170
|
+
fc.validate_flow_d_preflight_args(flash_args)
|
|
171
|
+
except FlashPlanError as err:
|
|
172
|
+
return fail(str(err), CODE_INVALID)
|
|
173
|
+
|
|
174
|
+
preamble = "".join(
|
|
175
|
+
[f"SelectEmuBySN {serial}\n" if serial is not None else "",
|
|
176
|
+
f"si SWD\nspeed {speed}\ndevice {device}\nconnect\n"]
|
|
177
|
+
)
|
|
178
|
+
script = raw_script(preamble, sized)
|
|
179
|
+
argv = (
|
|
180
|
+
"JLinkExe", "-device", device, "-if", "SWD", "-speed", str(speed),
|
|
181
|
+
"-ExitOnError", "1", "-NoGui", "1", "-CommanderScript",
|
|
182
|
+
)
|
|
183
|
+
report["plan"] = {"argv": list(argv), "jlinkScript": (fc._DISABLE_FW_UPDATE + script).splitlines()}
|
|
184
|
+
summary = ", ".join(
|
|
185
|
+
f"{w['size']} B at {w['address']} ({w['sectorSpan']['count']} sector(s))" for w in report["raw"]["writes"]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if ctx.dry_run:
|
|
189
|
+
msg = f"{METHOD}[{entry_id}]: would write {summary}; no reset; nothing spawned"
|
|
190
|
+
lines.append(f" {msg}")
|
|
191
|
+
return 0, entry("ok", 0, msg), lines
|
|
192
|
+
|
|
193
|
+
# The CLI `--confirm` (or ALP_FLASH_FORCE=1) ONLY: a manifest's `flash_args.confirm`
|
|
194
|
+
# is project-controlled and must never arm a raw MRAM overwrite.
|
|
195
|
+
confirmed = bool(ctx.force_confirm)
|
|
196
|
+
if not confirmed:
|
|
197
|
+
msg = (
|
|
198
|
+
f"{METHOD}[{entry_id}]: would write {summary} -- NOT written: "
|
|
199
|
+
f"{confirm_gate_note('--confirm was not given (flash_args.confirm does not arm --raw)')}"
|
|
200
|
+
)
|
|
201
|
+
lines.append(f" {msg}")
|
|
202
|
+
return 0, entry("planned", 0, msg), lines
|
|
203
|
+
|
|
204
|
+
place = os.environ.get(RESERVATION_ENV, "").strip()
|
|
205
|
+
refusal, verified = _reservation_refusal(
|
|
206
|
+
place, exe, selection.usb_path if selection is not None else None
|
|
207
|
+
)
|
|
208
|
+
if refusal is not None or verified is None:
|
|
209
|
+
return fail(refusal or "reservation could not be verified", CODE_RESERVATION)
|
|
210
|
+
# Every later spawn (guard, DPIDR preflight, write, read-back) uses exactly the path that
|
|
211
|
+
# was verified here -- the env var is not read again.
|
|
212
|
+
exe = verified
|
|
213
|
+
report["jlink"]["binary"] = exe
|
|
214
|
+
report["jlink"]["reservation"] = {
|
|
215
|
+
"env": RESERVATION_ENV, "place": place, "verified": "labgrid+session-lease", "usbPath": selection.usb_path,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ── the wrong-board guard, exactly as for a Flow D write ──
|
|
219
|
+
armed = not dpidr_preflight_unarmed(FLOW_D_METHOD, flash_args, None)
|
|
220
|
+
if ctx.require_dpidr and not armed:
|
|
221
|
+
return fail(
|
|
222
|
+
"ALP_FLASH_REQUIRE_DPIDR=1 is set and flash_args.expect_dpidr / jlink_device are "
|
|
223
|
+
"not both set -- refusing to write with no wrong-board guard", CODE_INVALID,
|
|
224
|
+
)
|
|
225
|
+
if guard is not None:
|
|
226
|
+
refusal = fc._probe_guard_refusal(
|
|
227
|
+
guard, fc._jlink_program(ctx.venv_bin, exe), None, ctx.workspace
|
|
228
|
+
)
|
|
229
|
+
if refusal is not None:
|
|
230
|
+
return fail(refusal, None, probe_refusal=guard.tripped_code)
|
|
231
|
+
guard.fresh = True
|
|
232
|
+
facts: dict[str, Any] = {}
|
|
233
|
+
refusal = fc._flow_d_preflight(
|
|
234
|
+
FlashInputs(
|
|
235
|
+
artefact="", flash_args=flash_args, core_id=entry_id, sku=ctx.sku,
|
|
236
|
+
dry_run=False, force_confirm=ctx.force_confirm,
|
|
237
|
+
),
|
|
238
|
+
ctx.venv_bin, ctx.workspace, probe_guard=guard, facts=facts, jlink_exe=exe,
|
|
239
|
+
)
|
|
240
|
+
if refusal is not None:
|
|
241
|
+
tripped = guard is not None and guard.tripped == refusal
|
|
242
|
+
return fail(refusal, None, probe_refusal=guard.tripped_code if tripped else None)
|
|
243
|
+
|
|
244
|
+
# ── write + verify (J-Link's flash cache), no reset ──
|
|
245
|
+
plan = FlashPlan(argv=argv, ok_message="", jlink_script=script)
|
|
246
|
+
outcome = fc._execute(plan, ctx.capture, ctx.venv_bin, ctx.workspace, guard, jlink_exe=exe)
|
|
247
|
+
transcript = f"{outcome.stdout}\n{outcome.stderr}"
|
|
248
|
+
report["jlink"]["dpidr"] = facts.get("dpidr") or dpidr_in(transcript)
|
|
249
|
+
report["jlink"]["verification"] = fc.VERIFICATION_CACHE
|
|
250
|
+
report["jlink"]["verificationNote"] = fc.VERIFICATION_NOTE
|
|
251
|
+
report["jlink"]["transcriptTail"] = fc.transcript_tail(transcript)
|
|
252
|
+
if guard is not None and guard.tripped:
|
|
253
|
+
return fail(guard.tripped, None, probe_refusal=guard.tripped_code)
|
|
254
|
+
if not outcome.success:
|
|
255
|
+
return fail(f"the write session failed: {fc._capture_tail(outcome) or 'J-Link failed'}")
|
|
256
|
+
if reset_failures(transcript):
|
|
257
|
+
return fail(
|
|
258
|
+
"J-Link reported a halt/reset failure during a write that must not reset: "
|
|
259
|
+
+ ", ".join(reset_failures(transcript))
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
message = f"{METHOD}[{entry_id}]: wrote {summary}; cache-verified; no reset command was sent"
|
|
263
|
+
if ctx.readback:
|
|
264
|
+
writes = [
|
|
265
|
+
{"address": w["address"], "size": w["size"], "path": w["path"], "sha256": w["sha256"]}
|
|
266
|
+
for w in report["raw"]["writes"]
|
|
267
|
+
]
|
|
268
|
+
failure = fc._flow_d_readback(
|
|
269
|
+
plan, outcome, ctx, writes, report, guard, exe, entry_id=entry_id
|
|
270
|
+
)
|
|
271
|
+
if failure is not None:
|
|
272
|
+
code, text = failure
|
|
273
|
+
return fail(text, code if code.startswith("flash.readback") else None,
|
|
274
|
+
probe_refusal=code[len("flash.probe-"):] if code.startswith("flash.probe-") else None)
|
|
275
|
+
if report["jlink"].get("verification") == VERIFICATION_READBACK:
|
|
276
|
+
message += "; read back in a fresh J-Link session (sha256 match)"
|
|
277
|
+
message += (
|
|
278
|
+
" -- loadbin may have halted the core; power-cycle the board so the Secure Enclave boots "
|
|
279
|
+
"the restored contents"
|
|
280
|
+
)
|
|
281
|
+
lines.append(f" ok: {message}")
|
|
282
|
+
return 0, entry("ok", 0, message, preflight_unarmed=not armed), lines
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _platform_refusal() -> str | None:
|
|
286
|
+
"""`pwd`/`grp`/`getuid` and `O_NOFOLLOW` are POSIX-only, imported lazily so that
|
|
287
|
+
`import tan.cli` works everywhere; the raw-write interlock needs them, so it refuses
|
|
288
|
+
where they are missing rather than guessing."""
|
|
289
|
+
try:
|
|
290
|
+
import grp # noqa: F401
|
|
291
|
+
import pwd # noqa: F401
|
|
292
|
+
except ImportError:
|
|
293
|
+
return "unsupported on this platform: the --raw reservation interlock needs POSIX pwd/grp"
|
|
294
|
+
if os.name == "nt" or not hasattr(os, "getuid") or not hasattr(os, "O_NOFOLLOW"):
|
|
295
|
+
return "unsupported on this platform: the --raw reservation interlock needs POSIX uid/O_NOFOLLOW"
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _current_user() -> str:
|
|
300
|
+
"""The account name of the real uid (never `USER`/`LOGNAME`, which the caller controls)."""
|
|
301
|
+
import pwd
|
|
302
|
+
|
|
303
|
+
return pwd.getpwuid(os.getuid()).pw_name
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _private_group(gid: int) -> bool:
|
|
307
|
+
"""Whether `gid` is a group only the current user belongs to (a user-private group)."""
|
|
308
|
+
import grp
|
|
309
|
+
import pwd
|
|
310
|
+
|
|
311
|
+
me = _current_user()
|
|
312
|
+
try:
|
|
313
|
+
members = set(grp.getgrgid(gid).gr_mem)
|
|
314
|
+
except KeyError:
|
|
315
|
+
return False
|
|
316
|
+
return members <= {me} and all(u.pw_name == me for u in pwd.getpwall() if u.pw_gid == gid)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _node_problem(node: str, st: os.stat_result) -> str | None:
|
|
320
|
+
mode = st.st_mode
|
|
321
|
+
sticky_dir = stat.S_ISDIR(mode) and bool(mode & stat.S_ISVTX)
|
|
322
|
+
if st.st_uid not in (0, os.getuid()):
|
|
323
|
+
return f"{node} is owned by uid {st.st_uid}, not root or you"
|
|
324
|
+
if stat.S_ISLNK(mode):
|
|
325
|
+
return None # a symlink's own mode bits are always 0777 and mean nothing
|
|
326
|
+
if mode & stat.S_IWOTH and not sticky_dir:
|
|
327
|
+
return f"{node} is world-writable"
|
|
328
|
+
if mode & stat.S_IWGRP and not sticky_dir and not (
|
|
329
|
+
st.st_gid == os.getgid() and _private_group(st.st_gid)
|
|
330
|
+
):
|
|
331
|
+
return f"{node} is group-writable by a group others belong to"
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _chain_problem(start: str) -> str | None:
|
|
336
|
+
node = start
|
|
337
|
+
while True:
|
|
338
|
+
try:
|
|
339
|
+
st = os.lstat(node)
|
|
340
|
+
except OSError:
|
|
341
|
+
return f"{node} cannot be inspected"
|
|
342
|
+
problem = _node_problem(node, st)
|
|
343
|
+
if problem:
|
|
344
|
+
return problem
|
|
345
|
+
parent = os.path.dirname(node)
|
|
346
|
+
if parent == node:
|
|
347
|
+
return None
|
|
348
|
+
node = parent
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _unsafe(path: str) -> str | None:
|
|
352
|
+
"""Why `path` cannot be trusted as an interlock binary, or `None`. It must be absolute,
|
|
353
|
+
resolve (symlinks followed) to an executable regular file outside the cwd, and BOTH chains
|
|
354
|
+
-- the given path itself and its directories as written (where a symlink lives), and the
|
|
355
|
+
resolved target with its parents -- must be owned by root or the current user and not
|
|
356
|
+
writable by others or by a group anyone else belongs to (a sticky directory is allowed)."""
|
|
357
|
+
if not os.path.isabs(path):
|
|
358
|
+
return "is not an absolute path"
|
|
359
|
+
real = os.path.realpath(path)
|
|
360
|
+
cwd = os.path.realpath(os.getcwd())
|
|
361
|
+
if real == cwd or real.startswith(cwd + os.sep):
|
|
362
|
+
return "lives under the current directory"
|
|
363
|
+
if not (os.path.isfile(real) and os.access(real, os.X_OK)):
|
|
364
|
+
return "is not an executable file"
|
|
365
|
+
return _chain_problem(os.path.abspath(path)) or _chain_problem(real)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# What the shim defaults to so `labgrid-client` finds the coordinator (bin/JLinkExe, jlink-run.sh).
|
|
369
|
+
_DEFAULT_COORDINATOR = "100.64.0.1:20408"
|
|
370
|
+
#: Interpreter-steering variables a hostile environment could use to run its own code inside
|
|
371
|
+
#: a Python `labgrid-client`.
|
|
372
|
+
_PYTHON_ENV = ("PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP", "PYTHONINSPECT", "PYTHONEXECUTABLE")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _labgrid_client() -> str | None:
|
|
376
|
+
"""The absolute `labgrid-client` to run: `TAN_LABGRID_CLIENT`, else the first of the fixed
|
|
377
|
+
directories holding a trustworthy one. Never a `$PATH` search."""
|
|
378
|
+
configured = os.environ.get(LABGRID_ENV, "").strip()
|
|
379
|
+
candidates = (
|
|
380
|
+
[configured] if configured
|
|
381
|
+
else [os.path.join(os.path.expanduser(d), "labgrid-client") for d in LABGRID_DIRS]
|
|
382
|
+
)
|
|
383
|
+
for candidate in candidates:
|
|
384
|
+
if os.path.exists(candidate) and _unsafe(candidate) is None:
|
|
385
|
+
return os.path.realpath(candidate)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _lease_refusal(place: str) -> tuple[str | None, str | None]:
|
|
390
|
+
"""Refusal text unless THIS session proves it acquired `place`: `TAN_LEASE_NONCE` is set,
|
|
391
|
+
well-formed, and equals the nonce in this user's own 0600 lease file for exactly that
|
|
392
|
+
place. Any missing, unreadable, mis-owned, mis-moded or mismatching piece refuses. Returns
|
|
393
|
+
`(refusal, changed)`: the `changed=` recorded at acquire time, to be compared with the
|
|
394
|
+
place's CURRENT labgrid `changed:` so a stale lease from an earlier acquisition is refused."""
|
|
395
|
+
how = (
|
|
396
|
+
f"Acquire the place from this shell with `eval \"$(scripts/bench/tan-lease.sh acquire "
|
|
397
|
+
f"{place})\"` so {NONCE_ENV} and {LEASE_DIR}/{place}.lease agree; a place held by another "
|
|
398
|
+
"session of the same labgrid user is not yours to write."
|
|
399
|
+
)
|
|
400
|
+
if not _PLACE_RE.fullmatch(place):
|
|
401
|
+
return f"the place name {place!r} is not a plain labgrid place name. {how}", None
|
|
402
|
+
nonce = os.environ.get(NONCE_ENV, "").strip()
|
|
403
|
+
if not _NONCE_RE.fullmatch(nonce):
|
|
404
|
+
return f"{NONCE_ENV} is not set to a lease nonce, so this session has not acquired {place}. {how}", None
|
|
405
|
+
directory = os.path.expanduser(LEASE_DIR)
|
|
406
|
+
path = os.path.join(directory, f"{place}.lease")
|
|
407
|
+
# Opened with O_NOFOLLOW and judged by fstat of the OPEN descriptors: no lstat-then-open race.
|
|
408
|
+
try:
|
|
409
|
+
dfd = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
410
|
+
except OSError:
|
|
411
|
+
return f"there is no lease directory {directory}: this session never acquired {place} with the lease helper. {how}", None
|
|
412
|
+
try:
|
|
413
|
+
dst = os.fstat(dfd)
|
|
414
|
+
try:
|
|
415
|
+
ffd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
416
|
+
except OSError:
|
|
417
|
+
return (
|
|
418
|
+
f"there is no readable regular lease file {path} (symlinks are refused): this "
|
|
419
|
+
f"session never acquired {place} with the lease helper. {how}"
|
|
420
|
+
), None
|
|
421
|
+
try:
|
|
422
|
+
fst = os.fstat(ffd)
|
|
423
|
+
if not stat.S_ISDIR(dst.st_mode) or dst.st_uid != os.getuid() or dst.st_mode & 0o077:
|
|
424
|
+
return f"the lease directory {directory} must be a directory owned by you with mode 0700. {how}", None
|
|
425
|
+
if not stat.S_ISREG(fst.st_mode) or fst.st_uid != os.getuid() or fst.st_mode & 0o077:
|
|
426
|
+
return f"the lease file {path} must be a regular file owned by you with mode 0600. {how}", None
|
|
427
|
+
with os.fdopen(ffd, encoding="utf-8", closefd=False) as fh:
|
|
428
|
+
text = fh.read(4096)
|
|
429
|
+
except (OSError, UnicodeDecodeError):
|
|
430
|
+
return f"the lease file {path} cannot be read. {how}", None
|
|
431
|
+
finally:
|
|
432
|
+
os.close(ffd)
|
|
433
|
+
finally:
|
|
434
|
+
os.close(dfd)
|
|
435
|
+
fields = dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
|
|
436
|
+
recorded = fields.get("nonce", "")
|
|
437
|
+
# Validate the file's nonce's shape too, then compare bytes in constant time.
|
|
438
|
+
if (
|
|
439
|
+
fields.get("place") != place
|
|
440
|
+
or not _NONCE_RE.fullmatch(recorded)
|
|
441
|
+
or not hmac.compare_digest(recorded.encode("ascii"), nonce.encode("ascii"))
|
|
442
|
+
):
|
|
443
|
+
return (
|
|
444
|
+
f"{NONCE_ENV} does not match the lease on {place} -- another session holds that lease, "
|
|
445
|
+
f"not this one. {how}"
|
|
446
|
+
), None
|
|
447
|
+
changed = fields.get("changed", "").strip()
|
|
448
|
+
if not changed:
|
|
449
|
+
return (
|
|
450
|
+
f"the lease on {place} records no labgrid `changed` value, so it cannot be bound to "
|
|
451
|
+
f"one acquisition. {how}"
|
|
452
|
+
), None
|
|
453
|
+
return None, changed
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _reservation_refusal(
|
|
457
|
+
place: str, exe: str | None, usb_path: str | None
|
|
458
|
+
) -> tuple[str | None, str | None]:
|
|
459
|
+
"""`(refusal, verified_path)`: a refusal text, or `None` plus the REAL path of the wrapper
|
|
460
|
+
to spawn from here on. Fails closed on any ambiguity. Needs: the place named
|
|
461
|
+
(`JLINK_RUN_PLACE`); the J-Link program tan will run IS the wrapper configured in
|
|
462
|
+
`TAN_JLINK_WRAPPER` (absolute, symlink-resolved, outside the cwd, trustworthy ownership and
|
|
463
|
+
modes on both chains -- a marker string in a binary found on PATH proves nothing);
|
|
464
|
+
`labgrid-client` (absolute, fixed directories) reports THIS uid's host/user as the single
|
|
465
|
+
holder of the place; and the probe tan selected IS the leased place's `swd` USB path."""
|
|
466
|
+
need = (
|
|
467
|
+
f"Needs: {RESERVATION_ENV}=<labgrid place you hold>, {WRAPPER_ENV}=<absolute path of the "
|
|
468
|
+
"reservation-enforcing JLinkExe wrapper> and tan running THAT program (--jlink "
|
|
469
|
+
"<that path>, or it first on PATH), --probe-usb-path <the place's swd port>, and "
|
|
470
|
+
"`labgrid-client` able to reach the coordinator."
|
|
471
|
+
)
|
|
472
|
+
if not place:
|
|
473
|
+
return f"refusing to overwrite MRAM without a held bench reservation: {RESERVATION_ENV} is not set. {need}", None
|
|
474
|
+
unsupported = _platform_refusal()
|
|
475
|
+
if unsupported is not None:
|
|
476
|
+
return f"{unsupported}. {need}", None
|
|
477
|
+
lease, leased_changed = _lease_refusal(place)
|
|
478
|
+
if lease is not None:
|
|
479
|
+
return lease, None
|
|
480
|
+
configured = os.environ.get(WRAPPER_ENV, "").strip()
|
|
481
|
+
if not configured:
|
|
482
|
+
return f"{WRAPPER_ENV} is not set, so no J-Link program can be recognised as the reservation wrapper. {need}", None
|
|
483
|
+
why = _unsafe(configured)
|
|
484
|
+
if why is not None:
|
|
485
|
+
return f"{WRAPPER_ENV}={configured}: {why}. {need}", None
|
|
486
|
+
real = os.path.realpath(configured)
|
|
487
|
+
if not exe or not os.path.isabs(exe) or os.path.realpath(exe) != real:
|
|
488
|
+
return (
|
|
489
|
+
f"the J-Link program tan would run ({exe}) is not the configured wrapper "
|
|
490
|
+
f"({configured}); a raw or look-alike JLinkExe would not enforce the lease. {need}"
|
|
491
|
+
), None
|
|
492
|
+
text, why_show = _labgrid_show(place)
|
|
493
|
+
me = f"{socket.gethostname()}/{_current_user()}"
|
|
494
|
+
holder = lease_holder(text or "")
|
|
495
|
+
if holder != me:
|
|
496
|
+
return (
|
|
497
|
+
f"you do not hold the labgrid place {place} (acquired by: "
|
|
498
|
+
f"{holder or 'nobody / not readable / ambiguous'}; you are {me}"
|
|
499
|
+
+ (f"; labgrid-client: {why_show}" if why_show else "")
|
|
500
|
+
+ f"). {need}"
|
|
501
|
+
), None
|
|
502
|
+
current_changed = lease_changed(text or "")
|
|
503
|
+
if current_changed is None or current_changed != leased_changed:
|
|
504
|
+
return (
|
|
505
|
+
f"the lease on {place} was written for an earlier acquisition (labgrid `changed:` was "
|
|
506
|
+
f"{leased_changed!r}, is {current_changed or 'not reported'!r}): the place has been "
|
|
507
|
+
"released/re-acquired since, so this lease is stale. Release and acquire it again "
|
|
508
|
+
"with the lease helper. " + need
|
|
509
|
+
), None
|
|
510
|
+
leased = lease_swd_path(text or "")
|
|
511
|
+
if leased is None or usb_path is None or leased != usb_path:
|
|
512
|
+
return (
|
|
513
|
+
f"the selected probe (--probe-usb-path {usb_path or 'not given'}) is not the leased "
|
|
514
|
+
f"place's swd port ({leased or 'not reported by labgrid'}) -- refusing to write to a "
|
|
515
|
+
f"probe the reservation does not cover. {need}"
|
|
516
|
+
), None
|
|
517
|
+
return None, real
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _labgrid_show(place: str) -> tuple[str | None, str]:
|
|
521
|
+
"""`(output, why)` of `labgrid-client -p <place> show`: the output, or `None` with a reason
|
|
522
|
+
it could not be had."""
|
|
523
|
+
client = _labgrid_client()
|
|
524
|
+
if client is None:
|
|
525
|
+
return None, (
|
|
526
|
+
f"no trustworthy labgrid-client found (set {LABGRID_ENV}=<absolute path>, or install "
|
|
527
|
+
f"it in {', '.join(LABGRID_DIRS)})"
|
|
528
|
+
)
|
|
529
|
+
env = spawn_env({"LG_COORDINATOR": os.environ.get("LG_COORDINATOR") or _DEFAULT_COORDINATOR})
|
|
530
|
+
for name in _PYTHON_ENV:
|
|
531
|
+
env.pop(name, None)
|
|
532
|
+
try:
|
|
533
|
+
done = subprocess.run(
|
|
534
|
+
[client, "-p", place, "show"], capture_output=True, text=True, timeout=30,
|
|
535
|
+
check=False, env=env,
|
|
536
|
+
)
|
|
537
|
+
except (OSError, subprocess.SubprocessError) as err:
|
|
538
|
+
return None, f"could not run {client}: {err}"
|
|
539
|
+
if done.returncode != 0:
|
|
540
|
+
tail = (done.stderr or done.stdout).strip().splitlines()[-1:] or ["no output"]
|
|
541
|
+
return None, f"{client} exited {done.returncode}: {tail[0]}"
|
|
542
|
+
return done.stdout, ""
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _mram_window(ctx: Any) -> tuple[int, int]:
|
|
546
|
+
"""`(base, length)` of the SKU's MRAM from the SoC metadata of the SDK in use: the SoC
|
|
547
|
+
document's `soc_flash_base` and the SKU's OWN variant `mram_mb` (E3 ships 1.5 MB and
|
|
548
|
+
5.5 MB parts, so there is no family fallback). Refuses -- never guesses -- when the SDK
|
|
549
|
+
metadata, the variant or either number cannot be read."""
|
|
550
|
+
from tan.commands.build_output import read_sdk_som_and_soc
|
|
551
|
+
from tan.core.size import resolve_variant
|
|
552
|
+
|
|
553
|
+
metadata_root = os.path.join(ctx.sdk_root, "metadata") if ctx.sdk_root else None
|
|
554
|
+
walked = (
|
|
555
|
+
read_sdk_som_and_soc(metadata_root, ctx.sku)
|
|
556
|
+
if metadata_root and ctx.sku and os.path.isdir(metadata_root)
|
|
557
|
+
else None
|
|
558
|
+
)
|
|
559
|
+
if walked is None:
|
|
560
|
+
raise RawError(
|
|
561
|
+
f"cannot bound the write: no readable SoM preset / SoC metadata for '{ctx.sku}' "
|
|
562
|
+
f"under {metadata_root} -- refusing to guess the MRAM window"
|
|
563
|
+
)
|
|
564
|
+
silicon, silicon_variant, variants, _soc_flash_mb, _cores = walked
|
|
565
|
+
variant = resolve_variant(silicon_variant, ctx.sku, variants)
|
|
566
|
+
mb = variant.get("mram_mb") if variant else None
|
|
567
|
+
if not isinstance(mb, (int, float)) or isinstance(mb, bool) or mb <= 0:
|
|
568
|
+
raise RawError(
|
|
569
|
+
f"cannot bound the write: '{ctx.sku}' does not resolve to a SoC variant that states "
|
|
570
|
+
"mram_mb (a family default would be wrong, e.g. E3 has 1.5 MB parts) -- refusing"
|
|
571
|
+
)
|
|
572
|
+
parts = silicon.split(":")
|
|
573
|
+
try:
|
|
574
|
+
with open(os.path.join(metadata_root, "socs", *parts[:2], f"{parts[2]}.json"), encoding="utf-8") as fh:
|
|
575
|
+
base = json.load(fh).get("soc_flash_base")
|
|
576
|
+
except (OSError, ValueError, IndexError):
|
|
577
|
+
base = None
|
|
578
|
+
if not isinstance(base, int) or isinstance(base, bool) or base <= 0:
|
|
579
|
+
raise RawError(
|
|
580
|
+
f"cannot bound the write: the SoC document for '{ctx.sku}' states no soc_flash_base "
|
|
581
|
+
"-- refusing to assume the MRAM base"
|
|
582
|
+
)
|
|
583
|
+
return base, int(mb * 1024 * 1024)
|