Skip to content

fix(shim): prune orphan shims on reshim - #274

Merged
CalvinAllen merged 1 commit into
mainfrom
fix/shim/prune-orphan-shims
Sep 1, 2026
Merged

CalvinAllen merged 1 commit into
mainfrom
fix/shim/prune-orphan-shims

Conversation

@CalvinAllen

Copy link
Copy Markdown
Contributor

Resolves #273

🐛 The bug

dtvem doctor reported shim-map cache drift and told the user to run dtvem reshim. Running it changed nothing, so doctor reported the same drift forever.

⚠ Shim-map cache is out of sync with disk  [fixable]
  On disk, missing from cache:  pytest, python3, ruff, virtualenv
  Run `dtvem reshim` to rebuild the cache from the installed runtimes.

🔍 Root cause

Rehash rebuilt the shim-map cache from the executables found in the installed runtime versions, but only ever added shim files — it never enumerated shims/, and RemoveShim had zero non-test callers. The cache is rebuilt-from-scan (can shrink); the shims directory was append-only (can only grow). The two sets could only diverge, and doctor diffs exactly those two sets.

Orphans came from two sources:

  • pytest, ruff, virtualenv — pip console scripts shimmed by a post-pip install reshim, whose backing Scripts/*.exe later went away.
  • python3 — created unconditionally at install time from Python's static Shims() list, but Windows python-build-standalone ships no python3.exe, so the disk scan never re-recorded it. (Install no longer does this as of the DiscoverShimsForVersion change already on main; this cleans up the shims that change left behind.)

Invoking an orphan shim leaked an internal error, too — mapShimToRuntime fell back to treating the shim name as a runtime name:

❯ ruff --help
dtvem shim error: runtime provider not found: runtime provider 'ruff' not found

Worse, that dead shim sits early on PATH and shadows a real ruff installed elsewhere.

✅ The fix

Rehash now prunes. After rebuilding the map it deletes shim files absent from it, and reports what it removed. Guards on the destructive path:

  • 🛡️ dtvem and dtvem-shim are never pruned — installs before 0.4 kept dtvem-shim in shims/ rather than bin/, and deleting it would break every other shim
  • 🛡️ Only files that are really shim binaries are removed (.exe at the shim path on Windows; exec bit on Unix), so a stray file dropped in shims/ survives
  • 🛡️ An empty scan skips pruning entirely — that signals an unreadable versions tree, not "everything was uninstalled"
  • 🛡️ Removal failures are collected and reported per-shim, not fatal; a shim executing right now cannot be deleted on Windows

Unclaimed shims fall through to PATH. mapShimToRuntime now reports whether any runtime actually claims the name. Unclaimed shims hand off to the real command deeper in PATH with a warning naming the fix, instead of fabricating a provider lookup.

uninstall already calls Rehash, so removing a runtime version now cleans up its shims as a side effect.

🧪 Verification

End-to-end against a synthetic DTVEM_ROOT reproducing the reported state (Python 3.13.11 providing only python + pip; pytest, python3, ruff, virtualenv shims on disk; cache knowing only python):

Before — reproduces the report exactly:

⚠ Shim-map cache is out of sync with disk  [fixable]
  On disk, missing from cache:  pip, pytest, python3, ruff, virtualenv

After dtvem reshim:

✓ Created 2 shims for 1 runtime(s)
→ Removed 4 orphan shim(s): pytest, python3, ruff, virtualenv
✓ Shim-map cache matches disk

Orphan shim invocation, both branches:

❯ shims/git.exe --version
⚠ 'git' is a leftover dtvem shim; running C:\Program Files\Git\mingw64\bin\git.exe instead
→ Run 'dtvem reshim' to remove it.

git version 2.51.2.windows.1
❯ shims/ruff.exe --help
✗ 'ruff' is not managed by dtvem and was not found in PATH
→ This is a leftover dtvem shim. Run 'dtvem reshim' to remove it.

Confirmed a known runtime with no version configured still takes the existing system-PATH fallback rather than the new unknown-shim path.

11 new tests (prune keeps backed shims / removes unbacked / takes the Windows .cmd companion / protects dtvem binaries / leaves non-shim files alone / empty scan keeps shims, a Rehash regression test asserting cache and disk agree afterward, and mapShimToRuntime coverage for claimed and unclaimed names).

./rnr check passes — 26 packages, 0 lint issues.

`dtvem doctor` reported shim-map cache drift that `dtvem reshim` could
never clear, so doctor kept prescribing a command that did nothing.

Rehash rebuilt the cache from the executables found in the installed
runtime versions but only ever *added* shim files, so the two sets could
only diverge. A shim whose backing executable was gone -- a pip or npm
package that was later uninstalled, a removed runtime version, or a
phantom shim from an older dtvem that trusted the provider's static
Shims() list -- stayed in shims/ forever. RemoveShim had no non-test
callers at all.

Rehash now deletes shim files absent from the freshly-scanned map, and
reports what it removed. Guards on the destructive path:

  - dtvem and dtvem-shim are never pruned; installs before 0.4 kept
    dtvem-shim in shims/ rather than bin/
  - only files that are really shim binaries are removed, so a stray
    file dropped in shims/ survives
  - an empty scan skips pruning entirely -- that signals an unreadable
    versions tree, not "everything was uninstalled"
  - removal failures are collected and reported, not fatal; a shim
    executing right now cannot be deleted on Windows

Invoking an orphan shim also leaked an internal error: mapShimToRuntime
fell back to treating the shim name as a runtime name, so `ruff --help`
died with "runtime provider not found: runtime provider 'ruff' not
found" while shadowing a real ruff deeper in PATH. It now reports
whether any runtime claims the name, and unclaimed shims hand off to the
real command in PATH with a warning naming the fix.
@CalvinAllen
CalvinAllen merged commit 1c011c0 into main Sep 1, 2026
11 checks passed
@CalvinAllen
CalvinAllen deleted the fix/shim/prune-orphan-shims branch September 1, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Shim-map cache is out of sync with disk

1 participant