# SignalK Universal Installer (v2) - Windows (WSL2 + Podman Machine) bootstrap. # # We install the WSL2 platform (no user-facing distro), install the Podman CLI, # and let `podman machine` create and own its own Linux VM - then run the # regular Linux installer INSIDE that VM via `podman machine ssh`. This mirrors # the macOS installer exactly. The container stack never runs on Windows # directly; it runs in the Podman Machine VM. # # Why this model (and NOT `wsl --install -d Debian`): podman machine ships a # Linux VM that already has systemd as PID 1 and rootless podman set up, so # there is no OOBE username prompt, no /etc/wsl.conf systemd-enablement, and no # default-user provisioning to manage. The earlier Debian-distro approach had # to do all of that by hand and was fragile. This is also what the v1 installer # did on Windows. The Linux installer is distro-portable (apt on Debian, dnf on # Fedora, pre-baked packages on the podman-machine image), so it runs cleanly # in the VM. # # Requirements: # - Windows 11 (or Server 2022) with WSL2. # - Hardware virtualization. In a guest VM, nested virtualization must be # enabled on the host (see Show-VirtualizationHelp / docs). # # Limitations: # - Native Windows (no WSL) is not supported (deferred per design doc). # - USB serial passthrough requires usbipd-win - link in docs/installation.md. param( [string]$MachineName = 'signalk', [int]$MachineMemoryMB = 0, # 0 = auto-size from host RAM (see below) [int]$MachineCpus = 2, [switch]$NoPause, # skip the "press Enter to close" pause at the end [switch]$NoPrompt, # don't ask for vessel identity / admin login [string]$InstallerVersion, # Release channel, mirroring installer/linux/install.sh. ValidateSet accepts # the same three spellings install.sh does and rejects anything else with a # native PowerShell error, so the VM never sees a value bash would refuse. [ValidateSet('release', 'master', 'dev')] [string]$Channel = 'release', # UDP ports carrying NMEA data INTO the stack (a Yacht Devices / Actisense # gateway streaming to the boat PC, typically 2000 or 10110). Opt-in: the # right ports are a property of the boat's gateway, not something the # installer can detect, and opening ports nobody uses is not free. # .\install.ps1 -NmeaUdpPorts 2000,10110 [int[]]$NmeaUdpPorts = @(), [string]$InstallerBaseUrl = 'https://dirkwa.github.io/signalk-universal-installer' ) if (-not $InstallerVersion) { $InstallerVersion = if ($env:INSTALLER_VERSION) { $env:INSTALLER_VERSION } else { '0.0.0-scaffold' } } # Resolve the channel to a base URL, matching installer/linux/install.sh:51-61: # the site publishes the latest RELEASE at its root and master under /dev, and # an explicit base URL beats the channel (the escape hatch for local checkouts, # mirrors and CI). # # $InstallerBaseUrl has a DEFAULT, so it is never empty and `if (-not $x)` # cannot tell "caller passed one" from "fell back to the default". # $PSBoundParameters.ContainsKey is the test that can - it is the PowerShell # analogue of bash's ${VAR:-default} precedence, and without it -Channel would # silently lose to the default base URL on every run. # # Lowercase the channel before it goes anywhere. PowerShell's ValidateSet is # CASE-INSENSITIVE and passes the caller's original spelling through, so # `-Channel MASTER` validates here and then matches no branch of install.sh's # case statement, aborting the install inside the VM with a message about a # value the operator believed was legal. $SkSiteRoot = 'https://dirkwa.github.io/signalk-universal-installer' $Channel = $Channel.ToLowerInvariant() $SkExplicitBase = $PSBoundParameters.ContainsKey('InstallerBaseUrl') if (-not $SkExplicitBase) { $InstallerBaseUrl = if ($Channel -eq 'release') { $SkSiteRoot } else { "$SkSiteRoot/dev" } } $ErrorActionPreference = 'Stop' # The TCP ports the stack may serve on, in ONE place: 80/443 standard web, # 3000/3443 the declined-standard fallback, 3003 updater, 3004 doctor. Used both # to open the firewall at install time and (interpolated into the generated # signalk-run.ps1) to remove those rules on uninstall, so the two never drift. $SignalkPorts = @(80, 443, 3000, 3443, 3003, 3004) # NOTE on output encoding: wsl.exe emits UTF-16LE, so a captured `& wsl ...` # comes back with a NUL between every character - Get-VersionFrom strips those # NULs to parse the version. We deliberately do NOT set # [Console]::OutputEncoding globally: that would make PowerShell decode every # child process as UTF-16, which mangles the UTF-8 streaming output of # `podman` (and its progress bars) into garbage. So we only capture+clean wsl's # output, and let podman write straight to the console. function Info($msg) { Write-Host "[i] $msg" -ForegroundColor Cyan } function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green } function Warn($msg) { Write-Warning $msg } function Section($msg) { Write-Host ""; Write-Host "== $msg ==" -ForegroundColor White } # Hold the window open before the script exits, so the user can read the result # - `iex` runs us in their session, and `exit` would otherwise close the window # instantly. Only pause when there's an interactive user AND a console to read a # key from; skip it under -NoPause, when stdin is redirected/piped, or in a # non-interactive host (CI, scheduled task) where it would hang forever. function Wait-BeforeExit { if ($NoPause) { return } if (-not [Environment]::UserInteractive) { return } if ([Console]::IsInputRedirected) { return } try { Write-Host "" Write-Host "Press any key to close..." -ForegroundColor DarkGray [void]$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } catch { # No usable console (e.g. ISE / redirected host) - don't block. $null = $_ } } function Die($msg) { Write-Error $msg; Wait-BeforeExit; exit 1 } # Run a native command best-effort: swallow stdout+stderr and NEVER throw, even # under $ErrorActionPreference='Stop' (where a native command writing to stderr # can surface as a terminating NativeCommandError). Returns the exit code. Use # for flaky Store-backed calls like `wsl --install`/`wsl --update`, whose 403 # from the msstore backend must not abort the installer. function Invoke-BestEffort { param([Parameter(Mandatory)][string]$Exe, [string[]]$Arguments) try { & $Exe @Arguments *>$null return $LASTEXITCODE } catch { # native stderr surfaced as an error record under Stop, or the exe # wasn't found - either way report failure (don't trust a stale # $LASTEXITCODE from an earlier command). return 1 } } # Run a native command with its output STREAMING to the console (so progress # bars show), but without $ErrorActionPreference='Stop' turning the command's # stderr writes into a terminating NativeCommandError. `podman machine init` # writes normal progress ("Getting image source signatures", "Copying blob") to # stderr, which under Stop would abort the script mid-pull. We redirect stderr to # stdout and force Continue for the call, then return the real exit code so the # caller decides success/failure. function Invoke-Streaming { param([Parameter(Mandatory)][string]$Exe, [string[]]$Arguments) # Stream the command's output (stdout+stderr) to the console as PLAIN TEXT, # and return ONLY its exit code. # # Pipeline, stage by stage: # 2>&1 merge stderr into the stream so we see progress # (podman writes "Getting image source signatures" # etc. to stderr). # | ForEach-Object {"$_"} stringify each item. Under Continue, native # stderr arrives as ErrorRecords, which the host # would otherwise render as scary red # "NativeCommandError" blocks - alarming to a user # even though it's just progress. Stringifying # renders them as ordinary lines. # | Out-Host display the lines and emit NOTHING to the # pipeline, so the function returns only the exit # code (a bare call would leak podman's text into # the return, making `$rc -ne 0` wrongly true). # PowerShell does not connect stdin to the pipeline, so piping the OUTPUT does # not affect podman's stdin/tty - `machine init`'s `wsl --import` runs fine. $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & $Exe @Arguments 2>&1 | ForEach-Object { "$_" } | Out-Host } finally { $ErrorActionPreference = $prev } return $LASTEXITCODE } # Run a bash script inside the podman machine, immune to BOTH hazards on the # PowerShell -> podman -> WSL path: # 1. Quote mangling - passing a script as `bash -lc '