Created
August 24, 2026 08:14
-
-
Save marvinrabe/1528644546d8661a4972eff3e6faec0c to your computer and use it in GitHub Desktop.
install.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #requires -version 5.1 | |
| <# | |
| .SYNOPSIS | |
| Prepares a Windows computer for working with the R&P app template. | |
| .DESCRIPTION | |
| Installs and configures WSL 2 with Ubuntu, Docker Desktop, Git for Windows, | |
| Visual Studio Code, the Dev Containers extension, and a GitHub SSH key. | |
| Existing installations are detected and skipped. New installations use the | |
| latest versions available from WSL or WinGet. | |
| The script is intended to be run from a Gist like this: | |
| irm <RAW-GIST-URL> | iex | |
| If the current PowerShell is not elevated, Windows displays one UAC prompt. | |
| All remaining steps run without interaction. The script never restarts the | |
| computer automatically. | |
| #> | |
| $setup = { | |
| param([string] $ResultPath) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $ProgressPreference = "SilentlyContinue" | |
| $script:RestartRequired = $false | |
| $script:RerunAfterRestart = $false | |
| $script:Winget = $null | |
| $summary = [System.Collections.Generic.List[string]]::new() | |
| function Write-Step { | |
| param([string] $Message) | |
| Write-Host "`n==> $Message" -ForegroundColor Cyan | |
| } | |
| function Refresh-Path { | |
| $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") | |
| $userPath = [Environment]::GetEnvironmentVariable("Path", "User") | |
| $env:Path = "$machinePath;$userPath" | |
| } | |
| function Resolve-Executable { | |
| param( | |
| [string] $Command, | |
| [string[]] $CandidatePaths = @() | |
| ) | |
| $found = Get-Command $Command -ErrorAction SilentlyContinue | |
| if ($null -ne $found) { | |
| return $found.Source | |
| } | |
| foreach ($candidate in $CandidatePaths) { | |
| if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { | |
| return $candidate | |
| } | |
| } | |
| return $null | |
| } | |
| function Initialize-WinGet { | |
| Write-Step "Pruefe Windows Package Manager (WinGet)" | |
| $wingetPath = Resolve-Executable -Command "winget.exe" -CandidatePaths @( | |
| (Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\winget.exe") | |
| ) | |
| if (-not $wingetPath) { | |
| try { | |
| Add-AppxPackage ` | |
| -RegisterByFamilyName ` | |
| -MainPackage "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" ` | |
| -ErrorAction Stop | |
| } | |
| catch { | |
| Write-Host "WinGet ist nicht registriert; repariere die Installation ..." | |
| [Net.ServicePointManager]::SecurityProtocol = ` | |
| [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 | |
| Install-PackageProvider ` | |
| -Name NuGet ` | |
| -Force ` | |
| -Scope CurrentUser ` | |
| -Confirm:$false | Out-Null | |
| Install-Module ` | |
| -Name Microsoft.WinGet.Client ` | |
| -Repository PSGallery ` | |
| -Scope CurrentUser ` | |
| -Force ` | |
| -AllowClobber ` | |
| -Confirm:$false | |
| Import-Module Microsoft.WinGet.Client -Force | |
| Repair-WinGetPackageManager -Force -Latest | Out-Null | |
| } | |
| Refresh-Path | |
| $wingetPath = Resolve-Executable -Command "winget.exe" -CandidatePaths @( | |
| (Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps\winget.exe") | |
| ) | |
| } | |
| if (-not $wingetPath) { | |
| throw "WinGet konnte nicht installiert oder gefunden werden." | |
| } | |
| $script:Winget = $wingetPath | |
| & $script:Winget source update --disable-interactivity | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Die WinGet-Paketquellen konnten nicht aktualisiert werden." | |
| } | |
| } | |
| function Test-WinGetPackage { | |
| param([string] $Id) | |
| $output = & $script:Winget list ` | |
| --id $Id ` | |
| --exact ` | |
| --source winget ` | |
| --accept-source-agreements ` | |
| --disable-interactivity 2>&1 | |
| return ($LASTEXITCODE -eq 0) -and (($output -join "`n") -match [regex]::Escape($Id)) | |
| } | |
| function Install-WinGetPackage { | |
| param( | |
| [string] $Name, | |
| [string] $Id, | |
| [string] $Command, | |
| [string[]] $CandidatePaths = @(), | |
| [ValidateSet("user", "machine")] | |
| [string] $Scope, | |
| [string] $Override = "" | |
| ) | |
| Write-Step "Pruefe $Name" | |
| if ((Resolve-Executable -Command $Command -CandidatePaths $CandidatePaths) -or | |
| (Test-WinGetPackage -Id $Id)) { | |
| Write-Host "$Name ist bereits installiert; ueberspringe Installation." | |
| $summary.Add("[OK] $Name war bereits installiert.") | |
| return | |
| } | |
| Write-Host "Installiere die aktuelle Version von $Name ..." | |
| $arguments = @( | |
| "install", | |
| "--id", $Id, | |
| "--exact", | |
| "--source", "winget", | |
| "--silent", | |
| "--accept-package-agreements", | |
| "--accept-source-agreements", | |
| "--disable-interactivity", | |
| "--no-upgrade" | |
| ) | |
| if ($Scope) { | |
| $arguments += @("--scope", $Scope) | |
| } | |
| if ($Override) { | |
| $arguments += @("--override", $Override) | |
| } | |
| & $script:Winget @arguments | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Installation von $Name fehlgeschlagen (WinGet-Code $LASTEXITCODE)." | |
| } | |
| Refresh-Path | |
| $summary.Add("[OK] $Name wurde installiert.") | |
| } | |
| function Get-WslFeatureState { | |
| $names = @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform") | |
| $states = foreach ($name in $names) { | |
| (Get-WindowsOptionalFeature -Online -FeatureName $name).State.ToString() | |
| } | |
| return @($states) | |
| } | |
| function Get-WslDistributions { | |
| $output = & wsl.exe --list --quiet 2>$null | |
| if ($LASTEXITCODE -ne 0) { | |
| return @() | |
| } | |
| return @( | |
| (($output -join "`n") -replace "`0", "") -split "`r?`n" | | |
| ForEach-Object { $_.Trim() } | | |
| Where-Object { $_ } | |
| ) | |
| } | |
| function Get-WslVersion { | |
| $output = & wsl.exe --version 2>$null | |
| if ($LASTEXITCODE -ne 0) { | |
| return $null | |
| } | |
| $match = [regex]::Match((($output -join "`n") -replace "`0", ""), "\d+(?:\.\d+){2,3}") | |
| if (-not $match.Success) { | |
| return $null | |
| } | |
| return [version] $match.Value | |
| } | |
| function Initialize-Wsl { | |
| Write-Step "Pruefe WSL 2 und Ubuntu" | |
| $featureStates = Get-WslFeatureState | |
| $featuresEnabled = @($featureStates | Where-Object { $_ -ne "Enabled" }).Count -eq 0 | |
| $ubuntuDistribution = @( | |
| Get-WslDistributions | Where-Object { $_ -match "^Ubuntu(?:-|$)" } | |
| ) | Select-Object -First 1 | |
| $ubuntuInstalled = $null -ne $ubuntuDistribution | |
| if (-not $featuresEnabled -and $ubuntuInstalled) { | |
| Write-Host "Aktiviere die fuer WSL 2 erforderlichen Windows-Komponenten ..." | |
| Enable-WindowsOptionalFeature ` | |
| -Online ` | |
| -FeatureName "Microsoft-Windows-Subsystem-Linux" ` | |
| -All ` | |
| -NoRestart | Out-Null | |
| Enable-WindowsOptionalFeature ` | |
| -Online ` | |
| -FeatureName "VirtualMachinePlatform" ` | |
| -All ` | |
| -NoRestart | Out-Null | |
| $script:RestartRequired = $true | |
| $script:RerunAfterRestart = $true | |
| } | |
| if (-not $ubuntuInstalled) { | |
| Write-Host "Installiere die aktuelle WSL-Version mit Ubuntu ..." | |
| & wsl.exe --install --distribution Ubuntu --no-launch --web-download | Out-Host | |
| $wslExitCode = $LASTEXITCODE | |
| if ($wslExitCode -notin @(0, 3010)) { | |
| throw "WSL/Ubuntu-Installation fehlgeschlagen (Code $wslExitCode)." | |
| } | |
| if (-not $featuresEnabled -or $wslExitCode -eq 3010) { | |
| $script:RestartRequired = $true | |
| } | |
| $summary.Add("[OK] WSL 2 mit Ubuntu wurde installiert.") | |
| } | |
| else { | |
| Write-Host "Ubuntu ist bereits unter WSL installiert; ueberspringe Installation." | |
| $summary.Add("[OK] WSL mit Ubuntu war bereits installiert.") | |
| } | |
| if (-not $script:RestartRequired) { | |
| $wslVersion = Get-WslVersion | |
| if (($null -eq $wslVersion) -or ($wslVersion -lt [version] "2.1.5")) { | |
| Write-Host "Aktualisiere WSL auf eine von Docker unterstuetzte Version ..." | |
| & wsl.exe --update --web-download | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "WSL konnte nicht aktualisiert werden." | |
| } | |
| } | |
| & wsl.exe --set-default-version 2 | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "WSL 2 konnte nicht als Standard gesetzt werden." | |
| } | |
| $ubuntuDistribution = @( | |
| Get-WslDistributions | Where-Object { $_ -match "^Ubuntu(?:-|$)" } | |
| ) | Select-Object -First 1 | |
| $ubuntuInstalled = $null -ne $ubuntuDistribution | |
| if ($ubuntuInstalled) { | |
| $verboseList = ((& wsl.exe --list --verbose 2>$null) -join "`n") -replace "`0", "" | |
| $escapedDistribution = [regex]::Escape($ubuntuDistribution) | |
| if ($verboseList -notmatch "(?m)^\s*\*?\s*$escapedDistribution\s+.*\s2\s*$") { | |
| Write-Host "Konvertiere $ubuntuDistribution zu WSL 2 ..." | |
| & wsl.exe --set-version $ubuntuDistribution 2 | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Ubuntu konnte nicht auf WSL 2 konvertiert werden." | |
| } | |
| } | |
| & wsl.exe --set-default $ubuntuDistribution | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Ubuntu konnte nicht als Standarddistribution gesetzt werden." | |
| } | |
| } | |
| } | |
| } | |
| function Initialize-Git { | |
| Write-Step "Konfiguriere Git" | |
| $git = Resolve-Executable -Command "git.exe" -CandidatePaths @( | |
| (Join-Path $env:ProgramFiles "Git\cmd\git.exe"), | |
| (Join-Path $env:LOCALAPPDATA "Programs\Git\cmd\git.exe") | |
| ) | |
| if (-not $git) { | |
| throw "Git wurde installiert, aber git.exe wurde nicht gefunden." | |
| } | |
| & $git config --global core.autocrlf input | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Git core.autocrlf konnte nicht konfiguriert werden." | |
| } | |
| $summary.Add("[OK] Git verwendet Linux-Zeilenenden (core.autocrlf=input).") | |
| } | |
| function Initialize-VsCodeExtension { | |
| Write-Step "Pruefe VS Code Dev Containers" | |
| $code = Resolve-Executable -Command "code.cmd" -CandidatePaths @( | |
| (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code\bin\code.cmd"), | |
| (Join-Path $env:ProgramFiles "Microsoft VS Code\bin\code.cmd") | |
| ) | |
| if (-not $code) { | |
| throw "VS Code wurde installiert, aber code.cmd wurde nicht gefunden." | |
| } | |
| $extensionId = "ms-vscode-remote.remote-containers" | |
| $extensions = & $code --list-extensions 2>$null | |
| if (@($extensions | Where-Object { $_ -ieq $extensionId }).Count -eq 0) { | |
| & $code --install-extension $extensionId | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Die VS Code Dev Containers-Erweiterung konnte nicht installiert werden." | |
| } | |
| $summary.Add("[OK] VS Code Dev Containers wurde installiert.") | |
| } | |
| else { | |
| Write-Host "Dev Containers ist bereits installiert; ueberspringe Installation." | |
| $summary.Add("[OK] VS Code Dev Containers war bereits installiert.") | |
| } | |
| } | |
| function Initialize-SshKey { | |
| Write-Step "Pruefe GitHub SSH-Key" | |
| $sshKeygen = Resolve-Executable -Command "ssh-keygen.exe" -CandidatePaths @( | |
| (Join-Path $env:WINDIR "System32\OpenSSH\ssh-keygen.exe"), | |
| (Join-Path $env:ProgramFiles "Git\usr\bin\ssh-keygen.exe") | |
| ) | |
| if (-not $sshKeygen) { | |
| throw "ssh-keygen.exe wurde nicht gefunden." | |
| } | |
| $sshDirectory = Join-Path $env:USERPROFILE ".ssh" | |
| if (-not (Test-Path -LiteralPath $sshDirectory)) { | |
| New-Item -ItemType Directory -Path $sshDirectory | Out-Null | |
| } | |
| $keyBase = Join-Path $sshDirectory "id_ed25519" | |
| $publicKeyPath = "$keyBase.pub" | |
| if (Test-Path -LiteralPath $keyBase) { | |
| $derivedKey = & $sshKeygen -y -P "" -f $keyBase 2>$null | |
| if ($LASTEXITCODE -eq 0 -and $derivedKey) { | |
| if (-not (Test-Path -LiteralPath $publicKeyPath)) { | |
| $derivedKey | Set-Content -LiteralPath $publicKeyPath -Encoding ascii | |
| } | |
| } | |
| else { | |
| $index = 0 | |
| do { | |
| $index++ | |
| $keyBase = Join-Path $sshDirectory "id_ed25519_github_$index" | |
| $publicKeyPath = "$keyBase.pub" | |
| } while ((Test-Path -LiteralPath $keyBase) -or (Test-Path -LiteralPath $publicKeyPath)) | |
| } | |
| } | |
| elseif (Test-Path -LiteralPath $publicKeyPath) { | |
| $index = 0 | |
| do { | |
| $index++ | |
| $keyBase = Join-Path $sshDirectory "id_ed25519_github_$index" | |
| $publicKeyPath = "$keyBase.pub" | |
| } while ((Test-Path -LiteralPath $keyBase) -or (Test-Path -LiteralPath $publicKeyPath)) | |
| } | |
| if (-not (Test-Path -LiteralPath $keyBase)) { | |
| & $sshKeygen ` | |
| -t ed25519 ` | |
| -N "" ` | |
| -C "$env:USERNAME@$env:COMPUTERNAME" ` | |
| -f $keyBase | Out-Host | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Der SSH-Key konnte nicht erstellt werden." | |
| } | |
| $summary.Add("[OK] Ein neuer passwortloser GitHub SSH-Key wurde erstellt.") | |
| } | |
| else { | |
| Write-Host "SSH-Key ist bereits vorhanden; ueberspringe Erstellung." | |
| $summary.Add("[OK] Ein vorhandener SSH-Key wird verwendet.") | |
| } | |
| if (-not (Test-Path -LiteralPath $publicKeyPath)) { | |
| throw "Der oeffentliche SSH-Key wurde nicht gefunden: $publicKeyPath" | |
| } | |
| $publicKey = (Get-Content -LiteralPath $publicKeyPath -Raw).Trim() | |
| try { | |
| Set-Clipboard -Value $publicKey -ErrorAction Stop | |
| $clipboardMessage = "Der SSH-Key wurde auch in die Zwischenablage kopiert." | |
| } | |
| catch { | |
| $clipboardMessage = "Der SSH-Key konnte nicht in die Zwischenablage kopiert werden." | |
| } | |
| $summary.Add("") | |
| $summary.Add("Oeffentlicher SSH-Key:") | |
| $summary.Add($publicKey) | |
| $summary.Add($clipboardMessage) | |
| $summary.Add("Bei GitHub hinterlegen: https://github.com/settings/ssh/new") | |
| } | |
| try { | |
| $build = [Environment]::OSVersion.Version.Build | |
| if (($build -ne 19045) -and ($build -lt 22631)) { | |
| throw "Nicht unterstuetzte Windows-Version (Build $build). Erforderlich ist Windows 10 22H2 oder Windows 11 23H2 oder neuer." | |
| } | |
| Write-Host "R&P Windows-Setup" -ForegroundColor Green | |
| Write-Host "Vorhandene Installationen werden beibehalten und uebersprungen." | |
| Initialize-Wsl | |
| Initialize-WinGet | |
| Install-WinGetPackage ` | |
| -Name "Git for Windows" ` | |
| -Id "Git.Git" ` | |
| -Command "git.exe" ` | |
| -CandidatePaths @( | |
| (Join-Path $env:ProgramFiles "Git\cmd\git.exe"), | |
| (Join-Path $env:LOCALAPPDATA "Programs\Git\cmd\git.exe") | |
| ) | |
| Install-WinGetPackage ` | |
| -Name "Visual Studio Code" ` | |
| -Id "Microsoft.VisualStudioCode" ` | |
| -Command "code.cmd" ` | |
| -Scope "user" ` | |
| -CandidatePaths @( | |
| (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code\bin\code.cmd"), | |
| (Join-Path $env:ProgramFiles "Microsoft VS Code\bin\code.cmd") | |
| ) | |
| Install-WinGetPackage ` | |
| -Name "Docker Desktop" ` | |
| -Id "Docker.DockerDesktop" ` | |
| -Command "Docker Desktop.exe" ` | |
| -CandidatePaths @( | |
| (Join-Path $env:LOCALAPPDATA "Programs\DockerDesktop\Docker Desktop.exe"), | |
| (Join-Path $env:ProgramFiles "Docker\Docker\Docker Desktop.exe") | |
| ) ` | |
| -Override "install --quiet --accept-license --backend=wsl-2 --user --no-windows-containers" | |
| Initialize-Git | |
| Initialize-VsCodeExtension | |
| Initialize-SshKey | |
| $summary.Insert(0, "R&P Windows-Setup erfolgreich abgeschlossen.") | |
| if ($script:RestartRequired) { | |
| $summary.Add("") | |
| $summary.Add("NEUSTART ERFORDERLICH: Windows muss jetzt einmal neu gestartet werden, damit WSL 2 aktiviert wird.") | |
| if ($script:RerunAfterRestart) { | |
| $summary.Add("Fuehre danach denselben Installationsbefehl erneut aus, um eine vorhandene WSL-1-Distribution zu konvertieren.") | |
| } | |
| } | |
| else { | |
| $summary.Add("") | |
| $summary.Add("Kein Windows-Neustart ist fuer WSL erforderlich.") | |
| } | |
| $summary | Set-Content -LiteralPath $ResultPath -Encoding utf8 | |
| Write-Host "`n$($summary -join "`n")" -ForegroundColor Green | |
| } | |
| catch { | |
| $errorSummary = @( | |
| "R&P Windows-Setup ist fehlgeschlagen.", | |
| "Fehler: $($_.Exception.Message)" | |
| ) | |
| $errorSummary | Set-Content -LiteralPath $ResultPath -Encoding utf8 | |
| Write-Error $_ | |
| throw | |
| } | |
| } | |
| $principal = [Security.Principal.WindowsPrincipal]::new( | |
| [Security.Principal.WindowsIdentity]::GetCurrent() | |
| ) | |
| $isAdministrator = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
| $resultFile = Join-Path ([IO.Path]::GetTempPath()) ("rp-windows-setup-{0}.txt" -f [guid]::NewGuid()) | |
| if ($isAdministrator) { | |
| & $setup -ResultPath $resultFile | |
| } | |
| else { | |
| Write-Host "Fordere einmalig Administratorrechte fuer WSL an ..." -ForegroundColor Yellow | |
| $escapedResultFile = $resultFile.Replace("'", "''") | |
| $payload = "& {`n$($setup.ToString())`n} -ResultPath '$escapedResultFile'" | |
| $elevatedScript = Join-Path ` | |
| ([IO.Path]::GetTempPath()) ` | |
| ("rp-windows-setup-elevated-{0}.ps1" -f [guid]::NewGuid()) | |
| $payload | Set-Content -LiteralPath $elevatedScript -Encoding utf8 | |
| try { | |
| $quotedScript = '"' + $elevatedScript + '"' | |
| $process = Start-Process ` | |
| -FilePath "powershell.exe" ` | |
| -Verb RunAs ` | |
| -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $quotedScript) ` | |
| -Wait ` | |
| -PassThru | |
| } | |
| finally { | |
| Remove-Item -LiteralPath $elevatedScript -Force -ErrorAction SilentlyContinue | |
| } | |
| if (Test-Path -LiteralPath $resultFile) { | |
| Write-Host "" | |
| Get-Content -LiteralPath $resultFile | ForEach-Object { Write-Host $_ } | |
| } | |
| if ($process.ExitCode -ne 0) { | |
| throw "Das erhoehte Windows-Setup ist fehlgeschlagen (Code $($process.ExitCode))." | |
| } | |
| } | |
| if (Test-Path -LiteralPath $resultFile) { | |
| Remove-Item -LiteralPath $resultFile -Force -ErrorAction SilentlyContinue | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment