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,1123 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan presets` -- what the New Project wizard is allowed to offer: the SoM
|
|
3
|
+
presets discovered in the resolved alp-sdk checkout, plus tan's own built-in
|
|
4
|
+
defaults.
|
|
5
|
+
|
|
6
|
+
Mirrors `crates/tan-cli/src/commands/presets.rs` and the three `tan-core`
|
|
7
|
+
helpers it composes (`presets::empty_preset_catalogue`,
|
|
8
|
+
`sdk_catalogue::parse_som_preset`, `bootstrap::runtime_for_topology_core`). One
|
|
9
|
+
module, deliberately: the whole command is two directory reads, a small YAML
|
|
10
|
+
read, and four constant lists.
|
|
11
|
+
|
|
12
|
+
**The customer never chooses an OS.** `data.soms[].cores[].os` is DERIVED, per
|
|
13
|
+
core, from the SoM's own topology in the SDK metadata -- a topology entry with a
|
|
14
|
+
`board:` is a Zephyr (Cortex-M) target, one with a `machine:` is a Yocto
|
|
15
|
+
(Cortex-A) one, and failing both the core-id class decides (`a<digit>` at a word
|
|
16
|
+
start -> yocto, else zephyr). It is a REPORT of what the silicon runs, which is
|
|
17
|
+
why there is no `--os` and no `--backend` flag here and why `osChoices` is a
|
|
18
|
+
built-in vocabulary list rather than a menu bound to any SoM. The heterogeneous
|
|
19
|
+
golden (`a55` -> yocto next to `m33` -> zephyr) is the case that proves it: both
|
|
20
|
+
values come back on one SoM, so a consumer cannot read the field as a choice.
|
|
21
|
+
|
|
22
|
+
**`cores[].type` and `cores[].allowedOs` (tan-cli#870).** The default `os` above
|
|
23
|
+
answers "what does this core run today"; a picker also needs "what MAY this core
|
|
24
|
+
run" -- and re-deriving that from the core id with a regex is the duplicated-truth
|
|
25
|
+
defect #870 exists to close: `alp-sdk-vscode@dev`'s
|
|
26
|
+
`packages/alp-webview/src/shared/coreRuntime.ts` re-derives the cortex-a/cortex-m
|
|
27
|
+
class from the core id with its own `/(^|[_-])m\\d/` heuristic, independently of
|
|
28
|
+
whatever this checkout's own SoC metadata says, and two independently-maintained
|
|
29
|
+
copies of the same rule are one edit away from disagreeing. (`alp-sdk-vscode#538`
|
|
30
|
+
itself was a different bug in the same wizard -- Bare-metal was offered for
|
|
31
|
+
every Cortex-M core and the scaffolder wrote it a Zephyr app anyway, which
|
|
32
|
+
`alp_project.py` then refused because `os: baremetal` needs `cmake-args`, not
|
|
33
|
+
because a Cortex-M was offered Bare-metal -- so these fields would not, by
|
|
34
|
+
themselves, have prevented #538; the duplicated-truth argument above is the
|
|
35
|
+
motivation that stands.) Both fields are read from the checkout, never invented here:
|
|
36
|
+
`type` is the raw `metadata/socs/<vendor>/<family>/<part>.json` `cores[].type`
|
|
37
|
+
string (`cortex-a32`, `cortex-m55`, ...), resolved via the SoM preset's own
|
|
38
|
+
`silicon:` key; `allowedOs` is that type's cross-class OS subtracted from the
|
|
39
|
+
checkout's own `board.schema.json` `os:` enum, using `tan.core.os_class` --
|
|
40
|
+
the SAME cortex-a/cortex-m convention
|
|
41
|
+
`tan.planner.validate._enforce_os_matches_core_class` gates a build on --
|
|
42
|
+
reused rather than re-implemented so the wizard's picker and the build-time
|
|
43
|
+
gate can never disagree on the cross-class exclusion itself (a Cortex-A is
|
|
44
|
+
never offered, and never accepted for, Zephyr; a Cortex-M is never offered,
|
|
45
|
+
and never accepted for, Yocto -- both read `tan.core.os_class.cross_class_os`).
|
|
46
|
+
That is narrower than "can never disagree" full stop: for an UNRESOLVED core
|
|
47
|
+
type (`core_type == ""`), `allowedOs` here degrades all the way to `[]`
|
|
48
|
+
(tan-cli#914's `allowed_os_for_core` guard), while `_enforce_os_matches_core_class`
|
|
49
|
+
does not share that guard -- it calls `cross_class_os` directly, which treats
|
|
50
|
+
`""` as just another off-class type and still accepts `os: baremetal` /
|
|
51
|
+
`os: off` for it. Deliberately narrower in presets than in the gate: the
|
|
52
|
+
wizard offers nothing rather than guess, the gate still allows the two
|
|
53
|
+
values every core, resolved or not, may always take (see `_soc_lookups`'s own
|
|
54
|
+
docstring for why this reads `tan.core.os_class` and not `tan.planner.topology`
|
|
55
|
+
directly: the latter's process-global SDK-root binding is the wrong contract
|
|
56
|
+
for a command exercised by dozens of independent per-test checkouts).
|
|
57
|
+
`allowedOs` degrades to `[]` on a schema miss; `type` degrades to `""` -- and
|
|
58
|
+
drags `allowedOs` to `[]` -- on a SoC-lookup miss (no `metadata/socs/**`
|
|
59
|
+
entry for the SoM's `silicon:`), and identically on a SoC-lookup HIT whose
|
|
60
|
+
`type` value is not itself a JSON string (tan-cli#957 -- a schema-invalid
|
|
61
|
+
SoC JSON `soc-spec-v1.schema.json` forbids but `tan` does not itself
|
|
62
|
+
validate against before reading). Neither miss nor malformed value fails the
|
|
63
|
+
command (a synthetic/partial/invalid `--sdk-root` is tolerated elsewhere) --
|
|
64
|
+
no SoM detail here is load-bearing enough to fail `tan presets` over.
|
|
65
|
+
|
|
66
|
+
**`allowedOs: []` alone does not say which of the two above degrades fired --
|
|
67
|
+
`type` does.** Two different unknowns both wire as an empty `allowedOs`
|
|
68
|
+
array with `ok: true` and no issue code: a SoC-lookup miss (no
|
|
69
|
+
`metadata/socs/**` entry, or no entry for this core id -- `type` is ALSO
|
|
70
|
+
`""`), and a schema miss (`type` resolves fine, but the checkout carries no
|
|
71
|
+
`board.schema.json` -- `_os_choices()` returns `None`, `allowed_os_lookup`
|
|
72
|
+
returns `[]` before ever reaching the unresolved-type guard in
|
|
73
|
+
`allowed_os_for_core`). A consumer that needs to tell them apart reads
|
|
74
|
+
`type` alongside `allowedOs`, never `allowedOs` alone: `type == ""` means
|
|
75
|
+
the core's type itself is unresolved; a non-empty `type` with `allowedOs ==
|
|
76
|
+
[]` means the type resolved but this checkout has no schema to validate an
|
|
77
|
+
`os:` value against. Neither shape means "no OS is legal" -- that reading of
|
|
78
|
+
an empty array does not exist on this wire.
|
|
79
|
+
|
|
80
|
+
**Which rule decides where the presets come from.** SoM presets are alp-sdk
|
|
81
|
+
content, so the two candidate readings are "vendor a capture like `tan init`
|
|
82
|
+
does" and "read the resolved checkout". This reads the checkout, on two
|
|
83
|
+
invariants from
|
|
84
|
+
`alp-sdk/docs/superpowers/specs/2026-07-29-tan-port-invariants.md`:
|
|
85
|
+
|
|
86
|
+
* **I-32** bans *shelling* the SDK -- `tan init`/`tan scaffold` read a vendored
|
|
87
|
+
`--emit scaffold` tree "without ever shelling the SDK" because they must work
|
|
88
|
+
with NO checkout present. Nothing here spawns a subprocess (there is no
|
|
89
|
+
subprocess to time out); a `scandir` over `<sdk>/metadata/e1m_modules` is not a
|
|
90
|
+
shell-out, so I-32 is untouched. Anti-pattern #22 in that doc is *shelling
|
|
91
|
+
`init`* instead of reading the vendored tree -- not "reading a checkout".
|
|
92
|
+
* **I-26** ("tan must never learn a hardware fact ... `metadata/**` stays in
|
|
93
|
+
alp-sdk") forbids the vendored alternative HERE, and forbids it hardest: a
|
|
94
|
+
baked SKU list is precisely the hardware fact tan may not hold. Every SKU,
|
|
95
|
+
family, core id and runtime below is read from the checkout at runtime; this
|
|
96
|
+
file names no SKU, no address, no pin and no vendor. With no checkout
|
|
97
|
+
resolvable the SDK-derived lists are legitimately EMPTY plus a warning, which
|
|
98
|
+
is what the oracle does -- so it has no reason to carry a capture.
|
|
99
|
+
|
|
100
|
+
**An unresolved SDK is exit 0 with a warning, not a failure.** The built-in
|
|
101
|
+
defaults are still useful without a checkout, and `presets.sdk-root-unresolved`
|
|
102
|
+
is a FROZEN issue code (`contract/issue-codes.json`): the wizard matches it with
|
|
103
|
+
`===` to know its SoM list is empty for an environmental reason. Promote it to an
|
|
104
|
+
error and the wizard falls back to a static catalogue that carries no `cores`,
|
|
105
|
+
which scaffolds a heterogeneous SoM single-core with no IPC.
|
|
106
|
+
|
|
107
|
+
**Every failure is an envelope, never a traceback.** Both directory reads and
|
|
108
|
+
every file read swallow their IO errors exactly as the Rust's `read_dir(..)` /
|
|
109
|
+
`read_to_string(..).ok()` chains do -- a missing tree, an unreadable entry, a
|
|
110
|
+
non-UTF-8 byte or a wrong-shaped SoM is a skipped entry, never an abort -- and
|
|
111
|
+
the catch-all in `presets` is the backstop for whatever nobody enumerated.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
from __future__ import annotations
|
|
115
|
+
|
|
116
|
+
import functools
|
|
117
|
+
import json
|
|
118
|
+
import os
|
|
119
|
+
import re
|
|
120
|
+
import sys
|
|
121
|
+
from dataclasses import dataclass, replace
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
from typing import Callable
|
|
124
|
+
|
|
125
|
+
import typer
|
|
126
|
+
|
|
127
|
+
from tan.core.metadata_schema import (
|
|
128
|
+
missing_schema_note,
|
|
129
|
+
soc_spec_schema_path,
|
|
130
|
+
som_preset_schema_path,
|
|
131
|
+
validate_document,
|
|
132
|
+
)
|
|
133
|
+
from tan.core.os_class import allowed_os_for_core, infer_runtime_for_core_id
|
|
134
|
+
from tan.core.sdk_discovery import (
|
|
135
|
+
ActiveSdk,
|
|
136
|
+
global_default_foreign_project_issue,
|
|
137
|
+
project_pin_issue,
|
|
138
|
+
resolve_sdk_tiered,
|
|
139
|
+
with_sdk_search,
|
|
140
|
+
)
|
|
141
|
+
from tan.core.shapes import SDK_MARKER, rejected_sdk_root_message
|
|
142
|
+
from tan.core.som_schema_version import (
|
|
143
|
+
SOM_SCHEMA_VERSION,
|
|
144
|
+
is_supported_som_schema_version,
|
|
145
|
+
skipped_presets_message,
|
|
146
|
+
)
|
|
147
|
+
from tan.core.global_flags import accept_global_flags
|
|
148
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
149
|
+
from tan.exit_codes import ExitCode
|
|
150
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
151
|
+
from tan.soc_ref import resolve_soc_path as _resolve_soc_path
|
|
152
|
+
|
|
153
|
+
#: `data.schemaVersion` for this command's payload.
|
|
154
|
+
DATA_SCHEMA_VERSION = "1"
|
|
155
|
+
|
|
156
|
+
#: `SOM_SCHEMA_VERSION` (imported above, `tan.core.som_schema_version`) is the
|
|
157
|
+
#: one `schema_version:` a SoM preset may declare. `parse_som_preset` refuses
|
|
158
|
+
#: anything else rather than guessing at a shape it does not know, so such a
|
|
159
|
+
#: preset is SKIPPED here, as the oracle skips an unknown version
|
|
160
|
+
#: (`parse_som_preset(..).ok()?`) -- but no longer silently: `read_soms`
|
|
161
|
+
#: collects every skipped version and `presets()` reports them as ONE
|
|
162
|
+
#: `SOM_SCHEMA_SKIPPED_CODE` warning (tan-cli#1278: against a pre-v2 SDK every
|
|
163
|
+
#: preset is skipped, and `skus: []` with no issue read as "this SDK has no
|
|
164
|
+
#: SoMs").
|
|
165
|
+
|
|
166
|
+
#: `warning`: SoMs the SDK ships are missing from the listing.
|
|
167
|
+
SOM_SCHEMA_SKIPPED_CODE = "presets.som-schema-version-skipped"
|
|
168
|
+
|
|
169
|
+
#: The FROZEN issue code for "no checkout resolved" (`contract/issue-codes.json`,
|
|
170
|
+
#: severity `warning`). Spelled once; the extension matches it with `===`.
|
|
171
|
+
SDK_UNRESOLVED_CODE = "presets.sdk-root-unresolved"
|
|
172
|
+
SDK_UNRESOLVED_MESSAGE = (
|
|
173
|
+
"alp-sdk root is unresolved. Returning built-in defaults and empty SDK preset lists."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
#: What the reader GOT instead, for the `--sdk-root`-was-given-and-rejected
|
|
177
|
+
#: branch (`tan.core.shapes.rejected_sdk_root_message`, tan-cli#497). The no-flag
|
|
178
|
+
#: message above is byte-pinned by the `presets-no-sdk` golden envelope and the
|
|
179
|
+
#: `["presets","--format","json"]` oracle-parity case and is NOT touched; this
|
|
180
|
+
#: branch is unreachable from either, because neither passes the flag.
|
|
181
|
+
SDK_UNRESOLVED_CONSEQUENCE = "Returning built-in defaults and empty SDK preset lists."
|
|
182
|
+
|
|
183
|
+
#: Built-in, SDK-independent defaults, verbatim from
|
|
184
|
+
#: `tan_core::presets::empty_preset_catalogue`. Order is the wire order (these are
|
|
185
|
+
#: lists, not sets, in the envelope) and the golden pins it.
|
|
186
|
+
#:
|
|
187
|
+
#: `LIBRARIES` is the per-core `cores.<id>.libraries` token set; the ADR-0018
|
|
188
|
+
#: curated `boardLibraries` a top-level `libraries:` entry names are discovered
|
|
189
|
+
#: from the checkout instead -- two different vocabularies, which is why the
|
|
190
|
+
#: envelope carries both.
|
|
191
|
+
LIBRARIES = ["etl", "fmt", "nlohmann_json", "doctest", "lvgl", "mbedtls", "cmsis_dsp", "littlefs"]
|
|
192
|
+
INFERENCE_BACKENDS = ["auto", "cpu", "ethos_u", "drpai", "deepx_dxm1"]
|
|
193
|
+
LOG_LEVELS = ["error", "warn", "info", "debug", "trace"]
|
|
194
|
+
#: The OS VOCABULARY, not a picker: `cores[].os` is derived per core (see the
|
|
195
|
+
#: module docstring). Nothing may bind this list to a SoM or to a `--os` flag.
|
|
196
|
+
OS_CHOICES = ["zephyr", "yocto", "baremetal"]
|
|
197
|
+
|
|
198
|
+
#: SoM preset directory names/files this command considers, from `read_soms`.
|
|
199
|
+
SOM_PREFIX = "E1M-"
|
|
200
|
+
SOM_DIR_YAML = "som.yaml"
|
|
201
|
+
|
|
202
|
+
#: `board`/`machine` as a key of a FLOW mapping (`m33: { board: x }`), for the
|
|
203
|
+
#: no-PyYAML fallback. Anchored on a brace, comma or whitespace so `zephyr_board:`
|
|
204
|
+
#: is not mistaken for `board:`.
|
|
205
|
+
_FLOW_KEY_RE = re.compile(r"[{,\s](board|machine)\s*:")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class SomCore:
|
|
210
|
+
"""One core of a SoM topology: its id, the runtime that core's class runs,
|
|
211
|
+
its raw SoC core type, and the `os:` values that type may legally take
|
|
212
|
+
(tan-cli#870). `id`/`os` are the emitted-order fields the frozen
|
|
213
|
+
`SomCoreEntry` in `presets.rs` already carries; `type`/`allowed_os` are
|
|
214
|
+
additive and both default to their empty value (`""` / `()`) when the
|
|
215
|
+
checkout doesn't resolve them -- see the module docstring."""
|
|
216
|
+
|
|
217
|
+
id: str
|
|
218
|
+
os: str
|
|
219
|
+
type: str = ""
|
|
220
|
+
allowed_os: tuple[str, ...] = ()
|
|
221
|
+
|
|
222
|
+
def as_dict(self) -> dict[str, object]:
|
|
223
|
+
return {
|
|
224
|
+
"id": self.id,
|
|
225
|
+
"os": self.os,
|
|
226
|
+
"type": self.type,
|
|
227
|
+
"allowedOs": list(self.allowed_os),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass(frozen=True)
|
|
232
|
+
class Som:
|
|
233
|
+
"""One discovered SoM preset. Field order matches `SomEntry`."""
|
|
234
|
+
|
|
235
|
+
sku: str
|
|
236
|
+
display_name: str
|
|
237
|
+
family: str
|
|
238
|
+
cores: tuple[SomCore, ...]
|
|
239
|
+
|
|
240
|
+
def as_dict(self) -> dict[str, object]:
|
|
241
|
+
return {
|
|
242
|
+
"sku": self.sku,
|
|
243
|
+
"displayName": self.display_name,
|
|
244
|
+
"family": self.family,
|
|
245
|
+
"cores": [c.as_dict() for c in self.cores],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class SomShapeError(Exception):
|
|
250
|
+
"""The text is not a v2 SoM preset. Caller SKIPS the entry, as the oracle's
|
|
251
|
+
`parse_som_preset(..).ok()?` does -- one malformed SoM must not empty the
|
|
252
|
+
catalogue or fail the command."""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class SomSchemaVersionError(SomShapeError):
|
|
256
|
+
"""The preset declares a `schema_version` this tan does not read. Still a
|
|
257
|
+
skip, but one `read_soms` can report: `.version` is what the file said."""
|
|
258
|
+
|
|
259
|
+
def __init__(self, version: object) -> None:
|
|
260
|
+
super().__init__(
|
|
261
|
+
f"unsupported schema_version {version!r} (this CLI consumes v{SOM_SCHEMA_VERSION})"
|
|
262
|
+
)
|
|
263
|
+
self.version = version
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# The derived runtime (tan_core::bootstrap::runtime_for_topology_core)
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def runtime_for_core(core_id: str, *, board: bool, machine: bool) -> str:
|
|
272
|
+
"""The runtime one topology entry naturally runs: `board:` -> zephyr,
|
|
273
|
+
`machine:` -> yocto, else the core-id heuristic.
|
|
274
|
+
|
|
275
|
+
`board` wins when both are present, matching the oracle's match arms. ONE
|
|
276
|
+
owner of this mapping in the Rust (`tan presets` reports it, `tan bootstrap`
|
|
277
|
+
gates on it) -- so when `bootstrap` lands in this port it must call THIS, not
|
|
278
|
+
a copy, or the two can disagree about which host can build the project.
|
|
279
|
+
"""
|
|
280
|
+
if board:
|
|
281
|
+
return "zephyr"
|
|
282
|
+
if machine:
|
|
283
|
+
return "yocto"
|
|
284
|
+
return infer_runtime_for_core_id(core_id)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# SoM preset reading (tan_core::sdk_catalogue::parse_som_preset, narrowed)
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _clean(value: object) -> str | None:
|
|
293
|
+
"""`str_clean`: a YAML scalar as a string, dropping the placeholder `TBD`.
|
|
294
|
+
|
|
295
|
+
Non-strings are `None` (serde's `as_str()`), and the value is NOT trimmed --
|
|
296
|
+
only the `TBD` comparison is -- because the oracle emits the scalar as
|
|
297
|
+
written.
|
|
298
|
+
"""
|
|
299
|
+
if not isinstance(value, str):
|
|
300
|
+
return None
|
|
301
|
+
return None if value.strip() == "TBD" else value
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def parse_som_preset(
|
|
305
|
+
text: str,
|
|
306
|
+
*,
|
|
307
|
+
core_type_lookup: "Callable[[str | None], dict[str, str]] | None" = None,
|
|
308
|
+
allowed_os_lookup: "Callable[[str], list[str]] | None" = None,
|
|
309
|
+
metadata_root: Path | None = None,
|
|
310
|
+
source: Path | str | None = None,
|
|
311
|
+
warnings: list[str] | None = None,
|
|
312
|
+
skipped: list[str] | None = None,
|
|
313
|
+
) -> Som:
|
|
314
|
+
"""The `sku`/`display_name`/`family`/`topology` slice of a SoM preset.
|
|
315
|
+
|
|
316
|
+
Narrower than the oracle's `parse_som_preset` on purpose: the envelope
|
|
317
|
+
carries exactly these four things (plus, per core, the tan-cli#870
|
|
318
|
+
`type`/`allowed_os` enrichment below), and reading `pad_routes`,
|
|
319
|
+
`i2c_devices` or `capabilities` here would be tan learning hardware facts it
|
|
320
|
+
has no field to report (I-26).
|
|
321
|
+
|
|
322
|
+
`schema_version` is guarded FIRST, before any other key is trusted -- the
|
|
323
|
+
same skew guard every other SDK-file consumer carries. Raises
|
|
324
|
+
`SomShapeError` for every unusable shape; the caller skips the entry.
|
|
325
|
+
|
|
326
|
+
*core_type_lookup*/*allowed_os_lookup* are optional so every existing
|
|
327
|
+
single-argument call (this file's own tests) keeps parsing `id`/`os` alone,
|
|
328
|
+
unchanged; `read_soms` is the one caller that supplies both, bound to the
|
|
329
|
+
checkout it read *text* from.
|
|
330
|
+
|
|
331
|
+
*metadata_root*/*source*/*warnings* (tan-cli#964): when *metadata_root* is
|
|
332
|
+
given, the RAW parsed YAML mapping (not this function's own reshaped
|
|
333
|
+
return value -- see `_load_som_yaml`'s `raw_out`) is checked against
|
|
334
|
+
`som-preset-v2.schema.json` and every violation is appended to *warnings*
|
|
335
|
+
(a caller-owned list, so several presets read in one `tan presets` run
|
|
336
|
+
share one collector rather than each returning its own list a caller has
|
|
337
|
+
to flatten). This is the WARN half of #964's decided rule -- `tan presets`
|
|
338
|
+
reports and continues, it never fails a listing over one schema-invalid
|
|
339
|
+
preset -- so a violation here is never raised, only reported.
|
|
340
|
+
`bootstrap_cmd`'s own call site passes these too, and REFUSES on a
|
|
341
|
+
non-empty result instead; the schema check itself does not know or care
|
|
342
|
+
which policy its caller applies. Only run past the `schema_version`
|
|
343
|
+
guard: a preset that fails THAT guard is already an unusable shape the
|
|
344
|
+
caller skips entirely (`SomShapeError`), and validating a document this
|
|
345
|
+
function is about to discard would just duplicate that verdict under a
|
|
346
|
+
different name. Silently skipped (not "validated clean") when the
|
|
347
|
+
no-PyYAML fallback parsed *text* -- `_load_som_yaml`'s `raw_out` stays
|
|
348
|
+
empty there, because `scan_som_preset` builds no full mapping to check.
|
|
349
|
+
|
|
350
|
+
*skipped* (tan-cli#964 review, major 6, "skip-but-disclose"): a second,
|
|
351
|
+
independent caller-owned collector -- when the schema file is simply
|
|
352
|
+
ABSENT, `validate_document` returns `[]` (nothing to add to *warnings*)
|
|
353
|
+
and this collects `missing_schema_note`'s disclosure instead, so a
|
|
354
|
+
caller can tell "checked, no violations" apart from "not checked at
|
|
355
|
+
all" rather than the two looking identical (`issues: []` either way).
|
|
356
|
+
"""
|
|
357
|
+
raw_holder: list[dict[str, object]] = []
|
|
358
|
+
doc = _load_som_yaml(text, raw_out=raw_holder)
|
|
359
|
+
version = doc.get("schema_version")
|
|
360
|
+
# `bool` is excluded explicitly: Python's `True == 1` is true and `isinstance(
|
|
361
|
+
# True, int)` is too, so `schema_version: true` would pass the guard, where
|
|
362
|
+
# serde's `as_i64()` on a bool is `None` and the oracle rejects the file.
|
|
363
|
+
if not is_supported_som_schema_version(version):
|
|
364
|
+
raise SomSchemaVersionError(version)
|
|
365
|
+
if metadata_root is not None and raw_holder:
|
|
366
|
+
schema_path = som_preset_schema_path(metadata_root)
|
|
367
|
+
resolved_source = source if source is not None else "<som preset>"
|
|
368
|
+
if skipped is not None:
|
|
369
|
+
note = missing_schema_note(schema_path, source=resolved_source)
|
|
370
|
+
if note is not None:
|
|
371
|
+
skipped.append(note)
|
|
372
|
+
if warnings is not None:
|
|
373
|
+
warnings.extend(validate_document(raw_holder[0], schema_path, resolved_source))
|
|
374
|
+
sku = _clean(doc.get("sku")) or ""
|
|
375
|
+
silicon = _clean(doc.get("silicon"))
|
|
376
|
+
core_types = core_type_lookup(silicon) if core_type_lookup is not None else {}
|
|
377
|
+
# `.get`, not `[..]`: the guard above happens to reject every shape that
|
|
378
|
+
# reaches here without a `cores` key, but a KeyError is the one failure
|
|
379
|
+
# this command may not have, so it does not lean on that.
|
|
380
|
+
raw_cores: tuple[SomCore, ...] = tuple(doc.get("cores") or ())
|
|
381
|
+
cores = tuple(
|
|
382
|
+
replace(
|
|
383
|
+
c,
|
|
384
|
+
type=core_types.get(c.id, ""),
|
|
385
|
+
allowed_os=tuple(allowed_os_lookup(core_types.get(c.id, "")))
|
|
386
|
+
if allowed_os_lookup is not None
|
|
387
|
+
else (),
|
|
388
|
+
)
|
|
389
|
+
for c in raw_cores
|
|
390
|
+
)
|
|
391
|
+
return Som(
|
|
392
|
+
sku=sku,
|
|
393
|
+
# `display_name` falls back to `sku`, then to `""`.
|
|
394
|
+
display_name=_clean(doc.get("display_name")) or sku,
|
|
395
|
+
family=_clean(doc.get("family")) or "",
|
|
396
|
+
cores=cores,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _load_som_yaml(text: str, *, raw_out: list[dict[str, object]] | None = None) -> dict[str, object]:
|
|
401
|
+
"""`{schema_version, sku, display_name, family, silicon, cores}` from a SoM
|
|
402
|
+
preset.
|
|
403
|
+
|
|
404
|
+
PyYAML when it is importable, else `scan_som_preset`. tan's runtime
|
|
405
|
+
dependencies are `typer` + `rich` only -- and the frozen binary is built from
|
|
406
|
+
a venv holding exactly those (`scripts/build_binary.sh`), so on the SHIPPED
|
|
407
|
+
artifact the fallback is not a rare path, it is THE path. It therefore may
|
|
408
|
+
not be a degraded answer: `cores` missing or empty is the one thing this
|
|
409
|
+
command must never quietly report, because the wizard scaffolds IPC off it
|
|
410
|
+
and a heterogeneous SoM would come out single-core.
|
|
411
|
+
|
|
412
|
+
`cores` is already a `list[SomCore]` here rather than raw topology, because
|
|
413
|
+
the two readers see topology differently (a parsed mapping vs. indented
|
|
414
|
+
lines) and the runtime derivation must happen once, in `runtime_for_core`.
|
|
415
|
+
|
|
416
|
+
*raw_out* (tan-cli#964), when given, gets the RAW `yaml.safe_load` mapping
|
|
417
|
+
appended (before any reshaping) -- the shape `som-preset-v2.schema.json`
|
|
418
|
+
actually describes (a `topology:` object, not this function's own `cores:
|
|
419
|
+
list[SomCore]` return value, which the schema has never seen and would
|
|
420
|
+
reject on sight). Left empty on the no-PyYAML fallback: `scan_som_preset`
|
|
421
|
+
never builds a full mapping, only the handful of scalars/cores this
|
|
422
|
+
function's own return shape needs, so there is no raw document to
|
|
423
|
+
validate there -- `parse_som_preset` treats an empty `raw_out` as "cannot
|
|
424
|
+
validate", not as "validated clean".
|
|
425
|
+
"""
|
|
426
|
+
try:
|
|
427
|
+
import yaml # noqa: PLC0415 (optional at runtime, by design)
|
|
428
|
+
except ImportError:
|
|
429
|
+
return scan_som_preset(text)
|
|
430
|
+
try:
|
|
431
|
+
doc = yaml.safe_load(text)
|
|
432
|
+
except Exception as err: # noqa: BLE001 -- yaml.YAMLError and anything a loader raises
|
|
433
|
+
raise SomShapeError(f"could not be parsed as YAML: {err}") from err
|
|
434
|
+
if not isinstance(doc, dict):
|
|
435
|
+
# `root.as_mapping().unwrap_or_default()` -> an empty map -> the
|
|
436
|
+
# schema_version guard rejects it. Same outcome, stated here.
|
|
437
|
+
return {}
|
|
438
|
+
if raw_out is not None:
|
|
439
|
+
raw_out.append(doc)
|
|
440
|
+
topology = doc.get("topology")
|
|
441
|
+
cores: list[SomCore] = []
|
|
442
|
+
if isinstance(topology, dict):
|
|
443
|
+
for core_id, node in topology.items():
|
|
444
|
+
if not isinstance(core_id, str):
|
|
445
|
+
continue # `id.as_str()?` -- a non-string key is not a core
|
|
446
|
+
entry = node if isinstance(node, dict) else {}
|
|
447
|
+
cores.append(
|
|
448
|
+
SomCore(
|
|
449
|
+
core_id,
|
|
450
|
+
runtime_for_core(
|
|
451
|
+
core_id,
|
|
452
|
+
board=_clean(entry.get("board")) is not None,
|
|
453
|
+
machine=_clean(entry.get("machine")) is not None,
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
return {
|
|
458
|
+
"schema_version": doc.get("schema_version"),
|
|
459
|
+
"sku": doc.get("sku"),
|
|
460
|
+
"display_name": doc.get("display_name"),
|
|
461
|
+
"family": doc.get("family"),
|
|
462
|
+
"silicon": doc.get("silicon"),
|
|
463
|
+
"cores": cores,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def scan_som_preset(text: str) -> dict[str, object]:
|
|
468
|
+
"""The no-PyYAML reader: the five top-level scalars (`schema_version`,
|
|
469
|
+
`sku`, `display_name`, `family`, `silicon`) plus the `topology:` block's
|
|
470
|
+
core ids and whether each declares `board:`/`machine:`.
|
|
471
|
+
|
|
472
|
+
Deliberately NOT a YAML parser -- it answers only what the envelope carries,
|
|
473
|
+
the same bargain `validate_cmd._load_yaml` and `generate_cmd._scan_som_sku`
|
|
474
|
+
strike. What it must get right on the real files is the topology, which in
|
|
475
|
+
every shipped SoM preset is a plain two-space block mapping with interleaved
|
|
476
|
+
comments; the flow form (`m33: { board: x }`) is handled too because the
|
|
477
|
+
oracle's own unit tests use it.
|
|
478
|
+
|
|
479
|
+
Ponytail note: line-oriented, so a block scalar or an anchor in a SoM preset
|
|
480
|
+
would read wrong. No shipped preset has either; PyYAML covers them wherever
|
|
481
|
+
it is installed, and the upgrade path if one appears is to vendor a real
|
|
482
|
+
parser, not to widen this.
|
|
483
|
+
"""
|
|
484
|
+
out: dict[str, object] = {}
|
|
485
|
+
cores: list[SomCore] = []
|
|
486
|
+
# `None` until `topology:` opens; then the indent its core ids sit at.
|
|
487
|
+
topology_indent: int | None = None
|
|
488
|
+
in_topology = False
|
|
489
|
+
# The core id being read, and which of `board:`/`machine:` it has declared.
|
|
490
|
+
open_core: list = [None, False, False]
|
|
491
|
+
|
|
492
|
+
def close_core() -> None:
|
|
493
|
+
core_id, board, machine = open_core
|
|
494
|
+
if core_id is not None:
|
|
495
|
+
cores.append(
|
|
496
|
+
SomCore(core_id, runtime_for_core(core_id, board=board, machine=machine))
|
|
497
|
+
)
|
|
498
|
+
open_core[:] = [None, False, False]
|
|
499
|
+
|
|
500
|
+
for raw in text.splitlines():
|
|
501
|
+
stripped = raw.strip()
|
|
502
|
+
if not stripped or stripped.startswith("#"):
|
|
503
|
+
continue
|
|
504
|
+
indent = len(raw) - len(raw.lstrip())
|
|
505
|
+
key, sep, value = stripped.partition(":")
|
|
506
|
+
key = key.strip()
|
|
507
|
+
|
|
508
|
+
if indent == 0:
|
|
509
|
+
close_core()
|
|
510
|
+
in_topology = bool(sep) and key == "topology"
|
|
511
|
+
topology_indent = None
|
|
512
|
+
if not sep:
|
|
513
|
+
continue
|
|
514
|
+
if key == "schema_version":
|
|
515
|
+
out["schema_version"] = _scalar_int(value)
|
|
516
|
+
elif key in ("sku", "display_name", "family", "silicon"):
|
|
517
|
+
out[key] = _scalar(value)
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
if not in_topology or not sep:
|
|
521
|
+
continue
|
|
522
|
+
if topology_indent is None:
|
|
523
|
+
topology_indent = indent
|
|
524
|
+
if indent <= topology_indent:
|
|
525
|
+
# A core id. Its value may be an inline flow mapping carrying the
|
|
526
|
+
# very keys the block form puts on the following lines.
|
|
527
|
+
close_core()
|
|
528
|
+
open_core[0] = key
|
|
529
|
+
for flow_key in _FLOW_KEY_RE.findall(f" {value}"):
|
|
530
|
+
open_core[1 if flow_key == "board" else 2] = True
|
|
531
|
+
elif open_core[0] is not None:
|
|
532
|
+
# A key of the current core. `_scalar` so an empty or `TBD` value
|
|
533
|
+
# does not count as declared -- `str_clean` drops both.
|
|
534
|
+
if key == "board" and _scalar(value) is not None:
|
|
535
|
+
open_core[1] = True
|
|
536
|
+
elif key == "machine" and _scalar(value) is not None:
|
|
537
|
+
open_core[2] = True
|
|
538
|
+
close_core()
|
|
539
|
+
out["cores"] = cores
|
|
540
|
+
return out
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _scalar(value: str) -> str | None:
|
|
544
|
+
"""A block-mapping scalar: trailing ` #comment` dropped, surrounding quotes
|
|
545
|
+
stripped, `TBD` and empty treated as absent (`str_clean`)."""
|
|
546
|
+
text = value
|
|
547
|
+
hashed = text.find(" #")
|
|
548
|
+
if hashed != -1:
|
|
549
|
+
text = text[:hashed]
|
|
550
|
+
text = text.strip()
|
|
551
|
+
if len(text) >= 2 and text[0] == text[-1] and text[0] in "\"'":
|
|
552
|
+
text = text[1:-1]
|
|
553
|
+
return _clean(text) or None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _scalar_int(value: str) -> int | None:
|
|
557
|
+
"""A BARE integer scalar, or `None`. Quotes are not stripped first, so
|
|
558
|
+
`schema_version: "1"` is `None` here exactly as serde's `as_i64()` on a
|
|
559
|
+
string is -- a quoted version is not a v1 preset."""
|
|
560
|
+
text = value.split(" #", 1)[0].strip()
|
|
561
|
+
try:
|
|
562
|
+
return int(text)
|
|
563
|
+
except ValueError:
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
# Discovery
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _entries(directory: Path) -> list[os.DirEntry]:
|
|
573
|
+
"""`directory`'s entries, or `[]` when it will not read -- `read_dir(..)`'s
|
|
574
|
+
`else { return Vec::new() }`. A missing `metadata/` tree is an empty
|
|
575
|
+
catalogue, not an error."""
|
|
576
|
+
try:
|
|
577
|
+
with os.scandir(directory) as it:
|
|
578
|
+
return list(it)
|
|
579
|
+
except OSError:
|
|
580
|
+
return []
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# `_resolve_soc_path` (tan-cli#917): was a verbatim second copy of the path
|
|
584
|
+
# arithmetic `tan.planner.som_metadata.resolve_soc_path` also implements --
|
|
585
|
+
# now both import the single definition in the leaf module `tan.soc_ref`,
|
|
586
|
+
# which imports neither `tan.planner` nor anything else that reads real files
|
|
587
|
+
# at import time, so importing it here does not reintroduce the `bind_sdk_root`
|
|
588
|
+
# rebinding hazard `_soc_lookups`'s docstring (below) documents avoiding.
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _soc_lookups(
|
|
592
|
+
sdk_root: str,
|
|
593
|
+
*,
|
|
594
|
+
warnings: list[str] | None = None,
|
|
595
|
+
skipped: list[str] | None = None,
|
|
596
|
+
) -> tuple[
|
|
597
|
+
"Callable[[str | None], dict[str, str]] | None",
|
|
598
|
+
"Callable[[str], list[str]] | None",
|
|
599
|
+
]:
|
|
600
|
+
"""The tan-cli#870 `(core_type_lookup, allowed_os_lookup)` pair, bound to
|
|
601
|
+
*sdk_root*, or `(None, None)` when *sdk_root* names no metadata tree at
|
|
602
|
+
all (matching `read_soms`'s own "missing tree is an empty catalogue, not
|
|
603
|
+
an error").
|
|
604
|
+
|
|
605
|
+
`core_type_lookup` resolves a SoM's `silicon:` key to
|
|
606
|
+
`metadata/socs/<vendor>/<family>/<part>.json` and reads its `cores[].type`
|
|
607
|
+
strings. `allowed_os_lookup` reuses `tan.core.os_class` -- the SAME
|
|
608
|
+
cortex-a/cortex-m convention `tan.planner.validate._enforce_os_matches_core_class`
|
|
609
|
+
gates a build on -- run against *sdk_root*'s own `board.schema.json` `os:`
|
|
610
|
+
enum, so this command's answer and the build-time rule cannot drift apart
|
|
611
|
+
(the whole point of #870).
|
|
612
|
+
|
|
613
|
+
**Deliberately does NOT import `tan.planner`.** `tan.planner` requires
|
|
614
|
+
`bind_sdk_root` before its first import and then refuses to rebind to a
|
|
615
|
+
different root for the rest of the process -- the right contract for
|
|
616
|
+
`tan build`'s one big planning pass per process, the wrong one for `tan
|
|
617
|
+
presets`, which `read_soms` calls from dozens of independent tests each
|
|
618
|
+
against their own disposable synthetic SDK root. Routing through it here
|
|
619
|
+
made the FIRST such test to run in an xdist worker permanently bind that
|
|
620
|
+
whole worker to its throwaway root, so every later test in it that needed
|
|
621
|
+
a DIFFERENT (often the real) root failed outright -- measured while
|
|
622
|
+
building #870: 292 unrelated `tests/parity/test_planner_emit_parity.py`
|
|
623
|
+
cases turned from passing to `PlannerRootError` this way. `tan.core.os_class`
|
|
624
|
+
exists precisely so this file (and any future binding-free consumer) can
|
|
625
|
+
reuse the convention without paying that cost; see its own docstring.
|
|
626
|
+
Every IO error below (a missing/unreadable/non-JSON SoC or schema file) is
|
|
627
|
+
a skipped/empty answer, never a raise -- `type`/`allowedOs` degrade to
|
|
628
|
+
their empty default the same way every other field in this file does.
|
|
629
|
+
|
|
630
|
+
*warnings* (tan-cli#964), when given, collects every `soc-spec-v1`
|
|
631
|
+
violation `core_type_lookup` finds -- the WARN half of #964's decided
|
|
632
|
+
rule; `tan presets` still degrades `type` to `""` exactly as before (the
|
|
633
|
+
comment inside `core_type_lookup` below is UNCHANGED, the `isinstance`
|
|
634
|
+
guard stays), it just now also SAYS which file and field it degraded.
|
|
635
|
+
Deduplicated by `soc_path` within one `_soc_lookups` binding, so a family
|
|
636
|
+
of SoMs that share one `silicon:` ref (common -- V2N101/V2N102/V2M101/
|
|
637
|
+
V2M102 are one PCB) does not repeat the same violation once per SoM.
|
|
638
|
+
|
|
639
|
+
*skipped* (tan-cli#964 review, major 6): the same "skip-but-disclose"
|
|
640
|
+
collector `parse_som_preset` takes, deduplicated the same way *warnings*
|
|
641
|
+
is above.
|
|
642
|
+
"""
|
|
643
|
+
root = Path(sdk_root) / "metadata"
|
|
644
|
+
if not root.is_dir():
|
|
645
|
+
return None, None
|
|
646
|
+
|
|
647
|
+
_warned_soc_paths: set[str] = set()
|
|
648
|
+
|
|
649
|
+
def core_type_lookup(silicon: str | None) -> dict[str, str]:
|
|
650
|
+
soc_path = _resolve_soc_path(silicon, root)
|
|
651
|
+
if soc_path is None:
|
|
652
|
+
return {}
|
|
653
|
+
try:
|
|
654
|
+
doc = json.loads(soc_path.read_text(encoding="utf-8"))
|
|
655
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
656
|
+
return {}
|
|
657
|
+
if str(soc_path) not in _warned_soc_paths:
|
|
658
|
+
_warned_soc_paths.add(str(soc_path))
|
|
659
|
+
schema_path = soc_spec_schema_path(root)
|
|
660
|
+
if skipped is not None:
|
|
661
|
+
note = missing_schema_note(schema_path, source=soc_path)
|
|
662
|
+
if note is not None:
|
|
663
|
+
skipped.append(note)
|
|
664
|
+
if warnings is not None:
|
|
665
|
+
warnings.extend(validate_document(doc, schema_path, soc_path))
|
|
666
|
+
cores = doc.get("cores") if isinstance(doc, dict) else None
|
|
667
|
+
if not isinstance(cores, list):
|
|
668
|
+
return {}
|
|
669
|
+
# `c.get("type", "")` is UNVALIDATED against `soc-spec-v1.schema.json`'s
|
|
670
|
+
# own `"type": {"type": "string"}` -- a clean, schema-valid SDK never
|
|
671
|
+
# produces anything else, but this file's whole job (tan-cli#870) is
|
|
672
|
+
# reading a `--sdk-root` tree `validate_metadata.py` never ran against,
|
|
673
|
+
# exactly the hand-authored/mid-port state `porting-a-new-som` leaves a
|
|
674
|
+
# SoC JSON in. A non-string `type` here is NOT a `str | None` under
|
|
675
|
+
# `isinstance`, and this dict feeds BOTH `SomCore.type` (written
|
|
676
|
+
# verbatim to the wire by `as_dict`) and `allowed_os_for_core`'s
|
|
677
|
+
# `(core_type or "").lower()` -- a non-string reaching either one is
|
|
678
|
+
# a wire leak or an `AttributeError` respectively (tan-cli#957). Every
|
|
679
|
+
# non-string normalises to `""`, the SAME "unresolved" sentinel a
|
|
680
|
+
# missing/unreadable SoC file already degrades `type` to two lines
|
|
681
|
+
# above -- a malformed `type` and a missing one are indistinguishable
|
|
682
|
+
# to every downstream consumer, on purpose.
|
|
683
|
+
return {
|
|
684
|
+
c["id"]: c["type"] if isinstance(c.get("type"), str) else ""
|
|
685
|
+
for c in cores
|
|
686
|
+
if isinstance(c, dict) and isinstance(c.get("id"), str)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
@functools.lru_cache(maxsize=None)
|
|
690
|
+
def _os_choices() -> tuple[str, ...] | None:
|
|
691
|
+
# No `BOARD_SCHEMA`-style fallback to a globally bound "in-tree"
|
|
692
|
+
# schema: unlike the planner, `presets` never has a second tree to
|
|
693
|
+
# fall back to -- it is reading exactly one checkout throughout, so a
|
|
694
|
+
# checkout with no schema of its own answers `None` (degrade), not a
|
|
695
|
+
# DIFFERENT checkout's enum.
|
|
696
|
+
schema_path = root / "schemas" / "board.schema.json"
|
|
697
|
+
try:
|
|
698
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
699
|
+
return tuple(schema["$defs"]["core_entry"]["properties"]["os"]["enum"])
|
|
700
|
+
except (OSError, UnicodeDecodeError, ValueError, KeyError, TypeError):
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
def allowed_os_lookup(core_type: str) -> list[str]:
|
|
704
|
+
choices = _os_choices()
|
|
705
|
+
if choices is None:
|
|
706
|
+
return []
|
|
707
|
+
# `allowed_os_for_core` (tan.core.os_class) is the single place the
|
|
708
|
+
# "" == unresolved sentinel -> [] degrade lives, shared with
|
|
709
|
+
# `tan.planner.topology._allowed_os_for_core` -- see its own
|
|
710
|
+
# docstring for why (tan-cli#914: the two must never drift apart on
|
|
711
|
+
# this, the same reason this file reuses `tan.core.os_class` at all).
|
|
712
|
+
return allowed_os_for_core(core_type, choices)
|
|
713
|
+
|
|
714
|
+
return core_type_lookup, allowed_os_lookup
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def read_soms(
|
|
718
|
+
sdk_root: str,
|
|
719
|
+
*,
|
|
720
|
+
warnings: list[str] | None = None,
|
|
721
|
+
skipped: list[str] | None = None,
|
|
722
|
+
version_skipped: list[object] | None = None,
|
|
723
|
+
) -> list[Som]:
|
|
724
|
+
"""SoM presets under `<sdk>/metadata/e1m_modules`, sorted by SKU.
|
|
725
|
+
|
|
726
|
+
Both layouts the SDK has used are supported, as in the oracle: a flat
|
|
727
|
+
`E1M-X.yaml`, or an `E1M-X/som.yaml` directory. An entry that is not
|
|
728
|
+
`E1M-*`, carries no yaml, will not read, is not UTF-8, or is not a v2 preset
|
|
729
|
+
is skipped -- never an error.
|
|
730
|
+
|
|
731
|
+
*version_skipped* (tan-cli#1278), when given, collects the declared
|
|
732
|
+
`schema_version` of every preset skipped for declaring the wrong one, so
|
|
733
|
+
the caller can say so instead of returning a silently shorter list.
|
|
734
|
+
|
|
735
|
+
*warnings* (tan-cli#964), when given, collects every `soc-spec-v1` /
|
|
736
|
+
`som-preset-v2` violation found while reading -- see `_soc_lookups` and
|
|
737
|
+
`parse_som_preset`'s own docstrings for what feeds it. *skipped*
|
|
738
|
+
(tan-cli#964 review, major 6) does the same for the "skip-but-disclose"
|
|
739
|
+
half: a note per schema file this walk found simply absent.
|
|
740
|
+
"""
|
|
741
|
+
metadata_root = Path(sdk_root) / "metadata"
|
|
742
|
+
root = metadata_root / "e1m_modules"
|
|
743
|
+
core_type_lookup, allowed_os_lookup = _soc_lookups(
|
|
744
|
+
sdk_root, warnings=warnings, skipped=skipped
|
|
745
|
+
)
|
|
746
|
+
found: list[Som] = []
|
|
747
|
+
for entry in _entries(root):
|
|
748
|
+
if not entry.name.startswith(SOM_PREFIX):
|
|
749
|
+
continue
|
|
750
|
+
path = Path(entry.path)
|
|
751
|
+
if _is_dir(entry):
|
|
752
|
+
yaml_path = path / SOM_DIR_YAML
|
|
753
|
+
elif entry.name.endswith(".yaml"):
|
|
754
|
+
yaml_path = path
|
|
755
|
+
else:
|
|
756
|
+
continue
|
|
757
|
+
try:
|
|
758
|
+
text = yaml_path.read_text(encoding="utf-8")
|
|
759
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
760
|
+
# `read_to_string(..).ok()?`. `encoding="utf-8"` explicitly (I-27):
|
|
761
|
+
# the default decodes with the host locale, and the shipped presets
|
|
762
|
+
# carry non-ASCII prose, so a bare read raises on a cp1252 Windows
|
|
763
|
+
# host and passes on ubuntu CI.
|
|
764
|
+
continue
|
|
765
|
+
try:
|
|
766
|
+
found.append(
|
|
767
|
+
parse_som_preset(
|
|
768
|
+
text,
|
|
769
|
+
core_type_lookup=core_type_lookup,
|
|
770
|
+
allowed_os_lookup=allowed_os_lookup,
|
|
771
|
+
metadata_root=metadata_root,
|
|
772
|
+
source=yaml_path,
|
|
773
|
+
warnings=warnings,
|
|
774
|
+
skipped=skipped,
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
except SomSchemaVersionError as err:
|
|
778
|
+
if version_skipped is not None:
|
|
779
|
+
version_skipped.append(err.version)
|
|
780
|
+
continue
|
|
781
|
+
except SomShapeError:
|
|
782
|
+
continue
|
|
783
|
+
found.sort(key=lambda s: s.sku)
|
|
784
|
+
return found
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _is_dir(entry: os.DirEntry) -> bool:
|
|
788
|
+
"""`entry.path.is_dir()` -- symlinks FOLLOWED, matching the oracle's
|
|
789
|
+
`Path::is_dir()` (not the non-following `DirEntry::file_type()` that
|
|
790
|
+
`examples` mirrors). `OSError` -> not a directory."""
|
|
791
|
+
try:
|
|
792
|
+
return entry.is_dir()
|
|
793
|
+
except OSError:
|
|
794
|
+
return False
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def read_board_libraries(sdk_root: str) -> list[str]:
|
|
798
|
+
"""The ADR-0018 curated library names: the stem of each
|
|
799
|
+
`<sdk>/metadata/libraries/<name>.yaml`, sorted and de-duplicated.
|
|
800
|
+
|
|
801
|
+
`README*` is skipped case-insensitively; a non-yaml entry is skipped. These
|
|
802
|
+
are the values a board.yaml TOP-LEVEL `libraries:` entry names -- a different
|
|
803
|
+
vocabulary from the built-in per-core `LIBRARIES` tokens.
|
|
804
|
+
"""
|
|
805
|
+
root = Path(sdk_root) / "metadata" / "libraries"
|
|
806
|
+
names: set[str] = set()
|
|
807
|
+
for entry in _entries(root):
|
|
808
|
+
name = entry.name
|
|
809
|
+
if not name.endswith(".yaml") or name.lower().startswith("readme"):
|
|
810
|
+
continue
|
|
811
|
+
# `trim_end_matches(".yaml")` strips REPEATED suffixes, so `a.yaml.yaml`
|
|
812
|
+
# is `a`; `removesuffix` would leave `a.yaml`.
|
|
813
|
+
while name.endswith(".yaml"):
|
|
814
|
+
name = name[: -len(".yaml")]
|
|
815
|
+
names.add(name)
|
|
816
|
+
return sorted(names)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
# ---------------------------------------------------------------------------
|
|
820
|
+
# Project/SDK resolution (util.rs::resolve_cli_project_context)
|
|
821
|
+
# ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _posix(value: str) -> str:
|
|
825
|
+
"""`to_posix`: backslashes to forward slashes, nothing else.
|
|
826
|
+
|
|
827
|
+
A string, never a `Path` round-trip: `str(Path("./sdk"))` is `sdk`, dropping
|
|
828
|
+
the `./` the user typed, while Rust's `Path::new("./sdk").to_string_lossy()`
|
|
829
|
+
keeps it -- and `./sdk` is what the golden pins for both `sdk.root` and
|
|
830
|
+
`data.sdkRoot`.
|
|
831
|
+
"""
|
|
832
|
+
return value.replace("\\", "/")
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def resolve_project_paths(project: str | None, board_yaml: str | None) -> tuple[str, str]:
|
|
836
|
+
"""`(project.root, project.boardYaml)` as the envelope reports them: the
|
|
837
|
+
normalised absolute workspace root, and `board.yaml` under it.
|
|
838
|
+
|
|
839
|
+
Normalised (`.` dropped, `..` popped) like `normalize_path`, because the root
|
|
840
|
+
is joined from the CWD and `--project`. The board path is then joined WITHOUT
|
|
841
|
+
normalising, matching `resolve_board_yaml_path` -- `--board-yaml ../x.yaml`
|
|
842
|
+
reports `<root>/../x.yaml` on both implementations.
|
|
843
|
+
"""
|
|
844
|
+
try:
|
|
845
|
+
cwd = os.getcwd()
|
|
846
|
+
except OSError:
|
|
847
|
+
cwd = "." # `current_dir().unwrap_or_else(|_| PathBuf::from("."))`
|
|
848
|
+
root = _posix(os.path.normpath(os.path.join(cwd, project or ".")))
|
|
849
|
+
leaf = board_yaml or "board.yaml"
|
|
850
|
+
if os.path.isabs(leaf):
|
|
851
|
+
return root, _posix(leaf)
|
|
852
|
+
# No separator when the root already ends in one: at a drive root (`C:/`)
|
|
853
|
+
# a blind join would emit `C://board.yaml`.
|
|
854
|
+
sep = "" if root.endswith(("/", "\\")) else "/"
|
|
855
|
+
return root, f"{root}{sep}{_posix(leaf)}"
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def resolve_sdk(sdk_root: str | None, workspace_root: str) -> ActiveSdk:
|
|
859
|
+
"""The resolved checkout as an `ActiveSdk` (posix `.path`), `.path is None`
|
|
860
|
+
when nothing usable resolved.
|
|
861
|
+
|
|
862
|
+
`resolve_sdk_tiered` walks the four tiers (`--sdk-root` > project pin >
|
|
863
|
+
global default > discovery) and is TERMINAL on `--sdk-root` (I-31): a typo'd
|
|
864
|
+
flag never falls through to a lower tier and silently reports a different
|
|
865
|
+
SDK. The loader-marker check here is what core's `resolve_project_context`
|
|
866
|
+
re-applies on top of that -- so an invalid `--sdk-root` resolves to nothing
|
|
867
|
+
(empty lists + the frozen warning), which is the oracle's behaviour and NOT
|
|
868
|
+
the same as "resolved something else".
|
|
869
|
+
|
|
870
|
+
`.broken_project_pin` (tan-cli#263 review) and `.foreign_global_default_for`
|
|
871
|
+
(tan-cli#464) both carry through onto the returned `ActiveSdk` -- INCLUDING
|
|
872
|
+
when this call does NOT resolve to a usable checkout (tan-cli#468). This
|
|
873
|
+
used to return a bare `None` on that branch (`active.path` is `None`, or
|
|
874
|
+
the loader marker is missing), dropping both facts on the floor: neither
|
|
875
|
+
caller could surface a fact it was never given, so a workspace with, say,
|
|
876
|
+
a broken `.alp/sdk-path` pin AND no OTHER tier resolving anything reported
|
|
877
|
+
`presets.sdk-root-unresolved` / `clean.sdk-root-not-found` alone, with no
|
|
878
|
+
`sdk.project-pin-unresolved` alongside it -- the same silent-drop shape
|
|
879
|
+
tan-cli#263/#464 exist to close, just on the "resolved to nothing at all"
|
|
880
|
+
branch rather than the "resolved to something else" one those issues
|
|
881
|
+
named. Both callers now check `.path is not None` rather than this
|
|
882
|
+
function's own truthiness -- an `ActiveSdk` with `path=None` is still
|
|
883
|
+
truthy (it is never `None` any more), and unpacking one is exactly how a
|
|
884
|
+
caller reads `broken_project_pin`/`foreign_global_default_for` even when
|
|
885
|
+
there is no checkout to report.
|
|
886
|
+
Returned as `ActiveSdk` itself, not a growing tuple: a fact one caller
|
|
887
|
+
needs must not force every other caller's unpacking to grow a matching
|
|
888
|
+
positional slot (the same reasoning `tan.core.sdk_discovery.SdkRootResolution`
|
|
889
|
+
documents).
|
|
890
|
+
"""
|
|
891
|
+
active = resolve_sdk_tiered(sdk_root, Path(workspace_root))
|
|
892
|
+
if active.path is None or not Path(active.path).joinpath(*SDK_MARKER).exists():
|
|
893
|
+
return ActiveSdk(
|
|
894
|
+
None, active.tier, active.broken_project_pin, active.foreign_global_default_for
|
|
895
|
+
)
|
|
896
|
+
return ActiveSdk(
|
|
897
|
+
_posix(active.path), active.tier, active.broken_project_pin,
|
|
898
|
+
active.foreign_global_default_for,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _version_skipped_issue(sdk_path: str, versions: list[object]) -> Issue:
|
|
903
|
+
"""ONE warning for every preset `read_soms` skipped on `schema_version`
|
|
904
|
+
(tan-cli#1278), not one per preset: against a pre-v2 SDK that is every
|
|
905
|
+
preset it ships, and twenty identical lines bury the remedy."""
|
|
906
|
+
count = len(versions)
|
|
907
|
+
noun = "SoM preset" if count == 1 else "SoM presets"
|
|
908
|
+
where = _posix(str(Path(sdk_path) / "metadata" / "e1m_modules"))
|
|
909
|
+
return Issue(
|
|
910
|
+
SOM_SCHEMA_SKIPPED_CODE,
|
|
911
|
+
"warning",
|
|
912
|
+
skipped_presets_message(f"skipped {count} {noun} under {where}", versions),
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
# ---------------------------------------------------------------------------
|
|
917
|
+
# Text rendering
|
|
918
|
+
# ---------------------------------------------------------------------------
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def render_presets_text(
|
|
922
|
+
soms: list[Som], board_libraries: list[str], verbose: bool
|
|
923
|
+
) -> list[str]:
|
|
924
|
+
"""The count line, then one line per SoM.
|
|
925
|
+
|
|
926
|
+
Takes `Som` rather than the bare SKU strings it used to: the count line
|
|
927
|
+
answered nothing on its own, and every field the fuller answer needs was
|
|
928
|
+
already parsed and then thrown away by the caller.
|
|
929
|
+
"""
|
|
930
|
+
lines = [
|
|
931
|
+
f"presets: skus={len(soms)} libraries={len(LIBRARIES)} "
|
|
932
|
+
f"boardLibraries={len(board_libraries)}"
|
|
933
|
+
]
|
|
934
|
+
if not soms:
|
|
935
|
+
return lines
|
|
936
|
+
sku_width = max(len(s.sku) for s in soms)
|
|
937
|
+
for som in soms:
|
|
938
|
+
lines.append(f" {som.sku:<{sku_width}} {som.display_name}")
|
|
939
|
+
if verbose:
|
|
940
|
+
cores = ", ".join(f"{c.id} ({c.os})" for c in som.cores)
|
|
941
|
+
lines.append(f" {'':<{sku_width}} family: {som.family}; cores: {cores}")
|
|
942
|
+
return lines
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# ---------------------------------------------------------------------------
|
|
946
|
+
# Entry point
|
|
947
|
+
# ---------------------------------------------------------------------------
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def presets(
|
|
951
|
+
project: str = typer.Option(
|
|
952
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
953
|
+
),
|
|
954
|
+
board_yaml: str = typer.Option(
|
|
955
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
956
|
+
),
|
|
957
|
+
sdk_root: str = typer.Option(None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."),
|
|
958
|
+
verbose: bool = typer.Option(
|
|
959
|
+
False, "--verbose", help="Also show each SKU's family and core/OS pairs."
|
|
960
|
+
),
|
|
961
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
962
|
+
) -> None:
|
|
963
|
+
"""List the SDK's SoM presets plus tan's built-in defaults."""
|
|
964
|
+
json_mode = output_format == "json"
|
|
965
|
+
|
|
966
|
+
root, board_path = ".", "./board.yaml"
|
|
967
|
+
sdk: ActiveSdk | None = None
|
|
968
|
+
soms: list[Som] = []
|
|
969
|
+
board_libraries: list[str] = []
|
|
970
|
+
issues: list[Issue] = []
|
|
971
|
+
exit_code = ExitCode.SUCCESS
|
|
972
|
+
try:
|
|
973
|
+
root, board_path = resolve_project_paths(project, board_yaml)
|
|
974
|
+
sdk = resolve_sdk(sdk_root, root)
|
|
975
|
+
# tan-cli#468: `resolve_sdk` now always returns an `ActiveSdk` -- never
|
|
976
|
+
# a bare `None` -- so `broken_project_pin`/`foreign_global_default_for`
|
|
977
|
+
# are read off it unconditionally, whether or not `.path` resolved.
|
|
978
|
+
# The `--sdk-root` case is unaffected: that tier is terminal, so both
|
|
979
|
+
# fields stay `None` there regardless of which branch runs below.
|
|
980
|
+
issues = [
|
|
981
|
+
issue
|
|
982
|
+
for issue in (
|
|
983
|
+
project_pin_issue(sdk.broken_project_pin, sdk.tier),
|
|
984
|
+
global_default_foreign_project_issue(sdk.foreign_global_default_for),
|
|
985
|
+
)
|
|
986
|
+
if issue is not None
|
|
987
|
+
]
|
|
988
|
+
if sdk.path is not None:
|
|
989
|
+
schema_warnings: list[str] = []
|
|
990
|
+
schema_skipped: list[str] = []
|
|
991
|
+
version_skipped: list[object] = []
|
|
992
|
+
soms = read_soms(
|
|
993
|
+
sdk.path, warnings=schema_warnings, skipped=schema_skipped,
|
|
994
|
+
version_skipped=version_skipped,
|
|
995
|
+
)
|
|
996
|
+
board_libraries = read_board_libraries(sdk.path)
|
|
997
|
+
# tan-cli#964: warn-and-continue, never refuse -- `tan presets`
|
|
998
|
+
# already tolerates a missing/malformed SoM detail (see the
|
|
999
|
+
# module docstring's "no SoM detail here is load-bearing enough
|
|
1000
|
+
# to fail `tan presets` over"); a schema-invalid `cores[].type`
|
|
1001
|
+
# is now VISIBLE (named file, pointer, and what was found) rather
|
|
1002
|
+
# than silently degrading to `type: ""` with no trace of why.
|
|
1003
|
+
issues.extend(
|
|
1004
|
+
Issue("presets.metadata-schema-invalid", "warning", w)
|
|
1005
|
+
for w in schema_warnings
|
|
1006
|
+
)
|
|
1007
|
+
# tan-cli#964 review (major 6, "skip-but-disclose"): an absent
|
|
1008
|
+
# schema file used to degrade SILENTLY (`schema_warnings` stays
|
|
1009
|
+
# empty because there was nothing to check against), which is
|
|
1010
|
+
# `issues: []` -- byte-identical to a validated-clean run for a
|
|
1011
|
+
# `--format json` consumer. `info`, not `warning`: nothing here
|
|
1012
|
+
# says a document is wrong. Deduplicated (`dict.fromkeys`): every
|
|
1013
|
+
# SoM this run reads through the same missing schema reports it
|
|
1014
|
+
# once, not once per SoM.
|
|
1015
|
+
issues.extend(
|
|
1016
|
+
Issue("presets.metadata-schema-unchecked", "info", w)
|
|
1017
|
+
for w in dict.fromkeys(schema_skipped)
|
|
1018
|
+
)
|
|
1019
|
+
if version_skipped:
|
|
1020
|
+
issues.append(_version_skipped_issue(sdk.path, version_skipped))
|
|
1021
|
+
else:
|
|
1022
|
+
# tan-cli#497 defect 7 (the sibling of `examples_cmd`'s): a
|
|
1023
|
+
# rejected `--sdk-root` is named, so the reader can see WHICH path
|
|
1024
|
+
# failed the loader-marker check instead of only that "the alp-sdk
|
|
1025
|
+
# root is unresolved".
|
|
1026
|
+
issues.append(
|
|
1027
|
+
Issue(
|
|
1028
|
+
SDK_UNRESOLVED_CODE,
|
|
1029
|
+
"warning",
|
|
1030
|
+
rejected_sdk_root_message(sdk_root, SDK_UNRESOLVED_CONSEQUENCE)
|
|
1031
|
+
if sdk_root
|
|
1032
|
+
else with_sdk_search(SDK_UNRESOLVED_MESSAGE, root),
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
1036
|
+
# No registry entry applies (`contract/issue-codes.json` covers
|
|
1037
|
+
# `bootstrap.*`/`debug-config.*` only), so this follows the port's
|
|
1038
|
+
# `<command>.internal-failure` spelling. Unreachable through any
|
|
1039
|
+
# enumerated path -- every IO and parse error above is already swallowed
|
|
1040
|
+
# the way the oracle swallows it -- and here so the one nobody thought of
|
|
1041
|
+
# arrives as an envelope instead of a traceback on stderr with an empty
|
|
1042
|
+
# stdout the extension renders as nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# This claim was FALSE for a while without anyone noticing (tan-cli#957):
|
|
1045
|
+
# `core_type_lookup`'s `cores[].type` read had no `isinstance` guard,
|
|
1046
|
+
# so a schema-invalid SoC JSON with a truthy non-string `type` (an
|
|
1047
|
+
# int, a list, a dict, `true`) reached `os_class.py`'s
|
|
1048
|
+
# `(core_type or "").lower()` and raised `AttributeError` right here,
|
|
1049
|
+
# through this very backstop -- an enumerated path this comment did
|
|
1050
|
+
# not count. `core_type_lookup` and `tan.core.os_class` both guard
|
|
1051
|
+
# that input now (see their own docstrings), closing THIS module's
|
|
1052
|
+
# path. That is what is actually known -- not that every path into
|
|
1053
|
+
# this `except` is closed for good: tan-cli#962 found the identical
|
|
1054
|
+
# unguarded read still live in `tan.planner.kconfig._emit_inference`
|
|
1055
|
+
# after this same sweep, on a different call site this backstop
|
|
1056
|
+
# cannot see. This `except` exists because that guarantee -- "every
|
|
1057
|
+
# path a caller might one day add is guarded" -- is not something
|
|
1058
|
+
# this file can check, not because it has been proven true.
|
|
1059
|
+
sdk, soms, board_libraries = None, [], []
|
|
1060
|
+
issues = [
|
|
1061
|
+
Issue(
|
|
1062
|
+
"presets.internal-failure",
|
|
1063
|
+
"error",
|
|
1064
|
+
f"presets failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1065
|
+
)
|
|
1066
|
+
]
|
|
1067
|
+
exit_code = ExitCode.INTERNAL_FAILURE
|
|
1068
|
+
|
|
1069
|
+
skus = [s.sku for s in soms]
|
|
1070
|
+
if json_mode:
|
|
1071
|
+
emit(
|
|
1072
|
+
Envelope(
|
|
1073
|
+
"presets",
|
|
1074
|
+
Project.resolved(root, board_path),
|
|
1075
|
+
{
|
|
1076
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
1077
|
+
# `null`, not omitted, when unresolved: the wizard reads this
|
|
1078
|
+
# key directly and the golden pins the null.
|
|
1079
|
+
"sdkRoot": sdk.path if sdk is not None else None,
|
|
1080
|
+
"skus": skus,
|
|
1081
|
+
"soms": [s.as_dict() for s in soms],
|
|
1082
|
+
"libraries": LIBRARIES,
|
|
1083
|
+
"boardLibraries": board_libraries,
|
|
1084
|
+
"inferenceBackends": INFERENCE_BACKENDS,
|
|
1085
|
+
"logLevels": LOG_LEVELS,
|
|
1086
|
+
"osChoices": OS_CHOICES,
|
|
1087
|
+
},
|
|
1088
|
+
issues,
|
|
1089
|
+
exit_code,
|
|
1090
|
+
# tan-cli#468: `sdk` is now a real `ActiveSdk` (never bare
|
|
1091
|
+
# `None`) whenever `resolve_sdk` ran at all -- `.path` is what
|
|
1092
|
+
# says whether it found a usable checkout, `sdk is not None`
|
|
1093
|
+
# alone no longer does.
|
|
1094
|
+
sdk=SdkInfo.from_resolution(sdk.path, sdk)
|
|
1095
|
+
if sdk is not None and sdk.path is not None
|
|
1096
|
+
else None,
|
|
1097
|
+
)
|
|
1098
|
+
)
|
|
1099
|
+
else:
|
|
1100
|
+
for line in render_presets_text(soms, board_libraries, verbose):
|
|
1101
|
+
print(line, file=sys.stderr)
|
|
1102
|
+
for issue in issues:
|
|
1103
|
+
# tan-cli#964 review (minor 11): the two schema codes' own
|
|
1104
|
+
# `message` (`<file>: <pointer>: <what jsonschema found>` /
|
|
1105
|
+
# `<file>: not validated -- no schema at <path>...`) does not
|
|
1106
|
+
# itself say "schema" for the INVALID half, so a text-mode
|
|
1107
|
+
# reader could not tell a schema violation apart from any other
|
|
1108
|
+
# warning at a glance -- unlike the REFUSE path's own
|
|
1109
|
+
# "does not validate against soc-spec-v1" wording. Tagged here,
|
|
1110
|
+
# at the print site, rather than folded into `message` itself:
|
|
1111
|
+
# `message` is the same string on the JSON envelope, which this
|
|
1112
|
+
# PR's own tests (and the review's own worked example) pin
|
|
1113
|
+
# verbatim.
|
|
1114
|
+
tag = "schema: " if issue.code.endswith(".metadata-schema-invalid") else ""
|
|
1115
|
+
print(f"presets: {tag}{issue.message}", file=sys.stderr)
|
|
1116
|
+
raise typer.Exit(int(exit_code))
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# tan-cli#261: adds the six oracle `GlobalArgs` flags this command was still
|
|
1120
|
+
# missing (`--all`/`--ci`/`--no-color`/`--non-interactive`/`--quiet`/
|
|
1121
|
+
# `--target`) on top of `--board-yaml`/`--verbose`, already declared and read
|
|
1122
|
+
# above; see `tan.core.global_flags`.
|
|
1123
|
+
presets = accept_global_flags(presets)
|