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,247 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Pure model + string builders for `tan image` -- port of
3
+ `crates/tan-core/src/image_bundle.rs` plus the `path_guard::is_plain_relative`
4
+ seam it depends on.
5
+
6
+ The IO (tar/gzip/sha256/copy/mkdir) lives in `tan.commands.image_cmd`; every
7
+ *decision* that does not touch the filesystem is here so it stays unit-testable:
8
+ the bundle-manifest shape + key order, the OS-independent forward-slash artefact
9
+ paths, and the ok-slice gate.
10
+
11
+ Two deliberate contract fixes carried over from the Rust:
12
+ - the helper entry carries `chip` (the real schema field) instead of the
13
+ perpetually-`null` `role` the retired Python emitted;
14
+ - artefact paths are built as forward-slash STRINGS, never from a `Path`, so
15
+ the machine contract never leaks a Windows `\\` separator.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import ntpath
21
+ import os
22
+ import re
23
+ from typing import Any
24
+
25
+ #: Bundle-manifest `schema_version` (matches the system-manifest v1 line).
26
+ BUNDLE_SCHEMA_VERSION = 1
27
+ #: `generated_by` value -- tan-branded (the retired Python emitted `west alp-image`).
28
+ GENERATED_BY = "tan image"
29
+ #: Bundle root directory name, under the build root.
30
+ BUNDLE_DIR = "image-bundle"
31
+ #: Per-slice archive subdirectory name.
32
+ SLICES_DIR = "slices"
33
+ #: Helper-MCU firmware subdirectory name.
34
+ HELPERS_DIR = "helper-mcus"
35
+ #: Output bundle-manifest filename.
36
+ BUNDLE_MANIFEST = "bundle-manifest.json"
37
+
38
+ #: The not-yet-built sentinel a SoM preset uses for a helper firmware that has no
39
+ #: release yet. A legitimate state, so it is a warning, never a failure.
40
+ PENDING_SENTINEL = "TBD"
41
+
42
+ _SEPARATORS = re.compile(r"[/\\]")
43
+
44
+
45
+ def is_plain_relative(path: str) -> bool:
46
+ """True when `path` is safe to join onto a trusted base: relative, and built
47
+ only from ordinary names. Port of `tan_core::path_guard::is_plain_relative`.
48
+
49
+ `os.path.isabs()` is NOT this check, for the same reason `Path::is_absolute()`
50
+ is not: on Windows it is false for `/x`, `\\x` and `C:x`, none of which
51
+ contains a `..` either, yet every one of them makes `base / p` discard part
52
+ or all of `base`. An empty path and a bare `.` are rejected too: both make
53
+ `base / p` resolve to `base` itself, so a caller meaning "a file under base"
54
+ would act on base.
55
+
56
+ Platform-dependent exactly as `Path::components()` is: `C:foo` is a drive
57
+ prefix on Windows and an ordinary filename on POSIX.
58
+ """
59
+ if os.name == "nt":
60
+ drive, rest = os.path.splitdrive(path)
61
+ if drive:
62
+ return False # Component::Prefix -- `C:`, `C:/`, `\\\\server\\share`
63
+ if rest[:1] in ("/", "\\"):
64
+ return False # Component::RootDir -- `\\x` and `/x` both
65
+ parts = _SEPARATORS.split(rest)
66
+ else:
67
+ if path.startswith("/"):
68
+ return False # Component::RootDir
69
+ parts = path.split("/")
70
+ parts = [p for p in parts if p != ""] # repeated separators collapse
71
+ if not parts:
72
+ return False # "" and "." and "/" all yield nothing usable
73
+ # `.` is normalized away EXCEPT at the start of the path, where it is a real
74
+ # `Component::CurDir` -- so `a/./b` passes and `./a` does not.
75
+ for index, part in enumerate(parts):
76
+ if part == "." and index > 0:
77
+ continue
78
+ if part in (".", ".."):
79
+ return False
80
+ return True
81
+
82
+
83
+ def is_plain_filename(name: str) -> bool:
84
+ """True when `name` is safe to use as ONE filename component -- the check
85
+ [`slice_archive_name`] needs, which is strictly narrower than
86
+ [`is_plain_relative`].
87
+
88
+ `is_plain_relative` answers "is this a safe RELATIVE PATH", so it correctly
89
+ accepts nested shapes: `a/b` and `a/./b` are both plain relative paths. A
90
+ value that gets interpolated into a single filename may not contain a
91
+ separator at all -- `a/b` there means a `slices/a/` subdirectory that was
92
+ never created, not a filename.
93
+
94
+ Both separators AND a Windows drive prefix are rejected on BOTH platforms,
95
+ unlike `is_plain_relative`'s deliberately platform-dependent component scan.
96
+ The resulting string is not only a local filename: [`slice_artefact_rel`]
97
+ folds it into the machine contract in `bundle-manifest.json`, which is read
98
+ on whatever OS consumes the bundle, and a `\\` a Linux `tan image` treated
99
+ as an ordinary character is a separator to the Windows consumer that reads
100
+ it back.
101
+
102
+ The drive check is `ntpath.splitdrive`, run unconditionally, NOT
103
+ `os.path.splitdrive` behind an `os.name == "nt"` guard (tan-cli#499 defect 9
104
+ REVIEW). The guarded version applied the cross-OS rule above to the
105
+ separators and then abandoned it one line later: on POSIX,
106
+ `os.path.splitdrive` is `posixpath`'s, which never finds a drive, so a
107
+ `core_id` of `C:foo` was accepted and written to `bundle-manifest.json` as
108
+ `C:foo-zephyr.tar.gz` -- a drive-RELATIVE path to the Windows consumer that
109
+ reads the bundle back, which is precisely the hazard the separator rule is
110
+ stated to prevent. One rule, applied to every component of it.
111
+ """
112
+ if not name or name in (".", ".."):
113
+ return False
114
+ if _SEPARATORS.search(name):
115
+ return False
116
+ if ntpath.splitdrive(name)[0]:
117
+ return False # `C:` / `C:foo` -- a drive prefix, not a name
118
+ return True
119
+
120
+
121
+ def slice_archive_name(core_id: str, os_name: str) -> str | None:
122
+ """Archive filename for a slice: `<core_id>-<os>.tar.gz`.
123
+
124
+ `None` when `core_id`/`os` are not safe to fold into a filename that will be
125
+ joined onto the bundle's `slices/` dir. Both come straight off
126
+ system-manifest.yaml -- a deliberately tolerant reader, not a validator --
127
+ and this used to interpolate them unchecked: a `core_id` of
128
+ `../../../../home/u/x` or (Windows) `C:/x` escaped `image-bundle/slices/`
129
+ entirely, so the caller's unlink + create could clobber an arbitrary
130
+ `*.tar.gz` outside the build tree. The guard lives HERE, not at the call
131
+ site, because [`slice_artefact_rel`] needs the identical check for the
132
+ manifest's `artefact` string -- a per-call-site guard is how this class of
133
+ bug keeps recurring.
134
+
135
+ The check is [`is_plain_filename`], NOT `is_plain_relative` (tan-cli#499
136
+ defect 9): the path guard accepts a nested `a/b`, which produced
137
+ `slices/a/b-zephyr.tar.gz` in a directory nothing had created, so
138
+ `image_cmd._tar_gzip_dir` raised and the WHOLE `tan image` run aborted at
139
+ exit 3 with no `bundle-manifest.json` and already-tarred slices orphaned --
140
+ where the equally-invalid `../x` took the intended `image.slice-unsafe-name`
141
+ warning-skip and still produced a bundle. This is a deliberate divergence
142
+ from the frozen oracle, which shares the defect; `is_plain_relative` itself
143
+ is unchanged, so its own nested-path contract (`a/./b` is plain) still holds.
144
+ """
145
+ if not is_plain_filename(core_id) or not is_plain_filename(os_name):
146
+ return None
147
+ return f"{core_id}-{os_name}.tar.gz"
148
+
149
+
150
+ def slice_artefact_rel(core_id: str, os_name: str) -> str | None:
151
+ """Forward-slash relative artefact path: `slices/<core_id>-<os>.tar.gz`.
152
+ `None` for the same unsafe shapes [`slice_archive_name`] rejects."""
153
+ name = slice_archive_name(core_id, os_name)
154
+ return None if name is None else f"{SLICES_DIR}/{name}"
155
+
156
+
157
+ def helper_artefact_rel(basename: str) -> str:
158
+ """Forward-slash relative artefact path: `helper-mcus/<basename>`."""
159
+ return f"{HELPERS_DIR}/{basename}"
160
+
161
+
162
+ def helper_firmware_candidates(
163
+ raw: str, build_root: str, sdk_root: str | None
164
+ ) -> list[tuple[str, str]]:
165
+ """Labeled `(root, absolute_candidate)` pairs for a helper's declared
166
+ `firmware_path`, in resolution order (alp-sdk#330).
167
+
168
+ alp-sdk's `som-preset-v2.schema.json` defines a relative `firmware_path` as
169
+ repository-relative -- relative to the SDK checkout, not to this project's
170
+ build tree. `build_root` is still tried FIRST, ahead of `sdk_root`,
171
+ mirroring the precedence `flash_plan.resolve_artefact_path` already
172
+ established for the identical two-roots ambiguity (tan-cli#301, #322): a
173
+ helper artefact the CURRENT build actually produced under `build/` is the
174
+ one this run just made, and must win over a same-named file the SDK
175
+ happens to ship. `sdk_root` is the fallback that lets a genuinely
176
+ SDK-shipped firmware (the common case) resolve at all -- build-root-only
177
+ resolution never could, which is the whole bug.
178
+
179
+ An absolute `raw` yields itself alone, unchanged: `sdk_root` never applies
180
+ to it (matches `system_manifest.anchor_under`'s own absolute passthrough).
181
+
182
+ Returned in full even when nothing resolves, so the caller can name every
183
+ root it tried and the absolute path each one produced (issue #330: the old
184
+ message named only the raw relative string, so the reporter had to go find
185
+ the file by hand to prove it existed).
186
+ """
187
+ if os.path.isabs(raw):
188
+ return [("absolute path", raw)]
189
+ candidates = [("build root", os.path.join(build_root, raw))]
190
+ if sdk_root is not None:
191
+ candidates.append(("sdk root", os.path.join(sdk_root, raw)))
192
+ return candidates
193
+
194
+
195
+ def slice_should_bundle(status: Any) -> bool:
196
+ """A slice is bundled iff it built successfully (`status == "ok"`)."""
197
+ return status == "ok"
198
+
199
+
200
+ def slice_entry(
201
+ core_id: str, os_name: str, artefact: str, sha256: str, size: int
202
+ ) -> dict[str, Any]:
203
+ """One bundled slice's manifest entry; key order IS the on-disk order."""
204
+ return {
205
+ "core_id": core_id,
206
+ "os": os_name,
207
+ "artefact": artefact,
208
+ "sha256": sha256,
209
+ "size": size,
210
+ }
211
+
212
+
213
+ def helper_entry(
214
+ name: str | None, chip: str | None, artefact: str, sha256: str, size: int
215
+ ) -> dict[str, Any]:
216
+ """One bundled helper firmware's manifest entry. `name`/`chip` are `null`
217
+ only if the manifest omitted them -- and `chip`, not the retired Python's
218
+ always-`null` `role`, is the field."""
219
+ return {
220
+ "name": name,
221
+ "chip": chip,
222
+ "artefact": artefact,
223
+ "sha256": sha256,
224
+ "size": size,
225
+ }
226
+
227
+
228
+ def assemble_bundle_manifest(
229
+ hw_info: dict,
230
+ boot_order: list,
231
+ slices: list[dict[str, Any]],
232
+ helper_mcus: list[dict[str, Any]],
233
+ ) -> dict[str, Any]:
234
+ """The written `bundle-manifest.json`, and the envelope's `data`. Insertion
235
+ order IS the key order: schema_version, generated_by, hw_info, slices,
236
+ helper_mcus, boot_order. Pure: no IO."""
237
+ return {
238
+ "schema_version": BUNDLE_SCHEMA_VERSION,
239
+ "generated_by": GENERATED_BY,
240
+ # Carried verbatim off the YAML so additive keys (`eeprom`, anything a
241
+ # newer SDK adds) survive into the bundle -- see
242
+ # `system_manifest.raw_passthrough`.
243
+ "hw_info": hw_info,
244
+ "slices": slices,
245
+ "helper_mcus": helper_mcus,
246
+ "boot_order": boot_order,
247
+ }
tan/core/inert.py ADDED
@@ -0,0 +1,147 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The one spelling of "tan accepts this option and does nothing with it"
3
+ (tan-cli#886).
4
+
5
+ `--help` marks an option inert in four structurally different situations, and
6
+ until this module existed the difference lived only in free prose that varied
7
+ per site: "Accepted by other commands; not implemented for `build` yet
8
+ (tan-cli#427)", "Accepted for compatibility (tan-cli#290)", "(unused:
9
+ faultdecode is HW-free)", "(unused; see below)". Four meanings, six spellings,
10
+ two of which said nothing at all once the option was read out of its
11
+ surrounding help block.
12
+
13
+ **Why that is a defect and not a style nit.** `alp-sdk-vscode` records tan's
14
+ whole option surface into a golden (`test/golden/tan-surface/surface.json`)
15
+ and renders the inert ones for the customer. Only ONE of the four kinds will
16
+ ever start acting; the other three are permanent by design. Telling someone
17
+ "not implemented yet, see tan-cli#427" about `doctor --build` invites them to
18
+ wait for a flag that is never going to act. With nothing structured to read,
19
+ the consumer's only options were to pattern-match tan's prose -- fragile, and
20
+ a condition pinned to one spelling is blind to the other five -- or to
21
+ hand-maintain its own copy of a table tan already knows
22
+ (alp-sdk-vscode#577 did the latter).
23
+
24
+ **The marker.** Every inert option's help ends with a single whitespace-free
25
+ token, `(inert:KIND)` or `(inert:KIND:REF)`:
26
+
27
+ --build Accepted for compatibility: ... (inert:compatibility:tan-cli#290)
28
+ --project Project root. Not read: ... (inert:not-applicable)
29
+
30
+ `deferred` has no live instance to show, so this block cannot illustrate it.
31
+ `--plan` filled the slot until tan-cli#427 RETIRED that flag, and
32
+ `--no-auto-bootstrap` took it over until the same issue retired that one too.
33
+ Measured on this tree with the census gate's own walk: 458 options, 120 marked
34
+ -- parity 117, compatibility 1, not-applicable 2, deferred 0. (A hand-rolled
35
+ walk reports 2: typer vendors click, so `isinstance(cmd, click.Group)` is
36
+ False and the recursion never starts. Use `hasattr(cmd, "list_commands")`.)
37
+
38
+ The kind stays in the closed vocabulary because it is the right marker the
39
+ next time tan declares a flag it has not built yet. A RETIRED flag is NOT it
40
+ and carries no marker at all: its value selects a real, distinct refusal
41
+ rather than being accepted and dropped.
42
+
43
+ Three properties of that shape are deliberate, and each rules out a rendering
44
+ defect measured on the real `--help`:
45
+
46
+ 1. **Parentheses, not square brackets.** Typer runs this app with
47
+ `rich_markup_mode="rich"`, so help text is rich MARKUP: a `[inert:deferred]`
48
+ marker parses as a style tag and renders as *nothing at all* -- measured, the
49
+ whole token vanishes from `tan build --help`. `\\[` escapes it, but only
50
+ while the markup mode stays `rich`, so the escape is a second thing to keep
51
+ true. Parentheses need neither.
52
+ 2. **No whitespace inside the token.** Rich wraps help text at spaces. A
53
+ `(inert: deferred, ref: tan-cli#427)` marker splits across two lines in a
54
+ narrow terminal and a consumer's regex has to re-join them first; this one
55
+ cannot split.
56
+ 3. **The ref lives in the marker, not a second time in the prose.** The prose
57
+ the twelve `build` flags shared already ended `(tan-cli#427).`; keeping both
58
+ would be two places to keep true for one fact.
59
+
60
+ **`deferred` is the only kind that requires a ref**, and `inert_help` refuses
61
+ to render one without it. That requirement is the whole point of publishing
62
+ the kind: `deferred` means "an upstream issue tracks its arrival", so a
63
+ `deferred` marker naming no issue is a promise with nothing behind it. The
64
+ permanent kinds MAY carry a ref (`compatibility` and `parity` both name the
65
+ issue that explains the history) and a consumer must not read the presence of
66
+ a ref as "this will arrive" -- the KIND is what says that, never the ref.
67
+ """
68
+ from __future__ import annotations
69
+
70
+ import re
71
+
72
+ #: An upstream issue tracks this option's arrival: it is accepted now, it does
73
+ #: nothing now, and it is expected to start acting. The ONLY kind that is not
74
+ #: permanent, and the only one that requires a ref.
75
+ DEFERRED = "deferred"
76
+
77
+ #: Kept so an existing caller's command line keeps parsing, after the
78
+ #: behaviour it used to select stopped being conditional. Permanent.
79
+ COMPATIBILITY = "compatibility"
80
+
81
+ #: Accepted only because a sibling surface accepts it -- the v0.4.1 oracle's
82
+ #: clap `GlobalArgs` are `global = true`, so every verb parses all ten
83
+ #: (tan-cli#261). Permanent; see `tan.core.global_flags`.
84
+ PARITY = "parity"
85
+
86
+ #: Structurally meaningless for THIS command -- there is no behaviour for it
87
+ #: to select, no matter what tan implements later. Permanent.
88
+ NOT_APPLICABLE = "not-applicable"
89
+
90
+ #: The closed vocabulary. A consumer switches on exactly these; anything else
91
+ #: is a tan bug, not a value to fall back on.
92
+ INERT_KINDS: tuple[str, ...] = (DEFERRED, COMPATIBILITY, PARITY, NOT_APPLICABLE)
93
+
94
+ #: The kinds that will never start acting. Stated as its own tuple rather than
95
+ #: as "everything except DEFERRED" so adding a fifth kind forces a decision
96
+ #: about which side it lands on instead of inheriting one.
97
+ PERMANENT_KINDS: tuple[str, ...] = (COMPATIBILITY, PARITY, NOT_APPLICABLE)
98
+
99
+ #: How the marker is read back OUT of rendered `--help` text -- published in
100
+ #: `contract/README.md` for the extension, and used by this repo's own gate
101
+ #: (`tests/gates/test_inert_option_markers.py`) so both sides read one regex.
102
+ MARKER_RE = re.compile(r"\(inert:(?P<kind>[a-z-]+)(?::(?P<ref>[^)\s]+))?\)")
103
+
104
+ #: The only ref spelling tan emits. `tan-cli#N` rather than a full URL: it is
105
+ #: what every other cross-reference in this CLI's prose already uses, and it
106
+ #: survives the line-wrapping rule above (a URL would not be shorter).
107
+ REF_RE = re.compile(r"^tan-cli#[1-9][0-9]*$")
108
+
109
+
110
+ def inert_help(prose: str, kind: str, ref: str | None = None) -> str:
111
+ """Render `prose` with the inert marker appended.
112
+
113
+ Raises `ValueError` -- at IMPORT time, since every call site is a
114
+ `typer.Option(...)` default evaluated when its module loads -- rather than
115
+ emitting a marker a consumer cannot switch on. A malformed marker that
116
+ only fails a test run would still have shipped in someone's branch;
117
+ failing the import fails the CLI itself.
118
+ """
119
+ if kind not in INERT_KINDS:
120
+ raise ValueError(
121
+ f"unknown inert kind {kind!r}; expected one of {', '.join(INERT_KINDS)}. "
122
+ "The vocabulary is closed on purpose -- a consumer switches on it "
123
+ "(tan-cli#886). Adding a kind means adding it to INERT_KINDS, to "
124
+ "PERMANENT_KINDS or not, and to contract/README.md."
125
+ )
126
+ if kind == DEFERRED and ref is None:
127
+ raise ValueError(
128
+ "a deferred option must name the issue tracking its arrival: "
129
+ "`deferred` is the one kind that promises the flag will start "
130
+ "acting, and a promise with no issue behind it is exactly what "
131
+ "tan-cli#886 asked to make impossible. Use a permanent kind "
132
+ f"({', '.join(PERMANENT_KINDS)}) if nothing tracks it."
133
+ )
134
+ if ref is not None and not REF_RE.match(ref):
135
+ raise ValueError(
136
+ f"inert ref {ref!r} is not of the form 'tan-cli#123'. One spelling "
137
+ "keeps the consumer's extraction regex (MARKER_RE) exact."
138
+ )
139
+ stripped = prose.strip()
140
+ if not stripped:
141
+ raise ValueError(
142
+ "inert help needs prose of its own: the marker says WHICH KIND of "
143
+ "inert, never what the option would have meant. An option whose "
144
+ "help is only a marker reads as nameless in `--help`."
145
+ )
146
+ marker = f"(inert:{kind})" if ref is None else f"(inert:{kind}:{ref})"
147
+ return f"{stripped} {marker}"
@@ -0,0 +1,128 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Where tan may take the J-Link Commander binary from (tan-cli#1336).
3
+
4
+ Flow D spawns the J-Link tool with the user's privileges while a probe is
5
+ attached, for the probe listing, the DPIDR preflight, the write and the
6
+ read-back. The workspace `.venv` is part of the PROJECT -- a checkout can ship
7
+ its own `.venv/bin/JLinkExe` -- so tan's "a west-capable venv is trusted"
8
+ rule (right for `west`) must not reach this program. It is resolved ONLY from:
9
+
10
+ 1. an explicit `--jlink <path>` (a CLI input);
11
+ 2. the `TAN_JLINK` environment variable (an environment input);
12
+ 3. `PATH` as the user's own environment has it -- never with the project venv
13
+ prepended, and skipping any PATH entry that is a virtualenv bin directory inside the project
14
+ directory (an activated project `.venv/bin` is still the project's);
15
+ 4. a known SEGGER install root: `/opt/SEGGER/*`, `/Applications/SEGGER/*`,
16
+ `%ProgramFiles%` / `%ProgramFiles(x86)%` `\\SEGGER\\*`.
17
+
18
+ Never a manifest field: `build/system-manifest.yaml` is regenerated from the
19
+ checkout and is exactly the surface a hostile project controls.
20
+
21
+ The ONE resolution is used by every J-Link spawn in a run, so the listing that
22
+ verifies the probe, the preflight that verifies the board, the write and the
23
+ read-back all execute the same binary, and the envelope reports it as
24
+ `jlink.binary`.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import glob
29
+ import os
30
+ import sys
31
+ from collections.abc import Mapping
32
+ from dataclasses import dataclass
33
+
34
+ from tan.core.tool_lookup import resolve_tool
35
+
36
+ #: The binary identities, in preference order (same as `flash_plan._JLINK_BINARIES`).
37
+ JLINK_NAMES = ("JLinkExe", "JLink")
38
+
39
+ #: The environment override.
40
+ ENV_OVERRIDE = "TAN_JLINK"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class JlinkBinary:
45
+ """A resolved J-Link binary: absolute `path` and where it came from."""
46
+
47
+ path: str
48
+ source: str
49
+
50
+
51
+ def _install_roots(env: Mapping[str, str], platform: str) -> list[str]:
52
+ roots: list[str] = []
53
+ if platform.startswith("win"):
54
+ for var in ("ProgramFiles", "ProgramFiles(x86)"):
55
+ base = env.get(var)
56
+ if base:
57
+ roots += sorted(glob.glob(os.path.join(base, "SEGGER", "JLink*")), reverse=True)
58
+ elif platform == "darwin":
59
+ roots += sorted(glob.glob("/Applications/SEGGER/JLink*"), reverse=True)
60
+ else:
61
+ roots += sorted(glob.glob("/opt/SEGGER/JLink*"), reverse=True)
62
+ return roots
63
+
64
+
65
+ def _in_dir(directory: str, platform: str) -> str | None:
66
+ suffixes = ("", ".exe") if platform.startswith("win") else ("",)
67
+ for name in JLINK_NAMES:
68
+ for suffix in suffixes:
69
+ candidate = os.path.join(directory, name + suffix)
70
+ if os.path.isfile(candidate):
71
+ return candidate
72
+ return None
73
+
74
+
75
+ def _inside(path: str, root: str) -> bool:
76
+ try:
77
+ real_path, real_root = os.path.realpath(path), os.path.realpath(root)
78
+ return os.path.commonpath([real_path, real_root]) == real_root
79
+ except (ValueError, OSError):
80
+ return False
81
+
82
+
83
+ def _is_project_venv_bin(entry: str, project_dir: str) -> bool:
84
+ """A PATH entry that is a virtualenv `bin`/`Scripts` directory (its parent holds
85
+ `pyvenv.cfg`) living INSIDE the project directory."""
86
+ return _inside(entry, project_dir) and os.path.isfile(
87
+ os.path.join(os.path.dirname(os.path.realpath(entry)), "pyvenv.cfg")
88
+ )
89
+
90
+
91
+ def _path_without_project(env: Mapping[str, str], project_dir: str | None) -> dict[str, str]:
92
+ """`env` with every PATH entry that is a project virtualenv's bin directory removed
93
+ -- an ACTIVATED project `.venv` on the user's PATH is still the project's, and the
94
+ whole point of tan-cli#1336 is that the project's venv never supplies this binary."""
95
+ path = env.get("PATH")
96
+ if not project_dir or not path:
97
+ return dict(env)
98
+ kept = [d for d in path.split(os.pathsep) if d and not _is_project_venv_bin(d, project_dir)]
99
+ return {**env, "PATH": os.pathsep.join(kept)}
100
+
101
+
102
+ def resolve_jlink(
103
+ cli_path: str | None,
104
+ env: Mapping[str, str] | None = None,
105
+ *,
106
+ platform: str | None = None,
107
+ project_dir: str | None = None,
108
+ ) -> JlinkBinary | None:
109
+ """The J-Link binary tan may spawn, or `None` when none is found. An
110
+ explicit `cli_path` / `TAN_JLINK` that does not name an existing file
111
+ resolves to `None` rather than falling through: the operator named a
112
+ binary, and quietly using a different one is the thing this refuses."""
113
+ env = os.environ if env is None else env
114
+ platform = sys.platform if platform is None else platform
115
+ for value, source in ((cli_path, "the --jlink flag"), (env.get(ENV_OVERRIDE), f"{ENV_OVERRIDE}")):
116
+ if value:
117
+ path = os.path.abspath(value)
118
+ return JlinkBinary(path, source) if os.path.isfile(path) else None
119
+ path_env = _path_without_project(env, project_dir)
120
+ for name in JLINK_NAMES:
121
+ found = resolve_tool(name, path_env).resolved
122
+ if found:
123
+ return JlinkBinary(os.path.abspath(found), "PATH")
124
+ for root in _install_roots(env, platform):
125
+ found = _in_dir(root, platform)
126
+ if found:
127
+ return JlinkBinary(found, f"SEGGER install {root}")
128
+ return None