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,204 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Whether a SoM's hw_rev -- explicit, or (absent that) its DEFAULT -- is
|
|
3
|
+
buildable, read directly off the alp-sdk metadata tree, without importing
|
|
4
|
+
`tan.planner` (see `tan/core/example_catalog.py`'s own docstring for why:
|
|
5
|
+
`tan.planner.paths` binds `REPO = sdk_root()` at import time, so pulling it
|
|
6
|
+
into `init_cmd` before an SDK is even resolved raises `PlannerRootError`).
|
|
7
|
+
|
|
8
|
+
tan-cli#743. `tan init --template minimal-app --som <sku>` for a SoM whose
|
|
9
|
+
default hw_rev is not buildable (a `reserved`/`tbd` revision) exits 0 and scaffolds a project whose FIRST `tan validate`
|
|
10
|
+
hard-errors::
|
|
11
|
+
|
|
12
|
+
sdk-compat: SoM <sku> hw_rev '<rev>' exists but is not buildable
|
|
13
|
+
(status: 'tbd').
|
|
14
|
+
|
|
15
|
+
`init` never writes an explicit `hw_rev:` into a scaffolded `minimal-app`/
|
|
16
|
+
vendored-template board.yaml, so the value `validate` refuses there is the
|
|
17
|
+
one the SDK loader falls back to -- `hw_rev or
|
|
18
|
+
som_preset.get("default_hw_rev")` (`scripts/alp_orchestrate/loader.py:1241`
|
|
19
|
+
and siblings) -- i.e. the SoM preset's OWN `default_hw_rev:`. `validate` is
|
|
20
|
+
not wrong here: the fact is real and correctly published (the family's
|
|
21
|
+
`metadata/e1m_modules/<family>/hw-revisions.yaml` `<rev>: {status: tbd}`). The
|
|
22
|
+
defect is that `init` stays silent about a fact it could have read from the
|
|
23
|
+
exact same preset it already resolved -- so this module reads that one fact
|
|
24
|
+
(the SKU's effective hw_rev + the family's declared status for it) so `init`
|
|
25
|
+
can warn about it instead.
|
|
26
|
+
|
|
27
|
+
tan-cli#1008 review majors 1+2: an `--from-example`/`--topology` copy can
|
|
28
|
+
carry an EXPLICIT `hw_rev:` of its own (`tan/core/scaffold.vendored_som`
|
|
29
|
+
reads it off the planned board.yaml) -- the original example's own revision,
|
|
30
|
+
or one that survived an INTRA-family retarget (`retarget_board_yaml_som`
|
|
31
|
+
only drops the sibling `hw_rev:` on a CROSS-family retarget -- review round
|
|
32
|
+
4 -- since within one family the value is still a real, declared revision
|
|
33
|
+
for the new SKU too). `hw_rev_not_buildable`'s caller passes that explicit
|
|
34
|
+
value through when there is one, so the pair this module judges (and the
|
|
35
|
+
warning names) is always the one actually written to disk, never a value
|
|
36
|
+
inferred from `--som`/`DEFAULT_SOM_SKU` alone.
|
|
37
|
+
|
|
38
|
+
WHY A WARNING AND NOT A REFUSAL: `minimal-app` is deliberately the one
|
|
39
|
+
template every OTHER template's own refusal message names as the escape
|
|
40
|
+
hatch -- "Use --template minimal-app, which is vendor-neutral and scaffolds
|
|
41
|
+
any SoM" (`tan/core/scaffold.py`'s vendored-scaffold refusal text). If this
|
|
42
|
+
check also refused, a SoM in `status: preliminary` would have no self-contained
|
|
43
|
+
scaffolding path at all -- worse than the silent defect it replaces. This
|
|
44
|
+
mirrors `example_catalog.unsupported_som`'s own precedent (warn, still
|
|
45
|
+
write) for the identical reason: refusing there "made `tan init
|
|
46
|
+
--from-example` unusable for nearly the whole AEN family, which is worse
|
|
47
|
+
than the original defect".
|
|
48
|
+
|
|
49
|
+
Deliberately duplicates the tiny SKU -> family-directory map
|
|
50
|
+
`scripts/alp_project_loader._sku_family` also carries (`AEN`/`V2N`/`V2M`
|
|
51
|
+
-> `aen`/`v2n`/`v2n-m1`) and the buildable-status set
|
|
52
|
+
`scripts/alp_orchestrate/sdk_compat._NOT_BUILDABLE_STATUSES` (`reserved`,
|
|
53
|
+
`tbd`, or a missing `status:` key) rather than importing either: neither
|
|
54
|
+
module is on `sys.path` for a `tan init --template` invocation, which reads
|
|
55
|
+
`--sdk-root` only as a metadata SOURCE, never as an importable package.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import re
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Optional
|
|
63
|
+
|
|
64
|
+
#: Mirrors `scripts/alp_project_loader._SKU_FAMILY` / `_sku_family` verbatim.
|
|
65
|
+
_SKU_FAMILY = re.compile(r"^E1M-(AEN|V2N|V2M)")
|
|
66
|
+
_FAMILY_DIR = {"AEN": "aen", "V2N": "v2n", "V2M": "v2n-m1"}
|
|
67
|
+
|
|
68
|
+
#: Mirrors `scripts/alp_orchestrate/sdk_compat._NOT_BUILDABLE_STATUSES`
|
|
69
|
+
#: verbatim (alp-sdk #1025): `reserved`, `tbd`, and a missing `status:` key
|
|
70
|
+
#: are all refused; every other declared status is buildable.
|
|
71
|
+
_NOT_BUILDABLE_STATUSES = frozenset({"reserved", "tbd"})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HwRevNotBuildable:
|
|
75
|
+
"""The hw_rev a scaffolded board.yaml resolves to (its own explicit
|
|
76
|
+
`hw_rev:`, or -- absent that -- the SoM preset's `default_hw_rev:`), and
|
|
77
|
+
why `tan validate` will refuse it.
|
|
78
|
+
|
|
79
|
+
`has_buildable_alternative` -- tan-cli#1008 review minor -- is whether
|
|
80
|
+
the SAME family table declares at least one OTHER hw_rev whose status
|
|
81
|
+
permits a build: the "or until board.yaml names a buildable `hw_rev:`
|
|
82
|
+
explicitly" remedy is only real advice when this is true. A family that
|
|
83
|
+
publishes exactly one hw_rev, and that one not buildable, would make the
|
|
84
|
+
clause advice that reproduces the identical refusal, rc 2, every time."""
|
|
85
|
+
|
|
86
|
+
__slots__ = ("sku", "hw_rev", "status", "has_buildable_alternative")
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self, sku: str, hw_rev: str, status: Optional[str], has_buildable_alternative: bool
|
|
90
|
+
) -> None:
|
|
91
|
+
self.sku = sku
|
|
92
|
+
self.hw_rev = hw_rev
|
|
93
|
+
self.status = status
|
|
94
|
+
self.has_buildable_alternative = has_buildable_alternative
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _safe_load_mapping(path: Path) -> Optional[dict]:
|
|
98
|
+
"""`yaml.safe_load(path)`, or None for every reason a caller here must
|
|
99
|
+
treat as "nothing to check" rather than an error: absent, unreadable,
|
|
100
|
+
unparseable, or parsed to something other than a mapping.
|
|
101
|
+
|
|
102
|
+
Imports PyYAML lazily, inside the call, not at module scope: this module
|
|
103
|
+
is reached from `init_cmd`, which `tan/cli.py` static-imports on EVERY
|
|
104
|
+
invocation, and `tests/gates/test_cli_import_is_lean.py` (tan-cli#810)
|
|
105
|
+
pins PyYAML as loaded only by `new_som_cmd`/`kconfig_cmd`'s single-use
|
|
106
|
+
paths -- a module-scope `import yaml` here would load it on `tan
|
|
107
|
+
--version` too.
|
|
108
|
+
|
|
109
|
+
tan-cli#1116 review round 2: `UnicodeDecodeError` is caught alongside
|
|
110
|
+
`OSError` -- it is a `ValueError`, not an `OSError`, so a non-UTF-8
|
|
111
|
+
`hw-revisions.yaml` used to raise raw past `hw_rev_not_buildable`'s own
|
|
112
|
+
written "Never raises" contract (measured escaping through that real
|
|
113
|
+
caller before this fix).
|
|
114
|
+
"""
|
|
115
|
+
import yaml
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
text = path.read_text(encoding="utf-8")
|
|
119
|
+
except (OSError, UnicodeDecodeError):
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
doc = yaml.safe_load(text)
|
|
123
|
+
except yaml.YAMLError:
|
|
124
|
+
return None
|
|
125
|
+
return doc if isinstance(doc, dict) else None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def hw_rev_not_buildable(
|
|
129
|
+
sdk_root: Path, sku: str, hw_rev: Optional[str] = None
|
|
130
|
+
) -> Optional[HwRevNotBuildable]:
|
|
131
|
+
"""Whether `hw_rev` -- or, when `None`/empty, `sku`'s own
|
|
132
|
+
`default_hw_rev:` -- is buildable per its family's `hw-revisions.yaml`.
|
|
133
|
+
Returns None for "nothing to report" -- covering every reason a caller
|
|
134
|
+
here must NOT warn over, deliberately mirroring
|
|
135
|
+
`example_catalog.unsupported_som`'s own contract:
|
|
136
|
+
|
|
137
|
+
* `sku` doesn't match the known SoM SKU pattern at all,
|
|
138
|
+
* no SoM preset file for `sku` in this checkout,
|
|
139
|
+
* `hw_rev` is `None`/empty and the preset declares no (or a malformed)
|
|
140
|
+
`default_hw_rev:` to fall back to,
|
|
141
|
+
* no family `hw-revisions.yaml` table (an in-development family with
|
|
142
|
+
nothing to check against yet),
|
|
143
|
+
* the family table doesn't declare `hw_rev` as a key at all --
|
|
144
|
+
existence is `revision_known()`'s question
|
|
145
|
+
(`tan/planner/sdk_compat.py`), not this one's; an unknown revision
|
|
146
|
+
has no status to have read, so there is nothing here to judge,
|
|
147
|
+
* the declared status permits a build.
|
|
148
|
+
|
|
149
|
+
Never raises: a scaffold must not fail because a metadata file could
|
|
150
|
+
not be read.
|
|
151
|
+
"""
|
|
152
|
+
match = _SKU_FAMILY.match(sku)
|
|
153
|
+
family_dir = _FAMILY_DIR.get(match.group(1)) if match else None
|
|
154
|
+
if family_dir is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
preset = _safe_load_mapping(sdk_root / "metadata" / "e1m_modules" / f"{sku}.yaml")
|
|
158
|
+
if preset is None:
|
|
159
|
+
return None
|
|
160
|
+
if not hw_rev:
|
|
161
|
+
hw_rev = preset.get("default_hw_rev")
|
|
162
|
+
if not isinstance(hw_rev, str) or not hw_rev:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
table = _safe_load_mapping(
|
|
166
|
+
sdk_root / "metadata" / "e1m_modules" / family_dir / "hw-revisions.yaml"
|
|
167
|
+
)
|
|
168
|
+
if table is None:
|
|
169
|
+
return None
|
|
170
|
+
revisions = table.get("hw_revisions")
|
|
171
|
+
if not isinstance(revisions, dict):
|
|
172
|
+
return None
|
|
173
|
+
entry = revisions.get(hw_rev)
|
|
174
|
+
if not isinstance(entry, dict):
|
|
175
|
+
# Mirrors `alp_orchestrate/sdk_compat.revision_buildable` verbatim: a
|
|
176
|
+
# key present but not a dict (None, a bare string, ...) is a
|
|
177
|
+
# malformed entry, not an absent one -- it has no status, so it is
|
|
178
|
+
# not buildable. Only a truly absent key stays "nothing to judge"
|
|
179
|
+
# (unknown, `revision_known()`'s question).
|
|
180
|
+
if hw_rev not in revisions:
|
|
181
|
+
return None
|
|
182
|
+
return HwRevNotBuildable(sku, hw_rev, None, _has_other_buildable(revisions, hw_rev))
|
|
183
|
+
|
|
184
|
+
status = entry.get("status")
|
|
185
|
+
if isinstance(status, str) and status not in _NOT_BUILDABLE_STATUSES:
|
|
186
|
+
return None
|
|
187
|
+
return HwRevNotBuildable(
|
|
188
|
+
sku, hw_rev, status if isinstance(status, str) else None,
|
|
189
|
+
_has_other_buildable(revisions, hw_rev),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _has_other_buildable(revisions: dict, hw_rev: str) -> bool:
|
|
194
|
+
"""Whether `revisions` (the family's `hw_revisions:` map) declares any
|
|
195
|
+
key OTHER than `hw_rev` whose `status:` permits a build -- the fact that
|
|
196
|
+
decides whether "or name a buildable `hw_rev:` explicitly" is real
|
|
197
|
+
advice or a dead end (tan-cli#1008 review minor)."""
|
|
198
|
+
return any(
|
|
199
|
+
other_rev != hw_rev
|
|
200
|
+
and isinstance(other_entry, dict)
|
|
201
|
+
and isinstance(other_entry.get("status"), str)
|
|
202
|
+
and other_entry.get("status") not in _NOT_BUILDABLE_STATUSES
|
|
203
|
+
for other_rev, other_entry in revisions.items()
|
|
204
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The one SoM-preset `schema_version` this tan reads, and the message for a
|
|
3
|
+
preset it skips because it declares another.
|
|
4
|
+
|
|
5
|
+
alp-sdk#2024 moved every `metadata/e1m_modules/*.yaml` preset to
|
|
6
|
+
`schema_version: 2` (`som-preset-v2.schema.json`, `write_authority` required,
|
|
7
|
+
`npu_population` removed). tan-cli#1297 moved tan's readers with it, and every
|
|
8
|
+
one of them refuses a preset declaring anything else rather than guessing at a
|
|
9
|
+
shape it does not know. Against an SDK that predates #2024 (released alp-sdk
|
|
10
|
+
`v0.16.0` and older) that refusal reaches EVERY preset, so `tan presets`
|
|
11
|
+
listed `skus: []` and `tan size` reported every budget as `unreadable SoM
|
|
12
|
+
preset`, with nothing saying why. `skipped_presets_message` is the why: one
|
|
13
|
+
sentence, shared by both commands so they cannot word the same fact two ways.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
|
|
20
|
+
#: The one `schema_version:` a SoM preset may declare.
|
|
21
|
+
#: `som-preset-v2.schema.json` pins it `const: 2`.
|
|
22
|
+
SOM_SCHEMA_VERSION = 2
|
|
23
|
+
|
|
24
|
+
#: The upstream change that introduced v2, named in the message so a reader
|
|
25
|
+
#: can tell "old SDK" from "broken preset".
|
|
26
|
+
_V2_ORIGIN = "alp-sdk#2024"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_supported_som_schema_version(version: object) -> bool:
|
|
30
|
+
"""True only for the bare integer `SOM_SCHEMA_VERSION`. `bool` is excluded
|
|
31
|
+
explicitly: `True == 1` and `isinstance(True, int)` both hold in Python."""
|
|
32
|
+
return isinstance(version, int) and not isinstance(version, bool) and version == SOM_SCHEMA_VERSION
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _remedy(versions: list[object]) -> str:
|
|
36
|
+
ints = [v for v in versions if isinstance(v, int) and not isinstance(v, bool)]
|
|
37
|
+
if ints and len(ints) == len(versions) and max(ints) < SOM_SCHEMA_VERSION:
|
|
38
|
+
return (
|
|
39
|
+
f"the bound alp-sdk predates som-preset v{SOM_SCHEMA_VERSION} "
|
|
40
|
+
f"({_V2_ORIGIN}); point --sdk-root at an alp-sdk whose "
|
|
41
|
+
f"metadata/schemas/ ships som-preset-v{SOM_SCHEMA_VERSION}.schema.json, "
|
|
42
|
+
"or use a tan release that matches this SDK"
|
|
43
|
+
)
|
|
44
|
+
if ints and len(ints) == len(versions) and min(ints) > SOM_SCHEMA_VERSION:
|
|
45
|
+
return "the bound alp-sdk is newer than this tan; upgrade tan"
|
|
46
|
+
return "fix the preset's schema_version or use an alp-sdk whose presets match this tan"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def skipped_presets_message(subject: str, versions: Iterable[object]) -> str:
|
|
50
|
+
"""`<subject> declare(s) schema_version <v[, v...]> (this tan reads
|
|
51
|
+
som-preset schema_version 2): <remedy>.`
|
|
52
|
+
|
|
53
|
+
*subject* names what was skipped ("12 SoM presets under <dir>", "the SoM
|
|
54
|
+
preset for E1M-AEN801"); *versions* is every skipped preset's declared
|
|
55
|
+
value, duplicates allowed -- they are collapsed, in first-seen order, and
|
|
56
|
+
an absent key reads as `None`, exactly what the preset said."""
|
|
57
|
+
seen = list(versions)
|
|
58
|
+
shown = ", ".join(dict.fromkeys(repr(v) for v in seen))
|
|
59
|
+
return (
|
|
60
|
+
f"{subject}: schema_version {shown} (this tan reads "
|
|
61
|
+
f"som-preset schema_version {SOM_SCHEMA_VERSION}) -- {_remedy(seen)}."
|
|
62
|
+
)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The one shared rule for building a subprocess child's environment.
|
|
3
|
+
|
|
4
|
+
**Every spawn site under `python/tan/` must route its `env=` through
|
|
5
|
+
[`spawn_env`] (or, for a caller that already owns its own mutable base dict,
|
|
6
|
+
[`restore_ld_library_path`] directly) rather than build one by hand.**
|
|
7
|
+
tan-cli#992 is what happens when that is left to 25 independent call sites
|
|
8
|
+
instead: one gets it right and the other 24 leak.
|
|
9
|
+
|
|
10
|
+
## The rule
|
|
11
|
+
|
|
12
|
+
PyInstaller's Linux ONEDIR bootloader points `LD_LIBRARY_PATH` at the frozen
|
|
13
|
+
app's own bundled `_internal/` lib dir -- so the FROZEN `tan` binary finds its
|
|
14
|
+
OWN bundled shared libraries -- and stashes the caller's original value in
|
|
15
|
+
`LD_LIBRARY_PATH_ORIG` before doing so. Every child process `tan` spawns
|
|
16
|
+
(`west`, `git`, `dxcom`, a J-Link tool, and everything THOSE spawn in turn) is
|
|
17
|
+
a SYSTEM program, never a bundled one, so unless the spawn site restores
|
|
18
|
+
`LD_LIBRARY_PATH_ORIG` before spawning, that child inherits tan's bundled path
|
|
19
|
+
ahead of the system one and can silently resolve one of tan's own bundled
|
|
20
|
+
shared libraries instead of the host's -- only surfacing when the two
|
|
21
|
+
versions actually disagree in a way that breaks something.
|
|
22
|
+
|
|
23
|
+
Measured, tan-cli#990/#992, on real CI: `tan bootstrap` spawned `west sdk
|
|
24
|
+
install`, which shelled `tar --xz`, which loaded tan's bundled
|
|
25
|
+
`liblzma.so.5` (older than the host's) ahead of the system one and failed
|
|
26
|
+
outright:
|
|
27
|
+
|
|
28
|
+
xz: /home/runner/.local/bin/tan-cli-lib/_internal/liblzma.so.5:
|
|
29
|
+
version `XZ_5.4' not found (required by xz)
|
|
30
|
+
/usr/bin/tar: Child returned status 1
|
|
31
|
+
|
|
32
|
+
`LD_LIBRARY_PATH_ORIG` is the PREFERRED trigger: it is PyInstaller's own
|
|
33
|
+
marker that its bootloader modified `LD_LIBRARY_PATH` for THIS process, and
|
|
34
|
+
it carries the exact value to restore.
|
|
35
|
+
|
|
36
|
+
**It is not the ONLY trigger, and tan-cli#1189 is what that cost.** Until
|
|
37
|
+
then this module treated an absent `ORIG` as proof that no override had
|
|
38
|
+
happened -- "a frozen run whose `LD_LIBRARY_PATH_ORIG` is absent" was listed
|
|
39
|
+
as a host that never had the problem. Measured on a bare `ubuntu:24.04`, the
|
|
40
|
+
identical `liblzma.so.5` failure came back with tan-cli#992's fix in place,
|
|
41
|
+
so on that host the restore did not fire. A host with no `LD_LIBRARY_PATH`
|
|
42
|
+
of its own is precisely the host where the bundle becomes the ONLY entry on
|
|
43
|
+
the child's search path -- the worst case, not the safe one.
|
|
44
|
+
|
|
45
|
+
So the second trigger is the fact that does not depend on the bootloader
|
|
46
|
+
having recorded anything: is the bundle dir ([`_frozen_bundle_dir`], i.e.
|
|
47
|
+
`sys._MEIPASS` on a frozen run) currently ON `LD_LIBRARY_PATH`? Both
|
|
48
|
+
triggers are still false on `python -m tan` from source (this repo's own
|
|
49
|
+
dev/CI/test runs, where `sys.frozen` is unset) and on macOS/Windows
|
|
50
|
+
(`LD_LIBRARY_PATH` is not their loader variable), so this still never
|
|
51
|
+
touches a host that never had the problem -- and it still never guesses at a
|
|
52
|
+
fallback value: with no `ORIG`, the only defensible restore is REMOVAL.
|
|
53
|
+
|
|
54
|
+
Lifted out of `tan.commands.bootstrap_cmd.Runner._env`, which worked out this
|
|
55
|
+
rule first (tan-cli#990) but was exactly one spawn site out of ~25 under
|
|
56
|
+
`python/tan/` -- this module is the ONE place the rule is written down,
|
|
57
|
+
per tan-cli#992. `Runner._env` itself now calls [`restore_ld_library_path`]
|
|
58
|
+
rather than carrying its own copy of the restore logic.
|
|
59
|
+
"""
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import os
|
|
63
|
+
import sys
|
|
64
|
+
from collections.abc import Mapping
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _frozen_bundle_dir() -> str | None:
|
|
68
|
+
"""The directory a PyInstaller ONEDIR bootloader puts on
|
|
69
|
+
`LD_LIBRARY_PATH` so the frozen binary finds its OWN shared libraries --
|
|
70
|
+
`sys._MEIPASS`, which for a onedir freeze is the `_internal/` tree.
|
|
71
|
+
`None` on a source run (this repo's own dev/CI/test runs), where
|
|
72
|
+
`sys.frozen` is unset and there is no bundle to strip."""
|
|
73
|
+
if not getattr(sys, "frozen", False):
|
|
74
|
+
return None
|
|
75
|
+
bundle = getattr(sys, "_MEIPASS", None)
|
|
76
|
+
return bundle if isinstance(bundle, str) and bundle else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _without_bundle_dir(value: str, bundle: str) -> str | None:
|
|
80
|
+
"""`value` (an `os.pathsep`-joined search path) with every entry that IS
|
|
81
|
+
`bundle` removed -- or `None` when `bundle` does not appear in it at all,
|
|
82
|
+
which is the caller's signal that there is nothing to undo.
|
|
83
|
+
|
|
84
|
+
Compared with `os.path.normpath`, so a trailing slash or a `./` segment
|
|
85
|
+
cannot smuggle the bundle past the match. Empty entries are dropped from
|
|
86
|
+
the result: stripping the bundle out of `"<bundle>"` or
|
|
87
|
+
`"<bundle>:<bundle>"` must not leave a bare `":"`, which some loaders
|
|
88
|
+
read as "search the current directory" -- the same hazard the
|
|
89
|
+
`orig == ""` branch below is written to avoid."""
|
|
90
|
+
entries = value.split(os.pathsep)
|
|
91
|
+
target = os.path.normpath(bundle)
|
|
92
|
+
if not any(entry and os.path.normpath(entry) == target for entry in entries):
|
|
93
|
+
return None
|
|
94
|
+
kept = [entry for entry in entries if entry and os.path.normpath(entry) != target]
|
|
95
|
+
return os.pathsep.join(kept)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ld_library_path_needs_restore() -> bool:
|
|
99
|
+
"""Would a child spawned from THIS process inherit the frozen bundle's
|
|
100
|
+
lib dir on `LD_LIBRARY_PATH`?
|
|
101
|
+
|
|
102
|
+
The predicate a caller uses to decide whether it may skip building a
|
|
103
|
+
child environment at all. `bootstrap_cmd.Runner._env` is the one such
|
|
104
|
+
caller: it returns `None` (meaning `subprocess.run(env=None)`, "inherit
|
|
105
|
+
this process's unrestored environment") on its fast path, and that fast
|
|
106
|
+
path is only safe when this returns `False`."""
|
|
107
|
+
if os.environ.get("LD_LIBRARY_PATH_ORIG") is not None:
|
|
108
|
+
return True
|
|
109
|
+
bundle = _frozen_bundle_dir()
|
|
110
|
+
if bundle is None:
|
|
111
|
+
return False
|
|
112
|
+
current = os.environ.get("LD_LIBRARY_PATH")
|
|
113
|
+
if not current:
|
|
114
|
+
return False
|
|
115
|
+
return _without_bundle_dir(current, bundle) is not None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _restore_from_orig(env: dict[str, str], orig: str) -> None:
|
|
119
|
+
"""The bootloader recorded a marker, so restore exactly what it saved.
|
|
120
|
+
|
|
121
|
+
POPS the key rather than setting `""` when the host had no
|
|
122
|
+
`LD_LIBRARY_PATH` before the bootloader touched it -- some dynamic
|
|
123
|
+
loaders read an explicit empty value as "search the current directory",
|
|
124
|
+
which is worse than the key being absent."""
|
|
125
|
+
if orig:
|
|
126
|
+
env["LD_LIBRARY_PATH"] = orig
|
|
127
|
+
else:
|
|
128
|
+
env.pop("LD_LIBRARY_PATH", None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _strip_bundle_dir(env: dict[str, str]) -> None:
|
|
132
|
+
"""No marker was recorded -- remove the bundle dir from `LD_LIBRARY_PATH`
|
|
133
|
+
if it is on it (tan-cli#1189).
|
|
134
|
+
|
|
135
|
+
Deliberately does NOT invent a replacement value: with no
|
|
136
|
+
`LD_LIBRARY_PATH_ORIG` the only defensible restore is REMOVAL. A frozen
|
|
137
|
+
run whose path does not carry the bundle is left completely alone -- the
|
|
138
|
+
bootloader never touched it, so neither does this."""
|
|
139
|
+
bundle = _frozen_bundle_dir()
|
|
140
|
+
if bundle is None:
|
|
141
|
+
return
|
|
142
|
+
current = os.environ.get("LD_LIBRARY_PATH")
|
|
143
|
+
if not current:
|
|
144
|
+
return
|
|
145
|
+
stripped = _without_bundle_dir(current, bundle)
|
|
146
|
+
if stripped is None:
|
|
147
|
+
return
|
|
148
|
+
if stripped:
|
|
149
|
+
env["LD_LIBRARY_PATH"] = stripped
|
|
150
|
+
else:
|
|
151
|
+
env.pop("LD_LIBRARY_PATH", None)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def restore_ld_library_path(env: dict[str, str]) -> None:
|
|
155
|
+
"""Mutate `env` IN PLACE: undo the frozen bootloader's `LD_LIBRARY_PATH`
|
|
156
|
+
override, reading the REAL process environment (`os.environ`) -- never
|
|
157
|
+
`env` itself, which may already be a partially-built child env with no
|
|
158
|
+
such key.
|
|
159
|
+
|
|
160
|
+
A no-op on every host that never had the problem: a source run, macOS,
|
|
161
|
+
Windows, or a frozen run where the bundle is not on the search path.
|
|
162
|
+
|
|
163
|
+
Two triggers, not one -- see this module's docstring for why an absent
|
|
164
|
+
`LD_LIBRARY_PATH_ORIG` is NOT proof that no override happened
|
|
165
|
+
(tan-cli#1189), and [`_restore_from_orig`] / [`_strip_bundle_dir`] for
|
|
166
|
+
what each arm does."""
|
|
167
|
+
orig = os.environ.get("LD_LIBRARY_PATH_ORIG")
|
|
168
|
+
if orig is not None:
|
|
169
|
+
_restore_from_orig(env, orig)
|
|
170
|
+
return
|
|
171
|
+
_strip_bundle_dir(env)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def spawn_env(
|
|
175
|
+
overrides: Mapping[str, str] | None = None,
|
|
176
|
+
*,
|
|
177
|
+
base: Mapping[str, str] | None = None,
|
|
178
|
+
) -> dict[str, str]:
|
|
179
|
+
"""The `env=` every subprocess spawn in `tan` hands its child: a fresh
|
|
180
|
+
copy of `base` (`os.environ` when not given) with
|
|
181
|
+
[`restore_ld_library_path`] applied, then `overrides` layered on top.
|
|
182
|
+
|
|
183
|
+
`base`, when given, is typically an already-adjusted mapping a caller
|
|
184
|
+
built for its own reasons (e.g. `with_venv_on_path`'s PATH prepend) --
|
|
185
|
+
`spawn_env` copies it rather than mutating it, then applies the SAME
|
|
186
|
+
restore on top, so a caller can freely compose
|
|
187
|
+
`with_venv_on_path(spawn_env(), tool)` or
|
|
188
|
+
`spawn_env(base=with_venv_on_path(dict(os.environ), tool))`
|
|
189
|
+
interchangeably; the two touch disjoint keys (`PATH` vs
|
|
190
|
+
`LD_LIBRARY_PATH`) so the order never matters.
|
|
191
|
+
|
|
192
|
+
Always returns an explicit `dict`, never `None`:
|
|
193
|
+
`subprocess.run(env=None)` means "inherit this process's own
|
|
194
|
+
(unrestored) environment", which is exactly the leak this module exists
|
|
195
|
+
to close -- no call site may spawn with an implicit/`None` environment.
|
|
196
|
+
"""
|
|
197
|
+
env = dict(base) if base is not None else dict(os.environ)
|
|
198
|
+
restore_ld_library_path(env)
|
|
199
|
+
if overrides:
|
|
200
|
+
env.update(overrides)
|
|
201
|
+
return env
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Re-derive the sysbuild image prefix of a slice's per-core `alp.conf` after
|
|
3
|
+
token substitution (alp-sdk#866, consumer side).
|
|
4
|
+
|
|
5
|
+
On a `--sysbuild` slice the planner passes the per-core `alp.conf` as
|
|
6
|
+
`-D<image>_EXTRA_CONF_FILE=<build>/alp.conf`, where `<image>` is the
|
|
7
|
+
basename of the app directory handed to `west build` -- sysbuild's own name
|
|
8
|
+
for the application image (`get_filename_component(app_name ${APP_DIR}
|
|
9
|
+
NAME)`). The planner computes that basename from the directory it EMITTED
|
|
10
|
+
in. A tokened plan's app dir is `${PROJECT_ROOT}/...`, so the same plan run
|
|
11
|
+
from a differently-named project root (`tan build --plan-from plan.json
|
|
12
|
+
--execute` in `/b/bar` for a plan emitted in `/a/foo`) names image `foo`
|
|
13
|
+
while sysbuild builds image `bar`: Zephyr ignores the unknown
|
|
14
|
+
`foo_EXTRA_CONF_FILE` and the image builds WITHOUT its per-core
|
|
15
|
+
configuration, with no error. alp-sdk's own orchestrator comment says a
|
|
16
|
+
consumer that relocates the project root must re-derive the prefix; this is
|
|
17
|
+
that re-derivation. `tan/planner/orchestrator.py` is an upstream mirror and
|
|
18
|
+
stays as emitted.
|
|
19
|
+
|
|
20
|
+
Rewrite, not refuse: the arg is identified by its VALUE (this slice's own
|
|
21
|
+
`<buildDir>/alp.conf`), not by its prefix, so retargeting it to the image
|
|
22
|
+
sysbuild will actually build is unambiguous, and the right prefix is a pure
|
|
23
|
+
function of the substituted command. Refusing would make every relocated
|
|
24
|
+
tokened plan for a `boot:`/OTA project unbuildable, when the planner already
|
|
25
|
+
said exactly how to fix it. Any other `-D<x>_EXTRA_CONF_FILE` (one naming
|
|
26
|
+
`mcuboot`, say) is not this slice's `alp.conf` and is left alone.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from dataclasses import replace
|
|
33
|
+
|
|
34
|
+
from tan.core.build_plan import BuildPlan, Slice, SliceCommand
|
|
35
|
+
|
|
36
|
+
_IMAGE_SCOPED = re.compile(r"^-D(?P<image>[^=]+)_EXTRA_CONF_FILE=(?P<value>.*)$", re.S)
|
|
37
|
+
|
|
38
|
+
#: `west build` options that take a separate value -- skipped when locating
|
|
39
|
+
#: the positional app-dir argument (same set alp-sdk's seam1 comparator uses).
|
|
40
|
+
_OPTIONS_WITH_VALUE = frozenset({"-b", "-d", "-p"})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def west_app_dir_basename(args: list[str]) -> str | None:
|
|
44
|
+
"""Basename of `west build`'s app-dir argument: the first non-option arg
|
|
45
|
+
after `build`, or `None` when there is none."""
|
|
46
|
+
try:
|
|
47
|
+
i = args.index("build") + 1
|
|
48
|
+
except ValueError:
|
|
49
|
+
return None
|
|
50
|
+
while i < len(args) and args[i].startswith("-"):
|
|
51
|
+
i += 2 if args[i] in _OPTIONS_WITH_VALUE else 1
|
|
52
|
+
if i >= len(args) or args[i] == "--":
|
|
53
|
+
return None
|
|
54
|
+
name = args[i].replace("\\", "/").rstrip("/").rsplit("/", 1)[-1]
|
|
55
|
+
return name or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_slice_alp_conf(value: str, build_dir: str) -> bool:
|
|
59
|
+
v = value.replace("\\", "/")
|
|
60
|
+
b = build_dir.replace("\\", "/").rstrip("/")
|
|
61
|
+
return v == f"{b}/alp.conf" or v.endswith(f"/{b}/alp.conf")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rescope_command(command: SliceCommand, build_dir: str) -> SliceCommand:
|
|
65
|
+
"""`command` with its per-core `-D<image>_EXTRA_CONF_FILE` renamed to the
|
|
66
|
+
image `west build` will create; unchanged when not sysbuild, when the app
|
|
67
|
+
dir is unknown, or when the prefix already matches."""
|
|
68
|
+
args = command.args
|
|
69
|
+
if "--sysbuild" not in args:
|
|
70
|
+
return command
|
|
71
|
+
image = west_app_dir_basename(args)
|
|
72
|
+
if image is None:
|
|
73
|
+
return command
|
|
74
|
+
new_args = []
|
|
75
|
+
for arg in args:
|
|
76
|
+
m = _IMAGE_SCOPED.match(arg) if isinstance(arg, str) else None
|
|
77
|
+
if m and m.group("image") != image and _is_slice_alp_conf(m.group("value"), build_dir):
|
|
78
|
+
arg = f"-D{image}_EXTRA_CONF_FILE={m.group('value')}"
|
|
79
|
+
new_args.append(arg)
|
|
80
|
+
if new_args == args:
|
|
81
|
+
return command
|
|
82
|
+
return replace(command, args=new_args)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rescope_sysbuild_extra_conf(plan: BuildPlan) -> BuildPlan:
|
|
86
|
+
"""`plan` with every slice's command passed through `rescope_command`."""
|
|
87
|
+
slices: list[Slice] = []
|
|
88
|
+
for sl in plan.slices:
|
|
89
|
+
if sl.command is None:
|
|
90
|
+
slices.append(sl)
|
|
91
|
+
continue
|
|
92
|
+
cmd = rescope_command(sl.command, sl.build_dir)
|
|
93
|
+
slices.append(sl if cmd is sl.command else replace(sl, command=cmd))
|
|
94
|
+
return replace(plan, slices=slices)
|