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,168 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Will Zephyr's devicetree LINT pass run for a build on this host?
3
+
4
+ Zephyr compiles the devicetree with its own Python `gen_defines.py`; `dtc`
5
+ is not needed for that and a build with no `dtc` anywhere still links a real
6
+ `zephyr.elf`. What `dtc` is used for is a SECOND, diagnostics-only pass --
7
+ `zephyr/cmake/modules/dts.cmake` runs the compiler over the assembled `.dts`
8
+ under `if(DTC)`, above the verbatim comment *"This is just to generate
9
+ warnings and errors; we discard the output"*, with `-E unit_address_vs_reg`
10
+ and (when the binary accepts it) `-Wunique_unit_address_if_enabled`. No
11
+ `dtc`, no `if(DTC)` body, no diagnostics -- and nothing on either side says
12
+ so, because the block is skipped rather than failed.
13
+
14
+ **Why this module exists at all: tan-cli#1176/#1178 changed the premise.**
15
+ `tan bootstrap` acquires the Zephyr SDK with `--no-hosttools`
16
+ (`tan.core.toolchain_provision.NO_HOSTTOOLS_FLAG`), so the install carries no
17
+ `hosttools/` tree -- and `hosttools/` is where the SDK's own `dtc` lives
18
+ (measured on a real `zephyr-sdk-1.0.1`:
19
+ `hosttools/sysroots/x86_64-pokysdk-linux/usr/bin/dtc`, `Version: DTC v1.7.0+`,
20
+ beside `openocd`/`fdtdump`/`fdtget`/`fdtoverlay`/`fdtput`). Before #1178 the
21
+ SDK put a `dtc` in CMake's reach on every install; now it does not, so the
22
+ lint pass silently stopped running on the default path. That is the whole of
23
+ tan-cli#1192.
24
+
25
+ **This partially reverses a decision `tan.commands.doctor_cmd`'s own module
26
+ docstring records as deliberate** -- `dtc`/`gperf` are named there as the
27
+ frozen Rust oracle's `--build` check vocabulary, NOT ported. That decision was
28
+ correct when it was made and its premise is gone: it was written when `dtc`
29
+ arrived with the SDK, so a `dtc` probe could only ever restate what
30
+ `zephyrSdk` already said. `gperf` stays unported; only `dtc` is reopened here,
31
+ and only as the devicetree-lint question, never as a bare is-it-on-PATH probe.
32
+
33
+ ## The two CMake mechanisms this mirrors, and why BOTH are needed
34
+
35
+ `find_program(DTC dtc)` (`zephyr/cmake/modules/FindDtc.cmake`) searches
36
+ `CMAKE_PREFIX_PATH` BEFORE the ambient `PATH`, and the Zephyr SDK's own
37
+ `cmake/zephyr/host-tools.cmake` is what puts its `hosttools/` on that prefix
38
+ list -- `list(APPEND CMAKE_PREFIX_PATH ${HOST_TOOLS_HOME}/usr)`, where
39
+ `HOST_TOOLS_HOME` is the poky sysroot on Linux, `<sdk>/hosttools` on macOS,
40
+ and on WINDOWS is never given a `dtc`-bearing prefix at all (that branch
41
+ appends only `qemu`/`qemu-arc`/`openocd`, and the Windows hosttools archive
42
+ ships neither `dtc` nor `gperf` -- `tan.core.bootstrap`'s
43
+ `manual_install_windows` prose records the `7z l` measurement). So a probe
44
+ that looked only at `PATH` would report "no lint" on a host whose SDK carries
45
+ one, which is a report that lies; [`hosttools_bin_dir`] is the other half.
46
+
47
+ `find_package(Dtc 1.4.6)` (`dts.cmake`, line 10 of the same file) is the
48
+ second mechanism and the one that makes this check worth having a `warn` arm
49
+ at all. `FindDtc.cmake` runs `dtc --version`, scrapes `Version: DTC v?X.Y.Z`
50
+ out of it, hands the result to `find_package_handle_standard_args`, and then:
51
+
52
+ if(NOT Dtc_FOUND)
53
+ # DTC was found but version requirement is not met, or dtc was not working.
54
+ # Treat it as DTC was never found by resetting the result from `find_program()`
55
+ set(DTC DTC-NOTFOUND CACHE FILEPATH "Path to a program" FORCE)
56
+ endif()
57
+
58
+ A `dtc` that IS installed but is older than [`DTC_MIN_VERSION`], or whose
59
+ `--version` does not run or does not parse, is therefore reset to
60
+ `DTC-NOTFOUND` and the lint is skipped -- with no message. That host is the
61
+ one this module has something to say to: somebody installed `dtc` on purpose,
62
+ believes the lint runs, and it does not. A host with NO `dtc` is the ordinary,
63
+ supported, documented post-#1178 state and gets no warning at all; see
64
+ `doctor_cmd.devicetree_lint_check` for the severity argument.
65
+
66
+ Pure: no subprocess, no filesystem, no environment. `doctor_cmd` owns the IO
67
+ (`_resolve_dtc`) and builds the `Check`.
68
+ """
69
+ from __future__ import annotations
70
+
71
+ import re
72
+ from pathlib import Path
73
+ from typing import NamedTuple
74
+
75
+ #: `zephyr/cmake/modules/dts.cmake`'s own `find_package(Dtc 1.4.6)`. A `dtc`
76
+ #: below this is reset to `DTC-NOTFOUND` by `FindDtc.cmake` -- see the module
77
+ #: docstring. Compared component-wise, `>=`, non-EXACT, exactly as CMake's
78
+ #: `find_package_handle_standard_args` compares a `VERSION_VAR`.
79
+ DTC_MIN_VERSION: tuple[int, int, int] = (1, 4, 6)
80
+
81
+ #: `FindDtc.cmake`'s own scrape, transcribed: `string(REGEX MATCH "Version:
82
+ #: DTC v?([0-9]+[.][0-9]+[.][0-9]+).*" ...)`. Deliberately NOT anchored and
83
+ #: deliberately tolerant of a trailing suffix -- the SDK's own binary answers
84
+ #: `Version: DTC v1.7.0+` (measured), whose `+` CMake drops the same way this
85
+ #: does.
86
+ _DTC_VERSION_RE = re.compile(r"Version: DTC v?([0-9]+)[.]([0-9]+)[.]([0-9]+)")
87
+
88
+ #: Where the resolved `dtc` came from. `hosttools` means the Zephyr SDK's own
89
+ #: copy, reached through the `CMAKE_PREFIX_PATH` append `host-tools.cmake`
90
+ #: does; `path` means the ambient `PATH`, which CMake searches second;
91
+ #: `absent` means neither had one, which is what a `--no-hosttools` install
92
+ #: with no distro `dtc` looks like.
93
+ ORIGIN_HOSTTOOLS = "hosttools"
94
+ ORIGIN_PATH = "path"
95
+ ORIGIN_ABSENT = "absent"
96
+
97
+
98
+ class DtcResolution(NamedTuple):
99
+ """What a host's `dtc` situation is, in the terms CMake would resolve it.
100
+
101
+ `version` is `None` both when there is no binary to ask and when the
102
+ binary answered something `FindDtc.cmake`'s own regex would not match --
103
+ the two are distinguished by `origin`, not by this field, because CMake
104
+ does not distinguish them either: both end at `DTC-NOTFOUND`.
105
+ """
106
+
107
+ path: str | None
108
+ origin: str
109
+ version: tuple[int, int, int] | None
110
+
111
+
112
+ def parse_dtc_version(text: str | None) -> tuple[int, int, int] | None:
113
+ """`dtc --version` output -> `(major, minor, patch)`, or `None` when
114
+ `FindDtc.cmake`'s regex would not have matched it either."""
115
+ if not text:
116
+ return None
117
+ found = _DTC_VERSION_RE.search(text)
118
+ if found is None:
119
+ return None
120
+ return (int(found.group(1)), int(found.group(2)), int(found.group(3)))
121
+
122
+
123
+ def format_version(version: tuple[int, int, int]) -> str:
124
+ return ".".join(str(part) for part in version)
125
+
126
+
127
+ def hosttools_bin_dir(sdk_install_dir: Path, host_os: str, host_arch: str) -> Path | None:
128
+ """The directory `find_program(DTC dtc)` would find the SDK's own `dtc`
129
+ in, given an install at `sdk_install_dir` -- or `None` on a host whose
130
+ SDK never puts one on `CMAKE_PREFIX_PATH`.
131
+
132
+ One branch per branch of `<sdk>/cmake/zephyr/host-tools.cmake`, whose
133
+ `list(APPEND CMAKE_PREFIX_PATH ${HOST_TOOLS_HOME}/usr)` is what makes the
134
+ bundle findable; `find_program` appends `bin` to a prefix itself.
135
+
136
+ * `linux` -- `hosttools/sysroots/<arch>-pokysdk-linux/usr/bin`, the arch
137
+ token coming from `cmake_host_system_information(... OS_PLATFORM)`, i.e.
138
+ `uname -m`, which is the same token `doctor_cmd._host_os_arch_tags`
139
+ already normalises (`x86_64`/`aarch64`).
140
+ * `macos` -- `hosttools/usr/bin`; the per-component `opt/` trees are
141
+ symlinked into that one directory by the SDK itself.
142
+ * `windows` -- `None`, and that is a fact about the SDK, not a gap here:
143
+ the Windows branch appends only `qemu`, `qemu-arc` and `openocd`
144
+ prefixes, and the Windows hosttools archive ships no `dtc` to append a
145
+ prefix for. On Windows `dtc` can only ever come from `PATH`.
146
+ """
147
+ if host_os == "linux":
148
+ return (
149
+ sdk_install_dir
150
+ / "hosttools"
151
+ / "sysroots"
152
+ / f"{host_arch}-pokysdk-linux"
153
+ / "usr"
154
+ / "bin"
155
+ )
156
+ if host_os == "macos":
157
+ return sdk_install_dir / "hosttools" / "usr" / "bin"
158
+ return None
159
+
160
+
161
+ def lint_will_run(resolution: DtcResolution) -> bool:
162
+ """`Dtc_FOUND`, computed the way `FindDtc.cmake` computes it: a binary was
163
+ resolved AND its version parsed AND that version is at least
164
+ [`DTC_MIN_VERSION`]. Anything else is the `set(DTC DTC-NOTFOUND ... FORCE)`
165
+ reset, and `dts.cmake`'s `if(DTC)` block does not run."""
166
+ if resolution.path is None or resolution.version is None:
167
+ return False
168
+ return resolution.version >= DTC_MIN_VERSION
@@ -0,0 +1,229 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Shared, hardened directory-removal primitives (tan-cli#790).
3
+
4
+ **Extracted from `tan.commands.clean_cmd`, verbatim logic**, where these six
5
+ names (`is_link`, `os_error_text`, `_reraise_removal_failure`,
6
+ `_retry_after_clearing_readonly`, `_RMTREE_HOOK`, `remove_dir`) first landed
7
+ for `tan clean` and were battle-tested against the frozen v0.4.1 oracle on
8
+ real read-only build outputs, real junctions, and real symlinked build roots
9
+ (see each function's own docstring for the specific oracle-diff that shaped
10
+ it). `tan sdk remove` needs the identical hardening -- a real `tan bootstrap`
11
+ tree can carry a read-only `.git/objects/**` entry from a shallow clone, and
12
+ neither command's job is to reinvent "how does this host actually let me
13
+ delete a stubborn directory" independently.
14
+
15
+ **Lives in `tan.core`, not a second copy in `tan.commands.sdk_cmd`, and not
16
+ left where it was**, for the same reason `sdk_discovery.py`'s own module
17
+ docstring gives for why IT moved out of a command module: `tan.core` may
18
+ import no `tan.commands.*` module (the invariant `tan.core.shapes` and
19
+ `tan.core.sdk_discovery` already state and rely on), so a `tan/core/`
20
+ caller — this module has none yet, but `tan.core.sdk_removal` (tan-cli#790)
21
+ is the first — could not reach these primitives while they lived inside
22
+ `clean_cmd.py` without creating exactly the `tan.core` -> `tan.commands`
23
+ inversion that rule exists to forbid.
24
+
25
+ `clean_cmd.py` re-imports every one of these six names at its own module
26
+ level (`from tan.core.dir_removal import ...`) rather than losing them: its
27
+ own internal call sites still say the bare `_remove_dir(...)` /
28
+ `_retry_after_clearing_readonly(...)`, resolved through `clean_cmd`'s own
29
+ module globals at call time, so `monkeypatch.setattr(clean_cmd, "_remove_dir",
30
+ ...)` in the existing test suite keeps working unchanged -- the monkeypatch
31
+ only cares which module attribute it is overwriting, never which module
32
+ originally defined the value that attribute pointed at.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import os
37
+ import shutil
38
+ import stat
39
+ import sys
40
+
41
+
42
+ def is_link(path: str) -> bool:
43
+ """Whether `path` is a link that must not be followed -- a POSIX symlink, a
44
+ Windows directory symlink, OR a Windows JUNCTION.
45
+
46
+ **`os.path.islink` is not this test.** On Windows `ntpath.islink` returns
47
+ True only for `IO_REPARSE_TAG_SYMLINK`; a junction is
48
+ `IO_REPARSE_TAG_MOUNT_POINT`, and `stat.S_ISLNK` is False for it as well.
49
+ Measured on this host: for `build/` junctioned at an out-of-tree directory,
50
+ `os.path.islink` and `S_ISLNK` both report False while
51
+ `st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT`. A guard written on
52
+ `os.path.islink` therefore lets a junction reach `shutil.rmtree` -- which
53
+ has its OWN, correct check (`shutil._rmtree_islink`, mirrored here) and
54
+ refuses, so nothing outside the tree is destroyed, but the junction is then
55
+ never cleaned and the run reports a spurious `remove-failed`. This was a
56
+ live defect in the first cut of `tan clean`'s port, caught only by diffing
57
+ against the Rust binary.
58
+ """
59
+ try:
60
+ st = os.lstat(path)
61
+ except (OSError, ValueError):
62
+ return False
63
+ if stat.S_ISLNK(st.st_mode):
64
+ return True
65
+ attributes = getattr(st, "st_file_attributes", 0)
66
+ return bool(
67
+ attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
68
+ and getattr(st, "st_reparse_tag", 0)
69
+ == getattr(stat, "IO_REPARSE_TAG_MOUNT_POINT", -1)
70
+ )
71
+
72
+
73
+ def os_error_text(err: BaseException) -> str:
74
+ """An `OSError` rendered the way Rust's `io::Error` Display renders it:
75
+ `<system message> (os error <code>)`.
76
+
77
+ Python's own `str(OSError)` is `[WinError 32] <message>: '<path>'`, which
78
+ both differs from the oracle and repeats a path the message already names.
79
+ The Windows error code (`winerror`) is preferred over the translated
80
+ `errno`, matching Rust, which reports the raw OS code.
81
+
82
+ One character still differs on Windows: `FormatMessageW` ends its sentences
83
+ with a period and Rust keeps it, while Python's `strerror` strips it. Not
84
+ synthesized here -- guessing at punctuation inside a system message is worse
85
+ than a documented one-character divergence in a warning string.
86
+ """
87
+ if not isinstance(err, OSError):
88
+ return str(err)
89
+ code = getattr(err, "winerror", None) or err.errno
90
+ if err.strerror is None or code is None:
91
+ return str(err)
92
+ return f"{err.strerror} (os error {code})"
93
+
94
+
95
+ #: The two functions `shutil.rmtree` hands its error hook that CANNOT be called
96
+ #: with one positional argument. On POSIX `rmtree` runs the fd-based
97
+ #: `_rmtree_safe_fd` walk, which reports failures of `os.open` (shutil 3.12
98
+ #: lines 682 and 781) and `os.close` (692/712/791/808) through the same hook as
99
+ #: the one-argument `os.scandir`/`os.unlink`/`os.rmdir`/`os.lstat`. `os.open`
100
+ #: needs `flags` and `os.close` takes an fd, not the path the hook is handed --
101
+ #: so retrying either is a `TypeError`, not a repair. Windows' `_rmtree_unsafe`
102
+ #: never passes these, which is why the crash class this guards is POSIX-only.
103
+ _NOT_RETRYABLE_WITH_PATH_ALONE = frozenset({os.open, os.close})
104
+
105
+
106
+ def _reraise_removal_failure(func, path, exc) -> None:
107
+ """Re-raise the failure `shutil.rmtree` reported, so the caller's
108
+ `except (OSError, ValueError)` sees it and can report it as a removal
109
+ failure rather than a silent no-op.
110
+
111
+ `exc` is the exception under `onexc` and an `exc_info` TUPLE under the
112
+ deprecated `onerror` (see `_RMTREE_HOOK`), so both shapes are unwrapped
113
+ here. A hook that has nothing to re-raise still must not return quietly --
114
+ `rmtree` would then report the tree as removed -- so the fallback states the
115
+ operation that failed.
116
+ """
117
+ if isinstance(exc, tuple) and len(exc) == 3:
118
+ exc = exc[1]
119
+ if isinstance(exc, BaseException):
120
+ raise exc
121
+ raise OSError(f"{getattr(func, '__name__', func)} failed on {path}")
122
+
123
+
124
+ def _retry_after_clearing_readonly(func, path, exc=None) -> None:
125
+ """`shutil.rmtree` error hook: clear the read-only bit and retry once.
126
+
127
+ Rust's `remove_dir_all` deletes a read-only file on Windows outright (it
128
+ passes `FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE`), where `shutil.rmtree`
129
+ fails the WHOLE tree with `[WinError 5] Access is denied`. Measured against
130
+ the Rust binary, from `tan clean`'s own port: one read-only file inside
131
+ `build/` had Rust remove the build dir and exit 0 while a naive port left
132
+ every artefact in place and warned. Read-only build outputs -- and, for
133
+ `tan sdk remove`'s own caller, read-only `.git/objects/**` entries left by
134
+ a shallow `git clone` -- are ORDINARY, not exotic, so this is the primary
135
+ path here, not a fallback.
136
+
137
+ `st_mode | S_IWUSR` rather than a bare `S_IWRITE`: on POSIX the latter would
138
+ replace the whole mode with `0o200` and strip the owner's read/execute bits
139
+ from a directory mid-walk. A failure here propagates out of `rmtree` and is
140
+ the caller's to report.
141
+
142
+ The retry is only attempted for a `func` that a path alone can drive
143
+ ([`_NOT_RETRYABLE_WITH_PATH_ALONE`]). It used to end in a bare `func(path)`,
144
+ so a build directory the invoking user OWNS but cannot open -- a
145
+ `chmod -R a-r` or tar-preserved tree, where `os.chmod` SUCCEEDS and
146
+ `os.open` is what failed -- raised `TypeError: open() missing required
147
+ argument 'flags' (pos 2)`. That is neither `OSError` nor `ValueError`, so it
148
+ used to sail past `tan clean`'s best-effort guard, abort the target loop and
149
+ hit its outer catch-all: exit 5, `clean.internal-failure`, and every
150
+ remaining target silently skipped. Measured on the oracle for the same
151
+ tree: exit 0, a warning, and the rest of the run completing -- which is
152
+ what re-raising the original failure (rather than a `TypeError`) restores.
153
+
154
+ **Also clears the PARENT directory's own write bit** (tan-cli#790,
155
+ `sdk.remove`'s own maintainer follow-up correction). On POSIX, `unlink`
156
+ and `rmdir` consult only the CONTAINING directory's write+execute
157
+ permission -- a file or directory's OWN mode bits are irrelevant to
158
+ whether it can be removed. So the chmod above, alone, fixes nothing on
159
+ POSIX for the one case that is genuinely common there: a read-only
160
+ directory (`chmod 555 somedir`, or exactly what a bootstrapped SDK tree's
161
+ `.git/objects/xx/` subdirectories carry after certain git versions'
162
+ packing) defeats the unlink of every child inside it, and `path` here is
163
+ the CHILD's path, whose own mode was never the problem. Measured: with
164
+ only `path` chmod'd, `shutil.rmtree` over a tree containing one such
165
+ directory still raised `PermissionError` on the first child inside it.
166
+ Chmod'ing `os.path.dirname(path)` too -- best-effort, a failure here is
167
+ swallowed rather than propagated, because the ORIGINAL failure (or
168
+ whatever `func(path)` raises next) is the honest one to report if this
169
+ does not fix it -- closes that gap. This is additive to the Windows
170
+ read-only-ATTRIBUTE fix above, not a replacement for it: the two failure
171
+ shapes (an NTFS attribute on the file itself; a POSIX permission bit on
172
+ its parent) are independent and this function now clears both.
173
+ """
174
+ if func in _NOT_RETRYABLE_WITH_PATH_ALONE:
175
+ _reraise_removal_failure(func, path, exc)
176
+ # tan-cli#1482: never follow a link -- chmod'ing through it would change the
177
+ # permissions of a file OUTSIDE the tree being removed, and `os.stat` on a
178
+ # dangling link raised before the parent fix below could run. A link needs
179
+ # only its parent's write bit (POSIX unlink consults the parent alone).
180
+ try:
181
+ if not stat.S_ISLNK(os.lstat(path).st_mode):
182
+ os.chmod(path, os.lstat(path).st_mode | stat.S_IWUSR)
183
+ except OSError:
184
+ pass
185
+ parent = os.path.dirname(path)
186
+ if parent and parent != path:
187
+ try:
188
+ os.chmod(parent, os.stat(parent).st_mode | stat.S_IWUSR | stat.S_IXUSR)
189
+ except OSError:
190
+ pass
191
+ try:
192
+ func(path)
193
+ except TypeError:
194
+ # Belt and braces for a future `shutil` that routes one more
195
+ # many-argument callable through this hook: the removal still failed,
196
+ # and it must be reported as such rather than escaping as a `TypeError`.
197
+ _reraise_removal_failure(func, path, exc)
198
+
199
+
200
+ #: `shutil.rmtree`'s error-hook keyword. `onerror` is deprecated from 3.12 and
201
+ #: scheduled for removal; `onexc` does not exist before it. Selected once here so
202
+ #: the call site stays a single expression on either interpreter -- the handler
203
+ #: signature is compatible because it ignores its third argument, which is the
204
+ #: only thing the two hooks disagree about (`exc_info` tuple vs exception).
205
+ _RMTREE_HOOK = "onexc" if sys.version_info >= (3, 12) else "onerror"
206
+
207
+
208
+ def remove_dir(path: str) -> None:
209
+ """Remove a directory target recursively, never following a link out of the
210
+ tree, and clearing a read-only attribute/bit in the way instead of failing
211
+ the whole removal on it.
212
+
213
+ A link ([`is_link`]) is unlinked ITSELF, exactly as the oracle's
214
+ `remove_dir_all` does on Windows: verified against the Rust binary with
215
+ `build/` junctioned at an out-of-tree directory -- the junction goes, the
216
+ target's contents stay. `shutil.rmtree` handles the ordinary case and never
217
+ recurses through a link INSIDE the tree, so both arms are contained.
218
+
219
+ `os.rmdir` before `os.unlink`: on Windows a junction or directory symlink is
220
+ removed by `RemoveDirectory`, and `unlink` fails on it; on POSIX `rmdir`
221
+ fails on a symlink and `unlink` is what removes it.
222
+ """
223
+ if is_link(path):
224
+ try:
225
+ os.rmdir(path)
226
+ except OSError:
227
+ os.unlink(path)
228
+ return
229
+ shutil.rmtree(path, **{_RMTREE_HOOK: _retry_after_clearing_readonly})
tan/core/doctor_git.py ADDED
@@ -0,0 +1,172 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """SDK-provenance git helpers, extracted out of `tan.commands.doctor_cmd`
3
+ (tan-cli#797 module-size budget) rather than left to grow that file further.
4
+
5
+ Every spawn here resolves `git` through `tan.core.tool_lookup.resolve_tool`
6
+ FIRST and threads the result through as `executable=` on the actual
7
+ `subprocess`/`probe` call -- never a bare `"git"` argv[0], which on Windows
8
+ `CreateProcess` searches the current directory for ahead of `%PATH%`
9
+ (tan-cli#797, the same hazard class tan-cli#567/#488 closed for
10
+ flash/size/west). `doctor_cmd` re-exports every name here (a plain
11
+ `from tan.core.doctor_git import ...`) so `doctor_cmd._resolve_git_executable`
12
+ etc. keep resolving for every existing caller and test.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import subprocess
18
+ from pathlib import Path
19
+
20
+ from tan.core.probe import PROBE_TIMEOUT_S, isolated_cwd, probe
21
+ from tan.core.subprocess_env import spawn_env
22
+ from tan.core.tool_lookup import resolve_tool
23
+
24
+
25
+ def _resolve_git_executable() -> str | None:
26
+ """The absolute path to `git`, resolved through the shared
27
+ `tool_lookup.resolve_tool` walk -- never `shutil.which`, which on Windows
28
+ inserts the current directory ahead of `%PATH%` (see `tool_lookup`'s own
29
+ docstring). `None` when `git` is not on PATH at all, which every caller
30
+ below must treat as "no git provenance to report", not a crash
31
+ (tan-cli#797): a doctor that raises because git is absent is worse than
32
+ the bare-argv0 spawn it replaces.
33
+ """
34
+ return resolve_tool("git", os.environ).resolved
35
+
36
+
37
+ def _is_own_git_checkout(root: str, git_exe: str | None) -> bool:
38
+ """Whether `root` is itself the TOP of a git checkout, not merely nested
39
+ somewhere inside an unrelated ENCLOSING one (tan-cli#488 defect 4).
40
+
41
+ `git -C <root> ...` discovery walks UPWARD looking for a `.git`, so an SDK
42
+ with no `.git` of its own -- an extracted release archive, vendored under
43
+ a customer's own application repository, which this port explicitly
44
+ supports (`tan.commands.sdk_cmd.check_sdk_readiness`'s own docstring names
45
+ exactly this shape) -- answers every git query with the ENCLOSING repo's
46
+ state instead of "not a checkout". `_git_short_commit`/`_git_behind_
47
+ upstream` below both call this FIRST and refuse (`None`) rather than
48
+ misattribute a foreign repository's commit or upstream-skew as the SDK's
49
+ own.
50
+
51
+ Compares the RESOLVED `--show-toplevel` against the RESOLVED `root`:
52
+ lexical string comparison alone would false-negative on a symlinked or
53
+ differently-cased path to the same directory.
54
+
55
+ `git_exe` -- the PATH-RESOLVED absolute `git`, from `_resolve_git_executable`
56
+ (tan-cli#797). `None` when `git` is not on PATH at all: refuses outright
57
+ rather than falling back to a bare `probe(["git", ...])`, which would
58
+ re-open the exact cwd-shadowing hazard this resolution exists to close.
59
+ """
60
+ if git_exe is None:
61
+ return False
62
+ top = probe(["git", "-C", root, "rev-parse", "--show-toplevel"], executable=git_exe)
63
+ if top is None:
64
+ return False
65
+ try:
66
+ return Path(top.strip()).resolve() == Path(root).resolve()
67
+ except OSError:
68
+ return False
69
+
70
+
71
+ def _git_short_commit(root: str, git_exe: str | None) -> str | None:
72
+ """`git -C <root> rev-parse --short HEAD`, or `None` when `root` is not a
73
+ git checkout of its own (e.g. an extracted SDK release archive -- including
74
+ one vendored inside a customer's own git repository, see
75
+ `_is_own_git_checkout`) or `git_exe` is `None` (git not on PATH)."""
76
+ if not _is_own_git_checkout(root, git_exe):
77
+ return None
78
+ out = probe(["git", "-C", root, "rev-parse", "--short", "HEAD"], executable=git_exe)
79
+ if out is None:
80
+ return None
81
+ commit = out.strip()
82
+ return commit or None
83
+
84
+
85
+ def _git_behind_upstream(root: str, git_exe: str | None) -> int | None:
86
+ """Commit count `HEAD` is behind its upstream tracking ref, without
87
+ fetching. `None` when there is no upstream, `root` is not a git checkout
88
+ of its own (see `_is_own_git_checkout`), or `git_exe` is `None` (git not
89
+ on PATH)."""
90
+ if not _is_own_git_checkout(root, git_exe):
91
+ return None
92
+ out = probe(
93
+ ["git", "-C", root, "rev-list", "--count", "HEAD..@{upstream}"], executable=git_exe
94
+ )
95
+ if out is None:
96
+ return None
97
+ try:
98
+ return int(out.strip())
99
+ except ValueError:
100
+ return None
101
+
102
+
103
+ def classify_git_core_longpaths(exit_code: int | None, stdout: str) -> bool | None:
104
+ """The three-way verdict for a `git config --get core.longpaths`
105
+ invocation -- the git-side counterpart to `doctor_cmd._long_paths_enabled`'s
106
+ registry read, split out as its own pure function (mirroring
107
+ `tan_core::host_env::classify_git_core_longpaths`) so the exact mapping
108
+ tan-cli#306 argues hardest about is unit-tested without needing a real
109
+ `git` invocation for every case.
110
+
111
+ * exit 0 -> the stdout value, parsed with git's own boolean grammar.
112
+ * exit 1 -> `False`. `git config --get` documents this code as "the key
113
+ is not set in any scope (system/global/local)" -- git's own default,
114
+ and the state a fresh `HOME` is in (tan-cli#306's exact repro).
115
+ * anything else (`git` not on PATH, a malformed config file, a
116
+ permissions error) -> `None`: uncertain, not guessed.
117
+ """
118
+ if exit_code == 0:
119
+ value = stdout.strip().lower()
120
+ return value not in ("false", "no", "off", "0")
121
+ if exit_code == 1:
122
+ return False
123
+ return None
124
+
125
+
126
+ def _git_core_longpaths(git_exe: str | None) -> bool | None:
127
+ """Read git's own EFFECTIVE `core.longpaths` (system -> global precedence
128
+ -- NOT the project repo's local config, see below -- resolved by `git config --get` itself rather than tan
129
+ re-implementing that precedence by hand) via a real `git` subprocess.
130
+
131
+ A SEPARATE axis from `doctor_cmd._long_paths_enabled` on purpose
132
+ (tan-cli#306): the registry governs manifested Win32 API calls; it does
133
+ nothing for git, which `west update` uses for every project
134
+ clone/checkout and which refuses a long path unless ITS OWN setting says
135
+ so -- the registry read alone reported `pass` on a fresh `HOME` while
136
+ `west update` died on `hal_nxp`'s `tf-psa-crypto` tree.
137
+
138
+ Not built on `tan.core.probe.probe`: `probe()` collapses "ran and exited
139
+ non-zero" (exit 1, meaning "unset") and "could not run at all" (meaning
140
+ "unknown") to the same `None`, and `classify_git_core_longpaths` needs to
141
+ tell those apart.
142
+
143
+ `git_exe` -- the PATH-RESOLVED absolute `git` from `_resolve_git_executable`
144
+ (tan-cli#797). `None` when git is not on PATH: answers "unknown" rather
145
+ than spawning the bare `"git"` this used to hand straight to
146
+ `subprocess.run`, which on Windows searches the CURRENT DIRECTORY (the
147
+ customer project's, since doctor never `chdir`s) ahead of `%PATH%`.
148
+ """
149
+ if git_exe is None:
150
+ return None
151
+ try:
152
+ # Run from a fresh EMPTY directory (not inside any repository): from the
153
+ # project's cwd `git config --get` also merges that repo's LOCAL config,
154
+ # but `west update` clones fresh module repos that never inherit it, so
155
+ # only the system/global scopes predict whether those clones succeed.
156
+ with isolated_cwd() as empty:
157
+ out = subprocess.run(
158
+ ["git", "config", "--get", "core.longpaths"],
159
+ cwd=empty,
160
+ executable=git_exe,
161
+ capture_output=True,
162
+ text=True,
163
+ encoding="utf-8",
164
+ errors="replace",
165
+ stdin=subprocess.DEVNULL,
166
+ timeout=PROBE_TIMEOUT_S,
167
+ env=spawn_env(),
168
+ check=False,
169
+ )
170
+ except (OSError, ValueError, subprocess.SubprocessError):
171
+ return None
172
+ return classify_git_core_longpaths(out.returncode, out.stdout)