tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/planner/template.py
ADDED
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`--emit scaffold`: a NEW project's files, rendered from the SDK's catalog.
|
|
3
|
+
|
|
4
|
+
RELOCATED (was alp-sdk `scripts/alp_template.py`). Only the `--emit scaffold`
|
|
5
|
+
path came across -- `render_to_envelope()` and everything it reaches. What stayed
|
|
6
|
+
behind stayed because nothing in `tan` reaches it and a second copy would be a
|
|
7
|
+
second source of truth:
|
|
8
|
+
|
|
9
|
+
* `render()` / `RenderPlan` / `DestinationNotEmptyError` -- the disk-writing
|
|
10
|
+
front door (`alp_template.py render`, and `alp generate`'s `sku=` form). The
|
|
11
|
+
one line of its `plan()` this path actually uses is the FILE ORDER, inlined
|
|
12
|
+
below as `_ordered_files`.
|
|
13
|
+
* `validate()` / `ValidateResult` / `_count_passed` -- the twister self-test that
|
|
14
|
+
renders a template into a temp dir and builds it. An SDK CI gate, not a
|
|
15
|
+
customer path.
|
|
16
|
+
* `default_sku()` and the `argparse` CLI -- callers that stayed in alp-sdk.
|
|
17
|
+
|
|
18
|
+
**This is NOT `tan/core/scaffold.py`, and the two must not be merged.** That
|
|
19
|
+
module serves `tan init` from a VENDORED capture under `tan/templates/vendored/`
|
|
20
|
+
and consults no SDK checkout at all (invariant I-32), deliberately -- see its own
|
|
21
|
+
docstring on why re-deriving the SDK's `_scaffold_cmakelists()` rewrite in tan
|
|
22
|
+
would be a silent `ALP_SDK_ROOT`-guess bug. This module is the OTHER path: it
|
|
23
|
+
runs behind `tan.planner_cli`, which has already bound a real SDK root, so it
|
|
24
|
+
reads that checkout's LIVE `metadata/templates/catalog-v1.json` and the canonical
|
|
25
|
+
example the record names, exactly as `scripts/alp_project.py` did. Two paths, two
|
|
26
|
+
purposes: `tan init` must keep working with no SDK anywhere; `--emit scaffold`
|
|
27
|
+
exists to be byte-identical to the SDK's own front door, which no vendored
|
|
28
|
+
snapshot can promise for a template x SKU pair nobody captured.
|
|
29
|
+
|
|
30
|
+
Two relocation changes, no behavioural one:
|
|
31
|
+
|
|
32
|
+
* **`REPO` / `METADATA_ROOT` are the BOUND SDK checkout** (`paths.py`), not a
|
|
33
|
+
`__file__` walk. The walk landed on the SDK because the file lived there;
|
|
34
|
+
inside `tan` it would land on the `tan` package and every `metadata/**` and
|
|
35
|
+
`examples/**` path would be wrong.
|
|
36
|
+
* **`plan()` collapsed to `_ordered_files`.** `render_to_envelope` called
|
|
37
|
+
`plan()` for one value -- `tuple(sorted(record["files"]["user_owned"]))` -- and
|
|
38
|
+
re-validated the params the very next line already validates. The rest of
|
|
39
|
+
`plan()` served `render()`, which did not come across.
|
|
40
|
+
|
|
41
|
+
Docstrings below still name `render()`, `validate()`, `plan()`/`RenderPlan` and
|
|
42
|
+
`alp_template.py render`: those are alp-sdk's own paths, and the sentences are
|
|
43
|
+
true of the file this moved from. They are kept verbatim so the move stays
|
|
44
|
+
diffable against it.
|
|
45
|
+
|
|
46
|
+
SPLIT (tan-cli#1142): this module was 2206 lines against `MODULE_CAP = 800`.
|
|
47
|
+
`template_pins.py` (SoM/board-metadata reads and rename derivation) and
|
|
48
|
+
`template_rewrite.py` (board.yaml/CMakeLists.txt/README.md text rewrites) now
|
|
49
|
+
carry roughly half the body each; this module keeps the catalog/parameter
|
|
50
|
+
half plus `render_to_envelope`/`emit_scaffold` themselves, since those are
|
|
51
|
+
what `cli.py` imports and where the hand-port audit is densest. All three
|
|
52
|
+
files are the SAME `HAND_PORT_SOURCES` entry for `scripts/alp_template.py` --
|
|
53
|
+
see `template_pins.py`'s own module docstring for why that split is allowed
|
|
54
|
+
(`MIRRORED_PREFIX` bars a `PINNED_HASHES` module's shape from diverging from a
|
|
55
|
+
moving upstream file it is 3-way-merged against; it says nothing about a
|
|
56
|
+
`HAND_PORT_SOURCES` one, which upstream never merges into) and for the load-
|
|
57
|
+
bearing detail of how the two new modules import `TemplateError` back from
|
|
58
|
+
here without a real import cycle.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
import json
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
from typing import Any
|
|
66
|
+
|
|
67
|
+
from tan.core.document_guards import SHAPE_NOUN, DocumentGuards
|
|
68
|
+
|
|
69
|
+
from .paths import METADATA_ROOT, REPO
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"ParameterError",
|
|
73
|
+
"SkuNotSupportedError",
|
|
74
|
+
"TemplateError",
|
|
75
|
+
"TemplateNotFoundError",
|
|
76
|
+
"emit_scaffold",
|
|
77
|
+
"render_to_envelope",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
CATALOG = REPO / "metadata" / "templates" / "catalog-v1.json"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ordered_files(
|
|
84
|
+
record: dict[str, Any], *, doc: Any, field: str,
|
|
85
|
+
) -> tuple[str, ...]:
|
|
86
|
+
"""The envelope's file ORDER: the record's own `files.user_owned` list,
|
|
87
|
+
sorted. Verbatim from `plan()`, which did not otherwise come across (see the
|
|
88
|
+
module docstring). `files.generated` is never in it -- those artefacts are
|
|
89
|
+
emitted later, at build time, by `tan build`.
|
|
90
|
+
|
|
91
|
+
tan-cli#1077: the double subscript was bare. `KeyError: 'files'` on a
|
|
92
|
+
record missing it, `KeyError: 'user_owned'` one level in, and
|
|
93
|
+
`TypeError: '<' not supported between instances of 'int' and 'str'`
|
|
94
|
+
out of `sorted()` on a list whose entries are not the strings the
|
|
95
|
+
schema declares. All three keys are `required` in
|
|
96
|
+
`template-catalog-v1.schema.json` (`items: {"type": "string"}` for the
|
|
97
|
+
list), so naming them is never stricter than that schema."""
|
|
98
|
+
files = _require_key(record, "files", dict, doc=doc, field=field)
|
|
99
|
+
return tuple(sorted(
|
|
100
|
+
_require_field(name, str, doc=doc,
|
|
101
|
+
field=f"{field}.files.user_owned[{i}]")
|
|
102
|
+
for i, name in enumerate(_require_key(
|
|
103
|
+
files, "user_owned", list, doc=doc, field=f"{field}.files"))))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TemplateError(Exception):
|
|
107
|
+
"""Base for every error alp_template raises -- never a bare
|
|
108
|
+
KeyError/ValueError escapes to a caller."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TemplateNotFoundError(TemplateError):
|
|
112
|
+
"""No record with this id in the catalog."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ParameterError(TemplateError):
|
|
116
|
+
"""An unknown parameter name, a type mismatch, or a constraint
|
|
117
|
+
violation (enum / minimum / maximum)."""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SkuNotSupportedError(TemplateError):
|
|
121
|
+
"""`render_to_envelope`'s --sku is not in the record's declared
|
|
122
|
+
`supported.som_skus` -- a hard error, never a best-effort render."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PathEscapeError(TemplateError):
|
|
126
|
+
"""A catalog-declared `files.user_owned` path resolves outside its
|
|
127
|
+
example/destination root (alp-sdk#1126). The schema itself puts no
|
|
128
|
+
shape constraint on these entries (plain strings, no pattern), so
|
|
129
|
+
this is enforced at the point of use, not by schema validation
|
|
130
|
+
alone -- containment is checked on the RESOLVED path (symlinks
|
|
131
|
+
followed), not by pattern-matching for `..`."""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AmbiguousCoresError(TemplateError):
|
|
135
|
+
"""`find_template_by_cores()`'s `cores` topology matches more than
|
|
136
|
+
one catalog record -- naming the candidates rather than guessing
|
|
137
|
+
which one the caller meant (use `--template` to disambiguate)."""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_catalog(catalog_path: Path | None = None) -> dict[str, Any]:
|
|
141
|
+
"""Parse `metadata/templates/catalog-v1.json` -- the FOURTH document
|
|
142
|
+
this module reads, and its only JSON one (tan-cli#1077).
|
|
143
|
+
|
|
144
|
+
The three YAML documents already go through `_require_mapping_doc`;
|
|
145
|
+
this decode did not. A catalog that is legal JSON but not an object (a
|
|
146
|
+
bare list, a bare scalar) reached every caller's `doc.get("templates",
|
|
147
|
+
...)` as a raw `AttributeError`, and a catalog that is not legal JSON
|
|
148
|
+
at all escaped as a raw `json.JSONDecodeError` -- and `cli._emit_
|
|
149
|
+
scaffold` catches `TemplateError` and nothing else, so both reached a
|
|
150
|
+
CLI user as a traceback. That is the defect this whole family
|
|
151
|
+
(tan-cli#1025 -> #1034 -> #1037/#1048 -> #1052 -> this) exists to
|
|
152
|
+
close, one document at a time.
|
|
153
|
+
|
|
154
|
+
The `noun` is "a JSON object", not the register's shared default "a
|
|
155
|
+
YAML mapping": same helper, same message shape, one word that is true
|
|
156
|
+
of THIS document. The three YAML callers keep their default and their
|
|
157
|
+
byte-identical message.
|
|
158
|
+
|
|
159
|
+
The ABSENT-document half is curated here too (tan-cli#1077 review). The
|
|
160
|
+
first cut deferred it as "equally true of all four documents this
|
|
161
|
+
module reads"; measured, it was not -- `_load_som_doc` and
|
|
162
|
+
`_board_route_entries` carried an `is_file()` check and `_docs_ref` an
|
|
163
|
+
`except OSError`, so three of the module's five reads were ALREADY
|
|
164
|
+
handled and only this one and `render_to_envelope`'s example
|
|
165
|
+
`board.yaml` were bare. A false symmetry claim in a docstring is the
|
|
166
|
+
same defect class this round exists to close, so both are guarded and
|
|
167
|
+
the claim is gone. `except OSError`, not a pre-flight `is_file()`: a
|
|
168
|
+
present-but-unreadable path (a directory, a permissions error) is named
|
|
169
|
+
too, not only a missing one.
|
|
170
|
+
|
|
171
|
+
tan-cli#1133 finished that and corrected the COUNT: this module makes
|
|
172
|
+
SIX filesystem reads, not five -- the four documents (this catalog, the
|
|
173
|
+
SoM preset, the board metadata, the example `board.yaml`), `_docs_ref`'s
|
|
174
|
+
`sdk_version.yaml`, and `_rendered_bytes`' per-file `read_bytes()`,
|
|
175
|
+
which no round of this family had looked at and which was raw on three
|
|
176
|
+
separate errnos (PR #1160 review, MAJOR 1). All six are curated now and
|
|
177
|
+
no `is_file()` pre-flight is left. Re-derive by grepping
|
|
178
|
+
`read_text|read_bytes|safe_load|json.loads` here rather than inheriting:
|
|
179
|
+
the version of this claim one revision ago was off by one, in the
|
|
180
|
+
direction that hid a live defect.
|
|
181
|
+
|
|
182
|
+
tan-cli#1084: the read itself is `DocumentGuards.read_catalog_document`
|
|
183
|
+
now -- the SAME body, moved to `tan/core/document_guards.py` so
|
|
184
|
+
`tan/core/example_catalog.py`'s second implementation of this read
|
|
185
|
+
produces byte-identical messages instead of the raw
|
|
186
|
+
`FileNotFoundError`/`JSONDecodeError`/`AttributeError` it used to.
|
|
187
|
+
"""
|
|
188
|
+
return _GUARDS.read_catalog_document(catalog_path or CATALOG)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def find_template(
|
|
192
|
+
doc: dict[str, Any], template_id: str, *, path: Any = CATALOG,
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""The catalog record whose `id:` is @template_id.
|
|
195
|
+
|
|
196
|
+
tan-cli#1077: `rec["id"]` was bare at BOTH sites -- the match scan and
|
|
197
|
+
the not-found message -- so a `templates:` entry with no `id:` (or one
|
|
198
|
+
that is not a mapping at all) raised `KeyError: 'id'` / `TypeError`
|
|
199
|
+
instead of a curated error naming the catalog, the record and the
|
|
200
|
+
type. @path is a MESSAGE LABEL only, never read.
|
|
201
|
+
|
|
202
|
+
Its `CATALOG` default is the bound checkout's own catalog -- exactly
|
|
203
|
+
the file `load_catalog()` reads when no caller overrode IT, so the two
|
|
204
|
+
defaults agree and the label cannot lie for the one caller that omits
|
|
205
|
+
it (`tan/planner/cli.py:182`, whose `load_catalog()` is also
|
|
206
|
+
default-pathed). It CAN lie for a caller that passes a custom
|
|
207
|
+
`catalog_path` to `load_catalog` and then omits `path` here; there is
|
|
208
|
+
no such caller today (`render_to_envelope` threads the resolved path
|
|
209
|
+
through, and every test that asserts on the file name passes it
|
|
210
|
+
explicitly), and making it required would mean editing `cli.py` and
|
|
211
|
+
the pre-existing `test_find_template_by_cores.py` fixtures for a label
|
|
212
|
+
(tan-cli#1077 review nit -- recorded rather than silently accepted).
|
|
213
|
+
|
|
214
|
+
Every id is resolved up front, as the not-found path always did (and
|
|
215
|
+
as the match scan did for every record up to the match). A record
|
|
216
|
+
malformed AFTER the requested one therefore reds where it used to be
|
|
217
|
+
skipped: a new refusal, of a document the schema never accepted (`id`
|
|
218
|
+
is `required` on every record), asserted as its own test rather than
|
|
219
|
+
folded in.
|
|
220
|
+
"""
|
|
221
|
+
records = _catalog_templates(doc, path=path)
|
|
222
|
+
ids = [_require_key(rec, "id", str, doc=path, field=f"templates[{i}]")
|
|
223
|
+
for i, rec in enumerate(records)]
|
|
224
|
+
for rec, rec_id in zip(records, ids):
|
|
225
|
+
if rec_id == template_id:
|
|
226
|
+
return rec
|
|
227
|
+
known = ", ".join(sorted(ids))
|
|
228
|
+
raise TemplateNotFoundError(
|
|
229
|
+
f"no template {template_id!r} in catalog (known: {known})")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def find_template_by_cores(
|
|
233
|
+
doc: dict[str, Any], cores: dict[str, str], *, path: Any = CATALOG,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
"""Select the catalog record whose `cores:` topology (core id ->
|
|
236
|
+
os) is EXACTLY `cores` -- alp-sdk#1652's `--cores` scaffold input.
|
|
237
|
+
|
|
238
|
+
RELOCATED verbatim from alp-sdk's `scripts/alp_template.py` (the
|
|
239
|
+
HAND_PORT_HASHES entry for this file). This is a SELECTOR over the
|
|
240
|
+
catalog's existing templates, not a generic skeleton renderer: an
|
|
241
|
+
IDE wizard names the core/OS topology it wants (e.g. `{"m55_hp":
|
|
242
|
+
"zephyr", "m55_he": "zephyr"}`) instead of naming a template id,
|
|
243
|
+
and gets back whichever already-gated example matches -- never an
|
|
244
|
+
arbitrary, never-built combination. See the issue's recorded
|
|
245
|
+
decision: the scaffold's value to a customer is that the generated
|
|
246
|
+
app builds on their SoM, which only holds for a topology this SDK
|
|
247
|
+
already ships and twister-gates.
|
|
248
|
+
|
|
249
|
+
No exact match -> TemplateNotFoundError naming the topologies that
|
|
250
|
+
ARE on offer. More than one exact match -> AmbiguousCoresError
|
|
251
|
+
naming the candidate ids (use --template to disambiguate; this can
|
|
252
|
+
happen when two templates share a core/OS shape but differ in
|
|
253
|
+
what they actually do, e.g. an RPMsg demo vs a compute-offload
|
|
254
|
+
demo on the same SoM).
|
|
255
|
+
|
|
256
|
+
`tan init`'s customer-facing selector (`--topology`) does NOT call
|
|
257
|
+
this function -- it runs with no SDK checkout bound at module-import
|
|
258
|
+
time (invariant I-32) and cannot import `tan.planner` for that
|
|
259
|
+
reason (see `tan/core/example_catalog.py`'s own docstring on the
|
|
260
|
+
same constraint), so it carries a small standalone re-implementation
|
|
261
|
+
instead. This copy is the one `tan.planner_cli --emit scaffold
|
|
262
|
+
--cores` (the developer/parity entry that mirrors alp-sdk's own argv
|
|
263
|
+
1:1) actually calls, and the one `HAND_PORT_HASHES` audits against
|
|
264
|
+
alp-sdk's original.
|
|
265
|
+
|
|
266
|
+
@path is the same message-label-only keyword `find_template` takes,
|
|
267
|
+
with the same `CATALOG` default and the same caveat -- see its
|
|
268
|
+
docstring.
|
|
269
|
+
|
|
270
|
+
tan-cli#1077: `_topology`'s `{c["id"]: c["os"] ...}` and the ambiguous
|
|
271
|
+
branch's `rec["id"]` were bare subscripts on catalog-sourced mappings,
|
|
272
|
+
and all three `doc.get("templates", [])` iterations were unguarded.
|
|
273
|
+
`id`/`os` are `required` on every `cores[]` entry and `id` on every
|
|
274
|
+
record, so requiring them is not stricter than the schema. Each
|
|
275
|
+
topology is computed ONCE now (it used to be recomputed per record per
|
|
276
|
+
branch) and the ids of the MATCHES are resolved before the >1 test, so
|
|
277
|
+
the single-match record's `id:` is checked too -- that is what makes
|
|
278
|
+
`cli._emit_scaffold`'s own `record["id"]` (`planner/cli.py:186`) safe,
|
|
279
|
+
the one site of this defect that lives outside this module.
|
|
280
|
+
"""
|
|
281
|
+
def _topology(rec: Any, index: int) -> dict[str, str]:
|
|
282
|
+
field = f"templates[{index}]"
|
|
283
|
+
_require_field(rec, dict, doc=path, field=field)
|
|
284
|
+
entries = _require_field(rec.get("cores", []), list,
|
|
285
|
+
doc=path, field=f"{field}.cores")
|
|
286
|
+
return {
|
|
287
|
+
_require_key(core, "id", str, doc=path,
|
|
288
|
+
field=f"{field}.cores[{j}]"):
|
|
289
|
+
_require_key(core, "os", str, doc=path,
|
|
290
|
+
field=f"{field}.cores[{j}]")
|
|
291
|
+
for j, core in enumerate(entries)}
|
|
292
|
+
|
|
293
|
+
indexed = list(enumerate(_catalog_templates(doc, path=path)))
|
|
294
|
+
topologies = {index: _topology(rec, index) for index, rec in indexed}
|
|
295
|
+
matches = [(index, rec) for index, rec in indexed
|
|
296
|
+
if topologies[index] == cores]
|
|
297
|
+
if not matches:
|
|
298
|
+
known = sorted(
|
|
299
|
+
{tuple(sorted(topo.items())) for topo in topologies.values()})
|
|
300
|
+
raise TemplateNotFoundError(
|
|
301
|
+
f"no template with cores topology {cores!r} in catalog "
|
|
302
|
+
f"(known topologies: {known})")
|
|
303
|
+
ids = sorted(
|
|
304
|
+
_require_key(rec, "id", str, doc=path, field=f"templates[{index}]")
|
|
305
|
+
for index, rec in matches)
|
|
306
|
+
if len(matches) > 1:
|
|
307
|
+
raise AmbiguousCoresError(
|
|
308
|
+
f"cores topology {cores!r} matches multiple templates "
|
|
309
|
+
f"{ids} -- use --template to disambiguate")
|
|
310
|
+
return matches[0][1]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _coerce(spec: dict[str, Any], raw: Any) -> Any:
|
|
314
|
+
"""Coerce a CLI-style string override to the parameter's declared
|
|
315
|
+
type. Values already of the right type (e.g. an untouched default,
|
|
316
|
+
or a native value a Python caller passed directly) pass through.
|
|
317
|
+
|
|
318
|
+
`spec["type"]` / `spec["name"]` stay BARE subscripts on purpose
|
|
319
|
+
(tan-cli#1077): this function and `_check_constraints` are reached
|
|
320
|
+
only from `_resolve_params` (`:346` and `:347`, verified by the
|
|
321
|
+
tan-cli#1077 review), and only after `_record_parameters` has required
|
|
322
|
+
both keys on every spec. Guarding them a second time here would be a
|
|
323
|
+
second register for the same fact.
|
|
324
|
+
|
|
325
|
+
That reasoning covers the SHAPE of `_check_constraints`'s
|
|
326
|
+
`spec.get("constraints")` and its `constraints["enum"]` /
|
|
327
|
+
`["minimum"]` / `["maximum"]` reads -- and only their shape, and only
|
|
328
|
+
since `_require_constraints` joined `_record_parameters`. Before that,
|
|
329
|
+
this docstring's claim was written while a sixth unguarded subscript
|
|
330
|
+
sat one function over (tan-cli#1077 review, MAJOR 1).
|
|
331
|
+
|
|
332
|
+
It does NOT cover what `_check_constraints` then DOES with them: a
|
|
333
|
+
schema-VALID `type: string` (or `enum`) parameter carrying
|
|
334
|
+
`constraints.minimum`/`maximum` is legal per `$defs/parameter`, and
|
|
335
|
+
`_require_constraints` guarantees the `int` that would make a bare
|
|
336
|
+
`"a" < 5` raise a `TypeError`. tan-cli#1087 closed that: `_check_
|
|
337
|
+
constraints` now refuses `minimum`/`maximum` on any non-`integer`
|
|
338
|
+
`type` with a curated `ParameterError` before it ever compares, so the
|
|
339
|
+
bare `TypeError` this paragraph used to describe as latent no longer
|
|
340
|
+
happens -- do not read it as still open."""
|
|
341
|
+
if not isinstance(raw, str):
|
|
342
|
+
return raw
|
|
343
|
+
ptype = spec["type"]
|
|
344
|
+
if ptype == "integer":
|
|
345
|
+
try:
|
|
346
|
+
return int(raw)
|
|
347
|
+
except ValueError as exc:
|
|
348
|
+
raise ParameterError(
|
|
349
|
+
f"{spec['name']}: {raw!r} is not an integer") from exc
|
|
350
|
+
if ptype == "boolean":
|
|
351
|
+
if raw.lower() in ("1", "true", "yes"):
|
|
352
|
+
return True
|
|
353
|
+
if raw.lower() in ("0", "false", "no"):
|
|
354
|
+
return False
|
|
355
|
+
raise ParameterError(f"{spec['name']}: {raw!r} is not a boolean")
|
|
356
|
+
return raw # string / enum stay strings
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _check_constraints(template_id: str, spec: dict[str, Any], value: Any) -> None:
|
|
360
|
+
"""Enforce `spec["constraints"]` against the coerced @value.
|
|
361
|
+
|
|
362
|
+
tan-cli#1087: `minimum`/`maximum` are only well-typed against `type:
|
|
363
|
+
integer` -- `$defs/parameter.type` is exactly `string`/`integer`/
|
|
364
|
+
`boolean`/`enum`, none of the other three compare against an `int`
|
|
365
|
+
bound, and the schema does not cross-reference `type` against
|
|
366
|
+
`constraints` at all. `boolean` is refused too, on purpose, even though
|
|
367
|
+
`bool < int` never raises -- a bound that cannot crash still is not one
|
|
368
|
+
that means anything on a boolean knob. A schema addition of a fifth
|
|
369
|
+
numeric `type` would need a matching line here.
|
|
370
|
+
|
|
371
|
+
DIVERGES FROM alp-sdk on this input class, deliberately: `scripts/
|
|
372
|
+
alp_template.py`'s own `_check_constraints` has no such guard and still
|
|
373
|
+
raises the bare `TypeError` this closes. alp-sdk#1916 tracks closing the
|
|
374
|
+
gap; until then tan refuses where alp-sdk still crashes.
|
|
375
|
+
|
|
376
|
+
`constraints.enum` is the SAME inapplicability class one line below --
|
|
377
|
+
an `integer` parameter can never satisfy it, since the schema forces
|
|
378
|
+
`enum` items to strings -- and is DELIBERATELY LEFT OPEN here: it fails
|
|
379
|
+
loudly with a value-blaming message rather than crashing, so it stayed
|
|
380
|
+
out of #1087's scope. alp-sdk#1916 tracks it too.
|
|
381
|
+
"""
|
|
382
|
+
constraints = spec.get("constraints") or {}
|
|
383
|
+
if "enum" in constraints and value not in constraints["enum"]:
|
|
384
|
+
raise ParameterError(
|
|
385
|
+
f"{template_id}: {spec['name']}={value!r} not in "
|
|
386
|
+
f"{constraints['enum']}")
|
|
387
|
+
for bound in ("minimum", "maximum"):
|
|
388
|
+
if bound in constraints and spec["type"] != "integer":
|
|
389
|
+
raise ParameterError(
|
|
390
|
+
f"{template_id}: {spec['name']}={value!r} is type "
|
|
391
|
+
f"{spec['type']!r}; constraints.{bound} "
|
|
392
|
+
f"({constraints[bound]!r}) only applies "
|
|
393
|
+
f"to type 'integer'")
|
|
394
|
+
if "minimum" in constraints and value < constraints["minimum"]:
|
|
395
|
+
raise ParameterError(
|
|
396
|
+
f"{template_id}: {spec['name']}={value!r} < minimum "
|
|
397
|
+
f"{constraints['minimum']}")
|
|
398
|
+
if "maximum" in constraints and value > constraints["maximum"]:
|
|
399
|
+
raise ParameterError(
|
|
400
|
+
f"{template_id}: {spec['name']}={value!r} > maximum "
|
|
401
|
+
f"{constraints['maximum']}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _resolve_params(
|
|
405
|
+
record: dict[str, Any], params: dict[str, Any] | None,
|
|
406
|
+
*, doc: Any, field: str,
|
|
407
|
+
) -> dict[str, Any]:
|
|
408
|
+
"""Resolve every declared parameter to its effective value (override
|
|
409
|
+
or default), rejecting any name the record doesn't declare -- this
|
|
410
|
+
can never invent a knob the catalog doesn't have.
|
|
411
|
+
|
|
412
|
+
tan-cli#1077: `p["name"]`, `spec["default"]` and `record["id"]` (twice)
|
|
413
|
+
were bare subscripts on a catalog-sourced mapping. The spec shapes are
|
|
414
|
+
resolved once by `_record_parameters`, so `_coerce`/`_check_constraints`
|
|
415
|
+
below keep reading `spec["type"]`/`spec["name"]`/`spec["default"]` bare
|
|
416
|
+
-- see `_coerce`'s docstring."""
|
|
417
|
+
specs = _record_parameters(record, doc=doc, field=field)
|
|
418
|
+
declared = {spec["name"]: spec for spec in specs}
|
|
419
|
+
template_id = _require_key(record, "id", str, doc=doc, field=field)
|
|
420
|
+
params = dict(params or {})
|
|
421
|
+
unknown = sorted(set(params) - set(declared))
|
|
422
|
+
if unknown:
|
|
423
|
+
raise ParameterError(
|
|
424
|
+
f"{template_id}: unknown parameter(s) {unknown}; declared: "
|
|
425
|
+
f"{sorted(declared) or '(none)'}")
|
|
426
|
+
|
|
427
|
+
resolved: dict[str, Any] = {}
|
|
428
|
+
for name, spec in declared.items():
|
|
429
|
+
value = _coerce(spec, params.get(name, spec["default"]))
|
|
430
|
+
_check_constraints(template_id, spec, value)
|
|
431
|
+
resolved[name] = value
|
|
432
|
+
return resolved
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _substitutions_for(
|
|
436
|
+
record: dict[str, Any], resolved: dict[str, Any], *, doc: Any, field: str,
|
|
437
|
+
) -> dict[str, list[tuple[str, str]]]:
|
|
438
|
+
"""dest-relative file -> [(literal_to_replace, new_value_str), ...].
|
|
439
|
+
|
|
440
|
+
Reads an opt-in `substitute: {"file": ..., "literal": <optional,
|
|
441
|
+
defaults to str(default)>}` key on a parameter record. No parameter
|
|
442
|
+
the shipped catalog declares today carries this key (the schema
|
|
443
|
+
forbids it -- additionalProperties: false), so this is a no-op for
|
|
444
|
+
every real template; see the module docstring and
|
|
445
|
+
tests/scripts/test_alp_template.py's synthetic-fixture case.
|
|
446
|
+
|
|
447
|
+
tan-cli#1077: `sub["file"]` was a bare subscript and `sub` itself was
|
|
448
|
+
never shape-checked, so a `substitute:` block with no `file:` raised
|
|
449
|
+
`KeyError: 'file'` and a non-mapping one raised `AttributeError` from
|
|
450
|
+
`.get("literal", ...)`. Guarded in the ORIGINAL order -- a spec whose
|
|
451
|
+
override equals its default still returns early, untouched, exactly as
|
|
452
|
+
before, so this adds no refusal to a document that used to render.
|
|
453
|
+
"""
|
|
454
|
+
per_file: dict[str, list[tuple[str, str]]] = {}
|
|
455
|
+
for index, spec in enumerate(
|
|
456
|
+
_record_parameters(record, doc=doc, field=field)):
|
|
457
|
+
sub = spec.get("substitute")
|
|
458
|
+
if not sub:
|
|
459
|
+
continue
|
|
460
|
+
sub_field = f"{field}.parameters[{index}].substitute"
|
|
461
|
+
value = resolved[spec["name"]]
|
|
462
|
+
if value == spec["default"]:
|
|
463
|
+
continue # override equals default: nothing to change
|
|
464
|
+
literal = _require_field(sub, dict, doc=doc, field=sub_field).get(
|
|
465
|
+
"literal", str(spec["default"]))
|
|
466
|
+
target = _require_key(sub, "file", str, doc=doc, field=sub_field)
|
|
467
|
+
per_file.setdefault(target, []).append((literal, str(value)))
|
|
468
|
+
return per_file
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _safe_join(root: Path, rel: str, *, what: str) -> Path:
|
|
472
|
+
"""Join `rel` onto `root` and require the RESOLVED result stay
|
|
473
|
+
beneath the RESOLVED root (alp-sdk#1126).
|
|
474
|
+
|
|
475
|
+
Resolve-then-contain, not pattern-match-for-`..`: `(root / rel)`
|
|
476
|
+
already neutralises nothing on its own -- pathlib lets an absolute
|
|
477
|
+
`rel` replace `root` outright (`Path("a") / "/etc/passwd" ==
|
|
478
|
+
Path("/etc/passwd")`), and a lexical `..` scan misses a `rel` that
|
|
479
|
+
walks back out through a symlink placed inside `root`. Resolving
|
|
480
|
+
both sides and checking containment catches all three forms
|
|
481
|
+
(traversal, absolute paths, symlink escape) with one check.
|
|
482
|
+
|
|
483
|
+
tan-cli#1133 (PR #1160 review, found while driving `_rendered_bytes`'s
|
|
484
|
+
symlink-loop shape): `resolve()` itself can fail, and it fails
|
|
485
|
+
DIFFERENTLY per interpreter -- the same family as the tan-cli#1127
|
|
486
|
+
`is_file()` trap, one method over. Against a self-referential symlink,
|
|
487
|
+
measured non-root:
|
|
488
|
+
|
|
489
|
+
3.12.3 RuntimeError("Symlink loop from '<path>'") <- raised
|
|
490
|
+
3.13.15 returns the path unchanged
|
|
491
|
+
3.14.7 returns the path unchanged
|
|
492
|
+
|
|
493
|
+
So on 3.12.3 a looped template source file escaped `emit_scaffold` as a
|
|
494
|
+
raw `RuntimeError` -- not even an `OSError`, so no read guard downstream
|
|
495
|
+
could ever have caught it -- while on 3.13/3.14 the same tree fell
|
|
496
|
+
through to the read and was curated there. Both arms are curated now.
|
|
497
|
+
The two messages still DIFFER by interpreter, because the failure
|
|
498
|
+
genuinely happens at different points, and inventing one message for
|
|
499
|
+
both would mean lying about where it broke on one of them; what is
|
|
500
|
+
identical across all three is the CLASS the caller contracts for."""
|
|
501
|
+
try:
|
|
502
|
+
root = root.resolve()
|
|
503
|
+
candidate = (root / rel).resolve()
|
|
504
|
+
except RecursionError:
|
|
505
|
+
# `RecursionError` SUBCLASSES `RuntimeError`, so the clause below
|
|
506
|
+
# would otherwise report a runaway recursion inside `resolve()` as
|
|
507
|
+
# `cannot resolve <what> ...` -- a curated message about the wrong
|
|
508
|
+
# thing, which is its own defect class (PR #1160 review round 2).
|
|
509
|
+
# The measured shape is the plain `RuntimeError("Symlink loop from
|
|
510
|
+
# ...")`; nothing here is a claim about recursion depth.
|
|
511
|
+
raise
|
|
512
|
+
except (OSError, RuntimeError) as exc:
|
|
513
|
+
raise TemplateError(
|
|
514
|
+
f"cannot resolve {what} {rel!r} under {root}: "
|
|
515
|
+
f"{getattr(exc, 'strerror', None) or exc}") from exc
|
|
516
|
+
if not candidate.is_relative_to(root):
|
|
517
|
+
raise PathEscapeError(f"{what} {rel!r} escapes root {root}")
|
|
518
|
+
return candidate
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _rendered_bytes(
|
|
522
|
+
template_id: str,
|
|
523
|
+
record: dict[str, Any],
|
|
524
|
+
files: tuple[str, ...],
|
|
525
|
+
resolved: dict[str, Any],
|
|
526
|
+
base_dir: Path,
|
|
527
|
+
*,
|
|
528
|
+
doc: Any,
|
|
529
|
+
field: str,
|
|
530
|
+
) -> list[tuple[str, bytes]]:
|
|
531
|
+
"""Read + apply every declared-parameter substitution for `files`
|
|
532
|
+
(a RenderPlan.files list), returning [(relpath, bytes), ...] in the
|
|
533
|
+
same order. Shared by render()'s disk-write loop and
|
|
534
|
+
render_to_envelope()'s in-memory capture -- the same bytes a
|
|
535
|
+
customer gets from `alp_template.py render` are what `--emit
|
|
536
|
+
scaffold` hands back as JSON `contents` (see the module docstring).
|
|
537
|
+
|
|
538
|
+
tan-cli#1077: `record["example"]` was bare here and again twice in
|
|
539
|
+
`render_to_envelope`. `example` is `required` in the schema (and
|
|
540
|
+
pattern-constrained to `^examples/<dir>/<dir>$`), so a record without
|
|
541
|
+
it raised `KeyError: 'example'`; a non-string one raised a raw
|
|
542
|
+
`TypeError` from `_safe_join`'s `root / rel`.
|
|
543
|
+
|
|
544
|
+
tan-cli#1133 review (PR #1160 MAJOR 1): the read below was the FOURTH
|
|
545
|
+
absent-`try` site and the busiest of the six (4-7 files per scaffold);
|
|
546
|
+
the substitution branch's `.decode` was a FIFTH, LATENT one (no shipped
|
|
547
|
+
catalog can declare a `substitute:`). Per-cell measurement, and the
|
|
548
|
+
schema evidence for that, in `tests/planner/test_emit_scaffold_
|
|
549
|
+
unreadable_metadata.py`."""
|
|
550
|
+
example = _safe_join(
|
|
551
|
+
base_dir, _require_key(record, "example", str, doc=doc, field=field),
|
|
552
|
+
what="template example directory")
|
|
553
|
+
file_subs = _substitutions_for(record, resolved, doc=doc, field=field)
|
|
554
|
+
out: list[tuple[str, bytes]] = []
|
|
555
|
+
for rel in files:
|
|
556
|
+
path = _safe_join(example, rel, what="template source file")
|
|
557
|
+
data = _require_readable_bytes(path, what="template source file")
|
|
558
|
+
subs = file_subs.get(rel)
|
|
559
|
+
if subs:
|
|
560
|
+
try:
|
|
561
|
+
text = data.decode("utf-8")
|
|
562
|
+
except UnicodeDecodeError as exc:
|
|
563
|
+
raise TemplateError(
|
|
564
|
+
f"{template_id}: {rel} is not valid UTF-8 text, cannot "
|
|
565
|
+
f"have substitutions applied ({exc})") from exc
|
|
566
|
+
for literal, value in subs:
|
|
567
|
+
if literal not in text:
|
|
568
|
+
raise ParameterError(
|
|
569
|
+
f"{template_id}: substitution literal {literal!r} "
|
|
570
|
+
f"not found in {rel}")
|
|
571
|
+
text = text.replace(literal, value)
|
|
572
|
+
data = text.encode("utf-8")
|
|
573
|
+
out.append((rel, data))
|
|
574
|
+
return out
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------------
|
|
578
|
+
# --emit scaffold (issue #864): in-memory, SKU-parameterised capture
|
|
579
|
+
# ---------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
#: The malformed-document register, bound to THIS module's error class.
|
|
582
|
+
#:
|
|
583
|
+
#: tan-cli#1084 MOVED the register itself to `tan/core/document_guards.py`
|
|
584
|
+
#: (definitions, message shapes, schema-strictness claim and the recorded
|
|
585
|
+
#: falsy-value asymmetry all live in that module's docstring). It did not
|
|
586
|
+
#: change a single one of them, and it did not change a call site: the names
|
|
587
|
+
#: below are the SAME objects this module has used since tan-cli#1073/#1077,
|
|
588
|
+
#: so every `_require_field(...)` / `_require_key(...)` / `_catalog_templates
|
|
589
|
+
#: (...)` call in this file is byte-identical to what those PRs landed.
|
|
590
|
+
#:
|
|
591
|
+
#: The move exists because `tan/core/example_catalog.py` is a DELIBERATE
|
|
592
|
+
#: second implementation of the catalog read (`tan.planner` binds its SDK root
|
|
593
|
+
#: at module-import time, so `tan init`'s SDK-free path cannot import this
|
|
594
|
+
#: module at all -- see that file's own docstring) and, after tan-cli#1077,
|
|
595
|
+
#: this module refused a malformed catalog with a curated error while that one
|
|
596
|
+
#: still crashed raw. One register, two callers, one exception class each:
|
|
597
|
+
#: `cli._emit_scaffold` catches `TemplateError` and nothing else, so the class
|
|
598
|
+
#: is a constructor argument rather than a fixed type.
|
|
599
|
+
#:
|
|
600
|
+
#: `_require_constraints` below did NOT move -- it guards `$defs/parameter`'s
|
|
601
|
+
#: bounds, which only this module's parameter resolution reads, so it has no
|
|
602
|
+
#: second caller to prove it shared. `require_readable_text` (tan-cli#1085)
|
|
603
|
+
#: is the file-read half `render_to_envelope`'s example `board.yaml` read
|
|
604
|
+
#: used to run by hand -- now the same definition `read_catalog_document`
|
|
605
|
+
#: itself calls.
|
|
606
|
+
_GUARDS = DocumentGuards(TemplateError)
|
|
607
|
+
|
|
608
|
+
_SHAPE_NOUN = SHAPE_NOUN
|
|
609
|
+
_require_mapping_doc = _GUARDS.require_mapping_doc
|
|
610
|
+
_require_field = _GUARDS.require_field
|
|
611
|
+
_require_key = _GUARDS.require_key
|
|
612
|
+
_require_readable_text = _GUARDS.require_readable_text
|
|
613
|
+
_catalog_templates = _GUARDS.catalog_templates
|
|
614
|
+
#: tan-cli#1133's four. The first cut of that fix defined the two YAML ones
|
|
615
|
+
#: as module-level functions HERE, on the register's stated membership bar
|
|
616
|
+
#: ("a shape RUN BY more than one consumer module"): `example_catalog.py`,
|
|
617
|
+
#: the register's second consumer, reads JSON only. PR #1160's review asked
|
|
618
|
+
#: the question the other way round and it is the better question --
|
|
619
|
+
#: `read_catalog_document` is already read + parse + mapping-check for JSON,
|
|
620
|
+
#: so keeping the identical composite for YAML in a CONSUMER split one
|
|
621
|
+
#: question across two homes for no reason a reader could reconstruct. The
|
|
622
|
+
#: import-closure half of the original argument was also simply wrong: seven
|
|
623
|
+
#: `tan/core/**` modules defer `import yaml` into a function body for exactly
|
|
624
|
+
#: this purpose. Moved, and `template.py` is 71 lines lighter for it -- at
|
|
625
|
+
#: the time argued as mattering MORE here than in an ordinary module,
|
|
626
|
+
#: because `_module_size_budget_core.MIRRORED_PREFIX`'s comment was read as
|
|
627
|
+
#: barring a split of this file. WRONG: that comment describes
|
|
628
|
+
#: `PINNED_HASHES` modules (3-way-merged against a moving upstream file,
|
|
629
|
+
#: where a shape change would conflict on every future upstream hunk), and
|
|
630
|
+
#: this module is `HAND_PORT_SOURCES` instead -- see `template_pins.py`'s
|
|
631
|
+
#: module docstring for the correction and the actual split (tan-cli#1142).
|
|
632
|
+
_require_readable_bytes = _GUARDS.require_readable_bytes
|
|
633
|
+
_parse_yaml_mapping = _GUARDS.require_yaml_mapping_doc
|
|
634
|
+
_read_yaml_mapping = _GUARDS.read_yaml_mapping
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _record_parameters(record: Any, *, doc: Any, field: str) -> list[Any]:
|
|
638
|
+
"""The record's `parameters:` list, every spec shape-checked once.
|
|
639
|
+
|
|
640
|
+
tan-cli#1077: `{p["name"]: p for p in record.get("parameters", [])}`
|
|
641
|
+
(`:229`) and `spec["default"]` (`:239`, `:263`, `:265`) were bare, so
|
|
642
|
+
a spec missing either raised `KeyError`, and a non-list `parameters:`
|
|
643
|
+
raised a raw `TypeError`. `name`/`type`/`description`/`default` are
|
|
644
|
+
all `required` in the schema's `$defs/parameter`; `default` carries no
|
|
645
|
+
declared type there, so only its PRESENCE is required here, and
|
|
646
|
+
`description` is not read by this module at all and is not required
|
|
647
|
+
here either -- guarding a key nobody subscripts would be exactly the
|
|
648
|
+
stricter-than-the-schema this issue rules out.
|
|
649
|
+
|
|
650
|
+
`constraints:` is checked here too, via `_require_constraints` --
|
|
651
|
+
tan-cli#1077 review MAJOR 1, whose six measured failures (five raw
|
|
652
|
+
`TypeError`s and one SILENT dropped bound) that helper's own docstring
|
|
653
|
+
records.
|
|
654
|
+
|
|
655
|
+
Called by BOTH `_resolve_params` and `_substitutions_for`, which each
|
|
656
|
+
walked the same list bare; the check is idempotent, so the second
|
|
657
|
+
call re-proves what the first did rather than trusting a caller.
|
|
658
|
+
"""
|
|
659
|
+
_require_field(record, dict, doc=doc, field=field)
|
|
660
|
+
specs = _require_field(record.get("parameters", []), list,
|
|
661
|
+
doc=doc, field=f"{field}.parameters")
|
|
662
|
+
for index, spec in enumerate(specs):
|
|
663
|
+
spec_field = f"{field}.parameters[{index}]"
|
|
664
|
+
_require_key(spec, "name", str, doc=doc, field=spec_field)
|
|
665
|
+
_require_key(spec, "type", str, doc=doc, field=spec_field)
|
|
666
|
+
_require_key(spec, "default", doc=doc, field=spec_field)
|
|
667
|
+
_require_constraints(spec, doc=doc, field=spec_field)
|
|
668
|
+
return specs
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _require_constraints(spec: dict[str, Any], *, doc: Any, field: str) -> None:
|
|
672
|
+
"""`constraints:` and the three bounds `_check_constraints` reads.
|
|
673
|
+
|
|
674
|
+
OPTIONAL in the schema, so an absent block is legal and this returns
|
|
675
|
+
without a word; only a PRESENT one is shape-checked, against
|
|
676
|
+
`$defs/parameter`'s own `constraints` object (`enum` an array,
|
|
677
|
+
`minimum`/`maximum` integers).
|
|
678
|
+
|
|
679
|
+
tan-cli#1077 review MAJOR 1: `_check_constraints` read
|
|
680
|
+
`spec.get("constraints") or {}` and then membership-tested and
|
|
681
|
+
subscripted it, INSIDE a function the first sweep table declared
|
|
682
|
+
cleared. Re-driven verbatim on the tree this PR opened with:
|
|
683
|
+
|
|
684
|
+
constraints: 3 TypeError: argument of type 'int' is
|
|
685
|
+
not iterable :307
|
|
686
|
+
constraints: ['enum'] TypeError: list indices must be
|
|
687
|
+
integers or slices, not str :307
|
|
688
|
+
constraints: ['minimum'] same :311
|
|
689
|
+
constraints: ['maximum'] same :315
|
|
690
|
+
constraints: {enum: 3} TypeError: argument of type 'int' is
|
|
691
|
+
not iterable :307
|
|
692
|
+
constraints: {minimum: 'a'} TypeError: '<' not supported between
|
|
693
|
+
instances of 'int' and 'str' :311
|
|
694
|
+
constraints: 'abc' RENDERS -- every bound DROPPED
|
|
695
|
+
|
|
696
|
+
The last row is the serious one, and the same shape as the
|
|
697
|
+
`pins: 'E1M_GPIO_IO4'` character-iteration bug tan-cli#1052 found on
|
|
698
|
+
this file: no exception, wrong behaviour. `"enum" in "abc"` is a
|
|
699
|
+
SUBSTRING test, so every bound evaluated False and an out-of-range
|
|
700
|
+
override was ACCEPTED. The behaviour even depended on the spelling of
|
|
701
|
+
the junk -- `constraints: 'an enum'` DOES contain the substring and
|
|
702
|
+
took the `TypeError` branch instead.
|
|
703
|
+
"""
|
|
704
|
+
raw = spec.get("constraints")
|
|
705
|
+
if raw is None:
|
|
706
|
+
return
|
|
707
|
+
cons_field = f"{field}.constraints"
|
|
708
|
+
_require_field(raw, dict, doc=doc, field=cons_field)
|
|
709
|
+
if "enum" in raw:
|
|
710
|
+
_require_field(raw["enum"], list, doc=doc, field=f"{cons_field}.enum")
|
|
711
|
+
for bound in ("minimum", "maximum"):
|
|
712
|
+
if bound in raw:
|
|
713
|
+
_require_field(raw[bound], int, doc=doc,
|
|
714
|
+
field=f"{cons_field}.{bound}")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
#: `template_pins.py` / `template_rewrite.py` (tan-cli#1142 split -- see the
|
|
718
|
+
#: module docstring). Deliberately imported here, AFTER the exception classes
|
|
719
|
+
#: and the `_GUARDS` block above rather than in this module's top import
|
|
720
|
+
#: block: each of those two modules imports `TemplateError` (and
|
|
721
|
+
#: `template_pins.py` also imports `_require_field`/`_read_yaml_mapping`) back
|
|
722
|
+
#: FROM `.template`, so this is a real import cycle at the module-object
|
|
723
|
+
#: level. It resolves because Python does not re-execute an already-importing
|
|
724
|
+
#: module -- it looks up the requested names on the (partially built) module
|
|
725
|
+
#: object -- and by the time THIS line runs, `TemplateError` and the `_GUARDS`
|
|
726
|
+
#: bindings are already set as attributes on `tan.planner.template`. Moving
|
|
727
|
+
#: this import above the `_GUARDS` block (or moving that block below this
|
|
728
|
+
#: import) breaks the cycle the other way and fails at import time. The names
|
|
729
|
+
#: below are also `template.py`'s re-export surface for them: `import
|
|
730
|
+
#: tan.planner.template as m; m._load_som_doc` (and ~a dozen siblings) is how
|
|
731
|
+
#: `python/tests/` reaches these across the split, so removing one of these
|
|
732
|
+
#: bindings without checking its test callers first reproduces exactly the
|
|
733
|
+
#: silent-coverage-gap shape tan-cli#279/#778 exist to catch, one module over.
|
|
734
|
+
from .template_pins import (
|
|
735
|
+
_alias_for_pin,
|
|
736
|
+
_board_alias_to_entry,
|
|
737
|
+
_board_route_entries,
|
|
738
|
+
_core_board,
|
|
739
|
+
_default_preset_for_sku,
|
|
740
|
+
_derive_core_renames,
|
|
741
|
+
_derive_pin_doc_renames,
|
|
742
|
+
_derive_pin_macro_renames,
|
|
743
|
+
_derive_pin_renames,
|
|
744
|
+
_load_som_doc,
|
|
745
|
+
_pin_pad_and_macro,
|
|
746
|
+
_resolve_pin_target,
|
|
747
|
+
_topology_for_sku,
|
|
748
|
+
)
|
|
749
|
+
from .template_rewrite import (
|
|
750
|
+
_docs_ref,
|
|
751
|
+
_scaffold_bare_repo_paths,
|
|
752
|
+
_scaffold_cmakelists,
|
|
753
|
+
_scaffold_readme,
|
|
754
|
+
_strip_stale_core_prose,
|
|
755
|
+
_substitute_board_yaml_core,
|
|
756
|
+
_substitute_board_yaml_pin_docs,
|
|
757
|
+
_substitute_board_yaml_pin_macros,
|
|
758
|
+
_substitute_board_yaml_pins,
|
|
759
|
+
_substitute_board_yaml_sku,
|
|
760
|
+
_substitute_readme_pins,
|
|
761
|
+
_tag_resolves,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def render_to_envelope(
|
|
766
|
+
template_id: str,
|
|
767
|
+
sku: str,
|
|
768
|
+
params: dict[str, Any] | None = None,
|
|
769
|
+
*,
|
|
770
|
+
catalog_path: Path | None = None,
|
|
771
|
+
base_dir: Path | None = None,
|
|
772
|
+
metadata_root: Path | None = None,
|
|
773
|
+
) -> list[tuple[str, str]]:
|
|
774
|
+
"""Render `template_id` for `sku` entirely in memory: no dest_dir, no
|
|
775
|
+
disk write. Returns `[(path, contents), ...]` in the same sorted
|
|
776
|
+
order `plan()` computes -- the `{path, contents}[]` shape
|
|
777
|
+
`scripts/alp_project.py --emit scaffold` JSON-encodes (issue #864),
|
|
778
|
+
matching the shape `--emit build-plan`'s `configArtefacts` /
|
|
779
|
+
`sharedArtefacts` already use. `testcase.yaml` is never in this
|
|
780
|
+
envelope (dropped from the catalog's `files.user_owned`: SDK CI
|
|
781
|
+
wiring, not a user's project file).
|
|
782
|
+
|
|
783
|
+
`sku` MUST be one of the record's declared `supported.som_skus` --
|
|
784
|
+
SkuNotSupportedError (naming the supported set) otherwise, never a
|
|
785
|
+
silent best-effort render. The rendered `board.yaml`'s `som.sku:`
|
|
786
|
+
and top-level `preset:` are substituted for `sku`'s own default
|
|
787
|
+
board (metadata/e1m_modules/<sku>.yaml `default_board:`). The app
|
|
788
|
+
CORE is re-derived too (`_derive_core_renames`): `board.yaml`'s
|
|
789
|
+
`cores:` key(s) are rewritten
|
|
790
|
+
from the canonical example's own SoM core (e.g. `m55_hp`) to
|
|
791
|
+
`sku`'s own Zephyr-buildable core (e.g. `m33_sm` for E1M-V2N101)
|
|
792
|
+
whenever the canonical core isn't already valid for `sku` -- this
|
|
793
|
+
is the fix for issue #864's follow-up: the shallow `som.sku`-only
|
|
794
|
+
swap emitted a board.yaml `--emit zephyr-conf --core m55_hp` can't
|
|
795
|
+
build against for any cross-SoM-family sku. `prj.conf` is a byte-
|
|
796
|
+
identical passthrough when `sku` already matches the example's own
|
|
797
|
+
default (or shares its core ids); `board.yaml`/`src/*.c`/`src/*.h`
|
|
798
|
+
keep their sku/core/pin substitutions scoped to that case but ALWAYS
|
|
799
|
+
get `_scaffold_bare_repo_paths` (alp-sdk#1855: a bare, non-markdown-
|
|
800
|
+
link `docs/*.md`/`examples/<...>` mention in a comment is wrong for
|
|
801
|
+
a copied-out scaffold regardless of which sku was requested, same
|
|
802
|
+
reasoning as the next sentence). CMakeLists.txt and README.md are
|
|
803
|
+
ALSO scaffold-adapted regardless of `sku` (`_scaffold_cmakelists` /
|
|
804
|
+
`_scaffold_readme`) -- their in-tree `ALP_SDK_ROOT` guess and SDK-
|
|
805
|
+
tree-relative links/paths are wrong for a copied-out scaffold no
|
|
806
|
+
matter which sku was requested.
|
|
807
|
+
"""
|
|
808
|
+
catalog = catalog_path or CATALOG
|
|
809
|
+
doc = load_catalog(catalog)
|
|
810
|
+
record = find_template(doc, template_id, path=catalog)
|
|
811
|
+
# tan-cli#1077: everything below reads the CATALOG record, the fourth
|
|
812
|
+
# document of the malformed-document family and the only one decoded
|
|
813
|
+
# by `json.loads` + BARE SUBSCRIPT -- so its failure mode is
|
|
814
|
+
# `KeyError`, not the shape failure the three YAML documents produce.
|
|
815
|
+
# `record["supported"]["som_skus"]` (this line) and `record["example"]`
|
|
816
|
+
# (twice below) were bare double/single subscripts; the rest are
|
|
817
|
+
# guarded inside the helpers this function calls, each of which now
|
|
818
|
+
# takes the catalog path and this record's label so its curated
|
|
819
|
+
# message names THE FILE, THE FIELD and THE TYPE like the register at
|
|
820
|
+
# `tan/model/targets.py:312-323`. `find_template` has already required
|
|
821
|
+
# `record["id"] == template_id`, so labelling by id cannot itself lie.
|
|
822
|
+
rec_field = f"templates[{template_id!r}]"
|
|
823
|
+
supported = [
|
|
824
|
+
_require_field(entry, str, doc=catalog,
|
|
825
|
+
field=f"{rec_field}.supported.som_skus[{index}]")
|
|
826
|
+
for index, entry in enumerate(_require_key(
|
|
827
|
+
_require_key(record, "supported", dict,
|
|
828
|
+
doc=catalog, field=rec_field),
|
|
829
|
+
"som_skus", list, doc=catalog, field=f"{rec_field}.supported"))]
|
|
830
|
+
if sku not in supported:
|
|
831
|
+
raise SkuNotSupportedError(
|
|
832
|
+
f"{template_id}: sku {sku!r} is not supported "
|
|
833
|
+
f"(supported: {sorted(supported)})")
|
|
834
|
+
|
|
835
|
+
files = _ordered_files(record, doc=catalog, field=rec_field)
|
|
836
|
+
resolved = _resolve_params(record, params, doc=catalog, field=rec_field)
|
|
837
|
+
base = base_dir or REPO
|
|
838
|
+
metadata_root = metadata_root or METADATA_ROOT
|
|
839
|
+
preset = _default_preset_for_sku(sku, metadata_root)
|
|
840
|
+
|
|
841
|
+
example_rel = _require_key(record, "example", str,
|
|
842
|
+
doc=catalog, field=rec_field)
|
|
843
|
+
example_dir = _safe_join(base, example_rel, what="template example directory")
|
|
844
|
+
board_yaml_path = example_dir / "board.yaml"
|
|
845
|
+
# The other half of the ABSENT-document pair (tan-cli#1077 review) --
|
|
846
|
+
# see `load_catalog`'s docstring for the five-site measurement. The
|
|
847
|
+
# catalog's `example:` is drift-checked by alp-sdk's own
|
|
848
|
+
# `check_template_catalog.py`, but that gate runs on the SDK, not here,
|
|
849
|
+
# so a hand-edited catalog pointing at a directory with no `board.yaml`
|
|
850
|
+
# reached the user as a raw `FileNotFoundError`.
|
|
851
|
+
#
|
|
852
|
+
# tan-cli#1116 fixed a narrowed `except OSError` here (a non-UTF-8
|
|
853
|
+
# board.yaml escaped raw); tan-cli#1085 folded the fixed body into
|
|
854
|
+
# `DocumentGuards.require_readable_text`, the same definition
|
|
855
|
+
# `read_catalog_document` calls for the catalog's own read. Message
|
|
856
|
+
# text unchanged, one definition instead of two.
|
|
857
|
+
board_yaml_text = _require_readable_text(
|
|
858
|
+
board_yaml_path, what="template example board.yaml")
|
|
859
|
+
# tan-cli#1052: the fifth sibling of the same malformed-YAML family
|
|
860
|
+
# tan-cli#1025 -> #1034 -> #1037/#1048 swept through the SoM preset
|
|
861
|
+
# and the board metadata. THIS document is the catalog template's
|
|
862
|
+
# own `examples/<...>/board.yaml`, and every one of the four reads
|
|
863
|
+
# below was bare -- all four reachable from one command, `--emit
|
|
864
|
+
# scaffold --template peripheral --sku E1M-V2N101`. Measured on the
|
|
865
|
+
# pre-fix tree:
|
|
866
|
+
#
|
|
867
|
+
# <doc> = "- one\n- two\n" -> AttributeError: 'list' object has
|
|
868
|
+
# no attribute 'get' (:1542)
|
|
869
|
+
# cores: 3 -> AttributeError: 'int' object has
|
|
870
|
+
# no attribute 'keys' (:1542)
|
|
871
|
+
# som: 3 -> AttributeError: 'int' object has
|
|
872
|
+
# no attribute 'get' (:1543)
|
|
873
|
+
# pins: 3 -> TypeError: 'int' object is not
|
|
874
|
+
# iterable (:1551)
|
|
875
|
+
#
|
|
876
|
+
# Guarded through the same `_require_mapping_doc`/`_require_field`
|
|
877
|
+
# register the other two documents now use, so the next field added
|
|
878
|
+
# here inherits the rule instead of starting a sixth round. NOT
|
|
879
|
+
# stricter than `metadata/schemas/board.schema.json`: `pins:` is
|
|
880
|
+
# `type: array` with no `minItems`, so `pins: []` (and an absent or
|
|
881
|
+
# `null` `pins:`) still passes, as does `preset:` being absent -- it
|
|
882
|
+
# is optional, and only consulted when `pins:` is non-empty.
|
|
883
|
+
#
|
|
884
|
+
# Each read normalises `None` ONLY -- `[] if raw is None else raw`,
|
|
885
|
+
# never the `raw or []` the other two documents use. `or` collapses
|
|
886
|
+
# every falsy value, so a PRESENT but illegal scalar would be
|
|
887
|
+
# emptied instead of refused; measured on the first cut of this fix
|
|
888
|
+
# (tan-cli#1052 review), `pins: 0` / `pins: false` / `pins: ''` /
|
|
889
|
+
# `cores: 0` all rendered while `pins: 3` and `pins: true` raised.
|
|
890
|
+
# That degrades to empty rather than to garbage, but it is exactly
|
|
891
|
+
# the unpinned residual sibling this PR exists to stop leaving
|
|
892
|
+
# behind, so the falsy scalars are refused too.
|
|
893
|
+
#
|
|
894
|
+
# tan-cli#1133: the PARSE is guarded too. tan-cli#1116 fixed the READ
|
|
895
|
+
# here and left `yaml.safe_load` bare one line down, so a template
|
|
896
|
+
# example `board.yaml` that decoded fine but did not parse still raised
|
|
897
|
+
# a raw `yaml.parser.ParserError` through `emit_scaffold` -- measured on
|
|
898
|
+
# 3.12.3, 3.13.15 and 3.14.7 alike. Same `_parse_yaml_mapping` the two
|
|
899
|
+
# `metadata/**` documents above now use.
|
|
900
|
+
example_doc = _parse_yaml_mapping(
|
|
901
|
+
board_yaml_text, path=board_yaml_path,
|
|
902
|
+
what="template example board.yaml")
|
|
903
|
+
raw_cores = example_doc.get("cores")
|
|
904
|
+
original_core_ids = list(_require_field(
|
|
905
|
+
{} if raw_cores is None else raw_cores, dict,
|
|
906
|
+
doc=board_yaml_path, field="cores").keys())
|
|
907
|
+
raw_som = example_doc.get("som")
|
|
908
|
+
example_sku = _require_field(
|
|
909
|
+
{} if raw_som is None else raw_som, dict,
|
|
910
|
+
doc=board_yaml_path, field="som").get("sku", "")
|
|
911
|
+
core_renames = _derive_core_renames(original_core_ids, sku, metadata_root)
|
|
912
|
+
# `pins:` re-derivation (issue #876): each entry is either a bare
|
|
913
|
+
# pad string or a `{e1m, macro?, doc?}` mapping (same shape
|
|
914
|
+
# alp_orchestrate.loader's pins cross-check accepts); only the
|
|
915
|
+
# example's OWN preset (not an inline board def -- no catalog
|
|
916
|
+
# template ships one today) has a metadata/boards/<preset>.yaml to
|
|
917
|
+
# re-derive against.
|
|
918
|
+
raw_pins = example_doc.get("pins")
|
|
919
|
+
original_pins = list(_require_field(
|
|
920
|
+
[] if raw_pins is None else raw_pins, list,
|
|
921
|
+
doc=board_yaml_path, field="pins"))
|
|
922
|
+
source_preset = example_doc.get("preset")
|
|
923
|
+
if source_preset is not None:
|
|
924
|
+
# Same shape as `default_board:` one document over: a `preset:`
|
|
925
|
+
# that isn't a string reaches `metadata/boards/<preset>.yaml`
|
|
926
|
+
# as a path component and surfaces a curated-but-untrue message
|
|
927
|
+
# (measured pre-fix: `no metadata/boards/['a'].yaml for board
|
|
928
|
+
# ['a']`, which reads like a missing file rather than a
|
|
929
|
+
# malformed field). `None` is legal -- an inline board def, or
|
|
930
|
+
# a template with no `pins:` to re-derive.
|
|
931
|
+
_require_field(source_preset, str,
|
|
932
|
+
doc=board_yaml_path, field="preset")
|
|
933
|
+
pin_renames = (
|
|
934
|
+
_derive_pin_renames(original_pins, sku, source_preset, metadata_root)
|
|
935
|
+
if original_pins else {}
|
|
936
|
+
)
|
|
937
|
+
pin_macro_renames = (
|
|
938
|
+
_derive_pin_macro_renames(original_pins, sku, source_preset, metadata_root)
|
|
939
|
+
if original_pins else {}
|
|
940
|
+
)
|
|
941
|
+
pin_doc_renames = (
|
|
942
|
+
_derive_pin_doc_renames(original_pins, sku, source_preset, metadata_root)
|
|
943
|
+
if original_pins else {}
|
|
944
|
+
)
|
|
945
|
+
# README board-target rewrite (MAJOR C) still keys off a single
|
|
946
|
+
# "primary" app core -- the first m-prefixed core in
|
|
947
|
+
# original_core_ids, matching board.yaml's own declaration order
|
|
948
|
+
# (the same tie-break `_derive_core_renames`'s MAJOR D picks). One
|
|
949
|
+
# board id in the README prose is all MAJOR C ever rewrote, single-
|
|
950
|
+
# core template or not.
|
|
951
|
+
app_core_old = next((c for c in original_core_ids if c.startswith("m")), None)
|
|
952
|
+
app_core_sub = (
|
|
953
|
+
(app_core_old, core_renames[app_core_old])
|
|
954
|
+
if core_renames and app_core_old in core_renames else None
|
|
955
|
+
)
|
|
956
|
+
source_board = _core_board(example_sku, app_core_old, metadata_root)
|
|
957
|
+
target_board = _core_board(
|
|
958
|
+
sku, app_core_sub[1] if app_core_sub else app_core_old, metadata_root)
|
|
959
|
+
docs_ref = _docs_ref(base)
|
|
960
|
+
out: list[tuple[str, str]] = []
|
|
961
|
+
for rel, data in _rendered_bytes(
|
|
962
|
+
template_id, record, files, resolved, base,
|
|
963
|
+
doc=catalog, field=rec_field):
|
|
964
|
+
try:
|
|
965
|
+
text = data.decode("utf-8")
|
|
966
|
+
except UnicodeDecodeError as exc:
|
|
967
|
+
# render() copies any file as raw bytes; the JSON envelope
|
|
968
|
+
# cannot -- a future binary user_owned asset must fail
|
|
969
|
+
# cleanly here, not escape _run_scaffold_emit's `except
|
|
970
|
+
# TemplateError` as a raw traceback.
|
|
971
|
+
raise TemplateError(
|
|
972
|
+
f"{template_id}: {rel} is not valid UTF-8 text, cannot "
|
|
973
|
+
f"be JSON-encoded for --emit scaffold ({exc})") from exc
|
|
974
|
+
if rel == "board.yaml":
|
|
975
|
+
text = _substitute_board_yaml_sku(text, sku, preset)
|
|
976
|
+
for old, new in (core_renames or {}).items():
|
|
977
|
+
text = _substitute_board_yaml_core(text, old, new)
|
|
978
|
+
if pin_renames:
|
|
979
|
+
text = _substitute_board_yaml_pins(text, pin_renames, original_pins)
|
|
980
|
+
if pin_macro_renames:
|
|
981
|
+
text = _substitute_board_yaml_pin_macros(text, pin_macro_renames)
|
|
982
|
+
if pin_doc_renames:
|
|
983
|
+
text = _substitute_board_yaml_pin_docs(text, pin_doc_renames)
|
|
984
|
+
# A renamed pad can also survive as a stale mention in
|
|
985
|
+
# unrelated prose (issue #876 review MINOR 4) -- e.g.
|
|
986
|
+
# gpio-button-led's `preset:` header comment names the
|
|
987
|
+
# E1M-EVK pad in a continuation line the `preset:`
|
|
988
|
+
# substitution above never touches. Same treatment
|
|
989
|
+
# `_substitute_board_yaml_core` already gives a renamed
|
|
990
|
+
# core id (`_strip_stale_core_prose`).
|
|
991
|
+
for old in (pin_renames or {}):
|
|
992
|
+
text = _strip_stale_core_prose(text, old)
|
|
993
|
+
# alp-sdk#1855: board.yaml comments carry the same kind of
|
|
994
|
+
# bare alp-sdk-tree-only cross-reference README.md does
|
|
995
|
+
# (see `_BARE_REPO_PATH_RE`), but never went through any
|
|
996
|
+
# rewrite -- only README.md did.
|
|
997
|
+
text = _scaffold_bare_repo_paths(text, docs_ref)
|
|
998
|
+
elif rel.endswith("CMakeLists.txt"):
|
|
999
|
+
text = _scaffold_cmakelists(text)
|
|
1000
|
+
elif rel == "README.md":
|
|
1001
|
+
text = _scaffold_readme(
|
|
1002
|
+
text, example_rel, docs_ref,
|
|
1003
|
+
example_sku=example_sku, sku=sku,
|
|
1004
|
+
source_board=source_board, target_board=target_board,
|
|
1005
|
+
pin_renames=pin_renames)
|
|
1006
|
+
elif rel.endswith((".c", ".h")):
|
|
1007
|
+
# Same alp-sdk#1855 gap as board.yaml above -- a source
|
|
1008
|
+
# comment (e.g. cold-chain-monitor's src/main.c "(see
|
|
1009
|
+
# examples/ai/cold-chain-monitor/models/README.md)") is
|
|
1010
|
+
# never touched by any existing scaffold-adaptation pass.
|
|
1011
|
+
text = _scaffold_bare_repo_paths(text, docs_ref)
|
|
1012
|
+
out.append((rel, text))
|
|
1013
|
+
return out
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def emit_scaffold(
|
|
1017
|
+
template_id: str,
|
|
1018
|
+
sku: str,
|
|
1019
|
+
params: dict[str, Any] | None = None,
|
|
1020
|
+
*,
|
|
1021
|
+
catalog_path: Path | None = None,
|
|
1022
|
+
base_dir: Path | None = None,
|
|
1023
|
+
metadata_root: Path | None = None,
|
|
1024
|
+
) -> str:
|
|
1025
|
+
"""`--emit scaffold`'s STDOUT, byte for byte.
|
|
1026
|
+
|
|
1027
|
+
RELOCATED from `alp_project._run_scaffold_emit`'s two serialising lines. The
|
|
1028
|
+
envelope is `[{path, contents}, ...]` -- `render_to_envelope`'s ordered
|
|
1029
|
+
pairs, `json.dumps(..., indent=2)`, one trailing newline -- and alp-sdk's
|
|
1030
|
+
`scripts/check_emit_snapshots.py` diffs exactly these bytes against a
|
|
1031
|
+
committed golden, so the shape is the contract, not a presentation choice.
|
|
1032
|
+
"""
|
|
1033
|
+
envelope = render_to_envelope(
|
|
1034
|
+
template_id, sku, params, catalog_path=catalog_path, base_dir=base_dir,
|
|
1035
|
+
metadata_root=metadata_root)
|
|
1036
|
+
return json.dumps(
|
|
1037
|
+
[{"path": p, "contents": c} for p, c in envelope], indent=2) + "\n"
|