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,1456 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan generate` -- run the board-derived emitters and report what landed.
|
|
3
|
+
|
|
4
|
+
**tan invents no output of its own**, but it no longer spawns alp-sdk to produce
|
|
5
|
+
it either. All TWELVE targets render in-process from `tan.planner`
|
|
6
|
+
(`tan.planner_emit.IN_PROCESS_MODES`) -- the five modes the planner relocation
|
|
7
|
+
brought over first, the six whose renderers moved with it (`dts-overlay`,
|
|
8
|
+
`native-sim-overlay`, `hw-info-h`, `west-libraries`, `carrier-netlist` and
|
|
9
|
+
`zephyr-board`), plus `composed-route-table` (the older debug view of
|
|
10
|
+
`carrier-netlist`'s own route rows, ported alongside it). No interpreter on
|
|
11
|
+
PATH, no PYTHONPATH, no process.
|
|
12
|
+
|
|
13
|
+
The spawned engine survives as an ESCAPE HATCH, not a code path anything reaches
|
|
14
|
+
by default:
|
|
15
|
+
|
|
16
|
+
<python> <sdk>/scripts/alp_project.py --input <board.yaml> \
|
|
17
|
+
--emit <mode> --output <path> [--core <id>]
|
|
18
|
+
|
|
19
|
+
It runs only when `TAN_GENERATE_EXECUTOR=subprocess` pins it, or when the
|
|
20
|
+
in-process path cannot serve the checkout at all. Keeping it is what lets the
|
|
21
|
+
parity suite render the same target both ways and diff the bytes.
|
|
22
|
+
|
|
23
|
+
`tan.planner_emit` documents how `metadata/emit-registry-v1.json` decides which
|
|
24
|
+
renderer owns which mode. Either way the emitted bytes, the schemas behind them,
|
|
25
|
+
and every hardware fact they encode stay in alp-sdk (ADR-0017; I-26). There is no
|
|
26
|
+
template here, no SKU list, no address, no pin name -- `som.sku` is read from the
|
|
27
|
+
customer's own `board.yaml` purely to spell one directory name, exactly as the
|
|
28
|
+
oracle does.
|
|
29
|
+
|
|
30
|
+
**A fall back to spawning is never silent.** When the in-process path cannot
|
|
31
|
+
serve a checkout (not a usable checkout, unreadable emit registry, or a `tan`
|
|
32
|
+
build without `PyYAML`/`jsonschema`), the run still succeeds via the subprocess
|
|
33
|
+
-- but it says so, with a `generate.in-process-unavailable` warning naming the
|
|
34
|
+
reason and a `data.engine` that reports `subprocess` for every target.
|
|
35
|
+
`TAN_GENERATE_EXECUTOR=subprocess` pins the old path deliberately;
|
|
36
|
+
`=in-process` refuses rather than falling back, which is what makes the parity
|
|
37
|
+
suite able to assert the engine it thinks it measured.
|
|
38
|
+
|
|
39
|
+
A resolvable SDK checkout is still REQUIRED on both engines -- `metadata/**`
|
|
40
|
+
never relocated -- so without one this command refuses
|
|
41
|
+
(`generate.sdk-root-unresolved`). I-31 pins `scripts/alp_project.py` as tan's
|
|
42
|
+
SDK-root marker; the invariant that forbids shelling the SDK (I-32, and
|
|
43
|
+
anti-pattern 22 beside it) is scoped to `tan init`/`tan scaffold`, which read a
|
|
44
|
+
VENDORED `--emit scaffold` tree so they work with no checkout at all.
|
|
45
|
+
|
|
46
|
+
**Every failure path is an envelope, never a traceback.** A missing or
|
|
47
|
+
unreadable `board.yaml`, an unresolved SDK, a bad `--target`/`--core` pairing, a
|
|
48
|
+
`native_sim` overlay that would be truncated, an interpreter too old to run the
|
|
49
|
+
SDK scripts, a spawn that dies, returns garbage, or exits 0 without producing
|
|
50
|
+
its artefact (tan-cli#397) -- each has a
|
|
51
|
+
`generate.<code>` issue and an exit code below, and the command carries a
|
|
52
|
+
catch-all backstop for anything unforeseen. The extension parses stdout whole:
|
|
53
|
+
a traceback there renders as nothing at all, with no error.
|
|
54
|
+
|
|
55
|
+
The in-process engine WIDENS that surface, which is why `_emit_one_in_process`
|
|
56
|
+
catches as broadly as it does: a planner blow-up that used to die inside a
|
|
57
|
+
subprocess and arrive as an exit code now propagates into this process, and
|
|
58
|
+
`tan.planner.loader` reports a missing dependency by calling `sys.exit` -- a
|
|
59
|
+
`SystemExit`, which no `except Exception` would stop.
|
|
60
|
+
|
|
61
|
+
Exit codes, verbatim from the oracle: missing board / unresolved SDK / bad
|
|
62
|
+
target / unresolvable SKU are ValidationFailure (2); a too-old interpreter is
|
|
63
|
+
RuntimeFailure (1); any target whose emit failed is WriteFailure (3), as is
|
|
64
|
+
the overlay guard for an EXPLICIT `--target native-sim-overlay` (tan-cli#457's
|
|
65
|
+
DELIBERATE divergence: the oracle's own is existence-only and refuses a re-run
|
|
66
|
+
forever; ours is content-aware) -- but SUCCESS (0), dropped with a warning
|
|
67
|
+
instead, when it only rides along in the bare/`--all` default set (tan-cli#501).
|
|
68
|
+
|
|
69
|
+
**`--output` exists because CMake, not tan, decides where a Zephyr build reads
|
|
70
|
+
from.** Today, 98 of alp-sdk's example `CMakeLists.txt` files (of 167 total)
|
|
71
|
+
shell `${ALP_SDK_ROOT}/scripts/alp_project.py --input <board.yaml> --emit
|
|
72
|
+
zephyr-conf` directly at configure time, and 86 of those 98 read the result
|
|
73
|
+
from `${CMAKE_BINARY_DIR}/generated/alp.conf` (the other 12 read it from a
|
|
74
|
+
`CMAKE_CURRENT_BINARY_DIR`-relative path instead); `CMAKE_BINARY_DIR` is NOT the
|
|
75
|
+
project directory (an in-tree `west build examples/<...>` puts it under the
|
|
76
|
+
repo root). Without `--output`, `tan` would emit into
|
|
77
|
+
`<project>/build/generated/` and that build would read nothing. PLANNED, not
|
|
78
|
+
yet merged into any released alp-sdk or its `dev`/`main` (tan-cli#825): a
|
|
79
|
+
shared `cmake/alp.cmake` helper that replaces the 98 copy-pasted calls and
|
|
80
|
+
PROBES `generate --help` for `--output`, falling back to shelling
|
|
81
|
+
`scripts/alp_project.py` when it is absent. So the flag is what will make
|
|
82
|
+
`tan` the emitter for every example once that helper ships, without changing
|
|
83
|
+
anything that already works -- and it is already useful today for pointing
|
|
84
|
+
`tan generate` at a build dir that is not `<project>/build/`.
|
|
85
|
+
|
|
86
|
+
`--output` writes a FILE. It never puts the file's contents on stdout: stdout
|
|
87
|
+
carries the envelope and nothing else, in both formats.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from __future__ import annotations
|
|
91
|
+
|
|
92
|
+
import contextlib
|
|
93
|
+
import os
|
|
94
|
+
import subprocess
|
|
95
|
+
import sys
|
|
96
|
+
from dataclasses import dataclass
|
|
97
|
+
from pathlib import Path
|
|
98
|
+
|
|
99
|
+
import typer
|
|
100
|
+
|
|
101
|
+
# One home each for two decisions this command shares with the rest of the CLI:
|
|
102
|
+
# where an alp-sdk checkout is (I-31's marker, spelled once in `build_cmd`), and
|
|
103
|
+
# which interpreter name the SDK's own scripts run under -- a PATH name, never
|
|
104
|
+
# `sys.executable`, which is `tan` itself once PyInstaller has frozen it.
|
|
105
|
+
from tan import planner_emit
|
|
106
|
+
from tan.commands.sdk_cmd import NO_SDK_NEXT_STEPS
|
|
107
|
+
from tan.core.sdk_discovery import with_sdk_search
|
|
108
|
+
from tan.commands.doctor_cmd import probe, resolve_manifest_python_floor
|
|
109
|
+
from tan.core.fs_confine import PathEscapeError, resolve_confined
|
|
110
|
+
from tan.core.global_flags import accept_global_flags
|
|
111
|
+
from tan.core.link_refusal import refusal_code, split_coded_message
|
|
112
|
+
from tan.core.scaffold import top_level_key_name
|
|
113
|
+
from tan.core.sdk_discovery import (
|
|
114
|
+
_planner_python,
|
|
115
|
+
global_default_foreign_project_issue,
|
|
116
|
+
project_pin_issue,
|
|
117
|
+
resolve_sdk_root_wide,
|
|
118
|
+
sdk_ladder_divergence_issue,
|
|
119
|
+
)
|
|
120
|
+
from tan.core.shapes import rejected_sdk_root_message
|
|
121
|
+
from tan.core.subprocess_env import spawn_env
|
|
122
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
123
|
+
from tan.exit_codes import ExitCode
|
|
124
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
125
|
+
|
|
126
|
+
#: `data.schemaVersion` for this command's payload.
|
|
127
|
+
DATA_SCHEMA_VERSION = "1"
|
|
128
|
+
|
|
129
|
+
#: Pin the engine instead of letting `auto` decide. An env var, not a flag:
|
|
130
|
+
#: the PLANNED `cmake/alp.cmake` (unmerged, tan-cli#825) will build one argv
|
|
131
|
+
#: that must stay accepted verbatim, and this is an escape hatch for a host
|
|
132
|
+
#: where the in-process path misbehaves plus the seam the parity suite uses
|
|
133
|
+
#: to compare both engines against the same oracle.
|
|
134
|
+
#:
|
|
135
|
+
#: `subprocess` spawns alp-sdk for every target (the pre-relocation behaviour).
|
|
136
|
+
#: `in-process` refuses instead of falling back, so a run cannot quietly measure
|
|
137
|
+
#: the engine it was not testing.
|
|
138
|
+
EXECUTOR_ENV = "TAN_GENERATE_EXECUTOR"
|
|
139
|
+
EXECUTORS = ("auto", "in-process", "subprocess")
|
|
140
|
+
|
|
141
|
+
#: Every emit mode a bare `tan generate` / `--all` runs, in the order
|
|
142
|
+
#: `data.targets` reports them, mapped to its output path relative to the
|
|
143
|
+
#: workspace root. ONE structure, deliberately: the Rust carries the mode list
|
|
144
|
+
#: and the path table separately (`tan_core::ALL_EMIT_MODES` plus
|
|
145
|
+
#: `GENERATION_TARGET_CATALOG`) and tan-cli#165 exists because those two drifted
|
|
146
|
+
#: apart silently, the catalog having grown only four of the nine.
|
|
147
|
+
#:
|
|
148
|
+
#: The literals are `/`-separated because they are SDK conventions, not host
|
|
149
|
+
#: paths -- `_output_path` folds them component-wise so a Windows run cannot
|
|
150
|
+
#: emit `build/generated/alp.conf` beside `build\boards\<dir>` in one envelope
|
|
151
|
+
#: (tan-cli#165 review finding 4).
|
|
152
|
+
#:
|
|
153
|
+
#: `native-sim-overlay` is the one entry outside `build/generated/`: Zephyr
|
|
154
|
+
#: auto-discovers `boards/<board>.overlay` in the app's own SOURCE tree, so
|
|
155
|
+
#: `west build -b native_sim/native/64` only finds it there. That it writes into
|
|
156
|
+
#: a hand-editable tree is why `_overlay_would_overwrite` exists.
|
|
157
|
+
_OUTPUT_RELATIVE_PATH = {
|
|
158
|
+
"zephyr-conf": "build/generated/alp.conf",
|
|
159
|
+
"dts-overlay": "build/generated/alp.overlay",
|
|
160
|
+
"native-sim-overlay": "boards/native_sim_native_64.overlay",
|
|
161
|
+
"cmake-args": "build/generated/alp-cmake-args.txt",
|
|
162
|
+
"yocto-conf": "build/generated/alp-yocto.conf",
|
|
163
|
+
"carrier-netlist": "build/generated/carrier-netlist.json",
|
|
164
|
+
"west-libraries": "build/generated/alp-west-libs.yml",
|
|
165
|
+
"hw-info-h": "build/generated/alp_hw_info_build.h",
|
|
166
|
+
"os-topology": "build/generated/os-topology.json",
|
|
167
|
+
# Explicit-only (see EXPLICIT_ONLY_TARGETS) -- still in this table because
|
|
168
|
+
# each has one canonical path.
|
|
169
|
+
# `composed-route-table` is the older debug/demonstrator view of
|
|
170
|
+
# `carrier-netlist`'s own route rows -- alp-sdk's docs/cli.md lists it as
|
|
171
|
+
# "JSON route-table dump (demonstrator)", a maintainer-only tool with no
|
|
172
|
+
# product consumer. The Rust oracle's `tan_core::ALL_EMIT_MODES`
|
|
173
|
+
# (crates/tan-core/src/loader.rs) excludes it from the default/`--all`
|
|
174
|
+
# set for the same reason, so a bare `tan generate` never runs a full
|
|
175
|
+
# pad-route composition.
|
|
176
|
+
"composed-route-table": "build/generated/composed-route-table.json",
|
|
177
|
+
# `alp_sdk_ipc_contract_header()`, a helper PLANNED for alp-sdk's
|
|
178
|
+
# `cmake/alp.cmake` (unmerged, tan-cli#825), names exactly this one:
|
|
179
|
+
# `${CMAKE_BINARY_DIR}/generated/alp/system_ipc.h`.
|
|
180
|
+
"ipc-contract-h": "build/generated/alp/system_ipc.h",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#: The one target that is NOT defaultable: it hard-requires `--core` and writes
|
|
184
|
+
#: a DIRECTORY of files named per SKU+core, so a bare `tan generate` has no path
|
|
185
|
+
#: to give it. Reachable only via an explicit `--target zephyr-board --core <id>`.
|
|
186
|
+
ZEPHYR_BOARD = "zephyr-board"
|
|
187
|
+
|
|
188
|
+
#: The cross-core IPC contract header (`<alp/system_ipc.h>`), the second target
|
|
189
|
+
#: reachable only by an explicit `--target`. Not core-scoped -- the contract IS
|
|
190
|
+
#: the agreement between cores -- and not defaultable either: it renders a
|
|
191
|
+
#: board.yaml `ipc:` block, which four multicore examples have and the other 95
|
|
192
|
+
#: do not, so folding it into the default set would make a bare `tan generate`
|
|
193
|
+
#: report a failed target on almost every project.
|
|
194
|
+
IPC_CONTRACT_H = "ipc-contract-h"
|
|
195
|
+
|
|
196
|
+
#: The pad-route debug/demonstrator view (see `_OUTPUT_RELATIVE_PATH`'s
|
|
197
|
+
#: comment). Not defaultable: it is a maintainer-only tool with no product
|
|
198
|
+
#: consumer, so a bare `tan generate` must not pay for a full pad-route
|
|
199
|
+
#: composition on every project -- a prior revision listed this in
|
|
200
|
+
#: `_OUTPUT_RELATIVE_PATH` without also listing it here, which silently grew
|
|
201
|
+
#: the default set from 9 targets to 10.
|
|
202
|
+
COMPOSED_ROUTE_TABLE = "composed-route-table"
|
|
203
|
+
|
|
204
|
+
#: Targets an explicit `--target` can reach but the default/`--all` set never
|
|
205
|
+
#: does. `zephyr-board` is absent from `_OUTPUT_RELATIVE_PATH` (it writes a
|
|
206
|
+
#: directory, named per SKU+core); `composed-route-table` and
|
|
207
|
+
#: `ipc-contract-h` are both in it.
|
|
208
|
+
EXPLICIT_ONLY_TARGETS = frozenset(
|
|
209
|
+
{ZEPHYR_BOARD, COMPOSED_ROUTE_TABLE, IPC_CONTRACT_H}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
#: The default/`--all` target set. Derived, so it cannot disagree with the path
|
|
213
|
+
#: table above.
|
|
214
|
+
ALL_EMIT_MODES = tuple(
|
|
215
|
+
mode for mode in _OUTPUT_RELATIVE_PATH if mode not in EXPLICIT_ONLY_TARGETS
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
#: Targets `--core` optionally SCOPES, beyond `zephyr-board` which requires it.
|
|
219
|
+
#: Verbatim from `alp_project.py`'s own `--core` help. `carrier-netlist`,
|
|
220
|
+
#: `composed-route-table` and `native-sim-overlay` are excluded because the SDK
|
|
221
|
+
#: never reads `--core` for them at all, and `os-topology` because it accepts
|
|
222
|
+
#: the flag and then prints `--core is ignored for --emit os-topology
|
|
223
|
+
#: (project-level emit)` and ignores it -- the same "does nothing" shape, so all
|
|
224
|
+
#: four are refused rather than silently accepted.
|
|
225
|
+
CORE_SCOPABLE_TARGETS = (
|
|
226
|
+
"zephyr-conf",
|
|
227
|
+
"yocto-conf",
|
|
228
|
+
"cmake-args",
|
|
229
|
+
"dts-overlay",
|
|
230
|
+
"west-libraries",
|
|
231
|
+
"hw-info-h",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
#: Seconds a single `alp_project.py` emit may take. Generous: a cold emit
|
|
235
|
+
#: imports the whole orchestrator package and reads every metadata file for the
|
|
236
|
+
#: SKU. Bounded regardless, so a planner that wedges cannot hang a `--format
|
|
237
|
+
#: json` consumer with no envelope and no error -- the failure mode a missing
|
|
238
|
+
#: timeout produces is indistinguishable from a slow build.
|
|
239
|
+
EMIT_TIMEOUT_S = 300
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class GenerateError(Exception):
|
|
243
|
+
"""A refusal whose issue code and exit code are already decided.
|
|
244
|
+
|
|
245
|
+
`extra_issues` (tan-cli#900) are prepended ahead of the refusal's own
|
|
246
|
+
`Issue` by `refuse()` -- the `[*resolution_issues, Issue(...)]` shape
|
|
247
|
+
`clean_cmd._run` already uses. `None` by default: only the SDK-root-
|
|
248
|
+
unresolved raise below needs it (a broken pin surviving a refusal that
|
|
249
|
+
precedes resolution); every other raise here fires after `sdk_broken_pin`
|
|
250
|
+
is already known and reported through the success-path `issues` instead.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
code: str,
|
|
256
|
+
message: str,
|
|
257
|
+
exit_code: ExitCode,
|
|
258
|
+
extra_issues: list[Issue] | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
super().__init__(message)
|
|
261
|
+
self.code = code
|
|
262
|
+
self.message = message
|
|
263
|
+
self.exit_code = exit_code
|
|
264
|
+
self.extra_issues = extra_issues
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def zephyr_board_dir_name(sku: str, core_id: str) -> str | None:
|
|
268
|
+
"""`som.sku` + core id -> the SDK's `--emit zephyr-board` directory name:
|
|
269
|
+
`E1M-AEN801` + `m55_hp` -> `alp_e1m_aen801_m55_hp`.
|
|
270
|
+
|
|
271
|
+
`alp_project.py` keys every generated file under exactly this name and
|
|
272
|
+
strips it before joining onto `--output`, so `--output` must BE this
|
|
273
|
+
directory. A bare core id would collide across two SoMs that share one
|
|
274
|
+
(tan-cli#116 review finding 1) -- a project retargeted from E1M-AEN901 to
|
|
275
|
+
E1M-AEN801 would land each run's files beside the other SoM's stale ones.
|
|
276
|
+
|
|
277
|
+
`None` on any other prefix, mirroring the SDK's own `unrecognised SKU
|
|
278
|
+
prefix` guard. This is a naming convention, not a SKU list: no SKU is
|
|
279
|
+
enumerated anywhere in tan.
|
|
280
|
+
"""
|
|
281
|
+
if not sku.startswith("E1M-"):
|
|
282
|
+
return None
|
|
283
|
+
return f"alp_e1m_{sku[len('E1M-'):].lower()}_{core_id}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _board_sku(path: Path) -> str | None:
|
|
287
|
+
"""`som.sku` out of `board.yaml`, or `None` for every way that can fail.
|
|
288
|
+
|
|
289
|
+
`None` is not an error here -- the caller turns it into
|
|
290
|
+
`generate.board-sku-unresolved`, which is exactly what the oracle's
|
|
291
|
+
`read_to_string(..).ok().and_then(..)` chain produces for an absent,
|
|
292
|
+
unreadable, non-UTF-8 or wrong-shaped file. So a bad board.yaml is a
|
|
293
|
+
refusal with a code, never a traceback.
|
|
294
|
+
|
|
295
|
+
PyYAML when it is importable (a Zephyr workspace needs it anyway), else a
|
|
296
|
+
two-line scan for the one nested key this needs. tan ships no YAML
|
|
297
|
+
dependency, and the same reasoning as `validate_cmd._load_yaml` applies:
|
|
298
|
+
answer only the question actually asked.
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
text = path.read_text(encoding="utf-8")
|
|
302
|
+
except (OSError, UnicodeDecodeError):
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
import yaml # noqa: PLC0415 (optional at runtime, by design)
|
|
307
|
+
except ImportError:
|
|
308
|
+
return _scan_som_sku(text)
|
|
309
|
+
try:
|
|
310
|
+
doc = yaml.safe_load(text)
|
|
311
|
+
except Exception: # noqa: BLE001 -- yaml.YAMLError and anything a loader raises
|
|
312
|
+
return None
|
|
313
|
+
if not isinstance(doc, dict):
|
|
314
|
+
return None
|
|
315
|
+
som = doc.get("som")
|
|
316
|
+
if not isinstance(som, dict):
|
|
317
|
+
return None
|
|
318
|
+
sku = som.get("sku")
|
|
319
|
+
return sku if isinstance(sku, str) and sku else None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _scan_som_sku(text: str) -> str | None:
|
|
323
|
+
"""The no-PyYAML fallback: the `sku:` scalar inside the top-level `som:`
|
|
324
|
+
block. Deliberately not a YAML parser -- it answers one question.
|
|
325
|
+
|
|
326
|
+
tan-cli#1008 review round 5: the top-level-key check is
|
|
327
|
+
`tan.core.scaffold.top_level_key_name`, shared with
|
|
328
|
+
`bootstrap_cmd._scan_board_slice` and `scaffold._is_som_key_line`
|
|
329
|
+
rather than a fourth local copy of the same rule."""
|
|
330
|
+
inside = False
|
|
331
|
+
for raw in text.splitlines():
|
|
332
|
+
stripped = raw.strip()
|
|
333
|
+
if not stripped or stripped.startswith("#"):
|
|
334
|
+
continue
|
|
335
|
+
if not raw[:1].isspace():
|
|
336
|
+
inside = top_level_key_name(stripped) == "som"
|
|
337
|
+
continue
|
|
338
|
+
if inside:
|
|
339
|
+
key, sep, value = stripped.partition(":")
|
|
340
|
+
if sep and key.strip() == "sku":
|
|
341
|
+
return value.strip().strip("'\"") or None
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def resolve_targets(
|
|
346
|
+
target: str | None, all_targets: bool, core: str | None
|
|
347
|
+
) -> tuple[str, ...]:
|
|
348
|
+
"""Which emit modes to run. Raises `GenerateError` for every user-supplied
|
|
349
|
+
argument-shape mistake.
|
|
350
|
+
|
|
351
|
+
ValidationFailure, never InternalFailure: an unknown `--target` or a
|
|
352
|
+
`--core` paired with a target that does not consume it is an ordinary usage
|
|
353
|
+
mistake, and it used to surface identically to a genuine tan bug
|
|
354
|
+
(tan-cli#117 review finding 3). `--core` is refused rather than silently
|
|
355
|
+
ignored -- silently ignoring it lets a user believe it scoped something.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def invalid(message: str) -> GenerateError:
|
|
359
|
+
return GenerateError(
|
|
360
|
+
"generate.invalid-target", message, ExitCode.VALIDATION_FAILURE
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if all_targets and target is not None:
|
|
364
|
+
# tan-cli#498 defect 5. `--all` and `--target` name two different
|
|
365
|
+
# target sets, and `--all` used to win silently at the `return
|
|
366
|
+
# ALL_EMIT_MODES` below -- so `--all --target zephyr-board` DISCARDED
|
|
367
|
+
# the target the user named (`zephyr-board` is not in ALL_EMIT_MODES,
|
|
368
|
+
# so `build/boards/` was never created) with no field in the envelope
|
|
369
|
+
# recording the discard. It also opened the hole the `--core` guard
|
|
370
|
+
# below was written to close: with `target == ZEPHYR_BOARD` the guard
|
|
371
|
+
# accepted a `--core` that then narrowed six of the nine default emits
|
|
372
|
+
# (measured on `examples/bringup/board-selftest`:
|
|
373
|
+
# `build/generated/alp.conf` 3799 -> 1867 bytes, the a32_cluster slice
|
|
374
|
+
# and the `# --- core: ... ---` markers gone), while the run ended exit
|
|
375
|
+
# 3 blaming `yocto-conf` for an error the flag COMBINATION caused.
|
|
376
|
+
#
|
|
377
|
+
# The frozen oracle drops the target just as silently (measured: `--all
|
|
378
|
+
# --target zephyr-conf` returns the full default set at exit 0), and no
|
|
379
|
+
# golden pins the combination -- so this refusal is a deliberate,
|
|
380
|
+
# unpinned divergence. Refusing beats picking a winner: either choice
|
|
381
|
+
# ignores half of what was typed.
|
|
382
|
+
raise invalid(
|
|
383
|
+
f"`--all` and `--target {target}` name different target sets; pass one "
|
|
384
|
+
"or the other. `--all` (the default set) runs "
|
|
385
|
+
f"{', '.join(ALL_EMIT_MODES)}."
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if core is not None:
|
|
389
|
+
# `not all_targets` on BOTH arms since #498: with the guard above in
|
|
390
|
+
# front, `all_targets` implies `target is None`, so the qualifier is
|
|
391
|
+
# belt-and-braces -- but its absence on the `ZEPHYR_BOARD` arm is
|
|
392
|
+
# precisely what let `--all --target zephyr-board --core <id>` past a
|
|
393
|
+
# message whose own text says `--core` "does nothing for the
|
|
394
|
+
# default/--all target set".
|
|
395
|
+
core_is_valid_here = not all_targets and (
|
|
396
|
+
target == ZEPHYR_BOARD or target in CORE_SCOPABLE_TARGETS
|
|
397
|
+
)
|
|
398
|
+
if not core_is_valid_here:
|
|
399
|
+
raise invalid(
|
|
400
|
+
"`--core` is only valid with `--target zephyr-board` (required), or "
|
|
401
|
+
f"one of {', '.join(CORE_SCOPABLE_TARGETS)} (optional scoping); it "
|
|
402
|
+
"does nothing for the default/--all target set."
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if all_targets or target is None:
|
|
406
|
+
return ALL_EMIT_MODES
|
|
407
|
+
|
|
408
|
+
if target == ZEPHYR_BOARD:
|
|
409
|
+
if core is None:
|
|
410
|
+
raise invalid(
|
|
411
|
+
"`--target zephyr-board` requires `--core <id>` (it generates one "
|
|
412
|
+
"core's Zephyr board tree)."
|
|
413
|
+
)
|
|
414
|
+
return (ZEPHYR_BOARD,)
|
|
415
|
+
|
|
416
|
+
if target in _OUTPUT_RELATIVE_PATH:
|
|
417
|
+
return (target,)
|
|
418
|
+
|
|
419
|
+
raise invalid(f"Unsupported generate target '{target}'.")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _output_path(
|
|
423
|
+
workspace_root: Path, emit: str, board_dir_name: str | None
|
|
424
|
+
) -> Path:
|
|
425
|
+
"""Where `emit`'s output goes, with native separators throughout.
|
|
426
|
+
|
|
427
|
+
`zephyr-board` is the one target with no literal path: it writes a
|
|
428
|
+
directory per SKU+core under `build/boards/`, so
|
|
429
|
+
`west build --board-root build/boards` finds every generated board tree
|
|
430
|
+
without them colliding.
|
|
431
|
+
"""
|
|
432
|
+
if emit == ZEPHYR_BOARD:
|
|
433
|
+
# `run` resolves the name before reaching this target (refusing
|
|
434
|
+
# otherwise), so the fallback is only for a direct call.
|
|
435
|
+
return workspace_root / "build" / "boards" / (board_dir_name or "board")
|
|
436
|
+
path = workspace_root
|
|
437
|
+
for part in _OUTPUT_RELATIVE_PATH[emit].split("/"):
|
|
438
|
+
path = path / part
|
|
439
|
+
return path
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _resolve_output_override(
|
|
443
|
+
output: str, targets: tuple[str, ...], workspace_root: Path
|
|
444
|
+
) -> Path:
|
|
445
|
+
"""`--output` as an absolute path. Touches nothing on disk.
|
|
446
|
+
|
|
447
|
+
`--output` names ONE destination, so it needs exactly one target: paired
|
|
448
|
+
with the default set (or `--all`) it would silently mean "overwrite this
|
|
449
|
+
file eight times", the last emit winning. That is a usage mistake, so it is
|
|
450
|
+
ValidationFailure with the same `generate.invalid-target` code the other
|
|
451
|
+
argument-shape refusals carry.
|
|
452
|
+
"""
|
|
453
|
+
if len(targets) != 1:
|
|
454
|
+
raise GenerateError(
|
|
455
|
+
"generate.invalid-target",
|
|
456
|
+
"`--output` names a single destination, so it requires exactly one "
|
|
457
|
+
"`--target <mode>`; it cannot serve the default/--all target set.",
|
|
458
|
+
ExitCode.VALIDATION_FAILURE,
|
|
459
|
+
)
|
|
460
|
+
candidate = Path(output)
|
|
461
|
+
return candidate if candidate.is_absolute() else workspace_root / candidate
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _ensure_writable(output: Path, target: str) -> bool:
|
|
465
|
+
"""Create `output`'s parent and prove the destination is writable.
|
|
466
|
+
|
|
467
|
+
Returns True when the probe ITSELF created the destination file -- which it
|
|
468
|
+
then REMOVES again before returning, so nothing tan invented is on disk
|
|
469
|
+
when the emitter runs.
|
|
470
|
+
|
|
471
|
+
Done HERE rather than discovered when the spawned emitter trips over it, for
|
|
472
|
+
two reasons: a `PermissionError` traceback on the SDK's stderr is a terrible
|
|
473
|
+
issue message, and the parent directory has to be created either way --
|
|
474
|
+
real example `CMakeLists.txt` today ask for
|
|
475
|
+
`${CMAKE_BINARY_DIR}/generated/alp.conf` (and the PLANNED
|
|
476
|
+
`alp_sdk_zephyr_conf()`, unmerged, tan-cli#825, would do the same) in
|
|
477
|
+
a build tree where `generated/` does not exist yet.
|
|
478
|
+
|
|
479
|
+
**The unlink is tan-cli#498 defect 4's fix, and it is structural.** The
|
|
480
|
+
probe used to LEAVE its zero-byte file behind, which made "the file exists"
|
|
481
|
+
useless as proof that the emitter wrote anything -- so
|
|
482
|
+
`_missing_emit_output` demanded a NON-EMPTY file instead, and then failed
|
|
483
|
+
every emit that legitimately produces zero bytes. Removing the probe's own
|
|
484
|
+
artefact restores existence as honest evidence. It also retires the window
|
|
485
|
+
tan-cli#420 patched from the other end: no stray zero-byte
|
|
486
|
+
`build/generated/alp.conf` survives for a later standalone `west build` to
|
|
487
|
+
pick up as a valid (and silently empty) `EXTRA_CONF_FILE`.
|
|
488
|
+
|
|
489
|
+
It cannot cost a pre-existing file: `existed` is read BEFORE the `open`,
|
|
490
|
+
and a file that was already there is never touched (`"ab"` does not
|
|
491
|
+
truncate).
|
|
492
|
+
|
|
493
|
+
`ValueError` is caught beside `OSError` deliberately: a path carrying a
|
|
494
|
+
surrogate or an embedded NUL raises `UnicodeEncodeError`/`ValueError`, not
|
|
495
|
+
`OSError`, and letting one reach the command's catch-all backstop would
|
|
496
|
+
report an unwritable path as InternalFailure (5) instead of WriteFailure (3).
|
|
497
|
+
"""
|
|
498
|
+
try:
|
|
499
|
+
if target == ZEPHYR_BOARD:
|
|
500
|
+
# `--emit zephyr-board`'s `--output` IS the board directory.
|
|
501
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
502
|
+
return False
|
|
503
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
# Checked BEFORE the open, which is what creates it. Afterwards there
|
|
505
|
+
# is no way left to tell tan's probe from a file the user already had.
|
|
506
|
+
existed = output.exists()
|
|
507
|
+
# Append mode, not write: proving writability must not truncate a
|
|
508
|
+
# file whose emit could still be refused further down.
|
|
509
|
+
with output.open("ab"):
|
|
510
|
+
pass
|
|
511
|
+
if not existed:
|
|
512
|
+
# Best-effort: on the platform where this could fail (another
|
|
513
|
+
# process grabbed the name between the open and here) the emit is
|
|
514
|
+
# about to overwrite it anyway, and refusing a run over a
|
|
515
|
+
# successful writability proof would be the worse answer.
|
|
516
|
+
with contextlib.suppress(OSError):
|
|
517
|
+
output.unlink()
|
|
518
|
+
return True
|
|
519
|
+
return False
|
|
520
|
+
except (OSError, ValueError) as err:
|
|
521
|
+
raise GenerateError(
|
|
522
|
+
"generate.output-unwritable",
|
|
523
|
+
f"--output path `{output}` cannot be written: {err}",
|
|
524
|
+
ExitCode.WRITE_FAILURE,
|
|
525
|
+
) from err
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _discard_probe_file(output: Path) -> None:
|
|
529
|
+
"""Remove a zero-byte destination file left behind by a REFUSED emit at a
|
|
530
|
+
path this run created (tan-cli#420).
|
|
531
|
+
|
|
532
|
+
Narrower since tan-cli#498 defect 4 moved the probe's own cleanup into
|
|
533
|
+
`_ensure_writable` (which now unlinks what it created immediately, rather
|
|
534
|
+
than leaving it for this): what is left for this function is the emitter
|
|
535
|
+
that opened the destination, wrote nothing, and then failed. Kept for
|
|
536
|
+
exactly that, and still correct for it.
|
|
537
|
+
|
|
538
|
+
The envelope was always honest about this -- exit 3, `data.written == []`,
|
|
539
|
+
`generate.emit-failed` -- so what is fixed here is the DISK, not the
|
|
540
|
+
report. The file that survives a refused run is a zero-byte
|
|
541
|
+
`build/generated/alp.conf`, and the real example `CMakeLists.txt` hands
|
|
542
|
+
exactly that path to Zephyr as `EXTRA_CONF_FILE` (the PLANNED
|
|
543
|
+
`cmake/alp.cmake`, unmerged, tan-cli#825, would do the same once it
|
|
544
|
+
ships). Zephyr does not treat an empty conf file
|
|
545
|
+
as an error: it applies no configuration and says nothing. So the stray
|
|
546
|
+
turns a loud failure into a later build that silently carries none of the
|
|
547
|
+
project's alp config.
|
|
548
|
+
|
|
549
|
+
Within tan's own flow the refusal stops the build, which is why leaving it
|
|
550
|
+
was defensible. It is not the only flow: standalone `west build` against a
|
|
551
|
+
tree where a `tan generate` failed earlier reaches that configure with
|
|
552
|
+
nobody re-running generate in between, and hand-written firmware bypassing
|
|
553
|
+
the tan-driven path is first-class in this SDK.
|
|
554
|
+
|
|
555
|
+
Zero-byte is re-checked at unlink time rather than trusted from the probe:
|
|
556
|
+
between the probe and the refusal an emitter may have written a partial
|
|
557
|
+
file, and a partial artefact is evidence for the user to look at, not
|
|
558
|
+
litter to remove. Cleanup failure is swallowed -- the run has already
|
|
559
|
+
failed for a reason worth reporting, and "could not tidy up" must not
|
|
560
|
+
replace it.
|
|
561
|
+
"""
|
|
562
|
+
with contextlib.suppress(OSError):
|
|
563
|
+
if output.is_file() and output.stat().st_size == 0:
|
|
564
|
+
output.unlink()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _confine_default_outputs(
|
|
568
|
+
workspace_root: Path, targets: tuple[str, ...], board_dir_name: str | None
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Refuse the WHOLE run, before any target writes, if any DEFAULT output
|
|
571
|
+
path -- i.e. `--output` was not given -- would resolve outside the
|
|
572
|
+
project after symlink resolution (tan-cli#325): a pre-existing directory
|
|
573
|
+
symlink at, say, `<project>/build` used to carry `_output_path`'s literal
|
|
574
|
+
join to wherever that symlink pointed, while the envelope still reported
|
|
575
|
+
the logical in-project path (`build/generated/alp.conf`) as written.
|
|
576
|
+
|
|
577
|
+
`--output` is exempt on purpose -- it names the caller's OWN chosen
|
|
578
|
+
destination (see the module docstring's `--output` section: CMake, not
|
|
579
|
+
tan, decides where a Zephyr build reads from), so an explicit path there
|
|
580
|
+
is honoured as typed, not treated as an accidental escape.
|
|
581
|
+
"""
|
|
582
|
+
for mode in targets:
|
|
583
|
+
candidate = _output_path(workspace_root, mode, board_dir_name)
|
|
584
|
+
try:
|
|
585
|
+
resolve_confined(workspace_root, candidate)
|
|
586
|
+
except (PathEscapeError, OSError, ValueError) as err:
|
|
587
|
+
raise GenerateError(
|
|
588
|
+
"generate.write-escapes-project",
|
|
589
|
+
f"target '{mode}' would write to '{candidate}', which resolves "
|
|
590
|
+
f"outside the project root '{workspace_root}' ({err}); refusing "
|
|
591
|
+
"to generate any target for this run.",
|
|
592
|
+
ExitCode.WRITE_FAILURE,
|
|
593
|
+
) from err
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
_OVERLAY_BANNER = "--emit native-sim-overlay -- do not edit by hand."
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _overlay_would_overwrite(
|
|
600
|
+
workspace_root: Path,
|
|
601
|
+
targets: tuple[str, ...],
|
|
602
|
+
force: bool,
|
|
603
|
+
output_override: Path | None = None,
|
|
604
|
+
) -> bool:
|
|
605
|
+
"""True when this run would truncate a HAND-EDITED (or otherwise foreign)
|
|
606
|
+
`boards/native_sim_native_64.overlay` -- content-aware (like `init`/`scaffold`'s
|
|
607
|
+
`collect_file_changes`): a file lacking `_OVERLAY_BANNER` (`native_sim.py:87-88`'s
|
|
608
|
+
own) trips it. `output_override` replaces the default path when `--output`
|
|
609
|
+
redirected the emit.
|
|
610
|
+
|
|
611
|
+
Whether that turns into a whole-run refusal or a per-run target drop is the
|
|
612
|
+
CALLER's decision (tan-cli#501): an explicit `--target native-sim-overlay`
|
|
613
|
+
still refuses without `--force` (tan-cli#457); `native-sim-overlay` riding
|
|
614
|
+
along in the bare/`--all` default set instead drops the target and reports
|
|
615
|
+
an issue, so the caller always passes `force=False` here first to ask "is
|
|
616
|
+
this foreign at all" before deciding what to do about it.
|
|
617
|
+
"""
|
|
618
|
+
if force or "native-sim-overlay" not in targets:
|
|
619
|
+
return False
|
|
620
|
+
destination = output_override or _output_path(
|
|
621
|
+
workspace_root, "native-sim-overlay", None
|
|
622
|
+
)
|
|
623
|
+
try:
|
|
624
|
+
return destination.exists() and _OVERLAY_BANNER not in destination.read_text(
|
|
625
|
+
encoding="utf-8"
|
|
626
|
+
)
|
|
627
|
+
except (OSError, UnicodeDecodeError):
|
|
628
|
+
return True
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _python_too_old(python: str, floor: tuple[int, int]) -> str | None:
|
|
632
|
+
"""A message when `python` is below the SDK's floor, else `None`.
|
|
633
|
+
|
|
634
|
+
`floor` is the resolved SDK's OWN declared floor -- they use
|
|
635
|
+
`@dataclass(slots=True)`, so `resolve_manifest_python_floor` (I-24) reads
|
|
636
|
+
`<sdk>/metadata/bootstrap.json`'s `pythonMinVersion` live rather than a
|
|
637
|
+
second hardcoded 3.10 that could drift from the manifest's, or from
|
|
638
|
+
`model_cmd`'s own copy. `None` also covers "could not tell" -- a missing or
|
|
639
|
+
broken interpreter is a different failure the real spawn surfaces on its
|
|
640
|
+
own, and blocking on an unknown would refuse a perfectly good host. Bounded
|
|
641
|
+
by `probe`'s timeout, which is why this reuses it rather than spawning by
|
|
642
|
+
hand.
|
|
643
|
+
"""
|
|
644
|
+
out = probe([python, "-c", "import sys;print('%d.%d' % sys.version_info[:2])"])
|
|
645
|
+
if out is None:
|
|
646
|
+
return None
|
|
647
|
+
try:
|
|
648
|
+
major, minor = (int(part) for part in out.strip().splitlines()[-1].split(".")[:2])
|
|
649
|
+
except (IndexError, ValueError):
|
|
650
|
+
return None
|
|
651
|
+
if (major, minor) >= floor:
|
|
652
|
+
return None
|
|
653
|
+
return (
|
|
654
|
+
f"Python {major}.{minor} found at `{python}`, but alp-sdk requires Python "
|
|
655
|
+
f"{floor[0]}.{floor[1]}+. Put a newer `python` first on PATH "
|
|
656
|
+
"(VS Code users can instead set alpSdk.pythonPath)."
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _relative_or_full(workspace_root: Path, output_path: Path) -> str:
|
|
661
|
+
try:
|
|
662
|
+
return str(output_path.relative_to(workspace_root))
|
|
663
|
+
except ValueError:
|
|
664
|
+
return str(output_path)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _output_stamp(output: Path) -> tuple[int, int, int] | None:
|
|
668
|
+
"""`(st_ino, st_size, st_mtime_ns)` for `output`, or `None` when it does
|
|
669
|
+
not exist (or cannot be stat'ed).
|
|
670
|
+
|
|
671
|
+
Taken immediately before a spawned emit and compared immediately after, so
|
|
672
|
+
`_missing_emit_output` can tell "the emitter wrote a legitimately empty
|
|
673
|
+
file" from "the emitter never touched a file that was already there" --
|
|
674
|
+
the one case where mere existence is no evidence at all. Three fields
|
|
675
|
+
rather than one: a rewrite that lands on the same size and the same
|
|
676
|
+
coarse timestamp still moves the inode on any emitter that writes through
|
|
677
|
+
a temp file + rename, and a same-inode rewrite moves `st_mtime_ns`.
|
|
678
|
+
|
|
679
|
+
Known limit, and the direction it errs in ON PURPOSE: on a filesystem with
|
|
680
|
+
coarse timestamps (FAT's 2s, HFS+'s 1s -- not ext4/APFS/NTFS, where this
|
|
681
|
+
runs), an in-place rewrite of byte-identical content inside one tick is
|
|
682
|
+
indistinguishable from no write at all, and would be reported as a failed
|
|
683
|
+
emit. That is the safe side of the trade: a false FAILURE is loud, costs a
|
|
684
|
+
re-run and destroys nothing, while the false SUCCESS it replaces put a
|
|
685
|
+
previous board's `alp.conf` into the next `tan build` silently.
|
|
686
|
+
"""
|
|
687
|
+
try:
|
|
688
|
+
stat = output.stat()
|
|
689
|
+
except (OSError, ValueError):
|
|
690
|
+
return None
|
|
691
|
+
return (stat.st_ino, stat.st_size, stat.st_mtime_ns)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _missing_emit_output(
|
|
695
|
+
emit: str, output: Path, before: tuple[int, int, int] | None = None
|
|
696
|
+
) -> str | None:
|
|
697
|
+
"""Why a ZERO exit did not actually produce `output`, or `None` when it did.
|
|
698
|
+
|
|
699
|
+
tan-cli#397: `_emit_one` used to read `returncode == 0` as proof that a file
|
|
700
|
+
landed, and `written[]` was built from nothing else. A checkout whose
|
|
701
|
+
`scripts/alp_project.py` exits 0 down a path tan did not expect -- a
|
|
702
|
+
partially installed or partially cloned alp-sdk, a differently configured
|
|
703
|
+
emitter, a future SDK version -- then produced `ok: true`, exit 0,
|
|
704
|
+
`failed: []` and a `written[]` naming a file that is not on disk. The next
|
|
705
|
+
`tan build` configures Zephyr against a stale `alp.conf` or none at all, and
|
|
706
|
+
the symptom surfaces much later as wrong Kconfig in a built image with
|
|
707
|
+
nothing in the generate envelope pointing back at the missing artefact.
|
|
708
|
+
alp-sdk-vscode believes `written[]` outright -- it is the only signal the
|
|
709
|
+
extension has for what a generate produced, so it performs no existence
|
|
710
|
+
check of its own.
|
|
711
|
+
|
|
712
|
+
PRESENT, not non-empty -- **tan-cli#498 defect 4 corrected this.** The check
|
|
713
|
+
used to demand a non-zero size, for a reason true at the time (with
|
|
714
|
+
`--output`, `_ensure_writable`'s `open("ab")` had already created a
|
|
715
|
+
zero-byte file, so existence alone would have accepted tan's own probe).
|
|
716
|
+
The premise it rested on -- "No real emit is empty" -- is FALSE.
|
|
717
|
+
`alp_project.py`'s unscoped per-core emits write nothing and exit 0 when no
|
|
718
|
+
core matches the mode's OS class, which is the CORRECT answer: on the
|
|
719
|
+
shipped `examples/display/lvgl-dashboard-x-evk` (a Linux-only V2N board),
|
|
720
|
+
`--emit zephyr-conf` prints `alp_project: wrote ref/alp.conf (0 bytes)` and
|
|
721
|
+
exits 0, as does `--emit cmake-args`. Measured, both engines, same tree:
|
|
722
|
+
in-process reported exit 0 with those files in `written[]`; spawned
|
|
723
|
+
reported exit 3, `failed: ["zephyr-conf","cmake-args"]`, "exited 0 but did
|
|
724
|
+
not write ..." -- about files that were on disk. Worse with `--output`:
|
|
725
|
+
`probe_created_output` was True, so `_discard_probe_file` then UNLINKED the
|
|
726
|
+
artefact the SDK really produced. It also broke the parity contract this
|
|
727
|
+
module's docstring rests on -- the two engines must render a target the
|
|
728
|
+
same way for the parity suite to diff their bytes.
|
|
729
|
+
|
|
730
|
+
The probe ambiguity that motivated the size demand is closed at its source
|
|
731
|
+
instead: `_ensure_writable` now REMOVES the file it created before the
|
|
732
|
+
emitter runs, so a file present afterwards is the emitter's, whatever its
|
|
733
|
+
size. tan-cli#397's guarantee is intact in BOTH of its cases:
|
|
734
|
+
|
|
735
|
+
* nothing at the destination -> the file is still absent afterwards, and
|
|
736
|
+
still refused;
|
|
737
|
+
* something already at the destination (a stale artefact, or a file the
|
|
738
|
+
caller's `--output` pointed at) -> `before` carries its
|
|
739
|
+
[`_output_stamp`], and a destination that comes out of the emit exactly
|
|
740
|
+
as it went in is refused too. Without that arm the widened check would
|
|
741
|
+
have called an untouched stale `alp.conf` "written" and handed the next
|
|
742
|
+
`tan build` a file from a previous board -- exactly #397's failure, one
|
|
743
|
+
level up.
|
|
744
|
+
|
|
745
|
+
Only the SPAWNED engine needs this. `_emit_one_in_process` performs the
|
|
746
|
+
write itself, so its `None` is already earned.
|
|
747
|
+
"""
|
|
748
|
+
prefix = f"Generation failed for target '{emit}': the SDK emitter exited 0 but "
|
|
749
|
+
try:
|
|
750
|
+
if emit == ZEPHYR_BOARD:
|
|
751
|
+
if not output.is_dir():
|
|
752
|
+
return prefix + f"did not create the board directory `{output}`."
|
|
753
|
+
if not any(output.iterdir()):
|
|
754
|
+
return prefix + f"wrote no files into `{output}`."
|
|
755
|
+
return None
|
|
756
|
+
if not output.is_file():
|
|
757
|
+
return prefix + f"did not write `{output}`."
|
|
758
|
+
if before is not None and _output_stamp(output) == before:
|
|
759
|
+
return prefix + f"left `{output}` exactly as it found it."
|
|
760
|
+
except (OSError, ValueError) as err:
|
|
761
|
+
# Never swallowed into a success: a destination that cannot even be
|
|
762
|
+
# stat'ed is exactly the state this guard exists to refuse to call
|
|
763
|
+
# "written". `ValueError` sits beside `OSError` for the same reason
|
|
764
|
+
# `_ensure_writable` catches both -- a path carrying a surrogate or an
|
|
765
|
+
# embedded NUL raises that, and letting one reach the command's
|
|
766
|
+
# catch-all backstop would report it as InternalFailure (5).
|
|
767
|
+
return prefix + f"`{output}` could not be checked: {err}"
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _emit_one(
|
|
772
|
+
python: str,
|
|
773
|
+
script: Path,
|
|
774
|
+
board_path: Path,
|
|
775
|
+
emit: str,
|
|
776
|
+
output: Path,
|
|
777
|
+
core: str | None,
|
|
778
|
+
) -> str | None:
|
|
779
|
+
"""Run one emit. `None` on success, else the message for its
|
|
780
|
+
`generate.emit-failed` issue.
|
|
781
|
+
|
|
782
|
+
Every way a subprocess can fail is a message, not an exception: the binary
|
|
783
|
+
is absent or not executable (`OSError`), it wedges (`TimeoutExpired`), or it
|
|
784
|
+
exits non-zero (the SDK's own stderr is the most useful message there, so it
|
|
785
|
+
is forwarded verbatim when present).
|
|
786
|
+
|
|
787
|
+
A zero exit is necessary but NOT sufficient -- `_missing_emit_output` has
|
|
788
|
+
the whole of why (tan-cli#397).
|
|
789
|
+
"""
|
|
790
|
+
argv = [
|
|
791
|
+
python,
|
|
792
|
+
str(script),
|
|
793
|
+
"--input",
|
|
794
|
+
str(board_path),
|
|
795
|
+
"--emit",
|
|
796
|
+
emit,
|
|
797
|
+
"--output",
|
|
798
|
+
str(output),
|
|
799
|
+
]
|
|
800
|
+
# zephyr-board requires --core (already validated); the scopable targets
|
|
801
|
+
# accept it optionally, and forwarding it is what makes `--target
|
|
802
|
+
# zephyr-conf --core m55_hp` byte-identical to what a build materialises for
|
|
803
|
+
# that core (tan-cli#117 review finding 2).
|
|
804
|
+
if core is not None and (emit == ZEPHYR_BOARD or emit in CORE_SCOPABLE_TARGETS):
|
|
805
|
+
argv += ["--core", core]
|
|
806
|
+
|
|
807
|
+
# Taken BEFORE the spawn, so a zero exit that left an already-present
|
|
808
|
+
# destination untouched is still refused -- see `_missing_emit_output`.
|
|
809
|
+
before = _output_stamp(output)
|
|
810
|
+
try:
|
|
811
|
+
out = subprocess.run(
|
|
812
|
+
argv,
|
|
813
|
+
capture_output=True,
|
|
814
|
+
text=True,
|
|
815
|
+
encoding="utf-8",
|
|
816
|
+
errors="replace",
|
|
817
|
+
stdin=subprocess.DEVNULL,
|
|
818
|
+
timeout=EMIT_TIMEOUT_S,
|
|
819
|
+
env=spawn_env(),
|
|
820
|
+
check=False,
|
|
821
|
+
)
|
|
822
|
+
except (OSError, ValueError, subprocess.SubprocessError) as err:
|
|
823
|
+
return f"Generation failed for target '{emit}': {err}"
|
|
824
|
+
if out.returncode == 0:
|
|
825
|
+
return _missing_emit_output(emit, output, before)
|
|
826
|
+
stderr = (out.stderr or "").strip()
|
|
827
|
+
return stderr or f"Generation failed for target '{emit}'."
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _emit_one_in_process(
|
|
831
|
+
sdk_root: Path,
|
|
832
|
+
board_path: Path,
|
|
833
|
+
emit: str,
|
|
834
|
+
output: Path,
|
|
835
|
+
core: str | None,
|
|
836
|
+
*,
|
|
837
|
+
skip_advisories: list[str] | None = None,
|
|
838
|
+
) -> str | None:
|
|
839
|
+
"""Render one relocated emit with `tan.planner` and write it. `None` on
|
|
840
|
+
success, else the message for its `generate.emit-failed` issue.
|
|
841
|
+
|
|
842
|
+
*skip_advisories* (tan-cli#964 review, major 6): threaded to
|
|
843
|
+
`planner_emit.render`, which threads it to `load_board_yaml`/
|
|
844
|
+
`_render_route_table`'s own schema checks. Not threaded into the
|
|
845
|
+
`TREE_MODES` (`zephyr-board`) branch below -- that mode writes a whole
|
|
846
|
+
board directory keyed on SKU+core, a separate emit shape major 6's fix
|
|
847
|
+
does not extend to.
|
|
848
|
+
|
|
849
|
+
The exception funnel this command's contract depends on. In a subprocess
|
|
850
|
+
every planner blow-up -- a malformed `board.yaml`, a missing SoM preset, an
|
|
851
|
+
unreadable `metadata/**`, an `OrchestratorError`, a bare `ValueError` from
|
|
852
|
+
the Ethos-U sizing path (I-48) -- arrived as a non-zero exit code and a
|
|
853
|
+
string. In-process it propagates into THIS process instead, and an escaped
|
|
854
|
+
traceback puts nothing parseable on stdout: the extension renders no error
|
|
855
|
+
at all. So the catch is broad on purpose.
|
|
856
|
+
|
|
857
|
+
`SystemExit` is caught separately because it is not an `Exception`:
|
|
858
|
+
`tan.planner.loader` and `kconfig_symbols` call `sys.exit()` on a missing
|
|
859
|
+
dependency, which in-process would tear down the CLI past every envelope
|
|
860
|
+
guarantee.
|
|
861
|
+
|
|
862
|
+
`zephyr-board` splits off to `render_tree`/`write_tree`: it writes a
|
|
863
|
+
DIRECTORY of files named per SKU+core, and `output` for that target IS that
|
|
864
|
+
directory (the same contract `alp_project.py --emit zephyr-board --output
|
|
865
|
+
<dir>` had). One `if`, inside the same funnel, so its failures are coded
|
|
866
|
+
exactly like every other target's.
|
|
867
|
+
"""
|
|
868
|
+
try:
|
|
869
|
+
if emit in planner_emit.TREE_MODES:
|
|
870
|
+
planner_emit.write_tree(
|
|
871
|
+
planner_emit.render_tree(
|
|
872
|
+
emit, sdk_root=sdk_root, board_yaml=board_path, core=core
|
|
873
|
+
),
|
|
874
|
+
output,
|
|
875
|
+
)
|
|
876
|
+
else:
|
|
877
|
+
text = planner_emit.render(
|
|
878
|
+
emit, sdk_root=sdk_root, board_yaml=board_path, core=core,
|
|
879
|
+
skip_advisories=skip_advisories,
|
|
880
|
+
)
|
|
881
|
+
planner_emit.write(text, output)
|
|
882
|
+
except SystemExit as err:
|
|
883
|
+
return (
|
|
884
|
+
f"Generation failed for target '{emit}': the planner exited early "
|
|
885
|
+
f"(code {err.code})."
|
|
886
|
+
)
|
|
887
|
+
except planner_emit.PlannerEmitError as err:
|
|
888
|
+
# Ours, and already phrased for a human.
|
|
889
|
+
return f"Generation failed for target '{emit}': {err}"
|
|
890
|
+
except KeyboardInterrupt:
|
|
891
|
+
raise
|
|
892
|
+
except BaseException as err: # noqa: BLE001 -- see the docstring
|
|
893
|
+
# tan-cli#1350: keep a `diagnostics.link: itcm` refusal's own code
|
|
894
|
+
# (the loop below splits it back out).
|
|
895
|
+
link_code = refusal_code(err)
|
|
896
|
+
if link_code is not None:
|
|
897
|
+
return f"{link_code}: {err}"
|
|
898
|
+
return f"Generation failed for target '{emit}': {type(err).__name__}: {err}"
|
|
899
|
+
return None
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _resolve_engine(
|
|
903
|
+
targets: tuple[str, ...], sdk_root: Path
|
|
904
|
+
) -> tuple[dict[str, str], str | None]:
|
|
905
|
+
"""`({mode: engine}, fallback_reason)` -- decided once, before any write.
|
|
906
|
+
|
|
907
|
+
Up front rather than per target so `data.engine` describes the whole run and
|
|
908
|
+
a half-in-process run cannot exist. `fallback_reason` is non-`None` only when
|
|
909
|
+
the in-process path was WANTED and refused; a deliberate
|
|
910
|
+
`TAN_GENERATE_EXECUTOR=subprocess` is a choice, not a fallback, and reports
|
|
911
|
+
itself through `data.engine` alone.
|
|
912
|
+
"""
|
|
913
|
+
requested = os.environ.get(EXECUTOR_ENV, "auto").strip().lower() or "auto"
|
|
914
|
+
if requested not in EXECUTORS:
|
|
915
|
+
raise GenerateError(
|
|
916
|
+
"generate.invalid-executor",
|
|
917
|
+
f"{EXECUTOR_ENV}='{requested}' is not one of {', '.join(EXECUTORS)}.",
|
|
918
|
+
ExitCode.VALIDATION_FAILURE,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
eligible = [mode for mode in targets if mode in planner_emit.IN_PROCESS_MODES]
|
|
922
|
+
reason: str | None = None
|
|
923
|
+
if requested == "subprocess":
|
|
924
|
+
eligible = []
|
|
925
|
+
elif eligible:
|
|
926
|
+
reason = planner_emit.unavailable(sdk_root)
|
|
927
|
+
if reason is not None:
|
|
928
|
+
if requested == "in-process":
|
|
929
|
+
raise GenerateError(
|
|
930
|
+
"generate.in-process-unavailable",
|
|
931
|
+
f"{EXECUTOR_ENV}=in-process was requested but the in-process "
|
|
932
|
+
f"planner cannot serve this run: {reason}",
|
|
933
|
+
ExitCode.RUNTIME_FAILURE,
|
|
934
|
+
)
|
|
935
|
+
eligible = []
|
|
936
|
+
return (
|
|
937
|
+
{
|
|
938
|
+
mode: "in-process" if mode in eligible else "subprocess"
|
|
939
|
+
for mode in targets
|
|
940
|
+
},
|
|
941
|
+
reason,
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _resolve_board_path(board_yaml: str | None, workspace_root: Path) -> Path:
|
|
946
|
+
"""`--board-yaml` (absolute, or workspace-relative), else
|
|
947
|
+
`<workspace_root>/board.yaml`."""
|
|
948
|
+
if board_yaml:
|
|
949
|
+
candidate = Path(board_yaml)
|
|
950
|
+
return candidate if candidate.is_absolute() else workspace_root / candidate
|
|
951
|
+
return workspace_root / "board.yaml"
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
@dataclass(frozen=True)
|
|
955
|
+
class _ResolvedSdkRoot:
|
|
956
|
+
"""`_resolve_sdk_root`'s own return shape -- a named result, not a
|
|
957
|
+
growing tuple (the same reasoning `tan.core.sdk_discovery.SdkRootResolution`
|
|
958
|
+
documents): a fact one caller needs (`foreign_global_default_for`,
|
|
959
|
+
tan-cli#464) must not force every field before it to be re-counted by
|
|
960
|
+
position.
|
|
961
|
+
|
|
962
|
+
`.path` is `None` on the SAME two conditions `SdkRootResolution.path` is
|
|
963
|
+
(tan-cli#900): no `--sdk-root` and nothing else resolved, or a rejected
|
|
964
|
+
`--sdk-root`. `.reported` is unused there -- the raise site names the raw
|
|
965
|
+
`sdk_root` flag itself, not this field."""
|
|
966
|
+
|
|
967
|
+
path: Path | None
|
|
968
|
+
tier: str
|
|
969
|
+
reported: str
|
|
970
|
+
broken_project_pin: str | None = None
|
|
971
|
+
foreign_global_default_for: str | None = None
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _resolve_sdk_root(sdk_root: str | None, workspace_root: Path) -> _ResolvedSdkRoot:
|
|
975
|
+
"""The resolved checkout as a [`_ResolvedSdkRoot`] -- ALWAYS one, never a
|
|
976
|
+
bare `None` (tan-cli#900; the same fix tan-cli#468 made to
|
|
977
|
+
`presets_cmd.resolve_sdk`, applied here to this command's own WIDE ladder
|
|
978
|
+
instead of copying its narrow one). Collapsing to `None` the moment
|
|
979
|
+
nothing resolved discarded `.broken_project_pin` on that path: a
|
|
980
|
+
workspace whose `.alp/sdk-path` names a checkout that no longer exists,
|
|
981
|
+
with nothing else resolvable, reported `generate.sdk-root-unresolved`
|
|
982
|
+
alone -- the pin fact was never the customer's to lose. Callers now check
|
|
983
|
+
`.path is None`, the "unresolved" signal `SdkRootResolution.path` itself
|
|
984
|
+
already carries.
|
|
985
|
+
|
|
986
|
+
`--sdk-root` is TERMINAL: an explicit path that is not an SDK checkout
|
|
987
|
+
resolves to `.path is None` rather than falling through to discovery and
|
|
988
|
+
generating against a different checkout than the caller named (I-31); its
|
|
989
|
+
tier is always `sdkRootFlag`, matching the oracle's closed
|
|
990
|
+
`SdkSourceTier`.
|
|
991
|
+
|
|
992
|
+
Without `--sdk-root`: `.alp/sdk-path` project pin > the machine-global
|
|
993
|
+
default (`~/.alp/sdk-default`) > the WIDE positional walk
|
|
994
|
+
(`resolve_sdk_root_wide`, which reports its own tier) -- the wide ladder,
|
|
995
|
+
not the narrow one thirteen other commands take, because the oracle's
|
|
996
|
+
`generate` prefers the child `<ws>/alp-sdk` over a competing `../alp-sdk`
|
|
997
|
+
(tan-cli#263). No `ALP_SDK_ROOT` tier (tried and reverted -- see
|
|
998
|
+
`resolve_sdk_root_ladder`'s own docstring).
|
|
999
|
+
|
|
1000
|
+
`reported` is the string to put in the envelope's `sdk.root`: for
|
|
1001
|
+
`sdkRootFlag` it is the flag's raw text, NOT `str(path)` -- pathlib
|
|
1002
|
+
silently drops a trailing separator (`/srv/alp-sdk/` in, `/srv/alp-sdk`
|
|
1003
|
+
out), which the oracle does not do. Every other tier reports `str(path)`;
|
|
1004
|
+
those come from a pinned/discovered path, not user-typed argv, so there is
|
|
1005
|
+
no trailing-separator input to preserve.
|
|
1006
|
+
"""
|
|
1007
|
+
if sdk_root:
|
|
1008
|
+
candidate = Path(sdk_root)
|
|
1009
|
+
if (candidate / "scripts" / "alp_project.py").is_file():
|
|
1010
|
+
return _ResolvedSdkRoot(candidate, "sdkRootFlag", sdk_root)
|
|
1011
|
+
return _ResolvedSdkRoot(None, "sdkRootFlag", sdk_root)
|
|
1012
|
+
resolution = resolve_sdk_root_wide(None, workspace_root)
|
|
1013
|
+
found = resolution.path
|
|
1014
|
+
return _ResolvedSdkRoot(
|
|
1015
|
+
found,
|
|
1016
|
+
resolution.tier,
|
|
1017
|
+
str(found) if found is not None else "",
|
|
1018
|
+
resolution.broken_project_pin,
|
|
1019
|
+
resolution.foreign_global_default_for,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _finish(
|
|
1024
|
+
*,
|
|
1025
|
+
json_mode: bool,
|
|
1026
|
+
verbose: bool,
|
|
1027
|
+
quiet: bool,
|
|
1028
|
+
project: Project,
|
|
1029
|
+
targets: tuple[str, ...],
|
|
1030
|
+
written: list[str],
|
|
1031
|
+
failed: list[str],
|
|
1032
|
+
issues: list[Issue],
|
|
1033
|
+
exit_code: ExitCode,
|
|
1034
|
+
engine: dict[str, str] | None = None,
|
|
1035
|
+
sdk: SdkInfo | None = None,
|
|
1036
|
+
) -> None:
|
|
1037
|
+
data = {
|
|
1038
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
1039
|
+
"targets": list(targets),
|
|
1040
|
+
"written": written,
|
|
1041
|
+
"failed": failed,
|
|
1042
|
+
}
|
|
1043
|
+
# Present only when there is a run to describe. Every refusal resolves zero
|
|
1044
|
+
# targets, and `contract/envelopes/generate-board-yaml-missing` freezes that
|
|
1045
|
+
# shape for the Rust and Python harnesses alike -- an empty `engine: {}`
|
|
1046
|
+
# there would be a contract break reporting nothing.
|
|
1047
|
+
if engine:
|
|
1048
|
+
data["engine"] = engine
|
|
1049
|
+
if json_mode:
|
|
1050
|
+
emit(Envelope("generate", project, data, issues, exit_code, sdk=sdk))
|
|
1051
|
+
else:
|
|
1052
|
+
# stdout is the envelope channel in both modes; human text never
|
|
1053
|
+
# touches it.
|
|
1054
|
+
#
|
|
1055
|
+
# No summary line on a refusal (`targets` empty): "wrote 0/0 targets"
|
|
1056
|
+
# above a refusal message reads like a successful no-op run.
|
|
1057
|
+
#
|
|
1058
|
+
# `--quiet` silences the summary only -- never the issues. A CMake
|
|
1059
|
+
# `execute_process` shows this stderr verbatim when the emit fails, and
|
|
1060
|
+
# a quiet flag that swallowed the reason would leave the caller with a
|
|
1061
|
+
# bare `rv=1` again -- the exact defect the PLANNED `cmake/alp.cmake`
|
|
1062
|
+
# (unmerged, tan-cli#825) is meant to fix.
|
|
1063
|
+
if targets and not quiet:
|
|
1064
|
+
tail = f"; failed: {', '.join(failed)}" if failed else " targets"
|
|
1065
|
+
print(
|
|
1066
|
+
f"generate: wrote {len(written)}/{len(targets)}{tail}",
|
|
1067
|
+
file=sys.stderr,
|
|
1068
|
+
)
|
|
1069
|
+
for issue in issues:
|
|
1070
|
+
print(f"generate: {issue.message}", file=sys.stderr)
|
|
1071
|
+
if verbose:
|
|
1072
|
+
for target in targets:
|
|
1073
|
+
where = (engine or {}).get(target)
|
|
1074
|
+
print(
|
|
1075
|
+
f"target: {target}" + (f" ({where})" if where else ""),
|
|
1076
|
+
file=sys.stderr,
|
|
1077
|
+
)
|
|
1078
|
+
raise typer.Exit(int(exit_code))
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def generate(
|
|
1082
|
+
target: str = typer.Option(
|
|
1083
|
+
None,
|
|
1084
|
+
"--target",
|
|
1085
|
+
metavar="EMIT",
|
|
1086
|
+
help="Single generation target (e.g. zephyr-conf, dts-overlay, cmake-args).",
|
|
1087
|
+
),
|
|
1088
|
+
all_targets: bool = typer.Option(
|
|
1089
|
+
False, "--all", help="Run every default generation target."
|
|
1090
|
+
),
|
|
1091
|
+
core: str = typer.Option(
|
|
1092
|
+
None,
|
|
1093
|
+
"--core",
|
|
1094
|
+
metavar="CORE_ID",
|
|
1095
|
+
help="Core id: required by --target zephyr-board, optional scoping for some others.",
|
|
1096
|
+
),
|
|
1097
|
+
force: bool = typer.Option(
|
|
1098
|
+
False, "--force", help="Allow overwriting an existing native_sim overlay."
|
|
1099
|
+
),
|
|
1100
|
+
project: str = typer.Option(
|
|
1101
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
1102
|
+
),
|
|
1103
|
+
board_yaml: str = typer.Option(
|
|
1104
|
+
None, "--board-yaml", metavar="PATH", help="Explicit board.yaml path."
|
|
1105
|
+
),
|
|
1106
|
+
sdk_root: str = typer.Option(
|
|
1107
|
+
None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
|
|
1108
|
+
),
|
|
1109
|
+
output: str = typer.Option(
|
|
1110
|
+
None,
|
|
1111
|
+
"--output",
|
|
1112
|
+
metavar="PATH",
|
|
1113
|
+
help="Destination for a single --target (default: the SDK's own layout "
|
|
1114
|
+
"under the project root).",
|
|
1115
|
+
),
|
|
1116
|
+
quiet: bool = typer.Option(
|
|
1117
|
+
False, "--quiet", help="Suppress the non-essential summary line."
|
|
1118
|
+
),
|
|
1119
|
+
non_interactive: bool = typer.Option(
|
|
1120
|
+
False, "--non-interactive", hidden=True
|
|
1121
|
+
),
|
|
1122
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
1123
|
+
verbose: bool = typer.Option(
|
|
1124
|
+
False, "--verbose", help="List each target in text output."
|
|
1125
|
+
),
|
|
1126
|
+
) -> None:
|
|
1127
|
+
"""Generate board-derived output files via the SDK's emitters."""
|
|
1128
|
+
# `generate` never prompts, so `--non-interactive` is accepted and ignored:
|
|
1129
|
+
# it exists for the PLANNED alp-sdk `cmake/alp.cmake` (unmerged,
|
|
1130
|
+
# tan-cli#825), which is meant to pass it on every configure (as it will
|
|
1131
|
+
# to every `tan` invocation), and an unknown flag there would be a Click
|
|
1132
|
+
# usage error that fails the whole CMake configure.
|
|
1133
|
+
del non_interactive
|
|
1134
|
+
json_mode = output_format == "json"
|
|
1135
|
+
|
|
1136
|
+
# The as-GIVEN strings, never the resolved absolute paths: the envelope
|
|
1137
|
+
# reflects back what the caller typed, which is what keeps the conformance
|
|
1138
|
+
# golden reproducible on any machine (`project.root == "."`,
|
|
1139
|
+
# `boardYaml == "board.yaml"`).
|
|
1140
|
+
#
|
|
1141
|
+
# tan-cli#236: NOT built via `Project.resolved` -- that would existence-check
|
|
1142
|
+
# this as-given string against the real cwd, which is wrong the moment
|
|
1143
|
+
# `--project` differs from it (the resolved `board_path` below, not this
|
|
1144
|
+
# string, is what must be checked). `reported` is corrected in place, right
|
|
1145
|
+
# below, at the one guard where the two can disagree.
|
|
1146
|
+
reported = Project(
|
|
1147
|
+
root=project if project else ".",
|
|
1148
|
+
board_yaml=board_yaml if board_yaml else "board.yaml",
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
def finish(**kwargs) -> None:
|
|
1152
|
+
_finish(
|
|
1153
|
+
json_mode=json_mode,
|
|
1154
|
+
verbose=verbose,
|
|
1155
|
+
quiet=quiet,
|
|
1156
|
+
project=reported,
|
|
1157
|
+
sdk=sdk_info,
|
|
1158
|
+
**kwargs,
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
def refuse(err: GenerateError) -> None:
|
|
1162
|
+
# No `engine`: a refusal resolved no targets, so there is no run to
|
|
1163
|
+
# describe (and the committed conformance golden pins that data shape).
|
|
1164
|
+
# `sdk` still reports (via `finish`'s default) once it is resolved --
|
|
1165
|
+
# only a refusal that precedes/IS the SDK resolution itself leaves it
|
|
1166
|
+
# unset, matching the oracle (`generate.board-yaml-missing` and
|
|
1167
|
+
# `generate.sdk-root-unresolved` both carry no `sdk`; every guard past
|
|
1168
|
+
# a successful resolution does). `err.extra_issues` PREPENDED
|
|
1169
|
+
# (tan-cli#900), the `[*resolution_issues, Issue(...)]` shape
|
|
1170
|
+
# `clean_cmd._run` already uses.
|
|
1171
|
+
finish(
|
|
1172
|
+
targets=(),
|
|
1173
|
+
written=[],
|
|
1174
|
+
failed=[],
|
|
1175
|
+
issues=[*(err.extra_issues or []), Issue(err.code, "error", err.message)],
|
|
1176
|
+
exit_code=err.exit_code,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
engine: dict[str, str] = {}
|
|
1180
|
+
sdk_info: SdkInfo | None = None
|
|
1181
|
+
try:
|
|
1182
|
+
workspace_root = Path(os.path.abspath(project)) if project else Path.cwd()
|
|
1183
|
+
board_path = _resolve_board_path(board_yaml, workspace_root)
|
|
1184
|
+
|
|
1185
|
+
if not board_path.exists():
|
|
1186
|
+
# tan-cli#236: the refusal below says the file does not exist, so
|
|
1187
|
+
# the SAME envelope's `project.boardYaml` must not still name it
|
|
1188
|
+
# (Rust's own starkest instance of this bug -- see
|
|
1189
|
+
# `crates/tan-cli/src/commands/generate.rs`).
|
|
1190
|
+
reported = Project(root=reported.root, board_yaml=None)
|
|
1191
|
+
raise GenerateError(
|
|
1192
|
+
"generate.board-yaml-missing",
|
|
1193
|
+
"board.yaml path could not be resolved or the file does not exist.",
|
|
1194
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
resolved = _resolve_sdk_root(sdk_root, workspace_root)
|
|
1198
|
+
if resolved.path is None:
|
|
1199
|
+
# tan-cli#900: `resolved.broken_project_pin` survives even on
|
|
1200
|
+
# this refusal now, so a gone `.alp/sdk-path` target still
|
|
1201
|
+
# discloses instead of looking like no pin was ever set.
|
|
1202
|
+
pin_issue = project_pin_issue(resolved.broken_project_pin, resolved.tier)
|
|
1203
|
+
foreign_issue = global_default_foreign_project_issue(
|
|
1204
|
+
resolved.foreign_global_default_for
|
|
1205
|
+
)
|
|
1206
|
+
raise GenerateError(
|
|
1207
|
+
"generate.sdk-root-unresolved",
|
|
1208
|
+
# tan-cli#497 defect 7: a REJECTED `--sdk-root` names the
|
|
1209
|
+
# value. This refusal carries NO `sdk` block (see `refuse`'s
|
|
1210
|
+
# own comment above -- `sdk_info` is still `None` here), so
|
|
1211
|
+
# before this the typed path appeared nowhere in the envelope
|
|
1212
|
+
# at all, and the message opened by recommending the very flag
|
|
1213
|
+
# it had just rejected.
|
|
1214
|
+
rejected_sdk_root_message(sdk_root, "Nothing was generated.")
|
|
1215
|
+
if sdk_root
|
|
1216
|
+
# `tan sdk switch` refuses in this build (tan-cli#305) -- kept
|
|
1217
|
+
# the two mechanisms that actually work here (`--sdk-root`,
|
|
1218
|
+
# placing the project near a checkout) and swapped the third
|
|
1219
|
+
# for NO_SDK_NEXT_STEPS's honest "how to get one at all".
|
|
1220
|
+
else with_sdk_search(
|
|
1221
|
+
"alp-sdk root is unresolved. Use --sdk-root, place the project near an "
|
|
1222
|
+
f"alp-sdk checkout, or {NO_SDK_NEXT_STEPS}.",
|
|
1223
|
+
workspace_root,
|
|
1224
|
+
),
|
|
1225
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1226
|
+
extra_issues=[i for i in (pin_issue, foreign_issue) if i is not None],
|
|
1227
|
+
)
|
|
1228
|
+
resolved_sdk = resolved.path
|
|
1229
|
+
sdk_tier = resolved.tier
|
|
1230
|
+
sdk_reported = resolved.reported
|
|
1231
|
+
sdk_broken_pin = resolved.broken_project_pin
|
|
1232
|
+
sdk_foreign_default = resolved.foreign_global_default_for
|
|
1233
|
+
# Set only now: every guard from here on runs with a resolved SDK, so
|
|
1234
|
+
# the envelope carries `sdk` on both success and a later refusal
|
|
1235
|
+
# (`--target`/`--core` mistakes, an overlay guard, ...), matching the
|
|
1236
|
+
# oracle. The two guards above (`board.yaml` missing, this one) raise
|
|
1237
|
+
# before `sdk_info` exists, so their envelopes carry no `sdk` --
|
|
1238
|
+
# exactly the oracle's shape for both.
|
|
1239
|
+
sdk_info = SdkInfo(sdk_reported, sdk_tier)
|
|
1240
|
+
|
|
1241
|
+
# tan-cli#963: computed here, and seeded into `issues` before any
|
|
1242
|
+
# other entry, so a resolvable-but-broken pin (or a foreign global
|
|
1243
|
+
# default) reads FIRST on this path too -- matching both `refuse`'s
|
|
1244
|
+
# `[*extra_issues, Issue(...)]` shape above and `clean_cmd._run`'s
|
|
1245
|
+
# convention. These are resolution advisories (the run may have
|
|
1246
|
+
# consulted the wrong checkout entirely); the per-target
|
|
1247
|
+
# `generate.emit-failed` entries and `generate.in-process-unavailable`
|
|
1248
|
+
# below are outcome/context and stay appended, in place.
|
|
1249
|
+
pin_issue = project_pin_issue(sdk_broken_pin, sdk_tier)
|
|
1250
|
+
# tan-cli#464: same silence, one tier down -- a `globalDefault` answer
|
|
1251
|
+
# a DIFFERENT project's bootstrap relocation actually decided.
|
|
1252
|
+
foreign_issue = global_default_foreign_project_issue(sdk_foreign_default)
|
|
1253
|
+
issues: list[Issue] = [i for i in (pin_issue, foreign_issue) if i is not None]
|
|
1254
|
+
|
|
1255
|
+
targets = resolve_targets(target, all_targets, core)
|
|
1256
|
+
|
|
1257
|
+
# Which engine runs what -- before any guard that writes, so a bad
|
|
1258
|
+
# `TAN_GENERATE_EXECUTOR` refuses without leaving a directory behind.
|
|
1259
|
+
engine, fallback_reason = _resolve_engine(targets, resolved_sdk)
|
|
1260
|
+
|
|
1261
|
+
# zephyr-board always resolves alone, so its directory name is derived
|
|
1262
|
+
# once here rather than inside the emit loop.
|
|
1263
|
+
board_dir_name: str | None = None
|
|
1264
|
+
if ZEPHYR_BOARD in targets:
|
|
1265
|
+
sku = _board_sku(board_path)
|
|
1266
|
+
board_dir_name = (
|
|
1267
|
+
zephyr_board_dir_name(sku, core) if sku and core else None
|
|
1268
|
+
)
|
|
1269
|
+
if board_dir_name is None:
|
|
1270
|
+
raise GenerateError(
|
|
1271
|
+
"generate.board-sku-unresolved",
|
|
1272
|
+
"board.yaml's som.sku is missing or is not an E1M-* SKU; "
|
|
1273
|
+
"`--target zephyr-board` needs it to name the generated board "
|
|
1274
|
+
"directory (alp-sdk's `alp_e1m_<sku-slug>_<core>` convention).",
|
|
1275
|
+
ExitCode.VALIDATION_FAILURE,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
output_override = (
|
|
1279
|
+
_resolve_output_override(output, targets, workspace_root)
|
|
1280
|
+
if output
|
|
1281
|
+
else None
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
# tan-cli#501: an EXPLICIT `--target native-sim-overlay` still refuses
|
|
1285
|
+
# a foreign (bannerless) overlay without `--force` -- the user named
|
|
1286
|
+
# this file. But `native-sim-overlay` reaching `targets` only via the
|
|
1287
|
+
# bare/`--all` default set is different (a vendored scaffold ships one,
|
|
1288
|
+
# tan-cli#457 protects a hand edit): refusing the other eight targets
|
|
1289
|
+
# over it is wrong, and a generic `--force` truncating it is worse
|
|
1290
|
+
# (measured). So an implicit inclusion is left alone: dropped, reported.
|
|
1291
|
+
overlay_explicit = targets == ("native-sim-overlay",)
|
|
1292
|
+
if _overlay_would_overwrite(workspace_root, targets, False, output_override):
|
|
1293
|
+
if overlay_explicit:
|
|
1294
|
+
if not force:
|
|
1295
|
+
raise GenerateError(
|
|
1296
|
+
"generate.would-overwrite",
|
|
1297
|
+
"boards/native_sim_native_64.overlay already exists. Use "
|
|
1298
|
+
"--force to overwrite.",
|
|
1299
|
+
ExitCode.WRITE_FAILURE,
|
|
1300
|
+
)
|
|
1301
|
+
# else: explicit + --force -- fall through, write loop overwrites.
|
|
1302
|
+
else:
|
|
1303
|
+
targets = tuple(t for t in targets if t != "native-sim-overlay")
|
|
1304
|
+
# #3: keep `data.engine` in step with the drop (computed above).
|
|
1305
|
+
engine = {k: v for k, v in engine.items() if k != "native-sim-overlay"}
|
|
1306
|
+
# #2: may be a template's OWN vendored overlay (tan cannot
|
|
1307
|
+
# tell) -- the old `--force` remedy DESTROYED one (measured);
|
|
1308
|
+
# no clobber offered, an explicit `--target ... --force` still works.
|
|
1309
|
+
issues.append(
|
|
1310
|
+
Issue(
|
|
1311
|
+
"generate.overlay-not-owned",
|
|
1312
|
+
"warning",
|
|
1313
|
+
"boards/native_sim_native_64.overlay already exists and was "
|
|
1314
|
+
"not generated by tan -- this project's own file (a hand "
|
|
1315
|
+
"edit, or a template's vendored overlay); left untouched.",
|
|
1316
|
+
)
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# LAST of the guards, because it is the only one that WRITES: it creates
|
|
1320
|
+
# the parent directory (and, for `zephyr-board`, the output directory
|
|
1321
|
+
# itself). Running it earlier would leave strays behind for a run the
|
|
1322
|
+
# `som.sku` or overlay guard above was about to refuse.
|
|
1323
|
+
probe_created_output = False
|
|
1324
|
+
if output_override is not None:
|
|
1325
|
+
probe_created_output = _ensure_writable(output_override, targets[0])
|
|
1326
|
+
else:
|
|
1327
|
+
# No `--output`: every target writes its DEFAULT path, which must
|
|
1328
|
+
# stay confined to the project after symlink resolution
|
|
1329
|
+
# (tan-cli#325). Checked for every target up front, so a run with
|
|
1330
|
+
# several targets refuses before the first one writes rather than
|
|
1331
|
+
# leaving an earlier target's file on disk.
|
|
1332
|
+
_confine_default_outputs(workspace_root, targets, board_dir_name)
|
|
1333
|
+
|
|
1334
|
+
# Only the spawned engine cares which interpreter PATH offers -- the
|
|
1335
|
+
# in-process one IS the interpreter. Probing regardless would refuse a
|
|
1336
|
+
# fully in-process run on a host whose `python` is old and unused, and
|
|
1337
|
+
# cost it a subprocess to find out.
|
|
1338
|
+
python = ""
|
|
1339
|
+
if "subprocess" in engine.values():
|
|
1340
|
+
python = _planner_python(
|
|
1341
|
+
str(workspace_root), str(resolved_sdk) if resolved_sdk else None
|
|
1342
|
+
)
|
|
1343
|
+
floor, _floor_source = resolve_manifest_python_floor(str(resolved_sdk))
|
|
1344
|
+
if (too_old := _python_too_old(python, floor)) is not None:
|
|
1345
|
+
raise GenerateError(
|
|
1346
|
+
"generate.python-too-old", too_old, ExitCode.RUNTIME_FAILURE
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
script = resolved_sdk / "scripts" / "alp_project.py"
|
|
1350
|
+
written: list[str] = []
|
|
1351
|
+
failed: list[str] = []
|
|
1352
|
+
link_refused = False
|
|
1353
|
+
# tan-cli#964 review (major 6): one shared collector across every
|
|
1354
|
+
# in-process target this run renders, so a schema absent for the
|
|
1355
|
+
# whole checkout is disclosed ONCE (deduplicated below), not once per
|
|
1356
|
+
# target that happens to read it.
|
|
1357
|
+
schema_skip_advisories: list[str] = []
|
|
1358
|
+
for mode in targets:
|
|
1359
|
+
target_output = output_override or _output_path(
|
|
1360
|
+
workspace_root, mode, board_dir_name
|
|
1361
|
+
)
|
|
1362
|
+
if engine[mode] == "in-process":
|
|
1363
|
+
message = _emit_one_in_process(
|
|
1364
|
+
resolved_sdk, board_path, mode, target_output, core,
|
|
1365
|
+
skip_advisories=schema_skip_advisories,
|
|
1366
|
+
)
|
|
1367
|
+
else:
|
|
1368
|
+
message = _emit_one(
|
|
1369
|
+
python, script, board_path, mode, target_output, core
|
|
1370
|
+
)
|
|
1371
|
+
if message is None:
|
|
1372
|
+
written.append(_relative_or_full(workspace_root, target_output))
|
|
1373
|
+
else:
|
|
1374
|
+
failed.append(mode)
|
|
1375
|
+
link_code, message = split_coded_message(message)
|
|
1376
|
+
if link_code is not None:
|
|
1377
|
+
link_refused = True
|
|
1378
|
+
issues.append(
|
|
1379
|
+
Issue(link_code or "generate.emit-failed", "error", message))
|
|
1380
|
+
# tan-cli#420: don't leave the writability probe's own empty
|
|
1381
|
+
# file behind for a Zephyr configure to pick up as a valid
|
|
1382
|
+
# (and silently empty) EXTRA_CONF_FILE. Only the file this
|
|
1383
|
+
# run created, and only while it is still zero-byte.
|
|
1384
|
+
if probe_created_output and target_output == output_override:
|
|
1385
|
+
_discard_probe_file(target_output)
|
|
1386
|
+
|
|
1387
|
+
# tan-cli#964 review (major 6): "skip-but-disclose". One issue per
|
|
1388
|
+
# DISTINCT note (`dict.fromkeys` dedups while keeping first-seen
|
|
1389
|
+
# order) -- a run with several targets all missing the same schema
|
|
1390
|
+
# must not repeat the identical line once per target. `info`, not
|
|
1391
|
+
# `warning`: nothing here is wrong with the DOCUMENT, only with tan's
|
|
1392
|
+
# ability to check it, matching `presets`/`size`/`debug-config`'s own
|
|
1393
|
+
# severity split between `*.metadata-schema-invalid` (warning, a real
|
|
1394
|
+
# violation) and this code (info, nothing was checked).
|
|
1395
|
+
for note in dict.fromkeys(schema_skip_advisories):
|
|
1396
|
+
issues.append(Issue("generate.metadata-schema-unchecked", "info", note))
|
|
1397
|
+
|
|
1398
|
+
# APPENDED, after the per-target issues: what the user asked for comes
|
|
1399
|
+
# first and this is context about how it ran. Never omitted, though --
|
|
1400
|
+
# a fallback nobody can see is how the port got believed before it
|
|
1401
|
+
# happened.
|
|
1402
|
+
if fallback_reason is not None:
|
|
1403
|
+
issues.append(
|
|
1404
|
+
Issue(
|
|
1405
|
+
"generate.in-process-unavailable",
|
|
1406
|
+
"warning",
|
|
1407
|
+
"generated by spawning the SDK's own emitter instead of the "
|
|
1408
|
+
f"in-process planner: {fallback_reason}",
|
|
1409
|
+
)
|
|
1410
|
+
)
|
|
1411
|
+
# tan-cli#407: this command just wrote `build/generated/alp.conf`, the
|
|
1412
|
+
# DTS overlays and `alp_hw_info_build.h` out of one checkout's
|
|
1413
|
+
# `metadata/`, and `tan build` may plan the very same tree against
|
|
1414
|
+
# another -- both reporting `sourceTier: "discovery"`. Of the four wide
|
|
1415
|
+
# commands this is the one where the split becomes FILES on disk, so
|
|
1416
|
+
# the warning ships beside them. `sdk_root` is the RAW flag (never
|
|
1417
|
+
# reassigned in this function): an explicit root puts both ladders on
|
|
1418
|
+
# the terminal `sdkRootFlag` tier and the helper returns `None`.
|
|
1419
|
+
divergence = sdk_ladder_divergence_issue(sdk_root, workspace_root, wide=True)
|
|
1420
|
+
if divergence is not None:
|
|
1421
|
+
issues.append(divergence)
|
|
1422
|
+
except GenerateError as err:
|
|
1423
|
+
refuse(err)
|
|
1424
|
+
return
|
|
1425
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
1426
|
+
# `typer.Exit` cannot reach here: it is raised only from `_finish`,
|
|
1427
|
+
# which runs outside this try.
|
|
1428
|
+
refuse(
|
|
1429
|
+
GenerateError(
|
|
1430
|
+
"generate.internal-failure",
|
|
1431
|
+
f"generate failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
1432
|
+
ExitCode.INTERNAL_FAILURE,
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
return
|
|
1436
|
+
|
|
1437
|
+
finish(
|
|
1438
|
+
targets=targets,
|
|
1439
|
+
written=written,
|
|
1440
|
+
failed=failed,
|
|
1441
|
+
issues=issues,
|
|
1442
|
+
engine=engine,
|
|
1443
|
+
exit_code=(
|
|
1444
|
+
ExitCode.SUCCESS if not failed
|
|
1445
|
+
else ExitCode.VALIDATION_FAILURE if link_refused
|
|
1446
|
+
else ExitCode.WRITE_FAILURE
|
|
1447
|
+
),
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
# tan-cli#261: adds the two oracle `GlobalArgs` flags this command was still
|
|
1452
|
+
# missing (`--ci`/`--no-color`) on top of the five already declared above
|
|
1453
|
+
# (`--target`/`--all`/`--quiet`/`--verbose` read for real; `--non-interactive`
|
|
1454
|
+
# accepted and dropped the same way these two now are); see
|
|
1455
|
+
# `tan.core.global_flags`.
|
|
1456
|
+
generate = accept_global_flags(generate)
|