613 lines
24 KiB
PowerShell
613 lines
24 KiB
PowerShell
#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
|