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
tan/core/plan_tokens.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pure build-plan token substitution (alp-sdk #865, "hermetic build plans").
|
|
3
|
+
|
|
4
|
+
A **tokened** plan (`planPathMode: "tokened"`) carries literal placeholders --
|
|
5
|
+
`${SDK_ROOT}` / `${PROJECT_ROOT}` / `${PYTHON}` / `${TOOLCHAIN_ROOT}` -- in its
|
|
6
|
+
path-bearing string fields instead of baking in the emitting machine's
|
|
7
|
+
absolute paths. This module is the CONSUMER side: ONE blind string-
|
|
8
|
+
substitution pass swapping the tokens for tan's already-resolved values, plus
|
|
9
|
+
the guards that keep a wrong substitution from silently building the wrong
|
|
10
|
+
image.
|
|
11
|
+
|
|
12
|
+
Pure -- no IO. The caller (`tan.commands.build.token_substitution`) resolves
|
|
13
|
+
the SDK checkout root, the project root (the `board.yaml` directory), the
|
|
14
|
+
planner-venv Python and the toolchain root exactly ONCE and hands them in as
|
|
15
|
+
`TokenValues`; it also owns invoking `git` for the `sdkCommit` check
|
|
16
|
+
(`sdk_commit_mismatches` here only compares strings).
|
|
17
|
+
|
|
18
|
+
A plan without `planPathMode: "tokened"` is untouched by
|
|
19
|
+
`substitute_plan_tokens`: a byte-identical no-op ONLY when the key is ABSENT
|
|
20
|
+
(`plan_path_mode is None`, `:440`) -- present-but-other raises
|
|
21
|
+
`UnknownPlanPathMode` (`:445-446`) instead. Reachable via `--plan-from`
|
|
22
|
+
handing in an older plan with no `planPathMode` key at all -- tan's own
|
|
23
|
+
in-process planner (`tan.planner.buildplan.emit_build_plan`) tags every
|
|
24
|
+
plan it renders `planPathMode: "tokened"` unconditionally (tan-cli#853), so
|
|
25
|
+
an ABSENT `planPathMode` is no longer "every plan the SDK emits today".
|
|
26
|
+
|
|
27
|
+
tan-cli #89: an unresolved `${TOOLCHAIN_ROOT}` splits into two outcomes
|
|
28
|
+
depending on WHERE it survives substitution. `boardYaml` and
|
|
29
|
+
`sharedArtefacts[]` have no owning slice -- no dispatch seam to route a
|
|
30
|
+
skip/fail decision to -- so they keep the hard `UnresolvedToolchainRoot` this
|
|
31
|
+
pass always raised. A slice field, though, has an owning slice AND an owning
|
|
32
|
+
dispatch seam (`executionPolicy.missingTool`, the same knob a missing
|
|
33
|
+
`bitbake` already uses): this is a HOST-provisioning fact, not a plan/version
|
|
34
|
+
bug, so this pass reports it as a `DemotedSlice` instead of erroring the
|
|
35
|
+
whole plan, leaving the skip-vs-fail call to the caller at dispatch time.
|
|
36
|
+
`LeftoverToken` is unaffected either way -- an unknown token is a
|
|
37
|
+
version/bug fact, never demoted, always plan-fatal.
|
|
38
|
+
|
|
39
|
+
Substituting an unresolved token with an empty string would sail past the
|
|
40
|
+
leftover-token guard and build against the wrong tree -- so every unresolved
|
|
41
|
+
token is REFUSED (or demoted, per the rule above), never degraded to "".
|
|
42
|
+
|
|
43
|
+
Ported field-for-field from `crates/tan-core/src/plan_tokens.rs`
|
|
44
|
+
(`substitute_plan_tokens` / `substitute_slice` / `substitute_command_lenient`
|
|
45
|
+
/ `substitute_artefact` / `substitute_artefact_lenient`), with one field the
|
|
46
|
+
Rust `BuildSlice` does not carry: `slices[].appDir`, which the Python
|
|
47
|
+
`Slice` (`tan.core.build_plan`) does have. It is substituted immediately
|
|
48
|
+
after `buildDir` -- the other slice-level bare path field -- and, like every
|
|
49
|
+
other slice field, participates in TOOLCHAIN_ROOT demotion / LeftoverToken
|
|
50
|
+
the same as its siblings. `appDir` is nullable per the SDK schema (a Yocto
|
|
51
|
+
slice built from the stock-image token has none) -- `None` passes through
|
|
52
|
+
untouched, the same as `command.cwd`.
|
|
53
|
+
"""
|
|
54
|
+
import re
|
|
55
|
+
import sys
|
|
56
|
+
from dataclasses import dataclass, replace
|
|
57
|
+
from typing import Any
|
|
58
|
+
|
|
59
|
+
from tan.core.build_plan import (
|
|
60
|
+
DEFERRED_PLACEHOLDER_NAME,
|
|
61
|
+
BuildPlan,
|
|
62
|
+
Slice,
|
|
63
|
+
SliceCommand,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
PLAN_PATH_MODE_TOKENED = "tokened"
|
|
67
|
+
|
|
68
|
+
TOKEN_SDK_ROOT = "${SDK_ROOT}"
|
|
69
|
+
TOKEN_PROJECT_ROOT = "${PROJECT_ROOT}"
|
|
70
|
+
TOKEN_PYTHON = "${PYTHON}"
|
|
71
|
+
TOKEN_TOOLCHAIN_ROOT = "${TOOLCHAIN_ROOT}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class TokenValues:
|
|
76
|
+
"""tan's already-resolved substitution values. Resolve each exactly ONCE
|
|
77
|
+
for the whole plan -- never re-resolved per-slice.
|
|
78
|
+
|
|
79
|
+
`toolchain_root` is `None` (or blank) when this host has no toolchain
|
|
80
|
+
root resolved at all. Unresolved is NOT an error by itself: every plan
|
|
81
|
+
the SDK emits today names no `${TOOLCHAIN_ROOT}`, and those must keep
|
|
82
|
+
building on a host with no detectable toolchain install -- resolution is
|
|
83
|
+
lazy, the absence only becomes `UnresolvedToolchainRoot` when a plan
|
|
84
|
+
actually uses the token. Blank is folded into "unresolved" on purpose:
|
|
85
|
+
substituting `""` would turn `${TOOLCHAIN_ROOT}/bin/cmake` into the bare
|
|
86
|
+
`/bin/cmake` and sail past the leftover-token guard with nothing left to
|
|
87
|
+
catch -- the same hole `sdk_root` refuses.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
sdk_root: str
|
|
91
|
+
project_root: str
|
|
92
|
+
python: str
|
|
93
|
+
toolchain_root: str | None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#: The artefact kinds a placeholder may stay in, told apart by the artefact's
|
|
97
|
+
#: file name (the only thing a plan carries about it). `alp.conf` and every
|
|
98
|
+
#: other `*.conf` except `local.conf` is a Zephyr Kconfig fragment; `local.conf`
|
|
99
|
+
#: is a Yocto bitbake fragment. A `.cmake` file (`alp-baremetal.cmake`, read by
|
|
100
|
+
#: `-DCMAKE_PROJECT_INCLUDE`) or anything unrecognised is never exempt: CMake
|
|
101
|
+
#: WOULD expand `${NAME}`, to empty or to a host value.
|
|
102
|
+
KIND_KCONFIG = "kconfig"
|
|
103
|
+
KIND_BITBAKE = "bitbake"
|
|
104
|
+
|
|
105
|
+
PLAN_TOKEN_NAMES = frozenset(
|
|
106
|
+
t[2:-1] for t in (TOKEN_SDK_ROOT, TOKEN_PROJECT_ROOT, TOKEN_PYTHON, TOKEN_TOOLCHAIN_ROOT)
|
|
107
|
+
)
|
|
108
|
+
_BRACE_NAME = re.compile(r"\$\{(" + DEFERRED_PLACEHOLDER_NAME.pattern + r")\}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def artefact_kind(path: str) -> str | None:
|
|
112
|
+
"""`KIND_KCONFIG` / `KIND_BITBAKE`, or `None` for a file a placeholder
|
|
113
|
+
must never stay in."""
|
|
114
|
+
name = path.replace("\\", "/").rsplit("/", 1)[-1]
|
|
115
|
+
if name == "local.conf":
|
|
116
|
+
return KIND_BITBAKE
|
|
117
|
+
if name.endswith(".conf"):
|
|
118
|
+
return KIND_KCONFIG
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class DeferredPlaceholder:
|
|
124
|
+
"""A `${NAME}` left standing in a `configArtefacts[*].contents` value on
|
|
125
|
+
purpose (tan-cli#1302). `live_kconfig` is True when it sits on a
|
|
126
|
+
non-comment line of a Kconfig fragment: Kconfig does not expand `${NAME}`,
|
|
127
|
+
so the firmware would carry the literal text."""
|
|
128
|
+
|
|
129
|
+
name: str
|
|
130
|
+
field: str
|
|
131
|
+
live_kconfig: bool = False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _line_is_comment(text: str, start: int) -> bool:
|
|
135
|
+
line_start = text.rfind("\n", 0, start) + 1
|
|
136
|
+
return text[line_start:start].lstrip().startswith("#")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DeferredPolicy:
|
|
140
|
+
"""Which leftover `${NAME}` values a config artefact may keep (tan-cli#1302).
|
|
141
|
+
|
|
142
|
+
`listed` is the plan's own `deferredPlaceholders` (alp-sdk#2696), the
|
|
143
|
+
ONLY source: a name is admitted when the plan lists it, and nothing is
|
|
144
|
+
admitted when the plan carries no such key (`None`) -- every plan tan's
|
|
145
|
+
own planner renders carries one, so an absent key means an older or
|
|
146
|
+
hand-written plan, and that keeps the unresolved-token refusal.
|
|
147
|
+
|
|
148
|
+
`exempted` collects every placeholder admitted, in encounter order -- the
|
|
149
|
+
caller reports them.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, listed: tuple[str, ...] | None) -> None:
|
|
153
|
+
self._listed = frozenset(listed) if listed is not None else None
|
|
154
|
+
self.exempted: list[DeferredPlaceholder] = []
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def has_plan_list(self) -> bool:
|
|
158
|
+
return self._listed is not None
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def hintable(token: str, artefact_path: str) -> bool:
|
|
162
|
+
"""Whether a refusal of `token` in this artefact is one a plan
|
|
163
|
+
`deferredPlaceholders` entry would have cured."""
|
|
164
|
+
m = _BRACE_NAME.fullmatch(token)
|
|
165
|
+
return (
|
|
166
|
+
m is not None
|
|
167
|
+
and m.group(1) not in PLAN_TOKEN_NAMES
|
|
168
|
+
and artefact_kind(artefact_path) is not None
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def admit(self, field: str, token: str, artefact_path: str, text: str, start: int) -> bool:
|
|
172
|
+
"""Record and return True when `token` (a full `${...}` string found
|
|
173
|
+
at `start` in `text`, the contents of the artefact at `artefact_path`)
|
|
174
|
+
may stay."""
|
|
175
|
+
if not self.hintable(token, artefact_path):
|
|
176
|
+
return False
|
|
177
|
+
name = _BRACE_NAME.fullmatch(token).group(1) # type: ignore[union-attr]
|
|
178
|
+
if self._listed is None or name not in self._listed:
|
|
179
|
+
return False
|
|
180
|
+
live = artefact_kind(artefact_path) == KIND_KCONFIG and not _line_is_comment(text, start)
|
|
181
|
+
self.exempted.append(DeferredPlaceholder(name=name, field=field, live_kconfig=live))
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class DemotedSlice:
|
|
187
|
+
"""A slice whose fields still name `${TOOLCHAIN_ROOT}` with no host
|
|
188
|
+
value. Reported, not erred: the CALLER routes it to
|
|
189
|
+
`executionPolicy.missingTool` at dispatch -- this pass has no business
|
|
190
|
+
deciding skip vs fail, only noticing the condition and naming where it
|
|
191
|
+
hit."""
|
|
192
|
+
|
|
193
|
+
slice_index: int
|
|
194
|
+
core_id: str
|
|
195
|
+
field: str
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PlanTokenError(Exception):
|
|
199
|
+
"""Why `substitute_plan_tokens` refused to hand back a plan."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class LeftoverToken(PlanTokenError):
|
|
203
|
+
"""A `${...}`-shaped token survived substitution -- an unknown token (a
|
|
204
|
+
5th token this CLI doesn't resolve), an unterminated `${` (truncation/
|
|
205
|
+
typo), or a plan bug."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, field: str, token: str, deferrable: bool = False) -> None:
|
|
208
|
+
super().__init__(
|
|
209
|
+
f"plan field `{field}` still contains an unresolved token `{token}` after substitution"
|
|
210
|
+
)
|
|
211
|
+
self.field = field
|
|
212
|
+
self.token = token
|
|
213
|
+
#: A config-artefact `${NAME}` that a plan `deferredPlaceholders`
|
|
214
|
+
#: entry would have admitted (tan-cli#1302).
|
|
215
|
+
self.deferrable = deferrable
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class UnresolvedToolchainRoot(PlanTokenError):
|
|
219
|
+
"""The plan names `${TOOLCHAIN_ROOT}` but this host has no toolchain root
|
|
220
|
+
resolved, in a field with no owning slice to demote to."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, field: str) -> None:
|
|
223
|
+
super().__init__(
|
|
224
|
+
f"plan field `{field}` names `{TOKEN_TOOLCHAIN_ROOT}` but no toolchain root is "
|
|
225
|
+
f"resolved on this host"
|
|
226
|
+
)
|
|
227
|
+
self.field = field
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class UnknownPlanPathMode(PlanTokenError):
|
|
231
|
+
"""`planPathMode` is present but isn't the one value this pass knows
|
|
232
|
+
(`"tokened"`)."""
|
|
233
|
+
|
|
234
|
+
def __init__(self, mode: str) -> None:
|
|
235
|
+
super().__init__(f'unknown planPathMode `{mode}` (only "tokened" is defined)')
|
|
236
|
+
self.mode = mode
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def sdk_commit_mismatches(plan_commit: str, resolved_commit: str) -> bool:
|
|
240
|
+
"""The split-brain guard: whether a plan's `sdkCommit` (when present)
|
|
241
|
+
mismatches the resolved SDK checkout's actual HEAD. Compares by common-
|
|
242
|
+
length prefix so a short (`git rev-parse --short HEAD`) and a full
|
|
243
|
+
40-char SHA both compare correctly; case-insensitive. Either side blank
|
|
244
|
+
-- an older plan without `sdkCommit`, or a caller that could not resolve
|
|
245
|
+
`git rev-parse` (no `.git`, `git` missing) -- is "no signal", never a
|
|
246
|
+
mismatch: an SDK checkout with no `.git` (a release tarball) is a
|
|
247
|
+
normal, supported setup."""
|
|
248
|
+
a, b = plan_commit.strip(), resolved_commit.strip()
|
|
249
|
+
if not a or not b:
|
|
250
|
+
return False
|
|
251
|
+
n = min(len(a), len(b))
|
|
252
|
+
return a[:n].lower() != b[:n].lower()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _normalize(path: str) -> str:
|
|
256
|
+
"""Lexically normalize (collapse `.`/`..`, drop empty segments) without
|
|
257
|
+
touching the filesystem -- the Python analogue of tan-core's
|
|
258
|
+
`path_guard::normalize`."""
|
|
259
|
+
posix = path.replace("\\", "/")
|
|
260
|
+
is_absolute = posix.startswith("/")
|
|
261
|
+
parts = posix.split("/")
|
|
262
|
+
out: list[str] = []
|
|
263
|
+
for part in parts:
|
|
264
|
+
if part in ("", "."):
|
|
265
|
+
continue
|
|
266
|
+
if part == "..":
|
|
267
|
+
# Matches Rust's `out.pop()`: a no-op on an empty accumulator,
|
|
268
|
+
# never appends a literal "..".
|
|
269
|
+
if out:
|
|
270
|
+
out.pop()
|
|
271
|
+
continue
|
|
272
|
+
out.append(part)
|
|
273
|
+
result = "/".join(out)
|
|
274
|
+
return f"/{result}" if is_absolute else result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def project_root_diverges_from_exec_base(project_root: str, exec_base: str) -> bool:
|
|
278
|
+
"""Guard 3: whether `${PROJECT_ROOT}` (the resolved `board.yaml`'s
|
|
279
|
+
directory) diverges from the executor's actual base dir. They're the
|
|
280
|
+
same directory only in the default config -- when a plan is tokened,
|
|
281
|
+
substituting `${PROJECT_ROOT}` from one and executing slices under the
|
|
282
|
+
other would silently build against the wrong tree."""
|
|
283
|
+
a, b = _normalize(project_root), _normalize(exec_base)
|
|
284
|
+
if sys.platform.startswith("win"):
|
|
285
|
+
# Lexical normalize doesn't fold drive-letter case, so
|
|
286
|
+
# `--board-yaml e:/...` vs `--project E:/...` -- the same path --
|
|
287
|
+
# would otherwise false-fail this guard.
|
|
288
|
+
return a.lower() != b.lower()
|
|
289
|
+
return a != b
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _resolved_toolchain_root(values: TokenValues) -> str | None:
|
|
293
|
+
return values.toolchain_root if values.toolchain_root else None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _apply(values: TokenValues, raw: str) -> str:
|
|
297
|
+
out = raw.replace(TOKEN_SDK_ROOT, values.sdk_root)
|
|
298
|
+
out = out.replace(TOKEN_PROJECT_ROOT, values.project_root)
|
|
299
|
+
out = out.replace(TOKEN_PYTHON, values.python)
|
|
300
|
+
root = _resolved_toolchain_root(values)
|
|
301
|
+
if root is not None:
|
|
302
|
+
out = out.replace(TOKEN_TOOLCHAIN_ROOT, root)
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _find_brace_token_from(value: str, offset: int) -> tuple[int, str] | None:
|
|
307
|
+
"""First `${...}`-shaped substring in `value` at or after `offset`,
|
|
308
|
+
together with its start offset. An unterminated `${` (no closing `}`)
|
|
309
|
+
still counts and consumes the rest of the string -- nothing after an
|
|
310
|
+
unterminated brace could itself be a well-formed further token."""
|
|
311
|
+
start = value.find("${", offset)
|
|
312
|
+
if start == -1:
|
|
313
|
+
return None
|
|
314
|
+
rest = value[start:]
|
|
315
|
+
end = rest.find("}")
|
|
316
|
+
if end == -1:
|
|
317
|
+
return start, rest
|
|
318
|
+
return start, rest[: end + 1]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _sub_field_lenient(
|
|
322
|
+
field: str,
|
|
323
|
+
raw: str,
|
|
324
|
+
values: TokenValues,
|
|
325
|
+
deferred: DeferredPolicy | None = None,
|
|
326
|
+
artefact_path: str = "",
|
|
327
|
+
) -> tuple[str, bool]:
|
|
328
|
+
"""Substitute `values` into `raw`, then scan the WHOLE result for every
|
|
329
|
+
remaining `${...}`-shaped token -- not just the first: a field can carry
|
|
330
|
+
BOTH an unresolved `${TOOLCHAIN_ROOT}` and a genuinely unknown token, and
|
|
331
|
+
the unknown one must still fail loudly even when it comes second -- an
|
|
332
|
+
unknown token is a version/bug fact and outranks a provisioning fact.
|
|
333
|
+
Returns the substituted string plus whether an unresolved
|
|
334
|
+
`${TOOLCHAIN_ROOT}` was seen; the caller decides whether that's
|
|
335
|
+
plan-fatal (`_sub_field`) or demotable (`_substitute_slice` and its
|
|
336
|
+
helpers)."""
|
|
337
|
+
substituted = _apply(values, raw)
|
|
338
|
+
unresolved_toolchain = False
|
|
339
|
+
offset = 0
|
|
340
|
+
while True:
|
|
341
|
+
found = _find_brace_token_from(substituted, offset)
|
|
342
|
+
if found is None:
|
|
343
|
+
break
|
|
344
|
+
start, token = found
|
|
345
|
+
if token != TOKEN_TOOLCHAIN_ROOT:
|
|
346
|
+
if deferred is not None:
|
|
347
|
+
if deferred.admit(field, token, artefact_path, substituted, start):
|
|
348
|
+
offset = start + len(token)
|
|
349
|
+
continue
|
|
350
|
+
raise LeftoverToken(
|
|
351
|
+
field, token, deferrable=deferred.hintable(token, artefact_path)
|
|
352
|
+
)
|
|
353
|
+
raise LeftoverToken(field, token)
|
|
354
|
+
# Reaching here means values.toolchain_root is unresolved: `_apply`
|
|
355
|
+
# already replaced every occurrence when a value WAS resolved.
|
|
356
|
+
unresolved_toolchain = True
|
|
357
|
+
offset = start + len(token)
|
|
358
|
+
return substituted, unresolved_toolchain
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _sub_field(field: str, raw: str, values: TokenValues) -> str:
|
|
362
|
+
"""Plan-level sites (`boardYaml`, `sharedArtefacts[]`) have no owning
|
|
363
|
+
slice to route a missing-toolchain skip to, so they keep the hard,
|
|
364
|
+
byte-identical `UnresolvedToolchainRoot` this pass always raised."""
|
|
365
|
+
substituted, unresolved_toolchain = _sub_field_lenient(field, raw, values)
|
|
366
|
+
if unresolved_toolchain:
|
|
367
|
+
raise UnresolvedToolchainRoot(field)
|
|
368
|
+
return substituted
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _record_first(field: str, unresolved: bool, current: str | None) -> str | None:
|
|
372
|
+
"""Keep only the FIRST unresolved-toolchain field name; every field is
|
|
373
|
+
still substituted (and LeftoverToken-scanned) regardless, so a bug in a
|
|
374
|
+
LATER field of an already-demoted slice is never masked."""
|
|
375
|
+
return field if unresolved and current is None else current
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _substitute_artefact(field: str, art: dict[str, Any], values: TokenValues) -> dict[str, Any]:
|
|
379
|
+
out = dict(art)
|
|
380
|
+
out["path"] = _sub_field(f"{field}.path", art["path"], values)
|
|
381
|
+
out["contents"] = _sub_field(f"{field}.contents", art["contents"], values)
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _substitute_artefact_lenient(
|
|
386
|
+
field: str,
|
|
387
|
+
art: dict[str, Any],
|
|
388
|
+
values: TokenValues,
|
|
389
|
+
deferred: DeferredPolicy | None = None,
|
|
390
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
391
|
+
"""The slice-owned variant of `_substitute_artefact`: `configArtefacts`
|
|
392
|
+
live inside a slice, so an unresolved `${TOOLCHAIN_ROOT}` in one is
|
|
393
|
+
demotable too (the whole artefact list is stripped from a demoted
|
|
394
|
+
slice's output plan by the caller) -- a `${UNKNOWN}`, though, is still
|
|
395
|
+
`LeftoverToken`, scanned for here BEFORE the caller ever gets a chance to
|
|
396
|
+
strip the list."""
|
|
397
|
+
demoted_field: str | None = None
|
|
398
|
+
|
|
399
|
+
path_field = f"{field}.path"
|
|
400
|
+
path_sub, path_unresolved = _sub_field_lenient(path_field, art["path"], values)
|
|
401
|
+
demoted_field = _record_first(path_field, path_unresolved, demoted_field)
|
|
402
|
+
|
|
403
|
+
contents_field = f"{field}.contents"
|
|
404
|
+
contents_sub, contents_unresolved = _sub_field_lenient(
|
|
405
|
+
contents_field, art["contents"], values, deferred, path_sub
|
|
406
|
+
)
|
|
407
|
+
demoted_field = _record_first(contents_field, contents_unresolved, demoted_field)
|
|
408
|
+
|
|
409
|
+
out = dict(art)
|
|
410
|
+
out["path"] = path_sub
|
|
411
|
+
out["contents"] = contents_sub
|
|
412
|
+
return out, demoted_field
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _substitute_command_lenient(
|
|
416
|
+
base: str, cmd: SliceCommand, values: TokenValues
|
|
417
|
+
) -> tuple[SliceCommand, str | None]:
|
|
418
|
+
"""`base` is the plan path of the command being substituted --
|
|
419
|
+
`slices[N].command` for the slice's own, `slices[N].postCommands[M]` for
|
|
420
|
+
one of its post-build steps (tan-cli#550). Parameterised rather than
|
|
421
|
+
duplicated so a post-build step's `${SDK_ROOT}` / `${TOOLCHAIN_ROOT}` /
|
|
422
|
+
`${UNKNOWN}` is substituted, demoted and refused by exactly the same rules
|
|
423
|
+
as the slice's own command: those steps are spawned by the same executor,
|
|
424
|
+
so a token this pass left standing in one would be handed to
|
|
425
|
+
`subprocess` verbatim."""
|
|
426
|
+
demoted_field: str | None = None
|
|
427
|
+
|
|
428
|
+
new_cwd = cmd.cwd
|
|
429
|
+
if new_cwd is not None:
|
|
430
|
+
cwd_field = f"{base}.cwd"
|
|
431
|
+
new_cwd, cwd_unresolved = _sub_field_lenient(cwd_field, new_cwd, values)
|
|
432
|
+
demoted_field = _record_first(cwd_field, cwd_unresolved, demoted_field)
|
|
433
|
+
|
|
434
|
+
new_args: list[str] = []
|
|
435
|
+
for j, arg in enumerate(cmd.args):
|
|
436
|
+
field = f"{base}.args[{j}]"
|
|
437
|
+
sub, unresolved = _sub_field_lenient(field, arg, values)
|
|
438
|
+
new_args.append(sub)
|
|
439
|
+
demoted_field = _record_first(field, unresolved, demoted_field)
|
|
440
|
+
|
|
441
|
+
return replace(cmd, cwd=new_cwd, args=new_args), demoted_field
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _substitute_slice(
|
|
445
|
+
i: int, sl: Slice, values: TokenValues, deferred: DeferredPolicy | None = None
|
|
446
|
+
) -> tuple[Slice, str | None]:
|
|
447
|
+
"""Substitute every field of one slice, leniently for
|
|
448
|
+
`${TOOLCHAIN_ROOT}`: returns the first field that still names it
|
|
449
|
+
unresolved, if any, so the caller can build a `DemotedSlice` -- but a
|
|
450
|
+
`${UNKNOWN}` anywhere in the slice still raises `LeftoverToken`
|
|
451
|
+
immediately, ending the scan right there.
|
|
452
|
+
|
|
453
|
+
Field order mirrors `substitute_slice` in the Rust oracle exactly
|
|
454
|
+
(`buildDir`, then `configArtefacts`, then `env`, then `envAppendPath`,
|
|
455
|
+
then `command`), with `appDir` -- a field the Rust `BuildSlice` doesn't
|
|
456
|
+
carry -- inserted right after `buildDir`, the other bare slice-level path
|
|
457
|
+
field, and `postCommands` -- a field the oracle predates entirely
|
|
458
|
+
(alp-sdk #1344) -- last, immediately after the `command` it follows on the
|
|
459
|
+
wire and in execution order."""
|
|
460
|
+
demoted_field: str | None = None
|
|
461
|
+
|
|
462
|
+
build_dir_field = f"slices[{i}].buildDir"
|
|
463
|
+
build_dir, unresolved = _sub_field_lenient(build_dir_field, sl.build_dir, values)
|
|
464
|
+
demoted_field = _record_first(build_dir_field, unresolved, demoted_field)
|
|
465
|
+
|
|
466
|
+
# appDir is nullable (a Yocto slice built from the stock-image token has
|
|
467
|
+
# none) -- guarded the same shape as command.cwd below, not substituted
|
|
468
|
+
# when absent.
|
|
469
|
+
app_dir = sl.app_dir
|
|
470
|
+
if app_dir is not None:
|
|
471
|
+
app_dir_field = f"slices[{i}].appDir"
|
|
472
|
+
app_dir, unresolved = _sub_field_lenient(app_dir_field, app_dir, values)
|
|
473
|
+
demoted_field = _record_first(app_dir_field, unresolved, demoted_field)
|
|
474
|
+
|
|
475
|
+
new_artefacts: list[dict[str, Any]] = []
|
|
476
|
+
for j, art in enumerate(sl.config_artefacts):
|
|
477
|
+
base = f"slices[{i}].configArtefacts[{j}]"
|
|
478
|
+
new_art, art_demoted = _substitute_artefact_lenient(base, art, values, deferred)
|
|
479
|
+
new_artefacts.append(new_art)
|
|
480
|
+
if art_demoted is not None:
|
|
481
|
+
demoted_field = _record_first(art_demoted, True, demoted_field)
|
|
482
|
+
|
|
483
|
+
new_env: dict[str, str] = {}
|
|
484
|
+
for key, value in sorted(sl.env.items()):
|
|
485
|
+
field = f"slices[{i}].env.{key}"
|
|
486
|
+
sub, unresolved = _sub_field_lenient(field, value, values)
|
|
487
|
+
new_env[key] = sub
|
|
488
|
+
demoted_field = _record_first(field, unresolved, demoted_field)
|
|
489
|
+
|
|
490
|
+
new_env_append: dict[str, list[str]] = {}
|
|
491
|
+
for key, values_list in sorted(sl.env_append_path.items()):
|
|
492
|
+
new_list: list[str] = []
|
|
493
|
+
for k, value in enumerate(values_list):
|
|
494
|
+
field = f"slices[{i}].envAppendPath.{key}[{k}]"
|
|
495
|
+
sub, unresolved = _sub_field_lenient(field, value, values)
|
|
496
|
+
new_list.append(sub)
|
|
497
|
+
demoted_field = _record_first(field, unresolved, demoted_field)
|
|
498
|
+
new_env_append[key] = new_list
|
|
499
|
+
|
|
500
|
+
new_command = sl.command
|
|
501
|
+
if new_command is not None:
|
|
502
|
+
new_command, cmd_demoted = _substitute_command_lenient(
|
|
503
|
+
f"slices[{i}].command", new_command, values
|
|
504
|
+
)
|
|
505
|
+
if cmd_demoted is not None:
|
|
506
|
+
demoted_field = _record_first(cmd_demoted, True, demoted_field)
|
|
507
|
+
|
|
508
|
+
# tan-cli#550: the post-build steps are spawned by the same executor as
|
|
509
|
+
# `command`, so they are substituted by the same pass. Skipping them
|
|
510
|
+
# would hand `cmake --build ${PROJECT_ROOT}/...` to `subprocess` with the
|
|
511
|
+
# literal token still in it -- the executor has no second substitution
|
|
512
|
+
# pass to catch it, and neither the LeftoverToken nor the
|
|
513
|
+
# ${TOOLCHAIN_ROOT}-demotion guard would ever see the field.
|
|
514
|
+
new_post: list[SliceCommand] = []
|
|
515
|
+
for j, step in enumerate(sl.post_commands):
|
|
516
|
+
new_step, step_demoted = _substitute_command_lenient(
|
|
517
|
+
f"slices[{i}].postCommands[{j}]", step, values
|
|
518
|
+
)
|
|
519
|
+
new_post.append(new_step)
|
|
520
|
+
if step_demoted is not None:
|
|
521
|
+
demoted_field = _record_first(step_demoted, True, demoted_field)
|
|
522
|
+
|
|
523
|
+
new_slice = replace(
|
|
524
|
+
sl,
|
|
525
|
+
build_dir=build_dir,
|
|
526
|
+
app_dir=app_dir,
|
|
527
|
+
config_artefacts=new_artefacts,
|
|
528
|
+
env=new_env,
|
|
529
|
+
env_append_path=new_env_append,
|
|
530
|
+
command=new_command,
|
|
531
|
+
post_commands=tuple(new_post),
|
|
532
|
+
)
|
|
533
|
+
return new_slice, demoted_field
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def substitute_plan_tokens(
|
|
537
|
+
plan: BuildPlan, values: TokenValues, deferred: DeferredPolicy | None = None
|
|
538
|
+
) -> tuple[BuildPlan, list[DemotedSlice]]:
|
|
539
|
+
"""ONE blind string-substitution pass over every path-bearing string
|
|
540
|
+
field of `plan`, swapping the four literal tokens for `values`. A no-op
|
|
541
|
+
-- byte-identical, empty demotion list -- when `plan.plan_path_mode` is
|
|
542
|
+
absent. `UnknownPlanPathMode` when it's present but not exactly
|
|
543
|
+
`"tokened"`.
|
|
544
|
+
|
|
545
|
+
After substitution, any leftover `${...}`-shaped token anywhere touched
|
|
546
|
+
fails the whole pass loudly -- EXCEPT a leftover `${TOOLCHAIN_ROOT}`
|
|
547
|
+
confined to a slice's own fields, which is reported via the returned
|
|
548
|
+
`DemotedSlice` list and has its `configArtefacts` stripped (nothing will
|
|
549
|
+
ever consume them this run), but the plan as a whole still succeeds. The
|
|
550
|
+
same token surviving in `boardYaml`/`sharedArtefacts[]` (no owning
|
|
551
|
+
slice) is still the hard `UnresolvedToolchainRoot` this pass always
|
|
552
|
+
raised.
|
|
553
|
+
|
|
554
|
+
`deferred` (tan-cli#1302) lets a leftover `${NAME}` that the policy admits
|
|
555
|
+
stay, byte-identical, in a slice's `configArtefacts[*].contents` -- and
|
|
556
|
+
nowhere else; `deferred.exempted` reports them. A demoted slice's
|
|
557
|
+
admissions are dropped with its stripped artefacts.
|
|
558
|
+
|
|
559
|
+
Ordering matches the Rust oracle: `boardYaml` first (hard site), then
|
|
560
|
+
every slice in order (each slice's own `configArtefacts` substituted
|
|
561
|
+
-- and stripped on demotion -- WITH it), then `sharedArtefacts` last
|
|
562
|
+
(hard site, cross-slice)."""
|
|
563
|
+
if plan.plan_path_mode is None:
|
|
564
|
+
# A fresh top-level object, matching Rust's `plan.clone()` -- returning
|
|
565
|
+
# `plan` itself would let a downstream mutation of the "output" plan
|
|
566
|
+
# silently alias back into the caller's input.
|
|
567
|
+
return replace(plan), []
|
|
568
|
+
if plan.plan_path_mode != PLAN_PATH_MODE_TOKENED:
|
|
569
|
+
raise UnknownPlanPathMode(plan.plan_path_mode)
|
|
570
|
+
|
|
571
|
+
# Hard site 1/2: no owning slice to demote to.
|
|
572
|
+
board_yaml = _sub_field("boardYaml", plan.board_yaml, values)
|
|
573
|
+
|
|
574
|
+
demoted: list[DemotedSlice] = []
|
|
575
|
+
new_slices: list[Slice] = []
|
|
576
|
+
for i, sl in enumerate(plan.slices):
|
|
577
|
+
mark = len(deferred.exempted) if deferred is not None else 0
|
|
578
|
+
new_slice, demoted_field = _substitute_slice(i, sl, values, deferred)
|
|
579
|
+
if demoted_field is not None:
|
|
580
|
+
if deferred is not None:
|
|
581
|
+
del deferred.exempted[mark:]
|
|
582
|
+
demoted.append(DemotedSlice(slice_index=i, core_id=sl.core_id, field=demoted_field))
|
|
583
|
+
# Strip AFTER the slice's fields (including these artefacts' own
|
|
584
|
+
# contents) have been fully scanned -- a ${UNKNOWN} inside a
|
|
585
|
+
# demoted slice's configArtefact contents must still hard-fail
|
|
586
|
+
# as LeftoverToken before it is ever cleared here.
|
|
587
|
+
new_slice = replace(new_slice, config_artefacts=[])
|
|
588
|
+
new_slices.append(new_slice)
|
|
589
|
+
|
|
590
|
+
# Hard site 2/2: sharedArtefacts are cross-slice -- same "no owning
|
|
591
|
+
# slice" reasoning as boardYaml above. Substituted AFTER all slices.
|
|
592
|
+
new_shared = [
|
|
593
|
+
_substitute_artefact(f"sharedArtefacts[{i}]", art, values)
|
|
594
|
+
for i, art in enumerate(plan.shared_artefacts)
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
replace(plan, board_yaml=board_yaml, slices=new_slices, shared_artefacts=new_shared),
|
|
599
|
+
demoted,
|
|
600
|
+
)
|