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
tan/core/consent.py ADDED
@@ -0,0 +1,104 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The one implementation of `GlobalArgs::can_prompt()` — the gate every
3
+ command must pass before it prompts a human or mutates the host.
4
+
5
+ Ported from the Rust oracle's `GlobalArgs::can_prompt()`, whose own
6
+ `--non-interactive` help text states the rule the port must honour verbatim:
7
+
8
+ Never prompt. A command with a documented default takes it (`tan init`
9
+ scaffolds `zephyr-app` into `.`); one without fails instead of asking
10
+ (`tan scaffold` needs `--name`). **The same rule applies unasked when
11
+ stdin or stderr is not a terminal — piped, redirected, or a CI runner.**
12
+
13
+ That last sentence is the half a re-derivation keeps dropping, and dropping it
14
+ is not cosmetic. Before this module existed the check was written out by hand
15
+ in four places and **one of them was wrong**: `doctor --fix` (tan-cli#91)
16
+ tested only `not non_interactive and not ci and not json_mode` and omitted both
17
+ `isatty()` calls, so a CI runner that redirected its output but did not happen
18
+ to pass `--ci` got **unattended host mutation** — demonstrated live with fully
19
+ captured pipes, where `tan doctor --fix` spawned four real `winget install`
20
+ runs (`Git.Git`, `Kitware.CMake`, `Python.Python.3.12`, `Ninja-build.Ninja`)
21
+ with nobody watching. A redirected stdio stream is the single most common shape
22
+ of an automated run, so the omitted condition was the one that mattered most.
23
+
24
+ Hence one function, imported. Duplicating a consent gate means every future
25
+ copy is another chance to drop the clause that makes it a consent gate at all.
26
+
27
+ **Why BOTH `stdin` and `stderr`, not just `stdin`.** A prompt is a
28
+ question-and-answer pair and each half needs its own real terminal: `stdin`
29
+ carries the answer, and `stderr` — never `stdout`, which belongs to the
30
+ envelope — carries the question. `tan doctor --fix < /dev/null` has no way to
31
+ receive consent; `tan doctor --fix 2>log` has no way to ask for it, and would
32
+ block on a question the user never saw. Requiring both is what makes "the user
33
+ actually agreed to this" true rather than merely likely.
34
+
35
+ **Not `stdout`.** Under `--format json` stdout is a single parsed envelope, and
36
+ `| jq` is a normal, fully-interactive way to run tan. Testing `stdout.isatty()`
37
+ would refuse consent in a session where the human is sitting right there. The
38
+ `json_mode` flag already covers the case that actually matters.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ from tan.env import stderr_is_tty, stdin_is_tty
43
+
44
+
45
+ def can_prompt(*, non_interactive: bool, ci: bool, json_mode: bool) -> bool:
46
+ """Whether this invocation may prompt the user, or take any other action
47
+ that needs a human's live consent (installing a toolchain, overwriting a
48
+ file, relocating a checkout).
49
+
50
+ All five conditions must hold. The three flags are the caller's explicit
51
+ "do not ask me" signals; the two `isatty()` calls are the same rule applied
52
+ **unasked**, for the automated runs that never thought to pass a flag.
53
+
54
+ A command with a documented default takes it when this returns `False`; one
55
+ without a default fails instead of asking.
56
+
57
+ tan-cli#488: `sys.stdin` itself, not just the result of calling `.isatty()`
58
+ on it, can be `None` -- a process launched with its standard handles
59
+ detached (a GUI launcher, `pythonw`-style spawn, or a shell that closed fd
60
+ 0 before exec, `0<&-`) leaves `sys.stdin` unbound rather than merely
61
+ non-interactive, and a bare `sys.stdin.isatty()` then raises
62
+ `AttributeError: 'NoneType' object has no attribute 'isatty'`. This is the
63
+ ONE place that guard belongs: every caller of `can_prompt` (`doctor_cmd`,
64
+ `scaffold_cmd`) reaches this line before any caller-side guard could run,
65
+ so duplicating the `is not None` check downstream (as doctor_cmd.py's
66
+ `fix_suppressed_issue` still does, for its own separate per-condition
67
+ explanation) does not help -- this call is the one that crashes first.
68
+
69
+ tan-cli#488 round 5: the SAME detachment guards ONLY `sys.stdin`, not
70
+ `sys.stderr` -- the very next operand of this same `and` chain, and just
71
+ as reachable: the identical detached-process shape can leave `sys.stdin`
72
+ live (redirected to a real, non-tty file) while `sys.stderr` alone is
73
+ unbound, since each handle is detached independently by whatever spawned
74
+ the process. Verified against the real binary, not a mock: a `pty.fork()`
75
+ child with a genuine tty on stdin (`isatty() is True`, so the chain
76
+ reaches this line) and `stderr` closed before `exec` crashed
77
+ `tan doctor --fix` with exactly this `AttributeError` -- `sys.stdin is not
78
+ None` alone does not protect the `sys.stderr.isatty()` call one line
79
+ below it.
80
+
81
+ tan-cli#488 round 6: `is not None` was never the whole guard either --
82
+ it stops a detached (`None`) handle from crashing `.isatty()`, but not a
83
+ handle that EXISTS and simply has no `.isatty()` method, which is
84
+ exactly what `sys.stderr` becomes under `tan --format json`
85
+ (`tan.cli.main` tees it through `_TeeStderr`: `write`/`flush`/
86
+ `getvalue` only). This function's own `not json_mode` operand happens to
87
+ short-circuit ahead of the `sys.stderr` checks before that shape is ever
88
+ reached today, so `can_prompt` itself was never observed to crash on
89
+ it -- but `tan.commands.build_cmd._dispatch` hand-rolled the identical
90
+ `is not None and .isatty()` pair with no `json_mode` operand in front of
91
+ it at all, and DID crash, on a real `tan run --format json`
92
+ (`AttributeError: '_TeeStderr' object has no attribute 'isatty'`,
93
+ measured). Both operands now route through `tan.env.stdin_is_tty`/
94
+ `stderr_is_tty` -- the one shared probe (tan-cli#288) that already
95
+ wraps both exception classes -- rather than leaving this copy's safety
96
+ resting on an operand order a future edit could reshuffle.
97
+ """
98
+ return (
99
+ not non_interactive
100
+ and not ci
101
+ and not json_mode
102
+ and stdin_is_tty()
103
+ and stderr_is_tty()
104
+ )
@@ -0,0 +1,245 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`colors`: the default console filter for `tan monitor` (tan-cli#1315).
3
+
4
+ A serial console shows bytes from a device nobody vouches for. Passing them
5
+ straight to the user's terminal lets that device write the clipboard (OSC 52),
6
+ retitle the window, and move or erase the screen. miniterm's own `default`
7
+ filter is safe but prints Zephyr's colours as literal `ESC[1;32m` text. This
8
+ filter keeps colours and nothing else, as an ALLOWLIST (a denylist of known
9
+ bad sequences loses to the next terminal quirk):
10
+
11
+ * a character is output only if it is printable (`unicodedata` category not
12
+ in Cc, Cf, Cs, Co, Cn, Zl, Zp, which also removes bidi/format controls and
13
+ DEL) or one of `\\r \\n \\t \\b`;
14
+ * the ONLY escape ever output is an SGR sequence the filter builds itself,
15
+ `ESC [ <codes> m`, re-emitted canonically from the parsed codes and never
16
+ passed through as received. Allowed codes: 0 1 2 3 4 22 23 24 27 30-37 39
17
+ 40-47 49 90-97 100-107, and 38/48 with `;5;n` or `;2;r;g;b` (0-255). Any
18
+ other code (conceal 8, blink 5/6, ...) is dropped and the rest kept; a
19
+ malformed 38/48 drops the remainder. The colon form (`38:2::1:2:3`) is not
20
+ recognised, so that sequence is neutralised whole;
21
+ * every other ESC sequence (non-SGR CSI, OSC, DCS, APC, PM, SOS, charset
22
+ selects, lone ESC), every C1 control (U+0080-U+009F; U+009B/9D/90/98/9E/9F
23
+ start a sequence exactly like their 7-bit forms), CAN, SUB, DEL and every
24
+ other control character becomes one visible placeholder. CAN and SUB abort
25
+ a sequence in progress, as ECMA-48 terminals do;
26
+ * the placeholder is `·`, or `?` when the output stream cannot encode it; a
27
+ `\\r \\n \\t \\b` following a dropped escape is still honoured;
28
+ * it is stream-safe (a sequence split across reads is buffered) and bounded:
29
+ an over-long or unterminated sequence emits a placeholder and resyncs to
30
+ ground state without printing the buffered payload.
31
+
32
+ Accepted residuals (not filterable without breaking colour output): text drawn
33
+ with foreground equal to background is invisible; `\\b` and `\\r` let a device
34
+ overwrite text already on the line; some printable-category characters
35
+ (unassigned-looking Lo/Mn combining marks, zero-width letters) render as
36
+ nothing. A character the output stream's encoding cannot represent (e.g. `世`
37
+ on cp437) is replaced by the placeholder too, so it cannot raise in miniterm's
38
+ reader thread.
39
+
40
+ Because the only escape that can reach the terminal is one the filter
41
+ generated, the terminal is always in ground state when it sees it, which
42
+ removes the filter-vs-terminal parser differential.
43
+
44
+ The class is defined by exec'ing `SOURCE`, so the same text can also be run
45
+ inside a spawned `python -c` (`BOOTSTRAP`) where `tan` is not importable
46
+ (frozen builds), and the in-process and subprocess consoles cannot drift.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ SOURCE = r'''
52
+ import sys as _sys
53
+ import unicodedata as _ud
54
+
55
+
56
+ def _placeholder():
57
+ enc = getattr(_sys.stdout, "encoding", None) or "ascii"
58
+ try:
59
+ "·".encode(enc)
60
+ return "·"
61
+ except (UnicodeError, LookupError):
62
+ return "?"
63
+
64
+
65
+ class ColorsFilter:
66
+ PLACEHOLDER = "·" # instances pick `?` when stdout cannot encode it
67
+ _KEEP = "\r\n\t\b"
68
+ _BAD_CATEGORIES = frozenset(["Cc", "Cf", "Cs", "Co", "Cn", "Zl", "Zp"])
69
+ _SGR_SIMPLE = frozenset(
70
+ [0, 1, 2, 3, 4, 22, 23, 24, 27, 39, 49,
71
+ *range(30, 38), *range(40, 48), *range(90, 98), *range(100, 108)]
72
+ )
73
+ _MAX_PARAMS = 32
74
+ _MAX_CSI = 64
75
+ _MAX_STR = 4096
76
+ _MAX_INTER = 16
77
+
78
+ def __init__(self):
79
+ self.PLACEHOLDER = _placeholder()
80
+ self._enc = getattr(_sys.stdout, "encoding", None) or "ascii"
81
+ self._enc_ok = {}
82
+ self._state = "ground" # ground|esc|inter|csi|str|stresc
83
+ self._buf = ""
84
+
85
+ def tx(self, text):
86
+ return text
87
+
88
+ def echo(self, text):
89
+ return text
90
+
91
+ def rx(self, text):
92
+ out = []
93
+ for ch in text:
94
+ self._step(ch, out)
95
+ return "".join(out)
96
+
97
+ def _reset(self):
98
+ self._state = "ground"
99
+ self._buf = ""
100
+
101
+ def _drop(self, out):
102
+ out.append(self.PLACEHOLDER)
103
+ self._reset()
104
+
105
+ def _encodable(self, ch):
106
+ ok = self._enc_ok.get(ch)
107
+ if ok is None:
108
+ try:
109
+ ch.encode(self._enc)
110
+ ok = True
111
+ except (UnicodeError, LookupError):
112
+ ok = False
113
+ if len(self._enc_ok) < 4096:
114
+ self._enc_ok[ch] = ok
115
+ return ok
116
+
117
+ def _printable(self, ch):
118
+ if ch in self._KEEP:
119
+ return True
120
+ return _ud.category(ch) not in self._BAD_CATEGORIES and self._encodable(ch)
121
+
122
+ def _sgr(self, params):
123
+ # `params` is already known to match ^[0-9;]{0,32}$
124
+ if params == "":
125
+ return "\x1b[0m"
126
+ vals = [int(p) if p else 0 for p in params.split(";")]
127
+ kept, i = [], 0
128
+ while i < len(vals):
129
+ v = vals[i]
130
+ if v in (38, 48):
131
+ mode = vals[i + 1] if i + 1 < len(vals) else None
132
+ if mode == 5 and i + 2 < len(vals) and vals[i + 2] <= 255:
133
+ kept += [v, 5, vals[i + 2]]
134
+ i += 3
135
+ continue
136
+ if mode == 2 and i + 4 < len(vals) and max(vals[i + 2:i + 5]) <= 255:
137
+ kept += [v, 2, *vals[i + 2:i + 5]]
138
+ i += 5
139
+ continue
140
+ break # malformed extended colour: drop the remainder
141
+ if v in self._SGR_SIMPLE:
142
+ kept.append(v)
143
+ i += 1
144
+ if not kept:
145
+ return ""
146
+ return "\x1b[" + ";".join(str(k) for k in kept) + "m"
147
+
148
+ def _step(self, ch, out):
149
+ st = self._state
150
+ o = ord(ch)
151
+ if st == "ground":
152
+ if ch == "\x1b":
153
+ self._state = "esc"
154
+ elif o == 0x9B:
155
+ self._state, self._buf = "csi", ""
156
+ elif o in (0x90, 0x98, 0x9D, 0x9E, 0x9F):
157
+ self._state, self._buf = "str", ""
158
+ elif self._printable(ch):
159
+ out.append(ch)
160
+ else:
161
+ out.append(self.PLACEHOLDER) # C0, C1, DEL, CAN, SUB, format, ...
162
+ return
163
+ if ch in "\x18\x1a": # CAN / SUB abort any sequence in progress
164
+ self._drop(out)
165
+ return
166
+ if st == "esc":
167
+ if ch == "[":
168
+ self._state, self._buf = "csi", ""
169
+ elif ch in "]PX^_":
170
+ self._state, self._buf = "str", ""
171
+ elif 0x20 <= o <= 0x2F:
172
+ self._state, self._buf = "inter", ""
173
+ elif ch == "\x1b":
174
+ out.append(self.PLACEHOLDER) # ESC ESC: still in esc
175
+ elif 0x30 <= o <= 0x7E:
176
+ self._drop(out) # two-byte sequence (ESC c, ESC =, ...)
177
+ else:
178
+ self._drop(out)
179
+ self._step(ch, out) # honour newline etc.; C1 starts its own sequence
180
+ elif st == "inter":
181
+ if 0x20 <= o <= 0x2F:
182
+ self._buf += ch
183
+ if len(self._buf) > self._MAX_INTER:
184
+ self._drop(out)
185
+ elif 0x30 <= o <= 0x7E:
186
+ self._drop(out) # ESC ( B etc.
187
+ elif ch == "\x1b":
188
+ self._drop(out)
189
+ self._state = "esc"
190
+ else:
191
+ self._drop(out)
192
+ self._step(ch, out)
193
+ elif st == "csi":
194
+ if 0x20 <= o <= 0x3F:
195
+ self._buf += ch
196
+ if len(self._buf) > self._MAX_CSI:
197
+ self._drop(out)
198
+ elif 0x40 <= o <= 0x7E:
199
+ params = self._buf
200
+ if (
201
+ ch == "m"
202
+ and len(params) <= self._MAX_PARAMS
203
+ and all(c in "0123456789;" for c in params)
204
+ ):
205
+ out.append(self._sgr(params))
206
+ self._reset()
207
+ else:
208
+ self._drop(out)
209
+ elif ch == "\x1b":
210
+ self._drop(out)
211
+ self._state = "esc"
212
+ else:
213
+ self._drop(out) # any other control or non-ASCII aborts it
214
+ self._step(ch, out)
215
+ elif st == "str":
216
+ if ch == "\x07" or o == 0x9C:
217
+ self._drop(out)
218
+ elif ch == "\x1b":
219
+ self._state = "stresc"
220
+ else:
221
+ self._buf += ch
222
+ if len(self._buf) > self._MAX_STR:
223
+ self._drop(out)
224
+ elif st == "stresc":
225
+ if ch == "\\":
226
+ self._drop(out) # ESC \ = ST
227
+ else:
228
+ self._drop(out) # anything else aborts the string...
229
+ self._state = "esc" # ...and the ESC starts a new sequence
230
+ if ch != "\x1b":
231
+ self._step(ch, out)
232
+ '''
233
+
234
+ #: Run by `python -c` for the spawned console: define the filter, register it
235
+ #: in miniterm's own table, hand over to miniterm's `main()` (argv follows).
236
+ BOOTSTRAP = (
237
+ SOURCE
238
+ + "\nfrom serial.tools import miniterm as _m\n"
239
+ + "_m.TRANSFORMATIONS['colors'] = ColorsFilter\n"
240
+ + "_m.main()\n"
241
+ )
242
+
243
+ _ns: dict = {}
244
+ exec(compile(SOURCE, "<tan.core.console_filter.SOURCE>", "exec"), _ns) # noqa: S102
245
+ ColorsFilter = _ns["ColorsFilter"]