diff --git a/.github/workflows/integration-test-migrate-ruby-windows-system.yml b/.github/workflows/integration-test-migrate-ruby-windows-system.yml index 5b453e4..15fc8f5 100644 --- a/.github/workflows/integration-test-migrate-ruby-windows-system.yml +++ b/.github/workflows/integration-test-migrate-ruby-windows-system.yml @@ -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 diff --git a/.github/workflows/integration-test-migrate-ruby-windows-uru.yml b/.github/workflows/integration-test-migrate-ruby-windows-uru.yml new file mode 100644 index 0000000..e2e8f7d --- /dev/null +++ b/.github/workflows/integration-test-migrate-ruby-windows-uru.yml @@ -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 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index af53de7..0377243 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -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 diff --git a/src/main.go b/src/main.go index 9c527bc..4119443 100644 --- a/src/main.go +++ b/src/main.go @@ -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() { diff --git a/src/migrations/ruby/uru/provider.go b/src/migrations/ruby/uru/provider.go new file mode 100644 index 0000000..a628e85 --- /dev/null +++ b/src/migrations/ruby/uru/provider.go @@ -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 \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)) + } +} diff --git a/src/migrations/ruby/uru/provider_test.go b/src/migrations/ruby/uru/provider_test.go new file mode 100644 index 0000000..c17cc51 --- /dev/null +++ b/src/migrations/ruby/uru/provider_test.go @@ -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") + } +}