fix(shim): handle Windows .exe case + seed shim-map at install#245
Merged
CalvinAllen merged 3 commits intomainfrom Apr 23, 2026
Merged
fix(shim): handle Windows .exe case + seed shim-map at install#245CalvinAllen merged 3 commits intomainfrom
CalvinAllen merged 3 commits intomainfrom
Conversation
On Windows, getShimName read os.Args[0] and stripped ".exe" via byte-exact strings.TrimSuffix. This broke whenever the invoker passed an uppercase extension — most notably Python's shutil.which, which returns paths like "mmdc.EXE" when the system PATHEXT contains ".EXE" (the common default on Windows). With the uppercase extension left attached, the shim name fell through the shim-map cache lookup (keyed by lowercase base names) AND the provider prefix-match (Shims() returns lowercase), and dtvem-shim surfaced a misleading "runtime provider not found: runtime provider 'mmdc.EXE' not found" error for a command that was otherwise correctly installed. Extract shimNameFromPath as a pure function and use filepath.Ext + strings.EqualFold to strip .exe / .EXE / .Exe regardless of case. Add a table-driven test that covers lowercase, uppercase, mixed-case, bare names, and non-.exe extensions (which must be preserved). Note: only the extension stripping is case-normalized — the base shim name itself is left as-is, consistent with the existing behavior of downstream lookups.
Runtime providers' createShims() only wrote shim files to disk — they did
not update the shim-map cache. That left an install-time gap where the
shim files for node/npm/npx (and python/pip, ruby/gem, etc.) existed, but
the cache entry mapping them back to their runtime did not.
In practice the gap was papered over by dtvem-shim's provider-prefix
fallback in mapShimToRuntime: calling "npm" still resolved to "node"
because the Node provider's Shims() list contains "npm". But the moment
something invoked a shim whose name wasn't on any provider's Shims() list
(e.g., a globally-installed npm package like mmdc), and the cache hadn't
yet been rebuilt by "dtvem reshim" or the post-install reshim prompt, the
shim errored with "runtime provider not found" — even though the shim file
had been placed correctly.
Fix by registering the core shims in the cache at install time:
- Add MergeShimMap(entries) to cache.go — merges new entries into the
on-disk map, creating it if absent, and resets the in-memory cache so
subsequent LoadShimMap reflects the merged state. Preferred over
Rehash() for install-time registration because it doesn't require
re-scanning every installed runtime's bin directory.
- Add Manager.CreateShimsForRuntime(runtimeName, shimNames) — creates
the shim files AND calls MergeShimMap so files and cache stay in sync.
- Update node, python, and ruby provider createShims() to use
CreateShimsForRuntime instead of bare CreateShims.
Bare CreateShims is kept for any caller that genuinely only wants the shim
files (for example, tests that don't care about the cache), but the
in-repo runtime providers all route through the new method.
Tests:
- 4 new MergeShimMap tests covering fresh cache creation, disjoint
merge, overwrite semantics, and in-memory cache invalidation.
- CreateShimsForRuntime is covered compositionally by existing shim
creation tests + the new MergeShimMap tests, matching the codebase's
convention of testing primitives rather than the manager layer
(which requires os.Executable() plumbing).
shimNameFromPath used filepath.Base, which only recognizes the host OS separator. On Linux, Windows-style test paths failed to split, leaving the directory attached to the name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related shim-resolution bugs surfaced by running Python-driven tools (
mmdcinvoked viasubprocessbyshutil.which-resolved paths) against a globallynpm install -g-ed mermaid-cli:Case-sensitive
.exestripping —getShimNameusedstrings.TrimSuffix(shimName, ".exe")onos.Args[0]. Windows command resolution viaPATHEXT(which defaults to uppercase.EXEon many systems) causes Python'sshutil.which— and any other tool that preserves the case returned byPATHEXT-based path lookup — to produce paths likemmdc.EXE. The uppercase extension survivedTrimSuffix, so the shim name passed downstream was"mmdc.EXE". That key was absent from the shim-map cache (lowercased at build time), the provider-registry prefix match (HasPrefix("mmdc.EXE", "node"/"npm"/"npx")all false), and the provider lookup (no registeredmmdc.EXEprovider) — producing the misleadingruntime provider not found: runtime provider 'mmdc.EXE' not founderror for a command that was otherwise correctly installed and shimmed.Install-time shim-map gap — runtime providers'
createShims()wrote the shim binaries for the runtime's core commands (node/npm/npx,python/pip/...,ruby/gem/...) but did not updatecache/shim-map.json.SaveShimMapwas only ever called from the fullRehashpath. In practice this was papered over bymapShimToRuntime's provider-fallback —npmresolved viaNode.Shims()membership even when the cache didn't know it — but the fallback only matches core shim names and breaks down the moment a globally installed npm package (e.g.,mmdc) gets invoked before any subsequent reshim has run.Changes
Fix 1 —
fix(shim): strip .exe suffix case-insensitively when resolving shim name(cdb883c)getShimName→shimNameFromPath(pure function, testable).filepath.Ext+strings.EqualFold(ext, ".exe")so.exe/.EXE/.Exeall strip..exeextensions (.bat,.cmd) are preserved unchanged.src/cmd/shim/main_test.gowith 7-case table-driven test (unix bare, lowercase/uppercase/mixed-case.exe, forward-slash paths, bare names, non-.exepreservation).Fix 2 —
fix(shim): seed shim-map cache during runtime install(7e2ec38)shim.MergeShimMap(entries ShimMap)— merges entries into the on-disk map, creating it when absent; resets the in-memory cache so subsequentLoadShimMapreads the merged state. Preferred overRehash()at install time because it doesn't re-scan every runtime's bin directory.Manager.CreateShimsForRuntime(runtimeName, shimNames)— creates shim files and registers them in the cache in one call, keeping the two representations in sync by construction.node,python, andrubyprovidercreateShims()all switched from bareCreateShimstoCreateShimsForRuntime.CreateShimsretained for tests and any future caller that only needs file creation.TestMergeShimMap_*tests covering fresh cache creation, disjoint merge, overwrite semantics, and in-memory cache invalidation.Why two fixes, not one
Each is necessary but not sufficient. Fix 1 alone still leaves users broken on a cold install (cache is empty until the first successful reshim, and Python-invoked shims can't rely on the provider-prefix fallback when the caller is a non-core binary like
mmdc). Fix 2 alone still leaves Python-driven invocations broken even after the cache is correctly populated, because case-sensitive extension stripping yields a cache-miss key. Both bugs have to be fixed to get Python-driven Windows tooling working reliably against dtvem shims.Test plan
go test ./src/internal/shim/... ./src/cmd/shim/...— all tests pass (11 new cases: 7TestShimNameFromPath+ 4TestMergeShimMap_*)./rnr check(format, lint, full test suite) — exit 0dtvem install node <ver>, inspect~/.dtvem/cache/shim-map.json— core node/npm/npx entries should appear immediately (previously would only appear after first reshim)npm install -g @mermaid-js/mermaid-cli && dtvem reshim && python -c "import subprocess; subprocess.run(['mmdc', '--version'])"— should succeed (previously failed withruntime provider 'mmdc.EXE' not foundbecause Python'sshutil.whichreturns uppercase.EXE)Scope notes
filepath.Exton unix-style shim paths returns"",EqualFold("", ".exe")isfalse, strip is a no-op.CreateShimssignature unchanged;CreateShimsForRuntimeis additive.MMDC), that's a separate consideration and likely belongs inmapShimToRuntimerather thangetShimName.