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,714 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`--emit scaffold` text rewrites: board.yaml/CMakeLists.txt/README.md.
3
+
4
+ SPLIT out of `tan/planner/template.py` (tan-cli#1142; see `template_pins.py`'s
5
+ module docstring for why the split is allowed under `HAND_PORT_SOURCES` and
6
+ why `MIRRORED_PREFIX` does not bar it -- both files are the same story, not
7
+ repeated here).
8
+
9
+ WHAT'S HERE: every function that takes a rendered file's TEXT and a rename
10
+ (sku, core, pin, macro, doc, docs ref) and returns rewritten text -- the
11
+ `board.yaml` sku/core/pin substitutions, the CMakeLists.txt ALP_SDK_ROOT
12
+ hardening, and the README.md link/board/pin rewrites -- plus the two small
13
+ metadata-adjacent lookups those rewrites need (`_tag_resolves`, `_docs_ref`).
14
+ What is NOT here: anything that reads `metadata/e1m_modules/**` or
15
+ `metadata/boards/**` to DERIVE a rename in the first place -- that's
16
+ `template_pins.py`.
17
+
18
+ Every docstring below is UNCHANGED from `template.py` -- issue numbers,
19
+ tan-cli numbers and worked examples all still refer to that module's own
20
+ history, because this split moved code, not its provenance.
21
+
22
+ Imports `TemplateError` back FROM `.template`, the same circular-at-the-
23
+ module-object-level direction `template_pins.py` uses and explains at length
24
+ in its own docstring -- read that one first if this import looks backwards.
25
+ It works here for the same reason: `template.py` binds `TemplateError` (its
26
+ exception classes, near the top of the file) before its own
27
+ `from .template_rewrite import ...` line runs.
28
+
29
+ Also imports `_pin_pad_and_macro` from `.template_pins` -- NOT circular: by
30
+ the time `template.py` reaches its own `from .template_rewrite import ...`
31
+ line, its preceding `from .template_pins import ...` line (tan-cli#1142's
32
+ NameError fix) has already run `template_pins.py` to completion, so this
33
+ module sees a fully-built `tan.planner.template_pins`, same as any ordinary
34
+ import.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import posixpath
39
+ import re
40
+ import subprocess
41
+ from pathlib import Path
42
+ from typing import Any
43
+
44
+ import yaml
45
+
46
+ from tan.core.subprocess_env import spawn_env
47
+
48
+ from .template import TemplateError
49
+ from .template_pins import _pin_pad_and_macro
50
+
51
+
52
+ # Matches board.yaml's `som:\n sku: E1M-...` line and the top-level
53
+ # `preset: <name>` line -- through end-of-line (incl. any trailing inline
54
+ # comment), so a value CHANGE can drop a comment describing the OLD SoM
55
+ # (e.g. `sku: E1M-AEN801 # Alif Ensemble E8 SoM` must not survive as a
56
+ # stale label once the value becomes E1M-V2N101). Unbounded (no count=):
57
+ # every match is inspected so a board.yaml with more than one matching
58
+ # `sku:`/`preset:` line -- ambiguous, could silently rewrite a decoy while
59
+ # the real som.sku/preset line survives untouched -- hard-errors instead
60
+ # of guessing which one is real.
61
+ _SOM_SKU_RE = re.compile(r"(?m)^(\s*sku:\s*)(E1M-[A-Z0-9]+)[^\n]*$")
62
+ _PRESET_RE = re.compile(r"(?m)^(preset:\s*)(\S+)[^\n]*$")
63
+
64
+
65
+ def _substitute_board_yaml_sku(text: str, sku: str, preset: str) -> str:
66
+ def _sub_sku(m: re.Match[str]) -> str:
67
+ # Value unchanged -> leave the WHOLE line (incl. any comment)
68
+ # untouched: this is the byte-passthrough guarantee for sku ==
69
+ # the example's own default.
70
+ return m.group(0) if m.group(2) == sku else f"{m.group(1)}{sku}"
71
+
72
+ text, n_sku = _SOM_SKU_RE.subn(_sub_sku, text)
73
+ if n_sku != 1:
74
+ raise TemplateError(
75
+ f"board.yaml must have exactly one `som.sku:` line to "
76
+ f"substitute (found {n_sku})")
77
+
78
+ def _sub_preset(m: re.Match[str]) -> str:
79
+ return m.group(0) if m.group(2) == preset else f"{m.group(1)}{preset}"
80
+
81
+ text, n_preset = _PRESET_RE.subn(_sub_preset, text)
82
+ if n_preset != 1:
83
+ raise TemplateError(
84
+ f"board.yaml must have exactly one top-level `preset:` line "
85
+ f"to substitute (found {n_preset})")
86
+ return text
87
+
88
+
89
+ _LIBRARY_CORE_SCOPE_RE = re.compile(r"(cores:\s*\[)([^\]]*)(\])")
90
+
91
+
92
+ def _strip_stale_core_prose(text: str, old: str) -> str:
93
+ """Delete any full comment LINE naming `old` in PROSE form (issue
94
+ #864 Fable-review MINOR F) -- e.g. gpio-button-led's board.yaml
95
+ carries `# Single-core slice: M55-HP runs the demo. M55-HE
96
+ inherits...` directly above `cores:\\n m55_hp:`, which the plain
97
+ `m55_hp:` key-line regex below never touches (different case,
98
+ hyphen instead of underscore). Matches case-insensitively with `_`
99
+ /`-` interchangeable. A hardware-specific sentence about the
100
+ canonical SoM's OTHER core/topology doesn't have a sensible
101
+ equivalent on a different SoM family, so deleting the line is
102
+ safer than guessing a replacement."""
103
+ prose = re.escape(old).replace("_", "[_-]")
104
+ line_re = re.compile(rf"(?mi)^[ \t]*#.*\b{prose}\b.*\n?")
105
+ return line_re.sub("", text)
106
+
107
+
108
+ def _substitute_board_yaml_core(text: str, old: str, new: str) -> str:
109
+ """Rewrite the `cores:` mapping's single top-level `<old>:` key to
110
+ `<new>:`. The per-core content underneath (`app:`, `peripherals:`)
111
+ is core-id-agnostic -- metadata/schemas/board.schema.json's
112
+ `core_entry` says every field is optional and inherits the SoM
113
+ preset's `topology.<core_id>` default, so only the KEY changes.
114
+
115
+ Also renames `old` wherever a top-level `libraries:` entry scopes
116
+ itself to this core via a `cores: [<id>, ...]` flow list (e.g.
117
+ cold-chain-monitor's `libraries: [{name: tflite-micro, cores:
118
+ [m55_hp]}]`) -- `alp_orchestrate.loader._normalize_libraries` hard-
119
+ errors if that list still names a core id that no longer exists
120
+ once the `cores:` mapping key above is renamed ("libraries: entry
121
+ '<name>' is scoped to core '<old>', which is not declared under
122
+ `cores:`"). Also strips any comment line describing `old` in prose
123
+ (see `_strip_stale_core_prose`)."""
124
+ text = _strip_stale_core_prose(text, old)
125
+ pattern = re.compile(rf"(?m)^(\s*){re.escape(old)}:([ \t]*)$")
126
+ new_text, n = pattern.subn(lambda m: f"{m.group(1)}{new}:{m.group(2)}", text)
127
+ if n != 1:
128
+ raise TemplateError(
129
+ f"board.yaml must have exactly one `cores.{old}:` line to "
130
+ f"re-derive to {new!r} (found {n})")
131
+
132
+ def _fix_scope_list(m: re.Match[str]) -> str:
133
+ inner = re.sub(rf"\b{re.escape(old)}\b", new, m.group(2))
134
+ return f"{m.group(1)}{inner}{m.group(3)}"
135
+
136
+ return _LIBRARY_CORE_SCOPE_RE.sub(_fix_scope_list, new_text)
137
+
138
+
139
+ def _substitute_board_yaml_pins(
140
+ text: str, renames: dict[str, str], original_pins: list[Any],
141
+ ) -> str:
142
+ """Rewrite each renamed pad wherever a `pins:` entry names it --
143
+ scoped to the two shapes a `pins:` list item can take (issue #876
144
+ review MINOR 3), not a blanket `\\b<pad>\\b` replace over the whole
145
+ file (a pad name can also appear in unrelated prose, e.g. gpio-
146
+ button-led's `preset:` header comment -- see `_strip_stale_core_
147
+ prose`, reused for pins in `render_to_envelope`):
148
+
149
+ * the dict form's `e1m: <old>` field (`(e1m:\\s*)<pad>\\b`), and
150
+ * the bare pad-string list-item form (`- <old>`,
151
+ `([ \\t]*-[ \\t]*)<pad>\\b`) the schema also allows -- a template
152
+ using this form had its pad left stale by the dict-only regex
153
+ (silent `--emit zephyr-conf` failure downstream: the exact class
154
+ of bug #876 exists to kill), and a MIXED bare + dict entry for
155
+ the same pad hid it entirely (the dict match alone satisfied the
156
+ old "at least one occurrence" guard).
157
+
158
+ `original_pins` supplies the EXPECTED occurrence count per pad (how
159
+ many entries -- bare or dict -- actually name it), so the rewrite
160
+ is verified exact rather than "found at least one"."""
161
+ for old, new in renames.items():
162
+ expected = sum(
163
+ 1 for item in original_pins
164
+ if _pin_pad_and_macro(item)[0] == old
165
+ )
166
+ dict_pattern = re.compile(rf"(e1m:\s*){re.escape(old)}\b")
167
+ text, n_dict = dict_pattern.subn(lambda m: f"{m.group(1)}{new}", text)
168
+ bare_pattern = re.compile(rf"(?m)^([ \t]*-[ \t]*){re.escape(old)}\b")
169
+ text, n_bare = bare_pattern.subn(lambda m: f"{m.group(1)}{new}", text)
170
+ if n_dict + n_bare != expected:
171
+ raise TemplateError(
172
+ f"board.yaml `pins:` re-derive of `{old}` -> {new!r}: "
173
+ f"expected {expected} occurrence(s), rewrote "
174
+ f"{n_dict + n_bare}")
175
+ return text
176
+
177
+
178
+ def _substitute_board_yaml_pin_macros(text: str, renames: dict[str, str]) -> str:
179
+ """Companion to `_substitute_board_yaml_pins`: rewrite each `pins:`
180
+ entry's `macro:` field per `_derive_pin_macro_renames`'s map, the
181
+ same scoped-to-the-key approach (`(macro:\\s*)<old>\\b`) -- `macro:`
182
+ only ever appears in the dict form (a bare pad-string entry has
183
+ no `macro:` at all)."""
184
+ for old, new in renames.items():
185
+ pattern = re.compile(rf"(macro:\s*){re.escape(old)}\b")
186
+ new_text, n = pattern.subn(lambda m: f"{m.group(1)}{new}", text)
187
+ if n < 1:
188
+ raise TemplateError(
189
+ f"board.yaml `pins:` has no `macro: {old}` entry to "
190
+ f"re-derive to {new!r}")
191
+ text = new_text
192
+ return text
193
+
194
+
195
+ def _substitute_board_yaml_pin_docs(text: str, renames: dict[str, str | None]) -> str:
196
+ """Companion to `_substitute_board_yaml_pins`: rewrite (or drop) a
197
+ `pins:` entry's `doc:` field per `_derive_pin_doc_renames`'s map
198
+ (issue #876 review MAJOR 2) -- `doc:` only ever appears in the
199
+ dict form. A `None` value means the target route has no `doc:` of
200
+ its own; the loader falls back to the resolved board's own `doc:`
201
+ in that case, so the field is dropped entirely rather than left
202
+ describing the wrong board."""
203
+ for old_doc, new_doc in renames.items():
204
+ old_quoted = re.escape(f'"{old_doc}"')
205
+ if new_doc is not None:
206
+ pattern = re.compile(rf"(doc:\s*){old_quoted}")
207
+ new_text, n = pattern.subn(lambda m: f'{m.group(1)}"{new_doc}"', text)
208
+ else:
209
+ pattern = re.compile(rf",\s*doc:\s*{old_quoted}")
210
+ new_text, n = pattern.subn("", text)
211
+ if n < 1:
212
+ raise TemplateError(
213
+ f'board.yaml `pins:` has no `doc: "{old_doc}"` entry to '
214
+ f"re-derive")
215
+ text = new_text
216
+ return text
217
+
218
+
219
+ # ---------------------------------------------------------------------
220
+ # --emit scaffold content adaptation (issue #864 follow-up)
221
+ # ---------------------------------------------------------------------
222
+ #
223
+ # Every catalog template's user_owned files are the SDK's own example,
224
+ # verbatim -- correct for render()'s documented byte-for-byte contract
225
+ # (validate()'s in-tree twister self-test relies on exactly that), but
226
+ # wrong for a scaffold a customer unpacks OUTSIDE the SDK tree: a
227
+ # `west build ... examples/<...>` argument naming a path that doesn't
228
+ # exist in their project, `../`-relative links that only resolve
229
+ # inside the SDK checkout, and a CMakeLists.txt that silently guesses
230
+ # `../../..` for ALP_SDK_ROOT (correct only for the in-tree example,
231
+ # never a copied-out scaffold -- the retired tan-cli generator hard-
232
+ # failed on exactly this: "ALP_SDK_ROOT is not set"). These transforms
233
+ # run ONLY in render_to_envelope() (the `--emit scaffold` path, for
234
+ # EVERY sku including the canonical example's own) -- render()/
235
+ # validate() stay byte-for-byte faithful to the real example, since
236
+ # that's what validate()'s temp-dir twister run is proving builds.
237
+
238
+ _ALP_SDK_ROOT_GUESS_RE = re.compile(
239
+ r"if\(DEFINED ENV\{ALP_SDK_ROOT\}\)\n"
240
+ r" set\(ALP_SDK_ROOT \$ENV\{ALP_SDK_ROOT\}\)\n"
241
+ r"else\(\)\n"
242
+ r" get_filename_component\(ALP_SDK_ROOT \$\{CMAKE_CURRENT_SOURCE_DIR\}(?:/\.\.)+ ABSOLUTE\)\n"
243
+ r"endif\(\)"
244
+ )
245
+ # Was `cold-chain-monitor`'s own shape until alp-sdk#1400 converted it to the
246
+ # guess shape above: no ALP_SDK_ROOT resolution at all, just a hardcoded
247
+ # in-tree-relative path straight to `alp_project.py` (worse than the guess --
248
+ # no override was even possible). No example carries it at the pinned SDK
249
+ # commit any more; kept as a defensive branch, not a live path.
250
+ _HARDCODED_ALP_PROJECT_PY_RE = re.compile(
251
+ r"\$\{CMAKE_CURRENT_SOURCE_DIR\}(?:/\.\.)+/scripts/alp_project\.py"
252
+ )
253
+ # Anything that only resolves against a real alp-sdk checkout, i.e. that a
254
+ # scaffold copied OUT of the SDK tree cannot satisfy unless ALP_SDK_ROOT has
255
+ # been rewritten into a hard requirement: the shared `cmake/alp.cmake`
256
+ # include, either helper it would define, or a direct `alp_project.py` shell
257
+ # (the `cmake/alp.cmake` include and its helpers are PLANNED and unmerged,
258
+ # tan-cli#825 -- `alp_project.py` is the only spelling any real example
259
+ # carries today; the other two are matched so this stays correct once that
260
+ # helper ships).
261
+ _SDK_ROOT_DEPENDENT_RE = re.compile(
262
+ r"cmake/alp\.cmake|alp_sdk_zephyr_conf|alp_sdk_ipc_contract_header"
263
+ r"|alp_project\.py")
264
+ _ALP_SDK_ROOT_REQUIRED_BLOCK = (
265
+ # Issue #864 Fable-review MAJOR E: the ORIGINAL block here checked
266
+ # only `ENV{ALP_SDK_ROOT}` while the message also advertised
267
+ # `-DALP_SDK_ROOT=...` -- a customer passing ONLY the -D cache
268
+ # variable still hit the FATAL_ERROR (ENV{} was never set), and
269
+ # even a customer setting BOTH had the -D value silently clobbered
270
+ # by `set(ALP_SDK_ROOT $ENV{ALP_SDK_ROOT})`. Check + prefer
271
+ # whichever is actually DEFINED; only fall back to the env var when
272
+ # the cache variable itself isn't set.
273
+ "if(NOT DEFINED ALP_SDK_ROOT AND NOT DEFINED ENV{ALP_SDK_ROOT})\n"
274
+ " message(FATAL_ERROR\n"
275
+ " \"ALP_SDK_ROOT is not set -- point it at your alp-sdk checkout, \"\n"
276
+ " \"e.g. `export ALP_SDK_ROOT=/path/to/alp-sdk` or "
277
+ "`-DALP_SDK_ROOT=/path/to/alp-sdk`.\")\n"
278
+ "endif()\n"
279
+ "if(NOT DEFINED ALP_SDK_ROOT)\n"
280
+ " set(ALP_SDK_ROOT $ENV{ALP_SDK_ROOT})\n"
281
+ "endif()"
282
+ )
283
+
284
+ # The guess block does not stand alone: most examples introduce it with
285
+ # a comment paragraph that TEACHES the in-tree `../../..` fallback --
286
+ # hello-world/cold-chain-monitor's "In-tree the SDK is the example's
287
+ # grandparent directory; out-of-tree customers point ALP_SDK_ROOT at
288
+ # their checkout", gpio-button-led's "in-tree we resolve it as the
289
+ # example's grandparent directory". Substituting only the code left
290
+ # that prose above a block that has NO fallback and hard-fails instead,
291
+ # so the emitted scaffold documented behaviour it did not have. Rewrite
292
+ # the paragraph with the code it describes.
293
+ _STALE_SDK_ROOT_PROSE_RE = re.compile(r"ALP_SDK_ROOT|grandparent", re.IGNORECASE)
294
+ _ALP_SDK_ROOT_ACCURATE_COMMENT = (
295
+ "# Resolve the alp-sdk root. This project lives OUTSIDE the SDK\n"
296
+ "# tree, so there is nothing to guess: ALP_SDK_ROOT must name your\n"
297
+ "# alp-sdk checkout, set in the environment or passed as\n"
298
+ "# `-DALP_SDK_ROOT=/path/to/alp-sdk`."
299
+ )
300
+
301
+
302
+ def _rewrite_stale_sdk_root_comment(head: str) -> str:
303
+ """Rewrite the comment paragraph introducing the ALP_SDK_ROOT block.
304
+
305
+ `head` is everything in the CMakeLists.txt BEFORE the guess block.
306
+ Its trailing run of `#` lines (optionally separated from the block
307
+ by blank lines) is that block's prose. The run is split into
308
+ paragraphs on bare `#` separator lines, and the first paragraph
309
+ naming `ALP_SDK_ROOT` or the grandparent fallback is replaced with
310
+ `_ALP_SDK_ROOT_ACCURATE_COMMENT`; any further matching paragraph is
311
+ dropped rather than duplicating it. Paragraphs about anything else
312
+ are kept verbatim -- gpio-button-led's run leads with a "board.yaml
313
+ -> build/generated/alp.conf at configure time." banner that stays
314
+ true. A file whose block has no comment run above it (i2c-master,
315
+ mproc-mailbox) is returned unchanged.
316
+ """
317
+ lines = head.split("\n")
318
+ i = len(lines) - 1
319
+ while i >= 0 and not lines[i].strip():
320
+ i -= 1
321
+ end = i + 1
322
+ while i >= 0 and lines[i].lstrip().startswith("#"):
323
+ i -= 1
324
+ start = i + 1
325
+ if start >= end:
326
+ return head
327
+
328
+ out: list[str] = []
329
+ para: list[str] = []
330
+ replaced = False
331
+
332
+ def _flush() -> None:
333
+ nonlocal replaced
334
+ if not para:
335
+ return
336
+ if _STALE_SDK_ROOT_PROSE_RE.search("\n".join(para)):
337
+ if not replaced:
338
+ out.extend(_ALP_SDK_ROOT_ACCURATE_COMMENT.split("\n"))
339
+ replaced = True
340
+ else:
341
+ out.extend(para)
342
+ para.clear()
343
+
344
+ for line in lines[start:end]:
345
+ if line.strip() == "#":
346
+ _flush()
347
+ out.append(line)
348
+ else:
349
+ para.append(line)
350
+ _flush()
351
+ lines[start:end] = out
352
+ return "\n".join(lines)
353
+
354
+
355
+ def _scaffold_cmakelists(text: str) -> str:
356
+ """Replace an in-tree-relative ALP_SDK_ROOT guess with a hard
357
+ requirement, and rewrite the comment paragraph that describes it.
358
+
359
+ One shape is live across the catalog's example CMakeLists.txt files
360
+ today: the `if(DEFINED ENV{ALP_SDK_ROOT}) ... else()
361
+ get_filename_component(...)` guess most examples carry immediately
362
+ above a direct `execute_process(... scripts/alp_project.py ...)`
363
+ call (PLANNED to become `include(${ALP_SDK_ROOT}/cmake/alp.cmake)`
364
+ once that helper merges -- unmerged, tan-cli#825). A second,
365
+ defensive-only shape is also matched -- see `_HARDCODED_ALP_PROJECT_PY_RE`
366
+ above for why it is no longer live and why the branch stays anyway. The
367
+ guess shape resolves only for the in-tree example; a scaffold a customer
368
+ unpacks elsewhere needs the value supplied, so it becomes a
369
+ FATAL_ERROR-if-unset block -- the guess shape's `include()` line
370
+ already names `${ALP_SDK_ROOT}` and needs no further rewriting; the
371
+ (defensive-only) hardcoded shape's path would be rewritten to
372
+ `${ALP_SDK_ROOT}/scripts/alp_project.py` alongside inserting the
373
+ block if it were ever matched.
374
+
375
+ Each guess-block hit is substituted through a loop rather than
376
+ `subn`: the block's own preceding comment run has to be rewritten
377
+ with it (`_rewrite_stale_sdk_root_comment`, alp-sdk#1390), and the
378
+ replacement block is not itself a guess block, so the next `search`
379
+ cannot re-find what was just substituted.
380
+
381
+ A CMakeLists.txt with no SDK-root-dependent line at all (e.g.
382
+ multicore-rpmsg's `linux/CMakeLists.txt`) is legitimately returned
383
+ unchanged. One that DOES depend on the SDK root but carries an
384
+ unrecognised resolution shape raises: this used to be a silent
385
+ best-effort no-op, which shipped every scaffolded project an
386
+ `include()`/`alp_project.py` path that resolves only inside an SDK
387
+ checkout -- broken on the very first thing a new customer does, with
388
+ nothing failing here to say so."""
389
+ pos, hit = 0, False
390
+ while True:
391
+ m = _ALP_SDK_ROOT_GUESS_RE.search(text, pos)
392
+ if not m:
393
+ break
394
+ hit = True
395
+ head = _rewrite_stale_sdk_root_comment(text[: m.start()])
396
+ text = head + _ALP_SDK_ROOT_REQUIRED_BLOCK + text[m.end():]
397
+ pos = len(head) + len(_ALP_SDK_ROOT_REQUIRED_BLOCK)
398
+ if hit:
399
+ return text
400
+ if _ALP_SDK_ROOT_REQUIRED_BLOCK in text:
401
+ return text # already hardened (idempotent)
402
+ if _HARDCODED_ALP_PROJECT_PY_RE.search(text):
403
+ text = _HARDCODED_ALP_PROJECT_PY_RE.sub(
404
+ "${ALP_SDK_ROOT}/scripts/alp_project.py", text)
405
+ return text.replace(
406
+ "execute_process(\n",
407
+ _ALP_SDK_ROOT_REQUIRED_BLOCK + "\n\nexecute_process(\n", 1)
408
+ dependent = _SDK_ROOT_DEPENDENT_RE.search(text)
409
+ if dependent:
410
+ raise TemplateError(
411
+ f"CMakeLists.txt depends on the SDK root (`{dependent.group(0)}`) "
412
+ f"but carries no recognised ALP_SDK_ROOT resolution block to "
413
+ f"rewrite into a hard requirement -- a scaffold of it would ship "
414
+ f"a path that only resolves inside an alp-sdk checkout. Use the "
415
+ f"`if(DEFINED ENV{{ALP_SDK_ROOT}}) ... else() "
416
+ f"get_filename_component(...) endif()` shape the other examples "
417
+ f"use, or teach `_scaffold_cmakelists` the new one.")
418
+ return text
419
+
420
+
421
+ _RELATIVE_LINK_RE = re.compile(r"\]\((\.\./[^)\s]+)\)")
422
+
423
+ # alp-sdk#1855: `_RELATIVE_LINK_RE` above only rewrites a MARKDOWN-style
424
+ # `](../docs/x.md)` link, and only inside README.md (the one file
425
+ # `_scaffold_readme` runs on). A board.yaml/src/main.c comment names the
426
+ # same kind of alp-sdk-tree-only path in prose instead -- no `[...](...)`
427
+ # around it at all -- so it never matches and survives a scaffold
428
+ # verbatim (e.g. i2c-master's board.yaml/src/main.c both say "see
429
+ # examples/v2n/v2n-temp-sensor for ..." with no disclaimer that it isn't
430
+ # part of this scaffolded project, unlike the DELIBERATELY-disclaimed
431
+ # i2c-scanner mention two paragraphs above it in the same file). Narrow
432
+ # on purpose: only the two prefixes actually found bare like this
433
+ # (`docs/*.md`, `examples/<category>/<name>[/<subpath>]`) -- a
434
+ # `scripts/`/`metadata/` mention is left alone, since every such mention
435
+ # in the catalog today is either already `${ALP_SDK_ROOT}`-qualified in
436
+ # a CMakeLists.txt or purely descriptive prose that names a script/file
437
+ # by convention rather than telling the customer to open it.
438
+ _BARE_REPO_PATH_RE = re.compile(
439
+ r"\b(?:docs/[\w./-]+\.md|examples/[\w-]+/[\w-]+(?:/[\w./-]+)?)\b")
440
+
441
+
442
+ def _scaffold_bare_repo_paths(text: str, docs_ref: str) -> str:
443
+ """Rewrite a bare `docs/*.md` or `examples/<category>/<name>
444
+ [/<subpath>]` mention into the same absolute GitHub URL form
445
+ `_scaffold_readme`'s `_fix_link` gives a markdown-style link --
446
+ see `_BARE_REPO_PATH_RE`'s comment for why this exists as a
447
+ SEPARATE pass rather than widening that one. Best-effort / no-op
448
+ when neither prefix appears."""
449
+ def _sub(m: re.Match[str]) -> str:
450
+ target = m.group(0)
451
+ kind = "blob" if "." in target.rsplit("/", 1)[-1] else "tree"
452
+ return f"https://github.com/alplabai/alp-sdk/{kind}/{docs_ref}/{target}"
453
+
454
+ return _BARE_REPO_PATH_RE.sub(_sub, text)
455
+
456
+
457
+ def _tag_resolves(base_dir: Path, tag: str) -> bool:
458
+ """Whether `tag` exists in `base_dir`'s git checkout.
459
+
460
+ Local-only: `git rev-parse` against the checkout's own refs, never a
461
+ network call -- scaffolding must work offline, and a scaffold that
462
+ stalled on `git ls-remote` would be a worse defect than the dead link
463
+ this guards. A checkout that fetched from origin has origin's tags, so
464
+ "resolves here" is the closest offline proxy for "resolves on GitHub"
465
+ available, and every way it can be wrong (no git binary, tarball
466
+ export, `--no-tags` clone, shallow CI checkout) fails the same
467
+ direction: no tag found, pin to `main`, links stay live.
468
+
469
+ Ported verbatim from alp-sdk `scripts/alp_template.py::_tag_resolves`
470
+ (issue #1508 / alp-sdk#1535), except for one RELOCATED divergence:
471
+ `env=spawn_env()` below is a tan-only addition (tan-cli#992) -- alp-sdk's
472
+ own copy never restores it because alp-sdk never ships as a frozen
473
+ PyInstaller bundle whose LD_LIBRARY_PATH would otherwise leak into the
474
+ spawned `git`."""
475
+ try:
476
+ return subprocess.run(
477
+ ["git", "-C", str(base_dir), "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"],
478
+ capture_output=True,
479
+ env=spawn_env(),
480
+ check=False,
481
+ ).returncode == 0
482
+ except (OSError, subprocess.SubprocessError): # no git binary, not a repo
483
+ return False
484
+
485
+
486
+ def _docs_ref(base_dir: Path) -> str:
487
+ """The GitHub ref a scaffolded README's doc links should pin to
488
+ (issue #864 Fable-review MINOR H): `metadata/sdk_version.yaml`'s
489
+ own `v<version>` tag when `status: released` (a released checkout's
490
+ docs are stable at that tag; linking `main` could point at docs
491
+ that have since changed or moved), else `main` -- an unreleased/
492
+ development checkout has no matching tag yet to pin to.
493
+
494
+ The tag has to RESOLVE, not merely be declared (tan-cli#846, porting
495
+ alp-sdk#1535). Between an rc cut and its GA tag `sdk_version.yaml`
496
+ says `version: 0.16.0` / `status: released` while only
497
+ `v0.16.0-rc1` exists on the bound checkout -- branching on the
498
+ declared pair alone put a dead
499
+ `https://github.com/alplabai/alp-sdk/blob/v0.16.0/docs/...` link in
500
+ every project scaffolded in that window. A missing tag degrades to
501
+ `main` instead of shipping a 404.
502
+
503
+ A `sdk_version.yaml` that parses but is not itself a mapping (e.g.
504
+ a bare list) degrades to `main` the same way a missing file does,
505
+ rather than raising: this is a README doc-link decision, not a
506
+ fatal `--emit scaffold` input, so a malformed version file should
507
+ cost a stale-but-safe link, not the whole scaffold (tan-cli#1037 --
508
+ a PR #1034 round-two review nit had described this read as safe on
509
+ the reasoning that `yaml.safe_load(...) or {}` only ever produces a
510
+ dict-or-`{}` result; a non-empty bare-list/scalar document is
511
+ neither, and reached `doc.get(...)` bare, `AttributeError: 'list'
512
+ object has no attribute 'get'`).
513
+
514
+ tan-cli#1116 review round 2: `UnicodeDecodeError` and `yaml.YAMLError`
515
+ are caught alongside `OSError` -- neither is an `OSError`, so a non-UTF-8
516
+ or syntactically-invalid `sdk_version.yaml` used to raise raw past this
517
+ same "cost a stale-but-safe link, not the whole scaffold" contract
518
+ (both measured escaping before this fix)."""
519
+ try:
520
+ doc = yaml.safe_load(
521
+ (base_dir / "metadata" / "sdk_version.yaml").read_text(encoding="utf-8")
522
+ ) or {}
523
+ except (OSError, UnicodeDecodeError, yaml.YAMLError):
524
+ return "main"
525
+ if not isinstance(doc, dict):
526
+ return "main"
527
+ version = doc.get("version")
528
+ if doc.get("status") == "released" and version and _tag_resolves(base_dir, f"v{version}"):
529
+ return f"v{version}"
530
+ return "main"
531
+
532
+
533
+ def _substitute_readme_pins(text: str, renames: dict[str, str]) -> str:
534
+ """Rewrite a scaffolded README's `ALP_<old_pad>` mentions to
535
+ `ALP_<new_pad>` for every renamed pin (issue #876 review MINOR 4)
536
+ -- e.g. gpio-button-led's README teaches `ALP_E1M_GPIO_IO4` as THE
537
+ button pin, which becomes actively wrong prose once the pad itself
538
+ has changed for a cross-family sku.
539
+
540
+ Paragraph-scoped (split on blank lines): a paragraph that ALREADY
541
+ mentions BOTH the old and the new `ALP_<pad>` form (e.g. i2c-
542
+ master's "resolves to `ALP_E1M_I2C0` on the E1M EVK and
543
+ `ALP_E1M_X_I2C0` on the E1M-X EVK" cross-EVK teaching sentence) is
544
+ left alone -- it's correct, portable prose about the alias
545
+ mechanism itself, not a stale claim about which pad THIS scaffold
546
+ uses, and blindly substituting would turn it into a duplicate,
547
+ factually wrong statement ("... on the E1M EVK" would then name
548
+ the E1M-X pad).
549
+
550
+ A `` `ALP_<old_pad>` (index N) `` parenthetical (e.g. gpio-button-
551
+ led's "`ALP_E1M_GPIO_PWM0` (index 26)") names N per `old_pad`'s
552
+ OWN family's `ALP_E1M_GPIO_<class><N>` numbering
553
+ (`include/alp/e1m_pinout.h`'s canonical IO0..25 = 0..25, PWM0..7 =
554
+ 26..33 order) -- a DIFFERENT numbering on a cross-family target
555
+ (E1M-X's `include/alp/e1m_x_pinout.h` has 36 IOs, so its PWM0..7
556
+ sits at 36..43, not the source family's index at all). The route
557
+ data available here (`metadata/boards/*.yaml` `e1m_routes:`
558
+ entries: `e1m`/`macro`/`board_alias`/`doc`, no index column) can't
559
+ re-derive `new_pad`'s own index, so rather than carry the stale
560
+ source-family number forward as if it were true of the target,
561
+ drop the parenthetical along with the pad it was describing."""
562
+ if not renames:
563
+ return text
564
+ paragraphs = text.split("\n\n")
565
+ for i, para in enumerate(paragraphs):
566
+ changed = para
567
+ for old, new in renames.items():
568
+ old_tok, new_tok = f"ALP_{old}", f"ALP_{new}"
569
+ if old_tok in para and new_tok in para:
570
+ continue # already-correct dual-EVK teaching prose
571
+ changed = re.sub(
572
+ rf"`{re.escape(old_tok)}`(?:\s*\(index\s+\d+\))?",
573
+ f"`{new_tok}`", changed)
574
+ changed = re.sub(rf"\b{re.escape(old_tok)}\b", new_tok, changed)
575
+ paragraphs[i] = changed
576
+ return "\n\n".join(paragraphs)
577
+
578
+
579
+ def _scaffold_readme(
580
+ text: str,
581
+ example_path: str,
582
+ docs_ref: str,
583
+ example_sku: str = "",
584
+ sku: str = "",
585
+ source_board: str | None = None,
586
+ target_board: str | None = None,
587
+ pin_renames: dict[str, str] | None = None,
588
+ ) -> str:
589
+ """Every vendored README's `../`-relative links (`../../../docs/
590
+ x.md`, a sibling example's `../i2c-scanner/`, ...) resolve against
591
+ the CANONICAL example's OWN position inside the alp-sdk tree --
592
+ dangling once copied out as a standalone scaffold. Rewrite each to
593
+ an absolute GitHub URL (pinned to `docs_ref` -- see `_docs_ref`)
594
+ instead. Also rewrites the one non-existent-once-copied-out token
595
+ every Build section carries: a `west build ...` invocation naming
596
+ THIS template's own repo-relative example path -- the scaffold IS
597
+ the project root wherever the customer unpacks it, so that argument
598
+ becomes `.`. Best-effort (neither pattern found -> text returned
599
+ unchanged); per-template narrative prose (e.g. `tan build
600
+ alp-sdk/examples/...` invocations, cross-references phrased as
601
+ prose rather than a link) is intentionally not scaffold-normalised
602
+ by this pass.
603
+
604
+ Two more issue #864 Fable-review fixes, both applied unconditionally
605
+ (best-effort, no-op when the pattern is absent):
606
+
607
+ * MAJOR B -- `-DEXTRA_ZEPHYR_MODULES=$(pwd)` only registers the
608
+ alp-sdk checkout as a Zephyr module when `$(pwd)` IS that
609
+ checkout (true in-tree); in a copied-out scaffold `$(pwd)` is the
610
+ SCAFFOLD dir, so the module never registers and the documented
611
+ `west build` fails (`CONFIG_ALP_*` unset, `<alp/*.h>`
612
+ unresolvable). Rewritten to `$ALP_SDK_ROOT`, the same var the
613
+ hardened CMakeLists.txt now requires (`_scaffold_cmakelists`).
614
+
615
+ * MAJOR C -- the canonical example's own SoM label ("# Example for
616
+ E1M-AEN801:") and qualified Zephyr board target
617
+ (`alp_e1m_aen801_m55_hp/ae822fa0e5597ls0/rtss_hp`) otherwise
618
+ survive a cross-family sku swap untouched (a V2N101 scaffold
619
+ shipping `-b alp_e1m_aen801_m55_hp/...`; the real
620
+ `alp_e1m_v2n101_m33_sm/r9a09g056n48gbg/cm33` appears nowhere).
621
+ `source_board`/`target_board` are the qualified board id
622
+ (`_core_board`) for the example's own sku / the requested sku's
623
+ re-derived app core respectively. Every source README carries
624
+ the full `/<soc>/<core>` suffix (issue #720), so the exact
625
+ qualified `source_board` string is matched first, consuming that
626
+ suffix along with the short prefix; a SHORT board-id-prefix
627
+ (before the first `/`) word-boundary match then ALSO runs
628
+ unconditionally, for any remaining bare mention that names only
629
+ the board directory (no soc/core), e.g. a `zephyr/boards/alp/
630
+ <board>/` doc link -- a README carrying both shapes gets both
631
+ rewritten, not just whichever one matches first.
632
+
633
+ * `_m33_sm` (RZ/V2N system-manager) scaffold targets -- that board
634
+ family's DEFAULT flasher is `rzv2n_mtd_flash`
635
+ (zephyr/boards/alp/e1m_v2n101_m33_sm/board.cmake,
636
+ e1m_v2m101_m33_sm/board.cmake), which is SSH-to-the-booted-A55
637
+ and always needs `--host`/`ALP_V2N_SSH_HOST` -- a bare `west
638
+ flash` carried over verbatim from an AEN801 (JLink) source
639
+ README silently can't reach the board. Every `west flash` line
640
+ immediately following one of THIS scaffold's own board-target
641
+ lines is rewritten to `west flash --host <board-ip>`; an
642
+ unrelated `west flash` elsewhere in the prose is left alone.
643
+
644
+ `pin_renames` (issue #876 review MINOR 4) is `_derive_pin_renames`'s
645
+ map -- see `_substitute_readme_pins`.
646
+ """
647
+ def _fix_link(m: re.Match[str]) -> str:
648
+ target = posixpath.normpath(f"{example_path}/{m.group(1)}")
649
+ kind = "blob" if "." in target.rsplit("/", 1)[-1] else "tree"
650
+ return f"](https://github.com/alplabai/alp-sdk/{kind}/{docs_ref}/{target})"
651
+
652
+ text = _RELATIVE_LINK_RE.sub(_fix_link, text)
653
+ # A bare (non-link) mention of the example's OWN path -- e.g. a
654
+ # `west build -b <board> <example_path>` argument -- becomes `.`
655
+ # (the scaffold IS the project root wherever it lands). alp-sdk#1855:
656
+ # a MULTI-slice template's README also names a bare SUBPATH of its
657
+ # own example dir this way (mproc-mailbox's `west build -b
658
+ # <board> examples/multicore/mproc-mailbox/peer`, for the HE-side
659
+ # peer/ core) -- the plain `example_path` match above never fires
660
+ # for that (its `(?!\S)` boundary fails on the following `/peer`),
661
+ # so the `/peer` suffix survived verbatim, naming a path that
662
+ # exists only inside the alp-sdk tree. Capture + keep any trailing
663
+ # `/<subpath>` so it becomes `./<subpath>` instead of vanishing.
664
+ text = re.sub(
665
+ rf"(?<!\S){re.escape(example_path)}(/\S+)?(?!\S)",
666
+ lambda m: "." + (m.group(1) or ""), text)
667
+ text = text.replace(
668
+ "-DEXTRA_ZEPHYR_MODULES=$(pwd)", "-DEXTRA_ZEPHYR_MODULES=$ALP_SDK_ROOT")
669
+ if source_board and target_board:
670
+ # Every source README carries the full `/<soc>/<core>` suffix
671
+ # (issue #720), so match the exact qualified string first --
672
+ # its `/<soc>/<core>` suffix is consumed along with the short
673
+ # prefix, avoiding the OLD soc/core suffix being left dangling
674
+ # after the NEW (already fully qualified) `target_board`, e.g.
675
+ # `alp_e1m_v2n101_m33_sm/r9a09g056n48gbg/cm33/ae822fa0e5597ls0/rtss_hp`.
676
+ # The short board-id-prefix (before the first `/`) word-
677
+ # boundary match then ALSO runs, unconditionally -- not only
678
+ # as a fallback when the qualified string is absent -- so a
679
+ # README naming the board BOTH ways (a qualified `west build`
680
+ # line and a separate bare `zephyr/boards/alp/<board>/` doc
681
+ # link) gets both rewritten. `(?!/)` keeps it from re-matching
682
+ # the prefix of a string that's ALREADY (still) fully
683
+ # qualified -- either one this same call just substituted in
684
+ # (leaving `target_board` intact) or, in the sku==example_sku
685
+ # passthrough case, `source_board` itself, still present
686
+ # verbatim after the no-op `replace` above -- which would
687
+ # otherwise get its own `/<soc>/<core>` suffix duplicated onto
688
+ # the end a second time.
689
+ if source_board in text:
690
+ text = text.replace(source_board, target_board)
691
+ source_marker = source_board.split("/", 1)[0]
692
+ text = re.sub(rf"\b{re.escape(source_marker)}\b(?!/)", target_board, text)
693
+ # The `_m33_sm` (RZ/V2N system-manager) board family's DEFAULT
694
+ # flasher is `rzv2n_mtd_flash` (zephyr/boards/alp/
695
+ # e1m_v2n101_m33_sm/board.cmake, e1m_v2m101_m33_sm/board.cmake),
696
+ # which is SSH-to-the-booted-A55 and always needs `--host`/
697
+ # `ALP_V2N_SSH_HOST` -- a bare `west flash` carried over
698
+ # verbatim from an AEN801 (JLink) source README silently can't
699
+ # reach the board. Every `west flash` line immediately
700
+ # following one of THIS scaffold's own board-target lines is
701
+ # rewritten (a multi-core README can carry more than one), so
702
+ # a two-core scaffold doesn't leave its second flash line
703
+ # bare; an unrelated `west flash` elsewhere in the prose is
704
+ # left alone.
705
+ if target_board.split("/", 1)[0].endswith("_m33_sm"):
706
+ marker = re.escape(target_board)
707
+ text = re.sub(
708
+ rf"({marker}[^\n]*\n)west flash\b",
709
+ r"\1west flash --host <board-ip>",
710
+ text)
711
+ if example_sku and sku and example_sku != sku:
712
+ text = text.replace(example_sku, sku)
713
+ text = _substitute_readme_pins(text, pin_renames or {})
714
+ return text