tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/commands/init_cmd.py
ADDED
|
@@ -0,0 +1,1740 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan init` -- scaffold a new project. A fresh customer's FIRST command.
|
|
3
|
+
|
|
4
|
+
Composition, not logic: resolve the template/name/destination, ask
|
|
5
|
+
`tan.core.scaffold` for the planned files, diff them against disk, then preview
|
|
6
|
+
or write -- and fold whatever comes back into exactly one envelope. Mirrors
|
|
7
|
+
`crates/tan-cli/src/commands/init/` (`mod.rs` + `from_example.rs`'s shared
|
|
8
|
+
`finish` step + `response.rs`'s envelope builders).
|
|
9
|
+
|
|
10
|
+
Four properties this file exists to hold:
|
|
11
|
+
|
|
12
|
+
**Every failure is a coded issue, never a traceback.** An unknown template, an
|
|
13
|
+
unwritable destination, a destination that is a file, files already in the way,
|
|
14
|
+
a missing SDK checkout for `--from-example`, a vendored template tree that will
|
|
15
|
+
not read, a `--som` whose SoM family has no vendored tree at all
|
|
16
|
+
(`init.som-unsupported`, tan-cli#579) -- each maps to an `issues[].code` and an
|
|
17
|
+
exit code. This is the port's recurring bug class: an uncaught exception
|
|
18
|
+
replaces the envelope with a Python traceback on stderr, and the extension
|
|
19
|
+
parses stdout, so it renders NOTHING with no error visible on either side.
|
|
20
|
+
The catch-all at the bottom of `init` is the
|
|
21
|
+
backstop for the case nobody enumerated.
|
|
22
|
+
|
|
23
|
+
**`--preview` writes nothing.** Not one directory, not one file, not even the
|
|
24
|
+
`.alp/sdk-path` pin. It is checked BEFORE the overwrite guard, because a
|
|
25
|
+
read-only operation has nothing to be guarded against -- the Rust learned that
|
|
26
|
+
the hard way: the guard used to run first and reject a preview with
|
|
27
|
+
`init.would-overwrite` on a project with local edits, failing the one operation
|
|
28
|
+
that could safely have answered.
|
|
29
|
+
|
|
30
|
+
**Nothing but the envelope on stdout.** Human text goes to stderr in both
|
|
31
|
+
formats; the JSON envelope is the only thing ever written to stdout.
|
|
32
|
+
|
|
33
|
+
**The OS is not an option (I-01/I-02).** No `--os`, no `--backend`. A core's
|
|
34
|
+
runtime follows its Cortex class, so a scaffolded `board.yaml` never carries a
|
|
35
|
+
top-level `os:` key and nothing here can be asked to put one there.
|
|
36
|
+
|
|
37
|
+
NOT PORTED, and honest about it: the interactive prompts (a missing `--template`
|
|
38
|
+
/`--name`/`--destination` takes the non-interactive default -- `zephyr-app`,
|
|
39
|
+
empty, `.` -- exactly as the Rust does with no terminal attached).
|
|
40
|
+
|
|
41
|
+
**`--board-yaml` and `--cores` are real, not parse-only.** `--board-yaml`
|
|
42
|
+
(Alp Studio's "render this resolved board.yaml verbatim") swaps the planned
|
|
43
|
+
board.yaml for the caller's file byte-for-byte; a template with no board.yaml
|
|
44
|
+
to override is a hard error (`init.board-yaml-unsupported`) rather than a
|
|
45
|
+
silent no-op that drops the caller's file. `--cores` (heterogeneous
|
|
46
|
+
scaffolding) validates + splices companion cores and a default RPMsg channel
|
|
47
|
+
into the SAME board.yaml `plan_template_files` already planned -- see
|
|
48
|
+
`tan.core.scaffold.splice_companion_cores`. `--from-example` ignores `--cores`
|
|
49
|
+
(by design: the example ships its own `cores:` topology) but DOES retarget
|
|
50
|
+
`--som` onto the copied board.yaml's `som:` block (`retarget_board_yaml_som`,
|
|
51
|
+
tan-cli#890/#1029) -- it is not ignored.
|
|
52
|
+
|
|
53
|
+
**`--template`'s id space is NOT the SDK's example catalog.** `TEMPLATE_IDS`
|
|
54
|
+
(`tan.core.scaffold`) is tan's own curated, vendored subset -- six starter
|
|
55
|
+
templates, five of which map onto five entries of the broader SDK catalog
|
|
56
|
+
(`metadata/templates/catalog-v1.json`, alp-sdk) under DIFFERENT ids (its
|
|
57
|
+
`minimal`/`sensor`/`iot`/`edge-ai`/`diagnostics` are this file's
|
|
58
|
+
`zephyr-app`/`sensor-starter`/`iot-starter`/`edge-ai-starter`/
|
|
59
|
+
`board-diagnostics`). `multicore-mailbox` (tan-cli#864) is the one id spelled
|
|
60
|
+
the SAME on both sides; `minimal-app` has no catalog counterpart at all, and the
|
|
61
|
+
catalog's `peripheral`/`multicore-rpmsg`/`gateway` entries have no `--template`
|
|
62
|
+
counterpart here -- reach them (and any other SDK example) with
|
|
63
|
+
`--from-example` instead, which copies the example's own tree verbatim
|
|
64
|
+
(I-32: no vendoring needed, but an SDK checkout is). Deliberately different
|
|
65
|
+
vocabularies, not a bug to "fix" by renaming one to match the other. The
|
|
66
|
+
`--template` help STRING is the one place this port knowingly diverges from
|
|
67
|
+
the oracle's own wording (tan-cli#1): the oracle's clap help is silent on the
|
|
68
|
+
catalog split, and a customer hitting it (e.g. trying `--template peripheral`)
|
|
69
|
+
gets no pointer to `--from-example` -- so the help text here says more than
|
|
70
|
+
`crates/tan-cli/src/cli.rs`'s does, on purpose, tracked, and pinned by
|
|
71
|
+
`test_help_distinguishes_template_ids_from_the_sdk_example_catalog`. This is
|
|
72
|
+
NOT the same claim as the paragraph below: that one is about which FLAGS are
|
|
73
|
+
listed at all, not about every flag's help wording matching byte-for-byte.
|
|
74
|
+
|
|
75
|
+
**`--topology` selects a template BY hardware topology instead of by id or
|
|
76
|
+
example path (alp-sdk#1652, tan-cli#996) -- and it is NOT `--cores`.**
|
|
77
|
+
`--cores` (above) splices a companion core onto an ALREADY-CHOSEN template's
|
|
78
|
+
board.yaml; `--topology core_id:os[,core_id:os...]` chooses WHICH template (or
|
|
79
|
+
SDK example) to use in the first place, by asking the SDK catalog which
|
|
80
|
+
record's own `cores:` declaration matches exactly -- a wizard that knows its
|
|
81
|
+
hardware ("M55-HP running Zephyr, A55 running Yocto") but not the catalog's
|
|
82
|
+
naming can ask this way instead of guessing a `--template`/`--from-example`
|
|
83
|
+
value. Mutually exclusive with both `--template` and `--from-example` (all
|
|
84
|
+
three answer the same question: which project to scaffold). Needs a resolved
|
|
85
|
+
SDK checkout, like `--from-example` -- the topology lives only in the SDK's
|
|
86
|
+
live catalog, never tan's own vendored set. Exactly one match resolves like
|
|
87
|
+
`--from-example <that record's example path>` would; zero matches or more
|
|
88
|
+
than one is a hard refusal (`init.topology-not-found` /
|
|
89
|
+
`init.topology-ambiguous`) naming the topologies or candidates that ARE on
|
|
90
|
+
offer -- silently picking the first of several candidates would hide the
|
|
91
|
+
other option from a customer who never knew it existed. See
|
|
92
|
+
`tan.core.example_catalog.find_example_by_cores`'s docstring for why this is
|
|
93
|
+
a small standalone re-implementation of alp-sdk's `find_template_by_cores`
|
|
94
|
+
rather than a call into `tan.planner.template`'s hand-ported copy.
|
|
95
|
+
|
|
96
|
+
**`--all`, `--target`, `--verbose`, `--quiet`, `--no-color` are accepted and
|
|
97
|
+
genuinely IGNORED here -- matching the oracle, not merely tolerated.** Every
|
|
98
|
+
one is a member of clap's `GlobalArgs` (`global = true`), so the real `tan
|
|
99
|
+
init --quiet`/`--verbose`/... parses, exits 0, AND lists every one of them in
|
|
100
|
+
`tan init --help` -- but `crates/tan-cli/src/commands/init/` never READS any
|
|
101
|
+
of the five (grep confirms: they appear only in `GlobalArgs` test-struct
|
|
102
|
+
literals, never in `init`'s own logic -- unlike `doctor`/`clean`, `init` calls
|
|
103
|
+
neither `style::render_report` nor `Theme::from_args`). Rejecting them here
|
|
104
|
+
would NOT be more honest, it would be wrong: it would make this port refuse
|
|
105
|
+
an argv the shipped binary accepts. Declared visible (not `hidden=True`,
|
|
106
|
+
unlike `clean_cmd.clean`'s equivalent block) so `tan init --help` LISTS all
|
|
107
|
+
five, matching the oracle -- docs/cli.md tabulates all five as `tan init`
|
|
108
|
+
flags, and a hidden option renders identically to a missing one there. (Their
|
|
109
|
+
own help STRINGS are still free to be tan's plain-English wording rather than
|
|
110
|
+
clap's doc-comment text; only their presence is the contract.)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
from __future__ import annotations
|
|
114
|
+
|
|
115
|
+
import json
|
|
116
|
+
import os
|
|
117
|
+
import sys
|
|
118
|
+
from dataclasses import dataclass, field
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
import typer
|
|
122
|
+
|
|
123
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
124
|
+
from tan.core.sdk_discovery import with_sdk_search
|
|
125
|
+
from tan.core.alp_conf_pregen import pregeneration_message
|
|
126
|
+
from tan.core.system_manifest import load_yaml_document
|
|
127
|
+
from tan.core.fs_confine import PathEscapeError, resolve_confined
|
|
128
|
+
from tan.core.global_flags import accept_global_flags
|
|
129
|
+
from tan.core.example_catalog import (
|
|
130
|
+
AmbiguousCoresTopologyError,
|
|
131
|
+
CoresTopologyNotFoundError,
|
|
132
|
+
MalformedCatalogError,
|
|
133
|
+
catalog_unreadable,
|
|
134
|
+
find_example_by_cores,
|
|
135
|
+
parse_topology_arg,
|
|
136
|
+
unsupported_som,
|
|
137
|
+
)
|
|
138
|
+
from tan.core.som_buildability import hw_rev_not_buildable
|
|
139
|
+
from tan.core.sdk_discovery import (
|
|
140
|
+
global_default_foreign_project_issue,
|
|
141
|
+
resolve_sdk_root_wide,
|
|
142
|
+
sdk_ladder_divergence_issue,
|
|
143
|
+
)
|
|
144
|
+
from tan.core.scaffold import (
|
|
145
|
+
DEFAULT_SOM_SKU,
|
|
146
|
+
DEFAULT_TEMPLATE_ID,
|
|
147
|
+
TEMPLATE_SUPPORTED_SKUS,
|
|
148
|
+
TEMPLATE_IDS,
|
|
149
|
+
CoresError,
|
|
150
|
+
ExampleReadError,
|
|
151
|
+
FileChange,
|
|
152
|
+
FlowStyleSomError,
|
|
153
|
+
PlannedFile,
|
|
154
|
+
ScaffoldWriteError,
|
|
155
|
+
SomBlockUnsupportedError,
|
|
156
|
+
TemplateDataError,
|
|
157
|
+
UnreadableSomBlockError,
|
|
158
|
+
UnsupportedSomError,
|
|
159
|
+
collect_file_changes,
|
|
160
|
+
is_plain_relative,
|
|
161
|
+
parse_cores,
|
|
162
|
+
plan_template_files,
|
|
163
|
+
posix,
|
|
164
|
+
read_example_tree,
|
|
165
|
+
retarget_board_yaml_som,
|
|
166
|
+
scaffold_tree_preview,
|
|
167
|
+
sdk_pointer_json,
|
|
168
|
+
splice_companion_cores,
|
|
169
|
+
vendored_app_core_key,
|
|
170
|
+
vendored_core_ids,
|
|
171
|
+
vendored_som,
|
|
172
|
+
write_files,
|
|
173
|
+
)
|
|
174
|
+
from tan.core.board_files import retarget_example_board_files
|
|
175
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
176
|
+
from tan.exit_codes import ExitCode
|
|
177
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
178
|
+
|
|
179
|
+
#: `data.schemaVersion` for this command's payload.
|
|
180
|
+
DATA_SCHEMA_VERSION = "1"
|
|
181
|
+
|
|
182
|
+
#: `vendored_core_ids`'s `os` value is the raw `os:` child line text, quotes
|
|
183
|
+
#: and all (a vendored board.yaml spells an off companion `os: "off"`) --
|
|
184
|
+
#: stripped for the collision message so it reads `(os: off)`, not `(os:
|
|
185
|
+
#: "off")`. Mirrors Rust's `os.trim_matches('"')`.
|
|
186
|
+
DOUBLE_QUOTE = '"'
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class InitError(Exception):
|
|
190
|
+
"""A failure with its issue code and exit code already decided.
|
|
191
|
+
|
|
192
|
+
`partial` carries the files that DID land when a write failed part-way:
|
|
193
|
+
`written: []` for a project that is actually half on disk leaves a consumer
|
|
194
|
+
with no idea what to clean up or reopen.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
code: str,
|
|
200
|
+
message: str,
|
|
201
|
+
exit_code: ExitCode,
|
|
202
|
+
*,
|
|
203
|
+
partial: tuple[list[str], list[str]] = ([], []),
|
|
204
|
+
) -> None:
|
|
205
|
+
super().__init__(message)
|
|
206
|
+
self.code = code
|
|
207
|
+
self.message = message
|
|
208
|
+
self.exit_code = exit_code
|
|
209
|
+
self.partial = partial
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class _Outcome:
|
|
214
|
+
"""A completed (non-error) run: preview, overwrite-guard refusal, or write.
|
|
215
|
+
|
|
216
|
+
`destination` and `project_root` are DELIBERATELY two different fields:
|
|
217
|
+
`destination` is where the caller pointed tan (`--destination`/`--project`,
|
|
218
|
+
unchanged by `--name`) and is what the JSON envelope's `project.root` /
|
|
219
|
+
`data.destination` pin -- `project_root` is `destination` + the resolved
|
|
220
|
+
`--name` subdirectory, i.e. where files actually landed. Text mode's
|
|
221
|
+
"created" line must report `project_root`: reporting `destination` there
|
|
222
|
+
(tan-cli#4) told an operator running `--name blink-e2e` that tan had
|
|
223
|
+
created `proj` when it had actually created `proj/blink-e2e`.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
template_id: str
|
|
227
|
+
destination: str
|
|
228
|
+
project_root: str
|
|
229
|
+
preview: bool
|
|
230
|
+
file_changes: list[FileChange]
|
|
231
|
+
files: list[PlannedFile]
|
|
232
|
+
written: list[str] = field(default_factory=list)
|
|
233
|
+
unchanged: list[str] = field(default_factory=list)
|
|
234
|
+
sdk_pinned: str | None = None
|
|
235
|
+
issues: list[Issue] = field(default_factory=list)
|
|
236
|
+
exit_code: ExitCode = ExitCode.SUCCESS
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Envelope assembly
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _data(
|
|
245
|
+
*,
|
|
246
|
+
template_id: str,
|
|
247
|
+
destination: str,
|
|
248
|
+
preview: bool,
|
|
249
|
+
file_changes: list[FileChange],
|
|
250
|
+
written: list[str],
|
|
251
|
+
unchanged: list[str],
|
|
252
|
+
sdk_pinned: str | None,
|
|
253
|
+
) -> dict:
|
|
254
|
+
return {
|
|
255
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
256
|
+
"templateId": template_id,
|
|
257
|
+
"destination": destination,
|
|
258
|
+
"preview": preview,
|
|
259
|
+
"fileChanges": [
|
|
260
|
+
{"relativePath": c.relative_path, "kind": c.kind} for c in file_changes
|
|
261
|
+
],
|
|
262
|
+
"written": written,
|
|
263
|
+
"unchanged": unchanged,
|
|
264
|
+
"sdkPinned": sdk_pinned,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _stderr(line: str) -> None:
|
|
269
|
+
print(line, file=sys.stderr)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _sdk_reportable(sdk: _Sdk | None) -> bool:
|
|
273
|
+
"""Whether `sdk` is resolved AND a real checkout -- the one gate
|
|
274
|
+
`_sdk_block` used to apply internally before tan-cli#922 moved it to the
|
|
275
|
+
call site (see `_sdk_block`'s own docstring for why). Gated on the loader
|
|
276
|
+
marker, matching `build_output.resolve_project_context` ("the envelope's
|
|
277
|
+
`sdk` only records what core's own loader-marker check accepted"), and
|
|
278
|
+
NOT merely on `sdk is not None`. `_resolve_sdk_root` returns an explicit
|
|
279
|
+
`--sdk-root` unvalidated, and `_pin_sdk` then silently declines to pin a
|
|
280
|
+
path that is not a checkout -- so reporting `sdk` there would advertise a
|
|
281
|
+
checkout that is about to be pinned and is not."""
|
|
282
|
+
return sdk is not None and _is_sdk_checkout(sdk.path)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _sdk_block(sdk: _Sdk) -> SdkInfo:
|
|
286
|
+
"""The envelope's `sdk` block for a resolved, checked-out [`_Sdk`] --
|
|
287
|
+
ALWAYS one, never a bare `None` (tan-cli#922: the same shape tan-cli#900
|
|
288
|
+
fixed for `examples_cmd._resolve_sdk`/`generate_cmd._resolve_sdk_root`,
|
|
289
|
+
applied here). Callers gate the CALL itself on `_sdk_reportable(sdk)`
|
|
290
|
+
rather than this function collapsing to `None` -- moving the "should the
|
|
291
|
+
envelope carry an `sdk` key at all" decision to the caller, the same
|
|
292
|
+
place every other resolution wrapper in this repo makes it
|
|
293
|
+
(`.path is None`), rather than folding it into this function's return
|
|
294
|
+
type. `SdkInfo.root` has no representable "unresolved" state of its own
|
|
295
|
+
(it is a bare `str`, unlike `_ResolvedSdk`/`_SdkResolution`/
|
|
296
|
+
`_ResolvedSdkRoot`'s `path: X | None`), so this function could never have
|
|
297
|
+
represented "not reportable" WITHIN an `SdkInfo` the way those do --
|
|
298
|
+
`| None` on the return was the only way to spell that, and spelling it
|
|
299
|
+
there instead of at the call site is exactly the shape
|
|
300
|
+
`tests/gates/test_sdk_resolution_wrapper_is_not_optional.py` flags.
|
|
301
|
+
|
|
302
|
+
tan-cli#491 defect 5: `init` passed no `sdk=` to `Envelope(...)` on ANY
|
|
303
|
+
path, so the key was absent from all four outcomes and at both resolution
|
|
304
|
+
tiers, while the frozen v0.4.1 oracle emits `sdk:{root,sourceTier}` for the
|
|
305
|
+
identical argv (`test_oracle_parity.json`, the `init` capture:
|
|
306
|
+
`"sdk": {"root": "../rust-sdk", "sourceTier": "sdkRootFlag"}`). It was the
|
|
307
|
+
only field naming WHICH checkout a run is about to permanently pin --
|
|
308
|
+
`data.sdkPinned` is `null` on the preview, refusal and error paths.
|
|
309
|
+
|
|
310
|
+
`SdkInfo.from_resolution`, not a raw `SdkInfo(root, tier)`: `_Sdk` carries
|
|
311
|
+
`tier` and `foreign_global_default_for`, which is the shape that
|
|
312
|
+
constructor reads (`tests/gates/test_sdk_info_is_built_from_a_resolution.py`).
|
|
313
|
+
`init` still surfaces the foreign-global-default warning through its own
|
|
314
|
+
explicit `global_default_foreign_project_issue` call, computed BEFORE
|
|
315
|
+
`_pin_sdk` writes -- see that call site.
|
|
316
|
+
|
|
317
|
+
Note the ROOT reported is `_Sdk.display`, i.e. absolute, where the oracle
|
|
318
|
+
echoed `--sdk-root` verbatim. That divergence is `_Sdk`'s own, already
|
|
319
|
+
deliberate and already documented there (tan-cli#263: a relative pointer
|
|
320
|
+
persisted into `.alp/sdk-path` resolves against the wrong cwd later); this
|
|
321
|
+
field simply reports the same value `data.sdkPinned` does.
|
|
322
|
+
"""
|
|
323
|
+
return SdkInfo.from_resolution(sdk.display, sdk)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _emit_error(json_mode: bool, err: InitError, sdk: _Sdk | None = None) -> None:
|
|
327
|
+
"""The error envelope. Note the asymmetry with `_emit_outcome`, which is
|
|
328
|
+
contract, not an oversight: an error reports `project.root: null` and an
|
|
329
|
+
EMPTY `templateId`/`destination` even when the CLI had already resolved
|
|
330
|
+
both, because nothing was created and there is no project to point a
|
|
331
|
+
consumer at. `contract/envelopes/init-invalid-template` pins it.
|
|
332
|
+
|
|
333
|
+
`sdk` defaults to `None` because the two `_emit_error` call sites sit in
|
|
334
|
+
handlers that can be reached BEFORE `_resolve_sdk_root` has run at all (a
|
|
335
|
+
bad `--template`, an unreadable `--board-yaml`); there is genuinely nothing
|
|
336
|
+
to report then, and the key stays absent rather than becoming `null`.
|
|
337
|
+
"""
|
|
338
|
+
written, unchanged = err.partial
|
|
339
|
+
if json_mode:
|
|
340
|
+
emit(
|
|
341
|
+
Envelope(
|
|
342
|
+
"init",
|
|
343
|
+
Project(root=None, board_yaml=None),
|
|
344
|
+
_data(
|
|
345
|
+
template_id="",
|
|
346
|
+
destination="",
|
|
347
|
+
preview=False,
|
|
348
|
+
file_changes=[],
|
|
349
|
+
written=written,
|
|
350
|
+
unchanged=unchanged,
|
|
351
|
+
sdk_pinned=None,
|
|
352
|
+
),
|
|
353
|
+
[Issue(err.code, "error", err.message)],
|
|
354
|
+
err.exit_code,
|
|
355
|
+
sdk=_sdk_block(sdk) if _sdk_reportable(sdk) else None,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
_stderr(f"init: {err.message}")
|
|
360
|
+
raise typer.Exit(int(err.exit_code))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _emit_outcome(json_mode: bool, outcome: _Outcome, sdk: _Sdk | None = None) -> None:
|
|
364
|
+
if json_mode:
|
|
365
|
+
emit(
|
|
366
|
+
Envelope(
|
|
367
|
+
"init",
|
|
368
|
+
# The destination, not the nested project root: `project.root`
|
|
369
|
+
# is where the caller pointed tan, and the golden pins `"."`.
|
|
370
|
+
Project(root=outcome.destination, board_yaml=None),
|
|
371
|
+
_data(
|
|
372
|
+
template_id=outcome.template_id,
|
|
373
|
+
destination=outcome.destination,
|
|
374
|
+
preview=outcome.preview,
|
|
375
|
+
file_changes=outcome.file_changes,
|
|
376
|
+
written=outcome.written,
|
|
377
|
+
unchanged=outcome.unchanged,
|
|
378
|
+
sdk_pinned=outcome.sdk_pinned,
|
|
379
|
+
),
|
|
380
|
+
outcome.issues,
|
|
381
|
+
outcome.exit_code,
|
|
382
|
+
sdk=_sdk_block(sdk) if _sdk_reportable(sdk) else None,
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
elif outcome.preview:
|
|
386
|
+
_stderr(f"init: preview for template '{outcome.template_id}'")
|
|
387
|
+
for issue in outcome.issues:
|
|
388
|
+
_stderr(f"init: {issue.message}")
|
|
389
|
+
_stderr(scaffold_tree_preview(outcome.files).rstrip("\n"))
|
|
390
|
+
elif outcome.exit_code != ExitCode.SUCCESS:
|
|
391
|
+
# Refused (the overwrite guard): nothing was written, so there is no
|
|
392
|
+
# "created" line to print, only the issue(s) that explain why.
|
|
393
|
+
for issue in outcome.issues:
|
|
394
|
+
_stderr(f"init: {issue.message}")
|
|
395
|
+
else:
|
|
396
|
+
# A successful write may still carry a warning (e.g.
|
|
397
|
+
# `init.example-missing-board-yaml`) -- print it, then the "created"
|
|
398
|
+
# line: files DID land, so unlike the refusal branch above this must
|
|
399
|
+
# never suppress it.
|
|
400
|
+
for issue in outcome.issues:
|
|
401
|
+
_stderr(f"init: {issue.message}")
|
|
402
|
+
# `project_root`, not `destination`: see `_Outcome`'s docstring.
|
|
403
|
+
_stderr(f"init: created '{outcome.project_root}' from template '{outcome.template_id}'")
|
|
404
|
+
_stderr(f" written: {len(outcome.written)}, unchanged: {len(outcome.unchanged)}")
|
|
405
|
+
raise typer.Exit(int(outcome.exit_code))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Input resolution
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _resolve_template(template: str | None) -> str:
|
|
414
|
+
if template is None:
|
|
415
|
+
return DEFAULT_TEMPLATE_ID
|
|
416
|
+
if template not in TEMPLATE_IDS:
|
|
417
|
+
# Message pinned verbatim by `contract/envelopes/init-invalid-template`.
|
|
418
|
+
# Do not append the valid-template list here: the golden diffs
|
|
419
|
+
# `issues[].message` byte-for-byte, and a list that grows with the
|
|
420
|
+
# registry would break it on every new template. `tan explain` is where
|
|
421
|
+
# the catalogue is published.
|
|
422
|
+
raise InitError(
|
|
423
|
+
"init.invalid-template",
|
|
424
|
+
f"Unknown template '{template}'.",
|
|
425
|
+
ExitCode.VALIDATION_FAILURE,
|
|
426
|
+
)
|
|
427
|
+
return template
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _resolve_name(name: str | None) -> str:
|
|
431
|
+
"""`--name` names a SUBDIRECTORY under the destination; empty means scaffold
|
|
432
|
+
straight into it. Guarded because this is the input that decides WHERE files
|
|
433
|
+
land -- an unchecked `..`/absolute value put the project root (and, with
|
|
434
|
+
`--force`, an overwrite target) anywhere the process can write."""
|
|
435
|
+
if name is None:
|
|
436
|
+
return ""
|
|
437
|
+
if name == "" or is_plain_relative(name):
|
|
438
|
+
return name
|
|
439
|
+
raise InitError(
|
|
440
|
+
"init.invalid-name",
|
|
441
|
+
f"Invalid --name '{name}': must be a plain relative path (no '..', absolute, "
|
|
442
|
+
f"or drive-rooted segments).",
|
|
443
|
+
ExitCode.VALIDATION_FAILURE,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _cwd_or_dot() -> Path:
|
|
448
|
+
"""`Path.cwd()`, falling back to `"."` when the cwd has been removed
|
|
449
|
+
(tan-cli#494 defect 10).
|
|
450
|
+
|
|
451
|
+
Unguarded, `Path.cwd()`'s `FileNotFoundError` escaped to `init`'s catch-all
|
|
452
|
+
as `init.internal-failure` / exit 5 with an empty preview, where the oracle
|
|
453
|
+
exits 0 and emits the full envelope (measured: same argv, same deleted cwd).
|
|
454
|
+
`clean_cmd._cli_workspace_root` and `presets_cmd.resolve_project_paths`
|
|
455
|
+
already wrap the identical call this way, quoting the Rust
|
|
456
|
+
`current_dir().unwrap_or_else(|_| PathBuf::from("."))`; `init` -- the one
|
|
457
|
+
command that also WRITES its answer into `.alp/sdk-path` -- was the one
|
|
458
|
+
that skipped it.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
return Path.cwd()
|
|
462
|
+
except OSError:
|
|
463
|
+
return Path(".")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _join_display(dest: str, name: str) -> str:
|
|
467
|
+
"""Mirror `PathBuf::from(dest).join(name)`'s `.display()` text
|
|
468
|
+
(`from_example.rs:70-74`): literal string concatenation with the native
|
|
469
|
+
separator, never pathlib's silent drop of a leading `.` component. This is
|
|
470
|
+
for the TEXT-mode "created" line only -- `project_root` (the `Path`) still
|
|
471
|
+
does the real filesystem work; `pathlib.Path(".") / "my-app"` normalises
|
|
472
|
+
away the `.`, so `--name my-app` in `.` would otherwise print `'my-app'`
|
|
473
|
+
where the oracle prints `'./my-app'` (`'.\\my-app'` on Windows)."""
|
|
474
|
+
if not name:
|
|
475
|
+
return dest
|
|
476
|
+
if dest.endswith(("/", "\\")):
|
|
477
|
+
return f"{dest}{name}"
|
|
478
|
+
return f"{dest}{os.sep}{name}"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@dataclass(frozen=True)
|
|
482
|
+
class _Sdk:
|
|
483
|
+
"""A resolved alp-sdk checkout: the `path` to use, and the `display` string
|
|
484
|
+
to RECORD (`data.sdkPinned`, `.alp/sdk-path`) -- always the SAME absolute
|
|
485
|
+
directory, `path` as a `Path` for filesystem calls and `display` as its
|
|
486
|
+
posix-separator string for the wire/pointer file.
|
|
487
|
+
|
|
488
|
+
Both branches of [`_resolve_sdk_root`] build this from an already-absolute
|
|
489
|
+
path (an explicit `--sdk-root` is anchored against the real cwd; the
|
|
490
|
+
resolved-ladder branch was absolute already, being built from
|
|
491
|
+
`workspace_root`). tan-cli#263: an EARLIER revision recorded an explicit
|
|
492
|
+
`--sdk-root` verbatim -- `.\\alp-sdk` stayed `.\\alp-sdk` -- on the theory
|
|
493
|
+
that the oracle's `resolve_sdk_root` also returns the flag as-is and the two
|
|
494
|
+
binaries must write identical bytes into the same pointer file. That theory
|
|
495
|
+
only held for the value's IN-PROCESS use (resolved against THIS invocation's
|
|
496
|
+
cwd, in THIS run); persisted into `.alp/sdk-path`, the same relative string
|
|
497
|
+
is read back later by an unrelated invocation with a DIFFERENT cwd (a
|
|
498
|
+
`tan sdk current` run from inside the new project itself, typically), where
|
|
499
|
+
it silently resolves to the wrong directory or nowhere at all -- exactly the
|
|
500
|
+
layout the maintainer hit (`--sdk-root .\\alp-sdk` from the parent, read back
|
|
501
|
+
from the child). A persisted pointer has no cwd of its own to inherit, so it
|
|
502
|
+
must be self-contained: absolute. (A project-root-relative form was also
|
|
503
|
+
considered -- `.alp/sdk-path` lives inside the project it describes, and
|
|
504
|
+
that would survive the project being moved as a whole. Rejected here because
|
|
505
|
+
nothing on the READ side re-anchors a relative pointer against the project
|
|
506
|
+
root today -- `_pointer_target`/`_has_loader_script` hand it straight to
|
|
507
|
+
`Path()`, which is exactly the cwd-dependent behaviour this fix removes --
|
|
508
|
+
and because absolute matches what every OTHER tier already stores, with no
|
|
509
|
+
reader change required.)
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
path: Path
|
|
513
|
+
display: str
|
|
514
|
+
#: `SdkRootResolution.tier` carried through (tan-cli#491 defect 5) -- the
|
|
515
|
+
#: `sourceTier` half of the envelope's `sdk` block, which `init` was the
|
|
516
|
+
#: only wide-ladder command not to emit at all. `_resolve_sdk_root` had
|
|
517
|
+
#: the tier in hand on both branches and dropped it here, so neither
|
|
518
|
+
#: emitter could report which mechanism answered, on ANY of the four
|
|
519
|
+
#: outcome paths (preview, write, overwrite refusal, every `_emit_error`).
|
|
520
|
+
#: The explicit-flag branch is `"sdkRootFlag"` by construction; the ladder
|
|
521
|
+
#: branch carries `resolve_sdk_root_wide`'s own answer.
|
|
522
|
+
tier: str = "sdkRootFlag"
|
|
523
|
+
#: `SdkRootResolution.foreign_global_default_for` carried through
|
|
524
|
+
#: (tan-cli#464). `None` on the `--sdk-root` branch (an explicit flag is
|
|
525
|
+
#: never "foreign" -- there is nothing to fall through from) and on every
|
|
526
|
+
#: tier above `globalDefault`; set only when THIS run resolved through the
|
|
527
|
+
#: machine-global `~/.alp/sdk-default` pointer and it was last written for
|
|
528
|
+
#: a DIFFERENT project. `init` is the command this fact matters most for
|
|
529
|
+
#: (more than `tan build`'s own tan-cli#464 fix): a pin written here is
|
|
530
|
+
#: PERMANENT, so silently baking a foreign checkout's path into a brand
|
|
531
|
+
#: new project's `.alp/sdk-path` is worse than one build silently using
|
|
532
|
+
#: the wrong SDK once -- every later command in the new project inherits
|
|
533
|
+
#: it until someone notices and re-pins by hand.
|
|
534
|
+
foreign_global_default_for: str | None = None
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _resolve_sdk_root(sdk_root: str | None, workspace_root: Path) -> _Sdk | None:
|
|
538
|
+
"""`--sdk-root` when given, else `.alp/sdk-path` project pin >
|
|
539
|
+
machine-global default (`~/.alp/sdk-default`) > the WIDE positional walk.
|
|
540
|
+
No `ALP_SDK_ROOT` tier (tried and reverted -- see
|
|
541
|
+
`resolve_sdk_root_ladder`'s own docstring).
|
|
542
|
+
|
|
543
|
+
`tan.core.sdk_discovery.resolve_sdk_root_wide`, not the narrow ladder its thirteen
|
|
544
|
+
sibling commands take (tan-cli#263, measured against the oracle): where a
|
|
545
|
+
workspace holds both a child `<ws>/alp-sdk` and a competing sibling
|
|
546
|
+
`../alp-sdk`, the oracle's `init` pins the CHILD. The narrow ladder pinned
|
|
547
|
+
the sibling -- and `init` does not merely resolve an SDK, it WRITES the
|
|
548
|
+
answer to `.alp/sdk-path`, which then outranks discovery for every later
|
|
549
|
+
command in that project. Getting it wrong here binds the wrong checkout
|
|
550
|
+
permanently, so this one call site is worth a second helper.
|
|
551
|
+
|
|
552
|
+
An explicit `--sdk-root` is expanded (`~`/`~user`) then anchored against
|
|
553
|
+
the process's real cwd with `os.path.abspath` -- lexical only (matching
|
|
554
|
+
`tan.core.sdk_discovery._abs_posix`'s own reasoning), never `Path.resolve()`, so a
|
|
555
|
+
project reached through a symlink keeps the name the user typed and a
|
|
556
|
+
not-yet-existing path still resolves. `expanduser` first: `abspath` alone
|
|
557
|
+
does not expand `~`, so `--sdk-root ~/alp-sdk` would otherwise anchor to
|
|
558
|
+
`<cwd>/~/alp-sdk` -- a pre-existing gap, but persisting the wrong path into
|
|
559
|
+
`.alp/sdk-path` (rather than just misusing it for this one invocation)
|
|
560
|
+
makes it worth closing here. Anchored against the true cwd, not
|
|
561
|
+
`workspace_root`: `--project` picks which workspace `init` operates ON, it
|
|
562
|
+
does not rebase every OTHER relative CLI argument onto it, and no sibling
|
|
563
|
+
option in this command does that either."""
|
|
564
|
+
if sdk_root:
|
|
565
|
+
resolved = Path(os.path.abspath(os.path.expanduser(sdk_root)))
|
|
566
|
+
return _Sdk(resolved, posix(resolved), "sdkRootFlag")
|
|
567
|
+
# `_broken_pin` unused: `init` is what WRITES `.alp/sdk-path`, so a broken
|
|
568
|
+
# pin already sitting in the parent workspace is superseded by this run's
|
|
569
|
+
# own resolution rather than something for `init` itself to disclose --
|
|
570
|
+
# `sdk current`/`build`/`doctor` (tan-cli#263 review) are what customers
|
|
571
|
+
# see it through after the fact. `foreign_global_default_for` is NOT
|
|
572
|
+
# dropped the same way (tan-cli#464 review): unlike a broken pin, which
|
|
573
|
+
# this run's own resolution supersedes, a foreign `globalDefault` is
|
|
574
|
+
# exactly what THIS run is about to resolve through and permanently pin
|
|
575
|
+
# -- `init()` surfaces it via `global_default_foreign_project_issue`
|
|
576
|
+
# before `_pin_sdk` ever writes.
|
|
577
|
+
resolution = resolve_sdk_root_wide(None, workspace_root)
|
|
578
|
+
found = resolution.path
|
|
579
|
+
if found is None:
|
|
580
|
+
return None
|
|
581
|
+
return _Sdk(
|
|
582
|
+
found,
|
|
583
|
+
posix(found),
|
|
584
|
+
resolution.tier,
|
|
585
|
+
foreign_global_default_for=resolution.foreign_global_default_for,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _is_sdk_checkout(root: Path) -> bool:
|
|
590
|
+
"""`scripts/alp_project.py` is THE marker for an alp-sdk checkout (I-31)."""
|
|
591
|
+
return (root / "scripts" / "alp_project.py").is_file()
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _sdk_root_flag_unresolved_issue(
|
|
595
|
+
sdk_root: str | None, resolved_sdk: _Sdk | None
|
|
596
|
+
) -> Issue | None:
|
|
597
|
+
"""tan-cli#642: an explicit `--sdk-root` that does not resolve to a real
|
|
598
|
+
alp-sdk checkout used to fail SILENTLY on the template path.
|
|
599
|
+
|
|
600
|
+
`_resolve_sdk_root`'s explicit-flag branch builds a `_Sdk` from
|
|
601
|
+
`--sdk-root` UNVALIDATED -- unlike the ladder branch, it never checks
|
|
602
|
+
`_is_sdk_checkout` before returning, so a typo'd path (or tan-cli#642's
|
|
603
|
+
more realistic route: a previously-valid `--sdk-root` that `tan
|
|
604
|
+
bootstrap` has since relocated) reaches `_finish` -> `_pin_sdk`, which
|
|
605
|
+
declines to write `.alp/sdk-path` for anything that fails
|
|
606
|
+
`_is_sdk_checkout` -- correctly, that write really would pin a checkout
|
|
607
|
+
that is not one -- but until this fix said NOTHING about it: `ok:true`,
|
|
608
|
+
`issues:[]`, `sdkPinned:null`, the whole project scaffolded normally,
|
|
609
|
+
though the README promises `init` "pins the SDK checkout in
|
|
610
|
+
.alp/sdk-path". A later command then silently fell through to
|
|
611
|
+
`~/.alp/sdk-default` -- some OTHER project's last `bootstrap`, in a
|
|
612
|
+
shared or multi-project setup.
|
|
613
|
+
|
|
614
|
+
EMISSION CONDITION: `--sdk-root` was given (non-empty) AND the path it
|
|
615
|
+
resolves to has no `scripts/alp_project.py` under it. `None` when
|
|
616
|
+
`--sdk-root` was not given at all -- that is the ordinary, no-SDK-
|
|
617
|
+
requested case every other ladder tier already covers with its own (or
|
|
618
|
+
no) diagnostic, not this one's to explain -- and `None` on the
|
|
619
|
+
`--from-example` path, which already hard-refuses the identical
|
|
620
|
+
condition as `init.sdk-root-unresolved` before this could ever run (an
|
|
621
|
+
SDK checkout is NOT optional there, since the thing being copied lives
|
|
622
|
+
inside one).
|
|
623
|
+
"""
|
|
624
|
+
if not sdk_root or resolved_sdk is None or _is_sdk_checkout(resolved_sdk.path):
|
|
625
|
+
return None
|
|
626
|
+
return Issue(
|
|
627
|
+
"init.sdk-root-invalid",
|
|
628
|
+
"warning",
|
|
629
|
+
f"--sdk-root '{sdk_root}' does not resolve to an alp-sdk checkout "
|
|
630
|
+
f"(tried '{resolved_sdk.display}', no 'scripts/alp_project.py' "
|
|
631
|
+
f"found there); the project was scaffolded without pinning an SDK "
|
|
632
|
+
f"(.alp/sdk-path was not written). Re-run with a --sdk-root that "
|
|
633
|
+
f"resolves, or {NO_SDK_NEXT_STEPS}.",
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _example_som_catalog_issue(
|
|
638
|
+
resolved_sdk: _Sdk, example_src: str, som: str, subject_label: str
|
|
639
|
+
) -> Issue | None:
|
|
640
|
+
"""tan-cli#890/#1101: whether `--som som` against the SDK scaffold
|
|
641
|
+
catalog's record for `example_src` is (a) declared unsupported, (b)
|
|
642
|
+
could not be checked at all, or (c) neither -- extracted out of
|
|
643
|
+
`init()` itself (tan-cli#1101 review minor: this was inline and pushed
|
|
644
|
+
`init()` to the 6th-largest function in the tree) since the whole block
|
|
645
|
+
is exactly this seam: four already-resolved values in, one optional
|
|
646
|
+
`Issue` out, no other state.
|
|
647
|
+
|
|
648
|
+
`unsupported_som` returning `None` is ambiguous by its own documented
|
|
649
|
+
contract -- EITHER "checked, --som is fine (or there was no record to
|
|
650
|
+
check against)" OR "could not check at all". This second, narrower read
|
|
651
|
+
(`catalog_unreadable`) distinguishes the two, and returns its reason
|
|
652
|
+
ALREADY led correctly for which of its two shapes actually happened (a
|
|
653
|
+
genuinely unreadable catalog document vs. one that read fine but whose
|
|
654
|
+
matching record did not) -- see that function's own docstring, and the
|
|
655
|
+
module docstring section it points at, rather than re-deriving either
|
|
656
|
+
lead here. `ok`/`exitCode` and the scaffold itself stay exactly as they
|
|
657
|
+
are either way (refusing here would be the tightening tan-cli#1084
|
|
658
|
+
promised not to do) -- this only stops the envelope from claiming a
|
|
659
|
+
check that never ran.
|
|
660
|
+
"""
|
|
661
|
+
supported = unsupported_som(resolved_sdk.path, example_src, som)
|
|
662
|
+
if supported is not None:
|
|
663
|
+
return Issue(
|
|
664
|
+
"init.example-som-unsupported",
|
|
665
|
+
"warning",
|
|
666
|
+
f"{subject_label} declares supported.som_skus "
|
|
667
|
+
f"{list(supported)} in the SDK scaffold catalog; --som "
|
|
668
|
+
f"'{som}' is outside that set, and `alp_project.py --emit "
|
|
669
|
+
f"scaffold` refuses the same pair. The files were still "
|
|
670
|
+
f"written -- check the scaffolded board.yaml against your "
|
|
671
|
+
f"SoM's topology before building, or widen som_skus in "
|
|
672
|
+
f"the catalog if the example really does support it.",
|
|
673
|
+
)
|
|
674
|
+
check_skipped_reason = catalog_unreadable(resolved_sdk.path, example_src)
|
|
675
|
+
if check_skipped_reason is not None:
|
|
676
|
+
return Issue(
|
|
677
|
+
"init.example-som-unchecked",
|
|
678
|
+
"warning",
|
|
679
|
+
f"{subject_label}: {check_skipped_reason}, so whether --som "
|
|
680
|
+
f"'{som}' is supported could not be checked. The files were "
|
|
681
|
+
f"still written -- verify '{som}' against {subject_label}'s "
|
|
682
|
+
f"supported SoMs before building.",
|
|
683
|
+
)
|
|
684
|
+
return None
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
# ---------------------------------------------------------------------------
|
|
688
|
+
# The two planning paths
|
|
689
|
+
# ---------------------------------------------------------------------------
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _plan_from_template(
|
|
693
|
+
template: str | None, som: str | None, cores_raw: str | None
|
|
694
|
+
) -> tuple[str, list[PlannedFile]]:
|
|
695
|
+
template_id = _resolve_template(template)
|
|
696
|
+
sku = som or DEFAULT_SOM_SKU
|
|
697
|
+
# Checked BEFORE anything is planned. A template whose SDK catalog entry
|
|
698
|
+
# restricts `supported.som_skus` must refuse every other `--som` here --
|
|
699
|
+
# never render it against the wrong family tree (silent, `exitCode 0`) and
|
|
700
|
+
# never let it fall through to `init.template-unreadable` (which blames
|
|
701
|
+
# the installation for a wrong argument). See TEMPLATE_SUPPORTED_SKUS.
|
|
702
|
+
supported = TEMPLATE_SUPPORTED_SKUS.get(template_id)
|
|
703
|
+
if supported is not None and sku not in supported:
|
|
704
|
+
plural = "s" if len(supported) > 1 else ""
|
|
705
|
+
allowed = ", ".join(f"'{s}'" for s in supported)
|
|
706
|
+
raise InitError(
|
|
707
|
+
"init.invalid-som",
|
|
708
|
+
f"Template '{template_id}' supports only SoM SKU{plural} "
|
|
709
|
+
f"{allowed}; got '{sku}'.",
|
|
710
|
+
ExitCode.VALIDATION_FAILURE,
|
|
711
|
+
)
|
|
712
|
+
try:
|
|
713
|
+
files = plan_template_files(template_id, sku)
|
|
714
|
+
except UnsupportedSomError as err:
|
|
715
|
+
# tan-cli#579. A VALIDATION failure, not an internal one: the tree is
|
|
716
|
+
# not missing from the install, it was never captured -- alp-sdk's
|
|
717
|
+
# scaffold catalog declares no entry for this SoM family -- so the
|
|
718
|
+
# actionable thing is the `--som`/`--template` pair the customer typed.
|
|
719
|
+
# Distinct from `init.invalid-som` above, which says "this TEMPLATE
|
|
720
|
+
# supports one SKU"; this one says "tan has no scaffold for this SoM
|
|
721
|
+
# family at all, in any family-split template". `UnsupportedSomError`
|
|
722
|
+
# itself records why refusing beats vendoring or warning.
|
|
723
|
+
raise InitError(
|
|
724
|
+
"init.som-unsupported", str(err), ExitCode.VALIDATION_FAILURE
|
|
725
|
+
) from err
|
|
726
|
+
except TemplateDataError as err:
|
|
727
|
+
# tan's own template data is unreadable -- a broken installation (or a
|
|
728
|
+
# frozen binary built without the template `--add-data`), not a project
|
|
729
|
+
# problem, so INTERNAL_FAILURE rather than a validation code that would
|
|
730
|
+
# send the customer looking at their own board.yaml.
|
|
731
|
+
raise InitError(
|
|
732
|
+
"init.template-unreadable", str(err), ExitCode.INTERNAL_FAILURE
|
|
733
|
+
) from err
|
|
734
|
+
except SomBlockUnsupportedError as err:
|
|
735
|
+
# tan-cli#1029/#1041, defensive: every vendored tree's board.yaml is
|
|
736
|
+
# tan's OWN template data (`tan/templates/vendored/`, captured
|
|
737
|
+
# block-style, pinned by `test_scaffold_content_oracle_parity.py`),
|
|
738
|
+
# never reachable in practice -- ANY of `FlowStyleSomError`'s or
|
|
739
|
+
# `UnreadableSomBlockError`'s shapes here means the shipped template
|
|
740
|
+
# data itself is broken, not a customer argument, so this maps to
|
|
741
|
+
# the same code/exit as `TemplateDataError` above rather than the
|
|
742
|
+
# validation-failure `_plan_from_example` raises for the
|
|
743
|
+
# customer-reachable copy of these same shapes.
|
|
744
|
+
raise InitError(
|
|
745
|
+
"init.template-unreadable", str(err), ExitCode.INTERNAL_FAILURE
|
|
746
|
+
) from err
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
cores = parse_cores(cores_raw)
|
|
750
|
+
except CoresError as err:
|
|
751
|
+
raise InitError(
|
|
752
|
+
"init.invalid-cores", err.message, ExitCode.VALIDATION_FAILURE
|
|
753
|
+
) from err
|
|
754
|
+
if cores:
|
|
755
|
+
files = _apply_cores(template_id, files, cores)
|
|
756
|
+
return template_id, files
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _apply_cores(
|
|
760
|
+
template_id: str, files: list[PlannedFile], cores: list[tuple[str, str]]
|
|
761
|
+
) -> list[PlannedFile]:
|
|
762
|
+
"""Validate `--cores` against the plan's OWN board.yaml, then splice
|
|
763
|
+
companion cores (+ a default RPMsg channel) into it. Reads the app core
|
|
764
|
+
and any pre-declared companions straight off the PLANNED board.yaml
|
|
765
|
+
(`vendored_app_core_key`/`vendored_core_ids`) rather than a second,
|
|
766
|
+
independently-derived per-template lookup, so this check can never
|
|
767
|
+
disagree with what actually gets spliced -- the exact drift the Rust
|
|
768
|
+
port guards against with its `vendored_*_app_core_for_sku` family.
|
|
769
|
+
A plan with no board.yaml (or no `cores:` block) has nothing to validate
|
|
770
|
+
or splice against; every registered template plans one today."""
|
|
771
|
+
board = next((f for f in files if f.relative_path == "board.yaml"), None)
|
|
772
|
+
if board is None:
|
|
773
|
+
return files
|
|
774
|
+
app_core = vendored_app_core_key(board.content)
|
|
775
|
+
if app_core is None:
|
|
776
|
+
return files
|
|
777
|
+
|
|
778
|
+
# The app core's runtime is fixed (the scaffolded src/ + prj.conf are
|
|
779
|
+
# Zephyr); reject a contradictory --cores request instead of silently
|
|
780
|
+
# overriding it.
|
|
781
|
+
for core_id, os_value in cores:
|
|
782
|
+
if core_id == app_core and os_value != "zephyr":
|
|
783
|
+
raise InitError(
|
|
784
|
+
"init.invalid-cores",
|
|
785
|
+
f"Core '{app_core}' is this SoM's app core and runs zephyr; "
|
|
786
|
+
f"--cores requested '{os_value}'. Omit the entry or use "
|
|
787
|
+
f"{app_core}:zephyr.",
|
|
788
|
+
ExitCode.VALIDATION_FAILURE,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# A vendored scaffold may already pre-declare a companion core (e.g.
|
|
792
|
+
# edge-ai ships `a55_cluster`, os: "off") -- a --cores id colliding with
|
|
793
|
+
# one would append a SECOND, duplicate `cores:` mapping key. Reject
|
|
794
|
+
# rather than silently drop the caller's os or silently override the
|
|
795
|
+
# scaffold's. Checked BEFORE the zephyr-companion refusal below: a
|
|
796
|
+
# collision with an already-declared companion is the more specific,
|
|
797
|
+
# more actionable conflict (name the existing declaration), whatever OS
|
|
798
|
+
# the caller's entry asked for.
|
|
799
|
+
existing = dict(vendored_core_ids(board.content))
|
|
800
|
+
for core_id, _os_value in cores:
|
|
801
|
+
if core_id != app_core and core_id in existing:
|
|
802
|
+
raise InitError(
|
|
803
|
+
"init.invalid-cores",
|
|
804
|
+
f"Core '{core_id}' is already declared by the {template_id} "
|
|
805
|
+
f"scaffold (os: {existing[core_id].strip(DOUBLE_QUOTE)}); edit "
|
|
806
|
+
f"board.yaml to change its os instead of passing --cores.",
|
|
807
|
+
ExitCode.VALIDATION_FAILURE,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# tan-cli#643: a --cores entry naming a DIFFERENT core than the plan's
|
|
811
|
+
# app core, requesting `zephyr` or `baremetal`, is the app-core-swap
|
|
812
|
+
# request this mechanism cannot honor -- `splice_companion_cores` only
|
|
813
|
+
# ever adds an `os:` child to a companion, never an `app:` (that source
|
|
814
|
+
# directory does not exist for anything but the template's own app
|
|
815
|
+
# core), so the result would be a second core silently declared with no
|
|
816
|
+
# source to build there, next to a REQUESTED core-id/os pair that
|
|
817
|
+
# `board.yaml` never actually honors. Before this fix that shape reached
|
|
818
|
+
# `splice_companion_cores` unrejected: `--cores m55_he:zephyr` on a
|
|
819
|
+
# single-core `--som E1M-AEN301` request left the app pinned to the
|
|
820
|
+
# template's guessed `m55_hp`, added the requested `m55_he` app-less, and
|
|
821
|
+
# spliced an unrequested default RPMsg carve-out between the two -- all
|
|
822
|
+
# at `ok:true`/`issues:[]` (the AEN bench e2e that filed the issue: an
|
|
823
|
+
# operator asking for one core got a two-core project on the wrong one).
|
|
824
|
+
# `baremetal` reaches the identical dead end via a different door: the
|
|
825
|
+
# planner's own `_enforce_loader_rules` (tan/planner/validate.py) refuses
|
|
826
|
+
# an app-less `os: baremetal` slice just as hard as an app-less `os:
|
|
827
|
+
# zephyr` one -- `--cores <id>:baremetal` would still plan to `ok:true`
|
|
828
|
+
# here and only fail two commands later, at `tan build`, against a
|
|
829
|
+
# board.yaml the customer never edited by hand.
|
|
830
|
+
#
|
|
831
|
+
# `yocto` is NOT unconditionally safe either -- it reaches the identical
|
|
832
|
+
# dead end on a Cortex-M companion through a THIRD door: the planner's
|
|
833
|
+
# `_enforce_os_matches_core_class` (tan/planner/validate.py) rejects a
|
|
834
|
+
# Cortex-M core running Yocto exactly as hard as it rejects a Cortex-A
|
|
835
|
+
# core running Zephyr. Measured against `E1M-AEN801` (a Cortex-M
|
|
836
|
+
# `m55_he` companion, app core `m55_hp`): `--cores m55_he:yocto` planned
|
|
837
|
+
# to `ok:true`/`issues:[]` here, and `tan validate` on the result then
|
|
838
|
+
# raised `validate.schema-violation` ("its runtime is determined by the
|
|
839
|
+
# core class ... got os: 'yocto'") -- the exact #643 shape, just reached
|
|
840
|
+
# through the one os: value the original fix left unchecked. `tan init`
|
|
841
|
+
# is deliberately SDK-free (module docstring above) and cannot resolve a
|
|
842
|
+
# companion's real `topology.<core>.type`, so this reads the same `a`- vs
|
|
843
|
+
# `m`-prefix convention every SoM topology already follows
|
|
844
|
+
# (`a32_cluster`/`a55_cluster` are Cortex-A; `m33`/`m33_sm`/`m55_hp`/
|
|
845
|
+
# `m55_he` are Cortex-M -- true across all 11 `metadata/e1m_modules/
|
|
846
|
+
# *.yaml` topologies, measured, and the same heuristic
|
|
847
|
+
# `tan/planner/project_emit/hw_info.py` already relies on) to accept
|
|
848
|
+
# `yocto` only for an `a`-prefixed companion id. `off` is the one os:
|
|
849
|
+
# value `_enforce_loader_rules` accepts app-less on ANY core class, so it
|
|
850
|
+
# is unconditionally allowed regardless of prefix (and
|
|
851
|
+
# `splice_companion_cores` now quotes it `"off"` so it round-trips as the
|
|
852
|
+
# schema's string, not YAML 1.1's bareword boolean). Refuse instead of
|
|
853
|
+
# guessing which core the caller actually meant.
|
|
854
|
+
for core_id, os_value in cores:
|
|
855
|
+
if core_id == app_core or os_value == "off":
|
|
856
|
+
continue
|
|
857
|
+
if os_value == "yocto" and core_id.startswith("a"):
|
|
858
|
+
continue
|
|
859
|
+
raise InitError(
|
|
860
|
+
"init.invalid-cores",
|
|
861
|
+
f"Core '{core_id}' requests {os_value}, but this plan's app "
|
|
862
|
+
f"core is '{app_core}' -- --cores can only splice '{core_id}' "
|
|
863
|
+
f"in as an app-less companion, and the only os: values a "
|
|
864
|
+
f"companion can honor app-less are 'off' (always) or 'yocto' "
|
|
865
|
+
f"(only when '{core_id}' is a Cortex-A core, i.e. its id starts "
|
|
866
|
+
f"with 'a') -- {os_value} is not one of those. If '{core_id}' "
|
|
867
|
+
f"is meant to host the app instead of '{app_core}', pick a "
|
|
868
|
+
f"--template/--som combination whose app core is '{core_id}', "
|
|
869
|
+
f"or scaffold with --board-yaml to declare it yourself.",
|
|
870
|
+
ExitCode.VALIDATION_FAILURE,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
spliced = splice_companion_cores(board.content, cores)
|
|
874
|
+
return [
|
|
875
|
+
PlannedFile("board.yaml", spliced) if f.relative_path == "board.yaml" else f
|
|
876
|
+
for f in files
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _apply_board_yaml_override(
|
|
881
|
+
files: list[PlannedFile],
|
|
882
|
+
subject_label: str,
|
|
883
|
+
board_yaml_path: str | None,
|
|
884
|
+
*,
|
|
885
|
+
allow_add: bool = False,
|
|
886
|
+
) -> list[PlannedFile]:
|
|
887
|
+
"""Honor `--board-yaml`: swap the planned board.yaml for the caller's file
|
|
888
|
+
verbatim -- this lets Alp Studio adopt `tan init` as its project render.
|
|
889
|
+
Shared by the template and `--from-example` paths (mirrors `init/mod.rs`
|
|
890
|
+
step 5b and `init/from_example.rs`'s identical block).
|
|
891
|
+
|
|
892
|
+
`allow_add` (set only on the `--from-example` path) lets this ADD the
|
|
893
|
+
caller's file as the project's board.yaml when the example plans none --
|
|
894
|
+
the real escape hatch the `init.example-missing-board-yaml` warning names.
|
|
895
|
+
Every registered TEMPLATE plans its own board.yaml, so a plan with none on
|
|
896
|
+
the template path is still a hard error rather than a silent no-op that
|
|
897
|
+
would drop the caller's file."""
|
|
898
|
+
if board_yaml_path is None:
|
|
899
|
+
return files
|
|
900
|
+
try:
|
|
901
|
+
with open(board_yaml_path, encoding="utf-8", newline="") as handle:
|
|
902
|
+
content = handle.read()
|
|
903
|
+
except (OSError, ValueError) as err:
|
|
904
|
+
raise InitError(
|
|
905
|
+
"init.board-yaml-unreadable",
|
|
906
|
+
f"--board-yaml '{board_yaml_path}' could not be read: {err}",
|
|
907
|
+
ExitCode.VALIDATION_FAILURE,
|
|
908
|
+
) from err
|
|
909
|
+
if not any(f.relative_path == "board.yaml" for f in files):
|
|
910
|
+
if allow_add:
|
|
911
|
+
return [*files, PlannedFile("board.yaml", content)]
|
|
912
|
+
raise InitError(
|
|
913
|
+
"init.board-yaml-unsupported",
|
|
914
|
+
f"--board-yaml was given but {subject_label} has no board.yaml to override.",
|
|
915
|
+
ExitCode.VALIDATION_FAILURE,
|
|
916
|
+
)
|
|
917
|
+
return [
|
|
918
|
+
PlannedFile("board.yaml", content) if f.relative_path == "board.yaml" else f
|
|
919
|
+
for f in files
|
|
920
|
+
]
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _copied_board_cores(files: list[PlannedFile]) -> object:
|
|
924
|
+
"""The copied board.yaml's `cores:` mapping, or `None` when there is no
|
|
925
|
+
board.yaml or it does not parse -- `pregeneration_message` then names a
|
|
926
|
+
`<core-id>` placeholder instead of guessing."""
|
|
927
|
+
board = next((f.content for f in files if f.relative_path == "board.yaml"), None)
|
|
928
|
+
if board is None:
|
|
929
|
+
return None
|
|
930
|
+
try:
|
|
931
|
+
doc = load_yaml_document(board)
|
|
932
|
+
except Exception: # noqa: BLE001 -- unparseable degrades to the placeholder
|
|
933
|
+
return None
|
|
934
|
+
return doc.get("cores") if isinstance(doc, dict) else None
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def _plan_from_example(
|
|
938
|
+
src: str, som: str | None, sdk: _Sdk | None, search_note: str = ""
|
|
939
|
+
) -> tuple[str, list[PlannedFile]]:
|
|
940
|
+
"""Copy an SDK example directory verbatim. The ONE init path that needs a
|
|
941
|
+
checkout, because the thing it copies lives in one."""
|
|
942
|
+
src = src.strip()
|
|
943
|
+
if not src:
|
|
944
|
+
raise InitError(
|
|
945
|
+
"init.invalid-example",
|
|
946
|
+
"--from-example requires a non-empty example source directory.",
|
|
947
|
+
ExitCode.VALIDATION_FAILURE,
|
|
948
|
+
)
|
|
949
|
+
if not is_plain_relative(src):
|
|
950
|
+
raise InitError(
|
|
951
|
+
"init.invalid-example",
|
|
952
|
+
f"Invalid example '{src}': must be a relative path under the SDK "
|
|
953
|
+
f"examples/ directory.",
|
|
954
|
+
ExitCode.VALIDATION_FAILURE,
|
|
955
|
+
)
|
|
956
|
+
if sdk is None or not _is_sdk_checkout(sdk.path):
|
|
957
|
+
raise InitError(
|
|
958
|
+
"init.sdk-root-unresolved",
|
|
959
|
+
"alp-sdk root is unresolved. Use --sdk-root or run near an alp-sdk "
|
|
960
|
+
"checkout to copy an example."
|
|
961
|
+
+ (f" (tried '{sdk.display}')" if sdk is not None else "")
|
|
962
|
+
+ search_note,
|
|
963
|
+
ExitCode.VALIDATION_FAILURE,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
examples_root = sdk.path / "examples"
|
|
967
|
+
example_dir = examples_root / src
|
|
968
|
+
# Containment guard on the RESOLVED paths, on top of the lexical check
|
|
969
|
+
# above: it is what defeats a directory-symlink escape, which no amount of
|
|
970
|
+
# string inspection can see.
|
|
971
|
+
try:
|
|
972
|
+
contained = example_dir.resolve(strict=True).is_relative_to(
|
|
973
|
+
examples_root.resolve(strict=True)
|
|
974
|
+
)
|
|
975
|
+
except OSError:
|
|
976
|
+
contained = False
|
|
977
|
+
if not contained:
|
|
978
|
+
raise InitError(
|
|
979
|
+
"init.example-not-found",
|
|
980
|
+
f"Example '{src}' was not found under the SDK examples/ directory.",
|
|
981
|
+
ExitCode.VALIDATION_FAILURE,
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
files = read_example_tree(example_dir)
|
|
986
|
+
except ExampleReadError as err:
|
|
987
|
+
if err.not_found:
|
|
988
|
+
raise InitError(
|
|
989
|
+
"init.example-not-found",
|
|
990
|
+
f"Example '{src}' was not found under the SDK examples/ directory.",
|
|
991
|
+
ExitCode.VALIDATION_FAILURE,
|
|
992
|
+
) from err
|
|
993
|
+
raise InitError(
|
|
994
|
+
"init.example-unreadable",
|
|
995
|
+
f"Example '{src}' could not be read: {err}",
|
|
996
|
+
ExitCode.RUNTIME_FAILURE,
|
|
997
|
+
) from err
|
|
998
|
+
if not files:
|
|
999
|
+
raise InitError(
|
|
1000
|
+
"init.example-not-found",
|
|
1001
|
+
f"Example '{src}' contains no files to copy.",
|
|
1002
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
if som:
|
|
1006
|
+
# tan-cli#1351: read BEFORE board.yaml is retargeted below, which
|
|
1007
|
+
# would erase the example's own SKU. `boards/<old board>.{conf,
|
|
1008
|
+
# overlay}` never applies on another SKU's board target.
|
|
1009
|
+
files = retarget_example_board_files(files, som, sdk.path / "metadata")
|
|
1010
|
+
# Retarget the copied board.yaml onto the chosen SoM, so an example can
|
|
1011
|
+
# be scaffolded onto the customer's own module rather than the example's.
|
|
1012
|
+
try:
|
|
1013
|
+
files = [
|
|
1014
|
+
PlannedFile(f.relative_path, retarget_board_yaml_som(f.content, som))
|
|
1015
|
+
if f.relative_path == "board.yaml"
|
|
1016
|
+
else f
|
|
1017
|
+
for f in files
|
|
1018
|
+
]
|
|
1019
|
+
except FlowStyleSomError as err:
|
|
1020
|
+
# tan-cli#1029. Before this, a flow-style `som:` block
|
|
1021
|
+
# (`som: {sku: ..., hw_rev: ...}`) made `retarget_board_yaml_som`
|
|
1022
|
+
# silently return the example's board.yaml byte-for-byte
|
|
1023
|
+
# unchanged -- `--som` discarded with `issues: []` and exit 0,
|
|
1024
|
+
# indistinguishable from success. Refusing here means the files
|
|
1025
|
+
# are never written rather than written wrong.
|
|
1026
|
+
raise InitError(
|
|
1027
|
+
"init.som-flow-style-unsupported",
|
|
1028
|
+
f"Example '{src}' board.yaml: {err}",
|
|
1029
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1030
|
+
) from err
|
|
1031
|
+
except UnreadableSomBlockError as err:
|
|
1032
|
+
# tan-cli#1041 (the amendment). Same symptom as the flow-style
|
|
1033
|
+
# refusal immediately above -- `--som` silently discarded,
|
|
1034
|
+
# `issues: []`, exit 0 -- for the three shapes that are not
|
|
1035
|
+
# flow-style at all: a `*alias`, an un-overridden `<<:` merge
|
|
1036
|
+
# key (inside the `som:` block or at the document root), and a
|
|
1037
|
+
# flow mapping split across more than one physical line. A
|
|
1038
|
+
# DIFFERENT code from `init.som-flow-style-unsupported`
|
|
1039
|
+
# (rather than folding these into it) because the customer
|
|
1040
|
+
# remediation reads the same but the diagnosis does not -- a
|
|
1041
|
+
# tool consuming this code some day should not have to string-
|
|
1042
|
+
# match the message to tell "your `som:` is flow style" apart
|
|
1043
|
+
# from "your `som:` uses a YAML feature (alias/merge) this
|
|
1044
|
+
# scaffolder does not follow".
|
|
1045
|
+
raise InitError(
|
|
1046
|
+
"init.som-block-unsupported",
|
|
1047
|
+
f"Example '{src}' board.yaml: {err}",
|
|
1048
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1049
|
+
) from err
|
|
1050
|
+
except SomBlockUnsupportedError as err:
|
|
1051
|
+
# tan-cli#1060 review: `SomBlockUnsupportedError`'s own docstring
|
|
1052
|
+
# promises every call site catches THIS base, not either leaf
|
|
1053
|
+
# above, so a THIRD leaf (the next `som:` spelling this file
|
|
1054
|
+
# learns to refuse) needs no call site touched outside
|
|
1055
|
+
# `scaffold.py`. Before this trailing clause that promise was
|
|
1056
|
+
# false here specifically -- the two `except` blocks above catch
|
|
1057
|
+
# only the two leaves that exist today, so an unenumerated third
|
|
1058
|
+
# leaf would fall through uncaught and surface as
|
|
1059
|
+
# `init.internal-failure` (measured with a temporary third leaf:
|
|
1060
|
+
# exit code 5, message "init failed unexpectedly: ..."), loud but
|
|
1061
|
+
# not the customer-actionable `ExitCode.VALIDATION_FAILURE` its
|
|
1062
|
+
# two siblings give today. Same code as the generic
|
|
1063
|
+
# `UnreadableSomBlockError` case just above (the customer
|
|
1064
|
+
# remediation is the same "this `som:` block isn't one tan's
|
|
1065
|
+
# scaffolder can retarget"); a leaf specific enough to need its
|
|
1066
|
+
# own code/message earns its own `except` clause ahead of this
|
|
1067
|
+
# one, same as `UnreadableSomBlockError` did.
|
|
1068
|
+
raise InitError(
|
|
1069
|
+
"init.som-block-unsupported",
|
|
1070
|
+
f"Example '{src}' board.yaml: {err}",
|
|
1071
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1072
|
+
) from err
|
|
1073
|
+
return f"example:{src}", files
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def _plan_from_topology(
|
|
1077
|
+
topology_raw: str, som: str | None, sdk: _Sdk | None, search_note: str = ""
|
|
1078
|
+
) -> tuple[str, list[PlannedFile]]:
|
|
1079
|
+
"""Resolve `--topology` to a catalog record, then delegate to
|
|
1080
|
+
`_plan_from_example` exactly as if `--from-example <record's example>`
|
|
1081
|
+
had been given -- one resolution step ahead of the existing path, not a
|
|
1082
|
+
second file-copying implementation."""
|
|
1083
|
+
try:
|
|
1084
|
+
cores = parse_topology_arg(topology_raw)
|
|
1085
|
+
except ValueError as err:
|
|
1086
|
+
raise InitError(
|
|
1087
|
+
"init.invalid-topology", str(err), ExitCode.VALIDATION_FAILURE
|
|
1088
|
+
) from err
|
|
1089
|
+
if sdk is None or not _is_sdk_checkout(sdk.path):
|
|
1090
|
+
raise InitError(
|
|
1091
|
+
"init.sdk-root-unresolved",
|
|
1092
|
+
"alp-sdk root is unresolved. Use --sdk-root or run near an "
|
|
1093
|
+
"alp-sdk checkout to select a template by --topology."
|
|
1094
|
+
+ (f" (tried '{sdk.display}')" if sdk is not None else "")
|
|
1095
|
+
+ search_note,
|
|
1096
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1097
|
+
)
|
|
1098
|
+
try:
|
|
1099
|
+
src = find_example_by_cores(sdk.path, cores)
|
|
1100
|
+
except MalformedCatalogError as err:
|
|
1101
|
+
# tan-cli#1084: a malformed catalog escaped BOTH handlers below as a
|
|
1102
|
+
# raw traceback. Handled on its own class, not on the shared
|
|
1103
|
+
# `CoresTopologyError` base, so the envelope names the catalog
|
|
1104
|
+
# rather than implying the requested topology was the problem.
|
|
1105
|
+
raise InitError(
|
|
1106
|
+
"init.catalog-malformed", str(err), ExitCode.VALIDATION_FAILURE
|
|
1107
|
+
) from err
|
|
1108
|
+
except CoresTopologyNotFoundError as err:
|
|
1109
|
+
raise InitError(
|
|
1110
|
+
"init.topology-not-found", str(err), ExitCode.VALIDATION_FAILURE
|
|
1111
|
+
) from err
|
|
1112
|
+
except AmbiguousCoresTopologyError as err:
|
|
1113
|
+
raise InitError(
|
|
1114
|
+
"init.topology-ambiguous", str(err), ExitCode.VALIDATION_FAILURE
|
|
1115
|
+
) from err
|
|
1116
|
+
return _plan_from_example(src, som, sdk, search_note)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# ---------------------------------------------------------------------------
|
|
1120
|
+
# Diff, then preview / guard / write
|
|
1121
|
+
# ---------------------------------------------------------------------------
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def overwrite_refusal_message(changes: list[FileChange]) -> str:
|
|
1125
|
+
"""Name the files that actually collide, and offer the non-destructive
|
|
1126
|
+
option first. `--force` is the only escape the old wording named, which
|
|
1127
|
+
made overwriting look like the intended next step rather than the last
|
|
1128
|
+
resort."""
|
|
1129
|
+
colliding = [c.relative_path for c in changes if c.kind == "update"]
|
|
1130
|
+
listed = ", ".join(sorted(colliding))
|
|
1131
|
+
return (
|
|
1132
|
+
f"{len(colliding)} file(s) would be overwritten: {listed}. "
|
|
1133
|
+
"Run with --preview to see the full plan, --destination <DIR> or "
|
|
1134
|
+
"--name <NAME> to write somewhere else, or --force to overwrite."
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _finish(
|
|
1139
|
+
template_id: str,
|
|
1140
|
+
destination: str,
|
|
1141
|
+
project_root: Path,
|
|
1142
|
+
project_root_display: str,
|
|
1143
|
+
files: list[PlannedFile],
|
|
1144
|
+
*,
|
|
1145
|
+
preview: bool,
|
|
1146
|
+
force: bool,
|
|
1147
|
+
sdk: _Sdk | None,
|
|
1148
|
+
) -> _Outcome:
|
|
1149
|
+
changes = collect_file_changes(project_root, files)
|
|
1150
|
+
# tan-cli#1484: an existing `.alp/sdk-path` that names a DIFFERENT checkout
|
|
1151
|
+
# is a planned change like any other file, so the would-overwrite guard
|
|
1152
|
+
# below covers it. Re-pinning the project to another SDK silently is the
|
|
1153
|
+
# failure this prevents.
|
|
1154
|
+
pin_change = _sdk_pin_change(project_root, sdk)
|
|
1155
|
+
if pin_change is not None:
|
|
1156
|
+
changes.append(pin_change)
|
|
1157
|
+
|
|
1158
|
+
if preview:
|
|
1159
|
+
# Before the overwrite guard, deliberately: a preview touches no disk,
|
|
1160
|
+
# so there is nothing to guard, and guarding it turned a read-only
|
|
1161
|
+
# question into a failure on any project with local edits.
|
|
1162
|
+
return _Outcome(
|
|
1163
|
+
template_id=template_id,
|
|
1164
|
+
destination=destination,
|
|
1165
|
+
project_root=project_root_display,
|
|
1166
|
+
preview=True,
|
|
1167
|
+
file_changes=changes,
|
|
1168
|
+
files=files,
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
if any(c.kind == "update" for c in changes) and not force:
|
|
1172
|
+
return _Outcome(
|
|
1173
|
+
template_id=template_id,
|
|
1174
|
+
destination=destination,
|
|
1175
|
+
project_root=project_root_display,
|
|
1176
|
+
preview=False,
|
|
1177
|
+
file_changes=changes,
|
|
1178
|
+
files=files,
|
|
1179
|
+
issues=[
|
|
1180
|
+
Issue(
|
|
1181
|
+
"init.would-overwrite",
|
|
1182
|
+
"error",
|
|
1183
|
+
overwrite_refusal_message(changes),
|
|
1184
|
+
)
|
|
1185
|
+
],
|
|
1186
|
+
exit_code=ExitCode.WRITE_FAILURE,
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
# A destination that exists as a FILE would otherwise surface as an
|
|
1190
|
+
# errno-flavoured mkdir/write failure a few frames later, naming a path
|
|
1191
|
+
# inside a file. Say it plainly, and before anything is written.
|
|
1192
|
+
if project_root.exists() and not project_root.is_dir():
|
|
1193
|
+
raise InitError(
|
|
1194
|
+
"init.write-failed",
|
|
1195
|
+
f"Destination '{posix(project_root)}' exists and is not a directory.",
|
|
1196
|
+
ExitCode.WRITE_FAILURE,
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
try:
|
|
1200
|
+
result = write_files(project_root, files)
|
|
1201
|
+
except ScaffoldWriteError as err:
|
|
1202
|
+
raise InitError(
|
|
1203
|
+
"init.write-failed",
|
|
1204
|
+
f"Failed to write files: {err}",
|
|
1205
|
+
ExitCode.WRITE_FAILURE,
|
|
1206
|
+
partial=(err.partial.written, err.partial.unchanged),
|
|
1207
|
+
) from err
|
|
1208
|
+
|
|
1209
|
+
try:
|
|
1210
|
+
sdk_pinned = _pin_sdk(project_root, sdk)
|
|
1211
|
+
except (PathEscapeError, OSError, ValueError) as err:
|
|
1212
|
+
# The project's own files already landed (`result` above); `partial`
|
|
1213
|
+
# carries them so a consumer knows what to clean up rather than
|
|
1214
|
+
# reading `written: []` for a project that is really half on disk --
|
|
1215
|
+
# same reasoning as the `ScaffoldWriteError` branch just above.
|
|
1216
|
+
raise InitError(
|
|
1217
|
+
"init.write-failed",
|
|
1218
|
+
f"refusing to pin the SDK: {err}",
|
|
1219
|
+
ExitCode.WRITE_FAILURE,
|
|
1220
|
+
partial=(result.written, result.unchanged),
|
|
1221
|
+
) from err
|
|
1222
|
+
|
|
1223
|
+
return _Outcome(
|
|
1224
|
+
template_id=template_id,
|
|
1225
|
+
destination=destination,
|
|
1226
|
+
project_root=project_root_display,
|
|
1227
|
+
preview=False,
|
|
1228
|
+
file_changes=changes,
|
|
1229
|
+
files=files,
|
|
1230
|
+
written=result.written,
|
|
1231
|
+
unchanged=result.unchanged,
|
|
1232
|
+
sdk_pinned=sdk_pinned,
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def _sdk_pin_change(project_root: Path, sdk: _Sdk | None) -> FileChange | None:
|
|
1237
|
+
"""`update` for `.alp/sdk-path` when it already pins a different checkout
|
|
1238
|
+
than the one this run resolved (or is unreadable); `None` otherwise."""
|
|
1239
|
+
if sdk is None or not _is_sdk_checkout(sdk.path):
|
|
1240
|
+
return None
|
|
1241
|
+
pointer = project_root / ".alp" / "sdk-path"
|
|
1242
|
+
if not pointer.is_file():
|
|
1243
|
+
return None
|
|
1244
|
+
try:
|
|
1245
|
+
existing = json.loads(pointer.read_text(encoding="utf-8")).get("sdkPath")
|
|
1246
|
+
except (OSError, ValueError, AttributeError):
|
|
1247
|
+
existing = None
|
|
1248
|
+
if existing == sdk.display:
|
|
1249
|
+
return None
|
|
1250
|
+
return FileChange(".alp/sdk-path", "update")
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def _pin_sdk(project_root: Path, sdk: _Sdk | None) -> str | None:
|
|
1254
|
+
"""Record the resolved SDK in `<project>/.alp/sdk-path` so the new project is
|
|
1255
|
+
reproducible without a separate `tan sdk switch`.
|
|
1256
|
+
|
|
1257
|
+
`None` and "not a real checkout" are a silent skip HERE -- an optional
|
|
1258
|
+
convenience pointer is never a reason to fail a `tan init` whose real
|
|
1259
|
+
files already landed. Reached only after the write, so `--preview` can
|
|
1260
|
+
never trip it. Not silent on the WIRE any more when the caller passed an
|
|
1261
|
+
explicit `--sdk-root` that failed the checkout check: `init()`'s own
|
|
1262
|
+
`_sdk_root_flag_unresolved_issue` (tan-cli#642) reports that specific
|
|
1263
|
+
case as a coded warning before this function ever runs, since this
|
|
1264
|
+
function's job is only the write, not the diagnosis, and an unresolved
|
|
1265
|
+
default-ladder SDK (no `--sdk-root` given at all) is not a misconfig this
|
|
1266
|
+
layer has anything to say about.
|
|
1267
|
+
|
|
1268
|
+
A confinement ESCAPE is different: a pre-existing symlinked (or
|
|
1269
|
+
junctioned) `.alp`, or a symlinked parent of it, used to carry this write
|
|
1270
|
+
to wherever that link pointed while still reporting the logical
|
|
1271
|
+
`.alp/sdk-path` as pinned (tan-cli#325, the same defect `scaffold.py`'s
|
|
1272
|
+
`write_files` and `generate_cmd`'s default outputs already close). Checked
|
|
1273
|
+
with the SAME shared guard those two use (`tan.core.fs_confine`) before
|
|
1274
|
+
anything is written, and raised rather than swallowed -- `_finish` turns
|
|
1275
|
+
it into a coded `init.write-failed` refusal instead of a silent
|
|
1276
|
+
`sdkPinned: null` that would hide bytes landing outside the project. Any
|
|
1277
|
+
OTHER write failure (permission denied, a full disk) still returns `None`:
|
|
1278
|
+
it is a real filesystem problem, not an escape, and the existing
|
|
1279
|
+
silent-skip contract for those is unchanged.
|
|
1280
|
+
"""
|
|
1281
|
+
if sdk is None or not _is_sdk_checkout(sdk.path):
|
|
1282
|
+
return None
|
|
1283
|
+
sdk_path = sdk.display
|
|
1284
|
+
pointer = resolve_confined(project_root, project_root / ".alp" / "sdk-path")
|
|
1285
|
+
try:
|
|
1286
|
+
pointer.parent.mkdir(parents=True, exist_ok=True)
|
|
1287
|
+
with pointer.open("w", encoding="utf-8", newline="") as handle:
|
|
1288
|
+
handle.write(sdk_pointer_json(sdk_path))
|
|
1289
|
+
except OSError:
|
|
1290
|
+
return None
|
|
1291
|
+
return sdk_path
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
# ---------------------------------------------------------------------------
|
|
1295
|
+
# Entry point
|
|
1296
|
+
# ---------------------------------------------------------------------------
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def init(
|
|
1300
|
+
template: str = typer.Option(
|
|
1301
|
+
None,
|
|
1302
|
+
"--template",
|
|
1303
|
+
metavar="ID",
|
|
1304
|
+
help=(
|
|
1305
|
+
f"Project template: {', '.join(TEMPLATE_IDS)}. tan's own curated "
|
|
1306
|
+
f"starter set -- a different, smaller id space than the SDK's "
|
|
1307
|
+
f"example catalog; use --from-example for any other SDK example "
|
|
1308
|
+
f"(e.g. peripheral-io/gpio-button-led for a button+LED starter)."
|
|
1309
|
+
),
|
|
1310
|
+
),
|
|
1311
|
+
from_example: str = typer.Option(
|
|
1312
|
+
None,
|
|
1313
|
+
"--from-example",
|
|
1314
|
+
metavar="DIR",
|
|
1315
|
+
help="Copy an SDK example (e.g. peripheral-io/hello-world) instead of a template.",
|
|
1316
|
+
),
|
|
1317
|
+
name: str = typer.Option(
|
|
1318
|
+
None, "--name", metavar="NAME", help="Subdirectory to create under the destination."
|
|
1319
|
+
),
|
|
1320
|
+
destination: str = typer.Option(
|
|
1321
|
+
None, "--destination", metavar="PATH", help="Destination directory (defaults to '.')."
|
|
1322
|
+
),
|
|
1323
|
+
som: str = typer.Option(
|
|
1324
|
+
None,
|
|
1325
|
+
"--som",
|
|
1326
|
+
# tan-cli#720: `--sku` accepted as an alias. The value IS a SKU string
|
|
1327
|
+
# -- `tan presets` prints it under `skus=`, `board.yaml` nests it as
|
|
1328
|
+
# `som.sku`, and `tan pinmux`/`tan new-som` spell the same value
|
|
1329
|
+
# `--sku`. A customer who scaffolds with `--som` and then runs
|
|
1330
|
+
# `tan pinmux --som ...` was rejected for the flag `init` had just
|
|
1331
|
+
# taught them. Each command keeps its existing name FIRST, so help
|
|
1332
|
+
# text and every existing script are unchanged; only an alias is added.
|
|
1333
|
+
"--sku",
|
|
1334
|
+
metavar="SKU",
|
|
1335
|
+
help="SoM SKU to target in the generated board.yaml (alias: --sku).",
|
|
1336
|
+
),
|
|
1337
|
+
board_yaml: str = typer.Option(
|
|
1338
|
+
None,
|
|
1339
|
+
"--board-yaml",
|
|
1340
|
+
metavar="PATH",
|
|
1341
|
+
help="Render this file verbatim as the scaffolded board.yaml instead of the planned one.",
|
|
1342
|
+
),
|
|
1343
|
+
cores: str = typer.Option(
|
|
1344
|
+
None,
|
|
1345
|
+
"--cores",
|
|
1346
|
+
metavar="CORES",
|
|
1347
|
+
help=(
|
|
1348
|
+
"Comma-separated cores for a heterogeneous project, `id[:os]` "
|
|
1349
|
+
"(e.g. `m33_sm:zephyr,a55_cluster:yocto`). OS is inferred from "
|
|
1350
|
+
"the id when omitted, but that inference is only honored for "
|
|
1351
|
+
"the plan's app core -- any other id can only be spliced in "
|
|
1352
|
+
"app-less, as `:off` or (on a Cortex-A id) `:yocto`, so a bare "
|
|
1353
|
+
"companion id like `m55_he` infers `:zephyr` and is refused "
|
|
1354
|
+
"unless `m55_he` is the app core. SPLICES onto an already-"
|
|
1355
|
+
"chosen template/example -- to instead CHOOSE which template to "
|
|
1356
|
+
"use by its hardware topology, see --topology. Mutually "
|
|
1357
|
+
"exclusive with --topology, which has no already-chosen "
|
|
1358
|
+
"template left for this to splice onto."
|
|
1359
|
+
),
|
|
1360
|
+
),
|
|
1361
|
+
topology: str = typer.Option(
|
|
1362
|
+
None,
|
|
1363
|
+
"--topology",
|
|
1364
|
+
metavar="TOPOLOGY",
|
|
1365
|
+
help=(
|
|
1366
|
+
"core_id:os[,core_id:os...] hardware topology (e.g. "
|
|
1367
|
+
"'m55_hp:zephyr,m55_he:zephyr'); an alternative to --template/"
|
|
1368
|
+
"--from-example that SELECTS whichever SDK catalog template's "
|
|
1369
|
+
"own cores: topology matches exactly, instead of naming a "
|
|
1370
|
+
"template id or example path directly. Not --cores, which "
|
|
1371
|
+
"splices a companion core onto an ALREADY-CHOSEN template "
|
|
1372
|
+
"instead of choosing one. Mutually exclusive with --template, "
|
|
1373
|
+
"--from-example, AND --cores (there is no already-chosen "
|
|
1374
|
+
"template left for --cores to splice onto -- add the extra "
|
|
1375
|
+
"core to --topology itself instead); needs a resolved SDK "
|
|
1376
|
+
"checkout."
|
|
1377
|
+
),
|
|
1378
|
+
),
|
|
1379
|
+
preview: bool = typer.Option(
|
|
1380
|
+
False, "--preview", help="Show the planned files and write nothing."
|
|
1381
|
+
),
|
|
1382
|
+
force: bool = typer.Option(
|
|
1383
|
+
False, "--force", help="Allow overwriting files that already exist."
|
|
1384
|
+
),
|
|
1385
|
+
project: str = typer.Option(
|
|
1386
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1387
|
+
),
|
|
1388
|
+
sdk_root: str = typer.Option(
|
|
1389
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1390
|
+
),
|
|
1391
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
1392
|
+
verbose: bool = typer.Option(
|
|
1393
|
+
False, "--verbose", help="Emit additional diagnostic detail."
|
|
1394
|
+
),
|
|
1395
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-essential output."),
|
|
1396
|
+
no_color: bool = typer.Option(
|
|
1397
|
+
False, "--no-color", help="Disable ANSI color in text output."
|
|
1398
|
+
),
|
|
1399
|
+
target: str = typer.Option(
|
|
1400
|
+
None,
|
|
1401
|
+
"--target",
|
|
1402
|
+
metavar="EMIT",
|
|
1403
|
+
help="Generation target (e.g. zephyr-conf, dts-overlay, cmake-args, yocto-conf).",
|
|
1404
|
+
),
|
|
1405
|
+
all_targets: bool = typer.Option(
|
|
1406
|
+
False, "--all", help="Run command against all relevant targets."
|
|
1407
|
+
),
|
|
1408
|
+
) -> None:
|
|
1409
|
+
"""Scaffold a new project from a template or an SDK example."""
|
|
1410
|
+
# The five options above are `clap`'s `GlobalArgs` members that
|
|
1411
|
+
# `crates/tan-cli/src/commands/init/` accepts and never reads (see the
|
|
1412
|
+
# module docstring) -- declared here (NOT `hidden=True`, unlike
|
|
1413
|
+
# `clean_cmd.clean`'s equivalent block) because the real `tan init --help`
|
|
1414
|
+
# DOES list all five: they are `global = true` in clap, so every
|
|
1415
|
+
# subcommand's help renders them whether or not that subcommand's own
|
|
1416
|
+
# code reads them. Hiding them here would parse identically but make
|
|
1417
|
+
# `tan init --help`'s TEXT diverge from the oracle's.
|
|
1418
|
+
del verbose, quiet, no_color, target, all_targets
|
|
1419
|
+
json_mode = output_format == "json"
|
|
1420
|
+
|
|
1421
|
+
# Bound BEFORE the `try` (tan-cli#491 defect 5): both `except` arms emit an
|
|
1422
|
+
# envelope, and an `InitError` raised before `_resolve_sdk_root` runs (a bad
|
|
1423
|
+
# `--template`, an unreadable `--board-yaml`) must not turn the `sdk` block
|
|
1424
|
+
# into an `UnboundLocalError` inside the very handler whose job is to keep a
|
|
1425
|
+
# traceback off stdout.
|
|
1426
|
+
resolved_sdk: _Sdk | None = None
|
|
1427
|
+
try:
|
|
1428
|
+
# `--destination`, then the global `--project`, then `.`. `--name` does
|
|
1429
|
+
# NOT answer this: it names a subdirectory INSIDE the destination, so
|
|
1430
|
+
# "which directory does my-app go in" is still a real question with
|
|
1431
|
+
# `--name` set.
|
|
1432
|
+
dest = destination if destination else (project if project else ".")
|
|
1433
|
+
resolved_name = _resolve_name(name)
|
|
1434
|
+
# `project_root` (the `Path`) does the real filesystem work, so it goes
|
|
1435
|
+
# through pathlib; `project_root_display` is the TEXT-mode "created"
|
|
1436
|
+
# line's own literal string join (`_join_display`), because pathlib's
|
|
1437
|
+
# `Path(dest) / name` silently drops a leading `.` from `dest` that the
|
|
1438
|
+
# oracle's `PathBuf::join` keeps (`tan-cli` review, scaffold-cx#5). The
|
|
1439
|
+
# destination itself is echoed back verbatim (unjoined) in the envelope.
|
|
1440
|
+
project_root = Path(dest) / resolved_name if resolved_name else Path(dest)
|
|
1441
|
+
project_root_display = _join_display(dest, resolved_name)
|
|
1442
|
+
|
|
1443
|
+
workspace_root = Path(os.path.abspath(project)) if project else _cwd_or_dot()
|
|
1444
|
+
resolved_sdk = _resolve_sdk_root(sdk_root, workspace_root)
|
|
1445
|
+
# tan-cli#407, and this is the command where it matters most: `init`
|
|
1446
|
+
# does not merely resolve an SDK, it WRITES the answer to
|
|
1447
|
+
# `.alp/sdk-path` (`_record_sdk_pin`, below), and that pointer then
|
|
1448
|
+
# outranks BOTH discovery tiers for every later command in the new
|
|
1449
|
+
# project. So the collision is settled here, permanently, in the user's
|
|
1450
|
+
# favour or against it -- and the wide ladder settles it on the CHILD
|
|
1451
|
+
# `<ws>/alp-sdk` while thirteen commands would have taken the lateral
|
|
1452
|
+
# `../alp-sdk`. Disclosing which one got bound is the whole point:
|
|
1453
|
+
# after this run the divergence is genuinely GONE (both ladders read
|
|
1454
|
+
# the pin), so this is the last moment the fact is observable at all.
|
|
1455
|
+
# Computed BEFORE `_finish` for exactly that reason.
|
|
1456
|
+
divergence_issue = sdk_ladder_divergence_issue(sdk_root, workspace_root, wide=True)
|
|
1457
|
+
# tan-cli#464 review: `init` is the command a foreign `globalDefault`
|
|
1458
|
+
# hurts most of all -- `_pin_sdk` below is about to bake whatever
|
|
1459
|
+
# `resolved_sdk` names into `<project>/.alp/sdk-path` PERMANENTLY, so
|
|
1460
|
+
# a caller resolving a checkout a DIFFERENT project's bootstrap
|
|
1461
|
+
# relocation actually wrote must be told before that pin lands, not
|
|
1462
|
+
# after. Computed here, alongside `divergence_issue`, for the same
|
|
1463
|
+
# reason: this is the last moment the fact is cheap to disclose --
|
|
1464
|
+
# once pinned, every later command in the new project reads the pin
|
|
1465
|
+
# tier instead and this warning never fires again.
|
|
1466
|
+
foreign_issue = global_default_foreign_project_issue(
|
|
1467
|
+
resolved_sdk.foreign_global_default_for if resolved_sdk is not None else None
|
|
1468
|
+
)
|
|
1469
|
+
# tan-cli#642: computed here, alongside the other two SDK-resolution
|
|
1470
|
+
# warnings above, for the same reason -- this is the last moment the
|
|
1471
|
+
# fact is cheap to disclose, before `_finish` -> `_pin_sdk` silently
|
|
1472
|
+
# declines to write `.alp/sdk-path` for it.
|
|
1473
|
+
sdk_root_invalid_issue = _sdk_root_flag_unresolved_issue(sdk_root, resolved_sdk)
|
|
1474
|
+
|
|
1475
|
+
# `--topology`, `--template`, `--from-example` all answer the same
|
|
1476
|
+
# question (which project to scaffold) -- refuse rather than
|
|
1477
|
+
# silently prefer one, the same posture `find_example_by_cores`
|
|
1478
|
+
# itself takes for an ambiguous topology match (tan-cli#996).
|
|
1479
|
+
_chosen = [n for n, v in (
|
|
1480
|
+
("--topology", topology), ("--template", template),
|
|
1481
|
+
("--from-example", from_example),
|
|
1482
|
+
) if v is not None]
|
|
1483
|
+
if len(_chosen) > 1:
|
|
1484
|
+
raise InitError(
|
|
1485
|
+
"init.scaffold-input-conflict",
|
|
1486
|
+
f"tan init takes at most one of --template, --from-example, "
|
|
1487
|
+
f"--topology; got {', '.join(_chosen)}.",
|
|
1488
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
# tan-cli#1001 review: `--topology` ALREADY selects the template by
|
|
1492
|
+
# its full hardware topology -- `--cores` combined with it has
|
|
1493
|
+
# nothing left to splice onto, since there is no "already-chosen
|
|
1494
|
+
# template" yet for `--cores` to attach a companion core to (unlike
|
|
1495
|
+
# the `--template`/default path below, where `_plan_from_template`
|
|
1496
|
+
# genuinely reads `cores`). Before this check, `--cores` on the
|
|
1497
|
+
# `--topology` path was silently discarded: `ok: true`, exit 0,
|
|
1498
|
+
# `issues: []`, and no trace of the requested core in the written
|
|
1499
|
+
# `board.yaml` -- measured. Refuse instead, the same posture the
|
|
1500
|
+
# `_chosen` conflict above takes for two scaffold-source flags.
|
|
1501
|
+
# `--from-example` + `--cores` is a DIFFERENT, pre-existing case
|
|
1502
|
+
# (the example ships its own board.yaml, so `--cores` is silently
|
|
1503
|
+
# ignored there too, documented at `_plan_from_example`'s call site
|
|
1504
|
+
# below) -- not touched here; only the `--topology` combination is
|
|
1505
|
+
# new in this PR and had no such note.
|
|
1506
|
+
# tan-cli#1484: `--board-yaml` replaces the planned board.yaml
|
|
1507
|
+
# verbatim AFTER `--cores` was spliced into it, so the core vanished
|
|
1508
|
+
# with `ok: true`. Same silent-discard shape, same refusal.
|
|
1509
|
+
if isinstance(board_yaml, str) and isinstance(cores, str):
|
|
1510
|
+
raise InitError(
|
|
1511
|
+
"init.scaffold-input-conflict",
|
|
1512
|
+
"tan init --board-yaml supplies the whole board.yaml, so --cores "
|
|
1513
|
+
"would be silently discarded. Put the core in your board.yaml, "
|
|
1514
|
+
"or drop --board-yaml.",
|
|
1515
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
if topology is not None and cores is not None:
|
|
1519
|
+
raise InitError(
|
|
1520
|
+
"init.scaffold-input-conflict",
|
|
1521
|
+
"tan init --topology already selects the template by its "
|
|
1522
|
+
"full hardware topology; --cores has nothing left to splice "
|
|
1523
|
+
"onto and would be silently ignored. Add the extra core to "
|
|
1524
|
+
"--topology instead (e.g. --topology "
|
|
1525
|
+
"m55_hp:zephyr,a32_cluster:yocto), or drop --topology and "
|
|
1526
|
+
"use --template/--from-example with --cores.",
|
|
1527
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
# tan-cli#1463: where the ladder looked, appended to the unresolved
|
|
1531
|
+
# refusal. An explicit `--sdk-root` is terminal (I-31), so it adds none.
|
|
1532
|
+
# Computed only when a refusal needs it: `Path.absolute()` raises if the
|
|
1533
|
+
# cwd has been removed, which a preview must still survive.
|
|
1534
|
+
search_note = ""
|
|
1535
|
+
if (topology is not None or from_example is not None) and (
|
|
1536
|
+
resolved_sdk is None or not _is_sdk_checkout(resolved_sdk.path)
|
|
1537
|
+
):
|
|
1538
|
+
search_note = with_sdk_search("", workspace_root, sdk_root)
|
|
1539
|
+
if topology is not None:
|
|
1540
|
+
template_id, files = _plan_from_topology(topology, som, resolved_sdk, search_note)
|
|
1541
|
+
# `find_example_by_cores` resolved to an example, so this is the
|
|
1542
|
+
# same shape `_plan_from_example` returns directly below.
|
|
1543
|
+
subject_label = f"example '{template_id[len('example:') :]}'"
|
|
1544
|
+
elif from_example is not None:
|
|
1545
|
+
template_id, files = _plan_from_example(from_example, som, resolved_sdk, search_note)
|
|
1546
|
+
# `--cores` is ignored on this path (the example ships its own
|
|
1547
|
+
# board.yaml); `--board-yaml`'s subject names the example, not a
|
|
1548
|
+
# template id -- `template_id` here is `"example:<src>"`.
|
|
1549
|
+
subject_label = f"example '{template_id[len('example:') :]}'"
|
|
1550
|
+
else:
|
|
1551
|
+
template_id, files = _plan_from_template(template, som, cores)
|
|
1552
|
+
subject_label = f"template '{template_id}'"
|
|
1553
|
+
|
|
1554
|
+
# `--topology` resolves to an example the same way `--from-example`
|
|
1555
|
+
# does (`template_id` starts "example:" either way), so both share
|
|
1556
|
+
# the same `allow_add`/SoM-support/missing-board.yaml treatment below
|
|
1557
|
+
# -- a topology-resolved example is not a template's own plan and can
|
|
1558
|
+
# equally have no board.yaml or a supported.som_skus mismatch.
|
|
1559
|
+
is_example_shaped = template_id.startswith("example:")
|
|
1560
|
+
|
|
1561
|
+
files = _apply_board_yaml_override(
|
|
1562
|
+
files, subject_label, board_yaml, allow_add=is_example_shaped
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
# `tan build` discovers a project's board.yaml at its root; an example
|
|
1566
|
+
# tree with none would otherwise succeed here and only fail two
|
|
1567
|
+
# commands later at `tan build` ("no board.yaml found"). WARN at
|
|
1568
|
+
# scaffold time instead of refusing the copy outright: 57 of 66
|
|
1569
|
+
# examples/aen/* (raw west/twister examples, not meant for the `tan
|
|
1570
|
+
# build` flow) have no board.yaml, and refusing `tan init
|
|
1571
|
+
# --from-example` for nearly the whole AEN family is worse than
|
|
1572
|
+
# scaffolding something that still needs one. `--board-yaml` (above,
|
|
1573
|
+
# `allow_add=True` on this path) is the real fix, so it never fires
|
|
1574
|
+
# when one was given. Every registered TEMPLATE plans its own
|
|
1575
|
+
# board.yaml (tan.core.scaffold), so this can only trip on
|
|
1576
|
+
# --from-example.
|
|
1577
|
+
# tan-cli#890: this path retargets `--som` onto the copied board.yaml
|
|
1578
|
+
# without asking the catalog whether the example supports that SKU.
|
|
1579
|
+
# WARNS rather than refuses, for the same reason as the board.yaml
|
|
1580
|
+
# case just below; `tan/core/example_catalog.py` carries the full
|
|
1581
|
+
# reasoning and the "cannot tell means silent" rule.
|
|
1582
|
+
example_som_issue = None
|
|
1583
|
+
if is_example_shaped and som is not None and resolved_sdk is not None:
|
|
1584
|
+
example_src = template_id[len("example:") :]
|
|
1585
|
+
example_som_issue = _example_som_catalog_issue(
|
|
1586
|
+
resolved_sdk, example_src, som, subject_label
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
missing_board_yaml_issue = None
|
|
1590
|
+
if is_example_shaped and not any(f.relative_path == "board.yaml" for f in files):
|
|
1591
|
+
missing_board_yaml_issue = Issue(
|
|
1592
|
+
"init.example-missing-board-yaml",
|
|
1593
|
+
"warning",
|
|
1594
|
+
f"{subject_label} has no board.yaml, so `tan build` will not "
|
|
1595
|
+
f"find a board to build here; pass --board-yaml to add one.",
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
# alp-sdk#866: the copied example may read a pre-generated
|
|
1599
|
+
# `generated/alp.conf` its own prose cannot produce outside alp-sdk.
|
|
1600
|
+
# `--from-example` copies verbatim, so say which command does -- see
|
|
1601
|
+
# `tan.core.alp_conf_pregen`.
|
|
1602
|
+
pregen_issue = None
|
|
1603
|
+
if is_example_shaped and resolved_sdk is not None:
|
|
1604
|
+
pregen_text = pregeneration_message(
|
|
1605
|
+
files, _copied_board_cores(files), resolved_sdk.display, subject_label
|
|
1606
|
+
)
|
|
1607
|
+
if pregen_text is not None:
|
|
1608
|
+
pregen_issue = Issue("init.alp-conf-pregeneration", "info", pregen_text)
|
|
1609
|
+
|
|
1610
|
+
# tan-cli#743: a scaffolded board.yaml with no explicit `hw_rev:`
|
|
1611
|
+
# resolves, at validate/build time, to its SoM preset's own
|
|
1612
|
+
# `default_hw_rev:` -- so a SoM whose default revision the SDK
|
|
1613
|
+
# itself marks not-buildable (`status: reserved`/`tbd`/absent)
|
|
1614
|
+
# produces a project whose FIRST `tan validate` hard-errors, with
|
|
1615
|
+
# `init` never having said a word. `--board-yaml` renders customer
|
|
1616
|
+
# content verbatim, whose effective SKU/hw_rev this command does not
|
|
1617
|
+
# parse, so that path is left alone.
|
|
1618
|
+
#
|
|
1619
|
+
# tan-cli#1008 review majors 1+2: the SKU/hw_rev this check judges
|
|
1620
|
+
# are read off the PLANNED board.yaml itself (`vendored_som`), not
|
|
1621
|
+
# `--som`/`DEFAULT_SOM_SKU` -- `--som` alone missed a not-buildable
|
|
1622
|
+
# SKU a bare `--from-example`/`--topology` already writes to disk
|
|
1623
|
+
# (`--som` is `None` there), and `retarget_board_yaml_som` only drops
|
|
1624
|
+
# an example's explicit `hw_rev:` on a CROSS-family retarget (review
|
|
1625
|
+
# round 4) -- an unretargeted example's own value, or one surviving
|
|
1626
|
+
# a same-family retarget, must be read rather than assumed absent.
|
|
1627
|
+
# This also obsoletes the old `is_example_shaped` gate on
|
|
1628
|
+
# `--som`-required-to-know-the-SKU: the file always carries a `sku:`
|
|
1629
|
+
# once one has been planned, on every path (template, example,
|
|
1630
|
+
# topology) alike.
|
|
1631
|
+
hw_rev_issue = None
|
|
1632
|
+
if board_yaml is None and resolved_sdk is not None:
|
|
1633
|
+
board_file = next((f for f in files if f.relative_path == "board.yaml"), None)
|
|
1634
|
+
if board_file is not None:
|
|
1635
|
+
try:
|
|
1636
|
+
file_sku, file_hw_rev = vendored_som(board_file.content)
|
|
1637
|
+
except SomBlockUnsupportedError:
|
|
1638
|
+
# tan-cli#1029/#1041. Only reachable here with no
|
|
1639
|
+
# `--som` at all (a `--som` retarget onto any of these
|
|
1640
|
+
# shapes already refused earlier, in
|
|
1641
|
+
# `_plan_from_example`) -- a `som:` block this advisory
|
|
1642
|
+
# check cannot read (flow style, an alias, an
|
|
1643
|
+
# un-overridden merge key -- see `SomBlockUnsupportedError`'s
|
|
1644
|
+
# two leaves), so it degrades the same way an unparsed
|
|
1645
|
+
# `--board-yaml` already does above: this specific pair
|
|
1646
|
+
# is simply not judged, not a hard failure.
|
|
1647
|
+
file_sku, file_hw_rev = None, None
|
|
1648
|
+
else:
|
|
1649
|
+
file_sku, file_hw_rev = None, None
|
|
1650
|
+
effective_sku = file_sku if file_sku is not None else (
|
|
1651
|
+
som if is_example_shaped else (som or DEFAULT_SOM_SKU)
|
|
1652
|
+
)
|
|
1653
|
+
if effective_sku is not None:
|
|
1654
|
+
not_buildable = hw_rev_not_buildable(
|
|
1655
|
+
resolved_sdk.path, effective_sku, file_hw_rev
|
|
1656
|
+
)
|
|
1657
|
+
if not_buildable is not None:
|
|
1658
|
+
status_repr = (
|
|
1659
|
+
f"status: {not_buildable.status!r}"
|
|
1660
|
+
if not_buildable.status is not None
|
|
1661
|
+
else "carries no `status:` key"
|
|
1662
|
+
)
|
|
1663
|
+
if file_hw_rev is not None:
|
|
1664
|
+
resolution_clause = (
|
|
1665
|
+
f"the scaffolded board.yaml explicitly sets `hw_rev: "
|
|
1666
|
+
f"{file_hw_rev}`"
|
|
1667
|
+
)
|
|
1668
|
+
promotion_clause = "this hardware revision is promoted"
|
|
1669
|
+
else:
|
|
1670
|
+
resolution_clause = (
|
|
1671
|
+
"the scaffolded board.yaml sets no explicit `hw_rev:` "
|
|
1672
|
+
"and so resolves to this SoM's default"
|
|
1673
|
+
)
|
|
1674
|
+
promotion_clause = "its default hardware revision is promoted"
|
|
1675
|
+
alternative_clause = (
|
|
1676
|
+
", or until board.yaml names a buildable `hw_rev:` explicitly"
|
|
1677
|
+
if not_buildable.has_buildable_alternative
|
|
1678
|
+
else ""
|
|
1679
|
+
)
|
|
1680
|
+
hw_rev_issue = Issue(
|
|
1681
|
+
"init.hw-rev-not-buildable",
|
|
1682
|
+
"warning",
|
|
1683
|
+
f"SoM {not_buildable.sku} hw_rev {not_buildable.hw_rev!r} "
|
|
1684
|
+
f"exists but is not buildable ({status_repr}) -- the check "
|
|
1685
|
+
f"`tan validate` will refuse next, since {resolution_clause}. "
|
|
1686
|
+
f"The files were still written -- `tan build` will not work "
|
|
1687
|
+
f"against this SoM until {promotion_clause}{alternative_clause}.",
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
outcome = _finish(
|
|
1691
|
+
template_id,
|
|
1692
|
+
dest,
|
|
1693
|
+
project_root,
|
|
1694
|
+
project_root_display,
|
|
1695
|
+
files,
|
|
1696
|
+
preview=preview,
|
|
1697
|
+
force=force,
|
|
1698
|
+
sdk=resolved_sdk,
|
|
1699
|
+
)
|
|
1700
|
+
if example_som_issue is not None:
|
|
1701
|
+
outcome.issues.append(example_som_issue)
|
|
1702
|
+
if missing_board_yaml_issue is not None:
|
|
1703
|
+
outcome.issues.append(missing_board_yaml_issue)
|
|
1704
|
+
if hw_rev_issue is not None:
|
|
1705
|
+
outcome.issues.append(hw_rev_issue)
|
|
1706
|
+
if divergence_issue is not None:
|
|
1707
|
+
outcome.issues.append(divergence_issue)
|
|
1708
|
+
if foreign_issue is not None:
|
|
1709
|
+
outcome.issues.append(foreign_issue)
|
|
1710
|
+
if sdk_root_invalid_issue is not None:
|
|
1711
|
+
outcome.issues.append(sdk_root_invalid_issue)
|
|
1712
|
+
if pregen_issue is not None:
|
|
1713
|
+
outcome.issues.append(pregen_issue)
|
|
1714
|
+
except InitError as err:
|
|
1715
|
+
_emit_error(json_mode, err, resolved_sdk)
|
|
1716
|
+
return
|
|
1717
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
1718
|
+
# Nothing gets to replace the envelope with a traceback. `typer.Exit`
|
|
1719
|
+
# cannot reach here: it is only ever raised from the emit helpers, which
|
|
1720
|
+
# run outside this try (or from within the handler above, which
|
|
1721
|
+
# propagates).
|
|
1722
|
+
_emit_error(
|
|
1723
|
+
json_mode,
|
|
1724
|
+
InitError(
|
|
1725
|
+
"init.internal-failure",
|
|
1726
|
+
f"init failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1727
|
+
ExitCode.INTERNAL_FAILURE,
|
|
1728
|
+
),
|
|
1729
|
+
resolved_sdk,
|
|
1730
|
+
)
|
|
1731
|
+
return
|
|
1732
|
+
|
|
1733
|
+
_emit_outcome(json_mode, outcome, resolved_sdk)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
# tan-cli#261: adds the two oracle `GlobalArgs` flags this command was still
|
|
1737
|
+
# missing (`--ci`/`--non-interactive`) on top of the five already declared
|
|
1738
|
+
# above (`--verbose`/`--quiet`/`--no-color`/`--target`/`--all`, all read for
|
|
1739
|
+
# real); see `tan.core.global_flags`.
|
|
1740
|
+
init = accept_global_flags(init)
|