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,637 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan run` -- build the project, then run it: execute the produced
3
+ `native_sim` binary for a host target, or flash a hardware target.
4
+
5
+ Port of `crates/tan-cli/src/commands/run/mod.rs`. A THIN ORCHESTRATOR: it
6
+ reuses `tan build`'s engine (`build_cmd._build`, the same engine
7
+ `build_cmd.build` calls) and, on a hardware target with `--flash`,
8
+ `tan flash`'s engine (`flash_cmd._run`) -- never re-deriving either. The pure
9
+ decision -- execute vs flash vs short-circuit -- lives in `tan.core.run`; this
10
+ file resolves paths, probes the filesystem for a runnable `native_sim`
11
+ binary, and spawns it.
12
+
13
+ **`run` is a DISTINCT command, not an alias for `build` or `flash`.**
14
+ `tan build --help` and `tan flash --help` list a disjoint option set from
15
+ `tan run --help` (verified against the released oracle binary: `run` has
16
+ `--flash`/`--core` and neither `--plan`/`--materialise`/`--native` from
17
+ `build` nor `--dry-run`/`--helper`/`--skip-missing-tools` from `flash`), and
18
+ `crates/tan-cli/src/cli.rs` declares `Run(RunArgs)` as its own `Commands`
19
+ variant dispatched to its own module -- never routed through `Command::Build`
20
+ or `Command::Flash`. What IS shared is the *engine*: `run` composes the same
21
+ two engines `build` and `flash` already own, exactly once each, rather than
22
+ re-implementing a third copy of either.
23
+
24
+ **The `native_sim_target`/`manifest_written` signal is now real.**
25
+ `tan.commands.build.execute.execute_slices` writes the post-build
26
+ `system-manifest.yaml` as a side effect of every dispatch (`tan build`'s own
27
+ CLI invocation gets it too, not just `run`'s), and records the two signals
28
+ `decide_run_action` needs via `execute.last_manifest_write()` -- a
29
+ same-process recorder, not a widened return value, because `execute_slices`
30
+ is reached only through `tan.commands.build_cmd._dispatch` / `_build`, both
31
+ out of THIS module's ownership for this change (a disjoint parallel-unit
32
+ split, not an architecture choice); see `execute.py`'s own module docstring
33
+ for the full reasoning and why reading the recorder here is still safe
34
+ against the R1 staleness defect (three attempts in the Rust oracle) that the
35
+ module doc below and `test_execute_native_arm_refuses_stale_exe_when_
36
+ manifest_write_unconfirmed` both pin. `_run` resets the recorder immediately
37
+ before calling the build engine (`execute.reset_last_manifest_write()`) so a
38
+ build that never reaches dispatch -- an early coded refusal, or a
39
+ monkeypatched `_build` stub in a test -- reads the honest default
40
+ `(False, None)` rather than a previous invocation's leftover.
41
+
42
+ **`--flash`/`--core` reach the flash engine once the build confirms a
43
+ hardware target.** `decide_run_action`'s own decision table still makes a
44
+ silent `BUILD_ONLY` no-op unreachable under `flash_requested=True` regardless
45
+ of `native_sim_target`/`manifest_written` (`tan.core.run`'s own tests pin
46
+ this) -- a hardware target without a confirmed manifest write still refuses
47
+ via `MANIFEST_STALE`, never guesses. `--core` is forwarded verbatim to the
48
+ native flash path's `--core` once the `FLASH` arm is actually reached
49
+ (`_flash_args_for` is unit-tested in isolation, the same way the Rust oracle
50
+ tests `flash_args_for`).
51
+ """
52
+ from __future__ import annotations
53
+
54
+ import os
55
+ import subprocess
56
+ import sys
57
+ from pathlib import Path
58
+ from typing import Any
59
+
60
+ import typer
61
+
62
+ from tan.commands import flash_cmd
63
+ from tan.commands.build import execute
64
+ from tan.commands.build_cmd import BuildError, _build
65
+ from tan.core.flash_plan import resolve_artefact_path
66
+ from tan.core.global_flags import accept_global_flags
67
+ from tan.core.plan_exec import normalize_path
68
+ from tan.core.run import RunAction, decide_run_action, native_sim_exe_beside, native_sim_slice
69
+ from tan.core.sdk_discovery import (
70
+ _abs_posix,
71
+ global_default_foreign_project_issue,
72
+ project_pin_issue,
73
+ resolve_sdk_root_ladder,
74
+ )
75
+ from tan.core.shapes import is_sdk_root, rejected_sdk_root_message
76
+ from tan.core.subprocess_env import spawn_env
77
+ from tan.core.system_manifest import SystemManifestError, parse_system_manifest
78
+ from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
79
+ from tan.exit_codes import ExitCode
80
+ from tan.output_format import FORMAT_HELP, OutputFormat
81
+
82
+ #: `data.exec` skip reason JSON mode reports when a `native_sim` binary was
83
+ #: found but not executed (never spawned under `--format json`: a process
84
+ #: that never closes stdout would hang the one-envelope-per-invocation
85
+ #: contract). Verbatim from the Rust oracle's `NATIVE_SIM_JSON_SKIP_REASON`.
86
+ _NATIVE_SIM_JSON_SKIP_REASON = (
87
+ "native_sim exec skipped in --format json (run in text mode to execute)"
88
+ )
89
+
90
+ _MANIFEST_STALE_MESSAGE = (
91
+ "run: --flash refused — this build's own outcome does not confirm it "
92
+ "is safe to flash (either the target could not be determined, or this "
93
+ "run's system-manifest.yaml write failed). Check the build output "
94
+ "above, then retry `tan run --flash`."
95
+ )
96
+
97
+ _NATIVE_SIM_UNAVAILABLE_MESSAGE = (
98
+ "run: native_sim target, but this build produced no runnable "
99
+ "zephyr.exe (slice skipped/failed, or artefact missing) — see build "
100
+ "output above."
101
+ )
102
+
103
+
104
+ def _find_native_sim_exe(base: str, sdk_root: str | None) -> str | None:
105
+ """Locate the produced `native_sim` executable from the post-build
106
+ `system-manifest.yaml` under `<base>/build`. `None` when there's no
107
+ manifest, no native_sim slice, the slice didn't build `ok` THIS run, or
108
+ the binary isn't on disk -- so an unbuilt/absent binary and a
109
+ skipped/failed slice (which would otherwise resolve to a STALE
110
+ `zephyr.exe` left from a previous run) both fall through to a refusal
111
+ instead of silently executing old firmware. Mirrors
112
+ `run/mod.rs::find_native_sim_exe`, including its `<base>/build` anchor
113
+ (`base.join("build")`, run/mod.rs:232) -- `base` is the PROJECT root
114
+ (`_run`'s `build_root` argument), never the `build/` dir itself, so this
115
+ function does the same `os.path.join(base, "build")` the oracle does
116
+ rather than assuming its caller already appended it."""
117
+ build_root = os.path.join(base, "build")
118
+ try:
119
+ text = Path(build_root, "system-manifest.yaml").read_text(
120
+ encoding="utf-8", errors="replace"
121
+ )
122
+ except OSError:
123
+ return None
124
+ try:
125
+ manifest = parse_system_manifest(text)
126
+ except SystemManifestError:
127
+ return None
128
+ slice_ = native_sim_slice(manifest)
129
+ if slice_ is None or slice_.get("status") != "ok":
130
+ return None
131
+ artefact = slice_.get("output_artefact") or ""
132
+ if not artefact:
133
+ return None
134
+ elf_path = resolve_artefact_path(artefact, build_root, sdk_root, os.path.isfile)
135
+ exe_path = native_sim_exe_beside(elf_path)
136
+ return exe_path if os.path.isfile(exe_path) else None
137
+
138
+
139
+ def _exec_native_sim(exe: str) -> tuple[bool, int | None]:
140
+ """Run the `native_sim` binary with inherited stdio (it streams live), and
141
+ report `(ok, returncode)`. Only called in text mode -- see the module
142
+ doc."""
143
+ print(f"run: executing {exe}", file=sys.stderr)
144
+ try:
145
+ proc = subprocess.run([exe], env=spawn_env())
146
+ return proc.returncode == 0, proc.returncode
147
+ except OSError as err:
148
+ print(f"run: failed to launch {exe}: {err}", file=sys.stderr)
149
+ return False, None
150
+
151
+
152
+ def _with_exec(
153
+ build_data: dict[str, Any] | None, exec_payload: dict[str, Any]
154
+ ) -> dict[str, Any] | None:
155
+ """Nest `exec_payload` under `data.exec`, mirroring the oracle's own guard
156
+ (`and_then(Value::as_object_mut)`, run/mod.rs:337-341 and :415): only when
157
+ `build_data` is already an object. `build_data: None` -- unreachable today
158
+ since a successful `_build` always returns a dict, but a shape the
159
+ envelope contract allows -- passes through unchanged rather than
160
+ synthesising `{"exec": ...}` the oracle would never emit for `data:
161
+ null`."""
162
+ if not isinstance(build_data, dict):
163
+ return build_data
164
+ return {**build_data, "exec": exec_payload}
165
+
166
+
167
+ def _flash_args_for(build_root: str, core: str | None) -> dict[str, Any]:
168
+ """The `flash_cmd._run` kwargs for `run --flash`, anchored on the resolved
169
+ project `build_root` (`_run`'s own project-root argument, matching the
170
+ Rust oracle's `base`) -- never `"."`. `flash_cmd._run` derives
171
+ `<app_path>/build` itself when `build_root_arg` is `None`
172
+ (`_abs_join(app_dir, "build")`, flash_cmd.py:752), exactly the way
173
+ `flash::run` derives `build_root` from `FlashArgs { build_root: None,
174
+ .. }` -- so passing the project root as `app_path` with
175
+ `build_root_arg: None` makes `run --flash` probe `<build_root>/build/
176
+ system-manifest.yaml`, the SAME file this run's own `tan build` step just
177
+ wrote, rather than `<build_root>/system-manifest.yaml` (nothing writes
178
+ that) or a bare `"."` resolved against a different `cwd` under
179
+ `--project <dir>`. Mirrors the Rust oracle's `flash_args_for`
180
+ (run/mod.rs:168-177)."""
181
+ return {"app_path": build_root, "build_root_arg": None, "core": core}
182
+
183
+
184
+ def _execute_native_arm(
185
+ build_root: str,
186
+ sdk_root: str | None,
187
+ manifest_written: bool,
188
+ build_exit: ExitCode,
189
+ build_data: dict[str, Any] | None,
190
+ build_issues: list[Issue],
191
+ json_mode: bool,
192
+ ) -> tuple[ExitCode, dict[str, Any] | None, list[Issue], list[str]]:
193
+ """The `RunAction.EXECUTE_NATIVE` arm: run this build's `native_sim`
194
+ binary, or report that there isn't a trustworthy one. Mirrors the Rust
195
+ oracle's `execute_native_arm` (run/mod.rs:140-150), including its
196
+ `manifest_written` gate: `_find_native_sim_exe` trusts an on-disk
197
+ `zephyr.exe` from the manifest's `status: ok`, but that manifest is only
198
+ THIS run's when the post-build write actually succeeded. An unconfirmed
199
+ write (a Windows sharing violation, a failed emit) means a PREVIOUS run's
200
+ ok-status manifest and its `zephyr.exe` may still be on disk; executing
201
+ that would report success for an edit this run never compiled -- so the
202
+ probe is never even attempted when the write is unconfirmed."""
203
+ exe = _find_native_sim_exe(build_root, sdk_root) if manifest_written else None
204
+ if exe is None:
205
+ issues = [
206
+ *build_issues,
207
+ Issue("run.native-sim-unavailable", "error", _NATIVE_SIM_UNAVAILABLE_MESSAGE),
208
+ ]
209
+ text = _build_text_lines(build_data, build_issues) + [_NATIVE_SIM_UNAVAILABLE_MESSAGE]
210
+ return ExitCode.RUNTIME_FAILURE, build_data, issues, text
211
+ if json_mode:
212
+ # Never spawned under `--format json` -- see the module doc.
213
+ data = _with_exec(
214
+ build_data,
215
+ {
216
+ "executed": False,
217
+ "reason": _NATIVE_SIM_JSON_SKIP_REASON,
218
+ "binary": exe,
219
+ },
220
+ )
221
+ return build_exit, data, build_issues, []
222
+ ok, rc = _exec_native_sim(exe)
223
+ exit_code = ExitCode.SUCCESS if ok else ExitCode.RUNTIME_FAILURE
224
+ data = _with_exec(build_data, {"binary": exe, "ok": ok, "rc": rc})
225
+ text = _build_text_lines(build_data, build_issues)
226
+ issues = list(build_issues)
227
+ if not ok:
228
+ message = (
229
+ f"{exe} exited with code {rc}"
230
+ if rc is not None
231
+ else f"{exe} did not run to completion"
232
+ )
233
+ issues.append(Issue("run.exec-failed", "error", message))
234
+ text.append(f"run: {message}")
235
+ return exit_code, data, issues, text
236
+
237
+
238
+ def _build_text_lines(data: dict[str, Any] | None, issues: list[Issue]) -> list[str]:
239
+ """The same text-mode recap `build_cmd.build` prints, reused here so a
240
+ `tan run` that stops at the build step reads identically to `tan build`
241
+ would have."""
242
+ lines = [f"{issue.severity}: {issue.message}" for issue in issues]
243
+ for result in (data or {}).get("slices", []):
244
+ reason = f" — {result['reason']}" if "reason" in result else ""
245
+ lines.append(f"{result['status']}: {result['coreId']} [{result['backend']}]{reason}")
246
+ return lines
247
+
248
+
249
+ def _run(
250
+ *,
251
+ build_root: str,
252
+ sdk_root: str | None,
253
+ sdk_root_for_stamp: str | None,
254
+ board_yaml: str | None,
255
+ flash: bool,
256
+ core: str | None,
257
+ json_mode: bool,
258
+ # Defaulted, not required: several existing unit tests call `_run`
259
+ # directly with the pre-tan-cli#809 kwarg set, exercising paths this flag
260
+ # never reaches (`BUILD_ONLY`/`MANIFEST_STALE`/`EXECUTE_NATIVE`) or
261
+ # stubbing `flash_cmd._run` themselves. `False` preserves the exact
262
+ # pre-#809 behaviour for every one of them.
263
+ confirm: bool = False,
264
+ # tan-cli#1252: defaulted for the same reason `confirm` above is -- the
265
+ # tests that call `_run` directly predate it and reach paths it never
266
+ # touches.
267
+ atoc_unqueryable: bool = False,
268
+ # tan-cli#1267: defaulted for the same reason.
269
+ replace_atoc: bool = False,
270
+ ) -> tuple[ExitCode, dict[str, Any] | None, list[Issue], list[str]]:
271
+ """Everything between the resolved paths and the envelope. Returns
272
+ `(exit_code, data, issues, text_lines)`."""
273
+ # Reset BEFORE calling the build engine, not after: see the module doc
274
+ # and `execute.reset_last_manifest_write`'s own docstring for why an
275
+ # unreset recorder would leak a PREVIOUS invocation's signal into a build
276
+ # that never reaches dispatch (an early `BuildError`, or a monkeypatched
277
+ # `_build` stub in a test).
278
+ execute.reset_last_manifest_write()
279
+ try:
280
+ build_exit, build_data, build_issues = _build(
281
+ plan_from=None,
282
+ build_root=build_root,
283
+ sdk_root=sdk_root,
284
+ sdk_root_for_stamp=sdk_root_for_stamp,
285
+ board_yaml=board_yaml,
286
+ )
287
+ except BuildError as err:
288
+ # A CODED refusal (no SDK, no board.yaml, an unparsable plan, ...),
289
+ # not a tan bug -- `build_cmd.build` catches this the same way, and
290
+ # `run` must retag it identically: same issue code, same exit code,
291
+ # only `command` differs.
292
+ build_exit, build_data, build_issues = (
293
+ err.exit_code,
294
+ None,
295
+ [Issue(err.code, "error", err.message)],
296
+ )
297
+ build_ok = build_exit == ExitCode.SUCCESS
298
+
299
+ # The real signal from THIS build's own dispatch (see the module doc) --
300
+ # never a re-read of `system-manifest.yaml` off disk afterward.
301
+ manifest_written, native_sim_target = execute.last_manifest_write()
302
+
303
+ action = decide_run_action(build_ok, native_sim_target, flash, manifest_written)
304
+ # tan-cli#1267 review: the two ATOC flags only mean something to a write.
305
+ ignored = _ignored_flash_flags(action, replace_atoc, atoc_unqueryable)
306
+
307
+ if action in (RunAction.BUILD_FAILED, RunAction.BUILD_ONLY):
308
+ text = _build_text_lines(build_data, build_issues)
309
+ if action is RunAction.BUILD_ONLY and not json_mode:
310
+ text.append("run: built; pass --flash to program the board.")
311
+ return _with_ignored(ignored, (build_exit, build_data, build_issues, text))
312
+
313
+ if action is RunAction.MANIFEST_STALE:
314
+ issues = [*build_issues, Issue("run.manifest-stale", "error", _MANIFEST_STALE_MESSAGE)]
315
+ text = _build_text_lines(build_data, build_issues) + [_MANIFEST_STALE_MESSAGE]
316
+ return _with_ignored(ignored, (ExitCode.RUNTIME_FAILURE, build_data, issues, text))
317
+
318
+ if action is RunAction.EXECUTE_NATIVE:
319
+ return _with_ignored(ignored, _execute_native_arm(
320
+ build_root, sdk_root, manifest_written, build_exit, build_data, build_issues, json_mode
321
+ ))
322
+
323
+ # RunAction.FLASH: hardware target, `--flash`, this run's manifest write
324
+ # confirmed -- reuse the native flash path, targeting the SAME project
325
+ # `build_root` this run just built (not a bare "." under a different cwd).
326
+ flash_exit, flash_data, flash_issues, flash_text, _flash_sdk = flash_cmd._run(
327
+ **_flash_args_for(build_root, core),
328
+ sdk_root_arg=sdk_root,
329
+ board_yaml=board_yaml,
330
+ helper=None,
331
+ dry_run=False,
332
+ skip_missing_tools=False,
333
+ capture=json_mode,
334
+ cwd=build_root,
335
+ # tan-cli#809: `run` had no way to arm the flash confirm gate, so a
336
+ # hardware target (e.g. E1M-AEN801's Flow D) always landed every
337
+ # slice as `planned` and exited 1 with `flash.nothing-flashed` --
338
+ # whose own remedy names `--confirm`, a flag `run` rejected. The gate
339
+ # itself stays deliberate (see flash_plan.py); this only gives `run`
340
+ # the same opt-in `tan flash --confirm` already has.
341
+ confirm_flag=confirm,
342
+ # tan-cli#1252: forwarded for a sharper reason than `--confirm` above.
343
+ # `dry_run=False` is HARDCODED on this call, so `run --flash --confirm`
344
+ # on an AEN Flow D slice can never take a preview path -- it goes
345
+ # straight to the whole-ATOC guard. Without this line that command
346
+ # would hit a refusal naming a flag `run` does not accept.
347
+ atoc_unqueryable=atoc_unqueryable,
348
+ # tan-cli#1267: same reasoning -- with `dry_run=False` hardcoded, a
349
+ # Flow A slice whose runner refuses would name a flag `run` lacked.
350
+ replace_atoc=replace_atoc,
351
+ )
352
+ # tan-cli#1482: carry the build step's own issues + recap onto this arm as
353
+ # every other arm does -- a partial build (a skipped slice, a manifest-write
354
+ # warning) still counts as build_ok, and dropping its warnings here left a
355
+ # flash result with no sign that a core was not rebuilt.
356
+ flash_issues = [*build_issues, *flash_issues]
357
+ flash_text = _build_text_lines(build_data, build_issues) + list(flash_text)
358
+ return flash_exit, flash_data, flash_issues, flash_text
359
+
360
+
361
+ def _ignored_flash_flags(
362
+ action: RunAction, replace_atoc: bool, atoc_unqueryable: bool
363
+ ) -> str | None:
364
+ """The warning for `--replace-atoc`/`--atoc-unqueryable` on a `run` that
365
+ will not flash, or `None`. Both only act on a write; dropping them with
366
+ no word would let an operator believe an override or acknowledgement
367
+ took effect (tan-cli#1267 review)."""
368
+ flags = [
369
+ flag
370
+ for flag, given in (("--replace-atoc", replace_atoc), ("--atoc-unqueryable", atoc_unqueryable))
371
+ if given
372
+ ]
373
+ if action is RunAction.FLASH or not flags:
374
+ return None
375
+ return (
376
+ f"run: {' and '.join(flags)} had no effect -- this run did not flash "
377
+ "anything (they only act on a write, which needs --flash on a hardware "
378
+ "target whose build succeeded)."
379
+ )
380
+
381
+
382
+ def _with_ignored(ignored: str | None, result):
383
+ """`result` (`_run`'s 4-tuple) with the ignored-flag warning appended
384
+ to its issues and text, when there is one."""
385
+ if ignored is None:
386
+ return result
387
+ exit_code, data, issues, text = result
388
+ return (
389
+ exit_code,
390
+ data,
391
+ [*issues, Issue("run.flash-flags-ignored", "warning", ignored)],
392
+ [*text, ignored],
393
+ )
394
+
395
+
396
+ def run(
397
+ flash: bool = typer.Option(
398
+ False,
399
+ "--flash",
400
+ help="Program the board after building (hardware targets only). Required "
401
+ "opt-in: without it, `run` on a hardware project builds and reports but "
402
+ "never flashes. Arming the write also needs --confirm (or "
403
+ "ALP_FLASH_FORCE=1, or flash_args.confirm: true in the manifest); "
404
+ "without it every slice comes back `planned` and the run exits "
405
+ "non-zero. An Alif Flow D slice (alif_mram_jlink) has a SECOND gate and "
406
+ "can instead come back `failed` once armed, until --atoc-unqueryable "
407
+ "acknowledges that the write replaces the entire ATOC (tan-cli#1252). "
408
+ "Ignored for a native_sim/host target, which always runs "
409
+ "the produced binary and never flashes.",
410
+ ),
411
+ core: str = typer.Option(
412
+ None,
413
+ "--core",
414
+ metavar="CORE_ID",
415
+ help="With --flash, flash only the slice with this core_id (forwarded "
416
+ "verbatim to the native flash path's --core).",
417
+ ),
418
+ confirm: bool = typer.Option(
419
+ False,
420
+ "--confirm",
421
+ help="With --flash, arm the confirm gate and actually write the device. "
422
+ "Without it (and without ALP_FLASH_FORCE=1 or flash_args.confirm: true "
423
+ "in the manifest) a hardware target is only previewed -- every slice "
424
+ "comes back `planned`, nothing reaches the device, and the run exits "
425
+ "non-zero (tan-cli#809). On an Alif Flow D slice (alif_mram_jlink) this "
426
+ "arms the write but does not acknowledge that it replaces the entire "
427
+ "ATOC: such a slice additionally needs --atoc-unqueryable and is refused "
428
+ "(`failed`, not `planned`) without it (tan-cli#1252). Ignored without "
429
+ "--flash.",
430
+ ),
431
+ atoc_unqueryable: bool = typer.Option(
432
+ False,
433
+ "--atoc-unqueryable",
434
+ # tan-cli#1252 review: `run` deliberately does NOT offer the manifest
435
+ # spelling as an alternative here, unlike `tan flash`'s own help. Every
436
+ # `run --flash` REGENERATES build/system-manifest.yaml before flashing
437
+ # (RunAction.FLASH requires THIS run's own manifest write), and
438
+ # `planner/orchestrator._slice_flash_recipe` composes `flash_args` from
439
+ # a fresh dict carrying only the keys it knows -- so an operator-added
440
+ # `atoc_unqueryable` is destroyed by the same command that would then
441
+ # demand it. On `run` the flag is the only spelling that survives.
442
+ help="With --flash on an Alif Flow D slice (alif_mram_jlink), acknowledge "
443
+ "that the write REPLACES the entire ATOC: Flow D cannot enumerate what is "
444
+ "resident, so any boot entry the new ATOC does not name is silently delisted "
445
+ "while the SES still reports \"[SES] ATOC ok\". Without it such a slice "
446
+ "refuses. Use the flag here rather than flash_args.atoc_unqueryable: true -- "
447
+ "that manifest spelling works for `tan flash`, which reads the manifest as "
448
+ "it stands, but every `run --flash` regenerates build/system-manifest.yaml "
449
+ "first and a hand-added key does not survive it. This acknowledges the "
450
+ "replacement only -- arming the write itself still needs --confirm "
451
+ "(tan-cli#1252).",
452
+ ),
453
+ replace_atoc: bool = typer.Option(
454
+ False,
455
+ "--replace-atoc",
456
+ help="With --flash on a zephyr_west_flash slice whose west runner is "
457
+ "alif_flash (Flow A, over the SE-UART), override that runner's pre-burn "
458
+ "ATOC guard, which otherwise refuses a write that would silently delist a "
459
+ "resident entry it does not name, or whose read of the resident ATOC could "
460
+ "not be verified (tan-cli#1267). Reaches at most one write per run (narrow "
461
+ "with --core). Flag only: there is no manifest spelling. "
462
+ "Not the same as --atoc-unqueryable (Flow D), and never accepted in its "
463
+ "place; on any other slice tan warns instead of passing it.",
464
+ ),
465
+ project: str = typer.Option(
466
+ None, "--project", metavar="PATH", help="Project root (defaults to '.')."
467
+ ),
468
+ board_yaml: str = typer.Option(
469
+ None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
470
+ ),
471
+ sdk_root: str = typer.Option(
472
+ None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
473
+ ),
474
+ output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
475
+ ) -> None:
476
+ """Build the project, then run it: execute the produced native_sim binary
477
+ for a host target, or (with --flash) program a hardware target."""
478
+ json_mode = output_format == "json"
479
+
480
+ # tan-cli#1482: the resolution prologue lives INSIDE the catch-all (as in
481
+ # `build_cmd.build`, tan-cli#488 defect 8) so a deleted cwd or a resolver
482
+ # raise becomes a `run.internal-failure` envelope, not a raw traceback.
483
+ project_obj = Project(root=None, board_yaml=None)
484
+ sdk: SdkInfo | None = None
485
+ sdk_tier = "none"
486
+ sdk_broken_pin: str | None = None
487
+ sdk_foreign_default: str | None = None
488
+ try:
489
+ # Same resolution `build_cmd.build` performs (`run` builds via the same
490
+ # engine, so it must anchor on the same project) -- see that function for
491
+ # the reasoning behind each step.
492
+ cwd = Path.cwd()
493
+ workspace_root = cwd if project is None else Path(os.path.join(str(cwd), project))
494
+ if board_yaml is not None and not os.path.isabs(board_yaml):
495
+ board_yaml = os.path.join(str(workspace_root), board_yaml)
496
+ if board_yaml is None and (workspace_root / "board.yaml").is_file():
497
+ board_yaml = str(workspace_root / "board.yaml")
498
+ build_root = str(Path(board_yaml).parent) if board_yaml else str(workspace_root)
499
+ build_root = _abs_posix(build_root)
500
+ if board_yaml is not None:
501
+ board_yaml = _abs_posix(board_yaml)
502
+
503
+ # Same ladder `build_cmd.build` resolves -- `--sdk-root` > `.alp/sdk-path`
504
+ # project pin > the machine-global default (`~/.alp/sdk-default`) > the
505
+ # positional walk (`resolve_sdk_root_ladder`); `run` builds via the same
506
+ # engine, so it must agree with `build` on which checkout that is. No
507
+ # `ALP_SDK_ROOT` tier (tried and reverted -- see `resolve_sdk_root_ladder`'s
508
+ # own docstring).
509
+ sdk_resolution = resolve_sdk_root_ladder(sdk_root, workspace_root)
510
+ resolved_sdk_root = sdk_resolution.path
511
+ sdk_tier = sdk_resolution.tier
512
+ sdk_broken_pin = sdk_resolution.broken_project_pin
513
+ sdk_foreign_default = sdk_resolution.foreign_global_default_for
514
+ # tan-cli#257/#258 -- the exact guard `build_cmd.build` applies, for the
515
+ # exact same reason: this line was a VERBATIM COPY of the one that carried
516
+ # the defect, so fixing only `build` would have left its twin here.
517
+ # `resolve_sdk_root_ladder` returns an explicit `--sdk-root` UNVALIDATED
518
+ # (I-31 terminal-for-REPORTING, matching the oracle's
519
+ # `resolve_sdk_tiered`), which is correct for a caller that only reports
520
+ # the tier and wrong for one that ACTS on the path: a bogus `--sdk-root`
521
+ # sailed through as `sdk.sourceTier: "sdkRootFlag"` and was then refused
522
+ # for the NEXT missing thing, telling the customer their project is broken
523
+ # when the flag they had just typed is what was wrong.
524
+ #
525
+ # Guarded HERE rather than inside the shared ladder because every other
526
+ # caller depends on it staying unvalidated -- the same placement
527
+ # `build_cmd`, `clean_cmd.sdk_root_resolves` and `flash_cmd._resolve_sdk`
528
+ # already chose. An unresolvable explicit root is treated as no root at
529
+ # all, so the refusal downstream is the honest "no alp-sdk checkout found"
530
+ # and no `sdk` key is emitted, matching the oracle.
531
+ if sdk_tier == "sdkRootFlag" and not is_sdk_root(resolved_sdk_root):
532
+ resolved_sdk_root = None
533
+ # tan-cli#1463: an EXPLICIT `--sdk-root` that did not resolve is a coded
534
+ # refusal, not a silent "no root". Without it the typo fell through to the
535
+ # build engine and surfaced as an unrelated downstream failure. No
536
+ # `--sdk-root` + no checkout found keeps the engine's own behaviour (the
537
+ # engine owns that refusal), so only the explicit-flag case is new here.
538
+ sdk_refusal = None
539
+ if sdk_root and sdk_root.strip() and resolved_sdk_root is None:
540
+ refusal_msg = rejected_sdk_root_message(sdk_root, "Nothing was built or run.")
541
+ sdk_refusal = (
542
+ ExitCode.RUNTIME_FAILURE,
543
+ None,
544
+ [Issue("run.sdk-root-unresolved", "error", refusal_msg)],
545
+ [f"run: {refusal_msg}"],
546
+ )
547
+ sdk_root = str(resolved_sdk_root) if resolved_sdk_root is not None else None
548
+ sdk = SdkInfo(sdk_root, sdk_tier) if sdk_root is not None else None
549
+ # Same normalized, workspace-root-anchored stamp identity `build_cmd.build`
550
+ # computes (tan-cli#163) -- `_build` now requires it (the sdk-switch-
551
+ # pristine guard's stamp comparison, threaded through from `execute_slices`
552
+ # rather than self-discovered), and `run` reuses the same engine so it must
553
+ # resolve it the same way, not just `sdk_root` itself.
554
+ sdk_root_for_stamp = (
555
+ str(normalize_path(workspace_root / sdk_root)) if sdk_root is not None else None
556
+ )
557
+ # tan-cli#236: `boardYaml` reported only when the file really exists -- an
558
+ # explicit `--board-yaml` skips the `is_file()` discovery guard above.
559
+ project_obj = Project.resolved(build_root, board_yaml)
560
+
561
+ if sdk_refusal is not None:
562
+ exit_code, data, issues, text_lines = sdk_refusal
563
+ else:
564
+ exit_code, data, issues, text_lines = _run(
565
+ build_root=build_root,
566
+ sdk_root=sdk_root,
567
+ sdk_root_for_stamp=sdk_root_for_stamp,
568
+ board_yaml=board_yaml,
569
+ flash=flash,
570
+ core=core,
571
+ confirm=confirm,
572
+ atoc_unqueryable=atoc_unqueryable,
573
+ replace_atoc=replace_atoc,
574
+ json_mode=json_mode,
575
+ )
576
+ except Exception as err: # noqa: BLE001 -- see build_cmd.build's identical guard
577
+ exit_code = ExitCode.INTERNAL_FAILURE
578
+ data = None
579
+ issues = [Issue("run.internal-failure", "error", f"{type(err).__name__}: {err}")]
580
+ text_lines = [f"run: internal failure: {type(err).__name__}: {err}"]
581
+
582
+ # tan-cli#263 review: `run` builds via the same engine as `build` and must
583
+ # disclose the same silently-missed project pin.
584
+ pin_issue = project_pin_issue(sdk_broken_pin, sdk_tier)
585
+ if pin_issue is not None:
586
+ issues = [pin_issue, *issues]
587
+ # tan-cli#464: same argument, one tier down -- `run` must disclose a
588
+ # `globalDefault` answer a DIFFERENT project's bootstrap relocation
589
+ # actually decided, the same as `build` now does.
590
+ foreign_issue = global_default_foreign_project_issue(sdk_foreign_default)
591
+ if foreign_issue is not None:
592
+ issues = [foreign_issue, *issues]
593
+ # tan-cli#497 defect 4: both of the above went into `issues` and NOWHERE
594
+ # else, while text mode prints `text_lines` -- so the warnings reached
595
+ # `--format json` and were silently discarded on the DEFAULT path. `tan
596
+ # build` in the identical workspace printed the pin line and `tan run` did
597
+ # not, which is precisely the silence tan-cli#263 exists to remove.
598
+ # Prefixed `{severity}: {message}`, the shape `_build_text_lines` already
599
+ # uses for a build issue, and taken from the head of `issues` so the two
600
+ # channels can never disagree about which warnings applied or in what
601
+ # order.
602
+ warning_count = (pin_issue is not None) + (foreign_issue is not None)
603
+ if warning_count:
604
+ text_lines = [
605
+ f"{issue.severity}: {issue.message}" for issue in issues[:warning_count]
606
+ ] + text_lines
607
+
608
+ # Built ONCE, for both formats: `Envelope.__init__` appends the
609
+ # tan-cli#407 `sdk.discovery-divergent` warning at the shared seam
610
+ # (`_with_sdk_divergence`), beyond the pin/foreign pair this function
611
+ # already prepends by hand above (tan-cli#464). Text mode used to render
612
+ # `text_lines`, built before any `Envelope` existed, so a seam-appended
613
+ # divergence issue reached `--format json` and was silent on the default
614
+ # channel (tan-cli#799) -- diffed against the pre-envelope `issues` list
615
+ # (by value: `Issue` is a frozen dataclass) so only what the seam ADDED
616
+ # is rendered, never a duplicate of the pin/foreign pair already in
617
+ # `text_lines`.
618
+ envelope = Envelope("run", project_obj, data, issues, exit_code, sdk=sdk)
619
+ seam_extra = [issue for issue in envelope.issues if issue not in issues]
620
+ if seam_extra:
621
+ text_lines = [
622
+ f"{issue.severity}: {issue.message}" for issue in seam_extra
623
+ ] + text_lines
624
+
625
+ if json_mode:
626
+ emit(envelope)
627
+ else:
628
+ for line in text_lines:
629
+ print(line, file=sys.stderr)
630
+ raise typer.Exit(int(exit_code))
631
+
632
+
633
+ # tan-cli#261: adds the seven oracle `GlobalArgs` flags this command was
634
+ # still missing (`--all`/`--ci`/`--no-color`/`--non-interactive`/`--quiet`/
635
+ # `--target`/`--verbose`) on top of `--board-yaml`, already declared and read
636
+ # above; see `tan.core.global_flags`.
637
+ run = accept_global_flags(run)