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,499 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Read the SDK scaffold catalog's declared SoM support for ONE example, and
|
|
3
|
+
(tan-cli PR #996 Part 2) select a catalog record BY its `cores:` hardware
|
|
4
|
+
topology instead of by id or example path.
|
|
5
|
+
|
|
6
|
+
`find_example_by_cores` below is `tan init`'s customer-facing counterpart to
|
|
7
|
+
alp-sdk#1652's `find_template_by_cores` (`scripts/alp_template.py`,
|
|
8
|
+
hand-ported into `tan/planner/template.py` for the freshness-gate audit and
|
|
9
|
+
`tan.planner_cli --emit scaffold --cores`'s parity mirror). It is
|
|
10
|
+
DELIBERATELY a second, small, standalone implementation rather than a call
|
|
11
|
+
into `tan.planner.template.find_template_by_cores` -- same reasoning as
|
|
12
|
+
`unsupported_som` below: `tan.planner.paths` binds `REPO = sdk_root()` at
|
|
13
|
+
MODULE scope, so importing anything under `tan.planner` before
|
|
14
|
+
`bind_sdk_root` has run raises `PlannerRootError`, and `init_cmd.py`'s whole
|
|
15
|
+
SDK-free path (`tan init --template ...`, no checkout in sight, invariant
|
|
16
|
+
I-32) must keep working. The topology match itself is `json.loads(...)` plus
|
|
17
|
+
a list scan and a dict-equality check -- duplicating that one screen of pure
|
|
18
|
+
logic costs far less than a bind-then-deferred-import dance for every `tan
|
|
19
|
+
init` invocation, template-selecting or not.
|
|
20
|
+
|
|
21
|
+
**Distinct flag, distinct name, on purpose (tan-cli#996 review).** `tan init`
|
|
22
|
+
already has a `--cores` flag (`init_cmd.py`) that SPLICES a companion core
|
|
23
|
+
onto an already-chosen template's board.yaml -- a different feature entirely
|
|
24
|
+
from choosing WHICH template to use by its hardware topology. The customer
|
|
25
|
+
surface for this module's function is `--topology`, never `--cores`: the
|
|
26
|
+
same flag spelling meaning two different things depending on whether
|
|
27
|
+
`--template`/`--from-example` was also given would be a documentation
|
|
28
|
+
problem forever, not a one-time naming choice. See `init_cmd.py`'s `init()`
|
|
29
|
+
option help text for both flags' cross-reference to the other.
|
|
30
|
+
|
|
31
|
+
tan-cli#890. `tan init --from-example` copies an example tree and, when
|
|
32
|
+
`--som` is given, retargets the copied `board.yaml` onto that SKU. It never
|
|
33
|
+
consulted the catalog, so it answered the opposite of the SDK for the same
|
|
34
|
+
question:
|
|
35
|
+
|
|
36
|
+
$ alp_project.py --emit scaffold --template multicore-mailbox --sku E1M-AEN301
|
|
37
|
+
alp_project: multicore-mailbox: sku 'E1M-AEN301' is not supported
|
|
38
|
+
(supported: ['E1M-AEN801']) rc=1
|
|
39
|
+
$ tan init --from-example multicore/mproc-mailbox --som E1M-AEN301
|
|
40
|
+
exitCode 0 | ok True | issues 0
|
|
41
|
+
|
|
42
|
+
Measured worse than same-family: `--som E1M-V2N101` retargets that dual-M55
|
|
43
|
+
Alif example onto a Renesas part whose topology is `['a55_cluster', 'm33_sm']`
|
|
44
|
+
-- so the written `board.yaml` declares `m55_hp` and `m55_he`, neither of
|
|
45
|
+
which exists there. `tan validate` does refuse it a command later
|
|
46
|
+
(`ALP-B007`), but `init` reported `ok: true` with no issue at all, and an IDE
|
|
47
|
+
scaffolding through this path shows the customer green.
|
|
48
|
+
|
|
49
|
+
WHY THIS READS THE CATALOG DIRECTLY RATHER THAN THROUGH `tan.planner`
|
|
50
|
+
---------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
`tan/planner/template.py` already has `load_catalog` and a
|
|
53
|
+
`SkuNotSupportedError`, and it is a hash-audited mirror of alp-sdk's
|
|
54
|
+
`scripts/alp_template.py` -- so it must not be edited here. Importing it is
|
|
55
|
+
also not free: `tan.planner.paths` binds `REPO = sdk_root()` at MODULE scope,
|
|
56
|
+
so any `tan.planner.*` import before `bind_sdk_root` raises
|
|
57
|
+
|
|
58
|
+
PlannerRootError: tan.planner was imported before `bind_sdk_root(...)`;
|
|
59
|
+
its metadata paths freeze at import time
|
|
60
|
+
|
|
61
|
+
(measured). Pulling that into `init_cmd` would either break every SDK-free
|
|
62
|
+
`tan init` path or need a bind-then-deferred-import dance, for what is
|
|
63
|
+
`json.loads(path.read_text())` plus a list scan. `load_catalog` is exactly
|
|
64
|
+
that one line, so nothing here duplicates planner LOGIC -- and the lookup
|
|
65
|
+
below is by `example` path, which the mirror does not do at all (it looks
|
|
66
|
+
records up by `id`).
|
|
67
|
+
|
|
68
|
+
WHY BOTH READS GO THROUGH `tan/core/document_guards.py` (tan-cli#1084)
|
|
69
|
+
----------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
A second implementation of a read is only safe while the two answer the same
|
|
72
|
+
question the same way, and this one had stopped. tan-cli#1073 extracted a
|
|
73
|
+
malformed-document register into `tan/planner/template.py` and tan-cli#1077
|
|
74
|
+
extended it to the catalog, so THAT reader refuses a malformed
|
|
75
|
+
`catalog-v1.json` with a curated error naming the file, the field and the
|
|
76
|
+
type -- while this one still decoded it through a bare `json.loads` and then
|
|
77
|
+
`.get`/subscripted the result. Re-derived on `dev@be3a44b6` before fixing
|
|
78
|
+
(`tests/core/test_example_catalog_malformed_catalog.py` carries the full
|
|
79
|
+
table): nine shapes crashed RAW here (`JSONDecodeError`, `AttributeError`,
|
|
80
|
+
`TypeError`, `KeyError`, `FileNotFoundError`) where the planner refused
|
|
81
|
+
cleanly, four more were SILENTLY MIS-READ, and two of them escaped
|
|
82
|
+
`unsupported_som`'s own written "Never raises" contract.
|
|
83
|
+
|
|
84
|
+
Worse than either: `tests/gates/
|
|
85
|
+
test_example_catalog_cores_selector_agrees_with_planner.py` asserts the two
|
|
86
|
+
selectors AGREE, and covered only well-formed input -- so the divergence was
|
|
87
|
+
invisible to CI. Two implementations of one read that agree on good input and
|
|
88
|
+
diverge on bad, with a test asserting they agree.
|
|
89
|
+
|
|
90
|
+
The register therefore MOVED to `tan/core/document_guards.py` -- a module with
|
|
91
|
+
no `tan.planner` in its import closure, so the constraint above still holds
|
|
92
|
+
and nothing here is a copy. `template.py` binds the same objects, so its call
|
|
93
|
+
sites are byte-identical to what #1073/#1077 landed, and neutering one method
|
|
94
|
+
there reds tests on BOTH sides. The message shapes, the exception TYPE each
|
|
95
|
+
side raises, and what deliberately did NOT move (tan-cli#1085) are all
|
|
96
|
+
documented in that module.
|
|
97
|
+
|
|
98
|
+
THE ONE REMAINING DIVERGENCE, AND THE TWO NEW REFUSALS (tan-cli#1084)
|
|
99
|
+
----------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
`find_example_by_cores` excludes a record with no (or a falsy) `example:`
|
|
102
|
+
from its match set; `find_template_by_cores` does not filter on that field at
|
|
103
|
+
all, because it returns the RECORD where this returns the PATH. So on a
|
|
104
|
+
hand-edited catalog whose matching record has no `example:`, the planner still
|
|
105
|
+
answers "found" and this answers "not found". That gap is dev's, is unreachable
|
|
106
|
+
against a schema-valid catalog (`example` is `required`, `pattern`
|
|
107
|
+
`^examples/...`), and is left ALONE here -- closing it would change this
|
|
108
|
+
function's documented contract and is a selection question, not the strictness
|
|
109
|
+
question #1084 is about. What changed is that it is no longer undocumented:
|
|
110
|
+
`test_the_example_field_is_the_one_documented_divergence` pins it by name.
|
|
111
|
+
|
|
112
|
+
Two refusals ARE new, both of documents the schema never accepted, and both
|
|
113
|
+
taken to MATCH the planner rather than invented here:
|
|
114
|
+
|
|
115
|
+
* the matched record's `id:` is resolved before the ambiguity test, as
|
|
116
|
+
`find_template_by_cores` has done since tan-cli#1077 -- so a unique match
|
|
117
|
+
with a missing or non-string `id:` refuses where dev returned it happily.
|
|
118
|
+
`id` is `required` on every record.
|
|
119
|
+
* the matched record's `example:` must be a string. dev ran it through
|
|
120
|
+
`str(...)`, so a record carrying `example: 3` resolved to the literal src
|
|
121
|
+
`'3'` and `tan init` went looking for a directory of that name -- silent,
|
|
122
|
+
and the worst outcome in the whole re-derived table.
|
|
123
|
+
|
|
124
|
+
WHY A WARNING AND NOT A REFUSAL
|
|
125
|
+
--------------------------------
|
|
126
|
+
|
|
127
|
+
This path has its own written precedent. `--from-example` used to REFUSE an
|
|
128
|
+
example with no `board.yaml`, and that "made `tan init --from-example`
|
|
129
|
+
unusable for nearly the whole AEN family, which is worse than the original
|
|
130
|
+
defect" -- so it warns and still writes every file
|
|
131
|
+
(`init.example-missing-board-yaml`). The same reasoning holds here:
|
|
132
|
+
`E1M-AEN301` really does carry both `m55_hp` and `m55_he`, so the scaffold it
|
|
133
|
+
would have produced is plausibly fine. What was wrong is that NOTHING
|
|
134
|
+
CHECKED, and that is what the warning fixes.
|
|
135
|
+
|
|
136
|
+
WHY `catalog_unreadable` EXISTS, AND WHAT IT COVERS (tan-cli#1101)
|
|
137
|
+
--------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
`unsupported_som`'s own contract folds catalog-absent, catalog-unreadable,
|
|
140
|
+
catalog-malformed, unreadable-matching-record, no-record-for-this-example,
|
|
141
|
+
and sku-is-supported into one indistinguishable `None` -- exactly right for
|
|
142
|
+
"should `--from-example` refuse", wrong for "did the check even run".
|
|
143
|
+
`catalog_unreadable` is the second, narrower read that answers the second
|
|
144
|
+
question, so `--from-example --som` can warn "this check did not run"
|
|
145
|
+
instead of reporting `issues: []` on a check that never happened.
|
|
146
|
+
|
|
147
|
+
It reports two DIFFERENT shapes, and (tan-cli#1101 review MINOR) says so in
|
|
148
|
+
two DIFFERENT sentences rather than one that overstates the second:
|
|
149
|
+
|
|
150
|
+
* the catalog DOCUMENT itself could not be decoded, or its top-level shape
|
|
151
|
+
(a JSON object, a `templates:` list) does not match its schema --
|
|
152
|
+
non-UTF-8 bytes, a directory where a file was expected, a permissions
|
|
153
|
+
failure (tan-cli#1101 review BLOCKER: a `PermissionError` on a directory
|
|
154
|
+
the caller cannot even `stat()` used to escape a `catalog.exists()`
|
|
155
|
+
pre-flight raw, undoing the #1096 crash-safety this whole issue is
|
|
156
|
+
about -- there is no pre-flight now, matching
|
|
157
|
+
`document_guards.read_catalog_document`'s own "not a pre-flight
|
|
158
|
+
`is_file()`" reasoning), invalid JSON, a non-object document, a
|
|
159
|
+
non-list `templates:`. This is genuinely "could not be read".
|
|
160
|
+
* the document reads fine and a record's `example:` DOES match, but that
|
|
161
|
+
SAME record's `supported:` / `supported.som_skus:` is not the shape its
|
|
162
|
+
schema declares -- missing entirely, the wrong type, or (tan-cli#1101
|
|
163
|
+
review MINOR) an absent key masquerading as "no `supported.som_skus`"
|
|
164
|
+
in `unsupported_som`'s own docstring, which conflated it with the
|
|
165
|
+
genuinely-silent empty-list case. The document was readable; ONE
|
|
166
|
+
record's declared support was not. Calling that "could not be read"
|
|
167
|
+
sends a customer looking for a corrupt file that does not exist.
|
|
168
|
+
|
|
169
|
+
In both cases the SoM-support check for THIS example could not run, which
|
|
170
|
+
is `catalog_unreadable`'s whole question -- unlike a record for a DIFFERENT
|
|
171
|
+
example that cannot be read, still skipped rather than fatal
|
|
172
|
+
(`_declared_som_skus`'s own per-record-skip precedent, unchanged: a record
|
|
173
|
+
whose OWN `example:` is unreadable cannot be attributed to the example being
|
|
174
|
+
asked about, so it is simply not evidence either way), and unlike a
|
|
175
|
+
well-formed catalog with simply no record for this example at all, which is
|
|
176
|
+
`unsupported_som`'s "COMMON case, not an edge one" silence (66 example
|
|
177
|
+
directories, 9 catalogued templates) -- there was nothing to check there,
|
|
178
|
+
not a check that failed. A catalog that is ABSENT altogether is the same
|
|
179
|
+
silence, for a different reason: `unsupported_som`'s own long-standing "no
|
|
180
|
+
catalog in this checkout (an older SDK; --from-example worked there before
|
|
181
|
+
this gate and must keep working)" precedent -- warning on it would add noise
|
|
182
|
+
to every pre-catalog checkout's `--from-example --som`, not close a gap.
|
|
183
|
+
Distinguished from every OTHER `MalformedCatalogError` by `err.__cause__`
|
|
184
|
+
rather than a pre-flight stat: `read_catalog_document`'s
|
|
185
|
+
`except (OSError, UnicodeDecodeError) as exc: raise self.error(...) from
|
|
186
|
+
exc` is the only site in the whole guarded read that chains a
|
|
187
|
+
`FileNotFoundError` as the cause, so `isinstance(err.__cause__,
|
|
188
|
+
FileNotFoundError)` names exactly "the file was never there" and nothing
|
|
189
|
+
else -- a directory, a permissions failure, and every record-level shape
|
|
190
|
+
error all chain something else (or nothing).
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
from __future__ import annotations
|
|
194
|
+
|
|
195
|
+
from pathlib import Path
|
|
196
|
+
from typing import Any
|
|
197
|
+
|
|
198
|
+
from tan.core.document_guards import DocumentGuards
|
|
199
|
+
|
|
200
|
+
#: The catalog, relative to an alp-sdk checkout root.
|
|
201
|
+
CATALOG_RELATIVE = Path("metadata") / "templates" / "catalog-v1.json"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CoresTopologyError(Exception):
|
|
205
|
+
"""Base for every refusal this module's catalog reads raise -- never a
|
|
206
|
+
bare KeyError/ValueError/TypeError escapes to a caller. `init_cmd.py`
|
|
207
|
+
catches each subclass and turns it into an `InitError` with the right
|
|
208
|
+
issue code for that case."""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class MalformedCatalogError(CoresTopologyError, ValueError):
|
|
212
|
+
"""`metadata/templates/catalog-v1.json` is not the shape its schema
|
|
213
|
+
declares -- unreadable, not JSON, not a JSON object, or a record/field
|
|
214
|
+
of the wrong type (tan-cli#1084).
|
|
215
|
+
|
|
216
|
+
A `ValueError` as well, so the curated message lands in the same family
|
|
217
|
+
as the register `tan/model/targets.py:312-323` established; a
|
|
218
|
+
`CoresTopologyError` as well, so a caller that catches only this
|
|
219
|
+
module's base still cannot be handed a raw traceback. Measured before
|
|
220
|
+
adding the second base: nothing between `find_example_by_cores` and
|
|
221
|
+
`init()` catches a bare `ValueError` (`init_cmd.py:846` and `:1119`
|
|
222
|
+
both do, and neither encloses `_plan_from_topology`'s call), so the MI
|
|
223
|
+
cannot make a refusal disappear into an unrelated handler --
|
|
224
|
+
`test_a_malformed_catalog_is_not_swallowed_by_an_unrelated_handler`
|
|
225
|
+
pins that.
|
|
226
|
+
|
|
227
|
+
`find_example_by_cores` RAISES it; `unsupported_som` catches it and
|
|
228
|
+
returns None, which is that function's whole documented contract.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
#: This module's binding of the shared malformed-document register --
|
|
233
|
+
#: literally the same three checks + two catalog readers
|
|
234
|
+
#: `tan/planner/template.py` uses, differing only in the exception class each
|
|
235
|
+
#: side's caller contracts for (`init_cmd._plan_from_topology` catches
|
|
236
|
+
#: `CoresTopologyError`; `planner/cli._emit_scaffold` catches
|
|
237
|
+
#: `TemplateError`). Every curated message below is therefore byte-identical
|
|
238
|
+
#: to the planner's for the same malformed document, which is what
|
|
239
|
+
#: `tests/gates/test_example_catalog_cores_selector_agrees_with_planner.py`
|
|
240
|
+
#: now asserts directly rather than assuming.
|
|
241
|
+
_GUARDS = DocumentGuards(MalformedCatalogError)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def unsupported_som(sdk_root: Path, example_src: str, sku: str) -> tuple[str, ...] | None:
|
|
245
|
+
"""The catalog's `supported.som_skus` for `example_src`, when `sku` is
|
|
246
|
+
NOT in it. `None` means "nothing to report" -- and that covers every
|
|
247
|
+
reason, deliberately:
|
|
248
|
+
|
|
249
|
+
* no catalog in this checkout (an older SDK; `--from-example` worked
|
|
250
|
+
there before this gate and must keep working),
|
|
251
|
+
* catalog present but unparseable, or not the shape expected,
|
|
252
|
+
* no record for this example -- the COMMON case, not an edge one: the
|
|
253
|
+
catalog declares 9 templates while `examples/aen/` alone holds 66
|
|
254
|
+
directories, so an undeclared example has no support set to check
|
|
255
|
+
and inventing one would be this defect pointed the other way,
|
|
256
|
+
* a record whose `supported.som_skus` is present as an empty list
|
|
257
|
+
(`[]`) -- NOT a missing or malformed `supported`/`som_skus` key
|
|
258
|
+
(tan-cli#1101 review MINOR corrected this bullet, which used to
|
|
259
|
+
read as covering both): those raise `MalformedCatalogError` same as
|
|
260
|
+
any other malformed field, so THIS function still folds them into
|
|
261
|
+
`None`, but `catalog_unreadable` (below, see its module-docstring
|
|
262
|
+
section) reports them as a check that could not run, not silence,
|
|
263
|
+
* `sku` is in the set.
|
|
264
|
+
|
|
265
|
+
Never raises: a scaffold must not fail because a catalog could not be
|
|
266
|
+
read. The caller reports the returned set as a warning.
|
|
267
|
+
|
|
268
|
+
tan-cli#1084: that contract was WRITTEN but not held -- measured on
|
|
269
|
+
`dev@be3a44b6`, `templates: 3` escaped as `TypeError: 'int' object is
|
|
270
|
+
not iterable` and a record with `supported: 3` as `AttributeError:
|
|
271
|
+
'int' object has no attribute 'get'`. Both are now curated by the
|
|
272
|
+
shared register and caught here, so every listed reason really does
|
|
273
|
+
return None. No outcome that was already `None` or a support set
|
|
274
|
+
changes; only the two that raised.
|
|
275
|
+
"""
|
|
276
|
+
catalog = sdk_root / CATALOG_RELATIVE
|
|
277
|
+
# The catalog spells it `examples/<src>`; `--from-example` takes `<src>`.
|
|
278
|
+
# Compared with `/` separators on both sides -- `example_src` is already
|
|
279
|
+
# validated as a plain relative path by the caller, and the catalog is
|
|
280
|
+
# authored with forward slashes regardless of host.
|
|
281
|
+
wanted = f"examples/{example_src.strip().strip('/')}"
|
|
282
|
+
try:
|
|
283
|
+
supported = _declared_som_skus(catalog, wanted)
|
|
284
|
+
except MalformedCatalogError:
|
|
285
|
+
return None
|
|
286
|
+
if not supported or sku in supported:
|
|
287
|
+
return None
|
|
288
|
+
return tuple(str(s) for s in supported)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _declared_som_skus(catalog: Path, wanted: str) -> list[Any] | None:
|
|
292
|
+
"""`supported.som_skus` of the record whose `example` is @wanted, or
|
|
293
|
+
None when no record claims it. Raises `MalformedCatalogError` for a
|
|
294
|
+
catalog-level shape problem, or for a malformed MATCH.
|
|
295
|
+
|
|
296
|
+
A record whose own `example` cannot be read is SKIPPED rather than
|
|
297
|
+
fatal -- the scan is looking for one specific path, and a record it
|
|
298
|
+
cannot read is not that record. That is exactly what dev's
|
|
299
|
+
`isinstance(record, dict)` + `str(record.get("example", ""))` coercion
|
|
300
|
+
did, kept deliberately so a malformed record cannot silence a warning
|
|
301
|
+
a well-formed one later in the list would have produced.
|
|
302
|
+
"""
|
|
303
|
+
doc = _GUARDS.read_catalog_document(catalog)
|
|
304
|
+
for index, record in enumerate(_GUARDS.catalog_templates(doc, path=catalog)):
|
|
305
|
+
field = f"templates[{index}]"
|
|
306
|
+
try:
|
|
307
|
+
example = _GUARDS.require_key(record, "example", str,
|
|
308
|
+
doc=catalog, field=field)
|
|
309
|
+
except MalformedCatalogError:
|
|
310
|
+
continue
|
|
311
|
+
if example.strip().strip("/") != wanted:
|
|
312
|
+
continue
|
|
313
|
+
return _GUARDS.require_key(
|
|
314
|
+
_GUARDS.require_key(record, "supported", dict,
|
|
315
|
+
doc=catalog, field=field),
|
|
316
|
+
"som_skus", list, doc=catalog, field=f"{field}.supported")
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def catalog_unreadable(sdk_root: Path, example_src: str) -> str | None:
|
|
321
|
+
"""Whether the SoM-support check for `example_src` could not run at all
|
|
322
|
+
-- see "WHY `catalog_unreadable` EXISTS, AND WHAT IT COVERS" above for
|
|
323
|
+
the full reasoning (tan-cli#1101). `None` for a catalog that is ABSENT
|
|
324
|
+
or that reads cleanly (whether or not it supports `example_src`); the
|
|
325
|
+
already-composed, correctly-led reason string otherwise -- "the SDK
|
|
326
|
+
scaffold catalog could not be read" only for a genuine document-level
|
|
327
|
+
failure, a different lead for a document that read fine but whose
|
|
328
|
+
matching record did not.
|
|
329
|
+
"""
|
|
330
|
+
catalog = sdk_root / CATALOG_RELATIVE
|
|
331
|
+
try:
|
|
332
|
+
doc = _GUARDS.read_catalog_document(catalog)
|
|
333
|
+
_GUARDS.catalog_templates(doc, path=catalog)
|
|
334
|
+
except MalformedCatalogError as err:
|
|
335
|
+
if isinstance(err.__cause__, FileNotFoundError):
|
|
336
|
+
return None
|
|
337
|
+
return f"the SDK scaffold catalog could not be read ({err})"
|
|
338
|
+
wanted = f"examples/{example_src.strip().strip('/')}"
|
|
339
|
+
try:
|
|
340
|
+
_declared_som_skus(catalog, wanted)
|
|
341
|
+
except MalformedCatalogError as err:
|
|
342
|
+
return (
|
|
343
|
+
f"the SDK scaffold catalog's declared support for this "
|
|
344
|
+
f"example is not the shape its schema declares ({err})"
|
|
345
|
+
)
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# `--topology`: select a catalog record BY its cores: hardware topology
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class CoresTopologyNotFoundError(CoresTopologyError):
|
|
355
|
+
"""No catalog record's `cores:` topology exactly equals the requested
|
|
356
|
+
one. `known_topologies` names every topology the catalog DOES offer, the
|
|
357
|
+
same "never a bare error, always the candidates" spirit
|
|
358
|
+
`AmbiguousCoresTopologyError` follows for its own case."""
|
|
359
|
+
|
|
360
|
+
def __init__(self, cores: dict[str, str], known_topologies: list[tuple[tuple[str, str], ...]]):
|
|
361
|
+
self.cores = cores
|
|
362
|
+
self.known_topologies = known_topologies
|
|
363
|
+
super().__init__(
|
|
364
|
+
f"no catalog template with cores topology {cores!r} "
|
|
365
|
+
f"(known topologies: {known_topologies})"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class AmbiguousCoresTopologyError(CoresTopologyError):
|
|
370
|
+
"""More than one catalog record shares the requested `cores:` topology.
|
|
371
|
+
|
|
372
|
+
The correct behaviour is to name every candidate and let the customer
|
|
373
|
+
choose with `--template`/`--from-example` -- never to silently pick the
|
|
374
|
+
first. This is NOT a hypothetical: measured against the live alp-sdk
|
|
375
|
+
v0.16.0 catalog, the single-core `{"m55_hp": "zephyr"}` topology (the
|
|
376
|
+
shape a single-M55 AEN scaffold is most likely to ask for) already
|
|
377
|
+
matches five templates (`diagnostics`, `iot`, `minimal`, `peripheral`,
|
|
378
|
+
`sensor`) -- see `tests/planner/test_find_template_by_cores.py::
|
|
379
|
+
test_the_live_catalog_is_ambiguous_for_a_single_m55_hp_zephyr_topology`.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def __init__(self, cores: dict[str, str], candidate_ids: list[str]):
|
|
383
|
+
self.cores = cores
|
|
384
|
+
self.candidate_ids = candidate_ids
|
|
385
|
+
super().__init__(
|
|
386
|
+
f"cores topology {cores!r} matches multiple catalog templates "
|
|
387
|
+
f"{candidate_ids} -- use --template or --from-example to "
|
|
388
|
+
f"disambiguate"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _topology(record: Any, index: int, catalog: Path) -> dict[str, str]:
|
|
393
|
+
"""One record's `cores:` topology (core id -> os), every shape checked
|
|
394
|
+
on the shared register.
|
|
395
|
+
|
|
396
|
+
A LINE-FOR-LINE mirror of `find_template_by_cores`'s own `_topology`
|
|
397
|
+
(`tan/planner/template.py`), down to the `templates[i].cores[j]` field
|
|
398
|
+
labels -- so a malformed record produces the byte-identical curated
|
|
399
|
+
message on both sides. tan-cli#1084: this was
|
|
400
|
+
`{c["id"]: c["os"] for c in record.get("cores") or ()}`, which raised
|
|
401
|
+
`KeyError`/`TypeError` on four shapes and silently mis-read a
|
|
402
|
+
non-string `id` into a topology that could never match.
|
|
403
|
+
|
|
404
|
+
`record.get("cores", [])`, not dev's `or ()`: an ABSENT `cores:` still
|
|
405
|
+
degrades to `{}` exactly as before, but a present `cores: null` is
|
|
406
|
+
refused as `got NoneType` rather than silently emptied -- the planner's
|
|
407
|
+
behaviour, and `cores` is `required` with `minItems: 1` in the schema,
|
|
408
|
+
so no catalog that was ever valid stops loading.
|
|
409
|
+
"""
|
|
410
|
+
field = f"templates[{index}]"
|
|
411
|
+
_GUARDS.require_field(record, dict, doc=catalog, field=field)
|
|
412
|
+
entries = _GUARDS.require_field(record.get("cores", []), list,
|
|
413
|
+
doc=catalog, field=f"{field}.cores")
|
|
414
|
+
return {
|
|
415
|
+
_GUARDS.require_key(core, "id", str, doc=catalog,
|
|
416
|
+
field=f"{field}.cores[{j}]"):
|
|
417
|
+
_GUARDS.require_key(core, "os", str, doc=catalog,
|
|
418
|
+
field=f"{field}.cores[{j}]")
|
|
419
|
+
for j, core in enumerate(entries)}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def find_example_by_cores(sdk_root: Path, cores: dict[str, str]) -> str:
|
|
423
|
+
"""The `--from-example`-compatible `src` (e.g. `multicore/mailbox`, no
|
|
424
|
+
leading `examples/`) whose catalog record's `cores:` topology is EXACTLY
|
|
425
|
+
`cores` -- `tan init --topology`'s resolution step.
|
|
426
|
+
|
|
427
|
+
Raises `CoresTopologyNotFoundError` (no exact match),
|
|
428
|
+
`AmbiguousCoresTopologyError` (more than one) or
|
|
429
|
+
`MalformedCatalogError` (the catalog is not the shape its schema
|
|
430
|
+
declares) -- all three refuse rather than guess, unlike
|
|
431
|
+
`unsupported_som` above, which is a best-effort warning path. There is
|
|
432
|
+
no project yet to scaffold without a real answer here: "which template"
|
|
433
|
+
is not a fact `init` can degrade silently on the way "is this SoM
|
|
434
|
+
supported" can.
|
|
435
|
+
|
|
436
|
+
A record with no declared `example` is excluded from the match set
|
|
437
|
+
rather than raising -- the ONE place the two selectors deliberately
|
|
438
|
+
still differ, and dev's own behaviour, kept rather than changed. It and
|
|
439
|
+
the two new refusals this function does add are set out under THE ONE
|
|
440
|
+
REMAINING DIVERGENCE in the module docstring.
|
|
441
|
+
"""
|
|
442
|
+
catalog = sdk_root / CATALOG_RELATIVE
|
|
443
|
+
doc = _GUARDS.read_catalog_document(catalog)
|
|
444
|
+
indexed = list(enumerate(_GUARDS.catalog_templates(doc, path=catalog)))
|
|
445
|
+
topologies = {index: _topology(rec, index, catalog)
|
|
446
|
+
for index, rec in indexed}
|
|
447
|
+
matches = [(index, rec) for index, rec in indexed
|
|
448
|
+
if rec.get("example") and topologies[index] == cores]
|
|
449
|
+
if not matches:
|
|
450
|
+
known = sorted(
|
|
451
|
+
{tuple(sorted(topo.items())) for topo in topologies.values()}
|
|
452
|
+
)
|
|
453
|
+
raise CoresTopologyNotFoundError(cores, known)
|
|
454
|
+
ids = sorted(
|
|
455
|
+
_GUARDS.require_key(rec, "id", str, doc=catalog,
|
|
456
|
+
field=f"templates[{index}]")
|
|
457
|
+
for index, rec in matches)
|
|
458
|
+
if len(matches) > 1:
|
|
459
|
+
raise AmbiguousCoresTopologyError(cores, ids)
|
|
460
|
+
index, record = matches[0]
|
|
461
|
+
example = _GUARDS.require_key(record, "example", str, doc=catalog,
|
|
462
|
+
field=f"templates[{index}]")
|
|
463
|
+
return example.strip().strip("/").removeprefix("examples/")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def parse_topology_arg(raw: str) -> dict[str, str]:
|
|
467
|
+
"""`--topology core_id:os[,core_id:os...]` -- STRICT `id:os` pairs, no OS
|
|
468
|
+
inference (unlike `--cores`' splice parser, `parse_cores`): a selector
|
|
469
|
+
must match a catalog record's topology exactly, so an inferred OS could
|
|
470
|
+
silently look up a topology the caller never actually typed. Mirrors
|
|
471
|
+
alp-sdk's `alp_project._parse_cores_arg` (the same parser
|
|
472
|
+
`find_template_by_cores`'s own CLI front door uses).
|
|
473
|
+
|
|
474
|
+
Raises `ValueError` on any malformed entry -- the caller wraps this in
|
|
475
|
+
an `InitError` (`init.invalid-topology`).
|
|
476
|
+
"""
|
|
477
|
+
cores: dict[str, str] = {}
|
|
478
|
+
for entry in raw.split(","):
|
|
479
|
+
entry = entry.strip()
|
|
480
|
+
if not entry:
|
|
481
|
+
continue
|
|
482
|
+
if ":" not in entry:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f"--topology entry {entry!r} must be core_id:os (e.g. "
|
|
485
|
+
f"m55_hp:zephyr)"
|
|
486
|
+
)
|
|
487
|
+
core_id, _, os_name = entry.partition(":")
|
|
488
|
+
core_id, os_name = core_id.strip(), os_name.strip()
|
|
489
|
+
if not core_id or not os_name:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"--topology entry {entry!r} must be core_id:os (e.g. "
|
|
492
|
+
f"m55_hp:zephyr)"
|
|
493
|
+
)
|
|
494
|
+
if core_id in cores:
|
|
495
|
+
raise ValueError(f"--topology names {core_id!r} more than once")
|
|
496
|
+
cores[core_id] = os_name
|
|
497
|
+
if not cores:
|
|
498
|
+
raise ValueError("--topology must name at least one core_id:os pair")
|
|
499
|
+
return cores
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Per-example facets (`category`, `som`, `board`, `cores`, `coreCount`,
|
|
3
|
+
`osSet`, `declares`) read out of alp-sdk's generated `metadata/catalog.json`.
|
|
4
|
+
|
|
5
|
+
tan-cli#484: `tan examples` used to publish only
|
|
6
|
+
`id`/`sourceDir`/`title`/`description`, so no consumer could filter the
|
|
7
|
+
catalogue by core count, OS, or SoM -- not even by category, though
|
|
8
|
+
`examples/` has always been grouped into one. alp-sdk's companion issue
|
|
9
|
+
answered this by deriving exactly these facets through its topology resolver
|
|
10
|
+
(`scripts/gen_catalog.py::_resolved_core_facets`, which calls the SAME
|
|
11
|
+
`alp_project.py --emit os-topology` path `tan build` itself plans a project
|
|
12
|
+
against) and publishing them pre-computed in `metadata/catalog.json`
|
|
13
|
+
(`build_examples()`). Measured against alp-sdk `origin/dev`
|
|
14
|
+
(`20baaaf19`): the facets are already there.
|
|
15
|
+
|
|
16
|
+
**tan reads that generated JSON; it does not resolve topology itself.**
|
|
17
|
+
I-26 keeps `metadata/**` hardware-fact resolution in alp-sdk -- this module
|
|
18
|
+
is the `examples` analogue of `tan.core.error_catalog` reading
|
|
19
|
+
`metadata/error-catalog.json` rather than re-deriving diagnostics, and of
|
|
20
|
+
`tan.core.example_catalog` reading `metadata/templates/catalog-v1.json`
|
|
21
|
+
rather than re-resolving SoM support. A facet this module cannot find in the
|
|
22
|
+
catalogue is left OFF the entry, never guessed: `cores`/`coreCount`/`osSet`
|
|
23
|
+
resolve through topology and reproducing that logic here would be exactly
|
|
24
|
+
the drift ADR-0020 exists to prevent.
|
|
25
|
+
|
|
26
|
+
**Degrades silently, never refuses `tan examples`.** A checkout predating
|
|
27
|
+
`catalog.json`, or one where it fails to parse, still lists every example by
|
|
28
|
+
directory discovery exactly as before -- the catalogue is enrichment, not a
|
|
29
|
+
second source of truth for WHICH examples exist. Mirrors
|
|
30
|
+
`tan.core.example_catalog.unsupported_som`'s own "never raises, absence
|
|
31
|
+
means nothing to report" contract for the neighbouring
|
|
32
|
+
`templates/catalog-v1.json` reader.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
#: `<sdk_root>/metadata/catalog.json`, generated by `scripts/gen_catalog.py`.
|
|
42
|
+
CATALOG_RELATIVE = Path("metadata") / "catalog.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ExampleFacets:
|
|
47
|
+
"""One example's catalogue-derived facets. `category` is always known --
|
|
48
|
+
it is the catalogue's own grouping key, mirrored from the example's
|
|
49
|
+
parent directory -- every other field is optional, exactly as
|
|
50
|
+
`gen_catalog.py` leaves it: a key it has nothing to say for is omitted
|
|
51
|
+
from the source document entirely rather than emitted null or empty
|
|
52
|
+
(no `som:` on a portable example; `cores`/`coreCount`/`osSet` all
|
|
53
|
+
absent together when the board's topology did not resolve, e.g.
|
|
54
|
+
`SdkRevisionNotBuildable`)."""
|
|
55
|
+
|
|
56
|
+
category: str
|
|
57
|
+
som: str | None
|
|
58
|
+
board: str | None
|
|
59
|
+
cores: tuple[dict, ...] | None
|
|
60
|
+
core_count: int | None
|
|
61
|
+
os_set: tuple[str, ...] | None
|
|
62
|
+
declares: dict[str, bool] | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_example_facets(sdk_root: Path) -> dict[str, ExampleFacets]:
|
|
66
|
+
"""`{sourceDir: ExampleFacets}` for every example the bound checkout's
|
|
67
|
+
catalogue knows about, or `{}` when there is nothing usable to read.
|
|
68
|
+
|
|
69
|
+
Never raises: a missing file (older SDK), an unreadable one, invalid
|
|
70
|
+
JSON, or a document that is not shaped the way `gen_catalog.py` writes
|
|
71
|
+
it all collapse to "no facets this run" -- the caller already has a
|
|
72
|
+
complete example list from directory discovery and must not lose any of
|
|
73
|
+
it because the enrichment source is stale, absent, or malformed.
|
|
74
|
+
"""
|
|
75
|
+
path = sdk_root / CATALOG_RELATIVE
|
|
76
|
+
try:
|
|
77
|
+
raw = path.read_text(encoding="utf-8")
|
|
78
|
+
except (OSError, UnicodeDecodeError):
|
|
79
|
+
return {}
|
|
80
|
+
try:
|
|
81
|
+
doc = json.loads(raw)
|
|
82
|
+
except ValueError:
|
|
83
|
+
return {}
|
|
84
|
+
if not isinstance(doc, dict):
|
|
85
|
+
return {}
|
|
86
|
+
by_category = doc.get("examples")
|
|
87
|
+
if not isinstance(by_category, dict):
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
facets: dict[str, ExampleFacets] = {}
|
|
91
|
+
for category, entries in by_category.items():
|
|
92
|
+
if not isinstance(category, str) or not isinstance(entries, list):
|
|
93
|
+
continue
|
|
94
|
+
for entry in entries:
|
|
95
|
+
if not isinstance(entry, dict):
|
|
96
|
+
continue
|
|
97
|
+
name = entry.get("name")
|
|
98
|
+
if not isinstance(name, str) or not name:
|
|
99
|
+
continue
|
|
100
|
+
# `discover_examples`' own `source_dir` shape (`_subdirectories`
|
|
101
|
+
# walk), always `/`-joined -- this is the join key the two sides
|
|
102
|
+
# meet on, not `entry["path"]` (which additionally carries the
|
|
103
|
+
# `examples/` prefix `gen_catalog.py` writes it with).
|
|
104
|
+
facets[f"{category}/{name}"] = _facets_from_entry(category, entry)
|
|
105
|
+
return facets
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _facets_from_entry(category: str, entry: dict) -> ExampleFacets:
|
|
109
|
+
som = entry.get("som")
|
|
110
|
+
board = entry.get("board")
|
|
111
|
+
cores = entry.get("cores")
|
|
112
|
+
core_count = entry.get("coreCount")
|
|
113
|
+
os_set = entry.get("osSet")
|
|
114
|
+
declares = entry.get("declares")
|
|
115
|
+
|
|
116
|
+
# Element-guarded, not just list/dict-guarded: `examples_cmd.py::as_dict`
|
|
117
|
+
# does `dict(core)` on every `cores` element it emits, and a non-mapping
|
|
118
|
+
# element (a hand-edited catalogue, or a future `gen_catalog.py`
|
|
119
|
+
# regression) would raise `ValueError` there with no envelope at all --
|
|
120
|
+
# exactly the crash this whole module exists to never let through.
|
|
121
|
+
# Malformed elements/pairs are dropped, mirroring the "skip the bad
|
|
122
|
+
# element, keep the good ones" shape already used one level up for
|
|
123
|
+
# `entries`/individual catalogue rows -- an all-bad list/dict collapses
|
|
124
|
+
# to an empty tuple/dict rather than `None`, same as a catalogue that
|
|
125
|
+
# genuinely declared nothing (`declares: {}` is a real, present
|
|
126
|
+
# `gen_catalog.py` shape, not a degradation).
|
|
127
|
+
return ExampleFacets(
|
|
128
|
+
category=category,
|
|
129
|
+
som=som if isinstance(som, str) else None,
|
|
130
|
+
board=board if isinstance(board, str) else None,
|
|
131
|
+
cores=(
|
|
132
|
+
tuple(c for c in cores if isinstance(c, dict)) if isinstance(cores, list) else None
|
|
133
|
+
),
|
|
134
|
+
# `isinstance(x, bool)` is excluded first: `bool` is an `int`
|
|
135
|
+
# subclass in Python, so `coreCount: true` would otherwise pass the
|
|
136
|
+
# `int` check and publish a boolean where the contract promises a
|
|
137
|
+
# core count.
|
|
138
|
+
core_count=(
|
|
139
|
+
core_count
|
|
140
|
+
if isinstance(core_count, int) and not isinstance(core_count, bool)
|
|
141
|
+
else None
|
|
142
|
+
),
|
|
143
|
+
os_set=(
|
|
144
|
+
tuple(v for v in os_set if isinstance(v, str)) if isinstance(os_set, list) else None
|
|
145
|
+
),
|
|
146
|
+
declares=(
|
|
147
|
+
{k: v for k, v in declares.items() if isinstance(k, str) and isinstance(v, bool)}
|
|
148
|
+
if isinstance(declares, dict)
|
|
149
|
+
else None
|
|
150
|
+
),
|
|
151
|
+
)
|