tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""SoM/SoC metadata readers -- the memory map, the capability set, the SoC ref.
|
|
4
|
+
|
|
5
|
+
RELOCATED (was alp-sdk `scripts/alp_project_loader.py`). These are the last
|
|
6
|
+
functions `tan/planner/*` reached for across the repo boundary: `carveout.py`,
|
|
7
|
+
`partition.py` and `loader.py` want `resolve_memory_map`, `libraries.py` and
|
|
8
|
+
`kconfig.py` want `resolve_capabilities`, and `kconfig.py` wants
|
|
9
|
+
`silicon_to_kconfig` + `som_unpopulated_capabilities`. While they lived in
|
|
10
|
+
alp-sdk every one of those module-scope imports dragged `alp_project` ->
|
|
11
|
+
`alp_project_loader` -> `alp_orchestrate` back into the process, so the planner
|
|
12
|
+
emitted from `tan` but still LOADED the SDK's Python.
|
|
13
|
+
|
|
14
|
+
**They read metadata; they do not carry it.** Every path below resolves under
|
|
15
|
+
`.paths.METADATA_ROOT`, i.e. the bound alp-sdk checkout -- the SoC JSON, the
|
|
16
|
+
SoM preset, the silicon-Kconfig registry. `metadata/**` stays in alp-sdk
|
|
17
|
+
(ADR-0017 / I-26): what moved is the derivation, never the fact. A copy of any
|
|
18
|
+
of these tables inside `tan` would be a second source of truth.
|
|
19
|
+
|
|
20
|
+
Implementations are the SDK's, moved substantially as-is: their precedence
|
|
21
|
+
rules (a SoM `memory_map:` override beating the SoC variant, SoC-level
|
|
22
|
+
`memory_regions` beating the per-bank derivation, SoM capabilities beating SoC
|
|
23
|
+
defaults, then the per-SKU `unpopulated` restriction forcing 0/False) are
|
|
24
|
+
accumulated behaviour that `tests/parity/test_planner_emit_parity.py` pins byte
|
|
25
|
+
for byte -- not something to re-derive.
|
|
26
|
+
|
|
27
|
+
`resolve_soc_path` itself is defined in the leaf module `tan.soc_ref` (ADR-0028
|
|
28
|
+
Task 2 follow-up) and re-exported here, so `tan.model.targets` can use it
|
|
29
|
+
without pulling in this package's own import-time metadata reads.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import functools
|
|
35
|
+
import json
|
|
36
|
+
import re
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from tan.soc_ref import resolve_soc_path # noqa: F401 (re-export: see below)
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------
|
|
43
|
+
# SKU-family mapping
|
|
44
|
+
# ---------------------------------------------------------------------
|
|
45
|
+
#
|
|
46
|
+
# The SKU-prefix -> family-dir map is a small, pure derivation with no
|
|
47
|
+
# second on-disk source, so we keep it inline. When a new SoM family
|
|
48
|
+
# lands, add the entry here + update the schema's `som.sku` pattern.
|
|
49
|
+
# (The silicon -> Kconfig mapping, by contrast, lives in the versioned
|
|
50
|
+
# registry below -- see silicon_to_kconfig().)
|
|
51
|
+
|
|
52
|
+
_SKU_FAMILY = re.compile(r"^E1M-(AEN|V2N|V2M)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sku_family(sku: str) -> str:
|
|
56
|
+
"""Return the SoM family directory name for a SKU string."""
|
|
57
|
+
m = _SKU_FAMILY.match(sku)
|
|
58
|
+
if m is None:
|
|
59
|
+
raise ValueError(f"unrecognised SoM SKU pattern: {sku}")
|
|
60
|
+
return {"AEN": "aen", "V2N": "v2n", "V2M": "v2n-m1"}[m.group(1)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Silicon ref -> Zephyr SoC-select Kconfig symbol.
|
|
64
|
+
#
|
|
65
|
+
# This is NOT a hand-maintained table: the symbol is computed from the
|
|
66
|
+
# ref, and the single source of the allowlist is the versioned registry
|
|
67
|
+
# at metadata/registries/silicon-kconfig.json.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@functools.lru_cache(maxsize=None)
|
|
71
|
+
def _load_silicon_kconfig(metadata_root: Path) -> tuple[str, frozenset[str]]:
|
|
72
|
+
"""Load (socSymbolPrefix, knownSilicon) from *metadata_root*'s versioned
|
|
73
|
+
registry. Cache is keyed on *metadata_root* so a second root in the
|
|
74
|
+
same process doesn't silently reuse the first root's table (#1485)."""
|
|
75
|
+
registry = Path(metadata_root) / "registries" / "silicon-kconfig.json"
|
|
76
|
+
data = json.loads(registry.read_text(encoding="utf-8"))
|
|
77
|
+
return data["socSymbolPrefix"], frozenset(data["knownSilicon"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def silicon_to_kconfig(silicon: str | None, metadata_root: Path) -> str | None:
|
|
81
|
+
"""Return the Zephyr Kconfig symbol that selects *silicon*, or ``None``.
|
|
82
|
+
|
|
83
|
+
The symbol is computed as ``socSymbolPrefix + ref.upper().replace(':','_')``;
|
|
84
|
+
e.g. ``alif:ensemble:e7`` -> ``ALP_SOC_ALIF_ENSEMBLE_E7``. ``None`` is
|
|
85
|
+
returned for any ref not in the registry allowlist (so an accelerator
|
|
86
|
+
such as ``deepx:dx:m1`` -- or an unknown ref -- emits no CONFIG line).
|
|
87
|
+
|
|
88
|
+
*metadata_root* is REQUIRED -- callers must pass the project's own
|
|
89
|
+
``project.effective_metadata_root()``, never a module default: a
|
|
90
|
+
silent fall-through to the SDK's own in-tree registry regardless of a
|
|
91
|
+
project's `--metadata-root` override was alp-sdk#1485's exact defect,
|
|
92
|
+
and the module-level `SILICON_KCONFIG_REGISTRY` constant this replaces
|
|
93
|
+
was the last site in tan's planner still shaped that way (tan-cli#868).
|
|
94
|
+
"""
|
|
95
|
+
if silicon is None:
|
|
96
|
+
return None
|
|
97
|
+
prefix, known = _load_silicon_kconfig(metadata_root)
|
|
98
|
+
if silicon not in known:
|
|
99
|
+
return None
|
|
100
|
+
return prefix + silicon.upper().replace(":", "_")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# resolve_soc_path() moved to the leaf module `tan.soc_ref` (imported above)
|
|
104
|
+
# so that `tan.model.targets` can use it without pulling in `tan.planner`'s
|
|
105
|
+
# import-time metadata reads. Re-exported here (rather than inlined) so this
|
|
106
|
+
# module's own four call sites, and every external `from
|
|
107
|
+
# tan.planner.som_metadata import resolve_soc_path`, keep working unchanged.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_silicon_variant(
|
|
111
|
+
sku_preset: dict[str, Any],
|
|
112
|
+
metadata_root: Path,
|
|
113
|
+
) -> dict[str, Any] | None:
|
|
114
|
+
"""Resolve a SoM preset to its matching SoC-JSON variant entry.
|
|
115
|
+
|
|
116
|
+
Two paths, in order of preference:
|
|
117
|
+
|
|
118
|
+
1. Forward: the SoM preset's top-level `silicon_variant:` field
|
|
119
|
+
names the exact `variants[].order_code` to pick out.
|
|
120
|
+
2. Reverse fallback: scan the SoC JSON's `variants[]` for one
|
|
121
|
+
whose `alp_module_skus` array contains the SoM SKU.
|
|
122
|
+
|
|
123
|
+
Returns the matched variant dict (with keys order_code, package,
|
|
124
|
+
mram_mb, sram_kb, optional_features, ...) or None when neither
|
|
125
|
+
path resolves (the SoC JSON has no variant for this SKU AND the
|
|
126
|
+
preset declares no silicon_variant, or declares `silicon_variant:
|
|
127
|
+
TBD` per the no-inventing-values rule).
|
|
128
|
+
"""
|
|
129
|
+
soc_path = resolve_soc_path(sku_preset.get("silicon"), metadata_root)
|
|
130
|
+
if soc_path is None or not soc_path.is_file():
|
|
131
|
+
return None
|
|
132
|
+
soc_spec = json.loads(soc_path.read_text(encoding="utf-8"))
|
|
133
|
+
variants = soc_spec.get("variants") or []
|
|
134
|
+
|
|
135
|
+
declared = sku_preset.get("silicon_variant")
|
|
136
|
+
# alp-sdk #1048: case/whitespace-insensitive TBD match ("tbd", "Tbd",
|
|
137
|
+
# " TBD " all match) -- a bare `!= "TBD"` let an all-lowercase
|
|
138
|
+
# `silicon_variant: tbd` read as a real order_code instead of being
|
|
139
|
+
# dropped as the placeholder it was hand-typed to mean.
|
|
140
|
+
declared_is_tbd = isinstance(declared, str) and declared.strip().upper() == "TBD"
|
|
141
|
+
if declared and not declared_is_tbd:
|
|
142
|
+
for v in variants:
|
|
143
|
+
if v.get("order_code") == declared:
|
|
144
|
+
return v
|
|
145
|
+
# Forward declared but not found -- noisy fall-through to reverse lookup.
|
|
146
|
+
|
|
147
|
+
sku = sku_preset.get("sku")
|
|
148
|
+
if sku:
|
|
149
|
+
for v in variants:
|
|
150
|
+
if sku in (v.get("alp_module_skus") or []):
|
|
151
|
+
return v
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_memory_map(
|
|
156
|
+
sku_preset: dict[str, Any],
|
|
157
|
+
metadata_root: Path,
|
|
158
|
+
) -> list[dict[str, Any]]:
|
|
159
|
+
"""Derive the effective memory-region table for a SoM.
|
|
160
|
+
|
|
161
|
+
Precedence:
|
|
162
|
+
1. If the SoM preset declares `memory_map:` explicitly (used
|
|
163
|
+
only for non-stock partitioning -- e.g. reserving SRAM for
|
|
164
|
+
a hardware secure enclave outside the default layout), it
|
|
165
|
+
wins verbatim.
|
|
166
|
+
2. Otherwise the loader derives the region list from the SoC
|
|
167
|
+
JSON variant resolved via `silicon_variant:`. Each named
|
|
168
|
+
SRAM bank becomes one region; the MRAM becomes one region.
|
|
169
|
+
Per-core TCM banks (SRAM names ending `_<core>_(ITCM|DTCM)`)
|
|
170
|
+
carry `accessible_from: [<core>]`; un-suffixed banks are
|
|
171
|
+
shared across every core declared in the variant's parent
|
|
172
|
+
SoC `cores[]` list.
|
|
173
|
+
|
|
174
|
+
The returned dicts have the keys defined by the memory_region
|
|
175
|
+
schema: `name`, `size_kib`, `accessible_from`, `cacheable`,
|
|
176
|
+
`write_authority` (always set explicitly on derived rows) (plus
|
|
177
|
+
optional `base` only when the SoM preset's override declares one
|
|
178
|
+
-- silicon-default bases stay unset, so downstream emitters know
|
|
179
|
+
to use the silicon's defaults).
|
|
180
|
+
|
|
181
|
+
Returns an empty list when the silicon_variant cannot be resolved
|
|
182
|
+
(e.g. a preset with `silicon_variant: TBD`) -- callers should treat
|
|
183
|
+
that as "memory layout pending the HW-config writeup".
|
|
184
|
+
"""
|
|
185
|
+
declared = sku_preset.get("memory_map")
|
|
186
|
+
if declared:
|
|
187
|
+
# SoM-side override wins; the loader trusts the maintainer.
|
|
188
|
+
return list(declared)
|
|
189
|
+
|
|
190
|
+
variant = _resolve_silicon_variant(sku_preset, metadata_root)
|
|
191
|
+
if not variant:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
# Re-load the SoC JSON to grab the cores[] topology (the variant
|
|
195
|
+
# dict alone doesn't carry per-core ids).
|
|
196
|
+
soc_path = resolve_soc_path(sku_preset.get("silicon"), metadata_root)
|
|
197
|
+
if soc_path is None or not soc_path.is_file():
|
|
198
|
+
return []
|
|
199
|
+
soc_spec = json.loads(soc_path.read_text(encoding="utf-8"))
|
|
200
|
+
soc_cores = [c.get("id") for c in soc_spec.get("cores", []) if c.get("id")]
|
|
201
|
+
|
|
202
|
+
regions: list[dict[str, Any]] = []
|
|
203
|
+
|
|
204
|
+
# Prefer SoC-level fixed memory_regions when present — these carry
|
|
205
|
+
# authoritative base addresses (e.g. RZ/V2N OCRAM at 0x00010000).
|
|
206
|
+
soc_memory_regions = soc_spec.get("memory_regions")
|
|
207
|
+
if soc_memory_regions:
|
|
208
|
+
# Every SoC-level region is RAM (no SoC declares a flash window
|
|
209
|
+
# here), so each is runtime-writable by the application.
|
|
210
|
+
return [{**r, "write_authority": "customer_runtime"}
|
|
211
|
+
for r in soc_memory_regions]
|
|
212
|
+
|
|
213
|
+
# MRAM as one region (size in KiB; mram_mb -> *1024).
|
|
214
|
+
mram_mb = variant.get("mram_mb")
|
|
215
|
+
if isinstance(mram_mb, (int, float)) and mram_mb > 0:
|
|
216
|
+
regions.append({
|
|
217
|
+
"name": "mram_main",
|
|
218
|
+
"size_kib": int(mram_mb * 1024),
|
|
219
|
+
"accessible_from": list(soc_cores),
|
|
220
|
+
"cacheable": True,
|
|
221
|
+
# The whole-device alias spans MCUboot/ATOC/slot0/storage
|
|
222
|
+
# rows of differing authority, so no single value is true;
|
|
223
|
+
# `composite` is the schema's explicit word for that
|
|
224
|
+
# (ADR-0034 clause 4: stated, never defaulted).
|
|
225
|
+
"write_authority": "composite",
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
# SRAM banks. Per-core TCM banks get `accessible_from: [<core>]`;
|
|
229
|
+
# the rest are shared across every core in the SoC.
|
|
230
|
+
sram_banks = variant.get("sram_banks_kb") or {}
|
|
231
|
+
for bank_name, size_kib in sram_banks.items():
|
|
232
|
+
if not isinstance(size_kib, int) or size_kib <= 0:
|
|
233
|
+
continue
|
|
234
|
+
# Detect per-core TCM by suffix.
|
|
235
|
+
accessible: list[str] = list(soc_cores)
|
|
236
|
+
for core_id in soc_cores:
|
|
237
|
+
suffix_token = f"_{core_id.upper()}_"
|
|
238
|
+
if suffix_token in bank_name.upper():
|
|
239
|
+
accessible = [core_id]
|
|
240
|
+
break
|
|
241
|
+
# TCMs are typically non-cacheable; bulk SRAM is cacheable.
|
|
242
|
+
is_tcm = "ITCM" in bank_name.upper() or "DTCM" in bank_name.upper()
|
|
243
|
+
regions.append({
|
|
244
|
+
"name": bank_name.lower(),
|
|
245
|
+
"size_kib": int(size_kib),
|
|
246
|
+
"accessible_from": accessible,
|
|
247
|
+
"cacheable": not is_tcm,
|
|
248
|
+
# SRAM/TCM is RAM: runtime-writable by the application.
|
|
249
|
+
"write_authority": "customer_runtime",
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return regions
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def resolve_capabilities(
|
|
256
|
+
sku_preset: dict[str, Any],
|
|
257
|
+
metadata_root: Path,
|
|
258
|
+
) -> dict[str, Any]:
|
|
259
|
+
"""Compose silicon capabilities from the SoC JSON with SoM-side overrides/extensions.
|
|
260
|
+
|
|
261
|
+
Returns a dict where SoM-declared keys override SoC-declared defaults for the same key
|
|
262
|
+
(e.g., V2N's `cau: true` via the GD32 bridge overrides the RZ/V2N silicon's `cau: false`).
|
|
263
|
+
Keys that exist on only one side are passed through unchanged.
|
|
264
|
+
|
|
265
|
+
Precedence:
|
|
266
|
+
1. Resolve the SoC ref via the ``silicon:`` field (same path used by
|
|
267
|
+
``_resolve_silicon_variant`` and ``resolve_memory_map``).
|
|
268
|
+
2. Read ``soc["capabilities"]`` (defaults to ``{}`` when absent --
|
|
269
|
+
older SoC JSONs that pre-date this field continue to work).
|
|
270
|
+
3. Read ``sku_preset.get("capabilities", {})``.
|
|
271
|
+
4. Merge; SoM-side wins on key collision so that SoM add-on chips /
|
|
272
|
+
GD32 bridge capabilities can override the host silicon's defaults.
|
|
273
|
+
5. Apply the SKU's ``silicon_capabilities.unpopulated`` RESTRICTION
|
|
274
|
+
list: each listed silicon capability is forced to 0 (count) /
|
|
275
|
+
``False`` (flag). A SKU can only remove what the silicon offers
|
|
276
|
+
(enforced by scripts/validate_metadata.py); presets without the
|
|
277
|
+
field keep the full silicon capability set.
|
|
278
|
+
"""
|
|
279
|
+
soc_path = resolve_soc_path(sku_preset.get("silicon"), metadata_root)
|
|
280
|
+
soc_caps: dict[str, Any] = {}
|
|
281
|
+
if soc_path is not None and soc_path.is_file():
|
|
282
|
+
soc_spec = json.loads(soc_path.read_text(encoding="utf-8"))
|
|
283
|
+
soc_caps = soc_spec.get("capabilities") or {}
|
|
284
|
+
|
|
285
|
+
som_caps: dict[str, Any] = sku_preset.get("capabilities") or {}
|
|
286
|
+
|
|
287
|
+
# SoM side wins on collision (bridge / add-on overrides silicon default).
|
|
288
|
+
merged: dict[str, Any] = {**soc_caps, **som_caps}
|
|
289
|
+
|
|
290
|
+
# GPU: a per-die fact (variants[].optional_features.gpu_mali_g31, the one
|
|
291
|
+
# source) -- the SoC-level capabilities block cannot carry it because
|
|
292
|
+
# four of the eight RZ/V2N dies are fused without the Mali-G31.
|
|
293
|
+
variant = _resolve_silicon_variant(sku_preset, metadata_root)
|
|
294
|
+
mali = ((variant or {}).get("optional_features") or {}).get("gpu_mali_g31")
|
|
295
|
+
if mali is not None:
|
|
296
|
+
merged["gpu2d"] = bool(mali)
|
|
297
|
+
|
|
298
|
+
# SKU-level restriction: capabilities the silicon offers but this SKU
|
|
299
|
+
# leaves unpopulated (per-SKU granularity -- one family, many SKUs).
|
|
300
|
+
# Preserve the value class so count-style caps (ethos_u55_count, ...)
|
|
301
|
+
# restrict to 0 and flag-style caps restrict to False.
|
|
302
|
+
for name in som_unpopulated_capabilities(sku_preset):
|
|
303
|
+
base = soc_caps.get(name)
|
|
304
|
+
if isinstance(base, int) and not isinstance(base, bool):
|
|
305
|
+
merged[name] = 0
|
|
306
|
+
else:
|
|
307
|
+
merged[name] = False
|
|
308
|
+
return merged
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def som_unpopulated_capabilities(sku_preset: dict[str, Any]) -> list[str]:
|
|
312
|
+
"""Return the SKU's `silicon_capabilities.unpopulated` list (or []).
|
|
313
|
+
|
|
314
|
+
Single accessor for the per-SKU capability RESTRICTION block so this
|
|
315
|
+
module (`resolve_capabilities`), the emitters (`kconfig.py`, which passes
|
|
316
|
+
`-DALP_SOM_<TOKEN>` only for restricted SKUs) and alp-sdk's header
|
|
317
|
+
generator (`gen_soc_caps.py`) agree on where the field lives.
|
|
318
|
+
"""
|
|
319
|
+
block = sku_preset.get("silicon_capabilities") or {}
|
|
320
|
+
if not isinstance(block, dict):
|
|
321
|
+
return []
|
|
322
|
+
names = block.get("unpopulated") or []
|
|
323
|
+
if not isinstance(names, list):
|
|
324
|
+
return []
|
|
325
|
+
return [str(n) for n in names]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Duplicate-key-rejecting YAML/JSON loaders (alp-sdk issue #1127).
|
|
4
|
+
|
|
5
|
+
RELOCATED (was alp-sdk `scripts/strict_loaders.py`, a top-level module outside
|
|
6
|
+
`scripts/alp_orchestrate/`, hand-ported the same way `zephyr_board.py` /
|
|
7
|
+
`project_loader.py` were -- named in `HAND_PORT_SOURCES` (not `PINNED_HASHES`,
|
|
8
|
+
since it has no `scripts/alp_orchestrate/` counterpart to pin by filename), but
|
|
9
|
+
its staleness hash lives in its OWN `STRICT_LOADERS_HASH` /
|
|
10
|
+
`STRICT_LOADERS_PINNED_SDK_COMMIT` pair, not `HAND_PORT_HASHES` -- see
|
|
11
|
+
`test_planner_relocation_freshness.py`'s STRICT_LOADERS_* comment for why).
|
|
12
|
+
|
|
13
|
+
`yaml.safe_load` and `json.loads` silently keep only the LAST value of a
|
|
14
|
+
duplicated mapping key -- `yaml.safe_load("som: a\\nsom: b\\n")` returns
|
|
15
|
+
`{'som': 'b'}` with no error or warning. In a hand-edited board.yaml,
|
|
16
|
+
SoM preset, or chip/SoC manifest, a duplicated key is invisible in a
|
|
17
|
+
diff and silently drops hardware configuration. `strict_yaml_load()`
|
|
18
|
+
and `strict_json_loads()` are the one place `tan/planner/loader.py`'s
|
|
19
|
+
`_load_yaml` / `_load_json` route through instead of the raw stdlib
|
|
20
|
+
loaders, so a board.yaml `tan build` accepts is the same board.yaml
|
|
21
|
+
`tan validate` (which shells the SDK validator, itself routed through
|
|
22
|
+
alp-sdk's own copy of this module) accepts.
|
|
23
|
+
|
|
24
|
+
`fast_safe_load()` is the lenient sibling: exactly `yaml.safe_load`
|
|
25
|
+
semantics (duplicate keys keep the last value) for hot readers that do not
|
|
26
|
+
opt into rejection (`sdk_compat.load_family_table`). Both YAML paths parse
|
|
27
|
+
with libyaml's C parser whenever PyYAML was built with it, and fall back to
|
|
28
|
+
the pure-Python one otherwise (alp-sdk #2328).
|
|
29
|
+
|
|
30
|
+
Zero dependencies beyond PyYAML on purpose, same rationale as alp-sdk's
|
|
31
|
+
copy: this is a leaf every metadata-ingestion boundary can import without
|
|
32
|
+
creating a cycle.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import yaml
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DuplicateKeyError(ValueError):
|
|
45
|
+
"""Raised when a YAML/JSON mapping repeats a key."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _no_duplicates_mapping_constructor(
|
|
49
|
+
loader: yaml.constructor.SafeConstructor, node: yaml.MappingNode
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
# Duplicate-key detection must run on the node's OWN explicit pairs
|
|
52
|
+
# BEFORE `flatten_mapping()` splices in `<<: *anchor` merge-key
|
|
53
|
+
# pairs -- otherwise a spec-legal merge override (the anchor
|
|
54
|
+
# supplies a default, a sibling explicit key overrides it) reads as
|
|
55
|
+
# the same key twice and gets rejected as a duplicate, when the
|
|
56
|
+
# explicit value should just win, exactly like the stdlib loader
|
|
57
|
+
# does. Scanning only the node's own pairs also leaves two merge
|
|
58
|
+
# SOURCES quietly disagreeing on a key to the normal YAML
|
|
59
|
+
# first-wins merge semantics, not a hard error -- this loader's job
|
|
60
|
+
# is to catch a literal duplicate key typed twice in one mapping.
|
|
61
|
+
seen: set[Any] = set()
|
|
62
|
+
for key_node, _value_node in node.value:
|
|
63
|
+
if key_node.tag == "tag:yaml.org,2002:merge":
|
|
64
|
+
continue
|
|
65
|
+
key = loader.construct_object(key_node, deep=True)
|
|
66
|
+
if key in seen:
|
|
67
|
+
mark = key_node.start_mark
|
|
68
|
+
raise DuplicateKeyError(
|
|
69
|
+
f"duplicate key {key!r} at line {mark.line + 1}, "
|
|
70
|
+
f"column {mark.column + 1}"
|
|
71
|
+
)
|
|
72
|
+
seen.add(key)
|
|
73
|
+
|
|
74
|
+
loader.flatten_mapping(node)
|
|
75
|
+
mapping: dict[str, Any] = {}
|
|
76
|
+
for key_node, value_node in node.value:
|
|
77
|
+
key = loader.construct_object(key_node, deep=True)
|
|
78
|
+
mapping[key] = loader.construct_object(value_node, deep=True)
|
|
79
|
+
return mapping
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# libyaml's C parser when PyYAML was built with it (its wheels are), the
|
|
83
|
+
# pure-Python one otherwise. Same SafeConstructor either way, so the same
|
|
84
|
+
# values and the same duplicate-key rejection -- only ~10x faster to parse
|
|
85
|
+
# (alp-sdk #2328).
|
|
86
|
+
_SAFE_LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _StrictLoader(_SAFE_LOADER):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_StrictLoader.add_constructor(
|
|
94
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _no_duplicates_mapping_constructor
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def strict_yaml_load(text: str, source: str | Path = "<string>") -> Any:
|
|
99
|
+
"""`yaml.safe_load`, but raise `DuplicateKeyError` on a repeated mapping key."""
|
|
100
|
+
try:
|
|
101
|
+
return yaml.load(text, Loader=_StrictLoader)
|
|
102
|
+
except DuplicateKeyError as e:
|
|
103
|
+
raise DuplicateKeyError(f"{source}: {e}") from e
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def fast_safe_load(text: str) -> Any:
|
|
107
|
+
"""Exactly `yaml.safe_load(text)`, parsed by libyaml when available.
|
|
108
|
+
|
|
109
|
+
For the hot metadata readers that want stdlib semantics (duplicate keys
|
|
110
|
+
keep the last value) rather than `strict_yaml_load`'s rejection.
|
|
111
|
+
"""
|
|
112
|
+
return yaml.load(text, Loader=_SAFE_LOADER)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _no_duplicates_object_pairs_hook(
|
|
116
|
+
pairs: list[tuple[str, Any]],
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
mapping: dict[str, Any] = {}
|
|
119
|
+
for key, value in pairs:
|
|
120
|
+
if key in mapping:
|
|
121
|
+
raise DuplicateKeyError(f"duplicate key {key!r}")
|
|
122
|
+
mapping[key] = value
|
|
123
|
+
return mapping
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def strict_json_loads(text: str, source: str | Path = "<string>") -> Any:
|
|
127
|
+
"""`json.loads`, but raise `DuplicateKeyError` on a repeated object key."""
|
|
128
|
+
try:
|
|
129
|
+
return json.loads(text, object_pairs_hook=_no_duplicates_object_pairs_hook)
|
|
130
|
+
except DuplicateKeyError as e:
|
|
131
|
+
raise DuplicateKeyError(f"{source}: {e}") from e
|