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/commands/diff_cmd.py
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan diff` -- show how `normalize_board_model` changes the parsed
|
|
3
|
+
board.yaml (tan-cli#260).
|
|
4
|
+
|
|
5
|
+
Mirrors `crates/tan-cli/src/commands/diff.rs` plus the two `tan-core` helpers
|
|
6
|
+
it composes (`model::{parse_board_model, normalize_board_model}`,
|
|
7
|
+
`diff::{collect_diff_entries, prune_nulls}`).
|
|
8
|
+
|
|
9
|
+
**Why this is NOT a generic recursive JSON differ, unlike the Rust.** The Rust
|
|
10
|
+
parses the WHOLE `board.yaml` into a typed `BoardModel`, normalizes it, and
|
|
11
|
+
diffs the two full trees with a generic recursive walk
|
|
12
|
+
(`tan_core::diff::collect_recursive`). `normalize_board_model`
|
|
13
|
+
(`crates/tan-core/src/model.rs:217-237`) only ever CLEARS four top-level
|
|
14
|
+
fields, and only ever to `None`/absent -- it never adds a key and never
|
|
15
|
+
changes one that survives:
|
|
16
|
+
|
|
17
|
+
* schema version < 2: `libraries` if it deserialized to an EMPTY list, `iot`
|
|
18
|
+
if none of its four toggles is `true`, `inference` if both its fields are
|
|
19
|
+
empty/absent.
|
|
20
|
+
* schema version >= 2: `os` unconditionally (v2 moves it into `cores:`).
|
|
21
|
+
|
|
22
|
+
Every other known field (`som`, `preset`, `cores`, `ipc`, `diagnostics`,
|
|
23
|
+
`populated`, `chips`, `e1m_routes`) is IDENTICAL between the parsed and
|
|
24
|
+
normalized model, so a full recursive diff would recurse into each, find
|
|
25
|
+
`before == after`, and contribute zero entries -- the same outcome this
|
|
26
|
+
module reaches directly, without building or comparing either side's full
|
|
27
|
+
tree. A `DiffEntry.kind` is therefore always `"removed"` here; `"added"`/
|
|
28
|
+
`"changed"` are unreachable through `normalize_board_model` and are kept only
|
|
29
|
+
so the wire shape (`DiffKind`: `added`/`removed`/`changed`) stays the
|
|
30
|
+
contract's, not because this module can produce them today.
|
|
31
|
+
|
|
32
|
+
**PyYAML is required.** Unlike the shallow top-level-shape scanners in
|
|
33
|
+
`validate_cmd`/`presets_cmd` (scalar-vs-block only), computing this diff needs
|
|
34
|
+
real nested values -- is `iot:` a mapping, is any of its four toggles `true`,
|
|
35
|
+
is `inference:`'s `backend` empty -- which a line-oriented fallback cannot
|
|
36
|
+
answer. tan ships no YAML dependency of its own (`typer` + `rich` only), so a
|
|
37
|
+
build with no PyYAML installed refuses with `diff.pyyaml-unavailable`
|
|
38
|
+
(`RUNTIME_FAILURE`, matching `validate_cmd`'s `spawn-not-implemented`
|
|
39
|
+
precedent for "this build cannot do that yet") rather than guessing.
|
|
40
|
+
|
|
41
|
+
**YAML 1.1 vs 1.2 boolean literals.** PyYAML's default `SafeLoader` resolves
|
|
42
|
+
YAML 1.1's full loose bool vocabulary (`on`/`off`/`yes`/`no`/`y`/`n`, any
|
|
43
|
+
case) to `bool`; `serde_yaml` (YAML 1.2 core schema) resolves only the six
|
|
44
|
+
canonical `true`/`True`/`TRUE`/`false`/`False`/`FALSE` spellings and leaves
|
|
45
|
+
everything else a plain string -- measured against the oracle:
|
|
46
|
+
`schemaVersion: 1` + `os: on` is `changes: [{"path":"libraries",...}]` at exit
|
|
47
|
+
0 there (`os` is a `String` field, resolved to the literal text `"on"`,
|
|
48
|
+
untouched at schema version 1). The stock loader instead hands `_parse_fields`
|
|
49
|
+
a Python `bool` for `os` -- and even after `_string_scalar_field` stopped
|
|
50
|
+
refusing an out-of-`str` scalar outright (tan-cli#570), a `bool` there still
|
|
51
|
+
coerces to the CANONICAL text `"true"`, not the original spelling `"on"`,
|
|
52
|
+
which would silently corrupt the diff's `before` value rather than merely
|
|
53
|
+
refuse it. `_load_document` therefore still parses with `_Yaml12BoolLoader`, a
|
|
54
|
+
`SafeLoader` subclass with the YAML 1.1 bool resolver's `on`/`off`/`yes`/`no`/
|
|
55
|
+
`y`/`n` patterns removed, rather than plain `yaml.safe_load`, so those
|
|
56
|
+
spellings reach `_parse_fields` as the plain strings `serde_yaml` would
|
|
57
|
+
produce in the first place.
|
|
58
|
+
|
|
59
|
+
**Scope of the structural checks below.** `_parse_fields` validates the
|
|
60
|
+
TOP-LEVEL type of every known `BoardModel` field (is `cores:` a mapping, is
|
|
61
|
+
`ipc:` a list, ...) because a real Rust type mismatch anywhere in the document
|
|
62
|
+
fails the WHOLE typed parse (`ParseError::Yaml`), and reporting success with a
|
|
63
|
+
wrong diff would be a worse defect than an over-eager refusal. It does NOT
|
|
64
|
+
validate the shape of values `diff` never reads (`cores.*.peripherals`,
|
|
65
|
+
`e1m_routes.*`, ...) -- those fields are never touched by
|
|
66
|
+
`normalize_board_model` and never enter this module's output; going a level
|
|
67
|
+
deeper than "top-level key has the right YAML kind" would just be more parser
|
|
68
|
+
tan does not need for the one question this command answers.
|
|
69
|
+
|
|
70
|
+
`iot`'s four toggles and `inference.backend`/`inference.default_arena_kib` are
|
|
71
|
+
the one exception: they gate `compute_diff_entries`'s pruning decision
|
|
72
|
+
directly, so an unchecked wrong type there does not just under-refuse -- it
|
|
73
|
+
mis-classifies pruning and fabricates a diff entry the oracle never emits
|
|
74
|
+
(measured: `iot: {wifi: "yes"}` used to report `ok:true` with a manufactured
|
|
75
|
+
`{"path":"iot","kind":"removed",...}` entry; the oracle is exit 2
|
|
76
|
+
`diff.schema-violation`). `iot`'s four toggles must each be `bool` or absent.
|
|
77
|
+
`inference.default_arena_kib` must be a non-negative integer (`u32` range) or
|
|
78
|
+
absent. `inference.backend` is a `String` field checked the same way
|
|
79
|
+
`_string_scalar_field` now checks `os`/`preset` (tan-cli#570) -- only the
|
|
80
|
+
compound shapes (`list`/`dict`) no `String` field can ever hold are rejected;
|
|
81
|
+
any other scalar PyYAML resolves it to (even a bare `5` or `true`) is accepted
|
|
82
|
+
as non-empty, exactly as the oracle's own String-field coercion treats it
|
|
83
|
+
(measured: `inference: {backend: 5}` is `unchanged: true` at exit 0 on the
|
|
84
|
+
oracle, never pruned) -- only `_inference_is_empty`'s stringification needed
|
|
85
|
+
fixing to stop miscounting a non-`str` truthy `backend` as blank.
|
|
86
|
+
|
|
87
|
+
`som:`'s own shape (must be a mapping, not a bare SKU string) is checked with
|
|
88
|
+
the exact oracle wording via Python's own `repr()` -- which is actually the
|
|
89
|
+
*more* correct implementation of the two: the Rust's `python_repr` hand-mimics
|
|
90
|
+
Python's `repr()` from a `serde_yaml` (YAML 1.2) value and has one known gap
|
|
91
|
+
against real Python semantics on YAML 1.1-vs-1.2 boolean/null resolution
|
|
92
|
+
(`tan_core::validate` module docs); this module calls `repr()` on a value
|
|
93
|
+
`_Yaml12BoolLoader` (YAML 1.2's narrower bool vocabulary, the same rules
|
|
94
|
+
`serde_yaml` uses) actually parsed, so there is nothing left to approximate.
|
|
95
|
+
|
|
96
|
+
**The SDK cross-check (tan-cli#455).** Everything above only asks whether
|
|
97
|
+
`diff` can PARSE the document well enough to normalize it -- a real type at
|
|
98
|
+
each field it reads. It has no notion of the SDK's own semantic rules (a SoM
|
|
99
|
+
SKU pattern, whether a preset actually exists, the closed `peripherals:`
|
|
100
|
+
enum) -- those live in `metadata/schemas/board.schema.json` and are enforced
|
|
101
|
+
only by the SDK's own `scripts/validate_board_yaml.py`, which `validate`
|
|
102
|
+
spawns and this module, until now, never did. That let a board.yaml
|
|
103
|
+
`validate` refuses outright (e.g. an unknown SoM SKU, a nonexistent preset, an
|
|
104
|
+
unlisted peripheral token -- four real `jsonschema` violations, measured)
|
|
105
|
+
still reach `compute_diff_entries` and report a clean, meaningless
|
|
106
|
+
`unchanged: true` -- the same file, the same project, two commands
|
|
107
|
+
disagreeing about whether it is usable at all. Fixed by REUSING `validate`'s
|
|
108
|
+
own spawn-and-analyze machinery (`analyze_validator_output`, the same
|
|
109
|
+
`scripts/validate_board_yaml.py` argv, imported from `validate_cmd` rather
|
|
110
|
+
than re-checked here) whenever an SDK has actually resolved: a non-clean
|
|
111
|
+
verdict becomes a `ParseFailure("schema-violation", ...)`, flowing through
|
|
112
|
+
the exact same failure path a structural mismatch already does. Since
|
|
113
|
+
tan-cli#1484 the engine is the one `tan validate` selects (in-process by
|
|
114
|
+
default, the spawn only under `TAN_VALIDATE_ENGINE=subprocess`). `diff` still
|
|
115
|
+
never requires an SDK on its own -- with none resolved (or a stub checkout
|
|
116
|
+
missing `validate_board_yaml.py` -- some of this module's own tests hand it
|
|
117
|
+
exactly that) it falls back to the structural checks alone, exactly as
|
|
118
|
+
before. This is a deliberate divergence from the oracle, not a port gap: the
|
|
119
|
+
oracle's own `diff.exe` has the identical contradiction (measured against
|
|
120
|
+
`target/debug/tan.exe`) -- `diff.rs` has never called into the SDK's
|
|
121
|
+
validator either, so this fix does not chase parity, it changes behaviour.
|
|
122
|
+
|
|
123
|
+
**Review round (tan-cli#455 follow-up).** The first cut of the cross-check
|
|
124
|
+
above swallowed every reason the validator subprocess could fail to produce a
|
|
125
|
+
verdict -- a timeout, an unrunnable interpreter -- as a silent no-op, which
|
|
126
|
+
falls back to reporting a clean diff exactly as if no cross-check had ever
|
|
127
|
+
run: the #455 bug, reopened for a narrower trigger. It also flattened every
|
|
128
|
+
non-clean OUTCOME (`missing-preset`, `hardware-revision`, a crashed
|
|
129
|
+
validator's `failed`) into `diff.schema-violation`, the same conflation
|
|
130
|
+
`validate_cmd.analyze_validator_output` exists to prevent for `validate`
|
|
131
|
+
itself. `_reject_if_sdk_validator_disagrees` now mirrors `validate_cmd`'s own
|
|
132
|
+
split in full: guard 3 (`resolve_manifest_python_floor` + `_python_too_old`,
|
|
133
|
+
refusing BEFORE the spawn that would otherwise crash), a `TimeoutExpired`
|
|
134
|
+
that reclassifies to `failed` rather than a silent skip, an unstartable
|
|
135
|
+
subprocess that refuses as `spawn-failed` (`RUNTIME_FAILURE`) rather than a
|
|
136
|
+
silent skip, and `ParseFailure(result.outcome, ...)` instead of a hardcoded
|
|
137
|
+
`"schema-violation"` -- so `diff.failed`/`diff.missing-preset`/
|
|
138
|
+
`diff.hardware-revision`/`diff.hardware-revision-unknown`/
|
|
139
|
+
`diff.hardware-revision-not-buildable`/`diff.spawn-failed`/
|
|
140
|
+
`diff.python-too-old` are now real, registered outcomes alongside
|
|
141
|
+
`diff.schema-violation`. The last two `hardware-revision-*` arrived with
|
|
142
|
+
tan-cli#1262, which added validator exits 4 and 5 to the shared
|
|
143
|
+
`_STATUS_OUTCOME` and so widened THIS command's wire as a side effect; both
|
|
144
|
+
registry entries name this file as `emittedBy`, so this list is where a
|
|
145
|
+
reader looking one of them up lands. The ONLY
|
|
146
|
+
remaining silent no-op is the ORIGINAL one this section already documented:
|
|
147
|
+
no `validate_board_yaml.py` at the resolved checkout at all -- there is
|
|
148
|
+
nothing there to reuse, which is different in kind from a reuse attempt that
|
|
149
|
+
started and failed.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
from __future__ import annotations
|
|
153
|
+
|
|
154
|
+
import json
|
|
155
|
+
import os
|
|
156
|
+
import re
|
|
157
|
+
import subprocess
|
|
158
|
+
from dataclasses import dataclass
|
|
159
|
+
from pathlib import Path
|
|
160
|
+
from typing import Any
|
|
161
|
+
|
|
162
|
+
import typer
|
|
163
|
+
|
|
164
|
+
from tan.commands.doctor_cmd import resolve_manifest_python_floor
|
|
165
|
+
from tan.commands.generate_cmd import _python_too_old
|
|
166
|
+
from tan.commands.presets_cmd import resolve_project_paths, resolve_sdk
|
|
167
|
+
from tan.commands.validate_cmd import (
|
|
168
|
+
OUTCOME_CLEAN,
|
|
169
|
+
OUTCOME_FAILED,
|
|
170
|
+
VALIDATOR_SCRIPT,
|
|
171
|
+
VALIDATOR_TIMEOUT_S,
|
|
172
|
+
_Finding,
|
|
173
|
+
run_in_process_engine,
|
|
174
|
+
skew_warning,
|
|
175
|
+
subprocess_engine_requested,
|
|
176
|
+
_synthesised_finding,
|
|
177
|
+
analyze_validator_output,
|
|
178
|
+
)
|
|
179
|
+
from tan.core.sdk_discovery import _planner_python_resolution, sdk_resolution_issues
|
|
180
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
181
|
+
from tan.exit_codes import ExitCode
|
|
182
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
183
|
+
from tan.core.shapes import yaml_kind
|
|
184
|
+
from tan.core.subprocess_env import spawn_env
|
|
185
|
+
|
|
186
|
+
#: `data.schemaVersion` for this command's payload -- the envelope payload's
|
|
187
|
+
#: own version, unrelated to `board.yaml`'s `schemaVersion:`.
|
|
188
|
+
DATA_SCHEMA_VERSION = "1"
|
|
189
|
+
|
|
190
|
+
#: Serialized `Iot`/`Inference` field order (`crates/tan-core/src/model.rs`'s
|
|
191
|
+
#: struct declaration order) -- `preserve_order` serde_json keeps this order
|
|
192
|
+
#: for the wire `before` value, and it is fixed regardless of the YAML
|
|
193
|
+
#: source's own key order, so it is spelled out here rather than derived from
|
|
194
|
+
#: dict iteration.
|
|
195
|
+
_IOT_FIELDS = ("wifi", "mqtt", "ble", "tls")
|
|
196
|
+
_INFERENCE_FIELDS = ("backend", "default_arena_kib")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ParseFailure(Exception):
|
|
200
|
+
"""A `board.yaml` this offline diff cannot process. `code` is the
|
|
201
|
+
`diff.<code>` issue suffix; `message` already carries the oracle's
|
|
202
|
+
`board.yaml is not valid[ YAML]: ...` prefix so callers pass it straight
|
|
203
|
+
through."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, code: str, message: str, exit_code: ExitCode = ExitCode.VALIDATION_FAILURE):
|
|
206
|
+
self.code = code
|
|
207
|
+
self.message = message
|
|
208
|
+
self.exit_code = exit_code
|
|
209
|
+
super().__init__(message)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(frozen=True)
|
|
213
|
+
class DiffEntry:
|
|
214
|
+
path: str
|
|
215
|
+
kind: str # "added" | "removed" | "changed" -- see module docstring
|
|
216
|
+
before: Any = None
|
|
217
|
+
after: Any = None
|
|
218
|
+
|
|
219
|
+
def as_dict(self) -> dict[str, Any]:
|
|
220
|
+
out: dict[str, Any] = {"path": self.path, "kind": self.kind}
|
|
221
|
+
if self.before is not None:
|
|
222
|
+
out["before"] = self.before
|
|
223
|
+
if self.after is not None:
|
|
224
|
+
out["after"] = self.after
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
#: YAML 1.2 core schema bool literals -- `true`/`True`/`TRUE`/`false`/`False`/
|
|
229
|
+
#: `FALSE` only. Everything YAML 1.1 additionally resolved to bool (`on`/
|
|
230
|
+
#: `off`/`yes`/`no`/`y`/`n`, any case) is deliberately absent: those are the
|
|
231
|
+
#: exact patterns `_yaml_1_2_bool_loader` strips from PyYAML's own resolver,
|
|
232
|
+
#: so `re.compile` never sees them either -- one list, not two that could
|
|
233
|
+
#: drift apart.
|
|
234
|
+
_YAML_1_2_BOOL_PATTERN = re.compile(r"^(?:true|True|TRUE|false|False|FALSE)$")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _yaml_1_2_bool_loader(yaml_module: Any) -> type:
|
|
238
|
+
"""A `yaml_module.SafeLoader` subclass with the YAML 1.1-only loose bool
|
|
239
|
+
literals removed from implicit resolution, so a scalar like `on`/`off`/
|
|
240
|
+
`yes`/`no`/`y`/`n` (any case) parses as a plain STRING -- matching
|
|
241
|
+
`serde_yaml`'s YAML 1.2 core-schema bool tag (see the module docstring's
|
|
242
|
+
"YAML 1.1 vs 1.2 boolean literals" note). Takes the imported `yaml`
|
|
243
|
+
module rather than importing it itself, so a build with no PyYAML
|
|
244
|
+
installed never touches `yaml.SafeLoader` at all -- `_load_document`
|
|
245
|
+
only calls this after its own optional import already succeeded.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
class Yaml12BoolLoader(yaml_module.SafeLoader):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
# `add_implicit_resolver` only APPENDS; the stock YAML-1.1 bool resolver
|
|
252
|
+
# would still match first and win. Copy the resolver table with every
|
|
253
|
+
# existing bool entry stripped, then append the narrower one, so this
|
|
254
|
+
# loader's `tag:yaml.org,2002:bool` entries are exactly the six literals
|
|
255
|
+
# above -- nothing from the base `SafeLoader` (used unmodified everywhere
|
|
256
|
+
# else in tan) is touched.
|
|
257
|
+
Yaml12BoolLoader.yaml_implicit_resolvers = {
|
|
258
|
+
first: [pair for pair in resolvers if pair[0] != "tag:yaml.org,2002:bool"]
|
|
259
|
+
for first, resolvers in yaml_module.SafeLoader.yaml_implicit_resolvers.items()
|
|
260
|
+
}
|
|
261
|
+
Yaml12BoolLoader.add_implicit_resolver(
|
|
262
|
+
"tag:yaml.org,2002:bool", _YAML_1_2_BOOL_PATTERN, list("tTfF")
|
|
263
|
+
)
|
|
264
|
+
return Yaml12BoolLoader
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _load_document(text: str) -> Any:
|
|
268
|
+
"""The raw YAML document, or a `ParseFailure` matching `ParseError`'s two
|
|
269
|
+
reachable variants on this path (`Yaml`, and the `som:`-shape pre-check).
|
|
270
|
+
`EmptyDocument`/`NotAMapping` are NOT reachable here -- those are
|
|
271
|
+
`validate_cmd`'s `reject_non_mapping_document`, which `diff` never calls;
|
|
272
|
+
a null or bare-scalar document degrades to the default (empty) model here,
|
|
273
|
+
matching `parse_board_model`'s own TS-parity leniency.
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
import yaml # noqa: PLC0415 (optional at runtime, by design)
|
|
277
|
+
except ImportError as err:
|
|
278
|
+
raise ParseFailure(
|
|
279
|
+
"pyyaml-unavailable",
|
|
280
|
+
"this build of tan has no YAML support installed, so `tan diff` cannot "
|
|
281
|
+
"compute a normalization diff.",
|
|
282
|
+
ExitCode.RUNTIME_FAILURE,
|
|
283
|
+
) from err
|
|
284
|
+
try:
|
|
285
|
+
return yaml.load(text, Loader=_yaml_1_2_bool_loader(yaml))
|
|
286
|
+
except Exception as err: # noqa: BLE001 -- yaml.YAMLError and anything a loader raises
|
|
287
|
+
raise ParseFailure("schema-violation", f"board.yaml is not valid YAML: {err}") from err
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
#: tan-cli#408: one implementation, in `tan.core.shapes` -- `pinmux_cmd.py`
|
|
291
|
+
#: carried a byte-identical copy. The module docstring's scope note (this is
|
|
292
|
+
#: not a claim of matching serde's exact wording) moved with it.
|
|
293
|
+
_yaml_kind = yaml_kind
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _typed_field(doc: dict, key: str, expected: type, label: str) -> Any:
|
|
297
|
+
"""`doc[key]` if absent or already `expected`-shaped, else a
|
|
298
|
+
`ParseFailure` mirroring the whole-document failure a struct-typed
|
|
299
|
+
`serde_yaml` deserialize would raise for the same mismatch."""
|
|
300
|
+
value = doc.get(key)
|
|
301
|
+
if value is None or isinstance(value, expected):
|
|
302
|
+
return value
|
|
303
|
+
raise ParseFailure(
|
|
304
|
+
"schema-violation",
|
|
305
|
+
f"board.yaml is not valid YAML: {key}: expected {label}, got {_yaml_kind(value)}",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _string_scalar_field(doc: dict, key: str) -> str | None:
|
|
310
|
+
"""`doc[key]` coerced the way a `serde_yaml` `String`-typed field actually
|
|
311
|
+
deserializes: ANY scalar coerces into a string (only the compound shapes
|
|
312
|
+
`list`/`dict` a `String` field can never hold are refused) -- unlike
|
|
313
|
+
`_typed_field(..., str, ...)`, which wrongly demanded the YAML node
|
|
314
|
+
already BE a Python `str` (tan-cli#570). Matches
|
|
315
|
+
`_check_inference_field_types`'s existing treatment of
|
|
316
|
+
`inference.backend`, which this module's own docstring already claimed
|
|
317
|
+
`os`/`preset` shared before the code actually did.
|
|
318
|
+
|
|
319
|
+
Measured against the oracle (tan-cli#570): `os: true` -> `"true"`,
|
|
320
|
+
`os: 5` -> `"5"`. `bool` is special-cased to lowercase text rather than
|
|
321
|
+
falling through to `str(value)` (which would yield Python's `"True"`) --
|
|
322
|
+
every canonical YAML 1.2 bool spelling this loader can produce
|
|
323
|
+
(`_Yaml12BoolLoader`) already normalizes to Python's `True`/`False`, so
|
|
324
|
+
there is no source-text case to preserve here the way there would be for
|
|
325
|
+
a number or a YAML timestamp.
|
|
326
|
+
"""
|
|
327
|
+
value = doc.get(key)
|
|
328
|
+
if value is None:
|
|
329
|
+
return None
|
|
330
|
+
if isinstance(value, (list, dict)):
|
|
331
|
+
raise ParseFailure(
|
|
332
|
+
"schema-violation",
|
|
333
|
+
f"board.yaml is not valid YAML: {key}: expected a string, got {_yaml_kind(value)}",
|
|
334
|
+
)
|
|
335
|
+
if isinstance(value, str):
|
|
336
|
+
return value
|
|
337
|
+
if isinstance(value, bool):
|
|
338
|
+
return "true" if value else "false"
|
|
339
|
+
return str(value)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
#: `u32::MAX` -- the upper bound `inference.default_arena_kib` (`u32` in the
|
|
343
|
+
#: Rust model) accepts. Measured against the oracle: `4294967295` is exit 0,
|
|
344
|
+
#: `4294967296` is exit 2 `inference.default_arena_kib: ... expected u32`.
|
|
345
|
+
_U32_MAX = 0xFFFFFFFF
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _typed_nested(mapping: dict, key: str, path: str, expected: type, label: str) -> Any:
|
|
349
|
+
"""Like `_typed_field`, but for a key nested one level under an
|
|
350
|
+
already-`dict`-shaped `mapping` -- `path` is the dotted diagnostic path
|
|
351
|
+
(`"iot.wifi"`) rather than a bare top-level key."""
|
|
352
|
+
value = mapping.get(key)
|
|
353
|
+
if value is None or isinstance(value, expected):
|
|
354
|
+
return value
|
|
355
|
+
raise ParseFailure(
|
|
356
|
+
"schema-violation",
|
|
357
|
+
f"board.yaml is not valid YAML: {path}: expected {label}, got {_yaml_kind(value)}",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _check_iot_field_types(iot: dict | None) -> None:
|
|
362
|
+
"""Each of `iot`'s four toggles must be `bool` or absent -- checked
|
|
363
|
+
before `compute_diff_entries` ever asks whether the group is prunable
|
|
364
|
+
(see the module docstring's "scope of the structural checks" note)."""
|
|
365
|
+
if iot is None:
|
|
366
|
+
return
|
|
367
|
+
for field in _IOT_FIELDS:
|
|
368
|
+
_typed_nested(iot, field, f"iot.{field}", bool, "a boolean")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _check_inference_field_types(inference: dict | None) -> None:
|
|
372
|
+
"""`inference.default_arena_kib` must be a non-negative `u32`-range
|
|
373
|
+
integer or absent. `inference.backend` is a `String` field: matching the
|
|
374
|
+
`os`/`preset` leniency documented at the top of the module, only the
|
|
375
|
+
compound shapes (`list`/`dict`) no `String` field can ever hold are
|
|
376
|
+
rejected here -- every other scalar PyYAML resolves it to is accepted,
|
|
377
|
+
same as the oracle's own coercion (`_inference_is_empty` is what needed
|
|
378
|
+
fixing to stop mis-treating a non-`str` truthy `backend` as blank)."""
|
|
379
|
+
if inference is None:
|
|
380
|
+
return
|
|
381
|
+
backend = inference.get("backend")
|
|
382
|
+
if isinstance(backend, (list, dict)):
|
|
383
|
+
raise ParseFailure(
|
|
384
|
+
"schema-violation",
|
|
385
|
+
f"board.yaml is not valid YAML: inference.backend: expected a string, "
|
|
386
|
+
f"got {_yaml_kind(backend)}",
|
|
387
|
+
)
|
|
388
|
+
arena = inference.get("default_arena_kib")
|
|
389
|
+
if arena is not None and (
|
|
390
|
+
isinstance(arena, bool) or not isinstance(arena, int) or not 0 <= arena <= _U32_MAX
|
|
391
|
+
):
|
|
392
|
+
raise ParseFailure(
|
|
393
|
+
"schema-violation",
|
|
394
|
+
"board.yaml is not valid YAML: inference.default_arena_kib: expected a "
|
|
395
|
+
f"non-negative 32-bit integer, got {_yaml_kind(arena)}",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _parse_fields(doc: Any) -> tuple[int, str | None, list | None, dict | None, dict | None]:
|
|
400
|
+
"""`(effective_schema_version, os, libraries, iot, inference)` -- the only
|
|
401
|
+
values `normalize_board_model` can ever act on. Raises `ParseFailure` for
|
|
402
|
+
every document shape that would fail the Rust's typed parse; see the
|
|
403
|
+
module docstring for exactly how far the type checking goes.
|
|
404
|
+
"""
|
|
405
|
+
if doc is None:
|
|
406
|
+
doc = {}
|
|
407
|
+
if not isinstance(doc, dict):
|
|
408
|
+
raise ParseFailure(
|
|
409
|
+
"schema-violation",
|
|
410
|
+
f"board.yaml is not valid YAML: invalid type: {_yaml_kind(doc)}, expected a mapping",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
som = doc.get("som")
|
|
414
|
+
if som is not None and not isinstance(som, dict):
|
|
415
|
+
raise ParseFailure(
|
|
416
|
+
"schema-violation",
|
|
417
|
+
"board.yaml is not valid: `som:` must be a mapping carrying a `sku:` key, but "
|
|
418
|
+
f"a scalar was given ({som!r}). Write it as:\n som:\n sku: <SKU>",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
schema_version = doc.get("schemaVersion")
|
|
422
|
+
schema_version_ok = isinstance(schema_version, int) and not isinstance(schema_version, bool)
|
|
423
|
+
# `schemaVersion` is `u32` in the Rust model (`model.rs:18`), so the upper
|
|
424
|
+
# bound matters just as much as the lower one -- tan-cli#571: without the
|
|
425
|
+
# `_U32_MAX` ceiling (already applied to `inference.default_arena_kib`
|
|
426
|
+
# below), a value above it silently fell through and got treated as
|
|
427
|
+
# `>= 2`, the opposite of the oracle's exit-2 refusal.
|
|
428
|
+
if schema_version is not None and (
|
|
429
|
+
not schema_version_ok or schema_version < 0 or schema_version > _U32_MAX
|
|
430
|
+
):
|
|
431
|
+
raise ParseFailure(
|
|
432
|
+
"schema-violation",
|
|
433
|
+
f"board.yaml is not valid YAML: schemaVersion: expected a non-negative 32-bit "
|
|
434
|
+
f"integer, got {_yaml_kind(schema_version)}",
|
|
435
|
+
)
|
|
436
|
+
effective_version = schema_version if schema_version is not None else 1
|
|
437
|
+
|
|
438
|
+
os_value = _string_scalar_field(doc, "os")
|
|
439
|
+
libraries = _typed_field(doc, "libraries", list, "a sequence")
|
|
440
|
+
iot = _typed_field(doc, "iot", dict, "a mapping")
|
|
441
|
+
inference = _typed_field(doc, "inference", dict, "a mapping")
|
|
442
|
+
_check_iot_field_types(iot)
|
|
443
|
+
_check_inference_field_types(inference)
|
|
444
|
+
|
|
445
|
+
# Fields `diff` never reads (never touched by normalize_board_model, so
|
|
446
|
+
# never contribute a diff entry either way) -- top-level shape checked
|
|
447
|
+
# only, per the module docstring's scope note.
|
|
448
|
+
_string_scalar_field(doc, "preset")
|
|
449
|
+
_typed_field(doc, "cores", dict, "a mapping")
|
|
450
|
+
_typed_field(doc, "ipc", list, "a sequence")
|
|
451
|
+
_typed_field(doc, "diagnostics", dict, "a mapping")
|
|
452
|
+
_typed_field(doc, "populated", dict, "a mapping")
|
|
453
|
+
_typed_field(doc, "chips", list, "a sequence")
|
|
454
|
+
_typed_field(doc, "e1m_routes", dict, "a mapping")
|
|
455
|
+
|
|
456
|
+
return effective_version, os_value, libraries, iot, inference
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _iot_any_enabled(iot: dict) -> bool:
|
|
460
|
+
return any(iot.get(k) is True for k in _IOT_FIELDS)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _iot_pruned(iot: dict) -> dict:
|
|
464
|
+
return {k: iot[k] for k in _IOT_FIELDS if iot.get(k) is not None}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _inference_is_empty(inference: dict) -> bool:
|
|
468
|
+
"""`backend` is empty only when absent or an explicit empty string --
|
|
469
|
+
any OTHER present scalar (`_check_inference_field_types` has already
|
|
470
|
+
rejected the compound shapes) counts as non-empty regardless of its YAML
|
|
471
|
+
type, matching the oracle's own `String`-field coercion (measured:
|
|
472
|
+
`inference: {backend: 5}` is `unchanged: true`, never pruned). Naively
|
|
473
|
+
defaulting a non-`str` `backend` to `""` here -- as this used to -- is
|
|
474
|
+
exactly the bug: it silently treated a present, non-empty `backend` as
|
|
475
|
+
blank and let `compute_diff_entries` fabricate a diff entry the oracle
|
|
476
|
+
never emits."""
|
|
477
|
+
backend = inference.get("backend")
|
|
478
|
+
if backend is not None and backend != "":
|
|
479
|
+
return False
|
|
480
|
+
return inference.get("default_arena_kib") is None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _inference_pruned(inference: dict) -> dict:
|
|
484
|
+
return {k: inference[k] for k in _INFERENCE_FIELDS if inference.get(k) is not None}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def compute_diff_entries(
|
|
488
|
+
effective_version: int,
|
|
489
|
+
os_value: str | None,
|
|
490
|
+
libraries: list | None,
|
|
491
|
+
iot: dict | None,
|
|
492
|
+
inference: dict | None,
|
|
493
|
+
) -> list[DiffEntry]:
|
|
494
|
+
"""The `normalize_board_model` effect as `DiffEntry` list, sorted by path
|
|
495
|
+
(matching `collect_diff_entries`'s own `sort_by(path)` -- alphabetical
|
|
496
|
+
among `inference`/`iot`/`libraries`/`os` needs no explicit sort since at
|
|
497
|
+
most one of `{inference, iot, libraries}` XOR `{os}` group is ever
|
|
498
|
+
populated, but sorting keeps the guarantee explicit rather than accidental).
|
|
499
|
+
"""
|
|
500
|
+
entries: list[DiffEntry] = []
|
|
501
|
+
if effective_version < 2:
|
|
502
|
+
if libraries is not None and len(libraries) == 0:
|
|
503
|
+
entries.append(DiffEntry("libraries", "removed", before=[]))
|
|
504
|
+
if iot is not None and not _iot_any_enabled(iot):
|
|
505
|
+
entries.append(DiffEntry("iot", "removed", before=_iot_pruned(iot)))
|
|
506
|
+
if inference is not None and _inference_is_empty(inference):
|
|
507
|
+
entries.append(DiffEntry("inference", "removed", before=_inference_pruned(inference)))
|
|
508
|
+
else:
|
|
509
|
+
if os_value is not None:
|
|
510
|
+
entries.append(DiffEntry("os", "removed", before=os_value))
|
|
511
|
+
entries.sort(key=lambda e: e.path)
|
|
512
|
+
return entries
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
_KIND_LABEL = {"added": "ADDED", "removed": "REMOVED", "changed": "CHANGED"}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _format_value(value: Any) -> str:
|
|
519
|
+
"""`<undefined>` for `None`, JSON-quoted for a string, compact JSON
|
|
520
|
+
otherwise, truncated to 117 chars + `...` past 120 -- verbatim from
|
|
521
|
+
`diff.rs`'s `format_value`. `ensure_ascii=False`: `serde_json::to_string`
|
|
522
|
+
emits raw UTF-8, never a `\\uXXXX` escape, for a non-ASCII board.yaml
|
|
523
|
+
string."""
|
|
524
|
+
if value is None:
|
|
525
|
+
return "<undefined>"
|
|
526
|
+
if isinstance(value, str):
|
|
527
|
+
return json.dumps(value, ensure_ascii=False)
|
|
528
|
+
raw = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
529
|
+
if len(raw) > 120:
|
|
530
|
+
return raw[:117] + "..."
|
|
531
|
+
return raw
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _render_text(entries: list[DiffEntry], board_path: str, quiet: bool) -> list[str]:
|
|
535
|
+
if not entries:
|
|
536
|
+
return ["diff: no effective-config differences detected."]
|
|
537
|
+
lines = [f"diff: {len(entries)} differences in {board_path}"]
|
|
538
|
+
if not quiet:
|
|
539
|
+
for entry in entries:
|
|
540
|
+
lines.append(
|
|
541
|
+
f"{_KIND_LABEL[entry.kind]} {entry.path}: "
|
|
542
|
+
f"{_format_value(entry.before)} -> {_format_value(entry.after)}"
|
|
543
|
+
)
|
|
544
|
+
return lines
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _data(
|
|
548
|
+
board_path: str, entries: list[DiffEntry], *, unchanged: bool | None = None
|
|
549
|
+
) -> dict[str, Any]:
|
|
550
|
+
"""`unchanged` defaults to `len(entries) == 0` for the success path, but a
|
|
551
|
+
FAILURE envelope's `DiffData` hardcodes `unchanged: false` regardless of
|
|
552
|
+
its (always-empty) `changes` list -- verbatim from `diff.rs`'s `failure()`
|
|
553
|
+
-- so `_emit_failure` passes it explicitly rather than letting an empty
|
|
554
|
+
`changes: []` compute `unchanged: true` for a run that never got far
|
|
555
|
+
enough to answer that question.
|
|
556
|
+
|
|
557
|
+
**No `validatorExitStatus` here, knowingly (tan-cli#1262).** That change
|
|
558
|
+
gave `validate` a `data.validatorExitStatus` carrying the spawned
|
|
559
|
+
validator's raw returncode, so a consumer keeps the distinction when an
|
|
560
|
+
exit falls through `_STATUS_OUTCOME.get(..., OUTCOME_FAILED)`. `diff`
|
|
561
|
+
reuses the same `_spawn_validator`/`_STATUS_OUTCOME` and so inherited the
|
|
562
|
+
two new `hardware-revision-*` outcome codes -- but NOT the raw status,
|
|
563
|
+
because it never reaches this function on that path: the outcome travels
|
|
564
|
+
as a `ParseFailure.code` through `_emit_failure`, which builds its `data`
|
|
565
|
+
from `_data(...)` alone and has no channel for a number. So a `tan diff`
|
|
566
|
+
against an unmapped validator exit is still lossy, arriving as
|
|
567
|
+
`diff.failed` with nothing to re-derive it from. Threading the status
|
|
568
|
+
through `_spawn_validator` -> `ParseFailure` -> `_emit_failure` is a
|
|
569
|
+
second wire change on a second command and is tracked separately; it was
|
|
570
|
+
deliberately NOT folded into #1262."""
|
|
571
|
+
return {
|
|
572
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
573
|
+
"boardYamlPath": board_path,
|
|
574
|
+
"unchanged": (len(entries) == 0) if unchanged is None else unchanged,
|
|
575
|
+
"changeCount": len(entries),
|
|
576
|
+
"changes": [e.as_dict() for e in entries],
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _emit_failure(
|
|
581
|
+
*,
|
|
582
|
+
json_mode: bool,
|
|
583
|
+
root: str,
|
|
584
|
+
board_path: str,
|
|
585
|
+
code: str,
|
|
586
|
+
message: str,
|
|
587
|
+
exit_code: ExitCode,
|
|
588
|
+
text_lines: list[str],
|
|
589
|
+
sdk: SdkInfo | None = None,
|
|
590
|
+
sdk_context_issues: list[Issue] | None = None,
|
|
591
|
+
) -> None:
|
|
592
|
+
"""Mirrors `diff.rs`'s `failure(...)`: the JSON issue message and the
|
|
593
|
+
text-mode lines are independent strings, not one derived from the other
|
|
594
|
+
(`board-yaml-missing`'s text line reads differently from its issue
|
|
595
|
+
message) -- callers supply `text_lines` verbatim, matching the Rust
|
|
596
|
+
call sites' own hand-written `vec![...]`. Unlike the success path's
|
|
597
|
+
`_render_text`, these lines are NOT filtered by `--quiet` -- measured
|
|
598
|
+
against the oracle: `diff --quiet` on every failure prints the identical
|
|
599
|
+
lines a plain `diff` does.
|
|
600
|
+
|
|
601
|
+
`sdk` is the resolved `--sdk-root` block, carried through to the failure
|
|
602
|
+
envelope exactly as the success envelope carries it -- measured against
|
|
603
|
+
the oracle: `diff --sdk-root <path>` against a missing board.yaml still
|
|
604
|
+
reports `sdk.root`/`sdk.sourceTier` on the exit-2 envelope; dropping it
|
|
605
|
+
on the failure path (as this used to) is a real divergence, not just an
|
|
606
|
+
asymmetry with the success path.
|
|
607
|
+
"""
|
|
608
|
+
if json_mode:
|
|
609
|
+
emit(
|
|
610
|
+
Envelope(
|
|
611
|
+
"diff",
|
|
612
|
+
Project.resolved(root, board_path),
|
|
613
|
+
_data(board_path, [], unchanged=False),
|
|
614
|
+
[*(sdk_context_issues or []), Issue(f"diff.{code}", "error", message)],
|
|
615
|
+
exit_code,
|
|
616
|
+
sdk=sdk,
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
stream = typer.get_text_stream("stderr")
|
|
621
|
+
# tan-cli#478 review finding 6: the JSON envelope has carried the
|
|
622
|
+
# foreign-default pair since the `Envelope` seam landed, but the
|
|
623
|
+
# DEFAULT text path never read `sdk_context_issues` at all -- a
|
|
624
|
+
# customer running plain `tan diff` (no `--format json`) saw only
|
|
625
|
+
# `text_lines` and never learned another project's checkout decided
|
|
626
|
+
# this. Printed first, matching the JSON list's own ordering.
|
|
627
|
+
for issue in sdk_context_issues or []:
|
|
628
|
+
stream.write(f"{issue.message}\n")
|
|
629
|
+
for line in text_lines:
|
|
630
|
+
stream.write(f"{line}\n")
|
|
631
|
+
raise typer.Exit(int(exit_code))
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _spawn_validator(
|
|
635
|
+
python_binary: str, script: str, board_path: str, *, used_workspace_venv: bool = True
|
|
636
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
637
|
+
"""`(outcome, findings)` for one run of the SDK's own validator, or a
|
|
638
|
+
`ParseFailure("spawn-failed", ...)` when the subprocess could not even be
|
|
639
|
+
started -- `validate_cmd`'s own split (tan-cli#262/#455 review round),
|
|
640
|
+
reused rather than re-derived: a launch that never happened is not a
|
|
641
|
+
verdict, and must not be swallowed into a silent "nothing to report".
|
|
642
|
+
|
|
643
|
+
`used_workspace_venv` (tan-cli#652) is forwarded verbatim to
|
|
644
|
+
`_synthesised_finding`: `False` means `python_binary` is a bare PATH
|
|
645
|
+
fallback (no `tan bootstrap` workspace venv resolved for this project),
|
|
646
|
+
which is what lets a missing-module crash there name `tan bootstrap` as
|
|
647
|
+
the remedy instead of surfacing a raw `ModuleNotFoundError`."""
|
|
648
|
+
command_line = f"{python_binary} {script} --input {board_path}"
|
|
649
|
+
try:
|
|
650
|
+
out = subprocess.run(
|
|
651
|
+
[python_binary, script, "--input", board_path],
|
|
652
|
+
capture_output=True,
|
|
653
|
+
text=True,
|
|
654
|
+
encoding="utf-8",
|
|
655
|
+
errors="replace",
|
|
656
|
+
stdin=subprocess.DEVNULL,
|
|
657
|
+
timeout=VALIDATOR_TIMEOUT_S,
|
|
658
|
+
env=spawn_env(),
|
|
659
|
+
check=False,
|
|
660
|
+
)
|
|
661
|
+
except subprocess.TimeoutExpired:
|
|
662
|
+
# The child STARTED, so this is a verdict that never arrived, not a
|
|
663
|
+
# launch failure -- mirrors validate_cmd's own tan-cli#262 shape.
|
|
664
|
+
return OUTCOME_FAILED, (
|
|
665
|
+
_Finding(
|
|
666
|
+
"error",
|
|
667
|
+
f"the SDK validator did not finish within {VALIDATOR_TIMEOUT_S}s "
|
|
668
|
+
f"and was killed: {command_line}",
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
except (OSError, ValueError, subprocess.SubprocessError) as err:
|
|
672
|
+
raise ParseFailure(
|
|
673
|
+
"spawn-failed",
|
|
674
|
+
f"could not run the SDK validator ({command_line}): {err}",
|
|
675
|
+
ExitCode.RUNTIME_FAILURE,
|
|
676
|
+
) from err
|
|
677
|
+
result = analyze_validator_output(out.returncode, out.stderr)
|
|
678
|
+
if result.outcome != OUTCOME_CLEAN and not result.findings:
|
|
679
|
+
# A non-clean run must never reach a consumer as "zero issues",
|
|
680
|
+
# which reads as no problem -- `to_cli_issues`' own synthesis.
|
|
681
|
+
return result.outcome, (
|
|
682
|
+
_synthesised_finding(
|
|
683
|
+
result.outcome, out.stderr, used_workspace_venv=used_workspace_venv
|
|
684
|
+
),
|
|
685
|
+
)
|
|
686
|
+
return result.outcome, result.findings
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _reject_if_sdk_validator_disagrees(sdk_info: SdkInfo, root: str, board_path: str) -> None:
|
|
690
|
+
"""Raise `ParseFailure(<outcome>, ...)` when the validator engine `tan
|
|
691
|
+
validate` uses (the in-process port by default; the SDK's own
|
|
692
|
+
`scripts/validate_board_yaml.py` under `TAN_VALIDATE_ENGINE=subprocess`)
|
|
693
|
+
finds this board.yaml invalid, or its own environment cannot
|
|
694
|
+
even answer that question (tan-cli#455; see the module docstring's "SDK
|
|
695
|
+
cross-check" / "Review round" sections for the false-clean bug this
|
|
696
|
+
closes and why the outcome, not a hardcoded `"schema-violation"`, is what
|
|
697
|
+
reaches the wire).
|
|
698
|
+
|
|
699
|
+
A no-op ONLY when there is nothing to reuse at all: no
|
|
700
|
+
`validate_board_yaml.py` at the resolved checkout (a stub/incomplete SDK
|
|
701
|
+
-- this module's own `--sdk-root` tests hand it exactly that, and must
|
|
702
|
+
keep passing unmodified). Every attempt that STARTS and then fails to
|
|
703
|
+
reach a verdict -- guard 3's interpreter floor, an unstartable
|
|
704
|
+
subprocess, a timeout -- refuses instead.
|
|
705
|
+
"""
|
|
706
|
+
script = os.path.join(sdk_info.root, *VALIDATOR_SCRIPT)
|
|
707
|
+
if not subprocess_engine_requested():
|
|
708
|
+
# Same engine `tan validate` defaults to (tan tan-cli#1484): the
|
|
709
|
+
# in-process port, so the two commands cannot disagree. A checkout with
|
|
710
|
+
# no `metadata/` (a stub) has nothing to validate against -- a no-op.
|
|
711
|
+
if not os.path.isdir(os.path.join(sdk_info.root, "metadata")):
|
|
712
|
+
return
|
|
713
|
+
_status, _stderr, result = run_in_process_engine(board_path, Path(sdk_info.root))
|
|
714
|
+
if result.outcome == OUTCOME_CLEAN:
|
|
715
|
+
return
|
|
716
|
+
detail = "; ".join(finding.message for finding in result.findings)
|
|
717
|
+
if result.crashed:
|
|
718
|
+
# The port crashed: board.yaml was never judged (tan-cli#1523).
|
|
719
|
+
raise ParseFailure(
|
|
720
|
+
result.outcome,
|
|
721
|
+
f"could not check board.yaml before diffing: {detail}",
|
|
722
|
+
)
|
|
723
|
+
skew = skew_warning(Path(sdk_info.root))
|
|
724
|
+
raise ParseFailure(
|
|
725
|
+
result.outcome,
|
|
726
|
+
"board.yaml is not valid: tan's validator rejects it -- run "
|
|
727
|
+
f"`tan validate` for full diagnostics: {detail}"
|
|
728
|
+
+ (f" (caveat: {skew})" if skew else ""),
|
|
729
|
+
)
|
|
730
|
+
if not os.path.isfile(script):
|
|
731
|
+
return
|
|
732
|
+
# tan-cli#652: `used_workspace_venv` is threaded through to
|
|
733
|
+
# `_spawn_validator`/`_synthesised_finding` so a missing-module crash on
|
|
734
|
+
# the bare-PATH fallback names `tan bootstrap` as the remedy.
|
|
735
|
+
python_binary, used_workspace_venv = _planner_python_resolution(
|
|
736
|
+
os.path.abspath(root), sdk_info.root
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
# Guard 3 (validate_cmd.py:1013-1015's own): a spawned interpreter below
|
|
740
|
+
# the SDK's declared pythonMinVersion dies inside alp-sdk's
|
|
741
|
+
# `@dataclass(slots=True)` scripts with a traceback -- refuse with the
|
|
742
|
+
# actual defect before that traceback ever happens, rather than let
|
|
743
|
+
# `_spawn_validator` reclassify it to a generic `failed`.
|
|
744
|
+
floor, _floor_source = resolve_manifest_python_floor(sdk_info.root)
|
|
745
|
+
if (too_old := _python_too_old(python_binary, floor)) is not None:
|
|
746
|
+
raise ParseFailure("python-too-old", too_old)
|
|
747
|
+
|
|
748
|
+
outcome, findings = _spawn_validator(
|
|
749
|
+
python_binary, script, board_path, used_workspace_venv=used_workspace_venv
|
|
750
|
+
)
|
|
751
|
+
if outcome == OUTCOME_CLEAN:
|
|
752
|
+
return
|
|
753
|
+
# `finding.message`, not a `(severity, message)` unpack: tan-cli#498
|
|
754
|
+
# defect 2 turned `validate_cmd`'s findings into a `_Finding` record so a
|
|
755
|
+
# rich `error[ALP-Bxxx]` block's code, hint and source range survive the
|
|
756
|
+
# walk. This module reuses that parser wholesale, so it follows the shape.
|
|
757
|
+
# The bare `.message` is deliberate here -- `diff` folds every finding into
|
|
758
|
+
# ONE sentence and points the reader at `tan validate` for the full
|
|
759
|
+
# diagnostics, which is where the code and hint are rendered.
|
|
760
|
+
detail = "; ".join(finding.message for finding in findings)
|
|
761
|
+
raise ParseFailure(
|
|
762
|
+
outcome,
|
|
763
|
+
"board.yaml is not valid: the SDK's own validator rejects it -- run "
|
|
764
|
+
f"`tan validate` for full diagnostics: {detail}",
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def diff(
|
|
769
|
+
ctx: typer.Context,
|
|
770
|
+
project: str = typer.Option(
|
|
771
|
+
None, "--project", metavar="PATH", help="Project root (defaults to current directory)."
|
|
772
|
+
),
|
|
773
|
+
board_yaml: str = typer.Option(
|
|
774
|
+
None,
|
|
775
|
+
"--board-yaml",
|
|
776
|
+
metavar="PATH",
|
|
777
|
+
help="Explicit board.yaml path (overrides project resolution).",
|
|
778
|
+
),
|
|
779
|
+
sdk_root: str = typer.Option(
|
|
780
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
781
|
+
),
|
|
782
|
+
target: str = typer.Option( # accepted, not read
|
|
783
|
+
None,
|
|
784
|
+
"--target",
|
|
785
|
+
metavar="EMIT",
|
|
786
|
+
help="Generation target (e.g. zephyr-conf, dts-overlay, cmake-args, yocto-conf).",
|
|
787
|
+
),
|
|
788
|
+
all_targets: bool = typer.Option( # accepted, not read
|
|
789
|
+
False, "--all", help="Run command against all relevant targets."
|
|
790
|
+
),
|
|
791
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
792
|
+
verbose: bool = typer.Option( # accepted, not read
|
|
793
|
+
False, "--verbose", help="Emit additional diagnostic detail."
|
|
794
|
+
),
|
|
795
|
+
quiet: bool = typer.Option(
|
|
796
|
+
False, "--quiet", help="Suppress non-essential output (omits the per-change lines)."
|
|
797
|
+
),
|
|
798
|
+
no_color: bool = typer.Option( # accepted, not read; diff emits no ANSI color
|
|
799
|
+
False, "--no-color", help="Disable ANSI color in text output."
|
|
800
|
+
),
|
|
801
|
+
non_interactive: bool = typer.Option( # accepted, not read; diff never prompts
|
|
802
|
+
False, "--non-interactive", help="Never prompt."
|
|
803
|
+
),
|
|
804
|
+
ci: bool = typer.Option( # accepted, not read
|
|
805
|
+
False, "--ci", help="CI mode: implies non-interactive and disables color."
|
|
806
|
+
),
|
|
807
|
+
) -> None:
|
|
808
|
+
"""Show how board.yaml normalization changes the effective config.
|
|
809
|
+
|
|
810
|
+
`--target`/`--all`/`--verbose`/`--no-color`/`--non-interactive`/`--ci` are
|
|
811
|
+
declared, not consumed: `diff` reads only the project's own board.yaml
|
|
812
|
+
plus, now, `--sdk-root` -- solely to echo the resolved SDK in the
|
|
813
|
+
envelope's `sdk` block, matching the oracle (measured: `diff --sdk-root
|
|
814
|
+
<path>` reports `sdk.root`/`sdk.sourceTier` on both the success AND the
|
|
815
|
+
board-yaml-missing failure envelope; `diff` still never READS anything
|
|
816
|
+
from the checkout). The oracle's clap `GlobalArgs` are `global = true`,
|
|
817
|
+
so every verb accepts all of them and a caller passing one through
|
|
818
|
+
unconditionally must not get a parse error.
|
|
819
|
+
"""
|
|
820
|
+
del target, all_targets, verbose, no_color, non_interactive, ci
|
|
821
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
822
|
+
json_mode = resolved_format == "json"
|
|
823
|
+
|
|
824
|
+
root, board_path = resolve_project_paths(project, board_yaml)
|
|
825
|
+
sdk = resolve_sdk(sdk_root, root)
|
|
826
|
+
# tan-cli#468: `resolve_sdk` now always returns an `ActiveSdk`, never a
|
|
827
|
+
# bare `None` -- `sdk.path` is what says whether a usable checkout
|
|
828
|
+
# resolved, so `sdk_info` guards on that instead of `sdk`'s own
|
|
829
|
+
# (now-unconditional) truthiness.
|
|
830
|
+
sdk_info = SdkInfo.from_resolution(sdk.path, sdk) if sdk.path is not None else None
|
|
831
|
+
# tan-cli#478: `SdkInfo.from_resolution` above carries the pair, and
|
|
832
|
+
# `Envelope.__init__` appends it to the JSON envelope for every command --
|
|
833
|
+
# that seam alone is enough for `--format json`, and dedupes by code, so
|
|
834
|
+
# computing it again here changes nothing there. But `diff`'s text mode
|
|
835
|
+
# never touches `Envelope` at all (`_emit_failure`'s `else` branch and the
|
|
836
|
+
# success branch below both write straight to stderr), so without this
|
|
837
|
+
# the DEFAULT (non-JSON) path stayed silent -- tan-cli#478 review finding
|
|
838
|
+
# 6. Computed here, once, and threaded into both text branches below.
|
|
839
|
+
#
|
|
840
|
+
# Unconditional now (tan-cli#468): `sdk.broken_project_pin`/
|
|
841
|
+
# `sdk.foreign_global_default_for` are populated even when `sdk.path` is
|
|
842
|
+
# `None`, so a broken pin with nothing else resolving reaches `diff`'s
|
|
843
|
+
# text mode too, not only the resolved-checkout case.
|
|
844
|
+
sdk_context_issues: list[Issue] = sdk_resolution_issues(
|
|
845
|
+
sdk.broken_project_pin, sdk.tier, sdk.foreign_global_default_for
|
|
846
|
+
)
|
|
847
|
+
board_file = Path(board_path)
|
|
848
|
+
|
|
849
|
+
if not board_file.exists():
|
|
850
|
+
_emit_failure(
|
|
851
|
+
json_mode=json_mode,
|
|
852
|
+
root=root,
|
|
853
|
+
board_path=board_path,
|
|
854
|
+
code="board-yaml-missing",
|
|
855
|
+
message="board.yaml path could not be resolved or the file does not exist.",
|
|
856
|
+
exit_code=ExitCode.VALIDATION_FAILURE,
|
|
857
|
+
text_lines=["diff: board.yaml path is unresolved or missing."],
|
|
858
|
+
sdk=sdk_info,
|
|
859
|
+
sdk_context_issues=sdk_context_issues,
|
|
860
|
+
)
|
|
861
|
+
return
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
text = board_file.read_text(encoding="utf-8")
|
|
865
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
866
|
+
_emit_failure(
|
|
867
|
+
json_mode=json_mode,
|
|
868
|
+
root=root,
|
|
869
|
+
board_path=board_path,
|
|
870
|
+
code="internal-failure",
|
|
871
|
+
message=f"could not read board.yaml: {err}",
|
|
872
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
873
|
+
text_lines=["diff: internal failure", str(err)],
|
|
874
|
+
sdk=sdk_info,
|
|
875
|
+
sdk_context_issues=sdk_context_issues,
|
|
876
|
+
)
|
|
877
|
+
return
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
doc = _load_document(text)
|
|
881
|
+
effective_version, os_value, libraries, iot, inference = _parse_fields(doc)
|
|
882
|
+
if sdk_info is not None:
|
|
883
|
+
_reject_if_sdk_validator_disagrees(sdk_info, root, board_path)
|
|
884
|
+
entries = compute_diff_entries(effective_version, os_value, libraries, iot, inference)
|
|
885
|
+
except ParseFailure as failure:
|
|
886
|
+
header = (
|
|
887
|
+
"diff: internal failure"
|
|
888
|
+
if failure.exit_code == ExitCode.INTERNAL_FAILURE
|
|
889
|
+
else "diff: validation failure"
|
|
890
|
+
if failure.exit_code == ExitCode.VALIDATION_FAILURE
|
|
891
|
+
else "diff: runtime failure"
|
|
892
|
+
)
|
|
893
|
+
_emit_failure(
|
|
894
|
+
json_mode=json_mode,
|
|
895
|
+
root=root,
|
|
896
|
+
board_path=board_path,
|
|
897
|
+
code=failure.code,
|
|
898
|
+
message=failure.message,
|
|
899
|
+
exit_code=failure.exit_code,
|
|
900
|
+
text_lines=[header, failure.message],
|
|
901
|
+
sdk=sdk_info,
|
|
902
|
+
sdk_context_issues=sdk_context_issues,
|
|
903
|
+
)
|
|
904
|
+
return
|
|
905
|
+
except Exception as err: # noqa: BLE001 -- the envelope IS the error contract
|
|
906
|
+
message = f"diff failed unexpectedly: {err.__class__.__name__}: {err}"
|
|
907
|
+
_emit_failure(
|
|
908
|
+
json_mode=json_mode,
|
|
909
|
+
root=root,
|
|
910
|
+
board_path=board_path,
|
|
911
|
+
code="internal-failure",
|
|
912
|
+
message=message,
|
|
913
|
+
exit_code=ExitCode.INTERNAL_FAILURE,
|
|
914
|
+
text_lines=["diff: internal failure", message],
|
|
915
|
+
sdk=sdk_info,
|
|
916
|
+
sdk_context_issues=sdk_context_issues,
|
|
917
|
+
)
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
if json_mode:
|
|
921
|
+
emit(
|
|
922
|
+
Envelope(
|
|
923
|
+
"diff",
|
|
924
|
+
Project.resolved(root, board_path),
|
|
925
|
+
_data(board_path, entries),
|
|
926
|
+
sdk_context_issues,
|
|
927
|
+
ExitCode.SUCCESS,
|
|
928
|
+
sdk=sdk_info,
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
else:
|
|
932
|
+
stream = typer.get_text_stream("stderr")
|
|
933
|
+
# tan-cli#478 review finding 6: printed unconditionally, unlike the
|
|
934
|
+
# `--quiet`-suppressed entries below -- a foreign-checkout warning is
|
|
935
|
+
# not the "non-essential" output `--quiet` exists to trim.
|
|
936
|
+
for issue in sdk_context_issues:
|
|
937
|
+
stream.write(f"{issue.message}\n")
|
|
938
|
+
for line in _render_text(entries, board_path, quiet):
|
|
939
|
+
stream.write(f"{line}\n")
|
|
940
|
+
raise typer.Exit(int(ExitCode.SUCCESS))
|