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,1522 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan new-som` -- scaffold the metadata for porting a brand-new SoM.
|
|
3
|
+
|
|
4
|
+
Port of `alp-sdk`'s `scripts/alp_cli/new_som.py`: the vendor-N+1 porting kit.
|
|
5
|
+
Generates the two metadata skeletons a new SoM port needs (the SoM preset
|
|
6
|
+
YAML and, when absent, the SoC spec JSON), with every schema-required
|
|
7
|
+
hardware-fact field present as an explicit TBD placeholder -- values are
|
|
8
|
+
NEVER invented -- and prints the numbered porting checklist.
|
|
9
|
+
|
|
10
|
+
Today's shipped `tan new-som` (`crates/tan-cli/src/commands/sdk_cli.rs`) is a
|
|
11
|
+
thin, stdio-inheriting forward to `python -m alp_cli new-som`. This file
|
|
12
|
+
becomes the real, in-process implementation; nothing here re-derives a
|
|
13
|
+
hardware fact the SDK's `metadata/**` already owns (ADR-0017, invariant
|
|
14
|
+
I-26) -- every SKU, part number, address or pin name in the generated
|
|
15
|
+
skeleton is either a caller-supplied argument or an explicit `TBD`.
|
|
16
|
+
|
|
17
|
+
Differences from the alp_cli original, and why:
|
|
18
|
+
|
|
19
|
+
* **`--format json` emits the envelope; the default text mode is the
|
|
20
|
+
original's, verbatim.** The original has no `--format`/`--json` flag, and
|
|
21
|
+
the Rust forwarder parses `--format` for this verb (clap `GlobalArgs`,
|
|
22
|
+
`global = true` -- confirmed live: `tan.exe new-som --format json
|
|
23
|
+
--sdk-root <bad>` reaches the SDK-root-unresolved failure, not a parse
|
|
24
|
+
error) without ever synthesising a `--json` for the child the way
|
|
25
|
+
`faultdecode`'s does (`sdk_cli.rs::build_argv`), so a successful run on
|
|
26
|
+
the oracle is plain text either way. This file used to match that split
|
|
27
|
+
exactly -- `--format` accepted and thrown away (`del ... output_format`),
|
|
28
|
+
zero `emit(` calls in the module. tan-cli#399 ends it: `contract/README.md`
|
|
29
|
+
states that the vscode extension drives `tan <cmd> --format json` and
|
|
30
|
+
hard-depends on `{command, ok, exitCode, project, data, issues}`, and a
|
|
31
|
+
verb that answers that flag with 1238 bytes of prose fails the consumer's
|
|
32
|
+
`isEnvelope` guard, which then reports the whole scaffold as the literal
|
|
33
|
+
string `Command completed.` with the planned file list unreachable. So
|
|
34
|
+
`--format json` now emits one envelope carrying the planned/written paths
|
|
35
|
+
and the porting checklist as `data`, and every refusal carries a
|
|
36
|
+
`new-som.failed` code instead of `cli.main`'s generic `cli.parse-error`.
|
|
37
|
+
WITHOUT `--format json` nothing changed: stdout carries the same
|
|
38
|
+
human-readable lines the original wrote there (skeleton validation notes,
|
|
39
|
+
`Created <path>`, the checklist), stderr the same error lines, matching
|
|
40
|
+
`click.echo(..., err=...)` verbatim -- which is what the oracle-parity
|
|
41
|
+
tests byte-compare.
|
|
42
|
+
|
|
43
|
+
* **`--sdk-root`/`--project` are new, and required.** The original ran
|
|
44
|
+
FROM WITHIN an alp-sdk checkout (`REPO_ROOT =
|
|
45
|
+
Path(__file__).resolve().parents[2]`) and never needed to ask where the
|
|
46
|
+
SDK was. `tan` is a separate, standalone binary, so this file resolves
|
|
47
|
+
the target checkout the same way every other metadata-facing command
|
|
48
|
+
does (`sdk_cmd.resolve_sdk_tiered`: `--sdk-root` > project pin > global
|
|
49
|
+
default > discovery) before doing anything else -- this is not a new
|
|
50
|
+
user-facing surface either: `tan new-som` already accepts `--sdk-root`
|
|
51
|
+
today, via the Rust forwarder's own `GlobalArgs`.
|
|
52
|
+
|
|
53
|
+
* **Interactive prompts use `click.prompt`/`click.Choice`, not
|
|
54
|
+
`questionary`.** The original's arrow-key select menus need a dependency
|
|
55
|
+
`tan` does not carry -- the package's runtime dependency set is a
|
|
56
|
+
deliberately short, enumerated list (`typer`, `rich`, `pyyaml`,
|
|
57
|
+
`jsonschema`; see `pyproject.toml`), and adding a fifth for one
|
|
58
|
+
command's menu widget is the opposite of that discipline. The SAME
|
|
59
|
+
questions are asked, in the same order, with the same defaults; only the
|
|
60
|
+
answer widget differs (type-and-choose instead of arrow-key-and-enter).
|
|
61
|
+
|
|
62
|
+
* **The SKU-prefix -> family-directory mapping is asked of the SDK, not
|
|
63
|
+
reproduced.** `_family_hw_revisions` cross-checks `--default-hw-rev`
|
|
64
|
+
against the new SKU's family's `hw-revisions.yaml`, which needs the
|
|
65
|
+
family directory (`aen`, `v2n`, ...) for a SKU prefix. That map
|
|
66
|
+
(`alp_project_loader._sku_family`) is hand-maintained and SDK-internal
|
|
67
|
+
with, by its own comment, "no second on-disk source" -- so it cannot be
|
|
68
|
+
read out of `metadata/**` the way every other fact in this file is, and
|
|
69
|
+
copying its dict into `tan` would be exactly the RFC #843 drift
|
|
70
|
+
(hand-porting SDK logic instead of consuming the SDK) this port exists
|
|
71
|
+
to avoid. `_resolve_sku_family` below instead asks the TARGET SDK's own
|
|
72
|
+
`alp_project_loader.py` for the answer via a short-lived subprocess,
|
|
73
|
+
matching the original's best-effort semantics (`None` on any failure --
|
|
74
|
+
unresolvable script, unrecognised prefix, or a brand-new family with no
|
|
75
|
+
mapping yet -- falls through to the "add this to the checklist"
|
|
76
|
+
branch, same as the original's `ImportError`-guarded import).
|
|
77
|
+
|
|
78
|
+
* **`--sku`'s help text (and the matching interactive prompt) say
|
|
79
|
+
"E1M-<UPPERCASE> shaped" instead of the oracle's "e.g. E1M-XYZ101"**
|
|
80
|
+
(`scripts/alp_cli/new_som.py:404`). Both describe the same `_SKU_RE`
|
|
81
|
+
pattern; the oracle's wording embeds a literal example SKU, and I-26's
|
|
82
|
+
hardware-fact gate (`tests/gates/test_no_new_hardware_facts.py`) would
|
|
83
|
+
then have to allowlist a string that names no real part just to keep the
|
|
84
|
+
wording verbatim, so this file describes the pattern shape instead.
|
|
85
|
+
|
|
86
|
+
`DEFAULT_BOARD` (`"E1M-EVK"`) is a UI default only, not a value this file
|
|
87
|
+
trusts: every accepted `--default-board` (including the unedited default)
|
|
88
|
+
is cross-checked against `metadata/boards/*.yaml`'s real `name:` values
|
|
89
|
+
before anything is rendered, so a stale literal here fails LOUD --
|
|
90
|
+
"default board 'E1M-EVK' does not match any `name:` in metadata/boards/"
|
|
91
|
+
-- rather than silently shipping a wrong one; see the allowlist entry in
|
|
92
|
+
`tests/gates/test_no_new_hardware_facts.py`.
|
|
93
|
+
"""
|
|
94
|
+
from __future__ import annotations
|
|
95
|
+
|
|
96
|
+
import json
|
|
97
|
+
import os
|
|
98
|
+
import re
|
|
99
|
+
import sys
|
|
100
|
+
from pathlib import Path
|
|
101
|
+
|
|
102
|
+
import click
|
|
103
|
+
import typer
|
|
104
|
+
|
|
105
|
+
from tan.commands.doctor_cmd import probe
|
|
106
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
107
|
+
from tan.core.metadata_schema import schema_errors
|
|
108
|
+
from tan.core.new_som_ospi import is_alif_ensemble, render_ospi_memories_and_hyperram
|
|
109
|
+
from tan.core.sdk_discovery import (
|
|
110
|
+
_planner_python,
|
|
111
|
+
global_default_foreign_project_issue,
|
|
112
|
+
project_pin_issue,
|
|
113
|
+
resolve_sdk_tiered,
|
|
114
|
+
)
|
|
115
|
+
from tan.core.shapes import SDK_MARKER, matches_glob_suffix, rejected_sdk_root_message
|
|
116
|
+
from tan.env import stderr_is_tty, stdin_is_tty
|
|
117
|
+
from tan.envelope import Envelope, Issue, Project, emit
|
|
118
|
+
from tan.exit_codes import ExitCode
|
|
119
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
120
|
+
|
|
121
|
+
_SKU_RE = re.compile(r"^E1M-[A-Z0-9-]+$")
|
|
122
|
+
_SOC_REF_RE = re.compile(r"^[a-z0-9-]+:[a-z0-9-]+:[a-z0-9-]+$")
|
|
123
|
+
_FAMILY_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
124
|
+
_CORE_ID_RE = re.compile(r"^[a-z][a-z0-9_]+$")
|
|
125
|
+
#: `som-preset-v2.schema.json`'s own `default_hw_rev` pattern, verbatim
|
|
126
|
+
#: (`metadata/schemas/som-preset-v2.schema.json` `properties.default_hw_rev
|
|
127
|
+
#: .pattern`). tan-cli#496 defect 5: this used to be checked ONLY by the
|
|
128
|
+
#: post-render schema self-check, which -- for a brand-new family, where the
|
|
129
|
+
#: `_family_hw_revisions` cross-check below is skipped -- reported a bad
|
|
130
|
+
#: value as `new-som.internal-failure` ("a tan bug, never bad user input"),
|
|
131
|
+
#: not the `new-som.failed` every other bad flag gets. Validated up front now
|
|
132
|
+
#: (`.fullmatch`, not `.match`: a trailing `$` still matches just before a
|
|
133
|
+
#: FINAL newline in non-MULTILINE mode, which is exactly the shape defect 3's
|
|
134
|
+
#: multi-line injection needs to slip past a `.match` check unnoticed).
|
|
135
|
+
_HW_REV_RE = re.compile(r"^[a-z0-9_-]+$")
|
|
136
|
+
|
|
137
|
+
#: Canonical backend keys already known to the device dispatcher, plus the
|
|
138
|
+
#: schema-legal lowercase `tbd` placeholder -- verbatim from the original.
|
|
139
|
+
INFERENCE_BACKENDS = ("ethos_u", "drpai", "deepx_dxm1", "tbd")
|
|
140
|
+
ETHOS_U_VARIANTS = ("u55", "u85")
|
|
141
|
+
|
|
142
|
+
#: `data.schemaVersion` for `--format json` (tan-cli#399). 1 because this is
|
|
143
|
+
#: the first shape `new-som` has ever put on the wire -- there is no earlier
|
|
144
|
+
#: one to be compatible with.
|
|
145
|
+
DATA_SCHEMA_VERSION = 1
|
|
146
|
+
|
|
147
|
+
DEFAULT_CORES = ("tbd_core0",)
|
|
148
|
+
#: UI default only -- see the module docstring and the gate allowlist entry.
|
|
149
|
+
DEFAULT_BOARD = "E1M-EVK"
|
|
150
|
+
DEFAULT_HW_REV = "r1"
|
|
151
|
+
|
|
152
|
+
#: `scripts/alp_project.py` is THE marker for an alp-sdk checkout (I-31).
|
|
153
|
+
#: `tan sdk switch` refuses in this build (tan-cli#305) -- kept the two
|
|
154
|
+
#: mechanisms that actually work here (`--sdk-root`, placing the project near
|
|
155
|
+
#: a checkout, both live tiers of `resolve_sdk_tiered`) and swapped the third
|
|
156
|
+
#: for `NO_SDK_NEXT_STEPS`'s honest "how to get one at all".
|
|
157
|
+
_SDK_ROOT_UNRESOLVED = (
|
|
158
|
+
"alp-sdk root is unresolved. Use --sdk-root, place the project near an "
|
|
159
|
+
f"alp-sdk checkout, or {NO_SDK_NEXT_STEPS}."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _split_cores_csv(_ctx: click.Context, _param: click.Parameter, value: str | None):
|
|
164
|
+
if value is None:
|
|
165
|
+
return None
|
|
166
|
+
return tuple(s.strip() for s in value.split(",") if s.strip())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _check_output_root(_ctx: click.Context, _param: click.Parameter, value: str | None) -> str | None:
|
|
170
|
+
"""`click.Path(file_okay=False)` equivalent for `--output-root` -- the
|
|
171
|
+
oracle's own type (`scripts/alp_cli/new_som.py:432`). An existing
|
|
172
|
+
REGULAR FILE is rejected outright; a directory that does not exist yet
|
|
173
|
+
is fine (it is created later). Reproduced as an explicit check rather
|
|
174
|
+
than relied on falling out of a later `mkdir`/`write_text`, matching
|
|
175
|
+
the sibling `--elf`/`--file` checks in `faultdecode_cmd.py`
|
|
176
|
+
(`_check_elf_path`, `_check_file_path`)."""
|
|
177
|
+
if value is None:
|
|
178
|
+
return None
|
|
179
|
+
if Path(value).is_file():
|
|
180
|
+
raise typer.BadParameter(f"'{value}' is a file.", param_hint="'--output-root'")
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _envelope(data, issues: list[Issue], exit_code: ExitCode) -> None:
|
|
185
|
+
"""THE `new-som` envelope. `project` is hardcoded null/null on purpose:
|
|
186
|
+
this command scaffolds metadata INTO an alp-sdk checkout and never reads a
|
|
187
|
+
`board.yaml` -- `--project` is only one input tier to the SDK-root ladder,
|
|
188
|
+
so reporting a project root here would claim a resolution that never
|
|
189
|
+
happened (`envelope.Project.resolved`'s own "reporting is the seam" note).
|
|
190
|
+
The checkout that was actually scaffolded is `data.sdkRoot`."""
|
|
191
|
+
emit(Envelope("new-som", Project(root=None, board_yaml=None), data, issues, exit_code))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _fail(message: str, exit_code: ExitCode = ExitCode.RUNTIME_FAILURE, *,
|
|
195
|
+
json_mode: bool = False, extra_issues: list[Issue] | None = None) -> None:
|
|
196
|
+
"""Refuse: one error to stderr (or one envelope under `--format json`) and
|
|
197
|
+
exit -- 1 by default, the flat exit code the alp_cli original's `_fail`
|
|
198
|
+
always used (`raise SystemExit(1)`) for every validation failure it can
|
|
199
|
+
raise (bad SKU, unknown board, ...); prefix reworded from `alp new-som:`
|
|
200
|
+
to `new-som:` (RFC #837: the binary is `tan`). `exit_code` overrides this
|
|
201
|
+
for the one failure this port adds that the original never had to: the
|
|
202
|
+
`--sdk-root`/`--project` resolution preflight below (the original always
|
|
203
|
+
ran FROM WITHIN a checkout). That check mirrors the Rust forwarder's own
|
|
204
|
+
preflight (`crates/tan-cli/src/commands/sdk_cli.rs::run`), which exits
|
|
205
|
+
`ExitCode::ValidationFailure` (2) for it specifically -- confirmed live:
|
|
206
|
+
`tan.exe new-som --sdk-root <bad>` exits 2, not 1 (every OTHER new-som
|
|
207
|
+
failure, including a bad-exit from the forwarded child, is RuntimeFailure
|
|
208
|
+
(1) there too, matching this default).
|
|
209
|
+
|
|
210
|
+
ONE code for every refusal, `new-som.failed`, matching the spelling the
|
|
211
|
+
v0.4.1 oracle's own forwarder emits for the identical class of refusal --
|
|
212
|
+
the alternative was a family of codes no consumer asked for, each of which
|
|
213
|
+
would then be a frozen wire string (tan-cli#399). Before this, a
|
|
214
|
+
`--format json` refusal reached `cli.main`'s generic fallback and went out
|
|
215
|
+
as `command: "cli"` / `cli.parse-error`, so the two paths of one command
|
|
216
|
+
disagreed about whose envelope a consumer was reading.
|
|
217
|
+
|
|
218
|
+
`data` is `{"subcommand": "new-som"}`, not `null`, matching the oracle
|
|
219
|
+
byte-for-byte -- measured live: `target/debug/tan.exe new-som --format
|
|
220
|
+
json --sdk-root <bad>` answers `data:{"subcommand":"new-som"}`, exit 2.
|
|
221
|
+
That is the SAME generic forwarder-preflight shape `sdk_cli.rs::run`
|
|
222
|
+
hands every `alp-*` forward it refuses before spawning a child (the
|
|
223
|
+
class faultdecode/model/monitor share), not something specific to any
|
|
224
|
+
one refusal reason here.
|
|
225
|
+
|
|
226
|
+
`extra_issues` (tan-cli#926) PREPENDS ahead of `new-som.failed`, the
|
|
227
|
+
`clean_cmd._run` shape; `None` at every other call site in this file."""
|
|
228
|
+
if json_mode:
|
|
229
|
+
_envelope(
|
|
230
|
+
{"subcommand": "new-som"},
|
|
231
|
+
[*(extra_issues or []), Issue("new-som.failed", "error", message)], exit_code,
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
typer.echo(f"new-som: {message}", err=True)
|
|
235
|
+
raise typer.Exit(int(exit_code))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _internal_error(subject: str, schema: str, errors: list[str], json_mode: bool) -> None:
|
|
239
|
+
"""A generated skeleton that does not validate against its own schema is a
|
|
240
|
+
`tan` BUG, never bad user input -- reported as one, and never left to
|
|
241
|
+
`cli.main`'s parse-error fallback under `--format json` (tan-cli#399).
|
|
242
|
+
Shared by the preset and SoC-spec self-checks, which differ only in the
|
|
243
|
+
two names."""
|
|
244
|
+
message = (
|
|
245
|
+
f"INTERNAL ERROR -- generated {subject} does not validate against {schema}:"
|
|
246
|
+
)
|
|
247
|
+
if json_mode:
|
|
248
|
+
_envelope(
|
|
249
|
+
None,
|
|
250
|
+
[Issue("new-som.internal-failure", "error", " ".join([message, *errors]))],
|
|
251
|
+
ExitCode.RUNTIME_FAILURE,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
typer.echo(f"new-som: {message}", err=True)
|
|
255
|
+
for err in errors:
|
|
256
|
+
typer.echo(f" - {err}", err=True)
|
|
257
|
+
raise typer.Exit(int(ExitCode.RUNTIME_FAILURE))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _has_control_char(value: str) -> bool:
|
|
261
|
+
r"""True if ``value`` carries a C0 control character (0x00-0x1F) or DEL
|
|
262
|
+
(0x7F) -- the full ASCII control set, not just C0.
|
|
263
|
+
|
|
264
|
+
tan-cli#496 round-2 major 2: DEL alone used to pass the narrower
|
|
265
|
+
`ord(ch) < 0x20` check every call site here used to write inline, reach
|
|
266
|
+
a rendered YAML double-quoted scalar unescaped (`_yaml_dquote` below
|
|
267
|
+
only escapes `\` and `"`), and crash the unguarded
|
|
268
|
+
`yaml.safe_load(preset_text)` self-check with a raw
|
|
269
|
+
`yaml.reader.ReaderError` -- exit 1, zero bytes on stdout under
|
|
270
|
+
`--format json` (measured: `--display-name $'X\x7fY'`). One shared
|
|
271
|
+
check closes it at every call site instead of widening each inline
|
|
272
|
+
comparison by hand.
|
|
273
|
+
"""
|
|
274
|
+
return any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in value)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _yaml_dquote(value: str) -> str:
|
|
278
|
+
r"""Escape ``value`` for embedding in a YAML double-quoted scalar.
|
|
279
|
+
|
|
280
|
+
YAML double-quoted style uses C-like escapes, so `\` and `"` are the
|
|
281
|
+
only characters that need escaping here (control characters are
|
|
282
|
+
rejected up front by the command's validation, `_has_control_char`).
|
|
283
|
+
"""
|
|
284
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _yaml_scalar(value: str) -> str:
|
|
288
|
+
"""A single-line YAML scalar for ``value``, chosen by PyYAML's OWN
|
|
289
|
+
emitter -- plain, single- or double-quoted, whichever round-trips
|
|
290
|
+
exactly -- rather than a hand-rolled quoting rule.
|
|
291
|
+
|
|
292
|
+
tan-cli#496 defect 3: `_yaml_dquote` above is a SECOND, independently
|
|
293
|
+
maintained escaping implementation, and it only ever covered
|
|
294
|
+
`display_name`; `default_hw_rev`/`default_board` were spliced RAW, so
|
|
295
|
+
`--default-hw-rev $'r1\\ncapabilities: {secure_element: true}'` did not
|
|
296
|
+
need to defeat any quoting at all -- the literal newline in the raw
|
|
297
|
+
argument became a second YAML mapping-key line the instant it was
|
|
298
|
+
f-string-joined into the preset, planting a fabricated hardware
|
|
299
|
+
capability the schema self-check never sees (it only re-parses the
|
|
300
|
+
ALREADY-INJECTED document). Routing both fields through the real
|
|
301
|
+
emitter instead removes the second implementation, not just patches it.
|
|
302
|
+
|
|
303
|
+
Raises ``ValueError`` -- "must not contain newlines or other control
|
|
304
|
+
characters" -- for a ``value`` this cannot represent on one line; the
|
|
305
|
+
caller pre-refuses those before render time, so this is the backstop,
|
|
306
|
+
not the primary gate (`fail()`-worded messages belong at the call site,
|
|
307
|
+
which knows the flag name).
|
|
308
|
+
|
|
309
|
+
tan-cli#496 round-2 blocker 1: this used to call `yaml.safe_dump(value)`
|
|
310
|
+
at PyYAML's DEFAULT 80-column `best_width` and keep only
|
|
311
|
+
`.splitlines()[0]` -- fine for a value that fits in 80 columns, but
|
|
312
|
+
PyYAML folds anything longer at a space, so any `--default-board`/
|
|
313
|
+
`--default-hw-rev` past that width was SILENTLY TRUNCATED into the
|
|
314
|
+
generated preset and reported as a clean success (a SoM vendor
|
|
315
|
+
hardware fact, corrupted with no error). Blocker 2: at greater length
|
|
316
|
+
the fold can land INSIDE an emitted quoted scalar, so the truncated
|
|
317
|
+
text is not even valid YAML -- `new_som`'s unguarded
|
|
318
|
+
`yaml.safe_load(preset_text)` self-check then raised a raw
|
|
319
|
+
`yaml.scanner.ScannerError`, exit 1 with zero bytes on stdout under
|
|
320
|
+
`--format json` (measured: `--default-board "$(python3 -c "print('word
|
|
321
|
+
'*30)")"`). `width=float("inf")` tells the emitter never to fold at
|
|
322
|
+
all, so whatever style it picks always lands on ONE content line --
|
|
323
|
+
`dumped[0]` then keeps the full value, and any trailing lines are only
|
|
324
|
+
PyYAML's own `...` document-end marker, never truncated content.
|
|
325
|
+
"""
|
|
326
|
+
if _has_control_char(value):
|
|
327
|
+
raise ValueError("must not contain newlines or other control characters")
|
|
328
|
+
# tan-cli#810: deferred, because `cli.py` static-imports every command
|
|
329
|
+
# module -- a module-scope `import yaml` here was paid by `tan --version`.
|
|
330
|
+
# `sys.modules` caches it, so this file's other three call sites cost a
|
|
331
|
+
# dict lookup. `tests/gates/test_cli_import_is_lean.py` holds the rule.
|
|
332
|
+
import yaml # noqa: PLC0415
|
|
333
|
+
|
|
334
|
+
dumped = yaml.safe_dump(value, width=float("inf")).splitlines()
|
|
335
|
+
return dumped[0] if dumped else "''"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _som_schema_path(sdk_root: Path) -> Path:
|
|
339
|
+
return sdk_root / "metadata" / "schemas" / "som-preset-v2.schema.json"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _soc_schema_path(sdk_root: Path) -> Path:
|
|
343
|
+
return sdk_root / "metadata" / "schemas" / "soc-spec-v1.schema.json"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _current_sku_pattern(schema_path: Path) -> str:
|
|
347
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
348
|
+
return schema["properties"]["sku"]["pattern"]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _is_yaml_board_file(name: str) -> bool:
|
|
352
|
+
"""True if @name has a ``.yaml`` suffix -- case-sensitively on POSIX,
|
|
353
|
+
case-INSENSITIVELY on Windows, matching ``Path.glob``'s own
|
|
354
|
+
``case_sensitive=None`` default (platform casing rules) exactly.
|
|
355
|
+
|
|
356
|
+
tan-cli#1127 review round 2: replacing ``boards_dir.glob("*.yaml")``
|
|
357
|
+
with ``os.listdir`` plus a plain ``entry.endswith(".yaml")`` would have
|
|
358
|
+
silently NARROWED the match on Windows -- a ``Foo.YAML`` board file
|
|
359
|
+
that ``glob`` used to enumerate there is invisible to a case-sensitive
|
|
360
|
+
suffix compare, dropping its ``name:`` from the known set and turning a
|
|
361
|
+
legitimate ``--default-board`` into a hard scaffold failure. This
|
|
362
|
+
exists so the listing helper below matches exactly what it replaced,
|
|
363
|
+
not a narrower set that happens to agree on POSIX. tan-cli#1132 hit the
|
|
364
|
+
identical question twice more (``*.conf``/``*.overlay``, ``*.json``), so
|
|
365
|
+
the RULE itself now lives once in ``shapes.matches_glob_suffix`` and this
|
|
366
|
+
is its ``.yaml`` application -- name, signature and contract unchanged;
|
|
367
|
+
only the place the casing decision is written moved.
|
|
368
|
+
"""
|
|
369
|
+
return matches_glob_suffix(name, ".yaml")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _known_board_names(sdk_root: Path) -> set[str] | None:
|
|
373
|
+
"""Board ``name:`` values from ``<sdk_root>/metadata/boards/``, or
|
|
374
|
+
``None`` when the directory is missing OR listable-but-empty/degenerate
|
|
375
|
+
(see the ``return names or None`` note below) -- the closed set of
|
|
376
|
+
shared carrier boards lives in the SDK checkout, not under
|
|
377
|
+
``--output-root``.
|
|
378
|
+
|
|
379
|
+
tan-cli#1116 review round 2/3 replaced a ``boards_dir.is_dir()``
|
|
380
|
+
pre-flight with ``boards_dir.glob("*.yaml")`` wrapped in one ``except
|
|
381
|
+
OSError``. That primitive choice was itself only ACCIDENTALLY correct,
|
|
382
|
+
tan-cli#1127 found: ``Path.glob`` raises ``PermissionError`` for a
|
|
383
|
+
permission-denied parent OR grandparent directory on 3.12.3, so the
|
|
384
|
+
``except OSError`` here was genuinely load-bearing THERE -- but on
|
|
385
|
+
3.13.15 AND 3.14.7 ``glob`` instead returns an EMPTY iterator for the
|
|
386
|
+
identical shape, silently (measured; driven against real
|
|
387
|
+
``python-build-standalone`` 3.13.15/3.14.7 builds plus this project's
|
|
388
|
+
pinned 3.12.3). On those two interpreters the ``except OSError`` clause
|
|
389
|
+
never fired at all, and this function still answered ``None`` only
|
|
390
|
+
because ``return names or None`` below happens to treat "found nothing"
|
|
391
|
+
and "permission denied" as the same outcome. A refactor touching only
|
|
392
|
+
that return line (``return names or None`` -> ``return names``, which
|
|
393
|
+
reads as a harmless tidy-up) would have silently reintroduced the
|
|
394
|
+
escaping-``PermissionError`` defect on 3.13/3.14 while staying green on
|
|
395
|
+
3.12 -- nothing at the call site would have said why.
|
|
396
|
+
|
|
397
|
+
tan-cli#1127 makes the correctness deliberate: the listing now goes
|
|
398
|
+
through ``os.listdir``, which raises ``OSError`` (``PermissionError``
|
|
399
|
+
included) for a permission-denied ancestor -- parent or grandparent --
|
|
400
|
+
on all three measured interpreters, unlike ``Path.glob``. The ``except
|
|
401
|
+
OSError`` below is genuinely load-bearing on every supported
|
|
402
|
+
interpreter now, not only 3.12; nothing about this function's
|
|
403
|
+
permission-handling remains interpreter-dependent.
|
|
404
|
+
|
|
405
|
+
Review round 2 (PR #1129) caught a second, independent way the
|
|
406
|
+
primitive swap could have silently changed behaviour: ``Path.glob``'s
|
|
407
|
+
default ``case_sensitive=None`` matches by PLATFORM casing rules --
|
|
408
|
+
case-sensitive on POSIX, case-INSENSITIVE on Windows -- so
|
|
409
|
+
``boards_dir.glob("*.yaml")`` used to enumerate a ``Foo.YAML`` on
|
|
410
|
+
Windows, and a plain ``entry.endswith(".yaml")`` over ``os.listdir``
|
|
411
|
+
would not (measured identically on all three interpreters run on this
|
|
412
|
+
POSIX box: ``glob("*.yaml")`` -> ``['lower.yaml']``,
|
|
413
|
+
``glob("*.yaml", case_sensitive=False)`` -> ``['lower.yaml',
|
|
414
|
+
'upper.YAML']``, a plain ``endswith(".yaml")`` -> ``['lower.yaml']`` only).
|
|
415
|
+
``_is_yaml_board_file`` above preserves that Windows matching exactly,
|
|
416
|
+
by delegating to ``shapes.matches_glob_suffix`` (tan-cli#1132, where
|
|
417
|
+
the case-fold moved) -- this function's contract is "list what
|
|
418
|
+
`Path.glob("*.yaml")` used to list, correctly, on every interpreter",
|
|
419
|
+
not "list what a case-sensitive suffix compare finds".
|
|
420
|
+
"""
|
|
421
|
+
boards_dir = sdk_root / "metadata" / "boards"
|
|
422
|
+
import yaml # noqa: PLC0415 -- deferred, see `_yaml_scalar` (tan-cli#810)
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
entries = os.listdir(boards_dir)
|
|
426
|
+
except OSError:
|
|
427
|
+
return None
|
|
428
|
+
candidates = sorted(
|
|
429
|
+
boards_dir / entry for entry in entries if _is_yaml_board_file(entry)
|
|
430
|
+
)
|
|
431
|
+
names: set[str] = set()
|
|
432
|
+
for path in candidates:
|
|
433
|
+
try:
|
|
434
|
+
doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
435
|
+
except (OSError, yaml.YAMLError, UnicodeDecodeError):
|
|
436
|
+
# tan-cli#415: `UnicodeDecodeError` is a `ValueError`, not a
|
|
437
|
+
# `yaml.YAMLError`, so a non-UTF-8 board file escaped this
|
|
438
|
+
# best-effort scan as an unhandled traceback instead of being
|
|
439
|
+
# skipped the same way an unparseable one already is.
|
|
440
|
+
#
|
|
441
|
+
# tan-cli#1116 review round 2: `OSError` is caught too -- a file
|
|
442
|
+
# this listing enumerated but cannot itself be READ (a per-file
|
|
443
|
+
# `chmod 000`, distinct from the containing-directory failure
|
|
444
|
+
# the `except OSError` around `os.listdir` above already
|
|
445
|
+
# covers) raised a raw `PermissionError` past this same
|
|
446
|
+
# best-effort scan (measured escaping before this fix).
|
|
447
|
+
continue
|
|
448
|
+
if isinstance(doc, dict) and isinstance(doc.get("name"), str):
|
|
449
|
+
names.add(doc["name"])
|
|
450
|
+
# tan-cli#1127: still doing real work, not a vestige of the retired
|
|
451
|
+
# `Path.glob` accident. `os.listdir` above already turns "denied
|
|
452
|
+
# ancestor" into an explicit `None` via `except OSError`; this line's
|
|
453
|
+
# remaining job is coalescing a LISTABLE-BUT-EMPTY result (no `.yaml`
|
|
454
|
+
# files at all, or every one present failed to parse / carried no
|
|
455
|
+
# `name:`) into that same "not resolvable" `None` -- the caller
|
|
456
|
+
# (`board_names is not None and default_board not in board_names`)
|
|
457
|
+
# treats `None` as "can't verify, don't gate on it" and would otherwise
|
|
458
|
+
# hard-fail every scaffold against an empty `known:` list for a
|
|
459
|
+
# directory that is present but genuinely has nothing in it yet.
|
|
460
|
+
return names or None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _resolve_sku_family(sku: str, sdk_root: Path) -> str | None:
|
|
464
|
+
"""The SKU's family DIRECTORY name (``aen``, ``v2n``, ...), resolved by
|
|
465
|
+
asking the TARGET SDK's own ``alp_project_loader._sku_family`` -- never
|
|
466
|
+
reproduced in `tan` (I-26; see the module docstring). Best-effort: any
|
|
467
|
+
failure (script missing, unrecognised prefix, broken interpreter) is
|
|
468
|
+
``None``, matching the original's `ImportError`-guarded import.
|
|
469
|
+
"""
|
|
470
|
+
script = sdk_root / "scripts" / "alp_project_loader.py"
|
|
471
|
+
if not script.is_file():
|
|
472
|
+
return None
|
|
473
|
+
out = probe(
|
|
474
|
+
[
|
|
475
|
+
_planner_python(str(sdk_root), str(sdk_root)),
|
|
476
|
+
"-c",
|
|
477
|
+
"import sys; sys.path.insert(0, sys.argv[1]); "
|
|
478
|
+
"from alp_project_loader import _sku_family; "
|
|
479
|
+
"print(_sku_family(sys.argv[2]))",
|
|
480
|
+
os.path.abspath(sdk_root / "scripts"), # probe runs from an empty cwd
|
|
481
|
+
sku,
|
|
482
|
+
]
|
|
483
|
+
)
|
|
484
|
+
if out is None:
|
|
485
|
+
return None
|
|
486
|
+
family_dir = out.strip()
|
|
487
|
+
return family_dir or None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _family_hw_revisions(
|
|
491
|
+
sku: str, output_root: Path, sdk_root: Path
|
|
492
|
+
) -> tuple[Path, set[str]] | None:
|
|
493
|
+
"""(path, rev keys) of the SKU family's hw-revisions.yaml, or None.
|
|
494
|
+
|
|
495
|
+
None means "not resolvable at scaffold time" -- a brand-new family with
|
|
496
|
+
no SKU-prefix mapping / no hw-revisions file yet (creating one is a
|
|
497
|
+
porting-checklist step).
|
|
498
|
+
|
|
499
|
+
tan-cli#1116 review round 2/3: NOT a ``path.is_file()`` pre-flight --
|
|
500
|
+
that used to decide whether to try THIS root's candidate or fall
|
|
501
|
+
through to the next one, and it raised a raw ``PermissionError`` for a
|
|
502
|
+
permission-denied ancestor directory on 3.12.3 AND 3.13.15 (measured
|
|
503
|
+
escaping before this fix on both) -- NOT "every supported interpreter"
|
|
504
|
+
as an earlier draft of this note claimed: on 3.14.7 ``is_file()``
|
|
505
|
+
itself already returns ``False`` for that same shape (the same
|
|
506
|
+
swallowed-errno version skew the other round-2 fixes in this PR
|
|
507
|
+
describe; `is_file()` is not exempt from it after all), so the OLD
|
|
508
|
+
pre-flight would have silently taken the wrong branch there too -- not
|
|
509
|
+
a crash, but a permission failure on THIS root silently treated as
|
|
510
|
+
"not here, try the next root" instead of the genuine "found it, but
|
|
511
|
+
it's broken" this function's own docstring below distinguishes.
|
|
512
|
+
Reading straight through instead:
|
|
513
|
+
``FileNotFoundError``/``IsADirectoryError``/``NotADirectoryError`` all
|
|
514
|
+
mean "not usable AT THIS ROOT", so they fall through to the next
|
|
515
|
+
candidate exactly as a ``False`` ``is_file()`` used to; any OTHER
|
|
516
|
+
failure (permission, a non-UTF-8 file, invalid YAML) means the file IS
|
|
517
|
+
there and IS the one for this SKU, so it stops the search with ``None``
|
|
518
|
+
rather than silently trying a second root that was never going to be
|
|
519
|
+
the right answer.
|
|
520
|
+
|
|
521
|
+
tan-cli#1127 re-checked this function against the ``_known_board_names``
|
|
522
|
+
finding next door and found it does NOT need the same
|
|
523
|
+
``Path.glob``-to-``os.listdir`` swap: this function never calls
|
|
524
|
+
``Path.glob`` (or any other directory-listing primitive) in the first
|
|
525
|
+
place -- it reads ``path.read_text()`` directly, with no stat-based
|
|
526
|
+
pre-flight, which is exactly the "read straight through" shape the
|
|
527
|
+
paragraph above already describes. ``Path.read_text()`` raises
|
|
528
|
+
``PermissionError`` for a permission-denied ancestor identically on
|
|
529
|
+
3.12.3, 3.13.15 and 3.14.7 (measured, both for the immediate parent and
|
|
530
|
+
a grandparent directory denied) -- unlike ``Path.glob``/``Path.is_dir()``/
|
|
531
|
+
``Path.is_file()``, plain reads were never subject to that version skew.
|
|
532
|
+
This function's permission-handling was ALREADY deliberate, not
|
|
533
|
+
accidental, before tan-cli#1127; nothing here changed.
|
|
534
|
+
"""
|
|
535
|
+
family_dir = _resolve_sku_family(sku, sdk_root)
|
|
536
|
+
if family_dir is None:
|
|
537
|
+
return None
|
|
538
|
+
import yaml # noqa: PLC0415 -- deferred, see `_yaml_scalar` (tan-cli#810)
|
|
539
|
+
|
|
540
|
+
for root in (output_root, sdk_root):
|
|
541
|
+
path = root / "metadata" / "e1m_modules" / family_dir / "hw-revisions.yaml"
|
|
542
|
+
try:
|
|
543
|
+
doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
544
|
+
except (FileNotFoundError, IsADirectoryError, NotADirectoryError):
|
|
545
|
+
continue
|
|
546
|
+
except (OSError, yaml.YAMLError, UnicodeDecodeError):
|
|
547
|
+
# tan-cli#415: `UnicodeDecodeError` is caught alongside
|
|
548
|
+
# `yaml.YAMLError` -- it is a `ValueError`, not a
|
|
549
|
+
# `yaml.YAMLError`, and previously escaped uncaught rather than
|
|
550
|
+
# resolving to "not resolvable". `OSError` (tan-cli#1116 review
|
|
551
|
+
# round 2) covers a per-file `chmod 000`, distinct from the
|
|
552
|
+
# ancestor-directory case the `except` above now handles by
|
|
553
|
+
# falling through instead of stopping.
|
|
554
|
+
return None
|
|
555
|
+
revs = doc.get("hw_revisions")
|
|
556
|
+
if isinstance(revs, dict):
|
|
557
|
+
return path, {str(k) for k in revs}
|
|
558
|
+
return None
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _interactive(
|
|
563
|
+
sku: str | None,
|
|
564
|
+
soc_ref: str | None,
|
|
565
|
+
family: str | None,
|
|
566
|
+
vendor: str | None,
|
|
567
|
+
display_name: str | None,
|
|
568
|
+
inference_backend: str | None,
|
|
569
|
+
ethos_u_variant: str | None,
|
|
570
|
+
cores: tuple[str, ...] | None,
|
|
571
|
+
default_board: str | None,
|
|
572
|
+
default_hw_rev: str | None,
|
|
573
|
+
):
|
|
574
|
+
"""Ask the same questions, in the same order, the original's
|
|
575
|
+
`questionary`-based `_interactive` did -- see the module docstring for
|
|
576
|
+
why the answer widget is `click.prompt`/`click.Choice` here instead.
|
|
577
|
+
|
|
578
|
+
Every prompt below is `err=True`: `click.prompt`'s OWN default is
|
|
579
|
+
`err=False` (the question goes to stdout), which is backwards for a
|
|
580
|
+
command whose consent gate (`tan.core.consent.can_prompt`,
|
|
581
|
+
`_interactive`'s only caller checks stdin+STDERR) already promises the
|
|
582
|
+
question rides stderr and the answer rides stdin -- stdout stays free
|
|
583
|
+
for whatever the invocation is piping or redirecting (tan-cli#496
|
|
584
|
+
defect 6). Without this, `tan new-som > log.txt` with a real terminal
|
|
585
|
+
on stdin/stderr writes the question into `log.txt` instead of the
|
|
586
|
+
terminal and blocks on a question the human never saw.
|
|
587
|
+
"""
|
|
588
|
+
if sku is None:
|
|
589
|
+
sku = click.prompt("New SoM SKU (E1M-<UPPERCASE> shaped)", err=True).strip()
|
|
590
|
+
if soc_ref is None:
|
|
591
|
+
soc_ref = click.prompt(
|
|
592
|
+
"Silicon triple-colon ref (vendor:family:part, e.g. nxp:imx9:imx95)", err=True
|
|
593
|
+
).strip()
|
|
594
|
+
if family is None:
|
|
595
|
+
family = click.prompt(
|
|
596
|
+
"Human-readable family slug (e.g. nxp-imx9)", err=True
|
|
597
|
+
).strip()
|
|
598
|
+
if vendor is None:
|
|
599
|
+
vendor = click.prompt(
|
|
600
|
+
"Vendor display name for the SoC JSON",
|
|
601
|
+
default=soc_ref.split(":")[0] if _SOC_REF_RE.match(soc_ref) else "",
|
|
602
|
+
err=True,
|
|
603
|
+
).strip()
|
|
604
|
+
if display_name is None:
|
|
605
|
+
display_name = click.prompt(
|
|
606
|
+
"Display name",
|
|
607
|
+
default=f"{sku} ({vendor} -- scaffold, silicon facts TBD)",
|
|
608
|
+
err=True,
|
|
609
|
+
).strip()
|
|
610
|
+
if inference_backend is None:
|
|
611
|
+
inference_backend = click.prompt(
|
|
612
|
+
"Inference backend (silicon-determined; pick `tbd` when unknown)",
|
|
613
|
+
type=click.Choice(INFERENCE_BACKENDS),
|
|
614
|
+
default="tbd",
|
|
615
|
+
err=True,
|
|
616
|
+
)
|
|
617
|
+
if inference_backend == "ethos_u" and ethos_u_variant is None:
|
|
618
|
+
ethos_u_variant = click.prompt(
|
|
619
|
+
"Primary Ethos-U variant",
|
|
620
|
+
type=click.Choice(ETHOS_U_VARIANTS),
|
|
621
|
+
err=True,
|
|
622
|
+
)
|
|
623
|
+
if cores is None:
|
|
624
|
+
answer = click.prompt(
|
|
625
|
+
"Canonical core ids, comma-separated (leave the tbd placeholder "
|
|
626
|
+
"when unknown)",
|
|
627
|
+
default=",".join(DEFAULT_CORES),
|
|
628
|
+
err=True,
|
|
629
|
+
)
|
|
630
|
+
cores = tuple(s.strip() for s in answer.split(",") if s.strip())
|
|
631
|
+
if default_board is None:
|
|
632
|
+
default_board = click.prompt(
|
|
633
|
+
"Default board (stock carrier)", default=DEFAULT_BOARD, err=True
|
|
634
|
+
).strip()
|
|
635
|
+
if default_hw_rev is None:
|
|
636
|
+
default_hw_rev = click.prompt(
|
|
637
|
+
"Default hw rev", default=DEFAULT_HW_REV, err=True
|
|
638
|
+
).strip()
|
|
639
|
+
return (
|
|
640
|
+
sku,
|
|
641
|
+
soc_ref,
|
|
642
|
+
family,
|
|
643
|
+
vendor,
|
|
644
|
+
display_name,
|
|
645
|
+
inference_backend,
|
|
646
|
+
ethos_u_variant,
|
|
647
|
+
cores,
|
|
648
|
+
default_board,
|
|
649
|
+
default_hw_rev,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _render_preset(
|
|
654
|
+
sku: str,
|
|
655
|
+
soc_ref: str,
|
|
656
|
+
family: str,
|
|
657
|
+
display_name: str,
|
|
658
|
+
inference_backend: str,
|
|
659
|
+
ethos_u_variant: str | None,
|
|
660
|
+
cores: tuple[str, ...],
|
|
661
|
+
default_board: str,
|
|
662
|
+
default_hw_rev: str,
|
|
663
|
+
) -> str:
|
|
664
|
+
vendor_slug, family_slug, part_slug = soc_ref.split(":")
|
|
665
|
+
soc_rel = f"metadata/socs/{vendor_slug}/{family_slug}/{part_slug}.json"
|
|
666
|
+
|
|
667
|
+
lines: list[str] = []
|
|
668
|
+
a = lines.append
|
|
669
|
+
a(f"# Stock preset skeleton for {sku}, generated by `tan new-som`.")
|
|
670
|
+
a("#")
|
|
671
|
+
a("# Every value below marked TBD awaits the authoritative hardware")
|
|
672
|
+
a("# config (datasheet / schematic / BOM). Fill facts in from the")
|
|
673
|
+
a("# primary source named next to each field -- NEVER guess (see")
|
|
674
|
+
a("# docs/porting-new-som.md).")
|
|
675
|
+
a("")
|
|
676
|
+
a("schema_version: 2")
|
|
677
|
+
a("")
|
|
678
|
+
a("# SKU is assigned by Alp Lab product planning. A brand-new family")
|
|
679
|
+
a("# also needs one alternation added to the `sku:` pattern in")
|
|
680
|
+
a("# metadata/schemas/som-preset-v2.schema.json (porting guide step 3).")
|
|
681
|
+
a(f"sku: {sku}")
|
|
682
|
+
a("")
|
|
683
|
+
a("# Human-readable family slug (NOT the directory name; the SKU-prefix")
|
|
684
|
+
a("# -> family-directory map lives in scripts/alp_project.py).")
|
|
685
|
+
a(f"family: {family}")
|
|
686
|
+
a("")
|
|
687
|
+
a(f"# Triple-colon silicon ref; must resolve to {soc_rel}.")
|
|
688
|
+
a(f"silicon: {soc_ref}")
|
|
689
|
+
a("")
|
|
690
|
+
a("# Vendor order code from the datasheet's Ordering Information table;")
|
|
691
|
+
a(f"# must match a variants[].order_code in {soc_rel}.")
|
|
692
|
+
a("# While TBD, the loader falls back to variants[].alp_module_skus")
|
|
693
|
+
a("# reverse lookup.")
|
|
694
|
+
a("silicon_variant: TBD")
|
|
695
|
+
a("")
|
|
696
|
+
a(f'display_name: "{_yaml_dquote(display_name)}"')
|
|
697
|
+
a("")
|
|
698
|
+
a("# Populated on-module chips -- the module BOM / schematic is")
|
|
699
|
+
a("# authoritative. Chip values are slugs matching a")
|
|
700
|
+
a("# metadata/chips/<chip>.yaml manifest. The legal role keys are the")
|
|
701
|
+
a("# CLOSED set in som-preset-v2.schema.json `on_module:` (pmic_main,")
|
|
702
|
+
a("# pmic_secondary, clock_generator, rtc_external, temperature_sensor,")
|
|
703
|
+
a("# eeprom, secure_element, wifi_ble, supervisor_mcu, ethernet_phy,")
|
|
704
|
+
a("# nor_flash, emmc, npu, pcie_mux, ospi_memories, hyperram,")
|
|
705
|
+
a("# i2c_devices). Delete the TBD rows for roles this module does NOT")
|
|
706
|
+
a("# populate; add rows for the ones it does.")
|
|
707
|
+
a("on_module:")
|
|
708
|
+
a(f" silicon: {soc_ref}")
|
|
709
|
+
a(" pmic_main: TBD # main PMIC part (BOM)")
|
|
710
|
+
a(" eeprom: TBD # identity EEPROM part (BOM)")
|
|
711
|
+
if is_alif_ensemble(vendor_slug, family_slug):
|
|
712
|
+
lines.extend(render_ospi_memories_and_hyperram())
|
|
713
|
+
a(" # i2c_devices: # per-bus I2C device tables;")
|
|
714
|
+
a(" # <bus_name>: # bus names are a per-family")
|
|
715
|
+
a(" # bus_master: TBD # fact (schematic). Chip")
|
|
716
|
+
a(" # devices: # slugs + 7-bit addresses come")
|
|
717
|
+
a(' # - { chip: <slug>, role: <role>, address_7bit: "TBD" }')
|
|
718
|
+
a("")
|
|
719
|
+
a("# Off-SoC module memory in the canonical cross-family shape.")
|
|
720
|
+
a("# Authoritative source: the module BOM (DRAM + flash parts).")
|
|
721
|
+
a("# On-die SRAM/MRAM is derived from the SoC variant, never declared here.")
|
|
722
|
+
a("memory:")
|
|
723
|
+
a(" dram_mbit: TBD # external volatile memory, Mbit")
|
|
724
|
+
a(" flash_mbit: TBD # external non-volatile memory, Mbit")
|
|
725
|
+
a("")
|
|
726
|
+
a("# Inference-accelerator selection -- silicon-determined (SoC")
|
|
727
|
+
a("# datasheet), never customer-facing. `auto_order` is the ordered")
|
|
728
|
+
a("# ALP_INFERENCE_BACKEND_AUTO preference, best first; its first entry")
|
|
729
|
+
a("# IS the preferred backend, and `cpu` (the floor), when listed, goes")
|
|
730
|
+
a("# last. `tbd` is the schema-legal placeholder backend;")
|
|
731
|
+
a("# scripts/check_inference_backend_parity.py cross-checks every entry")
|
|
732
|
+
a("# against the device dispatcher and accepts `tbd` ONLY while")
|
|
733
|
+
a("# `status.preliminary:` below is true, so this scaffold commits green")
|
|
734
|
+
a("# as-is. Replace `tbd` with the real canonical keys (ethos_u, drpai,")
|
|
735
|
+
a("# deepx_dxm1, ..., then cpu) before clearing the preliminary flag.")
|
|
736
|
+
a("inference:")
|
|
737
|
+
a(f" auto_order: [{inference_backend}]")
|
|
738
|
+
if inference_backend == "ethos_u":
|
|
739
|
+
# Primary variant only. Which Ethos-U instances the part carries is
|
|
740
|
+
# silicon-determined -- the SDK derives it from the SoC JSON npus[] /
|
|
741
|
+
# capabilities.ethos_uNN_count -- so the preset does NOT enumerate them
|
|
742
|
+
# (the removed `npu_population` field is never scaffolded).
|
|
743
|
+
a(f" ethos_u_variant: {ethos_u_variant}")
|
|
744
|
+
a("")
|
|
745
|
+
a("# capabilities: -- OPTIONAL, omitted in the skeleton. Declare ONLY")
|
|
746
|
+
a("# keys the SoM ADDS on top of the silicon capabilities (e.g. an")
|
|
747
|
+
a("# on-module secure element or bridge accelerator); silicon caps come")
|
|
748
|
+
a(f"# from {soc_rel} `capabilities:`.")
|
|
749
|
+
a("")
|
|
750
|
+
a("# Per-core default runtime + app mapping. Keys MUST match cores[].id")
|
|
751
|
+
a(f"# in {soc_rel} (cross-checked by pr-metadata-validate). Rename any")
|
|
752
|
+
a("# tbd placeholder id in lockstep with the SoC JSON, then fill each")
|
|
753
|
+
a("# entry: `machine:` + `toolchain:` for Yocto A-class cores, `board:`")
|
|
754
|
+
a("# + `toolchain:` for Zephyr M-class cores (see E1M-AEN801.yaml /")
|
|
755
|
+
a("# E1M-V2N102.yaml for the two shapes).")
|
|
756
|
+
a("topology:")
|
|
757
|
+
for core in cores:
|
|
758
|
+
a(f" {core}: {{}}")
|
|
759
|
+
a("")
|
|
760
|
+
a("# Memory layout (SRAM banks + on-die flash) is derived from the SoC")
|
|
761
|
+
a("# variant resolved via `silicon_variant:` -- see the SoC JSON")
|
|
762
|
+
a("# `variants[].sram_banks_kb`. Declare a memory_map: block here ONLY")
|
|
763
|
+
a("# for non-stock partitioning. Every row you author MUST carry")
|
|
764
|
+
a("# `write_authority:` (REQUIRED since som-preset v2; the schema")
|
|
765
|
+
a("# rejects a row without it). It records WHO may write the region,")
|
|
766
|
+
a("# and when: customer_image (written only by the flash tool),")
|
|
767
|
+
a("# vendor_image (factory-provisioned), customer_runtime (writable by")
|
|
768
|
+
a("# the application; the ONLY value an IPC carve-out or runtime mount")
|
|
769
|
+
a("# may land on), secure_enclave (written by the Secure Enclave at")
|
|
770
|
+
a("# provisioning), none (an explicit no-writer, not a \"not sure yet\"")
|
|
771
|
+
a("# catch-all), composite (a whole-device alias spanning rows of")
|
|
772
|
+
a("# different authority). Absent means unresolved, never")
|
|
773
|
+
a("# customer_runtime. See docs/porting-new-som.md.")
|
|
774
|
+
a("")
|
|
775
|
+
a("# Vendor mailbox / IPC controller -- the vendor reference manual /")
|
|
776
|
+
a("# hand-written HW config is authoritative. The channel reservations")
|
|
777
|
+
a("# below are the SDK-standard convention shared by every released")
|
|
778
|
+
a("# preset; keep them unless the controller has fewer channels.")
|
|
779
|
+
a("mailbox:")
|
|
780
|
+
a(" controller: TBD")
|
|
781
|
+
a(" channels:")
|
|
782
|
+
a(" - { id: 0, reserved_for: alp_default_rpmsg }")
|
|
783
|
+
a(" - { id: 1, reserved_for: app }")
|
|
784
|
+
a(" - { id: 2, reserved_for: app }")
|
|
785
|
+
a(" - { id: 3, reserved_for: power_mgmt }")
|
|
786
|
+
a("")
|
|
787
|
+
a("# pad_routes: -- OPTIONAL, omitted in the skeleton. Once the")
|
|
788
|
+
a("# schematic is available, either (a) list every E1M pad that routes")
|
|
789
|
+
a("# through an on-module mediator chip (see E1M-V2N102.yaml), (b)")
|
|
790
|
+
a("# declare an explicit empty list to assert \"no mediator, everything")
|
|
791
|
+
a("# is SoC-direct\", or (c) list pads with")
|
|
792
|
+
a("# `dispatch: TBD` while routing is pending. Omitted, the loader")
|
|
793
|
+
a("# treats every pad as SoC-direct -- resolve this before clearing")
|
|
794
|
+
a("# status.partial_hw_config.")
|
|
795
|
+
a("")
|
|
796
|
+
a("# helper_firmware: -- OPTIONAL, omitted in the skeleton. One entry")
|
|
797
|
+
a("# per independently-flashed on-module helper MCU image.")
|
|
798
|
+
a("# Omit when the module has none.")
|
|
799
|
+
a("")
|
|
800
|
+
a("# Must resolve against metadata/e1m_modules/<family-dir>/hw-revisions.yaml.")
|
|
801
|
+
a(f"default_hw_rev: {_yaml_scalar(default_hw_rev)}")
|
|
802
|
+
a("")
|
|
803
|
+
a("# Stock carrier board this SoM ships on (see metadata/boards/).")
|
|
804
|
+
a(f"default_board: {_yaml_scalar(default_board)}")
|
|
805
|
+
a("")
|
|
806
|
+
a("# Keep both flags true until every TBD above is resolved from the")
|
|
807
|
+
a("# authoritative HW config. `preliminary: true` is also the marker")
|
|
808
|
+
a("# that lets the parity gate accept the `tbd` inference backend.")
|
|
809
|
+
a("status:")
|
|
810
|
+
a(" preliminary: true")
|
|
811
|
+
a(" partial_hw_config: true")
|
|
812
|
+
a("")
|
|
813
|
+
return "\n".join(lines)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _soc_skeleton(sku: str, soc_ref: str, vendor: str, cores: tuple[str, ...]) -> dict:
|
|
817
|
+
_, family_slug, part_slug = soc_ref.split(":")
|
|
818
|
+
core_rows = []
|
|
819
|
+
for core in cores:
|
|
820
|
+
# `count: 1` is the schema minimum, NOT a datasheet fact -- the
|
|
821
|
+
# notes[] entry below flags it (JSON has no comments; this file
|
|
822
|
+
# uses the schema-sanctioned `_pending_reason` + `notes` fields).
|
|
823
|
+
core_rows.append({"id": core, "type": "TBD", "count": 1})
|
|
824
|
+
return {
|
|
825
|
+
"soc_spec_version": 1,
|
|
826
|
+
"ref": soc_ref,
|
|
827
|
+
"vendor": vendor,
|
|
828
|
+
"family": family_slug,
|
|
829
|
+
"part": part_slug,
|
|
830
|
+
"status": "preliminary",
|
|
831
|
+
"pending_reference_manual_ingestion": True,
|
|
832
|
+
"_pending_reason": (
|
|
833
|
+
"Scaffolded by `tan new-som`: every silicon fact in this file "
|
|
834
|
+
"is a placeholder pending datasheet / reference-manual "
|
|
835
|
+
"ingestion. peripherals: {} means unknown, not zero."
|
|
836
|
+
),
|
|
837
|
+
"cores": core_rows,
|
|
838
|
+
"npus": [],
|
|
839
|
+
"peripherals": {},
|
|
840
|
+
"variants": [
|
|
841
|
+
{
|
|
842
|
+
"order_code": "TBD",
|
|
843
|
+
"notes": (
|
|
844
|
+
"Placeholder row: replace order_code with the vendor's "
|
|
845
|
+
"Ordering Information part number, then mirror it in the "
|
|
846
|
+
"SoM preset's silicon_variant."
|
|
847
|
+
),
|
|
848
|
+
"alp_module_skus": [sku],
|
|
849
|
+
}
|
|
850
|
+
],
|
|
851
|
+
"notes": [
|
|
852
|
+
"JSON has no comments -- scaffold TODOs live in this notes[] "
|
|
853
|
+
"array plus _pending_reason (both schema-sanctioned fields).",
|
|
854
|
+
"cores[]: the tbd id / type TBD / count 1 rows are "
|
|
855
|
+
"schema-minimum placeholders, NOT datasheet facts; rename the "
|
|
856
|
+
"ids in lockstep with the SoM preset's topology: keys.",
|
|
857
|
+
"vendor/family/part hold the ref slugs until the datasheet "
|
|
858
|
+
"display names are filled in.",
|
|
859
|
+
],
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# tan-cli#964: the validator itself moved to `tan.core.metadata_schema` --
|
|
864
|
+
# every READ consumer (`tan build`, `tan generate`, `tan presets`, `tan
|
|
865
|
+
# size`, `tan bootstrap`) needed the SAME `jsonschema.Draft202012Validator`
|
|
866
|
+
# call this file already had for its WRITE-path self-check, and a second
|
|
867
|
+
# hand-rolled copy is exactly the per-consumer-guard drift #964 is about.
|
|
868
|
+
# `_schema_errors` is kept as the local name (both call sites below are
|
|
869
|
+
# unchanged) and still means exactly what it always has here: `source=None`,
|
|
870
|
+
# so a validation failure is reported against the in-memory generated
|
|
871
|
+
# skeleton (`pointer: message`), not a file on disk -- this scaffold has not
|
|
872
|
+
# been written yet when either call below runs.
|
|
873
|
+
_schema_errors = schema_errors
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _rollback_write_failure(
|
|
877
|
+
preset_path: Path,
|
|
878
|
+
preset_existed_before: bool,
|
|
879
|
+
preset_backup: bytes | None,
|
|
880
|
+
preset_write_attempted: bool,
|
|
881
|
+
soc_path: Path,
|
|
882
|
+
soc_doc,
|
|
883
|
+
) -> list[str]:
|
|
884
|
+
"""Undo whatever THIS invocation actually touched after a write failure,
|
|
885
|
+
and report exactly what could not be undone -- never more. Returns a
|
|
886
|
+
description string per candidate that could not be undone; an empty
|
|
887
|
+
list means the rollback is clean.
|
|
888
|
+
|
|
889
|
+
Three tan-cli#496 fixes live here:
|
|
890
|
+
|
|
891
|
+
* **Defect 2.** `preset_path` is RESTORED to its original bytes when it
|
|
892
|
+
pre-existed (a `--force` re-scaffold whose SoC-spec write then
|
|
893
|
+
failed), never deleted -- a destructive op must not remove a good,
|
|
894
|
+
pre-existing artifact before its replacement is proven good. A
|
|
895
|
+
brand-new preset (did not exist before this run) is still removed;
|
|
896
|
+
there is nothing of the customer's to lose there.
|
|
897
|
+
* **The finding against #496's own fix.** `soc_path` is removed only
|
|
898
|
+
when it PHYSICALLY EXISTS -- `Path.exists()` swallows `OSError`
|
|
899
|
+
(including the `NotADirectoryError` a regular file below
|
|
900
|
+
`--output-root` raises), unlike `unlink(missing_ok=True)`, which only
|
|
901
|
+
swallows `FileNotFoundError` and lets that one through. The issue's
|
|
902
|
+
own primary repro (a regular file at `<output-root>/metadata/socs`)
|
|
903
|
+
fails at `mkdir()` before `soc_path` is ever created, so `exists()`
|
|
904
|
+
is False and nothing is attempted -- the old code attempted the
|
|
905
|
+
unlink anyway, got a second `NotADirectoryError`, and told the
|
|
906
|
+
caller "this may be a half-written scaffold" for a path that was
|
|
907
|
+
never written at all.
|
|
908
|
+
* **tan-cli#496 round-2 blocker 3.** The preset branch below is gated
|
|
909
|
+
on `preset_write_attempted`, a flag the caller sets the instant the
|
|
910
|
+
preset write is ABOUT to start -- not on membership in a `written`
|
|
911
|
+
list the caller only appended to AFTER `write_text` returned
|
|
912
|
+
successfully. `Path.write_text` opens the file (truncating any
|
|
913
|
+
pre-existing content) and issues one `write()` call; an `OSError`
|
|
914
|
+
from THAT call (`ENOSPC`/`EDQUOT`/`EFBIG`/`EIO`) leaves a physically
|
|
915
|
+
created, partially-written file on disk but raises before the
|
|
916
|
+
caller's `written.append(preset_path)` ever ran. Gating on `written`
|
|
917
|
+
alone treated that file as never touched by this run and left the
|
|
918
|
+
partial write unrolled-back; `preset_write_attempted` covers that
|
|
919
|
+
case too, on top of the "wrote fine, the SoC-spec write after it
|
|
920
|
+
failed" case a `written` check alone already caught.
|
|
921
|
+
"""
|
|
922
|
+
cleanup_failures: list[str] = []
|
|
923
|
+
if preset_write_attempted:
|
|
924
|
+
if preset_existed_before:
|
|
925
|
+
if preset_backup is None:
|
|
926
|
+
cleanup_failures.append(
|
|
927
|
+
f"{preset_path} (no backup was captured; its content "
|
|
928
|
+
"cannot be safely restored -- left as written by this run)"
|
|
929
|
+
)
|
|
930
|
+
else:
|
|
931
|
+
try:
|
|
932
|
+
preset_path.write_bytes(preset_backup)
|
|
933
|
+
except OSError as exc:
|
|
934
|
+
cleanup_failures.append(
|
|
935
|
+
f"{preset_path} (could not restore its original content: {exc})"
|
|
936
|
+
)
|
|
937
|
+
else:
|
|
938
|
+
try:
|
|
939
|
+
if preset_path.exists():
|
|
940
|
+
preset_path.unlink()
|
|
941
|
+
except OSError as exc:
|
|
942
|
+
cleanup_failures.append(f"{preset_path} ({exc})")
|
|
943
|
+
if soc_doc is not None:
|
|
944
|
+
try:
|
|
945
|
+
if soc_path.exists():
|
|
946
|
+
soc_path.unlink()
|
|
947
|
+
except OSError as exc:
|
|
948
|
+
cleanup_failures.append(f"{soc_path} ({exc})")
|
|
949
|
+
return cleanup_failures
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def new_som(
|
|
953
|
+
sku: str = typer.Option(
|
|
954
|
+
None,
|
|
955
|
+
"--sku",
|
|
956
|
+
# tan-cli#720: `--som` accepted as an alias, matching `tan init`'s
|
|
957
|
+
# spelling of the same value. The refusal message below keeps naming
|
|
958
|
+
# `--sku`, the canonical form for this command.
|
|
959
|
+
"--som",
|
|
960
|
+
help="New SoM SKU (E1M-<UPPERCASE> shaped) (alias: --som).",
|
|
961
|
+
),
|
|
962
|
+
soc_ref: str = typer.Option(
|
|
963
|
+
None, "--soc-ref", help="Silicon triple-colon ref, e.g. nxp:imx9:imx95."
|
|
964
|
+
),
|
|
965
|
+
family: str = typer.Option(
|
|
966
|
+
None, "--family", help="Human-readable family slug, e.g. nxp-imx9."
|
|
967
|
+
),
|
|
968
|
+
vendor: str = typer.Option(
|
|
969
|
+
None,
|
|
970
|
+
"--vendor",
|
|
971
|
+
help="Vendor display name for the SoC JSON (default: the soc-ref vendor segment).",
|
|
972
|
+
),
|
|
973
|
+
display_name: str = typer.Option(
|
|
974
|
+
None, "--display-name", help="Preset display_name (default: derived from the SKU)."
|
|
975
|
+
),
|
|
976
|
+
inference_backend: str = typer.Option(
|
|
977
|
+
None,
|
|
978
|
+
"--inference-backend",
|
|
979
|
+
help="Silicon-determined inference backend "
|
|
980
|
+
f"({'/'.join(INFERENCE_BACKENDS)}; default: tbd).",
|
|
981
|
+
),
|
|
982
|
+
ethos_u_variant: str = typer.Option(
|
|
983
|
+
None,
|
|
984
|
+
"--ethos-u-variant",
|
|
985
|
+
help="Primary Ethos-U variant "
|
|
986
|
+
f"({'/'.join(ETHOS_U_VARIANTS)}); required with --inference-backend ethos_u.",
|
|
987
|
+
),
|
|
988
|
+
cores: str = typer.Option(
|
|
989
|
+
None,
|
|
990
|
+
"--cores",
|
|
991
|
+
callback=_split_cores_csv,
|
|
992
|
+
help="Comma-separated canonical core ids (default: tbd_core0).",
|
|
993
|
+
),
|
|
994
|
+
default_board: str = typer.Option(
|
|
995
|
+
None,
|
|
996
|
+
"--default-board",
|
|
997
|
+
help="Stock carrier board (a `name:` from metadata/boards/; "
|
|
998
|
+
"default: the SDK's stock development-board carrier).",
|
|
999
|
+
),
|
|
1000
|
+
default_hw_rev: str = typer.Option(
|
|
1001
|
+
None, "--default-hw-rev", help="Default hardware revision (default: r1)."
|
|
1002
|
+
),
|
|
1003
|
+
output_root: str = typer.Option(
|
|
1004
|
+
None,
|
|
1005
|
+
"--output-root",
|
|
1006
|
+
metavar="PATH",
|
|
1007
|
+
callback=_check_output_root,
|
|
1008
|
+
help="Root to generate metadata/ under (default: the SDK checkout).",
|
|
1009
|
+
),
|
|
1010
|
+
dry_run: bool = typer.Option(
|
|
1011
|
+
False, "--dry-run", help="Validate and print the planned files; write nothing."
|
|
1012
|
+
),
|
|
1013
|
+
force: bool = typer.Option(
|
|
1014
|
+
False, "--force", help="Overwrite an existing preset for this SKU."
|
|
1015
|
+
),
|
|
1016
|
+
project: str = typer.Option(
|
|
1017
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1018
|
+
),
|
|
1019
|
+
sdk_root: str = typer.Option(
|
|
1020
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1021
|
+
),
|
|
1022
|
+
board_yaml: str = typer.Option(None, "--board-yaml", hidden=True),
|
|
1023
|
+
target: str = typer.Option(None, "--target", hidden=True),
|
|
1024
|
+
all_targets: bool = typer.Option(False, "--all", hidden=True),
|
|
1025
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
1026
|
+
verbose: bool = typer.Option(False, "--verbose", hidden=True),
|
|
1027
|
+
quiet: bool = typer.Option(False, "--quiet", hidden=True),
|
|
1028
|
+
no_color: bool = typer.Option(False, "--no-color", hidden=True),
|
|
1029
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", hidden=True),
|
|
1030
|
+
ci: bool = typer.Option(False, "--ci", hidden=True),
|
|
1031
|
+
) -> None:
|
|
1032
|
+
"""Scaffold the metadata skeletons for porting a new SoM."""
|
|
1033
|
+
# The nine options above are clap's `GlobalArgs` members (`global = true`)
|
|
1034
|
+
# that the oracle accepts on EVERY verb, `new-som` included, and never
|
|
1035
|
+
# reads for this one -- declared here purely so the argv SURFACE matches:
|
|
1036
|
+
# `tan new-som --ci ...` exits the same as the same invocation without
|
|
1037
|
+
# `--ci` on the oracle; without this, it was a Click "No such option"
|
|
1038
|
+
# usage error (exit 2) instead. `--format` is no longer one of them: it
|
|
1039
|
+
# was accepted-and-deleted here until tan-cli#399, which is what made a
|
|
1040
|
+
# `--format json` scaffold print prose at the one consumer that parses
|
|
1041
|
+
# this flag -- see the docstring bullet above.
|
|
1042
|
+
del board_yaml, target, all_targets
|
|
1043
|
+
del verbose, quiet, no_color, non_interactive, ci
|
|
1044
|
+
resolved_format = output_format or OutputFormat.TEXT
|
|
1045
|
+
json_mode = resolved_format == "json"
|
|
1046
|
+
|
|
1047
|
+
report_lines: list[str] = []
|
|
1048
|
+
|
|
1049
|
+
def say(line: str = "") -> None:
|
|
1050
|
+
"""One line of the human report. Suppressed under `--format json`,
|
|
1051
|
+
where stdout carries the ONE envelope and nothing else -- the same
|
|
1052
|
+
lines survive there as `data.report`."""
|
|
1053
|
+
report_lines.append(line)
|
|
1054
|
+
if not json_mode:
|
|
1055
|
+
typer.echo(line)
|
|
1056
|
+
|
|
1057
|
+
def fail(message: str, exit_code: ExitCode = ExitCode.RUNTIME_FAILURE, *,
|
|
1058
|
+
extra_issues: list[Issue] | None = None) -> None:
|
|
1059
|
+
"""`_fail` with this run's output mode bound, so no refusal site has to
|
|
1060
|
+
remember to pass it -- a forgotten one would silently print prose to
|
|
1061
|
+
stdout under `--format json` and leave the envelope to `cli.main`."""
|
|
1062
|
+
_fail(message, exit_code, json_mode=json_mode, extra_issues=extra_issues)
|
|
1063
|
+
|
|
1064
|
+
# Mirrors the original's `type=click.Choice(...)` flag-level validation --
|
|
1065
|
+
# a Click-usage error (exit 2) BEFORE anything else runs, same as the
|
|
1066
|
+
# original raised it during argument parsing itself. Written as an
|
|
1067
|
+
# explicit check rather than `click_type=click.Choice(...)` on the
|
|
1068
|
+
# `typer.Option` above: that form crashes with an uncaught `StopIteration`
|
|
1069
|
+
# under this repo's pinned typer/click pair instead of the clean
|
|
1070
|
+
# `BadParameter` every other command in this package raises by hand (see
|
|
1071
|
+
# `sdk_cmd.sdk`'s `--format` check for the established pattern).
|
|
1072
|
+
if inference_backend is not None and inference_backend not in INFERENCE_BACKENDS:
|
|
1073
|
+
raise typer.BadParameter(
|
|
1074
|
+
f"{inference_backend!r} is not one of "
|
|
1075
|
+
f"{', '.join(repr(c) for c in INFERENCE_BACKENDS)}.",
|
|
1076
|
+
param_hint="'--inference-backend'",
|
|
1077
|
+
)
|
|
1078
|
+
if ethos_u_variant is not None and ethos_u_variant not in ETHOS_U_VARIANTS:
|
|
1079
|
+
raise typer.BadParameter(
|
|
1080
|
+
f"{ethos_u_variant!r} is not one of "
|
|
1081
|
+
f"{', '.join(repr(c) for c in ETHOS_U_VARIANTS)}.",
|
|
1082
|
+
param_hint="'--ethos-u-variant'",
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
workspace_root = Path.cwd() / project if project else Path.cwd()
|
|
1086
|
+
active = resolve_sdk_tiered(sdk_root, workspace_root)
|
|
1087
|
+
# tan-cli#926: computed before the `.path is None` refusal below (tan-cli#900 fix).
|
|
1088
|
+
pin_issue = project_pin_issue(active.broken_project_pin, active.tier)
|
|
1089
|
+
foreign_issue = global_default_foreign_project_issue(active.foreign_global_default_for)
|
|
1090
|
+
resolution_issues = [i for i in (pin_issue, foreign_issue) if i is not None]
|
|
1091
|
+
if not json_mode:
|
|
1092
|
+
for issue in resolution_issues:
|
|
1093
|
+
typer.echo(f"new-som: warning: {issue.message}", err=True)
|
|
1094
|
+
if active.path is None or not Path(active.path).joinpath(*SDK_MARKER).exists():
|
|
1095
|
+
# tan-cli#497 defect 7: a REJECTED `--sdk-root` names the value.
|
|
1096
|
+
# `resolve_sdk_tiered` is TERMINAL on the flag (I-31) and returns it
|
|
1097
|
+
# verbatim even when invalid, so the marker check right above is the
|
|
1098
|
+
# ONLY thing that knows the path was rejected -- and this refusal
|
|
1099
|
+
# carries no `sdk` block, so the typed value left no trace at all while
|
|
1100
|
+
# the message told the caller to "Use --sdk-root".
|
|
1101
|
+
fail(
|
|
1102
|
+
rejected_sdk_root_message(sdk_root, "No SoM scaffold was written.")
|
|
1103
|
+
if sdk_root
|
|
1104
|
+
else _SDK_ROOT_UNRESOLVED,
|
|
1105
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1106
|
+
extra_issues=resolution_issues,
|
|
1107
|
+
)
|
|
1108
|
+
return
|
|
1109
|
+
resolved_sdk = Path(active.path)
|
|
1110
|
+
# tan-cli#263 review: this command WRITES metadata skeletons into
|
|
1111
|
+
# `resolved_sdk` -- a silently-missed `.alp/sdk-path` pin means porting a
|
|
1112
|
+
# new SoM into the wrong checkout. Under `--format json` it rides the
|
|
1113
|
+
# envelope's `issues` as the already-frozen `sdk.project-pin-unresolved`
|
|
1114
|
+
# (tan-cli#399); in text mode it stays the stderr line it always was.
|
|
1115
|
+
# tan-cli#464: same reasoning -- this command writes metadata skeletons
|
|
1116
|
+
# into `resolved_sdk`, and a `globalDefault` answer a DIFFERENT project's
|
|
1117
|
+
# bootstrap relocation actually decided is exactly as dangerous here as a
|
|
1118
|
+
# silently-missed project pin.
|
|
1119
|
+
issues: list[Issue] = list(resolution_issues)
|
|
1120
|
+
|
|
1121
|
+
# -- 1. Gather inputs. Interactive prompts need a real terminal; in a
|
|
1122
|
+
# pipe / CI, fail fast naming exactly what is missing instead of an
|
|
1123
|
+
# opaque prompt-library abort.
|
|
1124
|
+
if sku is None or soc_ref is None or family is None:
|
|
1125
|
+
missing = [
|
|
1126
|
+
flag
|
|
1127
|
+
for flag, value in (("--sku", sku), ("--soc-ref", soc_ref), ("--family", family))
|
|
1128
|
+
if value is None
|
|
1129
|
+
]
|
|
1130
|
+
# `sys.stdin`/`sys.stderr` are bare references, not guaranteed
|
|
1131
|
+
# streams. Two distinct failures, and a guard has to cover both:
|
|
1132
|
+
# either can be `None` under a console-less launcher, and either can
|
|
1133
|
+
# be an object that EXISTS but has no `.isatty()` -- `cli.py`'s own
|
|
1134
|
+
# `_TeeStderr` under `--format json` is exactly that shape. A
|
|
1135
|
+
# hand-rolled `X is None or not X.isatty()` catches the first and
|
|
1136
|
+
# raises `AttributeError` on the second.
|
|
1137
|
+
#
|
|
1138
|
+
# tan-cli#496 round-2 major 1: BOTH streams are checked, not only
|
|
1139
|
+
# `stdin`. `_interactive`'s prompts are `err=True` (defect 6 above)
|
|
1140
|
+
# -- the question rides stderr, the answer rides stdin -- so a
|
|
1141
|
+
# redirected/piped stderr with a real stdin terminal has no way to
|
|
1142
|
+
# show the human the question, and the old stdin-only gate admitted
|
|
1143
|
+
# exactly that shape, blocking on a prompt nobody could see.
|
|
1144
|
+
#
|
|
1145
|
+
# tan-cli#488 round 5: both operands route through
|
|
1146
|
+
# `tan.env.stdin_is_tty`/`stderr_is_tty` -- the one shared probe
|
|
1147
|
+
# (tan-cli#288) that catches `AttributeError` (replaced stream) and
|
|
1148
|
+
# `ValueError` (closed stream) alike -- rather than repeating the
|
|
1149
|
+
# hand-rolled shape a sixth time. `tan.core.consent.can_prompt`
|
|
1150
|
+
# carries the same pair for the same reason.
|
|
1151
|
+
if not (stdin_is_tty() and stderr_is_tty()):
|
|
1152
|
+
fail(
|
|
1153
|
+
"stdin or stderr is not a terminal, so interactive prompts "
|
|
1154
|
+
"are unavailable; pass the missing required flag(s): "
|
|
1155
|
+
+ ", ".join(missing)
|
|
1156
|
+
)
|
|
1157
|
+
return
|
|
1158
|
+
if json_mode:
|
|
1159
|
+
# A prompt writes to stdout, which under `--format json` carries
|
|
1160
|
+
# the one envelope -- prompting there would corrupt the very
|
|
1161
|
+
# document the caller asked for, on a terminal that happens to be
|
|
1162
|
+
# attached (tan-cli#399).
|
|
1163
|
+
fail(
|
|
1164
|
+
"--format json makes stdout the envelope channel, so "
|
|
1165
|
+
"interactive prompts are unavailable; pass the missing "
|
|
1166
|
+
"required flag(s): " + ", ".join(missing)
|
|
1167
|
+
)
|
|
1168
|
+
return
|
|
1169
|
+
(
|
|
1170
|
+
sku,
|
|
1171
|
+
soc_ref,
|
|
1172
|
+
family,
|
|
1173
|
+
vendor,
|
|
1174
|
+
display_name,
|
|
1175
|
+
inference_backend,
|
|
1176
|
+
ethos_u_variant,
|
|
1177
|
+
cores,
|
|
1178
|
+
default_board,
|
|
1179
|
+
default_hw_rev,
|
|
1180
|
+
) = _interactive(
|
|
1181
|
+
sku,
|
|
1182
|
+
soc_ref,
|
|
1183
|
+
family,
|
|
1184
|
+
vendor,
|
|
1185
|
+
display_name,
|
|
1186
|
+
inference_backend,
|
|
1187
|
+
ethos_u_variant,
|
|
1188
|
+
cores,
|
|
1189
|
+
default_board,
|
|
1190
|
+
default_hw_rev,
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# Flag-driven (CI) defaults for everything not provided.
|
|
1194
|
+
if inference_backend is None:
|
|
1195
|
+
inference_backend = "tbd"
|
|
1196
|
+
if cores is None:
|
|
1197
|
+
cores = DEFAULT_CORES
|
|
1198
|
+
if default_board is None:
|
|
1199
|
+
default_board = DEFAULT_BOARD
|
|
1200
|
+
if default_hw_rev is None:
|
|
1201
|
+
default_hw_rev = DEFAULT_HW_REV
|
|
1202
|
+
output_root_path = Path(output_root) if output_root else resolved_sdk
|
|
1203
|
+
|
|
1204
|
+
# -- 2. Validate EVERYTHING before touching the filesystem, so a
|
|
1205
|
+
# rejected invocation never leaves half-written skeletons behind.
|
|
1206
|
+
if not _SKU_RE.match(sku):
|
|
1207
|
+
fail(f"SKU '{sku}' must look like E1M-<UPPERCASE>")
|
|
1208
|
+
return
|
|
1209
|
+
if not _SOC_REF_RE.match(soc_ref):
|
|
1210
|
+
fail(f"soc-ref '{soc_ref}' must be <vendor>:<family>:<part> (lowercase slugs)")
|
|
1211
|
+
return
|
|
1212
|
+
if not _FAMILY_RE.match(family):
|
|
1213
|
+
fail(f"family '{family}' must be a lowercase slug")
|
|
1214
|
+
return
|
|
1215
|
+
if not cores:
|
|
1216
|
+
fail(
|
|
1217
|
+
"--cores was given but contains no core ids "
|
|
1218
|
+
"(omit the flag for the tbd_core0 placeholder)"
|
|
1219
|
+
)
|
|
1220
|
+
return
|
|
1221
|
+
bad_cores = [c for c in cores if not _CORE_ID_RE.match(c)]
|
|
1222
|
+
if bad_cores:
|
|
1223
|
+
fail(f"core id(s) {bad_cores} must match ^[a-z][a-z0-9_]+$")
|
|
1224
|
+
return
|
|
1225
|
+
# tan-cli#496 defect 4: a repeated `--cores` id used to reach both
|
|
1226
|
+
# skeletons -- a duplicate `<id>: {}` mapping key in the preset's
|
|
1227
|
+
# `topology:` block (invalid YAML; PyYAML keeps the last, a strict
|
|
1228
|
+
# loader like alp-sdk's `validate_metadata.py` rejects it outright) and
|
|
1229
|
+
# two identical `cores[]` rows in the SoC JSON -- reported as a clean
|
|
1230
|
+
# success by both schema self-checks, since neither schema forbids a
|
|
1231
|
+
# repeated id within its own document. `tan init`'s sibling parser
|
|
1232
|
+
# (`tan.core.scaffold.parse_cores`) already refuses this; new-som didn't.
|
|
1233
|
+
dup_cores = sorted({c for c in cores if cores.count(c) > 1})
|
|
1234
|
+
if dup_cores:
|
|
1235
|
+
fail(f"--cores has duplicate id(s) {dup_cores}; every core id must be unique")
|
|
1236
|
+
return
|
|
1237
|
+
if inference_backend == "ethos_u" and ethos_u_variant is None:
|
|
1238
|
+
fail("--inference-backend ethos_u requires --ethos-u-variant (u55/u85)")
|
|
1239
|
+
return
|
|
1240
|
+
if vendor is None:
|
|
1241
|
+
vendor = soc_ref.split(":")[0]
|
|
1242
|
+
if display_name is None:
|
|
1243
|
+
display_name = f"{sku} ({vendor} -- scaffold, silicon facts TBD)"
|
|
1244
|
+
if _has_control_char(display_name):
|
|
1245
|
+
fail("display name must not contain newlines or other control characters")
|
|
1246
|
+
return
|
|
1247
|
+
# tan-cli#496 defect 3/5: `default_hw_rev` used to reach the RENDERED
|
|
1248
|
+
# preset unchecked by this command itself -- only the post-render schema
|
|
1249
|
+
# self-check saw it, and only after the value had already been spliced
|
|
1250
|
+
# into YAML text. A single-line-but-out-of-pattern value (`R1`) then
|
|
1251
|
+
# surfaced as `new-som.internal-failure` for a brand-new family (defect
|
|
1252
|
+
# 5: the `_family_hw_revisions` cross-check below is skipped there, the
|
|
1253
|
+
# one place that would otherwise have caught it as `new-som.failed`
|
|
1254
|
+
# first) -- a caller mistake misreported as a tan bug. A MULTI-LINE
|
|
1255
|
+
# value defeated the self-check entirely (defect 3): the injected lines
|
|
1256
|
+
# became sibling YAML keys, so `default_hw_rev` itself still read back
|
|
1257
|
+
# pattern-clean. `.fullmatch`, not `.match`+`$`: see `_HW_REV_RE`'s
|
|
1258
|
+
# comment for why `$` alone is not equivalent here.
|
|
1259
|
+
if not _HW_REV_RE.fullmatch(default_hw_rev):
|
|
1260
|
+
fail(f"default hw rev '{default_hw_rev}' must match {_HW_REV_RE.pattern!r}")
|
|
1261
|
+
return
|
|
1262
|
+
# `default_board` has no schema pattern (a bare `type: string`, since a
|
|
1263
|
+
# real carrier name may contain spaces/hyphens the SKU-shaped fields
|
|
1264
|
+
# don't), so the injection above is closed by refusing control
|
|
1265
|
+
# characters -- the one thing a single-line preset value can never
|
|
1266
|
+
# legitimately need -- rather than by a charset it does not have.
|
|
1267
|
+
if _has_control_char(default_board):
|
|
1268
|
+
fail("default board must not contain newlines or other control characters")
|
|
1269
|
+
return
|
|
1270
|
+
|
|
1271
|
+
# Resolve the cross-references the checklist used to defer: the stock
|
|
1272
|
+
# carrier must exist in metadata/boards/, and the hw rev must resolve in
|
|
1273
|
+
# the family hw-revisions file when that file exists (a brand-new family
|
|
1274
|
+
# has none yet -- that stays a checklist step).
|
|
1275
|
+
board_names = _known_board_names(resolved_sdk)
|
|
1276
|
+
if board_names is not None and default_board not in board_names:
|
|
1277
|
+
fail(
|
|
1278
|
+
f"default board '{default_board}' does not match any `name:` "
|
|
1279
|
+
f"in metadata/boards/ (known: {', '.join(sorted(board_names))})"
|
|
1280
|
+
)
|
|
1281
|
+
return
|
|
1282
|
+
hw_revs = _family_hw_revisions(sku, output_root_path, resolved_sdk)
|
|
1283
|
+
if hw_revs is not None:
|
|
1284
|
+
hwrev_path, revs = hw_revs
|
|
1285
|
+
if default_hw_rev not in revs:
|
|
1286
|
+
fail(
|
|
1287
|
+
f"default hw rev '{default_hw_rev}' does not resolve in "
|
|
1288
|
+
f"{hwrev_path} (known: {', '.join(sorted(revs))})"
|
|
1289
|
+
)
|
|
1290
|
+
return
|
|
1291
|
+
|
|
1292
|
+
preset_path = output_root_path / "metadata" / "e1m_modules" / f"{sku}.yaml"
|
|
1293
|
+
vendor_slug, family_slug, part_slug = soc_ref.split(":")
|
|
1294
|
+
soc_path = (
|
|
1295
|
+
output_root_path / "metadata" / "socs" / vendor_slug / family_slug / f"{part_slug}.json"
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
if preset_path.exists() and not force:
|
|
1299
|
+
fail(f"{preset_path} already exists (pass --force to overwrite)")
|
|
1300
|
+
return
|
|
1301
|
+
|
|
1302
|
+
# -- 3. Render + self-check both skeletons in memory (still nothing on
|
|
1303
|
+
# disk): the skeletons must be schema-valid on arrival. The one
|
|
1304
|
+
# sanctioned exception is a SKU outside the schema's current pattern --
|
|
1305
|
+
# extending that pattern IS a porting step (see below).
|
|
1306
|
+
try:
|
|
1307
|
+
preset_text = _render_preset(
|
|
1308
|
+
sku,
|
|
1309
|
+
soc_ref,
|
|
1310
|
+
family,
|
|
1311
|
+
display_name,
|
|
1312
|
+
inference_backend,
|
|
1313
|
+
ethos_u_variant,
|
|
1314
|
+
cores,
|
|
1315
|
+
default_board,
|
|
1316
|
+
default_hw_rev,
|
|
1317
|
+
)
|
|
1318
|
+
except ValueError as exc:
|
|
1319
|
+
# Backstop for `_yaml_scalar`, not the primary gate: the checks above
|
|
1320
|
+
# already refuse every value that would land here. Never reached in
|
|
1321
|
+
# a correct build; kept so a future field spliced in without its own
|
|
1322
|
+
# upfront check fails loud (`new-som.failed`) instead of an unhandled
|
|
1323
|
+
# traceback with zero bytes on stdout under `--format json`.
|
|
1324
|
+
fail(f"could not render the preset skeleton: {exc}")
|
|
1325
|
+
return
|
|
1326
|
+
soc_doc = None if soc_path.exists() else _soc_skeleton(sku, soc_ref, vendor, cores)
|
|
1327
|
+
|
|
1328
|
+
som_schema_path = _som_schema_path(resolved_sdk)
|
|
1329
|
+
soc_schema_path = _soc_schema_path(resolved_sdk)
|
|
1330
|
+
try:
|
|
1331
|
+
sku_needs_pattern = re.match(_current_sku_pattern(som_schema_path), sku) is None
|
|
1332
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
1333
|
+
# tan-cli#415: `som-preset-v2.schema.json` is SDK-supplied rather than
|
|
1334
|
+
# user-supplied, but an unreadable or non-UTF-8 copy must still reach a
|
|
1335
|
+
# coded envelope -- never a bare traceback with zero bytes on stdout.
|
|
1336
|
+
# `UnicodeDecodeError` is a `ValueError`, not an `OSError`, so it has to
|
|
1337
|
+
# be named explicitly beside it.
|
|
1338
|
+
fail(f"could not read {som_schema_path} ({exc})")
|
|
1339
|
+
return
|
|
1340
|
+
import yaml # noqa: PLC0415 -- deferred, see `_yaml_scalar` (tan-cli#810)
|
|
1341
|
+
|
|
1342
|
+
try:
|
|
1343
|
+
preset_doc = yaml.safe_load(preset_text)
|
|
1344
|
+
except yaml.YAMLError as exc:
|
|
1345
|
+
# tan-cli#496 round-2 major 2: this was unguarded -- a value that
|
|
1346
|
+
# slipped past every upfront check above (the DEL byte
|
|
1347
|
+
# `_has_control_char` now rejects; a hypothetical future field
|
|
1348
|
+
# spliced into `_render_preset` without its own check) reached the
|
|
1349
|
+
# rendered document and could still make it unparseable, raising a
|
|
1350
|
+
# raw `yaml.reader.ReaderError`/`yaml.scanner.ScannerError` -- exit
|
|
1351
|
+
# 1, zero bytes on stdout under `--format json`. Backstop, not the
|
|
1352
|
+
# primary gate, same shape as the `_render_preset` `ValueError`
|
|
1353
|
+
# catch just above.
|
|
1354
|
+
fail(f"could not parse the rendered preset skeleton: {exc}")
|
|
1355
|
+
return
|
|
1356
|
+
if preset_doc is not None:
|
|
1357
|
+
try:
|
|
1358
|
+
errors = _schema_errors(preset_doc, som_schema_path)
|
|
1359
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
1360
|
+
fail(f"could not read {som_schema_path} ({exc})")
|
|
1361
|
+
return
|
|
1362
|
+
if sku_needs_pattern:
|
|
1363
|
+
errors = [e for e in errors if not e.startswith("sku:")]
|
|
1364
|
+
if errors:
|
|
1365
|
+
_internal_error("preset", "som-preset-v2", errors, json_mode)
|
|
1366
|
+
say(
|
|
1367
|
+
"Preset skeleton validates against som-preset-v2"
|
|
1368
|
+
+ (" (except the sku pattern -- see step below)" if sku_needs_pattern else "")
|
|
1369
|
+
)
|
|
1370
|
+
if soc_doc is not None:
|
|
1371
|
+
try:
|
|
1372
|
+
errors = _schema_errors(soc_doc, soc_schema_path)
|
|
1373
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
1374
|
+
fail(f"could not read {soc_schema_path} ({exc})")
|
|
1375
|
+
return
|
|
1376
|
+
if errors:
|
|
1377
|
+
_internal_error("SoC spec", "soc-spec-v1", errors, json_mode)
|
|
1378
|
+
say("SoC spec skeleton validates against soc-spec-v1")
|
|
1379
|
+
|
|
1380
|
+
# -- 4. Write (or, under --dry-run, only report) the skeletons.
|
|
1381
|
+
#
|
|
1382
|
+
# `planned` is every path this run creates or would create, and `existing`
|
|
1383
|
+
# every one it deliberately left alone -- the two lists the extension's
|
|
1384
|
+
# scaffold panel renders (tan-cli#399), built from the same values the
|
|
1385
|
+
# human lines are built from so the two can never disagree.
|
|
1386
|
+
planned: list[Path] = [preset_path] + ([soc_path] if soc_doc is not None else [])
|
|
1387
|
+
existing: list[Path] = [] if soc_doc is not None else [soc_path]
|
|
1388
|
+
written: list[Path] = []
|
|
1389
|
+
# tan-cli#496 defect 2: captured BEFORE any write touches `preset_path`,
|
|
1390
|
+
# so a `--force` re-scaffold whose SoC-spec write later fails can
|
|
1391
|
+
# restore the customer's original preset instead of deleting it (see
|
|
1392
|
+
# `_rollback_write_failure`). `None` when the preset is brand new --
|
|
1393
|
+
# there was nothing to back up, and nothing must be restored either.
|
|
1394
|
+
preset_existed_before = preset_path.exists()
|
|
1395
|
+
preset_backup: bytes | None = None
|
|
1396
|
+
# tan-cli#496 round-2 blocker 3: set the instant the write is ABOUT to
|
|
1397
|
+
# start, not after it succeeds -- `written.append(preset_path)` below
|
|
1398
|
+
# only runs once `write_text` has already returned, so gating the
|
|
1399
|
+
# rollback on `preset_path in written` alone missed a write that
|
|
1400
|
+
# started, physically created/truncated the file, and THEN raised
|
|
1401
|
+
# (ENOSPC/EDQUOT/EFBIG/EIO) partway through. See
|
|
1402
|
+
# `_rollback_write_failure`'s docstring.
|
|
1403
|
+
preset_write_attempted = False
|
|
1404
|
+
if dry_run:
|
|
1405
|
+
say(f"Would create {preset_path}")
|
|
1406
|
+
if soc_doc is None:
|
|
1407
|
+
say(f"SoC spec already present: {soc_path} (not touched)")
|
|
1408
|
+
else:
|
|
1409
|
+
say(f"Would create {soc_path}")
|
|
1410
|
+
else:
|
|
1411
|
+
try:
|
|
1412
|
+
preset_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1413
|
+
if preset_existed_before:
|
|
1414
|
+
preset_backup = preset_path.read_bytes()
|
|
1415
|
+
preset_write_attempted = True
|
|
1416
|
+
preset_path.write_text(preset_text, encoding="utf-8", newline="\n")
|
|
1417
|
+
written.append(preset_path)
|
|
1418
|
+
if soc_doc is not None:
|
|
1419
|
+
soc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1420
|
+
soc_path.write_text(
|
|
1421
|
+
json.dumps(soc_doc, indent=2) + "\n", encoding="utf-8", newline="\n"
|
|
1422
|
+
)
|
|
1423
|
+
written.append(soc_path)
|
|
1424
|
+
except OSError as exc:
|
|
1425
|
+
# Never leave a half-written scaffold behind, and never destroy
|
|
1426
|
+
# a pre-existing one either -- `_rollback_write_failure` is both
|
|
1427
|
+
# halves of tan-cli#496 (defect 2's restore-not-delete and the
|
|
1428
|
+
# finding against this file's OWN earlier fix: only a path that
|
|
1429
|
+
# PHYSICALLY EXISTS is ever reported as a cleanup failure, so a
|
|
1430
|
+
# target this run never reached disk for is never called
|
|
1431
|
+
# "may be half-written") plus round-2 blocker 3's
|
|
1432
|
+
# attempted-not-just-succeeded tracking.
|
|
1433
|
+
cleanup_failures = _rollback_write_failure(
|
|
1434
|
+
preset_path,
|
|
1435
|
+
preset_existed_before,
|
|
1436
|
+
preset_backup,
|
|
1437
|
+
preset_write_attempted,
|
|
1438
|
+
soc_path,
|
|
1439
|
+
soc_doc,
|
|
1440
|
+
)
|
|
1441
|
+
if cleanup_failures:
|
|
1442
|
+
fail(
|
|
1443
|
+
f"could not write the skeletons ({exc}); cleanup also "
|
|
1444
|
+
"failed, so this may be a half-written scaffold -- "
|
|
1445
|
+
"could not remove: " + "; ".join(cleanup_failures)
|
|
1446
|
+
)
|
|
1447
|
+
else:
|
|
1448
|
+
# "Rolled back", not "removed": a --force re-scaffold whose
|
|
1449
|
+
# preset pre-existed was RESTORED, not deleted (defect 2).
|
|
1450
|
+
fail(f"could not write the skeletons ({exc}); rolled back any partial output")
|
|
1451
|
+
return
|
|
1452
|
+
say(f"Created {preset_path}")
|
|
1453
|
+
if soc_doc is None:
|
|
1454
|
+
say(f"SoC spec already present: {soc_path} (not touched)")
|
|
1455
|
+
else:
|
|
1456
|
+
say(f"Created {soc_path}")
|
|
1457
|
+
|
|
1458
|
+
# -- 5. Numbered next-steps checklist.
|
|
1459
|
+
soc_created = soc_doc is not None
|
|
1460
|
+
steps = [
|
|
1461
|
+
f"Fill every TBD in {preset_path.name}"
|
|
1462
|
+
+ (f" and {soc_path.name}" if soc_created else "")
|
|
1463
|
+
+ " from the authoritative datasheet / schematic / BOM"
|
|
1464
|
+
" -- never invent values.",
|
|
1465
|
+
]
|
|
1466
|
+
if sku_needs_pattern:
|
|
1467
|
+
steps.append(
|
|
1468
|
+
f"Extend the `sku:` pattern in "
|
|
1469
|
+
f"metadata/schemas/som-preset-v2.schema.json to accept {sku} "
|
|
1470
|
+
f"(docs/porting-new-som.md, schema-pattern step)."
|
|
1471
|
+
)
|
|
1472
|
+
steps.append(
|
|
1473
|
+
f'Register the silicon ref: add "{soc_ref}" to '
|
|
1474
|
+
f"metadata/registries/silicon-kconfig.json and the matching "
|
|
1475
|
+
f"ALP_SOC_* stanza in zephyr/Kconfig."
|
|
1476
|
+
)
|
|
1477
|
+
if hw_revs is None:
|
|
1478
|
+
# Only when the family hw-revisions file could not be resolved at
|
|
1479
|
+
# scaffold time (brand-new family); otherwise the rev was already
|
|
1480
|
+
# validated above.
|
|
1481
|
+
steps.append(
|
|
1482
|
+
f"Ensure default_hw_rev '{default_hw_rev}' resolves in "
|
|
1483
|
+
f"metadata/e1m_modules/<family-dir>/hw-revisions.yaml "
|
|
1484
|
+
f"(add the family file for a brand-new family)."
|
|
1485
|
+
)
|
|
1486
|
+
steps += [
|
|
1487
|
+
"Validate all metadata: python scripts/validate_metadata.py",
|
|
1488
|
+
"Regenerate derived headers: python scripts/gen_soc_caps.py "
|
|
1489
|
+
"&& python scripts/gen_board_header.py",
|
|
1490
|
+
"Run the conformance suite: tests/zephyr/conformance via twister (native_sim).",
|
|
1491
|
+
"Full walkthrough: docs/porting-new-som.md",
|
|
1492
|
+
]
|
|
1493
|
+
say("")
|
|
1494
|
+
say("Next steps:")
|
|
1495
|
+
for i, step in enumerate(steps, start=1):
|
|
1496
|
+
say(f" {i}. {step}")
|
|
1497
|
+
if dry_run:
|
|
1498
|
+
say("")
|
|
1499
|
+
say("Dry run -- validated OK, nothing was written.")
|
|
1500
|
+
|
|
1501
|
+
if json_mode:
|
|
1502
|
+
# The success envelope, and the whole point of tan-cli#399: a consumer
|
|
1503
|
+
# that parsed stdout on the failure path got 1238 bytes of prose here.
|
|
1504
|
+
# Paths are the native spelling `data.sdkPath` uses elsewhere -- only
|
|
1505
|
+
# the envelope's own `sdk.root` is normalised to forward slashes.
|
|
1506
|
+
_envelope(
|
|
1507
|
+
{
|
|
1508
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
1509
|
+
"sku": sku,
|
|
1510
|
+
"socRef": soc_ref,
|
|
1511
|
+
"sdkRoot": str(resolved_sdk),
|
|
1512
|
+
"outputRoot": str(output_root_path),
|
|
1513
|
+
"dryRun": dry_run,
|
|
1514
|
+
"planned": [str(p) for p in planned],
|
|
1515
|
+
"written": [str(p) for p in written],
|
|
1516
|
+
"existing": [str(p) for p in existing],
|
|
1517
|
+
"nextSteps": steps,
|
|
1518
|
+
"report": report_lines,
|
|
1519
|
+
},
|
|
1520
|
+
issues,
|
|
1521
|
+
ExitCode.SUCCESS,
|
|
1522
|
+
)
|