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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""board.yaml SoC-capability pass (ALP-B010).
|
|
3
|
+
|
|
4
|
+
PORTED from alp-sdk `scripts/alp_cli/validator.py` (`_compat_pass` and its
|
|
5
|
+
SoC-capability helpers). Directories are passed in by the caller (the bound
|
|
6
|
+
SDK checkout's `metadata/`); nothing here derives a root from `__file__`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from tan.core.board_diagnostic import Diagnostic, DiagnosticCollector
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Local on purpose: tan has no importable equivalent. `tan.planner.*` cannot be
|
|
21
|
+
# imported before an SDK root is bound (and this module must load without one),
|
|
22
|
+
# and `som_metadata.resolve_soc_path` is the metadata-root-rooted resolver the
|
|
23
|
+
# SDK file's own comment says NOT to reuse here (this site roots at a
|
|
24
|
+
# caller-injected `soc_dir`, alp-sdk#1096).
|
|
25
|
+
def split_silicon_ref(silicon: str | None) -> tuple[str, str, str] | None:
|
|
26
|
+
"""Split a `vendor:family:part` `silicon:` key into its three slugs.
|
|
27
|
+
|
|
28
|
+
None when *silicon* is falsy or not exactly 3 colon-separated parts
|
|
29
|
+
(alp-sdk `alp_project_loader.split_silicon_ref`).
|
|
30
|
+
"""
|
|
31
|
+
if not silicon:
|
|
32
|
+
return None
|
|
33
|
+
parts = silicon.split(":")
|
|
34
|
+
if len(parts) != 3:
|
|
35
|
+
return None
|
|
36
|
+
return parts[0], parts[1], parts[2]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _compat_pass(
|
|
40
|
+
data: dict[str, Any],
|
|
41
|
+
path: Path,
|
|
42
|
+
collector: DiagnosticCollector,
|
|
43
|
+
*,
|
|
44
|
+
som_dir: Path,
|
|
45
|
+
soc_dir: Path,
|
|
46
|
+
) -> None:
|
|
47
|
+
som = data.get("som")
|
|
48
|
+
sku = som.get("sku") if isinstance(som, dict) else None
|
|
49
|
+
silicon_ref = _silicon_ref_for_sku(sku, som_dir=som_dir)
|
|
50
|
+
if silicon_ref is None:
|
|
51
|
+
return
|
|
52
|
+
soc_caps = _load_soc_caps(silicon_ref, soc_dir=soc_dir)
|
|
53
|
+
if soc_caps is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# #602: `cores:` failing schema validation (wrong type, e.g. a string
|
|
57
|
+
# or list instead of a mapping) is already reported as ALP-B004 --
|
|
58
|
+
# guard here too instead of crashing on `.items()`.
|
|
59
|
+
cores = data.get("cores")
|
|
60
|
+
cores = cores if isinstance(cores, dict) else {}
|
|
61
|
+
for core_name, core in cores.items():
|
|
62
|
+
if not isinstance(core, dict):
|
|
63
|
+
continue
|
|
64
|
+
peripherals = core.get("peripherals") or []
|
|
65
|
+
if not isinstance(peripherals, list):
|
|
66
|
+
continue
|
|
67
|
+
for idx, periph in enumerate(peripherals):
|
|
68
|
+
# The board.yaml schema defines peripherals as an array of
|
|
69
|
+
# strings (e.g. ["can", "i2c"]). The schema pass already
|
|
70
|
+
# rejects non-string entries, so skip anything unexpected.
|
|
71
|
+
if not isinstance(periph, str):
|
|
72
|
+
continue
|
|
73
|
+
kind = periph
|
|
74
|
+
if _soc_has_kind(soc_caps, kind):
|
|
75
|
+
continue
|
|
76
|
+
# Best-effort line: the sequence loader only attaches position
|
|
77
|
+
# metadata to dict items; strings carry no __line__. Fall back
|
|
78
|
+
# to the core block's opening position.
|
|
79
|
+
line = core.get("__line__", 1)
|
|
80
|
+
col = core.get("__column__", 1)
|
|
81
|
+
collector.add(
|
|
82
|
+
Diagnostic(
|
|
83
|
+
# warning, not error: SoC peripherals JSON ingestion is
|
|
84
|
+
# incomplete for some parts, and some peripheral
|
|
85
|
+
# categories surface board-side rather than directly on
|
|
86
|
+
# the SoC (emmc / flash / ethernet via I/O controllers).
|
|
87
|
+
# A false-positive ALP-B010 must not block the build —
|
|
88
|
+
# surface the discrepancy and let the customer decide.
|
|
89
|
+
severity="warning",
|
|
90
|
+
path=path,
|
|
91
|
+
line=line,
|
|
92
|
+
col=col,
|
|
93
|
+
span=len(kind),
|
|
94
|
+
code="ALP-B010",
|
|
95
|
+
message=(
|
|
96
|
+
f"core '{core_name}': peripheral kind '{kind}' is not "
|
|
97
|
+
f"listed on silicon '{silicon_ref}' (SoC JSON may be "
|
|
98
|
+
f"incomplete or the peripheral is board-side)"
|
|
99
|
+
),
|
|
100
|
+
hint=(
|
|
101
|
+
f"verify the SoC truly lacks {kind} before removing "
|
|
102
|
+
f"this entry; if the SoC has it but the metadata is "
|
|
103
|
+
f"stale, update metadata/socs/.../*.json"
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _silicon_ref_for_sku(sku: str | None, *, som_dir: Path) -> str | None:
|
|
110
|
+
if not sku:
|
|
111
|
+
return None
|
|
112
|
+
for som_path in som_dir.rglob(f"{sku}.yaml"):
|
|
113
|
+
text = som_path.read_text(encoding="utf-8")
|
|
114
|
+
doc = yaml.safe_load(text) or {}
|
|
115
|
+
ref = doc.get("silicon")
|
|
116
|
+
if isinstance(ref, str):
|
|
117
|
+
return ref
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _load_soc_caps(silicon_ref: str, *, soc_dir: Path) -> dict[str, int] | None:
|
|
122
|
+
# split_silicon_ref(), not resolve_soc_path(): this site roots at the
|
|
123
|
+
# caller-injected `soc_dir`, not at a metadata root. Rebuilding it as
|
|
124
|
+
# `resolve_soc_path(ref, soc_dir.parent)` happens to be exact today only
|
|
125
|
+
# because every caller passes `<root>/socs` -- it would silently resolve
|
|
126
|
+
# somewhere else the first time one didn't (#1096).
|
|
127
|
+
parts = split_silicon_ref(silicon_ref)
|
|
128
|
+
if parts is None:
|
|
129
|
+
return None
|
|
130
|
+
vendor, family, part = parts
|
|
131
|
+
fp = soc_dir / vendor / family / f"{part}.json"
|
|
132
|
+
if not fp.is_file():
|
|
133
|
+
return None
|
|
134
|
+
doc = json.loads(fp.read_text(encoding="utf-8"))
|
|
135
|
+
peripherals = doc.get("peripherals", {}) if isinstance(doc, dict) else {}
|
|
136
|
+
return peripherals if isinstance(peripherals, dict) else {}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Map user-facing board.yaml peripheral kind names to the SoC JSON key
|
|
140
|
+
# prefixes that represent the underlying silicon capability. Entries
|
|
141
|
+
# here are only needed when the user-facing name differs from the SoC
|
|
142
|
+
# JSON key (e.g. 'counter' is a Zephyr driver-model class that maps to
|
|
143
|
+
# the SoC's timer_* counters; 'sensor' is a driver-model class with no
|
|
144
|
+
# silicon peripheral equivalent so it is always considered present).
|
|
145
|
+
_PERIPHERAL_KIND_ALIASES: dict[str, tuple[str, ...]] = {
|
|
146
|
+
# Zephyr COUNTER driver class backed by hardware timers.
|
|
147
|
+
"counter": ("timer",),
|
|
148
|
+
# Zephyr PWM driver class backed by hardware timers / PWM units.
|
|
149
|
+
"pwm": ("timer", "pwm"),
|
|
150
|
+
# Zephyr SENSOR driver class: software abstraction over various
|
|
151
|
+
# I2C/SPI sensors; no dedicated silicon peripheral block required.
|
|
152
|
+
# Always allow it -- the I2C/SPI bus is the actual constraint.
|
|
153
|
+
"sensor": (), # empty tuple = unconditionally present
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _soc_has_kind(caps: dict[str, int], kind: str) -> bool:
|
|
158
|
+
"""Map a peripheral 'kind' onto SoC capability keys.
|
|
159
|
+
|
|
160
|
+
'kind' is the user-facing peripheral category (i2c, spi, can, ...).
|
|
161
|
+
The SoC JSON uses the same names plus _lp suffixes for low-power
|
|
162
|
+
variants; presence in EITHER counts. Some peripherals also have
|
|
163
|
+
variant suffixes (e.g. can_fd) -- if the base name matches a key
|
|
164
|
+
whose value > 0 that also counts.
|
|
165
|
+
|
|
166
|
+
High-level Zephyr driver classes (counter, pwm, sensor) that do not
|
|
167
|
+
map 1-to-1 to SoC JSON keys are resolved via ``_PERIPHERAL_KIND_ALIASES``.
|
|
168
|
+
"""
|
|
169
|
+
# Alias table: if the kind has a (possibly empty) alias list, check
|
|
170
|
+
# those SoC key prefixes instead. An empty alias list means the kind
|
|
171
|
+
# is always considered present (software-only abstraction).
|
|
172
|
+
if kind in _PERIPHERAL_KIND_ALIASES:
|
|
173
|
+
aliases = _PERIPHERAL_KIND_ALIASES[kind]
|
|
174
|
+
if not aliases:
|
|
175
|
+
return True # unconditionally present (e.g. 'sensor')
|
|
176
|
+
for alias_prefix in aliases:
|
|
177
|
+
direct_keys = (alias_prefix, f"{alias_prefix}_lp")
|
|
178
|
+
if any((caps.get(k, 0) or 0) > 0 for k in direct_keys):
|
|
179
|
+
return True
|
|
180
|
+
for key, count in caps.items():
|
|
181
|
+
if key == alias_prefix or key.startswith(f"{alias_prefix}_"):
|
|
182
|
+
if (count or 0) > 0:
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Direct and LP-suffixed match.
|
|
187
|
+
direct_keys = (kind, f"{kind}_lp")
|
|
188
|
+
if any((caps.get(k, 0) or 0) > 0 for k in direct_keys):
|
|
189
|
+
return True
|
|
190
|
+
# Variant-suffixed match: e.g. user writes 'can', SoC JSON has 'can_fd'.
|
|
191
|
+
for key, count in caps.items():
|
|
192
|
+
if key == kind or key.startswith(f"{kind}_"):
|
|
193
|
+
if (count or 0) > 0:
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`validate_board_yaml.py`, in-process: the default engine behind `tan validate`.
|
|
3
|
+
|
|
4
|
+
PORTED from alp-sdk `scripts/validate_board_yaml.py` (tan-cli#270), pinned by
|
|
5
|
+
`tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES`.
|
|
6
|
+
The function returns what that script's process would have produced -- an exit
|
|
7
|
+
status, a stdout and a stderr -- so `tan.commands.validate_cmd` feeds it through
|
|
8
|
+
the SAME `analyze_validator_output` it always fed a spawned script's output
|
|
9
|
+
through, and the exit-code map, the issue codes and the envelope shape do not
|
|
10
|
+
move. `tests/parity/test_board_validator_parity.py` runs both engines over a
|
|
11
|
+
fixture corpus and asserts the three values are identical.
|
|
12
|
+
|
|
13
|
+
Two stages, as in the script:
|
|
14
|
+
|
|
15
|
+
1. the rich diagnostic validator (`board_validator`), whose findings are
|
|
16
|
+
rendered to stderr; any error ends the run at status 1;
|
|
17
|
+
2. the orchestrator's `load_board_yaml` -- here `tan.planner.loader`, already
|
|
18
|
+
in-process for `tan build` -- for the hard cross-field checks and the three
|
|
19
|
+
hw_rev refusals, which keep the script's own exit statuses 3, 4 and 5.
|
|
20
|
+
|
|
21
|
+
`metadata/**` is read from the bound SDK checkout (ADR-0017); the script's
|
|
22
|
+
`--metadata-root` / `--no-presets` / `--no-color` flags are not ported because
|
|
23
|
+
`tan validate` never passed them (the SDK-side script keeps serving
|
|
24
|
+
west wrappers and MCP).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from tan.core.board_diagnostic import render
|
|
33
|
+
from tan.core.board_validator import PlannerFacts, validate_board_yaml
|
|
34
|
+
|
|
35
|
+
#: The script's own exit statuses (`validate_board_yaml.py`), unchanged.
|
|
36
|
+
EXIT_SDK_REVISION_UNSUPPORTED = 3
|
|
37
|
+
EXIT_SDK_REVISION_UNKNOWN = 4
|
|
38
|
+
EXIT_SDK_REVISION_NOT_BUILDABLE = 5
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ValidatorRun:
|
|
43
|
+
"""What the script's process would have left behind."""
|
|
44
|
+
|
|
45
|
+
status: int
|
|
46
|
+
stdout: str
|
|
47
|
+
stderr: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _traceback_stderr(exc: BaseException) -> str:
|
|
51
|
+
"""An uncaught exception, in the one shape `analyze_validator_output`
|
|
52
|
+
recognises as a crash (`Traceback ...` header, `Type: message` last line),
|
|
53
|
+
so a tan-side fault is never read as a verdict about the customer's board."""
|
|
54
|
+
return (
|
|
55
|
+
"Traceback (most recent call last):\n"
|
|
56
|
+
f"{type(exc).__name__}: {exc}\n"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_board_validator(board_path: str, sdk_root: Path) -> ValidatorRun:
|
|
61
|
+
"""Validate *board_path* against the metadata of the SDK at *sdk_root*."""
|
|
62
|
+
try:
|
|
63
|
+
return _run(board_path, Path(sdk_root))
|
|
64
|
+
except Exception as exc: # the script's uncaught-exception exit 1
|
|
65
|
+
return ValidatorRun(1, "", _traceback_stderr(exc))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run(board_path: str, sdk_root: Path) -> ValidatorRun:
|
|
69
|
+
path = Path(board_path)
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
return ValidatorRun(1, "", f"FAIL {path}: file not found\n")
|
|
72
|
+
|
|
73
|
+
# Read first, as the script does: an undecodable board is its uncaught
|
|
74
|
+
# exception, and must not bind the planner to a root on the way.
|
|
75
|
+
source_text = path.read_text(encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
# `tan.planner` freezes its metadata paths at import, so the SDK root is
|
|
78
|
+
# bound BEFORE anything below imports it (see `tan.planner_root`).
|
|
79
|
+
from tan.planner_root import bind_sdk_root
|
|
80
|
+
|
|
81
|
+
bind_sdk_root(sdk_root)
|
|
82
|
+
from tan.planner.camera_owner import plan_cameras, resolve_cores
|
|
83
|
+
from tan.planner.slugs import _BLOCK_SLUGS
|
|
84
|
+
|
|
85
|
+
collector = validate_board_yaml(
|
|
86
|
+
path,
|
|
87
|
+
metadata_root=sdk_root / "metadata",
|
|
88
|
+
facts=PlannerFacts(
|
|
89
|
+
repo_root=sdk_root,
|
|
90
|
+
block_slugs=_BLOCK_SLUGS,
|
|
91
|
+
plan_cameras=plan_cameras,
|
|
92
|
+
resolve_cores=resolve_cores,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
stderr = "".join(
|
|
96
|
+
render(diag, source_text=source_text, color=False) + "\n"
|
|
97
|
+
for diag in collector
|
|
98
|
+
)
|
|
99
|
+
if collector.has_errors():
|
|
100
|
+
return ValidatorRun(1, "", stderr)
|
|
101
|
+
|
|
102
|
+
from tan.planner import (
|
|
103
|
+
OrchestratorError,
|
|
104
|
+
SdkRevisionNotBuildable,
|
|
105
|
+
SdkRevisionUnknown,
|
|
106
|
+
SdkRevisionUnsupported,
|
|
107
|
+
load_board_yaml,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
load_board_yaml(path)
|
|
112
|
+
except SdkRevisionUnknown as exc:
|
|
113
|
+
return ValidatorRun(
|
|
114
|
+
EXIT_SDK_REVISION_UNKNOWN, "", f"{stderr}FAIL sdk-compat: {exc}\n"
|
|
115
|
+
)
|
|
116
|
+
except SdkRevisionNotBuildable as exc:
|
|
117
|
+
return ValidatorRun(
|
|
118
|
+
EXIT_SDK_REVISION_NOT_BUILDABLE, "", f"{stderr}FAIL sdk-compat: {exc}\n"
|
|
119
|
+
)
|
|
120
|
+
except SdkRevisionUnsupported as exc:
|
|
121
|
+
return ValidatorRun(
|
|
122
|
+
EXIT_SDK_REVISION_UNSUPPORTED, "", f"{stderr}FAIL sdk-compat: {exc}\n"
|
|
123
|
+
)
|
|
124
|
+
except OrchestratorError as exc:
|
|
125
|
+
return ValidatorRun(1, "", f"{stderr}FAIL consistency: {exc}\n")
|
|
126
|
+
|
|
127
|
+
warnings = sum(1 for diag in collector if diag.severity == "warning")
|
|
128
|
+
verdict = f"{path}: clean ({warnings} warning(s))" if warnings else f"{path}: clean"
|
|
129
|
+
return ValidatorRun(0, verdict + "\n", stderr)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""board.yaml JSON-Schema pass -> rich diagnostics (ALP-B001..B004, B099).
|
|
3
|
+
|
|
4
|
+
PORTED from alp-sdk `scripts/alp_cli/validator.py` (`_schema_pass` and its
|
|
5
|
+
helpers). The schema file is read from the BOUND SDK checkout's
|
|
6
|
+
`metadata/schemas/` (ADR-0017: facts stay in alp-sdk) -- the caller passes the
|
|
7
|
+
path; nothing here derives a root from `__file__`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re as _re
|
|
14
|
+
from difflib import get_close_matches
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import jsonschema
|
|
19
|
+
|
|
20
|
+
from tan.core.board_diagnostic import Diagnostic, DiagnosticCollector
|
|
21
|
+
from tan.core.board_yaml_pos import node_position
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_board_schema(schema_path: Path) -> dict[str, Any]:
|
|
25
|
+
"""Load board.schema.json from *schema_path*."""
|
|
26
|
+
return json.loads(schema_path.read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def iter_schema_errors(
|
|
30
|
+
data: dict[str, Any], schema_path: Path
|
|
31
|
+
) -> list[jsonschema.ValidationError]:
|
|
32
|
+
"""Validate *data* against board.schema.json; errors sorted by path.
|
|
33
|
+
|
|
34
|
+
One schema file, one draft dialect (2020-12, matching the schema's own
|
|
35
|
+
`$schema` declaration), one error ordering.
|
|
36
|
+
"""
|
|
37
|
+
validator = jsonschema.Draft202012Validator(load_board_schema(schema_path))
|
|
38
|
+
# Stringify path parts: absolute_path mixes ints (array indices) and
|
|
39
|
+
# strs (keys); a raw list comparison would TypeError across siblings.
|
|
40
|
+
return sorted(validator.iter_errors(data),
|
|
41
|
+
key=lambda e: [str(p) for p in e.absolute_path])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _schema_pass(
|
|
45
|
+
data: dict[str, Any],
|
|
46
|
+
path: Path,
|
|
47
|
+
collector: DiagnosticCollector,
|
|
48
|
+
*,
|
|
49
|
+
schema_path: Path,
|
|
50
|
+
) -> None:
|
|
51
|
+
# Strip __pos__ keys before handing to jsonschema (they're not in the schema).
|
|
52
|
+
clean = _strip_pos(data)
|
|
53
|
+
for err in iter_schema_errors(clean, schema_path):
|
|
54
|
+
diag = _schema_error_to_diagnostic(err, data, path)
|
|
55
|
+
if diag is not None:
|
|
56
|
+
collector.add(diag)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _strip_pos(value: Any) -> Any:
|
|
60
|
+
if isinstance(value, dict):
|
|
61
|
+
return {
|
|
62
|
+
k: _strip_pos(v)
|
|
63
|
+
for k, v in value.items()
|
|
64
|
+
if not (isinstance(k, str) and k.startswith("__"))
|
|
65
|
+
}
|
|
66
|
+
if isinstance(value, list):
|
|
67
|
+
return [_strip_pos(v) for v in value]
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _walk(data: dict[str, Any], path_seq: list[Any]) -> dict[str, Any] | None:
|
|
72
|
+
"""Walk a path through the position-augmented document."""
|
|
73
|
+
cursor: Any = data
|
|
74
|
+
for step in path_seq:
|
|
75
|
+
if isinstance(cursor, dict) and step in cursor:
|
|
76
|
+
cursor = cursor[step]
|
|
77
|
+
elif isinstance(cursor, list) and isinstance(step, int) and step < len(cursor):
|
|
78
|
+
cursor = cursor[step]
|
|
79
|
+
else:
|
|
80
|
+
return None
|
|
81
|
+
return cursor if isinstance(cursor, dict) else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _schema_error_to_diagnostic(
|
|
85
|
+
err: jsonschema.ValidationError, data: dict[str, Any], path: Path
|
|
86
|
+
) -> Diagnostic | None:
|
|
87
|
+
abs_path = list(err.absolute_path)
|
|
88
|
+
parent = _walk(data, abs_path[:-1]) if abs_path else data
|
|
89
|
+
line = parent.get("__line__", 1) if parent else 1
|
|
90
|
+
col = parent.get("__column__", 1) if parent else 1
|
|
91
|
+
span = 1
|
|
92
|
+
|
|
93
|
+
if err.validator == "required":
|
|
94
|
+
missing = err.message.split("'")[1] if "'" in err.message else "?"
|
|
95
|
+
return Diagnostic(
|
|
96
|
+
severity="error",
|
|
97
|
+
path=path,
|
|
98
|
+
line=line,
|
|
99
|
+
col=col,
|
|
100
|
+
span=span,
|
|
101
|
+
code="ALP-B001",
|
|
102
|
+
message=f"required key '{missing}' is missing",
|
|
103
|
+
hint=f"add a '{missing}:' entry to this block",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if err.validator == "additionalProperties":
|
|
107
|
+
# jsonschema reports additionalProperties errors AT the object that
|
|
108
|
+
# rejected the extra key: `abs_path` is that object's OWN path, not
|
|
109
|
+
# the offending key's -- so the offending key is only ever
|
|
110
|
+
# recoverable from the message text, never from `abs_path[-1]`.
|
|
111
|
+
# (Nested case: a `server:` block with `additionalProperties:
|
|
112
|
+
# false` rejecting `tls_ca_bundle` reports `abs_path ==
|
|
113
|
+
# ["ota", "server"]`; `abs_path[-1]` is `"server"`, the block
|
|
114
|
+
# itself, not the unknown key -- which used to surface as the
|
|
115
|
+
# misleading "unknown key 'server'".)
|
|
116
|
+
_m = _re.search(r"'([^']+)'", err.message)
|
|
117
|
+
bad_key = _m.group(1) if _m else "?"
|
|
118
|
+
offending = _walk(data, abs_path) if abs_path else data
|
|
119
|
+
if offending and "__keys__" in offending and bad_key in offending["__keys__"]:
|
|
120
|
+
parent = offending
|
|
121
|
+
line, col = node_position(parent, bad_key, target="key")
|
|
122
|
+
span = len(str(bad_key))
|
|
123
|
+
allowed = list(err.schema.get("properties", {}).keys())
|
|
124
|
+
|
|
125
|
+
suggestion = get_close_matches(str(bad_key), allowed, n=1)
|
|
126
|
+
hint = f"did you mean '{suggestion[0]}'?" if suggestion else None
|
|
127
|
+
return Diagnostic(
|
|
128
|
+
severity="error",
|
|
129
|
+
path=path,
|
|
130
|
+
line=line,
|
|
131
|
+
col=col,
|
|
132
|
+
span=span,
|
|
133
|
+
code="ALP-B002",
|
|
134
|
+
message=f"unknown key '{bad_key}'",
|
|
135
|
+
hint=hint,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if err.validator in {"enum", "pattern"}:
|
|
139
|
+
if abs_path and parent and "__keys__" in parent:
|
|
140
|
+
key = abs_path[-1]
|
|
141
|
+
if key in parent["__keys__"]:
|
|
142
|
+
line, col = node_position(parent, key, target="value")
|
|
143
|
+
span = max(1, len(str(parent.get(key, ""))))
|
|
144
|
+
if err.validator == "enum":
|
|
145
|
+
allowed = err.schema.get("enum", [])
|
|
146
|
+
hint = f"expected one of: {', '.join(map(repr, allowed))}"
|
|
147
|
+
else:
|
|
148
|
+
hint = f"value must match pattern: {err.schema.get('pattern')}"
|
|
149
|
+
return Diagnostic(
|
|
150
|
+
severity="error",
|
|
151
|
+
path=path,
|
|
152
|
+
line=line,
|
|
153
|
+
col=col,
|
|
154
|
+
span=span,
|
|
155
|
+
code="ALP-B003",
|
|
156
|
+
message=err.message,
|
|
157
|
+
hint=hint,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if err.validator == "type":
|
|
161
|
+
if abs_path and parent and "__keys__" in parent:
|
|
162
|
+
key = abs_path[-1]
|
|
163
|
+
if key in parent["__keys__"]:
|
|
164
|
+
line, col = node_position(parent, key, target="value")
|
|
165
|
+
return Diagnostic(
|
|
166
|
+
severity="error",
|
|
167
|
+
path=path,
|
|
168
|
+
line=line,
|
|
169
|
+
col=col,
|
|
170
|
+
span=1,
|
|
171
|
+
code="ALP-B004",
|
|
172
|
+
message=err.message,
|
|
173
|
+
hint=f"expected type: {err.schema.get('type')}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Fallback for any validator we haven't mapped yet.
|
|
177
|
+
return Diagnostic(
|
|
178
|
+
severity="error",
|
|
179
|
+
path=path,
|
|
180
|
+
line=line,
|
|
181
|
+
col=col,
|
|
182
|
+
span=1,
|
|
183
|
+
code="ALP-B099",
|
|
184
|
+
message=err.message,
|
|
185
|
+
hint=None,
|
|
186
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Is the bound alp-sdk checkout's validator newer than the one tan ported?
|
|
3
|
+
|
|
4
|
+
`tan validate` runs a PORT of the SDK's board.yaml validator in-process, so a
|
|
5
|
+
checkout whose validator sources moved past the audited commit may carry rules
|
|
6
|
+
tan does not apply (a clean verdict that the SDK's own script would reject).
|
|
7
|
+
The four sources the port was cut from are hashed here; a differing hash in the
|
|
8
|
+
bound checkout is reported as a non-fatal warning. The hashes are kept equal to
|
|
9
|
+
`tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES` by
|
|
10
|
+
`tests/gates/test_board_validator_skew_pins.py`, so a re-port moves both.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
#: alp-sdk-relative source -> sha256 at the commit the port was audited against.
|
|
19
|
+
PORTED_SOURCE_HASHES: dict[str, str] = {
|
|
20
|
+
"scripts/alp_cli/validator.py": "ce44ba907c411f763e39b07c168f959dd4455da20d3ffba9f8c28eaa5a1f810d",
|
|
21
|
+
"scripts/alp_cli/diagnostic.py": "6a96eb120d73640196cefbd30d13e40a515d00d11368e440123594189cd16d96",
|
|
22
|
+
"scripts/alp_cli/yaml_pos.py": "c83580d86a8f2bc575e20b44f4893847a6a08f765774861f2d29a8bd61ae5b74",
|
|
23
|
+
"scripts/validate_board_yaml.py": "91e06880f068447f98a2c68f1ffb4b6da29c18d2fcadfa0232d982e6612557d4",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _digest(path: Path) -> str:
|
|
28
|
+
# CRLF -> LF so a Windows autocrlf checkout of identical sources is not "newer".
|
|
29
|
+
return hashlib.sha256(path.read_bytes().replace(b"\r\n", b"\n")).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def skewed_sources(sdk_root: Path) -> tuple[str, ...]:
|
|
33
|
+
"""SDK-relative validator sources whose bytes differ from the audited ones.
|
|
34
|
+
|
|
35
|
+
A source absent from the checkout (or unreadable) is skipped: an SDK that
|
|
36
|
+
dropped the script has no newer rules for tan to miss.
|
|
37
|
+
"""
|
|
38
|
+
skewed: list[str] = []
|
|
39
|
+
for rel, pinned in PORTED_SOURCE_HASHES.items():
|
|
40
|
+
path = Path(sdk_root, *rel.split("/"))
|
|
41
|
+
try:
|
|
42
|
+
if path.is_file() and _digest(path) != pinned:
|
|
43
|
+
skewed.append(rel)
|
|
44
|
+
except OSError:
|
|
45
|
+
continue
|
|
46
|
+
return tuple(skewed)
|