#Requires -Version 5 # OpenClacky Windows Installation Script # # Usage (standard): # powershell -c "irm https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.ps1 | iex" # # Usage (white-label / custom brand): # powershell -c "& ([scriptblock]::Create((irm 'https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.ps1'))) -BrandName 'MyCLI' -CommandName 'mycli'" # # Parameters: # -BrandName Display name shown in prompts (default: OpenClacky) # -CommandName CLI command name after install (default: openclacky) # -Region CDN region: china (default) or global (use global for non-China networks) # # WSL1 is preferred (shares Windows network stack — no mirrored networking needed). # If WSL1 import fails, the script falls back to WSL2 with mirrored networking. # If WSL is not installed at all, the script enables it and asks you to reboot. # After rebooting, run the same command again to complete installation. # # Development: .\install.ps1 -Local # Uses install.sh from the same directory as this script instead of CDN. param( [switch]$Local, [string]$BrandName = "", [string]$CommandName = "", [ValidateSet("china", "global")] [string]$Region = "china" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $env:WSL_UTF8 = "1" $global:DisplayName = if ($BrandName) { $BrandName } else { "OpenClacky" } $global:DisplayCmd = if ($CommandName) { $CommandName } else { "openclacky" } Write-Host "==> Region: $Region" $CLACKY_CDN_BASE_URL = if ($Region -eq "global") { "https://sg.oss.1024code.com" } else { "https://oss.1024code.com" } $CLACKY_INSTALL_SCRIPT_BASE_URL = if ($Region -eq "global") { "https://raw.githubusercontent.com" } else { "https://oss.1024code.com" } $CLACKY_CDN_PRIMARY_HOST = if ($Region -eq "global") { "sg.oss.1024code.com" } else { "oss.1024code.com" } Write-Host "==> CDN_BASE_URL: $CLACKY_CDN_BASE_URL" Write-Host "==> INSTALL_SCRIPT_BASE_URL: $CLACKY_INSTALL_SCRIPT_BASE_URL" Write-Host "==> CDN_PRIMARY_HOST: $CLACKY_CDN_PRIMARY_HOST" $CLACKY_CDN_BACKUP_HOST = "clackyai-1258723534.cos.ap-guangzhou.myqcloud.com" $INSTALL_PS1_COMMAND = "powershell -c `"irm $CLACKY_INSTALL_SCRIPT_BASE_URL/clacky-ai/openclacky/main/scripts/install.ps1 | iex`"" $INSTALL_SCRIPT_URL = "$CLACKY_INSTALL_SCRIPT_BASE_URL/clacky-ai/openclacky/main/scripts/install.sh" $UBUNTU_WSL_AMD64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz" $UBUNTU_WSL_AMD64_SHA256_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz.sha256" $UBUNTU_WSL_ARM64_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz" $UBUNTU_WSL_ARM64_SHA256_URL = "$CLACKY_CDN_BASE_URL/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz.sha256" $WSL_UPDATE_URL_X64 = "$CLACKY_CDN_BASE_URL/wsl.2.6.3.0.x64.msi" # Windows x64 (Win10+Win11) $WSL_UPDATE_URL_ARM64 = "$CLACKY_CDN_BASE_URL/wsl.2.6.3.0.arm64.msi" # Windows ARM64 $UBUNTU_WSL_DIR = "$env:SystemDrive\WSL\Ubuntu" # =========================================================================== # Shared Helpers # =========================================================================== function Write-Info { param($msg) Write-Host " [i] $msg" -ForegroundColor Cyan } function Write-Success { param($msg) Write-Host " [+] $msg" -ForegroundColor Green } function Write-Warn { param($msg) Write-Host " [!] $msg" -ForegroundColor Yellow } function Write-Fail { param($msg) Write-Host " [x] $msg" -ForegroundColor Red } function Write-Step { param($msg) Write-Host "`n==> $msg" -ForegroundColor Blue } # Exit codes (consumed by GUI installer to map to localized error messages). $EXIT_OK = 0 $EXIT_GENERIC_ERROR = 1 $EXIT_NETWORK_ERROR = 2 $EXIT_DISK_ERROR = 3 $EXIT_UNSUPPORTED_OS = 4 $EXIT_NOT_ADMIN = 5 $EXIT_REBOOT_REQUIRED = 6 $EXIT_WSL_ERROR = 7 function Test-IsAdmin { return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-SafeTempDir { # Use [IO.Path]::GetTempPath() instead of $env:TEMP. # $env:TEMP can return a short (8.3) path (e.g. C:\Users\USERNA~1\AppData\Local\Temp) # on systems where the user profile path contains spaces or non-ASCII characters, # which can break tools that don't handle 8.3 names correctly. # [IO.Path]::GetTempPath() always returns the full long path. $tempDir = [IO.Path]::GetTempPath().TrimEnd("\", "/") return $tempDir } # Robust file download: try curl first (shows progress), fall back to # Invoke-WebRequest. Returns $true on success, $false on failure. function Invoke-Download { param([string]$Url, [string]$OutFile) $urls = @($Url) try { if (([Uri]$Url).Host -eq $CLACKY_CDN_PRIMARY_HOST) { $urls += ([Uri]$Url).AbsoluteUri.Replace($CLACKY_CDN_PRIMARY_HOST, $CLACKY_CDN_BACKUP_HOST) } } catch {} foreach ($u in $urls) { if ($u -ne $Url) { Write-Warn "Primary download failed, retrying with backup mirror." } try { curl.exe -L --fail --progress-bar $u -o $OutFile if ($LASTEXITCODE -eq 0) { return $true } } catch {} try { Invoke-WebRequest -Uri $u -OutFile $OutFile -UseBasicParsing -TimeoutSec 60 return $true } catch {} } return $false } # Verify SHA256 of a local file against a remote .sha256 file. # Returns $true on match, or if the checksum file cannot be fetched (non-fatal). function Test-Sha256 { param([string]$FilePath, [string]$Sha256Url) $safeTemp = Get-SafeTempDir $sha256File = "$safeTemp\download.sha256" try { if (-not (Invoke-Download -Url $Sha256Url -OutFile $sha256File)) { Write-Warn "Could not download checksum file — skipping verification." return $true } $expectedLine = (Get-Content $sha256File -Raw).Trim() $expected = ($expectedLine -split '\s+')[0].ToLower() $actual = (Get-FileHash -Algorithm SHA256 -Path $FilePath).Hash.ToLower() if ($actual -ne $expected) { Write-Fail "Checksum mismatch!" Write-Fail " Expected : $expected" Write-Fail " Got : $actual" return $false } Write-Success "Checksum OK." return $true } finally { Remove-Item -Force -ErrorAction SilentlyContinue $sha256File } } # Use cmd.exe to avoid PS5 NativeCommandError and UTF-16LE mojibake on stderr. # exit 1 = WSL feature not enabled; exit 0 = WSL is functional. # Timeout 10s to avoid hanging when WSL is partially initialised. function Invoke-WslStatusExitCode { # Returns the exit code of `wsl --status`. # exit 0 = WSL fully enabled (Win11 / distro installed) # exit 1 = WSL feature not enabled # exit -1 = WSL enabled, no distro installed (Win10 wsl --list) # exit -444 = WSL enabled, no distro installed (Win10 wsl --status) # Only exit 1 means "WSL is not set up". All other codes mean WSL is functional. # Timeout (10s) is treated as exit 1 (WSL completely unresponsive). # # Note: Start-Process + -Redirect* loses ExitCode. Use System.Diagnostics.Process directly. $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "wsl.exe" $psi.Arguments = "--status" $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true try { $p = [System.Diagnostics.Process]::Start($psi) } catch { return 1 } $finished = $p.WaitForExit(10000) # 10 seconds if (-not $finished) { $p.Kill() Write-Info "WSL --status timed out (WSL not ready)." return 1 } return $p.ExitCode } # Returns $true if a distro named exactly "Ubuntu" is registered. # wsl --list outputs UTF-16LE regardless of WSL_UTF8; temporarily clear it and switch # OutputEncoding to Unicode so the output decodes correctly. function Test-UbuntuInstalled { $prevEnc = [Console]::OutputEncoding $prevUtf8 = $env:WSL_UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::Unicode $env:WSL_UTF8 = $null try { $out = (wsl.exe --list --quiet 2>$null) -join "`n" } finally { [Console]::OutputEncoding = $prevEnc $env:WSL_UTF8 = $prevUtf8 } # Whole-line match to avoid false positives from Ubuntu-22.04, Ubuntu-24.04, etc. return ($out -match '(?im)^Ubuntu\s*$') } # Returns 'arm64' or 'amd64' function Get-CpuArch { $arch = (Get-CimInstance Win32_Processor).Architecture # 12 = ARM64 if ($arch -eq 12) { return "arm64" } return "amd64" } function Prompt-Reboot { Write-Host "" Write-Warn "Please restart your computer." Write-Warn "After restarting, run the same command again:" Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow Write-Host "" Read-Host "Press Enter to exit" exit $EXIT_REBOOT_REQUIRED } # Download Ubuntu rootfs and verify checksum. Returns local tar path. function Get-UbuntuRootfs { $cpuArch = Get-CpuArch Write-Info "CPU architecture: $cpuArch" if ($cpuArch -eq "arm64") { $wslUrl = $UBUNTU_WSL_ARM64_URL $sha256Url = $UBUNTU_WSL_ARM64_SHA256_URL } else { $wslUrl = $UBUNTU_WSL_AMD64_URL $sha256Url = $UBUNTU_WSL_AMD64_SHA256_URL } $safeTemp = Get-SafeTempDir $tarPath = "$safeTemp\ubuntu-wsl-$cpuArch.tar.gz" $installDir = $UBUNTU_WSL_DIR # Disk space check (~4 GB needed: 350 MB download + ~1.5 GB imported + buffer) $drive = Split-Path -Qualifier $installDir $freeBytes = (Get-PSDrive ($drive.TrimEnd(':'))).Free if ($freeBytes -lt 4GB) { Write-Fail "Not enough disk space on $drive." Write-Fail " Available : $([math]::Round($freeBytes / 1GB, 1)) GB" Write-Fail " Required : ~4 GB" exit $EXIT_DISK_ERROR } # Check if a valid cached tarball exists (skip download if checksum passes) $needDownload = $true if (Test-Path $tarPath) { Write-Info "Found cached Ubuntu rootfs, verifying checksum..." if (Test-Sha256 -FilePath $tarPath -Sha256Url $sha256Url) { Write-Success "Cache valid — skipping download." $needDownload = $false } else { Write-Warn "Cache corrupted — re-downloading..." Remove-Item -Force $tarPath } } try { if ($needDownload) { Write-Step "Downloading Ubuntu rootfs (~350 MB)..." if (-not (Invoke-Download -Url $wslUrl -OutFile $tarPath)) { Write-Fail "Failed to download Ubuntu rootfs. Check your network and try again." exit $EXIT_NETWORK_ERROR } Write-Success "Download complete." Write-Step "Verifying checksum..." if (-not (Test-Sha256 -FilePath $tarPath -Sha256Url $sha256Url)) { Write-Fail "The downloaded file is corrupted. Please try again." exit $EXIT_NETWORK_ERROR } } return $tarPath } finally { # Keep the tarball as cache for future runs (e.g. after reboot) if (Test-Path $tarPath) { Write-Info "Keeping Ubuntu rootfs cache at $tarPath for future use." } } } # Import Ubuntu rootfs into WSL. # $WslVersion: 1 or 2 function Install-UbuntuRootfs { param([int]$WslVersion, [string]$TarPath = "") if (-not $TarPath) { $TarPath = Get-UbuntuRootfs } Write-Step "Importing Ubuntu into WSL$WslVersion (this may take a minute)..." New-Item -ItemType Directory -Force -Path $UBUNTU_WSL_DIR | Out-Null $wslOutput = wsl.exe --import Ubuntu $UBUNTU_WSL_DIR $TarPath --version $WslVersion 2>&1 if ($LASTEXITCODE -ne 0) { Write-Fail "wsl --import failed (exit $LASTEXITCODE)." if ($wslOutput) { Write-Fail "$wslOutput" } Write-Fail "Try removing $UBUNTU_WSL_DIR and running the script again." exit $EXIT_WSL_ERROR } Write-Success "Ubuntu (WSL$WslVersion) imported successfully." } function Test-WslNetwork { Write-Info "Checking WSL network connectivity..." wsl.exe -d Ubuntu -u root -- bash -c "curl -fsSL --max-time 3 --retry 1 $INSTALL_SCRIPT_URL -o /dev/null 2>/dev/null" | Out-Null if ($LASTEXITCODE -ne 0) { Write-Fail "WSL cannot reach $CLACKY_CDN_PRIMARY_HOST (curl exit $LASTEXITCODE)." Write-Fail "Please fix the network inside WSL and re-run this installer." exit $EXIT_NETWORK_ERROR } Write-Success "WSL network OK." } # Install OpenClacky inside the Ubuntu WSL distro. function Run-InstallInWsl { Write-Step "Installing $DisplayName inside WSL..." if ($Local) { # Convert Windows path to WSL path (e.g. C:\foo\bar -> /mnt/c/foo/bar) $scriptDir = Split-Path -Parent $MyInvocation.PSCommandPath $localScript = Join-Path $scriptDir "install.sh" if (-not (Test-Path $localScript)) { Write-Fail "Local mode: install.sh not found at $localScript" exit $EXIT_GENERIC_ERROR } $wslPath = ($localScript -replace '\', '/') -replace '^([A-Za-z]):', { '/mnt/' + $args[0].Groups[1].Value.ToLower() } Write-Info "Local mode: using $wslPath" wsl.exe -d Ubuntu -u root -- bash $wslPath --brand-name=$BrandName --command=$CommandName } else { Test-WslNetwork wsl.exe -d Ubuntu -u root -- bash -c "cd ~ && curl -fsSL $INSTALL_SCRIPT_URL | bash -s -- --brand-name=$BrandName --command=$CommandName" } if ($LASTEXITCODE -eq 2) { exit $EXIT_NETWORK_ERROR } if ($LASTEXITCODE -ne 0) { Write-Fail "Installation failed inside WSL (exit $LASTEXITCODE)." Write-Fail "You can retry manually:" Write-Host " wsl -d Ubuntu -u root -- bash -c `"curl -fsSL $INSTALL_SCRIPT_URL | bash`"" -ForegroundColor Yellow exit $EXIT_GENERIC_ERROR } } # Configure WSL2 mirrored networking so WSL can reach Windows localhost ports # (e.g. Chrome/Edge remote debugging on 127.0.0.1:9222). # Requires Win11 22H2+ (Build >= 22621). Silently skips on older Windows. function Set-Wsl2MirroredNetworking { try { $build = [System.Environment]::OSVersion.Version.Build if ($build -lt 22621) { Write-Warn "Windows Build $build < 22621, skipping WSL2 mirrored networking." return } $wslConfig = "$env:USERPROFILE\.wslconfig" $content = if (Test-Path $wslConfig) { Get-Content $wslConfig -Raw } else { "" } if ($content -match "networkingMode\s*=\s*mirrored") { Write-Info "WSL2 mirrored networking already configured." return } Write-Step "Configuring WSL2 mirrored networking..." Add-Content $wslConfig "`n[wsl2]`nnetworkingMode=mirrored" wsl.exe --shutdown Write-Success "WSL2 mirrored networking enabled." } catch { Write-Warn "Failed to configure WSL2 mirrored networking: $_" } } function Show-PostInstall { param([int]$WslVersion) Write-Host "" Write-Success "$DisplayName installed successfully (WSL$WslVersion)." Write-Host "" Write-Info "To use $DisplayName, first enter WSL:" Write-Host " wsl" -ForegroundColor Green Write-Host "" Write-Info "Then run ${DisplayName}:" Write-Host " $DisplayCmd" -ForegroundColor Green Write-Host "" Write-Info "Or start the Web UI:" Write-Host " $DisplayCmd server" -ForegroundColor Green Write-Host " Then open http://localhost:7070 in your browser" Write-Host "" } # =========================================================================== # Registry helpers (HKCU:\Software\OpenClacky\Install) # =========================================================================== $REG_ROOT = "HKCU:\Software\OpenClacky\Install" function Get-InstallReg { param([string]$Name, $Default = $null) try { $val = (Get-ItemProperty -Path $REG_ROOT -Name $Name -ErrorAction Stop).$Name return $val } catch { return $Default } } function Set-InstallReg { param([string]$Name, $Value) if (-not (Test-Path $REG_ROOT)) { New-Item -Path $REG_ROOT -Force | Out-Null } Set-ItemProperty -Path $REG_ROOT -Name $Name -Value $Value } function Remove-InstallReg { param([string]$Name) try { Remove-ItemProperty -Path $REG_ROOT -Name $Name -ErrorAction Stop } catch {} } # =========================================================================== # WSL2 Path — preferred, requires hardware virtualisation # =========================================================================== # Returns $true if WSL2 can import the real Ubuntu rootfs. function Test-VirtualisationSupported { param([string]$TarPath) Write-Info "Probing WSL2 availability..." $safeTemp = Get-SafeTempDir $probeName = "Wsl2Probe-$([guid]::NewGuid().ToString('N'))" $probeDir = "$safeTemp\$probeName" $ok = $false try { New-Item -ItemType Directory -Force -Path $probeDir | Out-Null Write-Info "[probe] Running: wsl --import $probeName $probeDir $TarPath --version 2" $probeOutput = wsl.exe --import $probeName $probeDir $TarPath --version 2 2>&1 if ($probeOutput) { Write-Info "WSL2 probe: $probeOutput" } $importExit = $LASTEXITCODE Write-Info "[probe] wsl --import exit code: $importExit" $ok = ($importExit -eq 0) } catch { Write-Info "[probe] Exception caught: $_" $ok = $false } finally { wsl.exe --unregister $probeName 2>$null | Out-Null Write-Info "[probe] $probeName unregistered." Remove-Item -Force -Recurse -ErrorAction SilentlyContinue $probeDir } Write-Info "[probe] Final result: ok=$ok" if ($ok) { Write-Info "WSL2 probe passed — using WSL2." } else { Write-Info "WSL2 probe failed (Hyper-V not available)." } return $ok } # Download and install the WSL2 kernel MSI from our CDN. function Install-WslKernel { param([switch]$Repair) $cpuArch = Get-CpuArch # Select the correct MSI for this CPU architecture. if ($cpuArch -eq "arm64") { $url = $WSL_UPDATE_URL_ARM64 } else { $url = $WSL_UPDATE_URL_X64 } $safeTemp = Get-SafeTempDir $msiPath = "$safeTemp\wsl_update.msi" Write-Step "Downloading WSL kernel update ($cpuArch)..." if (-not (Invoke-Download -Url $url -OutFile $msiPath)) { Write-Fail "Failed to download WSL kernel update. Check your network and try again." exit $EXIT_NETWORK_ERROR } if ($Repair) { # /fa = force repair all files; handles corrupt/partial installs where /i silently no-ops. Start-Process msiexec -Wait -ArgumentList "/fa", $msiPath, "/quiet", "/norestart" Remove-Item -Force -ErrorAction SilentlyContinue $msiPath Write-Success "WSL kernel repair attempted." return } Write-Info "Installing WSL kernel..." Start-Process msiexec -Wait -ArgumentList "/i", $msiPath, "/quiet", "/norestart" Write-Success "WSL kernel installed." Remove-Item -Force -ErrorAction SilentlyContinue $msiPath } # Enable WSL + VirtualMachinePlatform features, install kernel MSI, then reboot. function Enable-WslFeatures { Write-Step "Enabling WSL components..." dism /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null dism /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null Write-Success "WSL components enabled." Install-WslKernel Set-InstallReg -Name "WslFeaturesEnabled" -Value "1" Set-InstallReg -Name "InstallPhase" -Value "wsl-pending" Prompt-Reboot } # =========================================================================== # Main # =========================================================================== Write-Host "" Write-Host "$DisplayName Installation Script (Windows)" -ForegroundColor Cyan Write-Host "" if (-not (Test-IsAdmin)) { Write-Fail "Please re-run this script as Administrator:" Write-Host "" Write-Host " Right-click PowerShell -> 'Run as administrator', then:" -ForegroundColor Yellow Write-Host " $INSTALL_PS1_COMMAND" -ForegroundColor Yellow exit $EXIT_NOT_ADMIN } # Check minimum Windows version: WSL1 requires Build 16215 (Win10 1709). $osBuild = [System.Environment]::OSVersion.Version.Build if ($osBuild -lt 16215) { Write-Fail "Unsupported Windows version (Build $osBuild)." Write-Fail "WSL requires Windows 10 Build 16215 (version 1709) or later." Write-Fail "Please update Windows and try again." exit $EXIT_UNSUPPORTED_OS } Write-Info "Windows Build $osBuild — OK." # Step 1: Ensure WSL feature is enabled (same for WSL1 and WSL2) Write-Step "Checking WSL status..." $wslCode = Invoke-WslStatusExitCode Write-Info "WSL --status exit code: $wslCode" $installPhase = Get-InstallReg -Name "InstallPhase" -Default "" $wslFeaturesEnabled = Get-InstallReg -Name "WslFeaturesEnabled" -Default "" $kernelRepairAttempted = Get-InstallReg -Name "KernelRepairAttempted" -Default "" Write-Info "InstallPhase: '$installPhase'" Write-Info "WslFeaturesEnabled: '$wslFeaturesEnabled'" Write-Info "KernelRepairAttempted: '$kernelRepairAttempted'" if ($installPhase -eq "" -and $wslCode -ne 0) { # First run and WSL not ready: enable WSL features and reboot. Enable-WslFeatures # Always exits (prompts reboot) } # phase == wsl-pending + code 1: reboot happened but WSL still not ready; try kernel repair once. if ($installPhase -eq "wsl-pending" -and $wslCode -eq 1) { if ($kernelRepairAttempted -ne "1") { Write-Warn "WSL features were enabled but WSL is still not ready. Attempting kernel repair..." Set-InstallReg -Name "KernelRepairAttempted" -Value "1" Install-WslKernel -Repair Prompt-Reboot } else { Write-Warn "WSL kernel repair was already attempted but WSL is still not ready." exit $EXIT_WSL_ERROR } } # wslCode != 1 (0, -1, -444, 50, etc.): WSL is functional, continue. Remove-InstallReg -Name "InstallPhase" # Step 2: Install Ubuntu, preferring WSL1 (shares Windows network — no mirrored needed). # If WSL1 import fails, fall back to WSL2. # If the distro already exists, keep whatever version was previously installed. if (Test-UbuntuInstalled) { Write-Info "Ubuntu (WSL) already installed — skipping import." $wslVersion = Get-InstallReg -Name "WslVersion" -Default 2 } else { $tarPath = Get-UbuntuRootfs # Try WSL1 first Write-Info "Attempting WSL1 import..." $wsl1Ok = $false try { New-Item -ItemType Directory -Force -Path $UBUNTU_WSL_DIR | Out-Null wsl.exe --import Ubuntu $UBUNTU_WSL_DIR $tarPath --version 1 >$null 2>$null $wsl1Ok = ($LASTEXITCODE -eq 0) } catch { $wsl1Ok = $false } if ($wsl1Ok) { Write-Success "Ubuntu (WSL1) imported successfully." $wslVersion = 1 } else { # Clean up failed WSL1 attempt wsl.exe --unregister Ubuntu 2>$null | Out-Null Remove-Item -Force -Recurse -ErrorAction SilentlyContinue $UBUNTU_WSL_DIR Write-Info "WSL1 import failed, trying WSL2..." if (Test-VirtualisationSupported -TarPath $tarPath) { wsl.exe --set-default-version 2 >$null 2>$null Install-UbuntuRootfs -WslVersion 2 -TarPath $tarPath $wslVersion = 2 } else { if ($wslFeaturesEnabled -ne "1") { Write-Warn "Neither WSL1 nor WSL2 is available. Enabling WSL components..." Enable-WslFeatures # Always exits (prompts reboot) } Write-Fail "Failed to import Ubuntu into both WSL1 and WSL2." Write-Fail "Please ensure Windows Subsystem for Linux is enabled and try again." exit $EXIT_WSL_ERROR } } } Write-Success "WSL is ready." Run-InstallInWsl # For WSL2, configure mirrored networking AFTER install.sh succeeds (NAT is more # reliable for outbound traffic during installation). The shutdown here is safe # because installation is already complete. if ($wslVersion -eq 2) { Set-Wsl2MirroredNetworking } Set-InstallReg -Name "WslVersion" -Value $wslVersion Show-PostInstall -WslVersion $wslVersion