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
253 changes: 253 additions & 0 deletions .github/workflows/integration-test-uninstall.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
name: Integration Tests - Uninstall Scripts

on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'uninstall.sh'
- 'uninstall.ps1'
- 'install.sh'
- 'install.ps1'
- 'src/cmd/init.go'
- 'src/internal/path/**'
- '.github/workflows/integration-test-uninstall.yml'
pull_request:
branches: [main]
paths:
- 'uninstall.sh'
- 'uninstall.ps1'
- 'install.sh'
- 'install.ps1'
- 'src/cmd/init.go'
- 'src/internal/path/**'
- '.github/workflows/integration-test-uninstall.yml'

permissions:
contents: read

jobs:
unix:
name: Uninstall (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
env:
SHELL: /bin/bash
steps:
- name: Checkout code
uses: actions/checkout@v4

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

- name: Build dtvem
run: go build -o "$RUNNER_TEMP/dtvem" ./src

- name: Install dtvem as install.sh does
run: |
set -eu
if [ "$(uname -s)" = Linux ]; then
root="$HOME/.local/share/dtvem"
else
root="$HOME/.dtvem"
fi
mkdir -p "$root/bin"
cp "$RUNNER_TEMP/dtvem" "$root/bin/dtvem"

# The PATH line install.sh adds, and one the user wrote themselves
touch ~/.bashrc
{
echo ""
echo "# Added by dtvem installer"
echo "export PATH=\"$root/bin:\$PATH\""
echo "export DTVEM_VERBOSE=1"
} >> ~/.bashrc

# Adds the shims PATH line
"$root/bin/dtvem" init -y

# A runtime version holding a read-only directory and a link to a
# directory outside the root (as `npm link` makes)
version="$root/versions/node/22.0.0"
mkdir -p "$version/lib/node_modules" "$version/readonly" "$RUNNER_TEMP/linked-project"
echo keep > "$RUNNER_TEMP/linked-project/keep.txt"
ln -s "$RUNNER_TEMP/linked-project" "$version/lib/node_modules/linked"
touch "$version/readonly/file"
chmod 555 "$version/readonly"

echo "DTVEM_TEST_ROOT=$root" >> "$GITHUB_ENV"
cat ~/.bashrc

# On macOS this is the stock bash 3.2, which `curl | bash` runs there
- name: Uninstall
run: /bin/bash ./uninstall.sh --yes

- name: Verify
run: |
fail=0
check() {
if eval "$1"; then echo "✓ $2"; else echo "✗ $2"; fail=1; fi
}
check '[ ! -e "$DTVEM_TEST_ROOT" ]' "dtvem root removed"
check '[ -f "$RUNNER_TEMP/linked-project/keep.txt" ]' "linked directory outside the root kept"
check '! grep -q "Added by dtvem" ~/.bashrc' "dtvem PATH lines removed"
check '! grep -qF "$DTVEM_TEST_ROOT" ~/.bashrc' "no PATH line names the root"
check 'grep -q "DTVEM_VERBOSE" ~/.bashrc' "the user's own line kept"
cat ~/.bashrc
exit $fail

- name: Uninstall again (nothing to remove)
run: /bin/bash ./uninstall.sh --yes

- name: Custom DTVEM_ROOT keeps other files
run: |
set -eu
root="$RUNNER_TEMP/tools"
mkdir -p "$root/bin" "$root/shims" "$root/versions/ruby/3.4.1"
cp "$RUNNER_TEMP/dtvem" "$root/bin/dtvem"
echo keep > "$root/bin/other-tool"
echo keep > "$root/notes.txt"
DTVEM_ROOT="$root" /bin/bash ./uninstall.sh --yes
if [ ! -f "$root/bin/other-tool" ] || [ ! -f "$root/notes.txt" ]; then
echo "✗ files dtvem didn't create were removed"
exit 1
fi
if [ -e "$root/shims" ] || [ -e "$root/versions" ] || [ -e "$root/bin/dtvem" ]; then
echo "✗ dtvem's files are still there"
exit 1
fi
echo "✓ only dtvem's files were removed"

windows:
name: Uninstall (windows, ${{ matrix.shell }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
shell: [pwsh, powershell]
defaults:
run:
shell: pwsh
steps:
- name: Checkout code
uses: actions/checkout@v4

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

- name: Build dtvem
run: go build -o "$env:RUNNER_TEMP\dtvem.exe" ./src

- name: Install dtvem as install.ps1 does
run: |
$root = Join-Path $env:USERPROFILE '.dtvem'
New-Item -ItemType Directory "$root\bin" -Force | Out-Null
Copy-Item "$env:RUNNER_TEMP\dtvem.exe" "$root\bin\dtvem.exe"

# User PATH: the install directory (as install.ps1 adds it), a
# stale shims entry from an old install, and a %VAR% entry that
# must stay unexpanded
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
$userPath = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$key.SetValue('Path', "$root\bin;C:\old-install\.dtvem\shims;$userPath;%USERPROFILE%\keep-me", 'ExpandString')
$key.Close()

# System PATH: runner images store it as REG_SZ with no %VAR%
# entries. Make it REG_EXPAND_SZ with one, so the test shows the
# uninstaller keeps both.
$system = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', $true)
"System PATH was stored as $($system.GetValueKind('Path'))"
$systemPath = $system.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$system.SetValue('Path', "$systemPath;%SystemRoot%\keep-me", 'ExpandString')
$system.Close()

# System PATH: the shims directory (the runner is an administrator)
& "$root\bin\dtvem.exe" init -y
if ($LASTEXITCODE) { exit $LASTEXITCODE }

# A runtime version holding a read-only file, a path over 260
# characters, and a junction to a directory outside the root (as
# `npm link` makes)
$version = "$root\versions\node\22.0.0"
New-Item -ItemType Directory "$version\node_modules", "$env:RUNNER_TEMP\linked-project" -Force | Out-Null
Set-Content "$env:RUNNER_TEMP\linked-project\keep.txt" 'keep'
New-Item -ItemType Junction -Path "$version\node_modules\linked" -Target "$env:RUNNER_TEMP\linked-project" | Out-Null
Set-Content "$version\readonly.txt" 'x'
(Get-Item "$version\readonly.txt").IsReadOnly = $true
$deep = $version + (('\' + ('d' * 50)) * 6)
[IO.Directory]::CreateDirectory("\\?\$deep") | Out-Null
[IO.File]::WriteAllText("\\?\$deep\deep.txt", 'x')

# What RubyInstaller registered when older dtvem versions used it:
# a Programs and Features entry, a file type, the .rb association,
# a Start menu folder and a System PATH entry, all pointing at the
# deleted %TEMP%\dtvem-ruby-<version> directory
$staging = Join-Path $env:TEMP 'dtvem-ruby-3.3.9\installed'
$uninstall = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\RubyInstaller-3.3-x64-mingw-ucrt_is1'
New-Item -Path $uninstall -Force | Out-Null
New-ItemProperty -Path $uninstall -Name DisplayName -Value 'Ruby 3.3.9-1-x64' | Out-Null
New-ItemProperty -Path $uninstall -Name InstallLocation -Value "$staging\" | Out-Null
New-Item -Path 'HKCU:\Software\Classes\RubyFile\shell\open\command' -Force | Out-Null
Set-Item -Path 'HKCU:\Software\Classes\RubyFile\shell\open\command' -Value "`"$staging\bin\ruby.exe`" `"%1`" %*"
New-Item -Path 'HKCU:\Software\Classes\.rb\OpenWithProgids' -Force | Out-Null
Set-Item -Path 'HKCU:\Software\Classes\.rb' -Value 'RubyFile'
$menu = Join-Path ([Environment]::GetFolderPath('Programs')) 'Ruby 3.3.9-1-x64'
New-Item -ItemType Directory $menu -Force | Out-Null
$shortcut = (New-Object -ComObject WScript.Shell).CreateShortcut("$menu\Interactive Ruby.lnk")
$shortcut.TargetPath = "$staging\bin\irb.bat"
$shortcut.Save()
$system = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', $true)
$systemPath = $system.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$system.SetValue('Path', "$systemPath;$staging\bin", $system.GetValueKind('Path'))
$system.Close()

"DTVEM_TEST_ROOT=$root" >> $env:GITHUB_ENV

- name: Uninstall
run: |
& ${{ matrix.shell }} -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1 -Yes
if ($LASTEXITCODE) { exit $LASTEXITCODE }

- name: Verify
run: |
$failed = $false
function Check($ok, $name) {
if ($ok) { "✓ $name" } else { "✗ $name"; $script:failed = $true }
}

$user = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment')
$userPath = $user.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$userKind = $user.GetValueKind('Path')
$user.Close()
$system = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment')
$systemPath = $system.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$systemKind = $system.GetValueKind('Path')
$system.Close()

Check (-not (Test-Path $env:DTVEM_TEST_ROOT)) 'dtvem root removed'
Check (Test-Path "$env:RUNNER_TEMP\linked-project\keep.txt") 'junction target outside the root kept'
Check ($userPath -notmatch 'dtvem') "User PATH has no dtvem entries: $userPath"
Check ($userPath -like '*%USERPROFILE%\keep-me*') 'User PATH %VAR% entry kept unexpanded'
Check ($userKind -eq 'ExpandString') "User PATH is still REG_EXPAND_SZ ($userKind)"
Check ($systemPath -notmatch 'dtvem') "System PATH has no dtvem entries: $systemPath"
Check ($systemPath -match 'system32') 'System PATH keeps its other entries'
Check ($systemPath -like '*%SystemRoot%\keep-me*') 'System PATH %VAR% entry kept unexpanded'
Check ($systemKind -eq 'ExpandString') "System PATH is still REG_EXPAND_SZ ($systemKind)"
Check (-not (Test-Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\RubyInstaller-3.3-x64-mingw-ucrt_is1')) 'Programs and Features entry removed'
Check (-not (Test-Path 'HKCU:\Software\Classes\RubyFile')) 'RubyFile file type removed'
Check (-not (Get-Item 'HKCU:\Software\Classes\.rb').GetValue('')) '.rb association removed'
Check (Test-Path 'HKCU:\Software\Classes\.rb\OpenWithProgids') "other programs' .rb registrations kept"
Check (-not (Test-Path (Join-Path ([Environment]::GetFolderPath('Programs')) 'Ruby 3.3.9-1-x64'))) 'Start menu folder removed'
if ($failed) { exit 1 }

- name: Uninstall again (nothing to remove)
run: |
& ${{ matrix.shell }} -NoProfile -ExecutionPolicy Bypass -File .\uninstall.ps1 -Yes
if ($LASTEXITCODE) { exit $LASTEXITCODE }
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ jobs:
name: install-scripts
path: |
install.sh
uninstall.sh
install.ps1
uninstall.ps1
retention-days: 1

changelog:
Expand Down Expand Up @@ -279,7 +281,9 @@ jobs:
artifacts/build-windows-amd64/dtvem-${{ github.event.inputs.version }}-windows-amd64.zip
artifacts/build-windows-arm64/dtvem-${{ github.event.inputs.version }}-windows-arm64.zip
install.sh
uninstall.sh
install.ps1
uninstall.ps1
body: |
## What's New in v${{ github.event.inputs.version }}

Expand Down
31 changes: 23 additions & 8 deletions .github/workflows/script-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@ on:
paths:
- 'install.sh'
- 'install.ps1'
- 'uninstall.sh'
- 'uninstall.ps1'
- '.github/workflows/script-lint.yml'
pull_request:
branches: [ main ]
paths:
- 'install.sh'
- 'install.ps1'
- 'uninstall.sh'
- 'uninstall.ps1'
- '.github/workflows/script-lint.yml'

permissions:
contents: read

jobs:
shellcheck:
name: Lint install.sh
name: Lint install.sh and uninstall.sh
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -27,10 +33,10 @@ jobs:
uses: ludeeus/action-shellcheck@master
with:
scandir: '.'
additional_files: 'install.sh'
additional_files: 'install.sh uninstall.sh'

psscriptanalyzer:
name: Lint install.ps1
name: Lint install.ps1 and uninstall.ps1
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -40,10 +46,19 @@ jobs:
shell: pwsh
run: |
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser
# Exclude PSAvoidUsingWriteHost - Write-Host is intentional for colored installer output
$results = Invoke-ScriptAnalyzer -Path ./install.ps1 -Severity Error,Warning -ExcludeRule PSAvoidUsingWriteHost,PSUseBOMForUnicodeEncodedFile
if ($results) {
$results | Format-Table -AutoSize
$failed = $false
foreach ($script in './install.ps1', './uninstall.ps1') {
# Exclude PSAvoidUsingWriteHost - Write-Host is intentional for colored installer output
$results = Invoke-ScriptAnalyzer -Path $script -Severity Error,Warning -ExcludeRule PSAvoidUsingWriteHost,PSUseBOMForUnicodeEncodedFile
if ($results) {
Write-Host "Issues in ${script}:"
$results | Format-Table -AutoSize
$failed = $true
}
else {
Write-Host "✅ No issues found in $script"
}
}
if ($failed) {
exit 1
}
Write-Host "✅ No issues found in install.ps1"
20 changes: 19 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ cd src && go test -cover ./... # With coverage
| `build.yml` | PR, push to main | Lint, build, test on Windows/macOS/Linux. Posts coverage reports on PRs |
| `release.yml` | Manual dispatch | Full release: validate, build 5 platforms, create GitHub Release, notify |
| `commit-lint.yml` | PR | Validate PR titles follow conventional commits |
| `script-lint.yml` | PR (install scripts) | Lint install.sh and install.ps1 with shellcheck/PSScriptAnalyzer |
| `script-lint.yml` | PR (install scripts) | Lint the install and uninstall scripts with shellcheck/PSScriptAnalyzer |
| `integration-test-uninstall.yml` | PR (uninstall/install scripts, `init`, `internal/path`) | Install as the installer and `dtvem init` do, run the uninstall scripts, and check nothing is left (Linux, macOS bash 3.2, Windows pwsh and PowerShell 5.1) |
| `contributors.yml` | Push to main | Auto-update contributors section in README |
| `preview-changelog.yml` | PR | Preview release notes for PRs |
| `integration-test.yml` | Manual dispatch | Full integration test suite (runtimes + migrations) |
Expand Down Expand Up @@ -367,6 +368,23 @@ irm dtvem.io/install.ps1 | iex

Features: Auto platform detection, PATH configuration, runs `dtvem init`.

### Uninstallers

```bash
curl -fsSL dtvem.io/uninstall.sh | bash # --yes skips the prompt
```

```powershell
irm dtvem.io/uninstall.ps1 | iex # -Yes skips the prompt
```

They list what they will remove and ask first. They remove:
- **Every dtvem root**: `DTVEM_ROOT`, `XDG_DATA_HOME/dtvem`, `~/.local/share/dtvem` and `~/.dtvem`. A root not named `dtvem` or `.dtvem` (a custom `DTVEM_ROOT`) only loses the entries dtvem creates in it. The scripts refuse `/`, a drive root and the home directory.
- **PATH changes** from the installers and `dtvem init`. On Unix that is the `# Added by dtvem` / `# Added by dtvem installer` blocks in shell config files; lines without the marker are listed, never edited. On Windows it is User and System PATH entries, written back as in `writePathValue`.
- **RubyInstaller leftovers (Windows)**: what RubyInstaller registered when older dtvem versions installed Ruby with its `.exe` (Programs and Features entries, Start menu folders, PATH entries, the `.rb`/`.rbw` association, all pointing at a deleted `%TEMP%\dtvem-ruby-<version>`).

A PATH entry, line or registration is removed when it points into a root being removed, or no longer exists and was in a `dtvem`, `.dtvem` or `dtvem-ruby-*` directory; another install's existing directory is kept. Changes that need admin go through one UAC prompt. `uninstall.ps1` must stay ASCII: Windows PowerShell 5.1 reads a file without a BOM in the ANSI code page. When the installers or `dtvem init` change what they write, update the uninstallers to match.

---

## UI Output System
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ irm dtvem.io/install.ps1 | iex

See [Getting Started](https://dtvem.io/docs/user-guide/getting-started) for manual installation, building from source, and PATH configuration.

### 🗑️ Uninstalling

**macOS / Linux:**
```bash
curl -fsSL dtvem.io/uninstall.sh | bash
```

**Windows (PowerShell):**
```powershell
irm dtvem.io/uninstall.ps1 | iex
```

This removes dtvem, every runtime version installed with it, and the PATH changes it made, after showing what it will remove and asking first. Version files in your projects are left alone.

## 🚀 Quick Start

```bash
Expand Down
Loading
Loading