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,1852 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Dispatch each slice: assemble its env, apply the execution policy's
|
|
3
|
+
skip-vs-fail dispositions, resolve `west` from the workspace venv, spawn the
|
|
4
|
+
tool, stream its output, and report a per-slice outcome -- never an escaping
|
|
5
|
+
exception. Mirrors the dispatch loop in `tan-cli/src/commands/build/execute/
|
|
6
|
+
mod.rs`. `force_pristine` (`tan build --pristine`, tan-cli#427) is the manual
|
|
7
|
+
counterpart to the automatic stamp comparison below: it forces the SAME
|
|
8
|
+
[SdkStampAction.PRISTINE] decision the stamp comparison would make on its
|
|
9
|
+
own, inside the identical two structural safety guards, and reports
|
|
10
|
+
`build.pristine-skipped` (never silent) when one of those guards -- or a
|
|
11
|
+
build dir that was never configured at all -- declines the wipe.
|
|
12
|
+
What IS ported: the unknown-backend / null-command / unsafe-cwd / missing-tool
|
|
13
|
+
skip-vs-fail policy and dispatch order, the build-dir-must-exist-before-the-
|
|
14
|
+
tool-runs precondition, the `tool == "west"` rewrite to the workspace venv's
|
|
15
|
+
own `west` plus the matching PATH prepend (tan-cli#289/#106: `west` normally
|
|
16
|
+
lives ONLY inside the bootstrapped venv, and nothing activates it for a
|
|
17
|
+
GUI-launched editor -- without this every Zephyr slice skipped with `tool
|
|
18
|
+
'west' not found` on exactly the host the VS Code extension runs on; see
|
|
19
|
+
`tan.core.venv`), the sdk-switch-pristine guard (issue #52: wipe a slice's
|
|
20
|
+
build dir before dispatch when it was configured against a different SDK
|
|
21
|
+
root than this run resolved, then re-stamp it -- see
|
|
22
|
+
[_maybe_pristine_stale_sdk_build_dir]), (tan-cli#309, upstream tan-cli #97)
|
|
23
|
+
the Zephyr-boilerplate-loaded guard -- an `os: zephyr` slice that exits 0
|
|
24
|
+
without ever loading Zephyr's CMake boilerplate (`tan.commands.build.
|
|
25
|
+
manifest.zephyr_boilerplate_loaded`) is reported `failed`, not `ok`, since a
|
|
26
|
+
real exit code alone is not evidence the build produced firmware -- (see
|
|
27
|
+
[`last_manifest_write`]) the post-build `system-manifest.yaml` write, and
|
|
28
|
+
(tan-cli#550) the plan's per-slice `postCommands` plus the matching
|
|
29
|
+
baremetal-evidence guard: an `os: baremetal` slice's `command` is a `cmake -S
|
|
30
|
+
... -B .` that only CONFIGURES, so its `cmake --build .` arrives as a post
|
|
31
|
+
command ([`_run_post_commands`], with skip-vs-fail taken from the plan's own
|
|
32
|
+
`executionPolicy` per the SDK schema and ADR-0001 -- see
|
|
33
|
+
[`_missing_post_tool`]) and its linked output is checked against the plan's own
|
|
34
|
+
`artifacts.outputDir` ([`_baremetal_artefact_refusal`], whose three
|
|
35
|
+
NON-coverages are enumerated there) before the slice may be called built.
|
|
36
|
+
|
|
37
|
+
**tan-cli#307, a DELIBERATE divergence from the frozen Rust oracle.**
|
|
38
|
+
`crates/` is frozen (`docs/ROADMAP.md`'s standing rule), and the oracle's own
|
|
39
|
+
`execute/mod.rs` has the identical bug: `Command::new(&tool).current_dir(&cwd)`
|
|
40
|
+
lets `west` resolve its topdir from `cwd` alone, so an unrelated `.west`
|
|
41
|
+
ABOVE the project wins over the workspace `tan bootstrap` actually created,
|
|
42
|
+
purely because west never consults the workspace tan itself resolved. Fixed
|
|
43
|
+
Python-side only -- see [`_pin_west_workspace`].
|
|
44
|
+
|
|
45
|
+
**tan-cli#510, a second DELIBERATE divergence from the frozen Rust oracle.**
|
|
46
|
+
`execute/mod.rs:696`'s spawn is still `Command::new(&tool)` -- the bare
|
|
47
|
+
identity, handed straight to the platform's own resolver, whose Windows
|
|
48
|
+
search order includes a current-directory step ADR-0020 says an executor
|
|
49
|
+
must not consult. This port instead resolves `tool` to an absolute path with
|
|
50
|
+
a hardened lookup FIRST ([`_resolve_tool`]) and spawns that -- see this
|
|
51
|
+
module's own `_ToolResolution` docstring for why one resolver, not two. Not
|
|
52
|
+
back-ported to `crates/`, which ships to nobody (`docs/ROADMAP.md`); fixed
|
|
53
|
+
Python-side only.
|
|
54
|
+
|
|
55
|
+
**tan-cli#567, a third DELIBERATE divergence from the frozen Rust oracle** --
|
|
56
|
+
the one above, widened, and recorded here so the register stays complete.
|
|
57
|
+
Two things in this file still handed the platform a bare identity after #510.
|
|
58
|
+
(1) `_terminate`'s Windows `taskkill`: `CreateProcess` reaches
|
|
59
|
+
`%SystemRoot%\\System32` only AFTER the current directory, so a cancelled build
|
|
60
|
+
in a project carrying its own `taskkill.exe` ran that one -- now resolved
|
|
61
|
+
first, see [`_taskkill_program`]. (2) The lookup itself, on POSIX: an EMPTY
|
|
62
|
+
`PATH` entry (`PATH="$PATH:"`, `PATH=":$PATH"` -- both routine) means "the
|
|
63
|
+
current directory" to every POSIX consumer, and `shutil.which` duly joins `""`
|
|
64
|
+
with the name and probes it relative to cwd. Measured on `dev`,
|
|
65
|
+
`_resolve_tool("cwdprobe", {"PATH": ":/nope:"})` answered the RELATIVE string
|
|
66
|
+
`'cwdprobe'` for a file that existed only in the process's own working
|
|
67
|
+
directory, which `execute_slices` then spawned -- live on this path since #510,
|
|
68
|
+
because the Windows branch already filtered empty entries and the POSIX branch
|
|
69
|
+
did not. Empty entries are dropped before the walk; the same tree now answers
|
|
70
|
+
`None`. See `tan.core.tool_lookup.resolve_tool`, which is where that walk now
|
|
71
|
+
lives (this module keeps `_ToolResolution`/`_resolve_tool` as re-export
|
|
72
|
+
aliases). Not back-ported to `crates/`; fixed Python-side only.
|
|
73
|
+
|
|
74
|
+
**How the post-build write reaches `tan run` without a `build_cmd.py` change.**
|
|
75
|
+
The Rust oracle's executor (`execute/mod.rs::execute_slices_outcome`) returns
|
|
76
|
+
a `NativeBuildOutcome` bundling the dispatch result with the write's two
|
|
77
|
+
signals, and `commands::run` consumes that value directly. This port's
|
|
78
|
+
`execute_slices` is reached only through `tan.commands.build_cmd._dispatch` /
|
|
79
|
+
`_build`, both out of THIS unit's scope (a parallel, disjoint-files
|
|
80
|
+
work-split, not an architecture choice) -- so widening `execute_slices`'s
|
|
81
|
+
return type would need `_dispatch`'s own unpacking (`iter(execute_slices(...))`
|
|
82
|
+
/ `next(dispatched)`) to change too. Instead the write still happens as a
|
|
83
|
+
side effect of every `execute_slices` call (so `tan build`'s own CLI
|
|
84
|
+
invocation gets the file on disk exactly like `tan run`'s does), and the two
|
|
85
|
+
signals are exposed through [`last_manifest_write`] -- a same-process
|
|
86
|
+
recorder, not a return value. See that function's docstring for why reading
|
|
87
|
+
it is still safe against the R1 staleness defect the Rust oracle's own module
|
|
88
|
+
doc records (`decide_run_action` never consults it unless `build_ok`, and
|
|
89
|
+
`build_ok` cannot be `True` without `execute_slices` having just run to
|
|
90
|
+
completion in the SAME synchronous call)."""
|
|
91
|
+
import os
|
|
92
|
+
import queue
|
|
93
|
+
import shutil
|
|
94
|
+
import subprocess
|
|
95
|
+
import threading
|
|
96
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
97
|
+
from dataclasses import dataclass
|
|
98
|
+
from pathlib import Path
|
|
99
|
+
|
|
100
|
+
from tan.commands.build.user_defines_wipe import reconcile_user_defines, stamp_user_defines
|
|
101
|
+
from tan.core.user_defines import insert_after_dashdash
|
|
102
|
+
from tan.commands.build.configure_inputs import (
|
|
103
|
+
discover_configure_inputs,
|
|
104
|
+
read_configure_inputs_stamp,
|
|
105
|
+
resolve_zephyr_discovery_dir,
|
|
106
|
+
write_configure_inputs_stamp,
|
|
107
|
+
)
|
|
108
|
+
from tan.commands.build.manifest import (
|
|
109
|
+
build_dir_overridden,
|
|
110
|
+
cmake_cache_configured,
|
|
111
|
+
read_sdk_stamp,
|
|
112
|
+
resolve_zephyr_artefact,
|
|
113
|
+
write_post_build_manifest,
|
|
114
|
+
write_sdk_stamp,
|
|
115
|
+
zephyr_boilerplate_loaded,
|
|
116
|
+
)
|
|
117
|
+
from tan.commands.build.materialise import MaterialiseError, confine_to_build_root
|
|
118
|
+
from tan.commands.build.toolchain import host_scan_has_toolchain, verified_store_dir
|
|
119
|
+
from tan.core.plan_exec import (
|
|
120
|
+
CROSS_DRIVE_MSG,
|
|
121
|
+
ExecutionPolicy,
|
|
122
|
+
PolicyAction,
|
|
123
|
+
SdkStampAction,
|
|
124
|
+
assemble_slice_env,
|
|
125
|
+
cross_drive_source_refusal,
|
|
126
|
+
missing_tool_message,
|
|
127
|
+
pristine_suppression,
|
|
128
|
+
resolve_action,
|
|
129
|
+
sdk_stamp_action,
|
|
130
|
+
sdk_stamp_key,
|
|
131
|
+
)
|
|
132
|
+
from tan.core.plain_zephyr_plan import plain_route_target
|
|
133
|
+
from tan.core.subprocess_env import spawn_env
|
|
134
|
+
from tan.core.system_manifest import SliceRunResult
|
|
135
|
+
from tan.core.tool_lookup import ToolResolution, resolve_tool
|
|
136
|
+
from tan.core.venv import west_program, west_workspace_dir, with_venv_on_path
|
|
137
|
+
from tan.core.west_workspace_refusal import workspace_unresolved_refusal
|
|
138
|
+
from tan.core.zephyr_env import zephyr_env_overrides
|
|
139
|
+
from tan.envelope import Issue
|
|
140
|
+
from tan.commands.build.link_stale import insert_after_separator, stale_itcm_overlay_reset
|
|
141
|
+
|
|
142
|
+
if os.name != "nt":
|
|
143
|
+
import signal
|
|
144
|
+
|
|
145
|
+
# metadata/schemas/build-plan-v1.schema.json: slices[].backend enum. Rust's
|
|
146
|
+
# `Backend` (tan-core/src/build_plan.rs) matches this exactly, plus a catch-all
|
|
147
|
+
# `Unknown(String)` for anything else -- "native" is NOT a legal backend; a
|
|
148
|
+
# slice naming it must be refused by `executionPolicy.unknownBackend`
|
|
149
|
+
# (default fail), never dispatched.
|
|
150
|
+
KNOWN_BACKENDS = frozenset({"zephyr", "yocto", "baremetal"})
|
|
151
|
+
|
|
152
|
+
#: `tan_core::build_plan::CONSUMER_BUILD_ROOT` -- the top-level dir name a
|
|
153
|
+
#: slice's plan-supplied (relative, pre-`confine_to_build_root`) `cwd` must
|
|
154
|
+
#: start with before the sdk-switch-pristine guard will touch it. Duplicated
|
|
155
|
+
#: rather than imported to avoid a `tan.core.build_plan` <-> here cycle risk;
|
|
156
|
+
#: it is the SDK's own build-plan schema constant, not derived from anything
|
|
157
|
+
#: in this module.
|
|
158
|
+
_CONSUMER_BUILD_ROOT = "build"
|
|
159
|
+
|
|
160
|
+
# How often the output-draining loop checks `cancelled()` -- bounds
|
|
161
|
+
# cancellation latency without spinning the CPU.
|
|
162
|
+
_POLL_INTERVAL_S = 0.05
|
|
163
|
+
# How long to give a terminated process to exit on its own before escalating
|
|
164
|
+
# to a forceful kill.
|
|
165
|
+
_TERMINATE_GRACE_S = 1.0
|
|
166
|
+
|
|
167
|
+
#: Envelope-wire status vocabulary, from `SliceOutcome.status` -- verbatim
|
|
168
|
+
#: `tan.commands.build_cmd._WIRE_STATUS` (duplicated rather than imported:
|
|
169
|
+
#: `build_cmd.py` already imports this module, so the reverse import would be
|
|
170
|
+
#: circular). Also the manifest schema's own `slices[].status` vocabulary
|
|
171
|
+
#: (`ok`/`failed`/`skipped`), which is why [`_write_manifest_after_dispatch`]
|
|
172
|
+
#: reuses it for the post-build overlay too.
|
|
173
|
+
_WIRE_STATUS = {"succeeded": "ok", "skipped": "skipped"}
|
|
174
|
+
|
|
175
|
+
#: tan-cli#336: west's own literal message (`west/util.py::west_topdir`) when
|
|
176
|
+
#: neither an ancestor `.west` nor `$ZEPHYR_BASE` resolves a workspace --
|
|
177
|
+
#: watched for on a west slice's stdout so a failure carrying it can be
|
|
178
|
+
#: re-worded with what tan itself resolved (see [`execute_slices`]'s dispatch
|
|
179
|
+
#: loop). Substring match, not the whole line: west prefixes it with
|
|
180
|
+
#: "FATAL ERROR: ".
|
|
181
|
+
_WEST_NO_WORKSPACE_MSG = "Could not find a west workspace"
|
|
182
|
+
|
|
183
|
+
#: The Zephyr SDK cross toolchain is absent -- watched for on the same stdout,
|
|
184
|
+
#: for the same reason (tan-cli#419). Zephyr's own CMake says
|
|
185
|
+
#:
|
|
186
|
+
#: CMake Error at <zephyr>/cmake/modules/FindZephyr-sdk.cmake:160
|
|
187
|
+
#: (find_package): Could not find a package configuration file provided
|
|
188
|
+
#: by "Zephyr-sdk" (requested version 1.0)
|
|
189
|
+
#:
|
|
190
|
+
#: and the slice then exits 1, so without this the whole diagnosis is a bare
|
|
191
|
+
#: `terminated with exit code: 1` plus 4 KB of pass-through CMake text the
|
|
192
|
+
#: envelope never names. tan ALREADY HOLDS the answer -- `doctor`'s `zephyrSdk`
|
|
193
|
+
#: check reports it with the exact remedy -- so this is not a missing
|
|
194
|
+
#: capability, it is one the failing command does not surface. Measured on the
|
|
195
|
+
#: same host at the same moment: `doctor --build` said `zephyrSdk -> fail` with
|
|
196
|
+
#: the install command, while `build`'s envelope contained neither
|
|
197
|
+
#: "zephyr sdk" nor "sdk install".
|
|
198
|
+
#:
|
|
199
|
+
#: Matched on the package name rather than the `FindZephyr-sdk.cmake` path,
|
|
200
|
+
#: which carries the workspace's absolute location and a line number that
|
|
201
|
+
#: moves with upstream Zephyr.
|
|
202
|
+
_ZEPHYR_SDK_MISSING_MSG = 'provided by "Zephyr-sdk"'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class SliceOutcome:
|
|
207
|
+
core_id: str
|
|
208
|
+
status: str # "succeeded" | "skipped" | "failed" | "cancelled"
|
|
209
|
+
exit_code: int | None
|
|
210
|
+
message: str | None
|
|
211
|
+
#: Real on-disk `zephyr.elf` path after a successful build (absolute),
|
|
212
|
+
#: fed into the post-build manifest so `size`/`flash`/`run` find
|
|
213
|
+
#: the artefact west actually produced. `None` for every other status.
|
|
214
|
+
output_artefact: str | None = None
|
|
215
|
+
#: Real on-disk build directory (west's nested `<cwd>/build`, absolute).
|
|
216
|
+
#: `None` for every other status.
|
|
217
|
+
build_dir: str | None = None
|
|
218
|
+
#: The absolute path `_resolve_tool` actually resolved `command.tool` to
|
|
219
|
+
#: -- MAJOR 1 of the tan-cli#510 review. `None` when no tool was ever
|
|
220
|
+
#: resolved (unknown-backend/null-command/missing-tool/cwd/mkdir
|
|
221
|
+
#: refusals, all decided before resolution runs) OR when resolution
|
|
222
|
+
#: succeeded but landed on the SAME string the plan already named
|
|
223
|
+
#: (`tool == resolved`) -- information-free by construction, and the
|
|
224
|
+
#: dominant Zephyr shape once `west_program` has already rewritten
|
|
225
|
+
#: `tool` to the workspace venv's own absolute `west`. Carried as its own
|
|
226
|
+
#: field -- `data.slices[].resolvedTool` -- rather than folded into
|
|
227
|
+
#: `message`: `message` is the source of BOTH `data.slices[].reason`
|
|
228
|
+
#: (`build_cmd.py`) and the persisted `system-manifest.yaml`
|
|
229
|
+
#: `slices[].reason` (alp-sdk `metadata/schemas/system-manifest-v1.
|
|
230
|
+
#: schema.json`, "why a slice was skipped/failed") -- a resolved path is
|
|
231
|
+
#: neither, and stuffing it into `reason` silently added a key to every
|
|
232
|
+
#: green build's envelope and on-disk manifest. See `_text_recap` in
|
|
233
|
+
#: `build_cmd.py` for where this is surfaced in default text (failed and
|
|
234
|
+
#: cancelled slices only -- a success confirms the right tool already,
|
|
235
|
+
#: with nothing left to explain).
|
|
236
|
+
#:
|
|
237
|
+
#: tan-cli#510 review round 3, NIT: `None` here is genuinely AMBIGUOUS for
|
|
238
|
+
#: an already-absolute `command.tool` that then FAILS to launch -- both
|
|
239
|
+
#: "never resolved" (an earlier refusal) and "resolved, identical to the
|
|
240
|
+
#: plan's own `tool`" collapse to the same `null`, and `data.slices[]`
|
|
241
|
+
#: carries no separate `tool` key to disambiguate from the envelope
|
|
242
|
+
#: alone. Deliberate, not an oversight: the motivating case this field
|
|
243
|
+
#: exists for is a MISMATCH (a bare identity landing on a different
|
|
244
|
+
#: binary than the plan named) -- an absolute `tool` that fails is
|
|
245
|
+
#: already fully explained by `message`'s `failed to launch \`<tool>\`:
|
|
246
|
+
#: ...`, which names the identity verbatim, so the suppression rule
|
|
247
|
+
#: (`tool == resolved_tool`) is doing its job rather than hiding
|
|
248
|
+
#: something. A future consumer that needs "which identity did the plan
|
|
249
|
+
#: name" independent of "did it resolve to something different" should
|
|
250
|
+
#: get it as its own additive field (mirroring `resolvedTool`'s own
|
|
251
|
+
#: addition), not by overloading this one's `None` further.
|
|
252
|
+
resolved_tool: str | None = None
|
|
253
|
+
#: The text persisted as `system-manifest.yaml` `slices[].reason`, when it
|
|
254
|
+
#: must differ from `message` -- `None` (the common case) means "use
|
|
255
|
+
#: `message` verbatim". Exists ONLY for the missing-tool refusal
|
|
256
|
+
#: (tan-cli#510 review round 3, MAJOR): `message` there carries `-- searched
|
|
257
|
+
#: PATH: <every entry>` so the CUSTOMER'S OWN TERMINAL shows a fix they can
|
|
258
|
+
#: apply themselves (tan-cli#510's own acceptance bar) -- but that same
|
|
259
|
+
#: string, unredacted, is also what a customer pastes into a support
|
|
260
|
+
#: ticket, and `system-manifest.yaml` is a build ARTEFACT that outlives the
|
|
261
|
+
#: run and gets forwarded. Persisting the full `PATH` there leaks machine
|
|
262
|
+
#: layout (private directory names, sometimes credentials-in-paths) to
|
|
263
|
+
#: whoever the ticket reaches. The short form still answers "why" per the
|
|
264
|
+
#: alp-sdk schema's own field description; the searched detail stays
|
|
265
|
+
#: transient (this run's stdout/envelope only) -- see
|
|
266
|
+
#: [`_write_manifest_after_dispatch`]'s use of this field.
|
|
267
|
+
manifest_message: str | None = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _skip_or_fail(
|
|
271
|
+
core_id: str, action: PolicyAction, message: str, *, manifest_message: str | None = None
|
|
272
|
+
) -> SliceOutcome:
|
|
273
|
+
status = "skipped" if action is PolicyAction.SKIP else "failed"
|
|
274
|
+
return SliceOutcome(core_id, status, None, message, manifest_message=manifest_message)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class _ManifestWriteSignal:
|
|
279
|
+
manifest_written: bool = False
|
|
280
|
+
native_sim_target: bool | None = None
|
|
281
|
+
#: Why the write failed, when it did (`None` on success). A THIRD signal,
|
|
282
|
+
#: kept OUT of [`last_manifest_write`]'s tuple deliberately: that
|
|
283
|
+
#: function's 2-tuple shape is `tan.core.run.decide_run_action`'s own
|
|
284
|
+
#: contract (via `tan.commands.run_cmd`, out of this unit's scope), and
|
|
285
|
+
#: widening it would need that caller's own unpacking to change too.
|
|
286
|
+
#: [`last_manifest_write_failure`] exposes this one separately for
|
|
287
|
+
#: `tan.commands.build_cmd._build`, which folds it into the JSON
|
|
288
|
+
#: envelope's `issues` as the `build.manifest-write-failed` warning --
|
|
289
|
+
#: see that module's own call site.
|
|
290
|
+
write_failed_reason: str | None = None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
_last_manifest_write = _ManifestWriteSignal()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def reset_last_manifest_write() -> None:
|
|
297
|
+
"""Clear the recorded signal from the most recent [`execute_slices`] call
|
|
298
|
+
in this process. `tan run`'s `_run` calls this immediately before
|
|
299
|
+
invoking the build engine, so a build that never reaches dispatch (an
|
|
300
|
+
early coded refusal, or a monkeypatched stub in a test) reads the honest
|
|
301
|
+
default `(False, None)` below instead of a previous invocation's
|
|
302
|
+
leftover -- see [`last_manifest_write`] for why that default is always
|
|
303
|
+
safe regardless."""
|
|
304
|
+
global _last_manifest_write
|
|
305
|
+
_last_manifest_write = _ManifestWriteSignal()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def last_manifest_write() -> tuple[bool, bool | None]:
|
|
309
|
+
"""`(manifest_written, native_sim_target)` from the most recent
|
|
310
|
+
[`execute_slices`] call in this process -- the in-memory signal
|
|
311
|
+
`tan.core.run.decide_run_action` needs, exposed as a same-process
|
|
312
|
+
recorder rather than a return value (see this module's docstring for
|
|
313
|
+
why). Safe against staleness: `decide_run_action` only ever consults
|
|
314
|
+
these two values when `build_ok` is `True`
|
|
315
|
+
(`tan.commands.build_cmd._build`'s own exit code), and `build_ok` can
|
|
316
|
+
only be `True` after `_build` has run [`execute_slices`] to completion in
|
|
317
|
+
THIS SAME synchronous call -- there is no path to a successful build that
|
|
318
|
+
skips dispatch. Pair with [`reset_last_manifest_write`] before a build
|
|
319
|
+
that might not reach dispatch at all."""
|
|
320
|
+
return _last_manifest_write.manifest_written, _last_manifest_write.native_sim_target
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def last_manifest_write_failure() -> str | None:
|
|
324
|
+
"""Why the most recent [`execute_slices`] call's post-build manifest
|
|
325
|
+
write failed, or `None` on success -- see
|
|
326
|
+
[`_ManifestWriteSignal.write_failed_reason`] for why this is a separate
|
|
327
|
+
accessor rather than a third element of [`last_manifest_write`]'s
|
|
328
|
+
tuple."""
|
|
329
|
+
return _last_manifest_write.write_failed_reason
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
#: Coded envelope issues from the sdk-switch-pristine guard, one call's
|
|
333
|
+
#: worth -- verbatim `crates/tan-cli/.../execute/mod.rs`'s `sdk_switch_issues`,
|
|
334
|
+
#: which the Rust oracle folds into the JSON envelope (`build.sdk-switch-
|
|
335
|
+
#: pristine` / `build.sdk-switch-pristine-failed`) instead of leaving the
|
|
336
|
+
#: wipe stderr-only: the VS Code extension only ever sees the envelope, not
|
|
337
|
+
#: `on_output`'s stream. Same same-process-recorder pattern as
|
|
338
|
+
#: `_last_manifest_write`, for the same reason (see [`last_sdk_switch_issues`]
|
|
339
|
+
#: and this module's own docstring).
|
|
340
|
+
_last_sdk_switch_issues: list[Issue] = []
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def last_sdk_switch_issues() -> list[Issue]:
|
|
344
|
+
"""The `build.sdk-switch-pristine`/`build.sdk-switch-pristine-failed`
|
|
345
|
+
issues recorded by the most recent [`execute_slices`] call in this
|
|
346
|
+
process -- always freshly OVERWRITTEN (never appended-to) on every call,
|
|
347
|
+
so a build with nothing to wipe reads back the honest empty list rather
|
|
348
|
+
than a previous invocation's leftover."""
|
|
349
|
+
return list(_last_sdk_switch_issues)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
#: Coded envelope issues from the stale-configure-cache guard (tan-cli#655),
|
|
353
|
+
#: one call's worth -- same same-process-recorder pattern as
|
|
354
|
+
#: `_last_sdk_switch_issues`, for the same reason: the VS Code extension only
|
|
355
|
+
#: ever sees the envelope, not `on_output`'s stream, and a `-U` reset that
|
|
356
|
+
#: only ever showed up in stderr would be invisible there.
|
|
357
|
+
_last_configure_cache_issues: list[Issue] = []
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def last_configure_cache_issues() -> list[Issue]:
|
|
361
|
+
"""The `build.configure-cache-reset` issues recorded by the most recent
|
|
362
|
+
[`execute_slices`] call in this process -- always freshly OVERWRITTEN
|
|
363
|
+
(never appended-to), so a build with nothing to reset reads back the
|
|
364
|
+
honest empty list rather than a previous invocation's leftover."""
|
|
365
|
+
return list(_last_configure_cache_issues)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _drain_output(
|
|
369
|
+
proc: subprocess.Popen, on_output: Callable[[str], None], cancelled: Callable[[], bool]
|
|
370
|
+
) -> bool:
|
|
371
|
+
"""Stream `proc`'s stdout to `on_output` line by line, polling
|
|
372
|
+
`cancelled()` every iteration -- BEFORE waiting on the next line, not
|
|
373
|
+
only when none is pending. Returns True iff cancellation was observed.
|
|
374
|
+
|
|
375
|
+
Two separate problems, both live here:
|
|
376
|
+
|
|
377
|
+
1. A plain `for line in proc.stdout:` blocks on the next `readline()`
|
|
378
|
+
with no way to interleave a check -- a slice that produces no output
|
|
379
|
+
at all (e.g. `time.sleep(60)`) would never yield control back. Fixed
|
|
380
|
+
by reading on a background thread and polling a queue with a timeout
|
|
381
|
+
(Windows anonymous pipes don't support `select`).
|
|
382
|
+
2. Polling `cancelled()` only in the `except queue.Empty:` arm (the
|
|
383
|
+
first cut of this fix) is not enough: a CHATTY child -- `west build`
|
|
384
|
+
driving cmake/ninja is chatty by construction -- keeps the queue
|
|
385
|
+
non-empty forever, so that arm never runs and cancellation is never
|
|
386
|
+
observed. `cancelled()` must be checked unconditionally at the top of
|
|
387
|
+
every iteration, before the queue is touched at all.
|
|
388
|
+
"""
|
|
389
|
+
lines: queue.Queue[str | None] = queue.Queue()
|
|
390
|
+
|
|
391
|
+
def reader() -> None:
|
|
392
|
+
try:
|
|
393
|
+
assert proc.stdout is not None
|
|
394
|
+
for line in proc.stdout:
|
|
395
|
+
lines.put(line)
|
|
396
|
+
finally:
|
|
397
|
+
lines.put(None) # sentinel: stream closed
|
|
398
|
+
|
|
399
|
+
threading.Thread(target=reader, daemon=True).start()
|
|
400
|
+
|
|
401
|
+
while True:
|
|
402
|
+
if cancelled():
|
|
403
|
+
return True
|
|
404
|
+
try:
|
|
405
|
+
line = lines.get(timeout=_POLL_INTERVAL_S)
|
|
406
|
+
except queue.Empty:
|
|
407
|
+
continue
|
|
408
|
+
if line is None:
|
|
409
|
+
return False
|
|
410
|
+
on_output(line.rstrip("\n"))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _terminate(proc: subprocess.Popen) -> None:
|
|
414
|
+
"""Kill the whole process TREE, not just the direct child.
|
|
415
|
+
|
|
416
|
+
`west build` spawns cmake, which spawns ninja/make -- each inherits the
|
|
417
|
+
parent's stdout handle (Python's own `close_fds` default only closes fds
|
|
418
|
+
above 2). Killing just `proc` leaves a grandchild holding the pipe's
|
|
419
|
+
write end open: the reader thread's `for line in proc.stdout` never sees
|
|
420
|
+
EOF and blocks forever, and `Popen.__exit__` (which closes stdout) then
|
|
421
|
+
deadlocks on that same block. `start_new_session=True` at spawn (see
|
|
422
|
+
`execute_slices`) puts the child in its own POSIX process group so
|
|
423
|
+
`killpg` reaches every descendant; Windows has no process groups, so
|
|
424
|
+
`taskkill /T` walks the OS-tracked parent-PID tree instead.
|
|
425
|
+
|
|
426
|
+
**tan-cli#567.** That `taskkill` used to be spawned as a BARE `argv[0]`,
|
|
427
|
+
which is the one place in this file #510 did not reach. `CreateProcess`
|
|
428
|
+
with `lpApplicationName=NULL` searches the parent process's CURRENT
|
|
429
|
+
DIRECTORY *before* the system directories, and the current directory here
|
|
430
|
+
is the customer's project -- so a project carrying its own `taskkill.exe`
|
|
431
|
+
got that binary run, with whatever privileges tan has, the moment a build
|
|
432
|
+
was cancelled. `taskkill` really does live in `%SystemRoot%\\System32`,
|
|
433
|
+
which is on every sane `%PATH%`, so the hardened lookup finds the real one;
|
|
434
|
+
the `%SystemRoot%` fallback covers a stripped `%PATH%`, and a host where
|
|
435
|
+
NEITHER answers falls back to killing the direct child rather than spawning
|
|
436
|
+
a name the current directory could supply. That last case loses the
|
|
437
|
+
grandchildren, which is a worse cleanup but not a worse *hazard* -- and it
|
|
438
|
+
is unreachable on any Windows install that still has System32."""
|
|
439
|
+
if os.name == "nt":
|
|
440
|
+
taskkill = _taskkill_program()
|
|
441
|
+
if taskkill is None:
|
|
442
|
+
proc.kill()
|
|
443
|
+
else:
|
|
444
|
+
subprocess.run(
|
|
445
|
+
[taskkill, "/T", "/F", "/PID", str(proc.pid)],
|
|
446
|
+
capture_output=True,
|
|
447
|
+
env=spawn_env(),
|
|
448
|
+
check=False,
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
try:
|
|
452
|
+
pgid = os.getpgid(proc.pid)
|
|
453
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
454
|
+
try:
|
|
455
|
+
proc.wait(timeout=_TERMINATE_GRACE_S)
|
|
456
|
+
except subprocess.TimeoutExpired:
|
|
457
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
458
|
+
except ProcessLookupError:
|
|
459
|
+
pass # already exited between the cancellation check and here
|
|
460
|
+
proc.wait()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _taskkill_program() -> str | None:
|
|
464
|
+
"""The absolute path to Windows' own `taskkill`, or `None`.
|
|
465
|
+
|
|
466
|
+
`%PATH%` first, through the same hardened walk every other spawn in this
|
|
467
|
+
port now uses (`os.curdir` is never consulted); then `%SystemRoot%\\System32`
|
|
468
|
+
directly, because a `%PATH%` stripped of the system directories is a real
|
|
469
|
+
(if unusual) CI shape and losing the process-tree kill there would be a
|
|
470
|
+
regression this fix does not need to cause. `None` only when both miss --
|
|
471
|
+
see [`_terminate`] for why that answers with `proc.kill()` rather than a
|
|
472
|
+
bare name."""
|
|
473
|
+
resolved = resolve_tool("taskkill", os.environ).resolved
|
|
474
|
+
if resolved is not None:
|
|
475
|
+
return resolved
|
|
476
|
+
system32 = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "taskkill.exe"
|
|
477
|
+
return str(system32) if system32.is_file() else None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
#: tan-cli#567/#532: the ONE hardened lookup, now in `tan.core.tool_lookup`
|
|
481
|
+
#: so the flash write path and the size path spawn the SAME resolved path
|
|
482
|
+
#: instead of keeping a third and fourth opinion about it. Imported under the
|
|
483
|
+
#: private names this module's call sites (and `tests/commands/test_execute.py`)
|
|
484
|
+
#: already use -- the move is a relocation, not a rename. See that module's
|
|
485
|
+
#: docstring for `_ToolResolution`'s reasoning, which travelled with it.
|
|
486
|
+
_ToolResolution = ToolResolution
|
|
487
|
+
_resolve_tool = resolve_tool
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@dataclass(frozen=True)
|
|
491
|
+
class _StepResult:
|
|
492
|
+
"""One spawned process's outcome, with its three shapes kept apart.
|
|
493
|
+
|
|
494
|
+
Exactly one is ever populated: `exit_code` (the process ran to completion and
|
|
495
|
+
this is its real exit status, negative on a POSIX signal death),
|
|
496
|
+
`cancelled` (the caller's `cancelled()` went true mid-stream and the
|
|
497
|
+
process tree was terminated), or `launch_error` (the spawn itself raised
|
|
498
|
+
`OSError` -- the tool vanished between the availability check and here,
|
|
499
|
+
is a directory, lacks the executable bit, or is not a valid executable
|
|
500
|
+
format). A bare `int | None` return could not tell the second from the
|
|
501
|
+
third."""
|
|
502
|
+
|
|
503
|
+
exit_code: int | None = None
|
|
504
|
+
cancelled: bool = False
|
|
505
|
+
launch_error: str | None = None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _spawn_step(
|
|
509
|
+
program: str, args: Sequence[str], cwd: Path, env: dict[str, str],
|
|
510
|
+
on_line: Callable[[str], None], cancelled: Callable[[], bool],
|
|
511
|
+
) -> _StepResult:
|
|
512
|
+
"""Spawn ONE already-resolved program, stream its output, and report how
|
|
513
|
+
it ended -- the single spawn implementation shared by a slice's own
|
|
514
|
+
`command` and by each of its `postCommands` steps (tan-cli#550).
|
|
515
|
+
|
|
516
|
+
Extracted rather than duplicated for the post-build steps: the fd hygiene,
|
|
517
|
+
the process-group setup and the cancellation handshake below are all
|
|
518
|
+
load-bearing and subtly easy to get wrong a second time -- a post-build
|
|
519
|
+
`cmake --build` that ignored `cancelled()` would keep compiling after the
|
|
520
|
+
user pressed Ctrl-C, with tan already reporting the slice cancelled.
|
|
521
|
+
|
|
522
|
+
`program` is always the RESOLVED absolute path (tan-cli#510), never a
|
|
523
|
+
bare identity -- resolution happens in the caller, which is also the only
|
|
524
|
+
place that can report a miss against the right execution policy.
|
|
525
|
+
|
|
526
|
+
A context manager, not a bare `Popen(...)`: it closes stdout/stderr/stdin
|
|
527
|
+
on every exit path (success, cancellation, exception) -- a bare `Popen`
|
|
528
|
+
here leaked the pipe's file object (a `ResourceWarning` under
|
|
529
|
+
`-W error::ResourceWarning`, and a real fd leak across a long run).
|
|
530
|
+
|
|
531
|
+
`start_new_session=True` (POSIX: setsid) puts the child in its own process
|
|
532
|
+
group so `_terminate` can `killpg` the whole tree instead of only the
|
|
533
|
+
direct child; a documented no-op on Windows (subprocess.py's Windows
|
|
534
|
+
`_execute_child` takes and ignores it), where `_terminate` uses
|
|
535
|
+
`taskkill /T` instead."""
|
|
536
|
+
try:
|
|
537
|
+
with subprocess.Popen(
|
|
538
|
+
[program, *args],
|
|
539
|
+
cwd=str(cwd),
|
|
540
|
+
# tan-cli#992: re-applied at the literal spawn, not trusted from
|
|
541
|
+
# the caller alone -- `spawn_env(base=env)` is idempotent when
|
|
542
|
+
# `env` already went through it (as `execute_slices`'s does), so
|
|
543
|
+
# this is defense-in-depth, not a behaviour change, for the one
|
|
544
|
+
# caller that already gets it right, and a real fix for any
|
|
545
|
+
# future caller that doesn't.
|
|
546
|
+
env=spawn_env(base=env),
|
|
547
|
+
stdout=subprocess.PIPE,
|
|
548
|
+
stderr=subprocess.STDOUT,
|
|
549
|
+
text=True,
|
|
550
|
+
encoding="utf-8",
|
|
551
|
+
errors="replace",
|
|
552
|
+
bufsize=1,
|
|
553
|
+
start_new_session=True,
|
|
554
|
+
) as proc:
|
|
555
|
+
if _drain_output(proc, on_line, cancelled):
|
|
556
|
+
_terminate(proc)
|
|
557
|
+
return _StepResult(cancelled=True)
|
|
558
|
+
return _StepResult(exit_code=proc.wait())
|
|
559
|
+
except OSError as err:
|
|
560
|
+
return _StepResult(launch_error=str(err))
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _cwd_under_build_root(raw_cwd: str | None, cwd: Path, project_root: Path) -> bool:
|
|
564
|
+
"""Whether the sdk-switch-pristine wipe may touch this slice's `cwd`.
|
|
565
|
+
|
|
566
|
+
Starts from the Rust oracle's `Path::new(&cmd.cwd).components().next() ==
|
|
567
|
+
CONSUMER_BUILD_ROOT`: a plan cwd of `src/` (still a legal relative path)
|
|
568
|
+
must not let the wipe target land at `<project>/src/build`, which may
|
|
569
|
+
hold files the build never created. That first-component check alone let
|
|
570
|
+
`build/../src/c1` through, and so did a `build/c1` symlink into `src/`
|
|
571
|
+
(tan-cli#1388): `confine_to_build_root` only keeps a cwd inside the
|
|
572
|
+
PROJECT, not inside `build/`. So also refuse any `..` in the plan string,
|
|
573
|
+
and require the RESOLVED cwd to sit at or under `<project>/build` --
|
|
574
|
+
with `<project>/build` itself resolving to exactly that path, so a
|
|
575
|
+
symlinked build root cannot carry the wipe elsewhere."""
|
|
576
|
+
if not raw_cwd:
|
|
577
|
+
return False
|
|
578
|
+
parts = Path(raw_cwd).parts
|
|
579
|
+
if not parts or parts[0] != _CONSUMER_BUILD_ROOT or ".." in parts:
|
|
580
|
+
return False
|
|
581
|
+
project = project_root.resolve()
|
|
582
|
+
build_root = (project / _CONSUMER_BUILD_ROOT).resolve()
|
|
583
|
+
if build_root != project / _CONSUMER_BUILD_ROOT:
|
|
584
|
+
return False
|
|
585
|
+
resolved = cwd.resolve()
|
|
586
|
+
return resolved == build_root or build_root in resolved.parents
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _maybe_pristine_stale_sdk_build_dir(
|
|
590
|
+
core_id: str,
|
|
591
|
+
cwd: Path,
|
|
592
|
+
raw_cwd: str | None,
|
|
593
|
+
cmd_args: Sequence[str],
|
|
594
|
+
sdk_stamp_key_str: str | None,
|
|
595
|
+
on_output: Callable[[str], None],
|
|
596
|
+
*,
|
|
597
|
+
project_root: Path,
|
|
598
|
+
force_pristine: bool = False,
|
|
599
|
+
) -> list[Issue]:
|
|
600
|
+
"""Sdk-switch-pristine guard (issue #52): a build dir west configured
|
|
601
|
+
against a PREVIOUS `--sdk-root` makes it FATAL ERROR on this one ("please
|
|
602
|
+
clean it, use --pristine, or use --build-dir") -- a real failure that
|
|
603
|
+
otherwise reaches the user as a bare "terminated with exit code: 1".
|
|
604
|
+
Detect it here and wipe west's own nested build dir before the tool runs,
|
|
605
|
+
so the configure that follows is fresh instead of fatal, then re-stamp
|
|
606
|
+
the dir with this run's SDK identity BEFORE the tool is spawned (a
|
|
607
|
+
mid-configure failure still stamped correctly, since the dir really was
|
|
608
|
+
configured against it regardless of whether the build finishes).
|
|
609
|
+
|
|
610
|
+
`force_pristine` is `tan build --pristine` (tan-cli#163, wired tan-cli#427):
|
|
611
|
+
unconditionally treats the sdk-switch-pristine decision as
|
|
612
|
+
[`SdkStampAction.PRISTINE`] instead of consulting the stamp, for the
|
|
613
|
+
manual case the automatic stamp comparison doesn't (or can't yet) cover.
|
|
614
|
+
It is NOT a second wipe path -- it forces the SAME decision the automatic
|
|
615
|
+
check already makes, still inside the same two structural safety guards
|
|
616
|
+
below, so it still never touches a build dir tan cannot vouch for. Every
|
|
617
|
+
suppression (either guard, or a build dir that was never configured at
|
|
618
|
+
all) is reported as `build.pristine-skipped` rather than silently doing
|
|
619
|
+
nothing (tan-cli#183) -- "pristine" must never silently mean
|
|
620
|
+
"incremental".
|
|
621
|
+
|
|
622
|
+
Two guards (mirroring `resolve_zephyr_artefact`'s own refusal to trust a
|
|
623
|
+
build dir it cannot resolve) gate the whole function -- detection, wipe,
|
|
624
|
+
AND the stamp write -- so a `-d`/`--build-dir` override or a cwd outside
|
|
625
|
+
`CONSUMER_BUILD_ROOT` never gets touched. Best-effort throughout: a wipe
|
|
626
|
+
or write failure is reported via `on_output` but never raises -- it
|
|
627
|
+
fails toward a spurious future rebuild, never toward trusting a stale
|
|
628
|
+
build dir or crashing the build. Port of `execute/mod.rs`'s inline guard
|
|
629
|
+
(~line 505) plus `manifest.rs::write_sdk_stamp`'s call site.
|
|
630
|
+
|
|
631
|
+
Returns the coded `build.sdk-switch-pristine`/`build.sdk-switch-pristine-
|
|
632
|
+
failed`/`build.pristine-skipped` [`Issue`]s for this slice (verbatim
|
|
633
|
+
`execute/mod.rs`'s `sdk_switch_issues.push`, at `warning` severity like
|
|
634
|
+
the oracle's) -- empty when nothing was wiped and no `--pristine` was
|
|
635
|
+
asked for. `on_output` still gets the same "note: ..." text regardless
|
|
636
|
+
(this port's stderr stream is always-on, unlike the oracle's
|
|
637
|
+
text-mode-only `eprintln!`); the caller folds the returned issues into
|
|
638
|
+
the JSON envelope so the wipe -- or its suppression -- is not
|
|
639
|
+
stderr-only there."""
|
|
640
|
+
overridden = build_dir_overridden(cmd_args)
|
|
641
|
+
under_build_root = _cwd_under_build_root(raw_cwd, cwd, project_root)
|
|
642
|
+
issues: list[Issue] = []
|
|
643
|
+
|
|
644
|
+
# Probed only when `--pristine` was actually passed, so the non-pristine
|
|
645
|
+
# path keeps its existing IO exactly (one `cmake_cache_configured` call
|
|
646
|
+
# below, not two) -- mirrors the oracle's own `pristine_cache_configured`
|
|
647
|
+
# split.
|
|
648
|
+
cache_configured = cmake_cache_configured(cwd) if force_pristine else False
|
|
649
|
+
skipped = pristine_suppression(force_pristine, overridden, under_build_root, cache_configured)
|
|
650
|
+
if skipped is not None:
|
|
651
|
+
message = f"{core_id}: --pristine did NOT wipe the build dir — {skipped.reason()}"
|
|
652
|
+
on_output(f"note: {message}")
|
|
653
|
+
issues.append(Issue("build.pristine-skipped", "warning", message))
|
|
654
|
+
|
|
655
|
+
if overridden or not under_build_root:
|
|
656
|
+
return issues
|
|
657
|
+
|
|
658
|
+
cached = read_sdk_stamp(cwd)
|
|
659
|
+
if force_pristine:
|
|
660
|
+
# Reached only when the dir WAS configured -- a never-configured one
|
|
661
|
+
# was already reported by `pristine_suppression` above, so this arm
|
|
662
|
+
# needs no message of its own for that case.
|
|
663
|
+
action = SdkStampAction.PRISTINE if cache_configured else SdkStampAction.KEEP
|
|
664
|
+
else:
|
|
665
|
+
action = sdk_stamp_action(
|
|
666
|
+
cached, sdk_stamp_key_str, cmake_cache_configured(cwd), overridden, under_build_root
|
|
667
|
+
)
|
|
668
|
+
if action is SdkStampAction.PRISTINE:
|
|
669
|
+
new_root = sdk_stamp_key_str or "?"
|
|
670
|
+
if force_pristine:
|
|
671
|
+
message = f"{core_id}: --pristine passed; wiping build dir before dispatch"
|
|
672
|
+
elif cached is not None:
|
|
673
|
+
message = (
|
|
674
|
+
f"{core_id}: build dir was configured against SDK root `{cached}`; "
|
|
675
|
+
f"active SDK is `{new_root}` — running pristine"
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
message = (
|
|
679
|
+
f"{core_id}: build dir predates the SDK-switch stamp (no recorded "
|
|
680
|
+
f"SDK root); running pristine against the active SDK `{new_root}`"
|
|
681
|
+
)
|
|
682
|
+
on_output(f"note: {message}")
|
|
683
|
+
issues.append(Issue("build.sdk-switch-pristine", "warning", message))
|
|
684
|
+
try:
|
|
685
|
+
shutil.rmtree(cwd / "build")
|
|
686
|
+
except OSError as err:
|
|
687
|
+
# Best-effort: west's own FATAL ERROR below (if the wipe didn't
|
|
688
|
+
# fully land) at least now ships with a note explaining why.
|
|
689
|
+
failed = f"{core_id}: could not fully wipe the stale build dir: {err}"
|
|
690
|
+
on_output(f"note: {failed}")
|
|
691
|
+
issues.append(Issue("build.sdk-switch-pristine-failed", "warning", failed))
|
|
692
|
+
|
|
693
|
+
if sdk_stamp_key_str is not None:
|
|
694
|
+
try:
|
|
695
|
+
write_sdk_stamp(cwd, sdk_stamp_key_str)
|
|
696
|
+
except OSError:
|
|
697
|
+
pass # best-effort -- see this function's docstring
|
|
698
|
+
return issues
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
#: cmake cache-var reset applied by [`_maybe_reset_stale_configure_cache`] --
|
|
702
|
+
#: see that function's docstring for why exactly these two and no others.
|
|
703
|
+
_CONFIGURE_CACHE_RESET_ARGS = ["-UDTC_OVERLAY_FILE", "-UCONF_FILE"]
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _maybe_reset_stale_configure_cache(
|
|
707
|
+
core_id: str,
|
|
708
|
+
cwd: Path,
|
|
709
|
+
app_dir: str | None,
|
|
710
|
+
backend: str,
|
|
711
|
+
build_root: Path,
|
|
712
|
+
on_output: Callable[[str], None],
|
|
713
|
+
) -> tuple[list[str], list[Issue]]:
|
|
714
|
+
"""tan-cli#655: a newly-added (or removed) devicetree overlay or Kconfig
|
|
715
|
+
fragment at one of Zephyr's own auto-discovery locations (`app.overlay`,
|
|
716
|
+
`boards/*.overlay`, `socs/*.overlay`, `prj*.conf`, `boards/*.conf`,
|
|
717
|
+
`socs/*.conf`) was silently IGNORED by an already-configured build dir --
|
|
718
|
+
`tan build` reported `ok`, but the generated `zephyr.dts`/`.config` still
|
|
719
|
+
reflected the PREVIOUS overlay/fragment set, and only `tan clean` (a full
|
|
720
|
+
wipe) picked the change up.
|
|
721
|
+
|
|
722
|
+
This was NOT a "CMake never reconfigures" bug -- every zephyr slice's
|
|
723
|
+
command already carries at least one `-D` after `--` (`_slice_command`'s
|
|
724
|
+
`-DPython3_EXECUTABLE=${PYTHON}`, always present), so `west build`'s own
|
|
725
|
+
`cmake_opts`-non-empty check (`scripts/west_commands/build.py::do_run`)
|
|
726
|
+
already sets `run_cmake = True` and reconfigures on every `tan build`,
|
|
727
|
+
measured on a real Zephyr 4.4.1 tree, not inferred. The reconfigure was
|
|
728
|
+
ALREADY happening; it just produced a stale result.
|
|
729
|
+
|
|
730
|
+
Root cause, also measured against that same tree: Zephyr's own
|
|
731
|
+
`cmake/modules/configuration_files.cmake` resolves `DTC_OVERLAY_FILE`/
|
|
732
|
+
`CONF_FILE` by auto-discovery ONLY `if(NOT DEFINED ...)`, then
|
|
733
|
+
unconditionally CACHES whatever it found -- including "found nothing" --
|
|
734
|
+
as an ordinary CMake cache entry. Every later configure of the SAME
|
|
735
|
+
build dir sees the var already `DEFINED` from the cache and skips
|
|
736
|
+
discovery entirely, so a file that did not exist at the FIRST configure
|
|
737
|
+
stays invisible to every configure after, no matter how many times
|
|
738
|
+
reconfigure runs, until that one cache entry is cleared. An EDIT to an
|
|
739
|
+
already-discovered file is a different, already-solved case: Zephyr ties
|
|
740
|
+
every file it DID discover into `CMAKE_CONFIGURE_DEPENDS`
|
|
741
|
+
(`dts.cmake`/`kconfig.cmake`), so its own mtime bump alone triggers a
|
|
742
|
+
correct reconfigure -- only the SET of discoverable files changing
|
|
743
|
+
defeats the cache.
|
|
744
|
+
|
|
745
|
+
The reset is `-UDTC_OVERLAY_FILE -UCONF_FILE` on the configure that
|
|
746
|
+
follows a set change -- deliberately NOT `-UEXTRA_DTC_OVERLAY_FILE`/
|
|
747
|
+
`-UEXTRA_CONF_FILE` here. CORRECTION (tan-cli#1350): the old premise that
|
|
748
|
+
those two are re-resolved on every configure regardless of the cache is
|
|
749
|
+
only true of `EXTRA_CONF_FILE`, which every plan re-passes with `-D`.
|
|
750
|
+
`EXTRA_DTC_OVERLAY_FILE` is read with `zephyr_get(... CACHE ...)`, so a
|
|
751
|
+
value passed once survives in `CMakeCache.txt` after the plan stops passing
|
|
752
|
+
it -- see `link_stale.stale_itcm_overlay_reset`, which unsets it for the
|
|
753
|
+
one overlay tan itself ever passes that way. This slice's own command
|
|
754
|
+
already ends
|
|
755
|
+
with `-DEXTRA_CONF_FILE=<build_dir>/alp.conf` (`_slice_command`'s
|
|
756
|
+
per-core Kconfig wiring) -- appending `-UEXTRA_CONF_FILE` AFTER that in
|
|
757
|
+
the same argv would UNSET it instead (measured: `-D`/`-U` on the same
|
|
758
|
+
cache key apply in argv order), silently dropping the per-core fragment
|
|
759
|
+
tan itself wires in on every reset. Verified this narrower pair alone is
|
|
760
|
+
sufficient AND leaves `-DEXTRA_CONF_FILE` intact: reproduced the #655
|
|
761
|
+
bug against a real Zephyr 4.4.1 `west build` (new `app.overlay`, new
|
|
762
|
+
`boards/<board>.conf`, and a removed overlay all silently/fatally
|
|
763
|
+
ignored pre-fix), then confirmed `-UDTC_OVERLAY_FILE -UCONF_FILE`
|
|
764
|
+
appended after an existing `-DEXTRA_CONF_FILE=...` recovers all three
|
|
765
|
+
while the `-DEXTRA_CONF_FILE` fragment's own content still lands.
|
|
766
|
+
|
|
767
|
+
Scoped to `os: zephyr` slices (`DTC_OVERLAY_FILE`/`CONF_FILE` are
|
|
768
|
+
Zephyr-CMake-specific; a yocto/baremetal slice has neither -- and
|
|
769
|
+
`_slice_command`'s zephyr branch is the ONLY place that ever emits one,
|
|
770
|
+
always with `tool: "west"`, so checking `backend` alone is exactly as
|
|
771
|
+
precise as also checking the tool identity, mirroring
|
|
772
|
+
`_maybe_pristine_stale_sdk_build_dir`'s own choice not to gate on tool
|
|
773
|
+
either), and to a build dir CMake has already configured at least once
|
|
774
|
+
(`cmake_cache_configured` -- nothing is cached yet on a first build, so
|
|
775
|
+
there is nothing to reset; this call only lays down the baseline stamp).
|
|
776
|
+
Comparison uses [`tan.commands.build.configure_inputs`]'s existence-only
|
|
777
|
+
fingerprint of the discovery dir's auto-discovery locations, stamped
|
|
778
|
+
inside west's own nested build dir (mirrors the SDK-identity stamp
|
|
779
|
+
above) -- NOT the plan's `command.args`, which name every EXTRA_/-D flag
|
|
780
|
+
tan itself controls but say nothing about a file the CUSTOMER added by
|
|
781
|
+
hand.
|
|
782
|
+
|
|
783
|
+
tan-cli#798: `app_dir` is the plan's `appDir` -- the CONFIGURED path,
|
|
784
|
+
NOT the directory `west build` is actually pointed at (see
|
|
785
|
+
`tan.commands.build_cmd`'s "Note `appDir` is NOT the substituted value"
|
|
786
|
+
docstring). For the scaffolded `app: ./src` layout (96 of 105 alp-sdk
|
|
787
|
+
example core entries, and every `tan init` template but `minimal-app`),
|
|
788
|
+
Zephyr auto-discovers overlays/fragments at the PARENT of `app_dir`, so
|
|
789
|
+
globbing `app_dir` itself found nothing on every build and this guard
|
|
790
|
+
never fired. [`resolve_zephyr_discovery_dir`] anchors `app_dir` on
|
|
791
|
+
`build_root` (a relative `appDir` must not resolve against the `tan`
|
|
792
|
+
process's own CWD -- the same reasoning `_missing_app_dirs` and
|
|
793
|
+
`_substituted_app_dirs` already carry) and applies the identical
|
|
794
|
+
CMakeLists.txt-parent fallback `_zephyr_app_dir`
|
|
795
|
+
(`tan.planner.orchestrator`) uses for the real `west build` positional,
|
|
796
|
+
so this guard's glob lands on the same directory.
|
|
797
|
+
|
|
798
|
+
Returns `(extra_cmake_args, issues)`, mirroring
|
|
799
|
+
[`_maybe_pristine_stale_sdk_build_dir`]'s own return shape: append
|
|
800
|
+
`extra_cmake_args` to the spawn argv (empty when nothing changed);
|
|
801
|
+
`issues` carries the coded `build.configure-cache-reset` note at `info`
|
|
802
|
+
severity so a JSON-mode caller (the VS Code extension, envelope-only)
|
|
803
|
+
can see a reset happened, not just a `text`-mode `on_output` line. Best-
|
|
804
|
+
effort throughout: a stamp read/write failure is "no signal" (read) or
|
|
805
|
+
silently skipped (write), never raised -- fails toward one spurious
|
|
806
|
+
extra reset, never toward trusting a cache this function cannot
|
|
807
|
+
actually verify."""
|
|
808
|
+
if backend != "zephyr" or not app_dir:
|
|
809
|
+
return [], []
|
|
810
|
+
app_dir_path = resolve_zephyr_discovery_dir(app_dir, build_root)
|
|
811
|
+
if not cmake_cache_configured(cwd):
|
|
812
|
+
# Nothing cached yet to poison -- lay down this configure's own
|
|
813
|
+
# baseline so the NEXT build has something to compare against.
|
|
814
|
+
try:
|
|
815
|
+
write_configure_inputs_stamp(cwd, discover_configure_inputs(app_dir_path))
|
|
816
|
+
except OSError:
|
|
817
|
+
pass
|
|
818
|
+
return [], []
|
|
819
|
+
|
|
820
|
+
current = discover_configure_inputs(app_dir_path)
|
|
821
|
+
previous = read_configure_inputs_stamp(cwd)
|
|
822
|
+
try:
|
|
823
|
+
write_configure_inputs_stamp(cwd, current)
|
|
824
|
+
except OSError:
|
|
825
|
+
pass # best-effort -- see this function's docstring
|
|
826
|
+
if previous is not None and previous == current:
|
|
827
|
+
return [], []
|
|
828
|
+
|
|
829
|
+
added = sorted(current - (previous or frozenset()))
|
|
830
|
+
removed = sorted((previous or frozenset()) - current)
|
|
831
|
+
detail = "; ".join(
|
|
832
|
+
part
|
|
833
|
+
for part in (f"added {added}" if added else "", f"removed {removed}" if removed else "")
|
|
834
|
+
if part
|
|
835
|
+
) or "no prior record"
|
|
836
|
+
message = (
|
|
837
|
+
f"{core_id}: devicetree overlay/Kconfig fragment set changed ({detail}) -- "
|
|
838
|
+
f"resetting Zephyr's cached DTC_OVERLAY_FILE/CONF_FILE so the configure that "
|
|
839
|
+
f"follows re-discovers it (tan-cli#655)"
|
|
840
|
+
)
|
|
841
|
+
on_output(f"note: {message}")
|
|
842
|
+
return list(_CONFIGURE_CACHE_RESET_ARGS), [
|
|
843
|
+
Issue("build.configure-cache-reset", "info", message)
|
|
844
|
+
]
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# tan-cli#697. The refusal itself -- `cross_drive_source_refusal` -- is pure
|
|
848
|
+
# argv/path logic with no IO, so it lives in `tan.core.plan_exec` (imported
|
|
849
|
+
# above) rather than here, matching this repo's own convention of keeping
|
|
850
|
+
# pure decisions out of the command/IO module. `CROSS_DRIVE_MSG` (imported
|
|
851
|
+
# above as well) is matched, not imported, by `build_cmd._cross_drive_issues`
|
|
852
|
+
# to promote a refused slice's `reason` into a coded top-level `issues[]`
|
|
853
|
+
# entry -- same idiom as `_missing_tool_issues`' `` tool `...` not found ``
|
|
854
|
+
# text match, immediately below in this same file's `_ZEPHYR_SDK_MISSING_MSG`
|
|
855
|
+
# /`_WEST_NO_WORKSPACE_MSG` pattern. See `cross_drive_source_refusal`'s own
|
|
856
|
+
# docstring (`tan/core/plan_exec.py`) for the no-op conditions (mirroring
|
|
857
|
+
# `_pin_west_workspace`'s own guard, below) and why `args[-1]` -- this
|
|
858
|
+
# function's first, wrong, implementation -- never located the real source
|
|
859
|
+
# dir on a plan `orchestrator.py` actually emits.
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _pin_west_workspace(
|
|
863
|
+
cwd: Path, args: Sequence[str], workspace_dir: Path | None
|
|
864
|
+
) -> tuple[Path, list[str]]:
|
|
865
|
+
"""tan-cli#307: `west` resolves its own topdir purely from the SPAWNED
|
|
866
|
+
PROCESS's cwd (`west.util.west_topdir`/`west.app.main.WestApp.run`,
|
|
867
|
+
verified against the real package: no env var and no CLI flag override
|
|
868
|
+
it -- `$ZEPHYR_BASE` is consulted only as a last resort, when NOTHING is
|
|
869
|
+
found walking up from cwd at all) -- entirely independent of the binary
|
|
870
|
+
tan already resolved (`west_program`). An ancestor `.west` above the
|
|
871
|
+
project (a second Zephyr checkout, a vendor SDK, an old workspace) wins
|
|
872
|
+
over the workspace `tan bootstrap` actually created purely because west
|
|
873
|
+
never consults anything tan resolved; tan picking the right binary and
|
|
874
|
+
then letting that binary pick the wrong workspace is exactly tan-cli#307.
|
|
875
|
+
|
|
876
|
+
Fixed by making the CHILD's own cwd land inside `workspace_dir` -- but
|
|
877
|
+
that collides with `west build`'s OWN cwd dependency: with no
|
|
878
|
+
`-d`/`--build-dir` in `args` (the plan's normal shape), `west build`
|
|
879
|
+
defaults its build dir to `<cwd>/build`, exactly the location
|
|
880
|
+
[`resolve_zephyr_artefact`] expects afterward. Moving `cwd` to the
|
|
881
|
+
workspace topdir would silently move that output too. Decoupled by
|
|
882
|
+
adding an explicit `-d <original cwd>/build` ourselves before handing
|
|
883
|
+
`cwd` off to the topdir: the effective build location stays identical to
|
|
884
|
+
what `west` would have defaulted to, and `cwd`'s new value only ever
|
|
885
|
+
answers "which workspace is this", never "where do the artefacts land".
|
|
886
|
+
|
|
887
|
+
No-op (returns `cwd`/`args` verbatim) when: no workspace resolved
|
|
888
|
+
(`workspace_dir` is `None` -- the caller then keeps behaving exactly as
|
|
889
|
+
before this fix, matching every other `west_workspace_dir` consumer's
|
|
890
|
+
`None` fallback); the command is not `west build` (`args[0] != "build"`
|
|
891
|
+
-- this pin only knows how to preserve `west build`'s own
|
|
892
|
+
default-build-dir rule, not any other verb); or the plan itself already
|
|
893
|
+
redirects the build dir (`build_dir_overridden`) -- that value may
|
|
894
|
+
already be an absolute path west will honour regardless of `cwd`, or a
|
|
895
|
+
relative one this function has no safe way to re-anchor; either way a
|
|
896
|
+
caller-supplied `-d` is left exactly as given, never doubled up."""
|
|
897
|
+
if workspace_dir is None or not args or args[0] != "build" or build_dir_overridden(args):
|
|
898
|
+
return cwd, list(args)
|
|
899
|
+
return workspace_dir, [args[0], "-d", str(cwd / "build"), *args[1:]]
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
@dataclass(frozen=True)
|
|
903
|
+
class _PostOutcome:
|
|
904
|
+
"""How a slice's `postCommands` ended, when they did not all exit 0.
|
|
905
|
+
|
|
906
|
+
`manifest_message` is the short form persisted as `system-manifest.yaml`
|
|
907
|
+
`slices[].reason` when it must differ from `message` -- the same split, for
|
|
908
|
+
the same reason, as [`SliceOutcome.manifest_message`]: the missing-tool
|
|
909
|
+
refusal's `-- searched PATH: <every entry>` belongs in this run's terminal
|
|
910
|
+
and envelope, never in a build ARTEFACT that outlives the run and gets
|
|
911
|
+
forwarded with a support ticket (tan-cli#615 review, MAJOR 1)."""
|
|
912
|
+
|
|
913
|
+
status: str
|
|
914
|
+
exit_code: int | None
|
|
915
|
+
message: str
|
|
916
|
+
manifest_message: str | None = None
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _missing_post_tool(
|
|
920
|
+
where: str, tool: str, searched: str, policy: ExecutionPolicy | None
|
|
921
|
+
) -> _PostOutcome:
|
|
922
|
+
"""The refusal for a post-build step whose tool is not on this host.
|
|
923
|
+
|
|
924
|
+
Routed through `executionPolicy.missingTool`, exactly like the slice's own
|
|
925
|
+
`command` (tan-cli#615 review, MAJOR 3). An earlier round hardcoded
|
|
926
|
+
`failed` here, arguing that a slice whose command has already run and
|
|
927
|
+
rewritten the build tree cannot honestly be called "skipped". That
|
|
928
|
+
argument loses to two documents this port is bound by, neither of which it
|
|
929
|
+
cited: alp-sdk's `metadata/schemas/build-plan-v1.schema.json` AT THE PINNED
|
|
930
|
+
COMMIT says, in `postCommands`' own description, "`executionPolicy` applies
|
|
931
|
+
to each step exactly as it does to `command`"; and this repo's
|
|
932
|
+
`docs/adr/0001-pmt-contract-decoupling.md` says "`tan` applies the policy
|
|
933
|
+
the plan declares; it does not hardcode a copy of the planner's skip
|
|
934
|
+
rules". A hardcoded disposition here is the exact coupling ADR-0001 exists
|
|
935
|
+
to prevent.
|
|
936
|
+
|
|
937
|
+
tan-cli#550 is not reopened by honouring it: `skipped` is not `built`
|
|
938
|
+
either -- the slice is absent from the `N of M slice(s) built` count and
|
|
939
|
+
carries a reason naming the missing tool. What the safety argument DOES
|
|
940
|
+
buy is kept: the message says the configure already ran, so a `skipped`
|
|
941
|
+
here cannot be misread as "never attempted".
|
|
942
|
+
|
|
943
|
+
Only `missingTool` is reachable per step. `nullCommand` cannot be (a null
|
|
944
|
+
step is refused at parse time, `tan.core.build_plan._post_commands`) and
|
|
945
|
+
`unknownBackend` is a per-slice fact decided before any step runs."""
|
|
946
|
+
action = resolve_action(policy, "missing_tool", PolicyAction.SKIP)
|
|
947
|
+
short = f"{where} cannot run: {missing_tool_message(tool)}"
|
|
948
|
+
return _PostOutcome(
|
|
949
|
+
"skipped" if action is PolicyAction.SKIP else "failed",
|
|
950
|
+
None,
|
|
951
|
+
f"{short} -- searched {searched}. The slice's own configure already ran.",
|
|
952
|
+
manifest_message=short,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _run_post_commands(
|
|
957
|
+
core_id: str, post_commands: Sequence, build_root: Path, env: dict[str, str],
|
|
958
|
+
on_output: Callable[[str], None], cancelled: Callable[[], bool],
|
|
959
|
+
policy: ExecutionPolicy | None,
|
|
960
|
+
) -> _PostOutcome | None:
|
|
961
|
+
"""Run a slice's `postCommands` in order; `None` when every one of them
|
|
962
|
+
exited 0, else the [`_PostOutcome`] the slice must report.
|
|
963
|
+
|
|
964
|
+
The CONSUMER half of alp-sdk #1344 (tan-cli#550). The planner half landed
|
|
965
|
+
in `tan/planner/` with the #608 re-sync and already emits the step --
|
|
966
|
+
`{"tool": "cmake", "args": ["--build", "."]}` for every `os: baremetal`
|
|
967
|
+
slice -- but nothing here read the key, so such a slice ran only its
|
|
968
|
+
`cmake -S ... -B .` CONFIGURE and reported `ok` over a build tree holding
|
|
969
|
+
`CMakeCache.txt` and no object file, archive or executable.
|
|
970
|
+
|
|
971
|
+
Skip-vs-fail for a missing step tool is the plan's call, not this
|
|
972
|
+
function's -- see [`_missing_post_tool`]. A `cwd` that escapes the build
|
|
973
|
+
root and a spawn that raises stay hard failures, matching what the slice's
|
|
974
|
+
own `command` does with the same two conditions."""
|
|
975
|
+
total = len(post_commands)
|
|
976
|
+
for n, step in enumerate(post_commands, start=1):
|
|
977
|
+
label = " ".join([step.tool, *step.args])
|
|
978
|
+
where = f"slice `{core_id}` post-build step {n} of {total} (`{label}`)"
|
|
979
|
+
try:
|
|
980
|
+
# Same confinement the slice's own `command.cwd` gets: a plan is
|
|
981
|
+
# trusted input, never trusted enough to run a process outside
|
|
982
|
+
# the build root.
|
|
983
|
+
cwd = confine_to_build_root(build_root, step.cwd) if step.cwd else build_root
|
|
984
|
+
except MaterialiseError as err:
|
|
985
|
+
return _PostOutcome("failed", None, f"{where} was refused: {err.message}")
|
|
986
|
+
resolution = _resolve_tool(step.tool, env)
|
|
987
|
+
if resolution.resolved is None:
|
|
988
|
+
return _missing_post_tool(where, step.tool, resolution.searched, policy)
|
|
989
|
+
result = _spawn_step(resolution.resolved, step.args, cwd, env, on_output, cancelled)
|
|
990
|
+
if result.cancelled:
|
|
991
|
+
# Deliberately the same message the slice's own command produces:
|
|
992
|
+
# one Ctrl-C cancelled one slice, and which step was in flight is
|
|
993
|
+
# not what the caller asked.
|
|
994
|
+
return _PostOutcome("cancelled", None, f"slice `{core_id}` cancelled")
|
|
995
|
+
if result.launch_error is not None:
|
|
996
|
+
# Both "the tool vanished since the check above" and "this step's
|
|
997
|
+
# `cwd` does not exist"; the OSError text names which.
|
|
998
|
+
return _PostOutcome("failed", None, f"{where} could not run: {result.launch_error}")
|
|
999
|
+
if result.exit_code != 0:
|
|
1000
|
+
return _PostOutcome(
|
|
1001
|
+
"failed", result.exit_code,
|
|
1002
|
+
f"{where} terminated with exit code: {result.exit_code}",
|
|
1003
|
+
)
|
|
1004
|
+
return None
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
#: MEASURED, and the reason the guard below is PRESENCE-based rather than
|
|
1008
|
+
#: freshness-based (tan-cli#615 review, MAJOR 2 -- answered by disclosure, with
|
|
1009
|
+
#: the measurement, rather than by the requested mtime check).
|
|
1010
|
+
#:
|
|
1011
|
+
#: A "the artefact must be newer than this run started" rule was implemented
|
|
1012
|
+
#: and then withdrawn, because it cannot tell the two cases apart:
|
|
1013
|
+
#:
|
|
1014
|
+
#: * ORPHANED -- the app's CMakeLists.txt stopped defining the executable
|
|
1015
|
+
#: target, so a previous run's binary lingers in `outputDir` and nothing
|
|
1016
|
+
#: rewrites it. This is the case worth catching.
|
|
1017
|
+
#: * UP TO DATE -- nothing changed, so `cmake --build .` correctly relinks
|
|
1018
|
+
#: nothing and the (still valid, still current) binary keeps its old mtime.
|
|
1019
|
+
#: This is the ordinary incremental rebuild, and it is the common case.
|
|
1020
|
+
#:
|
|
1021
|
+
#: In BOTH the artefact is untouched by this run, so any "newer than the run"
|
|
1022
|
+
#: rule refuses both. Measured end to end: with the freshness check in place, a
|
|
1023
|
+
#: second `tan build` with NO source change at all reported
|
|
1024
|
+
#: `failed: m55_hp [baremetal]` -- a false failure on the commonest workflow
|
|
1025
|
+
#: there is, strictly worse than the defect it was closing.
|
|
1026
|
+
#:
|
|
1027
|
+
#: The other candidate discriminators were measured too, and none separates
|
|
1028
|
+
#: the cases: `CMakeCache.txt`'s mtime is PRESERVED across all three runs
|
|
1029
|
+
#: (fresh / no-op / orphaned) and `Makefile`'s is REWRITTEN in all three, so
|
|
1030
|
+
#: neither is a signal. Comparing the artefact against the app's own source
|
|
1031
|
+
#: tree (make's own out-of-date rule) does separate them, but false-refuses
|
|
1032
|
+
#: whenever any file under `appDir` is touched without triggering a relink --
|
|
1033
|
+
#: a `git checkout`, a README edit, an editor swap file -- and re-deriving
|
|
1034
|
+
#: when an artefact is current is the build system's job, not tan's
|
|
1035
|
+
#: (`docs/adr/0001-pmt-contract-decoupling.md`).
|
|
1036
|
+
#:
|
|
1037
|
+
#: So `cmake --build .` exiting 0 is taken as the build system's own statement
|
|
1038
|
+
#: that the artefact is current, and STALENESS ACROSS A REMOVED TARGET IS A
|
|
1039
|
+
#: DISCLOSED LIMIT of this guard -- see [`_baremetal_artefact_refusal`], where
|
|
1040
|
+
#: it is listed beside the other two.
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _output_dir_has_a_file(out_dir: Path) -> bool:
|
|
1044
|
+
"""Whether a slice's output directory holds at least one regular FILE.
|
|
1045
|
+
|
|
1046
|
+
Files only: `rglob` walks directories too, and an empty `CMakeFiles/`
|
|
1047
|
+
left behind by the configure is not an artefact.
|
|
1048
|
+
|
|
1049
|
+
An `OSError` on a single entry (a broken symlink, a file deleted between
|
|
1050
|
+
the walk and the `stat`, a permission hole) skips that entry rather than
|
|
1051
|
+
taking the guard down -- one unreadable file is not evidence either way.
|
|
1052
|
+
|
|
1053
|
+
Presence, not freshness -- see the block comment above this function for
|
|
1054
|
+
the measurement that ruled freshness out."""
|
|
1055
|
+
if not out_dir.is_dir():
|
|
1056
|
+
return False
|
|
1057
|
+
for path in out_dir.rglob("*"):
|
|
1058
|
+
try:
|
|
1059
|
+
if path.is_file():
|
|
1060
|
+
return True
|
|
1061
|
+
except OSError:
|
|
1062
|
+
continue
|
|
1063
|
+
return False
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _baremetal_artefact_refusal(sl, build_root: Path) -> str | None:
|
|
1067
|
+
"""Why an `os: baremetal` slice whose tools all exited 0 must still be
|
|
1068
|
+
reported `failed` -- `None` when there is nothing to refuse.
|
|
1069
|
+
|
|
1070
|
+
The second half of tan-cli#550. Running `postCommands` makes a COMPILE or
|
|
1071
|
+
LINK error fail the slice honestly, but it does not close the issue's own
|
|
1072
|
+
headline: an exit code is still not evidence that firmware exists. An app
|
|
1073
|
+
defining no executable target configures, "builds", and exits 0 with an
|
|
1074
|
+
empty output directory -- the issue's "green build and an empty output
|
|
1075
|
+
directory", verbatim.
|
|
1076
|
+
|
|
1077
|
+
The evidence is the plan's own `artifacts.outputDir`, which alp-sdk #1344
|
|
1078
|
+
added alongside `postCommands` and which the baremetal configure pins as
|
|
1079
|
+
`CMAKE_RUNTIME_OUTPUT_DIRECTORY` -- where the SDK itself says the slice's
|
|
1080
|
+
linked executables land, not a convention invented here.
|
|
1081
|
+
|
|
1082
|
+
THREE KNOWN LIMITS, all deliberate, none worked around:
|
|
1083
|
+
|
|
1084
|
+
1. Silent (`None`) when the plan names no `outputDir`. A plan from an
|
|
1085
|
+
alp-sdk predating #1344 gives this guard nothing to judge by, and
|
|
1086
|
+
inventing a location would fail slices that build fine. Those plans get
|
|
1087
|
+
the `postCommands` half of the fix and not this half.
|
|
1088
|
+
2. A baremetal slice whose app intentionally links no executable (a pure
|
|
1089
|
+
`add_library` tree) is refused. Same trade the `os: zephyr` boilerplate
|
|
1090
|
+
guard takes -- a core in `cores:` is firmware for that core, and a slice
|
|
1091
|
+
producing nothing flashable has nothing for `tan flash`/`size`/`image`
|
|
1092
|
+
to consume.
|
|
1093
|
+
3. **A previous run's binary satisfies this guard.** Edit an app's
|
|
1094
|
+
CMakeLists.txt to stop defining its executable target and rebuild IN
|
|
1095
|
+
PLACE, and the orphaned binary from the earlier run is still sitting in
|
|
1096
|
+
`outputDir`; the slice is reported `ok` and `tan flash` would write that
|
|
1097
|
+
stale image as though it were current. Deleting the build directory (or
|
|
1098
|
+
`tan clean`) is the only thing that surfaces it today. This one is a
|
|
1099
|
+
DISCLOSURE, not a design preference: a freshness check was implemented
|
|
1100
|
+
and withdrawn after measurement -- see the block comment above
|
|
1101
|
+
[`_output_dir_has_a_file`] for what was tried and why every candidate
|
|
1102
|
+
either false-failed the ordinary incremental rebuild or duplicated the
|
|
1103
|
+
build system's own out-of-date rule."""
|
|
1104
|
+
rel = sl.artifacts.get("outputDir") if isinstance(sl.artifacts, dict) else None
|
|
1105
|
+
if not isinstance(rel, str) or not rel:
|
|
1106
|
+
return None
|
|
1107
|
+
try:
|
|
1108
|
+
out_dir = confine_to_build_root(build_root, rel)
|
|
1109
|
+
except MaterialiseError as err:
|
|
1110
|
+
return (
|
|
1111
|
+
f"core `{sl.core_id}` is declared `os: baremetal`, but the output directory "
|
|
1112
|
+
f"its plan names cannot be checked: {err.message}"
|
|
1113
|
+
)
|
|
1114
|
+
if _output_dir_has_a_file(out_dir):
|
|
1115
|
+
return None
|
|
1116
|
+
return (
|
|
1117
|
+
f"core `{sl.core_id}` is declared `os: baremetal`, but its build produced no "
|
|
1118
|
+
f"artefact in `{rel}` -- the configure and every post-build step exited 0, yet "
|
|
1119
|
+
f"nothing was linked there. tan pins `CMAKE_RUNTIME_OUTPUT_DIRECTORY` at that "
|
|
1120
|
+
f"directory so `tan flash`/`size`/`image` can find the firmware, so an app whose "
|
|
1121
|
+
f"CMakeLists.txt defines no executable target (`add_executable(...)`) builds "
|
|
1122
|
+
f"nothing this core can run."
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def execute_slices(
|
|
1127
|
+
plan,
|
|
1128
|
+
*,
|
|
1129
|
+
build_root: Path,
|
|
1130
|
+
env_lookup: Callable[[str], str | None],
|
|
1131
|
+
gap_fillers: Sequence[tuple[str, str]],
|
|
1132
|
+
on_output: Callable[[str], None],
|
|
1133
|
+
cancelled: Callable[[], bool] = lambda: False,
|
|
1134
|
+
sdk_root: str | None = None,
|
|
1135
|
+
sdk_root_for_stamp: str | None = None,
|
|
1136
|
+
held_outcomes: Sequence[SliceOutcome] = (),
|
|
1137
|
+
force_pristine: bool = False,
|
|
1138
|
+
slice_refusals: Mapping[str, str] | None = None,
|
|
1139
|
+
user_defines: Mapping[str, Sequence[str]] | None = None,
|
|
1140
|
+
) -> list[SliceOutcome]:
|
|
1141
|
+
"""Dispatch every slice of `plan` and return one [`SliceOutcome`] per
|
|
1142
|
+
slice, in plan order.
|
|
1143
|
+
|
|
1144
|
+
`force_pristine` is `tan build --pristine` (tan-cli#427): threaded
|
|
1145
|
+
straight through to [`_maybe_pristine_stale_sdk_build_dir`] per slice --
|
|
1146
|
+
see that function's own docstring for the wipe/suppression decision it
|
|
1147
|
+
makes.
|
|
1148
|
+
|
|
1149
|
+
`sdk_root_for_stamp` -- the identity the sdk-switch-pristine guard keys
|
|
1150
|
+
its stamp comparison on, when it differs from `sdk_root` (the value the
|
|
1151
|
+
post-build manifest write still uses). Defaults to `sdk_root` itself.
|
|
1152
|
+
The caller (`tan.commands.build_cmd.build`) passes a NORMALIZED,
|
|
1153
|
+
workspace-root-anchored form here (`tan.core.plan_exec.normalize_path`)
|
|
1154
|
+
while leaving `sdk_root` raw -- mirroring the oracle's own split between
|
|
1155
|
+
`normalized_sdk_root_str` (stamp-only) and the unnormalized
|
|
1156
|
+
`resolve_sdk_root` result (`${SDK_ROOT}` token substitution, `sdk`
|
|
1157
|
+
envelope reporting, the manifest emit's checkout lookup): a relative
|
|
1158
|
+
`--sdk-root ../alp-sdk` must key the SAME as the absolute pointer `tan
|
|
1159
|
+
sdk switch` already pinned for the identical checkout (tan-cli#163),
|
|
1160
|
+
but must NOT change what `${SDK_ROOT}` substitutes to or what the
|
|
1161
|
+
manifest emit resolves.
|
|
1162
|
+
|
|
1163
|
+
`slice_refusals` -- `core_id -> message` for slices that must FAIL once
|
|
1164
|
+
they are otherwise about to run (tan-cli#1317); see the use site.
|
|
1165
|
+
|
|
1166
|
+
`held_outcomes` -- outcomes for slices the CALLER already decided not to
|
|
1167
|
+
dispatch (`tan.commands.build_cmd._dispatch` holds back a
|
|
1168
|
+
token-substitution-demoted slice before this function ever sees the
|
|
1169
|
+
plan, tan-cli #89). They are folded into the post-build manifest overlay
|
|
1170
|
+
([`_write_manifest_after_dispatch`]) alongside this call's own outcomes,
|
|
1171
|
+
but are NOT part of the returned list -- the caller already accounts for
|
|
1172
|
+
them in its own outcome list and return-value shape must not double them
|
|
1173
|
+
up. Omitting a held slice from the overlay would leave its
|
|
1174
|
+
`system-manifest.yaml` entry at a previous run's `status`/
|
|
1175
|
+
`output_artefact` forever, indistinguishable from a slice that actually
|
|
1176
|
+
built (oracle `execute/mod.rs:379-400`)."""
|
|
1177
|
+
policy = plan.execution_policy
|
|
1178
|
+
outcomes: list[SliceOutcome] = []
|
|
1179
|
+
sdk_switch_issues: list[Issue] = []
|
|
1180
|
+
configure_cache_issues: list[Issue] = []
|
|
1181
|
+
# Computed once per build (not per slice): the plan's `sdkCommit` reflects
|
|
1182
|
+
# THIS run's freshly emitted plan, not a per-slice fact. `None` when
|
|
1183
|
+
# `sdk_root` itself is unresolved -- see `sdk_stamp_key`'s docstring.
|
|
1184
|
+
stamp_root = sdk_root_for_stamp if sdk_root_for_stamp is not None else sdk_root
|
|
1185
|
+
sdk_stamp_key_str = sdk_stamp_key(stamp_root, plan.sdk_commit)
|
|
1186
|
+
# tan-cli#307: resolved ONCE for the whole run (same reasoning as
|
|
1187
|
+
# `flash_cmd.py`'s own `workspace_dir` -- see that module's docstring),
|
|
1188
|
+
# keyed on `build_root` like every other `west_workspace_dir`/
|
|
1189
|
+
# `west_program` call in this function already is. `None` when nothing
|
|
1190
|
+
# resolves (CI, an activated venv, the contract harness) -- every west
|
|
1191
|
+
# slice below then keeps its old cwd, matching the pre-fix behaviour
|
|
1192
|
+
# exactly (see [`_pin_west_workspace`]).
|
|
1193
|
+
sdk_root_path = Path(sdk_root) if sdk_root is not None else None
|
|
1194
|
+
workspace_dir = west_workspace_dir(str(build_root), sdk_root_path)
|
|
1195
|
+
# tan-cli#308: port of the oracle's `resolve_zephyr_base` -- the
|
|
1196
|
+
# workspace's own `zephyr/` checkout, filtered to a real directory so a
|
|
1197
|
+
# `workspace_dir` that resolved but was never `west update`d (no
|
|
1198
|
+
# `zephyr/` yet) does not hand `west` a `ZEPHYR_BASE` that does not
|
|
1199
|
+
# exist. `None` propagates through [`zephyr_env_overrides`] as "nothing
|
|
1200
|
+
# to fill", matching every other `workspace_dir` consumer's fallback.
|
|
1201
|
+
zephyr_base = workspace_dir / "zephyr" if workspace_dir is not None else None
|
|
1202
|
+
if zephyr_base is not None and not zephyr_base.is_dir():
|
|
1203
|
+
zephyr_base = None
|
|
1204
|
+
# tan-cli#1209: resolved ONCE for the whole run, same reasoning as
|
|
1205
|
+
# `zephyr_base` just above -- it depends only on `sdk_root` (this run's
|
|
1206
|
+
# checkout), never on a per-slice plan field, so recomputing it inside
|
|
1207
|
+
# the loop below would just repeat the same read+stamp-check on every
|
|
1208
|
+
# slice for no different answer. `None` when `sdk_root` is unresolved,
|
|
1209
|
+
# the manifest is missing/malformed, or the stamped store does not
|
|
1210
|
+
# match this checkout's pin -- [`zephyr_env_overrides`] then fills
|
|
1211
|
+
# nothing, matching today's behaviour exactly.
|
|
1212
|
+
#
|
|
1213
|
+
# tan-cli#1209 review MINOR: also `None` when `host_scan_has_toolchain
|
|
1214
|
+
# (sdk_root)` -- a USABLE `zephyr-sdk*` install (a real cross compiler
|
|
1215
|
+
# inside it, not just a name match) CMake's own prefix scan (or the
|
|
1216
|
+
# user package registry) can already find on this host, independent of
|
|
1217
|
+
# tan's store. tan's own store is `-t arm-zephyr-eabi` ONLY; forcing it
|
|
1218
|
+
# ahead of a fuller, scan-visible host SDK could fail a non-ARM slice
|
|
1219
|
+
# that configured fine unaided, and would make `tan build` trust a
|
|
1220
|
+
# different toolchain than `tan doctor` reports
|
|
1221
|
+
# (`doctor_cmd._zephyr_sdk_scan_roots` ranks that same store LAST, never
|
|
1222
|
+
# first). Checked here, not inside `verified_store_dir` itself: that
|
|
1223
|
+
# function answers "is the pin satisfied", a fact independent of what
|
|
1224
|
+
# else is on the host.
|
|
1225
|
+
#
|
|
1226
|
+
# tan-cli#1209 review MAJOR: `host_scan_has_toolchain` requires an
|
|
1227
|
+
# actual compiler inside the candidate, not merely a directory whose
|
|
1228
|
+
# name STARTS WITH `zephyr-sdk` -- an empty `~/zephyr-sdk-leftover/`
|
|
1229
|
+
# must not silently disable tan's own verified store below.
|
|
1230
|
+
verified_store = (
|
|
1231
|
+
verified_store_dir(sdk_root) if not host_scan_has_toolchain(sdk_root) else None
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
for sl in plan.slices:
|
|
1235
|
+
if sl.backend not in KNOWN_BACKENDS:
|
|
1236
|
+
outcomes.append(
|
|
1237
|
+
_skip_or_fail(
|
|
1238
|
+
sl.core_id,
|
|
1239
|
+
resolve_action(policy, "unknown_backend", PolicyAction.FAIL),
|
|
1240
|
+
f"unknown backend `{sl.backend}`",
|
|
1241
|
+
)
|
|
1242
|
+
)
|
|
1243
|
+
continue
|
|
1244
|
+
|
|
1245
|
+
if sl.command is None:
|
|
1246
|
+
outcomes.append(
|
|
1247
|
+
_skip_or_fail(
|
|
1248
|
+
sl.core_id,
|
|
1249
|
+
resolve_action(policy, "null_command", PolicyAction.SKIP),
|
|
1250
|
+
f"slice `{sl.core_id}` has no command",
|
|
1251
|
+
)
|
|
1252
|
+
)
|
|
1253
|
+
continue
|
|
1254
|
+
|
|
1255
|
+
# Dispatch order below matches the Rust oracle (execute/mod.rs):
|
|
1256
|
+
# unsafe-cwd, then create_dir_all, THEN the missing-tool skip/fail --
|
|
1257
|
+
# an escaping cwd is a plan defect and must stay loud even when the
|
|
1258
|
+
# tool also happens to be missing, not get silently absorbed into
|
|
1259
|
+
# missingTool's default-skip.
|
|
1260
|
+
|
|
1261
|
+
# Plans are trusted input, but writes (and here, the working
|
|
1262
|
+
# directory the tool runs in) stay confined under `build_root` --
|
|
1263
|
+
# same guard `materialise_plan` applies to artefact paths.
|
|
1264
|
+
try:
|
|
1265
|
+
cwd = (
|
|
1266
|
+
confine_to_build_root(build_root, sl.command.cwd)
|
|
1267
|
+
if sl.command.cwd
|
|
1268
|
+
else build_root
|
|
1269
|
+
)
|
|
1270
|
+
except MaterialiseError as err:
|
|
1271
|
+
outcomes.append(SliceOutcome(sl.core_id, "failed", None, err.message))
|
|
1272
|
+
continue
|
|
1273
|
+
|
|
1274
|
+
try:
|
|
1275
|
+
cwd.mkdir(parents=True, exist_ok=True)
|
|
1276
|
+
except OSError as err:
|
|
1277
|
+
outcomes.append(
|
|
1278
|
+
SliceOutcome(
|
|
1279
|
+
sl.core_id, "failed", None, f"cannot create build dir `{cwd}`: {err}"
|
|
1280
|
+
)
|
|
1281
|
+
)
|
|
1282
|
+
continue
|
|
1283
|
+
|
|
1284
|
+
tool = sl.command.tool
|
|
1285
|
+
is_west = tool == "west"
|
|
1286
|
+
if is_west:
|
|
1287
|
+
# tan-cli#289/#106: rewrite a bare `"west"` to the west-capable
|
|
1288
|
+
# workspace venv's own binary -- an explicit different tool the
|
|
1289
|
+
# plan named (an absolute path, or anything not literally
|
|
1290
|
+
# `"west"`) is never touched, matching the Rust oracle's own
|
|
1291
|
+
# `cmd.tool == "west"` exact-match precedence.
|
|
1292
|
+
tool = west_program(str(build_root), sdk_root)
|
|
1293
|
+
|
|
1294
|
+
# tan-cli#697: checked here -- right after `is_west` is known and
|
|
1295
|
+
# BEFORE tool resolution -- rather than down at the `_pin_west_
|
|
1296
|
+
# workspace` call site it mirrors: like the unsafe-cwd escape and
|
|
1297
|
+
# `cwd.mkdir` failure above, a cross-drive layout is a plan/host
|
|
1298
|
+
# defect that will crash regardless of which `west` binary this host
|
|
1299
|
+
# happens to have, so it must not be silently absorbed into
|
|
1300
|
+
# `executionPolicy.missingTool`'s default-skip on a host where
|
|
1301
|
+
# `west` is ALSO not on PATH.
|
|
1302
|
+
if is_west:
|
|
1303
|
+
cross_drive_refusal = cross_drive_source_refusal(workspace_dir, sl.command.args)
|
|
1304
|
+
if cross_drive_refusal is not None:
|
|
1305
|
+
outcomes.append(
|
|
1306
|
+
SliceOutcome(
|
|
1307
|
+
sl.core_id,
|
|
1308
|
+
"failed",
|
|
1309
|
+
None,
|
|
1310
|
+
f"slice `{sl.core_id}` refused before build: "
|
|
1311
|
+
f"{cross_drive_refusal.message}",
|
|
1312
|
+
# tan-cli#697 review: a short form for
|
|
1313
|
+
# `system-manifest.yaml` `slices[].reason` -- see
|
|
1314
|
+
# [`SliceOutcome.manifest_message`]'s own docstring;
|
|
1315
|
+
# same split the missing-tool refusal below already
|
|
1316
|
+
# makes.
|
|
1317
|
+
manifest_message=cross_drive_refusal.manifest_message,
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1320
|
+
continue
|
|
1321
|
+
|
|
1322
|
+
# tan-cli#308: the zephyr gap-fillers are computed PER SLICE (not
|
|
1323
|
+
# once for the whole run, unlike `workspace_dir`/`zephyr_base`
|
|
1324
|
+
# themselves) because "plan wins" depends on THIS slice's own
|
|
1325
|
+
# `env`/`env_append_path` -- a heterogeneous plan can have one slice
|
|
1326
|
+
# that already pins `EXTRA_ZEPHYR_MODULES` (an SDK-emitted plan's
|
|
1327
|
+
# `envAppendPath`) alongside one that doesn't, and the caller's
|
|
1328
|
+
# `gap_fillers` merge (`assemble_slice_env`) OVERWRITES a key
|
|
1329
|
+
# unconditionally -- computing this once, outside the loop, would
|
|
1330
|
+
# silently clobber the plan's own richer module list on every slice
|
|
1331
|
+
# that DOES pin it.
|
|
1332
|
+
#
|
|
1333
|
+
# Computed HERE, BEFORE tool resolution -- MAJOR 2 of the tan-cli#510
|
|
1334
|
+
# review: `_resolve_tool` used to run on `os.environ` directly, 46
|
|
1335
|
+
# lines before this assembly ever built the env the spawn below
|
|
1336
|
+
# actually gets, so a plan pinning its own `PATH` in `command.env`
|
|
1337
|
+
# was checked against the PARENT's PATH and then spawned against a
|
|
1338
|
+
# DIFFERENT one -- an identical plan, run with `env: {"PATH":
|
|
1339
|
+
# "<planbin>:..."}` and a different `dualtool` on the parent's own
|
|
1340
|
+
# PATH, resolved and reported the PARENT's copy while the CHILD (had
|
|
1341
|
+
# it been spawned unresolved, pre-fix) would have run the plan's. A
|
|
1342
|
+
# plan that pins `PATH` is asking for that PATH to be used --
|
|
1343
|
+
# matching pre-fix POSIX `Popen`, which always selected the spawned
|
|
1344
|
+
# binary via `os.get_exec_path(env)` (the SPAWN's own env), never
|
|
1345
|
+
# the calling process's.
|
|
1346
|
+
slice_gap_fillers = [
|
|
1347
|
+
*gap_fillers,
|
|
1348
|
+
*zephyr_env_overrides(
|
|
1349
|
+
zephyr_base,
|
|
1350
|
+
sdk_root_path,
|
|
1351
|
+
sl.env,
|
|
1352
|
+
sl.env_append_path,
|
|
1353
|
+
env_lookup,
|
|
1354
|
+
toolchain_store=verified_store,
|
|
1355
|
+
),
|
|
1356
|
+
]
|
|
1357
|
+
# Bound to a name rather than inlined into the `update` call: the
|
|
1358
|
+
# tan-cli#336 pop below needs to distinguish a `ZEPHYR_BASE` that
|
|
1359
|
+
# something AUTHORITATIVE put here (the plan's own `env`, or
|
|
1360
|
+
# tan-cli#308's gap filler) from one merely inherited off
|
|
1361
|
+
# `os.environ` -- and after the `update` the merged `env` can no
|
|
1362
|
+
# longer tell those apart.
|
|
1363
|
+
slice_env = dict(
|
|
1364
|
+
assemble_slice_env(sl.env, sl.env_append_path, env_lookup, slice_gap_fillers)
|
|
1365
|
+
)
|
|
1366
|
+
# tan-cli#992: `spawn_env()` restores tan's own bundled
|
|
1367
|
+
# `LD_LIBRARY_PATH` override before this slice's own `env`/
|
|
1368
|
+
# `envAppendPath` overlay -- a build slice spawns `west`/`cmake`/the
|
|
1369
|
+
# toolchain, all system programs, and this is the build path (a
|
|
1370
|
+
# `--flash` runs straight out of a build that just took this env).
|
|
1371
|
+
env = spawn_env()
|
|
1372
|
+
env.update(slice_env)
|
|
1373
|
+
# tan-cli#289/#106: the venv `west` spawns nested `west`/`bitbake`
|
|
1374
|
+
# (via `alp_orchestrate`) that resolve purely via PATH -- without
|
|
1375
|
+
# this they fail to find `west` exactly like the parent process
|
|
1376
|
+
# would have, unless the user activated the venv. A no-op when
|
|
1377
|
+
# `tool` did not resolve to an absolute venv path above.
|
|
1378
|
+
env = with_venv_on_path(env, tool)
|
|
1379
|
+
|
|
1380
|
+
# tan-cli#510: resolve the identity to an absolute path with the
|
|
1381
|
+
# SAME hardened lookup the availability check used to only report a
|
|
1382
|
+
# bool for, and spawn THAT -- never the bare `tool` -- below. `tool`
|
|
1383
|
+
# itself stays exactly what it was (the plan's identity, or west's
|
|
1384
|
+
# venv rewrite just above): `with_venv_on_path` and every message
|
|
1385
|
+
# that follows still needs to say what the PLAN named, not what it
|
|
1386
|
+
# resolved to, and [`_ToolResolution`]'s own docstring is why a
|
|
1387
|
+
# second, divergent resolution must never be written here again.
|
|
1388
|
+
# Resolved against THIS slice's own fully assembled `env`
|
|
1389
|
+
# (MAJOR 2), not `os.environ` -- see the env-assembly comment above.
|
|
1390
|
+
resolution = _resolve_tool(tool, env)
|
|
1391
|
+
if resolution.resolved is None:
|
|
1392
|
+
outcomes.append(
|
|
1393
|
+
_skip_or_fail(
|
|
1394
|
+
sl.core_id,
|
|
1395
|
+
resolve_action(policy, "missing_tool", PolicyAction.SKIP),
|
|
1396
|
+
f"{missing_tool_message(tool)} -- searched {resolution.searched}",
|
|
1397
|
+
# tan-cli#510 review round 3, MAJOR: the full searched-PATH
|
|
1398
|
+
# text stays in `message` (this run's stdout + envelope
|
|
1399
|
+
# `reason`) but must NOT reach the persisted
|
|
1400
|
+
# `system-manifest.yaml` -- see [`SliceOutcome.
|
|
1401
|
+
# manifest_message`]'s own docstring.
|
|
1402
|
+
manifest_message=missing_tool_message(tool),
|
|
1403
|
+
)
|
|
1404
|
+
)
|
|
1405
|
+
continue
|
|
1406
|
+
resolved_tool = resolution.resolved
|
|
1407
|
+
# tan-cli#1317: a per-slice refusal the CALLER decided (no usable host
|
|
1408
|
+
# Python for a `${PYTHON}` slice), applied only HERE -- after the
|
|
1409
|
+
# `null_command` and `missing_tool` skips above, so a slice that would
|
|
1410
|
+
# never have run is still skipped per `executionPolicy` -- and before
|
|
1411
|
+
# the destructive pristine wipe below.
|
|
1412
|
+
refusal = (slice_refusals or {}).get(sl.core_id)
|
|
1413
|
+
if refusal is not None:
|
|
1414
|
+
outcomes.append(SliceOutcome(sl.core_id, "failed", None, refusal))
|
|
1415
|
+
continue
|
|
1416
|
+
# tan-cli#1429: same placement and reasoning as the #1317 refusal just
|
|
1417
|
+
# above. `cwd` is the spawn cwd here: `_pin_west_workspace` only moves
|
|
1418
|
+
# it when `workspace_dir` resolved, and this refusal needs it not to.
|
|
1419
|
+
if is_west:
|
|
1420
|
+
no_workspace = workspace_unresolved_refusal(
|
|
1421
|
+
workspace_dir, list(sl.command.args), cwd, env.get("ZEPHYR_BASE"), sdk_root_path
|
|
1422
|
+
)
|
|
1423
|
+
if no_workspace is not None:
|
|
1424
|
+
outcomes.append(
|
|
1425
|
+
SliceOutcome(
|
|
1426
|
+
sl.core_id,
|
|
1427
|
+
"failed",
|
|
1428
|
+
None,
|
|
1429
|
+
f"slice `{sl.core_id}` refused before build: {no_workspace.message}",
|
|
1430
|
+
manifest_message=no_workspace.manifest_message,
|
|
1431
|
+
)
|
|
1432
|
+
)
|
|
1433
|
+
continue
|
|
1434
|
+
# MAJOR 1 of the tan-cli#510 review: `None` (never surfaced) whenever
|
|
1435
|
+
# resolution landed on the exact string the plan already named --
|
|
1436
|
+
# see [`SliceOutcome.resolved_tool`]'s own docstring.
|
|
1437
|
+
outcome_resolved_tool = resolved_tool if resolved_tool != tool else None
|
|
1438
|
+
|
|
1439
|
+
# Deliberately AFTER the missing-tool skip above (not right after
|
|
1440
|
+
# `cwd.mkdir`): the wipe is destructive, and the tool-availability
|
|
1441
|
+
# check is the only thing standing between "this slice is about to
|
|
1442
|
+
# be rebuilt" and "this slice is about to be skipped" -- running the
|
|
1443
|
+
# wipe first would delete the last good `zephyr.elf` for a rebuild
|
|
1444
|
+
# that then never happens on a host missing `west`.
|
|
1445
|
+
# tan-cli#1382: a changed user `-D` set wipes this slice's build dir
|
|
1446
|
+
# (see `user_defines_wipe`). Before the sdk-switch/--pristine guard so
|
|
1447
|
+
# that guard sees the post-wipe dir; the stamp is written after it,
|
|
1448
|
+
# BEFORE the spawn, because it records what this configure is given.
|
|
1449
|
+
ud_now = list(user_defines.get(sl.core_id, ())) if user_defines else []
|
|
1450
|
+
ud_stampable = False
|
|
1451
|
+
if sl.backend == "zephyr":
|
|
1452
|
+
ud_issues, ud_stampable = reconcile_user_defines(
|
|
1453
|
+
sl.core_id,
|
|
1454
|
+
cwd,
|
|
1455
|
+
ud_now,
|
|
1456
|
+
build_root=build_root,
|
|
1457
|
+
guards_ok=not build_dir_overridden(sl.command.args)
|
|
1458
|
+
and _cwd_under_build_root(sl.command.cwd, cwd, build_root),
|
|
1459
|
+
on_output=on_output,
|
|
1460
|
+
)
|
|
1461
|
+
configure_cache_issues.extend(ud_issues)
|
|
1462
|
+
|
|
1463
|
+
sdk_switch_issues.extend(
|
|
1464
|
+
_maybe_pristine_stale_sdk_build_dir(
|
|
1465
|
+
sl.core_id,
|
|
1466
|
+
cwd,
|
|
1467
|
+
sl.command.cwd,
|
|
1468
|
+
sl.command.args,
|
|
1469
|
+
sdk_stamp_key_str,
|
|
1470
|
+
on_output,
|
|
1471
|
+
project_root=build_root,
|
|
1472
|
+
force_pristine=force_pristine,
|
|
1473
|
+
)
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
if ud_stampable:
|
|
1477
|
+
stamp_user_defines(cwd, ud_now)
|
|
1478
|
+
|
|
1479
|
+
# tan-cli#655: AFTER the sdk-switch-pristine guard, not before -- a
|
|
1480
|
+
# wipe there removes `cwd/build` wholesale (this stamp lives inside
|
|
1481
|
+
# it, same as the SDK stamp), so this call must see the POST-wipe
|
|
1482
|
+
# state to correctly treat a just-wiped dir as "nothing cached yet"
|
|
1483
|
+
# rather than comparing against a stamp that no longer describes
|
|
1484
|
+
# anything on disk.
|
|
1485
|
+
configure_cache_reset_args, new_configure_cache_issues = (
|
|
1486
|
+
_maybe_reset_stale_configure_cache(
|
|
1487
|
+
sl.core_id, cwd, sl.app_dir, sl.backend, build_root, on_output
|
|
1488
|
+
)
|
|
1489
|
+
)
|
|
1490
|
+
configure_cache_issues.extend(new_configure_cache_issues)
|
|
1491
|
+
# tan-cli#1350: `link: itcm` -> `auto` must not keep the ITCM overlay.
|
|
1492
|
+
stale_overlay_args: list[str] = []
|
|
1493
|
+
if sl.backend == "zephyr":
|
|
1494
|
+
stale_overlay_args, stale_overlay_issues = stale_itcm_overlay_reset(
|
|
1495
|
+
cwd, sl.command.args)
|
|
1496
|
+
configure_cache_issues.extend(stale_overlay_issues)
|
|
1497
|
+
|
|
1498
|
+
if is_west and workspace_dir is not None and "ZEPHYR_BASE" not in slice_env:
|
|
1499
|
+
# tan-cli#336: a dangling `$ZEPHYR_BASE` inherited from the
|
|
1500
|
+
# ambient shell (seeded above by `dict(os.environ)`) OUTRANKS the
|
|
1501
|
+
# workspace tan just resolved -- west's own `set_zephyr_base`
|
|
1502
|
+
# (`west/app/main.py`) trusts an already-set `ZEPHYR_BASE`
|
|
1503
|
+
# UNCONDITIONALLY over the manifest-derived one, with no
|
|
1504
|
+
# existence check, so a stale value is never self-corrected.
|
|
1505
|
+
# This is a DIFFERENT hazard than tan-cli#307's, and the cwd pin
|
|
1506
|
+
# above does not cover it: the `build` extension's OWN internal
|
|
1507
|
+
# `west_topdir(self.source_dir)` call (zephyr's
|
|
1508
|
+
# `scripts/west_commands/build.py`) walks from the SLICE'S OWN
|
|
1509
|
+
# app directory, not from cwd. An app inside the workspace tree
|
|
1510
|
+
# (an SDK-bundled shim) still resolves fine via that ancestor
|
|
1511
|
+
# walk regardless of `ZEPHYR_BASE`; the user's own project -- the
|
|
1512
|
+
# normal shape `tan init` scaffolds, a SIBLING of the workspace,
|
|
1513
|
+
# never nested inside it -- has no ancestor `.west` at all, so
|
|
1514
|
+
# that walk falls through to `$ZEPHYR_BASE` and inherits
|
|
1515
|
+
# whatever this process saw. Verified against a real dual-core
|
|
1516
|
+
# Zephyr SDK slice pair: the SDK-shim core builds either way,
|
|
1517
|
+
# the user's-project core fails only when `ZEPHYR_BASE` is
|
|
1518
|
+
# stale-but-set.
|
|
1519
|
+
#
|
|
1520
|
+
# Popping the key (rather than pinning it to a value computed
|
|
1521
|
+
# here) lets west's OWN manifest-project lookup re-derive it --
|
|
1522
|
+
# exactly what already happens when `ZEPHYR_BASE` is unset
|
|
1523
|
+
# (verified: an unset `ZEPHYR_BASE` self-heals via the
|
|
1524
|
+
# manifest's "zephyr"-named project, `zephyr.base-prefer`
|
|
1525
|
+
# unset).
|
|
1526
|
+
#
|
|
1527
|
+
# Guarded on `slice_env`, NOT on `sl.env`: tan-cli#308's
|
|
1528
|
+
# `zephyr_env_overrides` fills this key as a gap filler for
|
|
1529
|
+
# precisely the slices that DON'T pin it themselves, so keying
|
|
1530
|
+
# off the plan alone would pop #308's freshly-computed value on
|
|
1531
|
+
# every slice #308 exists to serve and leave only the pop's
|
|
1532
|
+
# weaker self-heal behind. `slice_env` holds the plan's own env
|
|
1533
|
+
# AND the gap fillers merged, so "present in `slice_env`" is
|
|
1534
|
+
# exactly "something authoritative decided this" -- and only an
|
|
1535
|
+
# ambient, inherited `ZEPHYR_BASE` is dropped. The two fixes
|
|
1536
|
+
# compose in that order: #308 supplies the right value whenever
|
|
1537
|
+
# the workspace has a real `zephyr/`; #336 removes a stale
|
|
1538
|
+
# ambient one for the cases #308 cannot fill (no
|
|
1539
|
+
# `workspace_dir`, or a workspace not yet `west update`d).
|
|
1540
|
+
env.pop("ZEPHYR_BASE", None)
|
|
1541
|
+
|
|
1542
|
+
# tan-cli#307: pin `west build` to the workspace tan resolved rather
|
|
1543
|
+
# than letting west infer one from `cwd` -- see [`_pin_west_workspace`].
|
|
1544
|
+
# Every OTHER use of `cwd` in this function (the pristine guard just
|
|
1545
|
+
# above, `resolve_zephyr_artefact` below) deliberately still reads
|
|
1546
|
+
# the ORIGINAL `cwd`/`sl.command.args`, not these: this pin is a
|
|
1547
|
+
# spawn-time-only concern, not a change to where the build dir
|
|
1548
|
+
# itself lives on disk.
|
|
1549
|
+
spawn_cwd, spawn_args = (
|
|
1550
|
+
_pin_west_workspace(cwd, sl.command.args, workspace_dir)
|
|
1551
|
+
if is_west
|
|
1552
|
+
else (cwd, list(sl.command.args))
|
|
1553
|
+
)
|
|
1554
|
+
# Added to the SPAWN argv only -- `sl.command.args` (read again
|
|
1555
|
+
# below by `resolve_zephyr_artefact`/`build_dir_overridden`) stays
|
|
1556
|
+
# exactly what the plan named, so those checks never see a flag tan
|
|
1557
|
+
# itself injected. Order matters, and it is the REVERSE of what this
|
|
1558
|
+
# said before user `-D` existed (tan-cli#1382): cmake applies `-U`/`-D`
|
|
1559
|
+
# in argv order, so a `-U<key>` AFTER a `-D<key>=...` silently unsets
|
|
1560
|
+
# it (`-D CONF_FILE=prod.conf` on a cache-reset build). Every reset
|
|
1561
|
+
# goes right after `--`, BEFORE all plan and user `-D`, so a `-D` of
|
|
1562
|
+
# the same key always wins.
|
|
1563
|
+
spawn_args = insert_after_dashdash(spawn_args, configure_cache_reset_args)
|
|
1564
|
+
# tan-cli#1350: the stale-ITCM `-U` goes right after `--`, BEFORE every
|
|
1565
|
+
# `-D`, so a later `-DEXTRA_DTC_OVERLAY_FILE` (the plan's own, or a
|
|
1566
|
+
# user's) is applied after the unset and can never be erased by it.
|
|
1567
|
+
spawn_args = insert_after_separator(spawn_args, stale_overlay_args)
|
|
1568
|
+
|
|
1569
|
+
# tan-cli#336: watch the slice's own stdout for west's literal
|
|
1570
|
+
# "could not find a workspace" message so a failure carrying it can
|
|
1571
|
+
# be re-worded below with what tan itself resolved -- the bare
|
|
1572
|
+
# `terminated with exit code: N` a plain west failure gets otherwise
|
|
1573
|
+
# is "the least informative true statement available" (issue #336).
|
|
1574
|
+
# `nonlocal` into a fresh binding scoped to THIS iteration (declared
|
|
1575
|
+
# inside the loop, not hoisted above it), so a later slice's closure
|
|
1576
|
+
# never reads a previous one's leftover.
|
|
1577
|
+
saw_no_workspace = False
|
|
1578
|
+
saw_no_zephyr_sdk = False
|
|
1579
|
+
# `None` means "persist `message` verbatim" -- only a post-build
|
|
1580
|
+
# refusal that carries host detail sets it (tan-cli#615 review,
|
|
1581
|
+
# MAJOR 1). Re-bound per iteration so one slice's redaction can never
|
|
1582
|
+
# be persisted as the next slice's reason.
|
|
1583
|
+
manifest_message: str | None = None
|
|
1584
|
+
|
|
1585
|
+
def _watch_for_no_workspace(line: str) -> None:
|
|
1586
|
+
nonlocal saw_no_workspace, saw_no_zephyr_sdk
|
|
1587
|
+
if _WEST_NO_WORKSPACE_MSG in line:
|
|
1588
|
+
saw_no_workspace = True
|
|
1589
|
+
if _ZEPHYR_SDK_MISSING_MSG in line:
|
|
1590
|
+
saw_no_zephyr_sdk = True
|
|
1591
|
+
on_output(line)
|
|
1592
|
+
|
|
1593
|
+
# tan-cli#550: the spawn itself lives in [`_spawn_step`] so the
|
|
1594
|
+
# post-build steps below run through the SAME Popen/drain/terminate
|
|
1595
|
+
# path as the slice's own command -- one spawn implementation, not
|
|
1596
|
+
# two that can drift on cancellation handling or fd hygiene.
|
|
1597
|
+
step = _spawn_step(
|
|
1598
|
+
resolved_tool, spawn_args, spawn_cwd, env, _watch_for_no_workspace, cancelled
|
|
1599
|
+
)
|
|
1600
|
+
if step.cancelled:
|
|
1601
|
+
outcomes.append(
|
|
1602
|
+
SliceOutcome(
|
|
1603
|
+
sl.core_id,
|
|
1604
|
+
"cancelled",
|
|
1605
|
+
None,
|
|
1606
|
+
f"slice `{sl.core_id}` cancelled",
|
|
1607
|
+
resolved_tool=outcome_resolved_tool,
|
|
1608
|
+
)
|
|
1609
|
+
)
|
|
1610
|
+
continue
|
|
1611
|
+
if step.launch_error is not None:
|
|
1612
|
+
# The tool vanished between the availability check above and
|
|
1613
|
+
# here, is a directory, lacks the executable bit, or is not a
|
|
1614
|
+
# valid executable format -- any of these raise inside
|
|
1615
|
+
# `_spawn_step` rather than at the `_resolve_tool` precheck. A
|
|
1616
|
+
# failed slice, never a crash. The base message already names
|
|
1617
|
+
# both `tool` and `resolved_tool` explicitly (it IS the substance
|
|
1618
|
+
# of what failed, not a redundant echo of it) -- `resolved_tool=`
|
|
1619
|
+
# below still carries the SAME fact in the dedicated structured
|
|
1620
|
+
# field.
|
|
1621
|
+
outcomes.append(
|
|
1622
|
+
SliceOutcome(
|
|
1623
|
+
sl.core_id,
|
|
1624
|
+
"failed",
|
|
1625
|
+
None,
|
|
1626
|
+
f"failed to launch `{tool}` resolved to `{resolved_tool}`: "
|
|
1627
|
+
f"{step.launch_error}",
|
|
1628
|
+
resolved_tool=outcome_resolved_tool,
|
|
1629
|
+
)
|
|
1630
|
+
)
|
|
1631
|
+
continue
|
|
1632
|
+
# Neither cancelled nor a launch failure, so `_spawn_step` reached
|
|
1633
|
+
# `proc.wait()` and `code` is a real integer -- the only two shapes
|
|
1634
|
+
# that leave it `None` are the two `continue`d above.
|
|
1635
|
+
code = step.exit_code
|
|
1636
|
+
|
|
1637
|
+
status = "succeeded" if code == 0 else "failed"
|
|
1638
|
+
if code == 0:
|
|
1639
|
+
message = None
|
|
1640
|
+
elif is_west and saw_no_workspace and workspace_dir is not None:
|
|
1641
|
+
# tan-cli#336: west named no cause beyond its own exit code even
|
|
1642
|
+
# though tan was holding a resolved workspace path the whole
|
|
1643
|
+
# time -- name it, and the `ZEPHYR_BASE` this spawn actually saw.
|
|
1644
|
+
# After tan-cli#308 that value is usually the workspace's own
|
|
1645
|
+
# `zephyr/`; "unset" means #308 had nothing to fill and the #336
|
|
1646
|
+
# pop above ran. Either way it is the fact that distinguishes
|
|
1647
|
+
# "tan pointed west somewhere wrong" from "west never saw what
|
|
1648
|
+
# tan resolved". Plain string interpolation, not `!r`: a Windows
|
|
1649
|
+
# path's backslashes survive unescaped this way, matching every
|
|
1650
|
+
# other path already embedded in this module's messages.
|
|
1651
|
+
seen_zephyr_base = env.get("ZEPHYR_BASE") or "unset"
|
|
1652
|
+
message = (
|
|
1653
|
+
f"slice `{sl.core_id}` terminated with exit code: {code} -- west could not "
|
|
1654
|
+
f"find a workspace; tan resolved `{workspace_dir}`, but the spawned process "
|
|
1655
|
+
f"saw ZEPHYR_BASE={seen_zephyr_base}"
|
|
1656
|
+
)
|
|
1657
|
+
elif saw_no_zephyr_sdk:
|
|
1658
|
+
# tan-cli#419: name the cause and the remedy tan already knows.
|
|
1659
|
+
# tan-cli#1520: the remedy is `tan bootstrap` (verified archive),
|
|
1660
|
+
# not a hand-run `west sdk install -t arm-zephyr-eabi`, whose
|
|
1661
|
+
# setup.sh fetch skips the alp-sdk sha256 pin. Same constant
|
|
1662
|
+
# `doctor`'s `zephyrSdk` check uses, so the two cannot drift.
|
|
1663
|
+
from tan.commands.doctor_cmd import ZEPHYR_SDK_BOOTSTRAP_FIX
|
|
1664
|
+
|
|
1665
|
+
message = (
|
|
1666
|
+
f"slice `{sl.core_id}` terminated with exit code: {code} -- the Zephyr "
|
|
1667
|
+
f"SDK cross toolchain is not installed (CMake could not find the "
|
|
1668
|
+
f"`Zephyr-sdk` package). Run "
|
|
1669
|
+
f"`{ZEPHYR_SDK_BOOTSTRAP_FIX}`, then re-run the build; "
|
|
1670
|
+
f"`tan doctor --build` reports this as `zephyrSdk` with the same fix."
|
|
1671
|
+
)
|
|
1672
|
+
else:
|
|
1673
|
+
message = f"slice `{sl.core_id}` terminated with exit code: {code}"
|
|
1674
|
+
|
|
1675
|
+
# tan-cli#309 (upstream tan-cli #97): a core declared `os: zephyr`
|
|
1676
|
+
# whose CMakeLists.txt never calls `find_package(Zephyr ...)` still
|
|
1677
|
+
# configures and links fine under `west build -b <board>` (CMake
|
|
1678
|
+
# only emits a *dev* warning about the missing `project()` call), so
|
|
1679
|
+
# a real exit code 0 is NOT sufficient evidence -- without this the
|
|
1680
|
+
# out-of-the-box scaffold was reported `[+] ok` for a plain host
|
|
1681
|
+
# binary with no Zephyr in it at all. Checked only on an otherwise-
|
|
1682
|
+
# successful slice (a genuine build failure already speaks for
|
|
1683
|
+
# itself) and skipped when the slice redirects west's own build dir
|
|
1684
|
+
# (`-d`/`--build-dir`), where the evidence lives somewhere this
|
|
1685
|
+
# cannot see -- the same refusal `resolve_zephyr_artefact` below
|
|
1686
|
+
# already makes.
|
|
1687
|
+
if (
|
|
1688
|
+
status == "succeeded"
|
|
1689
|
+
and sl.backend == "zephyr"
|
|
1690
|
+
and not build_dir_overridden(sl.command.args)
|
|
1691
|
+
and not zephyr_boilerplate_loaded(cwd)
|
|
1692
|
+
):
|
|
1693
|
+
status = "failed"
|
|
1694
|
+
message = (
|
|
1695
|
+
f"core `{sl.core_id}` is declared `os: zephyr`, but the build in "
|
|
1696
|
+
f"`{sl.command.cwd or '.'}` never loaded Zephyr (no ZEPHYR_BASE in its "
|
|
1697
|
+
f"CMakeCache.txt and no zephyr/ output) — its CMakeLists.txt must call "
|
|
1698
|
+
f"`find_package(Zephyr REQUIRED HINTS $ENV{{ZEPHYR_BASE}})` before `project()`; "
|
|
1699
|
+
f"without it CMake builds a plain host binary, not firmware. Scaffold a working "
|
|
1700
|
+
f"app with `tan init --template zephyr-app`, or point the core's `app:` at one "
|
|
1701
|
+
f"that does."
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
# tan-cli#550, half 1: run the plan's `postCommands`. `west build` and
|
|
1705
|
+
# `bitbake` configure AND build in one invocation and carry none; a
|
|
1706
|
+
# baremetal slice's `cmake -S ... -B .` only CONFIGURES, so its
|
|
1707
|
+
# `cmake --build .` arrives here and MUST run before the slice can be
|
|
1708
|
+
# called built. Gated on the slice's own command having succeeded --
|
|
1709
|
+
# there is nothing to build on top of a failed configure -- and on the
|
|
1710
|
+
# zephyr-evidence guard above, for the same reason.
|
|
1711
|
+
if status == "succeeded" and sl.post_commands:
|
|
1712
|
+
post = _run_post_commands(
|
|
1713
|
+
sl.core_id, sl.post_commands, build_root, env, on_output, cancelled, policy
|
|
1714
|
+
)
|
|
1715
|
+
if post is not None:
|
|
1716
|
+
# The failing step's own exit code becomes the slice's `rc`:
|
|
1717
|
+
# unlike the evidence guards, this is not tan refusing a
|
|
1718
|
+
# result, it is a real process that really failed. A step that
|
|
1719
|
+
# never reached a process at all carries `None`, which the
|
|
1720
|
+
# single `SliceOutcome` construction below already renders as
|
|
1721
|
+
# a null `rc`.
|
|
1722
|
+
#
|
|
1723
|
+
# No early `continue` for the `cancelled`/`skipped` shapes:
|
|
1724
|
+
# every path from here to that construction is gated on
|
|
1725
|
+
# `status == "succeeded"`, so a non-succeeded post outcome
|
|
1726
|
+
# already reaches it untouched. A second append site would be
|
|
1727
|
+
# an untestable duplicate of the first (proven: a mutant that
|
|
1728
|
+
# removed the `skipped` case from such a branch changed no
|
|
1729
|
+
# observable field).
|
|
1730
|
+
status, code, message = post.status, post.exit_code, post.message
|
|
1731
|
+
# tan-cli#615 review, MAJOR 1: the missing-tool refusal's
|
|
1732
|
+
# searched-PATH text must not reach the persisted
|
|
1733
|
+
# `system-manifest.yaml` -- see [`SliceOutcome.manifest_message`].
|
|
1734
|
+
manifest_message = post.manifest_message
|
|
1735
|
+
|
|
1736
|
+
# tan-cli#550, half 2: an exit code is not evidence that firmware
|
|
1737
|
+
# exists. See [`_baremetal_artefact_refusal`] -- the same shape as the
|
|
1738
|
+
# zephyr guard above, keyed on the plan's own `artifacts.outputDir`.
|
|
1739
|
+
if status == "succeeded" and sl.backend == "baremetal":
|
|
1740
|
+
refusal = _baremetal_artefact_refusal(sl, build_root)
|
|
1741
|
+
if refusal is not None:
|
|
1742
|
+
status = "failed"
|
|
1743
|
+
message = refusal
|
|
1744
|
+
|
|
1745
|
+
# On success, resolve the real on-disk artefact west produced so the
|
|
1746
|
+
# post-build manifest points downstream consumers (`run`/`size`/
|
|
1747
|
+
# `flash`/`image`) at the elf that exists, not a plan-time guess.
|
|
1748
|
+
# Gated on the FINAL `status` (after the guard above), not the raw
|
|
1749
|
+
# exit code: a guard-failed slice has no real Zephyr artefact to
|
|
1750
|
+
# report even though the tool itself exited 0.
|
|
1751
|
+
output_artefact, slice_build_dir = (
|
|
1752
|
+
resolve_zephyr_artefact(cwd, sl.command.args) if status == "succeeded" else (None, None)
|
|
1753
|
+
)
|
|
1754
|
+
outcomes.append(
|
|
1755
|
+
SliceOutcome(
|
|
1756
|
+
sl.core_id,
|
|
1757
|
+
status,
|
|
1758
|
+
# A negative POSIX return code means the process died from a
|
|
1759
|
+
# signal -- Rust's `ExitStatus::code()` returns `None` for
|
|
1760
|
+
# that case (it has no single-integer exit code), so the
|
|
1761
|
+
# envelope's `rc` must be null too, not the raw `-N`. Stays
|
|
1762
|
+
# the tool's REAL exit code even when the guard above
|
|
1763
|
+
# overrode `status` to "failed" -- `west build` really did
|
|
1764
|
+
# exit 0, the guard is refusing the RESULT, not the exit.
|
|
1765
|
+
#
|
|
1766
|
+
# `code` is also `None` when a post-build step (tan-cli#550)
|
|
1767
|
+
# never reached a process at all -- its tool was missing, its
|
|
1768
|
+
# `cwd` escaped the build root, or the spawn raised -- which
|
|
1769
|
+
# is the same "no single-integer exit code" case the signal
|
|
1770
|
+
# death above is, and reaches the envelope's `rc` as null the
|
|
1771
|
+
# same way.
|
|
1772
|
+
None if code is None or code < 0 else code,
|
|
1773
|
+
message,
|
|
1774
|
+
output_artefact,
|
|
1775
|
+
slice_build_dir,
|
|
1776
|
+
resolved_tool=outcome_resolved_tool,
|
|
1777
|
+
manifest_message=manifest_message,
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
global _last_sdk_switch_issues, _last_configure_cache_issues
|
|
1782
|
+
_last_sdk_switch_issues = sdk_switch_issues
|
|
1783
|
+
_last_configure_cache_issues = configure_cache_issues
|
|
1784
|
+
|
|
1785
|
+
_write_manifest_after_dispatch(plan, build_root, [*outcomes, *held_outcomes], on_output, sdk_root)
|
|
1786
|
+
return outcomes
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _write_manifest_after_dispatch(
|
|
1790
|
+
plan,
|
|
1791
|
+
build_root: Path,
|
|
1792
|
+
outcomes: list[SliceOutcome],
|
|
1793
|
+
on_output: Callable[[str], None],
|
|
1794
|
+
sdk_root: str | None,
|
|
1795
|
+
) -> None:
|
|
1796
|
+
"""The output seam: write the post-build `system-manifest.yaml` (the
|
|
1797
|
+
contract `tan run`/`flash`/`size`/`image` read) reflecting this run's
|
|
1798
|
+
per-slice status, and record the in-memory signals `tan run`/
|
|
1799
|
+
`tan build` decide from -- see [`last_manifest_write`] and
|
|
1800
|
+
[`last_manifest_write_failure`]. Always attempted, even for zero slices,
|
|
1801
|
+
mirroring the Rust oracle's `execute_slices_outcome` (which calls
|
|
1802
|
+
`write_post_build_manifest` unconditionally after its dispatch loop, not
|
|
1803
|
+
only when at least one slice ran). `outcomes` here is the FULL set
|
|
1804
|
+
overlaid onto the manifest -- dispatched slices plus any the caller
|
|
1805
|
+
held back (see [`execute_slices`]'s `held_outcomes` parameter) -- not
|
|
1806
|
+
necessarily what [`execute_slices`] itself returns."""
|
|
1807
|
+
results = [
|
|
1808
|
+
SliceRunResult(
|
|
1809
|
+
core_id=o.core_id,
|
|
1810
|
+
status=_WIRE_STATUS.get(o.status, "failed"),
|
|
1811
|
+
output_artefact=o.output_artefact,
|
|
1812
|
+
build_dir=o.build_dir,
|
|
1813
|
+
# tan-cli#510 review round 3, MAJOR: `manifest_message`, when set,
|
|
1814
|
+
# is the short form that must land on disk instead of `message`
|
|
1815
|
+
# (the missing-tool refusal's `-- searched PATH: <every entry>`
|
|
1816
|
+
# leaks the customer's machine layout into a persisted, forwarded
|
|
1817
|
+
# artefact -- see [`SliceOutcome.manifest_message`]).
|
|
1818
|
+
reason=o.manifest_message if o.manifest_message is not None else o.message,
|
|
1819
|
+
)
|
|
1820
|
+
for o in outcomes
|
|
1821
|
+
]
|
|
1822
|
+
outcome = write_post_build_manifest(
|
|
1823
|
+
sdk_root=sdk_root,
|
|
1824
|
+
board_yaml=plan.board_yaml,
|
|
1825
|
+
base=str(build_root),
|
|
1826
|
+
plan_build_root=plan.build_root,
|
|
1827
|
+
results=results,
|
|
1828
|
+
plain_route=plain_route_target(plan),
|
|
1829
|
+
)
|
|
1830
|
+
global _last_manifest_write
|
|
1831
|
+
_last_manifest_write = _ManifestWriteSignal(
|
|
1832
|
+
manifest_written=outcome.write_failed_reason is None,
|
|
1833
|
+
native_sim_target=outcome.native_sim_target,
|
|
1834
|
+
write_failed_reason=outcome.write_failed_reason,
|
|
1835
|
+
)
|
|
1836
|
+
if outcome.write_failed_reason is not None:
|
|
1837
|
+
# Not silent -- mirrors the Rust oracle's identical `eprintln!` (via
|
|
1838
|
+
# `on_output`, this port's stderr-streaming callback: this function
|
|
1839
|
+
# has no envelope/issues list of its own to fold a warning `Issue`
|
|
1840
|
+
# into). `tan.commands.build_cmd._build` folds the SAME reason into
|
|
1841
|
+
# the JSON envelope's `issues` as `build.manifest-write-failed`
|
|
1842
|
+
# (`last_manifest_write_failure()`, see that module's own call
|
|
1843
|
+
# site) -- so the envelope-side half this docstring used to call
|
|
1844
|
+
# "out of this unit's scope" is covered too; this line is the
|
|
1845
|
+
# text-mode/stderr half only. Unlike the oracle (`execute/mod.rs`
|
|
1846
|
+
# gates its `eprintln!` on `text_mode`), this call fires in BOTH
|
|
1847
|
+
# `--format text` and `--format json` -- the same deliberate,
|
|
1848
|
+
# port-wide "stderr is always-on, never mode-gated" convention
|
|
1849
|
+
# `_maybe_pristine_stale_sdk_build_dir`'s docstring documents for
|
|
1850
|
+
# the sibling sdk-switch-pristine note; harmless since stdout, not
|
|
1851
|
+
# stderr, is the JSON envelope's only channel.
|
|
1852
|
+
on_output(f"note: skipped writing system-manifest.yaml — {outcome.write_failed_reason}")
|