# Build Machines Reproducible VirtualBox images for offloading compilation to dedicated hardware. Each VM self-registers with the Custodian State Hub on boot and connects back to the development workstation via SSH reverse tunnel. ## Prerequisites - **Packer** >= 1.10 (`packer version`) - **VirtualBox** >= 7.0 (`VBoxManage --version`) - **autossh** on both workstation and VM (installed automatically in VM image) - **State Hub** running on workstation (`cd ~/the-custodian/state-hub && make api`) ## Quick Start ### 1. Generate SSH keypair (one-time) ```bash ssh-keygen -t ed25519 -f ~/.ssh/id_build -N "" -C "build-agent" ``` ### 2. Build the OVA ```bash cd infra/build-machines/haskell packer init . packer build . ``` This produces `haskell-build-YYYYMMDD.ova` (~4-6 GB, depending on GHC versions). ### 3. Import and configure ```bash # Import the OVA VBoxManage import haskell-build-20260420.ova # Switch from NAT (build-time) to bridged networking scripts/setup-vm.sh haskell-build # Start the VM VBoxManage startvm haskell-build --type headless ``` ### 4. Inject credentials ```bash # Prepare a directory with keys and config mkdir -p ~/vm-keys/haskell-build cp ~/.ssh/id_build ~/vm-keys/haskell-build/ cp ~/.ssh/id_build.pub ~/vm-keys/haskell-build/ # Edit build-agent.env from template cp haskell/files/build-agent.env.template ~/vm-keys/haskell-build/build-agent.env # Edit SSH_RELAY_HOST to your workstation's LAN IP # Inject (VM must be running; uses temporary password auth) scripts/inject-keys.sh ~/vm-keys/haskell-build/ ``` ### 5. Install SSH config ```bash make install-ssh-config ``` ### 6. Verify ```bash make bridge-status # check tunnel is up ssh haskell-build # should connect via tunnel ./smoke-test.sh # full stack validation ``` ## Using the VM ```bash # Build a Haskell project remotely make remote-build PROJECT=~/projects/my-app # Run tests make remote-test PROJECT=~/projects/my-app # Interactive GHCi make remote-ghci PROJECT=~/projects/my-app # Fetch build artefacts back to workstation make fetch-artifacts PROJECT=~/projects/my-app # Check VM info make vm-info ``` ## Architecture ``` Workstation (WSL2) ├── state-hub (:8000) — sees capability entries, knows tunnel ports └── SSH listener — accepts reverse tunnel from VM Laptop (VirtualBox host) └── haskell-build VM (Ubuntu 24.04, bridged) ├── GHC 9.8.4 + 9.6.6 via GHCup ├── build-agent (systemd) — registers with state-hub on boot └── autossh: -R 12222→local:22, -L 18000→state-hub:8000 ``` The VM connects OUT to the workstation. Two tunnels in one SSH connection: - **Reverse** (`-R 12222:localhost:22`): workstation can SSH into VM - **Forward** (`-L 18000:localhost:8000`): VM can reach state-hub ## Port Registry See `port-registry.yml`. Range 12221-12230 supports up to 10 concurrent VMs. Each VM must use a unique port. ## Adding a GHC Version Post-Deployment ```bash ssh haskell-build "source ~/.ghcup/env && ghcup install ghc 9.10.1" ``` No image rebuild required. ## Troubleshooting **Tunnel not up:** - Check `journalctl -u build-agent` on the VM - Verify `SSH_RELAY_HOST` in `/etc/build-agent.env` is reachable from the VM - Ensure the workstation's SSH server accepts the build key **Capability not in state-hub:** - Check `curl http://127.0.0.1:8000/capability-catalog/?capability_type=haskell-build-agent` - The agent retries 20 times on boot; check logs for registration errors - The forward tunnel (`-L 18000:localhost:8000`) must be up before registration works **Build fails with missing libraries:** - The VM includes common Haskell build deps. For additional system libraries: `ssh haskell-build "sudo apt-get install -y libXXX-dev"` ## Updating the Image Re-run Packer to build a new OVA. Import alongside the existing VM or replace it. Build artefacts and keys live on the workstation (via rsync), not in the VM — the image is disposable.