Skip to content

Instantly share code, notes, and snippets.

@marvinrabe
Created August 24, 2026 08:14
Show Gist options
  • Select an option

  • Save marvinrabe/1528644546d8661a4972eff3e6faec0c to your computer and use it in GitHub Desktop.

Select an option

Save marvinrabe/1528644546d8661a4972eff3e6faec0c to your computer and use it in GitHub Desktop.
install.ps1
#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