Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 154 additions & 20 deletions build-and-test-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,38 @@

# Build-and-test driver for Windows via WSL.
# - installs WSL if missing
# - creates and starts an Ubuntu distribution
# - creates and starts a dedicated Ubuntu distribution
# - initializes it non-interactively: removes k3s, creates a sudo user, sets it default
# - Enter in the distro and run tesst/build-and-test-ubuntu.sh as the ops user
#
# WARNING: this script DESTROYS AND REBUILDS the WSL distribution named by
# $Distro on every run (`wsl --unregister`, which is irreversible). It therefore
# uses a dedicated name of its own rather than a common one like 'Ubuntu', and
# refuses to delete any distribution it did not create without confirmation.

[CmdletBinding()]
param(
# Skip the confirmation prompt before destroying an existing distribution.
# Intended for CI. Interactive users should leave this off.
[switch]$Force
)

$ErrorActionPreference = "Stop"

$Distro = "Ubuntu"
# The distribution this script owns. It is DESTROYED AND REBUILT on every run,
# so it deliberately does NOT default to a name a developer is likely to be
# using themselves ('Ubuntu', 'Ubuntu-24.04', ...). Override with WSL_DISTRO.
$Distro = if ($env:WSL_DISTRO) { $env:WSL_DISTRO } else { "openserverless-build" }

# The image the distribution is created from. WSL can install the same image
# under any name (`wsl --install -d <image> --name <distro>`), which is what
# keeps $Distro independent of whatever the user already has installed.
$Image = if ($env:WSL_IMAGE) { $env:WSL_IMAGE } else { "Ubuntu-24.04" }

# Marker file written inside the distribution during provisioning. Its presence
# is what lets a later run know the distro is this script's to destroy.
$OwnedMarker = "/etc/openserverless-build-distro"

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# Default UNIX account created inside the distribution (override with env vars).
Expand All @@ -43,16 +68,41 @@ function Test-WslInstalled {

Write-Host "Checking WSL is installed"
if (-not (Test-WslInstalled)) {
Write-Host "WSL not found. Installing WSL with the $Distro distribution..."
wsl.exe --install -d $Distro
Write-Host "WSL not found. Installing WSL..."
wsl.exe --install --no-distribution
Write-Warning "WSL was just installed. A reboot is usually required to finish setup."
Write-Warning "Please reboot Windows and re-run this script."
exit 0
}

Write-Host "Removing any existing $Distro distribution for a clean build"
Write-Host "Checking for an existing $Distro distribution"
$installed = (wsl.exe --list --quiet) -replace "`0", "" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
if ($installed -contains $Distro) {
# `wsl --unregister` deletes the distribution's virtual disk. It is
# IRREVERSIBLE - there is no recycle bin and no undo. Never do it to a
# distribution this script did not create without asking first.
$marker = (wsl.exe -d $Distro -u root -- sh -c "test -f $OwnedMarker && echo owned" 2>$null)
$isOwned = ($LASTEXITCODE -eq 0) -and (($marker -replace "`0", "").Trim() -eq "owned")

if (-not ($isOwned -or $Force)) {
Write-Host ""
Write-Warning "A WSL distribution named '$Distro' already exists, but it was not created by this script."
Write-Warning "Continuing DESTROYS it permanently, including every file inside it."
Write-Host ""
Write-Host " If this is a distribution you use, answer 'no' and set a different name instead:"
Write-Host " `$env:WSL_DISTRO = 'openserverless-build-2'; .\build-and-test-windows.ps1"
Write-Host ""
Write-Host " Existing distributions on this machine:"
$installed | ForEach-Object { Write-Host " $_" }
Write-Host ""

$answer = Read-Host "Permanently delete the '$Distro' distribution and rebuild it? Type 'yes' to confirm"
if ($answer -ne "yes") {
Write-Host "Aborted. Nothing was changed."
exit 1
}
}

Write-Host "Unregistering existing $Distro distribution..."
wsl.exe --unregister $Distro
if ($LASTEXITCODE -ne 0) {
Expand All @@ -61,10 +111,10 @@ if ($installed -contains $Distro) {
}
}

Write-Host "Installing a fresh $Distro distribution"
wsl.exe --install -d $Distro --no-launch
Write-Host "Installing a fresh $Distro distribution from the $Image image"
wsl.exe --install -d $Image --name $Distro --no-launch
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to install $Distro"
Write-Error "Failed to install $Distro from the $Image image"
exit $LASTEXITCODE
}

Expand All @@ -75,14 +125,52 @@ Write-Host "Initializing $Distro as root (removing k3s, docker, creating user '$
# creates '$WslUser' with docker + passwordless sudo.
$initScript = @"
set -e
set -o pipefail

export DEBIAN_FRONTEND=noninteractive

# Mark this distribution as created by (and therefore safe to be destroyed by)
# this script. Later runs check for this file before unregistering.
echo 'Created by build-and-test-windows.ps1. This distribution is rebuilt from scratch on every run.' > $OwnedMarker

# Drop the Windows PATH entries WSL injects. The [interop] setting written to
# /etc/wsl.conf below only takes effect after the restart, and this distro is
# recreated from scratch on every run - so provisioning itself would still see
# Docker Desktop's docker.exe and misbehave (get.docker.com warns about an
# "existing" docker and sleeps 40s). Strip them here so init is deterministic.
PATH="`$(printf '%s' "`$PATH" | tr ':' '\n' | grep -v '^/mnt/' | paste -sd: -)"
export PATH

# Remove k3s if it is installed inside the distribution.
if [ -x /usr/local/bin/k3s-killall.sh ]; then /usr/local/bin/k3s-killall.sh; fi
if [ -x /usr/local/bin/k3s-uninstall.sh ]; then /usr/local/bin/k3s-uninstall.sh; fi
if [ -x /usr/local/bin/k3s-agent-uninstall.sh ]; then /usr/local/bin/k3s-agent-uninstall.sh; fi

# Ensure docker is installed.
command -v docker >/dev/null 2>&1 || curl -sL get.docker.com | sh
# Ensure docker is installed INSIDE the distribution. WSL appends the Windows
# PATH, so a plain `command -v docker` also matches Docker Desktop's
# /mnt/c/.../docker.exe - which makes the install look done while the distro has
# no daemon at all. Only a binary outside /mnt counts.
has_linux_docker() {
case "`$(command -v docker 2>/dev/null)" in
''|/mnt/*) return 1 ;;
*) return 0 ;;
esac
}

# Download the installer to a file first: piping curl into sh hides a failed
# download, since sh happily runs an empty script.
if ! has_linux_docker; then
apt-get update
apt-get install -y ca-certificates curl
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
sh /tmp/get-docker.sh
rm -f /tmp/get-docker.sh
fi
has_linux_docker || { echo "docker installation failed: no native docker binary in the distribution" >&2; exit 1; }

# get.docker.com creates the 'docker' group, but create it defensively so the
# usermod below cannot fail on a partial install.
getent group docker >/dev/null || groupadd docker

# Create the build user if it does not already exist (idempotent).
if ! id -u '$WslUser' >/dev/null 2>&1; then
Expand All @@ -100,19 +188,39 @@ chmod 440 /etc/sudoers.d/$WslUser
sudo -u '$WslUser' mkdir -p /home/$WslUser/.ssh
sudo -u '$WslUser' chmod 700 /home/$WslUser/.ssh

# Make sure the docker daemon is running.
service docker start >/dev/null 2>&1 || true
# Make sure the docker daemon starts now AND on every subsequent boot: this
# distro is restarted (wsl --shutdown) right after provisioning, so a one-shot
# start would be lost.
if [ -d /run/systemd/system ]; then
systemctl enable --now docker
else
service docker start
fi

# Mount Windows drives with 'metadata' so Unix permissions (including the
# executable bit) are honored on /mnt/c. Without this, scripts on the mount
# cannot be run directly (fork/exec fails with "no such file or directory").
# Write a known-good /etc/wsl.conf. This distro is always created fresh by this
# script, so we own the file outright rather than patching it in place.
# Requires a `wsl --shutdown` to take effect (done below).
#
# systemd - needed so the docker service can be enabled/started.
# automount metadata- honor Unix permissions (incl. the executable bit) on
# /mnt/c; without it scripts on the mount cannot be run
# directly (fork/exec fails, "no such file or directory").
# appendWindowsPath - keep this distro ISOLATED from the Windows toolchain.
# Otherwise Docker Desktop's docker.exe leaks onto PATH
# and shadows/masquerades as a working docker install,
# even though this distro has no Desktop integration.
mkdir -p /etc
if [ ! -f /etc/wsl.conf ] || ! grep -q '^\[automount\]' /etc/wsl.conf; then
printf '[automount]\noptions = "metadata"\n' >> /etc/wsl.conf
elif ! grep -q 'metadata' /etc/wsl.conf; then
sed -i '/^\[automount\]/a options = "metadata"' /etc/wsl.conf
fi
cat > /etc/wsl.conf <<'EOF'
[boot]
systemd=true

[automount]
options = "metadata"

[interop]
enabled = true
appendWindowsPath = false
EOF
"@

# Write the script to a BOM-less, LF-normalized temp file (PS 5.1 stdin pipes
Expand Down Expand Up @@ -145,6 +253,32 @@ if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

Write-Host "Waiting for the Docker daemon in $Distro"
# After the restart the distro boots cold; give dockerd a chance to come up so
# the build script does not fail on a race. Use a temp script file rather than
# `bash -c '...'`: wsl.exe re-parses the command line and mangles inline quoting.
$waitScript = @'
systemctl start docker >/dev/null 2>&1 || true
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && exit 0
sleep 2
done
systemctl --no-pager status docker || true
exit 1
'@
$waitFile = Join-Path ([System.IO.Path]::GetTempPath()) "wsl-wait-$PID.sh"
[System.IO.File]::WriteAllText($waitFile, ($waitScript -replace "`r`n", "`n"), (New-Object System.Text.UTF8Encoding($false)))
try {
$wslWaitPath = (wsl.exe -d $Distro wslpath -a ($waitFile -replace '\\', '/')).Trim()
wsl.exe -d $Distro -u root -- bash "$wslWaitPath"
if ($LASTEXITCODE -ne 0) {
Write-Error "Docker daemon did not become ready in $Distro"
exit $LASTEXITCODE
}
} finally {
Remove-Item -LiteralPath $waitFile -ErrorAction SilentlyContinue
}

Write-Host "Running build-and-test-ubuntu.sh in $Distro as '$WslUser'"
# Run the build/test script as ops from the source directory. ops was created
# with the mount's owning uid/gid, so it can write the build output in place;
Expand Down