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,2236 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan build` -- the one command that turns a build plan into firmware.
|
|
3
|
+
|
|
4
|
+
Composition, not logic: this module resolves the project, ACQUIRES a plan (from
|
|
5
|
+
`--plan-from`, else by running the planner in-process -- there is no other way any
|
|
6
|
+
more; see `_emit_plan`), then hands it in turn to the four sub-project-1
|
|
7
|
+
libraries -- `parse_build_plan`, `apply_plan_token_substitution`,
|
|
8
|
+
`materialise_plan`, `execute_slices` -- and
|
|
9
|
+
folds the result into one envelope. Every decision those modules own stays
|
|
10
|
+
theirs; what lives here is the ORDER they run in, the mapping from their
|
|
11
|
+
`(code, message)` failures to an exit code, and the guarantee that not one of
|
|
12
|
+
them can escape as a traceback.
|
|
13
|
+
|
|
14
|
+
Three properties this file exists to hold:
|
|
15
|
+
|
|
16
|
+
**Order is the contract (I-20).** `materialise_plan` writes every
|
|
17
|
+
`sharedArtefacts` entry AND every slice's `configArtefacts` before
|
|
18
|
+
`execute_slices` is called at all. The plan carries no `sequential` flag and
|
|
19
|
+
no `inputHash`; the only thing making its slices independent is that all their
|
|
20
|
+
inputs are on disk before the first one starts. `tests/commands/
|
|
21
|
+
test_build_command.py` proves it by making the first slice's own command
|
|
22
|
+
assert the OTHER slice's config artefact already exists.
|
|
23
|
+
|
|
24
|
+
**Nothing but the envelope on stdout.** Slice output streams to stderr (see
|
|
25
|
+
`_stream`), the text-mode recap goes to stderr, and the JSON envelope is the
|
|
26
|
+
only thing ever written to stdout. The extension parses stdout whole; one
|
|
27
|
+
stray byte and it renders nothing, with no error on either side.
|
|
28
|
+
|
|
29
|
+
**`--plan-from` is a plan SOURCE, not a build trigger (v0.4.1 §gate).** The
|
|
30
|
+
oracle's `--plan-from` help says "Implies `--plan`", and measured against the
|
|
31
|
+
shipped `tan 0.4.1-dev` binary in fresh temp dirs, on
|
|
32
|
+
`tests/parity/oracle/multicore_rpmsg-aen.build-plan.json`, it means it -- even
|
|
33
|
+
against an EXPLICIT `--native`::
|
|
34
|
+
|
|
35
|
+
build --plan-from p.json -> rc 0, 0 files, data = the plan
|
|
36
|
+
build --plan-from p.json --native -> rc 0, 0 files, data = the plan
|
|
37
|
+
build --plan-from p.json --materialise -> rc 0, 6 files,
|
|
38
|
+
data = {schemaVersion, baseDir, written}
|
|
39
|
+
|
|
40
|
+
All three are reproduced here exactly (`_MODE_PLAN` / `_MODE_MATERIALISE`),
|
|
41
|
+
`--native` included. Two earlier revisions of this file each redefined one of
|
|
42
|
+
those argvs -- a bare `--plan-from` used to materialise AND dispatch, and then
|
|
43
|
+
`--plan-from --native` did -- so a v0.4.1 script that only ever INSPECTED a
|
|
44
|
+
plan silently began writing six files into its own tree. Changing what a
|
|
45
|
+
v0.4.1 argv MEANS is the whole surprise this gate exists to remove.
|
|
46
|
+
|
|
47
|
+
**`--execute` is a deliberate PORT ADDITION the oracle has no flag for.**
|
|
48
|
+
Taking a pinned, reviewed plan file and running it reproducibly is a normal
|
|
49
|
+
hermetic-CI request; v0.4.1's inability to do it is a LIMITATION of that CLI,
|
|
50
|
+
not a contract worth preserving -- there `--plan-from`'s implied `--plan`
|
|
51
|
+
outranks `--native`, so a file-supplied plan cannot be dispatched at all.
|
|
52
|
+
`--execute` supplies exactly that capability without changing what any v0.4.1
|
|
53
|
+
argv means. It is not a parity bug and not a test hook: a future reader who
|
|
54
|
+
"fixes" it by deleting it removes a supported capability. Its `data` is the
|
|
55
|
+
ORDINARY build shape -- `{schemaVersion, baseDir, slices[], warnings[]}` --
|
|
56
|
+
because it IS a native build and only the plan's SOURCE differs; `written` is
|
|
57
|
+
deliberately omitted, being byte-for-byte the pinned plan's own declared
|
|
58
|
+
artefact paths, which the one caller who has that file already holds. A
|
|
59
|
+
conflicting combination is refused with its own code, never resolved by silent
|
|
60
|
+
precedence (`--execute --materialise` -> `build.conflicting-flags`, exit 2;
|
|
61
|
+
`--pristine` with a mode that never dispatches (bare `--plan-from` or
|
|
62
|
+
`--materialise`, neither paired with `--execute`) -> the same
|
|
63
|
+
`build.conflicting-flags`, exit 2, because a wipe promised on the envelope
|
|
64
|
+
cannot be silently skipped either; `--execute` with a RETIRED flag ->
|
|
65
|
+
`build.flag-retired`, exit 2, because a flag this build refuses outright
|
|
66
|
+
cannot be honoured either way).
|
|
67
|
+
`--execute` is ADDED BY THIS PORT, not a v0.4.1 flag: there `--plan-from`
|
|
68
|
+
implies `--plan` and outranks `--native`, so a file-supplied plan cannot be
|
|
69
|
+
dispatched at all. Deliberate, not a parity gap. `--native` keeps v0.4.1's
|
|
70
|
+
behaviour of NOT overriding the `--plan` that `--plan-from` implies, which
|
|
71
|
+
`test_plan_from_shows_the_plan_and_writes_nothing_even_with_native` pins.
|
|
72
|
+
|
|
73
|
+
**The OS is not an option (I-01/I-02).** There is deliberately no `--os` and
|
|
74
|
+
no `--backend` flag. A core's runtime is derived from its Cortex class by the
|
|
75
|
+
planner (`tan/planner/topology.py`), and `slices[].backend`
|
|
76
|
+
arrives already resolved. Nor does this command filter the plan's slices: a
|
|
77
|
+
one-core `board.yaml` legitimately plans three (I-04), and "helpfully"
|
|
78
|
+
dropping the ones the customer did not name is how a Yocto slice silently
|
|
79
|
+
stops being built.
|
|
80
|
+
"""
|
|
81
|
+
import json
|
|
82
|
+
import os
|
|
83
|
+
import sys
|
|
84
|
+
import threading
|
|
85
|
+
import time
|
|
86
|
+
from dataclasses import dataclass, replace
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
import typer
|
|
90
|
+
|
|
91
|
+
from tan.commands.build.execute import (
|
|
92
|
+
KNOWN_BACKENDS,
|
|
93
|
+
SliceOutcome,
|
|
94
|
+
execute_slices,
|
|
95
|
+
last_configure_cache_issues,
|
|
96
|
+
last_manifest_write_failure,
|
|
97
|
+
last_sdk_switch_issues,
|
|
98
|
+
reset_last_manifest_write,
|
|
99
|
+
)
|
|
100
|
+
from tan.commands.build.materialise import MaterialiseError, materialise_plan
|
|
101
|
+
from tan.commands.build.token_substitution import (
|
|
102
|
+
TokenSubstitutionError,
|
|
103
|
+
apply_plan_token_substitution,
|
|
104
|
+
deferred_placeholder_issues,
|
|
105
|
+
)
|
|
106
|
+
from tan.commands.build.workspace_patches import workspace_patch_issues
|
|
107
|
+
from tan.commands.build.toolchain import ToolchainResolution, resolve_toolchain_root
|
|
108
|
+
from tan.core.user_defines import UserDefineError, apply_user_defines, define_pairs, user_defines_problem
|
|
109
|
+
from tan.core.board_files import all_topology_boards, unmatched_board_file_messages
|
|
110
|
+
from tan.core.plain_zephyr_plan import board_target_problem, normalise_defines, plain_zephyr_plan
|
|
111
|
+
from tan.core.build_plan import BuildPlan, PlanParseError, parse_build_plan
|
|
112
|
+
from tan.core.global_flags import accept_global_flags
|
|
113
|
+
from tan.core.link_refusal import refusal_code
|
|
114
|
+
from tan.core.plan_exec import (
|
|
115
|
+
CROSS_DRIVE_MSG,
|
|
116
|
+
MISSING_TOOL_RE,
|
|
117
|
+
PolicyAction,
|
|
118
|
+
normalize_path,
|
|
119
|
+
resolve_action,
|
|
120
|
+
)
|
|
121
|
+
from tan.core.west_workspace_refusal import WORKSPACE_UNRESOLVED_MSG
|
|
122
|
+
from tan.commands.build.host_python import refusal_message, resolve_build_python
|
|
123
|
+
from tan.core.plan_tokens import TOKEN_TOOLCHAIN_ROOT, DeferredPlaceholder
|
|
124
|
+
from tan.core.sdk_discovery import (
|
|
125
|
+
_abs_posix,
|
|
126
|
+
global_default_foreign_project_issue,
|
|
127
|
+
project_pin_issue,
|
|
128
|
+
resolve_sdk_root_ladder,
|
|
129
|
+
sdk_ladder_divergence_issue,
|
|
130
|
+
with_sdk_search,
|
|
131
|
+
)
|
|
132
|
+
from tan.core.shapes import SDK_MARKER, is_sdk_root, rejected_sdk_root_message
|
|
133
|
+
from tan.env import stderr_is_tty, terminal_width
|
|
134
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
135
|
+
from tan.exit_codes import ExitCode
|
|
136
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
137
|
+
|
|
138
|
+
#: `scripts/alp_project.py` is THE marker for an alp-sdk checkout -- the same
|
|
139
|
+
#: literal `tan_core::project` hardcodes (I-31). Renaming or relocating it
|
|
140
|
+
#: breaks every `--sdk-root` resolution in the CLI, so it is spelled once --
|
|
141
|
+
#: in `tan.core.shapes` since tan-cli#408. The import below re-exports it
|
|
142
|
+
#: here because a dozen modules already reach it through this one.
|
|
143
|
+
|
|
144
|
+
#: Envelope `data.slices[].status`, from `SliceOutcome.status`. The wire
|
|
145
|
+
#: vocabulary is the Rust one (`ok` / `skipped` / `failed`), NOT the executor's
|
|
146
|
+
#: internal spelling -- `succeeded` on the wire would break a consumer written
|
|
147
|
+
#: against the shipped binary. `cancelled` is unreachable from this command
|
|
148
|
+
#: (nothing here supplies a cancellation source) but maps to `failed` rather
|
|
149
|
+
#: than falling through to a KeyError if one is ever wired up.
|
|
150
|
+
_WIRE_STATUS = {"succeeded": "ok", "skipped": "skipped"}
|
|
151
|
+
|
|
152
|
+
#: What this run does with the plan it acquires -- the oracle's own precedence,
|
|
153
|
+
#: measured flag-combination by flag-combination against `target/debug/tan.exe`
|
|
154
|
+
#: on the AEN fixture: `--materialise` outranks everything (`--plan-from
|
|
155
|
+
#: --materialise --native` -> six files, `written` shape), the `--plan` implied
|
|
156
|
+
#: by `--plan-from` outranks even an explicit `--native` (`--plan-from
|
|
157
|
+
#: --native` -> the plan, zero files), and only a run naming NEITHER plan-mode
|
|
158
|
+
#: flag builds. `_MODE_NATIVE` is therefore both the default and what the
|
|
159
|
+
#: port-added `--execute` selects -- deliberately the SAME mode, so `--execute`
|
|
160
|
+
#: reports the same `data` shape a plain `tan build` does rather than a fourth
|
|
161
|
+
#: one no consumer is written for.
|
|
162
|
+
_MODE_PLAN = "plan"
|
|
163
|
+
_MODE_MATERIALISE = "materialise"
|
|
164
|
+
_MODE_NATIVE = "native"
|
|
165
|
+
|
|
166
|
+
#: The ONE top-level directory name a plan's `buildRoot` may name (tan-cli#566),
|
|
167
|
+
#: `tan_core::build_plan::CONSUMER_BUILD_ROOT` in the oracle. Duplicated from
|
|
168
|
+
#: `tan.commands.build.execute._CONSUMER_BUILD_ROOT` rather than imported, for
|
|
169
|
+
#: the reason that module's own copy already records: it is the SDK build-plan
|
|
170
|
+
#: schema's constant, not a value derived from anything in either module, and
|
|
171
|
+
#: the two uses are unrelated (that one gates the sdk-switch-pristine wipe on a
|
|
172
|
+
#: slice's plan-supplied `cwd`; this one refuses the whole plan). Importing a
|
|
173
|
+
#: neighbour's underscore-private name to save four characters would couple
|
|
174
|
+
#: this refusal to that guard's internals.
|
|
175
|
+
CONSUMER_BUILD_ROOT = "build"
|
|
176
|
+
|
|
177
|
+
#: tan-cli#427 resolved every flag `tan build` used to lump into one
|
|
178
|
+
#: refuse-everything tuple, ONE of three ways:
|
|
179
|
+
#:
|
|
180
|
+
#: 1. **Implemented for real** -- `--pristine` (see
|
|
181
|
+
#: `_maybe_pristine_stale_sdk_build_dir`'s `force_pristine`) and the
|
|
182
|
+
#: `--target`/`--all`/`--verbose`/`--quiet`/`--no-color`/
|
|
183
|
+
#: `--non-interactive`/`--ci` septet, which the oracle's own `cli.rs`
|
|
184
|
+
#: declares ONLY on the shared `GlobalArgs` struct (`BuildArgs` itself has
|
|
185
|
+
#: no fields for any of the seven) -- `build`'s Rust handler never reads
|
|
186
|
+
#: them either, so accept-and-drop via `accept_global_flags` (see the
|
|
187
|
+
#: bottom of this module) is not a narrower stand-in, it is the SAME
|
|
188
|
+
#: oracle behaviour every other command already gets from that decorator.
|
|
189
|
+
#: This also settles the "does `--target`/`--all` on `build` mean
|
|
190
|
+
#: `generate`'s emit-backend selection" question the issue raised: it does
|
|
191
|
+
#: not mean anything on either binary -- `generate`'s own handler is the
|
|
192
|
+
#: only one that ever reads the global field, so build and generate
|
|
193
|
+
#: sharing the spelling was never a meaning collision to resolve.
|
|
194
|
+
#: 2. **Retired as superseded** -- `--plan`, `--manifest`, `--manifest-from`:
|
|
195
|
+
#: see `_RETIRED_FLAGS` below. Still declared (never a Click parse error),
|
|
196
|
+
#: but refused with `build.flag-retired`, which names the replacement in
|
|
197
|
+
#: the message itself.
|
|
198
|
+
#: 3. **Nothing is left deferred.** `--no-auto-bootstrap` was the last one,
|
|
199
|
+
#: and it is now retired (bucket 2) rather than implemented, on a
|
|
200
|
+
#: maintainer decision closing tan-cli#427: `tan build` does not run
|
|
201
|
+
#: `tan bootstrap` implicitly and is not going to. Measured before
|
|
202
|
+
#: deciding -- no call path from `build` or `run` reaches bootstrap;
|
|
203
|
+
#: an unbootstrapped workspace surfaces as `build.plan-unavailable`
|
|
204
|
+
#: (`_emit_plan`'s `SystemExit` catch), never an automatic invocation.
|
|
205
|
+
#: So there was never a trigger for the flag to disable, and building one
|
|
206
|
+
#: just to give the flag something to switch off would add a heavy
|
|
207
|
+
#: implicit side effect (a `west update` and a venv install) to a command
|
|
208
|
+
#: whose job is to build. A flag that disables a behaviour this CLI does
|
|
209
|
+
#: not have is not deferred work -- it is a flag with nothing to mean, so
|
|
210
|
+
#: it teaches the explicit command instead.
|
|
211
|
+
|
|
212
|
+
#: The oracle spelling -> its replacement flag, for the three flags
|
|
213
|
+
#: `_refuse_retired` reports (tan-cli#427): "Two overlapping plan surfaces is
|
|
214
|
+
#: worse than one, so the oracle spellings go" (the issue's own maintainer
|
|
215
|
+
#: decision), but a retired flag's error must TEACH the replacement, not just
|
|
216
|
+
#: refuse -- a customer who typed `--plan` is told to type `--plan-from`, in
|
|
217
|
+
#: the message itself, never pointed at a tracker to work it out.
|
|
218
|
+
#:
|
|
219
|
+
#: `--plan` (bare, live-fetched) has no direct one-flag substitute -- `
|
|
220
|
+
#: --plan-from FILE` covers the file-sourced half of what it did, with
|
|
221
|
+
#: `--materialise`/`--execute` covering the act-on-it half; there is no
|
|
222
|
+
#: "preview the live plan and stop" mode left, which is the trade the
|
|
223
|
+
#: decision accepts in exchange for one plan surface instead of two.
|
|
224
|
+
#: `--manifest`/`--manifest-from` showed `build/system-manifest.yaml`
|
|
225
|
+
#: (a DIFFERENT artefact from the build plan) before or without a real
|
|
226
|
+
#: build; a native `tan build` already writes that exact file, and it is
|
|
227
|
+
#: plain YAML a caller can read directly -- no second command needed.
|
|
228
|
+
_RETIRED_FLAGS: dict[str, str] = {
|
|
229
|
+
"--plan": (
|
|
230
|
+
"`--plan` is retired: run `tan build --plan-from FILE` to inspect a build plan "
|
|
231
|
+
"instead (add `--materialise` or `--execute` to act on it)."
|
|
232
|
+
),
|
|
233
|
+
"--manifest": (
|
|
234
|
+
"`--manifest` is retired: a native `tan build` already writes "
|
|
235
|
+
"`build/system-manifest.yaml` for you to read directly -- no separate flag needed."
|
|
236
|
+
),
|
|
237
|
+
"--manifest-from": (
|
|
238
|
+
"`--manifest-from FILE` is retired: `system-manifest.yaml` is plain YAML -- open "
|
|
239
|
+
"FILE directly instead of routing it through `tan build`."
|
|
240
|
+
),
|
|
241
|
+
# Retired rather than implemented (tan-cli#427): there is no implicit
|
|
242
|
+
# bootstrap for it to switch off, and there is not going to be one, so the
|
|
243
|
+
# message teaches the explicit command rather than describing a toggle.
|
|
244
|
+
"--no-auto-bootstrap": (
|
|
245
|
+
"`--no-auto-bootstrap` is retired: `tan build` never bootstraps implicitly, so "
|
|
246
|
+
"there is nothing for it to disable -- run `tan bootstrap` yourself when a "
|
|
247
|
+
"workspace needs preparing."
|
|
248
|
+
),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class BuildError(Exception):
|
|
253
|
+
"""A build failure with its issue code and exit code already decided --
|
|
254
|
+
the shape every step in `_build` reports through, so the caller never has
|
|
255
|
+
to guess a code from an exception type."""
|
|
256
|
+
|
|
257
|
+
def __init__(self, code: str, message: str, exit_code: ExitCode) -> None:
|
|
258
|
+
super().__init__(message)
|
|
259
|
+
self.code = code
|
|
260
|
+
self.message = message
|
|
261
|
+
self.exit_code = exit_code
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _stream(line: str) -> None:
|
|
265
|
+
"""Slice output goes to stderr -- ALWAYS, both formats. stdout is the
|
|
266
|
+
envelope channel and carries nothing else; stderr carries no contract at
|
|
267
|
+
all, so there is nothing to keep separate between the two modes."""
|
|
268
|
+
print(line, file=sys.stderr, flush=True)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
#: Idle time -- since the last line ANY slice printed, or since dispatch
|
|
272
|
+
#: began if none has arrived yet -- before [`_Heartbeat`] starts announcing
|
|
273
|
+
#: it is still alive. Measured against tan-cli#287: a first-build CMake
|
|
274
|
+
#: configure sat fully silent for 234.2s; a warm-cache rebuild (21.1s) prints
|
|
275
|
+
#: output throughout and never crosses this. Short enough to catch the
|
|
276
|
+
#: former, long enough that ordinary tool-startup chatter never trips it.
|
|
277
|
+
_HEARTBEAT_SILENCE_THRESHOLD_S = 5.0
|
|
278
|
+
#: Refresh cadence once armed -- a live, in-place counter (`\r`, no
|
|
279
|
+
#: trailing newline), not a growing log.
|
|
280
|
+
_HEARTBEAT_TICK_S = 1.0
|
|
281
|
+
#: `tan.env.terminal_width`'s fallback when stderr's column count can't be
|
|
282
|
+
#: read at all. Should not be reachable -- `_Heartbeat` is only ever armed
|
|
283
|
+
#: behind an `isatty()` gate -- but naming a value here is still cheaper
|
|
284
|
+
#: than trusting that gate never has a gap.
|
|
285
|
+
_HEARTBEAT_WIDTH_FALLBACK = 80
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _heartbeat_line_width() -> int:
|
|
289
|
+
"""Usable columns for one self-overwriting heartbeat line: the
|
|
290
|
+
terminal's own width, minus one.
|
|
291
|
+
|
|
292
|
+
tan-cli#287 blocker: the erase pad used to be a HARDCODED 88 columns
|
|
293
|
+
while the message it erased ran 114+ chars (more once elapsed hits
|
|
294
|
+
1000s). On a terminal narrower than 114 columns -- 80 is the common
|
|
295
|
+
case -- `\r` rewinds only the terminal's OWN last row, so a message
|
|
296
|
+
that wraps grows a fresh row every tick instead of overwriting one:
|
|
297
|
+
~234 stacked "still building" rows over the measured 234.2s configure.
|
|
298
|
+
Sizing the message AND its erase pad off the actual terminal width,
|
|
299
|
+
every tick, removes the mismatch instead of guessing a bigger constant.
|
|
300
|
+
The `- 1` leaves the last column unwritten: filling a line to the exact
|
|
301
|
+
terminal width is what triggers autowrap on many terminals, which would
|
|
302
|
+
make the fix for the wrap defect one more way to trigger it.
|
|
303
|
+
|
|
304
|
+
tan-cli#564: measured through [`terminal_width`], which reads stderr's own
|
|
305
|
+
fd -- the handle the heartbeat is armed on and writes to. The previous
|
|
306
|
+
`shutil.get_terminal_size` call resolved `sys.__stdout__` instead, so
|
|
307
|
+
`tan build > log.txt` on that same 70-column terminal returned 79 rather
|
|
308
|
+
than 69 and `_tick`'s `message.ljust(width)` wrote a 79-char row onto a
|
|
309
|
+
70-column screen: it soft-wraps, `\r` rewinds only the last physical row,
|
|
310
|
+
and the "still building" line stacks a fresh row per tick -- exactly the
|
|
311
|
+
tan-cli#287 defect this width computation was added to remove.
|
|
312
|
+
|
|
313
|
+
# ponytail: re-reads the width every tick rather than caching it, so a
|
|
314
|
+
# terminal resized mid-build can leave a stale tick's tail past the
|
|
315
|
+
# newly narrower edge; not the reported defect (a STATIC undersized
|
|
316
|
+
# pad on an unchanged terminal) and not measured here -- revisit if a
|
|
317
|
+
# resize-mid-build report ever lands.
|
|
318
|
+
"""
|
|
319
|
+
columns = terminal_width(_HEARTBEAT_WIDTH_FALLBACK)
|
|
320
|
+
return max(columns - 1, 1)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class _Heartbeat:
|
|
324
|
+
"""The `on_output` callback `_dispatch` constructs and hands
|
|
325
|
+
`execute_slices` directly: relays every line to [`_stream`] like
|
|
326
|
+
before, and -- TTY + text-mode only -- prints a single self-overwriting
|
|
327
|
+
"still building" line once the child has gone quiet for
|
|
328
|
+
`_HEARTBEAT_SILENCE_THRESHOLD_S` (tan-cli#287: a four-minute silent
|
|
329
|
+
CMake configure read as a hang with nothing to say otherwise). The
|
|
330
|
+
instant real output resumes, the line is blanked so it never mixes
|
|
331
|
+
with the stream it was standing in for.
|
|
332
|
+
|
|
333
|
+
Armed by [`_dispatch`] itself, not handed down by its caller: `_build`'s
|
|
334
|
+
two callers -- `build_cmd.build` and `run_cmd._run` -- both
|
|
335
|
+
bottleneck through `_dispatch` before any slice runs, so constructing
|
|
336
|
+
the heartbeat there covers `tan run`'s identical silent-CMake window
|
|
337
|
+
too, not just a direct `tan build` (tan-cli#287 follow-up).
|
|
338
|
+
|
|
339
|
+
`execute_slices` calls `on_output` once per line and has no hook for
|
|
340
|
+
"a slice started" or "configure ended, compile began" -- adding one
|
|
341
|
+
would mean editing `tan.commands.build.execute`, outside this file's
|
|
342
|
+
scope -- so this cannot name PHASES, only measure silence across the
|
|
343
|
+
whole dispatch and say tan is still alive.
|
|
344
|
+
|
|
345
|
+
# ponytail: one wall-clock timer for the whole dispatch, not a per-slice
|
|
346
|
+
# or per-phase one -- upgrade to a per-slice "building <core>" line if
|
|
347
|
+
# `execute_slices` ever grows a slice-start hook.
|
|
348
|
+
|
|
349
|
+
A disabled instance (`enabled=False` -- not a TTY, or `--format json`)
|
|
350
|
+
still relays every line via `__call__`, but never starts its thread and
|
|
351
|
+
so never prints anything of its own: behaviourally identical to passing
|
|
352
|
+
`_stream` straight through."""
|
|
353
|
+
|
|
354
|
+
def __init__(self, enabled: bool) -> None:
|
|
355
|
+
self._enabled = enabled
|
|
356
|
+
self._start = time.monotonic()
|
|
357
|
+
self._last_output = self._start
|
|
358
|
+
self._lock = threading.Lock()
|
|
359
|
+
self._stop = threading.Event()
|
|
360
|
+
self._thread: threading.Thread | None = None
|
|
361
|
+
self._line_up = False
|
|
362
|
+
|
|
363
|
+
def __call__(self, line: str) -> None:
|
|
364
|
+
# Clear any live heartbeat line FIRST -- `_stream`'s `print` commits
|
|
365
|
+
# a trailing newline, so blanking AFTER it would scroll the
|
|
366
|
+
# heartbeat text away merged onto the same line as real output
|
|
367
|
+
# instead of erasing it.
|
|
368
|
+
self.note_output()
|
|
369
|
+
_stream(line)
|
|
370
|
+
|
|
371
|
+
def note_output(self) -> None:
|
|
372
|
+
with self._lock:
|
|
373
|
+
self._last_output = time.monotonic()
|
|
374
|
+
self._clear_locked()
|
|
375
|
+
|
|
376
|
+
def _clear_locked(self) -> None:
|
|
377
|
+
if self._line_up:
|
|
378
|
+
width = _heartbeat_line_width()
|
|
379
|
+
print("\r" + " " * width + "\r", end="", file=sys.stderr, flush=True)
|
|
380
|
+
self._line_up = False
|
|
381
|
+
|
|
382
|
+
def _tick(self) -> None:
|
|
383
|
+
while not self._stop.wait(_HEARTBEAT_TICK_S):
|
|
384
|
+
with self._lock:
|
|
385
|
+
idle = time.monotonic() - self._last_output
|
|
386
|
+
if idle < _HEARTBEAT_SILENCE_THRESHOLD_S:
|
|
387
|
+
continue
|
|
388
|
+
elapsed = time.monotonic() - self._start
|
|
389
|
+
width = _heartbeat_line_width()
|
|
390
|
+
# Truncated AND right-padded to `width`, both to the SAME
|
|
391
|
+
# value: truncation keeps this tick's own text off the
|
|
392
|
+
# terminal's last column (see `_heartbeat_line_width`), and
|
|
393
|
+
# the pad overwrites a longer PRIOR tick's tail (elapsed/
|
|
394
|
+
# idle growing from single to double digits, say) -- both
|
|
395
|
+
# ticks compute the same width, so the pad always covers it
|
|
396
|
+
# on an unchanged terminal.
|
|
397
|
+
message = (
|
|
398
|
+
f"... still building ({elapsed:.0f}s elapsed, no output for "
|
|
399
|
+
f"{idle:.0f}s -- CMake configure can take several minutes on a "
|
|
400
|
+
"first build)"
|
|
401
|
+
)[:width]
|
|
402
|
+
print("\r" + message.ljust(width), end="", file=sys.stderr, flush=True)
|
|
403
|
+
self._line_up = True
|
|
404
|
+
|
|
405
|
+
def __enter__(self) -> "_Heartbeat":
|
|
406
|
+
if self._enabled:
|
|
407
|
+
self._thread = threading.Thread(target=self._tick, daemon=True)
|
|
408
|
+
self._thread.start()
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
412
|
+
self._stop.set()
|
|
413
|
+
if self._thread is not None:
|
|
414
|
+
self._thread.join(timeout=_HEARTBEAT_TICK_S * 2)
|
|
415
|
+
with self._lock:
|
|
416
|
+
self._clear_locked()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _emit_plan(sdk_root: str | None, board_yaml: str | None) -> str:
|
|
420
|
+
"""Plan `board_yaml` against the SDK at `sdk_root`.
|
|
421
|
+
|
|
422
|
+
IN-PROCESS, always. `tan.planner_root.emit` binds the SDK root -- which is
|
|
423
|
+
where `metadata/**` and the fact-reader modules still live (ADR-0017) -- and
|
|
424
|
+
renders through the same `emit_artefact` dispatch `python -m tan.planner`
|
|
425
|
+
uses. No interpreter on PATH, no PYTHONPATH, and nothing under
|
|
426
|
+
`<sdk>/scripts` imported or spawned -- but NOT "no process" full stop:
|
|
427
|
+
`emit_build_plan` still spawns a short-lived `git rev-parse` (a stamped
|
|
428
|
+
`sdkCommit`, `buildplan.py:339-372`) whenever `git` resolves on PATH; with
|
|
429
|
+
none, it returns `None` first and the plan carries `sdkCommit: null`.
|
|
430
|
+
|
|
431
|
+
**The `<python> -m alp_orchestrate --emit build-plan` fallback was RETIRED
|
|
432
|
+
here, deliberately.** It fired when this build of `tan` could not import the
|
|
433
|
+
planner and quietly planned through the SDK's own copy instead -- and it
|
|
434
|
+
reported nothing at all when it did. Three reasons it had to go, any one
|
|
435
|
+
sufficient:
|
|
436
|
+
|
|
437
|
+
* It was SILENT. A run that fell back looked exactly like a run that did
|
|
438
|
+
not, which is the same defect class that let the planner relocation be
|
|
439
|
+
believed before it happened.
|
|
440
|
+
* It could only fire on a broken install. `jsonschema` and `PyYAML` are
|
|
441
|
+
declared `dependencies` in `pyproject.toml` and installed into the clean
|
|
442
|
+
venv `scripts/build_binary.sh` freezes, so the trigger is not a supported
|
|
443
|
+
layout -- and a missing dependency deserves `pip install jsonschema`, not
|
|
444
|
+
a different planner.
|
|
445
|
+
* It kept alp-sdk's `scripts/alp_orchestrate/` load-bearing forever. As long
|
|
446
|
+
as any tan path can reach it, it can never be deleted, and deleting it is
|
|
447
|
+
the point.
|
|
448
|
+
|
|
449
|
+
What replaces it is a coded refusal naming the missing dependency -- which is
|
|
450
|
+
strictly more actionable than a silent success against a second planner whose
|
|
451
|
+
version nothing checks.
|
|
452
|
+
"""
|
|
453
|
+
if sdk_root is None:
|
|
454
|
+
raise BuildError(
|
|
455
|
+
"build.plan-unavailable",
|
|
456
|
+
"no alp-sdk checkout found -- pass `--sdk-root <PATH>` or run from a project "
|
|
457
|
+
"beside one. Planning reads the SDK's `metadata/**`. Run `tan doctor` to see "
|
|
458
|
+
"which checkout tan resolves, if any.",
|
|
459
|
+
ExitCode.RUNTIME_FAILURE,
|
|
460
|
+
)
|
|
461
|
+
if board_yaml is None:
|
|
462
|
+
raise BuildError(
|
|
463
|
+
"build.plan-unavailable",
|
|
464
|
+
"no board.yaml found -- pass `--board-yaml <PATH>` or run from a project; "
|
|
465
|
+
"for a plain Zephyr example without a board.yaml (alp-sdk's bench examples), "
|
|
466
|
+
"pass `--board <zephyr-board-target>` instead. "
|
|
467
|
+
"Run `tan init` to create a project, or `tan examples` to list ready-made ones.",
|
|
468
|
+
ExitCode.RUNTIME_FAILURE,
|
|
469
|
+
)
|
|
470
|
+
# `--sdk-root` is terminal and returned as-is even when wrong (I-31), so the
|
|
471
|
+
# first thing planning reads is checked here rather than surfacing as a
|
|
472
|
+
# loader stack trace. The board schema, not the planner: the planner is ours
|
|
473
|
+
# now, and requiring the SDK to still carry `alp_orchestrate` would refuse
|
|
474
|
+
# precisely the releases this relocation exists to enable.
|
|
475
|
+
if not (Path(sdk_root) / "metadata" / "schemas" / "board.schema.json").is_file():
|
|
476
|
+
raise BuildError(
|
|
477
|
+
"build.plan-unavailable",
|
|
478
|
+
f"the SDK at `{sdk_root}` has no `metadata/schemas/board.schema.json` -- "
|
|
479
|
+
f"that path is not an alp-sdk checkout, or the checkout is incomplete.",
|
|
480
|
+
ExitCode.RUNTIME_FAILURE,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
from tan.planner_root import emit as _plan_emit
|
|
485
|
+
except ImportError as err: # pragma: no cover -- planner absent from this build
|
|
486
|
+
raise BuildError(
|
|
487
|
+
"build.plan-unavailable",
|
|
488
|
+
f"this build of `tan` cannot import its own planner ({err}). "
|
|
489
|
+
"Reinstall it, or install the planner's dependencies "
|
|
490
|
+
"(`pip install pyyaml jsonschema`).",
|
|
491
|
+
ExitCode.RUNTIME_FAILURE,
|
|
492
|
+
) from err
|
|
493
|
+
try:
|
|
494
|
+
return _plan_emit("build-plan", root=sdk_root, board_yaml=Path(board_yaml))
|
|
495
|
+
except ImportError as err:
|
|
496
|
+
# A planner dependency (`jsonschema`, `PyYAML`) is missing from THIS
|
|
497
|
+
# build. Named, not worked around: see the docstring on why the
|
|
498
|
+
# spawn-the-SDK's-planner fallback was retired.
|
|
499
|
+
raise BuildError(
|
|
500
|
+
"build.plan-unavailable",
|
|
501
|
+
f"the planner could not be imported ({err}) -- install its "
|
|
502
|
+
"dependencies (`pip install pyyaml jsonschema`) or reinstall `tan`.",
|
|
503
|
+
ExitCode.RUNTIME_FAILURE,
|
|
504
|
+
) from err
|
|
505
|
+
except SystemExit as err:
|
|
506
|
+
# `tan.planner.loader` / `kconfig_symbols` call `sys.exit()` on a missing
|
|
507
|
+
# dependency or an unbootstrapped Zephyr workspace. In a subprocess that
|
|
508
|
+
# was a non-zero rc; in-process it would tear down the whole CLI past
|
|
509
|
+
# every envelope guarantee.
|
|
510
|
+
raise BuildError(
|
|
511
|
+
"build.plan-unavailable",
|
|
512
|
+
f"the build-plan emit exited early (code {err.code}).",
|
|
513
|
+
ExitCode.RUNTIME_FAILURE,
|
|
514
|
+
) from err
|
|
515
|
+
except Exception as err:
|
|
516
|
+
# `diagnostics.link: itcm` refused (tan-cli#1350): a board.yaml input
|
|
517
|
+
# error with its own code, not a planner fault. Recognised by its code
|
|
518
|
+
# (`refusal_code`), not its class: the planner needs a bound SDK root
|
|
519
|
+
# to import.
|
|
520
|
+
if refusal_code(err) is not None:
|
|
521
|
+
raise BuildError(err.code, str(err), ExitCode.VALIDATION_FAILURE) from err
|
|
522
|
+
# Every planner failure is an envelope, never a traceback -- the same
|
|
523
|
+
# thing the subprocess boundary used to buy for free. Includes the bare
|
|
524
|
+
# `ValueError`s the Ethos-U sizing path still raises (I-48).
|
|
525
|
+
raise BuildError(
|
|
526
|
+
"build.plan-unavailable",
|
|
527
|
+
f"the build-plan emit failed: {type(err).__name__}: {err}",
|
|
528
|
+
ExitCode.RUNTIME_FAILURE,
|
|
529
|
+
) from err
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _acquire_plan(
|
|
533
|
+
plan_from: str | None, sdk_root: str | None, board_yaml: str | None
|
|
534
|
+
) -> tuple[str, BuildPlan]:
|
|
535
|
+
"""`(source text, parsed plan)`. The TEXT comes back too because
|
|
536
|
+
`_MODE_PLAN` echoes it: the oracle's plan output is a raw passthrough, not
|
|
537
|
+
a round-trip through its own structs -- measured by adding an unknown
|
|
538
|
+
top-level key to the fixture, which survives verbatim into `data`. Echoing
|
|
539
|
+
a re-serialised `BuildPlan` would silently drop exactly the forward-compat
|
|
540
|
+
keys a newer SDK adds, which is the drift the version-skew guard exists to
|
|
541
|
+
make loud."""
|
|
542
|
+
if plan_from is not None:
|
|
543
|
+
try:
|
|
544
|
+
text = Path(plan_from).read_text(encoding="utf-8")
|
|
545
|
+
except (OSError, UnicodeDecodeError) as err:
|
|
546
|
+
# UnicodeDecodeError is a ValueError, NOT an OSError, so `except
|
|
547
|
+
# OSError` alone let it fall through to the catch-all and report a
|
|
548
|
+
# bad INPUT as a tan bug (`build.internal-failure`, exit 5).
|
|
549
|
+
# Reachable on the exact flow `--plan-from` exists for: `tan build
|
|
550
|
+
# --plan --format json > plan.json` in Windows PowerShell 5.1
|
|
551
|
+
# writes UTF-16LE, and this is the replay half of that
|
|
552
|
+
# capture-then-replay loop.
|
|
553
|
+
raise BuildError(
|
|
554
|
+
"build.plan-unavailable",
|
|
555
|
+
f"failed to read plan file `{plan_from}`: {err}",
|
|
556
|
+
ExitCode.RUNTIME_FAILURE,
|
|
557
|
+
) from err
|
|
558
|
+
else:
|
|
559
|
+
text = _emit_plan(sdk_root, board_yaml)
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
return text, parse_build_plan(text)
|
|
563
|
+
except PlanParseError as err:
|
|
564
|
+
# Exit 1, NOT 2. A malformed plan reads like a validation failure and
|
|
565
|
+
# the semantic pull toward `ValidationFailure` is real -- but the
|
|
566
|
+
# shipped binary reports RuntimeFailure here
|
|
567
|
+
# (`crates/tan-cli/src/commands/build/plan_modes.rs:140-146` and
|
|
568
|
+
# `native.rs:110-116` both call `plan_error_run(..., "build.plan-
|
|
569
|
+
# invalid", ..., ExitCode::RuntimeFailure)`), and the exit ladder is a
|
|
570
|
+
# frozen contract. The consumer makes it worse than cosmetic:
|
|
571
|
+
# `alp-sdk-vscode/src/alpCli/service.ts:253-259` renders exit 2 as
|
|
572
|
+
# `severity: "warning"` and exit 1 as `"error"`, so "2" would downgrade
|
|
573
|
+
# a plan that will not parse -- a hard failure -- to a yellow banner.
|
|
574
|
+
# 2 stays for a genuine validation failure in Rust's own terms, e.g.
|
|
575
|
+
# `validate`'s schema violation, whose fixture pins exit 2.
|
|
576
|
+
raise BuildError(err.code, err.message, ExitCode.RUNTIME_FAILURE) from err
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _slice_result(core_id: str, backend: str, outcome: SliceOutcome) -> dict:
|
|
580
|
+
"""One `data.slices[]` entry. `rc`/`reason` are OMITTED when absent, not
|
|
581
|
+
null -- Rust's `skip_serializing_if = "Option::is_none"`.
|
|
582
|
+
|
|
583
|
+
`resolvedTool` is the ONE exception to that rule (tan-cli#510 review,
|
|
584
|
+
MAJOR 1): a NEW, additive field, ALWAYS present -- `null` rather than
|
|
585
|
+
omitted when [`SliceOutcome.resolved_tool`] is `None` -- because unlike
|
|
586
|
+
`rc`/`reason` it has no Rust oracle counterpart to mirror
|
|
587
|
+
`skip_serializing_if` against; a consumer that wants to know which
|
|
588
|
+
binary actually ran should not need to distinguish "the key is absent"
|
|
589
|
+
from "nothing to report" for a field this port alone defines. Never
|
|
590
|
+
folded into `reason`: that field is also the persisted
|
|
591
|
+
`system-manifest.yaml` `slices[].reason` (`_write_manifest_after_dispatch`
|
|
592
|
+
in `build/execute.py`), whose alp-sdk-owned schema defines it as "why a
|
|
593
|
+
slice was skipped/failed" -- a resolved path is neither."""
|
|
594
|
+
result = {
|
|
595
|
+
"coreId": core_id,
|
|
596
|
+
"backend": backend,
|
|
597
|
+
"status": _WIRE_STATUS.get(outcome.status, "failed"),
|
|
598
|
+
"resolvedTool": outcome.resolved_tool,
|
|
599
|
+
}
|
|
600
|
+
if outcome.exit_code is not None:
|
|
601
|
+
result["rc"] = outcome.exit_code
|
|
602
|
+
if outcome.message is not None:
|
|
603
|
+
result["reason"] = outcome.message
|
|
604
|
+
return result
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _dispatch(
|
|
608
|
+
plan: BuildPlan,
|
|
609
|
+
demotions,
|
|
610
|
+
build_root: Path,
|
|
611
|
+
sdk_root: str | None,
|
|
612
|
+
sdk_root_for_stamp: str | None,
|
|
613
|
+
*,
|
|
614
|
+
json_mode: bool = False,
|
|
615
|
+
pristine: bool = False,
|
|
616
|
+
slice_refusals: dict[str, str] | None = None,
|
|
617
|
+
user_defines: dict[str, list[str]] | None = None,
|
|
618
|
+
) -> tuple[list[SliceOutcome], list[Issue]]:
|
|
619
|
+
"""Run the plan's slices, holding back the ones token substitution demoted
|
|
620
|
+
-- and, since tan-cli#483, the ones whose `cores.<id>.app` resolved to a
|
|
621
|
+
directory that does not exist ([`_missing_app_dirs`]) -- both HELD
|
|
622
|
+
per-slice, matching the same per-slice fail/skip shape
|
|
623
|
+
`execute_slices`'s own unknown-backend/missing-tool checks already use,
|
|
624
|
+
rather than refusing the whole plan over one bad core: a two-core project
|
|
625
|
+
with one good `app:` and one bad one still builds the good core.
|
|
626
|
+
|
|
627
|
+
Arms its own [`_Heartbeat`] around the dispatch (tan-cli#287), rather
|
|
628
|
+
than taking one from the caller: `execute_slices` (below) is a single,
|
|
629
|
+
EAGER call that can run for minutes -- the loop after it only walks
|
|
630
|
+
results `execute_slices` already computed -- and this function is the
|
|
631
|
+
one seam every route into the build engine shares. `_build`'s two
|
|
632
|
+
callers, `build_cmd.build` (`tan build`) and `run_cmd._run`
|
|
633
|
+
(`tan run`), both bottleneck through here before any slice runs, so
|
|
634
|
+
arming the heartbeat at THIS level, not one up, is what covers `tan
|
|
635
|
+
run`'s identical silent-CMake window too -- the gap the previous
|
|
636
|
+
revision (a `_Heartbeat` built in `build()` and threaded down as
|
|
637
|
+
`on_output`) left, since `run_cmd.py:267` calls `_build` with no
|
|
638
|
+
`on_output` of its own.
|
|
639
|
+
|
|
640
|
+
`json_mode` is the one thing this function cannot discover for itself
|
|
641
|
+
(`sys.stderr.isatty()` it checks directly, same as `build()` used to).
|
|
642
|
+
`build()` passes its real value straight through. `run_cmd._run` takes
|
|
643
|
+
its own `json_mode` parameter but does not yet thread it into this call
|
|
644
|
+
(out of this file's scope to add -- see `run_cmd.py:267`), so its call
|
|
645
|
+
keeps the default `False`.
|
|
646
|
+
|
|
647
|
+
tan-cli#488 round 5 class sweep: `sys.stderr` can be `None` -- a process
|
|
648
|
+
launched with its standard handles detached (a GUI launcher, a
|
|
649
|
+
`pythonw`-style spawn, or a shell that closed fd 2 before exec) -- the
|
|
650
|
+
same condition `tan.core.consent.can_prompt` guards against. A bare
|
|
651
|
+
`sys.stderr.isatty()` here raised the identical `AttributeError:
|
|
652
|
+
'NoneType' object has no attribute 'isatty'` for a detached-stdio
|
|
653
|
+
`tan build`/`tan run`, caught only by `build()`'s own outer `except
|
|
654
|
+
Exception` and reported as a fabricated `build.internal-failure` instead
|
|
655
|
+
of simply running with the heartbeat disabled.
|
|
656
|
+
|
|
657
|
+
tan-cli#488 round 6: the round-5 fix only added `sys.stderr is not
|
|
658
|
+
None` in front of the same bare `.isatty()` call -- which stops a
|
|
659
|
+
`None` stderr from crashing this line, but not a stderr that EXISTS and
|
|
660
|
+
simply has no `.isatty()` method at all. `tan.cli.main` installs exactly
|
|
661
|
+
that shape under `--format json`: `sys.stderr` becomes `_TeeStderr`
|
|
662
|
+
(`cli.py`'s tee, `write`/`flush`/`getvalue` only, no `isatty`), which is
|
|
663
|
+
not `None`, so round 5's guard let the call through and
|
|
664
|
+
`run_cmd._run` -> `_build` -> here crashed with `AttributeError:
|
|
665
|
+
'_TeeStderr' object has no attribute 'isatty'` on every `tan run
|
|
666
|
+
--format json` -- exit 5, `run.internal-failure`, before a single slice
|
|
667
|
+
ever dispatched (measured against the real binary). This is the exact
|
|
668
|
+
condition `tan.env.stderr_is_tty` already exists to guard (tan-cli#288)
|
|
669
|
+
and `use_color`/`wrap_width` already share -- a `None` check does not
|
|
670
|
+
cover a stream that exists but lacks the method, which is the whole
|
|
671
|
+
class this line, `tan.core.consent.can_prompt` and `doctor_cmd.
|
|
672
|
+
fix_suppressed_issue` all shared the same wrong shape for. Now routed
|
|
673
|
+
through the one shared probe instead of a fourth hand-rolled copy.
|
|
674
|
+
# ponytail: the one live gap this leaves -- `tan run --format json` with
|
|
675
|
+
# a REAL terminal still attached to stderr would arm the heartbeat where
|
|
676
|
+
# it should stay silent (`run_cmd._run` does not thread its own
|
|
677
|
+
# `json_mode` into `_build`, so `_dispatch` always sees the default
|
|
678
|
+
# `False` on that path -- see `run_cmd.py:267`). stdout (the envelope
|
|
679
|
+
# channel) is untouched either way, and the crash this round fixes is
|
|
680
|
+
# closed regardless of `json_mode`'s value, since `stderr_is_tty()` is
|
|
681
|
+
# false for a `_TeeStderr` on its own merits; threading `json_mode`
|
|
682
|
+
# through `run_cmd._run` is a separate, narrower polish left for its own
|
|
683
|
+
# change, out of this file's scope to add.
|
|
684
|
+
|
|
685
|
+
A demoted slice still names a literal `${TOOLCHAIN_ROOT}` in its own
|
|
686
|
+
fields because this host resolved no toolchain. `execute_slices` does not
|
|
687
|
+
know about demotions, so dispatching one would spawn a command with an
|
|
688
|
+
unsubstituted token in its argv -- a silent wrong-path build, the exact
|
|
689
|
+
failure I-29 says a naive substitution reintroduces. Instead the demotion
|
|
690
|
+
is routed to `executionPolicy.missingTool` (default skip) here, which is
|
|
691
|
+
the seam the Rust executor uses for it too: it is a host-provisioning
|
|
692
|
+
fact, not a plan bug.
|
|
693
|
+
|
|
694
|
+
Precedence is preserved: a demoted slice that ALSO names an unknown
|
|
695
|
+
backend or carries no command is left to `execute_slices`, because both of
|
|
696
|
+
those outrank a provisioning fact and are checked first there.
|
|
697
|
+
|
|
698
|
+
`pristine` (`tan build --pristine`, tan-cli#427) is threaded straight
|
|
699
|
+
through to `execute_slices`'s own `force_pristine` -- see that
|
|
700
|
+
function's docstring, and `_maybe_pristine_stale_sdk_build_dir`'s, for
|
|
701
|
+
the wipe/suppression decision it makes per slice.
|
|
702
|
+
|
|
703
|
+
`sdk_root` is threaded straight through to `execute_slices` -- it is
|
|
704
|
+
THIS run's already-resolved `--sdk-root`/discovered checkout (`_build`'s
|
|
705
|
+
own parameter), the identity `execute_slices`'s sdk-switch-pristine guard
|
|
706
|
+
(issue #52) keys its stamp comparison on. Without it every stamp
|
|
707
|
+
comparison degrades to "unresolved", which never wipes anything -- see
|
|
708
|
+
`tan.core.plan_exec.sdk_stamp_key`. `sdk_root_for_stamp` is the SAME
|
|
709
|
+
checkout, NORMALIZED and anchored on the workspace root (`build()`'s own
|
|
710
|
+
`tan.core.plan_exec.normalize_path(workspace_root / sdk_root)`) -- passed
|
|
711
|
+
separately so a relative `--sdk-root ../alp-sdk` keys the stamp
|
|
712
|
+
IDENTICALLY to the absolute form `tan sdk switch` would pin for the same
|
|
713
|
+
checkout (tan-cli#163), without changing what `sdk_root` itself
|
|
714
|
+
substitutes for `${SDK_ROOT}` or reports as `sdk.root`.
|
|
715
|
+
|
|
716
|
+
Held slices are also threaded into `execute_slices` as `held_outcomes`
|
|
717
|
+
(tan-cli #89 MINOR 4, oracle `execute/mod.rs:379-400`): `execute_slices`
|
|
718
|
+
only ever sees the RUNNABLE subset of the plan (`replace(plan,
|
|
719
|
+
slices=runnable)` below), so its own post-build manifest overlay would
|
|
720
|
+
otherwise never learn a demoted slice existed at all -- leaving that
|
|
721
|
+
core's `system-manifest.yaml` entry at its plan-time (or a previous
|
|
722
|
+
run's) `status`/`output_artefact` forever, which `tan size`/
|
|
723
|
+
`tan debug-config` read with no status check of their own. Each held
|
|
724
|
+
outcome's `output_artefact` is the explicit empty string, never `None`:
|
|
725
|
+
the overlay (`overlay_run_results_raw`) treats `None` as "preserve
|
|
726
|
+
whatever is already there", which is correct for a slice this run never
|
|
727
|
+
touched but wrong for one THIS run knows never dispatched.
|
|
728
|
+
"""
|
|
729
|
+
held = {
|
|
730
|
+
d.slice_index: d
|
|
731
|
+
for d in demotions
|
|
732
|
+
if plan.slices[d.slice_index].backend in KNOWN_BACKENDS
|
|
733
|
+
and plan.slices[d.slice_index].command is not None
|
|
734
|
+
}
|
|
735
|
+
action = resolve_action(plan.execution_policy, "missing_tool", PolicyAction.SKIP)
|
|
736
|
+
failed = action is PolicyAction.FAIL
|
|
737
|
+
|
|
738
|
+
# tan-cli#483: a slice already held for `${TOOLCHAIN_ROOT}` keeps that
|
|
739
|
+
# verdict -- a bad `app:` is checked below only for the slices demotion
|
|
740
|
+
# left alone, so the two never disagree about the same index.
|
|
741
|
+
missing_app_dirs = {
|
|
742
|
+
i: msg for i, msg in _missing_app_dirs(plan, build_root).items() if i not in held
|
|
743
|
+
}
|
|
744
|
+
# tan-cli#517, filtered the same way and for the same reason: a slice
|
|
745
|
+
# that will not dispatch has no substitution to announce, and saying so
|
|
746
|
+
# anyway would bury the verdict that actually stopped it.
|
|
747
|
+
substituted_app_dirs = {
|
|
748
|
+
i: msg
|
|
749
|
+
for i, msg in _substituted_app_dirs(plan, build_root).items()
|
|
750
|
+
if i not in held and i not in missing_app_dirs
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
held_outcomes = {
|
|
754
|
+
i: SliceOutcome(
|
|
755
|
+
plan.slices[i].core_id,
|
|
756
|
+
"failed" if failed else "skipped",
|
|
757
|
+
None,
|
|
758
|
+
d.reason,
|
|
759
|
+
output_artefact="",
|
|
760
|
+
)
|
|
761
|
+
for i, d in held.items()
|
|
762
|
+
}
|
|
763
|
+
# A bad app dir is not a host-provisioning gap `executionPolicy` was ever
|
|
764
|
+
# meant to arbitrate (contrast the demotion above) -- it is the plan
|
|
765
|
+
# naming a path that does not exist, so it always FAILS the one slice
|
|
766
|
+
# rather than silently skipping it forward, matching the Expected
|
|
767
|
+
# section of tan-cli#483 ("not a warning that lets the build proceed").
|
|
768
|
+
held_outcomes.update(
|
|
769
|
+
{
|
|
770
|
+
i: SliceOutcome(plan.slices[i].core_id, "failed", None, msg, output_artefact="")
|
|
771
|
+
for i, msg in missing_app_dirs.items()
|
|
772
|
+
}
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
runnable = [
|
|
776
|
+
sl for i, sl in enumerate(plan.slices) if i not in held and i not in missing_app_dirs
|
|
777
|
+
]
|
|
778
|
+
# tan-cli#287: TTY + text-mode only (never `--format json` -- stdout is
|
|
779
|
+
# untouched either way, this is belt-and-suspenders for a machine caller
|
|
780
|
+
# that also inherited the terminal's stderr). `__enter__`/`__exit__` run
|
|
781
|
+
# regardless of what happens inside, so a dispatch that raises still
|
|
782
|
+
# stops the thread and blanks any line it had printed.
|
|
783
|
+
with _Heartbeat(enabled=not json_mode and stderr_is_tty()) as heartbeat:
|
|
784
|
+
dispatched = iter(
|
|
785
|
+
execute_slices(
|
|
786
|
+
replace(plan, slices=runnable),
|
|
787
|
+
build_root=build_root,
|
|
788
|
+
env_lookup=os.environ.get,
|
|
789
|
+
# tan-cli#308: no build_cmd.py-level gap fillers of our own --
|
|
790
|
+
# `execute_slices` fills ZEPHYR_BASE/EXTRA_ZEPHYR_MODULES
|
|
791
|
+
# itself, PER SLICE, from the west workspace it resolves
|
|
792
|
+
# internally (`tan.core.zephyr_env.zephyr_env_overrides`),
|
|
793
|
+
# exactly where the Rust oracle's own `execute_slices`
|
|
794
|
+
# computes them (`execute/mod.rs`, inside its own per-slice
|
|
795
|
+
# loop) -- not from an outer caller. This parameter stays for
|
|
796
|
+
# a caller-supplied override this port has none of yet.
|
|
797
|
+
gap_fillers=(),
|
|
798
|
+
on_output=heartbeat,
|
|
799
|
+
sdk_root=sdk_root,
|
|
800
|
+
sdk_root_for_stamp=sdk_root_for_stamp,
|
|
801
|
+
held_outcomes=held_outcomes.values(),
|
|
802
|
+
force_pristine=pristine,
|
|
803
|
+
slice_refusals=slice_refusals,
|
|
804
|
+
user_defines=user_defines,
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
outcomes: list[SliceOutcome] = []
|
|
809
|
+
issues: list[Issue] = []
|
|
810
|
+
for i, sl in enumerate(plan.slices):
|
|
811
|
+
demotion = held.get(i)
|
|
812
|
+
if demotion is not None:
|
|
813
|
+
outcomes.append(held_outcomes[i])
|
|
814
|
+
issues.append(
|
|
815
|
+
Issue(
|
|
816
|
+
# Same code as the plan-fatal sibling on purpose: the
|
|
817
|
+
# extension needs no new vocabulary, only a severity
|
|
818
|
+
# that says whether this stopped the build.
|
|
819
|
+
"build.toolchain-root-unresolved",
|
|
820
|
+
"error" if failed else "warning",
|
|
821
|
+
f"slice `{sl.core_id}` {'failed' if failed else 'skipped'}: "
|
|
822
|
+
f"{demotion.reason}",
|
|
823
|
+
)
|
|
824
|
+
)
|
|
825
|
+
continue
|
|
826
|
+
app_dir_message = missing_app_dirs.get(i)
|
|
827
|
+
if app_dir_message is not None:
|
|
828
|
+
outcomes.append(held_outcomes[i])
|
|
829
|
+
issues.append(Issue("build.app-dir-missing", "error", app_dir_message))
|
|
830
|
+
continue
|
|
831
|
+
substituted = substituted_app_dirs.get(i)
|
|
832
|
+
if substituted is not None:
|
|
833
|
+
issues.append(Issue("build.app-dir-substituted", "info", substituted))
|
|
834
|
+
outcome = next(dispatched)
|
|
835
|
+
outcomes.append(outcome)
|
|
836
|
+
if slice_refusals and outcome.message == slice_refusals.get(sl.core_id):
|
|
837
|
+
issues.append(Issue("build.host-python-unsuitable", "error", outcome.message))
|
|
838
|
+
# tan-cli#1351: per-board files naming a different board are silently
|
|
839
|
+
# ignored by Zephyr.
|
|
840
|
+
issues.extend(
|
|
841
|
+
Issue("build.board-file-unmatched", "warning", msg)
|
|
842
|
+
for msg in unmatched_board_file_messages(
|
|
843
|
+
[
|
|
844
|
+
(sl.backend, sl.command.args if sl.command else [], sl.app_dir)
|
|
845
|
+
for sl in plan.slices
|
|
846
|
+
],
|
|
847
|
+
build_root,
|
|
848
|
+
_known_boards(sdk_root_for_stamp),
|
|
849
|
+
)
|
|
850
|
+
)
|
|
851
|
+
return outcomes, issues
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _known_boards(sdk_root_for_stamp: str | None) -> list[str]:
|
|
855
|
+
"""Every board target the SDK's SoM presets declare (tan-cli#1403).
|
|
856
|
+
Takes the workspace-anchored, normalized SDK root (a relative `--sdk-root`
|
|
857
|
+
is anchored on the workspace, not the build root)."""
|
|
858
|
+
if not sdk_root_for_stamp:
|
|
859
|
+
return []
|
|
860
|
+
return all_topology_boards(Path(sdk_root_for_stamp) / "metadata")
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
#: `os` values the schema requires `cores.<id>.app` to resolve to a real
|
|
864
|
+
#: directory for (`metadata/schemas/board.schema.json`
|
|
865
|
+
#: `$defs/core_entry/properties/app`) -- `os: yocto` dispatches by
|
|
866
|
+
#: `recipe:` (a bitbake recipe name), never by this directory
|
|
867
|
+
#: (`tan/planner/orchestrator.py:338-353`), so checking it there would
|
|
868
|
+
#: refuse a buildable yocto slice over a path the build never reads.
|
|
869
|
+
_APP_DIR_REQUIRED_BACKENDS = frozenset({"zephyr", "baremetal"})
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _missing_app_dirs(plan: BuildPlan, build_root: Path) -> dict[int, str]:
|
|
873
|
+
"""Slice indices whose `cores.<id>.app` resolved to a path that is not a
|
|
874
|
+
real, existing directory (tan-cli#483), each mapped to the refusal
|
|
875
|
+
message.
|
|
876
|
+
|
|
877
|
+
Checked HERE, not inside `execute_slices`: by now `apply_plan_token_
|
|
878
|
+
substitution` has resolved `appDir` to a real path, or held the slice
|
|
879
|
+
back as a `${TOOLCHAIN_ROOT}` demotion -- `execute_slices`'s own tests
|
|
880
|
+
feed it synthetic plans with a placeholder `appDir` never meant to
|
|
881
|
+
exist. Neither `orchestrator.py`'s `_zephyr_app_dir` nor
|
|
882
|
+
`_resolve_app_path` checks existence (both FROZEN, `tan/planner/**`);
|
|
883
|
+
unfixed there, a nonexistent `app:` makes `_zephyr_app_dir`'s
|
|
884
|
+
CMakeLists.txt-fallback probe silently find the PROJECT ROOT's own
|
|
885
|
+
CMakeLists.txt instead.
|
|
886
|
+
|
|
887
|
+
Anchored on `build_root`, not the tan process's own CWD: `appDir` is a
|
|
888
|
+
plan path like `buildDir`/`command.cwd`, under the same anchoring
|
|
889
|
+
contract (`build-plan-v1.schema.json`, issue #596) `execute_slices`
|
|
890
|
+
already confines every slice's `cwd` to. The real CWD would refuse a
|
|
891
|
+
valid relative `appDir` from anywhere but the project (e.g.
|
|
892
|
+
`--build-root` from a parent dir), and misname it in the refusal.
|
|
893
|
+
|
|
894
|
+
A slice with no `command` (the planner already refused one --
|
|
895
|
+
`board-tree-missing`, `yocto-recipe-missing`, `no-command`, ...) is
|
|
896
|
+
skipped, mirroring the `${TOOLCHAIN_ROOT}` filter above: nothing was
|
|
897
|
+
going to dispatch for it, so reporting this instead would mask the
|
|
898
|
+
planner's own warning and flip an otherwise-`ok` exit code.
|
|
899
|
+
|
|
900
|
+
EXISTENCE only, not the schema's `prj.conf`/`CMakeLists.txt` contents
|
|
901
|
+
requirement: 96 of 105 enabled zephyr/baremetal examples on alp-sdk
|
|
902
|
+
`dev` are `app: ./src`, sources-only -- the shipped convention
|
|
903
|
+
`_zephyr_app_dir`'s own fallback serves, which a contents check would
|
|
904
|
+
refuse en masse."""
|
|
905
|
+
missing: dict[int, str] = {}
|
|
906
|
+
for i, sl in enumerate(plan.slices):
|
|
907
|
+
if (
|
|
908
|
+
sl.backend not in _APP_DIR_REQUIRED_BACKENDS
|
|
909
|
+
or sl.app_dir is None
|
|
910
|
+
or sl.command is None
|
|
911
|
+
):
|
|
912
|
+
continue
|
|
913
|
+
app_dir_path = Path(sl.app_dir)
|
|
914
|
+
if not app_dir_path.is_absolute():
|
|
915
|
+
app_dir_path = build_root / app_dir_path
|
|
916
|
+
if not app_dir_path.exists():
|
|
917
|
+
missing[i] = f"core `{sl.core_id}`: app directory does not exist: {app_dir_path}"
|
|
918
|
+
elif not app_dir_path.is_dir():
|
|
919
|
+
missing[i] = f"core `{sl.core_id}`: app path is not a directory: {app_dir_path}"
|
|
920
|
+
return missing
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
#: The one backend whose app dir the planner may SILENTLY substitute. Only
|
|
924
|
+
#: `_zephyr_app_dir` carries the parent fallback (`tan/planner/
|
|
925
|
+
#: orchestrator.py:397-401`); the `baremetal` arm beside it builds
|
|
926
|
+
#: `cmake -S <_resolve_app_path(app)>` with no `CMakeLists.txt` probe at all,
|
|
927
|
+
#: so nothing is ever substituted there and announcing one would be a lie.
|
|
928
|
+
#: Narrower than `_APP_DIR_REQUIRED_BACKENDS` above for that reason, not by
|
|
929
|
+
#: oversight.
|
|
930
|
+
_APP_DIR_SUBSTITUTING_BACKENDS = frozenset({"zephyr"})
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _substituted_app_dirs(plan: BuildPlan, build_root: Path) -> dict[int, str]:
|
|
934
|
+
"""Slice indices whose `cores.<id>.app` resolved to a real directory that
|
|
935
|
+
carries no `CMakeLists.txt`, so `west build` will be pointed at its
|
|
936
|
+
PARENT instead (tan-cli#517), each mapped to the announcement.
|
|
937
|
+
|
|
938
|
+
ANNOUNCED, never refused. The fallback is the shipped convention, not a
|
|
939
|
+
defect: 96 of 105 enabled app-carrying zephyr/baremetal core entries
|
|
940
|
+
across alp-sdk `dev`'s own examples are `app: ./src`, sources-only, with
|
|
941
|
+
the one `CMakeLists.txt` at the example root doing
|
|
942
|
+
`target_sources(app PRIVATE src/main.c)`. Refusing would break 91% of the
|
|
943
|
+
SDK's own examples, which is why tan-cli#517 asks for visibility and
|
|
944
|
+
explicitly not for a refusal. What was wrong was only that it happened
|
|
945
|
+
SILENTLY: on a multi-core project the parent IS the project root, which
|
|
946
|
+
is very often the OTHER core's application, so a typo in one core's
|
|
947
|
+
`app:` that still names an existing directory builds the wrong source
|
|
948
|
+
tree and reports success.
|
|
949
|
+
|
|
950
|
+
`severity: "info"`, not `"warning"`. The measurement above is the
|
|
951
|
+
argument: the substitution fires on the overwhelming majority of real
|
|
952
|
+
builds, so `"warning"` would put a permanent yellow item on nearly every
|
|
953
|
+
successful build in a consumer that surfaces `issues[]` on the ok path
|
|
954
|
+
(alp-sdk-vscode does, since alp-sdk-vscode#485) -- and a warning that is
|
|
955
|
+
always there is one nobody reads on the day it matters. `"info"` is the
|
|
956
|
+
severity this registry already uses for exactly this shape: something
|
|
957
|
+
was substituted/migrated on your behalf and you should be able to see it
|
|
958
|
+
(`debug-config.legacy-entry-migrated`).
|
|
959
|
+
|
|
960
|
+
Named BOTH ways, as the issue asks: the configured path and the
|
|
961
|
+
substituted one. Naming only the substituted one would be indis-
|
|
962
|
+
tinguishable from a correctly-configured self-contained app dir.
|
|
963
|
+
|
|
964
|
+
The probe MIRRORS `_zephyr_app_dir`'s own two-line condition rather than
|
|
965
|
+
reading the substituted path back out of `command.args`. `tan/planner/**`
|
|
966
|
+
is a hash-audited relocation of alp-sdk's `scripts/alp_orchestrate/**`
|
|
967
|
+
(`tests/gates/test_planner_relocation_freshness.py`) and cannot be
|
|
968
|
+
changed here, so the condition is stable; reading `command.args` instead
|
|
969
|
+
would bind this to the ARGV POSITION of `west build`'s app argument,
|
|
970
|
+
which is not a contract anywhere. Note `appDir` is NOT the substituted
|
|
971
|
+
value: `buildplan.py` emits `_resolve_app_path(app)` there, the
|
|
972
|
+
configured path, and only the `west build` positional gets
|
|
973
|
+
`_zephyr_app_dir`'s result -- which is what makes both halves of the
|
|
974
|
+
message recoverable from the plan at all.
|
|
975
|
+
|
|
976
|
+
Guarded exactly like `_missing_app_dirs`: no `command` means nothing was
|
|
977
|
+
going to dispatch, and a nonexistent `app:` is that function's refusal,
|
|
978
|
+
reported there and not softened into an advisory here.
|
|
979
|
+
"""
|
|
980
|
+
substituted: dict[int, str] = {}
|
|
981
|
+
for i, sl in enumerate(plan.slices):
|
|
982
|
+
if (
|
|
983
|
+
sl.backend not in _APP_DIR_SUBSTITUTING_BACKENDS
|
|
984
|
+
or sl.app_dir is None
|
|
985
|
+
or sl.command is None
|
|
986
|
+
):
|
|
987
|
+
continue
|
|
988
|
+
app_dir_path = Path(sl.app_dir)
|
|
989
|
+
if not app_dir_path.is_absolute():
|
|
990
|
+
app_dir_path = build_root / app_dir_path
|
|
991
|
+
if not app_dir_path.is_dir() or (app_dir_path / "CMakeLists.txt").is_file():
|
|
992
|
+
continue
|
|
993
|
+
parent = app_dir_path.parent
|
|
994
|
+
if not (parent / "CMakeLists.txt").is_file():
|
|
995
|
+
# `_zephyr_app_dir`'s third arm: neither has one, so it returns
|
|
996
|
+
# the configured path unchanged. Nothing was substituted, so
|
|
997
|
+
# there is nothing to announce -- the build fails in west/CMake
|
|
998
|
+
# naming the path the project actually configured.
|
|
999
|
+
continue
|
|
1000
|
+
substituted[i] = (
|
|
1001
|
+
f"core `{sl.core_id}`: app directory has no CMakeLists.txt "
|
|
1002
|
+
f"({app_dir_path}) -- building its parent instead ({parent}). This is the "
|
|
1003
|
+
f"`app: ./src` sources-only convention when intended; if it was not, "
|
|
1004
|
+
f"`cores.{sl.core_id}.app` names the wrong directory and this build is "
|
|
1005
|
+
f"against the parent's sources."
|
|
1006
|
+
)
|
|
1007
|
+
return substituted
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _backend_issues(plan: BuildPlan, outcomes: list[SliceOutcome]) -> list[Issue]:
|
|
1011
|
+
"""Name the backend string the plan sent that this CLI does not know.
|
|
1012
|
+
Recomputed from the slice rather than sniffed out of the outcome message:
|
|
1013
|
+
`executionPolicy.unknownBackend` already decided skip-vs-fail inside
|
|
1014
|
+
`execute_slices`, and the outcome's status is the honest read of it."""
|
|
1015
|
+
issues = []
|
|
1016
|
+
for sl, outcome in zip(plan.slices, outcomes, strict=True):
|
|
1017
|
+
if sl.backend in KNOWN_BACKENDS:
|
|
1018
|
+
continue
|
|
1019
|
+
failed = outcome.status == "failed"
|
|
1020
|
+
issues.append(
|
|
1021
|
+
Issue(
|
|
1022
|
+
"build.unknown-backend",
|
|
1023
|
+
"error" if failed else "warning",
|
|
1024
|
+
f"slice `{sl.core_id}` names unrecognised backend `{sl.backend}`"
|
|
1025
|
+
+ ("" if failed else " -- skipped"),
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
return issues
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _missing_tool_issues(plan: BuildPlan, outcomes: list[SliceOutcome]) -> list[Issue]:
|
|
1032
|
+
"""Name the tool a skipped (or, under `executionPolicy.missingTool: fail`,
|
|
1033
|
+
failed) slice could not find on this host.
|
|
1034
|
+
|
|
1035
|
+
Matched on `outcome.message` rather than re-probing PATH a second time:
|
|
1036
|
+
every missing-tool refusal `execute_slices` (`build/execute.py`) can
|
|
1037
|
+
produce carries [`MISSING_TOOL_RE`]'s shape somewhere in the message --
|
|
1038
|
+
a null-command skip reads `` has no command `` (and its reason already
|
|
1039
|
+
lives in `plan.warnings`, I-11), and an unknown-backend one is caught
|
|
1040
|
+
structurally by `_backend_issues` above -- so this recovers exactly the
|
|
1041
|
+
missing-tool cases and nothing else.
|
|
1042
|
+
|
|
1043
|
+
tan-cli#801: this used to require the marker at the message's START
|
|
1044
|
+
(`message.startswith("tool `")`), which matched only the slice's own
|
|
1045
|
+
`command` precheck. The #550 post-build-step refusal
|
|
1046
|
+
(`_missing_post_tool`) puts the SAME phrase after a `` slice `{core_id}`
|
|
1047
|
+
post-build step N of M (`{label}`) cannot run: `` lead-in, so it never
|
|
1048
|
+
started with the marker and this promoter silently dropped it -- the
|
|
1049
|
+
exact silence tan-cli#283 was filed to close, just for a different
|
|
1050
|
+
refusal site. `.search`, not `startswith`: the marker's position is not
|
|
1051
|
+
part of the contract, only whether the message carries it at all.
|
|
1052
|
+
|
|
1053
|
+
tan-cli#283: without this, `tan build` on a host missing `west`/`bitbake`
|
|
1054
|
+
reported each slice's specific reason only in `data.slices[].reason` --
|
|
1055
|
+
never in `issues[]`, the field a JSON consumer (the VS Code extension,
|
|
1056
|
+
CI) actually reads to decide whether anything needs flagging."""
|
|
1057
|
+
issues = []
|
|
1058
|
+
for sl, outcome in zip(plan.slices, outcomes, strict=True):
|
|
1059
|
+
message = outcome.message
|
|
1060
|
+
if message is None or not MISSING_TOOL_RE.search(message):
|
|
1061
|
+
continue
|
|
1062
|
+
failed = outcome.status == "failed"
|
|
1063
|
+
issues.append(
|
|
1064
|
+
Issue(
|
|
1065
|
+
"build.missing-tool",
|
|
1066
|
+
"error" if failed else "warning",
|
|
1067
|
+
f"slice `{sl.core_id}` {'failed' if failed else 'skipped'}: {message}",
|
|
1068
|
+
)
|
|
1069
|
+
)
|
|
1070
|
+
return issues
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _host_python_issues(build_python, mode) -> list[Issue]:
|
|
1074
|
+
"""tan-cli#1317. Where CMake will NOT run (`--materialise`) the refusal is a
|
|
1075
|
+
plain warning and the bare fallback stays. On the native dispatch it is a
|
|
1076
|
+
PER-SLICE outcome instead (`_python_refusals` -> `execute_slices`), applied
|
|
1077
|
+
after `executionPolicy`'s null-command / missing-tool skips."""
|
|
1078
|
+
if build_python.refusal is None or mode == _MODE_NATIVE:
|
|
1079
|
+
return []
|
|
1080
|
+
return [Issue("build.host-python-unsuitable", "warning", build_python.refusal)]
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def _python_refusals(build_python, demotions, mode) -> dict[str, str]:
|
|
1084
|
+
if build_python.refusal is None or mode != _MODE_NATIVE:
|
|
1085
|
+
return {}
|
|
1086
|
+
message = refusal_message(build_python.refusal)
|
|
1087
|
+
return {
|
|
1088
|
+
c: f"core `{c}`: {message}"
|
|
1089
|
+
for c in build_python.users - {d.core_id for d in demotions}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _cross_drive_issues(outcomes: list[SliceOutcome]) -> list[Issue]:
|
|
1094
|
+
"""tan-cli#697: promote `tan.core.plan_exec.cross_drive_source_refusal`'s
|
|
1095
|
+
refusal -- matched on its own `CROSS_DRIVE_MSG` marker, same idiom as
|
|
1096
|
+
`_missing_tool_issues` above -- from a per-slice `reason` into a coded
|
|
1097
|
+
top-level `issues[]` entry. Without this the envelope carried only the
|
|
1098
|
+
generic `build.slice-failed` header (`_build`, below) for a cross-drive
|
|
1099
|
+
project/workspace layout, indistinguishable from any other build
|
|
1100
|
+
failure to a JSON consumer that reads `issues[]` and never `data.
|
|
1101
|
+
slices[].reason`. Always `failed`, never `skipped`: this refusal is a
|
|
1102
|
+
hardcoded plan/host-layout defect (mirroring the unsafe-cwd escape
|
|
1103
|
+
above it in `execute.py`), not something `executionPolicy` governs."""
|
|
1104
|
+
issues = []
|
|
1105
|
+
for outcome in outcomes:
|
|
1106
|
+
if outcome.message is None or CROSS_DRIVE_MSG not in outcome.message:
|
|
1107
|
+
continue
|
|
1108
|
+
issues.append(Issue("build.cross-drive-workspace", "error", outcome.message))
|
|
1109
|
+
return issues
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _workspace_unresolved_issues(outcomes: list[SliceOutcome]) -> list[Issue]:
|
|
1113
|
+
"""tan-cli#1429: `_cross_drive_issues`' promotion for the refusal in
|
|
1114
|
+
`tan.core.west_workspace_refusal`, matched on its own marker."""
|
|
1115
|
+
return [
|
|
1116
|
+
Issue("build.workspace-unresolved", "error", o.message)
|
|
1117
|
+
for o in outcomes
|
|
1118
|
+
if o.message is not None and WORKSPACE_UNRESOLVED_MSG in o.message
|
|
1119
|
+
]
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _plan_warning_issues(warnings: list[dict]) -> list[Issue]:
|
|
1123
|
+
"""tan-cli#1000: promote the PLAN's own warnings into `issues[]`.
|
|
1124
|
+
|
|
1125
|
+
Third sibling of `_missing_tool_issues` (tan-cli#283/#801) and
|
|
1126
|
+
`_cross_drive_issues` (tan-cli#697), and filed because it was the door
|
|
1127
|
+
those two left open. `plan.warnings` reached exactly one place -- the
|
|
1128
|
+
envelope's `data.warnings` -- so the message that says WHY a build
|
|
1129
|
+
produced nothing was invisible to text mode and to any consumer reading
|
|
1130
|
+
`issues[]`, which is the field alp-sdk-vscode and CI actually branch on.
|
|
1131
|
+
|
|
1132
|
+
Measured on `E1M-AEN301`, a SKU `tan presets` lists: `tan init` rc=0,
|
|
1133
|
+
`tan validate` rc=0 "clean", `tan build` rc=1 with a four-line text
|
|
1134
|
+
output whose only cause-bearing words were ``slice `m55_hp` has no
|
|
1135
|
+
command``, while `data.warnings` held "SoM 'E1M-AEN301' core 'm55_hp'
|
|
1136
|
+
wants Zephyr board 'alp_e1m_aen301_m55_hp', which has no tree under
|
|
1137
|
+
zephyr/boards/alp/ -- board bring-up for this target has not happened
|
|
1138
|
+
yet."
|
|
1139
|
+
|
|
1140
|
+
ONE registered code for every warning, not `build.<the warning's own
|
|
1141
|
+
code>`: `build_plan.py`'s `_warnings` documents the warning codes as an
|
|
1142
|
+
OPEN set ("the forward compatibility is in the CODES being open"), so
|
|
1143
|
+
minting an issue code per warning code would emit unregistered codes the
|
|
1144
|
+
moment the planner adds one -- and `contract/issue-codes.json` is a
|
|
1145
|
+
closed registry whose consumer matches with `===`. The warning's own code
|
|
1146
|
+
travels inside the message instead, where an open set costs nothing.
|
|
1147
|
+
|
|
1148
|
+
Severity is always `warning`, never promoted to `error` on the count: a
|
|
1149
|
+
build that fails BECAUSE of these already carries its own
|
|
1150
|
+
`build.nothing-built`/`build.slice-failed` error beside them, and a
|
|
1151
|
+
partial build where one core lacks a board tree is genuinely not an
|
|
1152
|
+
error for the cores that did build.
|
|
1153
|
+
|
|
1154
|
+
Text mode needs no separate change -- `_print_text_issues` prints every
|
|
1155
|
+
surviving issue as ``{severity}: {message}``, and `_text_issues`'
|
|
1156
|
+
dedup (tan-cli#746) only drops a message that ENDS WITH a slice's own
|
|
1157
|
+
`reason`, which a warning's text does not.
|
|
1158
|
+
"""
|
|
1159
|
+
issues = []
|
|
1160
|
+
for warning in warnings:
|
|
1161
|
+
code = warning.get("code")
|
|
1162
|
+
core_id = warning.get("coreId")
|
|
1163
|
+
message = warning.get("message")
|
|
1164
|
+
where = f" {core_id}:" if core_id else ""
|
|
1165
|
+
issues.append(
|
|
1166
|
+
Issue("build.plan-warning", "warning", f"[{code}]{where} {message}")
|
|
1167
|
+
)
|
|
1168
|
+
return issues
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _toolchain_for_plan(plan_text: str) -> ToolchainResolution:
|
|
1172
|
+
"""This host's `${TOOLCHAIN_ROOT}` -- resolved ONLY when `plan_text`
|
|
1173
|
+
actually names the token (tan-cli#547).
|
|
1174
|
+
|
|
1175
|
+
The laziness is the point, and it is a property of THIS call site rather
|
|
1176
|
+
than of `resolve_toolchain_root` itself: no SDK plan names
|
|
1177
|
+
`${TOOLCHAIN_ROOT}` today, and every one of those builds must keep
|
|
1178
|
+
costing zero filesystem scans of `/opt` and `$HOME`. It also keeps the
|
|
1179
|
+
resolver's own failure modes (an unreadable scan root, an ambiguous
|
|
1180
|
+
host) entirely out of the way of a plan that would never have consumed
|
|
1181
|
+
the value.
|
|
1182
|
+
|
|
1183
|
+
Asked of the plan's re-serialised JSON, not of `plan_text` directly: a
|
|
1184
|
+
plan is free to spell the token's `$` as `\\u0024`, which a raw substring
|
|
1185
|
+
search would miss while `substitute_plan_tokens` -- which sees the
|
|
1186
|
+
DECODED string -- would not, leaving the slice demoted on a host that
|
|
1187
|
+
has a toolchain. `plan_text` is already known to parse (`_acquire_plan`
|
|
1188
|
+
returned a `BuildPlan` from it), so the round trip cannot fail here.
|
|
1189
|
+
|
|
1190
|
+
An untokened plan (`planPathMode` absent) never reaches substitution at
|
|
1191
|
+
all, but is not special-cased: it also never contains the token, so the
|
|
1192
|
+
same one check covers both.
|
|
1193
|
+
"""
|
|
1194
|
+
if TOKEN_TOOLCHAIN_ROOT not in json.dumps(json.loads(plan_text)):
|
|
1195
|
+
return ToolchainResolution(None)
|
|
1196
|
+
return resolve_toolchain_root()
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _build_root_is_consumer_default(build_root: str) -> bool:
|
|
1200
|
+
"""Whether a plan's `buildRoot` names the ONE tree this consumer reads
|
|
1201
|
+
back (tan-cli#566) -- `BuildPlan::build_root_is_consumer_default` in the
|
|
1202
|
+
oracle.
|
|
1203
|
+
|
|
1204
|
+
A PATH-COMPONENT comparison, not a string equality, and that distinction
|
|
1205
|
+
is load-bearing in both directions. MEASURED against `target/debug/tan`
|
|
1206
|
+
(`tan 0.4.1`), driving its live-emit native path through a stub planner
|
|
1207
|
+
so an arbitrary `buildRoot` could be handed to it, ten spellings, only
|
|
1208
|
+
that field varying:
|
|
1209
|
+
|
|
1210
|
+
build rc 0 accepted
|
|
1211
|
+
build/ rc 0 accepted <- a naive `!= "build"` refuses this
|
|
1212
|
+
./build rc 0 accepted <- and this
|
|
1213
|
+
out rc 1 build.unsupported-build-root
|
|
1214
|
+
build/sub rc 1 build.unsupported-build-root
|
|
1215
|
+
BUILD rc 1 build.unsupported-build-root (case SENSITIVE)
|
|
1216
|
+
(empty) rc 1 build.unsupported-build-root
|
|
1217
|
+
.. rc 1 build.unsupported-build-root
|
|
1218
|
+
../build rc 1 build.unsupported-build-root
|
|
1219
|
+
/tmp/abs/build rc 1 build.unsupported-build-root
|
|
1220
|
+
|
|
1221
|
+
`Path.parts` reproduces all ten: it drops a trailing separator and a
|
|
1222
|
+
leading `./` (so the two accepted aliases stay accepted), keeps `..` and
|
|
1223
|
+
a leading root as components of their own, and is empty for `""`.
|
|
1224
|
+
"""
|
|
1225
|
+
return Path(build_root).parts == (CONSUMER_BUILD_ROOT,)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _demoted_artefact_issues(plan: BuildPlan, demotions) -> list[Issue]:
|
|
1229
|
+
"""`executionPolicy.missingTool`, applied to a MATERIALISE run's token
|
|
1230
|
+
demotions (tan-cli#565) -- RAISES on `fail`, returns one warning per
|
|
1231
|
+
demoted slice on `skip`.
|
|
1232
|
+
|
|
1233
|
+
`substitute_plan_tokens` STRIPS a demoted slice's `configArtefacts`, so
|
|
1234
|
+
without this the files that slice's core needs are simply absent from
|
|
1235
|
+
both `written` and the disk, at exit 0 with `issues: []` -- a CI step
|
|
1236
|
+
that materialises and then runs `west build` for that core gets DEFAULT
|
|
1237
|
+
Kconfig instead of the project's, with no signal on either side. Routed
|
|
1238
|
+
to `missingTool` because a demotion is a host-provisioning fact, the same
|
|
1239
|
+
seam `_dispatch` routes it to for a run that goes on to dispatch.
|
|
1240
|
+
|
|
1241
|
+
NOT filtered the way `_dispatch`'s `held` is. There, a demoted slice
|
|
1242
|
+
whose backend is unknown or whose `command` is null is left alone because
|
|
1243
|
+
something else already outranks a provisioning fact AT DISPATCH. Nothing
|
|
1244
|
+
is being dispatched here -- the question is only whether a file was
|
|
1245
|
+
written -- so every demotion counts. MEASURED against the oracle with a
|
|
1246
|
+
four-slice plan (demoted + `command: null`; demoted + unknown backend;
|
|
1247
|
+
demoted + ordinary; undemoted): all three demoted slices were reported,
|
|
1248
|
+
under both actions, and only the undemoted slice's artefact was written.
|
|
1249
|
+
|
|
1250
|
+
The two shapes are the oracle's own, verbatim:
|
|
1251
|
+
|
|
1252
|
+
* `skip` -> exit 0, one `warning` PER slice, "slice `x`:
|
|
1253
|
+
configArtefacts not materialised — <reason>".
|
|
1254
|
+
* `fail` -> exit 1, `data: null`, ONE `error` whose message is every
|
|
1255
|
+
slice's "slice `x`: <reason>" sentence joined by newlines.
|
|
1256
|
+
|
|
1257
|
+
`reason` is threaded through verbatim from the demotion rather than
|
|
1258
|
+
re-derived, exactly as `_dispatch` does -- it is the second half of one
|
|
1259
|
+
toolchain resolution (`ToolchainResolution.advice`), and re-deriving it
|
|
1260
|
+
here would let the sentence drift from the value it explains.
|
|
1261
|
+
"""
|
|
1262
|
+
action = resolve_action(plan.execution_policy, "missing_tool", PolicyAction.SKIP)
|
|
1263
|
+
if action is PolicyAction.FAIL:
|
|
1264
|
+
raise BuildError(
|
|
1265
|
+
"build.toolchain-root-unresolved",
|
|
1266
|
+
"\n".join(f"slice `{d.core_id}`: {d.reason}" for d in demotions),
|
|
1267
|
+
ExitCode.RUNTIME_FAILURE,
|
|
1268
|
+
)
|
|
1269
|
+
return [
|
|
1270
|
+
Issue(
|
|
1271
|
+
"build.toolchain-root-unresolved",
|
|
1272
|
+
"warning",
|
|
1273
|
+
f"slice `{d.core_id}`: configArtefacts not materialised — {d.reason}",
|
|
1274
|
+
)
|
|
1275
|
+
for d in demotions
|
|
1276
|
+
]
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _build(
|
|
1280
|
+
*,
|
|
1281
|
+
# Defaulted so `mode` stays optional for the OTHER caller of this engine:
|
|
1282
|
+
# `run_cmd._run` builds unconditionally (there is no `tan run
|
|
1283
|
+
# --plan`/`--materialise`) and has no mode to pass.
|
|
1284
|
+
mode: str = _MODE_NATIVE,
|
|
1285
|
+
plan_from: str | None,
|
|
1286
|
+
build_root: str,
|
|
1287
|
+
sdk_root: str | None,
|
|
1288
|
+
sdk_root_for_stamp: str | None,
|
|
1289
|
+
board_yaml: str | None,
|
|
1290
|
+
# Defaulted for the same reason as `mode`: `run_cmd._run`
|
|
1291
|
+
# calls this with no format preference of its own today. Threaded
|
|
1292
|
+
# straight through to `_dispatch`, which is where the heartbeat this
|
|
1293
|
+
# gates is actually armed -- see that function's own docstring.
|
|
1294
|
+
json_mode: bool = False,
|
|
1295
|
+
# Defaulted for the same reason as `mode`/`json_mode`: `run_cmd._run` has
|
|
1296
|
+
# no `tan build --pristine` equivalent of its own yet (`tan run` has no
|
|
1297
|
+
# `--pristine` flag). Threaded straight through to `_dispatch`.
|
|
1298
|
+
pristine: bool = False,
|
|
1299
|
+
# tan-cli#1359: the synthesised single-Zephyr-slice plan text for a
|
|
1300
|
+
# `board.yaml`-less example (`tan build --board`). Replaces the planner.
|
|
1301
|
+
plain_plan_text: str | None = None,
|
|
1302
|
+
# tan-cli#1382: normalised `-D` args (and optional `--core` scope) spliced
|
|
1303
|
+
# into a PLANNED build's Zephyr slices. Unused on the plain route, which
|
|
1304
|
+
# already carries its defines in `plain_plan_text`.
|
|
1305
|
+
defines: list[str] | None = None,
|
|
1306
|
+
define_cores: list[str] | None = None,
|
|
1307
|
+
) -> tuple[ExitCode, dict, list[Issue]]:
|
|
1308
|
+
define_slices: list[str] = []
|
|
1309
|
+
if plain_plan_text is not None:
|
|
1310
|
+
text = plain_plan_text
|
|
1311
|
+
try:
|
|
1312
|
+
plan = parse_build_plan(text)
|
|
1313
|
+
except PlanParseError as err: # pragma: no cover -- tan's own plan
|
|
1314
|
+
raise BuildError(err.code, err.message, ExitCode.RUNTIME_FAILURE) from err
|
|
1315
|
+
if defines:
|
|
1316
|
+
define_slices = [sl.core_id for sl in plan.slices]
|
|
1317
|
+
else:
|
|
1318
|
+
text, plan = _acquire_plan(plan_from, sdk_root, board_yaml)
|
|
1319
|
+
if defines:
|
|
1320
|
+
try:
|
|
1321
|
+
text, define_slices = apply_user_defines(text, defines, define_cores)
|
|
1322
|
+
except UserDefineError as err:
|
|
1323
|
+
raise BuildError(err.code, err.message, ExitCode.VALIDATION_FAILURE) from err
|
|
1324
|
+
plan = parse_build_plan(text)
|
|
1325
|
+
|
|
1326
|
+
if mode == _MODE_PLAN:
|
|
1327
|
+
# Parsed (so a plan that will not load is still refused with its own
|
|
1328
|
+
# code -- measured: the oracle rejects `schemaVersion: 2` and an
|
|
1329
|
+
# unknown `executionPolicy` action in THIS mode too), then echoed
|
|
1330
|
+
# verbatim. No token substitution: the oracle shows a `planPathMode:
|
|
1331
|
+
# tokened` plan with no SDK resolvable at exit 0, unsubstituted, and
|
|
1332
|
+
# only refuses it once `--materialise` asks for it on disk.
|
|
1333
|
+
return ExitCode.SUCCESS, json.loads(text), []
|
|
1334
|
+
|
|
1335
|
+
# Substitution runs on the in-memory plan BEFORE materialise writes
|
|
1336
|
+
# anything and before any command is assembled, so an unresolvable token
|
|
1337
|
+
# can never reach disk or an argv. A no-op on an untokened plan -- e.g. an
|
|
1338
|
+
# old `--plan-from` file (the planner itself now tags every plan tokened).
|
|
1339
|
+
toolchain = _toolchain_for_plan(text)
|
|
1340
|
+
deferred: list[DeferredPlaceholder] = []
|
|
1341
|
+
build_python = resolve_build_python(plan, text, build_root, sdk_root)
|
|
1342
|
+
try:
|
|
1343
|
+
plan, demotions = apply_plan_token_substitution(
|
|
1344
|
+
plan,
|
|
1345
|
+
board_yaml_path=board_yaml,
|
|
1346
|
+
exec_base=build_root,
|
|
1347
|
+
sdk_root=sdk_root,
|
|
1348
|
+
python=build_python.python,
|
|
1349
|
+
toolchain_root=toolchain.root,
|
|
1350
|
+
toolchain_advice=toolchain.advice,
|
|
1351
|
+
deferred_out=deferred,
|
|
1352
|
+
project_root=build_root if plain_plan_text is not None else None,
|
|
1353
|
+
)
|
|
1354
|
+
except TokenSubstitutionError as err:
|
|
1355
|
+
# RuntimeFailure for every code this pass raises, `build.plan-invalid`
|
|
1356
|
+
# (an unknown `planPathMode`) included -- same ladder position the Rust
|
|
1357
|
+
# oracle gives them (`native.rs:132-142`), and the same code must not
|
|
1358
|
+
# mean two different exits depending on which module raised it.
|
|
1359
|
+
raise BuildError(err.code, err.message, ExitCode.RUNTIME_FAILURE) from err
|
|
1360
|
+
|
|
1361
|
+
python_issues = _host_python_issues(build_python, mode)
|
|
1362
|
+
|
|
1363
|
+
# tan-cli#566, and tan-cli#565's `fail` arm: BOTH sit here, between
|
|
1364
|
+
# substitution and `materialise_plan` below, because both must leave a
|
|
1365
|
+
# refused run with NOTHING on disk. `materialise_plan` runs unconditionally
|
|
1366
|
+
# and above the mode check, so a guard placed inside the `_MODE_MATERIALISE`
|
|
1367
|
+
# block below would refuse only AFTER the other slices' files had landed --
|
|
1368
|
+
# measured, the oracle's own refusals leave the tree empty in both cases.
|
|
1369
|
+
#
|
|
1370
|
+
# After substitution, not before, also measured: a plan that is BOTH
|
|
1371
|
+
# `planPathMode: legacy` and `buildRoot: out` is refused by the oracle with
|
|
1372
|
+
# `build.plan-invalid`, not `build.unsupported-build-root`, so the
|
|
1373
|
+
# substitution pass keeps its precedence.
|
|
1374
|
+
#
|
|
1375
|
+
# NATIVE only for the build root (tan-cli#566). The oracle accepts
|
|
1376
|
+
# `buildRoot: out` under `--materialise` AND `--plan` and refuses it only on
|
|
1377
|
+
# the path that goes on to DISPATCH -- measured, all three modes -- and that
|
|
1378
|
+
# split is the right one rather than an inconsistency to iron out: the
|
|
1379
|
+
# hazard is `system-manifest.yaml`, which only a dispatching run writes.
|
|
1380
|
+
# `_MODE_NATIVE` is also what `--execute` and `run_cmd._run` select, so both
|
|
1381
|
+
# of those are covered by the same one check. Nothing narrows to
|
|
1382
|
+
# `--plan-from`: every live plan the in-process planner emits has
|
|
1383
|
+
# `buildRoot: build` (`tan.planner.cli`/`planner_root.emit` default it to
|
|
1384
|
+
# `Path("build")` and `_emit_plan` never overrides), so this can only ever
|
|
1385
|
+
# fire on a plan file, which is exactly the shape the issue reports.
|
|
1386
|
+
if mode == _MODE_NATIVE and not _build_root_is_consumer_default(plan.build_root):
|
|
1387
|
+
raise BuildError(
|
|
1388
|
+
"build.unsupported-build-root",
|
|
1389
|
+
f"plan buildRoot `{plan.build_root}` is not `{CONSUMER_BUILD_ROOT}`; "
|
|
1390
|
+
"tan's flash/size/image read "
|
|
1391
|
+
"`<project>/build/system-manifest.yaml`, so building elsewhere "
|
|
1392
|
+
"would leave them reading a stale or missing manifest",
|
|
1393
|
+
ExitCode.RUNTIME_FAILURE,
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
# MATERIALISE only for the demotions (tan-cli#565): `_MODE_NATIVE` already
|
|
1397
|
+
# routes them, per slice, through `_dispatch`'s own `missing_tool` seam, and
|
|
1398
|
+
# `_MODE_PLAN` returned above without substituting at all. Raises on `fail`,
|
|
1399
|
+
# so nothing below runs; returns the per-slice warnings on `skip`, carried
|
|
1400
|
+
# to the return statement rather than emitted here.
|
|
1401
|
+
demotion_issues = (
|
|
1402
|
+
_demoted_artefact_issues(plan, demotions)
|
|
1403
|
+
if mode == _MODE_MATERIALISE and demotions
|
|
1404
|
+
else []
|
|
1405
|
+
)
|
|
1406
|
+
# tan-cli#1302: placeholders deliberately left in a config artefact, and a
|
|
1407
|
+
# malformed `deferredPlaceholders`. Reported on EVERY path that
|
|
1408
|
+
# materialises, so the native build says it too, not just `--materialise`.
|
|
1409
|
+
deferred_issues = deferred_placeholder_issues(plan, deferred)
|
|
1410
|
+
|
|
1411
|
+
# I-20. ALL of them, then dispatch -- never interleaved.
|
|
1412
|
+
try:
|
|
1413
|
+
materialise_plan(plan, Path(build_root))
|
|
1414
|
+
except MaterialiseError as err:
|
|
1415
|
+
raise BuildError(
|
|
1416
|
+
"build.materialise-failed", err.message, ExitCode.WRITE_FAILURE
|
|
1417
|
+
) from err
|
|
1418
|
+
|
|
1419
|
+
if mode == _MODE_MATERIALISE:
|
|
1420
|
+
# Write and stop -- the oracle dispatches nothing here, and its `data`
|
|
1421
|
+
# carries no `slices` at all. `written` is rebuilt from the plan's own
|
|
1422
|
+
# (post-substitution) artefact paths in `materialise_plan`'s documented
|
|
1423
|
+
# order, shared first then per slice, because those are the RELATIVE
|
|
1424
|
+
# strings the oracle reports; `materialise_plan` itself hands back
|
|
1425
|
+
# resolved absolute paths, and relativising those back re-introduces
|
|
1426
|
+
# every symlink/case difference `_abs_posix` exists to avoid.
|
|
1427
|
+
written = [a["path"] for a in plan.shared_artefacts]
|
|
1428
|
+
for sl in plan.slices:
|
|
1429
|
+
written.extend(a["path"] for a in sl.config_artefacts)
|
|
1430
|
+
return (
|
|
1431
|
+
ExitCode.SUCCESS,
|
|
1432
|
+
{"schemaVersion": "1", "baseDir": build_root, "written": written},
|
|
1433
|
+
demotion_issues + deferred_issues + python_issues,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
# Cleared before dispatch, mirroring `run_cmd.py`'s own pattern, so a
|
|
1437
|
+
# leftover signal from an earlier in-process `_build` (e.g. a test
|
|
1438
|
+
# harness calling `build()` more than once) never gets misread as THIS
|
|
1439
|
+
# run's own write outcome below.
|
|
1440
|
+
reset_last_manifest_write()
|
|
1441
|
+
# tan-cli#1376: advisory only -- never changes `ok`/the exit code.
|
|
1442
|
+
patch_issues = workspace_patch_issues(
|
|
1443
|
+
Path(build_root),
|
|
1444
|
+
sdk_root,
|
|
1445
|
+
has_zephyr_slice=any(sl.backend == "zephyr" for sl in plan.slices),
|
|
1446
|
+
)
|
|
1447
|
+
outcomes, issues = _dispatch(
|
|
1448
|
+
plan,
|
|
1449
|
+
demotions,
|
|
1450
|
+
Path(build_root),
|
|
1451
|
+
sdk_root,
|
|
1452
|
+
sdk_root_for_stamp,
|
|
1453
|
+
json_mode=json_mode,
|
|
1454
|
+
pristine=pristine,
|
|
1455
|
+
slice_refusals=_python_refusals(build_python, demotions, mode),
|
|
1456
|
+
user_defines={c: define_pairs(defines) for c in define_slices} if defines else None,
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
any_failed = any(o.status not in ("succeeded", "skipped") for o in outcomes)
|
|
1460
|
+
# tan-cli#283: a plan whose EVERY slice was skipped (e.g. `west`/
|
|
1461
|
+
# `bitbake` absent from PATH) used to fall straight through this `if
|
|
1462
|
+
# any_failed` gate -- `skipped` is deliberately not a failure status on
|
|
1463
|
+
# its own line above -- and report `ok: true, exitCode: 0, issues: []`.
|
|
1464
|
+
# A JSON consumer cannot tell that from a real build. A PARTIAL build
|
|
1465
|
+
# (some slice built -- e.g. only the Zephyr cores, on a host with no
|
|
1466
|
+
# Yocto toolchain) is a legitimate, deliberate outcome and must stay
|
|
1467
|
+
# `ok: true`; refusing outright only when NOTHING built at all is the
|
|
1468
|
+
# narrower fix, not "any skip is now a failure".
|
|
1469
|
+
nothing_built = (
|
|
1470
|
+
bool(outcomes)
|
|
1471
|
+
and not any_failed
|
|
1472
|
+
and not any(o.status == "succeeded" for o in outcomes)
|
|
1473
|
+
)
|
|
1474
|
+
exit_code = ExitCode.RUNTIME_FAILURE if (any_failed or nothing_built) else ExitCode.SUCCESS
|
|
1475
|
+
if any_failed:
|
|
1476
|
+
issues.insert(
|
|
1477
|
+
0, Issue("build.slice-failed", "error", "one or more slices failed to build")
|
|
1478
|
+
)
|
|
1479
|
+
elif nothing_built:
|
|
1480
|
+
issues.insert(
|
|
1481
|
+
0,
|
|
1482
|
+
Issue(
|
|
1483
|
+
"build.nothing-built",
|
|
1484
|
+
"error",
|
|
1485
|
+
"no slice was built -- every slice was skipped",
|
|
1486
|
+
),
|
|
1487
|
+
)
|
|
1488
|
+
issues.extend(_backend_issues(plan, outcomes))
|
|
1489
|
+
# tan-cli#283: named per skipped/failed slice, even in the PARTIAL case
|
|
1490
|
+
# `nothing_built` does not cover -- a consumer reading only `issues[]`
|
|
1491
|
+
# (not `data.slices[]`) must still be able to tell "2 of 3 slices built"
|
|
1492
|
+
# from "3 of 3".
|
|
1493
|
+
issues.extend(_missing_tool_issues(plan, outcomes))
|
|
1494
|
+
# tan-cli#697: named per cross-drive-refused slice, same reasoning as
|
|
1495
|
+
# `_missing_tool_issues` immediately above -- a JSON consumer reading
|
|
1496
|
+
# only `issues[]` must see the specific cause, not just the generic
|
|
1497
|
+
# `build.slice-failed` header this promotion sits alongside.
|
|
1498
|
+
issues.extend(_cross_drive_issues(outcomes))
|
|
1499
|
+
issues.extend(_workspace_unresolved_issues(outcomes))
|
|
1500
|
+
# tan-cli#1000: the PLAN's own warnings, same reasoning again -- these
|
|
1501
|
+
# are the only place the planner explains a `command: null` slice, and
|
|
1502
|
+
# they reached `data.warnings` alone until now.
|
|
1503
|
+
issues.extend(_plan_warning_issues(plan.warnings))
|
|
1504
|
+
issues.extend(deferred_issues)
|
|
1505
|
+
issues.extend(python_issues)
|
|
1506
|
+
issues.extend(patch_issues)
|
|
1507
|
+
# The sdk-switch-pristine wipe (issue #52) must not be stderr-only in
|
|
1508
|
+
# JSON mode -- the VS Code extension only ever sees the envelope, not
|
|
1509
|
+
# `_stream`'s output. Verbatim oracle codes/severity
|
|
1510
|
+
# (`crates/tan-cli/src/commands/build/execute/mod.rs:591,617`):
|
|
1511
|
+
# `build.sdk-switch-pristine` for the wipe itself, `-failed` when the
|
|
1512
|
+
# wipe could not fully land.
|
|
1513
|
+
issues.extend(last_sdk_switch_issues())
|
|
1514
|
+
# tan-cli#655: the stale-configure-cache reset must not be stderr-only in
|
|
1515
|
+
# JSON mode either -- same reasoning as the sdk-switch-pristine wipe
|
|
1516
|
+
# immediately above.
|
|
1517
|
+
issues.extend(last_configure_cache_issues())
|
|
1518
|
+
|
|
1519
|
+
manifest_reason = last_manifest_write_failure()
|
|
1520
|
+
# The plain route (`--board`) has its own writer (tan-cli#1381), so a failed
|
|
1521
|
+
# write there is as real as on the planned route (tan-cli#1517).
|
|
1522
|
+
if manifest_reason is not None:
|
|
1523
|
+
# A failed manifest write used to report only through `on_output`
|
|
1524
|
+
# (stderr) -- so the envelope said `ok: true, issues: []` while
|
|
1525
|
+
# `system-manifest.yaml` still named the PREVIOUS run's status and
|
|
1526
|
+
# artefact, which `tan size` re-derives and `tan flash` would go on
|
|
1527
|
+
# to program. Verbatim oracle code/message
|
|
1528
|
+
# (`crates/tan-cli/src/commands/build/execute/mod.rs:809-814`):
|
|
1529
|
+
# dropping this half is exactly how a stale manifest used to look
|
|
1530
|
+
# identical to `ok: true`.
|
|
1531
|
+
issues.append(
|
|
1532
|
+
Issue(
|
|
1533
|
+
"build.manifest-write-failed",
|
|
1534
|
+
"warning",
|
|
1535
|
+
f"system-manifest.yaml was not updated — {manifest_reason}",
|
|
1536
|
+
)
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
data = {
|
|
1540
|
+
"schemaVersion": "1",
|
|
1541
|
+
"baseDir": build_root,
|
|
1542
|
+
"slices": [
|
|
1543
|
+
_slice_result(sl.core_id, sl.backend, outcome)
|
|
1544
|
+
for sl, outcome in zip(plan.slices, outcomes, strict=True)
|
|
1545
|
+
],
|
|
1546
|
+
# The plan's own warnings, verbatim. A `command: null` slice is a
|
|
1547
|
+
# legitimate skip whose REASON lives only here (I-11) -- dropping the
|
|
1548
|
+
# list leaves the user a slice that silently did not build. Passed
|
|
1549
|
+
# through untyped on purpose: the schema says new warning codes may
|
|
1550
|
+
# appear without a schemaVersion bump, so a consumer must not treat
|
|
1551
|
+
# them as a closed set, and neither may this.
|
|
1552
|
+
"warnings": plan.warnings,
|
|
1553
|
+
}
|
|
1554
|
+
if defines and define_slices:
|
|
1555
|
+
data["defines"] = {"args": list(defines), "slices": define_slices}
|
|
1556
|
+
return exit_code, data, issues
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _plain_core_id(board: str, sdk_root: str | None) -> str | None:
|
|
1560
|
+
"""The planner core id (`m55_he`) the plain route's board target maps to,
|
|
1561
|
+
from the bound SDK's SoM presets; `None` (the slice keeps the board
|
|
1562
|
+
qualifier's name) when the SDK or the target is unknown (tan-cli#1370)."""
|
|
1563
|
+
if sdk_root is None or not is_sdk_root(sdk_root):
|
|
1564
|
+
return None
|
|
1565
|
+
try:
|
|
1566
|
+
from tan.planner_root import bind_sdk_root
|
|
1567
|
+
|
|
1568
|
+
bind_sdk_root(sdk_root)
|
|
1569
|
+
from tan.planner.plain_slice import plain_core_id
|
|
1570
|
+
|
|
1571
|
+
return plain_core_id(board, Path(sdk_root) / "metadata")
|
|
1572
|
+
except Exception: # noqa: BLE001 -- best effort; the fallback name still builds
|
|
1573
|
+
return None
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def _refuse_plain_route(
|
|
1577
|
+
board: str | None,
|
|
1578
|
+
define: list[str] | None,
|
|
1579
|
+
board_yaml: str | None,
|
|
1580
|
+
plan_from: str | None,
|
|
1581
|
+
mode: str,
|
|
1582
|
+
json_mode: bool,
|
|
1583
|
+
) -> None:
|
|
1584
|
+
"""Refuse a bad `--board` / `-D` combination up front (exit 2), else return."""
|
|
1585
|
+
conflict = None
|
|
1586
|
+
if board is not None and (board_yaml is not None or plan_from is not None):
|
|
1587
|
+
flag = "--board-yaml" if board_yaml is not None else "--plan-from"
|
|
1588
|
+
conflict = (
|
|
1589
|
+
"`--board` (a plain Zephyr example, no board.yaml) cannot be combined with "
|
|
1590
|
+
f"`{flag}` (a planned build). Pick one route."
|
|
1591
|
+
)
|
|
1592
|
+
elif board is not None and mode != _MODE_NATIVE:
|
|
1593
|
+
conflict = "`--board` only builds; drop `--materialise`."
|
|
1594
|
+
if conflict is not None:
|
|
1595
|
+
_refuse("build.conflicting-flags", conflict, ExitCode.VALIDATION_FAILURE, json_mode)
|
|
1596
|
+
problem = (board_target_problem(board) if board is not None else None) or normalise_defines(
|
|
1597
|
+
define or []
|
|
1598
|
+
)[1]
|
|
1599
|
+
if problem is not None:
|
|
1600
|
+
_refuse("build.invalid-argument", problem, ExitCode.VALIDATION_FAILURE, json_mode)
|
|
1601
|
+
reserved = user_defines_problem(normalise_defines(define or [])[0])
|
|
1602
|
+
if reserved is not None and reserved[0] == "build.define-reserved":
|
|
1603
|
+
_refuse("build.define-reserved", reserved[1], ExitCode.VALIDATION_FAILURE, json_mode)
|
|
1604
|
+
if reserved is not None:
|
|
1605
|
+
_refuse("build.invalid-argument", reserved[1], ExitCode.VALIDATION_FAILURE, json_mode)
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
def _refuse(code: str, message: str, exit_code: ExitCode, json_mode: bool) -> None:
|
|
1609
|
+
"""Report a refusal this command makes UP FRONT -- before any project is
|
|
1610
|
+
resolved, any plan read, and any byte written -- and exit.
|
|
1611
|
+
|
|
1612
|
+
One helper for both of them (a retired flag, and a conflicting flag pair)
|
|
1613
|
+
because they differ only in code, message and exit code. `project` is left
|
|
1614
|
+
unresolved: nothing was read, so reporting a root would be reporting work
|
|
1615
|
+
this run did not do."""
|
|
1616
|
+
issue = Issue(code, "error", message)
|
|
1617
|
+
if json_mode:
|
|
1618
|
+
emit(
|
|
1619
|
+
Envelope(
|
|
1620
|
+
"build",
|
|
1621
|
+
Project(root=None, board_yaml=None),
|
|
1622
|
+
{"message": message},
|
|
1623
|
+
[issue],
|
|
1624
|
+
exit_code,
|
|
1625
|
+
)
|
|
1626
|
+
)
|
|
1627
|
+
else:
|
|
1628
|
+
print(f"error: {message}", file=sys.stderr)
|
|
1629
|
+
raise typer.Exit(int(exit_code))
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def build(
|
|
1633
|
+
plan_from: str = typer.Option(
|
|
1634
|
+
None,
|
|
1635
|
+
"--plan-from",
|
|
1636
|
+
metavar="FILE",
|
|
1637
|
+
help="Read the build plan from a JSON file instead of invoking the SDK planner. "
|
|
1638
|
+
"Implies --plan: shows the plan and exits unless --materialise or --execute "
|
|
1639
|
+
"is given.",
|
|
1640
|
+
),
|
|
1641
|
+
materialise: bool = typer.Option(
|
|
1642
|
+
False,
|
|
1643
|
+
"--materialise",
|
|
1644
|
+
help="Write the plan's generated files (shared artefacts + per-slice config) "
|
|
1645
|
+
"under the build root and stop, instead of just showing the plan.",
|
|
1646
|
+
),
|
|
1647
|
+
# Accepted and INERT, exactly as in the oracle ("This is the default; the
|
|
1648
|
+
# flag is kept as an explicit opt-in" -- its own `--native` help). Never
|
|
1649
|
+
# read below on purpose: a bare build is already native, and letting it
|
|
1650
|
+
# override the `--plan` implied by `--plan-from` is the divergence
|
|
1651
|
+
# `test_plan_from_shows_the_plan_and_writes_nothing_even_with_native`
|
|
1652
|
+
# exists to keep out. `--execute` is the flag that dispatches a
|
|
1653
|
+
# file-supplied plan.
|
|
1654
|
+
native: bool = typer.Option(
|
|
1655
|
+
False,
|
|
1656
|
+
"--native",
|
|
1657
|
+
help="Build natively: materialise the plan, then run each slice's command. "
|
|
1658
|
+
"The default when no plan-mode flag is given. Does NOT override the --plan "
|
|
1659
|
+
"implied by --plan-from -- use --execute for that.",
|
|
1660
|
+
),
|
|
1661
|
+
execute: bool = typer.Option(
|
|
1662
|
+
False,
|
|
1663
|
+
"--execute",
|
|
1664
|
+
help="Materialise the plan AND run each slice's command, even when the plan "
|
|
1665
|
+
"came from --plan-from -- run a pinned, reviewed plan file reproducibly. "
|
|
1666
|
+
"Implies --materialise (nothing can run that was never written); reports the "
|
|
1667
|
+
"ordinary build result.",
|
|
1668
|
+
),
|
|
1669
|
+
build_root: str = typer.Option(
|
|
1670
|
+
None,
|
|
1671
|
+
"--build-root",
|
|
1672
|
+
metavar="DIR",
|
|
1673
|
+
help="Project tree the slices run under and artefacts are written below "
|
|
1674
|
+
"(default: the board.yaml's directory, else the current directory).",
|
|
1675
|
+
),
|
|
1676
|
+
sdk_root: str = typer.Option(
|
|
1677
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1678
|
+
),
|
|
1679
|
+
board_yaml: str = typer.Option(
|
|
1680
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
1681
|
+
),
|
|
1682
|
+
project: str = typer.Option(
|
|
1683
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1684
|
+
),
|
|
1685
|
+
board: str = typer.Option(
|
|
1686
|
+
None,
|
|
1687
|
+
"--board",
|
|
1688
|
+
metavar="TARGET",
|
|
1689
|
+
help="Build a plain Zephyr example that has no board.yaml (alp-sdk's bench "
|
|
1690
|
+
"examples) as one Zephyr slice for this board target, e.g. "
|
|
1691
|
+
"`alp_e1m_aen803_m55_he/ae822fa0e5597ls0/rtss_he`. The app is `--project` "
|
|
1692
|
+
"(default '.'); west, toolchain, SDK and host-Python resolve as for a planned "
|
|
1693
|
+
"build (tan-cli#1359).",
|
|
1694
|
+
),
|
|
1695
|
+
define: list[str] = typer.Option(
|
|
1696
|
+
None,
|
|
1697
|
+
"-D",
|
|
1698
|
+
"--define",
|
|
1699
|
+
metavar="NAME=VALUE",
|
|
1700
|
+
help="A CMake definition passed after `--` to west on every Zephyr slice "
|
|
1701
|
+
"(or those named by --core), repeatable (`-D SHIELD=...`, `-D CONFIG_X=y`). "
|
|
1702
|
+
"Applied AFTER the plan's own args so it overrides them, except "
|
|
1703
|
+
"EXTRA_CONF_FILE / EXTRA_DTC_OVERLAY_FILE, which are appended `;`-joined to "
|
|
1704
|
+
"tan's list; BOARD and Python3_EXECUTABLE are refused (tan-cli#1382).",
|
|
1705
|
+
),
|
|
1706
|
+
core: list[str] = typer.Option(
|
|
1707
|
+
None,
|
|
1708
|
+
"--core",
|
|
1709
|
+
metavar="ID",
|
|
1710
|
+
help="Scope -D to the Zephyr slice(s) with this coreId, repeatable "
|
|
1711
|
+
"(default: every Zephyr slice).",
|
|
1712
|
+
),
|
|
1713
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
1714
|
+
pristine: bool = typer.Option(
|
|
1715
|
+
False,
|
|
1716
|
+
"--pristine",
|
|
1717
|
+
help="Force-wipe every slice's build dir before dispatch, regardless of the "
|
|
1718
|
+
"recorded SDK-switch stamp (tan-cli#163) -- the manual counterpart to the "
|
|
1719
|
+
"automatic sdk-switch-pristine wipe, for a stale build dir the stamp heuristic "
|
|
1720
|
+
"doesn't (or can't yet) catch. Same wipe, same two safety guards (an explicit "
|
|
1721
|
+
"`-d`/`--build-dir` in the slice's own command, or a plan cwd outside `build/`): "
|
|
1722
|
+
"this never touches a dir tan can't vouch for. A slice the wipe declines -- for "
|
|
1723
|
+
"either guard, or because the dir was never configured -- says so on the "
|
|
1724
|
+
"envelope and in text (`build.pristine-skipped`), so \"pristine\" never "
|
|
1725
|
+
"silently means \"incremental\".",
|
|
1726
|
+
),
|
|
1727
|
+
# --- retired: parsed (never a Click typo error) but always refused, ------
|
|
1728
|
+
# naming the replacement in the message itself (tan-cli#427). NOT
|
|
1729
|
+
# `inert_help`-marked: unlike the accept-and-drop global flags, these are
|
|
1730
|
+
# READ for real -- their value selects a specific refusal, not "accepted
|
|
1731
|
+
# and does nothing" (see `_RETIRED_FLAGS`).
|
|
1732
|
+
plan: bool = typer.Option(
|
|
1733
|
+
False, "--plan", help="Retired -- superseded by `--plan-from` (tan-cli#427)."
|
|
1734
|
+
),
|
|
1735
|
+
manifest: bool = typer.Option(
|
|
1736
|
+
False,
|
|
1737
|
+
"--manifest",
|
|
1738
|
+
help="Retired -- a native `tan build` already writes `build/system-manifest.yaml` "
|
|
1739
|
+
"(tan-cli#427).",
|
|
1740
|
+
),
|
|
1741
|
+
manifest_from: str = typer.Option(
|
|
1742
|
+
None,
|
|
1743
|
+
"--manifest-from",
|
|
1744
|
+
metavar="FILE",
|
|
1745
|
+
help="Retired -- open FILE directly, it is plain YAML (tan-cli#427).",
|
|
1746
|
+
),
|
|
1747
|
+
# A real, working flag of the v0.4.1 oracle, retired here rather than
|
|
1748
|
+
# implemented (tan-cli#427) -- `tan build` has no implicit bootstrap for it
|
|
1749
|
+
# to switch off. LISTED in `--help` rather than hidden, for the same reason
|
|
1750
|
+
# the three above are: a reader comparing this surface to v0.4.1's needs to
|
|
1751
|
+
# see that tan knows the flag and is refusing it, which absence cannot say.
|
|
1752
|
+
no_auto_bootstrap: bool = typer.Option(
|
|
1753
|
+
False,
|
|
1754
|
+
"--no-auto-bootstrap",
|
|
1755
|
+
help="Retired -- `tan build` never bootstraps implicitly (tan-cli#427).",
|
|
1756
|
+
),
|
|
1757
|
+
) -> None:
|
|
1758
|
+
"""Build every slice of the project's build plan."""
|
|
1759
|
+
json_mode = output_format == "json"
|
|
1760
|
+
|
|
1761
|
+
# Before anything is resolved or read: a run naming a retired or deferred
|
|
1762
|
+
# flag does no work at all, and says which flag and why. Retired flags
|
|
1763
|
+
# first (`_RETIRED_FLAGS` already teaches the replacement in its own
|
|
1764
|
+
# message), then the one still-deferred flag -- same precedence reasoning
|
|
1765
|
+
# as before: a flag this build cannot honour in any combination gets the
|
|
1766
|
+
# accurate answer ahead of the `--execute`/`--materialise` conflict check
|
|
1767
|
+
# below, rather than a conflict message for a flag that would not have
|
|
1768
|
+
# worked anyway.
|
|
1769
|
+
retired_given = {
|
|
1770
|
+
"--plan": plan,
|
|
1771
|
+
"--manifest": manifest,
|
|
1772
|
+
"--manifest-from": manifest_from is not None,
|
|
1773
|
+
"--no-auto-bootstrap": no_auto_bootstrap,
|
|
1774
|
+
}
|
|
1775
|
+
for flag, was_given in retired_given.items():
|
|
1776
|
+
if was_given:
|
|
1777
|
+
_refuse(
|
|
1778
|
+
"build.flag-retired",
|
|
1779
|
+
_RETIRED_FLAGS[flag],
|
|
1780
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1781
|
+
json_mode,
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
if execute and materialise:
|
|
1785
|
+
# Two different answers to "what does this run leave behind" -- one
|
|
1786
|
+
# stops after writing, one goes on to dispatch. Refused with its own
|
|
1787
|
+
# code at exit 2 (this CLI's position for an invalid invocation, the
|
|
1788
|
+
# same `--format bogus` takes) instead of letting either win quietly:
|
|
1789
|
+
# a silent precedence win here is precisely what `--execute` exists to
|
|
1790
|
+
# replace.
|
|
1791
|
+
_refuse(
|
|
1792
|
+
"build.conflicting-flags",
|
|
1793
|
+
"`--execute` and `--materialise` cannot be combined: `--materialise` "
|
|
1794
|
+
"writes the plan's files and stops, `--execute` writes them and then "
|
|
1795
|
+
"runs each slice. Pick one (`--execute` already implies writing).",
|
|
1796
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1797
|
+
json_mode,
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
# The oracle's own precedence, measured, reproduced exactly -- `--native`
|
|
1801
|
+
# included: `--plan-from` implies `--plan`, and that implied `--plan` beats
|
|
1802
|
+
# an explicit `--native`, so `--plan-from --native` shows the plan and
|
|
1803
|
+
# writes nothing here too. `--execute` is the port's ADDED capability (see
|
|
1804
|
+
# the module docstring) and the only thing that dispatches a file-supplied
|
|
1805
|
+
# plan; it selects the same `_MODE_NATIVE` a plain `tan build` runs, so it
|
|
1806
|
+
# reports the same `data` rather than a shape of its own. With no
|
|
1807
|
+
# `--plan-from` it is a no-op -- that run already builds.
|
|
1808
|
+
if execute:
|
|
1809
|
+
mode = _MODE_NATIVE
|
|
1810
|
+
elif materialise:
|
|
1811
|
+
mode = _MODE_MATERIALISE
|
|
1812
|
+
elif plan_from is not None:
|
|
1813
|
+
mode = _MODE_PLAN
|
|
1814
|
+
else:
|
|
1815
|
+
mode = _MODE_NATIVE
|
|
1816
|
+
|
|
1817
|
+
# `--pristine`'s own help text promises "never silently means incremental"
|
|
1818
|
+
# -- but `_build` only ever threads `pristine` into `_dispatch` (see its
|
|
1819
|
+
# tail), and `_MODE_PLAN`/`_MODE_MATERIALISE` both return from `_build`
|
|
1820
|
+
# BEFORE `_dispatch` is reached (the former shows the plan and stops, the
|
|
1821
|
+
# latter writes config files and stops -- neither runs a slice, so neither
|
|
1822
|
+
# has a build dir to wipe). Left unchecked, `--pristine --plan-from p.json`
|
|
1823
|
+
# or `--pristine --materialise ...` used to exit 0 with `issues: []`,
|
|
1824
|
+
# having silently done nothing with the flag -- precisely the "declared
|
|
1825
|
+
# and does nothing" shape `--no-auto-bootstrap` above is refused for, and
|
|
1826
|
+
# the closed-issue tan-cli#183 defect class re-entering through the mode
|
|
1827
|
+
# flags instead of the flag that issue was about. Refused with the same
|
|
1828
|
+
# code and exit position `--execute --materialise` already uses for an
|
|
1829
|
+
# invalid combination, rather than added as a fourth mode-specific `data`
|
|
1830
|
+
# shape: `--pristine` has no meaning without a dispatch to apply it to, so
|
|
1831
|
+
# this is a conflicting-flags case, not a new envelope shape to design.
|
|
1832
|
+
# `--execute` is exempt: it selects `_MODE_NATIVE` (line 1918 above) and
|
|
1833
|
+
# dispatches for real, so `--pristine --plan-from p.json --execute` reaches
|
|
1834
|
+
# `_dispatch` and wipes exactly as `--pristine` alone does.
|
|
1835
|
+
if pristine and mode != _MODE_NATIVE:
|
|
1836
|
+
stops_after = (
|
|
1837
|
+
"writes the plan's files"
|
|
1838
|
+
if mode == _MODE_MATERIALISE
|
|
1839
|
+
else "shows the plan"
|
|
1840
|
+
)
|
|
1841
|
+
flag = "--materialise" if mode == _MODE_MATERIALISE else "--plan-from"
|
|
1842
|
+
_refuse(
|
|
1843
|
+
"build.conflicting-flags",
|
|
1844
|
+
f"`--pristine` has nothing to wipe without a dispatch: `{flag}` alone "
|
|
1845
|
+
f"only {stops_after} and stops before any slice runs. Add `--execute` "
|
|
1846
|
+
"to dispatch it for real, or drop `--pristine`.",
|
|
1847
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1848
|
+
json_mode,
|
|
1849
|
+
)
|
|
1850
|
+
|
|
1851
|
+
if core and not define:
|
|
1852
|
+
_refuse(
|
|
1853
|
+
"build.invalid-argument",
|
|
1854
|
+
"`--core` only scopes `-D`; pass `-D NAME=VALUE` with it.",
|
|
1855
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1856
|
+
json_mode,
|
|
1857
|
+
)
|
|
1858
|
+
plain_plan_text: str | None = None
|
|
1859
|
+
# The raw flag, kept before `sdk_root` is reassigned to the RESOLVED root (None
|
|
1860
|
+
# for a rejected flag) so the refusal below can name what the caller typed.
|
|
1861
|
+
sdk_root_flag = sdk_root
|
|
1862
|
+
if board is not None or define:
|
|
1863
|
+
_refuse_plain_route(board, define, board_yaml, plan_from, mode, json_mode)
|
|
1864
|
+
|
|
1865
|
+
# `util::cli_workspace_root`: `--project` joined to the (real) cwd, THEN
|
|
1866
|
+
# everything below anchors on this instead of the bare cwd -- board.yaml
|
|
1867
|
+
# discovery, the default build root, and SDK discovery. Was previously
|
|
1868
|
+
# missing entirely (this command had no `--project` at all): the extension
|
|
1869
|
+
# falls back to it when no project is active in the workspace
|
|
1870
|
+
# (`alp-sdk-vscode/src/west.ts`'s `alpBuild`, `--project <example> build`).
|
|
1871
|
+
# A Typer option Typer does not declare is a Click parse error (exit 2),
|
|
1872
|
+
# so the missing flag REFUSED outright rather than building silently; the
|
|
1873
|
+
# actual wrong-project risk is the relative `--board-yaml` anchor fixed
|
|
1874
|
+
# below. `project is None` (the overwhelming common case, no flag given)
|
|
1875
|
+
# makes `workspace_root == cwd`, byte-for-byte the prior behaviour -- this
|
|
1876
|
+
# only changes anything when `--project` is actually given.
|
|
1877
|
+
#
|
|
1878
|
+
# tan-cli#488 defect 8: safe defaults for every name the exception handler
|
|
1879
|
+
# below and the final `emit()` read, so a raise from ANYWHERE in this
|
|
1880
|
+
# resolution prologue -- not only inside `_build` -- still produces the
|
|
1881
|
+
# `build.internal-failure` envelope the guard below promises, instead of a
|
|
1882
|
+
# raw traceback with empty stdout. `Path.cwd()` immediately below is the
|
|
1883
|
+
# concrete case: a working directory deleted out from under the process
|
|
1884
|
+
# throws `FileNotFoundError` -- previously from OUTSIDE the `try`/`except`,
|
|
1885
|
+
# before `project`/`sdk`/`sdk_tier` had a real value, so it unwound through
|
|
1886
|
+
# typer's own traceback renderer instead of this command's error contract.
|
|
1887
|
+
# Mirrors `doctor_cmd.doctor`'s identical fix for the same defect class.
|
|
1888
|
+
project_envelope = Project(root=None, board_yaml=None)
|
|
1889
|
+
sdk: SdkInfo | None = None
|
|
1890
|
+
sdk_tier = "none"
|
|
1891
|
+
sdk_broken_pin: str | None = None
|
|
1892
|
+
sdk_foreign_default: str | None = None
|
|
1893
|
+
sdk_divergence: Issue | None = None
|
|
1894
|
+
try:
|
|
1895
|
+
cwd = Path.cwd()
|
|
1896
|
+
workspace_root = cwd if project is None else Path(os.path.join(str(cwd), project))
|
|
1897
|
+
|
|
1898
|
+
# Anchor an EXPLICIT `--board-yaml` on `workspace_root`, not the real cwd,
|
|
1899
|
+
# before anything derives from it (the default build root below included).
|
|
1900
|
+
# `_abs_posix` is purely lexical (`os.path.abspath`, which anchors on
|
|
1901
|
+
# `os.getcwd()`), so a relative `--board-yaml` left untouched re-anchors on
|
|
1902
|
+
# the real cwd instead -- matching Rust's `resolve_board_yaml_path`
|
|
1903
|
+
# (`crates/tan-core/src/project.rs:198-208`, which joins a relative
|
|
1904
|
+
# configured path onto `workspace_root`). Verified against the oracle in a
|
|
1905
|
+
# scratch tree (`tmp/app/board.yaml`, cwd=`tmp`): `tan --project app build
|
|
1906
|
+
# --board-yaml board.yaml --format json` must report `project.root` /
|
|
1907
|
+
# `project.boardYaml` under `tmp/app`, not `tmp` -- with a board.yaml also
|
|
1908
|
+
# sitting in the real cwd, the pre-fix anchor planned and built the WRONG
|
|
1909
|
+
# project without a word, exactly the failure class this port exists to
|
|
1910
|
+
# remove.
|
|
1911
|
+
if board_yaml is not None and not os.path.isabs(board_yaml):
|
|
1912
|
+
board_yaml = os.path.join(str(workspace_root), board_yaml)
|
|
1913
|
+
|
|
1914
|
+
# Resolution, in one place, before anything can fail -- and to ABSOLUTE,
|
|
1915
|
+
# BOTH sides, from the same anchor.
|
|
1916
|
+
#
|
|
1917
|
+
# The divergence guard in `apply_plan_token_substitution` compares these
|
|
1918
|
+
# two lexically (as the Rust oracle does), so they must move together: this
|
|
1919
|
+
# resolves both or neither, never one. But they must also both be absolute,
|
|
1920
|
+
# because `${PROJECT_ROOT}` is substituted from `board_yaml` and then
|
|
1921
|
+
# CONSUMED from a different directory -- each slice runs its command in its
|
|
1922
|
+
# own `command.cwd` (`<root>/build/<slice>`), and Zephyr resolves
|
|
1923
|
+
# `-DEXTRA_CONF_FILE` against the APPLICATION source dir, which for the
|
|
1924
|
+
# stock-shim slice is inside the SDK checkout entirely. Left relative, the
|
|
1925
|
+
# default `cd <project> && tan build` resolves `${PROJECT_ROOT}` to `"."`
|
|
1926
|
+
# and every zephyr slice dies -- `<sdk>/firmware/alp-stock-shim/./build/
|
|
1927
|
+
# <slice>/alp.conf: File not found`, and `ERROR: . doesn't contain a
|
|
1928
|
+
# CMakeLists.txt` -- which is the whole documented happy path. Rust never
|
|
1929
|
+
# hits this because it anchors first: `resolve_cli_project_context_inner`
|
|
1930
|
+
# builds `workspace_root = normalize_path(cwd.join(--project))` and derives
|
|
1931
|
+
# `board_yaml_path` by joining onto THAT, so both are absolute before the
|
|
1932
|
+
# guard ever runs (`crates/tan-cli/src/util.rs`, `crates/tan-core/src/
|
|
1933
|
+
# project.rs::resolve_board_yaml_path`).
|
|
1934
|
+
if board_yaml is None and (workspace_root / "board.yaml").is_file():
|
|
1935
|
+
# Already absolute (anchored on `workspace_root`, not a bare
|
|
1936
|
+
# relative "board.yaml") so `_abs_posix` below is a no-op
|
|
1937
|
+
# normalisation rather than a re-anchor onto the real cwd -- the
|
|
1938
|
+
# divergence that would reappear the moment `--project` differs
|
|
1939
|
+
# from cwd.
|
|
1940
|
+
board_yaml = str(workspace_root / "board.yaml")
|
|
1941
|
+
if board is not None:
|
|
1942
|
+
raise BuildError(
|
|
1943
|
+
"build.conflicting-flags",
|
|
1944
|
+
f"`--board` builds a project WITHOUT a board.yaml, but `{board_yaml}` "
|
|
1945
|
+
"exists -- drop `--board` to build it as a planned project.",
|
|
1946
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1947
|
+
)
|
|
1948
|
+
if build_root is None:
|
|
1949
|
+
build_root = str(Path(board_yaml).parent) if board_yaml else str(workspace_root)
|
|
1950
|
+
build_root = _abs_posix(build_root)
|
|
1951
|
+
if board_yaml is not None:
|
|
1952
|
+
board_yaml = _abs_posix(board_yaml)
|
|
1953
|
+
|
|
1954
|
+
# `--sdk-root` > the project's own `.alp/sdk-path` pin > the machine-global
|
|
1955
|
+
# default > the positional walk (`resolve_sdk_root_ladder`, above) --
|
|
1956
|
+
# previously this skipped straight from `--sdk-root` to the positional
|
|
1957
|
+
# walk, so `tan init`'s own pointer went unread the moment `tan build` ran
|
|
1958
|
+
# in the same directory.
|
|
1959
|
+
sdk_resolution = resolve_sdk_root_ladder(sdk_root, workspace_root)
|
|
1960
|
+
resolved_sdk_root = sdk_resolution.path
|
|
1961
|
+
sdk_tier = sdk_resolution.tier
|
|
1962
|
+
sdk_broken_pin = sdk_resolution.broken_project_pin
|
|
1963
|
+
sdk_foreign_default = sdk_resolution.foreign_global_default_for
|
|
1964
|
+
# tan-cli#407: computed HERE, while `sdk_root` still holds the raw
|
|
1965
|
+
# `--sdk-root` flag -- it is reassigned to the RESOLVED root a few lines
|
|
1966
|
+
# below, and a bogus flag is blanked to `None` in between, which would
|
|
1967
|
+
# make this warn about a discovery collision the user never reached
|
|
1968
|
+
# because they named a root explicitly.
|
|
1969
|
+
sdk_divergence = sdk_ladder_divergence_issue(sdk_root, workspace_root, wide=False)
|
|
1970
|
+
# tan-cli#257/#258: `resolve_sdk_root_ladder` returns an explicit
|
|
1971
|
+
# `--sdk-root` UNVALIDATED (I-31 terminal-for-REPORTING, matching the
|
|
1972
|
+
# oracle's `resolve_sdk_tiered`) -- fine for a caller that only reports
|
|
1973
|
+
# the tier, but `build` also ACTS on `resolved_sdk_root`, so a bogus flag
|
|
1974
|
+
# used to sail through as `sdk.sourceTier: "sdkRootFlag"`, reach
|
|
1975
|
+
# `_emit_plan` as a non-None `sdk_root`, and get refused for the NEXT
|
|
1976
|
+
# missing thing (`no board.yaml found`) instead -- telling the customer
|
|
1977
|
+
# their project is broken when the `--sdk-root` they just typed is what's
|
|
1978
|
+
# wrong, and reporting an `sdk` key the oracle never emits on this path.
|
|
1979
|
+
# Validated here, at the flag's own entry point, rather than in the
|
|
1980
|
+
# shared ladder (which every other caller also relies on staying
|
|
1981
|
+
# unvalidated) -- same shape as `clean_cmd.sdk_root_resolves` and
|
|
1982
|
+
# `flash_cmd._resolve_sdk`, the two callers that already guard their own
|
|
1983
|
+
# explicit `--sdk-root`. An unresolvable explicit root is treated as no
|
|
1984
|
+
# root at all: `_emit_plan` then gives its own "no alp-sdk checkout
|
|
1985
|
+
# found" refusal, and no `sdk` key is reported, matching the oracle.
|
|
1986
|
+
if sdk_tier == "sdkRootFlag" and not is_sdk_root(resolved_sdk_root):
|
|
1987
|
+
resolved_sdk_root = None
|
|
1988
|
+
sdk_root = str(resolved_sdk_root) if resolved_sdk_root is not None else None
|
|
1989
|
+
sdk = SdkInfo(sdk_root, sdk_tier) if sdk_root is not None else None
|
|
1990
|
+
if board is not None:
|
|
1991
|
+
plain_plan_text = plain_zephyr_plan(
|
|
1992
|
+
_abs_posix(str(workspace_root)),
|
|
1993
|
+
board,
|
|
1994
|
+
normalise_defines(define or [])[0],
|
|
1995
|
+
core_id=_plain_core_id(board, sdk_root),
|
|
1996
|
+
)
|
|
1997
|
+
# Absolute, `.`/`..`-collapsed, anchored on `workspace_root` -- what the
|
|
1998
|
+
# sdk-switch-pristine guard actually compares (tan-cli#163), kept
|
|
1999
|
+
# SEPARATE from `sdk_root` itself: an explicit `--sdk-root` is a
|
|
2000
|
+
# documented, supported relative form, and `sdk_root` unchanged still
|
|
2001
|
+
# feeds `${SDK_ROOT}` token substitution and the `sdk.root` envelope
|
|
2002
|
+
# field verbatim, matching the Rust oracle's own split between
|
|
2003
|
+
# `normalized_sdk_root_str` (stamp-only) and the raw `resolve_sdk_root`
|
|
2004
|
+
# result used everywhere else.
|
|
2005
|
+
sdk_root_for_stamp = (
|
|
2006
|
+
str(normalize_path(workspace_root / sdk_root)) if sdk_root is not None else None
|
|
2007
|
+
)
|
|
2008
|
+
# tan-cli#236: `boardYaml` reported only when the file really exists --
|
|
2009
|
+
# `board_yaml` itself stays unfiltered for `_build` below, which needs the
|
|
2010
|
+
# unconditional path.
|
|
2011
|
+
project_envelope = Project.resolved(build_root, board_yaml)
|
|
2012
|
+
|
|
2013
|
+
# tan-cli#287: the heartbeat that covers a silent slice (a first-build
|
|
2014
|
+
# CMake configure, measured at 234.2s with nothing printed) is armed
|
|
2015
|
+
# inside `_dispatch`, not here -- `_build`'s OTHER caller,
|
|
2016
|
+
# `run_cmd._run`, bottlenecks through the identical
|
|
2017
|
+
# `_dispatch` call and needs the same cover for a silent `tan run`. See
|
|
2018
|
+
# `_dispatch`'s own docstring for the TTY/`json_mode` gating (unchanged:
|
|
2019
|
+
# still never armed for `--format json`, stdout is untouched either way)
|
|
2020
|
+
# and for the mode-gating this also fixes for free -- `_MODE_PLAN`/
|
|
2021
|
+
# `_MODE_MATERIALISE` return from `_build` before `_dispatch` is ever
|
|
2022
|
+
# reached, so neither ticks a heartbeat for a slow in-process planner
|
|
2023
|
+
# that never runs CMake at all.
|
|
2024
|
+
exit_code, data, issues = _build(
|
|
2025
|
+
mode=mode,
|
|
2026
|
+
plan_from=plan_from,
|
|
2027
|
+
build_root=build_root,
|
|
2028
|
+
sdk_root=sdk_root,
|
|
2029
|
+
sdk_root_for_stamp=sdk_root_for_stamp,
|
|
2030
|
+
board_yaml=board_yaml,
|
|
2031
|
+
json_mode=json_mode,
|
|
2032
|
+
pristine=pristine,
|
|
2033
|
+
plain_plan_text=plain_plan_text,
|
|
2034
|
+
defines=normalise_defines(define or [])[0],
|
|
2035
|
+
define_cores=list(core) if core else None,
|
|
2036
|
+
)
|
|
2037
|
+
except BuildError as err:
|
|
2038
|
+
message = err.message
|
|
2039
|
+
if err.code == "build.sdk-root-unresolved":
|
|
2040
|
+
# tan-cli#1423: where the ladder looked, beside the message's own remedy.
|
|
2041
|
+
if (sdk_root_flag or "").strip():
|
|
2042
|
+
# `--sdk-root` is terminal (I-31): no other tier was tried, so
|
|
2043
|
+
# name the rejected value instead of recommending the flag or
|
|
2044
|
+
# claiming the ladder was searched (tan-cli#1521).
|
|
2045
|
+
message = rejected_sdk_root_message(sdk_root_flag, "Nothing was built.")
|
|
2046
|
+
else:
|
|
2047
|
+
message = with_sdk_search(message, workspace_root)
|
|
2048
|
+
exit_code, data, issues = err.exit_code, None, [Issue(err.code, "error", message)]
|
|
2049
|
+
except Exception as err: # noqa: BLE001 -- see below
|
|
2050
|
+
# The port's most-repeated defect class: an uncaught exception
|
|
2051
|
+
# escapes as a raw traceback, stdout stays empty, and the
|
|
2052
|
+
# extension renders nothing at all with no error on either side.
|
|
2053
|
+
# Anything that reaches here is a tan bug, so it is reported as
|
|
2054
|
+
# one -- with an envelope.
|
|
2055
|
+
exit_code = ExitCode.INTERNAL_FAILURE
|
|
2056
|
+
data = None
|
|
2057
|
+
issues = [Issue("build.internal-failure", "error", f"{type(err).__name__}: {err}")]
|
|
2058
|
+
|
|
2059
|
+
# These facts are all about how the SDK ROOT was RESOLVED, so all are
|
|
2060
|
+
# prepended ahead of whatever the build itself reported, and all survive a
|
|
2061
|
+
# run that never planned anything -- a refusal is exactly when knowing
|
|
2062
|
+
# which checkout answered matters most.
|
|
2063
|
+
#
|
|
2064
|
+
# tan-cli#263 review: `tan build` is the command a silently-missed
|
|
2065
|
+
# `.alp/sdk-path` pin hurts most -- it builds against whichever tier the
|
|
2066
|
+
# ladder fell through to and says nothing, where `sdk current` alone only
|
|
2067
|
+
# helps someone already suspicious enough to run it. tan-cli#407 is the
|
|
2068
|
+
# same argument for the two-checkout collision one tier below it, and
|
|
2069
|
+
# tan-cli#464 the same again for a `globalDefault` answer a DIFFERENT
|
|
2070
|
+
# project's bootstrap relocation actually decided -- `sdk current` was the
|
|
2071
|
+
# only place this warning reached before; `tan build` is the one that
|
|
2072
|
+
# silently compiles against the wrong checkout because of it.
|
|
2073
|
+
resolution_issues = [
|
|
2074
|
+
issue
|
|
2075
|
+
for issue in (
|
|
2076
|
+
project_pin_issue(sdk_broken_pin, sdk_tier),
|
|
2077
|
+
sdk_divergence,
|
|
2078
|
+
global_default_foreign_project_issue(sdk_foreign_default),
|
|
2079
|
+
)
|
|
2080
|
+
if issue is not None
|
|
2081
|
+
]
|
|
2082
|
+
if resolution_issues:
|
|
2083
|
+
issues = [*resolution_issues, *issues]
|
|
2084
|
+
|
|
2085
|
+
if json_mode:
|
|
2086
|
+
emit(Envelope("build", project_envelope, data, issues, exit_code, sdk=sdk))
|
|
2087
|
+
else:
|
|
2088
|
+
_print_text_issues(issues, data)
|
|
2089
|
+
_text_recap(mode, data)
|
|
2090
|
+
raise typer.Exit(int(exit_code))
|
|
2091
|
+
|
|
2092
|
+
|
|
2093
|
+
def _print_text_issues(issues: list[Issue], data: dict | None) -> None:
|
|
2094
|
+
"""Print the `issues[]` block of the text recap, minus any line the
|
|
2095
|
+
per-slice recap below is about to repeat verbatim (tan-cli#746).
|
|
2096
|
+
|
|
2097
|
+
Dedup and printing live in ONE function on purpose: a `_text_issues`
|
|
2098
|
+
that only FILTERED could be dropped from the call site and every test of
|
|
2099
|
+
it would stay green -- measured, that is exactly what happened (6 of 6
|
|
2100
|
+
passing with the call removed). Keeping the print here means a test that
|
|
2101
|
+
captures this function's output covers both halves.
|
|
2102
|
+
"""
|
|
2103
|
+
for issue in _text_issues(issues, data):
|
|
2104
|
+
print(f"{issue.severity}: {issue.message}", file=sys.stderr)
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
def _text_issues(issues: list[Issue], data: dict | None) -> list[Issue]:
|
|
2108
|
+
"""Drop an `issues[]` line whose text the per-slice recap is about to
|
|
2109
|
+
print verbatim anyway (tan-cli#746).
|
|
2110
|
+
|
|
2111
|
+
TEXT MODE ONLY -- the JSON envelope keeps every issue untouched. Two
|
|
2112
|
+
deliberate features collide here and neither is wrong on its own:
|
|
2113
|
+
`_missing_tool_issues` promotes a skip's reason into `issues[]` so a
|
|
2114
|
+
JSON consumer sees it (tan-cli#283), and that reason carries the literal
|
|
2115
|
+
searched-PATH list so a customer can fix it themselves (tan-cli#510).
|
|
2116
|
+
In `--format json` those are two different fields. In text they are two
|
|
2117
|
+
adjacent lines carrying the same string.
|
|
2118
|
+
|
|
2119
|
+
Measured on a Windows host with no `bitbake`, where the AEN801 SoM's
|
|
2120
|
+
`a32_cluster` yocto slice is an EXPECTED skip: 2807 + 2801 = 5608
|
|
2121
|
+
characters, twice the same 57-entry PATH, pushing the two `ok:` lines
|
|
2122
|
+
and the `2 of 3 slice(s) built` summary below it.
|
|
2123
|
+
|
|
2124
|
+
Matched on the reason text, not on the issue code: `_missing_tool_issues`
|
|
2125
|
+
is not the only producer that may wrap a slice reason, and a code list
|
|
2126
|
+
here would silently stop covering the next one. An issue whose message
|
|
2127
|
+
is NOT already on a slice line is always kept -- this only removes a
|
|
2128
|
+
second copy, never information.
|
|
2129
|
+
"""
|
|
2130
|
+
if not data:
|
|
2131
|
+
return issues
|
|
2132
|
+
reasons = {
|
|
2133
|
+
result["reason"]
|
|
2134
|
+
for result in (data.get("slices") or [])
|
|
2135
|
+
if isinstance(result, dict) and result.get("reason")
|
|
2136
|
+
}
|
|
2137
|
+
if not reasons:
|
|
2138
|
+
return issues
|
|
2139
|
+
return [
|
|
2140
|
+
issue
|
|
2141
|
+
for issue in issues
|
|
2142
|
+
if not any(
|
|
2143
|
+
reason in issue.message and issue.message.endswith(reason)
|
|
2144
|
+
for reason in reasons
|
|
2145
|
+
)
|
|
2146
|
+
]
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def _text_recap(mode: str, data: dict | None) -> None:
|
|
2150
|
+
"""The stderr recap for whichever mode ran. Three shapes because `data`
|
|
2151
|
+
has three shapes -- a plan-mode `data.slices[]` entry is a PLAN slice with
|
|
2152
|
+
no `status` at all, so the build recap below would `KeyError` on it."""
|
|
2153
|
+
if data is None:
|
|
2154
|
+
return
|
|
2155
|
+
if mode == _MODE_MATERIALISE:
|
|
2156
|
+
print(
|
|
2157
|
+
f"materialised {len(data['written'])} file(s) under {data['baseDir']}:",
|
|
2158
|
+
file=sys.stderr,
|
|
2159
|
+
)
|
|
2160
|
+
for rel in data["written"]:
|
|
2161
|
+
print(f" {rel}", file=sys.stderr)
|
|
2162
|
+
return
|
|
2163
|
+
if mode == _MODE_PLAN:
|
|
2164
|
+
print(
|
|
2165
|
+
f"build plan (schema v{data.get('schemaVersion')}) -- {data.get('sku')}",
|
|
2166
|
+
file=sys.stderr,
|
|
2167
|
+
)
|
|
2168
|
+
print(f" board.yaml: {data.get('boardYaml')}", file=sys.stderr)
|
|
2169
|
+
print(f" build root: {data.get('buildRoot')}", file=sys.stderr)
|
|
2170
|
+
slices = data.get("slices") or []
|
|
2171
|
+
print(f" slices ({len(slices)}):", file=sys.stderr)
|
|
2172
|
+
for sl in slices:
|
|
2173
|
+
print(
|
|
2174
|
+
f" - {sl.get('coreId')} [{sl.get('backend')}] -> {sl.get('buildDir')}",
|
|
2175
|
+
file=sys.stderr,
|
|
2176
|
+
)
|
|
2177
|
+
print(
|
|
2178
|
+
f" shared artefacts: {len(data.get('sharedArtefacts') or [])}",
|
|
2179
|
+
file=sys.stderr,
|
|
2180
|
+
)
|
|
2181
|
+
# tan-cli#1000, oracle parity: the retired Rust `summarize_plan`
|
|
2182
|
+
# (`crates/tan-core/src/build_plan.rs:358`, read at `2883cdf^`)
|
|
2183
|
+
# printed `warnings: 0` when empty and otherwise expanded every
|
|
2184
|
+
# entry as ` - [{code}] {coreId}: {message}`. The port counted them,
|
|
2185
|
+
# so this surface lost the same cause the ordinary build path did.
|
|
2186
|
+
plan_warnings = data.get("warnings") or []
|
|
2187
|
+
if not plan_warnings:
|
|
2188
|
+
print(" warnings: 0", file=sys.stderr)
|
|
2189
|
+
else:
|
|
2190
|
+
print(f" warnings ({len(plan_warnings)}):", file=sys.stderr)
|
|
2191
|
+
for warning in plan_warnings:
|
|
2192
|
+
core_id = warning.get("coreId")
|
|
2193
|
+
where = f" {core_id}:" if core_id else ""
|
|
2194
|
+
print(
|
|
2195
|
+
f" - [{warning.get('code')}]{where} {warning.get('message')}",
|
|
2196
|
+
file=sys.stderr,
|
|
2197
|
+
)
|
|
2198
|
+
return
|
|
2199
|
+
slices = data.get("slices", [])
|
|
2200
|
+
for result in slices:
|
|
2201
|
+
reason = f" -- {result['reason']}" if "reason" in result else ""
|
|
2202
|
+
# tan-cli#510 review, MAJOR 1: shown for a failed/cancelled slice
|
|
2203
|
+
# only (`missingTool`'s own refusal already names what it searched
|
|
2204
|
+
# in `reason` above, unaffected by this) -- a success needs nothing
|
|
2205
|
+
# more explained, and `resolvedTool` is already `null` whenever it
|
|
2206
|
+
# would just echo back the plan's own `tool` identity (see
|
|
2207
|
+
# `SliceOutcome.resolved_tool`'s docstring). Deliberately NOT folded
|
|
2208
|
+
# into `reason` itself -- see `_slice_result`'s own docstring for why.
|
|
2209
|
+
resolved_tool = result.get("resolvedTool")
|
|
2210
|
+
resolved_note = (
|
|
2211
|
+
f" (resolved to `{resolved_tool}`)"
|
|
2212
|
+
if result["status"] == "failed" and resolved_tool
|
|
2213
|
+
else ""
|
|
2214
|
+
)
|
|
2215
|
+
print(
|
|
2216
|
+
f"{result['status']}: {result['coreId']} [{result['backend']}]{reason}{resolved_note}",
|
|
2217
|
+
file=sys.stderr,
|
|
2218
|
+
)
|
|
2219
|
+
# tan-cli#283: a one-line summary AFTER every per-slice line, so a
|
|
2220
|
+
# partial or all-skipped build cannot be scrolled past -- the per-slice
|
|
2221
|
+
# lines above already carry this same count, but only for a reader who
|
|
2222
|
+
# counts them by hand.
|
|
2223
|
+
if slices:
|
|
2224
|
+
built = sum(1 for result in slices if result["status"] == "ok")
|
|
2225
|
+
print(f"{built} of {len(slices)} slice(s) built", file=sys.stderr)
|
|
2226
|
+
|
|
2227
|
+
|
|
2228
|
+
# tan-cli#427: `--target`/`--all`/`--verbose`/`--quiet`/`--no-color`/
|
|
2229
|
+
# `--non-interactive`/`--ci` are accept-and-drop here, same as on every other
|
|
2230
|
+
# command `accept_global_flags` already covers -- the oracle's own `cli.rs`
|
|
2231
|
+
# declares all seven ONLY on the shared `GlobalArgs` struct (`BuildArgs` has
|
|
2232
|
+
# no fields for any of them), so `build`'s Rust handler never reads them
|
|
2233
|
+
# either. `--project`/`--board-yaml`/`--sdk-root` are already declared above
|
|
2234
|
+
# for real, so this adds nothing for those three (`_declared_flags` skips a
|
|
2235
|
+
# flag `build` already reads).
|
|
2236
|
+
build = accept_global_flags(build)
|