Skip to content

Commit 7dde060

Browse files
authored
refactor(shim): implement lite ShimProvider interface (#75)
1 parent 3f11600 commit 7dde060

5 files changed

Lines changed: 121 additions & 85 deletions

File tree

CONTRIBUTING.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,28 +205,46 @@ For detailed guidelines and examples, see [Commit Convention Guide](docs/COMMIT_
205205
To add support for a new runtime (e.g., Ruby):
206206

207207
1. Create `src/runtimes/ruby/provider.go`
208-
2. Implement the `runtime.Provider` interface:
208+
2. Implement the `runtime.Provider` interface (which embeds `runtime.ShimProvider`):
209209
```go
210210
type Provider struct {}
211211

212+
// ShimProvider methods (used by the shim binary)
212213
func (p *Provider) Name() string { return "ruby" }
213214
func (p *Provider) DisplayName() string { return "Ruby" }
214-
// ... implement all other methods
215+
func (p *Provider) Shims() []string { return []string{"ruby", "gem", "bundle"} }
216+
func (p *Provider) ExecutablePath(version string) (string, error) { ... }
217+
func (p *Provider) IsInstalled(version string) (bool, error) { ... }
218+
func (p *Provider) ShouldReshimAfter(shimName string, args []string) bool { ... }
219+
220+
// Provider-only methods (used by CLI, can include net/http)
221+
func (p *Provider) Install(version string) error { ... }
222+
func (p *Provider) ListAvailable() ([]runtime.AvailableVersion, error) { ... }
223+
// ... implement remaining methods
215224
```
216225
3. Register the provider in the `init()` function:
217226
```go
218227
func init() {
219228
runtime.Register(NewProvider())
220229
}
221230
```
222-
4. Import the provider in `src/main.go`:
231+
4. Import the provider in **both** files:
232+
- `src/main.go` (for the CLI)
233+
- `src/cmd/shim/main.go` (for the shim)
223234
```go
224235
_ "github.com/dtvem/dtvem/src/runtimes/ruby"
225236
```
226-
5. Implement the `Shims()` method to define which executables this runtime provides
227-
6. Implement the `ShouldReshimAfter()` method for automatic reshim detection
228-
7. Add tests using the provider contract harness
229-
8. Update README.md to reflect the new runtime support
237+
5. Add tests using the provider contract harness
238+
6. Update README.md to reflect the new runtime support
239+
240+
### ShimProvider vs Provider
241+
242+
The `Provider` interface embeds `ShimProvider`. This separation exists for performance:
243+
244+
- **ShimProvider**: Minimal interface used by the shim binary. Methods here should NOT import `net/http` or other heavy dependencies.
245+
- **Provider**: Full interface for CLI operations. Can use any dependencies.
246+
247+
Go's linker eliminates unused code, so keeping `ShimProvider` methods dependency-free results in a smaller, faster shim binary (~4.5MB vs ~10MB).
230248

231249
## Testing Guidelines
232250

README.md

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -208,25 +208,23 @@ Reads .dtvem/runtimes.json → {"python": "3.11.0"}
208208
Executes: ~/.dtvem/versions/python/3.11.0/bin/python --version
209209
```
210210

211-
### Auto-Install Missing Versions
211+
### Missing Version Handling
212212

213-
When you run a command that requires a version that isn't installed yet, dtvem will offer to install it automatically:
213+
When you run a command that requires a version that isn't installed yet, dtvem will tell you how to install it:
214214

215215
```bash
216216
$ python --version
217-
⚠ Python 3.11.0 is not installed
218-
→ Install it now? [Y/n]: y
219-
→ Installing Python 3.11.0...
220-
✓ Successfully installed Python 3.11.0
221-
Python 3.11.0
217+
✗ Python 3.11.0 is configured but not installed
218+
→ To install, run: dtvem install python 3.11.0
222219
```
223220

224-
This works seamlessly with bulk install - just clone a repo with a `.dtvem/runtimes.json` file and start using it. The first time you run a command, dtvem will offer to install the required version.
225-
226-
**Environment Variable Control:**
227-
- `DTVEM_AUTO_INSTALL=false` - Disable auto-install in CI/automation
228-
- `DTVEM_AUTO_INSTALL=true` - Auto-install without prompting
229-
- Default - Interactive prompt
221+
**Bulk Install from Config:**
222+
Clone a repo with a `.dtvem/runtimes.json` file and run:
223+
```bash
224+
dtvem install # Prompts for confirmation
225+
dtvem install --yes # Skip confirmation
226+
```
227+
This installs all configured versions at once.
230228

231229
### System PATH Fallback
232230

@@ -369,10 +367,6 @@ System-wide default versions (JSON format):
369367
### Environment Variables
370368

371369
- `DTVEM_ROOT`: Override default dtvem directory (default: `~/.dtvem`)
372-
- `DTVEM_AUTO_INSTALL`: Control auto-install behavior when a configured version is missing
373-
- `false` - Disable auto-install prompts (useful for CI/automation)
374-
- `true` - Auto-install without prompting
375-
- Not set - Interactive prompt (default)
376370

377371
## Supported Runtimes
378372

@@ -462,31 +456,49 @@ Remove this installation? [y/N]: y
462456

463457
## Plugin System
464458

465-
dtvem uses an embedded plugin system with a registry pattern. Each runtime provider owns its shim definitions (executables it provides), making the architecture fully decentralized:
459+
dtvem uses an embedded plugin system with a registry pattern. Each runtime provider owns its shim definitions (executables it provides), making the architecture fully decentralized.
460+
461+
### Provider Interfaces
462+
463+
Providers implement two interfaces - a lightweight `ShimProvider` for the shim binary, and the full `Provider` for the CLI:
466464

467465
```go
468-
// Each runtime implements the Provider interface (19 methods)
469-
type Provider interface {
466+
// ShimProvider - minimal interface used by the shim binary
467+
// Excludes heavy dependencies like net/http to keep shim fast and small
468+
type ShimProvider interface {
470469
Name() string // e.g., "python"
471470
DisplayName() string // e.g., "Python"
472471
Shims() []string // e.g., ["python", "python3", "pip", "pip3"]
472+
ExecutablePath(version string) (string, error)
473+
IsInstalled(version string) (bool, error)
473474
ShouldReshimAfter(shimName, args) bool // e.g., detect "npm install -g"
475+
}
476+
477+
// Provider - full interface for CLI operations (embeds ShimProvider)
478+
type Provider interface {
479+
ShimProvider
474480
Install(version string) error
481+
ListAvailable() ([]AvailableVersion, error) // requires net/http
475482
DetectInstalled() ([]DetectedVersion, error)
476-
// ... 13 more methods
483+
// ... more methods
477484
}
485+
```
486+
487+
**Why two interfaces?** The shim binary runs on every `node`, `python`, `npm` command. By using `ShimProvider`, Go's linker eliminates unused code (like `net/http`), reducing shim size from ~10MB to ~4.5MB and improving startup time.
478488

489+
### Adding a New Runtime
490+
491+
1. Create `src/runtimes/<name>/provider.go`
492+
2. Implement the `Provider` interface (which includes all `ShimProvider` methods)
493+
3. Import in `src/main.go` and `src/cmd/shim/main.go`
494+
495+
```go
479496
// Runtimes auto-register on startup
480497
func init() {
481498
runtime.Register(NewProvider())
482499
}
483500
```
484501

485-
**Adding a new runtime is as simple as:**
486-
1. Create `src/runtimes/<name>/provider.go`
487-
2. Implement the `Provider` interface (including `Shims()` and `ShouldReshimAfter()`)
488-
3. Import in `src/main.go`
489-
490502
**That's it!** The shim mappings are automatically registered via the `Shims()` method, and automatic reshim detection works via `ShouldReshimAfter()`. No need to modify central mapping files.
491503

492504
## Development

src/cmd/shim/main.go

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ func runShim() error {
3434
// Determine which runtime this shim belongs to
3535
runtimeName := mapShimToRuntime(shimName)
3636

37-
// Get the runtime provider
38-
provider, err := runtime.Get(runtimeName)
37+
// Get the runtime provider (using ShimProvider interface for minimal dependencies)
38+
provider, err := runtime.GetShimProvider(runtimeName)
3939
if err != nil {
4040
return fmt.Errorf("runtime provider not found: %w", err)
4141
}
@@ -47,35 +47,22 @@ func runShim() error {
4747
return handleNoConfiguredVersion(shimName, runtimeName, provider)
4848
}
4949

50-
// Get the path to the actual executable
51-
execPath, err := provider.ExecutablePath(version)
50+
// Check if the version is installed
51+
installed, err := provider.IsInstalled(version)
5252
if err != nil {
53-
// Check if the version is not installed
54-
if strings.Contains(err.Error(), "not found") {
55-
// Offer to install it
56-
if shouldAutoInstall(provider.DisplayName(), version) {
57-
ui.Info("Installing %s %s...", provider.DisplayName(), version)
58-
59-
if installErr := provider.Install(version); installErr != nil {
60-
ui.Error("Failed to install %s %s: %v", provider.DisplayName(), version, installErr)
61-
ui.Info("Please install manually with: dtvem install %s %s", runtimeName, version)
62-
return fmt.Errorf("installation failed")
63-
}
53+
return fmt.Errorf("could not check if %s %s is installed: %w", runtimeName, version, err)
54+
}
6455

65-
ui.Success("Successfully installed %s %s", provider.DisplayName(), version)
56+
if !installed {
57+
ui.Error("%s %s is configured but not installed", provider.DisplayName(), version)
58+
ui.Info("To install, run: dtvem install %s %s", runtimeName, version)
59+
return fmt.Errorf("version not installed")
60+
}
6661

67-
// Retry getting the executable path
68-
execPath, err = provider.ExecutablePath(version)
69-
if err != nil {
70-
return fmt.Errorf("could not find %s %s after installation: %w", runtimeName, version, err)
71-
}
72-
} else {
73-
ui.Info("To install manually, run: dtvem install %s %s", runtimeName, version)
74-
return fmt.Errorf("installation declined")
75-
}
76-
} else {
77-
return fmt.Errorf("could not find %s %s: %w", runtimeName, version, err)
78-
}
62+
// Get the path to the actual executable
63+
execPath, err := provider.ExecutablePath(version)
64+
if err != nil {
65+
return fmt.Errorf("could not find %s %s executable: %w", runtimeName, version, err)
7966
}
8067

8168
// If the shim name differs from the base runtime name,
@@ -109,7 +96,7 @@ func runShim() error {
10996

11097
// handleNoConfiguredVersion handles the case when no dtvem version is configured
11198
// It attempts to fallback to system PATH or prompts for installation
112-
func handleNoConfiguredVersion(shimName, runtimeName string, provider runtime.Provider) error {
99+
func handleNoConfiguredVersion(shimName, runtimeName string, provider runtime.ShimProvider) error {
113100
// Try to find the executable deeper in PATH (system installation)
114101
systemPath := findInSystemPath(shimName)
115102

@@ -194,12 +181,6 @@ func findInSystemPath(execName string) string {
194181
return ""
195182
}
196183

197-
// shouldAutoInstall prompts the user to install a missing version.
198-
// Delegates to ui.PromptInstall for consistent behavior across CLI and shim.
199-
func shouldAutoInstall(displayName, version string) bool {
200-
return ui.PromptInstall(displayName, version)
201-
}
202-
203184
// getShimName returns the name of this shim binary
204185
func getShimName() string {
205186
shimPath := os.Args[0]
@@ -216,8 +197,8 @@ func getShimName() string {
216197
// This queries all registered providers for their shims, eliminating the need
217198
// for a central hardcoded mapping.
218199
func mapShimToRuntime(shimName string) string {
219-
// Get all registered providers
220-
providers := runtime.GetAll()
200+
// Get all registered providers (using ShimProvider interface)
201+
providers := runtime.GetAllShimProviders()
221202

222203
// Check each provider's shims for an exact match first
223204
for _, provider := range providers {

src/internal/runtime/provider.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Package runtime defines the provider interface and registry for runtime managers
22
package runtime
33

4-
// Provider defines the interface that all runtime providers must implement
5-
type Provider interface {
4+
// ShimProvider defines the minimal interface needed by the shim executable.
5+
// This interface excludes heavy operations like Install() and ListAvailable()
6+
// that require net/http and other dependencies not needed for shim execution.
7+
type ShimProvider interface {
68
// Name returns the name of the runtime (e.g., "python", "node", "ruby")
79
Name() string
810

@@ -14,6 +16,25 @@ type Provider interface {
1416
// Node.js returns ["node", "npm", "npx"]
1517
Shims() []string
1618

19+
// ExecutablePath returns the path to the main executable for a given version
20+
// For example, for Python 3.11.0, this might return "/path/to/python3.11"
21+
ExecutablePath(version string) (string, error)
22+
23+
// IsInstalled checks if a specific version is installed
24+
IsInstalled(version string) (bool, error)
25+
26+
// ShouldReshimAfter checks if the given command should trigger a reshim.
27+
// Returns true if the command installs or uninstalls global packages that add/remove executables.
28+
// The shimName parameter indicates which shim was invoked (e.g., "npm", "pip")
29+
// The args parameter contains the command arguments (e.g., ["install", "-g", "typescript"])
30+
ShouldReshimAfter(shimName string, args []string) bool
31+
}
32+
33+
// Provider defines the full interface that all runtime providers must implement.
34+
// It embeds ShimProvider and adds operations that require heavier dependencies.
35+
type Provider interface {
36+
ShimProvider
37+
1738
// Install downloads and installs a specific version of the runtime
1839
Install(version string) error
1940

@@ -27,13 +48,6 @@ type Provider interface {
2748
// This might query online sources or use cached data
2849
ListAvailable() ([]AvailableVersion, error)
2950

30-
// ExecutablePath returns the path to the main executable for a given version
31-
// For example, for Python 3.11.0, this might return "/path/to/python3.11"
32-
ExecutablePath(version string) (string, error)
33-
34-
// IsInstalled checks if a specific version is installed
35-
IsInstalled(version string) (bool, error)
36-
3751
// InstallPath returns the installation directory for a given version
3852
InstallPath(version string) (string, error)
3953

@@ -72,10 +86,4 @@ type Provider interface {
7286
// Used to provide help text to users if automatic package installation fails
7387
// Returns empty string if the runtime doesn't support global packages
7488
ManualPackageInstallCommand(packages []string) string
75-
76-
// ShouldReshimAfter checks if the given command should trigger a reshim.
77-
// Returns true if the command installs or uninstalls global packages that add/remove executables.
78-
// The shimName parameter indicates which shim was invoked (e.g., "npm", "pip")
79-
// The args parameter contains the command arguments (e.g., ["install", "-g", "typescript"])
80-
ShouldReshimAfter(shimName string, args []string) bool
8189
}

src/internal/runtime/registry.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,20 @@ func Unregister(name string) error {
132132
func GetRegistry() *Registry {
133133
return globalRegistry
134134
}
135+
136+
// GetShimProvider retrieves a provider as ShimProvider from the global registry.
137+
// This returns only the minimal interface needed by the shim.
138+
func GetShimProvider(name string) (ShimProvider, error) {
139+
return globalRegistry.Get(name)
140+
}
141+
142+
// GetAllShimProviders returns all providers as ShimProviders from the global registry.
143+
// This returns only the minimal interface needed by the shim.
144+
func GetAllShimProviders() []ShimProvider {
145+
providers := globalRegistry.GetAll()
146+
shimProviders := make([]ShimProvider, len(providers))
147+
for i, p := range providers {
148+
shimProviders[i] = p
149+
}
150+
return shimProviders
151+
}

0 commit comments

Comments
 (0)