# Einrichtung für den Dev Container unter Windows 11, für Studierende ohne viel Erfahrung mit # Kommandozeilen: erklärt jeden Schritt, Fragen mit [j/n] per Tastendruck. Windows 10 wird bewusst # nicht unterstützt (Meldung, dann Ende). # # Nutzung (PowerShell, kein Administrator nötig): # irm https://inf.zone/setup-windows.ps1 | iex # # Vorab prüft es, was sonst erst mittendrin scheitern würde, und hört mit einer Anleitung auf: # eingeschränkte PowerShell (verwaltete Geräte), Windows 10, Virtualisierung im BIOS aus, Fenster # unter einem anderen Konto; ARM-Prozessor und wenig Platz nur mit Nachfrage. # # Schritte (was schon da ist, wird genutzt und nicht verändert): # 1. WSL 2 mit Linux: WSL installieren (UAC-Abfrage; danach meist ein Neustart, das Skript läuft # nach der Anmeldung von selbst weiter), Ubuntu mit einem Konto, dessen Name und Passwort das # Skript abfragt. Gibt es schon eine Distribution, nimmt es die Standard-Distribution (WSL 1 → # auf Wunsch WSL 2) und ein Konto darin, wenn es eins gibt. # 2. Docker in der Distribution (Ubuntu-Paket docker.io), Konto in die Gruppe docker. Nicht VS Codes # eigenes „Install Docker in WSL“: Das ruft „wsl --install -d Ubuntu“ auf und scheitert, wenn # Ubuntu schon da ist (Wsl/InstallDistro/ERROR_ALREADY_EXISTS). Mit Docker Desktop: nichts. # 3. VS Code per winget (für den aktuellen Benutzer) # 4. Dev-Containers-Erweiterung, dazu die Einstellung dev.containers.executeInWSL; bei mehreren # VS-Code-Profilen fragt es, in welches # 5. Ordner für die Kursdateien wählen lassen, dort .devcontainer.json vom Server ablegen # (eine andere vorhandene nur auf Nachfrage ersetzen, mit Sicherung) # Zum Schluss öffnet es VS Code mit dem Ordner („Reopen in Container“). # # Server: Das Skript holt alles Weitere (.devcontainer.json, vor dem Neustart sich selbst) von # der Adresse, von der es geladen wurde (inf.zone, dev.inf.zone, Archiv, lokaler hugo server). # Die Datei ist deshalb überall dieselbe, nichts wird beim Deploy umgeschrieben. Auf der Website # steht der Aufruf nur per Shortcode windows-skript; deploy.sh/archive.sh prüfen Datei und # Adressen (scripts/check-windows-skript.sh). Getestet auf Windows 11. # Achtung beim Bearbeiten: „ und “ gelten in PowerShell als Anführungszeichen. In Strings mit # doppelten Anführungszeichen beenden sie den String; dort deshalb ' … ' mit -f verwenden. & { $ScriptName = 'setup-windows.ps1' $LocalDir = Join-Path $env:LOCALAPPDATA 'inf-einrichtung' # Kopie für die Zeit nach dem Neustart $ExtensionId = 'ms-vscode-remote.remote-containers' $Distro = 'Ubuntu' if ($PSVersionTable.PSEdition -eq 'Core' -and -not $IsWindows) { Write-Warning 'Dieses Skript ist nur für Windows gedacht.' return } if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { # Von einer Organisation verwaltete Rechner (AppLocker, WDAC): Vieles unten wäre verboten Write-Warning 'PowerShell ist auf diesem Rechner eingeschränkt, etwa weil ihn eine Firma oder Schule verwaltet.' Write-Warning 'Das Skript kann hier nichts einrichten. Nehmen Sie die Online-Variante (GitHub Codespace,' Write-Warning 'Anleitung auf der Website) oder fragen Sie im Tutorium.' return } $build = 0 try { $build = [int](Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop).CurrentBuildNumber } catch { } if ($build -and $build -lt 22000) { # Windows 10 (Build unter 22000) unterstützt das Skript bewusst nicht: ohne Updates von # Microsoft seit Oktober 2025, und getestet ist es nur auf Windows 11 Write-Warning 'Dieses Skript unterstützt nur Windows 11; Ihr Rechner hat Windows 10 oder älter.' Write-Warning 'Nehmen Sie die Online-Variante (GitHub Codespace, Anleitung auf der Website).' Write-Warning 'Es wurde nichts verändert.' return } $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Fortschrittsbalken bremst Downloads in PS 5.1 stark aus [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $IstArm = ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') function Write-Step($text) { Write-Host "`n==> $text" -ForegroundColor Cyan } function Write-Ok($text) { Write-Host " $text" -ForegroundColor Green } function Write-Info($text) { Write-Host " $text" } function Write-Abstand { Write-Host '' } # nach Ausgaben anderer Programme, damit unsere Texte abgesetzt sind # Unerwarteter Fehler: statt nur roter Meldung ein nächster Schritt trap { Write-Host '' Write-Host ' Etwas ist schiefgegangen. Führen Sie die Zeile von der Website noch einmal aus; klappt es' -ForegroundColor Yellow Write-Host ' dann nicht, bringen Sie ein Foto dieses Fensters ins Tutorium mit. Die Meldung:' -ForegroundColor Yellow break } function Disable-QuickEdit { # Alte Konsole (conhost; Windows 11 vor 22H2 oder umgestellt): Ein Klick ins Fenster markiert # Text und hält das Skript an, bis man Esc drückt; wer das nicht weiß, wartet ewig. Für dieses # Fenster abschalten (bleibt aus, bis es geschlossen wird). Windows Terminal hält nicht an. if ($env:WT_SESSION -or $Host.Name -ne 'ConsoleHost') { return } try { Add-Type -Namespace InfEinrichtung -Name Konsole -MemberDefinition ( '[DllImport("kernel32.dll")] public static extern IntPtr GetStdHandle(int n); ' + '[DllImport("kernel32.dll")] public static extern bool GetConsoleMode(IntPtr h, out uint m); ' + '[DllImport("kernel32.dll")] public static extern bool SetConsoleMode(IntPtr h, uint m);') $h = [InfEinrichtung.Konsole]::GetStdHandle(-10) # Eingabe $m = [uint32]0 if ([InfEinrichtung.Konsole]::GetConsoleMode($h, [ref]$m)) { # ohne ENABLE_QUICK_EDIT_MODE (0x40), mit ENABLE_EXTENDED_FLAGS (0x80), sonst wirkt es nicht $null = [InfEinrichtung.Konsole]::SetConsoleMode($h, [uint32](($m -band 0xFFBF) -bor 0x80)) } } catch { } } Disable-QuickEdit # Eingaben function Clear-Input { # Tasten, die vorher gedrückt wurden (etwa ein Enter zu viel), nicht als Antwort nehmen try { $Host.UI.RawUI.FlushInputBuffer() } catch { } } function Read-Taste([string]$frage, [string[]]$erlaubt) { # Ein Tastendruck genügt, kein Enter. Ohne echte Konsole (etwa per SSH) mit Enter. Clear-Input Write-Host " $frage " -NoNewline -ForegroundColor Yellow while ($true) { try { $c = [string]$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').Character } catch { $c = "$(Read-Host)".Trim() } if ($c -and $erlaubt -contains $c.ToLower()) { Write-Host $c; return $c.ToLower() } } } function Read-JaNein([string]$frage) { return (Read-Taste "$frage [j/n]" 'j', 'n', 'y') -ne 'n' } function Read-Zeile([string]$frage, [string]$hinweis = 'eintippen, dann Enter') { Clear-Input return "$(Read-Host " $frage ($hinweis)")".Trim() } function Read-Passwort { while ($true) { Clear-Input $pw = (New-Object Net.NetworkCredential('', (Read-Host ' Passwort (erscheint als Sternchen), dann Enter' -AsSecureString))).Password $pw2 = (New-Object Net.NetworkCredential('', (Read-Host ' Dasselbe Passwort noch einmal, dann Enter' -AsSecureString))).Password if (-not $pw) { Write-Warning 'Das Passwort darf nicht leer sein.'; continue } if ($pw -cne $pw2) { Write-Warning 'Die beiden Passwörter stimmen nicht überein. Bitte noch einmal.'; continue } return $pw } } # Server function Get-Server { # Nach dem Neustart läuft die lokale Kopie aus Request-Restart, ihr Server steht in server.txt # daneben. Sonst steht die Adresse in der eingetippten Zeile (irm /setup-windows.ps1 | iex): # äußerster Eintrag im Aufrufstapel (so in Windows PowerShell 5.1 geprüft, Konsole und -File). if ($Fortsetzung) { return (Get-Content (Join-Path (Split-Path $PSCommandPath) 'server.txt') -TotalCount 1).Trim() } $frames = @(Get-PSCallStack) [array]::Reverse($frames) foreach ($frame in $frames) { $text = "$($frame.Position.Text) $($frame.InvocationInfo.Line)" if ($text -match ('(https?://[^\s''"/]+)/' + [regex]::Escape($ScriptName))) { return $Matches[1] } } return 'https://inf.zone' } $Fortsetzung = [bool]($PSCommandPath -and (Test-Path (Join-Path (Split-Path $PSCommandPath) 'server.txt'))) $Server = Get-Server $DevcontainerUrl = "$Server/devcontainer.json" # Begrüßung Write-Host '' Write-Host ' ==============================================================' -ForegroundColor Cyan Write-Host ' Einrichtung für Inf-Einf-B' -ForegroundColor Cyan Write-Host ' ==============================================================' -ForegroundColor Cyan if ($Fortsetzung) { Write-Host '' Write-Host ' Nach dem Neustart geht die Einrichtung hier weiter. Lassen Sie dieses Fenster offen,' Write-Host ' bis „Fertig.“ erscheint.' } else { Write-Host '' Write-Host ' Dieses Skript richtet Ihren Rechner für die Übungen ein:' Write-Host ' 1. WSL mit Ubuntu (ein Linux, das in Windows läuft)' Write-Host ' 2. Docker, das darin die Kursumgebung startet' Write-Host ' 3. VS Code, den Editor' Write-Host ' 4. die Erweiterung „Dev Containers“ für VS Code' Write-Host ' 5. einen Ordner für Ihre Kursdateien' Write-Host ' Was davon schon da ist, nutzt es weiter. Das Ganze dauert etwa 15 bis 30 Minuten,' Write-Host ' oft mit einem Neustart von Windows zwischendurch.' Write-Host '' Write-Host ' So antworten Sie:' Write-Host ' - Fragen mit [j/n]: nur die Taste j (ja) oder n (nein) drücken, ohne Enter.' Write-Host ' - Namen und Passwörter: eintippen und mit Enter bestätigen.' Write-Host ' Lassen Sie dieses Fenster offen, bis „Fertig.“ erscheint. Bricht etwas ab, führen Sie die' Write-Host ' Zeile von der Website einfach noch einmal aus; was schon erledigt ist, wird übersprungen.' Write-Host '' Write-Host " (Geladen von $Server)" -ForegroundColor DarkGray Write-Host '' if (-not (Read-JaNein 'Einrichtung jetzt starten?')) { Write-Info 'Abgebrochen, es wurde nichts verändert.' return } } # Vorab: Passt der Rechner? Lieber jetzt eine klare Meldung als später ein Abbruch mittendrin. function Test-VirtualisierungAus { # $true nur, wenn sicher: Es läuft kein Hypervisor, und die Firmware meldet die Virtualisierung # (Intel VT-x, AMD-V) als aus. Läuft schon einer (etwa nach der WSL-Installation), meldet # Win32_Processor sie immer als aus, deshalb zuerst HypervisorPresent. Auf ARM-Rechnern ist sie # immer an, die Angabe dort nicht verlässlich. if ($IstArm) { return $false } try { if ((Get-CimInstance Win32_ComputerSystem).HypervisorPresent) { return $false } return [bool](@(Get-CimInstance Win32_Processor) | Where-Object { $_.VirtualizationFirmwareEnabled -eq $false }) } catch { return $false } } function Write-AnleitungVirtualisierung { $einstellung = '„Virtualization“ oder „Virtualisierung“' $modell = '' try { if ((Get-CimInstance Win32_Processor | Select-Object -First 1).Manufacturer -match 'AMD') { $einstellung = '„SVM Mode“ oder „AMD-V“ (manchmal „Virtualization“)' } else { $einstellung = '„Intel Virtualization Technology“ oder „VT-x“ (manchmal „Virtualization“)' } $cs = Get-CimInstance Win32_ComputerSystem $modell = "$($cs.Manufacturer) $($cs.Model)".Trim() } catch { } Write-Warning 'Auf Ihrem Rechner ist die Virtualisierung ausgeschaltet. WSL und Docker brauchen sie.' Write-Info 'Sie lässt sich in den Firmware-Einstellungen (BIOS/UEFI) einschalten, das dauert ein paar Minuten:' Write-Info ' 1. Speichern Sie, was offen ist. Öffnen Sie Start > Einstellungen > System > Wiederherstellung,' Write-Info ' bei „Erweiterter Start“ auf „Jetzt neu starten“. Dann: Problembehandlung > Erweiterte' Write-Info ' Optionen > UEFI-Firmwareeinstellungen > Neu starten. Fehlt der Eintrag: beim Einschalten' Write-Info ' F2, F10, Entf oder Esc drücken.' Write-Info ' 2. Suchen Sie dort (meist unter „Advanced“, „CPU“ oder „Security“) die Einstellung' Write-Info (' {0}' -f $einstellung) Write-Info ' und stellen Sie sie auf „Enabled“. Speichern und beenden, meist mit F10.' Write-Info ' 3. Führen Sie nach dem Start von Windows die Zeile von der Website noch einmal aus.' if ($modell) { Write-Info ('Finden Sie die Einstellung nicht, suchen Sie im Netz nach „{0} Virtualisierung BIOS“' -f $modell) Write-Info 'oder kommen Sie mit Ihrem Laptop ins Tutorium.' } else { Write-Info 'Finden Sie die Einstellung nicht, kommen Sie mit Ihrem Laptop ins Tutorium.' } } function Test-Internet { try { $null = Invoke-WebRequest -Uri $DevcontainerUrl -Method Head -UseBasicParsing -TimeoutSec 10; return $true } catch { return $false } } $ArmWeiter = $false if ($Fortsetzung) { # Direkt nach der Anmeldung ist das WLAN oft noch nicht verbunden (eduroam braucht etwas) if (-not (Test-Internet)) { Write-Info 'Warte auf die Internetverbindung ...' $online = $false foreach ($i in 1..12) { Start-Sleep -Seconds 5 if (Test-Internet) { $online = $true; break } } while (-not $online) { Write-Warning 'Keine Verbindung zum Internet. Verbinden Sie den Rechner mit dem WLAN.' if (-not (Read-JaNein 'Noch einmal versuchen?')) { Write-Info 'Führen Sie die Zeile von der Website noch einmal aus, sobald Sie online sind.' return } $online = Test-Internet } } $ArmWeiter = $IstArm # die Frage dazu kam schon vor dem Neustart } else { if (Test-VirtualisierungAus) { Write-AnleitungVirtualisierung return } # Als Administrator mit einem anderen Konto gestartet: Linux, Neustart-Fortsetzung und VS Code # landeten dann bei diesem Konto statt bei dem, mit dem man angemeldet ist $ich = [Security.Principal.WindowsIdentity]::GetCurrent().Name $angemeldet = $null try { $angemeldet = (Get-CimInstance Win32_ComputerSystem).UserName } catch { } if ($angemeldet -and $ich -and $angemeldet -ne $ich) { Write-Warning ('Dieses Fenster läuft unter dem Konto {0}, angemeldet sind Sie als {1}.' -f $ich, $angemeldet) Write-Warning 'Dann richtet das Skript alles für das falsche Konto ein. Schließen Sie das Fenster, öffnen' Write-Warning 'Sie PowerShell normal (nicht „Als Administrator ausführen“) und fügen Sie die Zeile dort ein.' if (-not (Read-JaNein 'Trotzdem weitermachen? (empfohlen: n)')) { return } } if ($IstArm) { # Das Image der Kursumgebung gibt es seit Oktober 2026 auch für arm64 (ghcr.io/inf-zone/codespace), # auf Snapdragon aber noch nicht erprobt; QEMU bleibt als Rückfall für amd64 (unten) Write-Warning 'Ihr Rechner hat einen ARM-Prozessor (etwa Snapdragon). Die Kursumgebung gibt es auch für' Write-Warning 'ARM, auf Laptops mit Snapdragon haben wir sie aber noch nicht selbst ausprobiert.' Write-Info 'Klappt etwas nicht, ist die Online-Variante der Ausweg (GitHub Codespace, Anleitung auf der Website).' if (-not (Read-JaNein 'Hier einrichten?')) { Write-Info 'Abgebrochen, es wurde nichts verändert.' return } $ArmWeiter = $true } try { # gemessen am 9. Oktober 2026: 18,7 GB (Ubuntu mit Kursumgebung 8,9 GB, VS Code 2 GB, WSL 1,1 GB) $frei = (Get-PSDrive ($env:SystemDrive.TrimEnd(':'))).Free / 1GB if ($frei -lt 25) { Write-Warning ('Auf Laufwerk {0} sind nur noch {1:N0} GB frei. Die Einrichtung braucht etwa 20 GB, danach' -f $env:SystemDrive, $frei) Write-Warning 'wollen Sie auch noch arbeiten. Räumen Sie besser vorher auf (Start > Einstellungen >' Write-Warning 'System > Speicher).' if (-not (Read-JaNein 'Trotzdem weitermachen?')) { return } } } catch { } } # 1. WSL 2 mit Linux function Test-Wsl { # Nur das aktuelle WSL (2.x) kennt --version; das alte, in Windows # eingebaute wsl.exe gibt dann nur die Hilfe aus und endet mit Fehler. if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { return $false } $ErrorActionPreference = 'Continue' $null = & wsl.exe --version 2>&1 return ($LASTEXITCODE -eq 0) } function Get-WslDistros { # Alle Distributionen mit Version und Standard-Markierung, ohne die von Docker Desktop. # Die Spalte STATE ist je nach Sprache verschieden, deshalb nur Name (vorn) und Version (hinten). $ErrorActionPreference = 'Continue' $env:WSL_UTF8 = '1' # sonst gibt wsl.exe UTF-16 aus $lines = & wsl.exe --list --verbose 2>$null if ($LASTEXITCODE -ne 0) { return } # ohne Distribution kommt hier nur eine Meldung foreach ($l in @($lines | Select-Object -Skip 1)) { if ("$l" -match '^\s*(\*?)\s*(\S+)\s.*\s([12])\s*$' -and $Matches[2] -notlike 'docker-desktop*') { [pscustomobject]@{ Name = $Matches[2]; Standard = ($Matches[1] -eq '*'); Version = [int]$Matches[3] } } } } function Test-RestartPending { # Windows wartet auf einen Neustart, etwa nach einer WSL-Installation in einem früheren Lauf return (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') } function Register-Ubuntu([bool]$neustartFaellig) { Write-Info 'Installiere Ubuntu, das dauert einige Minuten ...' Write-Abstand # Start-Process statt &: Ausgaben direkt auf die Konsole, nicht in den Rückgabewert Start-Process -FilePath wsl.exe -ArgumentList '--install', '-d', 'Ubuntu', '--no-launch' -NoNewWindow -Wait Write-Abstand if (-not $neustartFaellig -and -not (Get-WslDistros | Where-Object Name -eq 'Ubuntu')) { # Ohne Microsoft Store (gesperrt oder kaputt): direkt herunterladen. Nicht, solange WSL erst # nach einem Neustart läuft: Dann scheitert auch das, und es geht ohnehin nach dem Neustart weiter. Write-Info 'Zweiter Versuch über einen anderen Download-Weg ...' Write-Abstand Start-Process -FilePath wsl.exe -ArgumentList '--install', '-d', 'Ubuntu', '--no-launch', '--web-download' -NoNewWindow -Wait Write-Abstand } if (-not (Get-WslDistros | Where-Object Name -eq 'Ubuntu')) { # Ältere Ubuntu-Pakete registrieren sich erst beim ersten Start $launcher = "$env:LOCALAPPDATA\Microsoft\WindowsApps\ubuntu.exe" if (Test-Path $launcher) { Start-Process -FilePath $launcher -ArgumentList 'install', '--root' -NoNewWindow -Wait; Write-Abstand } } return [bool](Get-WslDistros | Where-Object Name -eq 'Ubuntu') } function Invoke-AlsRoot([string]$sh, [string[]]$argumente) { # Shell-Befehl als root in $Distro. Werte nur als Argumente (Passwörter als Base64), keine # doppelten Anführungszeichen im Befehl: PowerShell 5.1 gäbe sie kaputt an wsl.exe weiter. $ErrorActionPreference = 'Continue' $ausgabe = & wsl.exe -d $Distro -u root --exec sh -c $sh sh @argumente 2>&1 $ok = ($LASTEXITCODE -eq 0) if ($ausgabe) { $ausgabe | Out-Host; Write-Abstand } return $ok } function Set-LinuxAccount { # Ergebnis: 'ok', 'neustart' (WSL läuft noch nicht) oder 'fehler' $ErrorActionPreference = 'Continue' $uid = & wsl.exe -d $Distro --exec id -u 2>$null if ($LASTEXITCODE -ne 0) { return 'neustart' } if ("$uid".Trim() -ne '0') { # Es gibt schon ein Konto. Sein Passwort braucht die Einrichtung nicht (Docker kommt als root). $name = "$(& wsl.exe -d $Distro --exec id -un 2>$null)".Trim() Write-Ok ('In {0} gibt es schon Ihr Konto „{1}“.' -f $Distro, $name) return 'ok' } # Noch kein Konto: direkt als root anlegen, statt Ubuntu beim ersten Start fragen zu lassen Write-Info "Legen Sie jetzt ein Konto für $Distro (das Linux aus Schritt 1) an. Nehmen Sie ein neues" Write-Info 'Passwort, nicht Ihr Windows-Passwort, und speichern Sie es, etwa in einem Passwortmanager.' Write-Info 'Für die Übungen brauchen Sie es normalerweise nicht.' # Vorschlag aus dem Windows-Benutzernamen: ä → ae usw. (als Zeichencodes, damit es auch bei # verstümmelt geladenem Skript stimmt), Leerzeichen und andere Zeichen weg, höchstens 32 Zeichen $default = $env:USERNAME.ToLower().Replace([string][char]0xE4, 'ae').Replace([string][char]0xF6, 'oe').Replace([string][char]0xFC, 'ue').Replace([string][char]0xDF, 'ss') $default = $default -replace '[^a-z0-9_-]', '' if ($default.Length -gt 32) { $default = $default.Substring(0, 32) } if ($default -notmatch '^[a-z_]' -or $default -eq 'root') { $default = '' } Write-Host '' while ($true) { if ($default) { $user = Read-Zeile 'Benutzername für Linux' ('Enter übernimmt „{0}“, sonst eintippen und Enter' -f $default) } else { $user = Read-Zeile 'Benutzername für Linux' } if (-not $user) { $user = $default } if ($user -cmatch '^[a-z_][a-z0-9_-]{0,31}$' -and $user -ne 'root') { break } Write-Warning 'Nur Kleinbuchstaben ohne ä/ö/ü, Ziffern, _ und -, keine Leerzeichen;' Write-Warning 'z. B. „susi“ statt „Susi“.' } $pw = Read-Passwort # Gibt es das Konto schon (abgebrochener Lauf), nur Passwort und Standardbenutzer setzen; # Systemkonten (UID unter 1000 oder nobody) nie. $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${user}:$pw")) $sh = 'if id -u $1 >/dev/null 2>&1; then test $(id -u $1) -ge 1000 -a $(id -u $1) -lt 60000 || exit 1; ' + 'else useradd -m -s /bin/bash -G sudo $1 || exit 1; fi; ' + 'echo $2 | base64 -d | chpasswd && printf ''\n[user]\ndefault=%s\n'' $1 >> /etc/wsl.conf' $ok = Invoke-AlsRoot $sh @($user, $b64) # Neu starten, damit der Standardbenutzer aus /etc/wsl.conf gilt & wsl.exe --terminate $Distro | Out-Null if ($ok) { Write-Ok ('Konto „{0}“ angelegt.' -f $user); return 'ok' } Write-Warning 'Das Konto ließ sich nicht anlegen. Führen Sie die Zeile von der Website noch einmal aus und versuchen Sie' Write-Warning 'einen anderen Benutzernamen.' return 'fehler' } function Request-Restart { # Nach der Anmeldung läuft die Einrichtung einmal automatisch weiter, aus einer lokalen Kopie # (-File). Nicht "-Command irm … | iex": Das hält Microsoft Defender für Schadsoftware # (Trojan:Win32/Commando.A!ml) und blockiert es. Die Kopie bekommt ein UTF-8-BOM, sonst liest # PowerShell 5.1 sie als ANSI; server.txt sagt Get-Server, woher sie kam. Ganz am Ende wieder weg. # RunOnce-Einträge dürfen höchstens 260 Zeichen lang sein. New-Item -ItemType Directory -Path $LocalDir -Force | Out-Null $copy = Join-Path $LocalDir $ScriptName $bytes = (Invoke-WebRequest -Uri "$Server/$ScriptName" -UseBasicParsing).RawContentStream.ToArray() [IO.File]::WriteAllBytes($copy, [byte[]](@(0xEF, 0xBB, 0xBF) + $bytes)) Set-Content -Path (Join-Path $LocalDir 'server.txt') -Value $Server -Encoding ASCII $runOnce = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' if (-not (Test-Path $runOnce)) { New-Item -Path $runOnce | Out-Null } # fehlt auf manchen Systemen $cmd = "powershell.exe -NoProfile -NoExit -ExecutionPolicy Bypass -File `"$copy`"" New-ItemProperty -Path $runOnce -Name 'inf-einrichtung' -Value $cmd -PropertyType String -Force | Out-Null Write-Host '' Write-Host ' Windows muss jetzt neu starten. Speichern Sie vorher, was in anderen Programmen offen ist.' -ForegroundColor Yellow Write-Host ' Melden Sie sich danach wie gewohnt an; nach einigen Sekunden öffnet sich ein neues' -ForegroundColor Yellow Write-Host ' Fenster, und die Einrichtung geht weiter. Kommt keins: Zeile von der Website noch' -ForegroundColor Yellow Write-Host ' einmal ausführen.' -ForegroundColor Yellow if (Read-JaNein 'Jetzt neu starten?') { Restart-Computer } else { Write-Info 'Starten Sie Windows neu, wann es Ihnen passt; danach geht es von selbst weiter.' Write-Info 'Dieses Fenster können Sie jetzt schließen.' } } Write-Step 'Schritt 1 von 5: WSL und Linux' Write-Info 'Prüfe, was schon installiert ist. Das kann ein, zwei Minuten dauern, ohne dass sich hier' Write-Info 'etwas tut; bitte warten.' $freshWsl = $false if (-not (Test-Wsl)) { Write-Info 'Installiere WSL. Gleich fragt Windows, ob Sie Änderungen zulassen: Klicken Sie auf „Ja“.' Write-Info 'Dann öffnet sich ein zweites Fenster, das sich nach einigen Minuten von selbst schließt.' Write-Info 'Schließen Sie keins der beiden Fenster.' $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) # Ubuntu kommt danach ohne Administratorrechte, sonst gehörte es dem Administratorkonto try { if ($isAdmin) { & wsl.exe --install --no-distribution $code = $LASTEXITCODE Write-Abstand } else { $code = (Start-Process -FilePath wsl.exe -ArgumentList '--install', '--no-distribution' -Verb RunAs -Wait -PassThru).ExitCode } } catch { Write-Warning 'Ohne Ihre Zustimmung („Ja“) kann WSL nicht installiert werden.' Write-Warning 'Führen Sie die Zeile von der Website noch einmal aus und klicken Sie diesmal auf „Ja“.' return } if ($code -notin 0, 3010) { Write-Warning "Die Installation von WSL ist fehlgeschlagen (Code $code)." Write-Info 'Installieren Sie alle Updates (Start > Einstellungen > Windows Update) und führen Sie' Write-Info 'danach die Zeile von der Website noch einmal aus. Sonst: Foto dieses Fensters ins Tutorium.' return } if ($code -eq 3010) { # Windows meldet selbst, dass es neu starten muss: Linux erst danach Request-Restart return } $freshWsl = $true Write-Ok 'WSL ist installiert.' } $linux = 'neustart' # 'ok', 'neustart' (WSL läuft noch nicht) oder 'fehler' if (Test-Wsl) { # Vorhandene Distribution nutzen: die Standard-Distribution (in die VS Code auch Docker # installiert), sonst Ubuntu; erst wenn es keine gibt, Ubuntu installieren $distros = @(Get-WslDistros) $wahl = $distros | Where-Object Standard | Select-Object -First 1 if (-not $wahl) { $wahl = $distros | Where-Object Name -eq 'Ubuntu' | Select-Object -First 1 } if (-not $wahl) { $wahl = $distros | Select-Object -First 1 } if ($wahl) { $Distro = $wahl.Name Write-Ok "Linux ist schon da: $Distro. Das Skript nutzt es." if (-not $wahl.Standard) { & wsl.exe --set-default $Distro | Out-Null } # Standard war nur Docker Desktop if ($wahl.Version -ne 2) { Write-Info "$Distro läuft noch mit WSL 1, Docker braucht WSL 2. Die Umstellung dauert einige" Write-Info 'Minuten; Ihre Dateien darin bleiben erhalten.' if (-not (Read-JaNein "$Distro jetzt auf WSL 2 umstellen?")) { Write-Warning 'Ohne WSL 2 läuft Docker nicht, deshalb hört das Skript hier auf. Zum Umstellen' Write-Warning 'führen Sie die Zeile später noch einmal aus.' return } $ErrorActionPreference = 'Continue' & wsl.exe --set-version $Distro 2 | Out-Host $code = $LASTEXITCODE $ErrorActionPreference = 'Stop' Write-Abstand if ($code -ne 0) { Write-Warning "$Distro ließ sich nicht auf WSL 2 umstellen (Code $code)." Write-Info 'Starten Sie Windows neu und führen Sie die Zeile noch einmal aus. Sonst: Foto dieses' Write-Info 'Fensters ins Tutorium.' return } } $linux = Set-LinuxAccount } elseif (Register-Ubuntu ($freshWsl -or (Test-RestartPending))) { $Distro = 'Ubuntu' $linux = Set-LinuxAccount } } if ($linux -eq 'neustart' -and ($freshWsl -or (Test-RestartPending))) { Request-Restart return } if ($linux -ne 'ok') { Write-Warning 'Linux (WSL) ist nicht fertig eingerichtet, deshalb hört das Skript hier auf.' Write-Info 'Starten Sie Windows einmal neu und führen Sie danach die Zeile von der Website noch einmal' Write-Info 'aus. Klappt es dann immer noch nicht, kommen Sie mit einem Foto dieses Fensters ins Tutorium.' return } Write-Ok "WSL und $Distro sind bereit." # 2. Docker in Linux Write-Step 'Schritt 2 von 5: Docker' $dockerDesktop = Test-Path "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe" if ($dockerDesktop) { Write-Ok 'Docker Desktop ist installiert; VS Code nutzt es.' } else { $ErrorActionPreference = 'Continue' $linuxUser = "$(& wsl.exe -d $Distro --exec id -un 2>$null)".Trim() $null = & wsl.exe -d $Distro -u root --exec sh -c 'command -v docker' 2>$null if ($LASTEXITCODE -eq 0) { Write-Ok "Docker ist in $Distro schon installiert." } else { $null = & wsl.exe -d $Distro -u root --exec sh -c 'command -v apt-get' 2>$null if ($LASTEXITCODE -ne 0) { # Vorhandene Standard-Distribution ohne apt (Arch, Fedora, Alpine …) Write-Warning "Ihr Linux $Distro ist kein Ubuntu oder Debian; Docker kann das Skript dort nicht installieren." Write-Info "Installieren Sie Docker in $Distro selbst und führen Sie die Zeile danach noch einmal aus." return } Write-Info "Installiere Docker in $Distro, das dauert ein paar Minuten ..." # In einem neuen Ubuntu sperren apt-daily und unattended-upgrades apt oft direkt nach dem Start: # warten (Lock::Timeout) und mehrmals versuchen, nur die Meldungen des letzten Versuchs zeigen. # docker-buildx gibt es nicht in jedem Ubuntu; ohne geht es auch. Auf ARM dazu QEMU: Docker # lädt dort die arm64-Fassung der Kursumgebung, ein amd64-Image (etwa eine alte .devcontainer.json) # läuft so aber auch. $apt = 'apt-get -o DPkg::Lock::Timeout=120' $pakete = $(if ($ArmWeiter) { 'docker.io qemu-user-static binfmt-support' } else { 'docker.io' }) $sh = ('export DEBIAN_FRONTEND=noninteractive; for i in 1 2 3 4 5 6; do ' + 'out=$({0} update -qq 2>&1 >/dev/null && {0} install -y -qq {1} 2>&1 >/dev/null) && break; ' + 'test $i = 6 && {{ echo $out; exit 1; }}; sleep 20; done; ' + '{{ {0} install -y -qq docker-buildx >/dev/null 2>&1; true; }}') -f $apt, $pakete if (-not (Invoke-AlsRoot $sh @())) { Write-Warning 'Docker ließ sich nicht installieren. Prüfen Sie die Internetverbindung und' Write-Warning 'führen Sie die Zeile von der Website noch einmal aus. Sonst: Foto dieses Fensters ins Tutorium.' return } } # Konto in die Gruppe docker; Docker startet mit Linux (systemd, in neuen Ubuntu schon an) $sh = 'usermod -aG docker $1 && { test $(ps -p 1 -o comm=) = systemd || printf ''\n[boot]\nsystemd=true\n'' >> /etc/wsl.conf; } && ' + '{ systemctl enable docker >/dev/null 2>&1; true; }' $null = Invoke-AlsRoot $sh @($linuxUser) & wsl.exe --terminate $Distro | Out-Null # damit Gruppe und systemd gelten $laeuft = $false foreach ($i in 1..30) { # bis zu einer Minute: Linux startet neu, auf langsamen Laptops dauert das $null = & wsl.exe -d $Distro --exec docker info 2>$null if ($LASTEXITCODE -eq 0) { $laeuft = $true; break } Start-Sleep -Seconds 2 } $ErrorActionPreference = 'Stop' if (-not $laeuft) { Write-Warning 'Docker ist installiert, startet aber nicht. Starten Sie Windows neu und führen Sie' Write-Warning 'danach die Zeile von der Website noch einmal aus. Sonst: Foto ins Tutorium.' return } Write-Ok "Docker läuft in $Distro." } # 3. VS Code function Find-Code { # PATH neu einlesen, damit ein gerade installiertes VS Code gefunden wird $env:Path = $env:Path + ';' + [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') $cmd = Get-Command code.cmd -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } foreach ($p in "$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\code.cmd", "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd") { if (Test-Path $p) { return $p } } return $null } Write-Step 'Schritt 3 von 5: VS Code' $codeCmd = Find-Code if ($codeCmd) { Write-Ok 'VS Code ist schon installiert.' } else { if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { # Teil von Windows 11, registriert sich aber erst einige Zeit nach der ersten Anmeldung (neue # Laptops); so anstoßen, wie Microsoft es beschreibt (learn.microsoft.com, „Use WinGet“) try { Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe } catch { } } if (Get-Command winget -ErrorAction SilentlyContinue) { Write-Info 'Installiere VS Code, das dauert ein paar Minuten ...' Write-Abstand & winget install --id Microsoft.VisualStudioCode --exact --source winget --scope user --silent ` --accept-package-agreements --accept-source-agreements Write-Abstand $codeCmd = Find-Code } if (-not $codeCmd) { Write-Warning 'VS Code ließ sich nicht automatisch installieren. Laden Sie es von' Write-Warning 'https://code.visualstudio.com/ („Download for Windows“), installieren Sie es und' Write-Warning 'führen Sie die Zeile von der Website noch einmal aus.' return } Write-Ok 'VS Code ist installiert.' } # 4. Dev-Containers-Erweiterung, bei mehreren Profilen ins gewählte function Get-CodeProfiles { # Eigene Profile neben dem Standardprofil, aus der Ablage von VS Code (Name und Ordner) $f = Join-Path $env:APPDATA 'Code\User\globalStorage\storage.json' if (-not (Test-Path $f)) { return } try { $json = [IO.File]::ReadAllText($f, [Text.Encoding]::UTF8) | ConvertFrom-Json $json.userDataProfiles | Where-Object { $_.name -and $_.location } } catch { } } function Set-CodeSetting([string]$datei) { # dev.containers.executeInWSL: VS Code nutzt Docker in WSL. settings.json darf Kommentare # enthalten, deshalb als Text einfügen statt per JSON neu schreiben. Ist der Schlüssel schon da # (auch mit false), bleibt er. $zeile = '"dev.containers.executeInWSL": true' $text = '' if (Test-Path $datei) { $text = [IO.File]::ReadAllText($datei, [Text.Encoding]::UTF8) } if ($text -match '"dev\.containers\.executeInWSL"') { return } if ($text.Trim() -match '^(\{\s*\})?$') { $text = "{`n $zeile`n}`n" } else { $i = $text.IndexOf('{') if ($i -lt 0) { return } $text = $text.Insert($i + 1, "`n $zeile,") } New-Item -ItemType Directory -Path (Split-Path $datei) -Force | Out-Null [IO.File]::WriteAllText($datei, $text, (New-Object Text.UTF8Encoding $false)) } Write-Step 'Schritt 4 von 5: Erweiterung „Dev Containers“' $codeProfile = $null $profiles = @(Get-CodeProfiles) if ($profiles.Count -gt 0 -and $profiles.Count -le 8) { Write-Info 'In VS Code gibt es mehrere Profile. In welchem möchten Sie für den Kurs arbeiten?' Write-Info ' 1 Standardprofil (Default)' for ($i = 0; $i -lt $profiles.Count; $i++) { Write-Info (" {0} {1}" -f ($i + 2), $profiles[$i].name) } Write-Info 'Im Zweifel: 1.' $nr = [int](Read-Taste ("Taste 1 bis {0} drücken:" -f ($profiles.Count + 1)) (1..($profiles.Count + 1) | ForEach-Object { "$_" })) if ($nr -gt 1) { $codeProfile = $profiles[$nr - 2] } } elseif ($profiles.Count -gt 8) { Write-Info 'In VS Code gibt es viele Profile; die Erweiterung kommt ins Standardprofil.' } $profileArgs = @() $settings = Join-Path $env:APPDATA 'Code\User\settings.json' if ($codeProfile) { $profileArgs = @('--profile', $codeProfile.name) $settings = Join-Path $env:APPDATA ('Code\User\profiles\' + $codeProfile.location + '\settings.json') } $profileName = $(if ($codeProfile) { 'Profil „{0}“' -f $codeProfile.name } else { 'Standardprofil' }) # Immer mit --force: Die Liste eines Profils kann die Erweiterung nennen, obwohl ihre Dateien fehlen # (so gesehen nach einem Zurücksetzen); VS Code meldet dann eine nicht vorhandene Datei. $ErrorActionPreference = 'Continue' & $codeCmd --install-extension $ExtensionId --force @profileArgs | Out-Host $code = $LASTEXITCODE Write-Abstand if ($code -eq 0) { Write-Ok "Die Erweiterung ist installiert ($profileName)." } else { Write-Warning 'Die Erweiterung ließ sich nicht installieren. Später in VS Code: Strg+Shift+X drücken,' Write-Warning '„Dev Containers“ suchen und auf „Install“ klicken.' } $ErrorActionPreference = 'Stop' if (-not $dockerDesktop) { Set-CodeSetting $settings } # 5. Ordner wählen, .devcontainer.json ablegen function Select-Folder { # Der Windows-Dialog braucht einen STA-Thread; sonst nach dem Pfad fragen if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') { # Anführungszeichen weg: „Als Pfad kopieren“ im Explorer setzt welche $path = (Read-Zeile 'Pfad zum Ordner für die Kursdateien').Trim('"') if ($path) { return $path } else { return $null } } Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Application]::EnableVisualStyles() $dlg = New-Object System.Windows.Forms.FolderBrowserDialog $dlg.Description = 'Ordner für die Kursdateien wählen, z. B. einen neuen Ordner „inf“ in Ihrem Benutzerordner.' $dlg.SelectedPath = $env:USERPROFILE $dlg.ShowNewFolderButton = $true # Unsichtbares Fenster als Besitzer, damit der Dialog nicht hinter der Konsole aufgeht $owner = New-Object System.Windows.Forms.Form -Property @{ TopMost = $true } try { if ($dlg.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) { return $dlg.SelectedPath } return $null } finally { $owner.Dispose() $dlg.Dispose() } } function Test-Unter([string]$pfad, [string]$basis) { if (-not $basis) { return $false } $basis = $basis.TrimEnd('\') + '\' return ($pfad + '\').StartsWith($basis, [StringComparison]::OrdinalIgnoreCase) } Write-Step 'Schritt 5 von 5: Ordner für die Kursdateien' Write-Info 'Gleich öffnet sich ein Fenster. Klicken Sie dort gleich auf „Neuen Ordner erstellen“,' Write-Info 'tippen Sie „inf“, drücken Sie Enter und klicken Sie auf „OK“. Bitte nicht in Downloads,' Write-Info 'Desktop oder Dokumente anlegen.' $downloads = $null try { $downloads = (New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path } catch { } while ($true) { $dir = Select-Folder if (-not $dir) { Write-Warning 'Kein Ordner gewählt. Führen Sie die Zeile von der Website noch einmal aus;' Write-Warning 'was schon erledigt ist, wird übersprungen.' return } $dir = [IO.Path]::GetFullPath($dir) $grund = $null if (@($env:OneDrive, $env:OneDriveConsumer, $env:OneDriveCommercial) | Where-Object { Test-Unter $dir $_ }) { $grund = 'wird von OneDrive synchronisiert; das kann beim Arbeiten Dateien sperren.' } elseif (Test-Unter $dir $downloads) { $grund = 'liegt im Download-Ordner, den manche Programme aufräumen.' } if (-not $grund) { break } Write-Warning "Der Ordner $dir" Write-Warning $grund if (Read-JaNein 'Diesen Ordner trotzdem nehmen? (besser n: anderen Ordner wählen)') { break } } New-Item -ItemType Directory -Path $dir -Force | Out-Null Write-Ok "Ordner: $dir" $target = Join-Path $dir '.devcontainer.json' try { $neu = (Invoke-WebRequest -Uri $DevcontainerUrl -UseBasicParsing).RawContentStream.ToArray() } catch { Write-Warning ('Die Kursdatei ließ sich nicht von {0} laden. Prüfen Sie die' -f $Server) Write-Warning 'Internetverbindung und führen Sie die Zeile von der Website noch einmal aus.' return } if (Test-Path $target) { $alt = [IO.File]::ReadAllBytes($target) if ([Convert]::ToBase64String($alt) -eq [Convert]::ToBase64String($neu)) { Write-Ok '.devcontainer.json ist schon da und aktuell.' $neu = $null } else { Write-Info 'In diesem Ordner liegt schon eine andere .devcontainer.json (etwa eine ältere oder die von' Write-Info 'cs50.dev, ohne die Einrichtung für den Kurs). Für den Kurs brauchen Sie die des Kurses.' if (Read-JaNein 'Durch die des Kurses ersetzen? Die alte bleibt als .devcontainer.json.bak.') { Copy-Item $target "$target.bak" -Force } else { Write-Info 'Die vorhandene Datei bleibt.' $neu = $null } } } if ($neu) { [IO.File]::WriteAllBytes($target, $neu) Write-Ok '.devcontainer.json gespeichert.' } # Fertig # Kopie für den Neustart wird nicht mehr gebraucht if (Test-Path $LocalDir) { Remove-Item $LocalDir -Recurse -Force -ErrorAction SilentlyContinue } Write-Host '' Write-Host ' Fertig. Jetzt noch ein letzter Schritt in VS Code:' -ForegroundColor Green Write-Host '' if ($dockerDesktop) { Write-Host ' - Starten Sie zuerst Docker Desktop und warten Sie, bis es läuft.' } Write-Host ' - Fragt VS Code, ob Sie den Autoren der Dateien vertrauen: „Yes, I trust the authors“.' Write-Host ' - Klicken Sie unten rechts auf „Reopen in Container“. Ist der Hinweis verschwunden:' Write-Host ' Strg+Shift+P drücken, „Reopen in Container“ tippen und den Eintrag auswählen.' Write-Host ' - Beim ersten Mal lädt VS Code die Kursumgebung herunter; das dauert einige Minuten.' Write-Host ' Fertig ist es, wenn oben im Fenstertitel „[Dev Container]“ steht.' Write-Host '' Write-Host ' VS Code legt sich gleich über dieses Fenster. Die Schritte stehen auch in der Anleitung' Write-Host ' auf der Website (Schritt 4). Dieses Fenster können Sie schließen, sobald VS Code offen ist.' Write-Host '' if (Read-JaNein 'VS Code jetzt öffnen?') { # Die Erweiterung erkennt .devcontainer.json im Ordner und bietet „Reopen in Container“ selbst an & $codeCmd @profileArgs $dir } else { Write-Info ('Öffnen Sie VS Code später selbst, dann „Datei“ > „Ordner öffnen“ > {0}.' -f $dir) } }