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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/core/atomic_write.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""A durable atomic-write helper (`atomic_write_text`, plus the byte-exact
|
|
3
|
+
`atomic_write_bytes`) for a file more than one process or SDK checkout can
|
|
4
|
+
share -- fsync'd before the rename, not just atomic-in-naming.
|
|
5
|
+
|
|
6
|
+
tan-cli#516: `bootstrap_cmd.reconcile_west_manifest_path` wrote its temp
|
|
7
|
+
sibling with `Path.write_text` and a bare `os.replace`, with no `os.fsync` on
|
|
8
|
+
the temp before the rename and no directory `fsync` after it. `os.replace`'s
|
|
9
|
+
atomicity guarantee covers the RENAME only -- it says nothing about whether
|
|
10
|
+
the renamed-to content has actually reached stable storage. A crash between
|
|
11
|
+
the rename landing and the temp's data blocks flushing can leave `.west/
|
|
12
|
+
config` existing under its real name with truncated or missing content, on
|
|
13
|
+
any filesystem that does not happen to order the two together: ext4's
|
|
14
|
+
`auto_da_alloc` heuristic covers the common Linux case; XFS, btrfs, APFS,
|
|
15
|
+
NTFS via `MoveFileExW`, and network filesystems generally do not.
|
|
16
|
+
|
|
17
|
+
This is the SAME durability + symlink-safety + mode-preservation shape
|
|
18
|
+
`debug_config_cmd._atomic_write_launch_json` carried (tan-cli#489, review
|
|
19
|
+
findings 4+5) for the identical reason -- that function's own opening comment
|
|
20
|
+
already called its shape "matching `bootstrap_cmd.reconcile_west_manifest_
|
|
21
|
+
path`'s own pattern", so the two were always meant to agree; #516 is that
|
|
22
|
+
agreement breaking (the fsync fix landed on one call site, not the other, and
|
|
23
|
+
outlived it). Extracted here rather than re-copied inline a second time, so a
|
|
24
|
+
future edit to the durability sequence itself cannot re-diverge between call
|
|
25
|
+
sites the way it just did -- tan-cli#510 was exactly this failure mode: two
|
|
26
|
+
independent copies of a resolver that drifted apart. `debug_config_cmd.py`'s
|
|
27
|
+
own copy is gone as of the #516 review round -- `_write` now calls this
|
|
28
|
+
function directly, rather than keeping a second, hand-synchronised
|
|
29
|
+
implementation beside it -- so the docstring's promise and the code agree.
|
|
30
|
+
|
|
31
|
+
**Mode preservation mirrors `_atomic_write_launch_json` exactly**, because
|
|
32
|
+
this helper now serves BOTH call sites and one of them (`.vscode/
|
|
33
|
+
launch.json`) genuinely needs it: a `664`-mode file a shared checkout
|
|
34
|
+
convention set deliberately must not come back at `mkstemp`'s hardcoded
|
|
35
|
+
`0600` merely because it was rewritten. `.west/config` -- this helper's other
|
|
36
|
+
caller -- has no such convention to preserve, but reading a mode that already
|
|
37
|
+
exists and reapplying it after the swap is a no-op cost for that caller, not
|
|
38
|
+
a hazard, so one shape serves both rather than growing a caller-specific
|
|
39
|
+
branch.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import os
|
|
45
|
+
import stat
|
|
46
|
+
import tempfile
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def atomic_write_text(path: str, content: str, *, encoding: str = "utf-8") -> None:
|
|
50
|
+
"""Write `content` to `path` atomically and durably. Raises on any
|
|
51
|
+
failure; the caller decides how to report it.
|
|
52
|
+
|
|
53
|
+
* **Symlink-safe.** `os.path.realpath` resolves the real target FIRST,
|
|
54
|
+
both for where the temp sibling is created (the same directory, and so
|
|
55
|
+
the same filesystem -- `os.replace` requires that for its atomicity
|
|
56
|
+
guarantee and fails outright with `EXDEV` without it) and for what
|
|
57
|
+
`os.replace` itself targets: writing the temp beside the UNresolved
|
|
58
|
+
`path` and replacing that would put a regular file where a symlink was,
|
|
59
|
+
destroying the link, while the real file silently stops being updated.
|
|
60
|
+
* **Durable, not just atomic-in-naming.** `os.fsync` on the temp file's
|
|
61
|
+
own descriptor, before the rename, gets the CONTENT onto stable
|
|
62
|
+
storage; a POSIX directory `fsync` after the rename covers the RENAME
|
|
63
|
+
ENTRY surviving a crash too (Windows has no directory handle to fsync,
|
|
64
|
+
and `ReplaceFile`/`MoveFileExW` already journal the rename itself).
|
|
65
|
+
* **Mode-preserving.** The existing file's permission bits (`stat.S_IMODE`,
|
|
66
|
+
read BEFORE the write) are re-applied to the real path AFTER a
|
|
67
|
+
successful `os.replace` -- never to the temp file itself, since
|
|
68
|
+
`os.replace` swaps the directory entry, not the inode's own mode bits,
|
|
69
|
+
and `mkstemp`'s hardcoded `0600` would otherwise silently narrow
|
|
70
|
+
whatever the file used to be. A first-ever write (no existing file to
|
|
71
|
+
read a mode from) gets `0666 & ~umask` instead of keeping `mkstemp`'s
|
|
72
|
+
`0600` forever, so it starts out at the ordinary default a plain
|
|
73
|
+
`open(path, "w")` would have produced.
|
|
74
|
+
* **Cleans up its own temp on failure** -- the caller never needs a
|
|
75
|
+
matching `unlink` in its own `except` block. This covers more than
|
|
76
|
+
`OSError`: an unencodable `content` under `encoding` raises
|
|
77
|
+
`UnicodeEncodeError` (a `ValueError`) from `handle.write`, and an
|
|
78
|
+
unknown `encoding=` raises `LookupError` from `os.fdopen` itself --
|
|
79
|
+
both happen strictly between `mkstemp` creating the temp and the
|
|
80
|
+
rename, so both must unlink it too, or it leaks into the caller's
|
|
81
|
+
directory un-unlinked.
|
|
82
|
+
"""
|
|
83
|
+
# `content.encode(encoding)` raises `UnicodeEncodeError` (a `ValueError`)
|
|
84
|
+
# on unencodable content, or `LookupError` on an unknown `encoding=` --
|
|
85
|
+
# both used to surface later, from inside `os.fdopen`/`handle.write`,
|
|
86
|
+
# strictly between `mkstemp` creating the temp and the rename, which is
|
|
87
|
+
# why the old single-function version needed its own cleanup `except`
|
|
88
|
+
# for them. Encoding FIRST means either error now raises before any temp
|
|
89
|
+
# file exists at all, so there is nothing here left to clean up.
|
|
90
|
+
encoded = content.encode(encoding)
|
|
91
|
+
_atomic_write_bytes(path, encoded)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def atomic_write_bytes(path: str, data: bytes) -> None:
|
|
95
|
+
"""`atomic_write_text`'s byte-exact sibling -- same symlink-safety,
|
|
96
|
+
durability and mode-preservation, with no text encode/decode round trip
|
|
97
|
+
in between.
|
|
98
|
+
|
|
99
|
+
**The one caller that needs this over `atomic_write_text`
|
|
100
|
+
(`bootstrap_cmd._undo_relocation`'s registry rollback, review #904 third
|
|
101
|
+
round, nit) is restoring a BYTE snapshot taken before this run's own
|
|
102
|
+
write** (`_read_global_sdk_registry_bytes`), not producing fresh
|
|
103
|
+
content -- and that snapshot is not guaranteed UTF-8: this registry
|
|
104
|
+
tolerates a hand-edited or pre-this-fix file (`parse_registry`'s own
|
|
105
|
+
"never raises on malformed content" contract), so decoding it to `str`
|
|
106
|
+
and back through `atomic_write_text` could raise on exactly the
|
|
107
|
+
corrupted-but-was-there-before content a rollback exists to put back
|
|
108
|
+
unchanged. Restoring the literal bytes, atomically, is the only shape
|
|
109
|
+
that can't fail on its own input.
|
|
110
|
+
"""
|
|
111
|
+
_atomic_write_bytes(path, data)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _atomic_write_bytes(path: str, data: bytes) -> None:
|
|
115
|
+
resolved = os.path.realpath(path)
|
|
116
|
+
directory = os.path.dirname(resolved)
|
|
117
|
+
existing_mode: int | None = None
|
|
118
|
+
try:
|
|
119
|
+
existing_mode = stat.S_IMODE(os.stat(resolved).st_mode)
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
fd, tmp_path = tempfile.mkstemp(dir=directory, suffix=".tan-tmp")
|
|
123
|
+
try:
|
|
124
|
+
try:
|
|
125
|
+
handle = os.fdopen(fd, "wb")
|
|
126
|
+
except OSError:
|
|
127
|
+
# `fdopen` failing before a file object takes ownership of `fd`
|
|
128
|
+
# would otherwise leak the raw descriptor `mkstemp` opened.
|
|
129
|
+
# Best-effort: close if `fd` is still ours to close, ignore if
|
|
130
|
+
# not.
|
|
131
|
+
try:
|
|
132
|
+
os.close(fd)
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
raise
|
|
136
|
+
with handle:
|
|
137
|
+
handle.write(data)
|
|
138
|
+
handle.flush()
|
|
139
|
+
os.fsync(handle.fileno())
|
|
140
|
+
os.replace(tmp_path, resolved)
|
|
141
|
+
except OSError:
|
|
142
|
+
try:
|
|
143
|
+
os.unlink(tmp_path)
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
raise
|
|
147
|
+
if existing_mode is not None:
|
|
148
|
+
try:
|
|
149
|
+
os.chmod(resolved, existing_mode)
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
else:
|
|
153
|
+
try:
|
|
154
|
+
# No race-free way to READ the process umask -- the standard
|
|
155
|
+
# idiom sets a harmless value and reads back the PREVIOUS one,
|
|
156
|
+
# then restores it immediately. `os.umask` is PROCESS-global, so
|
|
157
|
+
# this brief set-then-restore is safe only under tan's own
|
|
158
|
+
# single-threaded use of this helper; a second thread doing
|
|
159
|
+
# anything umask-sensitive inside this same window would observe
|
|
160
|
+
# -- or set -- the wrong value.
|
|
161
|
+
current_umask = os.umask(0o022)
|
|
162
|
+
os.umask(current_umask)
|
|
163
|
+
os.chmod(resolved, 0o666 & ~current_umask)
|
|
164
|
+
except OSError:
|
|
165
|
+
pass
|
|
166
|
+
if os.name != "nt":
|
|
167
|
+
try:
|
|
168
|
+
dir_fd = os.open(directory, os.O_RDONLY)
|
|
169
|
+
try:
|
|
170
|
+
os.fsync(dir_fd)
|
|
171
|
+
finally:
|
|
172
|
+
os.close(dir_fd)
|
|
173
|
+
except OSError:
|
|
174
|
+
# Best-effort insurance for the RENAME entry surviving a crash, on
|
|
175
|
+
# top of the file's own fsync above (which already protects the
|
|
176
|
+
# content): some filesystems/mounts (overlayfs, certain FUSE/NFS
|
|
177
|
+
# setups) refuse a bare directory fsync outright.
|
|
178
|
+
pass
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Where a project's `board.yaml` is, and the one fact `tan scaffold` reports
|
|
3
|
+
out of it.
|
|
4
|
+
|
|
5
|
+
Two halves of the same question, and both had a copy-per-command problem:
|
|
6
|
+
|
|
7
|
+
* [`resolve_board_path`] is `validate_cmd._resolve_board_path`, MOVED here
|
|
8
|
+
rather than re-typed. `tan scaffold` needed exactly what `tan validate`
|
|
9
|
+
already does (tan-cli#1031), and this repo already carries SIX project/board
|
|
10
|
+
resolvers. A SEVENTH, written for `scaffold`, is precisely the drift
|
|
11
|
+
`tan.core.shapes`' own docstring is about; and it would be the same drift
|
|
12
|
+
alp-sdk-vscode#601/#633 had just finished deleting a second copy of THIS
|
|
13
|
+
generator to escape (the README's `## Wiring` section went missing on the
|
|
14
|
+
extension's side because a second copy existed at all). So the function
|
|
15
|
+
moved to a binding-free home both commands import.
|
|
16
|
+
|
|
17
|
+
**The criterion, stated so the next reader can CHECK the membership instead
|
|
18
|
+
of re-deriving it** -- this count has now been revised three times in a
|
|
19
|
+
docstring whose whole thesis is drift, which is its own argument for naming
|
|
20
|
+
the rule rather than the number. A member is a NAMED, REUSABLE function
|
|
21
|
+
under `python/tan/**` that turns the `--project`/`--board-yaml` pair into
|
|
22
|
+
the `board.yaml` path a command works from. Per-command inline joins do not
|
|
23
|
+
count; neither does anything under `tan/planner/**`, which resolves a board
|
|
24
|
+
DOCUMENT out of metadata rather than a path out of two flags. The six, with
|
|
25
|
+
the commands each serves:
|
|
26
|
+
|
|
27
|
+
tan/core/board_context.py:resolve_board_path validate, scaffold
|
|
28
|
+
presets_cmd.py:resolve_project_paths presets, pinmux, kconfig,
|
|
29
|
+
diff, clean, bootstrap
|
|
30
|
+
build_output.py:resolve_project_context size, image, model, west,
|
|
31
|
+
debug-config
|
|
32
|
+
inspect_cmd.py:resolve_debug_project_context inspect, trace,
|
|
33
|
+
support-bundle
|
|
34
|
+
flash_cmd.py:_resolve_project flash
|
|
35
|
+
generate_cmd.py:_resolve_board_path generate
|
|
36
|
+
|
|
37
|
+
FIVE of the six feed `tan.envelope.Project.resolved`, the reporting seam.
|
|
38
|
+
`generate_cmd._resolve_board_path` is the one that does not, deliberately:
|
|
39
|
+
`generate` reports the AS-GIVEN strings, built inline, because existence-
|
|
40
|
+
checking them against the real cwd is wrong the moment `--project` differs
|
|
41
|
+
from it (its own call site says so, citing tan-cli#236), and this resolver
|
|
42
|
+
answers only the path it READS. It is on the list anyway because it answers
|
|
43
|
+
the same question. A criterion of "uses `Project.resolved`" would have got
|
|
44
|
+
this exactly backwards on both ends: it would have EXCLUDED `generate_cmd`,
|
|
45
|
+
the one resolver that answers the question without the seam, while
|
|
46
|
+
ADMITTING a dozen things that are not resolvers at all -- measured at this
|
|
47
|
+
commit, there are 20 `Project.resolved(` call sites across 16 modules
|
|
48
|
+
(excluding the definition at `tan/envelope.py:222`), mostly command
|
|
49
|
+
callbacks doing an inline join. Hence the criterion above keys on what a
|
|
50
|
+
function DOES, not on which helper it happens to call.
|
|
51
|
+
|
|
52
|
+
Two earlier revisions of this paragraph were wrong in two different
|
|
53
|
+
directions -- one listed `generate_cmd`, the loosest match, while omitting
|
|
54
|
+
`inspect_cmd`, which does use the seam; the next inverted the reason above.
|
|
55
|
+
Three wrong revisions of one claim in a docstring whose subject is drift is
|
|
56
|
+
itself the argument for tan-cli#1091: prose does not hold a boundary, a
|
|
57
|
+
gate does.
|
|
58
|
+
|
|
59
|
+
The count did NOT go down: six before this change and six after, because
|
|
60
|
+
the sixth is validate's, relocated. Each is pinned to a different oracle's
|
|
61
|
+
reported shape, so they are distinct answers rather than duplicates, and
|
|
62
|
+
unifying them is not this change's job. What this change bought is that
|
|
63
|
+
`scaffold` did not make it seven.
|
|
64
|
+
|
|
65
|
+
**"One definition" here is now a gate, not prose (tan-cli#1091).**
|
|
66
|
+
`tests/gates/test_shared_helpers_have_one_definition.py` used to hard-assert
|
|
67
|
+
that every helper it owns lives in `tan/core/shapes.py`, reading its
|
|
68
|
+
ownership list out of that one file, so `resolve_board_path` could not join
|
|
69
|
+
it without generalising the gate from "shapes owns these" to a
|
|
70
|
+
`{name -> home module}` map. tan-cli#1091 did exactly that: the map is
|
|
71
|
+
`_SHARED_HELPERS` now, `resolve_board_path` is seeded in it pointed at this
|
|
72
|
+
module, and the per-helper assertion compares each entry against its OWN
|
|
73
|
+
declared home rather than a hard-coded `tan/core/shapes.py` literal. Its
|
|
74
|
+
sibling `tests/gates/test_shared_test_helpers_have_one_definition.py`
|
|
75
|
+
(tan-cli#1083) was already that shape and is what tan-cli#1091 copied.
|
|
76
|
+
NOT tan-cli#1081, which an earlier revision of this paragraph pointed at:
|
|
77
|
+
#1081's durable half already landed as #1083; the remainder of #1081's
|
|
78
|
+
scope (an outstanding `bound_sdk*` binder audit, tracked by PR #1102)
|
|
79
|
+
touches none of `TAN_ROOT`, `_SHARED_HELPERS`, or the per-entry home
|
|
80
|
+
assertion. A seventh private `_resolve_board_path` -- or a private
|
|
81
|
+
redefinition of this module's own --
|
|
82
|
+
now REDS `test_a_shared_helper_is_defined_exactly_once[resolve_board_path]`,
|
|
83
|
+
naming both files; this paragraph recorded the gap while it existed rather
|
|
84
|
+
than claiming it was already closed.
|
|
85
|
+
|
|
86
|
+
* [`read_board_context`] is the content of the generated module's
|
|
87
|
+
`// Board context: ...` line. Its wire spelling -- `<sku> / <os>`, with
|
|
88
|
+
[`UNSET`] for a half the document does not declare -- is the retired
|
|
89
|
+
alp-sdk-vscode generator's, kept verbatim so a module scaffolded from the
|
|
90
|
+
extension and one scaffolded by `tan` carry the same bytes.
|
|
91
|
+
|
|
92
|
+
**One spelling per envelope: the separator follows the ROOT** (tan-cli#1031,
|
|
93
|
+
found by CI on `windows-latest`; this box is Linux and cannot reproduce it).
|
|
94
|
+
[`resolve_board_path`] used to join the leaf with a hardcoded `/`, which is
|
|
95
|
+
right only while the root is posix-spelled. Given `--project C:\\w\\proj`
|
|
96
|
+
it answered the MIXED `C:\\w\\proj/board.yaml` while `project.root` stayed
|
|
97
|
+
`C:\\w\\proj` -- one envelope carrying two spellings of one directory, and
|
|
98
|
+
`C:\\w\\proj/board.yaml` is a spelling nothing contracts for and nobody
|
|
99
|
+
asked for. `tan validate` had emitted it since long before `scaffold` existed;
|
|
100
|
+
`scaffold`'s tests are simply the first to pass an absolute `--project`.
|
|
101
|
+
|
|
102
|
+
The invariant every OTHER reporter of this pair already satisfies is
|
|
103
|
+
`project.boardYaml == project.root + <sep> + <leaf>` -- asserted verbatim as
|
|
104
|
+
`f"{root}/board.yaml"` by `test_build_command.py:1182`,
|
|
105
|
+
`test_presets_command.py:702` and `test_inspect_command.py:163`. Those three
|
|
106
|
+
read `/` because THEIR root is posix-normalised upstream
|
|
107
|
+
(`build_output.to_posix`, `presets_cmd._posix`, `inspect_cmd._abs_posix`), NOT
|
|
108
|
+
because the leaf is posix by contract. So the fix is to derive the separator
|
|
109
|
+
from the root, not to posix the leaf.
|
|
110
|
+
|
|
111
|
+
`root` itself is deliberately NOT normalised here: `validate` and `scaffold`
|
|
112
|
+
report the string the caller TYPED (`generate` does the same), and re-spelling
|
|
113
|
+
it would be a behaviour change to a shipped command that no defect asks for.
|
|
114
|
+
A root with no separator at all (`"."`, `"proj"`, `"C:"`) keeps `/` on every
|
|
115
|
+
platform, so `"./board.yaml"` -- what every committed conformance golden
|
|
116
|
+
captures -- stays byte-identical across hosts. A root the caller spelled with
|
|
117
|
+
BOTH separators is left alone, `/` winning, so the answer is never more mixed
|
|
118
|
+
than the input.
|
|
119
|
+
|
|
120
|
+
`"C:"` is the one separator-less root where declining `ntpath.join` is not
|
|
121
|
+
merely a spelling difference but a DIFFERENT DIRECTORY, so it is called out
|
|
122
|
+
rather than left implicit (measured): this rule answers `C:/board.yaml`, the
|
|
123
|
+
root OF drive C:, while `ntpath.join("C:", "board.yaml")` answers
|
|
124
|
+
`C:board.yaml`, the drive-RELATIVE path naming the cwd ON C:. The input is
|
|
125
|
+
pathological -- `--project C:` is a drive letter, not a project -- and both
|
|
126
|
+
answers are defensible for it, but the divergence is real and a reader
|
|
127
|
+
checking this rule against the platform's own join deserves to meet it here
|
|
128
|
+
instead of discovering it. `tests/core/test_board_context.py` pins both
|
|
129
|
+
spellings.
|
|
130
|
+
|
|
131
|
+
The choice reads the root STRING, never `os.sep`, so the Windows branch is
|
|
132
|
+
fully exercised by a Linux run: `tests/core/test_board_context.py` drives it
|
|
133
|
+
against `ntpath.join`/`posixpath.join` as oracles. The Windows behaviour here
|
|
134
|
+
is therefore reasoned and unit-tested, not executed on Windows. Only
|
|
135
|
+
`os.path.isabs` stays host-aware, which is correct -- whether `C:\\x\\y` is
|
|
136
|
+
absolute genuinely IS a host question.
|
|
137
|
+
|
|
138
|
+
**What a reported path does and does not promise.** `tan.envelope.Project.
|
|
139
|
+
resolved` is the seam that turns a resolved path into `project.boardYaml`, and
|
|
140
|
+
it gates on `os.path.exists`, which is true for a DIRECTORY -- so a directory
|
|
141
|
+
named `board.yaml` reports a non-null path despite that method's own "only when
|
|
142
|
+
a file is really there". Pre-existing, and shared with `tan validate` and `tan
|
|
143
|
+
flash` (all three report through the same seam), so it is recorded here rather
|
|
144
|
+
than fixed asymmetrically in one command. It costs this module nothing:
|
|
145
|
+
[`read_board_context`] answers `unavailable` for that input either way, because
|
|
146
|
+
reading a directory raises and lands on the file-failure arm
|
|
147
|
+
(`test_a_directory_where_the_file_should_be_is_unavailable`).
|
|
148
|
+
|
|
149
|
+
**`board.yaml` ONLY, no SDK -- deliberately.** `tan scaffold` resolves no
|
|
150
|
+
alp-sdk checkout (its own module docstring says so, and one of tan-cli#1031's
|
|
151
|
+
four reported invocations passes no `--sdk-root` at all), and this module does
|
|
152
|
+
not change that. The consequence is stated rather than hidden: a v2
|
|
153
|
+
`board.yaml` that omits `os:` genuinely does not carry one -- alp-sdk's
|
|
154
|
+
`board.schema.json` says of `cores.<id>.os` that "The OS runtime is DERIVED
|
|
155
|
+
from the core's silicon class and is not selectable... Omit `os:` to take that
|
|
156
|
+
runtime", so the value then lives in the SoM preset, behind a checkout. That
|
|
157
|
+
half renders [`UNSET`]; it is not guessed from the core id. Resolving it would
|
|
158
|
+
mean walking `board.yaml -> som.sku -> metadata/e1m_modules/<sku>.yaml ->
|
|
159
|
+
silicon -> metadata/socs/**.json -> cores[].type ->
|
|
160
|
+
tan.core.os_class.default_os_from_core_type`, which would make a COMMENT's
|
|
161
|
+
bytes depend on which checkout happened to be discovered on the machine that
|
|
162
|
+
ran the scaffold. That is a bigger contract than a comment is worth; if it is
|
|
163
|
+
wanted it is a separate, argued change.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
from __future__ import annotations
|
|
167
|
+
|
|
168
|
+
import os
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
from typing import Any
|
|
171
|
+
|
|
172
|
+
#: What a resolved `board.yaml` renders for a field it does not declare. The
|
|
173
|
+
#: retired alp-sdk-vscode generator's own `?? "<unset>"` spelling
|
|
174
|
+
#: (`packages/alp-core/src/wizard/service.ts`, deleted in alp-sdk-vscode#601).
|
|
175
|
+
UNSET = "<unset>"
|
|
176
|
+
|
|
177
|
+
#: A `cores.<id>.os` value that PARKS a core rather than naming a runtime, so
|
|
178
|
+
#: it can never be the project's OS. Not a nicety: of the 100 example
|
|
179
|
+
#: `board.yaml` files in alp-sdk v0.16.0 (tag `v0.16.0`, `eb96112ba` -- an
|
|
180
|
+
#: earlier count of 99 here was measured off a stale `dev` working tree, not
|
|
181
|
+
#: the tag it cited), 53 declare `os:` at all and 51 of those declare ONLY
|
|
182
|
+
#: this -- reading the first declared value blindly would report `off` as the
|
|
183
|
+
#: board context for most real projects. Zero of the 100 carry a top-level
|
|
184
|
+
#: `os:`.
|
|
185
|
+
#:
|
|
186
|
+
#: The two that name a real runtime are the whole population this module can
|
|
187
|
+
#: answer without an SDK, so they are named rather than counted:
|
|
188
|
+
#: `examples/connectivity/modbus-server/board.yaml` (one core, `os: zephyr`)
|
|
189
|
+
#: and `examples/power-timing/power-managed-sensor/board.yaml` (`os: "off"`
|
|
190
|
+
#: on the parked HP core AND `os: zephyr` on the HE core -- the mixed shape
|
|
191
|
+
#: this constant exists for, and the one `test_a_parked_core_is_not_a_runtime`
|
|
192
|
+
#: pins). Every other example board renders the [`UNSET`] half.
|
|
193
|
+
OS_OFF = "off"
|
|
194
|
+
|
|
195
|
+
#: `cores.<id>.os` values that are declared but carry no runtime name. `""`
|
|
196
|
+
#: is not schema-valid (the enum is `zephyr|yocto|baremetal|off`); it is
|
|
197
|
+
#: rejected here anyway rather than being echoed into a C comment, since this
|
|
198
|
+
#: input is a hand-edited customer file and nothing upstream of here has
|
|
199
|
+
#: validated it.
|
|
200
|
+
_NOT_A_RUNTIME = ("", OS_OFF)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def resolve_board_path(project: str | None, board_yaml: str | None) -> tuple[str, str]:
|
|
204
|
+
"""Return `(project_root, board_yaml_path)`, both as the CLI reports them.
|
|
205
|
+
|
|
206
|
+
Mirrors `resolve_offline_board_path`: the root defaults to the literal `"."`
|
|
207
|
+
and the board path stays RELATIVE, which the conformance fixtures pin
|
|
208
|
+
(`project.root == "."`, `boardYamlPath == "./board.yaml"`).
|
|
209
|
+
|
|
210
|
+
**`board_yaml` -- the `--board-yaml` flag -- WINS over the project-relative
|
|
211
|
+
default whenever it is given**: absolute, verbatim; relative, joined onto
|
|
212
|
+
the root. That is what its help text has always claimed ("Explicit
|
|
213
|
+
board.yaml path (overrides project resolution)") and what tan-cli#1031
|
|
214
|
+
found `tan scaffold` was not honouring, because `scaffold` never called a
|
|
215
|
+
resolver at all.
|
|
216
|
+
|
|
217
|
+
Existence is NOT checked here, and must not be: `validate`'s refusal
|
|
218
|
+
message names the path it could not open, so nulling it at the resolver
|
|
219
|
+
would strip the path out of the very message that names it.
|
|
220
|
+
`tan.envelope.Project.resolved` is the seam that checks -- its own
|
|
221
|
+
docstring documents that split.
|
|
222
|
+
|
|
223
|
+
The separator follows the ROOT's own spelling -- see "One spelling per
|
|
224
|
+
envelope" in the module docstring for why, and for what the alternatives
|
|
225
|
+
cost.
|
|
226
|
+
"""
|
|
227
|
+
root = project if project else "."
|
|
228
|
+
if board_yaml and os.path.isabs(board_yaml):
|
|
229
|
+
return root, board_yaml
|
|
230
|
+
leaf = board_yaml or "board.yaml"
|
|
231
|
+
# Joined as STRINGS, not via pathlib: `Path(".") / "board.yaml"` normalises
|
|
232
|
+
# to `board.yaml`, but Rust's `Path::new(".").join("board.yaml")` keeps the
|
|
233
|
+
# `./`, and the conformance fixtures pin `"./board.yaml"`.
|
|
234
|
+
if root.endswith(("/", "\\")):
|
|
235
|
+
sep = ""
|
|
236
|
+
elif "\\" in root and "/" not in root:
|
|
237
|
+
sep = "\\"
|
|
238
|
+
else:
|
|
239
|
+
sep = "/"
|
|
240
|
+
return root, f"{root}{sep}{leaf}"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _board_mapping(board_yaml_path: str | None) -> dict[str, Any] | None:
|
|
244
|
+
"""`board.yaml` parsed to a mapping, or `None` for EVERY way that can
|
|
245
|
+
fail -- no path at all, a missing file, a directory, an unreadable one, a
|
|
246
|
+
non-UTF-8 byte, malformed YAML, an empty document, a document that parses
|
|
247
|
+
to something other than a mapping, or PyYAML not installed.
|
|
248
|
+
|
|
249
|
+
**Never raises**, and that is the contract, not an implementation detail:
|
|
250
|
+
`tan scaffold`'s job is to write a module, and it must still write one
|
|
251
|
+
when the `board.yaml` beside it is broken. `unavailable` is what the
|
|
252
|
+
generated line is FOR.
|
|
253
|
+
|
|
254
|
+
The broad `except` is deliberate rather than lazy. `UnicodeDecodeError` is
|
|
255
|
+
a `ValueError`, NOT an `OSError`, so an `except OSError` alone could never
|
|
256
|
+
catch it -- one undecodable byte in a customer's own board.yaml escaped
|
|
257
|
+
`tan model` as a traceback for exactly that reason (tan-cli#396). Nothing
|
|
258
|
+
upstream of here has validated this file.
|
|
259
|
+
|
|
260
|
+
PyYAML is a DECLARED BASE dependency (`python/pyproject.toml`'s
|
|
261
|
+
`dependencies = [... "pyyaml>=6" ...]`, which
|
|
262
|
+
`tests/gates/test_declared_dependencies.py` enforces) -- an earlier version
|
|
263
|
+
of this note claimed "tan ships no YAML dependency of its own", which is
|
|
264
|
+
simply false. The `except ImportError` is still right, for the reason
|
|
265
|
+
`tan.core.system_manifest._import_yaml` gives for its own copy: a frozen
|
|
266
|
+
`tan` built from a stale venv. It answers "nothing resolved" rather than a
|
|
267
|
+
coded envelope only because this is a comment line, not a manifest read.
|
|
268
|
+
"""
|
|
269
|
+
if not board_yaml_path:
|
|
270
|
+
return None
|
|
271
|
+
try:
|
|
272
|
+
import yaml # noqa: PLC0415 (optional at runtime, by design)
|
|
273
|
+
except ImportError:
|
|
274
|
+
return None
|
|
275
|
+
# `Path(...)` is built OUTSIDE the `try` on purpose. The backstop below is
|
|
276
|
+
# for a BROKEN FILE; a `board_yaml_path` that is not path-like at all is a
|
|
277
|
+
# caller bug, and swallowing its `TypeError` here would report "no board
|
|
278
|
+
# context" for a defect in `tan`'s own code. It is also what makes the
|
|
279
|
+
# `if not board_yaml_path` guard above load-bearing rather than decorative:
|
|
280
|
+
# measured, with the try widened to cover this line, deleting that guard
|
|
281
|
+
# left `test_no_path_at_all_is_unavailable` GREEN, because `Path(None)`'s
|
|
282
|
+
# `TypeError` landed on the file-failure arm and answered `None` by
|
|
283
|
+
# accident (tan-cli#1031's own mutation run, mutant G3).
|
|
284
|
+
path = Path(board_yaml_path)
|
|
285
|
+
try:
|
|
286
|
+
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
287
|
+
except Exception: # noqa: BLE001 -- the customer's hand-edited file, not ours
|
|
288
|
+
return None
|
|
289
|
+
return document if isinstance(document, dict) else None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _declared_os(board: dict[str, Any]) -> str | None:
|
|
293
|
+
"""The OS this `board.yaml` DECLARES, or `None` when it declares none.
|
|
294
|
+
|
|
295
|
+
Two shapes, because `board.yaml` has had two. A v1 document carries a
|
|
296
|
+
top-level `os:` -- the field the retired alp-sdk-vscode generator read
|
|
297
|
+
(`boardModel.os`) and the one that produced the
|
|
298
|
+
`// Board context: E1M-AEN801 / zephyr` line tan-cli#1031 asks for. A v2
|
|
299
|
+
document has no top-level `os` at all (alp-sdk's `board.schema.json`
|
|
300
|
+
declares no such property, and the extension's own `normalizeBoardModel`
|
|
301
|
+
deleted it on the way out: "v2: top-level os: has no meaning"); v2's home
|
|
302
|
+
for the field is `cores.<id>.os`, which the second arm reads.
|
|
303
|
+
|
|
304
|
+
Cores that DISAGREE collapse to `None` rather than picking one. A
|
|
305
|
+
heterogeneous project (`a55_cluster: yocto` + `m33_sm: zephyr`) has no
|
|
306
|
+
single OS, and a module scaffold is never told which core it is for
|
|
307
|
+
(`plan_module_files` receives the template definition and the name,
|
|
308
|
+
nothing else) -- so there is nothing to choose with.
|
|
309
|
+
|
|
310
|
+
`None` is also the honest answer for the common v2 project that declares
|
|
311
|
+
no `os:` anywhere; see the module docstring for why that is not resolved
|
|
312
|
+
from the SDK here.
|
|
313
|
+
"""
|
|
314
|
+
top = board.get("os")
|
|
315
|
+
if isinstance(top, str) and top not in _NOT_A_RUNTIME:
|
|
316
|
+
return top
|
|
317
|
+
cores = board.get("cores")
|
|
318
|
+
if not isinstance(cores, dict):
|
|
319
|
+
return None
|
|
320
|
+
declared = {
|
|
321
|
+
entry["os"]
|
|
322
|
+
for entry in cores.values()
|
|
323
|
+
if isinstance(entry, dict)
|
|
324
|
+
and isinstance(entry.get("os"), str)
|
|
325
|
+
and entry["os"] not in _NOT_A_RUNTIME
|
|
326
|
+
}
|
|
327
|
+
return declared.pop() if len(declared) == 1 else None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def read_board_context(board_yaml_path: str | None) -> str | None:
|
|
331
|
+
"""`"<som.sku> / <os>"` for the generated `// Board context:` line, or
|
|
332
|
+
`None` when there is no board context to report -- the caller then keeps
|
|
333
|
+
the `unavailable` placeholder that line has always carried, now meaning
|
|
334
|
+
"genuinely none" rather than "never looked" (tan-cli#1031).
|
|
335
|
+
|
|
336
|
+
**`som.sku` is the gate, not a field that may go [`UNSET`].** It is the one
|
|
337
|
+
value alp-sdk's `board.schema.json` requires at the top level (`required:
|
|
338
|
+
[som, cores]`, and `som.required: [sku]`), so a document without a usable
|
|
339
|
+
one is not a board -- reporting `<unset> / <unset>` for it would dress a
|
|
340
|
+
broken file up as a resolved board, and `project.boardYaml` would still
|
|
341
|
+
name the file for anyone who wants to look. Every other half may be
|
|
342
|
+
`<unset>`; see [`_declared_os`].
|
|
343
|
+
"""
|
|
344
|
+
board = _board_mapping(board_yaml_path)
|
|
345
|
+
if board is None:
|
|
346
|
+
return None
|
|
347
|
+
som = board.get("som")
|
|
348
|
+
sku = som.get("sku") if isinstance(som, dict) else None
|
|
349
|
+
if not isinstance(sku, str) or not sku:
|
|
350
|
+
return None
|
|
351
|
+
return f"{sku} / {_declared_os(board) or UNSET}"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Diagnostic data type + Rust-style renderer for the in-process board validator.
|
|
3
|
+
|
|
4
|
+
PORTED from alp-sdk `scripts/alp_cli/diagnostic.py` (pinned by
|
|
5
|
+
`tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES`).
|
|
6
|
+
|
|
7
|
+
One deliberate departure: the SDK file takes its colour from `colorama`
|
|
8
|
+
(`Fore.RED`, `Style.RESET_ALL`, ...), an undeclared optional dependency that
|
|
9
|
+
`colorama.init()` additionally wraps `sys.stderr` with, stripping every escape
|
|
10
|
+
when stderr is not a tty. tan declares neither, so the same three SGR
|
|
11
|
+
sequences are spelled out here. The in-process engine always renders with
|
|
12
|
+
`color=False` (`board_validator_run`): the bytes the SDK script's stderr carries
|
|
13
|
+
under a pipe, which is the only way tan ever ran it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
Severity = Literal["error", "warning", "note"]
|
|
26
|
+
|
|
27
|
+
# colorama's Fore.RED / Fore.YELLOW / Fore.CYAN / Style.RESET_ALL.
|
|
28
|
+
_RED = "\x1b[31m"
|
|
29
|
+
_YELLOW = "\x1b[33m"
|
|
30
|
+
_CYAN = "\x1b[36m"
|
|
31
|
+
_RESET = "\x1b[0m"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class Diagnostic:
|
|
36
|
+
severity: Severity
|
|
37
|
+
path: Path
|
|
38
|
+
line: int
|
|
39
|
+
col: int
|
|
40
|
+
span: int
|
|
41
|
+
code: str
|
|
42
|
+
message: str
|
|
43
|
+
hint: str | None = None
|
|
44
|
+
doc_url: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _doc_url(code: str) -> str:
|
|
48
|
+
base = os.environ.get("ALP_DIAG_BASE_URL", "docs/diagnostics")
|
|
49
|
+
return f"{base}/{code}.md"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _use_color(color: bool | None) -> bool:
|
|
53
|
+
if color is False:
|
|
54
|
+
return False
|
|
55
|
+
if color is True:
|
|
56
|
+
return True
|
|
57
|
+
if os.environ.get("NO_COLOR"):
|
|
58
|
+
return False
|
|
59
|
+
return sys.stdout.isatty()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def render(diag: Diagnostic, source_text: str, color: bool | None = None) -> str:
|
|
63
|
+
"""Render a diagnostic as a multi-line Rust-style block."""
|
|
64
|
+
use_color = _use_color(color)
|
|
65
|
+
|
|
66
|
+
def paint(s: str, hue: str) -> str:
|
|
67
|
+
return f"{hue}{s}{_RESET}" if use_color else s
|
|
68
|
+
|
|
69
|
+
sev_hue = {"error": _RED, "warning": _YELLOW, "note": _CYAN}[diag.severity]
|
|
70
|
+
header = f"{paint(f'{diag.severity}[{diag.code}]', sev_hue)}: {diag.message}"
|
|
71
|
+
arrow = f" --> {diag.path}:{diag.line}:{diag.col}"
|
|
72
|
+
|
|
73
|
+
lines = source_text.splitlines()
|
|
74
|
+
if 1 <= diag.line <= len(lines):
|
|
75
|
+
src_line = lines[diag.line - 1]
|
|
76
|
+
else:
|
|
77
|
+
src_line = ""
|
|
78
|
+
|
|
79
|
+
gutter_w = max(2, len(str(diag.line)))
|
|
80
|
+
blank_gutter = " " * gutter_w
|
|
81
|
+
src_block = (
|
|
82
|
+
f"{blank_gutter} |\n"
|
|
83
|
+
f"{str(diag.line).rjust(gutter_w)} | {src_line}\n"
|
|
84
|
+
f"{blank_gutter} | {' ' * (diag.col - 1)}{paint('^' * max(1, diag.span), sev_hue)}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
tail: list[str] = []
|
|
88
|
+
if diag.hint:
|
|
89
|
+
tail.append(f"{blank_gutter} = hint: {diag.hint}")
|
|
90
|
+
tail.append(f"{blank_gutter} = see: {diag.doc_url or _doc_url(diag.code)}")
|
|
91
|
+
|
|
92
|
+
return "\n".join([header, arrow, src_block, *tail, ""])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DiagnosticCollector:
|
|
96
|
+
"""Collect diagnostics across multiple validation passes."""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
self._items: list[Diagnostic] = []
|
|
100
|
+
|
|
101
|
+
def add(self, diag: Diagnostic) -> None:
|
|
102
|
+
self._items.append(diag)
|
|
103
|
+
|
|
104
|
+
def __iter__(self) -> Iterator[Diagnostic]:
|
|
105
|
+
return iter(self._items)
|
|
106
|
+
|
|
107
|
+
def __len__(self) -> int:
|
|
108
|
+
return len(self._items)
|
|
109
|
+
|
|
110
|
+
def has_errors(self) -> bool:
|
|
111
|
+
return any(d.severity == "error" for d in self._items)
|