Skip to content

Commit 3a8416e

Browse files
committed
fix(ruby): set LD_LIBRARY_PATH for Unix Ruby binaries
The Ruby binaries from ruby-builder on Unix require libruby.so which is located in the installation's lib directory. Added GetEnvironment method to Provider interface so providers can specify environment variables needed when executing their binaries. Ruby provider now sets: - LD_LIBRARY_PATH on Linux - DYLD_LIBRARY_PATH on macOS This fixes the "error while loading shared libraries: libruby.so.3.3: cannot open shared object file" error on Unix systems.
1 parent 01a163a commit 3a8416e

8 files changed

Lines changed: 118 additions & 11 deletions

File tree

src/cmd/install_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ func (m *mockProvider) ManualPackageInstallCommand(packages []string) string {
4646
return ""
4747
}
4848

49+
func (m *mockProvider) GetEnvironment(_ string) (map[string]string, error) {
50+
return map[string]string{}, nil
51+
}
52+
4953
func (m *mockProvider) GlobalVersion() (string, error) {
5054
return m.globalVersion, nil
5155
}

src/cmd/shim/main.go

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,23 @@ func runShim() error {
8585
execPath = adjustExecutablePath(execPath, shimName, runtimeName)
8686
ui.Debug("Final executable path: %s", execPath)
8787

88+
// Get provider-specific environment variables (e.g., LD_LIBRARY_PATH for Ruby)
89+
providerEnv, err := provider.GetEnvironment(version)
90+
if err != nil {
91+
ui.Debug("Failed to get provider environment: %v", err)
92+
providerEnv = map[string]string{}
93+
}
94+
for k, v := range providerEnv {
95+
ui.Debug("Provider env: %s=%s", k, v)
96+
}
97+
8898
// Check if this command should trigger a reshim after execution
8999
needsReshim := provider.ShouldReshimAfter(shimName, os.Args[1:])
90100

91101
// Execute the actual binary
92102
if needsReshim {
93103
// Need to run code after execution, so use exec.Command
94-
exitCode := executeCommandWithWait(execPath, os.Args[1:])
104+
exitCode := executeCommandWithWait(execPath, os.Args[1:], providerEnv)
95105

96106
// If command succeeded, prompt for reshim
97107
if exitCode == 0 {
@@ -101,7 +111,7 @@ func runShim() error {
101111
os.Exit(exitCode)
102112
} else {
103113
// Normal execution - use syscall.Exec on Unix for efficiency
104-
if err := executeCommand(execPath, os.Args[1:]); err != nil {
114+
if err := executeCommand(execPath, os.Args[1:], providerEnv); err != nil {
105115
return fmt.Errorf("failed to execute %s: %w", execPath, err)
106116
}
107117
}
@@ -123,8 +133,8 @@ func handleNoConfiguredVersion(shimName, runtimeName string, provider runtime.Sh
123133
ui.Info("Or see available versions: dtvem list-all %s", runtimeName)
124134
fmt.Fprintln(os.Stderr) // Empty line for spacing
125135

126-
// Execute the system version
127-
if err := executeCommand(systemPath, os.Args[1:]); err != nil {
136+
// Execute the system version (no provider env needed for system installations)
137+
if err := executeCommand(systemPath, os.Args[1:], nil); err != nil {
128138
return fmt.Errorf("failed to execute system %s: %w", shimName, err)
129139
}
130140
return nil
@@ -243,13 +253,13 @@ func adjustExecutablePath(execPath, shimName, runtimeName string) string {
243253
return execPath
244254
}
245255

246-
// executeCommand executes a command with the given arguments
247-
func executeCommand(execPath string, args []string) error {
256+
// executeCommand executes a command with the given arguments and provider environment
257+
func executeCommand(execPath string, args []string, providerEnv map[string]string) error {
248258
// Build full args (executable name + arguments)
249259
fullArgs := append([]string{execPath}, args...)
250260

251-
// Get current environment
252-
env := os.Environ()
261+
// Get current environment and apply provider overrides
262+
env := mergeEnvironment(os.Environ(), providerEnv)
253263

254264
// On Unix systems, use Exec to replace the current process
255265
// On Windows, Exec is not available, so we use StartProcess
@@ -280,12 +290,12 @@ func executeCommand(execPath string, args []string) error {
280290
}
281291

282292
// executeCommandWithWait executes a command and waits for it to complete, returning the exit code
283-
func executeCommandWithWait(execPath string, args []string) int {
293+
func executeCommandWithWait(execPath string, args []string, providerEnv map[string]string) int {
284294
// Build full args (executable name + arguments)
285295
fullArgs := append([]string{execPath}, args...)
286296

287-
// Get current environment
288-
env := os.Environ()
297+
// Get current environment and apply provider overrides
298+
env := mergeEnvironment(os.Environ(), providerEnv)
289299

290300
// Use exec.Command to run the command and wait for completion
291301
cmd := &exec.Cmd{
@@ -310,6 +320,43 @@ func executeCommandWithWait(execPath string, args []string) int {
310320
return 0
311321
}
312322

323+
// mergeEnvironment merges provider environment variables into the base environment.
324+
// Provider variables are prepended to existing values (for PATH-like variables) or set directly.
325+
func mergeEnvironment(baseEnv []string, providerEnv map[string]string) []string {
326+
if len(providerEnv) == 0 {
327+
return baseEnv
328+
}
329+
330+
// Build a map of existing environment variables for easy lookup
331+
envMap := make(map[string]string)
332+
for _, e := range baseEnv {
333+
if idx := strings.Index(e, "="); idx != -1 {
334+
key := e[:idx]
335+
value := e[idx+1:]
336+
envMap[key] = value
337+
}
338+
}
339+
340+
// Apply provider environment variables
341+
// For PATH-like variables (LD_LIBRARY_PATH, DYLD_LIBRARY_PATH), prepend the new value
342+
for key, value := range providerEnv {
343+
if existing, ok := envMap[key]; ok && existing != "" {
344+
// Prepend new value to existing (for PATH-like variables)
345+
envMap[key] = value + string(filepath.ListSeparator) + existing
346+
} else {
347+
envMap[key] = value
348+
}
349+
}
350+
351+
// Convert back to slice format
352+
result := make([]string, 0, len(envMap))
353+
for key, value := range envMap {
354+
result = append(result, key+"="+value)
355+
}
356+
357+
return result
358+
}
359+
313360
// promptReshim prompts the user to run reshim after installing global packages
314361
func promptReshim() {
315362
fmt.Fprintln(os.Stderr) // Empty line for spacing

src/internal/runtime/provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type ShimProvider interface {
2828
// The shimName parameter indicates which shim was invoked (e.g., "npm", "pip")
2929
// The args parameter contains the command arguments (e.g., ["install", "-g", "typescript"])
3030
ShouldReshimAfter(shimName string, args []string) bool
31+
32+
// GetEnvironment returns environment variables that should be set when executing
33+
// this runtime's binaries. For example, Ruby needs LD_LIBRARY_PATH set to find libruby.so.
34+
// Returns an empty map if no special environment is needed.
35+
GetEnvironment(version string) (map[string]string, error)
3136
}
3237

3338
// Provider defines the full interface that all runtime providers must implement.

src/internal/runtime/registry_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func (m *mockProvider) GlobalPackages(installPath string) ([]string, error)
3030
func (m *mockProvider) InstallGlobalPackages(version string, packages []string) error { return nil }
3131
func (m *mockProvider) ManualPackageInstallCommand(packages []string) string { return "" }
3232
func (m *mockProvider) ShouldReshimAfter(shimName string, args []string) bool { return false }
33+
func (m *mockProvider) GetEnvironment(_ string) (map[string]string, error) {
34+
return map[string]string{}, nil
35+
}
3336

3437
func TestNewRegistry(t *testing.T) {
3538
r := NewRegistry()

src/internal/shim/manager_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ func (m *mockProvider) GlobalPackages(installPath string) ([]string, error)
3737
func (m *mockProvider) InstallGlobalPackages(version string, packages []string) error { return nil }
3838
func (m *mockProvider) ManualPackageInstallCommand(packages []string) string { return "" }
3939
func (m *mockProvider) ShouldReshimAfter(shimName string, args []string) bool { return false }
40+
func (m *mockProvider) GetEnvironment(_ string) (map[string]string, error) {
41+
return map[string]string{}, nil
42+
}
4043

4144
func TestRuntimeShims(t *testing.T) {
4245
// Register test providers

src/runtimes/node/provider.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,12 @@ func (p *Provider) ShouldReshimAfter(shimName string, args []string) bool {
490490
return false
491491
}
492492

493+
// GetEnvironment returns environment variables needed to run Node.js binaries.
494+
// Node.js binaries are self-contained and don't require special environment setup.
495+
func (p *Provider) GetEnvironment(_ string) (map[string]string, error) {
496+
return map[string]string{}, nil
497+
}
498+
493499
// init registers the Node.js provider on package load
494500
func init() {
495501
if err := runtime.Register(NewProvider()); err != nil {

src/runtimes/python/provider.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,13 @@ func (p *Provider) ShouldReshimAfter(shimName string, args []string) bool {
687687
return cmd == "install" || cmd == "uninstall"
688688
}
689689

690+
// GetEnvironment returns environment variables needed to run Python binaries.
691+
// Python binaries from python-build-standalone are relocatable and don't require
692+
// special environment setup.
693+
func (p *Provider) GetEnvironment(_ string) (map[string]string, error) {
694+
return map[string]string{}, nil
695+
}
696+
690697
// init registers the Python provider on package load
691698
func init() {
692699
if err := runtime.Register(NewProvider()); err != nil {

src/runtimes/ruby/provider.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,38 @@ func (p *Provider) ShouldReshimAfter(shimName string, args []string) bool {
792792
return false
793793
}
794794

795+
// GetEnvironment returns environment variables needed to run Ruby binaries.
796+
// On Unix systems, Ruby from ruby-builder needs LD_LIBRARY_PATH (Linux) or
797+
// DYLD_LIBRARY_PATH (macOS) set to find libruby.so.
798+
func (p *Provider) GetEnvironment(version string) (map[string]string, error) {
799+
// Windows RubyInstaller binaries are self-contained, no special environment needed
800+
if goruntime.GOOS == constants.OSWindows {
801+
return map[string]string{}, nil
802+
}
803+
804+
// Get the install path for this version
805+
installPath, err := p.InstallPath(version)
806+
if err != nil {
807+
return nil, err
808+
}
809+
810+
// The lib directory contains libruby.so
811+
libPath := filepath.Join(installPath, "lib")
812+
813+
env := make(map[string]string)
814+
815+
// Set the appropriate library path based on platform
816+
if goruntime.GOOS == constants.OSDarwin {
817+
// macOS uses DYLD_LIBRARY_PATH
818+
env["DYLD_LIBRARY_PATH"] = libPath
819+
} else {
820+
// Linux uses LD_LIBRARY_PATH
821+
env["LD_LIBRARY_PATH"] = libPath
822+
}
823+
824+
return env, nil
825+
}
826+
795827
// init registers the Ruby provider on package load
796828
func init() {
797829
if err := runtime.Register(NewProvider()); err != nil {

0 commit comments

Comments
 (0)