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/ram_watch.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan flash --ram --watch` -- read-only memory sampling while a RAM-run image runs
|
|
3
|
+
(tan-cli#1436). Pure logic, no IO:
|
|
4
|
+
|
|
5
|
+
* [`parse_watch`]: one `--watch <addr>[:<words>][@<period-ms>]` spec -> [`WatchSpec`],
|
|
6
|
+
refusing an unaligned address, a zero or excessive word count, a period outside the
|
|
7
|
+
supported range, and any address the probe cannot safely read.
|
|
8
|
+
* [`schedule`]: the specs + the `--wait` window -> the sample times, merged in order.
|
|
9
|
+
* [`watch_lines`]: the J-Link Commander lines appended after `go` -- `mem32` reads
|
|
10
|
+
separated by `Sleep <ms>`. `mem32` only: no write command is ever produced.
|
|
11
|
+
* [`parse_samples`]: the load transcript -> the envelope's `data.watch[]`.
|
|
12
|
+
|
|
13
|
+
**Time basis.** J-Link Commander prints no timestamps, so a sample's `elapsedMs` is its
|
|
14
|
+
SCHEDULED offset after `go` (the `Sleep` sum), not a measurement; every `mem32` adds SWD
|
|
15
|
+
latency on top, so the real offset is later and drifts upward over a long window.
|
|
16
|
+
|
|
17
|
+
**Unsafe addresses.** The denylist is keyed by the attached core. For the M55 HE (the
|
|
18
|
+
only core a RAM-run attaches to) the HP TCM windows `[0x50000000, 0x58000000)` (ITCM and
|
|
19
|
+
DTCM globals) are refused: reading them from an HE attach is the bench's known-bad read,
|
|
20
|
+
it leaves the core unhaltable until a PIN reset (bench round 8, see `tan.core.ram_run`).
|
|
21
|
+
|
|
22
|
+
**Side effects.** `mem32` is read-only on memory, but a register read (a FIFO, a
|
|
23
|
+
clear-on-read status, a clock-gated block) may have side effects; tan cannot tell them
|
|
24
|
+
apart, so watch RAM or plain data registers.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
|
|
32
|
+
from tan.core.ram_run import HE_ALIAS, _ITCM_GLOBAL
|
|
33
|
+
|
|
34
|
+
CODE_INVALID = "flash.ram-watch-invalid"
|
|
35
|
+
CODE_UNSAFE = "flash.ram-watch-unsafe-address"
|
|
36
|
+
CODE_INCOMPLETE = "flash.ram-watch-incomplete"
|
|
37
|
+
|
|
38
|
+
DEFAULT_WORDS = 1
|
|
39
|
+
MAX_WORDS = 64
|
|
40
|
+
DEFAULT_PERIOD_MS = 100
|
|
41
|
+
MIN_PERIOD_MS = 10
|
|
42
|
+
MAX_PERIOD_MS = 60_000
|
|
43
|
+
MAX_WATCHES = 8
|
|
44
|
+
#: Upper bound on the `mem32` reads in one session, so a long `--wait` with a short
|
|
45
|
+
#: period cannot build a script of unbounded size.
|
|
46
|
+
MAX_SAMPLES = 2000
|
|
47
|
+
#: Per attached core, the spans its attach must never read. HE: the HP TCM windows
|
|
48
|
+
#: [HP ITCM global, HE ITCM global) -- ITCM and DTCM.
|
|
49
|
+
DENIED_SPANS = {"M55_HE": ((_ITCM_GLOBAL["M55_HP"], HE_ALIAS),)}
|
|
50
|
+
#: Wall-clock budget per `mem32` (SWD read + J-Link command overhead), generous.
|
|
51
|
+
PER_READ_BUDGET_S = 2.0
|
|
52
|
+
TIMEOUT_MARGIN_S = 60.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def session_timeout_s(specs: "Sequence[WatchSpec]", duration_ms: int) -> float:
|
|
56
|
+
"""The load session's spawn timeout: the window, plus a budget per scheduled read,
|
|
57
|
+
plus a margin."""
|
|
58
|
+
return duration_ms / 1000 + len(schedule(specs, duration_ms)) * PER_READ_BUDGET_S + TIMEOUT_MARGIN_S
|
|
59
|
+
|
|
60
|
+
_SPEC = re.compile(
|
|
61
|
+
r"^(?P<addr>0[xX][0-9A-Fa-f]{1,8}|[0-9]{1,10})(?::(?P<words>[0-9]+))?(?:@(?P<period>[0-9]+))?$"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WatchError(Exception):
|
|
66
|
+
"""A `--watch` refusal. `code` is the registered issue code."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, message: str, code: str = CODE_INVALID):
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
self.code = code
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class WatchSpec:
|
|
75
|
+
address: int
|
|
76
|
+
words: int
|
|
77
|
+
period_ms: int
|
|
78
|
+
text: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_watch(text: str, core: str = "M55_HE") -> WatchSpec:
|
|
82
|
+
match = _SPEC.match(text.strip()) if isinstance(text, str) else None
|
|
83
|
+
if match is None:
|
|
84
|
+
raise WatchError(
|
|
85
|
+
f"--watch {text!r} is not <addr>[:<words>][@<period-ms>] (addr is 0x-hex or decimal)"
|
|
86
|
+
)
|
|
87
|
+
raw = match["addr"]
|
|
88
|
+
address = int(raw, 16) if raw[:2].lower() == "0x" else int(raw)
|
|
89
|
+
words = int(match["words"]) if match["words"] is not None else DEFAULT_WORDS
|
|
90
|
+
period = int(match["period"]) if match["period"] is not None else DEFAULT_PERIOD_MS
|
|
91
|
+
if address > 0xFFFFFFFF:
|
|
92
|
+
raise WatchError(f"--watch {text!r}: address 0x{address:X} is beyond 32 bits")
|
|
93
|
+
if address % 4:
|
|
94
|
+
raise WatchError(f"--watch {text!r}: address 0x{address:08X} is not 4-byte aligned (mem32)")
|
|
95
|
+
if not 1 <= words <= MAX_WORDS:
|
|
96
|
+
raise WatchError(f"--watch {text!r}: {words} words is outside 1..{MAX_WORDS}")
|
|
97
|
+
if not MIN_PERIOD_MS <= period <= MAX_PERIOD_MS:
|
|
98
|
+
raise WatchError(
|
|
99
|
+
f"--watch {text!r}: period {period} ms is outside {MIN_PERIOD_MS}..{MAX_PERIOD_MS}"
|
|
100
|
+
)
|
|
101
|
+
last = address + 4 * words - 1
|
|
102
|
+
if last > 0xFFFFFFFF:
|
|
103
|
+
raise WatchError(f"--watch {text!r}: the read runs past the end of the address space")
|
|
104
|
+
for lo, hi in DENIED_SPANS.get(core, ()):
|
|
105
|
+
if address < hi and last >= lo:
|
|
106
|
+
raise WatchError(
|
|
107
|
+
f"--watch {text!r}: 0x{address:08X}..0x{last:08X} overlaps the HP TCM "
|
|
108
|
+
f"windows 0x{lo:08X}..0x{hi - 1:08X}; reading it from the HE attach "
|
|
109
|
+
"leaves the core unhaltable until a PIN reset",
|
|
110
|
+
CODE_UNSAFE,
|
|
111
|
+
)
|
|
112
|
+
return WatchSpec(address, words, period, text.strip())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def parse_watches(texts: Sequence[str], core: str = "M55_HE") -> list[WatchSpec]:
|
|
116
|
+
if len(texts) > MAX_WATCHES:
|
|
117
|
+
raise WatchError(f"{len(texts)} --watch options; at most {MAX_WATCHES}")
|
|
118
|
+
return [parse_watch(t, core) for t in texts]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def schedule(specs: Sequence[WatchSpec], duration_ms: int) -> list[tuple[int, int]]:
|
|
122
|
+
"""`(offset_ms, spec_index)` for every sample in `[0, duration_ms]`, time-ordered
|
|
123
|
+
(ties in spec order). Refuses more than [`MAX_SAMPLES`]."""
|
|
124
|
+
out = [
|
|
125
|
+
(t, i)
|
|
126
|
+
for i, s in enumerate(specs)
|
|
127
|
+
for t in range(0, max(duration_ms, 0) + 1, s.period_ms)
|
|
128
|
+
]
|
|
129
|
+
if len(out) > MAX_SAMPLES:
|
|
130
|
+
raise WatchError(
|
|
131
|
+
f"{len(out)} samples over {duration_ms} ms exceeds {MAX_SAMPLES}; "
|
|
132
|
+
"raise the period or shorten --wait"
|
|
133
|
+
)
|
|
134
|
+
return sorted(out)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def watch_lines(specs: Sequence[WatchSpec], duration_ms: int) -> list[str]:
|
|
138
|
+
"""Commander lines for after `go`: `mem32` reads with `Sleep` between, padded so the
|
|
139
|
+
session lasts the whole `--wait` window. Addresses and counts are ints rendered `0x%X`."""
|
|
140
|
+
lines: list[str] = []
|
|
141
|
+
now = 0
|
|
142
|
+
for offset, i in schedule(specs, duration_ms):
|
|
143
|
+
if offset > now:
|
|
144
|
+
lines.append(f"Sleep {offset - now}")
|
|
145
|
+
now = offset
|
|
146
|
+
lines.append(f"mem32 0x{specs[i].address:X}, 0x{specs[i].words:X}")
|
|
147
|
+
if duration_ms > now:
|
|
148
|
+
lines.append(f"Sleep {duration_ms - now}")
|
|
149
|
+
return lines
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_MEM32_LINE = re.compile(r"^([0-9A-Fa-f]{8}) = ((?:[0-9A-Fa-f]{8}(?:\s+|$))+)$")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_ECHO = re.compile(r"^J-Link>\s*mem32\s+0[xX]([0-9A-Fa-f]+)\s*,\s*0[xX]([0-9A-Fa-f]+)\s*$")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _blocks(transcript: str) -> list[tuple[int, list[tuple[int, list[int]]]]] | None:
|
|
159
|
+
"""Each echoed `J-Link>mem32 0xADDR, 0xN` with the dump lines that follow it (up to the
|
|
160
|
+
next `J-Link>` echo). `None` when the transcript echoes no `mem32` at all."""
|
|
161
|
+
blocks: list[tuple[int, list[tuple[int, list[int]]]]] = []
|
|
162
|
+
for raw in transcript.splitlines():
|
|
163
|
+
line = raw.strip()
|
|
164
|
+
echo = _ECHO.match(line)
|
|
165
|
+
if echo:
|
|
166
|
+
blocks.append((int(echo[1], 16), []))
|
|
167
|
+
elif line.startswith("J-Link>"):
|
|
168
|
+
blocks.append((-1, [])) # another command: closes the open block
|
|
169
|
+
else:
|
|
170
|
+
m = _MEM32_LINE.match(line)
|
|
171
|
+
if m and blocks:
|
|
172
|
+
blocks[-1][1].append((int(m[1], 16), [int(w, 16) for w in m[2].split()]))
|
|
173
|
+
reads = [b for b in blocks if b[0] >= 0]
|
|
174
|
+
return reads if reads else None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _collect(address: int, words: int, dumps: list[tuple[int, list[int]]]) -> list[int] | None:
|
|
178
|
+
values: list[int] = []
|
|
179
|
+
for base, got in dumps:
|
|
180
|
+
if base != address + 4 * len(values):
|
|
181
|
+
break
|
|
182
|
+
values.extend(got)
|
|
183
|
+
return values if len(values) == words else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def parse_samples(transcript: str, specs: Sequence[WatchSpec], duration_ms: int) -> list[dict]:
|
|
187
|
+
"""The envelope's `data.watch[]`: one entry per scheduled sample, in order, each
|
|
188
|
+
`{address, words, index, elapsedMs, values}`; `values` is `None` when its dump is
|
|
189
|
+
missing. Strict: only `ADDR = WWWWWWWW ...` lines count. When J-Link echoes the
|
|
190
|
+
commands, each echoed `mem32` is paired with the dumps that follow IT, so a missing
|
|
191
|
+
dump is `None` at its own index and shifts nothing; a transcript with no echo falls
|
|
192
|
+
back to matching dump lines in order against each sample's expected address."""
|
|
193
|
+
sched = schedule(specs, duration_ms)
|
|
194
|
+
blocks = _blocks(transcript)
|
|
195
|
+
flat = []
|
|
196
|
+
for raw in transcript.splitlines():
|
|
197
|
+
m = _MEM32_LINE.match(raw.strip())
|
|
198
|
+
if m:
|
|
199
|
+
flat.append((int(m[1], 16), [int(w, 16) for w in m[2].split()]))
|
|
200
|
+
out: list[dict] = []
|
|
201
|
+
cursor = 0
|
|
202
|
+
counts: dict[int, int] = {}
|
|
203
|
+
for n, (offset, i) in enumerate(sched):
|
|
204
|
+
spec = specs[i]
|
|
205
|
+
if blocks is not None:
|
|
206
|
+
got = None
|
|
207
|
+
if n < len(blocks) and blocks[n][0] == spec.address:
|
|
208
|
+
got = _collect(spec.address, spec.words, blocks[n][1])
|
|
209
|
+
else:
|
|
210
|
+
got = None
|
|
211
|
+
pos, values = cursor, []
|
|
212
|
+
while len(values) < spec.words and pos < len(flat):
|
|
213
|
+
base, words = flat[pos]
|
|
214
|
+
if base != spec.address + 4 * len(values):
|
|
215
|
+
break
|
|
216
|
+
values.extend(words)
|
|
217
|
+
pos += 1
|
|
218
|
+
if len(values) == spec.words:
|
|
219
|
+
got, cursor = values, pos
|
|
220
|
+
idx = counts.get(i, 0)
|
|
221
|
+
counts[i] = idx + 1
|
|
222
|
+
out.append(
|
|
223
|
+
{
|
|
224
|
+
"address": f"0x{spec.address:08X}",
|
|
225
|
+
"words": spec.words,
|
|
226
|
+
"index": idx,
|
|
227
|
+
"elapsedMs": offset,
|
|
228
|
+
"values": [f"0x{v:08X}" for v in got] if got is not None else None,
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
return out
|
tan/core/raw_write.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure facts for `tan flash --raw <file>@<addr>` (tan-cli#1446): a byte-exact,
|
|
3
|
+
sector-granular MRAM write for bench backup/restore.
|
|
4
|
+
|
|
5
|
+
Flow D writes a built image plus a signed ATOC. A bench that backed MRAM up with
|
|
6
|
+
`savebin` needs the saved sectors put back EXACTLY, and nothing else: no signing, no
|
|
7
|
+
address derivation, no reset. Everything here is IO-free; the J-Link session lives in
|
|
8
|
+
`tan.commands.flash_raw`.
|
|
9
|
+
|
|
10
|
+
**tan never invents an address.** Each `--raw` names its own address on the command
|
|
11
|
+
line; nothing is read from an `app-package-map.txt`, a manifest or a file name. That is
|
|
12
|
+
the whole ATOC/STOC rule: the Secure Enclave table lives in the last MRAM sectors, and a
|
|
13
|
+
write there is only ever one the user asked for by address.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from tan.core.flash_plan import commander_path, validate_commander_path
|
|
21
|
+
from tan.core.flow_d_report import SECTOR_BYTES, hex_addr, sector_span
|
|
22
|
+
|
|
23
|
+
CODE_INVALID = "flash.raw-invalid"
|
|
24
|
+
CODE_RESERVATION = "flash.raw-reservation-required"
|
|
25
|
+
CODE_FAILED = "flash.raw-failed"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RawError(ValueError):
|
|
29
|
+
"""A `--raw` input tan refuses before any spawn. `code` is the registered issue code."""
|
|
30
|
+
|
|
31
|
+
code = CODE_INVALID
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RawSpec:
|
|
36
|
+
path: str
|
|
37
|
+
address: int
|
|
38
|
+
size: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_raw(spec: str) -> RawSpec:
|
|
42
|
+
"""`<file>@<addr>` -> `RawSpec`. The LAST `@` splits, so a path may contain one; the
|
|
43
|
+
address must be an explicit hex literal (`0x80010000`)."""
|
|
44
|
+
path, sep, addr = spec.rpartition("@")
|
|
45
|
+
if not sep or not path or not addr:
|
|
46
|
+
raise RawError(f"--raw {spec!r} is not <file>@<address> (e.g. he_slot0.bin@0x80010000)")
|
|
47
|
+
if not addr.lower().startswith("0x"):
|
|
48
|
+
raise RawError(f"--raw {spec!r}: the address must be an explicit hex literal like 0x80010000")
|
|
49
|
+
try:
|
|
50
|
+
value = int(addr, 16)
|
|
51
|
+
except ValueError:
|
|
52
|
+
raise RawError(f"--raw {spec!r}: {addr!r} is not a hex address") from None
|
|
53
|
+
validate_commander_path(path, "the --raw file path")
|
|
54
|
+
return RawSpec(path=path, address=value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_ranges(
|
|
58
|
+
specs: Sequence[RawSpec], mram_bytes: int, base: int
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Refuse every unaligned, empty, out-of-MRAM or overlapping range. The loader rewrites
|
|
61
|
+
whole 16 KiB sectors and fills a short tail with 0xFF, so a blob whose address or size
|
|
62
|
+
is not sector-aligned would NOT be a byte-exact restore of what it names."""
|
|
63
|
+
if not specs:
|
|
64
|
+
raise RawError("--raw needs at least one <file>@<address>")
|
|
65
|
+
end_of_mram = base + mram_bytes
|
|
66
|
+
problems: list[str] = []
|
|
67
|
+
for s in specs:
|
|
68
|
+
where = f"{s.path}@{hex_addr(s.address)} ({s.size} B)"
|
|
69
|
+
if s.size <= 0:
|
|
70
|
+
problems.append(f"{where}: the file is empty")
|
|
71
|
+
continue
|
|
72
|
+
if s.address % SECTOR_BYTES:
|
|
73
|
+
problems.append(f"{where}: the address is not {SECTOR_BYTES // 1024} KiB sector-aligned")
|
|
74
|
+
if s.size % SECTOR_BYTES:
|
|
75
|
+
problems.append(
|
|
76
|
+
f"{where}: the size is not a whole number of {SECTOR_BYTES // 1024} KiB sectors -- "
|
|
77
|
+
"the loader would fill the rest of the last sector with 0xFF, so this is not "
|
|
78
|
+
"a byte-exact write; pad the saved blob to the sector boundary yourself"
|
|
79
|
+
)
|
|
80
|
+
if s.address < base or s.address + s.size > end_of_mram:
|
|
81
|
+
problems.append(
|
|
82
|
+
f"{where}: outside MRAM [{hex_addr(base)}, {hex_addr(end_of_mram)})"
|
|
83
|
+
)
|
|
84
|
+
ordered = sorted(specs, key=lambda s: s.address)
|
|
85
|
+
for a, b in zip(ordered, ordered[1:]):
|
|
86
|
+
if a.address + a.size > b.address:
|
|
87
|
+
problems.append(
|
|
88
|
+
f"{a.path}@{hex_addr(a.address)} and {b.path}@{hex_addr(b.address)} overlap"
|
|
89
|
+
)
|
|
90
|
+
if problems:
|
|
91
|
+
raise RawError("; ".join(problems))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def raw_script(preamble: str, specs: Sequence[RawSpec]) -> str:
|
|
95
|
+
"""The write session: `preamble` (everything up to and including `connect`), a
|
|
96
|
+
`loadbin` then `verifybin` per blob, `exit`. NO reset and NO `go`: the write must not
|
|
97
|
+
boot, halt-resume or reset anything."""
|
|
98
|
+
lines = [preamble.rstrip("\n")]
|
|
99
|
+
for s in specs:
|
|
100
|
+
lines.append(f"loadbin {commander_path(s.path)} {hex_addr(s.address)}")
|
|
101
|
+
for s in specs:
|
|
102
|
+
lines.append(f"verifybin {commander_path(s.path)} {hex_addr(s.address)}")
|
|
103
|
+
lines.append("exit")
|
|
104
|
+
return "\n".join(lines) + "\n"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def planned(specs: Sequence[RawSpec], sha256s: Sequence[str]) -> list[dict]:
|
|
108
|
+
"""Each blob as the envelope reports it: path, address, size, sha256, sector span."""
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
"path": s.path, "address": hex_addr(s.address), "size": s.size, "sha256": digest,
|
|
112
|
+
"sectorSpan": sector_span(s.address, s.size),
|
|
113
|
+
}
|
|
114
|
+
for s, digest in zip(specs, sha256s)
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def lease_holder(show_text: str) -> str | None:
|
|
119
|
+
"""The `acquired:` holder (`host/user`) in `labgrid-client -p <place> show` output,
|
|
120
|
+
strictly: exactly one distinct, non-empty value across the ` acquired:` lines, else
|
|
121
|
+
`None` (ambiguity is never a holder)."""
|
|
122
|
+
holders = {
|
|
123
|
+
line.split(":", 1)[1].strip()
|
|
124
|
+
for line in show_text.splitlines()
|
|
125
|
+
if line.startswith(" acquired:")
|
|
126
|
+
}
|
|
127
|
+
return holders.pop() if len(holders) == 1 and next(iter(holders), "x") else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def lease_swd_path(show_text: str) -> str | None:
|
|
131
|
+
"""The USB path of the `swd` resource in `labgrid-client -p <place> show` output (the
|
|
132
|
+
`'path': '3-4.2'` entry of the `Acquired|Matching resource 'swd'` block), or `None`.
|
|
133
|
+
The `matches:` list at the top names the same resource without a path, so only the
|
|
134
|
+
resource block counts."""
|
|
135
|
+
import re
|
|
136
|
+
|
|
137
|
+
inside = False
|
|
138
|
+
for line in show_text.splitlines():
|
|
139
|
+
if re.match(r"^(Acquired|Matching) resource 'swd'", line):
|
|
140
|
+
inside = True
|
|
141
|
+
continue
|
|
142
|
+
if inside:
|
|
143
|
+
if line and not line[0].isspace():
|
|
144
|
+
return None
|
|
145
|
+
found = re.search(r"'path': '([^']+)'", line)
|
|
146
|
+
if found:
|
|
147
|
+
return found.group(1)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def lease_changed(show_text: str) -> str | None:
|
|
152
|
+
"""The place's `changed:` timestamp in `labgrid-client show` output (labgrid 26.0 updates it
|
|
153
|
+
on every acquire/release), strictly: exactly one distinct, non-empty value, else `None`.
|
|
154
|
+
The lease file records it at acquire time so a lease cannot outlive its acquisition."""
|
|
155
|
+
values = {
|
|
156
|
+
line.split(":", 1)[1].strip()
|
|
157
|
+
for line in show_text.splitlines()
|
|
158
|
+
if line.startswith(" changed:")
|
|
159
|
+
}
|
|
160
|
+
return next(iter(values)) if len(values) == 1 and next(iter(values)) else None
|
tan/core/reset_plan.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""tan-cli#1452: the pure half of `tan reset` -- the one-pulse nRESET script.
|
|
3
|
+
|
|
4
|
+
`tan reset` is ONE clean pin pulse through J-Link Commander: `r0` (drive nRESET
|
|
5
|
+
low), a `sleep` of the pulse width, `r1` (release), `q`. It deliberately has none of
|
|
6
|
+
the machinery `tan flash`'s pin reset carries: no `connect`, no halt, no `RSetType`, no
|
|
7
|
+
connect-under-reset, no `g`, no retry. Each of those either wakes a target out
|
|
8
|
+
of STOP, loses the VBAT/BKRAM evidence of a non-waking STOP, or both.
|
|
9
|
+
`FORBIDDEN_VERBS` is what the tests hold the generated script to.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
DEFAULT_PULSE_MS = 100
|
|
16
|
+
MIN_PULSE_MS = 1
|
|
17
|
+
MAX_PULSE_MS = 10000
|
|
18
|
+
#: Commander has to open the probe, never attach to the target: a `connect`
|
|
19
|
+
#: against an Alif target in STOP fails (debug domain gated) or disturbs the
|
|
20
|
+
#: evidence this verb exists to preserve. Bench-proven script: r0 / sleep 100 / r1 / q.
|
|
21
|
+
_SAFE_TOKEN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
22
|
+
|
|
23
|
+
#: Commander verbs a reset script must never contain.
|
|
24
|
+
FORBIDDEN_VERBS = (
|
|
25
|
+
"connect", "h", "halt", "g", "go", "rsettype", "resettype", "reset", "r", "rx", "w1", "w2", "w4",
|
|
26
|
+
"loadbin", "loadfile", "erase", "unlock", "setpc", "step", "s",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResetArgError(ValueError):
|
|
31
|
+
"""A bad `--pulse-ms`."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_pulse_ms(pulse_ms: int) -> int:
|
|
35
|
+
if isinstance(pulse_ms, bool) or not isinstance(pulse_ms, int):
|
|
36
|
+
raise ResetArgError(f"--pulse-ms {pulse_ms!r} is not an integer")
|
|
37
|
+
if not MIN_PULSE_MS <= pulse_ms <= MAX_PULSE_MS:
|
|
38
|
+
raise ResetArgError(
|
|
39
|
+
f"--pulse-ms {pulse_ms} is outside {MIN_PULSE_MS}..{MAX_PULSE_MS} milliseconds"
|
|
40
|
+
)
|
|
41
|
+
return pulse_ms
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pulse_script(serial: str | None, pulse_ms: int) -> str:
|
|
45
|
+
"""`SelectEmuBySN <serial>` (only when a probe is selected), then
|
|
46
|
+
`r0`, `sleep <ms>`, `r1`, `q`. No `connect`."""
|
|
47
|
+
check_pulse_ms(pulse_ms)
|
|
48
|
+
if serial is not None and not _SAFE_TOKEN.match(serial):
|
|
49
|
+
raise ResetArgError(f"the J-Link serial {serial!r} is not a plain token")
|
|
50
|
+
pre = [f"SelectEmuBySN {serial}"] if serial else []
|
|
51
|
+
return "\n".join([*pre, "r0", f"sleep {pulse_ms}", "r1", "q"]) + "\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def script_verbs(script: str) -> list[str]:
|
|
55
|
+
"""The first token of every non-empty line, lower-cased."""
|
|
56
|
+
return [ln.split()[0].lower() for ln in script.splitlines() if ln.split()]
|
tan/core/run.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure post-build decision logic for `tan run` -- port of
|
|
3
|
+
`crates/tan-core/src/run.rs`. No IO: the subprocess spawn and the filesystem
|
|
4
|
+
probe live in `tan.commands.run_cmd`.
|
|
5
|
+
|
|
6
|
+
`tan run` is a THIN ORCHESTRATOR over `tan build`'s engine and (on a hardware
|
|
7
|
+
target, with `--flash`) `tan flash`'s engine -- it never re-derives either.
|
|
8
|
+
The only thing this module owns is the decision of WHICH of those two to
|
|
9
|
+
reach, from signals the build step that just ran produced: whether the build
|
|
10
|
+
succeeded, whether it could establish a host (`native_sim`) vs. hardware
|
|
11
|
+
target, whether `--flash` was requested, and whether the post-build
|
|
12
|
+
`system-manifest.yaml` write is confirmed to have succeeded. Flashing real
|
|
13
|
+
silicon is the dangerous path, so it is never the default action -- a
|
|
14
|
+
hardware target flashes only on an explicit `--flash`.
|
|
15
|
+
|
|
16
|
+
**`native_sim_target`/`manifest_written` must come from the SAME build run
|
|
17
|
+
`tan run` just performed, in memory -- never from re-reading
|
|
18
|
+
`system-manifest.yaml` off disk afterward.** The Rust oracle's own module doc
|
|
19
|
+
records why (three attempts, the last two of which re-read a stale on-disk
|
|
20
|
+
manifest and let a `--flash` slip through against the WRONG target). `tan build` writes the post-build `system-manifest.yaml` and
|
|
21
|
+
`execute.last_manifest_write()` supplies the real signal, so
|
|
22
|
+
`tan.commands.run_cmd` passes the genuine `native_sim_target` / `manifest_written`
|
|
23
|
+
pair and the FLASH and EXECUTE_NATIVE arms are reachable. `native_sim_target=None`
|
|
24
|
+
still means "could not be established", never "not native_sim".
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from enum import StrEnum
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
#: The Zephyr board target of a `native_sim` slice -- the robust
|
|
32
|
+
#: discriminator for the host build (an actual `board` value from the
|
|
33
|
+
#: manifest, not a build-dir-name guess). Verbatim from `tan_core::run`.
|
|
34
|
+
NATIVE_SIM_BOARD = "native_sim"
|
|
35
|
+
|
|
36
|
+
#: The literal binary Zephyr's `native_sim` target produces -- a fixed name on
|
|
37
|
+
#: every host OS. It sits beside `zephyr.elf` in the build's `zephyr/` output
|
|
38
|
+
#: dir.
|
|
39
|
+
#:
|
|
40
|
+
#: Module-private (the oracle's equivalent is `pub(crate)`, run.rs:166-169 --
|
|
41
|
+
#: the #83 fix): [`native_sim_exe_beside`] is the ONLY sanctioned way to spell
|
|
42
|
+
#: the swap. A third caller importing this constant to hand-roll
|
|
43
|
+
#: `parent().join(NATIVE_SIM_EXE)` again would silently reopen the door #83
|
|
44
|
+
#: closed -- see that function's docstring.
|
|
45
|
+
_NATIVE_SIM_EXE = "zephyr.exe"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RunAction(StrEnum):
|
|
49
|
+
"""What `tan run` does after its build step -- the pure decision,
|
|
50
|
+
decoupled from the subprocess/filesystem work so it is unit-testable
|
|
51
|
+
without a real build."""
|
|
52
|
+
|
|
53
|
+
#: The build failed -- short-circuit: never run or flash, report the build.
|
|
54
|
+
BUILD_FAILED = "build_failed"
|
|
55
|
+
#: A host target -- execute the produced `native_sim` binary.
|
|
56
|
+
EXECUTE_NATIVE = "execute_native"
|
|
57
|
+
#: A hardware target with an explicit `--flash`, and THIS run's manifest
|
|
58
|
+
#: write is confirmed to have succeeded -- flash the built image.
|
|
59
|
+
FLASH = "flash"
|
|
60
|
+
#: A hardware target WITHOUT `--flash` -- build succeeded, but programming
|
|
61
|
+
#: the board needs explicit consent: report + stop, leave hardware
|
|
62
|
+
#: untouched.
|
|
63
|
+
BUILD_ONLY = "build_only"
|
|
64
|
+
#: `--flash` was requested, but this run's OWN build outcome does not
|
|
65
|
+
#: confirm it is safe to flash: either the target could not be
|
|
66
|
+
#: established at all (`native_sim_target is None`), or it is hardware
|
|
67
|
+
#: but THIS run's manifest file write failed (`manifest_written` is
|
|
68
|
+
#: `False`). Refuse rather than flash from a build that can't vouch for
|
|
69
|
+
#: itself. Board untouched, same as `BUILD_ONLY`, but reported as an
|
|
70
|
+
#: error: the caller explicitly asked to flash and that request could not
|
|
71
|
+
#: be honored safely.
|
|
72
|
+
MANIFEST_STALE = "manifest_stale"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def decide_run_action(
|
|
76
|
+
build_ok: bool,
|
|
77
|
+
native_sim_target: bool | None,
|
|
78
|
+
flash_requested: bool,
|
|
79
|
+
manifest_written: bool,
|
|
80
|
+
) -> RunAction:
|
|
81
|
+
"""Decide what `tan run` does once the build step finished -- entirely
|
|
82
|
+
from signals THIS build produced, never from a post-hoc disk read (see
|
|
83
|
+
the module doc for why that used to be the actual defect).
|
|
84
|
+
|
|
85
|
+
- `native_sim_target` is `True`/`False` when this run's build established
|
|
86
|
+
a target (from the SDK's freshly-emitted system-manifest projection,
|
|
87
|
+
independent of whether the on-disk file write then succeeded), or
|
|
88
|
+
`None` when it could not (the emit itself failed/didn't parse) -- an
|
|
89
|
+
unconfirmed target, not "not native_sim".
|
|
90
|
+
- `manifest_written` is whether THIS run's `system-manifest.yaml` file
|
|
91
|
+
write actually succeeded -- the "write definitely succeeded" signal
|
|
92
|
+
that gates `--flash` for a confirmed hardware target (a failed write
|
|
93
|
+
means `flash` would next read a stale/wrong file).
|
|
94
|
+
|
|
95
|
+
Rules (verbatim from `tan_core::run::decide_run_action`):
|
|
96
|
+
- build failed -> BUILD_FAILED
|
|
97
|
+
- host target (`True`) -> EXECUTE_NATIVE
|
|
98
|
+
(never FLASH: native_sim is not flashable, regardless of `--flash`/
|
|
99
|
+
`manifest_written`)
|
|
100
|
+
- hardware (`False`) + `--flash` + write ok -> FLASH
|
|
101
|
+
- hardware (`False`) + `--flash`, write failed -> MANIFEST_STALE
|
|
102
|
+
- hardware (`False`), no `--flash` -> BUILD_ONLY
|
|
103
|
+
- unknown (`None`) + `--flash` -> MANIFEST_STALE (refuse)
|
|
104
|
+
- unknown (`None`), no `--flash` -> BUILD_ONLY
|
|
105
|
+
"""
|
|
106
|
+
if not build_ok:
|
|
107
|
+
return RunAction.BUILD_FAILED
|
|
108
|
+
if native_sim_target is True:
|
|
109
|
+
# A host target is never flashable -- settled by THIS run's own build
|
|
110
|
+
# outcome, so a failed/absent manifest FILE write can't flip a host
|
|
111
|
+
# build into FLASH.
|
|
112
|
+
return RunAction.EXECUTE_NATIVE
|
|
113
|
+
if native_sim_target is False:
|
|
114
|
+
if not flash_requested:
|
|
115
|
+
return RunAction.BUILD_ONLY
|
|
116
|
+
return RunAction.FLASH if manifest_written else RunAction.MANIFEST_STALE
|
|
117
|
+
# native_sim_target is None: this run's build could not confirm host vs.
|
|
118
|
+
# hardware at all. Flashing blind is never acceptable; a bare run still
|
|
119
|
+
# gets to report + stop like any other hardware-shaped build.
|
|
120
|
+
return RunAction.MANIFEST_STALE if flash_requested else RunAction.BUILD_ONLY
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_native_sim_board(board: Any) -> bool:
|
|
124
|
+
"""True for the bare `native_sim` board AND Zephyr's qualified board form
|
|
125
|
+
(`native_sim/native/64`, SoC/variant-qualified). Exact-matching only the
|
|
126
|
+
bare name let a qualified board name defeat the host-vs-hardware
|
|
127
|
+
discriminator on a perfectly fresh manifest. `"native_simulated_foo"`
|
|
128
|
+
deliberately does NOT match -- the required `/` after the prefix anchors
|
|
129
|
+
it to Zephyr's actual board-qualifier syntax instead of an arbitrary
|
|
130
|
+
substring."""
|
|
131
|
+
return isinstance(board, str) and (
|
|
132
|
+
board == NATIVE_SIM_BOARD or board.startswith(f"{NATIVE_SIM_BOARD}/")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def native_sim_slice(manifest: Any) -> dict[str, Any] | None:
|
|
137
|
+
"""The `native_sim` slice in a post-build manifest, if any -- identified
|
|
138
|
+
by its Zephyr board target ([`is_native_sim_board`]). `manifest` is a
|
|
139
|
+
`tan.core.system_manifest.SystemManifest`; its `slices` are raw dicts
|
|
140
|
+
(the manifest reader's own lossless-round-trip convention). Pure: no IO.
|
|
141
|
+
"""
|
|
142
|
+
return next(
|
|
143
|
+
(s for s in manifest.slices if is_native_sim_board(s.get("board"))), None
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def native_sim_exe_beside(elf: str) -> str:
|
|
148
|
+
"""Swap a native_sim slice's `output_artefact` for the runnable
|
|
149
|
+
[`_NATIVE_SIM_EXE`] that sits beside it. A manifest NEVER records the
|
|
150
|
+
`.exe`: `output_artefact` always names `zephyr.elf`, for every zephyr
|
|
151
|
+
slice including native_sim, so every consumer that wants the host
|
|
152
|
+
runnable has to make this swap -- and must all make the SAME one. This is
|
|
153
|
+
THE one spelling of the rule: `tan.commands.debug_config_cmd` calls this
|
|
154
|
+
function rather than carrying its own copy (the #83 fix this module's
|
|
155
|
+
`_NATIVE_SIM_EXE` docstring records).
|
|
156
|
+
|
|
157
|
+
Splits on the separator the path itself uses rather than normalising via
|
|
158
|
+
a path library, which on Windows would rejoin with `\\` and turn a
|
|
159
|
+
`/`-authored manifest path into a mixed `a/b\\zephyr.exe`."""
|
|
160
|
+
cut = max(elf.rfind("/"), elf.rfind("\\"))
|
|
161
|
+
return _NATIVE_SIM_EXE if cut < 0 else elf[: cut + 1] + _NATIVE_SIM_EXE
|