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/link_refusal.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`diagnostics.link: itcm` (tan-cli#1350) -- the pieces every planner consumer
|
|
3
|
+
and the flash path share without importing the planner.
|
|
4
|
+
|
|
5
|
+
The planner raises `LinkTargetError` (`tan/planner/link_target.py`), which
|
|
6
|
+
carries a `build.link-itcm-*` issue code. The planner needs a bound SDK root to
|
|
7
|
+
import, so a consumer that catches a planner failure recognises the refusal by
|
|
8
|
+
that code (duck-typed), never by class: `refusal_code(err)`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
#: Both refusal codes start with this; see `contract/issue-codes.json`.
|
|
14
|
+
CODE_PREFIX = "build.link-itcm-"
|
|
15
|
+
|
|
16
|
+
#: The `flash_method` the manifest carries for an ITCM-linked slice. NOT a
|
|
17
|
+
#: registered backend on purpose: the image is linked at 0x0 and must never be
|
|
18
|
+
#: signed or written to MRAM; `tan flash` refuses it and points at
|
|
19
|
+
#: `tan flash --ram`.
|
|
20
|
+
RAM_RUN_ONLY_METHOD = "ram_run_only"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def refusal_code(err: BaseException) -> str | None:
|
|
24
|
+
"""The `build.link-itcm-*` code `err` carries, else None."""
|
|
25
|
+
code = getattr(err, "code", None)
|
|
26
|
+
if isinstance(code, str) and code.startswith(CODE_PREFIX):
|
|
27
|
+
return code
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def split_coded_message(message: str) -> tuple[str | None, str]:
|
|
32
|
+
"""Inverse of the `"<code>: <text>"` form a string-returning consumer uses."""
|
|
33
|
+
if message.startswith(CODE_PREFIX):
|
|
34
|
+
code, _, text = message.partition(": ")
|
|
35
|
+
return code, text
|
|
36
|
+
return None, message
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
RAM_ONLY_PROJECT_CODE = "flash.ram-run-only-project"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ram_run_only_project_refusal(
|
|
43
|
+
slices: "list[tuple[str, str | None]]",
|
|
44
|
+
core: str | None,
|
|
45
|
+
helper: str | None,
|
|
46
|
+
*,
|
|
47
|
+
ram: bool = False,
|
|
48
|
+
) -> str | None:
|
|
49
|
+
"""Fail-closed rule for a project marked RAM-only (`diagnostics.link: itcm`).
|
|
50
|
+
|
|
51
|
+
`slices` is `(core_id, flash_method)` for every manifest slice. A plain
|
|
52
|
+
`tan flash` (not `--ram`) on a manifest holding any `ram_run_only` slice is
|
|
53
|
+
refused as a WHOLE run -- the project's other slices (e.g. the default
|
|
54
|
+
`m55_hp` stock shim, still planned for an MRAM write) must not be flashed
|
|
55
|
+
in a partial run -- UNLESS the operator explicitly selected only non-ram
|
|
56
|
+
slices (`--core <non-ram core>`) or only a helper (`--helper`). Returns the
|
|
57
|
+
refusal message, else None. `--ram` is never affected.
|
|
58
|
+
"""
|
|
59
|
+
if ram:
|
|
60
|
+
return None
|
|
61
|
+
ram_cores = sorted(c for c, m in slices if m == RAM_RUN_ONLY_METHOD)
|
|
62
|
+
if not ram_cores:
|
|
63
|
+
return None
|
|
64
|
+
if helper is not None:
|
|
65
|
+
return None
|
|
66
|
+
if core is not None and core not in ram_cores:
|
|
67
|
+
return None
|
|
68
|
+
first = ram_cores[0]
|
|
69
|
+
return (
|
|
70
|
+
f"flash: this project is marked RAM-only (board.yaml `diagnostics.link: "
|
|
71
|
+
f"itcm`; slice {', '.join(ram_cores)} is `ram_run_only`) -- refusing the "
|
|
72
|
+
f"whole run before any write, so a RAM-only project is never partly "
|
|
73
|
+
f"written to MRAM. RAM-run it with `tan flash --ram --core {first}`; to "
|
|
74
|
+
f"flash only another slice, name it explicitly with `--core <id>`; or "
|
|
75
|
+
f"remove `diagnostics.link` and rebuild."
|
|
76
|
+
)
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The ONE place `soc-spec-v1.schema.json` / `som-preset-v2.schema.json`
|
|
3
|
+
validation runs -- tan-cli#964.
|
|
4
|
+
|
|
5
|
+
Before this module, `_schema_errors` (`tan.commands.new_som_cmd`) ran
|
|
6
|
+
`jsonschema.Draft202012Validator` against a generated SoC-spec/SoM-preset
|
|
7
|
+
skeleton on the `tan new-som` WRITE path only. Every READ consumer --
|
|
8
|
+
`tan build`, `tan generate`, `tan presets`, `tan size`, `tan debug-config`,
|
|
9
|
+
`tan bootstrap` -- trusted `metadata/socs/**` / `metadata/e1m_modules/**`
|
|
10
|
+
unvalidated, so a schema-invalid field (`cores[].type` a number instead of a
|
|
11
|
+
string, most often) reached an `isinstance` guard downstream instead of a
|
|
12
|
+
coded, readable message. Five rounds of guards across #957/#962/#965/#969
|
|
13
|
+
are the measured cost of validating at the point of USE instead of the point
|
|
14
|
+
of READ: `presets_cmd.core_type_lookup`, `core/os_class.py`,
|
|
15
|
+
`planner/topology.py`, and twice in `planner/kconfig.py` each independently
|
|
16
|
+
rediscovered the same missing gate.
|
|
17
|
+
|
|
18
|
+
tan-cli#964's decided rule (posted as a comment on the issue, not re-derived
|
|
19
|
+
here): **validate on every read, always report, refuse only when the invalid
|
|
20
|
+
field feeds something the run puts on disk or on silicon.** This module does
|
|
21
|
+
the FIRST two verbs -- it validates and it always returns every violation as
|
|
22
|
+
a message that names the file. It does NOT decide refuse vs. warn: that is
|
|
23
|
+
each caller's call, made by what it DOES with the returned list (raise an
|
|
24
|
+
`OrchestratorError` and refuse, or fold the messages into the envelope's
|
|
25
|
+
`issues[]` as warnings and continue). Putting the validator in one place and
|
|
26
|
+
leaving the policy to the caller is what a "per-consumer guard" is not --
|
|
27
|
+
the five guards above were each written at the point a bad value was ABOUT
|
|
28
|
+
TO BE USED, independently rediscovering the same missing check; this module
|
|
29
|
+
is written at the point the document is READ, once, so nothing downstream of
|
|
30
|
+
a successful validation call needs to guard the field's TYPE again (the
|
|
31
|
+
`isinstance` guards stay anyway -- tan-cli#965 proved schema-valid can still
|
|
32
|
+
crash a consumer whose contract is narrower than the schema; see
|
|
33
|
+
`targets.py`'s `npu['mac_per_cycle']`, filed separately).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import functools
|
|
39
|
+
import json
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def soc_spec_schema_path(metadata_root: Path | str) -> Path:
|
|
45
|
+
"""`<sdk_root>/metadata/schemas/soc-spec-v1.schema.json`.
|
|
46
|
+
|
|
47
|
+
Takes the METADATA directory (`<sdk_root>/metadata`), not the SDK root --
|
|
48
|
+
every read-path caller (`planner.loader`, `presets_cmd`, `size_cmd`)
|
|
49
|
+
already has that path bound under a name like `metadata_root` or `root`;
|
|
50
|
+
`new_som_cmd`'s own `_soc_schema_path`/`_som_schema_path` take the SDK
|
|
51
|
+
root instead (their write-path callers have THAT bound) and are left as
|
|
52
|
+
they are rather than reconciled onto this convention -- two path helpers
|
|
53
|
+
that each match their own caller's already-bound variable is simpler
|
|
54
|
+
than a `sdk_root` vs. `metadata_root` footgun on one shared name.
|
|
55
|
+
"""
|
|
56
|
+
return Path(metadata_root) / "schemas" / "soc-spec-v1.schema.json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def som_preset_schema_path(metadata_root: Path | str) -> Path:
|
|
60
|
+
"""`<sdk_root>/metadata/schemas/som-preset-v2.schema.json`. See
|
|
61
|
+
`soc_spec_schema_path` for the `metadata_root` convention."""
|
|
62
|
+
return Path(metadata_root) / "schemas" / "som-preset-v2.schema.json"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def npu_ops_schema_path(metadata_root: Path | str) -> Path:
|
|
66
|
+
"""`<sdk_root>/metadata/schemas/npu-ops-v1.schema.json` (tan-cli#1298).
|
|
67
|
+
See `soc_spec_schema_path` for the `metadata_root` convention. Present on
|
|
68
|
+
alp-sdk `dev`; absent from SDK releases that predate it."""
|
|
69
|
+
return Path(metadata_root) / "schemas" / "npu-ops-v1.schema.json"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _posix(value: object) -> str:
|
|
73
|
+
"""`to_posix`: backslashes to forward slashes, nothing else.
|
|
74
|
+
|
|
75
|
+
Mirrors `build_output.to_posix` / `presets_cmd._posix` / `sdk_discovery._to_posix`
|
|
76
|
+
-- every other module that puts a filesystem path into an envelope field
|
|
77
|
+
or message keeps its own copy of this one-liner rather than importing
|
|
78
|
+
`tan.commands.build_output` from `tan.core` (which would be the pure-logic
|
|
79
|
+
layer reaching back into the command/IO layer). A schema-violation message
|
|
80
|
+
is exactly such a field: `f"{source}: ..."` on Windows renders
|
|
81
|
+
`sdk\\metadata\\socs\\...` unless the value is normalised first, and the
|
|
82
|
+
envelope convention (`build_output.py:34-37`) is forward slashes, always.
|
|
83
|
+
"""
|
|
84
|
+
return str(value).replace("\\", "/")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def schema_errors(doc: object, schema_path: Path, *, source: Path | str | None = None) -> list[str]:
|
|
88
|
+
"""Validate *doc* against the schema at *schema_path*; return one
|
|
89
|
+
formatted string per violation, sorted by JSON-pointer path, or `[]`
|
|
90
|
+
when it validates clean.
|
|
91
|
+
|
|
92
|
+
Moved here from `new_som_cmd._schema_errors` verbatim (tan-cli#964) --
|
|
93
|
+
that function is now a thin alias for this one, so there is exactly one
|
|
94
|
+
`jsonschema.Draft202012Validator(...)` call for SoC/SoM documents left in
|
|
95
|
+
the package (tan-cli#964 review, minor 7: `planner/loader.py` carries a
|
|
96
|
+
SEPARATE one for `board.yaml` against `BOARD_SCHEMA`, a different
|
|
97
|
+
document against a different schema this module was never the gap for --
|
|
98
|
+
"exactly one call in the package" overstated it), not two that could
|
|
99
|
+
drift on the SoC/SoM message format. `new_som_cmd`'s own two call
|
|
100
|
+
sites (the generated-skeleton self-check, run before anything is written)
|
|
101
|
+
pass `source=None` and get the EXACT original message shape back
|
|
102
|
+
(`f"{pointer}: {message}"`) -- unchanged, because that self-check
|
|
103
|
+
describes an in-memory document with no file of its own yet.
|
|
104
|
+
|
|
105
|
+
*source*, when given, is the file *doc* was read from, and PREFIXES every
|
|
106
|
+
message (`f"{source}: {pointer}: {message}"`) -- tan-cli#964's CX
|
|
107
|
+
requirement is that a violation name the file, the JSON pointer, and what
|
|
108
|
+
was found, all in one line, with no second lookup to learn which file
|
|
109
|
+
produced it. Every read-path caller (there is more than one file open at
|
|
110
|
+
once in `tan build`'s topology walk) passes a real `source`; only the
|
|
111
|
+
write-path self-check above passes `None`.
|
|
112
|
+
|
|
113
|
+
Raises whatever reading/parsing *schema_path* raises (`OSError`,
|
|
114
|
+
`UnicodeDecodeError`, `json.JSONDecodeError`) -- unchanged from the
|
|
115
|
+
original `_schema_errors`, and exactly what `new_som_cmd`'s two call
|
|
116
|
+
sites already catch to report a *schema* problem (`could not read
|
|
117
|
+
<path>`) distinctly from a *document* problem (`_internal_error`).
|
|
118
|
+
Read-path callers that want a schema-file failure folded into the same
|
|
119
|
+
message list instead of an exception use `validate_document` below.
|
|
120
|
+
"""
|
|
121
|
+
# tan-cli#810: the only `jsonschema` call in the package (well -- the only
|
|
122
|
+
# one outside `planner/loader.py`'s SEPARATE `board.yaml`-schema pass,
|
|
123
|
+
# which validates a different document against a different schema and
|
|
124
|
+
# was never the gap #964 closes), and it drags in `attr`, `referencing`
|
|
125
|
+
# and `jsonschema_specifications` behind it -- all of which every `tan`
|
|
126
|
+
# invocation used to load for this one line. Deferred here, same as the
|
|
127
|
+
# original. Validating on every READ (not just `new-som`'s write) makes
|
|
128
|
+
# this import land on more invocations than before -- `tan presets`/`tan
|
|
129
|
+
# size`/`tan build`/`tan generate` now pay it too -- but it stays deferred
|
|
130
|
+
# to the point of use rather than promoted to module scope, so a `tan`
|
|
131
|
+
# invocation that never reaches a SoC/SoM read (`tan --version`, `tan
|
|
132
|
+
# doctor` before any metadata walk, ...) still does not pay for it.
|
|
133
|
+
validator = _cached_validator(Path(schema_path))
|
|
134
|
+
prefix = f"{_posix(source)}: " if source is not None else ""
|
|
135
|
+
return [
|
|
136
|
+
f"{prefix}{'/'.join(str(p) for p in err.absolute_path) or '<root>'}: {err.message}"
|
|
137
|
+
for err in sorted(validator.iter_errors(doc), key=lambda e: list(e.absolute_path))
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@functools.lru_cache(maxsize=64)
|
|
142
|
+
def _cached_validator_by_stat(schema_path: str, mtime_ns: int, size: int) -> Any:
|
|
143
|
+
"""Build + cache one `jsonschema.Draft202012Validator` for *schema_path*,
|
|
144
|
+
keyed on its own `(mtime_ns, size)` -- tan-cli#964 review, minor 8.
|
|
145
|
+
|
|
146
|
+
Every `validate_document`/`schema_errors` call used to re-read and
|
|
147
|
+
re-`json.loads` the schema file and re-construct its validator from
|
|
148
|
+
scratch, EVERY time -- measured, this makes revalidation scale linearly
|
|
149
|
+
with the number of documents read against one schema: +0.08s at 11 SoMs,
|
|
150
|
+
+0.46s at 111 (the PR's own "within run-to-run noise" claim did not
|
|
151
|
+
reproduce). `(mtime_ns, size)` -- not just the path -- is the cache key so
|
|
152
|
+
a schema file edited mid-process (a `tan new-som` run immediately
|
|
153
|
+
followed by a `tan presets` in the same interpreter, or a test that
|
|
154
|
+
overwrites a fixture file between two calls in the SAME process) still
|
|
155
|
+
gets a fresh validator rather than a stale cached one; `lru_cache` does
|
|
156
|
+
not cache the exception a corrupt/unreadable file raises, so
|
|
157
|
+
`schema_errors`'s own "raises on unreadable schema" contract is
|
|
158
|
+
unaffected -- a failing call is retried, never permanently poisoned.
|
|
159
|
+
"""
|
|
160
|
+
import jsonschema # noqa: PLC0415
|
|
161
|
+
|
|
162
|
+
schema = json.loads(Path(schema_path).read_text(encoding="utf-8"))
|
|
163
|
+
return jsonschema.Draft202012Validator(schema)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _cached_validator(schema_path: Path) -> Any:
|
|
167
|
+
"""`_cached_validator_by_stat`, keyed off *schema_path*'s CURRENT
|
|
168
|
+
`os.stat()` -- the indirection `schema_errors` calls, so the `stat()`
|
|
169
|
+
itself (cheap, but a real syscall) still runs every call while the
|
|
170
|
+
expensive `read_text` + `json.loads` + `Draft202012Validator(...)`
|
|
171
|
+
construction only runs once per distinct `(path, mtime, size)`."""
|
|
172
|
+
stat = Path(schema_path).stat()
|
|
173
|
+
return _cached_validator_by_stat(str(schema_path), stat.st_mtime_ns, stat.st_size)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@functools.lru_cache(maxsize=64)
|
|
177
|
+
def _cached_schema_problem_by_stat(schema_path: str, mtime_ns: int, size: int) -> str | None:
|
|
178
|
+
"""What is wrong with the schema itself, or `None` when it is a valid
|
|
179
|
+
JSON Schema -- cached on the same `(mtime_ns, size)` key as the validator,
|
|
180
|
+
so the metaschema check runs once per schema file, not once per document.
|
|
181
|
+
|
|
182
|
+
`check_schema` is what turns `{"type": 5}` into a `SchemaError` with a
|
|
183
|
+
readable message; without it the same file reaches `iter_errors` and
|
|
184
|
+
escapes as a raw `TypeError` (tan-cli#1303)."""
|
|
185
|
+
import jsonschema # noqa: PLC0415
|
|
186
|
+
|
|
187
|
+
validator = _cached_validator_by_stat(schema_path, mtime_ns, size)
|
|
188
|
+
try:
|
|
189
|
+
jsonschema.Draft202012Validator.check_schema(validator.schema)
|
|
190
|
+
except jsonschema.exceptions.SchemaError as exc:
|
|
191
|
+
return f"not a valid JSON Schema: {exc.message}"
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _schema_problem(schema_path: Path) -> str | None:
|
|
196
|
+
"""`_cached_schema_problem_by_stat` keyed off *schema_path*'s current
|
|
197
|
+
`stat()`. Raises exactly what `_cached_validator` raises for a file that
|
|
198
|
+
is absent or unreadable, so `validate_document` keeps classifying those
|
|
199
|
+
the way it always has."""
|
|
200
|
+
stat = Path(schema_path).stat()
|
|
201
|
+
return _cached_schema_problem_by_stat(str(schema_path), stat.st_mtime_ns, stat.st_size)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def validate_document(doc: object, schema_path: Path, source: Path | str) -> list[str]:
|
|
205
|
+
"""Read-path `schema_errors`: never raises.
|
|
206
|
+
|
|
207
|
+
Two different "I could not check this" shapes, given two different
|
|
208
|
+
answers, and this is the one place that split lives (tan-cli#964's own
|
|
209
|
+
"must not fire on a checkout that is simply older ... in a way that
|
|
210
|
+
blocks a legitimate customer" AND "a missing or unreadable schema file
|
|
211
|
+
must not become 'everything is valid'" both apply here, to two different
|
|
212
|
+
causes):
|
|
213
|
+
|
|
214
|
+
* The schema file is simply ABSENT (`schema_path` does not exist). This
|
|
215
|
+
is the ORDINARY shape of an SDK checkout that predates this schema, or
|
|
216
|
+
a synthetic/partial metadata root a test built -- `presets_cmd.py`'s
|
|
217
|
+
own `_os_choices()` already treats a missing `board.schema.json` the
|
|
218
|
+
same way ("a checkout with no schema of its own answers `None`
|
|
219
|
+
(degrade), not ... a DIFFERENT checkout's enum"), silently, with no
|
|
220
|
+
warning. Matching that existing precedent: `[]`, no message. Measured
|
|
221
|
+
against this file's own tests: without this half, EVERY synthetic
|
|
222
|
+
metadata root the test suite builds (routinely no `metadata/schemas/`
|
|
223
|
+
at all) would carry a `*.metadata-schema-invalid` warning on every
|
|
224
|
+
single read, which is not "the document is wrong", it is "this test
|
|
225
|
+
fixture is deliberately minimal" -- exactly the false-positive noise
|
|
226
|
+
that would erode the signal a REAL violation is supposed to carry.
|
|
227
|
+
* The schema file EXISTS but cannot be read or parsed (permissions,
|
|
228
|
+
truncated, invalid JSON). This is a genuine anomaly on an otherwise-real
|
|
229
|
+
checkout -- unlike the absent case, there is no legitimate reason for
|
|
230
|
+
it, so it becomes ONE synthetic message rather than silently degrading
|
|
231
|
+
to "nothing to report". This is the half that keeps the promise: a
|
|
232
|
+
customer whose checkout's schema file itself is corrupt still gets
|
|
233
|
+
told their document could not be validated, not a false "clean".
|
|
234
|
+
A file that parses as JSON but is not a valid JSON Schema
|
|
235
|
+
(`{"type": 5}`) belongs to this half too: it is checked against the
|
|
236
|
+
metaschema first and reported as `not a valid JSON Schema: ...`,
|
|
237
|
+
where it used to reach `iter_errors` and escape as a raw `TypeError`
|
|
238
|
+
(tan-cli#1303).
|
|
239
|
+
|
|
240
|
+
`source` is required (unlike `schema_errors`'s optional one): every
|
|
241
|
+
caller of this function has a real file on disk to name; the one caller
|
|
242
|
+
that does not (`new_som_cmd`'s in-memory self-check) calls `schema_errors`
|
|
243
|
+
directly instead, precisely because it has no `source` to give.
|
|
244
|
+
|
|
245
|
+
tan-cli#1116 round 2: the ABSENT check is NOT `is_file()`/`exists()` --
|
|
246
|
+
a first pass here used a pre-flight `is_file()` guarded by `except
|
|
247
|
+
OSError: exists = True`, reasoning that `Path.is_file()` swallows only
|
|
248
|
+
`ENOENT`/`ENOTDIR`/`EBADF`/`ELOOP` (`pathlib`'s own `_IGNORED_ERRNOS`
|
|
249
|
+
list), not `EACCES`. That RAISING BEHAVIOUR stopped holding in Python
|
|
250
|
+
3.14, so `is_file()`/`is_dir()`/`exists()` swallow EVERY `OSError` --
|
|
251
|
+
including `EACCES` -- and return `False` on every one of them there,
|
|
252
|
+
version-dependent behaviour a version-independent contract cannot be
|
|
253
|
+
built on (measured: seam1's `parity.yml` job resolves
|
|
254
|
+
`python-version: "3.x"` to CPython 3.14.7, where the old guard was a
|
|
255
|
+
silent no-op and a permission-denied schema directory took the ABSENT
|
|
256
|
+
arm -- `[]`, "everything is valid," the exact outcome this docstring
|
|
257
|
+
forbids two paragraphs up).
|
|
258
|
+
|
|
259
|
+
Round 2's OWN first fix got the boundary wrong -- worth recording
|
|
260
|
+
precisely, since it explains why three independent readings (this
|
|
261
|
+
module's own first fix included) landed on the wrong version.
|
|
262
|
+
`_IGNORED_ERRNOS` the CONSTANT vanishes from `pathlib` a release
|
|
263
|
+
EARLIER, in 3.13 (`AttributeError` probing `pathlib._IGNORED_ERRNOS`
|
|
264
|
+
there) -- but the BEHAVIOUR it used to name survives past that removal:
|
|
265
|
+
`is_file()` on a permission-denied ancestor still raises
|
|
266
|
+
`PermissionError` on 3.13.15, exactly as on 3.12.3, and only starts
|
|
267
|
+
returning `False` in 3.14.7. Measured directly against all three real
|
|
268
|
+
interpreters, `errno=13` throughout:
|
|
269
|
+
|
|
270
|
+
3.12.3 is_file() -> raises PermissionError _IGNORED_ERRNOS: (2, 20, 9, 40)
|
|
271
|
+
3.13.15 is_file() -> raises PermissionError _IGNORED_ERRNOS: gone (AttributeError)
|
|
272
|
+
3.14.7 is_file() -> False _IGNORED_ERRNOS: gone (AttributeError)
|
|
273
|
+
|
|
274
|
+
So the constant and the behaviour it once named parted ways for one
|
|
275
|
+
full release: a guard that reasons from "the constant still exists" is
|
|
276
|
+
already wrong reading `pathlib` in 3.13, and a guard that reasons from
|
|
277
|
+
"I measured `is_file()` raise on the interpreters I have" is wrong the
|
|
278
|
+
moment 3.14 ships, whichever question it asked. Neither question is
|
|
279
|
+
safe to ask; the fix below asks neither.
|
|
280
|
+
|
|
281
|
+
The fix is to not stat at all: call `schema_errors` directly and let
|
|
282
|
+
the read itself answer, the same shape `_resolve_hw_rev`
|
|
283
|
+
(`perf_apply.py`) and `example_catalog.catalog_unreadable` already use.
|
|
284
|
+
`_cached_validator`'s own `Path(schema_path).stat()` raises
|
|
285
|
+
`FileNotFoundError` for a genuinely absent path and ONLY for that --
|
|
286
|
+
a directory raises `IsADirectoryError` once the read past `stat()`
|
|
287
|
+
reaches it, a file parent raises `NotADirectoryError`, an ELOOP symlink
|
|
288
|
+
loop and an EACCES ancestor both raise their own `OSError` -- so
|
|
289
|
+
`except FileNotFoundError` alone is the ABSENT half, precisely, on every
|
|
290
|
+
supported interpreter, because it never depends on what `is_file()`
|
|
291
|
+
happens to swallow this Python version.
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
problem = _schema_problem(Path(schema_path))
|
|
295
|
+
if problem is not None:
|
|
296
|
+
return [f"{_posix(source)}: could not validate against {_posix(schema_path)}: {problem}"]
|
|
297
|
+
return schema_errors(doc, schema_path, source=source)
|
|
298
|
+
except FileNotFoundError:
|
|
299
|
+
return []
|
|
300
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
301
|
+
return [f"{_posix(source)}: could not validate against {_posix(schema_path)}: {exc}"]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def schema_unusable_reason(schema_path: Path | str) -> str | None:
|
|
305
|
+
"""Why the schema at *schema_path* cannot be used to validate anything, or
|
|
306
|
+
`None` when it can -- or when it is simply ABSENT (`FileNotFoundError`;
|
|
307
|
+
`missing_schema_note` is the disclosure for that, and absence is not
|
|
308
|
+
"broken").
|
|
309
|
+
|
|
310
|
+
`validate_document` only turns an unreadable/unparseable schema FILE into
|
|
311
|
+
its one synthetic message. A file that is valid JSON but not a valid JSON
|
|
312
|
+
Schema (`{"type": 5}`) escapes it as a raw `TypeError` out of
|
|
313
|
+
`iter_errors` (measured), and an `OSError` other than `FileNotFoundError`
|
|
314
|
+
(a permission-denied ancestor) is indistinguishable from a document
|
|
315
|
+
violation by message alone. A caller that wants "schema unusable" kept
|
|
316
|
+
apart from "document violates schema" -- so a broken schema is never
|
|
317
|
+
blamed on the document -- asks this first (tan-cli#1298). `validate_document`
|
|
318
|
+
and its other callers are unchanged.
|
|
319
|
+
|
|
320
|
+
The bare `except Exception` is deliberate: this probes a third-party
|
|
321
|
+
library with an untrusted schema, whose failure types (`SchemaError`,
|
|
322
|
+
`TypeError`, unresolvable `$ref`) are not a closed set.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
import jsonschema # noqa: PLC0415
|
|
326
|
+
|
|
327
|
+
validator = _cached_validator(Path(schema_path))
|
|
328
|
+
jsonschema.Draft202012Validator.check_schema(validator.schema)
|
|
329
|
+
except FileNotFoundError:
|
|
330
|
+
return None
|
|
331
|
+
except OSError as exc:
|
|
332
|
+
# `str(exc)` embeds the offending path (`[Errno 13] Permission denied:
|
|
333
|
+
# '/abs/...'`); the OS's own wording alone is the part worth showing,
|
|
334
|
+
# and keeps absolute host paths out of anything built from this.
|
|
335
|
+
return f"{type(exc).__name__}: {exc.strerror or 'OS error'}"
|
|
336
|
+
except Exception as exc: # noqa: BLE001 -- see docstring
|
|
337
|
+
return f"{type(exc).__name__}: {exc}".splitlines()[0]
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def missing_schema_note(schema_path: Path | str, *, source: Path | str) -> str | None:
|
|
342
|
+
"""`None` when *schema_path* exists (nothing to disclose); otherwise a
|
|
343
|
+
one-line note that *source* was not validated because the schema itself
|
|
344
|
+
is absent.
|
|
345
|
+
|
|
346
|
+
tan-cli#964 review, major 6 ("ship skip-but-disclose"): before this
|
|
347
|
+
function existed, the ABSENT-schema half of `validate_document` was a
|
|
348
|
+
SILENT skip -- `[]`, indistinguishable on the wire from "validated
|
|
349
|
+
clean" (`{ok: true, exitCode: 0, issues: []}` either way). The review
|
|
350
|
+
measured that the customer this protected -- "an SDK checkout that
|
|
351
|
+
predates this schema" -- does not exist in the released fleet (every
|
|
352
|
+
tagged alp-sdk `v0.6.0`..`v0.16.0` ships both schemas), while the cost of
|
|
353
|
+
silence is real: `tan generate --target os-topology` against a metadata
|
|
354
|
+
root with `soc-spec-v1.schema.json` deleted writes
|
|
355
|
+
`build/generated/os-topology.json` from a wholly unvalidated SoC spec, at
|
|
356
|
+
`ok: true`, with `issues: []`.
|
|
357
|
+
|
|
358
|
+
Deliberately a SEPARATE function from `validate_document`, not a second
|
|
359
|
+
return value on it: `validate_document`'s own contract (never raises,
|
|
360
|
+
"the document is not known to be invalid") is unchanged by this --
|
|
361
|
+
`[]` in the absent-schema case is still correct, because nothing was
|
|
362
|
+
found wrong with the DOCUMENT, only with tan's ability to CHECK it. A
|
|
363
|
+
caller that wants the disclosure calls this too, alongside its existing
|
|
364
|
+
`validate_document` call, and decides its own severity/code for the
|
|
365
|
+
result -- `presets`/`size`/`debug-config` fold it in at `info` beside
|
|
366
|
+
their `*.metadata-schema-invalid` `warning`; `load_board_yaml`'s REFUSE
|
|
367
|
+
callers thread it into an advisory list instead of raising, since an
|
|
368
|
+
absent schema must never become a refusal (see `validate_document`'s own
|
|
369
|
+
docstring on why silently skipping is not the same failure this closes).
|
|
370
|
+
"""
|
|
371
|
+
if Path(schema_path).is_file():
|
|
372
|
+
return None
|
|
373
|
+
return (
|
|
374
|
+
f"{_posix(source)}: not validated -- no schema at {_posix(schema_path)} "
|
|
375
|
+
"in this checkout"
|
|
376
|
+
)
|