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/model/manifest.py
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Manifest data model for .alpmodel packages (canonical Python form).
|
|
3
|
+
|
|
4
|
+
`Target.caveats` is ADDITIVE AND OPTIONAL ON THE WIRE, deliberately (see
|
|
5
|
+
`_target_dict`): it is emitted only when non-empty, so a package with no
|
|
6
|
+
caveated target is byte-identical to one this module produced before the
|
|
7
|
+
field existed -- including alp-sdk's three committed C-test fixtures
|
|
8
|
+
(`tests/fixtures/alpmodel/minimal.alpmodel`,
|
|
9
|
+
`tests/unit/alpmodel_reader/src/fixture.h`, `tests/yocto/onnx_cpu_fixture.h`,
|
|
10
|
+
written by `tan.model._gen_fixture`), which therefore did NOT have to be
|
|
11
|
+
regenerated for it.
|
|
12
|
+
|
|
13
|
+
WHY IT IS SAFE TO ADD WITHOUT MOVING `package.CONTAINER_VERSION` -- measured,
|
|
14
|
+
not assumed. alp-sdk's on-device reader (`src/common/alp_model.c`,
|
|
15
|
+
`alp_model_parse`) walks the manifest key-by-key and ends BOTH its top-level
|
|
16
|
+
map loop and its per-target map loop with `else { ok = zcbor_any_skip(zs,
|
|
17
|
+
NULL); }`, so a key it does not know is skipped, not mis-indexed and not
|
|
18
|
+
rejected. `zcbor_any_skip` recurses through a nested list/map on a LOCAL state
|
|
19
|
+
copy (`zcbor_decode.c`), drawing nothing from the `zcbor_state_t zs[8]` backup
|
|
20
|
+
budget that reader sizes for its own explicit nesting. Compiled natively
|
|
21
|
+
against real zcbor and fed a container carrying this exact per-target
|
|
22
|
+
`caveats` list, that reader returns ALP_OK with every existing field
|
|
23
|
+
byte-identical. Bumping the CONTAINER_VERSION instead would make the same
|
|
24
|
+
reader return ALP_ERR_VERSION (-11) for every fielded device -- a breaking
|
|
25
|
+
change, for a field no on-device reader needs to read.
|
|
26
|
+
|
|
27
|
+
Keep the wire shape FLAT (a list of text strings). The reader's skip path
|
|
28
|
+
recurses in C per nesting level; a caveat payload has no reason to nest, and a
|
|
29
|
+
deeply nested one would spend stack the on-device reader never budgeted for.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
import json as _json
|
|
33
|
+
import cbor2 as _cbor2
|
|
34
|
+
from dataclasses import dataclass, field, asdict
|
|
35
|
+
|
|
36
|
+
MANIFEST_SCHEMA_VERSION = 1
|
|
37
|
+
|
|
38
|
+
_TENSOR_KEYS = {"dtype", "rank", "shape", "scale", "zp"}
|
|
39
|
+
_TARGET_KEYS = {"backend", "silicon_ref", "blob_format", "accel_config",
|
|
40
|
+
"arena", "requires", "blob", "compiler_version", "caveats"}
|
|
41
|
+
_COV_KEYS = {"backend", "accel_config", "status", "reason"}
|
|
42
|
+
|
|
43
|
+
# The blob formats the SDK can describe, ENFORCED at decode (tan-cli#1074):
|
|
44
|
+
# `from_dict` rejects a `targets[]` entry whose `blob_format` is not a member
|
|
45
|
+
# of this set, naming the offending value and the accepted set. Reconciled
|
|
46
|
+
# against every real producer on `dev` at the time this became enforced --
|
|
47
|
+
# `adapters/cpu.py` ("tflite"), `adapters/ethos_u.py` ("vela_tflite"),
|
|
48
|
+
# `adapters/drpai.py` ("drpai_dir"), `adapters/deepx.py` ("dxnn"),
|
|
49
|
+
# `adapters/executorch.py` ("executorch") -- plus `_gen_fixture.py`'s
|
|
50
|
+
# `_onnx_cpu_manifest` ("onnx", the issue #1254 regression fixture decoded by
|
|
51
|
+
# alp-sdk's real on-device reader, `tests/yocto/onnx_cpu_fixture.h`). The
|
|
52
|
+
# omission of "executorch" from this set is exactly why enforcement was
|
|
53
|
+
# deferred past the four other guards PR #1068 closed in the same module: the
|
|
54
|
+
# set itself had already drifted out of sync with a live, default-registered
|
|
55
|
+
# adapter, and an unenforced constant cannot be wrong loudly -- it silently
|
|
56
|
+
# stopped meaning what its own comment claimed.
|
|
57
|
+
#
|
|
58
|
+
# The authoritative ACCEPTOR of these six strings is alp-sdk's own
|
|
59
|
+
# `src/backends/inference/alp_model_select.c` `_fmt_enum()` -- this set is
|
|
60
|
+
# duplicated truth across two repos with no parity gate between them. If
|
|
61
|
+
# alp-sdk's reader ever adds or drops a case, this set has to move to match
|
|
62
|
+
# it (not the other way around): `_fmt_enum()` runs on real hardware, this
|
|
63
|
+
# set only decides what `tan` will hand it.
|
|
64
|
+
VALID_BLOB_FORMATS = frozenset({"vela_tflite", "tflite", "drpai_dir", "dxnn", "onnx", "executorch"})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _check_type(v: object, expected: type | tuple[type, ...], type_desc: str,
|
|
68
|
+
label: str, context: str) -> None:
|
|
69
|
+
"""The single type gate every guard in this module routes through --
|
|
70
|
+
shared by `_required_field` (a mapping key that must be there),
|
|
71
|
+
`_optional_field` (one that may not be) and `_check_element_types` (a
|
|
72
|
+
list element), so the curated message shape and the `bool` rule below are
|
|
73
|
+
written once rather than per call site.
|
|
74
|
+
|
|
75
|
+
`bool` is rejected whenever `expected` names `int` but not `bool` itself
|
|
76
|
+
(tan-cli#1058 review round 3): Python's `bool` is an `int` subclass, so
|
|
77
|
+
`isinstance(True, int)` is `True` and a plain `isinstance` check would
|
|
78
|
+
otherwise let `arena=True` / `requires["sram_kib"]=True` / `shape=[True]`
|
|
79
|
+
decode silently -- re-emitted on the CBOR wire as byte `0xf5` (major type
|
|
80
|
+
7, "true"), not an unsigned int -- straight into
|
|
81
|
+
`src/backends/inference/alp_model_select.c`'s fit gate as
|
|
82
|
+
`t->req_sram_kib`. A mechanism check here, not a per-field one, so every
|
|
83
|
+
current and future `(int, ...)` entry in `_TENSOR_TYPES`/`_TARGET_TYPES`/
|
|
84
|
+
`_REQUIRES_TYPES`/`_COVERAGE_TYPES`/`_TENSOR_ELEMENT_TYPES`/
|
|
85
|
+
`_TARGET_ELEMENT_TYPES` inherits it automatically. The one comparison in
|
|
86
|
+
this module that does NOT route through here is `from_dict`'s
|
|
87
|
+
schema-version gate, which therefore restates the rule in its own terms
|
|
88
|
+
(tan-cli#1064)."""
|
|
89
|
+
expected_types = expected if isinstance(expected, tuple) else (expected,)
|
|
90
|
+
if not isinstance(v, expected) or (isinstance(v, bool) and bool not in expected_types):
|
|
91
|
+
where = f" in {context}" if context else ""
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"malformed .alpmodel manifest: {label} must be {type_desc}, got {type(v).__name__}{where}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _required_field(d: dict, key: str, expected: type | tuple[type, ...], type_desc: str,
|
|
98
|
+
*, context: str = "") -> object:
|
|
99
|
+
"""Look up a required field in a decoded `.alpmodel` manifest document (a
|
|
100
|
+
JSON object or CBOR map that has already passed the document-level
|
|
101
|
+
`isinstance(d, dict)` guard in `from_json`/`from_cbor`), raising a
|
|
102
|
+
curated `ValueError` naming the field instead of letting the bare
|
|
103
|
+
subscript this replaces escape as a raw `KeyError` (field absent) or,
|
|
104
|
+
worse, being handed to a coercing constructor that manufactures a wrong
|
|
105
|
+
value from the wrong type (`from_cbor`'s old `bytes(d["src_sha"])`
|
|
106
|
+
zero-filled/byte-wise-coerced an int/list/bool `src_sha` into a bogus
|
|
107
|
+
hash with no diagnostic at all -- tan-cli#1023 review round 2, the
|
|
108
|
+
document-level guard stopped one field short of `name`/`src_sha`
|
|
109
|
+
themselves).
|
|
110
|
+
|
|
111
|
+
`context`, when given, is appended to both messages as "in <context>" --
|
|
112
|
+
`_decode_list_field` below passes e.g. `"targets[2]"` so a malformed
|
|
113
|
+
nested element's error names WHICH element, the same way the top-level
|
|
114
|
+
calls (no `context`) name the document itself. Genuinely the same check
|
|
115
|
+
both shapes need: `d` here is a decoded sub-document (a `targets[]`
|
|
116
|
+
element) exactly as much as it is the top-level manifest.
|
|
117
|
+
|
|
118
|
+
`.alpmodel` packages are machine-generated CBOR/JSON, not hand-authored
|
|
119
|
+
documents like `board.yaml` -- but `package.py`'s own bounds-checked
|
|
120
|
+
header reads already treat their bytes as wire data, not trusted
|
|
121
|
+
first-party output, and a corrupted `src_sha` (the model-source identity
|
|
122
|
+
field written by `build.py:222`) is exactly the class of corruption this
|
|
123
|
+
container's own bounds checks exist to catch. Fail loudly here, the same
|
|
124
|
+
way, rather than degrade silently.
|
|
125
|
+
|
|
126
|
+
The type half of the check lives in `_check_type` above, shared with
|
|
127
|
+
`_optional_field` and `_check_element_types`."""
|
|
128
|
+
if key not in d:
|
|
129
|
+
where = f" in {context}" if context else ""
|
|
130
|
+
raise ValueError(f"malformed .alpmodel manifest: missing required field {key!r}{where}")
|
|
131
|
+
_check_type(d[key], expected, type_desc, f"field {key!r}", context)
|
|
132
|
+
return d[key]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _optional_field(d: dict, key: str, expected: type | tuple[type, ...], type_desc: str,
|
|
136
|
+
*, context: str = "") -> object | None:
|
|
137
|
+
"""`_required_field`'s sibling for a field that is OPTIONAL on the wire:
|
|
138
|
+
ABSENT is fine (the dataclass default stands), PRESENT-but-wrong-typed
|
|
139
|
+
raises exactly the curated `ValueError` a required field would
|
|
140
|
+
(tan-cli#1056).
|
|
141
|
+
|
|
142
|
+
A separate helper rather than a flag on `_required_field`, because the
|
|
143
|
+
two differ ONLY in the missing-key branch and overloading that branch
|
|
144
|
+
would make each call site's intent depend on an argument instead of on
|
|
145
|
+
which function it names.
|
|
146
|
+
|
|
147
|
+
Returns `None` for an absent field. That is unambiguous rather than
|
|
148
|
+
merely convenient: no `expected` in this module admits `NoneType`, so a
|
|
149
|
+
field that is PRESENT and `None` raises above and never reaches the
|
|
150
|
+
return -- the caller can read `None` as "absent" without a sentinel."""
|
|
151
|
+
if key not in d:
|
|
152
|
+
return None
|
|
153
|
+
_check_type(d[key], expected, type_desc, f"field {key!r}", context)
|
|
154
|
+
return d[key]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _pick(d: dict, keys: set) -> dict:
|
|
158
|
+
return {k: d[k] for k in keys if k in d} # drop unknown keys + tolerate missing-known
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _check_nested_types(value: dict, inner_types: dict[str, tuple[type | tuple[type, ...], str]],
|
|
162
|
+
context: str) -> None:
|
|
163
|
+
"""One level of `_required_field` under a field `_decode_list_field` has
|
|
164
|
+
already confirmed is a `dict` (`value`) -- see `nested_types` on that
|
|
165
|
+
function and `_REQUIRES_TYPES` below for the why. Only keys present in
|
|
166
|
+
`inner_types` AND in `value` are checked; a missing inner key is left
|
|
167
|
+
alone, matching `_pick`'s tolerate-missing-known convention elsewhere in
|
|
168
|
+
this module."""
|
|
169
|
+
for inner_key, (inner_expected, inner_desc) in inner_types.items():
|
|
170
|
+
if inner_key in value:
|
|
171
|
+
_required_field(value, inner_key, inner_expected, inner_desc, context=context)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_element_types(value: list, expected: type | tuple[type, ...], type_desc: str,
|
|
175
|
+
context: str) -> None:
|
|
176
|
+
"""`_check_nested_types`'s list-shaped sibling: one level of `_check_type`
|
|
177
|
+
under a field `_check_element_fields` has already confirmed is a `list`
|
|
178
|
+
(tan-cli#1063). The dict helper above takes a per-KEY map; a list has no
|
|
179
|
+
keys, so this one takes a single ELEMENT type and names the offending
|
|
180
|
+
index where the dict side names the key -- `element 1 must be an int, got
|
|
181
|
+
str in inputs[0].shape` is the exact counterpart of `field 'sram_kib'
|
|
182
|
+
must be an int, got str in targets[0].requires`.
|
|
183
|
+
|
|
184
|
+
Every element is checked, not just the first: a `shape` whose second
|
|
185
|
+
entry is the corrupted one is as wrong as one whose first is."""
|
|
186
|
+
for i, elem in enumerate(value):
|
|
187
|
+
_check_type(elem, expected, type_desc, f"element {i}", context)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_schema_version(d: dict) -> None:
|
|
191
|
+
"""Both `from_json` and `from_cbor` route through `from_dict` (tan-cli#1039)
|
|
192
|
+
-- this check is therefore written ONCE and enforced on both reader
|
|
193
|
+
paths, rather than living only in the JSON-era code `from_dict` used to
|
|
194
|
+
be (`from_cbor` built a `Manifest` directly and never called `from_dict`
|
|
195
|
+
at all, so a future manifest version read by an older `tan` over CBOR --
|
|
196
|
+
the PRODUCTION reader path for `.alpmodel` packages -- was silently
|
|
197
|
+
parsed as v1 instead of refused).
|
|
198
|
+
|
|
199
|
+
`type(v) is not int`, NOT `isinstance` (tan-cli#1064): `bool` is an
|
|
200
|
+
`int` subclass and `True == 1`, so the bare comparison accepted `v=True`
|
|
201
|
+
as v1 -- and float `1.0` too. This is the one comparison in this module
|
|
202
|
+
that reads a decoded value without routing through
|
|
203
|
+
`_required_field`/`_check_type`, so it restates their `bool` rule in its
|
|
204
|
+
own terms rather than inheriting it. A future manifest version written
|
|
205
|
+
as `1.0` or `True` is a manifest this reader does not understand, and
|
|
206
|
+
must be refused as loudly as `v=2` already was."""
|
|
207
|
+
v = d.get("v", MANIFEST_SCHEMA_VERSION)
|
|
208
|
+
if type(v) is not int or v != MANIFEST_SCHEMA_VERSION:
|
|
209
|
+
raise ValueError(f"unsupported manifest version {v!r}; expected {MANIFEST_SCHEMA_VERSION}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _check_blob_formats(targets: list["Target"]) -> None:
|
|
213
|
+
"""`_TARGET_TYPES["blob_format"]` (used by `_check_element_fields` while
|
|
214
|
+
decoding `targets`) only confirms each target's `blob_format` is a `str`
|
|
215
|
+
-- it says nothing about WHICH string. `VALID_BLOB_FORMATS` was a real
|
|
216
|
+
constant instead of a comment for exactly the reason this function now
|
|
217
|
+
exists: an unlisted `blob_format` is now rejected ON DECODE (tan-cli#1074).
|
|
218
|
+
|
|
219
|
+
This is a READ-side guard only, not a write-side one -- `build.py`'s
|
|
220
|
+
`Blob(format=...)` construction never consults `VALID_BLOB_FORMATS` at
|
|
221
|
+
all, so a new adapter can still WRITE an unlisted format; it can no
|
|
222
|
+
longer do so silently, because the package this function's caller
|
|
223
|
+
guards is the same one every reader -- `tan model check`, flash, deploy
|
|
224
|
+
-- decodes. `python/tests/gates/test_blob_format_producers_stay_in_valid_blob_formats.py`
|
|
225
|
+
is the write-side half: it walks the adapter/fixture SOURCE for every
|
|
226
|
+
literal `blob_format` a producer emits and fails if one is missing from
|
|
227
|
+
this set, which is what actually keeps a new backend from shipping
|
|
228
|
+
silently -- this function alone only refuses to read the result back.
|
|
229
|
+
|
|
230
|
+
Called from `from_dict` once every target is already a `Target`
|
|
231
|
+
instance, rather than threaded through `_check_element_fields` as a
|
|
232
|
+
fourth kind of per-field check (type / nested / element) -- membership
|
|
233
|
+
against a closed value set is the only check in this module that is not
|
|
234
|
+
a shape check, and a purpose-built function reads more plainly than a new
|
|
235
|
+
parameter every OTHER `_decode_list_field` caller would have to pass
|
|
236
|
+
`None` for."""
|
|
237
|
+
for i, t in enumerate(targets):
|
|
238
|
+
if t.blob_format not in VALID_BLOB_FORMATS:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"malformed .alpmodel manifest: field 'blob_format' must be "
|
|
241
|
+
f"one of {sorted(VALID_BLOB_FORMATS)}, got {t.blob_format!r} "
|
|
242
|
+
f"in targets[{i}]"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _check_element_fields(elem: dict, item_keys: set, required: frozenset,
|
|
247
|
+
field_types: dict[str, tuple[type | tuple[type, ...], str]],
|
|
248
|
+
nested_types: dict | None, element_types: dict | None,
|
|
249
|
+
context: str) -> None:
|
|
250
|
+
"""Guard ONE decoded element of a nested list field.
|
|
251
|
+
|
|
252
|
+
`required` fields first (alphabetically, so a message names the same
|
|
253
|
+
field it always has), then the OPTIONAL remainder of `item_keys` --
|
|
254
|
+
`Target.compiler_version`/`caveats` are carried through to the
|
|
255
|
+
constructor by `_pick` but sit in no `required` set, so before
|
|
256
|
+
tan-cli#1056 nothing type-checked them at all and
|
|
257
|
+
`{"compiler_version": 12345, "caveats": "not-a-list"}` constructed
|
|
258
|
+
silently.
|
|
259
|
+
|
|
260
|
+
`field_types` gives each field a REAL `expected` type, not the
|
|
261
|
+
accepts-anything `object` an earlier version passed (tan-cli#1049).
|
|
262
|
+
`nested_types` routes an already-checked-`dict` value through
|
|
263
|
+
`_check_nested_types` (see `_REQUIRES_TYPES`); `element_types` routes an
|
|
264
|
+
already-checked-`list` value through `_check_element_types` (see
|
|
265
|
+
`_TENSOR_ELEMENT_TYPES`, tan-cli#1063)."""
|
|
266
|
+
for field_name in sorted(required) + sorted(item_keys - required):
|
|
267
|
+
expected, type_desc = field_types[field_name]
|
|
268
|
+
if field_name in required:
|
|
269
|
+
value = _required_field(elem, field_name, expected, type_desc, context=context)
|
|
270
|
+
else:
|
|
271
|
+
value = _optional_field(elem, field_name, expected, type_desc, context=context)
|
|
272
|
+
if value is None:
|
|
273
|
+
continue
|
|
274
|
+
inner_types = nested_types.get(field_name) if nested_types else None
|
|
275
|
+
if inner_types:
|
|
276
|
+
_check_nested_types(value, inner_types, context=f"{context}.{field_name}")
|
|
277
|
+
elem_type = element_types.get(field_name) if element_types else None
|
|
278
|
+
if elem_type:
|
|
279
|
+
_check_element_types(value, elem_type[0], elem_type[1],
|
|
280
|
+
context=f"{context}.{field_name}")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _decode_list_field(d: dict, key: str, item_keys: set, required: frozenset, ctor,
|
|
284
|
+
field_types: dict[str, tuple[type | tuple[type, ...], str]],
|
|
285
|
+
nested_types: dict[str, dict[str, tuple[type | tuple[type, ...], str]]]
|
|
286
|
+
| None = None,
|
|
287
|
+
element_types: dict[str, tuple[type | tuple[type, ...], str]]
|
|
288
|
+
| None = None) -> list:
|
|
289
|
+
"""Decode a nested `.alpmodel` manifest list field (`inputs`/`outputs`/
|
|
290
|
+
`targets`/`coverage`) into `ctor` instances, guarding both the field and
|
|
291
|
+
each element the way `_required_field` guards a top-level one
|
|
292
|
+
(tan-cli#1040). Before this, every reader built these lists straight out
|
|
293
|
+
of `ctor(**t)` (`from_dict`) or `ctor(**_pick(t, item_keys))` (the old
|
|
294
|
+
`from_cbor`) with no element-type or completeness check: a non-mapping
|
|
295
|
+
element degrades `_pick` to `{}` rather than raising (`k in 'abc'` is a
|
|
296
|
+
legal, false, substring check), so a raw `TypeError` out of the dataclass
|
|
297
|
+
constructor was the first thing to notice, not a curated `ValueError` --
|
|
298
|
+
`inputs=['abc']` -> `Tensor.__init__() missing 5 required positional
|
|
299
|
+
arguments`; an incomplete `targets[]` entry names 7. `from_dict`'s own
|
|
300
|
+
bare `Tensor(**t)` (no `_pick`) failed even earlier on the `**` unpack
|
|
301
|
+
itself. One helper now closes both readers, since `from_cbor` routes
|
|
302
|
+
through `from_dict`. The per-field work moved to `_check_element_fields`
|
|
303
|
+
above -- see it for what `field_types`/`nested_types`/`element_types`
|
|
304
|
+
each do.
|
|
305
|
+
|
|
306
|
+
The field-level check below (is `d[key]` a list at all) is the same
|
|
307
|
+
shape one step up: a `targets` value that decodes but isn't a list would
|
|
308
|
+
otherwise reach `enumerate()` and, for a string, iterate its CHARACTERS
|
|
309
|
+
as "elements" -- silently wrong, not a crash."""
|
|
310
|
+
elems = d.get(key, [])
|
|
311
|
+
if not isinstance(elems, list):
|
|
312
|
+
raise ValueError(
|
|
313
|
+
f"malformed .alpmodel manifest: field {key!r} must be a list, got {type(elems).__name__}"
|
|
314
|
+
)
|
|
315
|
+
out = []
|
|
316
|
+
for i, elem in enumerate(elems):
|
|
317
|
+
if not isinstance(elem, dict):
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f"malformed .alpmodel manifest: {key}[{i}] must be a mapping, "
|
|
320
|
+
f"got {type(elem).__name__}"
|
|
321
|
+
)
|
|
322
|
+
_check_element_fields(elem, item_keys, required, field_types,
|
|
323
|
+
nested_types, element_types, context=f"{key}[{i}]")
|
|
324
|
+
out.append(ctor(**_pick(elem, item_keys)))
|
|
325
|
+
return out
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
_TARGET_REQUIRED = frozenset(_TARGET_KEYS - {"compiler_version", "caveats"})
|
|
329
|
+
|
|
330
|
+
# Per-field `(expected type, type_desc)` for `_decode_list_field`'s element
|
|
331
|
+
# guard (tan-cli#1049) -- one entry per member of the `required` frozenset
|
|
332
|
+
# each caller below passes, so every required field of `Tensor`/`Target`/
|
|
333
|
+
# `Coverage` gets a real type check, not the `object`-accepts-anything one
|
|
334
|
+
# `_required_field`'s presence-only call used before -- `arena` and
|
|
335
|
+
# `requires["sram_kib"]` are exactly the figures
|
|
336
|
+
# `src/backends/inference/alp_model_select.c`'s fit gate reads, so a
|
|
337
|
+
# wrong-typed value there was the silent-wrong-value class, not a crash.
|
|
338
|
+
# `scale` accepts `(int, float)`: a whole-number scale is legal JSON/CBOR
|
|
339
|
+
# content (`1` is as valid as `1.0`) and the dataclass field is a plain
|
|
340
|
+
# `float` annotation, not an enforced constraint -- rejecting `int` here
|
|
341
|
+
# would be a new, undocumented restriction on well-formed input, not a
|
|
342
|
+
# defect fix.
|
|
343
|
+
_TENSOR_TYPES = {
|
|
344
|
+
"dtype": (str, "a string"),
|
|
345
|
+
"rank": (int, "an int"),
|
|
346
|
+
"shape": (list, "a list"),
|
|
347
|
+
"scale": ((int, float), "a number"),
|
|
348
|
+
"zp": (int, "an int"),
|
|
349
|
+
}
|
|
350
|
+
_TARGET_TYPES = {
|
|
351
|
+
"backend": (str, "a string"),
|
|
352
|
+
"silicon_ref": (str, "a string"),
|
|
353
|
+
"blob_format": (str, "a string"),
|
|
354
|
+
"accel_config": (str, "a string"),
|
|
355
|
+
"arena": (int, "an int"),
|
|
356
|
+
"requires": (dict, "a mapping"),
|
|
357
|
+
"blob": (int, "an int"),
|
|
358
|
+
# OPTIONAL on the wire (`_TARGET_REQUIRED` subtracts both), so these two
|
|
359
|
+
# reach `_check_element_fields` through `_optional_field`, not
|
|
360
|
+
# `_required_field`: absent is legal, present-but-wrong-typed is not.
|
|
361
|
+
# Before tan-cli#1056 they were in `_TARGET_KEYS` (so `_pick` handed them
|
|
362
|
+
# to the constructor) but in no type map at all, so
|
|
363
|
+
# `compiler_version=12345` / `caveats="not-a-list"` decoded silently.
|
|
364
|
+
"compiler_version": (str, "a string"),
|
|
365
|
+
"caveats": (list, "a list"),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# One level deeper than `_TARGET_TYPES["requires"]`, which only guards the
|
|
369
|
+
# container is a mapping at all (tan-cli#1049 review round 2). `sram_kib` is
|
|
370
|
+
# the other half of the same fit-gate figure `arena` is -- when present it
|
|
371
|
+
# must be an `int`, the same way `arena` itself already is. `op_features` is
|
|
372
|
+
# not checked here: nothing downstream reads it as anything but an opaque
|
|
373
|
+
# list yet, so adding a shape requirement for it now would be a new,
|
|
374
|
+
# undocumented restriction rather than closing a known gap.
|
|
375
|
+
_REQUIRES_TYPES = {
|
|
376
|
+
"sram_kib": (int, "an int"),
|
|
377
|
+
}
|
|
378
|
+
# Element types for a field whose own declared type is `list` -- the entries
|
|
379
|
+
# above guard the CONTAINER only, which is the same shape tan-cli#1049 closed
|
|
380
|
+
# for `Target.requires` one level down (tan-cli#1063). `shape` feeds
|
|
381
|
+
# tensor-shape maths downstream the way `requires["sram_kib"]` feeds
|
|
382
|
+
# `src/backends/inference/alp_model_select.c`'s fit gate, so
|
|
383
|
+
# `shape=["a","b"]` / `[None,None]` / `[{"a":1}]` / `[True,False]` were all
|
|
384
|
+
# silent-wrong values, not crashes. `caveats` is `list[str]` that `tan model
|
|
385
|
+
# build` and `tan model check --exact` render as customer-readable text, so a
|
|
386
|
+
# non-string element degrades a diagnostic surface silently (tan-cli#1056).
|
|
387
|
+
#
|
|
388
|
+
# `op_features` inside `requires` deliberately gets no entry here, for the
|
|
389
|
+
# same reason `_REQUIRES_TYPES` omits it: nothing downstream reads it as
|
|
390
|
+
# anything but an opaque list yet, so a shape requirement would be a new,
|
|
391
|
+
# undocumented restriction rather than a closed gap.
|
|
392
|
+
_TENSOR_ELEMENT_TYPES = {
|
|
393
|
+
"shape": (int, "an int"),
|
|
394
|
+
}
|
|
395
|
+
_TARGET_ELEMENT_TYPES = {
|
|
396
|
+
"caveats": (str, "a string"),
|
|
397
|
+
}
|
|
398
|
+
_COVERAGE_TYPES = {
|
|
399
|
+
"backend": (str, "a string"),
|
|
400
|
+
"accel_config": (str, "a string"),
|
|
401
|
+
"status": (str, "a string"),
|
|
402
|
+
"reason": (str, "a string"),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _json_default(d: dict) -> dict:
|
|
407
|
+
out = dict(d)
|
|
408
|
+
out["src_sha"] = d["src_sha"].hex() # bytes -> hex string for JSON
|
|
409
|
+
return out
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _target_dict(t: "Target") -> dict:
|
|
413
|
+
"""One target's wire form: `asdict`, minus an EMPTY `caveats`.
|
|
414
|
+
|
|
415
|
+
Dropping the empty case is what keeps this field additive in the strict
|
|
416
|
+
sense -- a package whose targets carry no caveat encodes to exactly the
|
|
417
|
+
bytes it did before `caveats` existed, so no committed fixture, no shipped
|
|
418
|
+
package and no on-device footprint moves for a field that has nothing to
|
|
419
|
+
say. Absent means "no caveats"; every reader must treat it that way (the
|
|
420
|
+
`_pick`-based `_decode_list_field` both `from_json` and `from_cbor` route
|
|
421
|
+
through already does, and so does `Target.caveats`'s own default)."""
|
|
422
|
+
d = asdict(t)
|
|
423
|
+
if not d["caveats"]:
|
|
424
|
+
del d["caveats"]
|
|
425
|
+
return d
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@dataclass
|
|
429
|
+
class Tensor:
|
|
430
|
+
dtype: str
|
|
431
|
+
rank: int
|
|
432
|
+
shape: list[int]
|
|
433
|
+
scale: float
|
|
434
|
+
zp: int
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@dataclass
|
|
438
|
+
class Target:
|
|
439
|
+
backend: str # cpu | ethos_u | drpai | deepx_dxm1
|
|
440
|
+
silicon_ref: str # e.g. "alif:ensemble:e8" or "*"
|
|
441
|
+
blob_format: str # one of VALID_BLOB_FORMATS; enforced by Manifest.from_dict
|
|
442
|
+
accel_config: str # "" when N/A
|
|
443
|
+
arena: int
|
|
444
|
+
requires: dict[str, object] # {"sram_kib": int, "op_features": list[str]}
|
|
445
|
+
blob: int # index into the package blob table
|
|
446
|
+
compiler_version: str = "" # e.g. "vela 4.1.0" | "passthrough"; "" when unknown
|
|
447
|
+
# The compiler's OWN unresolved caveats about the blob shipped for THIS
|
|
448
|
+
# target -- each already a complete, customer-readable sentence, carried
|
|
449
|
+
# verbatim from `adapters.Blob.caveats`. Today that is vela's "Compilation
|
|
450
|
+
# may be invalid or non-optimal" verdict when it fell back to its BUILT-IN
|
|
451
|
+
# default profile, whose memory model may not be the module's.
|
|
452
|
+
#
|
|
453
|
+
# This field exists because `arena` and `requires["sram_kib"]` beside it
|
|
454
|
+
# are exactly the figures alp-sdk's on-device selector consumes
|
|
455
|
+
# (`return e->arena_sram_kib == 0u || t->req_sram_kib <= e->arena_sram_kib;`,
|
|
456
|
+
# src/backends/inference/alp_model_select.c) -- so a blob compiled against
|
|
457
|
+
# a memory model the module does not have must not be able to reach a
|
|
458
|
+
# board with the package silent about it. `tan model check --exact` was
|
|
459
|
+
# already surfacing these; `tan model build` dropped them on the floor,
|
|
460
|
+
# which is the path that actually ships bytes to hardware.
|
|
461
|
+
#
|
|
462
|
+
# It is DIAGNOSTIC, never a substitute for refusing: a figure that cannot
|
|
463
|
+
# be derived is refused at the adapter
|
|
464
|
+
# (`ethos_u._refuse_zero_sram_footprint`), not caveated and shipped.
|
|
465
|
+
# Empty is omitted from the wire form entirely -- see `_target_dict`.
|
|
466
|
+
caveats: list[str] = field(default_factory=list)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@dataclass
|
|
470
|
+
class Coverage:
|
|
471
|
+
"""A backend the package did NOT include a blob for, and why."""
|
|
472
|
+
|
|
473
|
+
backend: str
|
|
474
|
+
accel_config: str
|
|
475
|
+
status: str # compiled | skipped | incompatible
|
|
476
|
+
reason: str
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass
|
|
480
|
+
class Manifest:
|
|
481
|
+
name: str
|
|
482
|
+
src_sha: bytes
|
|
483
|
+
inputs: list[Tensor] = field(default_factory=list)
|
|
484
|
+
outputs: list[Tensor] = field(default_factory=list)
|
|
485
|
+
targets: list[Target] = field(default_factory=list)
|
|
486
|
+
coverage: list[Coverage] = field(default_factory=list)
|
|
487
|
+
|
|
488
|
+
def to_dict(self) -> dict:
|
|
489
|
+
return {
|
|
490
|
+
"v": MANIFEST_SCHEMA_VERSION,
|
|
491
|
+
"name": self.name,
|
|
492
|
+
"src_sha": self.src_sha,
|
|
493
|
+
"inputs": [asdict(t) for t in self.inputs],
|
|
494
|
+
"outputs": [asdict(t) for t in self.outputs],
|
|
495
|
+
"targets": [_target_dict(t) for t in self.targets],
|
|
496
|
+
"coverage": [asdict(c) for c in self.coverage],
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@classmethod
|
|
500
|
+
def from_dict(cls, d: dict) -> "Manifest":
|
|
501
|
+
_check_schema_version(d)
|
|
502
|
+
# `name`/`src_sha` were still bare subscripts here even after
|
|
503
|
+
# `from_json`/`from_cbor`'s own document-level + field-level guards
|
|
504
|
+
# (tan-cli#1074): a `from_dict` mapping missing either key raised a
|
|
505
|
+
# raw `KeyError` naming only the key, not the curated `ValueError`
|
|
506
|
+
# every other field in this module raises -- no context, no "malformed
|
|
507
|
+
# .alpmodel manifest" framing, nothing a caller could route on
|
|
508
|
+
# alongside the rest of this module's error contract. `from_dict` is
|
|
509
|
+
# the shared POST-NORMALISATION entry point both `from_json` (hex
|
|
510
|
+
# str -> bytes) and `from_cbor` (bytearray -> bytes) route through
|
|
511
|
+
# after decoding their own wire form of `src_sha` -- its own contract
|
|
512
|
+
# is therefore `bytes`, matching what both callers, and the direct
|
|
513
|
+
# round-trip caller in `tests/model/test_manifest.py`, already hand
|
|
514
|
+
# it.
|
|
515
|
+
# Computed as locals, in the SAME left-to-right order the original
|
|
516
|
+
# `cls(...)` keyword-argument list used (name, src_sha, inputs,
|
|
517
|
+
# outputs, targets, coverage) -- `targets` needs to exist as a local
|
|
518
|
+
# before `_check_blob_formats` can run on it, but that must not
|
|
519
|
+
# change WHICH error a malformed document reports first: a manifest
|
|
520
|
+
# missing `name` AND carrying a bad target still reports the
|
|
521
|
+
# missing `name`, the document's own required field, ahead of a
|
|
522
|
+
# nested element's (PR #1098 review nit).
|
|
523
|
+
name = _required_field(d, "name", str, "a string")
|
|
524
|
+
src_sha = _required_field(d, "src_sha", bytes, "a bytes object")
|
|
525
|
+
inputs = _decode_list_field(d, "inputs", _TENSOR_KEYS, _TENSOR_KEYS, Tensor, _TENSOR_TYPES,
|
|
526
|
+
element_types=_TENSOR_ELEMENT_TYPES)
|
|
527
|
+
outputs = _decode_list_field(d, "outputs", _TENSOR_KEYS, _TENSOR_KEYS, Tensor, _TENSOR_TYPES,
|
|
528
|
+
element_types=_TENSOR_ELEMENT_TYPES)
|
|
529
|
+
targets = _decode_list_field(d, "targets", _TARGET_KEYS, _TARGET_REQUIRED, Target, _TARGET_TYPES,
|
|
530
|
+
nested_types={"requires": _REQUIRES_TYPES},
|
|
531
|
+
element_types=_TARGET_ELEMENT_TYPES)
|
|
532
|
+
_check_blob_formats(targets) # tan-cli#1074: reject an unlisted blob_format
|
|
533
|
+
coverage = _decode_list_field(d, "coverage", _COV_KEYS, _COV_KEYS, Coverage, _COVERAGE_TYPES)
|
|
534
|
+
return cls(name=name, src_sha=src_sha, inputs=inputs, outputs=outputs,
|
|
535
|
+
targets=targets, coverage=coverage)
|
|
536
|
+
|
|
537
|
+
def to_json(self) -> str:
|
|
538
|
+
return _json.dumps(_json_default(self.to_dict()), indent=2)
|
|
539
|
+
|
|
540
|
+
@classmethod
|
|
541
|
+
def from_json(cls, text: str) -> "Manifest":
|
|
542
|
+
d = _json.loads(text)
|
|
543
|
+
if not isinstance(d, dict):
|
|
544
|
+
# Mirrors the `preset` guard in `targets.resolve_targets`
|
|
545
|
+
# (tan-cli#1018): a `.alpmodel` manifest that decodes but is
|
|
546
|
+
# not a JSON object (e.g. a bare list or a bare scalar) must
|
|
547
|
+
# not reach the bare `d["src_sha"]` subscript below -- that
|
|
548
|
+
# raises a raw TypeError ("list indices must be integers or
|
|
549
|
+
# slices, not str" / "string indices must be integers, not
|
|
550
|
+
# 'str'") instead of a curated error a caller can distinguish
|
|
551
|
+
# from any other TypeError in the call stack (tan-cli#1023).
|
|
552
|
+
# `package.py`'s own bounds-checked reads of the untrusted
|
|
553
|
+
# header already treat manifest bytes as wire data, not
|
|
554
|
+
# trusted first-party output -- a corrupt/adversarial manifest
|
|
555
|
+
# fails loudly here for the same reason, rather than being
|
|
556
|
+
# silently tolerated.
|
|
557
|
+
raise ValueError(
|
|
558
|
+
f"malformed .alpmodel manifest: expected a JSON object, got {type(d).__name__}"
|
|
559
|
+
)
|
|
560
|
+
# `d["name"]`/`d["src_sha"]` were still bare here even after the
|
|
561
|
+
# document-level guard above -- a manifest missing either raised a
|
|
562
|
+
# raw KeyError, and a non-string `src_sha` raised a raw
|
|
563
|
+
# `TypeError: fromhex() argument must be str, not int` out of
|
|
564
|
+
# `bytes.fromhex` below, neither distinguishable from an unrelated
|
|
565
|
+
# bug in the call stack (tan-cli#1023 review round 2). `_required_field`
|
|
566
|
+
# closes both the same way the document guard above does.
|
|
567
|
+
_required_field(d, "name", str, "a string")
|
|
568
|
+
src_sha_hex = _required_field(d, "src_sha", str, "a hex string")
|
|
569
|
+
try:
|
|
570
|
+
d["src_sha"] = bytes.fromhex(src_sha_hex)
|
|
571
|
+
except ValueError as exc:
|
|
572
|
+
raise ValueError(f"malformed .alpmodel manifest: field 'src_sha' is not valid hex: {exc}") from exc
|
|
573
|
+
return cls.from_dict(d)
|
|
574
|
+
|
|
575
|
+
def to_cbor(self) -> bytes:
|
|
576
|
+
return _cbor2.dumps(self.to_dict())
|
|
577
|
+
|
|
578
|
+
# tan-cli#1055: cbor2's decode exceptions (`CBORDecodeEOF` and its
|
|
579
|
+
# siblings, common base `CBORDecodeError`) are NOT `ValueError`
|
|
580
|
+
# subclasses, while `from_json`'s `json.JSONDecodeError` IS -- so a caller
|
|
581
|
+
# catching `ValueError` around a `.alpmodel` read, the contract every other
|
|
582
|
+
# guard in this module exists to honour, caught a malformed JSON manifest
|
|
583
|
+
# and missed a malformed CBOR one, on the PRODUCTION reader path.
|
|
584
|
+
# `package.read_package`/`read_manifest_file` bounds-check the manifest
|
|
585
|
+
# region's offset and length (tan-cli#1045) but never that its bytes are
|
|
586
|
+
# well-formed CBOR: a bit flip, a truncated write or plain non-CBOR bytes
|
|
587
|
+
# land in the `except` below.
|
|
588
|
+
@classmethod
|
|
589
|
+
def from_cbor(cls, blob: bytes) -> "Manifest":
|
|
590
|
+
try:
|
|
591
|
+
d = _cbor2.loads(blob)
|
|
592
|
+
except _cbor2.CBORDecodeError as exc:
|
|
593
|
+
raise ValueError(
|
|
594
|
+
f"malformed .alpmodel manifest: not valid CBOR "
|
|
595
|
+
f"({type(exc).__name__}: {exc})"
|
|
596
|
+
) from exc
|
|
597
|
+
if not isinstance(d, dict):
|
|
598
|
+
# See `from_json` above (tan-cli#1023): guard before the bare
|
|
599
|
+
# `d["name"]` subscript below, reached from both
|
|
600
|
+
# `package.read_manifest_file` and `package.read_package`.
|
|
601
|
+
raise ValueError(
|
|
602
|
+
f"malformed .alpmodel manifest: expected a CBOR map, got {type(d).__name__}"
|
|
603
|
+
)
|
|
604
|
+
# Same gap as `from_json` above, plus a worse failure mode: CBOR
|
|
605
|
+
# preserves `src_sha`'s wire type (unlike JSON's hex string), so an
|
|
606
|
+
# unguarded `bytes(d["src_sha"])` didn't raise at all for a
|
|
607
|
+
# wrong-typed `src_sha` -- `bytes(int)` is a zero-fill constructor
|
|
608
|
+
# and `bytes(list[int])` a byte-wise one, so a manifest whose
|
|
609
|
+
# `src_sha` CBOR major type got flipped from byte-string to
|
|
610
|
+
# unsigned-int/array/bool silently produced a `Manifest` carrying an
|
|
611
|
+
# INVENTED source hash (tan-cli#1023 review round 2, finding 1).
|
|
612
|
+
# `_required_field` requires the real wire type before `bytes()`
|
|
613
|
+
# ever runs on it.
|
|
614
|
+
_required_field(d, "name", str, "a string")
|
|
615
|
+
src_sha_raw = _required_field(d, "src_sha", (bytes, bytearray), "a byte string")
|
|
616
|
+
# Normalise in place (bytearray -> bytes, mirroring `from_json`'s
|
|
617
|
+
# hex-string -> bytes normalisation two methods up) and hand off to
|
|
618
|
+
# `from_dict` -- which is what actually enforces `MANIFEST_SCHEMA_VERSION`
|
|
619
|
+
# and guards the nested `inputs`/`outputs`/`targets`/`coverage`
|
|
620
|
+
# decodes (tan-cli#1039, tan-cli#1040). Before this, `from_cbor` built
|
|
621
|
+
# the `Manifest` directly and never reached either check: CBOR is the
|
|
622
|
+
# PRODUCTION reader path for `.alpmodel` packages, so the stricter
|
|
623
|
+
# checks used to sit on the path that matters less.
|
|
624
|
+
d["src_sha"] = bytes(src_sha_raw)
|
|
625
|
+
return cls.from_dict(d)
|