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,736 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan migrate` / `tan lock` / `tan quality` -- the three `west`-delegating
|
|
3
|
+
extension commands that survive ADR-0020 Phase 4 (`west alp-build` itself was
|
|
4
|
+
retired; `tan build` is native now -- see `build_cmd.py`'s module doc). Port of
|
|
5
|
+
`crates/tan-cli/src/commands/build/workspace.rs::run` (the oracle's own,
|
|
6
|
+
now-narrowed, "legacy west forward" entry) and its `west_argv`/
|
|
7
|
+
`west_workspace_dir`/`west_program` helpers.
|
|
8
|
+
|
|
9
|
+
Every flag NOT declared on the command below (`--workspace`, `--no-verify`,
|
|
10
|
+
`--json`, `--junit`, `--sarif`, ...) belongs to the underlying `west
|
|
11
|
+
alp-<verb>` extension command, not to `tan` -- it is forwarded verbatim,
|
|
12
|
+
mirroring the oracle's own `[ARGS]...` catch-all
|
|
13
|
+
(`crates/tan-cli/src/cli.rs`'s `WestForwardArgs`). `ignore_unknown_options` in
|
|
14
|
+
each command's `context_settings` is what lets an unrecognised flag fall
|
|
15
|
+
through to that catch-all instead of Click rejecting it outright. `--check`/
|
|
16
|
+
`--preview`/`--apply` (`migrate`) and `--profile` (`quality`) used to be in
|
|
17
|
+
that undeclared set too, until tan-cli#454 (see below) made them real,
|
|
18
|
+
declared options for the one reason a flag in this file ever needs to be:
|
|
19
|
+
each is REQUIRED by its child with no default `tan` could supply.
|
|
20
|
+
|
|
21
|
+
Four defects this module shipped with, and what each fix costs:
|
|
22
|
+
|
|
23
|
+
**tan-cli#391 -- no child takes a positional.** `_run_forward` used to insert
|
|
24
|
+
the resolved app path as `argv[1]` whenever every forwarded token began with a
|
|
25
|
+
dash. `west alp-build` was the only `alp-*` command that ever declared an
|
|
26
|
+
app-path positional, and ADR-0020 Phase 4 retired it; the injection outlived
|
|
27
|
+
its target. The three surviving children declare flags only -- `alp_lock.py`:
|
|
28
|
+
`--check`, `--workspace`, `--board`; `alp_migrate.py`: `--check`, `--preview`,
|
|
29
|
+
`--apply`, `--all`, `--board`, `--no-verify`; `alp_quality.py`: `--profile`,
|
|
30
|
+
`--json`, `--junit`, `--sarif` -- so every realistic `tan migrate` / `tan lock`
|
|
31
|
+
run died on `west alp-migrate: error: unexpected arguments: ['<project>']`
|
|
32
|
+
before doing any work. `tan quality --profile quick` survived only by
|
|
33
|
+
accident (`quick` is a non-dash token, so the guard suppressed the injection);
|
|
34
|
+
`--profile=quick` is one token and did not. Deleting the injection alone is
|
|
35
|
+
NOT the whole fix: `run_cwd` is the west TOPDIR, so a child left to its own
|
|
36
|
+
`Path("board.yaml")` fallback resolves the WORKSPACE ROOT's board.yaml rather
|
|
37
|
+
than the project's. `_target_args` below names the project through the
|
|
38
|
+
`--board` flag `alp-migrate`/`alp-lock` really declare. `alp-quality` declares
|
|
39
|
+
no target flag at all and is therefore left cwd-targeted, exactly as before.
|
|
40
|
+
|
|
41
|
+
**tan-cli#395 -- the child's output was captured and discarded.** JSON mode
|
|
42
|
+
already spawned the child with `capture_output=True` and then read neither
|
|
43
|
+
stream; the previous version of this docstring defended that as "never the
|
|
44
|
+
child's own stdout, which for an interactive tool cannot be captured without
|
|
45
|
+
breaking it", but the capture had already happened one line above -- the only
|
|
46
|
+
decision actually being made was to throw the result away. `alp-quality`
|
|
47
|
+
exists to produce a report, `alp-migrate --preview` a diff, `alp-lock` a
|
|
48
|
+
resolved manifest, and a JSON-only caller could obtain none of them. `data`
|
|
49
|
+
now carries `stdout`, `stderr` and `westExitCode`, and the `<verb>.failed`
|
|
50
|
+
message is derived from the child's own stderr instead of a fixed sentence
|
|
51
|
+
telling the caller to re-run in a mode it does not have. The envelope's own
|
|
52
|
+
`exitCode` deliberately does NOT propagate the child's: `tan/exit_codes.py`
|
|
53
|
+
fixes tan's vocabulary (0/1/2/3/4/5), so a child's `2` would arrive
|
|
54
|
+
indistinguishable from tan's own `VALIDATION_FAILURE`. `data.westExitCode` is
|
|
55
|
+
what makes a lock conflict (`7`) distinguishable from west's own usage error.
|
|
56
|
+
This is a DELIBERATE divergence from the oracle, which calls `cmd.output()`
|
|
57
|
+
and never reads `out.stdout`/`out.stderr` either
|
|
58
|
+
(`crates/tan-cli/src/commands/build/workspace.rs`).
|
|
59
|
+
|
|
60
|
+
**tan-cli#405 -- tan's own globals leaked into the child.** These were the
|
|
61
|
+
only three commands registered with `ignore_unknown_options` AND the only
|
|
62
|
+
three that never called `accept_global_flags`, so seven of the ten flags in
|
|
63
|
+
`tan.core.global_flags._GLOBAL_FLAG_SPECS` were unknown here -- and an unknown
|
|
64
|
+
flag under `ignore_unknown_options` is not rejected, it is dropped into the
|
|
65
|
+
`ARGS...` catch-all and handed to a child that declares none of them.
|
|
66
|
+
`tan --ci quality --profile quick` became `west alp-quality --ci --profile
|
|
67
|
+
quick`; `tan --target cm33 quality` leaked the VALUE `cm33`, which then read
|
|
68
|
+
as a caller-supplied positional and silently changed the argv shape. All three
|
|
69
|
+
now call `accept_global_flags`. `--all` is the one collision: it is both a tan
|
|
70
|
+
global and a real `alp-migrate` flag, so `migrate` declares it for real and
|
|
71
|
+
forwards it back to the child after consuming it -- a bare
|
|
72
|
+
`accept_global_flags(migrate)` would swallow it, which is what the v0.4.1
|
|
73
|
+
oracle does.
|
|
74
|
+
|
|
75
|
+
**tan-cli#454 -- `quality` and `migrate` could never succeed.**
|
|
76
|
+
`alp_quality.py` declares `--profile {quick,pr,full,release}` REQUIRED;
|
|
77
|
+
`alp_migrate.py` declares a REQUIRED mutually-exclusive `(--check | --preview
|
|
78
|
+
| --apply)` group. Neither was ever declared on tan's own Typer surface --
|
|
79
|
+
both fell into the `ARGS...` catch-all like every other undeclared flag, which
|
|
80
|
+
happened to still work for a caller who typed the exact right token, but meant
|
|
81
|
+
`tan quality --help` never named `--profile` and `tan migrate --help` never
|
|
82
|
+
named any of the three modes, so a caller relying on `--help` (the entire
|
|
83
|
+
point of a documented CLI surface) had no way to discover what to pass. On a
|
|
84
|
+
correctly bootstrapped workspace, every invocation that omitted the required
|
|
85
|
+
flag reached `west`, which rejected it with its own argparse usage error --
|
|
86
|
+
forwarded back as the opaque `quality.failed` / `migrate.failed`, indistinguishable
|
|
87
|
+
from `west` itself failing for an unrelated reason. Fixed by declaring
|
|
88
|
+
`--profile` (`quality`) and `--check`/`--preview`/`--apply` (`migrate`) for
|
|
89
|
+
real -- their choice lists and mutual-exclusion read from
|
|
90
|
+
`scripts/west_commands/alp_quality.py` / `alp_migrate.py` verbatim, forwarded
|
|
91
|
+
back through `extra_args` exactly like `migrate`'s pre-existing `--all`
|
|
92
|
+
(tan-cli#405) -- and refusing EARLY, before `west` is ever spawned, with a
|
|
93
|
+
coded `quality.profile-required` / `migrate.mode-required` issue naming the
|
|
94
|
+
missing flag when none of the required ones was supplied. See
|
|
95
|
+
`_refuse_required`.
|
|
96
|
+
|
|
97
|
+
Text mode inherits stdio -- `alp-migrate`/`alp-lock`/`alp-quality` may prompt
|
|
98
|
+
or stream their own progress, exactly like the oracle's `west`-delegating path
|
|
99
|
+
(its own module doc: "Text mode inherits stdio so the build streams live"), so
|
|
100
|
+
there is nothing to capture there and nothing to report but the child's exit
|
|
101
|
+
code. JSON mode captures and reports ONE envelope.
|
|
102
|
+
"""
|
|
103
|
+
from __future__ import annotations
|
|
104
|
+
|
|
105
|
+
import os
|
|
106
|
+
import subprocess
|
|
107
|
+
from collections.abc import Sequence
|
|
108
|
+
from dataclasses import dataclass
|
|
109
|
+
from pathlib import Path
|
|
110
|
+
from typing import NoReturn
|
|
111
|
+
|
|
112
|
+
import typer
|
|
113
|
+
|
|
114
|
+
from tan.commands.build_output import ProjectContext, resolve_project_context, to_posix
|
|
115
|
+
from tan.core.global_flags import accept_global_flags
|
|
116
|
+
from tan.core.sdk_discovery import sdk_resolution_issues
|
|
117
|
+
from tan.core.subprocess_env import spawn_env
|
|
118
|
+
from tan.core.venv import west_program, west_workspace_dir, with_venv_on_path
|
|
119
|
+
from tan.envelope import Envelope, Issue, emit
|
|
120
|
+
from tan.exit_codes import ExitCode
|
|
121
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
122
|
+
|
|
123
|
+
#: Passed to `app.command(...)` for each forwarder below: lets an unrecognised
|
|
124
|
+
#: flag (the underlying `west alp-*` command's own surface) fall through to the
|
|
125
|
+
#: `args` catch-all instead of Click's usual "no such option" usage error.
|
|
126
|
+
FORWARD_CONTEXT_SETTINGS = {"ignore_unknown_options": True}
|
|
127
|
+
|
|
128
|
+
#: Kept as the underscore-named alias this module (and its own tests) always
|
|
129
|
+
#: called it -- the implementation moved to `tan.core.venv.west_workspace_dir`
|
|
130
|
+
#: (tan-cli#289 review) so `flash_cmd.py` shares it instead of importing a
|
|
131
|
+
#: private name across modules.
|
|
132
|
+
_west_workspace_dir = west_workspace_dir
|
|
133
|
+
|
|
134
|
+
#: The verbs whose child declares `--board` (tan-cli#391). `quality` is absent
|
|
135
|
+
#: on purpose: `alp_quality.py`'s surface is `--profile`/`--json`/`--junit`/
|
|
136
|
+
#: `--sarif` and nothing else, so there is no flag through which tan could name
|
|
137
|
+
#: a project and inventing one would be a usage error, not targeting.
|
|
138
|
+
_BOARD_TARGETED = frozenset({"migrate", "lock"})
|
|
139
|
+
|
|
140
|
+
#: How much of the child's stderr the `<verb>.failed` message may quote
|
|
141
|
+
#: (tan-cli#395). `data.stderr` keeps the stream VERBATIM and uncapped -- it is
|
|
142
|
+
#: the payload; this caps only the one-line summary, so a child that dumps a
|
|
143
|
+
#: megabyte on one line cannot turn a single issue into an unreadable envelope.
|
|
144
|
+
_MAX_MESSAGE_DETAIL = 500
|
|
145
|
+
|
|
146
|
+
#: Verbatim copy of `scripts/west_commands/alp_quality.py`'s own
|
|
147
|
+
#: `choices=("quick", "pr", "full", "release")` (tan-cli#454) -- read from the
|
|
148
|
+
#: SDK checkout the resolver points at, not guessed. Kept as one tuple so the
|
|
149
|
+
#: `--help` metavar, the `--profile <value>` validation and the "which one is
|
|
150
|
+
#: missing" refusal message can never drift apart from each other.
|
|
151
|
+
_QUALITY_PROFILES = ("quick", "pr", "full", "release")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _west_argv(subcommand: str, passthrough: list[str]) -> list[str]:
|
|
155
|
+
"""Port of `workspace.rs::west_argv`: `alp-<subcommand>` followed by the
|
|
156
|
+
forwarded args, verbatim."""
|
|
157
|
+
return [f"alp-{subcommand}", *passthrough]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _launch_error(err: OSError) -> str:
|
|
161
|
+
"""Port of `workspace.rs::west_launch_error`."""
|
|
162
|
+
if isinstance(err, FileNotFoundError):
|
|
163
|
+
return "west not found on PATH — run `tan bootstrap` and ensure west is on PATH."
|
|
164
|
+
return f"failed to launch west: {err}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _has_flag(args: Sequence[str], flag: str) -> bool:
|
|
168
|
+
"""Whether the caller already spelled `flag` themselves, in either form
|
|
169
|
+
argparse accepts for a value-taking option (`--board X`, `--board=X`).
|
|
170
|
+
|
|
171
|
+
argparse's prefix ABBREVIATION (`--boa X`) is deliberately not detected:
|
|
172
|
+
tan's own `--board` is prepended AHEAD of the forwarded tokens, so a
|
|
173
|
+
caller's later spelling -- abbreviated or not -- is the one argparse keeps.
|
|
174
|
+
The one case where that "last wins" safety net does not apply is
|
|
175
|
+
`alp-migrate`'s `--all`, which is MUTUALLY EXCLUSIVE with `--board` rather
|
|
176
|
+
than merely overridden by it; `--all` takes no value, and `migrate`
|
|
177
|
+
declares it for real below, so the exact-match check is what actually runs
|
|
178
|
+
for it.
|
|
179
|
+
"""
|
|
180
|
+
prefix = f"{flag}="
|
|
181
|
+
return any(arg == flag or arg.startswith(prefix) for arg in args)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _target_args(subcommand: str, child_args: Sequence[str], board_yaml: str) -> list[str]:
|
|
185
|
+
"""The flags that tell the child WHICH project to act on (tan-cli#391).
|
|
186
|
+
|
|
187
|
+
Empty for `quality` (no such flag exists), empty when the caller already
|
|
188
|
+
named a board themselves, and empty for `migrate --all` -- the child's
|
|
189
|
+
usage line is `[--all | --board BOARD]`, so adding `--board` behind a
|
|
190
|
+
caller's `--all` would trade one usage error for another.
|
|
191
|
+
"""
|
|
192
|
+
if subcommand not in _BOARD_TARGETED:
|
|
193
|
+
return []
|
|
194
|
+
if _has_flag(child_args, "--board"):
|
|
195
|
+
return []
|
|
196
|
+
if subcommand == "migrate" and _has_flag(child_args, "--all"):
|
|
197
|
+
return []
|
|
198
|
+
return ["--board", board_yaml]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _last_meaningful_line(stderr: str) -> str:
|
|
202
|
+
"""The child's own last word. Both shapes tan-cli#395 measured put the
|
|
203
|
+
actionable line LAST -- argparse prints its usage block and then `west
|
|
204
|
+
alp-migrate: error: ...`, and west prints its own usage and then `west:
|
|
205
|
+
unknown command "alp-lock"; ...` -- so the last non-blank line is the one
|
|
206
|
+
a caller needs. The full stream is in `data.stderr` regardless, which is
|
|
207
|
+
what makes this a summary rather than a lossy substitute."""
|
|
208
|
+
lines = [line.strip() for line in stderr.splitlines() if line.strip()]
|
|
209
|
+
if not lines:
|
|
210
|
+
return ""
|
|
211
|
+
detail = lines[-1]
|
|
212
|
+
return detail if len(detail) <= _MAX_MESSAGE_DETAIL else f"{detail[:_MAX_MESSAGE_DETAIL]}..."
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _child_failure_message(west_command: str, returncode: int, stderr: str) -> str:
|
|
216
|
+
"""tan-cli#395: the fixed "re-run without --format json to see the log"
|
|
217
|
+
told a JSON-only caller to use a channel it does not have, and named
|
|
218
|
+
neither the child's exit code nor its diagnostic. The fallback survives for
|
|
219
|
+
a child that fails silently -- there is genuinely nothing else to say
|
|
220
|
+
then."""
|
|
221
|
+
detail = _last_meaningful_line(stderr)
|
|
222
|
+
if not detail:
|
|
223
|
+
return (
|
|
224
|
+
f"`west {west_command}` failed with exit code {returncode}; the child "
|
|
225
|
+
"wrote nothing to stderr -- re-run without --format json to see the log."
|
|
226
|
+
)
|
|
227
|
+
return f"`west {west_command}` failed with exit code {returncode}: {detail}"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True)
|
|
231
|
+
class _Forward:
|
|
232
|
+
"""One resolved `west alp-*` invocation, computed once and shared by both
|
|
233
|
+
output modes so text and JSON can never spawn different argv."""
|
|
234
|
+
|
|
235
|
+
subcommand: str
|
|
236
|
+
west_bin: str
|
|
237
|
+
#: The `alp-<verb>` argv -- `argv[0]` is the verb, the `west` binary is NOT
|
|
238
|
+
#: part of it (see `full_argv`).
|
|
239
|
+
argv: tuple[str, ...]
|
|
240
|
+
run_cwd: str
|
|
241
|
+
env: dict[str, str]
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def west_command(self) -> str:
|
|
245
|
+
return self.argv[0]
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def full_argv(self) -> list[str]:
|
|
249
|
+
return [self.west_bin, *self.argv]
|
|
250
|
+
|
|
251
|
+
def data(self) -> dict[str, object]:
|
|
252
|
+
"""`data.args` is every argument handed to the `alp-<verb>` command --
|
|
253
|
+
tan's own `--board` included, tan's globals excluded (tan-cli#391/#405).
|
|
254
|
+
It used to echo the caller's tokens only, so the envelope could not
|
|
255
|
+
name the injected argument the child actually died on. `westCommand`
|
|
256
|
+
carries `argv[0]`, so `west <westCommand> <args...>` reconstructs the
|
|
257
|
+
exact child command line."""
|
|
258
|
+
return {
|
|
259
|
+
"schemaVersion": "1",
|
|
260
|
+
"westCommand": self.west_command,
|
|
261
|
+
"westCwd": self.run_cwd,
|
|
262
|
+
"args": list(self.argv[1:]),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _plan(
|
|
267
|
+
subcommand: str,
|
|
268
|
+
passthrough: Sequence[str],
|
|
269
|
+
extra_args: Sequence[str],
|
|
270
|
+
context: ProjectContext,
|
|
271
|
+
) -> _Forward:
|
|
272
|
+
west_cwd = context.workspace_root or "."
|
|
273
|
+
sdk_path = Path(context.sdk.root) if context.sdk is not None else None
|
|
274
|
+
west_bin = west_program(west_cwd, str(sdk_path) if sdk_path is not None else None)
|
|
275
|
+
workspace = _west_workspace_dir(west_cwd, sdk_path)
|
|
276
|
+
# `to_posix`: `str(PurePath)` renders with the platform separator (backslash
|
|
277
|
+
# on Windows), but Rust's `Path::to_string_lossy` merely slices the already
|
|
278
|
+
# forward-slash `west_cwd` string -- never re-rendering it -- so the oracle's
|
|
279
|
+
# `data.westCwd` stays forward-slash on every platform. Without this the
|
|
280
|
+
# port's envelope breaks the "platform-identical path" handshake contract
|
|
281
|
+
# (`tan_core::project::to_posix`) the very first time a workspace resolves.
|
|
282
|
+
run_cwd = to_posix(str(workspace)) if workspace is not None else to_posix(west_cwd)
|
|
283
|
+
|
|
284
|
+
child_args = [*passthrough, *extra_args]
|
|
285
|
+
argv = _west_argv(
|
|
286
|
+
subcommand, [*_target_args(subcommand, child_args, context.board_yaml), *child_args]
|
|
287
|
+
)
|
|
288
|
+
return _Forward(
|
|
289
|
+
subcommand=subcommand,
|
|
290
|
+
west_bin=west_bin,
|
|
291
|
+
argv=tuple(argv),
|
|
292
|
+
run_cwd=run_cwd,
|
|
293
|
+
# tan-cli#992: `spawn_env()` FIRST (the restored copy), THEN the venv
|
|
294
|
+
# PATH-prepend on top -- the two touch disjoint keys (`LD_LIBRARY_PATH`
|
|
295
|
+
# vs `PATH`) so the order never matters, but this is the one
|
|
296
|
+
# `env=` every caller below shares (`_Forward.env`, "computed once ...
|
|
297
|
+
# so text and JSON can never spawn different argv" per this
|
|
298
|
+
# dataclass's own docstring), so it is built correctly exactly once.
|
|
299
|
+
env=with_venv_on_path(spawn_env(), west_bin),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _spawn_captured(plan: _Forward) -> subprocess.CompletedProcess[str]:
|
|
304
|
+
"""JSON mode's child. `capture_output=True` was always here -- tan-cli#395
|
|
305
|
+
is that neither stream was ever READ afterwards, not that the capture was
|
|
306
|
+
missing. Raises `OSError` when `west` itself cannot be launched."""
|
|
307
|
+
return subprocess.run(
|
|
308
|
+
plan.full_argv,
|
|
309
|
+
cwd=plan.run_cwd,
|
|
310
|
+
# tan-cli#992: re-applied at the literal spawn (`plan.env` was
|
|
311
|
+
# already built via `spawn_env` in `_plan_forward` -- this is
|
|
312
|
+
# defense-in-depth, and idempotent).
|
|
313
|
+
env=spawn_env(base=plan.env),
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
encoding="utf-8",
|
|
317
|
+
errors="replace",
|
|
318
|
+
check=False,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _sdk_resolution_issues(context: ProjectContext) -> list[Issue]:
|
|
323
|
+
"""The `sdk_resolution_issues` pair, computed from `context`'s own
|
|
324
|
+
top-level fields rather than `context.sdk` (tan-cli#958). `size`/`image`/
|
|
325
|
+
`clean` already read `context.broken_project_pin`/`context.sdk_source_tier`/
|
|
326
|
+
`context.foreign_global_default_for` directly for exactly this reason:
|
|
327
|
+
those three fields are carried on `ProjectContext` UNCONDITIONALLY --
|
|
328
|
+
unlike `context.sdk`, which is `None` the moment nothing resolved to a
|
|
329
|
+
usable checkout. Gating on `sdk is not None` (the seam's own
|
|
330
|
+
`_with_sdk_resolution_advisories`, and this module's own previous shape)
|
|
331
|
+
silently drops `sdk.project-pin-unresolved` in exactly the one case a
|
|
332
|
+
broken pin matters most: when the ladder falls all the way through and
|
|
333
|
+
NOTHING backstops it. `Envelope.__init__`'s seam still runs this same pair
|
|
334
|
+
off `context.sdk` when `context.sdk` is not `None` -- redundant then, but
|
|
335
|
+
deduplicated by code, so calling it here first and letting the seam run
|
|
336
|
+
second never double-emits.
|
|
337
|
+
|
|
338
|
+
Reads the top-level triple unconditionally, `size_cmd.py`/`image_cmd.py`'s
|
|
339
|
+
own established idiom, rather than branching on `context.sdk is not None`
|
|
340
|
+
to read the nested `SdkInfo` copy: `resolve_project_context` is the one
|
|
341
|
+
production construction site (`build_output.py`'s own
|
|
342
|
+
`ProjectContext.broken_project_pin` docstring -- "carried through") and
|
|
343
|
+
always writes both the nested triple and the top-level twins from the same
|
|
344
|
+
`active` resolution, so the two agree by construction in every real run
|
|
345
|
+
and a branch to pick between them is dead-equivalent there -- it only ever
|
|
346
|
+
mattered for a hand-built fixture that set one copy and not the other.
|
|
347
|
+
"""
|
|
348
|
+
return sdk_resolution_issues(
|
|
349
|
+
context.broken_project_pin, context.sdk_source_tier, context.foreign_global_default_for
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _echo_sdk_resolution(context: ProjectContext) -> None:
|
|
354
|
+
"""tan-cli#478/#958: the SDK-resolution pair on stderr, for the text paths.
|
|
355
|
+
|
|
356
|
+
Every `--format json` path in this module gets the pair for free from
|
|
357
|
+
`Envelope.__init__`'s central seam PLUS the direct `_sdk_resolution_issues`
|
|
358
|
+
call each JSON branch below now makes (tan-cli#958 -- the seam alone drops
|
|
359
|
+
the pair when nothing resolved). NO text path reaches `Envelope` at all --
|
|
360
|
+
neither `_run_text` (which hands stdio to the child) nor
|
|
361
|
+
`_refuse_required`'s refusal (which never builds an envelope) -- so both
|
|
362
|
+
call this instead of open-coding the loop, and this reads the same
|
|
363
|
+
unconditional `context` fields the JSON branches do rather than gating on
|
|
364
|
+
`context.sdk`, so a third text path cannot be added that discloses in JSON
|
|
365
|
+
and stays silent on the screen: that is precisely how `_refuse_required`
|
|
366
|
+
shipped silent while `_run_text` did not, and how all three shipped silent
|
|
367
|
+
on a broken pin with no working fallback.
|
|
368
|
+
"""
|
|
369
|
+
for issue in _sdk_resolution_issues(context):
|
|
370
|
+
typer.echo(issue.message, err=True)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _run_text(plan: _Forward, context: ProjectContext) -> None:
|
|
374
|
+
"""Inherit stdio: the child streams its own progress and may prompt.
|
|
375
|
+
|
|
376
|
+
tan-cli#478 review finding 8: `_plan` resolves `west_bin`/`workspace` off
|
|
377
|
+
`context.sdk.root` (see `_plan`'s own `sdk_path` line), so this command
|
|
378
|
+
SPAWNS `west` out of the resolved SDK root -- same shape as `validate`'s
|
|
379
|
+
spawn, worse consequence, since the child is a real build rather than a
|
|
380
|
+
schema check. The JSON envelope below already discloses a foreign
|
|
381
|
+
`globalDefault` via `Envelope.__init__`'s central seam. Text mode never
|
|
382
|
+
reaches `Envelope` at all (the child inherits stdio directly), so this
|
|
383
|
+
is printed BEFORE the spawn -- once stdio is handed to the child, tan
|
|
384
|
+
has no stream left of its own to write a warning to. Takes the whole
|
|
385
|
+
`context` (tan-cli#958), not just `context.sdk`: a broken pin with NO
|
|
386
|
+
working fallback resolves `context.sdk` to `None`, and that is exactly
|
|
387
|
+
the case `_echo_sdk_resolution` must still disclose -- it is also the
|
|
388
|
+
case where `west_program`/`_west_workspace_dir` below silently fall back
|
|
389
|
+
to a bare `west` on PATH and the project root, so this warning is the
|
|
390
|
+
only signal a text-mode caller gets that the wrong `west` is about to run.
|
|
391
|
+
"""
|
|
392
|
+
_echo_sdk_resolution(context)
|
|
393
|
+
try:
|
|
394
|
+
result = subprocess.run(
|
|
395
|
+
plan.full_argv, cwd=plan.run_cwd, env=spawn_env(base=plan.env), check=False
|
|
396
|
+
)
|
|
397
|
+
except OSError as err:
|
|
398
|
+
typer.echo(f"{plan.subcommand}: {_launch_error(err)}", err=True)
|
|
399
|
+
raise typer.Exit(int(ExitCode.RUNTIME_FAILURE)) from err
|
|
400
|
+
if result.returncode == 0:
|
|
401
|
+
typer.echo(f"{plan.subcommand}: complete.", err=True)
|
|
402
|
+
raise typer.Exit(0)
|
|
403
|
+
# The log IS above, but the code the child chose is not in it -- tan's own
|
|
404
|
+
# exit stays 1 on this path too (tan-cli#395), so naming the child's code is
|
|
405
|
+
# the only way a text-mode caller can tell a `7` from a `2`.
|
|
406
|
+
typer.echo(
|
|
407
|
+
f"{plan.subcommand}: `west {plan.west_command}` failed with exit code "
|
|
408
|
+
f"{result.returncode} (see log above).",
|
|
409
|
+
err=True,
|
|
410
|
+
)
|
|
411
|
+
raise typer.Exit(int(ExitCode.RUNTIME_FAILURE))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _refuse_required(
|
|
415
|
+
subcommand: str, code: str, message: str, context: ProjectContext, output_format: OutputFormat
|
|
416
|
+
) -> NoReturn:
|
|
417
|
+
"""tan-cli#454: `alp-quality --profile` and `alp-migrate`'s one-of
|
|
418
|
+
`--check`/`--preview`/`--apply` are REQUIRED by the child's own argparse,
|
|
419
|
+
with no default tan could pick on a caller's behalf (guessing a quality
|
|
420
|
+
profile, or guessing whether to write board.yaml, is guessing wrong for
|
|
421
|
+
someone). Neither was ever declared on tan's own Typer surface, so
|
|
422
|
+
`tan quality --help` never named `--profile` and every realistic run
|
|
423
|
+
without it reached `west`, which rejected it with its OWN argparse usage
|
|
424
|
+
error -- forwarded back to the caller as an opaque `<verb>.failed`,
|
|
425
|
+
indistinguishable from `west` itself failing, and undiscoverable from
|
|
426
|
+
`--help`. Refusing HERE, before `west` is ever spawned, fixes both: no
|
|
427
|
+
child process runs, and the coded issue below names the exact missing
|
|
428
|
+
flag. `VALIDATION_FAILURE` (2), matching what a Click-level required
|
|
429
|
+
option would report, but under the REAL command name and a real code --
|
|
430
|
+
the same "no generic `cli.parse-error` fallback" precedent
|
|
431
|
+
`new_som_cmd.py`'s own `new-som.failed` sets, not `command: "cli"`.
|
|
432
|
+
|
|
433
|
+
This refusal's own `data` -- `{"schemaVersion": "1"}` only -- is
|
|
434
|
+
deliberately narrower than `_run_forward`'s JSON branches below, which
|
|
435
|
+
fill in `westCommand`/`westCwd`/`args`/`westExitCode`/`stdout`/`stderr`
|
|
436
|
+
for every path that actually reaches `_plan()`. No `west` invocation was
|
|
437
|
+
ever planned here -- there is no `westCommand` to name, no `westCwd` to
|
|
438
|
+
resolve, no child argv to report -- so there is nothing to backfill;
|
|
439
|
+
`faultdecode_cmd.py`'s own `data: None` refusal is the established
|
|
440
|
+
precedent for a bare payload on this exact kind of pre-spawn refusal.
|
|
441
|
+
Annotated `NoReturn`, not `None`: every caller ends its own function at
|
|
442
|
+
this call (`raise typer.Exit` always fires), and an accurate signature is
|
|
443
|
+
what lets a type checker prove the return type of a caller like
|
|
444
|
+
`_resolve_quality_profile` sound instead of just trusting the docstring.
|
|
445
|
+
tan-cli#958: the resolution pair below is PREPENDED (`clean_cmd.py`'s own
|
|
446
|
+
ordering), no longer left to `Envelope.__init__`'s `sdk`-gated seam."""
|
|
447
|
+
resolution_issues = _sdk_resolution_issues(context)
|
|
448
|
+
if output_format == "json":
|
|
449
|
+
emit(Envelope(
|
|
450
|
+
subcommand, context.project(), {"schemaVersion": "1"},
|
|
451
|
+
[*resolution_issues, Issue(code, "error", message)],
|
|
452
|
+
ExitCode.VALIDATION_FAILURE, sdk=context.sdk,
|
|
453
|
+
))
|
|
454
|
+
else:
|
|
455
|
+
# tan-cli#478 (PR #504 review): this branch printed the refusal ALONE
|
|
456
|
+
# while the JSON branch above disclosed the SDK-resolution pair -- and
|
|
457
|
+
# a bare `tan quality` / `tan migrate` lands here, having resolved the
|
|
458
|
+
# checkout `_plan` would have run `west` out of. Order as `_run_text`.
|
|
459
|
+
_echo_sdk_resolution(context)
|
|
460
|
+
typer.echo(f"{subcommand}: {message}", err=True)
|
|
461
|
+
raise typer.Exit(int(ExitCode.VALIDATION_FAILURE))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _run_forward(
|
|
465
|
+
subcommand: str,
|
|
466
|
+
passthrough: list[str],
|
|
467
|
+
project: str | None,
|
|
468
|
+
board_yaml: str | None,
|
|
469
|
+
sdk_root: str | None,
|
|
470
|
+
output_format: OutputFormat,
|
|
471
|
+
extra_args: Sequence[str] = (),
|
|
472
|
+
) -> None:
|
|
473
|
+
"""`extra_args` is what tan CONSUMED and hands back to the child --
|
|
474
|
+
`migrate`'s `--all` and nothing else today (tan-cli#405). The JSON branch
|
|
475
|
+
stays inline rather than moving to its own helper: both `<verb>.failed`
|
|
476
|
+
sites have to be reachable from one function for
|
|
477
|
+
`tests/gates/test_every_issue_code_is_registered.py`'s prefix-template scan
|
|
478
|
+
to resolve them."""
|
|
479
|
+
context = resolve_project_context(project, board_yaml, sdk_root)
|
|
480
|
+
plan = _plan(subcommand, passthrough, extra_args, context)
|
|
481
|
+
if output_format != "json":
|
|
482
|
+
_run_text(plan, context)
|
|
483
|
+
return
|
|
484
|
+
# tan-cli#958: PREPENDED below (`clean_cmd.py`'s own ordering convention)
|
|
485
|
+
# -- see `_sdk_resolution_issues`'s own docstring for why this reads
|
|
486
|
+
# `context`, not `context.sdk`.
|
|
487
|
+
resolution_issues = _sdk_resolution_issues(context)
|
|
488
|
+
try:
|
|
489
|
+
out = _spawn_captured(plan)
|
|
490
|
+
except OSError as err:
|
|
491
|
+
# No child ran, so there is no code to report -- `null`, not a
|
|
492
|
+
# fabricated 1. The keys stay present so a consumer sees ONE shape on
|
|
493
|
+
# every branch THAT REACHES A PLAN (tan-cli#395) -- `_refuse_required`'s
|
|
494
|
+
# own, earlier refusal never computes one and carries none of these
|
|
495
|
+
# keys; see that function's own docstring for why that is a separate,
|
|
496
|
+
# narrower shape rather than an omission here.
|
|
497
|
+
data = {**plan.data(), "westExitCode": None, "stdout": "", "stderr": ""}
|
|
498
|
+
exit_code = ExitCode.RUNTIME_FAILURE
|
|
499
|
+
issues = [*resolution_issues, Issue(f"{subcommand}.failed", "error", _launch_error(err))]
|
|
500
|
+
else:
|
|
501
|
+
# `or ""`: this is the boundary where another process's output enters
|
|
502
|
+
# the envelope, and a `None` would serialise as `null` for a field a
|
|
503
|
+
# consumer reads as text.
|
|
504
|
+
stdout, stderr = out.stdout or "", out.stderr or ""
|
|
505
|
+
data = {**plan.data(), "westExitCode": out.returncode, "stdout": stdout, "stderr": stderr}
|
|
506
|
+
if out.returncode == 0:
|
|
507
|
+
exit_code, issues = ExitCode.SUCCESS, list(resolution_issues)
|
|
508
|
+
else:
|
|
509
|
+
exit_code = ExitCode.RUNTIME_FAILURE
|
|
510
|
+
message = _child_failure_message(plan.west_command, out.returncode, stderr)
|
|
511
|
+
issues = [*resolution_issues, Issue(f"{subcommand}.failed", "error", message)]
|
|
512
|
+
emit(Envelope(subcommand, context.project(), data, issues, exit_code, sdk=context.sdk))
|
|
513
|
+
raise typer.Exit(int(exit_code))
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
#: Why the catch-all parameter is `west_args` and not the obvious `args`
|
|
517
|
+
#: (tan-cli#405): `accept_global_flags` returns a `def wrapper(*args, **kwargs)`
|
|
518
|
+
#: carrying a synthesised `__signature__`, but Typer resolves each parameter's
|
|
519
|
+
#: type through `typing.get_type_hints(callback)`, which reads the WRAPPER's own
|
|
520
|
+
#: `__annotations__` -- where the name `args` already means the varargs tuple.
|
|
521
|
+
#: A parameter named `args` therefore came back annotated `object` instead of
|
|
522
|
+
#: `list[str]`, and Typer aborted with `RuntimeError: Type not yet supported`
|
|
523
|
+
#: for every one of these three commands. The metavar is pinned to `ARGS...`
|
|
524
|
+
#: so nothing about the CLI surface or the help text moves with the rename.
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _resolve_migrate_extra_args(
|
|
528
|
+
check: bool,
|
|
529
|
+
preview: bool,
|
|
530
|
+
apply_changes: bool,
|
|
531
|
+
all_boards: bool,
|
|
532
|
+
west_args: Sequence[str],
|
|
533
|
+
project: str | None,
|
|
534
|
+
board_yaml: str | None,
|
|
535
|
+
sdk_root: str | None,
|
|
536
|
+
output_format: OutputFormat,
|
|
537
|
+
) -> list[str]:
|
|
538
|
+
"""The `alp-migrate` argv `migrate()` forwards past its own required-flag
|
|
539
|
+
check -- split out so `migrate()` stays a short dispatcher (the module
|
|
540
|
+
size gate's own per-function budget counts a docstring's lines too).
|
|
541
|
+
`--all` is declared on `migrate()` for real -- BOTH a tan global and an
|
|
542
|
+
`alp-migrate` flag (tan-cli#405); folded in here, after the mode(s), to
|
|
543
|
+
keep the child's own meaning.
|
|
544
|
+
|
|
545
|
+
`--check`/`--preview`/`--apply` (tan-cli#454) are a REQUIRED mutually-
|
|
546
|
+
exclusive group on the child's own argparse. NONE selected refuses EARLY
|
|
547
|
+
via `_refuse_required` -- unless `_has_flag` (see `_target_args`) finds
|
|
548
|
+
one already in `west_args`, the documented escape hatch
|
|
549
|
+
(`tan migrate -- --check`), in which case `modes` is left empty rather
|
|
550
|
+
than duplicating the caller's own token. MORE than one selected on tan's
|
|
551
|
+
own surface (`tan migrate --check --preview`) used to build the
|
|
552
|
+
contradictory argv itself and spawn `west`, reporting the opaque
|
|
553
|
+
`migrate.failed` the absence-refusal exists to remove -- refused the same
|
|
554
|
+
way, with the same code, naming the other failure.
|
|
555
|
+
"""
|
|
556
|
+
mode_flags = (("--check", check), ("--preview", preview), ("--apply", apply_changes))
|
|
557
|
+
modes = [flag for flag, chosen in mode_flags if chosen]
|
|
558
|
+
if len(modes) > 1:
|
|
559
|
+
_refuse_required(
|
|
560
|
+
"migrate",
|
|
561
|
+
"migrate.mode-required",
|
|
562
|
+
"only one of `--check`, `--preview`, `--apply` may be given "
|
|
563
|
+
"(`west alp-migrate`).",
|
|
564
|
+
resolve_project_context(project, board_yaml, sdk_root),
|
|
565
|
+
output_format,
|
|
566
|
+
)
|
|
567
|
+
if not modes and not any(_has_flag(west_args, flag) for flag, _chosen in mode_flags):
|
|
568
|
+
_refuse_required(
|
|
569
|
+
"migrate",
|
|
570
|
+
"migrate.mode-required",
|
|
571
|
+
"one of `--check`, `--preview`, `--apply` is required (`west alp-migrate`).",
|
|
572
|
+
resolve_project_context(project, board_yaml, sdk_root),
|
|
573
|
+
output_format,
|
|
574
|
+
)
|
|
575
|
+
return [*modes, *(["--all"] if all_boards else [])]
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def migrate(
|
|
579
|
+
west_args: list[str] = typer.Argument(None, metavar="ARGS..."),
|
|
580
|
+
project: str = typer.Option(
|
|
581
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
582
|
+
),
|
|
583
|
+
board_yaml: str = typer.Option(
|
|
584
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
585
|
+
),
|
|
586
|
+
sdk_root: str = typer.Option(
|
|
587
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
588
|
+
),
|
|
589
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
590
|
+
all_boards: bool = typer.Option(
|
|
591
|
+
False,
|
|
592
|
+
"--all",
|
|
593
|
+
help="Migrate every board in the workspace (`west alp-migrate --all`).",
|
|
594
|
+
),
|
|
595
|
+
check: bool = typer.Option(
|
|
596
|
+
False,
|
|
597
|
+
"--check",
|
|
598
|
+
help="Report version drift; nonzero on drift (`west alp-migrate --check`).",
|
|
599
|
+
),
|
|
600
|
+
preview: bool = typer.Option(
|
|
601
|
+
False,
|
|
602
|
+
"--preview",
|
|
603
|
+
help="Unified diff + diagnostic-v1 JSON, no writes (`west alp-migrate --preview`).",
|
|
604
|
+
),
|
|
605
|
+
apply_changes: bool = typer.Option(
|
|
606
|
+
False, "--apply", help="Rewrite board.yaml in place (`west alp-migrate --apply`)."
|
|
607
|
+
),
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Migrate board.yaml to the current schema (`west alp-migrate`)."""
|
|
610
|
+
resolved_args = list(west_args or [])
|
|
611
|
+
extra_args = _resolve_migrate_extra_args(
|
|
612
|
+
check, preview, apply_changes, all_boards, resolved_args,
|
|
613
|
+
project, board_yaml, sdk_root, output_format,
|
|
614
|
+
)
|
|
615
|
+
_run_forward(
|
|
616
|
+
"migrate",
|
|
617
|
+
resolved_args,
|
|
618
|
+
project,
|
|
619
|
+
board_yaml,
|
|
620
|
+
sdk_root,
|
|
621
|
+
output_format,
|
|
622
|
+
extra_args=extra_args,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def lock(
|
|
627
|
+
west_args: list[str] = typer.Argument(None, metavar="ARGS..."),
|
|
628
|
+
project: str = typer.Option(
|
|
629
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
630
|
+
),
|
|
631
|
+
board_yaml: str = typer.Option(
|
|
632
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
633
|
+
),
|
|
634
|
+
sdk_root: str = typer.Option(
|
|
635
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
636
|
+
),
|
|
637
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
638
|
+
) -> None:
|
|
639
|
+
"""Pin/lock library dependencies (`west alp-lock`)."""
|
|
640
|
+
_run_forward("lock", list(west_args or []), project, board_yaml, sdk_root, output_format)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _resolve_quality_profile(
|
|
644
|
+
profile: str | None,
|
|
645
|
+
west_args: Sequence[str],
|
|
646
|
+
project: str | None,
|
|
647
|
+
board_yaml: str | None,
|
|
648
|
+
sdk_root: str | None,
|
|
649
|
+
output_format: OutputFormat,
|
|
650
|
+
) -> str | None:
|
|
651
|
+
"""The `--profile` value `quality()` forwards past its own required-flag
|
|
652
|
+
check -- split out so `quality()` itself stays a short dispatcher (see
|
|
653
|
+
`_resolve_migrate_extra_args`'s own docstring for why this split exists).
|
|
654
|
+
|
|
655
|
+
tan-cli#454: `alp_quality.py` declares `--profile` REQUIRED with a fixed
|
|
656
|
+
choice list (`_QUALITY_PROFILES`, mirrored verbatim), and no default tan
|
|
657
|
+
could pick on a caller's behalf. Absence refuses EARLY via
|
|
658
|
+
`_refuse_required` -- unless `_has_flag` (see `_target_args`) finds
|
|
659
|
+
`--profile` already sitting in `west_args`, the documented escape hatch
|
|
660
|
+
(`tan quality -- --profile quick`), in which case this returns `None`
|
|
661
|
+
rather than refusing: `quality()` never parsed that copy, so it is not
|
|
662
|
+
THIS function's own value, only proof the flag already reaches `west`
|
|
663
|
+
unaided -- appending a second, tan-supplied `--profile` behind it would
|
|
664
|
+
duplicate the flag. An out-of-list VALUE (not absence) still raises the
|
|
665
|
+
standard Click usage error via `typer.BadParameter`, the established
|
|
666
|
+
pattern `new_som_cmd.py` uses for its own choice-shaped flags
|
|
667
|
+
(`click_type=click.Choice(...)` crashes under this repo's pinned
|
|
668
|
+
typer/click pair -- see that module's own comment).
|
|
669
|
+
"""
|
|
670
|
+
if profile is not None and profile not in _QUALITY_PROFILES:
|
|
671
|
+
raise typer.BadParameter(
|
|
672
|
+
f"{profile!r} is not one of {', '.join(repr(c) for c in _QUALITY_PROFILES)}.",
|
|
673
|
+
param_hint="'--profile'",
|
|
674
|
+
)
|
|
675
|
+
if profile is None:
|
|
676
|
+
if _has_flag(west_args, "--profile"):
|
|
677
|
+
return None
|
|
678
|
+
_refuse_required(
|
|
679
|
+
"quality",
|
|
680
|
+
"quality.profile-required",
|
|
681
|
+
"`--profile` is required (`west alp-quality --profile "
|
|
682
|
+
f"{{{','.join(_QUALITY_PROFILES)}}}`).",
|
|
683
|
+
resolve_project_context(project, board_yaml, sdk_root),
|
|
684
|
+
output_format,
|
|
685
|
+
)
|
|
686
|
+
return profile
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def quality(
|
|
690
|
+
west_args: list[str] = typer.Argument(None, metavar="ARGS..."),
|
|
691
|
+
project: str = typer.Option(
|
|
692
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
693
|
+
),
|
|
694
|
+
board_yaml: str = typer.Option(
|
|
695
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
696
|
+
),
|
|
697
|
+
sdk_root: str = typer.Option(
|
|
698
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
699
|
+
),
|
|
700
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
701
|
+
profile: str = typer.Option(
|
|
702
|
+
None,
|
|
703
|
+
"--profile",
|
|
704
|
+
metavar="{quick,pr,full,release}",
|
|
705
|
+
help="Quality profile to run (`west alp-quality --profile`; REQUIRED).",
|
|
706
|
+
),
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Run the board.yaml quality checks (`west alp-quality`)."""
|
|
709
|
+
resolved_args = list(west_args or [])
|
|
710
|
+
profile = _resolve_quality_profile(
|
|
711
|
+
profile, resolved_args, project, board_yaml, sdk_root, output_format
|
|
712
|
+
)
|
|
713
|
+
_run_forward(
|
|
714
|
+
"quality",
|
|
715
|
+
resolved_args,
|
|
716
|
+
project,
|
|
717
|
+
board_yaml,
|
|
718
|
+
sdk_root,
|
|
719
|
+
output_format,
|
|
720
|
+
# `None`: the caller already supplied `--profile` through the
|
|
721
|
+
# `ARGS...` escape hatch, and it is already in `resolved_args` --
|
|
722
|
+
# appending it again would duplicate the flag argparse then rejects.
|
|
723
|
+
extra_args=["--profile", profile] if profile is not None else [],
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# tan-cli#405: these were the last three commands not to accept the oracle's
|
|
728
|
+
# global set, and the only three where NOT accepting a flag meant handing it to
|
|
729
|
+
# a different program rather than refusing it -- `ignore_unknown_options` routes
|
|
730
|
+
# an unknown flag into the `ARGS...` catch-all, and that catch-all is the
|
|
731
|
+
# child's argv. Applied here, at the definition, for the reason
|
|
732
|
+
# `accept_global_flags` documents: by the time `cli.py` registers the command,
|
|
733
|
+
# the Click `Command` Typer built is already final.
|
|
734
|
+
migrate = accept_global_flags(migrate)
|
|
735
|
+
lock = accept_global_flags(lock)
|
|
736
|
+
quality = accept_global_flags(quality)
|