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,415 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Render a host-native filesystem path as a valid URI reference
3
+ (tan-cli#1097).
4
+
5
+ `validate`'s `--format diagnostic-v1` (LSP `uri`) and `--format sarif`
6
+ (`artifactLocation.uri`) both carry a field that is a URI reference by spec,
7
+ not a filesystem path -- SARIF 2.1.0 says so explicitly for
8
+ `artifactLocation.uri`, and LSP diagnostics are keyed by document URI, which
9
+ an editor compares against the URI of an open buffer. Both sites used to emit
10
+ `board_path` bare. On Windows that renders `C:\\w\\proj\\board.yaml`: the
11
+ drive-letter colon and the backslashes are not valid in that position, and
12
+ the string carries no scheme, so neither consumer can resolve it -- silently,
13
+ with no crash or error, exactly the class of defect tan-cli#1073/#1077/#1082/
14
+ #1084 already closed elsewhere in this repo.
15
+
16
+ This is a RENDERING concern of those two exporters, not a resolution one, so
17
+ it lives here rather than in `tan.core.board_context.resolve_board_path`
18
+ (which PR #1090 built to answer "where is the file", with a deliberate
19
+ separator-follows-root rule) or in `validate_cmd.py` itself. Only the two
20
+ `uri` fields are meant to go through [`path_to_uri_reference`]:
21
+ `data.boardYamlPath`, the `--input` argv passed to the SDK validator
22
+ subprocess, and every other `Path()` consumer in this repo stay host-native
23
+ on purpose -- `tests/commands/test_validate_command.py`'s
24
+ `test_absolute_board_yaml_uri_is_a_file_uri_but_boardyamlpath_stays_host_
25
+ native` asserts that split so a future "tidy this up" sweep does not unify
26
+ the two contracts (NOT this module's own test file -- that file proves
27
+ [`path_to_uri_reference`] in isolation and has no `boardYamlPath` of its own
28
+ to assert against).
29
+
30
+ ## The relative case is left relative -- deliberately, and scoped narrowly
31
+
32
+ A relative `path` (`resolve_board_path`'s own default, `"./board.yaml"`) is
33
+ returned as a relative reference rather than absolutised. It is already a
34
+ legal relative URI reference per RFC 3986 SS4.2 -- SARIF 2.1.0's
35
+ `artifactLocation.uri` accepts one (`format: uri-reference`). alp-sdk's
36
+ `metadata/schemas/diagnostic-v1.schema.json` (NOT this repo's -- it lives in
37
+ alp-sdk, `git ls-files` here has zero hits for it) types the analogous field
38
+ as a bare `"type": "string"` and accepts a raw filesystem path too, so it
39
+ constrains nothing either way and is not cited as support for staying
40
+ relative. Absolutising `path` here would (a) move the pre-existing pinned
41
+ golden (`test_validate_command.py`'s `"./board.yaml"` pins) and (b) bake the
42
+ process's CWD into an otherwise portable document.
43
+
44
+ **This module used to make NO claim about how a relative reference gets
45
+ resolved -- tan-cli#1117 closes that gap.** An earlier version of this
46
+ docstring (tan-cli#1097 review round 1) tried the same thing by having
47
+ `validate_cmd._sarif_document` declare a SARIF `originalUriBaseIds`/
48
+ `uriBaseId` pair -- round 2 review found that addition declared a base that
49
+ did not actually resolve the reference in the default case (an anchoring
50
+ mismatch between `root` and the CWD `board_path` is actually relative to,
51
+ compounded by a missing trailing slash), and reached that wrong answer
52
+ through an unguarded `Path.resolve()` that could raise on a caller-supplied
53
+ `--project` containing a symlink loop, crashing the command it touched.
54
+ Declaring an authoritatively WRONG base is worse than declaring none --
55
+ round 1's undefined base at least let a spec-conformant consumer fall back to
56
+ its own CWD and succeed. That work was reverted rather than patched a third
57
+ time in place; tan-cli#1117 was filed with both measurements (the wrong-base
58
+ `urljoin` result and the `Path.resolve(strict=False)` symlink-loop crash) so
59
+ whoever picked it up next would not rediscover either.
60
+
61
+ [`cwd_base_uri`] is that pick-up: it anchors on `Path.cwd()` -- never
62
+ `root`, which is what produced the anchoring mismatch -- and never resolves
63
+ a caller-supplied path, so the symlink-loop crash class cannot recur here.
64
+ `validate_cmd._sarif_document` wires it into `originalUriBaseIds`, gated on
65
+ [`is_absolute_path_reference`] so an absolute `--board-yaml` reference (which
66
+ already resolves on its own) never carries a `uriBaseId` SARIF 2.1.0 SS3.4.4
67
+ says it must not.
68
+ """
69
+ from __future__ import annotations
70
+
71
+ import os
72
+ import re
73
+ from pathlib import Path, PurePosixPath, PureWindowsPath
74
+ from urllib.parse import quote, quote_from_bytes
75
+
76
+ #: A Windows drive letter at the string's own start (`C:...`, no backslash
77
+ #: required yet) -- `resolve_board_path` never emits this bare, but
78
+ #: `--board-yaml` is caller-supplied and this oracle should not miss it.
79
+ #: Review round 1 measured this constant survives a mutation that deletes
80
+ #: it (`_is_windows_spelled` collapsing to `"\\" in path` alone): its only
81
+ #: DISTINCT input is a forward-slash-spelled drive path (`"c:/w/proj/
82
+ #: board.yaml"` -- a colon with no backslash anywhere), which the mutant
83
+ #: renders unchanged, i.e. with a first-segment colon a URI-reference
84
+ #: consumer parses as a scheme. `test_a_forward_slash_spelled_drive_path_
85
+ #: is_still_judged_windows` pins that input; see this module's own test
86
+ #: file for the per-branch (not whole-function) mutation that catches it.
87
+ #:
88
+ #: The leading `^` here is REDUNDANT with `.match()` (`_is_windows_spelled`
89
+ #: below uses `.match()`, not `.search()`) -- `re.Pattern.match()` already
90
+ #: only tries at the string's own start regardless of `^`, so removing `^`
91
+ #: alone changes nothing for any input (round 1 review round 2 measured
92
+ #: this directly: un-anchoring the compiled pattern is behaviourally
93
+ #: indistinguishable from the original under `.match()` for every input
94
+ #: tried). A genuine misclassification of a mid-string colon (e.g.
95
+ #: `"sub/board:1.yaml"`) needs BOTH the anchor removed from the pattern AND
96
+ #: the call switched to `.search()` at once -- this property is doubly
97
+ #: defended, not fragile, and `test_a_colon_later_in_a_relative_path_does_
98
+ #: not_misclassify_it_as_windows` (this module's test file) states it
99
+ #: directly rather than chasing a single-line mutation that does not exist.
100
+ _WINDOWS_DRIVE_RE = re.compile(r"^[A-Za-z]:")
101
+
102
+
103
+ def _is_windows_spelled(path: str) -> bool:
104
+ """Whether `path` is WINDOWS-spelled, judged from the string itself
105
+ rather than `os.name` (tan-cli#1089/#1090's own rule): a Windows-spelled
106
+ path reaches [`path_to_uri_reference`] on EVERY host, not only on the
107
+ `windows-latest` leg, so the oracle (`PureWindowsPath` vs
108
+ `PurePosixPath`) must come from the path's own spelling and never from
109
+ the running host's. Reading `os.name` would make the ubuntu and macos
110
+ legs answer the POSIX question about a Windows path -- which is the same
111
+ failure [`_absolute_path_to_file_uri`] below records for `Path.as_uri()`,
112
+ and it does not disappear on a Windows runner, it MOVES.
113
+
114
+ An earlier version of this docstring justified the rule with "this repo
115
+ has no Windows host". That is false and is corrected repo-wide in
116
+ tan-cli#1153: `parity.yml`'s `python-tests-shard` job runs this suite on
117
+ `windows-latest` (its matrix `os:` carries all three platforms, with
118
+ `runs-on: ${{ matrix.os }}`) on every `pull_request`. The rule above is
119
+ right for its own reason, not for that one.
120
+
121
+ A LEADING `/` is decided POSIX before any later backslash is even
122
+ consulted (review round 1 MAJOR 1, a real regression this fixed): a
123
+ backslash is a perfectly legal character inside a POSIX filename, e.g.
124
+ `/tmp/proj/we\\ird.yaml`. The pre-fix version of this function read
125
+ `"\\" in path` unconditionally and misrouted that exact path to the
126
+ Windows branch, which does not consider it absolute (`PureWindowsPath(
127
+ "/tmp/proj/we\\ird.yaml").is_absolute()` is `False` -- pathlib requires
128
+ a drive too), so it fell to the RELATIVE Windows arm and had its
129
+ backslash swapped for a slash: `/tmp/proj/we/ird.yaml`, silently naming
130
+ a DIFFERENT file than the one on disk, and losing its `file:` scheme
131
+ into the bargain. A leading `/` never appears at the start of a
132
+ Windows-spelled string this repo's own resolver produces (a Windows
133
+ drive path starts with a letter, a UNC path starts with `\\\\`), so this
134
+ is a safe, targeted disambiguator, not a heuristic that trades one
135
+ misclassification for another."""
136
+ if path.startswith("/"):
137
+ return False
138
+ return "\\" in path or bool(_WINDOWS_DRIVE_RE.match(path))
139
+
140
+
141
+ def _absolute_path_to_file_uri(path: PurePosixPath | PureWindowsPath) -> str:
142
+ """`path` -- which MUST be absolute by its own oracle's rules -- rendered
143
+ as an absolute `file:` URI, host-independently.
144
+
145
+ This is `pathlib.PurePath.as_uri()`'s own body, verbatim, minus the
146
+ `DeprecationWarning` 3.14 added to it. `PurePath.as_uri()` is deprecated
147
+ and scheduled for REMOVAL in Python 3.19 (tan-cli#1140), and it was this
148
+ module's exporter -- so the warning fired from the product module, not
149
+ only from tests, and the removal would break `tan validate --format
150
+ sarif`'s `artifactLocation.uri` outright. Carrying the body here retires
151
+ the deprecated call while keeping the emitted bytes identical;
152
+ `tests/core/test_uri_reference.py` pins that byte-identity against the
153
+ stdlib itself, per input, on every supported interpreter.
154
+
155
+ **The deprecation's own named replacement, `pathlib.Path.as_uri()`,
156
+ cannot be used here**, and that is measured, not assumed: from 3.14 it
157
+ delegates to `urllib.request.pathname2url(str(self), add_scheme=True)`,
158
+ which dispatches on `os.name`, so the conversion it performs is the
159
+ RUNNING HOST's, not the one the path is spelled in. The damage does not
160
+ disappear on a Windows runner, it MOVES. Measured on 3.14.7 from a POSIX
161
+ host, all nine Windows-spelled shapes in the test corpus come out wrong
162
+ (`C:\\w\\proj\\board.yaml` -> `"file:C%3A%5Cw%5Cproj%5Cboard.yaml"`
163
+ rather than `"file:///C:/w/proj/board.yaml"`) while every POSIX-spelled
164
+ one survives; under a simulated `os.name = "nt"` that inverts, and one of
165
+ the POSIX shapes it then breaks is `/tmp/proj/we\\ird.yaml` ->
166
+ `"file:///tmp/proj/we/ird.yaml"` -- silently naming a DIFFERENT file,
167
+ which is round 1 MAJOR 1, the regression [`_is_windows_spelled`] exists
168
+ to prevent. Below 3.14 there is no divergence on either host, and that is
169
+ what would have made the swap dangerous. The full four-cell account, and
170
+ the assertion that keeps it honest, are on the test file's
171
+ `test_pathlib_path_as_uri_is_not_a_usable_replacement_for_this_exporter`.
172
+
173
+ The deeper reason is structural: `Path` is CONCRETE and host-bound -- a
174
+ `PureWindowsPath` cannot become a `WindowsPath` on a POSIX host at all --
175
+ while [`_is_windows_spelled`] deliberately picks its oracle from the path
176
+ STRING, so BOTH branches are exercised on every runner rather than only
177
+ the one matching that runner's `os.name`. Routing the export through
178
+ `Path` hands that choice back to the host, which is the defect class
179
+ tan-cli#1105 and PR #1125 were bitten by.
180
+
181
+ `os.fsencode` below is the one host-sensitive call, and it is inherited
182
+ verbatim from the stdlib body rather than introduced: it resolves to
183
+ utf-8/`surrogateescape` on POSIX and utf-8/`surrogatepass` on Windows,
184
+ which differ only for a `str` carrying lone surrogates. Keeping it is
185
+ what makes this byte-identical to the call it replaces; it is not a
186
+ branch on `os.name`, and [`_is_windows_spelled`]'s "never the host's
187
+ rules" premise is untouched -- nothing below consults the host to decide
188
+ which SPELLING to emit.
189
+
190
+ The `.is_absolute()` guard is kept, and kept raising, for the reason
191
+ [`path_to_uri_reference`]'s docstring gives: the exporter must enforce
192
+ the very predicate its caller gates on, so gate and exporter cannot
193
+ drift apart. Dropping it would let a relative path through as a
194
+ malformed `file:` URI instead of the `ValueError` the caller's own
195
+ branch is written against."""
196
+ if not path.is_absolute():
197
+ raise ValueError("relative path can't be expressed as a file URI")
198
+
199
+ drive = path.drive
200
+ if len(drive) == 2 and drive[1] == ":":
201
+ # A path on a local drive => `file:///c:/a/b`.
202
+ prefix = "file:///" + drive
203
+ rest = path.as_posix()[2:]
204
+ elif drive:
205
+ # A path on a network drive => `file://host/share/a/b`.
206
+ prefix = "file:"
207
+ rest = path.as_posix()
208
+ else:
209
+ # A POSIX path => `file:///etc/hosts`.
210
+ prefix = "file://"
211
+ rest = str(path)
212
+ return prefix + quote_from_bytes(os.fsencode(rest))
213
+
214
+
215
+ def path_to_uri_reference(path: str) -> str:
216
+ """`path` rendered as a valid URI reference (RFC 3986).
217
+
218
+ [`_absolute_path_to_file_uri`] is the exporter for the absolute case,
219
+ and it carries the gate too: `.is_absolute()`, NOT `ntpath.isabs`/
220
+ `posixpath.isabs`. The load-bearing reason is that `.is_absolute()` IS
221
+ the predicate the exporter itself enforces, so the gate and the exporter
222
+ cannot disagree -- on any interpreter, now or later. Gating on a
223
+ DIFFERENT notion of "absolute" would call the exporter on a path it then
224
+ raises `ValueError` on. (That exporter used to be `PurePath.as_uri()`
225
+ called directly here; 3.14 deprecates it for removal in 3.19, so
226
+ tan-cli#1140 carried its body into this module byte-for-byte, guard
227
+ included, rather than let a removal take out `tan validate --format
228
+ sarif`'s `artifactLocation.uri`. See that function's own docstring for
229
+ why the deprecation's named replacement, `Path.as_uri()`, is not usable
230
+ here.)
231
+
232
+ A weaker reason used to stand here, and it has since expired
233
+ (tan-cli#1126). It said the two oracles simply disagree for a
234
+ rooted-but-driveless Windows path -- `ntpath.isabs(r"\\proj\\x")` is
235
+ `True` ("absolute on the current drive") against
236
+ `PureWindowsPath(r"\\proj\\x").is_absolute()`'s `False` (pathlib
237
+ requires BOTH a drive and a root) -- stated as a fixed stdlib fact.
238
+ It is not one. Measured on 3.12.3 / 3.13.15 / 3.14.7: `ntpath.isabs`
239
+ answers that shape `True` on 3.12 and `False` from **3.13** on, where
240
+ CPython made it agree with pathlib; `.is_absolute()` is `False` on all
241
+ three, unchanged. So on every supported interpreter but the `>=3.12`
242
+ floor itself the two now AGREE -- which changes nothing about this
243
+ gate, precisely because the choice never rested on them differing.
244
+ Two tests in `tests/core/test_uri_reference.py` had encoded the 3.12
245
+ answer as universal and failed the full suite on 3.14.7 for it; the
246
+ version-dependent fact now lives in one measured constant there
247
+ (`_NTPATH_ISABS_ACCEPTS_A_DRIVELESS_ROOT`), asserted on both sides of
248
+ the boundary rather than restated in prose here.
249
+
250
+ * An ABSOLUTE `path` becomes an absolute `file:` URI --
251
+ `file:///C:/w/proj/board.yaml` for the Windows-spelled root the issue
252
+ opened on, `file:///w/proj/board.yaml` for a POSIX one -- which is what
253
+ both consumers actually parse as a URI reference. Which oracle renders
254
+ it is decided by [`_is_windows_spelled`], not `os.name`: `PureWindowsPath`
255
+ for a Windows-spelled path, `PurePosixPath` otherwise. (Plain `Path`
256
+ would use the CI HOST's rules and misjudge a Windows-spelled string on
257
+ this repo's Linux CI -- the same trap PR #1089/#1090 already named.)
258
+ [`_absolute_path_to_file_uri`] itself percent-encodes non-ASCII and
259
+ reserved bytes (a space, a literal backslash inside a POSIX filename,
260
+ `#`, non-ASCII -- all measured), so nothing further is needed on this
261
+ branch.
262
+ * A RELATIVE, POSIX-spelled `path` (`resolve_board_path`'s own default,
263
+ `"./board.yaml"`) is returned percent-encoded but otherwise UNCHANGED
264
+ -- see this module's docstring's "relative case" section for why it
265
+ stays relative instead of being absolutised. `urllib.parse.quote`
266
+ (`safe="/"`, so the separator itself is untouched) closes the
267
+ remaining gap round 1 measured: a raw space or `#` in a relative
268
+ `path` is not a legal URI-reference character/is a fragment
269
+ delimiter, and the separator-less golden (`"./board.yaml"`) contains
270
+ neither, so quoting it is a no-op and the pin is untouched.
271
+ * A RELATIVE, WINDOWS-spelled `path` (`resolve_board_path("sub\\p",
272
+ None)`'s shape, `"sub\\p\\board.yaml"`) has its backslashes swapped for
273
+ forward slashes, then the same percent-encoding applied. A bare
274
+ backslash is not a legal character in a URI reference at all (RFC
275
+ 3986's `pchar` excludes it unencoded) -- leaving it in would still be
276
+ invalid, just differently invalid, and Windows itself accepts `/` as
277
+ an equally valid path separator, so the swap is meaning-preserving.
278
+
279
+ Known, deliberately undefended edge cases (round 1 nits, low
280
+ reachability from this repo's own resolver): a `\\\\?\\`-prefixed
281
+ Windows extended-length path renders its `\\\\?\\` host segment through
282
+ [`_absolute_path_to_file_uri`] unexamined (`file://%3F/C%3A/...`, the
283
+ drive demoted into the path) rather than being rejected or normalised;
284
+ and drive-letter casing is preserved verbatim from the input rather than
285
+ canonicalised, so `"c:/w/x"` and `"C:\\w\\x"` for the same file produce
286
+ differently-cased URIs. Neither is exercised by anything
287
+ `resolve_board_path` itself produces.
288
+ """
289
+ if _is_windows_spelled(path):
290
+ win = PureWindowsPath(path)
291
+ if win.is_absolute():
292
+ return _absolute_path_to_file_uri(win)
293
+ return quote(path.replace("\\", "/"), safe="/")
294
+ posix = PurePosixPath(path)
295
+ if posix.is_absolute():
296
+ return _absolute_path_to_file_uri(posix)
297
+ return quote(path, safe="/")
298
+
299
+
300
+ def is_absolute_path_reference(path: str) -> bool:
301
+ """Whether [`path_to_uri_reference`] renders `path` as an ABSOLUTE
302
+ `file:` URI -- the gate `_sarif_document` (tan-cli#1117) uses to decide
303
+ whether a location may carry SARIF's `uriBaseId` at all: 2.1.0 SS3.4.4
304
+ -- "If the uri property is an absolute URI, the uriBaseId property SHALL
305
+ be absent."
306
+
307
+ Delegates to [`path_to_uri_reference`] itself rather than RE-DERIVING
308
+ the same `_is_windows_spelled` + `.is_absolute()` branch a second time:
309
+ a `file:` scheme in that function's OUTPUT is produced by exactly its
310
+ two [`_absolute_path_to_file_uri`] calls, both already gated on the
311
+ identical `is_absolute()` check a second copy here would only
312
+ duplicate -- drifting the two apart the moment [`path_to_uri_reference`]
313
+ grows a branch this one does not learn about is exactly the
314
+ per-command-copy class of defect `tan.core.board_context`'s own module
315
+ docstring names for a resolver retyped instead of shared.
316
+
317
+ A first version of this docstring worried the sniff was unsound for a
318
+ driveless Windows path (`"C:board.yaml"`) on the theory that its
319
+ rendered form is "quoted unchanged" and therefore a syntactically valid
320
+ URI with scheme `C`. Measured, that is false: `quote(..., safe="/")`
321
+ does not treat `:` as safe, so `path_to_uri_reference("C:board.yaml")`
322
+ is actually `"C%3Aboard.yaml"` -- the colon is percent-encoded, and
323
+ `.startswith("file:")` on it is `False`, correctly. The SAME encoding
324
+ rule is what makes the sniff safe in general: a `file:` PREFIX can only
325
+ ever appear in the output through the two absolute-only
326
+ [`_absolute_path_to_file_uri`] branches -- any RELATIVE reference
327
+ containing a literal `"file:"` substring has its colon percent-encoded
328
+ before this check ever sees it
329
+ (`path_to_uri_reference("file:sub/board.yaml") == "file%3Asub/board
330
+ .yaml"`, measured), so it can never masquerade as one."""
331
+ return path_to_uri_reference(path).startswith("file:")
332
+
333
+
334
+ def cwd_base_uri() -> str:
335
+ """The SARIF `originalUriBaseIds` base for a RELATIVE `artifactLocation
336
+ .uri` (tan-cli#1117): the process's own working directory, as an
337
+ absolute `file:` URI with a TRAILING SLASH. **Can raise** -- see the
338
+ last section below; [`cwd_base_uri_or_none`] is the guarded call site
339
+ should actually use.
340
+
341
+ **Anchored on `Path.cwd()`, never on `--project`/`root`.**
342
+ `resolve_board_path` already prepends `root` onto the relative reference
343
+ itself (`--project sub` renders `"sub/board.yaml"`, not `"board.yaml"`),
344
+ so the reference is CWD-relative by construction. A base ALSO anchored on
345
+ `root` would apply that prefix a SECOND time -- `urljoin("file://.../sub/
346
+ ", "sub/board.yaml")` names `.../sub/sub/board.yaml`, which does not
347
+ exist -- reproducing the exact tan-cli#1097-round-2 defect this issue was
348
+ filed to fix (measured there: `root="."`'s base resolved `./board.yaml`
349
+ onto the WRONG file because the base and the reference were anchored to
350
+ two different roots). `tests/core/test_uri_reference.py`'s mutation for
351
+ this line swaps `Path.cwd()` for a `root`-derived path and reds on the
352
+ `--project sub` case for exactly that reason.
353
+
354
+ **The trailing slash is not decorative, and not unconditional.** RFC 3986
355
+ SS5.3's reference-resolution algorithm drops the base's own LAST PATH
356
+ SEGMENT before appending a relative reference: `urljoin("file:///a/b",
357
+ "c")` names `"file:///a/c"`, not `"file:///a/b/c"` -- a slash-less base
358
+ names a RESOURCE, indistinguishable from a file, one level above the
359
+ directory it reads as. Dropping the slash entirely reproduces the same
360
+ wrong-file mismatch tan-cli#1117 measured (`EXISTS? False` against the
361
+ real, differently-pathed file). But `Path.as_uri()` DOES already append
362
+ one at a filesystem root -- measured, `Path("/").as_uri() ==
363
+ "file:///"` -- so appending UNCONDITIONALLY doubles it there
364
+ (`"file:////"`, and `PureWindowsPath("C:/").as_uri()` -> `"file:///C://"`
365
+ the same way). SARIF 2.1.0 SS3.14.14 requires the value "SHALL end with a
366
+ single forward slash", NOTE 2 explaining that resolution works "by
367
+ simple concatenation" -- a doubled slash breaks exactly that
368
+ concatenation (`"file:////" + "./board.yaml"` is not the intended path,
369
+ even though `urljoin` itself tolerates it and recovers). Appending only
370
+ when not already slash-terminated keeps both properties -- always ends
371
+ in exactly one slash, never two.
372
+
373
+ **`Path.cwd()` CAN raise, even though it touches no caller-supplied
374
+ data.** `Path.cwd()` -> `os.getcwd()` raises `FileNotFoundError` (an
375
+ `OSError` subclass) when the process's OWN working directory has been
376
+ removed out from under it -- measured directly. This function does not
377
+ guard it: [`cwd_base_uri_or_none`] is the call site `validate_cmd.py`
378
+ actually uses, and it exists SPECIFICALLY because tan-cli#1117 review
379
+ round 2 measured that an unguarded call here reached `_sarif_document`
380
+ from INSIDE `validate_cmd.py`'s own `except Exception as err:` handler
381
+ and double-faulted the same way the reverted round-1 attempt's
382
+ `Path.resolve(strict=False)` symlink-loop crash did -- a `#1116`
383
+ never-raises instance reached through a filesystem call that happens not
384
+ to be caller-supplied, rather than through caller data itself. (For the
385
+ record, since it is the OTHER crash surface tan-cli#1117 was filed over:
386
+ `.resolve()`'s own symlink-loop behaviour is not uniform across pathlib
387
+ versions either -- measured directly, `python-build-standalone`
388
+ 3.12.3/3.13.15/3.14.7: on 3.12, `Path("loop").resolve(strict=False)`
389
+ raises `RuntimeError` regardless of `strict=False`'s own promise not to;
390
+ on 3.13/3.14 that promise IS kept for `strict=False` (returns the path
391
+ unresolved, no raise), but `strict=True` still raises `OSError [Errno
392
+ 40]` on both. This function calls neither `.resolve()` nor anything on a
393
+ caller-supplied path at all, so that particular crash class cannot recur
394
+ here regardless.)"""
395
+ base = Path.cwd().as_uri()
396
+ return base if base.endswith("/") else base + "/"
397
+
398
+
399
+ def cwd_base_uri_or_none() -> str | None:
400
+ """[`cwd_base_uri`], guarded against the one way it can raise: the
401
+ process's own working directory has been removed. `validate_cmd.py`'s
402
+ `validate()` calls this ONCE, before any guard in its own body can fire,
403
+ and threads the result through `_emit`/`_sarif_document` as a plain
404
+ value -- so nothing downstream of this call touches the filesystem for
405
+ the SARIF base again, and nothing there can raise from a missing CWD no
406
+ matter which branch of `validate()` eventually reaches the SARIF
407
+ exporter (tan-cli#1117 review round 2's blocker).
408
+
409
+ `None` means exactly what `dev` emitted before tan-cli#1117: a relative
410
+ `uri` with no declared base -- the safe fallback, not a degraded one;
411
+ see `_sarif_document`'s own docstring for how it is threaded through."""
412
+ try:
413
+ return cwd_base_uri()
414
+ except OSError:
415
+ return None
@@ -0,0 +1,152 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """User `-D NAME=VALUE` on a PLANNED build (tan-cli#1382).
3
+
4
+ A planned (`board.yaml`) build's Zephyr slice carries argv the planner emitted
5
+ (`west build -b <board> <app> -- -DPython3_EXECUTABLE=... -DEXTRA_CONF_FILE=...`).
6
+ alp-sdk examples document extra CMake definitions on top of that
7
+ (`-DSHIELD=...`, `-DCONFIG_...=...`), so `tan build -D` splices the user's
8
+ definitions into the plan BEFORE token substitution and dispatch:
9
+
10
+ * user `-D` goes AFTER the plan-emitted args, so CMake's last-wins lets it
11
+ override anything the plan set -- except the keys tan owns for correctness;
12
+ * `EXTRA_CONF_FILE` / `EXTRA_DTC_OVERLAY_FILE` (and a sysbuild `<image>_`
13
+ prefixed form) are lists tan already feeds (the per-core `alp.conf`), so the
14
+ user's value is APPENDED `;`-joined to the plan's, never a replacement;
15
+ * `BOARD` / `Python3_EXECUTABLE` are refused -- tan derives both.
16
+
17
+ Pure data-shaping: nothing here resolves or spawns anything.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import re
23
+
24
+ #: Keys tan owns outright; a user value would desynchronise the build.
25
+ RESERVED_KEYS = frozenset({"BOARD", "Python3_EXECUTABLE"})
26
+ #: Keys whose user value joins tan's list (`;`, a CMake list).
27
+ _APPEND_KEY = re.compile(
28
+ r"(?:[A-Za-z0-9_]+_)?(?:EXTRA_(?:CONF|DTC_OVERLAY)_FILE|SB_CONF_FILE|SB_EXTRA_CONF_FILE)"
29
+ )
30
+ _NAME = re.compile(r"-D([A-Za-z_][A-Za-z0-9_]*)(?::[A-Za-z]+)?(=.*)?", re.DOTALL)
31
+
32
+
33
+ class UserDefineError(Exception):
34
+ def __init__(self, code: str, message: str) -> None:
35
+ super().__init__(message)
36
+ self.code = code
37
+ self.message = message
38
+
39
+
40
+ def _split(define: str) -> tuple[str, str | None]:
41
+ m = _NAME.fullmatch(define)
42
+ assert m is not None # already validated by `normalise_defines`
43
+ return m.group(1), (m.group(2)[1:] if m.group(2) is not None else None)
44
+
45
+
46
+ def user_defines_problem(defines: list[str]) -> tuple[str, str] | None:
47
+ """`(code, message)` for a normalised `-D` list tan must refuse, else None."""
48
+ for d in defines:
49
+ if any(ord(c) < 0x20 or ord(c) == 0x7F for c in d):
50
+ return (
51
+ "build.invalid-argument",
52
+ f"`-D {d!r}` contains a control character (newline, tab, ...); "
53
+ "a CMake definition is one line.",
54
+ )
55
+ name, value = _split(d)
56
+ if name in RESERVED_KEYS:
57
+ return (
58
+ "build.define-reserved",
59
+ f"`-D {name}` cannot be overridden: tan derives `{name}` itself "
60
+ "(the board comes from the plan / `--board`, Python from the resolved host Python).",
61
+ )
62
+ if _APPEND_KEY.fullmatch(name) and not value:
63
+ return (
64
+ "build.invalid-argument",
65
+ f"`-D {name}` needs a value: it is appended to tan's own `{name}` list.",
66
+ )
67
+ return None
68
+
69
+
70
+ def _apply_to_args(args: list[str], defines: list[str]) -> list[str]:
71
+ out = list(args)
72
+ if "--" not in out:
73
+ out.append("--")
74
+ for d in defines:
75
+ name, value = _split(d)
76
+ if _APPEND_KEY.fullmatch(name):
77
+ idx = next(
78
+ (
79
+ i
80
+ for i in range(len(out) - 1, out.index("--"), -1)
81
+ if (m := _NAME.fullmatch(out[i])) is not None
82
+ and m.group(1) == name
83
+ and m.group(2) is not None
84
+ ),
85
+ None,
86
+ )
87
+ if idx is not None:
88
+ out[idx] = f"{out[idx]};{value}"
89
+ continue
90
+ out.append(d)
91
+ return out
92
+
93
+
94
+ def apply_user_defines(
95
+ plan_text: str, defines: list[str], cores: list[str] | None = None
96
+ ) -> tuple[str, list[str]]:
97
+ """The plan text with `defines` spliced into the targeted Zephyr slices'
98
+ argv, plus the coreIds changed. Targets: every Zephyr slice that has a
99
+ command, or only `cores` when given. Raises [`UserDefineError`] when `cores`
100
+ names a slice that is not such a target, or nothing is targeted."""
101
+ plan = json.loads(plan_text)
102
+ targets = [
103
+ s
104
+ for s in plan.get("slices", [])
105
+ if s.get("backend") == "zephyr" and isinstance(s.get("command"), dict)
106
+ ]
107
+ if cores:
108
+ known = {s["coreId"] for s in targets}
109
+ missing = [c for c in cores if c not in known]
110
+ if missing:
111
+ raise UserDefineError(
112
+ "build.invalid-argument",
113
+ f"`--core {', '.join(missing)}` is not a Zephyr slice of this plan "
114
+ f"(Zephyr slices: {', '.join(sorted(known)) or 'none'}).",
115
+ )
116
+ targets = [s for s in targets if s["coreId"] in cores]
117
+ if not targets:
118
+ raise UserDefineError(
119
+ "build.define-no-target",
120
+ "`-D` has no Zephyr slice to apply to: this plan has none with a build command.",
121
+ )
122
+ for s in targets:
123
+ s["command"]["args"] = _apply_to_args(s["command"]["args"], defines)
124
+ return json.dumps(plan, indent=2), [s["coreId"] for s in targets]
125
+
126
+
127
+ def define_pairs(defines: list[str]) -> list[str]:
128
+ """`NAME=VALUE` (or bare `NAME`) lines for normalised `-D` args -- the unit
129
+ the per-slice stamp records."""
130
+ return sorted(d[2:] for d in defines)
131
+
132
+
133
+ def changed_defines(previous: list[str], current: list[str]) -> list[str]:
134
+ """Names whose `-D` was added, removed or given a different value between
135
+ two stamped sets (sorted, de-duplicated)."""
136
+ diff = set(previous) ^ set(current)
137
+ return sorted({re.split(r"[:=]", d, maxsplit=1)[0] for d in diff})
138
+
139
+
140
+ def insert_after_dashdash(args: list[str], extra: list[str]) -> list[str]:
141
+ """`extra` placed right after `--`, i.e. BEFORE every plan/user `-D`.
142
+
143
+ Order is load-bearing: cmake applies `-U`/`-D` in argv order, so a `-U<key>`
144
+ after a `-D<key>=...` silently unsets it. Before them, a `-D` of the same
145
+ key wins. (Adds the `--` when the argv has none.)"""
146
+ if not extra:
147
+ return list(args)
148
+ out = list(args)
149
+ if "--" not in out:
150
+ out.append("--")
151
+ i = out.index("--") + 1
152
+ return out[:i] + list(extra) + out[i:]