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,565 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`~/.alp/sdk-defaults.json` -- the origin-keyed sibling of the single
|
|
3
|
+
machine-global `~/.alp/sdk-default` pointer (tan-cli#466, stage 2 of
|
|
4
|
+
tan-cli#464).
|
|
5
|
+
|
|
6
|
+
**The problem stage 1 (tan-cli#464) left open.** `~/.alp/sdk-default` is one
|
|
7
|
+
file, last-writer-wins across every project that ever relocates a checkout on
|
|
8
|
+
this host: bootstrap project A, then bootstrap project B, and A's very next
|
|
9
|
+
`tan build` silently resolves B's checkout through the `globalDefault` tier.
|
|
10
|
+
Stage 1 taught every ladder to DISCLOSE that (`sdk.global-default-foreign-
|
|
11
|
+
project`), which stops the silence but not the wrong answer -- A still builds
|
|
12
|
+
against B's SDK, now with a warning attached.
|
|
13
|
+
|
|
14
|
+
**The fix.** `tan bootstrap` records `origin -> sdkPath` in this registry,
|
|
15
|
+
where `origin` is the same absolute project root stage 1 already computes as
|
|
16
|
+
`writtenFor` -- the directory bootstrap ran in. `sdk_discovery.resolve_sdk_tiered`
|
|
17
|
+
then picks the DEEPEST registry key that CONTAINS the caller's
|
|
18
|
+
`workspace_root`, using the same containment test
|
|
19
|
+
(`sdk_discovery._workspace_under`) stage 1 already uses to decide "foreign". This is
|
|
20
|
+
NOT zero filesystem probing (an earlier version of this docstring, and of
|
|
21
|
+
`changelog.d/466.fixed.md`, claimed it was, and was wrong -- caught in review,
|
|
22
|
+
#904).
|
|
23
|
+
|
|
24
|
+
**What actually touches the filesystem, and how much (corrected a second time
|
|
25
|
+
in review, #904 second round -- the FIRST correction above was itself
|
|
26
|
+
wrong on two counts, and is superseded by this paragraph, not by the one
|
|
27
|
+
above it).** `covers` (`sdk_discovery._workspace_under`) runs once per registry
|
|
28
|
+
entry, unconditionally -- it IS the filter. `has_loader_script` and
|
|
29
|
+
`resolve_origin` (the ranking) run ONLY for a candidate that already passed
|
|
30
|
+
`covers`, i.e. once per COVERING entry, not once per registry entry.
|
|
31
|
+
Measured directly (call counts, not syscalls): a 201-entry registry with a
|
|
32
|
+
single covering entry among 200 non-covering siblings makes 201 `covers`
|
|
33
|
+
calls and exactly 1 `has_loader_script` call and 1 `resolve_origin` call --
|
|
34
|
+
"each candidate" was true only of `covers`.
|
|
35
|
+
|
|
36
|
+
The registry-size claim was ALSO wrong: resolution cost is
|
|
37
|
+
**O(registry size x path depth), not O(registry size) alone** -- every
|
|
38
|
+
`covers`/`resolve_origin` call resolves a real path, and `Path.resolve()`
|
|
39
|
+
`lstat`s every path COMPONENT, so a deeper caller costs more at the same
|
|
40
|
+
registry size. Measured (counting `os.lstat` calls, real directories, the
|
|
41
|
+
production `sdk_discovery._workspace_under`/`_has_loader_script`/
|
|
42
|
+
`_resolved_origin_depth_key` triple, 21 registry entries that ALL cover the
|
|
43
|
+
workspace -- the worst case, nothing skipped by `covers` or
|
|
44
|
+
`has_loader_script`): appending 20 extra path components to the SAME
|
|
45
|
+
workspace, at an unchanged registry size, costs exactly **420** more
|
|
46
|
+
`lstat` calls (21 entries x 1 workspace-resolving call site -- `covers`,
|
|
47
|
+
the only one of the two whose argument the measurement deepens -- x 20
|
|
48
|
+
components a component-`lstat`ing `Path.resolve()` now walks through).
|
|
49
|
+
`resolve_origin` resolves the *origin*, not the workspace, and the
|
|
50
|
+
measurement never deepens the origin, so it contributes zero to the delta
|
|
51
|
+
-- caught in review, #904 final round, major: an earlier revision of this
|
|
52
|
+
paragraph double-counted `resolve_origin` into the factorization (21 x 2 x
|
|
53
|
+
20 = 840) despite the measured delta being 420, and a re-derivation at a
|
|
54
|
+
different base depth (1,050 -> 1,470, same `420` delta) confirmed 420, not
|
|
55
|
+
840, is correct. The delta is reported ALONE, not alongside a base-count
|
|
56
|
+
absolute, because the absolute moves with the base depth chosen for the
|
|
57
|
+
measurement and an earlier revision of this paragraph cited one (**1,092**
|
|
58
|
+
vs **1,512**) without saying what that base depth was -- caught in review,
|
|
59
|
+
#904 third round, nit: an independent re-derivation at a different (but
|
|
60
|
+
equally unstated) base depth landed on **1,050**/**1,470**, the same `420`
|
|
61
|
+
delta but a different pair of absolutes, for no reason a reader of either
|
|
62
|
+
number alone could tell. Cite neither this number nor any other syscall
|
|
63
|
+
count for this function without also stating the registry shape (size, how
|
|
64
|
+
many entries cover the caller) and the base path depth it was measured at --
|
|
65
|
+
both factors independently move any ABSOLUTE count; the delta above is the
|
|
66
|
+
one number in this paragraph that does not need one, because it was measured
|
|
67
|
+
as a difference at two path depths under the same registry shape and cancels
|
|
68
|
+
out.
|
|
69
|
+
|
|
70
|
+
What IS true, and what the issue actually promised, is that there is no
|
|
71
|
+
directory WALK -- the candidate set is closed (only directories a real
|
|
72
|
+
`tan bootstrap` explicitly ran in can ever appear as a key), so resolution
|
|
73
|
+
cost does not grow with how deep the caller's workspace sits BELOW its
|
|
74
|
+
covering origin, only with the registry's own size and the ABSOLUTE depth of
|
|
75
|
+
the paths involved. A's subdirectory then resolves A's own entry before the
|
|
76
|
+
legacy pointer is ever consulted, at any depth below its own origin,
|
|
77
|
+
regardless of which project bootstrapped last.
|
|
78
|
+
|
|
79
|
+
The legacy single pointer is NOT retired. `tan bootstrap` keeps writing it
|
|
80
|
+
unconditionally, for skew safety: an old tan that predates this file reads
|
|
81
|
+
only `~/.alp/sdk-default` and never opens this one, so a mixed-version fleet
|
|
82
|
+
degrades to stage 1's disclosed-but-wrong behaviour, never to a hard failure.
|
|
83
|
+
A new tan consults the registry FIRST and falls back to the legacy pointer
|
|
84
|
+
only when no entry here covers the caller -- which is also when the stage-1
|
|
85
|
+
foreign-project warning still fires, now for a genuinely unregistered caller
|
|
86
|
+
rather than for every non-last bootstrap on the host.
|
|
87
|
+
|
|
88
|
+
**A pure `tan.core` module, deliberately.** The functions that need
|
|
89
|
+
filesystem judgement calls (whether one path contains another; whether an
|
|
90
|
+
`sdkPath` still names a real checkout; what a raw origin string resolves to)
|
|
91
|
+
are `sdk_discovery._workspace_under`, `sdk_discovery._has_loader_script`, and
|
|
92
|
+
`sdk_discovery._resolved_origin_depth_key` -- injected into `deepest_covering_entry`
|
|
93
|
+
as callables rather than imported, so this module stays free of any
|
|
94
|
+
`tan.commands.*` import, the same convention every other `tan/core` module
|
|
95
|
+
here already follows. `sdk_discovery.py` and `bootstrap_cmd.py` both import
|
|
96
|
+
this module instead of duplicating its parse/format logic.
|
|
97
|
+
"""
|
|
98
|
+
from __future__ import annotations
|
|
99
|
+
|
|
100
|
+
import json
|
|
101
|
+
import re
|
|
102
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
103
|
+
from typing import Any, Callable
|
|
104
|
+
|
|
105
|
+
#: Both wire shapes this registry's `updatedAt` can carry --
|
|
106
|
+
#: `generated_at_iso`/`wall_clock_iso`'s seconds form (`...:00Z`) and
|
|
107
|
+
#: milliseconds form (`...:00.000Z`) -- sort correctly against EACH OTHER
|
|
108
|
+
#: (millis form always sorts later within the same second, since `.` <
|
|
109
|
+
#: any digit... except it doesn't: `'Z'` (0x5A) sorts AFTER `'.'` (0x2E),
|
|
110
|
+
#: so a bare seconds-precision stamp outranks a same-second millis stamp as
|
|
111
|
+
#: a raw string, backwards from wall-clock order). Only a HAND-EDITED
|
|
112
|
+
#: registry can produce this today -- `with_entry`'s one production caller
|
|
113
|
+
#: always writes `millis=True` -- but this module explicitly anticipates
|
|
114
|
+
#: hand-edited registries (`parse_registry_updated_at`'s own docstring), so
|
|
115
|
+
#: the tie-break normalises rather than leaving mixed precision undefined.
|
|
116
|
+
_SECONDS_PRECISION_UPDATED_AT = re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})Z$")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _comparable_updated_at(value: str) -> str:
|
|
120
|
+
"""`value`, widened to millisecond precision if it is a bare
|
|
121
|
+
seconds-precision ISO-8601 stamp, so `>` on the result orders by actual
|
|
122
|
+
wall-clock recency instead of by which precision a hand-edited entry
|
|
123
|
+
happened to use. Anything that is not exactly that seconds-precision
|
|
124
|
+
shape -- already-millis, empty (`parse_registry_updated_at`'s
|
|
125
|
+
no-timestamp default), or not a recognisable stamp at all -- passes
|
|
126
|
+
through unchanged: this only closes the one concrete mis-order
|
|
127
|
+
(review, #904 final round, nit), it does not attempt to validate or
|
|
128
|
+
reformat arbitrary hand-edited garbage."""
|
|
129
|
+
match = _SECONDS_PRECISION_UPDATED_AT.match(value)
|
|
130
|
+
return f"{match.group(1)}.000Z" if match else value
|
|
131
|
+
|
|
132
|
+
#: `~/.alp/<REGISTRY_FILENAME>` -- named distinctly from the legacy
|
|
133
|
+
#: `sdk-default` (singular, no extension) so a directory listing of `~/.alp`
|
|
134
|
+
#: cannot mistake one file for the other, and so a plain grep for
|
|
135
|
+
#: `sdk-default` (the legacy pointer's own name) never silently matches this
|
|
136
|
+
#: file too.
|
|
137
|
+
REGISTRY_FILENAME = "sdk-defaults.json"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def registry_path(home_alp_dir: Path) -> Path:
|
|
141
|
+
"""`<home_alp_dir>/sdk-defaults.json`. `home_alp_dir` is the caller's own
|
|
142
|
+
`~/.alp` (`sdk_discovery._home_alp_dir()`), passed in rather than recomputed
|
|
143
|
+
here -- this module has no opinion on `HOME`/`USERPROFILE` resolution,
|
|
144
|
+
exactly like it has no opinion on filesystem containment."""
|
|
145
|
+
return home_alp_dir / REGISTRY_FILENAME
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_registry(raw: str | None) -> dict[str, str]:
|
|
149
|
+
"""`origin -> sdkPath`, best-effort. `raw` is the file's text, or `None`
|
|
150
|
+
when it could not be read at all (missing, permission-denied, non-UTF-8 --
|
|
151
|
+
`sdk_discovery._read_file`'s own contract).
|
|
152
|
+
|
|
153
|
+
Every failure shape degrades to `{}`, the empty registry -- a truncated
|
|
154
|
+
write from a concurrent `tan bootstrap`, a hand-edited syntax error, a
|
|
155
|
+
JSON array instead of an object, a value that is not itself an object, an
|
|
156
|
+
`sdkPath` that is missing or not a string. `{}` is exactly what "no
|
|
157
|
+
registry file at all" already means to every caller, so a malformed
|
|
158
|
+
registry degrades to the legacy single pointer -- never to a crash, and
|
|
159
|
+
never to a partial/best-guess reading of a corrupt file.
|
|
160
|
+
"""
|
|
161
|
+
if raw is None:
|
|
162
|
+
return {}
|
|
163
|
+
try:
|
|
164
|
+
parsed = json.loads(raw)
|
|
165
|
+
except ValueError:
|
|
166
|
+
return {}
|
|
167
|
+
if not isinstance(parsed, dict):
|
|
168
|
+
return {}
|
|
169
|
+
result: dict[str, str] = {}
|
|
170
|
+
for origin, entry in parsed.items():
|
|
171
|
+
if not isinstance(origin, str) or not origin:
|
|
172
|
+
continue
|
|
173
|
+
sdk_path = entry.get("sdkPath") if isinstance(entry, dict) else None
|
|
174
|
+
if isinstance(sdk_path, str) and sdk_path:
|
|
175
|
+
result[origin] = sdk_path
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def parse_registry_updated_at(raw: str | None) -> dict[str, str]:
|
|
180
|
+
"""`origin -> updatedAt`, the recency companion of `parse_registry` --
|
|
181
|
+
feeds `deepest_covering_entry`'s tie-break (review, #904 second round,
|
|
182
|
+
major: two origins that resolve to the SAME directory tie on depth, and
|
|
183
|
+
`depth > best_depth` alone keeps whichever one the sorted-key loop visits
|
|
184
|
+
first, i.e. the lexicographically smallest raw origin -- not the more
|
|
185
|
+
recent bootstrap).
|
|
186
|
+
|
|
187
|
+
A SEPARATE function rather than widening `parse_registry`'s own return
|
|
188
|
+
shape, for the same reason `load_raw` is separate from it: `parse_registry`
|
|
189
|
+
is the flattened `origin -> sdkPath` view several callers already depend
|
|
190
|
+
on staying exactly that shape, and every one of its existing callers has
|
|
191
|
+
no use for a timestamp.
|
|
192
|
+
|
|
193
|
+
Degrades exactly like `parse_registry` -- `{}` for a missing/malformed
|
|
194
|
+
file, and an origin whose ENTRY is not itself a dict-shaped object is
|
|
195
|
+
DROPPED, the identical shape `parse_registry` gives it (a bare string
|
|
196
|
+
value, `{"/proj/a": "/sdk/a"}`, is not an entry either function
|
|
197
|
+
recognises). Within a dict-shaped entry, a missing or non-string
|
|
198
|
+
`updatedAt` degrades to `""` instead -- an entry with no timestamp
|
|
199
|
+
(written by a pre-tie-break tan, or hand-edited) must still be RANKABLE
|
|
200
|
+
by depth, it only forfeits ties -- `""` sorts before every real
|
|
201
|
+
ISO-8601 stamp, so it never wins one.
|
|
202
|
+
"""
|
|
203
|
+
if raw is None:
|
|
204
|
+
return {}
|
|
205
|
+
try:
|
|
206
|
+
parsed = json.loads(raw)
|
|
207
|
+
except ValueError:
|
|
208
|
+
return {}
|
|
209
|
+
if not isinstance(parsed, dict):
|
|
210
|
+
return {}
|
|
211
|
+
result: dict[str, str] = {}
|
|
212
|
+
for origin, entry in parsed.items():
|
|
213
|
+
if not isinstance(origin, str) or not origin:
|
|
214
|
+
continue
|
|
215
|
+
if not isinstance(entry, dict):
|
|
216
|
+
continue
|
|
217
|
+
updated_at = entry.get("updatedAt")
|
|
218
|
+
result[origin] = updated_at if isinstance(updated_at, str) else ""
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_raw(raw: str | None) -> dict[str, Any]:
|
|
223
|
+
"""The WRITE side's counterpart to `parse_registry`: the registry's own
|
|
224
|
+
parsed-JSON shape, untouched, or `{}` on any read/parse failure (missing,
|
|
225
|
+
truncated, not even an object).
|
|
226
|
+
|
|
227
|
+
Deliberately NOT `parse_registry`'s flattened `origin -> sdkPath` view --
|
|
228
|
+
that view already discards the per-entry object wrapper
|
|
229
|
+
(`{"sdkPath": ..., "updatedAt": ...}`) an entry is stored as, so a writer
|
|
230
|
+
that read the FLATTENED view, patched one key, and wrote it back would
|
|
231
|
+
silently downgrade every OTHER origin's entry to a bare string the next
|
|
232
|
+
read still tolerates (`parse_registry` accepts only dict-shaped entries)
|
|
233
|
+
but which no longer round-trips its own wrapper. `with_entry` is written
|
|
234
|
+
against THIS shape for that reason.
|
|
235
|
+
"""
|
|
236
|
+
if raw is None:
|
|
237
|
+
return {}
|
|
238
|
+
try:
|
|
239
|
+
parsed = json.loads(raw)
|
|
240
|
+
except ValueError:
|
|
241
|
+
return {}
|
|
242
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def is_absolute_either_platform(value: str) -> bool:
|
|
246
|
+
"""Mirrors `sdk_discovery._pointer_written_for`'s own cross-platform absolute
|
|
247
|
+
check, for the same reason: this registry is one file shared by every
|
|
248
|
+
`tan` on the host, and a bare `Path(value).is_absolute()` is answered by
|
|
249
|
+
whichever pathlib flavour the READING platform picked. A relative,
|
|
250
|
+
empty, or drive-relative origin (a hand-edited registry, or a future
|
|
251
|
+
writer bug) is rejected under BOTH flavours rather than accepted under
|
|
252
|
+
whichever one the reader happens to be running.
|
|
253
|
+
|
|
254
|
+
PUBLIC (no leading underscore) because `sdk_removal.removal_would_take_out`
|
|
255
|
+
needs the identical judgement before it will consult the filesystem about a
|
|
256
|
+
stored value, and a cross-module import of a private name is a worse
|
|
257
|
+
dependency than an exported one (tan-cli#1053 review)."""
|
|
258
|
+
return PurePosixPath(value).is_absolute() or PureWindowsPath(value).is_absolute()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def deepest_covering_entry(
|
|
262
|
+
registry: dict[str, str],
|
|
263
|
+
workspace_root: Path,
|
|
264
|
+
*,
|
|
265
|
+
covers: Callable[[Path, str], bool],
|
|
266
|
+
has_loader_script: Callable[[Path], bool],
|
|
267
|
+
resolve_origin: Callable[[str], str],
|
|
268
|
+
updated_at: dict[str, str] | None = None,
|
|
269
|
+
) -> tuple[str, str] | None:
|
|
270
|
+
"""The deepest `(origin, sdkPath)` entry whose `origin` CONTAINS
|
|
271
|
+
`workspace_root` (`covers`) and whose `sdkPath` still resolves
|
|
272
|
+
(`has_loader_script`), or `None` when nothing qualifies.
|
|
273
|
+
|
|
274
|
+
"Deepest" is measured by the length of the origin's RESOLVED path
|
|
275
|
+
(`resolve_origin`), NOT the raw registry key string. That distinction is
|
|
276
|
+
load-bearing, found by review (#904): `covers` (`sdk_discovery._workspace_under`
|
|
277
|
+
in production) decides containment on `.resolve()`d paths, so a symlinked
|
|
278
|
+
origin makes the raw key's string length disagree with `covers`'s own
|
|
279
|
+
notion of "deeper" -- two origins reached through a symlink can be
|
|
280
|
+
identical, or even INVERTED in order, once resolved, while differing in
|
|
281
|
+
raw length. (A path-segment COUNT of the raw key does not fix this
|
|
282
|
+
either: a symlink can collapse or expand the segment count exactly as it
|
|
283
|
+
can the character count.) Ranking by the SAME resolved value `covers`
|
|
284
|
+
uses restores the exactness this function claims: every candidate this
|
|
285
|
+
loop ranks has already passed `covers(workspace_root, origin)`, so any
|
|
286
|
+
two of them are both (resolved) ancestors of the SAME resolved
|
|
287
|
+
`workspace_root` -- one ancestor of two ancestors of one point is always
|
|
288
|
+
an ancestor of the other (or they are the same directory), which makes
|
|
289
|
+
"contains the other's RESOLVED string as a prefix" and "is the shorter
|
|
290
|
+
RESOLVED string" the identical fact for this already-filtered set.
|
|
291
|
+
|
|
292
|
+
`resolve_origin` is injected the same way `covers` and `has_loader_script`
|
|
293
|
+
are, for the same reason: this module stays free of any direct filesystem
|
|
294
|
+
touch, even one as innocuous-looking as `Path.resolve()`.
|
|
295
|
+
|
|
296
|
+
A covering entry whose `sdkPath` fails `has_loader_script` is SKIPPED,
|
|
297
|
+
not merely deprioritised: the next-deepest covering entry (if any) is
|
|
298
|
+
tried instead, the same way the single legacy pointer degrades to a lower
|
|
299
|
+
tier today when ITS target fails the identical check -- a stale entry
|
|
300
|
+
left behind by a checkout that moved or was deleted must not block a
|
|
301
|
+
shallower, still-valid one from answering.
|
|
302
|
+
|
|
303
|
+
**A resolved-depth TIE is broken by `updated_at` recency, most-recent
|
|
304
|
+
wins** (review, #904 second round, major). Two DISTINCT raw origins can
|
|
305
|
+
resolve to the exact SAME directory -- a symlinked alias bootstrapped
|
|
306
|
+
once, then the same directory bootstrapped AGAIN later through its real
|
|
307
|
+
path -- and then tie on `depth` exactly. The pre-fix `depth > best_depth`
|
|
308
|
+
strict inequality never replaces a tie, so whichever origin
|
|
309
|
+
`registry.items()` visits FIRST keeps the win; since the file is written
|
|
310
|
+
with `sort_keys=True` (`registry_text`), that is the lexicographically
|
|
311
|
+
SMALLEST raw origin string, an accident of spelling with no relationship
|
|
312
|
+
to which bootstrap is authoritative. `updated_at` (origin -> its entry's
|
|
313
|
+
`updatedAt`, `sdk_default_registry.parse_registry_updated_at`) is
|
|
314
|
+
threaded through the same way `resolve_origin` is -- plain data, not a
|
|
315
|
+
filesystem touch -- and an origin missing a timestamp (a registry
|
|
316
|
+
written before this fix, or hand-edited) degrades to `""`, which never
|
|
317
|
+
outranks a real stamp and never breaks an existing tie either, so it is
|
|
318
|
+
exactly as decided as it always was. The comparison itself runs on
|
|
319
|
+
`_comparable_updated_at`'s output, not the raw string (review, #904
|
|
320
|
+
final round, nit): a raw string compare ranks a bare seconds-precision
|
|
321
|
+
stamp AHEAD of a same-second milliseconds one (`'Z'` > `'.'`), which
|
|
322
|
+
only a hand-edited entry can produce today, but this module already
|
|
323
|
+
anticipates hand-edited registries, so the tie-break normalises
|
|
324
|
+
precision rather than leaving that case undefined.
|
|
325
|
+
|
|
326
|
+
**Why recency, not collapsing the registry KEY onto its resolved origin
|
|
327
|
+
at write time (the other fix considered, and rejected, in review #904
|
|
328
|
+
second round):** resolving at WRITE time and storing that resolved value
|
|
329
|
+
AS the key would reintroduce, for the key, precisely the staleness this
|
|
330
|
+
function's own `resolve_origin` injection exists to avoid for the
|
|
331
|
+
RANKING -- a value baked in once at write time that the live filesystem
|
|
332
|
+
can silently outgrow (a symlink repointed after the fact, without a new
|
|
333
|
+
`tan bootstrap` ever touching that origin again), with no
|
|
334
|
+
`has_loader_script`-style fallback able to catch it, because the
|
|
335
|
+
original raw origin -- the only thing a future write could still
|
|
336
|
+
recognise -- would already be gone, overwritten by whatever it resolved
|
|
337
|
+
to at the moment it was written. Recency needs none of that: `origin`
|
|
338
|
+
keeps meaning exactly what `tan bootstrap` was run in for every entry,
|
|
339
|
+
forever, matching what `written_for`/`global_default_pointer_fix_hint`
|
|
340
|
+
already report elsewhere in this system, and the tie-break is decided by
|
|
341
|
+
a plain, append-only timestamp instead of a second filesystem-truth
|
|
342
|
+
baked into the registry's own keys.
|
|
343
|
+
"""
|
|
344
|
+
updated_at = updated_at or {}
|
|
345
|
+
best: tuple[str, str] | None = None
|
|
346
|
+
best_depth = -1
|
|
347
|
+
best_updated_at = ""
|
|
348
|
+
for origin, sdk_path in registry.items():
|
|
349
|
+
if not is_absolute_either_platform(origin):
|
|
350
|
+
continue
|
|
351
|
+
if not covers(workspace_root, origin):
|
|
352
|
+
continue
|
|
353
|
+
if not has_loader_script(Path(sdk_path)):
|
|
354
|
+
continue
|
|
355
|
+
depth = len(resolve_origin(origin).replace("\\", "/"))
|
|
356
|
+
entry_updated_at = _comparable_updated_at(updated_at.get(origin, ""))
|
|
357
|
+
if depth > best_depth or (depth == best_depth and entry_updated_at > best_updated_at):
|
|
358
|
+
best_depth = depth
|
|
359
|
+
best_updated_at = entry_updated_at
|
|
360
|
+
best = (origin, sdk_path)
|
|
361
|
+
return best
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def with_entry(
|
|
365
|
+
registry: dict[str, Any], *, origin: str, sdk_root: str, updated_at: str
|
|
366
|
+
) -> dict[str, str]:
|
|
367
|
+
"""`registry`, a raw parsed-JSON dict (may hold entries in any shape --
|
|
368
|
+
this is the WRITE side, applied to whatever `parse_registry`-adjacent
|
|
369
|
+
reading a writer already did), with `origin` keyed to `{"sdkPath":
|
|
370
|
+
sdk_root, "updatedAt": updated_at}`. Every other key is left untouched,
|
|
371
|
+
so one `tan bootstrap` run updating its own origin never drops another
|
|
372
|
+
project's entry.
|
|
373
|
+
|
|
374
|
+
**`updated_at` is the caller's to compute, not this function's** (review,
|
|
375
|
+
#904 second round, major -- `deepest_covering_entry`'s recency
|
|
376
|
+
tie-break): this module stays free of the wall clock the same way it
|
|
377
|
+
stays free of the filesystem, so every OTHER function here is a plain,
|
|
378
|
+
deterministic function of its arguments and stays that way, testable
|
|
379
|
+
without freezing time. The production caller
|
|
380
|
+
(`bootstrap_cmd._write_global_sdk_registry`) passes
|
|
381
|
+
`tan.core.timestamp.wall_clock_iso(millis=True)` -- **not**
|
|
382
|
+
`generated_at_iso`, and that distinction is load-bearing (review, #904
|
|
383
|
+
third round, major): `generated_at_iso` lets `SOURCE_DATE_EPOCH` win over
|
|
384
|
+
the clock, which is exactly right for a reproducible envelope and exactly
|
|
385
|
+
wrong here, because this field's entire job is to ORDER two writes
|
|
386
|
+
against each other on one host -- a value every bootstrap inside one
|
|
387
|
+
`SOURCE_DATE_EPOCH`-pinned shell would render identically, silently
|
|
388
|
+
reopening the very tie this tie-break exists to close. Millisecond
|
|
389
|
+
precision, deliberately finer than the legacy pointer's own `updatedAt`
|
|
390
|
+
(seconds, `_global_sdk_pointer_json`), because a whole SDK relocation
|
|
391
|
+
plus this write easily completing inside one second is not a corner case
|
|
392
|
+
worth losing the tie-break to.
|
|
393
|
+
|
|
394
|
+
**This function alone is still append-only** (review, #904, nit 1): it
|
|
395
|
+
only ever adds or overwrites `origin`, never drops another entry, even a
|
|
396
|
+
dead one. Pruning is a SEPARATE function, `prune_dead_origins` below
|
|
397
|
+
(tan-cli#905) -- the production writer
|
|
398
|
+
(`bootstrap_cmd._write_global_sdk_registry`) runs it right alongside this
|
|
399
|
+
one, on every relocating `tan bootstrap`, so the two together keep the
|
|
400
|
+
registry both current (this function) and bounded (that one) in the same
|
|
401
|
+
read-modify-write. See `prune_dead_origins`'s own docstring for what
|
|
402
|
+
"dead" means and why.
|
|
403
|
+
"""
|
|
404
|
+
updated = dict(registry)
|
|
405
|
+
updated[origin] = {"sdkPath": sdk_root, "updatedAt": updated_at}
|
|
406
|
+
return updated
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def prune_dead_origins(
|
|
410
|
+
registry: dict[str, Any], *, origin_exists: Callable[[str], bool]
|
|
411
|
+
) -> dict[str, Any]:
|
|
412
|
+
"""`registry` with every entry whose ORIGIN directory no longer exists on
|
|
413
|
+
this host dropped -- `with_entry`'s "append-only; nothing here prunes"
|
|
414
|
+
gap, closed (tan-cli#905).
|
|
415
|
+
|
|
416
|
+
**What "dead" means here, and why this test and no other.** The module
|
|
417
|
+
docstring above already establishes the candidate set is closed: a key
|
|
418
|
+
can only be here because a real `tan bootstrap` ran in that directory.
|
|
419
|
+
`covers` (`sdk_discovery._workspace_under`) decides a match by STRING
|
|
420
|
+
containment on `.resolve()`d paths and does not itself require either
|
|
421
|
+
side to exist -- so the one fact that makes an origin permanently
|
|
422
|
+
unmatchable, not merely unmatched by the CURRENT invocation, is that its
|
|
423
|
+
own directory is gone: no process can set its current working directory
|
|
424
|
+
to a path that does not exist, so no future `workspace_root` can ever
|
|
425
|
+
again be produced that resolves under a deleted origin (barring the
|
|
426
|
+
coincidence of something unrelated being created at that exact path
|
|
427
|
+
later, at which point the next real `tan bootstrap` there overwrites
|
|
428
|
+
this entry anyway, exactly as it would for a brand-new origin).
|
|
429
|
+
|
|
430
|
+
**Explicitly NOT the criterion: a dead `sdkPath`.**
|
|
431
|
+
`deepest_covering_entry` already SKIPS a covering entry whose `sdkPath`
|
|
432
|
+
fails `has_loader_script`, deliberately, because that origin's PROJECT
|
|
433
|
+
may simply be between bootstraps -- a checkout mid-move, or a project
|
|
434
|
+
that has not re-bootstrapped since its SDK relocated. That entry is
|
|
435
|
+
"merely unused right now", not dead: the very next `tan bootstrap` run
|
|
436
|
+
from that origin repairs it for free, and pruning it first would only
|
|
437
|
+
force a needless rediscovery. Origin existence is the only test this
|
|
438
|
+
module can run that tells the two apart without guessing at intent
|
|
439
|
+
(age-based pruning was considered and rejected for the same reason --
|
|
440
|
+
see the issue -- a long-idle but still-real project origin is
|
|
441
|
+
indistinguishable from an abandoned one by timestamp alone, and getting
|
|
442
|
+
that wrong deletes a live project's entry, not a stale one).
|
|
443
|
+
|
|
444
|
+
Not a filesystem touch itself: `origin_exists` is injected the same way
|
|
445
|
+
`covers`/`has_loader_script`/`resolve_origin` are injected into
|
|
446
|
+
`deepest_covering_entry`, so this module stays free of any direct `Path`
|
|
447
|
+
IO. The production caller (`bootstrap_cmd._write_global_sdk_registry`)
|
|
448
|
+
degrades an INCONCLUSIVE check (a `PermissionError`, or any other
|
|
449
|
+
exception) to "exists" -- an entry is dropped only on a confirmed
|
|
450
|
+
absence, never on an inability to check one, the same conservative bias
|
|
451
|
+
every other best-effort read in this module already applies.
|
|
452
|
+
|
|
453
|
+
Applies to every key in `registry`, including one shaped too oddly for
|
|
454
|
+
`parse_registry`/`parse_registry_updated_at` to have kept (a bare string
|
|
455
|
+
value, a non-dict entry): a malformed entry whose origin is gone is
|
|
456
|
+
exactly as dead as a well-formed one, and skipping it would leave the one
|
|
457
|
+
thing this function exists to bound -- the registry's own unbounded
|
|
458
|
+
growth (tan-cli#905's own complaint) -- unfixed for exactly the shapes
|
|
459
|
+
most likely to belong to a hand-edited or long-abandoned entry.
|
|
460
|
+
|
|
461
|
+
No concurrency story of its own: called from the SAME read-modify-write
|
|
462
|
+
`with_entry` already is, so a prune racing a second `tan bootstrap`'s
|
|
463
|
+
write carries the identical last-writer-wins tolerance
|
|
464
|
+
`_write_global_sdk_registry` already accepts for that write -- the
|
|
465
|
+
LOSER's entry (add or prune) is simply overwritten by the winner's next
|
|
466
|
+
write, never corrupted or partially applied.
|
|
467
|
+
"""
|
|
468
|
+
return {origin: entry for origin, entry in registry.items() if origin_exists(origin)}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def normalized_sdk_path(value: str) -> str:
|
|
472
|
+
"""A registry `sdkPath` reduced to the spelling its WRITER would have used,
|
|
473
|
+
so a stored value and a freshly-computed target compare equal.
|
|
474
|
+
|
|
475
|
+
`bootstrap_cmd._write_global_sdk_registry` posix-normalises every `sdkPath`
|
|
476
|
+
it writes (`sdk_discovery._to_posix`), so on POSIX this is already the
|
|
477
|
+
identity and the comparison was never at risk. On Windows it is not:
|
|
478
|
+
`write_registry`-shaped hand edits -- which this module's own
|
|
479
|
+
`parse_registry` contract explicitly accommodates, and which the registry
|
|
480
|
+
docs describe as a real shape a file can be left in -- spell the same
|
|
481
|
+
directory `C:\\Users\\me\\sdk`, and a raw `==` against the
|
|
482
|
+
forward-slash `C:/Users/me/sdk` this codebase computes everywhere else
|
|
483
|
+
then answers False for two names of ONE directory.
|
|
484
|
+
|
|
485
|
+
That mismatch is not cosmetic where it is used. `tan sdk remove` consults
|
|
486
|
+
it twice: to decide whether an install is a registered project's default
|
|
487
|
+
and must therefore be REFUSED without `--force` (tan-cli#790's first design
|
|
488
|
+
bar -- "silently orphaning any of the three is a worse failure than a
|
|
489
|
+
refusal that names exactly what would break"), and to prune the entry
|
|
490
|
+
afterwards. A False here silently removes an install another project still
|
|
491
|
+
points at, which is the one outcome that whole refusal exists to prevent.
|
|
492
|
+
|
|
493
|
+
Separator folding ONLY -- deliberately not `_abs_posix`, whose `abspath`
|
|
494
|
+
would anchor a relative stored value to the REMOVING process's cwd and
|
|
495
|
+
invent a match that the writer never wrote. This is the exact inverse of
|
|
496
|
+
the write, and nothing more. Case folding is NOT applied either: NTFS is
|
|
497
|
+
case-insensitive in practice, so two entries differing only in case name
|
|
498
|
+
one directory there too, but folding case would make this function wrong on
|
|
499
|
+
the case-SENSITIVE POSIX side. That fix landed where it belongs, as the
|
|
500
|
+
platform-aware comparison this result is FED INTO rather than smuggled in
|
|
501
|
+
here -- `sdk_removal.removal_would_take_out` (tan-cli#1053), which folds
|
|
502
|
+
case only where the platform does and consults the filesystem itself when
|
|
503
|
+
the lexical answer misses.
|
|
504
|
+
"""
|
|
505
|
+
return value.replace("\\", "/")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def prune_entries_by_sdk_path(
|
|
509
|
+
registry: dict[str, Any], *, sdk_path: str, matches: Callable[[str, str], bool]
|
|
510
|
+
) -> dict[str, Any]:
|
|
511
|
+
"""`registry` with every entry whose `sdkPath` NAMES `sdk_path` -- per
|
|
512
|
+
`matches`, not a raw `==`; see below -- dropped --
|
|
513
|
+
`tan sdk remove`'s own honesty obligation (tan-cli#790), the sibling of
|
|
514
|
+
[`prune_dead_origins`] pruning on the OTHER axis: that function drops an
|
|
515
|
+
entry whose ORIGIN (the project that registered it) no longer exists;
|
|
516
|
+
this one drops an entry whose TARGET (the SDK checkout it points at) was
|
|
517
|
+
just deleted by this very command.
|
|
518
|
+
|
|
519
|
+
Without this, a removed install stays registered as SOME other project's
|
|
520
|
+
machine-global default: that project's next `sdk current`/`tan build`
|
|
521
|
+
would still answer `sourceTier: "globalDefault"` naming a path that is
|
|
522
|
+
gone, degrading only as far as `check_sdk_readiness`'s `state:
|
|
523
|
+
"missing"` -- correctly REPORTED, never repaired, and never falling
|
|
524
|
+
through to a lower tier the way a dead `~/.alp/sdk-default` pointer
|
|
525
|
+
already does. `deepest_covering_entry` applies the identical
|
|
526
|
+
`has_loader_script` gate to a registry hit too, so leaving a dead entry
|
|
527
|
+
in place would not silently mis-resolve anyone -- it would just leave a
|
|
528
|
+
permanently-unusable entry in a file tan-cli#905 exists to keep bounded.
|
|
529
|
+
This closes the gap anyway, on tan-cli#905's own reasoning for pruning
|
|
530
|
+
eagerly: the registry is honest about what still resolves, not merely
|
|
531
|
+
eventually self-correcting about it.
|
|
532
|
+
|
|
533
|
+
`sdk_path` is the caller's to normalise (`sdk_discovery._abs_posix`,
|
|
534
|
+
posix-normalised the same way [`with_entry`]'s own `sdk_root` argument
|
|
535
|
+
already is) -- this module has no opinion on path normalisation, the same
|
|
536
|
+
division every other function here draws.
|
|
537
|
+
|
|
538
|
+
`matches` is the comparison, INJECTED rather than the raw `==` this was
|
|
539
|
+
(tan-cli#1053 review: the fifth site of that defect -- an alias-spelled
|
|
540
|
+
entry was correctly refused and then NOT pruned). Injected rather than
|
|
541
|
+
imported for the reason `deepest_covering_entry` takes `covers`/
|
|
542
|
+
`has_loader_script` that way: the real comparison touches the filesystem
|
|
543
|
+
and lives in a module that already imports THIS one. Required, not
|
|
544
|
+
defaulted -- a default would hand a caller back the defect.
|
|
545
|
+
|
|
546
|
+
Compares only entries whose `sdkPath` is present and a string; anything
|
|
547
|
+
shaped too oddly for `parse_registry` to have kept either cannot name
|
|
548
|
+
`sdk_path` at all or is as findable by [`prune_dead_origins`]'s broader
|
|
549
|
+
sweep -- this narrows on purpose, to the one axis its caller knows changed.
|
|
550
|
+
"""
|
|
551
|
+
result: dict[str, Any] = {}
|
|
552
|
+
for origin, entry in registry.items():
|
|
553
|
+
path = entry.get("sdkPath") if isinstance(entry, dict) else None
|
|
554
|
+
if isinstance(path, str) and matches(normalized_sdk_path(path), sdk_path):
|
|
555
|
+
continue
|
|
556
|
+
result[origin] = entry
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def registry_text(registry: dict[str, Any]) -> str:
|
|
561
|
+
"""The committed-to-disk rendering: sorted keys (a deterministic diff
|
|
562
|
+
across runs, and a stable read order for a human `cat`ing the file), two-
|
|
563
|
+
space indent matching every other pointer file this repo writes, a
|
|
564
|
+
trailing newline."""
|
|
565
|
+
return json.dumps(registry, indent=2, sort_keys=True) + "\n"
|