Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
echo "**Source:** Chocolatey" >> $GITHUB_STEP_SUMMARY
echo "**Version:** 3.2.x" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "*Note: No rbenv equivalent on Windows. See issue #122 for RubyInstaller/uru support.*" >> $GITHUB_STEP_SUMMARY
echo "*Note: For version manager migration on Windows, see the uru integration test.*" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Installed Versions" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
Expand Down
127 changes: 127 additions & 0 deletions .github/workflows/integration-test-migrate-ruby-windows-uru.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Integration Tests - Migrate Ruby from uru (Windows)

on:
workflow_call:
workflow_dispatch:

permissions:
contents: read

jobs:
migrate:
name: Ruby from uru (Windows)
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true

- name: Build dtvem
shell: bash
run: |
go build -v -ldflags="-s -w" -o dist/dtvem.exe ./src
go build -v -ldflags="-s -w" -o dist/dtvem-shim.exe ./src/cmd/shim

- name: Initialize dtvem
shell: bash
run: ./dist/dtvem.exe init --yes

- name: Add dtvem to PATH
shell: pwsh
run: |
"$env:USERPROFILE\.dtvem\shims" | Out-File -FilePath $env:GITHUB_PATH -Append
"$env:USERPROFILE\.dtvem\bin" | Out-File -FilePath $env:GITHUB_PATH -Append

- name: "Install Ruby 3.2 via Chocolatey"
shell: pwsh
run: |
# Install Ruby via Chocolatey (will be registered with uru)
choco install ruby --version=3.2.6.1 -y
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Host "Ruby installed at C:\tools\ruby32"
C:\tools\ruby32\bin\ruby.exe --version

- name: "Install uru"
shell: pwsh
run: |
# Download uru from Bitbucket releases
$uruVersion = "0.8.5"
$uruUrl = "https://bitbucket.org/jonforums/uru/downloads/uru-$uruVersion-windows-x86.7z"
$uruArchive = "$env:TEMP\uru.7z"
$uruDir = "C:\tools\uru"

# Create uru directory
New-Item -ItemType Directory -Force -Path $uruDir | Out-Null

# Download uru
Write-Host "Downloading uru $uruVersion..."
Invoke-WebRequest -Uri $uruUrl -OutFile $uruArchive

# Extract using 7z (available on GitHub runners)
Write-Host "Extracting uru..."
7z x $uruArchive -o"$uruDir" -y

# Add uru to PATH
$uruDir | Out-File -FilePath $env:GITHUB_PATH -Append

# Initialize uru
Write-Host "Installing uru..."
& "$uruDir\uru_rt.exe" admin install

Write-Host "uru installed successfully"

- name: "Register Ruby with uru"
shell: pwsh
run: |
# Register the Chocolatey Ruby with uru
Write-Host "Registering Ruby with uru..."
uru_rt.exe admin add C:\tools\ruby32\bin
Write-Host ""
Write-Host "Registered rubies:"
uru_rt.exe ls

- name: "Verify uru rubies.json exists"
shell: pwsh
run: |
$rubiesJson = "$env:USERPROFILE\.uru\rubies.json"
if (Test-Path $rubiesJson) {
Write-Host "rubies.json found at: $rubiesJson"
Get-Content $rubiesJson
} else {
Write-Host "ERROR: rubies.json not found!"
exit 1
}

- name: "Migrate uru Ruby to dtvem"
shell: bash
run: |
echo "=== Running migrate detection ==="
echo -e "1\n0\n" | ./dist/dtvem.exe migrate ruby || true
echo ""
echo "=== Verifying migration ==="
./dist/dtvem.exe list ruby

- name: "Verify migrated version"
shell: bash
run: |
./dist/dtvem.exe list ruby | grep -E "3\.2\." || (echo "ERROR: Expected Ruby 3.2.x to be migrated" && exit 1)
echo "SUCCESS: Ruby 3.2.x was migrated from uru"

- name: Generate summary
if: always()
shell: bash
run: |
echo "## Ruby Migration from uru (Windows)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Source:** uru" >> $GITHUB_STEP_SUMMARY
echo "**Version:** 3.2.x" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Installed Versions" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
./dist/dtvem.exe list ruby >> $GITHUB_STEP_SUMMARY 2>&1 || echo "No versions" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
4 changes: 4 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ jobs:
migrate-ruby-windows-system:
name: Migrate Ruby from System (Windows)
uses: ./.github/workflows/integration-test-migrate-ruby-windows-system.yml

migrate-ruby-windows-uru:
name: Migrate Ruby from uru (Windows)
uses: ./.github/workflows/integration-test-migrate-ruby-windows-uru.yml
1 change: 1 addition & 0 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
_ "github.com/dtvem/dtvem/src/migrations/ruby/rbenv"
_ "github.com/dtvem/dtvem/src/migrations/ruby/rvm"
_ "github.com/dtvem/dtvem/src/migrations/ruby/system"
_ "github.com/dtvem/dtvem/src/migrations/ruby/uru"
)

func main() {
Expand Down
170 changes: 170 additions & 0 deletions src/migrations/ruby/uru/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Package uru provides a migration provider for uru (multi-platform Ruby version manager).
package uru

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
goruntime "runtime"

"github.com/dtvem/dtvem/src/internal/migration"
)

// rubyEntry represents a Ruby installation in uru's rubies.json.
type rubyEntry struct {
ID string `json:"ID"`
TagLabel string `json:"TagLabel"`
Exe string `json:"Exe"`
Home string `json:"Home"`
GemHome string `json:"GemHome"`
Description string `json:"Description"`
}

// rubiesJSON represents the structure of uru's rubies.json file.
type rubiesJSON struct {
Version string `json:"Version"`
Rubies map[string]rubyEntry `json:"Rubies"`
}

// Provider implements the migration.Provider interface for uru.
type Provider struct{}

// NewProvider creates a new uru migration provider.
func NewProvider() *Provider {
return &Provider{}
}

// Name returns the identifier for this version manager.
func (p *Provider) Name() string {
return "uru"
}

// DisplayName returns the human-readable name.
func (p *Provider) DisplayName() string {
return "uru"
}

// Runtime returns the runtime this provider manages.
func (p *Provider) Runtime() string {
return "ruby"
}

// getUruHome returns the uru home directory.
// It checks URU_HOME environment variable first, then falls back to ~/.uru.
func (p *Provider) getUruHome() string {
if uruHome := os.Getenv("URU_HOME"); uruHome != "" {
return uruHome
}

home, err := os.UserHomeDir()
if err != nil {
return ""
}

return filepath.Join(home, ".uru")
}

// IsPresent checks if uru is installed on the system.
func (p *Provider) IsPresent() bool {
uruHome := p.getUruHome()
if uruHome == "" {
return false
}

rubiesPath := filepath.Join(uruHome, "rubies.json")
if _, err := os.Stat(rubiesPath); err == nil {
return true
}

return false
}

// DetectVersions finds all versions registered with uru.
func (p *Provider) DetectVersions() ([]migration.DetectedVersion, error) {
detected := make([]migration.DetectedVersion, 0)

uruHome := p.getUruHome()
if uruHome == "" {
return detected, nil
}

rubiesPath := filepath.Join(uruHome, "rubies.json")
data, err := os.ReadFile(rubiesPath)
if err != nil {
// If we can't read the file, just return empty list
return detected, nil //nolint:nilerr // Expected: no rubies.json means no uru rubies
}

var rubies rubiesJSON
if err := json.Unmarshal(data, &rubies); err != nil {
// Invalid JSON, return empty list
return detected, nil //nolint:nilerr // Expected: invalid JSON means no usable uru data
}

// Version pattern: major.minor.patch (e.g., "3.2.0")
versionRegex := regexp.MustCompile(`(\d+\.\d+\.\d+)`)

for tag, entry := range rubies.Rubies {
if entry.Home == "" {
continue
}

// Extract version from ID field (e.g., "3.2.0-p0" -> "3.2.0")
version := ""
if matches := versionRegex.FindStringSubmatch(entry.ID); len(matches) >= 2 {
version = matches[1]
}

if version == "" {
continue
}

// Build path to ruby executable
rubyExe := "ruby"
if goruntime.GOOS == "windows" {
rubyExe = "ruby.exe"
}
rubyPath := filepath.Join(entry.Home, rubyExe)

// Verify the executable exists
if _, err := os.Stat(rubyPath); err != nil {
continue
}

detected = append(detected, migration.DetectedVersion{
Version: version,
Path: rubyPath,
Source: fmt.Sprintf("uru (%s)", tag),
Validated: false,
})
}

return detected, nil
}

// CanAutoUninstall returns true because uru supports removing registered rubies.
func (p *Provider) CanAutoUninstall() bool {
return true
}

// UninstallCommand returns the command to remove a Ruby from uru's registry.
func (p *Provider) UninstallCommand(version string) string {
return fmt.Sprintf("uru admin rm %s", version)
}

// ManualInstructions returns instructions for manual removal.
func (p *Provider) ManualInstructions() string {
return "To remove a Ruby from uru's registry:\n" +
" 1. Run: uru admin rm <tag>\n" +
" 2. This only removes uru's reference, not the Ruby installation itself\n" +
" 3. To fully uninstall, also remove the Ruby directory manually"
}

// init registers the uru provider on package load.
func init() {
if err := migration.Register(NewProvider()); err != nil {
panic(fmt.Sprintf("failed to register uru migration provider: %v", err))
}
}
47 changes: 47 additions & 0 deletions src/migrations/ruby/uru/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package uru

import (
"testing"

"github.com/dtvem/dtvem/src/internal/migration"
)

func TestProvider(t *testing.T) {
harness := &migration.ProviderTestHarness{
Provider: NewProvider(),
ExpectedName: "uru",
Runtime: "ruby",
}
harness.RunAll(t)
}

func TestProvider_UninstallCommand(t *testing.T) {
p := NewProvider()

tests := []struct {
version string
expected string
}{
{version: "3.3.0", expected: "uru admin rm 3.3.0"},
{version: "3.2.2", expected: "uru admin rm 3.2.2"},
}

for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
result := p.UninstallCommand(tt.version)
if result != tt.expected {
t.Errorf("UninstallCommand(%q) = %q, want %q", tt.version, result, tt.expected)
}
})
}
}

func TestProvider_GetUruHome(t *testing.T) {
p := NewProvider()

// Test that getUruHome returns a non-empty string (uses home dir fallback)
home := p.getUruHome()
if home == "" {
t.Error("getUruHome() returned empty string, expected path to ~/.uru")
}
}