tan-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan scaffold` -- scaffold one module (a source/header pair + README) into
|
|
3
|
+
an EXISTING tan project. Distinct from `tan init`, which scaffolds a whole new
|
|
4
|
+
project: this command never WRITES `board.yaml`, never resolves an SDK, and
|
|
5
|
+
its template id space (`tan.core.module_template.MODULE_TEMPLATE_IDS`) is a
|
|
6
|
+
different, smaller registry than `tan init`'s own six.
|
|
7
|
+
|
|
8
|
+
It does now READ one (tan-cli#1031). Until then it never looked, so
|
|
9
|
+
`project.boardYaml` was hardcoded `null` and every generated module carried
|
|
10
|
+
`// Board context: unavailable` -- including a run that passed
|
|
11
|
+
`--board-yaml <p>/board.yaml`, whose own help text promises it "overrides
|
|
12
|
+
project resolution". That mattered beyond the comment: alp-sdk-vscode had just
|
|
13
|
+
DELETED its own TypeScript copy of this generator (alp-sdk-vscode#601/#633,
|
|
14
|
+
because a second copy is what let that copy's README lose its `## Wiring`
|
|
15
|
+
section and ship modules that never compiled), and that copy DID read
|
|
16
|
+
`board.yaml`. So the capability comes from here or from nowhere. The read goes
|
|
17
|
+
through `tan.core.board_context` -- `tan validate`'s own resolver, moved to a
|
|
18
|
+
shared home rather than cloned into a seventh one.
|
|
19
|
+
|
|
20
|
+
Composition, not logic: resolve the module name + template + destination
|
|
21
|
+
(`--name`/`--template`/`--destination`, and `tan.core.module_template`'s
|
|
22
|
+
registry) and the project's `board.yaml`, ask the registry for the planned
|
|
23
|
+
three files, diff them against disk
|
|
24
|
+
(`tan.core.scaffold.collect_file_changes`), then preview / guard / write --
|
|
25
|
+
folding whatever comes back into exactly one envelope. Mirrors
|
|
26
|
+
`crates/tan-cli/src/commands/scaffold.rs`, which the board read is a
|
|
27
|
+
deliberate, argued divergence from.
|
|
28
|
+
|
|
29
|
+
**`--name` is REQUIRED, with NO non-interactive default.** Unlike `tan init`'s
|
|
30
|
+
`--template` (defaults to `zephyr-app`) or `--name` (defaults to an empty
|
|
31
|
+
subdirectory), a module scaffold has no sane default name -- so the
|
|
32
|
+
non-interactive contract is a REFUSAL (`scaffold.name-required`, exit 2),
|
|
33
|
+
never a default (`crates/tan-cli/src/commands/scaffold.rs`'s own history,
|
|
34
|
+
CHANGELOG.md's #187-follow-up entry: "its non-interactive contract is a
|
|
35
|
+
refusal, not a default, since a module name has no sane one"). `--template`
|
|
36
|
+
DOES have a non-interactive default (`sensor-driver`, the registry's first
|
|
37
|
+
entry) when omitted.
|
|
38
|
+
|
|
39
|
+
**Interactivity mirrors the oracle's `GlobalArgs::can_prompt()`
|
|
40
|
+
(`crates/tan-cli/src/cli.rs`) exactly**: may only prompt when NOT
|
|
41
|
+
`--non-interactive`, NOT `--ci`, NOT `--format json`, AND both stdin and
|
|
42
|
+
stderr are real terminals. The last two matter more than they look: a
|
|
43
|
+
prompting library renders to stderr and reads through the controlling
|
|
44
|
+
terminal, so `stdin=tty, stderr=piped` -- every wrapper that captures output
|
|
45
|
+
while inheriting the terminal -- still cannot be prompted safely and must
|
|
46
|
+
refuse rather than hang. No CI runner, and no `pytest` subprocess, has a TTY,
|
|
47
|
+
so `--name`/`--template` are effectively always required in an automated run;
|
|
48
|
+
this is intentional (the module docstring for `can_prompt` documents the same
|
|
49
|
+
for the Rust binary: "no CI runner has a TTY"). The interactive fallback here
|
|
50
|
+
uses `click.prompt`/`click.Choice` rather than a `Select`/`Text` TUI widget,
|
|
51
|
+
the same simplification `tan.commands.new_som_cmd` already made and documents
|
|
52
|
+
(no arrow-key menu dependency for a path automated tests never exercise).
|
|
53
|
+
|
|
54
|
+
Every failure is a coded issue, never a traceback -- the backstop at the
|
|
55
|
+
bottom of [`scaffold`] converts any unexpected exception into
|
|
56
|
+
`scaffold.internal-failure` rather than letting it escape, matching
|
|
57
|
+
`init_cmd`/`debug_config_cmd`'s own catch-all.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import sys
|
|
63
|
+
from dataclasses import dataclass, field
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
import click
|
|
67
|
+
import typer
|
|
68
|
+
|
|
69
|
+
from tan.core.board_context import read_board_context, resolve_board_path
|
|
70
|
+
from tan.core.consent import can_prompt
|
|
71
|
+
from tan.core.module_template import (
|
|
72
|
+
DEFAULT_MODULE_TEMPLATE_ID,
|
|
73
|
+
MODULE_TEMPLATE_IDS,
|
|
74
|
+
create_module_scaffold_plan,
|
|
75
|
+
)
|
|
76
|
+
from tan.core.scaffold import FileChange, PlannedFile, ScaffoldWriteError, collect_file_changes
|
|
77
|
+
from tan.core.scaffold import scaffold_tree_preview as _tree_preview
|
|
78
|
+
from tan.core.scaffold import write_files
|
|
79
|
+
from tan.envelope import Envelope, Issue, Project, emit
|
|
80
|
+
from tan.exit_codes import ExitCode
|
|
81
|
+
from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
|
|
82
|
+
|
|
83
|
+
#: `data.schemaVersion` for this command's payload.
|
|
84
|
+
DATA_SCHEMA_VERSION = "1"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ScaffoldError(Exception):
|
|
88
|
+
"""A failure with its issue code and exit code already decided -- the
|
|
89
|
+
ONE exception type every resolution/planning/write step in [`scaffold`]
|
|
90
|
+
raises, mirrors `init_cmd.InitError`'s reason for existing: a single
|
|
91
|
+
exception class lets the whole computation run inside ONE `try`, with the
|
|
92
|
+
error-shaped envelope built exactly once, after it, in a SIBLING `except`
|
|
93
|
+
clause. Calling an emit-and-`typer.Exit` helper from a handler that is
|
|
94
|
+
itself still lexically nested INSIDE that same `try` does not work --
|
|
95
|
+
`typer.Exit` subclasses `RuntimeError`, so raising it from a nested
|
|
96
|
+
`except ScaffoldWriteError:` block is still within the outer try's
|
|
97
|
+
dynamic extent and gets re-caught by the outer `except Exception:`
|
|
98
|
+
backstop, turning a clean exit 3 into a misreported `scaffold.internal-
|
|
99
|
+
failure` at exit 5 (caught by this port's own oracle-diff smoke test,
|
|
100
|
+
not a golden -- there is no committed fixture for this shape).
|
|
101
|
+
|
|
102
|
+
`partial` carries the files that DID land when a write failed part-way:
|
|
103
|
+
`written: []` for a module half-written to disk would contradict the
|
|
104
|
+
filesystem, the same reasoning `InitError.partial` documents.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
code: str,
|
|
110
|
+
message: str,
|
|
111
|
+
exit_code: ExitCode,
|
|
112
|
+
*,
|
|
113
|
+
partial: tuple[list[str], list[str]] = ([], []),
|
|
114
|
+
) -> None:
|
|
115
|
+
super().__init__(message)
|
|
116
|
+
self.code = code
|
|
117
|
+
self.message = message
|
|
118
|
+
self.exit_code = exit_code
|
|
119
|
+
self.partial = partial
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class _Outcome:
|
|
124
|
+
"""A completed (non-error) run: preview, overwrite-guard refusal, or
|
|
125
|
+
write. Built and returned rather than emitted in place, so the exception
|
|
126
|
+
guard in [`scaffold`] can wrap the whole computation without also
|
|
127
|
+
catching `typer.Exit`."""
|
|
128
|
+
|
|
129
|
+
template_id: str
|
|
130
|
+
module_name: str
|
|
131
|
+
normalized_name: str
|
|
132
|
+
destination: str
|
|
133
|
+
preview: bool
|
|
134
|
+
file_changes: list[FileChange]
|
|
135
|
+
files: list[PlannedFile]
|
|
136
|
+
written: list[str] = field(default_factory=list)
|
|
137
|
+
unchanged: list[str] = field(default_factory=list)
|
|
138
|
+
exit_code: ExitCode = ExitCode.SUCCESS
|
|
139
|
+
issue: Issue | None = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Interactivity
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _need_name() -> ScaffoldError:
|
|
148
|
+
return ScaffoldError(
|
|
149
|
+
"scaffold.name-required",
|
|
150
|
+
"Module name is required. Use --name <name> or run interactively.",
|
|
151
|
+
ExitCode.VALIDATION_FAILURE,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _cancelled() -> ScaffoldError:
|
|
156
|
+
return ScaffoldError("scaffold.cancelled", "Cancelled.", ExitCode.RUNTIME_FAILURE)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _resolve_module_name(name: str | None, interactive: bool) -> str:
|
|
160
|
+
if name is not None:
|
|
161
|
+
return name
|
|
162
|
+
if not interactive:
|
|
163
|
+
raise _need_name()
|
|
164
|
+
try:
|
|
165
|
+
# `err=True`: `click.prompt`'s own default (`err=False`) writes the
|
|
166
|
+
# question to stdout, which corrupts whatever this run is piping or
|
|
167
|
+
# redirecting there and leaves a real terminal on stderr showing
|
|
168
|
+
# nothing to answer (tan-cli#496 defect 6, measured against the
|
|
169
|
+
# frozen oracle: `inquire::Text` renders to stderr).
|
|
170
|
+
raw = click.prompt("Module name", err=True)
|
|
171
|
+
except click.exceptions.Abort as err:
|
|
172
|
+
raise _cancelled() from err
|
|
173
|
+
stripped = raw.strip()
|
|
174
|
+
if not stripped:
|
|
175
|
+
raise _need_name()
|
|
176
|
+
return stripped
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_template(template: str | None, interactive: bool) -> str:
|
|
180
|
+
if template is not None:
|
|
181
|
+
if template not in MODULE_TEMPLATE_IDS:
|
|
182
|
+
raise ScaffoldError(
|
|
183
|
+
"scaffold.invalid-template",
|
|
184
|
+
f"Unknown module template '{template}'.",
|
|
185
|
+
ExitCode.VALIDATION_FAILURE,
|
|
186
|
+
)
|
|
187
|
+
return template
|
|
188
|
+
if not interactive:
|
|
189
|
+
return DEFAULT_MODULE_TEMPLATE_ID
|
|
190
|
+
try:
|
|
191
|
+
# See `_resolve_module_name`'s `err=True` note -- same defect.
|
|
192
|
+
return click.prompt(
|
|
193
|
+
"Select a module template", type=click.Choice(MODULE_TEMPLATE_IDS), err=True
|
|
194
|
+
)
|
|
195
|
+
except click.exceptions.Abort as err:
|
|
196
|
+
raise _cancelled() from err
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Envelope assembly
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _data(
|
|
205
|
+
*,
|
|
206
|
+
template_id: str,
|
|
207
|
+
module_name: str,
|
|
208
|
+
normalized_module_name: str,
|
|
209
|
+
destination: str,
|
|
210
|
+
preview: bool,
|
|
211
|
+
file_changes: list[FileChange],
|
|
212
|
+
written: list[str],
|
|
213
|
+
unchanged: list[str],
|
|
214
|
+
) -> dict:
|
|
215
|
+
return {
|
|
216
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
217
|
+
"templateId": template_id,
|
|
218
|
+
"moduleName": module_name,
|
|
219
|
+
"normalizedModuleName": normalized_module_name,
|
|
220
|
+
"destination": destination,
|
|
221
|
+
"preview": preview,
|
|
222
|
+
"fileChanges": [{"relativePath": c.relative_path, "kind": c.kind} for c in file_changes],
|
|
223
|
+
"written": written,
|
|
224
|
+
"unchanged": unchanged,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
_EMPTY_DATA_FIELDS = {
|
|
229
|
+
"template_id": "",
|
|
230
|
+
"module_name": "",
|
|
231
|
+
"normalized_module_name": "",
|
|
232
|
+
"destination": "",
|
|
233
|
+
"preview": False,
|
|
234
|
+
"file_changes": [],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _stderr(line: str) -> None:
|
|
239
|
+
print(line, file=sys.stderr)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _emit_error(json_mode: bool, err: ScaffoldError) -> None:
|
|
243
|
+
"""An error before (or during) a write: `project.root: null`, every
|
|
244
|
+
plan-shaped field empty. `written`/`unchanged` are usually empty too, EXCEPT
|
|
245
|
+
on a part-way write failure (`err.partial`), where they carry whatever
|
|
246
|
+
landed before it -- reporting `written: []` there would contradict the
|
|
247
|
+
filesystem. Mirrors `error_run`/`write_error_run` in the Rust
|
|
248
|
+
(`scaffold.rs`), which the wire shape is otherwise identical between.
|
|
249
|
+
"""
|
|
250
|
+
written, unchanged = err.partial
|
|
251
|
+
if json_mode:
|
|
252
|
+
emit(
|
|
253
|
+
Envelope(
|
|
254
|
+
"scaffold",
|
|
255
|
+
Project(root=None, board_yaml=None),
|
|
256
|
+
_data(**_EMPTY_DATA_FIELDS, written=written, unchanged=unchanged),
|
|
257
|
+
[Issue(err.code, "error", err.message)],
|
|
258
|
+
err.exit_code,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
_stderr(f"scaffold: {err.message}")
|
|
263
|
+
raise typer.Exit(int(err.exit_code))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _emit_outcome(json_mode: bool, outcome: _Outcome, board_yaml: str | None) -> None:
|
|
267
|
+
"""*board_yaml* is the RESOLVED path (tan-cli#1031): a PROJECT fact,
|
|
268
|
+
identical across all three outcome shapes, so not an `_Outcome` field.
|
|
269
|
+
`Project.resolved` is the blessed seam -- a path that resolved but is not
|
|
270
|
+
there reports `null`, as it does for `tan validate`. `_emit_error` keeps
|
|
271
|
+
its hardcoded `(None, None)`; that is the frozen error shape."""
|
|
272
|
+
project = Project.resolved(outcome.destination, board_yaml)
|
|
273
|
+
if json_mode:
|
|
274
|
+
emit(
|
|
275
|
+
Envelope(
|
|
276
|
+
"scaffold",
|
|
277
|
+
project,
|
|
278
|
+
_data(
|
|
279
|
+
template_id=outcome.template_id,
|
|
280
|
+
module_name=outcome.module_name,
|
|
281
|
+
normalized_module_name=outcome.normalized_name,
|
|
282
|
+
destination=outcome.destination,
|
|
283
|
+
preview=outcome.preview,
|
|
284
|
+
file_changes=outcome.file_changes,
|
|
285
|
+
written=outcome.written,
|
|
286
|
+
unchanged=outcome.unchanged,
|
|
287
|
+
),
|
|
288
|
+
[outcome.issue] if outcome.issue is not None else [],
|
|
289
|
+
outcome.exit_code,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
elif outcome.preview:
|
|
293
|
+
_stderr(
|
|
294
|
+
f"scaffold: preview for module '{outcome.normalized_name}' "
|
|
295
|
+
f"(template '{outcome.template_id}')"
|
|
296
|
+
)
|
|
297
|
+
# `_tree_preview` already ends in one `\n` (one per listed path); NOT
|
|
298
|
+
# stripped -- `_stderr`'s own `print()` adds a second, matching the
|
|
299
|
+
# oracle's own two-`CommandRun.text` -> `println!` shape byte-for-byte
|
|
300
|
+
# (measured: `tan scaffold --preview` ends the tree with `\n\n`).
|
|
301
|
+
_stderr(_tree_preview(outcome.files))
|
|
302
|
+
elif outcome.exit_code != ExitCode.SUCCESS:
|
|
303
|
+
# The overwrite guard. Deliberately NOT `outcome.issue.message` --
|
|
304
|
+
# the Rust's text-mode line here is a separate, shorter, hardcoded
|
|
305
|
+
# string (`scaffold.rs`'s guard block), not the JSON issue message
|
|
306
|
+
# ("One or more files would be overwritten. Use --force to allow
|
|
307
|
+
# updates.") rendered with a prefix; measured against the oracle.
|
|
308
|
+
_stderr("scaffold: would overwrite existing files; use --force to proceed.")
|
|
309
|
+
else:
|
|
310
|
+
_stderr(
|
|
311
|
+
f"scaffold: created module '{outcome.normalized_name}' "
|
|
312
|
+
f"(template '{outcome.template_id}')"
|
|
313
|
+
)
|
|
314
|
+
_stderr(f" written: {len(outcome.written)}, unchanged: {len(outcome.unchanged)}")
|
|
315
|
+
raise typer.Exit(int(outcome.exit_code))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
# Entry point
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def scaffold(
|
|
324
|
+
ctx: typer.Context,
|
|
325
|
+
template: str = typer.Option(
|
|
326
|
+
None,
|
|
327
|
+
"--template",
|
|
328
|
+
metavar="TEMPLATE",
|
|
329
|
+
help=f"Module template id ({', '.join(MODULE_TEMPLATE_IDS)}).",
|
|
330
|
+
),
|
|
331
|
+
name: str = typer.Option(
|
|
332
|
+
None, "--name", metavar="NAME", help="Module name (required)."
|
|
333
|
+
),
|
|
334
|
+
destination: str = typer.Option(
|
|
335
|
+
None,
|
|
336
|
+
"--destination",
|
|
337
|
+
metavar="DESTINATION",
|
|
338
|
+
help="Destination project root (default: current directory or --project).",
|
|
339
|
+
),
|
|
340
|
+
preview: bool = typer.Option(
|
|
341
|
+
False, "--preview", help="Show planned files without writing anything."
|
|
342
|
+
),
|
|
343
|
+
force: bool = typer.Option(
|
|
344
|
+
False, "--force", help="Allow overwriting existing files."
|
|
345
|
+
),
|
|
346
|
+
project: str = typer.Option(
|
|
347
|
+
None, "--project", metavar="PATH", help="Project root (defaults to current directory)."
|
|
348
|
+
),
|
|
349
|
+
board_yaml: str = typer.Option(
|
|
350
|
+
None,
|
|
351
|
+
"--board-yaml",
|
|
352
|
+
metavar="PATH",
|
|
353
|
+
help="Explicit board.yaml path (overrides project resolution).",
|
|
354
|
+
),
|
|
355
|
+
sdk_root: str = typer.Option(
|
|
356
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
357
|
+
),
|
|
358
|
+
target: str = typer.Option(
|
|
359
|
+
None,
|
|
360
|
+
"--target",
|
|
361
|
+
metavar="EMIT",
|
|
362
|
+
help="Generation target (e.g. zephyr-conf, dts-overlay, cmake-args, yocto-conf).",
|
|
363
|
+
),
|
|
364
|
+
all_targets: bool = typer.Option(
|
|
365
|
+
False, "--all", help="Run command against all relevant targets."
|
|
366
|
+
),
|
|
367
|
+
output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
|
|
368
|
+
verbose: bool = typer.Option(False, "--verbose", help="Emit additional diagnostic detail."),
|
|
369
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-essential output."),
|
|
370
|
+
no_color: bool = typer.Option(
|
|
371
|
+
False, "--no-color", help="Disable ANSI color in text output."
|
|
372
|
+
),
|
|
373
|
+
non_interactive: bool = typer.Option(
|
|
374
|
+
False,
|
|
375
|
+
"--non-interactive",
|
|
376
|
+
help="Never prompt; fail instead of asking when a required value is missing.",
|
|
377
|
+
),
|
|
378
|
+
ci: bool = typer.Option(
|
|
379
|
+
False, "--ci", help="CI mode: implies non-interactive and disables color."
|
|
380
|
+
),
|
|
381
|
+
) -> None:
|
|
382
|
+
"""Scaffold a module into an existing project."""
|
|
383
|
+
# `--sdk-root`/`--target`/`--all`/`--verbose`/`--quiet`/`--no-color` are
|
|
384
|
+
# `GlobalArgs` members the oracle's `scaffold.rs::run` never reads;
|
|
385
|
+
# declared (not `hidden=True`) so `--help` matches its listing.
|
|
386
|
+
# `--board-yaml` was in this list until tan-cli#1031 and is READ now.
|
|
387
|
+
del sdk_root, target, all_targets, verbose, quiet, no_color
|
|
388
|
+
|
|
389
|
+
resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
|
|
390
|
+
json_mode = resolved_format == "json"
|
|
391
|
+
interactive = can_prompt(non_interactive=non_interactive, ci=ci, json_mode=json_mode)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
module_name = _resolve_module_name(name, interactive)
|
|
395
|
+
template_id = _resolve_template(template, interactive)
|
|
396
|
+
|
|
397
|
+
dest = destination if destination else (project if project else ".")
|
|
398
|
+
project_root = Path(dest)
|
|
399
|
+
# tan-cli#1031. `tan validate`'s own resolver, MOVED to
|
|
400
|
+
# `tan.core.board_context` and called here -- not a second strategy.
|
|
401
|
+
_, board_path = resolve_board_path(dest, board_yaml)
|
|
402
|
+
board_context = read_board_context(board_path)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
plan = create_module_scaffold_plan(template_id, module_name, board_context)
|
|
406
|
+
except ValueError as err:
|
|
407
|
+
# Re-raised as `ScaffoldError`, never emitted from here directly:
|
|
408
|
+
# this `except` is still lexically INSIDE the outer `try` below,
|
|
409
|
+
# so a `typer.Exit` raised from an emit helper called here would
|
|
410
|
+
# be re-caught by this same function's own `except Exception`
|
|
411
|
+
# backstop (`typer.Exit` subclasses `RuntimeError`) -- see
|
|
412
|
+
# `ScaffoldError`'s own docstring for the mechanism and how this
|
|
413
|
+
# was actually caught (an oracle-diff smoke test, not a golden).
|
|
414
|
+
raise ScaffoldError(
|
|
415
|
+
"scaffold.invalid-name", str(err), ExitCode.VALIDATION_FAILURE
|
|
416
|
+
) from err
|
|
417
|
+
|
|
418
|
+
changes = collect_file_changes(project_root, plan.files)
|
|
419
|
+
has_updates = any(c.kind == "update" for c in changes)
|
|
420
|
+
|
|
421
|
+
if preview:
|
|
422
|
+
# Before the overwrite guard, deliberately -- a preview touches no
|
|
423
|
+
# disk, so it has nothing to be guarded against (same ordering
|
|
424
|
+
# `tan init` learned the hard way; see `tan.core.scaffold`'s
|
|
425
|
+
# `write_files` docstring for the sibling incident).
|
|
426
|
+
outcome = _Outcome(
|
|
427
|
+
template_id=template_id,
|
|
428
|
+
module_name=module_name,
|
|
429
|
+
normalized_name=plan.normalized_name,
|
|
430
|
+
destination=dest,
|
|
431
|
+
preview=True,
|
|
432
|
+
file_changes=changes,
|
|
433
|
+
files=plan.files,
|
|
434
|
+
)
|
|
435
|
+
elif has_updates and not force:
|
|
436
|
+
outcome = _Outcome(
|
|
437
|
+
template_id=template_id,
|
|
438
|
+
module_name=module_name,
|
|
439
|
+
normalized_name=plan.normalized_name,
|
|
440
|
+
destination=dest,
|
|
441
|
+
preview=False,
|
|
442
|
+
file_changes=changes,
|
|
443
|
+
files=plan.files,
|
|
444
|
+
exit_code=ExitCode.WRITE_FAILURE,
|
|
445
|
+
issue=Issue(
|
|
446
|
+
"scaffold.would-overwrite",
|
|
447
|
+
"error",
|
|
448
|
+
"One or more files would be overwritten. Use --force to allow updates.",
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
try:
|
|
453
|
+
result = write_files(project_root, plan.files)
|
|
454
|
+
except ScaffoldWriteError as err:
|
|
455
|
+
raise ScaffoldError(
|
|
456
|
+
"scaffold.write-failed",
|
|
457
|
+
f"Failed to write files: {err}",
|
|
458
|
+
ExitCode.WRITE_FAILURE,
|
|
459
|
+
partial=(err.partial.written, err.partial.unchanged),
|
|
460
|
+
) from err
|
|
461
|
+
outcome = _Outcome(
|
|
462
|
+
template_id=template_id,
|
|
463
|
+
module_name=module_name,
|
|
464
|
+
normalized_name=plan.normalized_name,
|
|
465
|
+
destination=dest,
|
|
466
|
+
preview=False,
|
|
467
|
+
file_changes=changes,
|
|
468
|
+
files=plan.files,
|
|
469
|
+
written=result.written,
|
|
470
|
+
unchanged=result.unchanged,
|
|
471
|
+
)
|
|
472
|
+
except ScaffoldError as err:
|
|
473
|
+
_emit_error(json_mode, err)
|
|
474
|
+
return
|
|
475
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
476
|
+
# `typer.Exit` cannot reach here: it is only ever raised from
|
|
477
|
+
# `_emit_error`, called from the SIBLING `except ScaffoldError` clause
|
|
478
|
+
# above -- outside this try's dynamic extent, so it propagates
|
|
479
|
+
# straight out rather than looping back into this handler.
|
|
480
|
+
_emit_error(
|
|
481
|
+
json_mode,
|
|
482
|
+
ScaffoldError(
|
|
483
|
+
"scaffold.internal-failure",
|
|
484
|
+
f"scaffold failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
485
|
+
ExitCode.INTERNAL_FAILURE,
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
_emit_outcome(json_mode, outcome, board_path)
|