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.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,134 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Is this path trustworthy as an interlock binary? (tan-cli#1452)
3
+
4
+ The same rule PR #1454 (`flash_raw.py`, tan-cli#1457) applies to the
5
+ reservation-enforcing J-Link wrapper: an ABSOLUTE path that resolves (symlinks
6
+ followed) to an executable regular file outside the cwd, where BOTH chains --
7
+ the path as written and the resolved target, each with its parents -- are owned
8
+ by root or the current user and are not writable by others or by a group anyone
9
+ else belongs to (a sticky directory is allowed).
10
+
11
+ `tan reset` additionally requires the per-session lease nonce under
12
+ `JLINK_RUN_PLACE` (tan-cli#1457): `reset_cmd` calls
13
+ `flash_raw._reservation_refusal`, the same gate `tan flash --raw` uses. This module
14
+ only judges the wrapper path; the two path checks are still separate copies.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import stat
20
+
21
+ WRAPPER_ENV = "TAN_JLINK_WRAPPER"
22
+
23
+
24
+ def _current_user() -> str:
25
+ """The account name of the real uid (never `USER`, which the caller controls)."""
26
+ import pwd # noqa: PLC0415 (POSIX only: this module must import on Windows)
27
+
28
+ return pwd.getpwuid(os.getuid()).pw_name
29
+
30
+
31
+ def _private_group(gid: int) -> bool:
32
+ """Whether `gid` is a group only the current user belongs to."""
33
+ import grp # noqa: PLC0415 (POSIX only)
34
+ import pwd # noqa: PLC0415
35
+
36
+ me = _current_user()
37
+ try:
38
+ members = set(grp.getgrgid(gid).gr_mem)
39
+ except KeyError:
40
+ return False
41
+ return members <= {me} and all(u.pw_name == me for u in pwd.getpwall() if u.pw_gid == gid)
42
+
43
+
44
+ def _node_problem(node: str, st: os.stat_result) -> str | None:
45
+ mode = st.st_mode
46
+ sticky_dir = stat.S_ISDIR(mode) and bool(mode & stat.S_ISVTX)
47
+ if st.st_uid not in (0, os.getuid()):
48
+ return f"{node} is owned by uid {st.st_uid}, not root or you"
49
+ if stat.S_ISLNK(mode):
50
+ return None # a symlink's own mode bits are always 0777 and mean nothing
51
+ if mode & stat.S_IWOTH and not sticky_dir:
52
+ return f"{node} is world-writable"
53
+ if mode & stat.S_IWGRP and not sticky_dir and not (
54
+ st.st_gid == os.getgid() and _private_group(st.st_gid)
55
+ ):
56
+ return f"{node} is group-writable by a group others belong to"
57
+ return None
58
+
59
+
60
+ def _chain_problem(start: str) -> str | None:
61
+ node = start
62
+ while True:
63
+ try:
64
+ st = os.lstat(node)
65
+ except OSError:
66
+ return f"{node} cannot be inspected"
67
+ problem = _node_problem(node, st)
68
+ if problem:
69
+ return problem
70
+ parent = os.path.dirname(node)
71
+ if parent == node:
72
+ return None
73
+ node = parent
74
+
75
+
76
+ def unsafe(path: str) -> str | None:
77
+ """Why `path` cannot be trusted as an interlock binary, or `None`."""
78
+ if not os.path.isabs(path):
79
+ return "is not an absolute path"
80
+ real = os.path.realpath(path)
81
+ cwd = os.path.realpath(os.getcwd())
82
+ if real == cwd or real.startswith(cwd + os.sep):
83
+ return "lives under the current directory"
84
+ if not (os.path.isfile(real) and os.access(real, os.X_OK)):
85
+ return "is not an executable file"
86
+ return _chain_problem(os.path.abspath(path)) or _chain_problem(real)
87
+
88
+
89
+ #: Short reason codes (no paths): the operator's own `TAN_JLINK_WRAPPER` is already known.
90
+ REASON_ENV_UNSET = "wrapper-env-unset"
91
+ REASON_PATH_UNSAFE = "wrapper-path-unsafe"
92
+ REASON_NOT_RESOLVED = "wrapper-not-resolved-binary"
93
+ REASON_UNSUPPORTED = "unsupported-platform"
94
+
95
+
96
+ def _supported() -> bool:
97
+ if not hasattr(os, "getuid"):
98
+ return False
99
+ try:
100
+ import grp # noqa: F401, PLC0415
101
+ import pwd # noqa: F401, PLC0415
102
+ except ImportError:
103
+ return False
104
+ return True
105
+
106
+
107
+ def configured_wrapper() -> tuple[str | None, str | None]:
108
+ """`(path, None)` when `TAN_JLINK_WRAPPER` names a safe wrapper, else `(None, reason code)`."""
109
+ if not _supported():
110
+ return None, REASON_UNSUPPORTED
111
+ configured = os.environ.get(WRAPPER_ENV, "").strip()
112
+ if not configured:
113
+ return None, REASON_ENV_UNSET
114
+ if unsafe(configured) is not None:
115
+ return None, REASON_PATH_UNSAFE
116
+ return configured, None
117
+
118
+
119
+ def is_configured_wrapper(exe: str | None) -> tuple[bool, str | None]:
120
+ """`(True, None)` only when `exe` IS the wrapper named by `TAN_JLINK_WRAPPER`
121
+ (absolute, trustworthy, same realpath). Otherwise `(False, reason code)`: a
122
+ marker string, a PATH name or a look-alike proves nothing. The trust is
123
+ operator-asserted: the variable attests a PATH, not that the file really masks
124
+ the other probes."""
125
+ if not _supported():
126
+ return False, REASON_UNSUPPORTED
127
+ configured = os.environ.get(WRAPPER_ENV, "").strip()
128
+ if not configured:
129
+ return False, REASON_ENV_UNSET
130
+ if unsafe(configured) is not None:
131
+ return False, REASON_PATH_UNSAFE
132
+ if not exe or not os.path.isabs(exe) or os.path.realpath(exe) != os.path.realpath(configured):
133
+ return False, REASON_NOT_RESOLVED
134
+ return True, None
@@ -0,0 +1,154 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """U-Boot autoboot break-in: pure matching/timing logic for `tan monitor
3
+ --break-uboot` (tan-cli#1315).
4
+
5
+ The bench recipe is "power-cycle the board, then spam a key on the console
6
+ until the `=> ` prompt appears". This module is that loop and nothing else: it
7
+ takes an already-open port-like object (anything with `write(bytes)` and
8
+ `read(size) -> bytes`; a pyserial `Serial`, a `loop://` port, an `rfc2217://`
9
+ or `socket://` URL port, or a test fake), keeps sending the interrupt key,
10
+ watches the received stream for the prompt, and stops the moment it is seen
11
+ or the deadline passes. It never opens a port and never controls power --
12
+ the operator or labgrid does the power-cycle.
13
+
14
+ The clock is injected so the timing logic is testable without sleeping.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import time
20
+ from collections.abc import Callable
21
+ from dataclasses import dataclass
22
+ from typing import Protocol
23
+
24
+ #: pyserial `Serial` reads below this many seconds keep the loop responsive
25
+ #: and also pace the key presses (one write per read window).
26
+ DEFAULT_INTERVAL_S = 0.1
27
+ DEFAULT_KEY = b" "
28
+ DEFAULT_PROMPT = b"=> "
29
+ DEFAULT_TIMEOUT_S = 30.0
30
+
31
+ #: How much received text the envelope carries (`bytesSeenTail`), and how
32
+ #: much is kept for prompt matching (a prompt cannot be longer than this).
33
+ TAIL_BYTES = 256
34
+ _MATCH_WINDOW = 4096
35
+
36
+
37
+ class BytePort(Protocol):
38
+ def write(self, data: bytes, /) -> object: ...
39
+ def read(self, size: int = 1, /) -> bytes: ...
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class BreakInResult:
44
+ caught: bool
45
+ elapsed_s: float
46
+ bytes_seen: int
47
+ tail: str
48
+ keys_sent: int = 0
49
+ #: Seconds from the first key until the first byte came back (None if none did).
50
+ first_byte_s: float | None = None
51
+ #: The last (up to 4 KiB of) raw bytes received, so a caller can carry them
52
+ #: into the next phase (`--capture` after `--break-uboot`).
53
+ window: bytes = b""
54
+
55
+
56
+ _SIMPLE_ESCAPES = {"r": b"\r", "n": b"\n", "t": b"\t", "\\": b"\\"}
57
+ _HEX = "0123456789abcdefABCDEF"
58
+
59
+
60
+ def parse_escaped(text: str, what: str) -> bytes:
61
+ """Turn a CLI string into bytes. Supported escapes: `\\xNN` (exactly two
62
+ hex digits), `\\r`, `\\n`, `\\t`, `\\\\`. Any other backslash sequence is
63
+ rejected rather than guessed at; other characters are taken literally
64
+ (UTF-8). Raises `ValueError` naming `what` on a bad escape or empty result.
65
+ """
66
+ out = bytearray()
67
+ i = 0
68
+ while i < len(text):
69
+ ch = text[i]
70
+ if ch != "\\":
71
+ out += ch.encode("utf-8")
72
+ i += 1
73
+ continue
74
+ nxt = text[i + 1 : i + 2]
75
+ if nxt in _SIMPLE_ESCAPES:
76
+ out += _SIMPLE_ESCAPES[nxt]
77
+ i += 2
78
+ elif nxt == "x" and len(text[i + 2 : i + 4]) == 2 and all(c in _HEX for c in text[i + 2 : i + 4]):
79
+ out.append(int(text[i + 2 : i + 4], 16))
80
+ i += 4
81
+ else:
82
+ raise ValueError(
83
+ f"{what} {text!r}: unsupported escape at position {i} "
84
+ "(allowed: \\xNN, \\r, \\n, \\t, \\\\)"
85
+ )
86
+ if not out:
87
+ raise ValueError(f"{what} must not be empty")
88
+ return bytes(out)
89
+
90
+
91
+ def _tail_text(buf: bytes) -> str:
92
+ if len(buf) > TAIL_BYTES:
93
+ buf = buf[-TAIL_BYTES:]
94
+ # Do not start mid-way through a multibyte UTF-8 character.
95
+ while buf and 0x80 <= buf[0] < 0xC0:
96
+ buf = buf[1:]
97
+ return buf.decode("utf-8", errors="replace")
98
+
99
+
100
+ def break_into_uboot(
101
+ port: BytePort,
102
+ *,
103
+ key: bytes = DEFAULT_KEY,
104
+ prompt: bytes = DEFAULT_PROMPT,
105
+ timeout_s: float = DEFAULT_TIMEOUT_S,
106
+ interval_s: float = DEFAULT_INTERVAL_S,
107
+ clock: Callable[[], float] = time.monotonic,
108
+ ) -> BreakInResult:
109
+ """Send `key` repeatedly until `prompt` is received or `timeout_s` passes.
110
+
111
+ One iteration = drain what is already waiting (return if the prompt is in
112
+ it), send a key if `interval_s` has passed since the last one, then one
113
+ `read` (the port's own read timeout, set by the caller to about
114
+ `interval_s`, paces the loop). The deadline is checked after the read, so
115
+ at least one key is always sent. At most the one key already in flight
116
+ when the prompt arrives is sent; none after.
117
+ """
118
+ start = clock()
119
+ deadline = start + timeout_s
120
+ window = b""
121
+ seen = 0
122
+ keys = 0
123
+ first_byte = None
124
+ last_write = None
125
+
126
+ def absorb(chunk: bytes) -> bool:
127
+ nonlocal window, seen, first_byte
128
+ if first_byte is None:
129
+ first_byte = clock() - start
130
+ seen += len(chunk)
131
+ window = (window + chunk)[-_MATCH_WINDOW:]
132
+ return prompt in window
133
+
134
+ def result(caught: bool) -> BreakInResult:
135
+ return BreakInResult(
136
+ caught, clock() - start, seen, _tail_text(window), keys, first_byte, window
137
+ )
138
+
139
+ while True:
140
+ # Drain everything already received BEFORE deciding to send another
141
+ # key, so no key goes out once the prompt is already in hand.
142
+ waiting = int(getattr(port, "in_waiting", 0) or 0)
143
+ if waiting and absorb(port.read(waiting) or b""):
144
+ return result(True)
145
+ now = clock()
146
+ if last_write is None or now - last_write >= interval_s:
147
+ port.write(key)
148
+ keys += 1
149
+ last_write = now
150
+ chunk = port.read(max(1, int(getattr(port, "in_waiting", 0) or 0))) or b""
151
+ if chunk and absorb(chunk):
152
+ return result(True)
153
+ if clock() >= deadline:
154
+ return result(False)