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,996 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The alp-sdk discovery ladders (tan-cli#408) -- `--sdk-root` > project pin >
|
|
3
|
+
machine-global default > positional discovery, in the NARROW and WIDE
|
|
4
|
+
flavours documented on `resolve_sdk_root_ladder` / `resolve_sdk_root_wide`
|
|
5
|
+
below -- plus the divergence warning between them and the interpreter
|
|
6
|
+
resolution that rides on the same tiers.
|
|
7
|
+
|
|
8
|
+
**Moved here from `tan/commands/build_cmd.py`, verbatim logic, tan-cli#408.**
|
|
9
|
+
Nineteen modules resolved the SDK through this code while it lived in a
|
|
10
|
+
2000+-line COMMAND module, and one of them was `tan/envelope.py` -- core
|
|
11
|
+
infrastructure every command's envelope passes through -- reaching back into
|
|
12
|
+
a command for it via a function-scoped import whose own comment said why:
|
|
13
|
+
"`build_cmd` imports this module at module level, so a top-level import here
|
|
14
|
+
would be circular." That is the dependency inverted: core importing from a
|
|
15
|
+
command. This module is the fix -- `tan.core` importing no command module
|
|
16
|
+
(the same invariant `tan.core.shapes` already states and relies on), so
|
|
17
|
+
`envelope.py` can import [`sdk_ladder_divergence_issue`] at the top of the
|
|
18
|
+
file rather than inside the one method that needed it.
|
|
19
|
+
|
|
20
|
+
**Finished here, tan-cli#408 review follow-up.** The first pass of this
|
|
21
|
+
extraction left `resolve_sdk_tiered` -- the single NARROW tiered resolver
|
|
22
|
+
both ladders share -- behind in `tan.commands.sdk_cmd`, reached through a
|
|
23
|
+
function-scoped import in each ladder function, on the reasoning that it was
|
|
24
|
+
"a much larger, separately-scoped change." Review of that first pass found
|
|
25
|
+
the opposite: `resolve_sdk_tiered` and the dozen filesystem-primitive
|
|
26
|
+
functions under it (the tier-pointer reads, the registry lookup, the two
|
|
27
|
+
positional-discovery walks) touch none of `typer`, `tan.envelope`,
|
|
28
|
+
`tan.output_format` or `tan.exit_codes` -- their only non-stdlib dependencies
|
|
29
|
+
were already `tan.core.shapes` and `tan.core.sdk_default_registry`, both
|
|
30
|
+
core. The whole cluster moved here in the same shape, verbatim, along with
|
|
31
|
+
`ActiveSdk` and the two `Issue` builders (`project_pin_issue`,
|
|
32
|
+
`global_default_foreign_project_issue`) `envelope.py` needs through
|
|
33
|
+
[`sdk_resolution_issues`]. Both ladder functions call `resolve_sdk_tiered`
|
|
34
|
+
directly now -- no function-scoped import left inside either one, because
|
|
35
|
+
the cycle it used to dodge (`sdk_discovery` -> `sdk_cmd` -> back into this
|
|
36
|
+
module's own ladders) no longer exists once `resolve_sdk_tiered` lives here
|
|
37
|
+
too.
|
|
38
|
+
|
|
39
|
+
`Issue` (`tan.envelope`, the wire type every `*_issue`/`*_issues` function
|
|
40
|
+
below returns) is the one name this module still cannot import at module
|
|
41
|
+
level: `tan.envelope` imports THIS module at module level
|
|
42
|
+
(`sdk_ladder_divergence_issue`, `sdk_resolution_issues`), so a top-level
|
|
43
|
+
import back would recreate exactly the cycle this extraction removes, just
|
|
44
|
+
with the two modules swapped. `TYPE_CHECKING` supplies the name for every
|
|
45
|
+
annotation below, which is what a real type checker (mypy, an IDE) resolves
|
|
46
|
+
against -- `from __future__ import annotations` defers every annotation to
|
|
47
|
+
string form, so this costs nothing at runtime, and it is the same idiom
|
|
48
|
+
`sdk_cmd._releases_opener` already uses for `OpenerDirector` one file over.
|
|
49
|
+
Each function that actually CONSTRUCTS an `Issue` imports it again,
|
|
50
|
+
function-scoped, at the point it runs. The deferral is safe NOT because
|
|
51
|
+
some caller has necessarily already imported `tan.envelope` first (tan-cli#408
|
|
52
|
+
review, nit: an earlier draft of this paragraph claimed exactly that, and it
|
|
53
|
+
does not hold -- measured in a process that imported ONLY this module,
|
|
54
|
+
`'tan.envelope' in sys.modules` is `False` right up to the call, and the
|
|
55
|
+
deferred import still succeeds). It is safe because by the time any function
|
|
56
|
+
in this module RUNS, this module's OWN top-level execution has already
|
|
57
|
+
finished and `tan.core.sdk_discovery` is fully registered in `sys.modules` --
|
|
58
|
+
so `tan.envelope`'s own top-level `from tan.core.sdk_discovery import
|
|
59
|
+
sdk_ladder_divergence_issue, sdk_resolution_issues` finds a complete module,
|
|
60
|
+
never a partially-initialized one, however `tan.envelope` first got
|
|
61
|
+
imported. A genuine cycle can only happen while ONE of the two modules is
|
|
62
|
+
still in the middle of its OWN top-level execution; nothing here runs at
|
|
63
|
+
that time.
|
|
64
|
+
|
|
65
|
+
**`typing.get_type_hints()` still raises on these annotations, and that is
|
|
66
|
+
not fixed by the `TYPE_CHECKING` guard -- it cannot be, and no wrapper
|
|
67
|
+
should try (tan-cli#408 review, minor 2 follow-up).** `TYPE_CHECKING` is
|
|
68
|
+
`False` at runtime, so the guarded import never executes and `Issue` is
|
|
69
|
+
never bound in this module's real `__dict__`; `get_type_hints()` resolves a
|
|
70
|
+
string annotation by `eval`-ing it against `func.__globals__`, which IS that
|
|
71
|
+
same `__dict__`, so `NameError: name 'Issue' is not defined` is still the
|
|
72
|
+
measured result of calling it on any function below that names `Issue`.
|
|
73
|
+
Making it resolve for real would mean binding the real `Issue` class at
|
|
74
|
+
module level, which is the exact cycle this whole module exists to avoid --
|
|
75
|
+
there is no way to have both. This is the identical tradeoff
|
|
76
|
+
`_releases_opener`'s own docstring already makes for `OpenerDirector`, for
|
|
77
|
+
the identical reason: `get_type_hints()` is live in this codebase
|
|
78
|
+
(`tan.core.global_flags.accept_global_flags`), but it is called ONLY on each
|
|
79
|
+
`*_cmd.py` module's top-level typer command callback -- never on an internal
|
|
80
|
+
helper in `tan/core/`, here or in `sdk_cmd.py` -- so the two never meet in
|
|
81
|
+
production. A guard test asserting `get_type_hints()` resolves across this
|
|
82
|
+
module's public functions was considered and rejected for the same reason:
|
|
83
|
+
it would be false by construction for every `Issue`-returning one, so it
|
|
84
|
+
would not catch a real regression, only encode this already-understood and
|
|
85
|
+
already-accepted limitation as a failing assertion.
|
|
86
|
+
"""
|
|
87
|
+
from __future__ import annotations
|
|
88
|
+
|
|
89
|
+
import json
|
|
90
|
+
import os
|
|
91
|
+
from dataclasses import dataclass
|
|
92
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
93
|
+
from typing import TYPE_CHECKING, Any
|
|
94
|
+
|
|
95
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
96
|
+
# Type-checker only -- see the module docstring for why this cannot be a
|
|
97
|
+
# runtime import. `get_type_hints()` needs this name in the module
|
|
98
|
+
# namespace to resolve the `Issue | None` / `list[Issue]` annotations
|
|
99
|
+
# below; it is never evaluated at import time (`TYPE_CHECKING` is False
|
|
100
|
+
# in a real run), so it adds no runtime dependency on `tan.envelope`.
|
|
101
|
+
from tan.envelope import Issue
|
|
102
|
+
|
|
103
|
+
from tan.core.sdk_default_registry import (
|
|
104
|
+
deepest_covering_entry,
|
|
105
|
+
parse_registry,
|
|
106
|
+
parse_registry_updated_at,
|
|
107
|
+
registry_path,
|
|
108
|
+
)
|
|
109
|
+
from tan.core.shapes import SDK_MARKER, is_sdk_root
|
|
110
|
+
from tan.core.venv import venv_python
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _abs_posix(path: str) -> str:
|
|
114
|
+
"""Rust's `normalize_path(cwd.join(p))` + `to_posix`: cwd-anchored,
|
|
115
|
+
lexically normalised, forward slashes.
|
|
116
|
+
|
|
117
|
+
`os.path.abspath`, NOT `Path.resolve()` -- abspath is purely lexical, so a
|
|
118
|
+
project reached through a symlink keeps the name the user typed, and a path
|
|
119
|
+
that does not exist yet still resolves. `resolve()` would rewrite both, and
|
|
120
|
+
the divergence guard compares STRINGS: rewriting one side's symlink but not
|
|
121
|
+
the other's is exactly how that guard starts firing on a healthy project.
|
|
122
|
+
|
|
123
|
+
Forward slashes because the result is substituted into `${PROJECT_ROOT}`
|
|
124
|
+
and lands in a CMake `-D` argument, where a Windows backslash is an escape.
|
|
125
|
+
"""
|
|
126
|
+
return os.path.abspath(path).replace("\\", "/")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def discover_sdk_root(workspace_root: Path) -> Path | None:
|
|
130
|
+
"""Find an alp-sdk checkout near `workspace_root`, mirroring Rust's
|
|
131
|
+
`discover_sdk_root` (`crates/tan-cli/src/util.rs`) candidate for
|
|
132
|
+
candidate: the root itself, then its CHILD `alp-sdk/`, then the sibling
|
|
133
|
+
`../alp-sdk`, then `../alp-sdk-upstream`, and only if none of those hit,
|
|
134
|
+
the nearest ENCLOSING checkout.
|
|
135
|
+
|
|
136
|
+
The child comes before the siblings deliberately (tan-cli #218): `tan
|
|
137
|
+
bootstrap` clones into `<ws>/alp-sdk`, so at that moment the checkout is a
|
|
138
|
+
CHILD of the cwd -- it only becomes the documented sibling once the user
|
|
139
|
+
has cd'd into a project. Discovery that checks root, siblings and
|
|
140
|
+
ancestors but not a child reports "alp-sdk root is unresolved" with the
|
|
141
|
+
checkout sitting right there.
|
|
142
|
+
|
|
143
|
+
A fixed candidate list, never a directory scan: probing `iterdir()` up the
|
|
144
|
+
whole ancestor chain reads a developer's entire home and drive root, which
|
|
145
|
+
is both slow and non-hermetic (it lets an unrelated checkout elsewhere on
|
|
146
|
+
the machine decide what a test resolves).
|
|
147
|
+
|
|
148
|
+
Deliberately just the positional walk, not the full ladder -- the project
|
|
149
|
+
pin and machine-global default tiers that outrank this one live in
|
|
150
|
+
[`resolve_sdk_root_ladder`], below, which every `--sdk-root`-less caller
|
|
151
|
+
should use instead of calling this directly (this function remains that
|
|
152
|
+
ladder's own final fallback tier, and the one place that still calls it
|
|
153
|
+
alone is `tan-cli#218`'s own regression test).
|
|
154
|
+
"""
|
|
155
|
+
parent = workspace_root.parent
|
|
156
|
+
for candidate in (
|
|
157
|
+
workspace_root,
|
|
158
|
+
workspace_root / "alp-sdk",
|
|
159
|
+
parent / "alp-sdk",
|
|
160
|
+
parent / "alp-sdk-upstream",
|
|
161
|
+
):
|
|
162
|
+
if is_sdk_root(candidate):
|
|
163
|
+
return candidate
|
|
164
|
+
for ancestor in workspace_root.parents:
|
|
165
|
+
if is_sdk_root(ancestor):
|
|
166
|
+
return ancestor
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ── filesystem primitives (every failure is a value, never an exception) ────
|
|
171
|
+
# Moved verbatim from `tan/commands/sdk_cmd.py`, tan-cli#408 review follow-up
|
|
172
|
+
# -- see the module docstring. `check_sdk_readiness`/`cached_sdk_versions`,
|
|
173
|
+
# which stay in `sdk_cmd.py` (they are readiness REPORTING, not resolution),
|
|
174
|
+
# import `_read_file`/`_has_loader_script`/`_home_alp_dir` back from here.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _read_file(path: Path) -> str | None:
|
|
178
|
+
"""`tan_core`'s injected `read_file`: contents, or `None` on ANY read
|
|
179
|
+
failure -- missing, a directory, permission-denied, non-UTF-8 bytes.
|
|
180
|
+
|
|
181
|
+
`encoding="utf-8"` explicitly (I-27): a bare `read_text()` decodes with the
|
|
182
|
+
host locale, so a pointer file or `sdk_version.yaml` carrying one non-ASCII
|
|
183
|
+
byte raises `UnicodeDecodeError` on a cp1252 Windows host and passes on
|
|
184
|
+
ubuntu CI. Swallowing it to `None` is what the Rust does and is what keeps
|
|
185
|
+
an unreadable file a reported fact instead of a traceback.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
return path.read_text(encoding="utf-8")
|
|
189
|
+
except (OSError, UnicodeDecodeError):
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _has_loader_script(root: Path) -> bool:
|
|
194
|
+
"""True when `root/scripts/alp_project.py` exists -- `util.rs`'s
|
|
195
|
+
`has_loader_script`. `Path.exists()` swallows its own `OSError`/`ValueError`
|
|
196
|
+
(a too-long path, an illegal name), so a pathological pointer value reads as
|
|
197
|
+
"not an SDK" rather than raising out of a tier lookup."""
|
|
198
|
+
return root.joinpath(*SDK_MARKER).exists()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _to_posix(path: Path) -> str:
|
|
202
|
+
"""`tan_core::project::to_posix`. The discovery tier's result is a reported
|
|
203
|
+
path, and every golden-pinned path field is platform-identical forward
|
|
204
|
+
slashes; `Path` renders `\\` on Windows."""
|
|
205
|
+
return str(path).replace("\\", "/")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _home_alp_dir() -> Path:
|
|
209
|
+
"""`~/.alp` -- `USERPROFILE` on Windows else `HOME`, falling back to `.`
|
|
210
|
+
when neither is set (`util.rs`'s `home_alp_dir`). Home of the global default
|
|
211
|
+
pointer and the install cache, and the reason the conformance harness
|
|
212
|
+
overrides BOTH variables: a developer's real `~/.alp/sdk-default` would
|
|
213
|
+
otherwise decide what `sdk current` reports."""
|
|
214
|
+
home = os.environ.get("USERPROFILE" if os.name == "nt" else "HOME")
|
|
215
|
+
return Path(home or ".") / ".alp"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def sdk_search_summary(workspace_root: Path) -> str:
|
|
219
|
+
"""tan-cli#1423: where `resolve_sdk_root_ladder` looked, tier by tier, for
|
|
220
|
+
a `*.sdk-root-*` refusal to quote beside its `--sdk-root` remedy. There is
|
|
221
|
+
no env-var remedy to add: `ALP_SDK_ROOT` is not a tier (see that ladder's
|
|
222
|
+
docstring)."""
|
|
223
|
+
root = Path(workspace_root).absolute()
|
|
224
|
+
near = ", ".join(
|
|
225
|
+
f"`{_to_posix(p)}`"
|
|
226
|
+
for p in (root, root / "alp-sdk", root.parent / "alp-sdk", root.parent / "alp-sdk-upstream")
|
|
227
|
+
)
|
|
228
|
+
return (
|
|
229
|
+
f"Neither `--sdk-root`, the project pin `{_to_posix(root / '.alp' / 'sdk-path')}` nor "
|
|
230
|
+
f"the global default `{_to_posix(_home_alp_dir() / 'sdk-default')}` named a usable "
|
|
231
|
+
f"checkout, and none was found at {near} or in a directory above "
|
|
232
|
+
f"`{_to_posix(root)}`."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def with_sdk_search(
|
|
237
|
+
message: str, workspace_root: Path | str, sdk_root_arg: str | None = None
|
|
238
|
+
) -> str:
|
|
239
|
+
"""tan-cli#1463: the ONE route every `<cmd>.sdk-root-*` refusal takes to name
|
|
240
|
+
where tan looked. Appends `sdk_search_summary` to `message`.
|
|
241
|
+
|
|
242
|
+
A REJECTED `--sdk-root` is terminal (I-31): no other tier was tried, so
|
|
243
|
+
`message` is returned untouched -- claiming the ladder was searched would be
|
|
244
|
+
false, and `rejected_sdk_root_message` already names the typed value.
|
|
245
|
+
"""
|
|
246
|
+
if (sdk_root_arg or "").strip():
|
|
247
|
+
return message
|
|
248
|
+
return f"{message} {sdk_search_summary(Path(workspace_root))}"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _read_pointer_json(pointer: Path) -> dict[str, Any] | None:
|
|
252
|
+
"""Parse a pointer file (`.alp/sdk-path`, `~/.alp/sdk-default`) into its
|
|
253
|
+
dict, or `None` on ANY failure -- missing, unreadable, invalid JSON, or a
|
|
254
|
+
non-dict shape (a list). Shared by every field reader over this shape so
|
|
255
|
+
`_pointer_target` and `_pointer_written_for` can never disagree about what
|
|
256
|
+
counts as an unreadable pointer.
|
|
257
|
+
"""
|
|
258
|
+
if not pointer.exists():
|
|
259
|
+
return None
|
|
260
|
+
raw = _read_file(pointer)
|
|
261
|
+
if raw is None:
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
parsed = json.loads(raw)
|
|
265
|
+
except ValueError:
|
|
266
|
+
return None
|
|
267
|
+
return parsed if isinstance(parsed, dict) else None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _pointer_target(pointer: Path) -> str | None:
|
|
271
|
+
"""The `sdkPath` out of a `{"sdkPath": ..., "updatedAt": ...}` pointer file.
|
|
272
|
+
|
|
273
|
+
One function for both pointers -- `tan_core`'s `resolve_active_sdk`
|
|
274
|
+
(`<workspace>/.alp/sdk-path`) and `resolve_global_default_sdk`
|
|
275
|
+
(`~/.alp/sdk-default`) are the same read of the same shape at two paths.
|
|
276
|
+
Every failure is `None`, matching the Rust's `.ok()?` chain: a hand-edited
|
|
277
|
+
pointer holding invalid JSON, a list, or no `sdkPath` at all must fall
|
|
278
|
+
through to the next tier, not abort the command.
|
|
279
|
+
"""
|
|
280
|
+
parsed = _read_pointer_json(pointer)
|
|
281
|
+
value = parsed.get("sdkPath") if parsed is not None else None
|
|
282
|
+
return value if isinstance(value, str) else None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _pointer_written_for(pointer: Path) -> str | None:
|
|
286
|
+
"""The optional `writtenFor` field alongside `sdkPath`/`updatedAt` in
|
|
287
|
+
`~/.alp/sdk-default` -- which project's bootstrap relocation last wrote
|
|
288
|
+
this machine-global pointer (tan-cli#464).
|
|
289
|
+
|
|
290
|
+
`None` covers both "no opinion" cases the same way, on purpose: a pointer
|
|
291
|
+
written by an older tan that predates this field, and one this tan wrote
|
|
292
|
+
for a run that never relocated a checkout FOR a project at all. Neither is
|
|
293
|
+
a claim that no other project wrote the pointer -- it is the absence of
|
|
294
|
+
evidence, and `resolve_sdk_tiered` must never manufacture a warning out of
|
|
295
|
+
it.
|
|
296
|
+
|
|
297
|
+
A non-`str`, empty, or non-absolute value is ALSO `None` (measured
|
|
298
|
+
tan-cli#464 review regression): `writtenFor: ""` used to pass the bare
|
|
299
|
+
`isinstance(value, str)` check here and reach `_workspace_under(ws, "")`,
|
|
300
|
+
which resolves `Path("")` to the process's cwd -- so whether the foreign
|
|
301
|
+
warning fired depended on where the caller happened to be standing, not
|
|
302
|
+
on anything the pointer actually recorded. Every OTHER project root this
|
|
303
|
+
field can legitimately hold is written by `bootstrap_cmd._run` as an
|
|
304
|
+
already-absolute `root` (`Project.resolved`'s own contract), so rejecting
|
|
305
|
+
a relative or blank one here is not narrowing real coverage ON THE
|
|
306
|
+
WRITER'S OWN PLATFORM -- but `~/.alp/sdk-default` is one pointer shared by
|
|
307
|
+
every tan on the host, and a bare `Path(value).is_absolute()` is answered
|
|
308
|
+
by whichever pathlib flavour the READER's OS picked: `PureWindowsPath`
|
|
309
|
+
needs a drive letter, so a legitimate `"/home/u/projB"` written by a
|
|
310
|
+
Linux/macOS tan degraded to "no opinion" the moment a Windows tan read it
|
|
311
|
+
back, and symmetrically `"C:/projB"` from a Windows writer is non-absolute
|
|
312
|
+
to `PurePosixPath` on the other two. Accepted here when EITHER
|
|
313
|
+
`PurePosixPath` or `PureWindowsPath` calls it absolute, so a value either
|
|
314
|
+
platform's tan legitimately wrote still counts, while `""`/`"."`/a bare
|
|
315
|
+
relative segment (`"projB/ws"`)/a drive-relative `"C:projB"` stay `None`
|
|
316
|
+
under both -- the safe degradation is unchanged, only which absolute
|
|
317
|
+
shapes clear it.
|
|
318
|
+
"""
|
|
319
|
+
parsed = _read_pointer_json(pointer)
|
|
320
|
+
value = parsed.get("writtenFor") if parsed is not None else None
|
|
321
|
+
if not isinstance(value, str) or not value:
|
|
322
|
+
return None
|
|
323
|
+
if not PurePosixPath(value).is_absolute() and not PureWindowsPath(value).is_absolute():
|
|
324
|
+
return None
|
|
325
|
+
return value
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _workspace_under(workspace_root: Path, root: str) -> bool:
|
|
329
|
+
"""Whether `workspace_root` IS `root`, or sits somewhere below it --
|
|
330
|
+
resolved on both sides so a `..`, a symlink, or Windows' case-folding
|
|
331
|
+
cannot spoof a match (mirrors `tan.core.fs_confine.resolve_confined`'s own
|
|
332
|
+
reasoning for the same comparison). Any resolution failure (a path shape
|
|
333
|
+
the host rejects outright) reads as "not under it" -- the caller treats
|
|
334
|
+
that as grounds for a WARNING, never a hard failure.
|
|
335
|
+
|
|
336
|
+
`RuntimeError` is caught alongside `OSError`/`ValueError` (review, #904,
|
|
337
|
+
minor 1): `Path.resolve()` re-raises an `ELOOP` (a symlink loop) as a
|
|
338
|
+
`RuntimeError` ("Symlink loop from ...") rather than an `OSError` --
|
|
339
|
+
pathlib's own choice, not this module's -- so a registry origin or a
|
|
340
|
+
legacy `writtenFor` naming a symlink loop must degrade the same as any
|
|
341
|
+
other unresolvable path, not raise out of a tier lookup. Pre-existing
|
|
342
|
+
from tan-cli#464 (both pointer tiers shared this gap before #466 added a
|
|
343
|
+
third caller of the same pattern); left uncaught here would contradict
|
|
344
|
+
`parse_registry`'s own "never raises" contract one function over.
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
return workspace_root.resolve().is_relative_to(Path(root).resolve())
|
|
348
|
+
except (OSError, ValueError, RuntimeError):
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _resolved_origin_depth_key(root: str) -> str:
|
|
353
|
+
"""`root`, resolved the same way `_workspace_under` resolves it for
|
|
354
|
+
containment -- reused as `sdk_default_registry.deepest_covering_entry`'s
|
|
355
|
+
depth-ranking key so ranking can never diverge from `covers`'s own notion
|
|
356
|
+
of "contains" (review, #904, major 1: a symlinked origin made the raw
|
|
357
|
+
registry-key string length disagree with `_workspace_under`'s resolved
|
|
358
|
+
containment test, so the WRONG registered SDK could win the "deepest"
|
|
359
|
+
tie-break with no warning at all).
|
|
360
|
+
|
|
361
|
+
Any resolution failure (including a symlink loop, `RuntimeError`) falls
|
|
362
|
+
back to the raw string rather than raising. IN PRODUCTION, via
|
|
363
|
+
`deepest_covering_entry`, this is only ever called after `covers` has
|
|
364
|
+
already resolved this SAME `root` successfully for this SAME candidate,
|
|
365
|
+
so a failure here would mean the filesystem changed between the two
|
|
366
|
+
calls -- treated as a same-caliber non-event as every other best-effort
|
|
367
|
+
read in this module, not a crash out of a tier lookup. That reasoning
|
|
368
|
+
made the `RuntimeError` arm of this guard genuinely hard to reach through
|
|
369
|
+
the production call graph alone (review, #904 second round, minor: a
|
|
370
|
+
narrowed `except (OSError, ValueError)` left the entire existing suite
|
|
371
|
+
green). `test_resolved_origin_depth_key_degrades_a_symlink_loop_instead_
|
|
372
|
+
of_raising` closes that by calling this function DIRECTLY, with no prior
|
|
373
|
+
`covers` call in front of it -- proving the degrade independent of
|
|
374
|
+
whether any production call order happens to exercise it.
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
return str(Path(root).resolve())
|
|
378
|
+
except (OSError, ValueError, RuntimeError):
|
|
379
|
+
return root
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _read_global_sdk_registry_raw() -> str | None:
|
|
383
|
+
"""`~/.alp/sdk-defaults.json`'s raw text, or `None` when unreadable --
|
|
384
|
+
read ONCE and shared by both views `resolve_sdk_tiered` needs
|
|
385
|
+
(`parse_registry`'s flattened `sdkPath` view and
|
|
386
|
+
`parse_registry_updated_at`'s recency view, review #904 second round,
|
|
387
|
+
major): reading the file twice would let a concurrent `tan bootstrap`'s
|
|
388
|
+
write land BETWEEN the two reads, so one view could see an origin the
|
|
389
|
+
other one does not -- not a crash (`deepest_covering_entry`'s
|
|
390
|
+
`updated_at.get(origin, "")` tolerates a missing key the same as a
|
|
391
|
+
pre-tie-break registry), but an avoidable extra race window for a file
|
|
392
|
+
this module already goes out of its way to read atomically-consistent
|
|
393
|
+
once.
|
|
394
|
+
|
|
395
|
+
Reuses `_read_file` -- the same swallow-every-read-failure primitive
|
|
396
|
+
every pointer read in this file already goes through -- rather than a
|
|
397
|
+
second bespoke try/except, so a non-UTF-8 registry degrades exactly like
|
|
398
|
+
a non-UTF-8 pointer does.
|
|
399
|
+
"""
|
|
400
|
+
return _read_file(registry_path(_home_alp_dir()))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _nearest_ancestor_sdk(start: Path) -> str | None:
|
|
404
|
+
"""The nearest ENCLOSING checkout, walking `start`'s parents upward.
|
|
405
|
+
`start` itself is deliberately not probed -- every caller checks it first
|
|
406
|
+
(`tan_core::project::nearest_ancestor_sdk`).
|
|
407
|
+
|
|
408
|
+
This is what makes the documented Quickstart resolve: `tan --project
|
|
409
|
+
examples/<cat>/<name>` puts the workspace root levels BELOW the checkout it
|
|
410
|
+
was invoked from (tan-cli #101). The walk yields at most ONE path, so it can
|
|
411
|
+
never turn an otherwise-unambiguous resolution into an ambiguous `None`.
|
|
412
|
+
"""
|
|
413
|
+
for ancestor in start.parents:
|
|
414
|
+
if _has_loader_script(ancestor):
|
|
415
|
+
return _to_posix(ancestor)
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def discover_workspace_sdk(workspace_root: Path) -> str | None:
|
|
420
|
+
"""Auto-discovery for the `discovery` tier: the workspace root itself or its
|
|
421
|
+
SIBLING `../alp-sdk`, else the nearest enclosing checkout. Two or more
|
|
422
|
+
candidates is ambiguous, which is `None` -- not a choice.
|
|
423
|
+
|
|
424
|
+
**Deliberately NOT [`discover_sdk_root`], above**, which is a different
|
|
425
|
+
Rust function (`util.rs`'s `discover_sdk_root`) with a WIDER candidate set:
|
|
426
|
+
it also probes the child `<ws>/alp-sdk` and `../alp-sdk-upstream`, and takes
|
|
427
|
+
the first match rather than requiring uniqueness. `sdk current` must mirror
|
|
428
|
+
`tan_core::discover_workspace_sdk` instead, per `resolve_sdk_tiered`'s own
|
|
429
|
+
doc comment: the tier it reports has to be what build/validate/doctor would
|
|
430
|
+
actually resolve here, or `sourceTier: "discovery"` names a path no other
|
|
431
|
+
command agrees with. Reusing the build-side helper would be the tempting
|
|
432
|
+
de-duplication and it would make the report lie.
|
|
433
|
+
"""
|
|
434
|
+
candidates: list[str] = []
|
|
435
|
+
lateral_hit = False
|
|
436
|
+
|
|
437
|
+
if _has_loader_script(workspace_root):
|
|
438
|
+
# Normalised BEFORE the dedup below: on Windows a root spelled with
|
|
439
|
+
# backslashes and its `parent/alp-sdk` sibling can name the same
|
|
440
|
+
# directory yet compare unequal as strings, which counted one SDK twice
|
|
441
|
+
# and reported "ambiguous".
|
|
442
|
+
candidates.append(_to_posix(workspace_root))
|
|
443
|
+
lateral_hit = True
|
|
444
|
+
|
|
445
|
+
# `parent != self` is pathlib's spelling of Rust's `Path::parent()` returning
|
|
446
|
+
# `None`: at a filesystem/drive root `Path("C:/").parent` is `Path("C:/")`
|
|
447
|
+
# itself, so an unguarded probe would invent a `C:/alp-sdk` candidate the
|
|
448
|
+
# Rust never considers -- flipping `sourceTier` from `none` to `discovery`
|
|
449
|
+
# for anyone whose cwd is a drive root.
|
|
450
|
+
parent = workspace_root.parent
|
|
451
|
+
if parent != workspace_root:
|
|
452
|
+
sibling = _to_posix(parent / "alp-sdk")
|
|
453
|
+
if _has_loader_script(Path(sibling)):
|
|
454
|
+
if sibling not in candidates:
|
|
455
|
+
candidates.append(sibling)
|
|
456
|
+
lateral_hit = True
|
|
457
|
+
|
|
458
|
+
# A strict fallback, gated on whether THIS folder's lateral probes answered
|
|
459
|
+
# -- never on the candidate count, which stays unchanged for a folder that
|
|
460
|
+
# resolved perfectly well but deduped against an earlier one.
|
|
461
|
+
if not lateral_hit:
|
|
462
|
+
ancestor = _nearest_ancestor_sdk(workspace_root)
|
|
463
|
+
if ancestor is not None:
|
|
464
|
+
candidates.append(ancestor)
|
|
465
|
+
|
|
466
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ── the four-tier precedence chain ──────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@dataclass(frozen=True)
|
|
473
|
+
class ActiveSdk:
|
|
474
|
+
"""What `sdk current` reports: the active path (or `None`) and the tier that
|
|
475
|
+
produced it. `tier` is the wire string, camelCase, from `SdkSourceTier`.
|
|
476
|
+
|
|
477
|
+
`broken_project_pin` (tan-cli#263) is the raw `sdkPath` a workspace
|
|
478
|
+
`.alp/sdk-path` pointer held when that file existed but its target failed
|
|
479
|
+
the loader-script check -- `None` on every other path, including "no
|
|
480
|
+
pointer file at all". Set once, in the one tier that can discover it,
|
|
481
|
+
then carried through every LOWER tier this call falls through to: a caller
|
|
482
|
+
that reports the tier which finally answered must still be able to say a
|
|
483
|
+
pin existed and did not, rather than silently looking as deliberate as a
|
|
484
|
+
workspace that was never pinned in the first place. Distinct from a
|
|
485
|
+
SIXTH `sourceTier` value on purpose -- the tier that actually supplied
|
|
486
|
+
`path` stays accurate; this is a supplementary fact about a REJECTED
|
|
487
|
+
candidate, reported by the caller via `issues[]` instead.
|
|
488
|
+
|
|
489
|
+
`foreign_global_default_for` (tan-cli#464) is the SAME shape of fact for a
|
|
490
|
+
different silence: `~/.alp/sdk-default` is machine-global and
|
|
491
|
+
last-writer-wins across every project that ever relocated a checkout on
|
|
492
|
+
this host, so a caller resolving through the `globalDefault` tier from a
|
|
493
|
+
workspace that is neither the project the pointer was last written for
|
|
494
|
+
nor under the SDK it names is reading an answer left behind by SOMEONE
|
|
495
|
+
ELSE'S bootstrap -- silently, before this. Set only on the `globalDefault`
|
|
496
|
+
tier (the only one this ambiguity can apply to); `None` covers both "the
|
|
497
|
+
pointer was written for (or covers) this workspace" and "the pointer
|
|
498
|
+
predates `writtenFor` and carries no opinion at all" -- deliberately the
|
|
499
|
+
same as `broken_project_pin`'s "no pointer" case, since neither is
|
|
500
|
+
evidence of a mismatch."""
|
|
501
|
+
|
|
502
|
+
path: str | None
|
|
503
|
+
tier: str
|
|
504
|
+
broken_project_pin: str | None = None
|
|
505
|
+
foreign_global_default_for: str | None = None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def resolve_sdk_tiered(sdk_root: str | None, workspace_root: Path) -> ActiveSdk:
|
|
509
|
+
"""`--sdk-root` > project pin > global default > discovery > nothing
|
|
510
|
+
(`util.rs`'s `resolve_sdk_tiered` + `tan_core`'s `resolve_sdk_source_tier`).
|
|
511
|
+
|
|
512
|
+
`--sdk-root` is TERMINAL and returned as-is **even when it is not a
|
|
513
|
+
checkout** (I-31). That is not an oversight to tidy: a bad `--sdk-root`
|
|
514
|
+
surfaces as a `missing` readiness naming the path the user typed, where the
|
|
515
|
+
Pythonic `if not valid: continue` would silently fall through to a lower
|
|
516
|
+
tier and report a DIFFERENT SDK than the one they asked for.
|
|
517
|
+
|
|
518
|
+
Both pointer tiers are best-effort by contrast -- each is used only while it
|
|
519
|
+
still points at a real checkout -- so a stale pointer falls through instead
|
|
520
|
+
of locking the user out of every command. The project pin's own fallthrough
|
|
521
|
+
is not silent, though (tan-cli#263): its raw target survives on the
|
|
522
|
+
returned `ActiveSdk.broken_project_pin` no matter which lower tier ends up
|
|
523
|
+
answering, so a caller can report "this workspace IS pinned, and the pin
|
|
524
|
+
does not resolve" instead of looking indistinguishable from a workspace
|
|
525
|
+
that was never pinned at all -- exactly what let a `tan init` run under a
|
|
526
|
+
since-moved project silently re-resolve a DIFFERENT alp-sdk checkout with
|
|
527
|
+
`ok: true`, `issues: []`.
|
|
528
|
+
|
|
529
|
+
The `globalDefault` tier carries the SAME kind of silence one tier down
|
|
530
|
+
(tan-cli#464): it is one pointer shared by every project on the host, so a
|
|
531
|
+
caller resolving through it from a workspace the pointer was not written
|
|
532
|
+
for -- and that is not even under the SDK it names -- gets no signal that
|
|
533
|
+
a DIFFERENT project's bootstrap relocation is what actually decided this
|
|
534
|
+
answer. `foreign_global_default_for` names that project when this run hit
|
|
535
|
+
exactly that case; `None` when the pointer covers this caller, or predates
|
|
536
|
+
`writtenFor` and has no opinion.
|
|
537
|
+
|
|
538
|
+
tan-cli#466 makes that answer CORRECT, not just disclosed, without moving
|
|
539
|
+
this tier or adding a sixth `SdkSourceTier`: before falling to the single
|
|
540
|
+
legacy pointer, this tier first consults `~/.alp/sdk-defaults.json`, an
|
|
541
|
+
origin-keyed registry every relocating `tan bootstrap` writes alongside
|
|
542
|
+
(never instead of) the legacy file, and picks the DEEPEST registry key
|
|
543
|
+
that contains `workspace_root` (`sdk_default_registry.
|
|
544
|
+
deepest_covering_entry`, using this same `_workspace_under` containment
|
|
545
|
+
test, with a resolved-depth TIE broken by which entry's `updatedAt` is
|
|
546
|
+
most recent -- review, #904 second round, major: two distinct raw
|
|
547
|
+
origins that alias the same directory, e.g. a symlink bootstrapped once
|
|
548
|
+
and the same directory bootstrapped again later through its real path,
|
|
549
|
+
resolve to identical depth). A registry hit still reports `sourceTier:
|
|
550
|
+
"globalDefault"` -- it IS the machine-default mechanism, keyed -- and
|
|
551
|
+
never carries `foreign_global_default_for`: a caller a registry entry
|
|
552
|
+
was written FOR is, by construction, not reading someone else's answer.
|
|
553
|
+
Only when NO registry entry covers the caller does resolution fall
|
|
554
|
+
through to the legacy pointer exactly as it did before this issue,
|
|
555
|
+
foreign-warning included.
|
|
556
|
+
"""
|
|
557
|
+
flag = (sdk_root or "").strip()
|
|
558
|
+
if flag:
|
|
559
|
+
return ActiveSdk(flag, "sdkRootFlag")
|
|
560
|
+
|
|
561
|
+
broken_project_pin: str | None = None
|
|
562
|
+
pin = _pointer_target(workspace_root / ".alp" / "sdk-path")
|
|
563
|
+
if pin is not None:
|
|
564
|
+
if _has_loader_script(Path(pin)):
|
|
565
|
+
return ActiveSdk(pin, "projectPin")
|
|
566
|
+
broken_project_pin = pin
|
|
567
|
+
|
|
568
|
+
registry_raw = _read_global_sdk_registry_raw()
|
|
569
|
+
registry_hit = deepest_covering_entry(
|
|
570
|
+
parse_registry(registry_raw),
|
|
571
|
+
workspace_root,
|
|
572
|
+
covers=_workspace_under,
|
|
573
|
+
has_loader_script=_has_loader_script,
|
|
574
|
+
resolve_origin=_resolved_origin_depth_key,
|
|
575
|
+
updated_at=parse_registry_updated_at(registry_raw),
|
|
576
|
+
)
|
|
577
|
+
if registry_hit is not None:
|
|
578
|
+
_origin, registry_sdk_path = registry_hit
|
|
579
|
+
return ActiveSdk(registry_sdk_path, "globalDefault", broken_project_pin, None)
|
|
580
|
+
|
|
581
|
+
default_pointer = _home_alp_dir() / "sdk-default"
|
|
582
|
+
default = _pointer_target(default_pointer)
|
|
583
|
+
if default is not None and _has_loader_script(Path(default)):
|
|
584
|
+
written_for = _pointer_written_for(default_pointer)
|
|
585
|
+
foreign = (
|
|
586
|
+
written_for
|
|
587
|
+
if written_for is not None
|
|
588
|
+
and not _workspace_under(workspace_root, written_for)
|
|
589
|
+
and not _workspace_under(workspace_root, default)
|
|
590
|
+
else None
|
|
591
|
+
)
|
|
592
|
+
return ActiveSdk(default, "globalDefault", broken_project_pin, foreign)
|
|
593
|
+
|
|
594
|
+
discovered = discover_workspace_sdk(workspace_root)
|
|
595
|
+
if discovered is not None:
|
|
596
|
+
return ActiveSdk(discovered, "discovery", broken_project_pin)
|
|
597
|
+
|
|
598
|
+
return ActiveSdk(None, "none", broken_project_pin)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def project_pin_issue(broken_project_pin: str | None, tier: str) -> Issue | None:
|
|
602
|
+
"""The tan-cli#263 warning for an unresolvable `.alp/sdk-path` project pin
|
|
603
|
+
-- shared by EVERY caller of `resolve_sdk_tiered` (directly, or through
|
|
604
|
+
`resolve_sdk_root_ladder`/`resolve_sdk_root_wide`, below), not just `sdk
|
|
605
|
+
current`. `None` when nothing was rejected, so every call site can do
|
|
606
|
+
`issue = project_pin_issue(broken, tier); if issue: issues.append(issue)`
|
|
607
|
+
unconditionally.
|
|
608
|
+
|
|
609
|
+
`tan build` is the caller this matters most for: a workspace whose pin
|
|
610
|
+
silently misses still gets a real build, against whichever SDK the ladder
|
|
611
|
+
fell through to, with no signal it was not the one `.alp/sdk-path` names
|
|
612
|
+
-- `sdk current` alone only helps someone already suspicious enough to run
|
|
613
|
+
it."""
|
|
614
|
+
if broken_project_pin is None:
|
|
615
|
+
return None
|
|
616
|
+
# Function-scoped: see the module docstring -- `tan.envelope` imports this
|
|
617
|
+
# module at module level, so a top-level import back would be circular.
|
|
618
|
+
from tan.envelope import Issue
|
|
619
|
+
|
|
620
|
+
return Issue(
|
|
621
|
+
"sdk.project-pin-unresolved",
|
|
622
|
+
"warning",
|
|
623
|
+
f'.alp/sdk-path names "{broken_project_pin}", which does not resolve '
|
|
624
|
+
f"to an alp-sdk checkout from the current directory -- falling "
|
|
625
|
+
f"through to the {tier} tier instead.",
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def global_default_foreign_project_issue(foreign_global_default_for: str | None) -> Issue | None:
|
|
630
|
+
"""The tan-cli#464 warning for a `globalDefault` answer that a DIFFERENT
|
|
631
|
+
project's bootstrap relocation actually decided: `~/.alp/sdk-default` is
|
|
632
|
+
one pointer, shared and last-writer-wins across every project that ever
|
|
633
|
+
relocates a checkout on this host, so the earlier of two projects can
|
|
634
|
+
silently start resolving the later one's SDK the moment the later one
|
|
635
|
+
bootstraps -- `ok: true`, `issues: []`, same as a caller that was never
|
|
636
|
+
pinned at all (the maintainer's own #464 repro).
|
|
637
|
+
|
|
638
|
+
`None` when `resolve_sdk_tiered` found nothing to warn about -- the
|
|
639
|
+
pointer covers this caller, or it predates `writtenFor` and carries no
|
|
640
|
+
opinion -- so every call site can do `issue =
|
|
641
|
+
global_default_foreign_project_issue(active.foreign_global_default_for);
|
|
642
|
+
if issue: issues.append(issue)` unconditionally, exactly like
|
|
643
|
+
`project_pin_issue`.
|
|
644
|
+
|
|
645
|
+
tan-cli#466: `resolve_sdk_tiered` sets `foreign_global_default_for` ONLY
|
|
646
|
+
when resolution fell all the way through to this single legacy pointer
|
|
647
|
+
-- never for a `~/.alp/sdk-defaults.json` registry hit, which by
|
|
648
|
+
construction was written FOR the resolving workspace. Resolution itself
|
|
649
|
+
is unchanged by THIS warning specifically: the root `sdk current`
|
|
650
|
+
reports here is the same root it would have reported before this
|
|
651
|
+
function existed -- #466 is what changed the root itself, for the
|
|
652
|
+
workspaces a registry entry now covers.
|
|
653
|
+
"""
|
|
654
|
+
if foreign_global_default_for is None:
|
|
655
|
+
return None
|
|
656
|
+
# Function-scoped: see `project_pin_issue`, above.
|
|
657
|
+
from tan.envelope import Issue
|
|
658
|
+
|
|
659
|
+
return Issue(
|
|
660
|
+
"sdk.global-default-foreign-project",
|
|
661
|
+
"warning",
|
|
662
|
+
f'The machine-global default SDK (~/.alp/sdk-default) was last set by '
|
|
663
|
+
f'a bootstrap relocation in "{foreign_global_default_for}", not by one '
|
|
664
|
+
f"here -- this workspace is falling through to that project's SDK, "
|
|
665
|
+
f"which may not be the checkout you expect. Pin this workspace "
|
|
666
|
+
f"explicitly with `--sdk-root <path>`, or bootstrap here, to stop "
|
|
667
|
+
f"relying on the shared default.",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def sdk_resolution_issues(
|
|
672
|
+
broken_project_pin: str | None, tier: str, foreign_global_default_for: str | None
|
|
673
|
+
) -> list[Issue]:
|
|
674
|
+
"""`project_pin_issue` + `global_default_foreign_project_issue`, together,
|
|
675
|
+
in the order `flash`/`size`/`image` -- the three callers this actually
|
|
676
|
+
has -- append them: each used to compute this pair only on the happy
|
|
677
|
+
path and skip it on a manifest-gate early return.
|
|
678
|
+
|
|
679
|
+
**Not the only copy.** Twelve other modules still hand-copy the pair
|
|
680
|
+
directly rather than through here -- most fold in a third, caller-
|
|
681
|
+
specific issue or use a different order, not the mechanical swap it
|
|
682
|
+
looks like; left as-is (tan-cli#464 review).
|
|
683
|
+
|
|
684
|
+
`[]` when neither fires -- a caller can `issues.extend(...)` unconditionally."""
|
|
685
|
+
issues: list[Issue] = []
|
|
686
|
+
pin_issue = project_pin_issue(broken_project_pin, tier)
|
|
687
|
+
if pin_issue is not None:
|
|
688
|
+
issues.append(pin_issue)
|
|
689
|
+
foreign_issue = global_default_foreign_project_issue(foreign_global_default_for)
|
|
690
|
+
if foreign_issue is not None:
|
|
691
|
+
issues.append(foreign_issue)
|
|
692
|
+
return issues
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@dataclass(frozen=True)
|
|
696
|
+
class SdkRootResolution:
|
|
697
|
+
"""`resolve_sdk_root_ladder`/`resolve_sdk_root_wide`'s own return type --
|
|
698
|
+
a NAMED result rather than a tuple, so a fact one caller needs (e.g.
|
|
699
|
+
`foreign_global_default_for`, tan-cli#464) never forces every OTHER
|
|
700
|
+
caller's unpacking to grow a matching positional slot. A four-element
|
|
701
|
+
tuple was tried here first and rejected on review: it would have
|
|
702
|
+
reproduced the exact silent-drop shape tan-cli#464 exists to close, at
|
|
703
|
+
the next field this ladder needs to carry.
|
|
704
|
+
|
|
705
|
+
Mirrors [`ActiveSdk`]'s same four facts (this IS `ActiveSdk` for the
|
|
706
|
+
two tiers it cannot see -- `discovery`/`none` -- carried through as a
|
|
707
|
+
distinct type only because `.path` here stays `Path`, the shape every
|
|
708
|
+
filesystem-touching caller of these two ladders already expects, where
|
|
709
|
+
`ActiveSdk.path` is the raw `str` its own callers compare and serialize
|
|
710
|
+
verbatim)."""
|
|
711
|
+
|
|
712
|
+
path: Path | None
|
|
713
|
+
tier: str
|
|
714
|
+
broken_project_pin: str | None = None
|
|
715
|
+
foreign_global_default_for: str | None = None
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def resolve_sdk_root_ladder(sdk_root_arg: str | None, workspace_root: Path) -> SdkRootResolution:
|
|
719
|
+
"""The NARROW of this port's TWO discovery ladders,
|
|
720
|
+
shared by the thirteen commands the oracle resolves narrowly (`build`,
|
|
721
|
+
`doctor`, `clean`, `run`, `flash`, `size`, `image`, `kconfig`, `validate`,
|
|
722
|
+
`presets`, `inspect`, `trace`, `sdk current`). The other three -- `init`,
|
|
723
|
+
`generate` and `examples` -- take [`resolve_sdk_root_wide`], below.
|
|
724
|
+
The measurement that splits 13 from 3 is the last paragraph here, and is
|
|
725
|
+
deliberately not repeated there.
|
|
726
|
+
|
|
727
|
+
`--sdk-root` (terminal, I-31) > the project's own pin (`.alp/sdk-path`,
|
|
728
|
+
written by `tan init` / relocated by `tan bootstrap`) > the machine-global
|
|
729
|
+
default (`~/.alp/sdk-default`) > `resolve_sdk_tiered`'s own NARROW
|
|
730
|
+
discovery probe > the wide positional walk (`discover_sdk_root`, above) as
|
|
731
|
+
the tail -- exactly the Rust oracle's closed `SdkSourceTier`
|
|
732
|
+
(`crates/tan-core/src/sdk.rs`): `SdkRootFlag`, `ProjectPin`,
|
|
733
|
+
`GlobalDefault`, `Discovery`, `None`. No sixth tier. Both ladders report
|
|
734
|
+
those same five values; only the discovery tier differs between them.
|
|
735
|
+
|
|
736
|
+
This is the fix for the worst reported CX defect in this port: `tan init`
|
|
737
|
+
writes `.alp/sdk-path` into the project it just scaffolded, but every
|
|
738
|
+
command below used to jump straight from `--sdk-root` to the positional
|
|
739
|
+
walk, skipping `resolve_sdk_tiered` entirely -- so that pointer was
|
|
740
|
+
silently ignored the moment `tan build` ran in that SAME directory,
|
|
741
|
+
unless the checkout also happened to sit beside the project by
|
|
742
|
+
coincidence. `resolve_sdk_tiered` already IS the oracle's `--sdk-root` >
|
|
743
|
+
project pin > global default > discovery chain; the only thing missing
|
|
744
|
+
from it here is the WIDER positional walk as one more fallback below
|
|
745
|
+
`discovery`, for callers ported before `resolve_sdk_tiered` existed.
|
|
746
|
+
|
|
747
|
+
An `ALP_SDK_ROOT` env-var tier was tried here and reverted: the oracle's
|
|
748
|
+
`util.rs::resolve_sdk_root` only ever WRITES that variable into a build
|
|
749
|
+
slice's env, never reads it back for discovery, and `SdkSourceTier` has no
|
|
750
|
+
slot for it -- a sixth `sourceTier` value in the envelope is a wire
|
|
751
|
+
contract change no consumer (the vscode extension, `tan sdk current
|
|
752
|
+
--json`) expects. The project-pin tier above already makes `tan init &&
|
|
753
|
+
tan build` compose without it.
|
|
754
|
+
|
|
755
|
+
**The narrow tier short-circuiting the wide one is deliberate, and
|
|
756
|
+
measured** (tan-cli#263, which proposed inverting it): the wide walk puts
|
|
757
|
+
the CHILD `<ws>/alp-sdk` ahead of the lateral `../alp-sdk`, so hoisting it
|
|
758
|
+
above `resolve_sdk_tiered` would flip every workspace holding both. Driven
|
|
759
|
+
through the oracle binary in constructed layouts, thirteen of its commands
|
|
760
|
+
-- `build`, `doctor`, `clean`, `run`, `flash`, `size`, `image`, `kconfig`,
|
|
761
|
+
`validate`, `presets`, `inspect`, `trace`, `sdk current` -- resolve the
|
|
762
|
+
LATERAL one there, i.e. the narrow order this ladder already has; only
|
|
763
|
+
`init`, `generate` and `examples` take the child. The oracle
|
|
764
|
+
carries two resolutions, not one, and this ladder mirrors the majority
|
|
765
|
+
(narrow) one plus the wide walk as its tail. Inverting it to match the
|
|
766
|
+
other three would move the SDK root under thirteen commands, and a moved
|
|
767
|
+
root is what `plan_exec.sdk_stamp_action` reads as a switch: every existing
|
|
768
|
+
such workspace would take the `build.sdk-switch-pristine` branch on its
|
|
769
|
+
next build and lose every slice's build dir. The three wide commands were
|
|
770
|
+
the real gap; they now have their own helper below, which is why this one
|
|
771
|
+
stays exactly as it is.
|
|
772
|
+
|
|
773
|
+
`SdkRootResolution.broken_project_pin` (tan-cli#263 review) is
|
|
774
|
+
`ActiveSdk.broken_project_pin` carried straight through: the raw
|
|
775
|
+
`.alp/sdk-path` target when that file exists but its target failed the
|
|
776
|
+
loader check, `None` otherwise -- even when a LOWER tier (global default,
|
|
777
|
+
discovery, or nothing) is what this call actually returns.
|
|
778
|
+
[`project_pin_issue`] turns it into the shared `sdk.project-pin-unresolved`
|
|
779
|
+
warning; every caller here should surface it, not just `sdk current` --
|
|
780
|
+
`tan build` is the one that matters most, since it is the command that
|
|
781
|
+
silently builds against whichever SDK the pin's fallthrough landed on.
|
|
782
|
+
`foreign_global_default_for` (tan-cli#464) is the same carry-through for
|
|
783
|
+
`ActiveSdk.foreign_global_default_for`, and
|
|
784
|
+
[`global_default_foreign_project_issue`] is its sibling warning.
|
|
785
|
+
"""
|
|
786
|
+
flag = (sdk_root_arg or "").strip()
|
|
787
|
+
if flag:
|
|
788
|
+
return SdkRootResolution(Path(flag), "sdkRootFlag")
|
|
789
|
+
|
|
790
|
+
tiered = resolve_sdk_tiered(None, workspace_root)
|
|
791
|
+
if tiered.path is not None:
|
|
792
|
+
return SdkRootResolution(
|
|
793
|
+
Path(tiered.path),
|
|
794
|
+
tiered.tier,
|
|
795
|
+
tiered.broken_project_pin,
|
|
796
|
+
tiered.foreign_global_default_for,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
found = discover_sdk_root(workspace_root)
|
|
800
|
+
if found is not None:
|
|
801
|
+
return SdkRootResolution(found, "discovery", tiered.broken_project_pin)
|
|
802
|
+
return SdkRootResolution(None, "none", tiered.broken_project_pin)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def resolve_sdk_root_wide(sdk_root_arg: str | None, workspace_root: Path) -> SdkRootResolution:
|
|
806
|
+
"""The WIDE ladder, for the three commands the oracle routes through
|
|
807
|
+
`util.rs`'s `resolve_sdk_root`: `init`, `generate`, `examples`.
|
|
808
|
+
Same [`SdkRootResolution`] shape as [`resolve_sdk_root_ladder`], same
|
|
809
|
+
field meanings.
|
|
810
|
+
|
|
811
|
+
Same tiers and the same five `SdkSourceTier` values as
|
|
812
|
+
[`resolve_sdk_root_ladder`] above -- `--sdk-root` > project pin > global
|
|
813
|
+
default > discovery -- with exactly one difference: the discovery tier is
|
|
814
|
+
the WIDE positional walk (`discover_sdk_root`) rather than
|
|
815
|
+
`resolve_sdk_tiered`'s narrow probe, so the CHILD `<ws>/alp-sdk` wins over
|
|
816
|
+
the lateral `../alp-sdk` and over an enclosing checkout. The measurement
|
|
817
|
+
behind which command belongs to which ladder lives in that docstring; two
|
|
818
|
+
copies of it would be two things to keep true.
|
|
819
|
+
|
|
820
|
+
`tan init` is why this is a second helper and not a tidy-up waiting to be
|
|
821
|
+
folded back in (tan-cli#263). In a workspace holding both a child checkout
|
|
822
|
+
and a competing sibling, the oracle pins the CHILD into `.alp/sdk-path`;
|
|
823
|
+
the narrow ladder pinned the sibling. That pointer then outranks discovery
|
|
824
|
+
for every later command in that project, so the wrong SDK is not a
|
|
825
|
+
one-command wobble -- it is bound permanently, by the command whose whole
|
|
826
|
+
job was to bind the right one.
|
|
827
|
+
|
|
828
|
+
Skipping the narrow discovery tier can never resolve LESS than the narrow
|
|
829
|
+
ladder would: its candidates (the workspace root itself, `../alp-sdk`, the
|
|
830
|
+
nearest enclosing checkout) are a subset of the wide walk's, so wherever
|
|
831
|
+
narrow hits, wide hits too -- it may only pick a nearer checkout.
|
|
832
|
+
"""
|
|
833
|
+
flag = (sdk_root_arg or "").strip()
|
|
834
|
+
if flag:
|
|
835
|
+
return SdkRootResolution(Path(flag), "sdkRootFlag")
|
|
836
|
+
|
|
837
|
+
tiered = resolve_sdk_tiered(None, workspace_root)
|
|
838
|
+
if tiered.path is not None and tiered.tier != "discovery":
|
|
839
|
+
return SdkRootResolution(
|
|
840
|
+
Path(tiered.path),
|
|
841
|
+
tiered.tier,
|
|
842
|
+
tiered.broken_project_pin,
|
|
843
|
+
tiered.foreign_global_default_for,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
found = discover_sdk_root(workspace_root)
|
|
847
|
+
if found is not None:
|
|
848
|
+
return SdkRootResolution(found, "discovery", tiered.broken_project_pin)
|
|
849
|
+
return SdkRootResolution(None, "none", tiered.broken_project_pin)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
#: tan-cli#407's warning code. Namespaced `sdk.` and not `build.`/`generate.`
|
|
853
|
+
#: on purpose: a caller on EITHER ladder emits this identical string, so a
|
|
854
|
+
#: consumer holding one envelope from each side matches them on the code, and
|
|
855
|
+
#: a per-command spelling would make the pair it has to correlate the one
|
|
856
|
+
#: thing that differs.
|
|
857
|
+
SDK_DISCOVERY_DIVERGENT = "sdk.discovery-divergent"
|
|
858
|
+
|
|
859
|
+
#: Both command groups spelled out in the warning itself, because the reader
|
|
860
|
+
#: is being told their toolchain is split across two checkouts and the useful
|
|
861
|
+
#: next question -- "which of my commands went where?" -- must not require
|
|
862
|
+
#: reading this source.
|
|
863
|
+
_NARROW_COMMANDS = (
|
|
864
|
+
"build, doctor, clean, run, flash, size, image, kconfig, validate, "
|
|
865
|
+
"presets, inspect, trace and sdk current"
|
|
866
|
+
)
|
|
867
|
+
_WIDE_COMMANDS = "init, generate and examples"
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def sdk_ladder_divergence_issue(
|
|
871
|
+
sdk_root_arg: str | None, workspace_root: Path, *, wide: bool
|
|
872
|
+
) -> Issue | None:
|
|
873
|
+
"""The tan-cli#407 warning for a workspace where the two ladders above
|
|
874
|
+
answer DIFFERENT checkouts -- `None` (the overwhelmingly common case)
|
|
875
|
+
when they agree, so every call site can do `issue =
|
|
876
|
+
sdk_ladder_divergence_issue(...); if issue: issues.append(issue)`
|
|
877
|
+
unconditionally, exactly like [`project_pin_issue`].
|
|
878
|
+
|
|
879
|
+
The divergence itself is deliberate and oracle-measured (see
|
|
880
|
+
[`resolve_sdk_root_ladder`]'s own docstring, and
|
|
881
|
+
`tests/commands/test_build_manifest.py`), and this does not try to remove
|
|
882
|
+
it. What it removes is the SILENCE: both ladders label their answer with
|
|
883
|
+
the same `SdkSourceTier` string, `"discovery"`, so in a workspace holding
|
|
884
|
+
both a child `<ws>/alp-sdk` and a lateral `../alp-sdk`, `tan generate`
|
|
885
|
+
emits `build/generated/alp.conf`, the DTS overlays and
|
|
886
|
+
`alp_hw_info_build.h` from one checkout's `metadata/` while `tan build`,
|
|
887
|
+
`tan size` and `tan trace` plan against the other -- and both envelopes
|
|
888
|
+
say `discovery`, leaving no field a consumer could compare. `sdk: {root,
|
|
889
|
+
sourceTier}` exists (#110) precisely so the vscode extension can tell
|
|
890
|
+
which SDK produced a result instead of guessing; one tier name for two
|
|
891
|
+
answers is that key failing at its only job.
|
|
892
|
+
|
|
893
|
+
A sixth `SdkSourceTier` value would be the other way to say this and is
|
|
894
|
+
rejected upstream for the same reason [`resolve_sdk_root_ladder`] rejects
|
|
895
|
+
an `ALP_SDK_ROOT` tier: the closed enum is a wire contract no consumer
|
|
896
|
+
expects to grow, and `test_build_manifest.py` pins the exact
|
|
897
|
+
`SdkRootResolution(path, "discovery", None, None)` both ladders return
|
|
898
|
+
here because that is the oracle's own answer. An `issues[]` entry adds a
|
|
899
|
+
fact without moving a reported one -- the same trade `broken_project_pin`
|
|
900
|
+
already makes.
|
|
901
|
+
|
|
902
|
+
`wide` only picks which side of the pair is labelled "this command"; both
|
|
903
|
+
sides name both checkouts, in the same order, so two envelopes describing
|
|
904
|
+
one collision read as one collision rather than two unrelated warnings.
|
|
905
|
+
|
|
906
|
+
Both roots are compared as `_abs_posix` strings rather than as `Path`s
|
|
907
|
+
because that is the spelling the message quotes and the spelling
|
|
908
|
+
`sdk.root` carries on the wire -- comparing anything else risks reporting
|
|
909
|
+
a "divergence" between two names for one directory. Safe here precisely
|
|
910
|
+
because both ladders derive every candidate from the same
|
|
911
|
+
`workspace_root`: the tiers ABOVE discovery (`--sdk-root`, project pin,
|
|
912
|
+
global default) are shared verbatim, so they can never be the pair that
|
|
913
|
+
differs.
|
|
914
|
+
"""
|
|
915
|
+
narrow_path = resolve_sdk_root_ladder(sdk_root_arg, workspace_root).path
|
|
916
|
+
wide_path = resolve_sdk_root_wide(sdk_root_arg, workspace_root).path
|
|
917
|
+
# One side unresolved is not a collision to disambiguate: the two
|
|
918
|
+
# envelopes already differ by `sourceTier` (`none` vs something), which is
|
|
919
|
+
# the very signal this warning exists to supply.
|
|
920
|
+
if narrow_path is None or wide_path is None:
|
|
921
|
+
return None
|
|
922
|
+
|
|
923
|
+
narrow = _abs_posix(str(narrow_path))
|
|
924
|
+
wider = _abs_posix(str(wide_path))
|
|
925
|
+
if narrow == wider:
|
|
926
|
+
return None
|
|
927
|
+
|
|
928
|
+
# Function-scoped: see the module docstring -- `tan.envelope` imports
|
|
929
|
+
# THIS function at module level (that top-level import is the whole
|
|
930
|
+
# point of tan-cli#408), so a top-level import back would be circular.
|
|
931
|
+
from tan.envelope import Issue
|
|
932
|
+
|
|
933
|
+
narrow_label = _NARROW_COMMANDS if wide else "this command"
|
|
934
|
+
wide_label = "this command" if wide else _WIDE_COMMANDS
|
|
935
|
+
return Issue(
|
|
936
|
+
SDK_DISCOVERY_DIVERGENT,
|
|
937
|
+
"warning",
|
|
938
|
+
f"two alp-sdk checkouts resolve from this directory and both report "
|
|
939
|
+
f'sourceTier "discovery": {narrow_label} uses "{narrow}", while '
|
|
940
|
+
f'{wide_label} uses "{wider}". Generated files and the build plan can '
|
|
941
|
+
f"therefore come from different SDK versions -- pin the one you mean "
|
|
942
|
+
f"with --sdk-root, or with `tan init --sdk-root <path>` to write it "
|
|
943
|
+
f"into .alp/sdk-path, which outranks both discovery tiers.",
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _planner_python_resolution(start: str, sdk_root: str | None) -> tuple[str, bool]:
|
|
948
|
+
"""Like [`_planner_python`], but also reports whether it resolved a
|
|
949
|
+
`tan bootstrap` workspace venv (`True`) or fell back to a bare PATH name
|
|
950
|
+
(`False`).
|
|
951
|
+
|
|
952
|
+
tan-cli#652: `validate_cmd`/`diff_cmd` need this flag to tell "this
|
|
953
|
+
interpreter is missing a dependency because no workspace venv exists yet
|
|
954
|
+
-- run `tan bootstrap`" from "this interpreter is missing a dependency
|
|
955
|
+
for some other reason" when a spawned SDK script dies importing one. A
|
|
956
|
+
bare fallback interpreter that HAPPENS to already have the SDK's
|
|
957
|
+
dependencies (the common from-source-install shape: `jsonschema` is also
|
|
958
|
+
one of tan's own declared dependencies, so it lands on the same
|
|
959
|
+
interpreter `pip install` used) must not be refused pre-emptively --
|
|
960
|
+
only a run that actually fails gets the remedy attached, and only when
|
|
961
|
+
this flag says there was no venv to blame instead.
|
|
962
|
+
"""
|
|
963
|
+
resolved = venv_python(start, sdk_root)
|
|
964
|
+
fallback = "python" if os.name == "nt" else "python3"
|
|
965
|
+
return resolved or fallback, resolved is not None
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _planner_python(start: str, sdk_root: str | None) -> str:
|
|
969
|
+
"""The interpreter a SPAWNED build step runs under.
|
|
970
|
+
|
|
971
|
+
Two callers remain now that planning and every `generate` target run
|
|
972
|
+
in-process: `${PYTHON}` token substitution (below), and
|
|
973
|
+
`generate_cmd`'s `TAN_GENERATE_EXECUTOR=subprocess` escape hatch.
|
|
974
|
+
|
|
975
|
+
Prefers the west-capable workspace venv's `python`
|
|
976
|
+
(`tan.core.venv.venv_python`, mirroring Rust's `venv_python`/
|
|
977
|
+
`resolved_planner_python`), because the SDK planner bakes ITS
|
|
978
|
+
`sys.executable` into every Zephyr slice as `-DPython3_EXECUTABLE=<...>`
|
|
979
|
+
(alp-sdk#787) -- a bare PATH `python3` may lack the `west` module
|
|
980
|
+
entirely, which surfaced as the planner's own `ModuleNotFoundError: No
|
|
981
|
+
module named 'west'` inside CMake configure (tan-cli, the
|
|
982
|
+
documented-install-path bug) rather than a working build.
|
|
983
|
+
|
|
984
|
+
Falls back to a bare PATH name -- `python3` off Windows, `python` on it,
|
|
985
|
+
mirroring `tan_core::project::resolve_python_binary` -- only when no
|
|
986
|
+
workspace venv resolves. NOT `sys.executable`: frozen by PyInstaller,
|
|
987
|
+
`sys.executable` is `tan` itself, so spawning it would just re-enter this
|
|
988
|
+
CLI; and this value is also the `${PYTHON}` substituted into the plan, so
|
|
989
|
+
it has to name an interpreter the slice can find, not this process.
|
|
990
|
+
|
|
991
|
+
A thin wrapper over [`_planner_python_resolution`] -- kept as its own
|
|
992
|
+
name because every existing caller wants only the interpreter path, not
|
|
993
|
+
the venv flag, and a second copy of the resolution logic is exactly the
|
|
994
|
+
kind of drift this repo's conventions warn against.
|
|
995
|
+
"""
|
|
996
|
+
return _planner_python_resolution(start, sdk_root)[0]
|