#!/usr/bin/env bash # Create a disposable Linux environment to validate the installer from a # genuinely clean slate — one that can be thrown away without touching the # environment you actually work in. # # This is the ONLY host-aware file in the tree. Everything else needs just a # Linux with Docker, which is what keeps other host types a later addition # rather than a rewrite. # # On WSL it creates a second distro. There is no .bat and no PowerShell script: # wsl.exe is callable from inside WSL, and wslpath converts the paths it wants. # A machine with no WSL at all needs `wsl --install` run once by hand first — # scripting a reboot-requiring Windows feature install is not worth it. # # Docker: borrowed by default, never installed twice # -------------------------------------------------- # WSL2 distros share one kernel and one network stack, so two dockerd instances # contend over docker0 and iptables and can disturb the daemon you depend on. # (That is why Docker Desktop runs one daemon in a dedicated distro and shares # its socket rather than installing one per distro.) # # REUSE_DOCKER=1 (default) borrow the host distro's daemon over /mnt/wsl. # Nothing is installed; nothing can conflict. # Requires `ctrl/dockerhost.sh share` once on the # distro that owns Docker. # REUSE_DOCKER=0 install a second daemon in the new distro. Only # if you specifically want to test a from-scratch # Docker install, and not on a machine you need. # # Borrowing is also the more honest test: rig never installs Docker anyway — it # is the documented prerequisite — so a clean box does not need its own to # exercise everything rig actually does. # # Usage: newbox.sh create | destroy [--purge] | status | shell set -euo pipefail cd "$(dirname "$0")" source ./lib/config.sh load_config REPO="$(cd .. && pwd)" # The distro is named after this environment, and that derived name is the ONLY # thing this script will ever destroy. See guard_name(). BOX="${BOX:-${CLUSTER}box}" BOX_USER="${BOX_USER:-dev}" # Borrow the host distro's Docker rather than installing a second daemon. REUSE_DOCKER="${REUSE_DOCKER:-1}" SHARED_SOCK=/mnt/wsl/shared-docker/docker.sock WSL_EXE=/mnt/c/Windows/System32/wsl.exe # ── host detection ───────────────────────────────────────────────────────── require_wsl() { if ! grep -qi microsoft /proc/version 2>/dev/null; then cat >&2 <<'EOF' newbox is WSL-only for now. If WSL is not installed, run `wsl --install` from an elevated Windows prompt first — see "Starting from plain Windows" in README.md. On native Linux you do not need it: rig already isolates environments by directory (own cluster, context, images and port block), so a second copy in a second directory is the clean slate. To validate the installer itself against a bare system, run ctrl/deps.sh against a stock Debian container instead. EOF exit 1 fi if [ ! -x "$WSL_EXE" ]; then echo "wsl.exe not found at $WSL_EXE" >&2 exit 1 fi } wsl_list() { "$WSL_EXE" -l -q 2>/dev/null | tr -d '\0\r'; } box_exists() { wsl_list | grep -qx "$BOX"; } # `wsl --unregister` permanently deletes a distro's filesystem. The whole safety # story is this function: only the name derived from this directory can ever be # a target, so a typo or a stray argument cannot destroy the distro you work in. guard_name() { local derived="${CLUSTER}box" if [ "$BOX" != "$derived" ]; then echo "refusing: BOX='$BOX' is not the name derived from this directory ('$derived')." >&2 echo "That guard exists because --unregister is irreversible." >&2 exit 1 fi if [ -z "$CLUSTER" ] || [ "$BOX" = "box" ]; then echo "refusing: empty environment name" >&2 exit 1 fi } # ── create ───────────────────────────────────────────────────────────────── rootfs_path() { local win_home; win_home=$(wslpath "$("$WSL_EXE" -d "$(wsl_list | head -1)" -e printf '%s' "$USERPROFILE" 2>/dev/null || true)" 2>/dev/null || true) # Simpler and reliable: use the current user's Windows home via /mnt/c. ls -d /mnt/c/Users/*/ 2>/dev/null | grep -viE '/(All Users|Default|Default User|Public)/$' | head -1 } build_rootfs() { local tar="$1" if [ -f "$tar" ]; then echo " rootfs cached: $(basename "$tar")" return fi echo " exporting a stock Debian rootfs (cached for next time)" local cid; cid=$(docker create debian:trixie-slim) docker export "$cid" > "$tar" docker rm -f "$cid" >/dev/null } provision() { echo " provisioning (root)" local hosts_block hosts_block=$(CLUSTER="$CLUSTER" HTTP_PORT="$HTTP_PORT" \ envsubst < ./hosts.tmpl 2>/dev/null || sed "s/\${CLUSTER}/$CLUSTER/g" ./hosts.tmpl) # Piped as stdin rather than a second script file, the same shape as any # remote provisioning heredoc. Everything here is idempotent so a failed run # can simply be repeated. "$WSL_EXE" -d "$BOX" -u root -- bash -s </dev/null if [ "$REUSE_DOCKER" = "1" ]; then # Borrow the host distro's daemon: CLI only, no dockerd, nothing to # conflict with. The GID must match the owner's or the shared socket is # unreadable here even though it is visible. install -m 0755 -d /etc/apt/keyrings if [ ! -f /etc/apt/keyrings/docker.asc ]; then curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc fi echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce-cli >/dev/null echo "export DOCKER_HOST=unix://$SHARED_SOCK" > /etc/profile.d/rig-docker-host.sh if [ -f /mnt/wsl/shared-docker/OWNER ]; then gid=\$(awk '/docker gid:/ {print \$3}' /mnt/wsl/shared-docker/OWNER) if [ -n "\$gid" ]; then getent group docker >/dev/null && groupmod -g "\$gid" docker || groupadd -g "\$gid" docker fi fi else # A second daemon. Only when deliberately testing a from-scratch install. install -m 0755 -d /etc/apt/keyrings if [ ! -f /etc/apt/keyrings/docker.asc ]; then curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc fi echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null fi id -u "$BOX_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$BOX_USER" usermod -aG sudo,docker "$BOX_USER" echo "$BOX_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-$BOX_USER chmod 0440 /etc/sudoers.d/90-$BOX_USER # systemd is off by default in WSL, and Docker needs it. Takes effect on the # next start of this distro, which is why create() terminates it below. cat > /etc/wsl.conf < /etc/sysctl.d/99-rig.conf </dev/null; then { echo ""; echo "# rig environment"; cat <<'HOSTS' $hosts_block HOSTS } >> /etc/hosts fi touch /etc/rig-provisioned PROVISION } create() { require_wsl guard_name local winhome; winhome=$(rootfs_path) [ -n "$winhome" ] || { echo "could not locate the Windows user directory" >&2; exit 1; } local tar="${winhome}rig-rootfs.tar" local installdir="${winhome}WSL/${BOX}" echo "creating '$BOX'" if [ "$REUSE_DOCKER" = "1" ]; then echo " docker: borrowing the host distro's daemon (nothing installed)" if [ ! -S "$SHARED_SOCK" ]; then echo echo " No shared socket yet. In the distro that owns Docker, run once:" echo " sudo bash ctrl/dockerhost.sh share" echo " That adds one systemd drop-in and nothing else; undo with 'unshare'." echo " Continuing — the box will be created, but Docker won't work in it" echo " until you do that." fi else echo echo " REUSE_DOCKER=0: installing a SECOND Docker daemon." echo " WSL distros share a network stack, so this can disturb Docker in" echo " the distro you work in. Ctrl-C now if that is a bad trade today." echo sleep 4 fi echo if box_exists; then echo " distro already registered" else build_rootfs "$tar" mkdir -p "$installdir" "$WSL_EXE" --import "$BOX" "$(wslpath -w "$installdir")" "$(wslpath -w "$tar")" --version 2 fi # Resumable: a partially-created box is finished rather than restarted. if "$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null; then echo " already provisioned" else provision echo " restarting the distro so systemd and group membership apply" "$WSL_EXE" --terminate "$BOX" # ONLY this distro; never --shutdown fi echo " copying rig in" tar c -C "$REPO" --exclude=def --exclude=.git --exclude=ctrl/.env . \ | "$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc "mkdir -p ~/rig && tar x -C ~/rig" echo echo " docker: $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'systemctl is-active docker 2>/dev/null || echo inactive')" echo echo "next:" echo " make newbox shell # a shell inside it" echo " then: cd ~/rig && make check && make deps && make cluster up" echo echo "For a browser on Windows to resolve the hostnames, paste this into" echo "C:\\Windows\\System32\\drivers\\etc\\hosts (it has no wildcard support):" CLUSTER="$CLUSTER" envsubst < ./hosts.tmpl 2>/dev/null | grep -v '^#' | grep -v '^$' | sed 's/^/ /' } # ── the rest ─────────────────────────────────────────────────────────────── destroy() { require_wsl guard_name if ! box_exists; then echo "no distro '$BOX' to remove" else echo "about to PERMANENTLY delete the distro '$BOX' and its filesystem." "$WSL_EXE" --terminate "$BOX" 2>/dev/null || true "$WSL_EXE" --unregister "$BOX" echo " unregistered" fi local winhome; winhome=$(rootfs_path) rm -rf "${winhome}WSL/${BOX}" 2>/dev/null || true if [ "${1:-}" = "--purge" ]; then rm -f "${winhome}rig-rootfs.tar" echo " cached rootfs removed" fi } status() { require_wsl echo "environment $CLUSTER" echo "distro $BOX" if box_exists; then echo "registered yes" echo "provisioned $("$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null && echo yes || echo no)" echo "docker $("$WSL_EXE" -d "$BOX" -u root -- bash -lc 'systemctl is-active docker 2>/dev/null' || echo unknown)" echo "rig copied $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'test -f ~/rig/Makefile && echo yes || echo no' 2>/dev/null)" else echo "registered no" fi echo echo "all distros (this one is never touched unless it is '$BOX'):" wsl_list | sed 's/^/ /' } shell() { require_wsl box_exists || { echo "no distro '$BOX' — run 'make newbox' first" >&2; exit 1; } "$WSL_EXE" -d "$BOX" -u "$BOX_USER" --cd '~' } case "${1:-status}" in create) create ;; destroy) shift; destroy "${1:-}" ;; status) status ;; shell) shell ;; *) echo "usage: $0 [create|destroy [--purge]|status|shell]" >&2; exit 1 ;; esac