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,204 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Whether a SoM's hw_rev -- explicit, or (absent that) its DEFAULT -- is
3
+ buildable, read directly off the alp-sdk metadata tree, without importing
4
+ `tan.planner` (see `tan/core/example_catalog.py`'s own docstring for why:
5
+ `tan.planner.paths` binds `REPO = sdk_root()` at import time, so pulling it
6
+ into `init_cmd` before an SDK is even resolved raises `PlannerRootError`).
7
+
8
+ tan-cli#743. `tan init --template minimal-app --som <sku>` for a SoM whose
9
+ default hw_rev is not buildable (a `reserved`/`tbd` revision) exits 0 and scaffolds a project whose FIRST `tan validate`
10
+ hard-errors::
11
+
12
+ sdk-compat: SoM <sku> hw_rev '<rev>' exists but is not buildable
13
+ (status: 'tbd').
14
+
15
+ `init` never writes an explicit `hw_rev:` into a scaffolded `minimal-app`/
16
+ vendored-template board.yaml, so the value `validate` refuses there is the
17
+ one the SDK loader falls back to -- `hw_rev or
18
+ som_preset.get("default_hw_rev")` (`scripts/alp_orchestrate/loader.py:1241`
19
+ and siblings) -- i.e. the SoM preset's OWN `default_hw_rev:`. `validate` is
20
+ not wrong here: the fact is real and correctly published (the family's
21
+ `metadata/e1m_modules/<family>/hw-revisions.yaml` `<rev>: {status: tbd}`). The
22
+ defect is that `init` stays silent about a fact it could have read from the
23
+ exact same preset it already resolved -- so this module reads that one fact
24
+ (the SKU's effective hw_rev + the family's declared status for it) so `init`
25
+ can warn about it instead.
26
+
27
+ tan-cli#1008 review majors 1+2: an `--from-example`/`--topology` copy can
28
+ carry an EXPLICIT `hw_rev:` of its own (`tan/core/scaffold.vendored_som`
29
+ reads it off the planned board.yaml) -- the original example's own revision,
30
+ or one that survived an INTRA-family retarget (`retarget_board_yaml_som`
31
+ only drops the sibling `hw_rev:` on a CROSS-family retarget -- review round
32
+ 4 -- since within one family the value is still a real, declared revision
33
+ for the new SKU too). `hw_rev_not_buildable`'s caller passes that explicit
34
+ value through when there is one, so the pair this module judges (and the
35
+ warning names) is always the one actually written to disk, never a value
36
+ inferred from `--som`/`DEFAULT_SOM_SKU` alone.
37
+
38
+ WHY A WARNING AND NOT A REFUSAL: `minimal-app` is deliberately the one
39
+ template every OTHER template's own refusal message names as the escape
40
+ hatch -- "Use --template minimal-app, which is vendor-neutral and scaffolds
41
+ any SoM" (`tan/core/scaffold.py`'s vendored-scaffold refusal text). If this
42
+ check also refused, a SoM in `status: preliminary` would have no self-contained
43
+ scaffolding path at all -- worse than the silent defect it replaces. This
44
+ mirrors `example_catalog.unsupported_som`'s own precedent (warn, still
45
+ write) for the identical reason: refusing there "made `tan init
46
+ --from-example` unusable for nearly the whole AEN family, which is worse
47
+ than the original defect".
48
+
49
+ Deliberately duplicates the tiny SKU -> family-directory map
50
+ `scripts/alp_project_loader._sku_family` also carries (`AEN`/`V2N`/`V2M`
51
+ -> `aen`/`v2n`/`v2n-m1`) and the buildable-status set
52
+ `scripts/alp_orchestrate/sdk_compat._NOT_BUILDABLE_STATUSES` (`reserved`,
53
+ `tbd`, or a missing `status:` key) rather than importing either: neither
54
+ module is on `sys.path` for a `tan init --template` invocation, which reads
55
+ `--sdk-root` only as a metadata SOURCE, never as an importable package.
56
+ """
57
+
58
+ from __future__ import annotations
59
+
60
+ import re
61
+ from pathlib import Path
62
+ from typing import Optional
63
+
64
+ #: Mirrors `scripts/alp_project_loader._SKU_FAMILY` / `_sku_family` verbatim.
65
+ _SKU_FAMILY = re.compile(r"^E1M-(AEN|V2N|V2M)")
66
+ _FAMILY_DIR = {"AEN": "aen", "V2N": "v2n", "V2M": "v2n-m1"}
67
+
68
+ #: Mirrors `scripts/alp_orchestrate/sdk_compat._NOT_BUILDABLE_STATUSES`
69
+ #: verbatim (alp-sdk #1025): `reserved`, `tbd`, and a missing `status:` key
70
+ #: are all refused; every other declared status is buildable.
71
+ _NOT_BUILDABLE_STATUSES = frozenset({"reserved", "tbd"})
72
+
73
+
74
+ class HwRevNotBuildable:
75
+ """The hw_rev a scaffolded board.yaml resolves to (its own explicit
76
+ `hw_rev:`, or -- absent that -- the SoM preset's `default_hw_rev:`), and
77
+ why `tan validate` will refuse it.
78
+
79
+ `has_buildable_alternative` -- tan-cli#1008 review minor -- is whether
80
+ the SAME family table declares at least one OTHER hw_rev whose status
81
+ permits a build: the "or until board.yaml names a buildable `hw_rev:`
82
+ explicitly" remedy is only real advice when this is true. A family that
83
+ publishes exactly one hw_rev, and that one not buildable, would make the
84
+ clause advice that reproduces the identical refusal, rc 2, every time."""
85
+
86
+ __slots__ = ("sku", "hw_rev", "status", "has_buildable_alternative")
87
+
88
+ def __init__(
89
+ self, sku: str, hw_rev: str, status: Optional[str], has_buildable_alternative: bool
90
+ ) -> None:
91
+ self.sku = sku
92
+ self.hw_rev = hw_rev
93
+ self.status = status
94
+ self.has_buildable_alternative = has_buildable_alternative
95
+
96
+
97
+ def _safe_load_mapping(path: Path) -> Optional[dict]:
98
+ """`yaml.safe_load(path)`, or None for every reason a caller here must
99
+ treat as "nothing to check" rather than an error: absent, unreadable,
100
+ unparseable, or parsed to something other than a mapping.
101
+
102
+ Imports PyYAML lazily, inside the call, not at module scope: this module
103
+ is reached from `init_cmd`, which `tan/cli.py` static-imports on EVERY
104
+ invocation, and `tests/gates/test_cli_import_is_lean.py` (tan-cli#810)
105
+ pins PyYAML as loaded only by `new_som_cmd`/`kconfig_cmd`'s single-use
106
+ paths -- a module-scope `import yaml` here would load it on `tan
107
+ --version` too.
108
+
109
+ tan-cli#1116 review round 2: `UnicodeDecodeError` is caught alongside
110
+ `OSError` -- it is a `ValueError`, not an `OSError`, so a non-UTF-8
111
+ `hw-revisions.yaml` used to raise raw past `hw_rev_not_buildable`'s own
112
+ written "Never raises" contract (measured escaping through that real
113
+ caller before this fix).
114
+ """
115
+ import yaml
116
+
117
+ try:
118
+ text = path.read_text(encoding="utf-8")
119
+ except (OSError, UnicodeDecodeError):
120
+ return None
121
+ try:
122
+ doc = yaml.safe_load(text)
123
+ except yaml.YAMLError:
124
+ return None
125
+ return doc if isinstance(doc, dict) else None
126
+
127
+
128
+ def hw_rev_not_buildable(
129
+ sdk_root: Path, sku: str, hw_rev: Optional[str] = None
130
+ ) -> Optional[HwRevNotBuildable]:
131
+ """Whether `hw_rev` -- or, when `None`/empty, `sku`'s own
132
+ `default_hw_rev:` -- is buildable per its family's `hw-revisions.yaml`.
133
+ Returns None for "nothing to report" -- covering every reason a caller
134
+ here must NOT warn over, deliberately mirroring
135
+ `example_catalog.unsupported_som`'s own contract:
136
+
137
+ * `sku` doesn't match the known SoM SKU pattern at all,
138
+ * no SoM preset file for `sku` in this checkout,
139
+ * `hw_rev` is `None`/empty and the preset declares no (or a malformed)
140
+ `default_hw_rev:` to fall back to,
141
+ * no family `hw-revisions.yaml` table (an in-development family with
142
+ nothing to check against yet),
143
+ * the family table doesn't declare `hw_rev` as a key at all --
144
+ existence is `revision_known()`'s question
145
+ (`tan/planner/sdk_compat.py`), not this one's; an unknown revision
146
+ has no status to have read, so there is nothing here to judge,
147
+ * the declared status permits a build.
148
+
149
+ Never raises: a scaffold must not fail because a metadata file could
150
+ not be read.
151
+ """
152
+ match = _SKU_FAMILY.match(sku)
153
+ family_dir = _FAMILY_DIR.get(match.group(1)) if match else None
154
+ if family_dir is None:
155
+ return None
156
+
157
+ preset = _safe_load_mapping(sdk_root / "metadata" / "e1m_modules" / f"{sku}.yaml")
158
+ if preset is None:
159
+ return None
160
+ if not hw_rev:
161
+ hw_rev = preset.get("default_hw_rev")
162
+ if not isinstance(hw_rev, str) or not hw_rev:
163
+ return None
164
+
165
+ table = _safe_load_mapping(
166
+ sdk_root / "metadata" / "e1m_modules" / family_dir / "hw-revisions.yaml"
167
+ )
168
+ if table is None:
169
+ return None
170
+ revisions = table.get("hw_revisions")
171
+ if not isinstance(revisions, dict):
172
+ return None
173
+ entry = revisions.get(hw_rev)
174
+ if not isinstance(entry, dict):
175
+ # Mirrors `alp_orchestrate/sdk_compat.revision_buildable` verbatim: a
176
+ # key present but not a dict (None, a bare string, ...) is a
177
+ # malformed entry, not an absent one -- it has no status, so it is
178
+ # not buildable. Only a truly absent key stays "nothing to judge"
179
+ # (unknown, `revision_known()`'s question).
180
+ if hw_rev not in revisions:
181
+ return None
182
+ return HwRevNotBuildable(sku, hw_rev, None, _has_other_buildable(revisions, hw_rev))
183
+
184
+ status = entry.get("status")
185
+ if isinstance(status, str) and status not in _NOT_BUILDABLE_STATUSES:
186
+ return None
187
+ return HwRevNotBuildable(
188
+ sku, hw_rev, status if isinstance(status, str) else None,
189
+ _has_other_buildable(revisions, hw_rev),
190
+ )
191
+
192
+
193
+ def _has_other_buildable(revisions: dict, hw_rev: str) -> bool:
194
+ """Whether `revisions` (the family's `hw_revisions:` map) declares any
195
+ key OTHER than `hw_rev` whose `status:` permits a build -- the fact that
196
+ decides whether "or name a buildable `hw_rev:` explicitly" is real
197
+ advice or a dead end (tan-cli#1008 review minor)."""
198
+ return any(
199
+ other_rev != hw_rev
200
+ and isinstance(other_entry, dict)
201
+ and isinstance(other_entry.get("status"), str)
202
+ and other_entry.get("status") not in _NOT_BUILDABLE_STATUSES
203
+ for other_rev, other_entry in revisions.items()
204
+ )
@@ -0,0 +1,62 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The one SoM-preset `schema_version` this tan reads, and the message for a
3
+ preset it skips because it declares another.
4
+
5
+ alp-sdk#2024 moved every `metadata/e1m_modules/*.yaml` preset to
6
+ `schema_version: 2` (`som-preset-v2.schema.json`, `write_authority` required,
7
+ `npu_population` removed). tan-cli#1297 moved tan's readers with it, and every
8
+ one of them refuses a preset declaring anything else rather than guessing at a
9
+ shape it does not know. Against an SDK that predates #2024 (released alp-sdk
10
+ `v0.16.0` and older) that refusal reaches EVERY preset, so `tan presets`
11
+ listed `skus: []` and `tan size` reported every budget as `unreadable SoM
12
+ preset`, with nothing saying why. `skipped_presets_message` is the why: one
13
+ sentence, shared by both commands so they cannot word the same fact two ways.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterable
19
+
20
+ #: The one `schema_version:` a SoM preset may declare.
21
+ #: `som-preset-v2.schema.json` pins it `const: 2`.
22
+ SOM_SCHEMA_VERSION = 2
23
+
24
+ #: The upstream change that introduced v2, named in the message so a reader
25
+ #: can tell "old SDK" from "broken preset".
26
+ _V2_ORIGIN = "alp-sdk#2024"
27
+
28
+
29
+ def is_supported_som_schema_version(version: object) -> bool:
30
+ """True only for the bare integer `SOM_SCHEMA_VERSION`. `bool` is excluded
31
+ explicitly: `True == 1` and `isinstance(True, int)` both hold in Python."""
32
+ return isinstance(version, int) and not isinstance(version, bool) and version == SOM_SCHEMA_VERSION
33
+
34
+
35
+ def _remedy(versions: list[object]) -> str:
36
+ ints = [v for v in versions if isinstance(v, int) and not isinstance(v, bool)]
37
+ if ints and len(ints) == len(versions) and max(ints) < SOM_SCHEMA_VERSION:
38
+ return (
39
+ f"the bound alp-sdk predates som-preset v{SOM_SCHEMA_VERSION} "
40
+ f"({_V2_ORIGIN}); point --sdk-root at an alp-sdk whose "
41
+ f"metadata/schemas/ ships som-preset-v{SOM_SCHEMA_VERSION}.schema.json, "
42
+ "or use a tan release that matches this SDK"
43
+ )
44
+ if ints and len(ints) == len(versions) and min(ints) > SOM_SCHEMA_VERSION:
45
+ return "the bound alp-sdk is newer than this tan; upgrade tan"
46
+ return "fix the preset's schema_version or use an alp-sdk whose presets match this tan"
47
+
48
+
49
+ def skipped_presets_message(subject: str, versions: Iterable[object]) -> str:
50
+ """`<subject> declare(s) schema_version <v[, v...]> (this tan reads
51
+ som-preset schema_version 2): <remedy>.`
52
+
53
+ *subject* names what was skipped ("12 SoM presets under <dir>", "the SoM
54
+ preset for E1M-AEN801"); *versions* is every skipped preset's declared
55
+ value, duplicates allowed -- they are collapsed, in first-seen order, and
56
+ an absent key reads as `None`, exactly what the preset said."""
57
+ seen = list(versions)
58
+ shown = ", ".join(dict.fromkeys(repr(v) for v in seen))
59
+ return (
60
+ f"{subject}: schema_version {shown} (this tan reads "
61
+ f"som-preset schema_version {SOM_SCHEMA_VERSION}) -- {_remedy(seen)}."
62
+ )
@@ -0,0 +1,201 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The one shared rule for building a subprocess child's environment.
3
+
4
+ **Every spawn site under `python/tan/` must route its `env=` through
5
+ [`spawn_env`] (or, for a caller that already owns its own mutable base dict,
6
+ [`restore_ld_library_path`] directly) rather than build one by hand.**
7
+ tan-cli#992 is what happens when that is left to 25 independent call sites
8
+ instead: one gets it right and the other 24 leak.
9
+
10
+ ## The rule
11
+
12
+ PyInstaller's Linux ONEDIR bootloader points `LD_LIBRARY_PATH` at the frozen
13
+ app's own bundled `_internal/` lib dir -- so the FROZEN `tan` binary finds its
14
+ OWN bundled shared libraries -- and stashes the caller's original value in
15
+ `LD_LIBRARY_PATH_ORIG` before doing so. Every child process `tan` spawns
16
+ (`west`, `git`, `dxcom`, a J-Link tool, and everything THOSE spawn in turn) is
17
+ a SYSTEM program, never a bundled one, so unless the spawn site restores
18
+ `LD_LIBRARY_PATH_ORIG` before spawning, that child inherits tan's bundled path
19
+ ahead of the system one and can silently resolve one of tan's own bundled
20
+ shared libraries instead of the host's -- only surfacing when the two
21
+ versions actually disagree in a way that breaks something.
22
+
23
+ Measured, tan-cli#990/#992, on real CI: `tan bootstrap` spawned `west sdk
24
+ install`, which shelled `tar --xz`, which loaded tan's bundled
25
+ `liblzma.so.5` (older than the host's) ahead of the system one and failed
26
+ outright:
27
+
28
+ xz: /home/runner/.local/bin/tan-cli-lib/_internal/liblzma.so.5:
29
+ version `XZ_5.4' not found (required by xz)
30
+ /usr/bin/tar: Child returned status 1
31
+
32
+ `LD_LIBRARY_PATH_ORIG` is the PREFERRED trigger: it is PyInstaller's own
33
+ marker that its bootloader modified `LD_LIBRARY_PATH` for THIS process, and
34
+ it carries the exact value to restore.
35
+
36
+ **It is not the ONLY trigger, and tan-cli#1189 is what that cost.** Until
37
+ then this module treated an absent `ORIG` as proof that no override had
38
+ happened -- "a frozen run whose `LD_LIBRARY_PATH_ORIG` is absent" was listed
39
+ as a host that never had the problem. Measured on a bare `ubuntu:24.04`, the
40
+ identical `liblzma.so.5` failure came back with tan-cli#992's fix in place,
41
+ so on that host the restore did not fire. A host with no `LD_LIBRARY_PATH`
42
+ of its own is precisely the host where the bundle becomes the ONLY entry on
43
+ the child's search path -- the worst case, not the safe one.
44
+
45
+ So the second trigger is the fact that does not depend on the bootloader
46
+ having recorded anything: is the bundle dir ([`_frozen_bundle_dir`], i.e.
47
+ `sys._MEIPASS` on a frozen run) currently ON `LD_LIBRARY_PATH`? Both
48
+ triggers are still false on `python -m tan` from source (this repo's own
49
+ dev/CI/test runs, where `sys.frozen` is unset) and on macOS/Windows
50
+ (`LD_LIBRARY_PATH` is not their loader variable), so this still never
51
+ touches a host that never had the problem -- and it still never guesses at a
52
+ fallback value: with no `ORIG`, the only defensible restore is REMOVAL.
53
+
54
+ Lifted out of `tan.commands.bootstrap_cmd.Runner._env`, which worked out this
55
+ rule first (tan-cli#990) but was exactly one spawn site out of ~25 under
56
+ `python/tan/` -- this module is the ONE place the rule is written down,
57
+ per tan-cli#992. `Runner._env` itself now calls [`restore_ld_library_path`]
58
+ rather than carrying its own copy of the restore logic.
59
+ """
60
+ from __future__ import annotations
61
+
62
+ import os
63
+ import sys
64
+ from collections.abc import Mapping
65
+
66
+
67
+ def _frozen_bundle_dir() -> str | None:
68
+ """The directory a PyInstaller ONEDIR bootloader puts on
69
+ `LD_LIBRARY_PATH` so the frozen binary finds its OWN shared libraries --
70
+ `sys._MEIPASS`, which for a onedir freeze is the `_internal/` tree.
71
+ `None` on a source run (this repo's own dev/CI/test runs), where
72
+ `sys.frozen` is unset and there is no bundle to strip."""
73
+ if not getattr(sys, "frozen", False):
74
+ return None
75
+ bundle = getattr(sys, "_MEIPASS", None)
76
+ return bundle if isinstance(bundle, str) and bundle else None
77
+
78
+
79
+ def _without_bundle_dir(value: str, bundle: str) -> str | None:
80
+ """`value` (an `os.pathsep`-joined search path) with every entry that IS
81
+ `bundle` removed -- or `None` when `bundle` does not appear in it at all,
82
+ which is the caller's signal that there is nothing to undo.
83
+
84
+ Compared with `os.path.normpath`, so a trailing slash or a `./` segment
85
+ cannot smuggle the bundle past the match. Empty entries are dropped from
86
+ the result: stripping the bundle out of `"<bundle>"` or
87
+ `"<bundle>:<bundle>"` must not leave a bare `":"`, which some loaders
88
+ read as "search the current directory" -- the same hazard the
89
+ `orig == ""` branch below is written to avoid."""
90
+ entries = value.split(os.pathsep)
91
+ target = os.path.normpath(bundle)
92
+ if not any(entry and os.path.normpath(entry) == target for entry in entries):
93
+ return None
94
+ kept = [entry for entry in entries if entry and os.path.normpath(entry) != target]
95
+ return os.pathsep.join(kept)
96
+
97
+
98
+ def ld_library_path_needs_restore() -> bool:
99
+ """Would a child spawned from THIS process inherit the frozen bundle's
100
+ lib dir on `LD_LIBRARY_PATH`?
101
+
102
+ The predicate a caller uses to decide whether it may skip building a
103
+ child environment at all. `bootstrap_cmd.Runner._env` is the one such
104
+ caller: it returns `None` (meaning `subprocess.run(env=None)`, "inherit
105
+ this process's unrestored environment") on its fast path, and that fast
106
+ path is only safe when this returns `False`."""
107
+ if os.environ.get("LD_LIBRARY_PATH_ORIG") is not None:
108
+ return True
109
+ bundle = _frozen_bundle_dir()
110
+ if bundle is None:
111
+ return False
112
+ current = os.environ.get("LD_LIBRARY_PATH")
113
+ if not current:
114
+ return False
115
+ return _without_bundle_dir(current, bundle) is not None
116
+
117
+
118
+ def _restore_from_orig(env: dict[str, str], orig: str) -> None:
119
+ """The bootloader recorded a marker, so restore exactly what it saved.
120
+
121
+ POPS the key rather than setting `""` when the host had no
122
+ `LD_LIBRARY_PATH` before the bootloader touched it -- some dynamic
123
+ loaders read an explicit empty value as "search the current directory",
124
+ which is worse than the key being absent."""
125
+ if orig:
126
+ env["LD_LIBRARY_PATH"] = orig
127
+ else:
128
+ env.pop("LD_LIBRARY_PATH", None)
129
+
130
+
131
+ def _strip_bundle_dir(env: dict[str, str]) -> None:
132
+ """No marker was recorded -- remove the bundle dir from `LD_LIBRARY_PATH`
133
+ if it is on it (tan-cli#1189).
134
+
135
+ Deliberately does NOT invent a replacement value: with no
136
+ `LD_LIBRARY_PATH_ORIG` the only defensible restore is REMOVAL. A frozen
137
+ run whose path does not carry the bundle is left completely alone -- the
138
+ bootloader never touched it, so neither does this."""
139
+ bundle = _frozen_bundle_dir()
140
+ if bundle is None:
141
+ return
142
+ current = os.environ.get("LD_LIBRARY_PATH")
143
+ if not current:
144
+ return
145
+ stripped = _without_bundle_dir(current, bundle)
146
+ if stripped is None:
147
+ return
148
+ if stripped:
149
+ env["LD_LIBRARY_PATH"] = stripped
150
+ else:
151
+ env.pop("LD_LIBRARY_PATH", None)
152
+
153
+
154
+ def restore_ld_library_path(env: dict[str, str]) -> None:
155
+ """Mutate `env` IN PLACE: undo the frozen bootloader's `LD_LIBRARY_PATH`
156
+ override, reading the REAL process environment (`os.environ`) -- never
157
+ `env` itself, which may already be a partially-built child env with no
158
+ such key.
159
+
160
+ A no-op on every host that never had the problem: a source run, macOS,
161
+ Windows, or a frozen run where the bundle is not on the search path.
162
+
163
+ Two triggers, not one -- see this module's docstring for why an absent
164
+ `LD_LIBRARY_PATH_ORIG` is NOT proof that no override happened
165
+ (tan-cli#1189), and [`_restore_from_orig`] / [`_strip_bundle_dir`] for
166
+ what each arm does."""
167
+ orig = os.environ.get("LD_LIBRARY_PATH_ORIG")
168
+ if orig is not None:
169
+ _restore_from_orig(env, orig)
170
+ return
171
+ _strip_bundle_dir(env)
172
+
173
+
174
+ def spawn_env(
175
+ overrides: Mapping[str, str] | None = None,
176
+ *,
177
+ base: Mapping[str, str] | None = None,
178
+ ) -> dict[str, str]:
179
+ """The `env=` every subprocess spawn in `tan` hands its child: a fresh
180
+ copy of `base` (`os.environ` when not given) with
181
+ [`restore_ld_library_path`] applied, then `overrides` layered on top.
182
+
183
+ `base`, when given, is typically an already-adjusted mapping a caller
184
+ built for its own reasons (e.g. `with_venv_on_path`'s PATH prepend) --
185
+ `spawn_env` copies it rather than mutating it, then applies the SAME
186
+ restore on top, so a caller can freely compose
187
+ `with_venv_on_path(spawn_env(), tool)` or
188
+ `spawn_env(base=with_venv_on_path(dict(os.environ), tool))`
189
+ interchangeably; the two touch disjoint keys (`PATH` vs
190
+ `LD_LIBRARY_PATH`) so the order never matters.
191
+
192
+ Always returns an explicit `dict`, never `None`:
193
+ `subprocess.run(env=None)` means "inherit this process's own
194
+ (unrestored) environment", which is exactly the leak this module exists
195
+ to close -- no call site may spawn with an implicit/`None` environment.
196
+ """
197
+ env = dict(base) if base is not None else dict(os.environ)
198
+ restore_ld_library_path(env)
199
+ if overrides:
200
+ env.update(overrides)
201
+ return env
@@ -0,0 +1,94 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Re-derive the sysbuild image prefix of a slice's per-core `alp.conf` after
3
+ token substitution (alp-sdk#866, consumer side).
4
+
5
+ On a `--sysbuild` slice the planner passes the per-core `alp.conf` as
6
+ `-D<image>_EXTRA_CONF_FILE=<build>/alp.conf`, where `<image>` is the
7
+ basename of the app directory handed to `west build` -- sysbuild's own name
8
+ for the application image (`get_filename_component(app_name ${APP_DIR}
9
+ NAME)`). The planner computes that basename from the directory it EMITTED
10
+ in. A tokened plan's app dir is `${PROJECT_ROOT}/...`, so the same plan run
11
+ from a differently-named project root (`tan build --plan-from plan.json
12
+ --execute` in `/b/bar` for a plan emitted in `/a/foo`) names image `foo`
13
+ while sysbuild builds image `bar`: Zephyr ignores the unknown
14
+ `foo_EXTRA_CONF_FILE` and the image builds WITHOUT its per-core
15
+ configuration, with no error. alp-sdk's own orchestrator comment says a
16
+ consumer that relocates the project root must re-derive the prefix; this is
17
+ that re-derivation. `tan/planner/orchestrator.py` is an upstream mirror and
18
+ stays as emitted.
19
+
20
+ Rewrite, not refuse: the arg is identified by its VALUE (this slice's own
21
+ `<buildDir>/alp.conf`), not by its prefix, so retargeting it to the image
22
+ sysbuild will actually build is unambiguous, and the right prefix is a pure
23
+ function of the substituted command. Refusing would make every relocated
24
+ tokened plan for a `boot:`/OTA project unbuildable, when the planner already
25
+ said exactly how to fix it. Any other `-D<x>_EXTRA_CONF_FILE` (one naming
26
+ `mcuboot`, say) is not this slice's `alp.conf` and is left alone.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import re
32
+ from dataclasses import replace
33
+
34
+ from tan.core.build_plan import BuildPlan, Slice, SliceCommand
35
+
36
+ _IMAGE_SCOPED = re.compile(r"^-D(?P<image>[^=]+)_EXTRA_CONF_FILE=(?P<value>.*)$", re.S)
37
+
38
+ #: `west build` options that take a separate value -- skipped when locating
39
+ #: the positional app-dir argument (same set alp-sdk's seam1 comparator uses).
40
+ _OPTIONS_WITH_VALUE = frozenset({"-b", "-d", "-p"})
41
+
42
+
43
+ def west_app_dir_basename(args: list[str]) -> str | None:
44
+ """Basename of `west build`'s app-dir argument: the first non-option arg
45
+ after `build`, or `None` when there is none."""
46
+ try:
47
+ i = args.index("build") + 1
48
+ except ValueError:
49
+ return None
50
+ while i < len(args) and args[i].startswith("-"):
51
+ i += 2 if args[i] in _OPTIONS_WITH_VALUE else 1
52
+ if i >= len(args) or args[i] == "--":
53
+ return None
54
+ name = args[i].replace("\\", "/").rstrip("/").rsplit("/", 1)[-1]
55
+ return name or None
56
+
57
+
58
+ def _is_slice_alp_conf(value: str, build_dir: str) -> bool:
59
+ v = value.replace("\\", "/")
60
+ b = build_dir.replace("\\", "/").rstrip("/")
61
+ return v == f"{b}/alp.conf" or v.endswith(f"/{b}/alp.conf")
62
+
63
+
64
+ def rescope_command(command: SliceCommand, build_dir: str) -> SliceCommand:
65
+ """`command` with its per-core `-D<image>_EXTRA_CONF_FILE` renamed to the
66
+ image `west build` will create; unchanged when not sysbuild, when the app
67
+ dir is unknown, or when the prefix already matches."""
68
+ args = command.args
69
+ if "--sysbuild" not in args:
70
+ return command
71
+ image = west_app_dir_basename(args)
72
+ if image is None:
73
+ return command
74
+ new_args = []
75
+ for arg in args:
76
+ m = _IMAGE_SCOPED.match(arg) if isinstance(arg, str) else None
77
+ if m and m.group("image") != image and _is_slice_alp_conf(m.group("value"), build_dir):
78
+ arg = f"-D{image}_EXTRA_CONF_FILE={m.group('value')}"
79
+ new_args.append(arg)
80
+ if new_args == args:
81
+ return command
82
+ return replace(command, args=new_args)
83
+
84
+
85
+ def rescope_sysbuild_extra_conf(plan: BuildPlan) -> BuildPlan:
86
+ """`plan` with every slice's command passed through `rescope_command`."""
87
+ slices: list[Slice] = []
88
+ for sl in plan.slices:
89
+ if sl.command is None:
90
+ slices.append(sl)
91
+ continue
92
+ cmd = rescope_command(sl.command, sl.build_dir)
93
+ slices.append(sl if cmd is sl.command else replace(sl, command=cmd))
94
+ return replace(plan, slices=slices)