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,331 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The seam `tan image` and `tan size` share: both consume what a build
3
+ PRODUCED, so both resolve the same project context, the same build root, and the
4
+ same `build/system-manifest.yaml`.
5
+
6
+ Written once here rather than twice in the two command modules -- that
7
+ duplication is how the two halves of the same rule drift, and I-18 (the
8
+ `west build` nesting) is exactly such a rule: it has to hold identically for the
9
+ slice `tan size` measures and the slice `tan image` archives.
10
+
11
+ Ports the CLI-side halves of `crates/tan-cli/src/commands/{size,image}.rs` --
12
+ `resolve_cli_project_context`, `resolve_sdk_root`, the `app_path`/`--build-root`
13
+ resolution and the manifest load -- all of which are byte-identical between the
14
+ two commands in the oracle.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+
23
+ from tan.core.sdk_discovery import resolve_sdk_root_ladder, resolve_sdk_tiered
24
+ from tan.core.shapes import SDK_MARKER
25
+ from tan.core.system_manifest import (
26
+ find_manifest,
27
+ manifest_candidates,
28
+ SystemManifest,
29
+ SystemManifestError,
30
+ parse_system_manifest,
31
+ )
32
+ from tan.envelope import Project, SdkInfo
33
+
34
+
35
+ def to_posix(path: str) -> str:
36
+ """Forward slashes always. Mirrors `tan_core::project::to_posix`: every field
37
+ in the extension/CLI handshake must be platform-identical."""
38
+ return path.replace("\\", "/")
39
+
40
+
41
+ def normalise(path: str) -> str:
42
+ """Rust's `normalize_path(cwd.join(p))`: cwd-anchored and lexically
43
+ normalised, in the platform's own separators.
44
+
45
+ `os.path.abspath`, not `Path.resolve()`: abspath is purely lexical, so a
46
+ project reached through a symlink keeps the name the user typed and a path
47
+ that does not exist yet still resolves.
48
+ """
49
+ return os.path.abspath(path)
50
+
51
+
52
+ def _has_loader(root: str) -> bool:
53
+ """`scripts/alp_project.py` is THE marker for an alp-sdk checkout -- the same
54
+ literal `tan_core::project` hardcodes (I-31). Spelled once, in
55
+ `build_cmd.SDK_MARKER`."""
56
+ return Path(root).joinpath(*SDK_MARKER).exists()
57
+
58
+
59
+ @dataclass
60
+ class ProjectContext:
61
+ """What both commands report and build their paths from."""
62
+
63
+ #: `project.root` -- posix, absolute.
64
+ workspace_root: str
65
+ #: The RESOLVER's own answer -- posix, absolute, joined onto `workspace_root`
66
+ #: unconditionally, matching `project.rs::resolve_board_yaml_path`: the
67
+ #: field names where one WOULD be. Existence is NOT checked here; `project()`
68
+ #: below is the seam that checks it (tan-cli#236).
69
+ board_yaml: str
70
+ #: The envelope's `sdk` block, or `None` when nothing resolved (absent, never
71
+ #: `null`).
72
+ sdk: SdkInfo | None
73
+ #: `ActiveSdk.broken_project_pin` carried through (tan-cli#263 review) --
74
+ #: `size`/`image`'s own `_run` turns this into the shared
75
+ #: `sdk.project-pin-unresolved` warning via `sdk_discovery.project_pin_issue`.
76
+ broken_project_pin: str | None = None
77
+ #: `ActiveSdk.tier`, unconditionally -- unlike `sdk` above, set even when
78
+ #: nothing resolved to a usable checkout (`sdk is None`), which is exactly
79
+ #: the shape `project_pin_issue` needs: a broken pin with NO working
80
+ #: fallback still has a real tier (`"none"`) to name in its message.
81
+ sdk_source_tier: str = "none"
82
+ #: `ActiveSdk.foreign_global_default_for` carried through (tan-cli#464) --
83
+ #: `size`/`image`'s own `_run` turns this into
84
+ #: `sdk_discovery.global_default_foreign_project_issue`, the sibling warning to
85
+ #: `broken_project_pin` above.
86
+ foreign_global_default_for: str | None = None
87
+
88
+ def project(self) -> Project:
89
+ return Project.resolved(self.workspace_root, self.board_yaml)
90
+
91
+
92
+ def resolve_project_context(
93
+ project_arg: str | None, board_yaml_arg: str | None, sdk_root_arg: str | None
94
+ ) -> ProjectContext:
95
+ """Port of `util::resolve_cli_project_context` for these two commands.
96
+
97
+ The `sdk` block is populated from the SAME resolution that produced
98
+ `workspace_root`, never a second lookup -- that is what `crate::sdk_report`
99
+ exists to guarantee. A `--sdk-root` that is not a checkout resolves to
100
+ NOTHING (core re-validates the loader marker), so the key is then absent
101
+ rather than advertising a path no command could use.
102
+ """
103
+ workspace_root = normalise(project_arg or ".")
104
+ configured = board_yaml_arg or "board.yaml"
105
+ board_yaml = (
106
+ configured
107
+ if os.path.isabs(configured)
108
+ else os.path.join(workspace_root, configured)
109
+ )
110
+ active = resolve_sdk_tiered(sdk_root_arg, Path(workspace_root))
111
+ # `resolve_sdk_tiered` returns `--sdk-root` as-is EVEN WHEN INVALID (I-31,
112
+ # deliberately, so `tan sdk` can report the path the user typed). The
113
+ # envelope's `sdk` must not: `resolve_cli_project_context` only records what
114
+ # core's own loader-marker check accepted.
115
+ sdk = (
116
+ SdkInfo.from_resolution(to_posix(active.path), active)
117
+ if active.path is not None and _has_loader(active.path)
118
+ else None
119
+ )
120
+ return ProjectContext(
121
+ workspace_root=to_posix(workspace_root),
122
+ board_yaml=to_posix(board_yaml),
123
+ sdk=sdk,
124
+ broken_project_pin=active.broken_project_pin,
125
+ sdk_source_tier=active.tier,
126
+ foreign_global_default_for=active.foreign_global_default_for,
127
+ )
128
+
129
+
130
+ def resolve_metadata_sdk_root(
131
+ sdk_root_arg: str | None, workspace_root: str
132
+ ) -> Path | None:
133
+ """The alp-sdk checkout `tan size` reads `metadata/**` out of -- port of
134
+ `util::resolve_sdk_root`: `--sdk-root` (terminal) > the project's own
135
+ `.alp/sdk-path` pin > the machine-global default (`~/.alp/sdk-default`) >
136
+ the positional walk (`resolve_sdk_root_ladder`, the NARROW ladder shared
137
+ with `build`/`doctor`/the ten other narrowly-resolving commands -- not the
138
+ wide one `init`/`generate`/`examples` take). No `ALP_SDK_ROOT` tier
139
+ (tried and reverted -- see `resolve_sdk_root_ladder`'s own docstring).
140
+
141
+ Kept separate from [`resolve_project_context`] because the oracle keeps them
142
+ separate: `size.rs` calls BOTH, and they can legitimately disagree -- a
143
+ child checkout supplies the memory budget while the envelope's `sdk` stays
144
+ absent. Collapsing them would change which budget resolves.
145
+
146
+ An ABSENT `--sdk-root` with no checkout found is NOT fatal (the retired
147
+ Python's `find_sdk_root` die is deliberately dropped): the budget resolves
148
+ `unknown` and measurement still runs. A non-empty `--sdk-root` that fails
149
+ the loader-marker check returns `None` here too, but `size` treats THAT as
150
+ fatal (`size.sdk-root-unresolved`, tan-cli#1463): a typo'd flag silently
151
+ ignored is the bug.
152
+ """
153
+ flag = (sdk_root_arg or "").strip()
154
+ if flag:
155
+ # Terminal: a bad `--sdk-root` must fail loudly as "no budget resolved",
156
+ # not silently fall through to some other checkout.
157
+ return Path(flag) if _has_loader(flag) else None
158
+ # `_broken_pin` unused here: `resolve_project_context` above already
159
+ # resolved (and each caller already reports) the SAME `.alp/sdk-path`
160
+ # pin's own status via `ProjectContext.broken_project_pin`; this walk is
161
+ # the deliberately SEPARATE metadata-budget resolution the module
162
+ # docstring describes, not a second envelope-facing resolution.
163
+ return resolve_sdk_root_ladder(None, Path(workspace_root)).path
164
+
165
+
166
+ def read_sdk_som_and_soc(
167
+ metadata_root: str, sku: str, *, warnings: list[str] | None = None,
168
+ skipped: list[str] | None = None, explain_unsupported: bool = False,
169
+ ) -> tuple[str, str | None, list[dict], float | None, list[tuple[str, float | None]]] | None:
170
+ """`(silicon, silicon_variant, variants, soc_flash_mb, soc_cores)` for
171
+ `sku`'s SoM preset + SoC JSON under `<sdk>/metadata` -- port of the ONE
172
+ metadata-layout walk `util::read_sdk_som_and_soc` performs on the Rust
173
+ side: `metadata/e1m_modules/<sku>.yaml` -> `silicon.split(':')` ->
174
+ `metadata/socs/<vendor>/<family>/<part>.json`. `None` when the SoM preset
175
+ itself cannot be read (missing, unreadable, wrong `schema_version`).
176
+
177
+ `tan size` and `tan debug-config` both resolve a SoC variant off this same
178
+ walk and must call ONE reader, not two that could disagree -- a schema
179
+ with no reader, then two readers that could quietly diverge, is the exact
180
+ drift alp-sdk#1026 itself is about. Leaf parsing stays
181
+ `size_cmd._read_som_preset`/`_read_soc`; this only sequences them. A
182
+ caller wanting a FINER "file missing" vs "file unreadable" split (`tan
183
+ size`'s two different budget notes) still does its own existence check on
184
+ `<metadata_root>/e1m_modules/<sku>.yaml` before calling this.
185
+
186
+ Imported locally, not at module level: `size_cmd` already imports this
187
+ module for `resolve_project_context`/`resolve_metadata_sdk_root`, so a
188
+ top-level import the other way would cycle.
189
+
190
+ *warnings* (tan-cli#964), when given, is threaded to BOTH leaf readers --
191
+ a caller that does not pass it is entirely unaffected: `_read_som_preset`/
192
+ `_read_soc` only validate when the caller supplies a collector, so this
193
+ parameter defaults to a no-op. `tan debug-config` now passes one too
194
+ (tan-cli#964 review, major 5) -- this docstring used to name it as the
195
+ one caller that did not; it was.
196
+
197
+ *skipped* (tan-cli#964 review, major 6): same threading, same default,
198
+ for the "skip-but-disclose" half -- `missing_schema_note`'s notes rather
199
+ than `validate_document`'s violations.
200
+ """
201
+ from tan.commands.size_cmd import _read_soc, _read_som_preset # noqa: PLC0415
202
+
203
+ preset_path = os.path.join(metadata_root, "e1m_modules", f"{sku}.yaml")
204
+ preset = _read_som_preset(
205
+ preset_path, metadata_root=metadata_root, warnings=warnings, skipped=skipped
206
+ )
207
+ if preset is None:
208
+ # Say WHY (tan-cli#1354 bench round 7: a stale alp-sdk checkout whose presets are
209
+ # schema_version 1 made every caller report a bare "unreadable"): a caller that
210
+ # passed a `skipped` collector AND `explain_unsupported` gets the real cause as a note
211
+ # (`tan size` keeps its own, richer `size.som-schema-version-skipped` instead).
212
+ if skipped is not None and explain_unsupported:
213
+ note = _unsupported_preset_note(preset_path)
214
+ if note is not None:
215
+ skipped.append(note)
216
+ return None
217
+ silicon, silicon_variant = preset
218
+ if not silicon:
219
+ return silicon, silicon_variant, [], None, []
220
+ parts = silicon.split(":")
221
+ if len(parts) != 3:
222
+ return silicon, silicon_variant, [], None, []
223
+ soc_path = os.path.join(metadata_root, "socs", parts[0], parts[1], f"{parts[2]}.json")
224
+ variants, soc_flash_mb, soc_cores = _read_soc(
225
+ soc_path, metadata_root=metadata_root, warnings=warnings, skipped=skipped
226
+ )
227
+ return silicon, silicon_variant, variants, soc_flash_mb, soc_cores
228
+
229
+
230
+ def _unsupported_preset_note(path: str) -> str | None:
231
+ """`<path>: not read -- unsupported SoM preset schema_version 1 (tan needs 2) --
232
+ update alp-sdk` when the preset parses but declares a version tan does not read;
233
+ `None` for any other reason it could not be used (missing, unparseable)."""
234
+ from tan.commands.size_cmd import _read_text # noqa: PLC0415
235
+ from tan.core.som_schema_version import SOM_SCHEMA_VERSION, is_supported_som_schema_version
236
+ from tan.core.system_manifest import load_yaml_document # noqa: PLC0415
237
+
238
+ text = _read_text(path)
239
+ if text is None:
240
+ return None
241
+ try:
242
+ root = load_yaml_document(text)
243
+ except Exception: # noqa: BLE001 -- unparseable is `unreadable`, not this
244
+ return None
245
+ if not isinstance(root, dict):
246
+ return None
247
+ version = root.get("schema_version")
248
+ if is_supported_som_schema_version(version):
249
+ return None
250
+ return (
251
+ f"{path.replace(chr(92), '/')}: not read -- unsupported SoM preset schema_version "
252
+ f"{version} (tan needs {SOM_SCHEMA_VERSION}) -- update alp-sdk"
253
+ )
254
+
255
+
256
+ def resolve_app_base(app_path: str | None, workspace_root: str) -> str:
257
+ """The directory whose `build/` is the default build root.
258
+
259
+ An explicit positional wins and is anchored on the CURRENT DIRECTORY, not on
260
+ `--project` -- it is typed at a shell prompt, so a relative value means what
261
+ the shell means by it (and `tan size app` therefore reports `project.root` as
262
+ the cwd while reading `app/build`, which is what the oracle does). `size`
263
+ spells "no positional" as the default `.`; `image` spells it as an absent
264
+ argument; both then fall back to the resolved workspace.
265
+
266
+ A STRING, joined with `os.path.join` -- see the note in
267
+ `tan.core.system_manifest`: the workspace root is posix and `PathBuf::join`
268
+ does not re-render it, so `pathlib` here would change every path this command
269
+ reports.
270
+ """
271
+ if app_path is not None and app_path != ".":
272
+ return os.path.join(os.getcwd(), app_path)
273
+ return workspace_root
274
+
275
+
276
+ def resolve_build_root(build_root_arg: str | None, app_base: str) -> str:
277
+ """`--build-root` absolute as-is, relative against the CURRENT DIRECTORY;
278
+ default `<app_base>/build`."""
279
+ if build_root_arg is None:
280
+ return os.path.join(app_base, "build")
281
+ if os.path.isabs(build_root_arg):
282
+ return build_root_arg
283
+ return os.path.join(os.getcwd(), build_root_arg)
284
+
285
+
286
+ class ManifestUnavailable(Exception):
287
+ """`build/system-manifest.yaml` could not be READ -- absent, unreadable, a
288
+ directory, or not UTF-8. All four are `*.manifest-unavailable` in the oracle,
289
+ including the decode failure (`read_to_string` returns an io error for
290
+ invalid UTF-8, so it never reaches the parser)."""
291
+
292
+ def __init__(self, path: str, detail: str) -> None:
293
+ super().__init__(detail)
294
+ self.path = path
295
+ self.detail = detail
296
+
297
+
298
+ class ManifestInvalid(Exception):
299
+ """It was read but is not a v1 system-manifest."""
300
+
301
+ def __init__(self, path: str, detail: str) -> None:
302
+ super().__init__(detail)
303
+ self.path = path
304
+ self.detail = detail
305
+
306
+
307
+ def load_manifest(build_root: str) -> tuple[str, SystemManifest]:
308
+ """Read + parse + version-guard the manifest, returning its raw text too --
309
+ `tan image` needs the bytes for the additive-field passthrough that a typed
310
+ projection would discard.
311
+
312
+ Every failure is one of the two exceptions above, so neither command can
313
+ ever let an OSError, a decode error or a YAML error escape as a traceback.
314
+ """
315
+ path = find_manifest(build_root)
316
+ try:
317
+ # `newline=""`: no universal-newline translation, matching Rust's
318
+ # `read_to_string`. Explicit encoding, never the locale default -- a
319
+ # cp1252 host would raise UnicodeDecodeError on a manifest with any
320
+ # non-ASCII byte (I-27's read-side rule).
321
+ with open(path, encoding="utf-8", newline="") as handle:
322
+ text = handle.read()
323
+ except (OSError, UnicodeDecodeError, ValueError) as err:
324
+ if isinstance(err, FileNotFoundError):
325
+ # Neither spelling exists: name both paths tried (tan-cli#1405).
326
+ path = " or ".join(manifest_candidates(build_root))
327
+ raise ManifestUnavailable(path, str(err)) from err
328
+ try:
329
+ return text, parse_system_manifest(text)
330
+ except SystemManifestError as err:
331
+ raise ManifestInvalid(path, err.message) from err