# PowerShell bootstrap script for ZaroPGx # Clones (or optionally updates) the repository and launches the startup script # # Usage: # Quick install (bypasses execution policy): # iwr -useb https://raw.githubusercontent.com/Zaromics/ZaroPGx/main/bootstrap.ps1 | iex # # Or download and run (may require execution policy change): # powershell -ExecutionPolicy Bypass -File bootstrap.ps1 # # System Requirements: # - Windows 10 22H2 (build 19045) or Windows 11 22H2 (build 22631) or higher # - 16GB RAM, modern 64-bit processor ( with Second Level Address Translation (SLAT) ) # - Hardware virtualization enabled in BIOS/UEFI [CmdletBinding()] param( [string]$RepoUrl = "https://github.com/Zaromics/ZaroPGx.git", [string]$ReleaseVersion = "0.4.1", [string]$Branch = "", [string]$TargetDir = "ZaroPGx", [switch]$Update, [switch]$SkipDependencyCheck, [string[]]$MissingDeps = @() ) if ([string]::IsNullOrWhiteSpace($Branch)) { $Branch = "v$ReleaseVersion" } # Under `iwr | iex` there is no script path, and MyInvocation.ScriptBlock does # not reliably contain the downloaded script. The elevation path below # downloads the selected release's complete bootstrap instead. Write-Host "Bootstrapping ZaroPGx..." -ForegroundColor Green Write-Host "════════════════════════" -ForegroundColor Green # Check and set execution policy for this process (doesn't require admin) $currentPolicy = Get-ExecutionPolicy -Scope Process if ($currentPolicy -eq "Restricted" -or $currentPolicy -eq "AllSigned") { Write-Host "Setting execution policy to Bypass for this session..." -ForegroundColor Yellow try { Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force Write-Host " [OK] Execution policy set for this session" -ForegroundColor Green } catch { Write-Host " [WARNING] Could not set execution policy: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host " You may need to run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor Yellow } } # Function to check if running as administrator function Test-Administrator { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # iwr | iex feeds the script on stdin, so Read-Host can see the pipe instead of the keyboard. function Read-ConsoleLine { param([string]$Prompt) if (-not [Console]::IsInputRedirected) { return Read-Host $Prompt } Write-Host "$Prompt " -NoNewline return [Console]::ReadLine() } # Under `iwr | iex` the script runs in the caller's own PowerShell, and `exit` ends # it: a window opened just to run this closes before its last message can be read. # Every exit after a message the user must read comes through here, which waits for # Enter first when someone is at the console (not under -NonInteractive or with # input redirected, where Read-Host would fail or read the pipe). function Exit-Bootstrap { param([int]$Code = 1) if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) { try { [void](Read-Host "Press Enter to close") } catch { } } exit $Code } # Local changes to tracked files, which `git rm --cached -r .` + `git reset --hard` # would discard: the working copy's and the index's (staged, even when the working # copy was then reverted), each against HEAD. git diff compares content after # .gitattributes' eol rules, so a file whose only change is CRLF line endings is not # listed; git status lists it, as its size changed. core.safecrlf=false silences # git's "CRLF will be replaced by LF" warning for each such file. Throws when git # fails, so local changes cannot be ruled out. function Get-TrackedChanges { $workingCopy = & git -c core.safecrlf=false -C $TargetDir diff --name-status HEAD -- if ($LASTEXITCODE -ne 0) { throw "git diff failed" } $index = & git -c core.safecrlf=false -C $TargetDir diff --cached --name-status HEAD -- if ($LASTEXITCODE -ne 0) { throw "git diff --cached failed" } @($workingCopy) + @($index) | Where-Object { $_ } | Select-Object -Unique } # WSL feature install often cannot start until Windows reboots. function Test-RebootPending { $cbs = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" $wu = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" if (Test-Path $cbs) { return $true } if (Test-Path $wu) { return $true } return $false } function Write-RebootRequired { Write-Host "" Write-Host "Windows needs a reboot before WSL can start." -ForegroundColor Yellow Write-Host "Reboot, then run the same bootstrap command again." -ForegroundColor Yellow Write-Host "After that, the first Ubuntu launch asks you to create a user. That prompt is once." -ForegroundColor Gray Write-Host "" } # Helper: Get Windows build number (used to decide if wsl --install is supported) function Get-WindowsBuildNumber { try { $build = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').CurrentBuildNumber return [int]$build } catch { return 0 } } # Helper: Get WSL version (Docker Desktop requires WSL 2.1.5+) function Get-WSLVersion { try { $wslVersionOutput = wsl --version 2>&1 | Out-String # Remove null characters that cause spacing issues in UTF-16 output $wslVersionOutput = $wslVersionOutput -replace '\0', '' if ($LASTEXITCODE -eq 0 -and $wslVersionOutput -match "WSL\s*version:\s*(\d+\.\d+\.\d+(?:\.\d+)?)") { return [version]$matches[1] } # If wsl --version doesn't work, we're on inbox/legacy WSL (pre-2.0) return [version]"0.0.0" } catch { return [version]"0.0.0" } } # Helper: Check if any WSL distribution is installed function Test-WSLDistribution { try { $distros = wsl --list --quiet 2>&1 | Out-String $distros = $distros.Trim() return ($LASTEXITCODE -eq 0 -and $distros -ne "") } catch { return $false } } function Test-DockerComposeVersion { $minimum = [version]"2.24.0" try { $raw = (docker compose version --short 2>$null | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or $raw -notmatch "v?(\d+\.\d+\.\d+)") { return $false } $version = [version]$matches[1] if ($version -lt $minimum) { Write-Host " [WARNING] Docker Compose $version found; ZaroPGx requires $minimum or newer" -ForegroundColor Yellow return $false } Write-Host " [OK] Docker Compose $version found" -ForegroundColor Green return $true } catch { return $false } } # Function to install dependencies using available package manager function Install-Dependencies { param([string[]]$MissingDeps) Write-Host "" Write-Host "Missing dependencies detected: $($MissingDeps -join ', ')" -ForegroundColor Yellow Write-Host "" # Check for winget (Windows Package Manager) $wingetAvailable = Get-Command winget -ErrorAction SilentlyContinue $chocoAvailable = Get-Command choco -ErrorAction SilentlyContinue if (-not $wingetAvailable -and -not $chocoAvailable) { Write-Host "No package manager found (winget or chocolatey)." -ForegroundColor Red Write-Host "" Write-Host "Please install dependencies manually:" -ForegroundColor Yellow Write-Host " Git: https://git-scm.com/downloads" -ForegroundColor Cyan Write-Host " Docker Desktop: https://www.docker.com/products/docker-desktop" -ForegroundColor Cyan Write-Host "and/or WSL 2: https://learn.microsoft.com/en-us/windows/wsl/install" -ForegroundColor Cyan Write-Host "" return $false } $isElevatedInstaller = $SkipDependencyCheck -and $MissingDeps.Count -gt 0 if (-not $isElevatedInstaller) { $response = Read-ConsoleLine "Would you like to automatically install missing dependencies? (y/N)" if ($response -notmatch '^[Yy]') { Write-Host "Installation of missing dependencies is cancelled. Please re-run or install dependencies manually." -ForegroundColor Yellow return $false } } # Check if we need elevation if (-not (Test-Administrator)) { Write-Host "Administrator privileges required for installation." -ForegroundColor Yellow Write-Host "Restarting script with elevation..." -ForegroundColor Cyan # Try multiple methods to get script path $scriptPath = $null if ($PSCommandPath -and (Test-Path $PSCommandPath)) { $scriptPath = $PSCommandPath Write-Host " Using script path: $scriptPath" -ForegroundColor Gray } elseif ($MyInvocation.MyCommand.Path -and (Test-Path $MyInvocation.MyCommand.Path)) { $scriptPath = $MyInvocation.MyCommand.Path Write-Host " Using script path: $scriptPath" -ForegroundColor Gray } elseif ($MyInvocation.ScriptName -and (Test-Path $MyInvocation.ScriptName)) { $scriptPath = $MyInvocation.ScriptName Write-Host " Using script path: $scriptPath" -ForegroundColor Gray } # If script path not found or running from memory, create temp file if (-not $scriptPath) { Write-Host " Script is running from memory, creating temporary file..." -ForegroundColor Gray $tempScript = Join-Path $env:TEMP "zaropgx-bootstrap-temp.ps1" # A function's invocation metadata is not the complete piped script. $scriptContent = $null # Method 1: A physical script path was already selected above. # Method 2: If that didn't work, try reading from PSCommandPath anyway if (-not $scriptContent -and $PSCommandPath) { try { $scriptContent = Get-Content $PSCommandPath -Raw -ErrorAction Stop Write-Host " Retrieved script from PSCommandPath" -ForegroundColor Gray } catch {} } # Method 3: For iwr | iex, download the complete release script. if (-not $scriptContent) { Write-Host " Downloading complete bootstrap script for elevation..." -ForegroundColor Gray try { # Convert git URL to raw content URL (remove .git suffix if present) $rawRepoUrl = $RepoUrl -replace '\.git$', '' $downloadUrl = "$rawRepoUrl/raw/$Branch/bootstrap.ps1" Write-Host " Download URL: $downloadUrl" -ForegroundColor Gray $scriptContent = (Invoke-WebRequest -Uri $downloadUrl -UseBasicParsing).Content } catch { Write-Host " [ERROR] Cannot retrieve script for elevation: $($_.Exception.Message)" -ForegroundColor Red Write-Host "" Write-Host " Workaround: Run this script as Administrator directly" -ForegroundColor Yellow Write-Host " Right-click PowerShell -> Run as Administrator, then:" -ForegroundColor Yellow Write-Host " cd `"$((Get-Location).Path)`"" -ForegroundColor Cyan Write-Host " .\bootstrap.ps1" -ForegroundColor Cyan Write-Host "" return $false } } # Save to temp file $scriptContent | Out-File -FilePath $tempScript -Encoding UTF8 -Force $scriptPath = $tempScript Write-Host " [OK] Temporary script created: $tempScript" -ForegroundColor Green } # Build arguments to pass to elevated process $arguments = @() if ($RepoUrl -ne "https://github.com/Zaromics/ZaroPGx.git") { $arguments += "-RepoUrl `"$RepoUrl`"" } if ($ReleaseVersion -ne "0.4.1") { $arguments += "-ReleaseVersion `"$ReleaseVersion`"" } if ($Branch -ne "v$ReleaseVersion") { $arguments += "-Branch `"$Branch`"" } if ($TargetDir -ne "ZaroPGx") { $arguments += "-TargetDir `"$TargetDir`"" } if ($Update) { $arguments += "-Update" } # Pass SkipDependencyCheck and MissingDeps to elevated process $arguments += "-SkipDependencyCheck" # Pass missing dependencies as a comma-separated string $depString = $MissingDeps -join ',' $arguments += "-MissingDeps '$depString'" Write-Host "" Write-Host " Starting elevated PowerShell window..." -ForegroundColor Cyan Write-Host " Please approve the UAC prompt if it appears." -ForegroundColor Yellow Write-Host "" try { $elevatedProcess = Start-Process powershell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" $($arguments -join ' ')" -PassThru -Wait # Clean up temp script if we created one if ($scriptPath -like "*zaropgx-bootstrap-temp.ps1") { try { Remove-Item $scriptPath -Force -ErrorAction SilentlyContinue } catch {} } if ($elevatedProcess.ExitCode -eq 3010) { Write-RebootRequired Exit-Bootstrap 0 } if ($elevatedProcess.ExitCode -ne 0) { Write-Host " Installation process exited with code: $($elevatedProcess.ExitCode)" -ForegroundColor Yellow Write-Host "" # Provide helpful error messages for common exit codes if ($elevatedProcess.ExitCode -eq -196608 -or $elevatedProcess.ExitCode -eq 1) { Write-Host " This may indicate:" -ForegroundColor Yellow Write-Host " - UAC prompt was cancelled" -ForegroundColor Gray Write-Host " - Script execution was blocked" -ForegroundColor Gray Write-Host " - An error occurred during installation" -ForegroundColor Gray Write-Host "" } return $false } # After successful installation, re-check dependencies and continue Write-Host "" Write-Host "Dependencies installed successfully. Re-checking..." -ForegroundColor Green return $true } catch { Write-Host " Failed to elevate privileges: $($_.Exception.Message)" -ForegroundColor Red Write-Host "" return $false } } # If SkipDependencyCheck is set and MissingDeps is provided, we're in the elevated process # Parse MissingDeps if it was passed as a comma-separated string if ($SkipDependencyCheck -and $MissingDeps.Count -gt 0) { Write-Host "" Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " ZaroPGx Dependency Installation (Elevated)" -ForegroundColor Cyan Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" Write-Host " This window is running with administrator privileges" -ForegroundColor Gray Write-Host " Installing required dependencies..." -ForegroundColor Gray Write-Host "" if ($MissingDeps.Count -eq 1 -and $MissingDeps[0] -match ',') { # Split comma-separated string into array $MissingDeps = $MissingDeps[0] -split ',' | ForEach-Object { $_.Trim() } } $installSuccess = $true # Install dependencies and exit (this is the elevated process) foreach ($dep in $MissingDeps) { Write-Host "" Write-Host "Installing $dep..." -ForegroundColor Yellow try { switch ($dep) { "Git" { if ($wingetAvailable) { winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { throw "Git installation failed with exit code $LASTEXITCODE" } } elseif ($chocoAvailable) { choco install git -y if ($LASTEXITCODE -ne 0) { throw "Git installation failed with exit code $LASTEXITCODE" } } } "Docker" { if ($wingetAvailable) { Write-Host "Installing Docker Desktop via winget..." -ForegroundColor Cyan Write-Host "(This may take several minutes)" -ForegroundColor Gray winget install --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { throw "Docker installation failed with exit code $LASTEXITCODE" } } elseif ($chocoAvailable) { choco install docker-desktop -y if ($LASTEXITCODE -ne 0) { throw "Docker installation failed with exit code $LASTEXITCODE" } } } "Compose" { if ($wingetAvailable) { winget upgrade --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { winget install --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements } if ($LASTEXITCODE -ne 0) { throw "Docker Compose installation failed with exit code $LASTEXITCODE" } } elseif ($chocoAvailable) { choco upgrade docker-desktop -y if ($LASTEXITCODE -ne 0) { throw "Docker Compose installation failed with exit code $LASTEXITCODE" } } } "WSL2" { # Prefer the modern, supported path: wsl --install (Win10 22H2+/Win11) # Windows 10 build 19045 (22H2) or Windows 11 build 22000+ support wsl --install $buildNumber = Get-WindowsBuildNumber $supportsWslInstall = ($buildNumber -ge 19041) if ($supportsWslInstall) { Write-Host "Installing WSL using 'wsl --install'..." -ForegroundColor Cyan # Set WSL2 as default BEFORE installing distributions try { wsl --set-default-version 2 2>&1 | Out-Null } catch {} # Install WSL and Ubuntu 22.04; default can be changed later if desired $wslInstallOut = wsl --install -d Ubuntu-22.04 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "WSL installation failed with exit code $LASTEXITCODE`n$wslInstallOut" } if ($wslInstallOut -match '(?i)restart is required|please reboot|reboot your computer') { $script:NeedsReboot = $true } if (-not $script:NeedsReboot) { # Update WSL to latest version (ensures we have WSL 2.1.5+) Write-Host "Updating WSL to latest version..." -ForegroundColor Cyan wsl --update if ($LASTEXITCODE -ne 0) { throw "WSL update failed with exit code $LASTEXITCODE" } } } elseif ($wingetAvailable) { Write-Host "Installing WSL from Microsoft Store via winget..." -ForegroundColor Cyan # Updated package ID for WSL in Microsoft Store; try legacy ID as fallback winget install --id Microsoft.WSL -e --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Host "Primary WSL package ID failed, trying legacy ID..." -ForegroundColor Yellow winget install --id Microsoft.WindowsSubsystemLinux -e --source winget --accept-package-agreements --accept-source-agreements } Write-Host "Enabling required Windows features for WSL..." -ForegroundColor Cyan Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart wsl --set-default-version 2 Write-Host "Restart may be required. After restart, install a distro with: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow $script:NeedsReboot = $true } else { Write-Host "Installing WSL2 manually..." -ForegroundColor Cyan # Enable WSL and Virtual Machine Platform features Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart # Set WSL default version to 2 wsl --set-default-version 2 Write-Host "If prompted for the WSL kernel, download from: https://github.com/microsoft/wsl/releases" -ForegroundColor Gray Write-Host "After reboot, install a distro with: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow $script:NeedsReboot = $true } } } Write-Host " [OK] $dep installation completed" -ForegroundColor Green } catch { Write-Host " [ERROR] Failed to install ${dep}: $($_.Exception.Message)" -ForegroundColor Red $installSuccess = $false } } Write-Host "" Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan if ($installSuccess) { if ($script:NeedsReboot -or (($MissingDeps -contains "WSL2") -and (Test-RebootPending))) { Write-RebootRequired exit 3010 } Write-Host " Installation Complete!" -ForegroundColor Green Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" Write-Host "Dependencies installed successfully!" -ForegroundColor Green Write-Host "" Write-Host "Important Next Steps:" -ForegroundColor Yellow Write-Host " 1. Close this window" -ForegroundColor Gray Write-Host " 2. Restart your terminal/PowerShell session" -ForegroundColor Gray Write-Host " 3. If you installed WSL2, you may need to restart your computer" -ForegroundColor Gray Write-Host " 4. Start Docker Desktop manually (if installed)" -ForegroundColor Gray Write-Host " 5. Re-run the bootstrap script" -ForegroundColor Gray Write-Host "" Write-Host "This window will close in 10 seconds..." -ForegroundColor Gray Start-Sleep -Seconds 10 exit 0 } else { Write-Host " Installation Completed with Errors" -ForegroundColor Yellow Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host "" Write-Host "Some dependencies failed to install." -ForegroundColor Yellow Write-Host "Please review the error messages above." -ForegroundColor Yellow Write-Host "" Write-Host "Press any key to close this window..." -ForegroundColor Gray $null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") exit 1 } } # Install missing dependencies (normal flow - when already running as admin) foreach ($dep in $MissingDeps) { Write-Host "" Write-Host "Installing $dep..." -ForegroundColor Yellow switch ($dep) { "Git" { if ($wingetAvailable) { winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements } elseif ($chocoAvailable) { choco install git -y } } "Docker" { if ($wingetAvailable) { Write-Host "Installing Docker Desktop via winget..." -ForegroundColor Cyan winget install --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements } elseif ($chocoAvailable) { choco install docker-desktop -y } } "Compose" { if ($wingetAvailable) { winget upgrade --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { winget install --id Docker.DockerDesktop -e --source winget --accept-package-agreements --accept-source-agreements } if ($LASTEXITCODE -ne 0) { throw "Docker Compose installation failed with exit code $LASTEXITCODE" } } elseif ($chocoAvailable) { choco upgrade docker-desktop -y if ($LASTEXITCODE -ne 0) { throw "Docker Compose installation failed with exit code $LASTEXITCODE" } } } "WSL2" { # Prefer the modern, supported path: wsl --install (Win10 22H2+/Win11) # Windows 10 build 19045 (22H2) or Windows 11 build 22000+ support wsl --install $buildNumber = Get-WindowsBuildNumber $supportsWslInstall = ($buildNumber -ge 19041) if ($supportsWslInstall) { Write-Host "Installing WSL using 'wsl --install'..." -ForegroundColor Cyan # Set WSL2 as default BEFORE installing distributions try { wsl --set-default-version 2 2>&1 | Out-Null } catch {} # Install WSL and Ubuntu 22.04; default can be changed later if desired $wslInstallOut = wsl --install -d Ubuntu-22.04 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "WSL installation failed with exit code $LASTEXITCODE`n$wslInstallOut" } if ($wslInstallOut -match '(?i)restart is required|please reboot|reboot your computer') { $script:NeedsReboot = $true } if (-not $script:NeedsReboot) { # Update WSL to latest version (ensures we have WSL 2.1.5+) Write-Host "Updating WSL to latest version..." -ForegroundColor Cyan wsl --update if ($LASTEXITCODE -ne 0) { throw "WSL update failed with exit code $LASTEXITCODE" } } } elseif ($wingetAvailable) { Write-Host "Installing WSL from Microsoft Store via winget..." -ForegroundColor Cyan # Updated package ID for WSL in Microsoft Store; try legacy ID as fallback winget install --id Microsoft.WSL -e --source winget --accept-package-agreements --accept-source-agreements 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Host "Primary WSL package ID failed, trying legacy ID..." -ForegroundColor Yellow winget install --id Microsoft.WindowsSubsystemLinux -e --source winget --accept-package-agreements --accept-source-agreements } Write-Host "Enabling required Windows features for WSL..." -ForegroundColor Cyan Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart wsl --set-default-version 2 Write-Host "Restart may be required. After restart, install a distro with: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow $script:NeedsReboot = $true } else { Write-Host "Installing WSL2 manually..." -ForegroundColor Cyan # Enable WSL and Virtual Machine Platform features Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart # Set WSL default version to 2 wsl --set-default-version 2 Write-Host "If prompted for the WSL kernel, download from: https://github.com/microsoft/wsl/releases" -ForegroundColor Gray Write-Host "After reboot, install a distro with: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow $script:NeedsReboot = $true } } } } if ($script:NeedsReboot -or (($MissingDeps -contains "WSL2") -and (Test-RebootPending))) { Write-RebootRequired Exit-Bootstrap 0 } Write-Host "" Write-Host "Dependencies installed! You may need to:" -ForegroundColor Green Write-Host " 1. Restart your computer (especially if WSL2 was installed)" -ForegroundColor Yellow Write-Host " 2. Run 'wsl --update' to ensure WSL version 2.1.5+ is installed" -ForegroundColor Yellow Write-Host " 3. Restart your terminal/PowerShell session" -ForegroundColor Yellow Write-Host " 4. Start Docker Desktop manually" -ForegroundColor Yellow Write-Host " 5. Re-run this bootstrap script" -ForegroundColor Yellow Write-Host "" return $true } # Check dependencies if ($SkipDependencyCheck -and $MissingDeps.Count -gt 0) { $installed = Install-Dependencies -MissingDeps $MissingDeps if (-not $installed) { Exit-Bootstrap 1 } exit 0 } if (-not $SkipDependencyCheck) { Write-Host "Checking dependencies..." -ForegroundColor Cyan $missingDeps = @() # Check WSL2 (Windows only) if ($IsWindows -or $env:OS -eq "Windows_NT") { $wslCmd = Get-Command wsl -ErrorAction SilentlyContinue if (-not $wslCmd) { $missingDeps += "WSL2" Write-Host " [WARNING] WSL2 not found" -ForegroundColor Yellow } else { # Check WSL version (Docker Desktop requires WSL 2.1.5+) $wslVersion = Get-WSLVersion $minRequiredVersion = [version]"2.1.5" $needsUpdate = $false if ($wslVersion -eq [version]"0.0.0") { Write-Host " [WARNING] WSL found but using legacy/inbox version (pre-2.0)" -ForegroundColor Yellow Write-Host " [WARNING] Docker Desktop requires WSL version 2.1.5 or higher" -ForegroundColor Yellow $needsUpdate = $true } elseif ($wslVersion -lt $minRequiredVersion) { Write-Host " [WARNING] WSL version $wslVersion found (Docker requires 2.1.5+)" -ForegroundColor Yellow $needsUpdate = $true } else { Write-Host " [OK] WSL version $wslVersion found" -ForegroundColor Green } # Offer to update WSL if outdated if ($needsUpdate) { Write-Host "" $updateResponse = Read-ConsoleLine "Would you like to update WSL now? (Y/n)" if ($updateResponse -notmatch '^[Nn]') { Write-Host " Updating WSL..." -ForegroundColor Cyan try { wsl --update if ($LASTEXITCODE -eq 0) { Write-Host " [OK] WSL updated successfully!" -ForegroundColor Green Write-Host " Note: You may need to restart your terminal" -ForegroundColor Gray } else { Write-Host " [WARNING] WSL update completed with warnings" -ForegroundColor Yellow } } catch { Write-Host " [WARNING] WSL update failed: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host " Please run 'wsl --update' manually as administrator" -ForegroundColor Yellow } } else { Write-Host " Skipping WSL update. Docker Desktop may not start correctly." -ForegroundColor Yellow } } # Check if WSL2 is properly configured and a distribution is installed try { $wslList = wsl --list --verbose 2>&1 | Out-String if ($LASTEXITCODE -eq 0) { if ($wslList -match "VERSION.*2") { $hasDistro = Test-WSLDistribution if ($hasDistro) { Write-Host " [OK] WSL2 distributions found" -ForegroundColor Green } else { Write-Host " [WARNING] WSL2 enabled but no distributions installed" -ForegroundColor Yellow Write-Host "" Write-Host " A Linux distribution is required for Docker in WSL2" -ForegroundColor Cyan Write-Host " Installing Ubuntu 22.04 (recommended)..." -ForegroundColor Yellow try { $ubuntuInstallOut = wsl --install -d Ubuntu-22.04 --no-launch 2>&1 | Out-String if ($LASTEXITCODE -eq 0) { Write-Host " [OK] Ubuntu 22.04 installed" -ForegroundColor Green Write-Host " Note: You'll need to set up a username/password on first use" -ForegroundColor Gray if ($ubuntuInstallOut -match '(?i)restart is required|please reboot|reboot your computer' -or (Test-RebootPending)) { Write-RebootRequired Exit-Bootstrap 0 } } else { Write-Host " [WARNING] Failed to install Ubuntu automatically" -ForegroundColor Yellow Write-Host " Please run: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow } } catch { Write-Host " [WARNING] Could not install Ubuntu: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host " Please run: wsl --install -d Ubuntu-22.04" -ForegroundColor Yellow } } } else { Write-Host " [WARNING] WSL found but WSL2 may not be set as default" -ForegroundColor Yellow Write-Host " [WARNING] Set WSL2 as default: wsl --set-default-version 2" -ForegroundColor Yellow } } else { Write-Host " [WARNING] WSL found but may not be properly configured" -ForegroundColor Yellow } } catch { Write-Host " [WARNING] WSL found but status check failed" -ForegroundColor Yellow } } } # Check Git $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { $missingDeps += "Git" } else { Write-Host " [OK] Git found" -ForegroundColor Green } # Check Docker $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue if (-not $dockerCmd) { $missingDeps += "Docker" } else { Write-Host " [OK] Docker found" -ForegroundColor Green # Check if Docker is running try { docker version 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Host " [WARNING] Docker is installed but not running. Please start Docker Desktop." -ForegroundColor Yellow } else { Write-Host " [OK] Docker is running" -ForegroundColor Green } } catch { Write-Host " [WARNING] Docker is installed but not running. Please start Docker Desktop." -ForegroundColor Yellow } } # start-docker.ps1 uses Compose V2 long-form env_file syntax. if (-not (Test-DockerComposeVersion)) { Write-Host " [WARNING] Docker Compose 2.24.0 or newer is required" -ForegroundColor Yellow # A Docker Desktop install includes Compose; do not install it twice. if ($missingDeps -notcontains "Docker") { $missingDeps += "Compose" } } # Handle missing dependencies if ($missingDeps.Count -gt 0) { $installed = Install-Dependencies -MissingDeps $missingDeps if (-not $installed) { Write-Host "" Write-Host "Please install the following and re-run this script:" -ForegroundColor Red foreach ($dep in $missingDeps) { Write-Host " - $dep" -ForegroundColor Yellow } Write-Host "" Write-Host "Installation links:" -ForegroundColor Cyan if ($missingDeps -contains "WSL2") { Write-Host " WSL2: https://learn.microsoft.com/en-us/windows/wsl/install" -ForegroundColor Gray Write-Host " WSL Update: Run 'wsl --update' in PowerShell (admin)" -ForegroundColor Gray } if ($missingDeps -contains "Git") { Write-Host " Git: https://git-scm.com/downloads" -ForegroundColor Gray } if ($missingDeps -contains "Docker") { Write-Host " Docker Desktop: https://www.docker.com/products/docker-desktop" -ForegroundColor Gray Write-Host " Requirements: Windows 10 22H2 (build 19045) or Windows 11" -ForegroundColor Gray Write-Host " WSL version 2.1.5 or higher" -ForegroundColor Gray } Write-Host "" Exit-Bootstrap 1 } # After installation, re-check dependencies to ensure they're now available Write-Host "" Write-Host "Re-checking dependencies after installation..." -ForegroundColor Cyan # Quick re-check - if still missing, prompt user $stillMissing = @() foreach ($dep in $missingDeps) { switch ($dep) { "Git" { $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { $stillMissing += "Git" } } "Docker" { $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue if (-not $dockerCmd) { $stillMissing += "Docker" } } "Compose" { if (-not (Test-DockerComposeVersion)) { $stillMissing += "Compose" } } "WSL2" { $wslCmd = Get-Command wsl -ErrorAction SilentlyContinue if (-not $wslCmd) { $stillMissing += "WSL2" } } } } if ($stillMissing.Count -gt 0) { Write-Host "" Write-Host "[WARNING] Some dependencies may still need configuration:" -ForegroundColor Yellow foreach ($dep in $stillMissing) { Write-Host " - $dep" -ForegroundColor Yellow } Write-Host "" Write-Host "You may need to:" -ForegroundColor Yellow Write-Host " 1. Restart your computer (especially if WSL2 was installed)" -ForegroundColor Gray Write-Host " 2. Run 'wsl --update' to ensure WSL 2.1.5+ is installed" -ForegroundColor Gray Write-Host " 3. Restart your terminal/PowerShell session" -ForegroundColor Gray Write-Host " 4. Re-run this bootstrap script" -ForegroundColor Gray Write-Host "" Write-Host "Re-run the same bootstrap command after completing those steps." -ForegroundColor Yellow Exit-Bootstrap 0 } else { Write-Host "[OK] All dependencies are now available!" -ForegroundColor Green } } if (-not (Test-DockerComposeVersion)) { Write-Host "Docker Compose 2.24.0 or newer is required before setup can continue." -ForegroundColor Red Exit-Bootstrap 1 } Write-Host "" } Write-Host "Repository URL: $RepoUrl" -ForegroundColor Cyan Write-Host "Branch: $Branch" -ForegroundColor Cyan Write-Host "Target directory: $TargetDir" -ForegroundColor Cyan if (-not (Test-Path $TargetDir)) { Write-Host "Cloning repository..." -ForegroundColor Yellow git clone --branch $Branch $RepoUrl $TargetDir if ($LASTEXITCODE -ne 0) { Write-Host "Clone failed." -ForegroundColor Red Exit-Bootstrap 1 } } else { Write-Host "Target directory already exists: $TargetDir" -ForegroundColor Gray if ($Update) { if (-not (Test-Path (Join-Path $TargetDir ".git"))) { Write-Host "Existing directory is not a Git repository; refusing to update." -ForegroundColor Red Exit-Bootstrap 1 } else { Write-Host "Updating repository (fast-forward only)..." -ForegroundColor Yellow # Only local changes to tracked files block an update: the checkout # and the line-ending resets below would carry or discard them. Line # endings alone are not a change (see Get-TrackedChanges). # Untracked files are not checked. A running install makes some that # an older .gitignore does not cover (0.3.2's missed the services' # rotated logs, data/*.log.1 to .5), and neither step deletes one. If # the release adds a file at the path of an untracked one, git refuses # the checkout and names the file (an ignored file there is # overwritten, as before). try { $trackedChanges = Get-TrackedChanges } catch { Write-Host "git diff failed, so local changes cannot be ruled out; refusing to update." -ForegroundColor Red Exit-Bootstrap 1 } if ($trackedChanges) { Write-Host "Tracked files have local changes; refusing to update, so nothing was changed:" -ForegroundColor Yellow $trackedChanges | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } # A release install is a detached HEAD: a commit there would be # left behind by the checkout. --index restores a staged version too. Write-Host "Stash them (git -C `"$TargetDir`" stash), re-run with -Update, then git -C `"$TargetDir`" stash pop --index." -ForegroundColor Yellow Exit-Bootstrap 1 } else { # Nothing real to lose. A file whose only change is CRLF line # endings still looks modified to git checkout, which refuses to # replace it, so check those files out again first. git -C $TargetDir rm --cached -r . 2>&1 | Out-Null git -C $TargetDir reset --hard 2>&1 | Out-Null # Release hotfix tags may be replaced deliberately. Force-fetch # tags so an existing clone does not retain an older tag object. git -C $TargetDir fetch --all --prune --force --tags if ($LASTEXITCODE -ne 0) { Write-Host "git fetch failed." -ForegroundColor Red; Exit-Bootstrap 1 } git -C $TargetDir checkout $Branch if ($LASTEXITCODE -ne 0) { Write-Host "git checkout failed. If git named untracked files the release would overwrite, move them aside and re-run." -ForegroundColor Red; Exit-Bootstrap 1 } git -C $TargetDir rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { git -C $TargetDir pull --ff-only if ($LASTEXITCODE -ne 0) { Write-Host "git pull failed (non-fast-forward)." -ForegroundColor Red; Exit-Bootstrap 1 } } # Re-normalize line endings after pull (in case .gitattributes was updated) Write-Host "Re-normalizing line endings..." -ForegroundColor Gray git -C $TargetDir rm --cached -r . 2>&1 | Out-Null git -C $TargetDir reset --hard 2>&1 | Out-Null } } } else { Write-Host "Skipping update (use -Update to fetch latest)." -ForegroundColor Gray # Check if we need to re-normalize line endings even without update. # A checkout made before .gitattributes forced LF on shell scripts, or one # whose scripts were rewritten with CRLF by a Windows editor or tool, has a # start-docker.sh that bash (WSL, the containers) cannot run. Clearing the # index and resetting checks every tracked file out again under the current # .gitattributes. The reset also overwrites local changes to tracked files, # so it refuses when there are any; line endings alone are not a change # (see Get-TrackedChanges). if (Test-Path (Join-Path $TargetDir ".git")) { $shFile = Join-Path $TargetDir "start-docker.sh" if (Test-Path $shFile) { # Check if file has CRLF line endings $content = Get-Content -Path $shFile -Raw if ($content -match "`r`n") { try { $trackedChanges = Get-TrackedChanges } catch { Write-Host "start-docker.sh has Windows (CRLF) line endings, but git diff failed, so local changes cannot be ruled out; not re-normalizing." -ForegroundColor Red Exit-Bootstrap 1 } if ($trackedChanges) { Write-Host "start-docker.sh has Windows (CRLF) line endings. Re-normalizing them would discard local changes to tracked files, so nothing was changed:" -ForegroundColor Yellow $trackedChanges | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } # --index restores a staged version too. Write-Host "Stash them (git -C `"$TargetDir`" stash), re-run, then restore them (git -C `"$TargetDir`" stash pop --index)." -ForegroundColor Yellow Exit-Bootstrap 1 } Write-Host "Detected incorrect line endings, re-normalizing files..." -ForegroundColor Yellow git -C $TargetDir rm --cached -r . 2>&1 | Out-Null git -C $TargetDir reset --hard 2>&1 | Out-Null Write-Host " [OK] Files re-normalized" -ForegroundColor Green } } } } } $startScript = Join-Path $TargetDir "start-docker.ps1" if (-not (Test-Path $startScript)) { Write-Host "start-docker.ps1 not found at: $startScript" -ForegroundColor Red Exit-Bootstrap 1 } Write-Host "Launching startup script..." -ForegroundColor Yellow Write-Host "Using local development configuration (.env.local)" -ForegroundColor Cyan # Launch start-docker.ps1 with execution policy bypass try { powershell.exe -ExecutionPolicy Bypass -File $startScript -AutoLocal $startScriptExitCode = $LASTEXITCODE if ($startScriptExitCode -ne 0) { Write-Host "start-docker.ps1 exited with code: $startScriptExitCode" -ForegroundColor Red if ($didPush) { Pop-Location } Exit-Bootstrap $startScriptExitCode } } catch { Write-Host "Failed to launch start-docker.ps1: $($_.Exception.Message)" -ForegroundColor Red if ($didPush) { Pop-Location } Exit-Bootstrap 1 }