diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f6c81b..249754a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 1.4.0 - 2024-12-26 +**Major update with multiple bug fixes and improvements:** + +### Bug Fixes +- **#37**: Fixed script failing when only one distro is installed (improved parsing) +- **#30/#35**: Added automatic `wsl --shutdown` before export to prevent "file in use" errors +- **#29**: Default distro setting is now preserved after move + +### Enhancements +- **#23**: Pre-check for NTFS compression on target folder (prevents corruption) +- Added CLI parameters for non-interactive mode: `-Distro`, `-Target`, `-Force`, `-NoShutdown` +- Improved visual feedback with colors and progress messages +- Shows distro info (WSL version, state, default status) during selection +- Displays export file size after completion + +### Technical +- Replaced `ConvertFrom-String` with robust manual parsing +- Better error messages with recovery suggestions + ## 1.3.2 - 2020-09-09 Fix: Bug in PS script: Import failed when target folder was entered with trailing slash. diff --git a/README.md b/README.md index dbc821a..47bf0f0 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,123 @@ -> [!Warning] -> As I switched the OS, I am not updating this script or fixing bugs anymore. -> Have a look at the issue tab for some known issues. -> Happy to merge PRs with fixes. - # Move WSL PowerShell script to move WSL 1 and WSL 2 distros VHDX file to a different location. ![Interactive Example](screencast.gif) +## Features (v1.4.0) + +- ✅ Supports both WSL 1 and WSL 2 +- ✅ Interactive and CLI modes +- ✅ Automatic WSL shutdown to prevent file locks +- ✅ Preserves default distro setting +- ✅ Pre-checks for NTFS compression (prevents corruption) +- ✅ Colored output with progress feedback + ## Usage +### Interactive Mode + +```powershell +./move-wsl.ps1 +``` + +1. Select your distro from the list +2. Enter your target path (e.g., `D:\wsl\ubuntu`) +3. Confirm the operation + +### CLI Mode (Non-Interactive) + +```powershell +# Basic usage +./move-wsl.ps1 -Distro "Ubuntu" -Target "D:\wsl\ubuntu" + +# Force mode (skip confirmations) +./move-wsl.ps1 -Distro "Ubuntu" -Target "D:\wsl\ubuntu" -Force + +# Skip WSL shutdown (not recommended) +./move-wsl.ps1 -Distro "Ubuntu" -Target "D:\wsl\ubuntu" -NoShutdown +``` + +### Parameters + +| Parameter | Description | +|-----------|-------------| +| `-Distro` | Name of the WSL distro to move | +| `-Target` | Destination folder path | +| `-Force` | Skip confirmation prompts and NTFS compression warning | +| `-NoShutdown` | Skip automatic WSL shutdown (not recommended) | + +## ⚠️ Important Notes + > **Warning** > -> This script uses official `wsl` commands and was used by a lot of people. Nevertheless some people had weird issues that resulted in broken WSL disks. -> Make sure you have a backup of your data, so you can restore in case of an error. +> This script uses official `wsl` commands and was used by many people. Make sure you have a backup of your data before proceeding. -Interactive way of moving wsl for Windows PowerShell. +### Before Running -1) `./move-wsl.ps1` -2) Select your distro -3) Enter your target (i.e. `D:\wsl target\ubuntu`) +1. **Backup important data** in your WSL distro +2. **Close all applications** using WSL +3. **Stop Docker Desktop** if moving Docker WSL distros -## Moving Docker WSL +### NTFS Compression Warning + +The script will check if the target folder has NTFS compression enabled. **Compressed folders can corrupt WSL images.** If detected, disable compression: -Before moving Docker WSL make sure to stop the Docker service. Otherwise Docker will crash and you may need to reset it to factory defaults. +1. Right-click the target folder +2. Properties → Advanced +3. Uncheck "Compress contents to save disk space" ## FAQ ### Default user was switched to root when moving a distro -Set your default user inside your distro by adding the following configuration to your `/etc/wsl.conf`. +Set your default user inside your distro by adding the following configuration to your `/etc/wsl.conf`: ```ini [user] default=YOUR_USERNAME ``` -If the file doesn't exist create it manually. Then exit your distro, terminate it (`wsl -t YOUR_DISTRO`) and start it again. For further options see [Microsoft Docs](https://docs.microsoft.com/en-us/windows/wsl/wsl-config#user). +If the file doesn't exist, create it manually. Then exit your distro, terminate it (`wsl -t YOUR_DISTRO`) and start it again. -Some distributions also allow settings the default user via command line with `YOUR_DISTRO config --default-user YOUR_USER` (e.g. `ubuntu config --default-user johndoe`). Make sure to shutdown your distro before (`wsl -t YOUR_DISTRO`). +Some distributions also allow setting the default user via command line: +```powershell +ubuntu config --default-user johndoe +``` ### Standard distro switched when moving it -Since we need to unregister to import it with the same name, the standard distro can be switched. Just set your standard distro again: +The script now automatically preserves the default distro setting (v1.4.0+). If using an older version: -```sh +```powershell wsl -s YOUR_DISTRO ``` +### Script cannot be loaded (not digitally signed) + +Run this command to allow the script: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +./move-wsl.ps1 +``` + ### WSL version was switched when moving distro -On import the distro will be registered with the current default WSL version. You can set your default WSL version with `wsl --set-default-version `. -When the WSL version was accidentally changed while moving, you can set the version with `wsl --set-version `. +On import, the distro will be registered with its original WSL version. If it changed: + +```powershell +wsl --set-version +``` + +## Moving Docker WSL + +Before moving Docker WSL, make sure to: + +1. Stop Docker Desktop completely +2. Wait a few seconds for processes to terminate +3. Run the script + +## License + +This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details. diff --git a/move-wsl.ps1 b/move-wsl.ps1 index 215ad60..7014204 100644 --- a/move-wsl.ps1 +++ b/move-wsl.ps1 @@ -1,119 +1,248 @@ +# ============================================================================ +# Move-WSL v1.4.0 +# PowerShell script to move WSL 1 and WSL 2 distros VHDX file to a different location. +# Fixes: #37, #30, #35, #29, #23 +# ============================================================================ + +param( + [string]$Distro, + [string]$Target, + [switch]$Force, + [switch]$NoShutdown +) + Set-StrictMode -Version latest; -function Cleanup() -{ +function Cleanup() { # Remove temporary file - Write-Host "Cleaning up ..."; + Write-Host "Cleaning up ..." -ForegroundColor Gray; Remove-Item -ErrorAction Ignore $tempFile; } -# function to get distros -function Get-Distros() -{ - # wsl seems to output Unicode always. When parsing results in PowerShell it will try to convert - # to Unicode strings (again) assuming it's in the Console.OutputEncoding code page (437 in my case). - # This causes incorrect results. We are forcing Console.OutputEncoding to be Unicode here - # to avoid the unnecessary conversion. - $consoleEncoding = [Console]::OutputEncoding; - [Console]::OutputEncoding = [System.Text.Encoding]::Unicode; - $result = wsl -l -v | ConvertFrom-String -PropertyNames SELECTED, NAME, STATE, VERSION | Select-Object -Skip 1; - [Console]::OutputEncoding = $consoleEncoding; +# Fixed Get-Distros function - now works +function Get-Distros() { + $env:WSL_UTF8 = 1 + + $wslOutput = wsl -l -v 2>&1 + + if ($LASTEXITCODE -ne 0) { + return @() + } + + $lines = ($wslOutput -replace "`0", "") -split "`r?`n" | Where-Object { $_.Trim() -ne "" } | Select-Object -Skip 1 + + $result = @() + foreach ($line in $lines) { + $cleanLine = $line -replace '^\s*', '' + if ([string]::IsNullOrWhiteSpace($cleanLine)) { continue } + + $isDefault = $cleanLine.StartsWith('*') + if ($isDefault) { + $cleanLine = $cleanLine.Substring(1).TrimStart() + } + + $parts = @($cleanLine -split '\s+' | Where-Object { $_ -ne "" }) + + if ($parts.Count -ge 3) { + $result += [PSCustomObject]@{ + SELECTED = if ($isDefault) { '*' } else { '' } + NAME = $parts[0] + STATE = $parts[1] + VERSION = $parts[2] + } + } + } + + return $result +} - return $result; +# Function to check if folder has NTFS compression (Fix #23) +function Test-FolderCompressed { + param([string]$Path) + + # Check parent folder if target doesn't exist yet + $checkPath = $Path + while (-not (Test-Path $checkPath) -and $checkPath.Length -gt 3) { + $checkPath = Split-Path $checkPath -Parent + } + + if (Test-Path $checkPath) { + try { + $attributes = (Get-Item $checkPath -Force).Attributes + return ($attributes -band [System.IO.FileAttributes]::Compressed) -ne 0 + } + catch { + return $false + } + } + return $false } -# get and make sure there are distros -Write-Host 'Getting distros...'; +# ============================================================================ +# MAIN SCRIPT +# ============================================================================ + +Write-Host "=== Move-WSL v1.4.0 ===" -ForegroundColor Cyan +Write-Host "Move your WSL distros to a new location" -ForegroundColor Gray + +# Get and make sure there are distros +Write-Host 'Getting distros...' -ForegroundColor Gray; $distros = @(Get-Distros); -$distroList = @($distros | ForEach-Object { $_.NAME }); -if ($distroList.Length -le 0) -{ - Write-Error 'No distro found'; + +if ($distros.Count -eq 0) { + Write-Error 'No WSL distro found. Make sure WSL is installed and you have at least one distro.'; Exit 1; } -# prompt and get the distro to move -Write-Host "Select distro to move:"; -$id = 0; -$distroList | ForEach-Object { Write-Host "$($id+1): $($distroList[$id])" -ForegroundColor Yellow; $id++; } -$selected = [int](Read-Host); -if (($selected -gt $distroList.Length) -or ($selected -le 0)) -{ - Write-Error "Invalid selection. Select a distro from 1 to $($distroList.Length)"; - Exit 1; +$distroList = @($distros | ForEach-Object { $_.NAME }); + +# Interactive mode if no parameters +if ([string]::IsNullOrEmpty($Distro)) { + Write-Host "Select distro to move:" -ForegroundColor Yellow; + $id = 0; + $distros | ForEach-Object { + $defaultMark = if ($_.SELECTED -eq '*') { " (default)" } else { "" } + Write-Host " $($id+1): $($_.NAME)$defaultMark [WSL$($_.VERSION), $($_.STATE)]" -ForegroundColor White + $id++ + } + + $selected = [int](Read-Host "Enter number"); + if (($selected -gt $distroList.Length) -or ($selected -le 0)) { + Write-Error "Invalid selection. Select a distro from 1 to $($distroList.Length)"; + Exit 1; + } + $distro = $distroList[$selected - 1]; + $selectedIndex = $selected - 1; +} +else { + if ($distroList -notcontains $Distro) { + Write-Error "Distro '$Distro' not found. Available: $($distroList -join ', ')"; + Exit 1; + } + $distro = $Distro; + $selectedIndex = [array]::IndexOf($distroList, $distro); } -$distro = $distroList[$selected - 1]; -# get target directory -Write-Host 'Enter WSL target directory:'; -$targetFolder = Read-Host; +# Check if this distro is the default (Fix #29) +$isDefault = $distros[$selectedIndex].SELECTED -eq '*' +if ($isDefault) { + Write-Host "Note: '$distro' is your default WSL distro. This will be preserved." -ForegroundColor Cyan +} -# check if the target folder is the root of a drive -if ($targetFolder.Length -le 3 -and $targetFolder.EndsWith(':\')) -{ - Write-Error 'Target folder cannot be root of a drive'; +# Get target directory +if ([string]::IsNullOrEmpty($Target)) { + Write-Host "Enter target directory:" -ForegroundColor Yellow; + $targetFolder = Read-Host; +} +else { + $targetFolder = $Target; +} + +# Validate target folder +if ($targetFolder.Length -le 3 -and $targetFolder.EndsWith(':\')) { + Write-Error 'Target folder cannot be root of a drive (e.g., D:\). Use a subfolder.'; Exit 1; } -$targetFolder = $targetFolder.trimend('\'); +$targetFolder = $targetFolder.TrimEnd('\'); + +# Check for NTFS compression (Fix #23) +if (Test-FolderCompressed $targetFolder) { + Write-Host "" + Write-Warning "Target folder has NTFS compression enabled!" + Write-Host " This can corrupt WSL images and cause data loss." -ForegroundColor Red + Write-Host " To disable: Right-click folder > Properties > Advanced > Uncheck 'Compress contents'" -ForegroundColor Yellow + + if (-not $Force) { + Write-Error "Operation aborted. Use -Force to override (not recommended)."; + Exit 1; + } + Write-Host " Proceeding anyway due to -Force flag..." -ForegroundColor Yellow +} -# confirm -$confirm = Read-Host "Move $($distro) to `"$($targetFolder)`"? (Y|n)"; -if ($confirm -ne 'Y') -{ - Write-Error 'User canceled'; - Exit 1; +# Confirm +if (-not $Force) { + $confirm = Read-Host "Move '$distro' to "$targetFolder"? (Y/n)"; + if ($confirm -ne 'Y' -and $confirm -ne 'y' -and $confirm -ne '') { + Write-Host 'Operation cancelled by user.' -ForegroundColor Yellow; + Exit 0; + } } # Create target dir if non existent -if (-not(Test-Path $targetFolder)) -{ +if (-not(Test-Path $targetFolder)) { + Write-Host "Creating target folder..." -ForegroundColor Gray New-Item -Path $targetFolder -ItemType 'directory' | Out-Null; - if (-not($?)) - { - Write-Error "Failed to create target folder `"$($targetFolder)`""; + if (-not($?)) { + Write-Error "Failed to create target folder "$targetFolder""; Exit 1; } -} elseif (Test-Path (-join($targetFolder, "\ext4.vhdx"))) -{ +} +elseif (Test-Path ( -join ($targetFolder, "\ext4.vhdx"))) { Write-Error "Target folder already contains an ext4.vhdx file that will get overwritten. Aborting."; Exit 1; } +# Shutdown WSL to release file locks (Fix #30 and #35) +if (-not $NoShutdown) { + Write-Host "Shutting down WSL to release file locks..." -ForegroundColor Yellow + wsl --shutdown 2>&1 | Out-Null + Start-Sleep -Seconds 2 + Write-Host " WSL shutdown complete." -ForegroundColor Green +} + # Export WSL image to tar file $tempFile = Join-Path $targetFolder "$($distro).tar"; -Write-Host "Exporting VHDX to `"$($tempFile)`" ..."; -& cmd /c wsl --export $distro "`"$tempFile`""; -if (-not($? -and (Test-Path $tempFile -PathType Leaf))) -{ - Write-Error "ERROR: Export failed"; +Write-Host "Exporting '$distro' to "$tempFile"..." -ForegroundColor Yellow; +Write-Host " This may take several minutes depending on distro size..." -ForegroundColor Gray + +& cmd /c wsl --export $distro ""$tempFile""; +if (-not($? -and (Test-Path $tempFile -PathType Leaf))) { + Write-Error "Export failed. Check if the distro is healthy with 'wsl -l -v'"; Cleanup; Exit 2; } +$tarSize = [math]::Round((Get-Item $tempFile).Length / 1MB, 2) +Write-Host " Export complete! ($tarSize MB)" -ForegroundColor Green + # Unregister WSL so we can register it again at new location -Write-Host "Unregistering WSL ..." +Write-Host "Unregistering old location..." -ForegroundColor Yellow & cmd /c wsl --unregister $distro | Out-Null # Importing WSL at new location -Write-Host "Importing $distro from $targetFolder..." -& cmd /c wsl --import $distro $targetFolder "`"$tempFile`"" --version $distros[$selected -1].VERSION; +Write-Host "Importing '$distro' to new location..." -ForegroundColor Yellow +& cmd /c wsl --import $distro $targetFolder ""$tempFile"" --version $distros[$selectedIndex].VERSION; # Validating +Write-Host "Validating import..." -ForegroundColor Gray $newDistros = @(Get-Distros); $newDistroList = @($newDistros | ForEach-Object { $_.NAME }); -if ($newDistroList -notcontains $distro) -{ - Write-Error "Import failed. Distro not found. Export file at $tempFile"; + +if ($newDistroList -notcontains $distro) { + Write-Error "Import failed! Distro not found after import. Export file preserved at: $tempFile"; Exit 3; } -if (-not(Test-Path "$($targetFolder)\ext4.vhdx") -And -not(Test-Path "$($targetFolder)\rootfs")) -{ - Write-Error "ERROR: Import failed. Target file/folder not found. Export file at $tempFile"; +if (-not(Test-Path "$($targetFolder)\ext4.vhdx") -And -not(Test-Path "$($targetFolder)\rootfs")) { + Write-Error "Import failed! Target file/folder not found. Export file preserved at: $tempFile"; Exit 4; } +# Restore default distro if it was default (Fix #29) +if ($isDefault) { + Write-Host "Restoring default distro setting..." -ForegroundColor Gray + wsl --set-default $distro 2>&1 | Out-Null +} + Cleanup; -Write-Host "Done!" -ForegroundColor Green; +Write-Host "Done! '$distro' has been moved to '$targetFolder'" -ForegroundColor Green; + +if ($isDefault) { + Write-Host "Default distro setting preserved." -ForegroundColor Green +} + +Write-Host "Tip: If your default user changed to root, add this to /etc/wsl.conf:" -ForegroundColor Gray +Write-Host " [user]" -ForegroundColor Gray +Write-Host " default=YOUR_USERNAME" -ForegroundColor Gray \ No newline at end of file