Skip to content

Commit 5720dbc

Browse files
authored
feat(ruby): add uru version manager detection (#130)
1 parent 2033a80 commit 5720dbc

6 files changed

Lines changed: 350 additions & 1 deletion

File tree

.github/workflows/integration-test-migrate-ruby-windows-system.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
echo "**Source:** Chocolatey" >> $GITHUB_STEP_SUMMARY
8080
echo "**Version:** 3.2.x" >> $GITHUB_STEP_SUMMARY
8181
echo "" >> $GITHUB_STEP_SUMMARY
82-
echo "*Note: No rbenv equivalent on Windows. See issue #122 for RubyInstaller/uru support.*" >> $GITHUB_STEP_SUMMARY
82+
echo "*Note: For version manager migration on Windows, see the uru integration test.*" >> $GITHUB_STEP_SUMMARY
8383
echo "" >> $GITHUB_STEP_SUMMARY
8484
echo "### Installed Versions" >> $GITHUB_STEP_SUMMARY
8585
echo '```' >> $GITHUB_STEP_SUMMARY
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
name: Integration Tests - Migrate Ruby from uru (Windows)
2+
3+
on:
4+
workflow_call:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
migrate:
12+
name: Ruby from uru (Windows)
13+
runs-on: windows-latest
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: '1.23'
22+
cache: true
23+
24+
- name: Build dtvem
25+
shell: bash
26+
run: |
27+
go build -v -ldflags="-s -w" -o dist/dtvem.exe ./src
28+
go build -v -ldflags="-s -w" -o dist/dtvem-shim.exe ./src/cmd/shim
29+
30+
- name: Initialize dtvem
31+
shell: bash
32+
run: ./dist/dtvem.exe init --yes
33+
34+
- name: Add dtvem to PATH
35+
shell: pwsh
36+
run: |
37+
"$env:USERPROFILE\.dtvem\shims" | Out-File -FilePath $env:GITHUB_PATH -Append
38+
"$env:USERPROFILE\.dtvem\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
39+
40+
- name: "Install Ruby 3.2 via Chocolatey"
41+
shell: pwsh
42+
run: |
43+
# Install Ruby via Chocolatey (will be registered with uru)
44+
choco install ruby --version=3.2.6.1 -y
45+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
46+
Write-Host "Ruby installed at C:\tools\ruby32"
47+
C:\tools\ruby32\bin\ruby.exe --version
48+
49+
- name: "Install uru"
50+
shell: pwsh
51+
run: |
52+
# Download uru from Bitbucket releases
53+
$uruVersion = "0.8.5"
54+
$uruUrl = "https://bitbucket.org/jonforums/uru/downloads/uru-$uruVersion-windows-x86.7z"
55+
$uruArchive = "$env:TEMP\uru.7z"
56+
$uruDir = "C:\tools\uru"
57+
58+
# Create uru directory
59+
New-Item -ItemType Directory -Force -Path $uruDir | Out-Null
60+
61+
# Download uru
62+
Write-Host "Downloading uru $uruVersion..."
63+
Invoke-WebRequest -Uri $uruUrl -OutFile $uruArchive
64+
65+
# Extract using 7z (available on GitHub runners)
66+
Write-Host "Extracting uru..."
67+
7z x $uruArchive -o"$uruDir" -y
68+
69+
# Add uru to PATH
70+
$uruDir | Out-File -FilePath $env:GITHUB_PATH -Append
71+
72+
# Initialize uru
73+
Write-Host "Installing uru..."
74+
& "$uruDir\uru_rt.exe" admin install
75+
76+
Write-Host "uru installed successfully"
77+
78+
- name: "Register Ruby with uru"
79+
shell: pwsh
80+
run: |
81+
# Register the Chocolatey Ruby with uru
82+
Write-Host "Registering Ruby with uru..."
83+
uru_rt.exe admin add C:\tools\ruby32\bin
84+
Write-Host ""
85+
Write-Host "Registered rubies:"
86+
uru_rt.exe ls
87+
88+
- name: "Verify uru rubies.json exists"
89+
shell: pwsh
90+
run: |
91+
$rubiesJson = "$env:USERPROFILE\.uru\rubies.json"
92+
if (Test-Path $rubiesJson) {
93+
Write-Host "rubies.json found at: $rubiesJson"
94+
Get-Content $rubiesJson
95+
} else {
96+
Write-Host "ERROR: rubies.json not found!"
97+
exit 1
98+
}
99+
100+
- name: "Migrate uru Ruby to dtvem"
101+
shell: bash
102+
run: |
103+
echo "=== Running migrate detection ==="
104+
echo -e "1\n0\n" | ./dist/dtvem.exe migrate ruby || true
105+
echo ""
106+
echo "=== Verifying migration ==="
107+
./dist/dtvem.exe list ruby
108+
109+
- name: "Verify migrated version"
110+
shell: bash
111+
run: |
112+
./dist/dtvem.exe list ruby | grep -E "3\.2\." || (echo "ERROR: Expected Ruby 3.2.x to be migrated" && exit 1)
113+
echo "SUCCESS: Ruby 3.2.x was migrated from uru"
114+
115+
- name: Generate summary
116+
if: always()
117+
shell: bash
118+
run: |
119+
echo "## Ruby Migration from uru (Windows)" >> $GITHUB_STEP_SUMMARY
120+
echo "" >> $GITHUB_STEP_SUMMARY
121+
echo "**Source:** uru" >> $GITHUB_STEP_SUMMARY
122+
echo "**Version:** 3.2.x" >> $GITHUB_STEP_SUMMARY
123+
echo "" >> $GITHUB_STEP_SUMMARY
124+
echo "### Installed Versions" >> $GITHUB_STEP_SUMMARY
125+
echo '```' >> $GITHUB_STEP_SUMMARY
126+
./dist/dtvem.exe list ruby >> $GITHUB_STEP_SUMMARY 2>&1 || echo "No versions" >> $GITHUB_STEP_SUMMARY
127+
echo '```' >> $GITHUB_STEP_SUMMARY

.github/workflows/integration-test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,7 @@ jobs:
111111
migrate-ruby-windows-system:
112112
name: Migrate Ruby from System (Windows)
113113
uses: ./.github/workflows/integration-test-migrate-ruby-windows-system.yml
114+
115+
migrate-ruby-windows-uru:
116+
name: Migrate Ruby from uru (Windows)
117+
uses: ./.github/workflows/integration-test-migrate-ruby-windows-uru.yml

src/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
_ "github.com/dtvem/dtvem/src/migrations/ruby/rbenv"
2424
_ "github.com/dtvem/dtvem/src/migrations/ruby/rvm"
2525
_ "github.com/dtvem/dtvem/src/migrations/ruby/system"
26+
_ "github.com/dtvem/dtvem/src/migrations/ruby/uru"
2627
)
2728

2829
func main() {
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Package uru provides a migration provider for uru (multi-platform Ruby version manager).
2+
package uru
3+
4+
import (
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"regexp"
10+
goruntime "runtime"
11+
12+
"github.com/dtvem/dtvem/src/internal/migration"
13+
)
14+
15+
// rubyEntry represents a Ruby installation in uru's rubies.json.
16+
type rubyEntry struct {
17+
ID string `json:"ID"`
18+
TagLabel string `json:"TagLabel"`
19+
Exe string `json:"Exe"`
20+
Home string `json:"Home"`
21+
GemHome string `json:"GemHome"`
22+
Description string `json:"Description"`
23+
}
24+
25+
// rubiesJSON represents the structure of uru's rubies.json file.
26+
type rubiesJSON struct {
27+
Version string `json:"Version"`
28+
Rubies map[string]rubyEntry `json:"Rubies"`
29+
}
30+
31+
// Provider implements the migration.Provider interface for uru.
32+
type Provider struct{}
33+
34+
// NewProvider creates a new uru migration provider.
35+
func NewProvider() *Provider {
36+
return &Provider{}
37+
}
38+
39+
// Name returns the identifier for this version manager.
40+
func (p *Provider) Name() string {
41+
return "uru"
42+
}
43+
44+
// DisplayName returns the human-readable name.
45+
func (p *Provider) DisplayName() string {
46+
return "uru"
47+
}
48+
49+
// Runtime returns the runtime this provider manages.
50+
func (p *Provider) Runtime() string {
51+
return "ruby"
52+
}
53+
54+
// getUruHome returns the uru home directory.
55+
// It checks URU_HOME environment variable first, then falls back to ~/.uru.
56+
func (p *Provider) getUruHome() string {
57+
if uruHome := os.Getenv("URU_HOME"); uruHome != "" {
58+
return uruHome
59+
}
60+
61+
home, err := os.UserHomeDir()
62+
if err != nil {
63+
return ""
64+
}
65+
66+
return filepath.Join(home, ".uru")
67+
}
68+
69+
// IsPresent checks if uru is installed on the system.
70+
func (p *Provider) IsPresent() bool {
71+
uruHome := p.getUruHome()
72+
if uruHome == "" {
73+
return false
74+
}
75+
76+
rubiesPath := filepath.Join(uruHome, "rubies.json")
77+
if _, err := os.Stat(rubiesPath); err == nil {
78+
return true
79+
}
80+
81+
return false
82+
}
83+
84+
// DetectVersions finds all versions registered with uru.
85+
func (p *Provider) DetectVersions() ([]migration.DetectedVersion, error) {
86+
detected := make([]migration.DetectedVersion, 0)
87+
88+
uruHome := p.getUruHome()
89+
if uruHome == "" {
90+
return detected, nil
91+
}
92+
93+
rubiesPath := filepath.Join(uruHome, "rubies.json")
94+
data, err := os.ReadFile(rubiesPath)
95+
if err != nil {
96+
// If we can't read the file, just return empty list
97+
return detected, nil //nolint:nilerr // Expected: no rubies.json means no uru rubies
98+
}
99+
100+
var rubies rubiesJSON
101+
if err := json.Unmarshal(data, &rubies); err != nil {
102+
// Invalid JSON, return empty list
103+
return detected, nil //nolint:nilerr // Expected: invalid JSON means no usable uru data
104+
}
105+
106+
// Version pattern: major.minor.patch (e.g., "3.2.0")
107+
versionRegex := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
108+
109+
for tag, entry := range rubies.Rubies {
110+
if entry.Home == "" {
111+
continue
112+
}
113+
114+
// Extract version from ID field (e.g., "3.2.0-p0" -> "3.2.0")
115+
version := ""
116+
if matches := versionRegex.FindStringSubmatch(entry.ID); len(matches) >= 2 {
117+
version = matches[1]
118+
}
119+
120+
if version == "" {
121+
continue
122+
}
123+
124+
// Build path to ruby executable
125+
rubyExe := "ruby"
126+
if goruntime.GOOS == "windows" {
127+
rubyExe = "ruby.exe"
128+
}
129+
rubyPath := filepath.Join(entry.Home, rubyExe)
130+
131+
// Verify the executable exists
132+
if _, err := os.Stat(rubyPath); err != nil {
133+
continue
134+
}
135+
136+
detected = append(detected, migration.DetectedVersion{
137+
Version: version,
138+
Path: rubyPath,
139+
Source: fmt.Sprintf("uru (%s)", tag),
140+
Validated: false,
141+
})
142+
}
143+
144+
return detected, nil
145+
}
146+
147+
// CanAutoUninstall returns true because uru supports removing registered rubies.
148+
func (p *Provider) CanAutoUninstall() bool {
149+
return true
150+
}
151+
152+
// UninstallCommand returns the command to remove a Ruby from uru's registry.
153+
func (p *Provider) UninstallCommand(version string) string {
154+
return fmt.Sprintf("uru admin rm %s", version)
155+
}
156+
157+
// ManualInstructions returns instructions for manual removal.
158+
func (p *Provider) ManualInstructions() string {
159+
return "To remove a Ruby from uru's registry:\n" +
160+
" 1. Run: uru admin rm <tag>\n" +
161+
" 2. This only removes uru's reference, not the Ruby installation itself\n" +
162+
" 3. To fully uninstall, also remove the Ruby directory manually"
163+
}
164+
165+
// init registers the uru provider on package load.
166+
func init() {
167+
if err := migration.Register(NewProvider()); err != nil {
168+
panic(fmt.Sprintf("failed to register uru migration provider: %v", err))
169+
}
170+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package uru
2+
3+
import (
4+
"testing"
5+
6+
"github.com/dtvem/dtvem/src/internal/migration"
7+
)
8+
9+
func TestProvider(t *testing.T) {
10+
harness := &migration.ProviderTestHarness{
11+
Provider: NewProvider(),
12+
ExpectedName: "uru",
13+
Runtime: "ruby",
14+
}
15+
harness.RunAll(t)
16+
}
17+
18+
func TestProvider_UninstallCommand(t *testing.T) {
19+
p := NewProvider()
20+
21+
tests := []struct {
22+
version string
23+
expected string
24+
}{
25+
{version: "3.3.0", expected: "uru admin rm 3.3.0"},
26+
{version: "3.2.2", expected: "uru admin rm 3.2.2"},
27+
}
28+
29+
for _, tt := range tests {
30+
t.Run(tt.version, func(t *testing.T) {
31+
result := p.UninstallCommand(tt.version)
32+
if result != tt.expected {
33+
t.Errorf("UninstallCommand(%q) = %q, want %q", tt.version, result, tt.expected)
34+
}
35+
})
36+
}
37+
}
38+
39+
func TestProvider_GetUruHome(t *testing.T) {
40+
p := NewProvider()
41+
42+
// Test that getUruHome returns a non-empty string (uses home dir fallback)
43+
home := p.getUruHome()
44+
if home == "" {
45+
t.Error("getUruHome() returned empty string, expected path to ~/.uru")
46+
}
47+
}

0 commit comments

Comments
 (0)