wsl2compact.ps1
|
<#PSScriptInfo
.VERSION 1.0.15 .GUID 0bdb28b7-a42d-433c-ac30-eaabd48eae63 .AUTHOR hyperphantasia .COMPANYNAME .COPYRIGHT (c) 2026 hyperphantasia. All rights reserved. .DESCRIPTION Save disk space on WSL2! PowerShell script that helps compacting the distro's ext4.vhdx and reclaim some hard drive space (using Optimize-VHD and diskpart as fallback). More on : https://github.com/hyperphantasia/WSL-VHDX-Compact .TAGS WSL WSL2 WindowsSubsystemForLinux VHDX vhd ext4 linux Compact DiskCleanup Hyper-V powershell disk-cleanup Optimize-VHD diskpart fstrim virtualization windows disk hdd space disk-management storage-management .LICENSEURI https://unlicense.org/ .PROJECTURI https://github.com/hyperphantasia/WSL-VHDX-Compact .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .SYNOPSIS Compact a WSL2 ext4.vhdx the automated way. .DESCRIPTION 1. Enumerates installed WSL2 distros via registry. 2. Distro(s) selection. 3. Resolves the distro BasePath from the registry. 4. Trims free space inside WSL. 5. Shuts down WSL. 6. Compacts ext4.vhdx using Optimize-VHD. Falls back to diskpart. 7. Report summary. .PARAMETER DistroName One or more WSL distro names to compact. Skips the interactive menu and confirmation prompt. .PARAMETER All Compact every registered WSL distro sequentially. Skips the interactive menu and confirmation prompt. .NOTES Must run as Administrator. On Windows Pro with Hyper-V, Optimize-VHD is preferred. On systems without the Hyper-V module, diskpart is used as fallback. .USAGE In an elevated terminal. Interactive: powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\wsl_compactor.ps1 Non-interactive: powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\wsl_compactor.ps1 -DistroName Ubuntu #> [CmdletBinding()] param( [string[]]$DistroName, [switch]$All ) #------------------------------------------------------------ # Step 0 - Admin check #------------------------------------------------------------ $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { throw "This script must be run as Administrator." } if ($All -and $DistroName) { throw "Specify either -DistroName or -All, not both." } #------------------------------------------------------------ # Step 1 - Enumerate distros from the registry #------------------------------------------------------------ $lxssKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss' # Grab all subkeys that have a DistributionName value $distros = Get-ChildItem $lxssKey | ForEach-Object { $props = Get-ItemProperty $_.PSPath [PSCustomObject]@{ Name = $props.DistributionName BasePath = $props.BasePath VhdFileName = $props.VhdFileName WSLVer = $props.Version } } if ($distros.Count -eq 0) { throw "No WSL distros found in the registry." } #------------------------------------------------------------ # Step 2 - Select distro(s) # -All -> every distro, non-interactive # -DistroName ... -> one or more named distros, non-interactive # (e.g. -DistroName Ubuntu, Debian) # no args, 1 distro -> that distro, still confirms # no args, >1 distro -> menu, pick a number OR "A" for all #------------------------------------------------------------ $nonInteractive = $false if ($All) { $selectedList = @($distros) $nonInteractive = $true } elseif ($DistroName) { $selectedList = foreach ($name in $DistroName) { $match = $distros | Where-Object { $_.Name -ieq $name } | Select-Object -First 1 if (-not $match) { $available = ($distros | Select-Object -ExpandProperty Name) -join ', ' throw "Distro '$name' was not found. Available distros: $available" } $match } $nonInteractive = $true } elseif ($distros.Count -gt 1) { Write-Host "Multiple distros detected. Please choose which one(s) to compact:`n" -ForegroundColor Cyan for ($i = 0; $i -lt $distros.Count; $i++) { Write-Host ("[{0}] {1} (WSL{2})" -f ($i + 1), $distros[$i].Name, $distros[$i].WSLVer) } Write-Host "[A] All distros" do { $choice = Read-Host "`nEnter a number (1-$($distros.Count)), or 'A' to compact all" if ($choice -match '^(a|all)$') { $selectedList = @($distros) $valid = $true } else { $valid = ($choice -as [int]) -and ($choice -ge 1) -and ($choice -le $distros.Count) if ($valid) { $selectedList = @($distros[[int]$choice - 1]) } else { Write-Warning "Please enter a valid integer between 1 and $($distros.Count), or 'A' for all." } } } until ($valid) } else { $selectedList = @($distros[$distros.Count - 1]) } #------------------------------------------------------------ # Step 3 - Resolve BasePath & vhdx for each selected distro #------------------------------------------------------------ $jobs = foreach ($sel in $selectedList) { $basePath = $sel.BasePath if ($basePath -like '\\?\*') { $basePath = $basePath.Substring(4) } if (-not (Test-Path -LiteralPath $basePath)) { throw "BasePath '$basePath' does not exist on disk for distro '$($sel.Name)'." } $vhdx = $null if ($sel.VhdFileName) { $candidate = Join-Path $basePath $sel.VhdFileName if (Test-Path -LiteralPath $candidate -PathType Leaf) { $vhdx = $candidate } } if (-not $vhdx) { if ((Test-Path -LiteralPath $basePath -PathType Leaf) -and $basePath -match '\.vhdx$') { $vhdx = $basePath } else { $vhdx = @( Join-Path $basePath 'ext4.vhdx' Join-Path $basePath 'LocalState\ext4.vhdx' ) | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $vhdx) { $vhdx = Get-ChildItem -LiteralPath $basePath -Filter '*.vhdx' -File -ErrorAction SilentlyContinue | Sort-Object Length -Descending | Select-Object -First 1 -ExpandProperty FullName } } } if (-not $vhdx) { throw "No .vhdx file found at or under '$basePath' for distro '$($sel.Name)'." } [PSCustomObject]@{ Name = $sel.Name BasePath = $basePath Vhdx = $vhdx PrevSize = (Get-Item $vhdx).Length } } Write-Host "`nThe following distro(s) will be compacted:" -ForegroundColor Magenta foreach ($j in $jobs) { Write-Host (" - {0}" -f $j.Name) -ForegroundColor DarkYellow Write-Host (" BasePath : {0}" -f $j.BasePath) Write-Host (" VHDX file: {0}" -f $j.Vhdx) } Write-Host "" if (-not $nonInteractive) { Write-Host "Are you sure you want to proceed? (Y/N)" -ForegroundColor DarkCyan -NoNewline $answer = Read-Host if ($answer.ToUpper() -ne 'Y') { Write-Warning "Operation canceled." exit } } else { Write-Host "Non-interactive mode enabled (via -DistroName / -All)" -ForegroundColor Gray } #------------------------------------------------------------ # Step 4 - Optional fstrim (per distro, while each is still running) #------------------------------------------------------------ foreach ($j in $jobs) { Write-Host "`nPreparation: logical cleanup with fstrim to discard unused blocks on '$($j.Name)'..." -ForegroundColor Cyan & wsl.exe -d "$($j.Name)" -u root -- fstrim -av if ($LASTEXITCODE -ne 0) { Write-Warning "fstrim returned a non-zero exit code for '$($j.Name)'. Continuing anyway." } } #------------------------------------------------------------ # Step 5 - Shutdown WSL (once, shuts down all running distros) #------------------------------------------------------------ Write-Host "`nShutting down WSL..." -ForegroundColor Cyan & wsl.exe --shutdown if ($LASTEXITCODE -ne 0) { throw "Failed to shut down WSL." } #------------------------------------------------------------ # Step 6 - Hyper-V Optimize-VHD first, fallback to diskpart (per distro) #------------------------------------------------------------ $hypervAvailable = $false try { Import-Module Hyper-V -ErrorAction Stop $hypervAvailable = $true } catch { $hypervAvailable = $false } if (-not $hypervAvailable) { Write-Warning "Hyper-V module is not available. Compacting with diskpart." } foreach ($j in $jobs) { $optimized = $false if ($hypervAvailable) { try { Write-Host "`nCompacting: optimizing VHDX for '$($j.Name)' using Hyper-V Optimize-VHD..." -ForegroundColor Cyan Write-Host "This might take a while." -ForegroundColor Gray Optimize-VHD -Path $j.Vhdx -Mode Full -ErrorAction Stop $optimized = $true } catch { Write-Warning "Optimize-VHD failed for '$($j.Name)'. Falling back to diskpart." } } if (-not $optimized) { $dpScript = @" select vdisk file="$($j.Vhdx)" attach vdisk readonly compact vdisk detach vdisk exit "@ $tempFile = [IO.Path]::GetTempFileName() Set-Content -LiteralPath $tempFile -Value $dpScript -Encoding ASCII Write-Host "Running DiskPart to compact the VHDX for '$($j.Name)'..." -ForegroundColor Cyan $lastPct = -1 try { & diskpart /s $tempFile | ForEach-Object { if ($_ -match '(\d+)\s+percent') { $pct = [int]$Matches[1] if ($pct -ne $lastPct) { Write-Host "$pct% completed" $lastPct = $pct } } elseif ($_ -match '\S') { Write-Host $_ } } $optimized = $true } finally { Remove-Item $tempFile -ErrorAction SilentlyContinue } } $j | Add-Member -NotePropertyName Optimized -NotePropertyValue $optimized $j | Add-Member -NotePropertyName CurrentSize -NotePropertyValue (Get-Item $j.Vhdx).Length } #------------------------------------------------------------ # Step 7 - Report results #------------------------------------------------------------ Write-Host "`n==================== Summary ====================" -ForegroundColor Yellow $totalPrev = 0 $totalCurrent = 0 foreach ($j in $jobs) { $saved = $j.PrevSize - $j.CurrentSize $totalPrev += $j.PrevSize $totalCurrent += $j.CurrentSize Write-Host "`nDistro: $($j.Name)" -ForegroundColor DarkYellow Write-Host (" Previous VHDX size: {0:N0} bytes ({1:N2} GB)" -f $j.PrevSize, ($j.PrevSize / 1GB)) -ForegroundColor Gray Write-Host (" Current VHDX size: {0:N0} bytes ({1:N2} GB)" -f $j.CurrentSize, ($j.CurrentSize / 1GB)) -ForegroundColor Gray Write-Host (" Saved: {0:N0} bytes ({1:N2} GB)" -f $saved, ($saved / 1GB)) -ForegroundColor Green if (-not $j.Optimized) { Write-Warning " The compaction step did not complete successfully for '$($j.Name)'." } } if ($jobs.Count -gt 1) { $totalSaved = $totalPrev - $totalCurrent Write-Host "`n---------------- Total across all distros ----------------" -ForegroundColor Yellow Write-Host (" Total previous size: {0:N0} bytes ({1:N2} GB)" -f $totalPrev, ($totalPrev / 1GB)) -ForegroundColor Gray Write-Host (" Total current size: {0:N0} bytes ({1:N2} GB)" -f $totalCurrent, ($totalCurrent / 1GB)) -ForegroundColor Gray Write-Host (" Total saved: {0:N0} bytes ({1:N2} GB)" -f $totalSaved, ($totalSaved / 1GB)) -ForegroundColor Green } if ($jobs | Where-Object { -not $_.Optimized }) { Write-Warning "`nOne or more distros did not compact successfully. See warnings above." } else { Write-Host "`nDone." -ForegroundColor Green } |