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/zephyr_env.py ADDED
@@ -0,0 +1,124 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Consumer-mechanism env gap-filler for `tan build`'s native executor: the
3
+ `ZEPHYR_BASE` / `EXTRA_ZEPHYR_MODULES` keys the plan deliberately does NOT
4
+ carry, filled in only when the plan didn't pin them ("plan wins / CLI fills
5
+ gaps"). Port of `zephyr_env_overrides`, `crates/tan-cli/src/commands/build/
6
+ execute/env.rs` -- confirmed against the compiled Rust unit tests in that
7
+ module (`cargo test -p alp-tan-cli --bin tan commands::build::execute::env::`,
8
+ all 5 passing) since the oracle binary's own `--plan-from` implies `--plan`
9
+ (v0.4.1 limitation, `build_cmd.py`'s own docstring) and so cannot be driven to
10
+ dispatch a synthetic plan end to end without a real `alp_orchestrate.py`
11
+ emission.
12
+
13
+ tan-cli#308: `ZEPHYR_BASE` is per ADR-0020 never carried by the plan at all --
14
+ it is pure consumer mechanism, always hand-derived from the west workspace
15
+ `tan` itself resolved -- so a stale ambient `$ZEPHYR_BASE` left over from a
16
+ `source zephyr-env.sh` (or an older `tan bootstrap` next-steps block, before
17
+ tan-cli#301) must not silently win for the spawned build child."""
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Callable
21
+ from pathlib import Path
22
+
23
+ from tan.core.plan_exec import apply_env_append
24
+
25
+
26
+ def zephyr_env_overrides(
27
+ zephyr_base: Path | None,
28
+ sdk_root: Path | None,
29
+ slice_env: dict[str, str],
30
+ env_append_path: dict[str, list[str]],
31
+ inherited: Callable[[str], str | None],
32
+ toolchain_store: Path | None = None,
33
+ ) -> list[tuple[str, str]]:
34
+ """Consumer-mechanism env the plan deliberately does NOT carry, filled in
35
+ as a gap-filler so `tan build` runs a plan slice with no manual setup:
36
+
37
+ * `ZEPHYR_BASE` -- the resolved workspace's zephyr. Per ADR-0020 the plan
38
+ never emits this; it is pure consumer mechanism, always hand-derived.
39
+ * `EXTRA_ZEPHYR_MODULES` -- the alp-sdk checkout, so `west build -b
40
+ <alp-board>` finds the SDK's boards. This now comes FROM the plan's
41
+ `env_append_path` for an SDK-emitted plan; the hand-derived value here
42
+ is only a FALLBACK for a plan that carries neither the slice-env pin
43
+ nor the `env_append_path` entry (plan wins / CLI fills gaps).
44
+ * `ZEPHYR_SDK_INSTALL_DIR` (tan-cli#1209) -- CMake's `FindZephyr-sdk.cmake`
45
+ only finds `~/.alp/toolchains/zephyr-sdk-<v>-arm-zephyr-eabi/` by being
46
+ TOLD where it is: the module's own prefix scan looks one level too
47
+ shallow under `$HOME` to ever see it. `toolchain_store` is that
48
+ store's path, already verified (`tan.commands.build.toolchain.
49
+ verified_store_dir`) against THIS checkout's pinned manifest digest --
50
+ callers pass `None` when it isn't verified, and this function fills
51
+ nothing in that case. Precedence, per the ruling: a plan slice pin
52
+ wins outright (checked below); an inherited `ZEPHYR_SDK_INSTALL_DIR`
53
+ naming a path that EXISTS is the user's deliberate choice and is taken
54
+ verbatim (never normalised, never probed for anything beyond
55
+ existence); only when it is unset, blank, or names a path that does
56
+ NOT exist does tan's own stamped store fill the gap (tan-cli#1209
57
+ review MINOR: an earlier version skipped the existence probe here,
58
+ diverging from `build.toolchain.resolve_toolchain_root`'s own rule for
59
+ the SAME variable -- "set but naming a path that does NOT exist -- and
60
+ the empty string -- fall THROUGH to the scan" -- and from
61
+ `doctor_cmd._zephyr_sdk_detected_root`'s stricter validity check; a
62
+ stale pointer to a deleted SDK made `tan build` fail cmake's configure
63
+ while `tan doctor` reported the store present). A wrong/missing
64
+ `sdk_root` or an unstamped/stale store already resolves
65
+ `toolchain_store` to `None` upstream, so this function's own
66
+ contribution is exactly the precedence, nothing more.
67
+
68
+ Never overrides a key THIS slice's env pins.
69
+
70
+ `inherited` is the parent-process env lookup (the same one the caller
71
+ already threads through to `assemble_slice_env` for `env_append_path`
72
+ seeding). The `EXTRA_ZEPHYR_MODULES` gap-filler must not just return the
73
+ bare SDK root: the caller's own gap-filler merge OVERWRITES rather than
74
+ appends (see `assemble_slice_env`'s docstring), so returning only the SDK
75
+ root here would silently replace a developer's own
76
+ `export EXTRA_ZEPHYR_MODULES=/my/module` with just the SDK root on any
77
+ plan that doesn't itself pin the key. Seed from `inherited` and append the
78
+ SDK root via the same `apply_env_append` (per-key separator, de-dup) the
79
+ plan-driven path uses, so the two paths agree on whether an inherited
80
+ value survives."""
81
+ out: list[tuple[str, str]] = []
82
+ if "ZEPHYR_BASE" not in slice_env and zephyr_base is not None:
83
+ out.append(("ZEPHYR_BASE", str(zephyr_base)))
84
+
85
+ # Plan wins: skip the hand-derived value when the plan carries
86
+ # EXTRA_ZEPHYR_MODULES (as a slice-env pin or an env_append_path entry).
87
+ if "EXTRA_ZEPHYR_MODULES" not in slice_env and "EXTRA_ZEPHYR_MODULES" not in env_append_path:
88
+ if sdk_root is not None:
89
+ sdk = str(sdk_root)
90
+ base: list[tuple[str, str]] = []
91
+ inherited_value = inherited("EXTRA_ZEPHYR_MODULES")
92
+ if inherited_value:
93
+ base.append(("EXTRA_ZEPHYR_MODULES", inherited_value))
94
+ apply_env_append(base, {"EXTRA_ZEPHYR_MODULES": [sdk]})
95
+ if base:
96
+ out.append(("EXTRA_ZEPHYR_MODULES", base[0][1]))
97
+
98
+ # tan-cli#1209: plan pin > inherited (verbatim, IFF it names a path that
99
+ # still exists -- a stale pointer to a deleted SDK is not tan's to keep
100
+ # deferring to, matching resolve_toolchain_root's rule for the same
101
+ # variable) > tan's own verified store > nothing.
102
+ if (
103
+ "ZEPHYR_SDK_INSTALL_DIR" not in slice_env
104
+ and not _inherited_path_exists(inherited("ZEPHYR_SDK_INSTALL_DIR"))
105
+ and toolchain_store is not None
106
+ ):
107
+ out.append(("ZEPHYR_SDK_INSTALL_DIR", str(toolchain_store)))
108
+ return out
109
+
110
+
111
+ def _inherited_path_exists(value: str | None) -> bool:
112
+ """Existence probe for an inherited env value that names a path -- the
113
+ same rule `build.toolchain.resolve_toolchain_root` already applies to
114
+ this SAME `ZEPHYR_SDK_INSTALL_DIR` variable: blank is never trusted, and
115
+ neither is a path that does not exist. `OSError` (an unencodable or
116
+ too-long path on this platform) reads as "cannot confirm it exists",
117
+ not a crash -- matching that function's own `except OSError: exists =
118
+ False`."""
119
+ if not value:
120
+ return False
121
+ try:
122
+ return Path(value).exists()
123
+ except OSError:
124
+ return False
tan/env.py ADDED
@@ -0,0 +1,185 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Shared environment-variable contracts, kept in one place so no second
3
+ hand-rolled copy can drift from it (tan-cli#288).
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import shutil
9
+ import sys
10
+
11
+
12
+ def no_color_requested() -> bool:
13
+ """Whether `NO_COLOR` disables colour, per the spec: PRESENCE, not
14
+ truthiness -- `NO_COLOR=` (set, empty) must still disable colour, same as
15
+ `NO_COLOR=0` or `NO_COLOR=false`. Mirrors the Rust oracle exactly
16
+ (`crates/tan-cli/src/style.rs:27`'s `var_os("NO_COLOR").is_none()`).
17
+
18
+ `faultdecode_cmd.py` and `size_cmd.py` each hand-rolled this check and
19
+ drifted: one did `if os.environ.get("NO_COLOR")` (truthy -- wrong, an
20
+ empty value read as unset), the other already did the correct presence
21
+ check. Do NOT reach for `typer.Option(..., envvar="NO_COLOR")` instead --
22
+ Click's bool-envvar coercion parses the STRING as a boolean (`1`/`true`/
23
+ `yes`/`on`, ...), so `NO_COLOR=` is "not a valid boolean" and crashes the
24
+ command outright, a regression neither original copy had.
25
+ """
26
+ return os.environ.get("NO_COLOR") is not None
27
+
28
+
29
+ def stderr_is_tty() -> bool:
30
+ """The one place that probes `sys.stderr.isatty()` -- `AttributeError`
31
+ when stderr has been replaced by something that doesn't implement it,
32
+ `ValueError` when it has been closed. Shared by `use_color` and
33
+ `wrap_width` below so neither hand-rolls its own copy of this
34
+ try/except (tan-cli#288, the exact drift this module's own docstring
35
+ exists to prevent).
36
+
37
+ tan-cli#488 round 6: promoted from `_stderr_is_tty` (module-private) to
38
+ a public name once `tan.core.consent.can_prompt`,
39
+ `tan.commands.doctor_cmd.fix_suppressed_issue` and
40
+ `tan.commands.build_cmd._dispatch` each turned out to hand-roll the exact
41
+ `X is not None and X.isatty()` shape this function already guards against
42
+ -- a `None` check alone stops the `AttributeError` from a detached
43
+ handle, but not from a handle that exists and simply has no `.isatty()`
44
+ at all, which is exactly what `tan.cli`'s `_TeeStderr` is under
45
+ `--format json`. Duplicating the try/except a fourth time is the same
46
+ drift this module's own docstring already names; importing the one
47
+ version is the fix, not writing a fifth."""
48
+ try:
49
+ return sys.stderr.isatty()
50
+ except (AttributeError, ValueError):
51
+ return False
52
+
53
+
54
+ def stdin_is_tty() -> bool:
55
+ """The `stdin` counterpart of `stderr_is_tty` above, same rationale and
56
+ same exception pair: `AttributeError` when `sys.stdin` is `None` (a
57
+ detached-stdio process) or has been replaced by something with no
58
+ `.isatty()`, `ValueError` when it has been closed. `tan.core.consent.
59
+ can_prompt` and `tan.commands.doctor_cmd.fix_suppressed_issue` each used
60
+ to hand-roll `sys.stdin is not None and sys.stdin.isatty()` -- correct
61
+ for today's `None`-or-real-stream shape, but the same one-operand-short
62
+ pattern the stderr side was fixed for tan-cli#488 round 5/6; sharing this
63
+ helper closes both sides the same way instead of leaving stdin's copy to
64
+ rediscover the lesson on its own."""
65
+ try:
66
+ return sys.stdin.isatty()
67
+ except (AttributeError, ValueError):
68
+ return False
69
+
70
+
71
+ def use_color(no_color: bool, ci: bool) -> bool:
72
+ """`Theme::from_args`: human text goes to stderr, so the TTY probe is
73
+ against stderr. Shared by every command's `--no-color`/`--ci` handling
74
+ (moved out of `size_cmd.py`, tan-cli's UX-polish sweep) so a second,
75
+ driftable copy never grows up beside this one -- `size_cmd._use_color`
76
+ now delegates here."""
77
+ if no_color or ci or no_color_requested():
78
+ return False
79
+ return stderr_is_tty()
80
+
81
+
82
+ #: The floor a resolved terminal width may never drop below, moved here from
83
+ #: `doctor_cmd._DOCTOR_TEXT_MIN_WIDTH` (tan-cli's `doctor` wrap-seam adoption,
84
+ #: PR #480) so `wrap_width` below and `doctor_cmd.doctor` can share the one
85
+ #: number instead of drifting into two: a real terminal narrower than this
86
+ #: (or a redirected/piped run whose `shutil.get_terminal_size` fallback would
87
+ #: otherwise apply) must not be left to wrap one word per line. `doctor_cmd`
88
+ #: imports this name rather than keeping its own copy.
89
+ TEXT_WRAP_MIN_WIDTH = 60
90
+
91
+ #: Rows handed to `shutil.get_terminal_size`'s `fallback` pair by
92
+ #: `terminal_width` below. Never read by any caller -- every width probe in
93
+ #: this CLI wants columns only -- but the parameter takes a `(columns, lines)`
94
+ #: pair, so naming the number here keeps each call site from restating a bare
95
+ #: `24` it does not care about.
96
+ _TERMINAL_HEIGHT_FALLBACK = 24
97
+
98
+
99
+ def terminal_width(fallback: int) -> int:
100
+ """The one place that MEASURES a terminal, beside `stderr_is_tty` above
101
+ which is the one place that PROBES one -- the column count of the stream
102
+ every text surface in this CLI actually prints to, `sys.stderr`.
103
+
104
+ tan-cli#564: `wrap_width` below, `doctor_cmd.doctor`'s report width and
105
+ `build_cmd._heartbeat_line_width` were each armed on `stderr_is_tty()`
106
+ and then sized off a bare `shutil.get_terminal_size(fallback=(100, 24))`,
107
+ which CPython resolves against `os.get_terminal_size(sys.__stdout__.
108
+ fileno())`. Nothing in text mode is written to stdout, so with stdout
109
+ redirected that ioctl raised `OSError [Errno 25] Inappropriate ioctl for
110
+ device` -- swallowed by `shutil` itself -- and every probe silently took
111
+ the hard-coded 100 columns while stderr's own fd read 70. The #536
112
+ isatty-guard consolidation moved the PROBE onto one shared helper but
113
+ left the MEASUREMENT on the other handle; this moves it.
114
+
115
+ `COLUMNS` comes first so `shutil`'s existing precedence is preserved
116
+ exactly: an operator who exports `COLUMNS=137` is overriding BOTH handles
117
+ on purpose, and that override must not become the one thing the move
118
+ takes away. A non-positive, empty or non-numeric value is ignored, and so
119
+ is a 0-column ioctl answer (a known pty shape) -- both exactly as
120
+ `shutil` ignores them, and because a literal 0 would reach
121
+ `build_cmd._heartbeat_line_width` as a one-column line.
122
+
123
+ The last step falls through to `shutil.get_terminal_size` rather than
124
+ straight to `fallback`, so the mirror-image run -- stderr redirected
125
+ while stdout is still a terminal -- resolves exactly the number it
126
+ resolves today. `fallback` is a plain column count; the
127
+ `(columns, lines)` pair `shutil` wants is assembled here.
128
+ """
129
+ try:
130
+ columns = int(os.environ["COLUMNS"])
131
+ except (KeyError, ValueError):
132
+ pass
133
+ else:
134
+ if columns > 0:
135
+ return columns
136
+ try:
137
+ # `AttributeError` when stderr has been replaced by something with no
138
+ # `.fileno()`, `ValueError` when it has been closed, `OSError` when the
139
+ # fd is real but not a terminal (the redirected case above).
140
+ measured = os.get_terminal_size(sys.stderr.fileno()).columns
141
+ except (AttributeError, ValueError, OSError):
142
+ measured = 0
143
+ if measured > 0:
144
+ return measured
145
+ return shutil.get_terminal_size(fallback=(fallback, _TERMINAL_HEIGHT_FALLBACK)).columns
146
+
147
+
148
+ def wrap_width() -> int | None:
149
+ """The column budget for hard-wrapping prose to stderr, or `None` when
150
+ stderr is not a terminal.
151
+
152
+ `None` is not "80 columns", it is "do not wrap at all" -- a caller must
153
+ print each logical line as one physical line. This is the design point
154
+ `explain`/`sdk current` adopt this seam for: `tan explain | grep` (or any
155
+ pipe) wants one record per line, and inserting a hard newline into a
156
+ piped stream breaks that in a way a real terminal's own non-destructive
157
+ soft-wrap never would. So the probe here is narrower than `use_color`'s:
158
+ a redirected/piped stream (not a tty) always returns `None`, full stop.
159
+
160
+ Deliberately takes no `no_color`/`ci` arguments, unlike `use_color`
161
+ above, and must not gain them: `--no-color` asks only to drop ANSI
162
+ colour, and `--ci` asks only to skip interactive prompts (`can_prompt`)
163
+ -- neither says anything about whether stderr is a pipe, and a user who
164
+ passed `--no-color` on a real terminal still wants readable line breaks.
165
+ Folding either flag in here would silently also turn off wrapping for
166
+ every `--no-color` invocation, which is a second, unrelated behaviour
167
+ change riding on a flag that never asked for it.
168
+
169
+ `doctor` deliberately does NOT use this seam: it resolves its width
170
+ unconditionally, so `tan doctor > report.txt` still comes out wrapped.
171
+ That is a difference in what the two kinds of output ARE, not an
172
+ oversight, and it is the reason this function exists as a separate
173
+ decision rather than something `render_check_lines` could have called
174
+ for everybody. `doctor` prints a REPORT -- prose a human reads top to
175
+ bottom, whose redirected form is a file someone opens later and wants
176
+ wrapped exactly as the terminal would have shown it. `explain` and
177
+ `sdk current` print RECORDS -- `Generation targets: ...`, ` path
178
+ <value>` -- whose redirected form is an input to `grep`/`cut`, where a
179
+ hard newline in the middle of a value is corruption. Shape of the
180
+ output decides, so a future command adopting this seam should ask which
181
+ of the two it is rather than copying whichever neighbour it read first.
182
+ """
183
+ if not stderr_is_tty():
184
+ return None
185
+ return max(terminal_width(100), TEXT_WRAP_MIN_WIDTH)