2026-02-01 05:42:05 +00:00
#!/bin/bash
#
# Build Archipelago Auto-Installer ISO (StartOS-like)
#
# This creates an ISO that automatically installs to the internal disk
# with minimal user interaction - similar to StartOS experience.
#
2026-02-03 21:43:33 +00:00
# CRITICAL: This script CAPTURES the LIVE SERVER state by default.
# Set DEV_SERVER to point to your development server.
#
# Usage:
# DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
# OR just: ./build-auto-installer-iso.sh (uses default server)
#
# To build from source instead:
# BUILD_FROM_SOURCE=1 ./build-auto-installer-iso.sh
#
2026-02-01 05:42:05 +00:00
# Features:
# - Pre-built root filesystem (no network needed during install)
# - Auto-detects internal disk (skips USB boot drive)
# - Automatic installation with progress display
# - Boots directly to web UI after install
#
2026-03-21 03:06:29 +00:00
# Image versions: sourced from scripts/image-versions.sh (single source of truth).
# All container image references MUST use the $*_IMAGE variables defined there.
#
# --- PLANNED REFACTOR (post-beta) ---
# This script is ~1870 lines and should be split into a modular library.
# Proposed structure:
# image-recipe/
# build-auto-installer-iso.sh — Main orchestrator (config, CLI args, step sequencing)
# lib/
# rootfs.sh — Step 1: Build root filesystem via Docker (~185 lines)
2026-03-27 18:19:49 +00:00
# installer-env.sh — Step 2: Build minimal installer via debootstrap (~80 lines)
2026-03-21 03:06:29 +00:00
# components.sh — Step 3: Add Archipelago components (binary, configs, web UI) (~120 lines)
# container-images.sh — Step 3b: Bundle container images for offline install (~330 lines)
# auto-install-script.sh — Step 4: Generate the embedded auto-install.sh (~615 lines)
# boot-config.sh — Step 5: Configure live boot auto-start + overlay squashfs (~215 lines)
# create-iso.sh — Step 6: Build final bootable ISO with xorriso/grub (~140 lines)
# Each lib/ script exports functions; main script sources them and calls in sequence.
# DO NOT split until tested on the build server — this is critical infrastructure.
# ---
#
2026-02-01 05:42:05 +00:00
set -e
2026-03-21 01:32:28 +00:00
# Source pinned image versions (single source of truth)
SCRIPT_DIR = " $( cd " $( dirname " $0 " ) " && pwd ) "
2026-04-28 15:00:58 -04:00
[ -f " $SCRIPT_DIR /../../scripts/image-versions.sh" ] && . " $SCRIPT_DIR /../../scripts/image-versions.sh"
2026-03-21 01:32:28 +00:00
2026-02-03 21:43:33 +00:00
# Configuration
DEV_SERVER = " ${ DEV_SERVER :- archipelago @192.168.1.228 } "
BUILD_FROM_SOURCE = " ${ BUILD_FROM_SOURCE :- 0 } "
2026-03-10 23:29:05 +00:00
UNBUNDLED = " ${ UNBUNDLED :- 0 } "
2026-03-12 00:19:30 +00:00
ARCH = " ${ ARCH :- x86_64 } "
2026-03-31 23:43:32 +01:00
# ── Sequential build numbering ─────────────────────────────────────────
# Increments on each build. Users see this in UI (Settings, sidebar).
# Counter persists in /opt/archipelago/build-counter (on build machine).
BUILD_COUNTER_FILE = "/opt/archipelago/build-counter"
if [ -f " $BUILD_COUNTER_FILE " ] ; then
BUILD_NUM = $(( $( cat " $BUILD_COUNTER_FILE " ) + 1 ))
else
BUILD_NUM = 1
fi
echo " $BUILD_NUM " | sudo tee " $BUILD_COUNTER_FILE " > /dev/null 2>/dev/null || BUILD_NUM = 1
GIT_SHORT = $( cd " $SCRIPT_DIR /.." && git rev-parse --short HEAD 2>/dev/null || echo "dev" )
2026-04-02 08:54:39 +01:00
# Version format: major.minor.patch-prerelease (semver)
2026-04-03 00:35:52 +01:00
# Read version from Cargo.toml (single source of truth)
BUILD_VERSION = $( grep '^version' " $SCRIPT_DIR /../core/archipelago/Cargo.toml" 2>/dev/null | head -1 | sed 's/version = "//;s/"//' || echo "0.0.0" )
2026-03-31 23:43:32 +01:00
echo "Build # ${ BUILD_NUM } ( ${ BUILD_VERSION } , commit ${ GIT_SHORT } )"
2026-03-12 00:19:30 +00:00
# Architecture-dependent variables
case " $ARCH " in
x86_64| amd64)
ARCH = "x86_64"
DEB_ARCH = "amd64"
LINUX_IMAGE_PKG = "linux-image-amd64"
GRUB_EFI_PKG = "grub-efi-amd64"
GRUB_EFI_SIGNED_PKG = "grub-efi-amd64-signed"
2026-03-21 01:11:05 +00:00
GRUB_PC_PKG = "grub-pc-bin"
2026-03-12 00:19:30 +00:00
GRUB_TARGET = "x86_64-efi"
2026-03-21 01:11:05 +00:00
GRUB_BIOS_TARGET = "i386-pc"
2026-03-12 00:19:30 +00:00
CONTAINER_PLATFORM = "linux/amd64"
LIB_DIR = " ${ LIB_DIR } "
;;
arm64| aarch64)
ARCH = "arm64"
DEB_ARCH = "arm64"
LINUX_IMAGE_PKG = "linux-image-arm64"
GRUB_EFI_PKG = "grub-efi-arm64"
GRUB_EFI_SIGNED_PKG = "grub-efi-arm64-signed"
2026-03-21 01:11:05 +00:00
GRUB_PC_PKG = ""
2026-03-12 00:19:30 +00:00
GRUB_TARGET = "arm64-efi"
2026-03-21 01:11:05 +00:00
GRUB_BIOS_TARGET = ""
2026-03-12 00:19:30 +00:00
CONTAINER_PLATFORM = "linux/arm64"
LIB_DIR = "aarch64-linux-gnu"
;;
*)
echo "❌ Unsupported architecture: $ARCH (use x86_64 or arm64)"
exit 1
;;
esac
2026-02-03 21:43:33 +00:00
2026-02-01 05:42:05 +00:00
SCRIPT_DIR = " $( cd " $( dirname " $0 " ) " && pwd ) "
WORK_DIR = " $SCRIPT_DIR /build/auto-installer"
OUTPUT_DIR = " $SCRIPT_DIR /results"
ROOTFS_DIR = " $WORK_DIR /rootfs"
INSTALLER_DIR = " $WORK_DIR /installer"
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago UNBUNDLED ISO (no pre-loaded apps) ║"
echo "╚════════════════════════════════════════════════════════════════╝"
else
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago Auto-Installer ISO (StartOS-like) ║"
echo "╚════════════════════════════════════════════════════════════════╝"
fi
2026-02-01 05:42:05 +00:00
echo ""
2026-02-03 21:43:33 +00:00
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo "📦 Mode: Building from SOURCE CODE"
2026-03-10 23:29:05 +00:00
elif [ " $UNBUNDLED " = "1" ] ; then
echo "📦 Mode: UNBUNDLED (apps downloaded on-demand from Marketplace)"
echo " Server: $DEV_SERVER (backend + web UI only)"
2026-02-03 21:43:33 +00:00
else
echo "📦 Mode: Capturing LIVE SERVER state"
echo " Server: $DEV_SERVER "
fi
2026-03-12 00:19:30 +00:00
echo "🏗️ Architecture: $ARCH ( $DEB_ARCH )"
2026-02-03 21:43:33 +00:00
echo ""
2026-02-01 05:42:05 +00:00
# Check for required tools
check_tools() {
local missing = ""
2026-02-14 16:44:20 +00:00
local can_install = false
# Check if we can auto-install (running as root on Debian/Ubuntu)
if [ " $EUID " -eq 0 ] && [ -f /etc/debian_version ] ; then
can_install = true
fi
2026-02-01 18:46:35 +00:00
# Check for docker or podman
if command -v docker >/dev/null 2>& 1; then
CONTAINER_CMD = "docker"
elif command -v podman >/dev/null 2>& 1; then
CONTAINER_CMD = "podman"
else
missing = " $missing docker-or-podman"
fi
2026-03-27 18:19:49 +00:00
for tool in xorriso mksquashfs; do
if ! command -v $tool >/dev/null 2>& 1; then
missing = " $missing $tool "
fi
done
# Check for isolinux MBR (needed for hybrid USB boot)
if [ ! -f /usr/lib/ISOLINUX/isohdpfx.bin ] && [ ! -f /usr/share/syslinux/isohdpfx.bin ] ; then
missing = " $missing isolinux"
2026-02-01 18:46:35 +00:00
fi
2026-03-27 18:19:49 +00:00
2026-02-01 05:42:05 +00:00
if [ -n " $missing " ] ; then
2026-03-27 18:19:49 +00:00
echo "Missing required tools: $missing "
2026-02-14 16:44:20 +00:00
if [ " $can_install " = true ] ; then
2026-03-27 18:19:49 +00:00
echo " Auto-installing missing dependencies..."
2026-02-14 16:44:20 +00:00
apt-get update -qq
2026-03-27 18:19:49 +00:00
2026-02-14 16:44:20 +00:00
if [[ " $missing " == *"xorriso" * ]] ; then
apt-get install -y xorriso
fi
2026-03-27 18:19:49 +00:00
if [[ " $missing " == *"mksquashfs" * ]] ; then
apt-get install -y squashfs-tools
2026-02-17 15:03:34 +00:00
fi
2026-03-27 18:19:49 +00:00
if [[ " $missing " == *"isolinux" * ]] ; then
apt-get install -y isolinux syslinux-common
fi
2026-02-14 16:44:20 +00:00
if [[ " $missing " == *"docker-or-podman" * ]] ; then
echo " Installing podman..."
apt-get install -y podman
CONTAINER_CMD = "podman"
fi
2026-03-27 18:19:49 +00:00
echo " Dependencies installed successfully!"
2026-02-14 16:44:20 +00:00
else
2026-03-27 18:19:49 +00:00
echo " Install with: sudo apt install xorriso squashfs-tools isolinux podman"
2026-02-14 16:44:20 +00:00
echo " Or run this script with sudo to auto-install"
exit 1
fi
fi
# Re-check after potential installation
if command -v docker >/dev/null 2>& 1; then
CONTAINER_CMD = "docker"
elif command -v podman >/dev/null 2>& 1; then
CONTAINER_CMD = "podman"
else
echo "❌ Container runtime still not available after installation"
2026-02-01 05:42:05 +00:00
exit 1
fi
2026-02-01 18:46:35 +00:00
echo "Using container runtime: $CONTAINER_CMD "
2026-03-27 11:12:31 +00:00
2026-03-28 21:01:10 +00:00
# Fix root podman D-Bus issue (sd-bus: Transport endpoint is not connected)
# When running as sudo, systemd cgroup manager can't reach the user D-Bus session.
if [ " $CONTAINER_CMD " = "podman" ] && [ " $( id -u) " = "0" ] ; then
2026-04-09 20:42:09 +02:00
if ! $CONTAINER_CMD run --rm debian:trixie true 2>/dev/null; then
2026-03-28 21:01:10 +00:00
echo " Root podman D-Bus issue detected, using cgroupfs manager"
CONTAINER_CMD = "podman --cgroup-manager=cgroupfs"
fi
fi
2026-05-06 09:23:57 -04:00
# Ensure insecure registry config for Archipelago app registries that are
# intentionally served over HTTP during ISO builds.
if [[ " $CONTAINER_CMD " == podman* ]] ; then
2026-03-27 11:12:31 +00:00
mkdir -p /etc/containers/registries.conf.d
cat > /etc/containers/registries.conf.d/archipelago.conf <<'REGCONF'
2026-05-06 09:23:57 -04:00
[[registry]]
location = "146.59.87.168:3000"
insecure = true
2026-03-27 11:12:31 +00:00
[[registry]]
2026-07-10 18:55:32 +01:00
location = "146.59.87.168:3000"
2026-03-27 11:12:31 +00:00
insecure = true
REGCONF
fi
2026-02-01 05:42:05 +00:00
}
check_tools
mkdir -p " $WORK_DIR "
mkdir -p " $OUTPUT_DIR "
2026-05-06 09:23:57 -04:00
container_pull() {
local image = " $1 "
if [[ " $CONTAINER_CMD " == podman* && " $image " == 146.59.87.168:3000/* ]] ; then
$CONTAINER_CMD pull --tls-verify= false --platform " $CONTAINER_PLATFORM " " $image "
else
$CONTAINER_CMD pull --platform " $CONTAINER_PLATFORM " " $image "
fi
}
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 1: Build complete root filesystem using Docker
# =============================================================================
echo "📦 Step 1: Building root filesystem..."
ROOTFS_TAR = " $WORK_DIR /archipelago-rootfs.tar"
if [ ! -f " $ROOTFS_TAR " ] || [ " $1 " == "--rebuild" ] ; then
echo " Using Docker to create Debian root filesystem..."
# Create a Dockerfile for building the rootfs
2026-03-12 00:19:30 +00:00
cat > " $WORK_DIR /Dockerfile.rootfs" <<DOCKERFILE
2026-04-18 22:57:51 -04:00
# ─── Stage 1: Build the FIPS mesh daemon .deb from upstream main ─────────
#
# FIPS (github.com/jmcorgan/fips) is a fast Nostr-keyed mesh routing
# protocol archipelago uses as its preferred non-Tor transport. We track
# upstream main per project decision (2026-04) — v0.2.0 isn't stable yet.
# The .deb is rebuilt every ISO build; Docker layer caching keeps the
# incremental cost low. Failure here fails the ISO build on purpose:
# we don't want to ship an ISO that silently skips FIPS.
FROM rust:1-slim-bookworm AS fips-builder
ENV DEBIAN_FRONTEND=noninteractive
2026-04-19 09:01:59 -04:00
# Build deps tracked as upstream fips adds transitive native deps:
# - libdbus-1-dev: libdbus-sys (observed 2026-04-19 rebuild)
# - libssl-dev: openssl dependencies
# - libnftnl-dev, libmnl-dev, clang, libclang-dev: rustables →
# bindgen (the gateway feature enables rustables for nftables
# integration). bindgen panics without libclang.so.
2026-04-18 22:57:51 -04:00
RUN apt-get update && apt-get install -y --no-install-recommends \\
git ca-certificates build-essential pkg-config dpkg-dev \\
2026-04-19 08:27:22 -04:00
libdbus-1-dev libssl-dev \\
2026-04-19 09:01:59 -04:00
clang libclang-dev libnftnl-dev libmnl-dev \\
2026-04-18 22:57:51 -04:00
&& rm -rf /var/lib/apt/lists/*
RUN cargo install --locked cargo-deb
RUN git clone --depth 1 https://github.com/jmcorgan/fips.git /src/fips
WORKDIR /src/fips
2026-04-19 08:54:53 -04:00
# fips-gateway is gated behind the `gateway` Cargo feature (depends on
# `rustables`). Without the feature, cargo doesn't build it, and
# cargo deb --no-build panics hunting for target/release/fips-gateway.
# Inspected upstream Cargo.toml 2026-04-19 — features.gateway = ["dep:rustables"].
2026-04-28 15:00:58 -04:00
RUN cargo build --release
2026-04-18 22:57:51 -04:00
RUN cargo deb --no-build
RUN cp target/debian/fips_*_amd64.deb /tmp/fips.deb
# ─── Stage 2: The actual Archipelago rootfs ──────────────────────────────
2026-04-09 20:42:09 +02:00
FROM debian:trixie
2026-02-01 05:42:05 +00:00
ENV DEBIAN_FRONTEND=noninteractive
2026-03-26 19:50:59 +00:00
# Preseed keyboard/console config to prevent console-setup.service failure
RUN echo "keyboard-configuration keyboard-configuration/layoutcode string us" | debconf-set-selections && \
echo "keyboard-configuration keyboard-configuration/model select Generic 105-key PC" | debconf-set-selections && \
echo "console-setup console-setup/charmap47 select UTF-8" | debconf-set-selections && \
2026-03-30 23:47:15 +01:00
echo "console-setup console-setup/codeset47 select Uni2" | debconf-set-selections && \
echo "console-setup console-setup/fontface47 select Terminus" | debconf-set-selections && \
echo "console-setup console-setup/fontsize-fb47 select 16" | debconf-set-selections
2026-03-26 19:50:59 +00:00
2026-03-25 21:21:27 +00:00
# Enable non-free-firmware repo — replace DEB822 sources with traditional format
# (DEB822 sed was silently failing, so just overwrite with known-good sources.list)
2026-04-09 20:42:09 +02:00
RUN echo "deb http://deb.debian.org/debian trixie main non-free-firmware" > /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian trixie-updates main non-free-firmware" >> /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian-security trixie-security main non-free-firmware" >> /etc/apt/sources.list && \
2026-03-25 21:21:27 +00:00
rm -f /etc/apt/sources.list.d/debian.sources
2026-03-25 16:56:02 +00:00
2026-03-14 03:17:12 +00:00
# Install all packages we need including nginx, podman, tor, and openssl (for self-signed certs)
2026-05-05 11:29:18 -04:00
RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install-recommends \
2026-03-12 00:19:30 +00:00
${LINUX_IMAGE_PKG} \
${GRUB_EFI_PKG} \
${GRUB_EFI_SIGNED_PKG} \
2026-03-26 09:12:16 +00:00
${GRUB_PC_PKG} \
2026-02-01 05:42:05 +00:00
systemd \
systemd-sysv \
dbus \
sudo \
network-manager \
2026-05-15 18:15:06 -04:00
wpasupplicant \
wireless-regdb \
iw \
rfkill \
polkitd \
2026-02-01 05:42:05 +00:00
openssh-server \
nginx \
2026-07-01 10:42:20 -04:00
avahi-daemon \
avahi-utils \
libnss-mdns \
2026-02-01 05:42:05 +00:00
podman \
2026-05-17 17:30:04 -04:00
catatonit \
2026-03-28 02:40:39 +00:00
uidmap \
slirp4netns \
2026-04-10 03:10:49 -04:00
passt \
aardvark-dns \
netavark \
nftables \
2026-03-28 02:40:39 +00:00
fuse-overlayfs \
2026-03-14 03:17:12 +00:00
tor \
2026-03-31 04:52:30 +01:00
python3 \
2026-02-01 05:42:05 +00:00
curl \
2026-03-25 15:52:26 +00:00
git \
2026-02-01 05:42:05 +00:00
vim-tiny \
2026-05-19 14:29:20 -04:00
nano \
2026-02-01 05:42:05 +00:00
ca-certificates \
2026-03-05 08:34:53 +00:00
openssl \
2026-03-18 13:53:27 +00:00
chrony \
2026-02-01 05:42:05 +00:00
locales \
console-setup \
keyboard-configuration \
2026-03-26 09:12:16 +00:00
cryptsetup \
2026-04-09 21:07:34 +01:00
cryptsetup-initramfs \
e2fsprogs \
2026-03-25 16:56:02 +00:00
firmware-realtek \
firmware-iwlwifi \
2026-03-28 13:06:34 +00:00
firmware-misc-nonfree \
2026-04-02 20:28:53 +01:00
firmware-linux-nonfree \
2026-07-02 08:00:39 -04:00
firmware-intel-graphics \
firmware-amd-graphics \
2026-03-25 18:25:01 +00:00
intel-microcode \
amd64-microcode \
2026-03-26 09:12:16 +00:00
xorg \
2026-04-01 22:31:45 +01:00
xdotool \
2026-03-26 09:12:16 +00:00
chromium \
unclutter \
2026-03-27 18:19:49 +00:00
fonts-liberation \
xfonts-base \
2026-03-27 22:26:56 +00:00
plymouth \
plymouth-themes \
2026-03-28 23:41:40 +00:00
zstd \
2026-04-02 16:15:04 +01:00
socat \
2026-03-31 04:52:30 +01:00
python3 \
2026-04-02 10:34:58 +01:00
apache2-utils \
2026-04-08 13:15:09 +02:00
wireguard-tools \
acpid \
acpi-support-base \
2026-02-01 05:42:05 +00:00
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
2026-03-27 18:19:49 +00:00
# Strip docs, man pages, and unused locales
RUN find /usr/share/doc -depth -type f ! -name copyright -delete 2>/dev/null || true && \
find /usr/share/doc -empty -delete 2>/dev/null || true && \
rm -rf /usr/share/man /usr/share/info /usr/share/lintian /usr/share/linda && \
find /usr/share/locale -maxdepth 1 -mindepth 1 ! -name 'en_US' ! -name 'locale.alias' -exec rm -rf {} + 2>/dev/null || true
2026-03-14 03:17:12 +00:00
# Install Tailscale from official repo
2026-04-09 20:42:09 +02:00
RUN curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list && \
2026-05-05 11:29:18 -04:00
apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install-recommends tailscale && \
2026-03-14 03:17:12 +00:00
apt-get clean && rm -rf /var/lib/apt/lists/*
2026-04-18 22:57:51 -04:00
# Install FIPS mesh daemon from the .deb built in stage 1. apt-get install
# resolves dependencies from trixie so a cross-dist build still lands cleanly.
COPY --from=fips-builder /tmp/fips.deb /tmp/fips.deb
2026-05-05 11:29:18 -04:00
RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install-recommends /tmp/fips.deb && \
2026-04-18 22:57:51 -04:00
apt-get clean && rm -rf /var/lib/apt/lists/* && rm /tmp/fips.deb
2026-02-01 05:42:05 +00:00
# Configure locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
2026-03-10 23:29:05 +00:00
# Create archipelago user with password "archipelago"
2026-02-01 05:42:05 +00:00
RUN useradd -m -s /bin/bash -G sudo archipelago && \
echo "archipelago:archipelago" | chpasswd && \
2026-03-10 23:29:05 +00:00
echo "root:archipelago" | chpasswd && \
2026-02-01 05:42:05 +00:00
echo "archipelago ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/archipelago
2026-03-10 23:29:05 +00:00
# Verify password hash was set (not locked)
RUN grep -q "^archipelago:\$" /etc/shadow && echo "Password set OK" || echo "WARNING: password may not be set"
2026-02-01 05:42:05 +00:00
# Set hostname
RUN echo "archipelago" > /etc/hostname
# Configure SSH
RUN mkdir -p /etc/ssh && \
sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config || true && \
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config || true
# Configure nginx for Archipelago
RUN rm -f /etc/nginx/sites-enabled/default
COPY nginx-archipelago.conf /etc/nginx/sites-available/archipelago
RUN ln -sf /etc/nginx/sites-available/archipelago /etc/nginx/sites-enabled/archipelago
2026-03-05 08:34:53 +00:00
# Install nginx snippets (PWA config, HTTPS app proxies)
COPY snippets/ /etc/nginx/snippets/
2026-07-01 10:42:20 -04:00
# Generate self-signed SSL certificate for HTTPS (PWA install + mic/camera
# access both require a secure context). SAN covers the install-time default
# hostname -- server.set-name regenerates this with the new hostname's SAN
# if a node is renamed.
2026-03-05 08:34:53 +00:00
RUN mkdir -p /etc/archipelago/ssl && \
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/archipelago/ssl/archipelago.key \
-out /etc/archipelago/ssl/archipelago.crt \
2026-07-01 10:42:20 -04:00
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" \
-addext "subjectAltName=DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" && \
2026-03-05 08:34:53 +00:00
chmod 600 /etc/archipelago/ssl/archipelago.key
2026-02-01 05:42:05 +00:00
# Create archipelago systemd service
COPY archipelago.service /etc/systemd/system/archipelago.service
2026-03-25 15:52:26 +00:00
COPY archipelago-update.service /etc/systemd/system/archipelago-update.service
COPY archipelago-update.timer /etc/systemd/system/archipelago-update.timer
2026-03-28 11:31:48 +00:00
COPY archipelago-doctor.service /etc/systemd/system/archipelago-doctor.service
COPY archipelago-doctor.timer /etc/systemd/system/archipelago-doctor.timer
2026-03-29 19:26:21 +01:00
COPY archipelago-tor-helper.service /etc/systemd/system/archipelago-tor-helper.service
COPY archipelago-tor-helper.path /etc/systemd/system/archipelago-tor-helper.path
2026-04-07 14:40:33 +01:00
COPY nostr-vpn.service /etc/systemd/system/nostr-vpn.service
2026-04-08 20:27:38 +02:00
COPY archipelago-wg.service /etc/systemd/system/archipelago-wg.service
2026-04-08 13:15:09 +02:00
COPY archipelago-wg-address.service /etc/systemd/system/archipelago-wg-address.service
2026-04-18 22:57:51 -04:00
COPY archipelago-fips.service /etc/systemd/system/archipelago-fips.service
2026-04-08 15:06:27 +02:00
COPY nostr-relay.service /etc/systemd/system/nostr-relay.service
COPY nostr-relay-config.toml /etc/archipelago/nostr-relay-config.toml
2026-04-08 13:15:09 +02:00
# WireGuard kernel module auto-load on boot
RUN echo "wireguard" >> /etc/modules-load.d/wireguard.conf
2026-02-01 05:42:05 +00:00
2026-04-23 03:04:58 -04:00
# Copy container doctor + reconcile scripts (referenced by services and the
# OTA update RPC; the reconcile systemd timer is gone as of Step 8a, but the
# script stays until Step 8b/c ports all manifests — update.rs still shells
# out to it during package updates).
2026-03-30 13:35:02 +01:00
RUN mkdir -p /home/archipelago/archy/scripts/lib
2026-03-28 23:41:40 +00:00
COPY container-doctor.sh /home/archipelago/archy/scripts/container-doctor.sh
COPY reconcile-containers.sh /home/archipelago/archy/scripts/reconcile-containers.sh
2026-03-30 13:35:02 +01:00
COPY container-specs.sh /home/archipelago/archy/scripts/container-specs.sh
2026-03-29 19:26:21 +01:00
COPY tor-helper.sh /opt/archipelago/scripts/tor-helper.sh
2026-03-30 13:35:02 +01:00
COPY lib/ /home/archipelago/archy/scripts/lib/
RUN chmod +x /home/archipelago/archy/scripts/*.sh /home/archipelago/archy/scripts/lib/*.sh /opt/archipelago/scripts/*.sh && \
2026-03-28 23:41:40 +00:00
chown -R archipelago:archipelago /home/archipelago/archy
2026-04-18 11:07:08 -04:00
# Enable cgroup delegation for rootless podman (CPU/memory limits require this)
RUN mkdir -p /etc/systemd/system/user@.service.d && \
printf '[Service]\nDelegate=cpu cpuset io memory pids\n' > /etc/systemd/system/user@.service.d/delegate.conf
# Allow unprivileged ping inside rootless containers
RUN printf 'net.ipv4.ping_group_range=0 2147483647\n' > /etc/sysctl.d/90-podman-ping.conf
2026-05-15 18:15:06 -04:00
# Archipelago's web UI manages Wi-Fi through the backend service, not a local
# desktop seat. Allow the dedicated system user to control NetworkManager.
RUN mkdir -p /etc/polkit-1/rules.d && \
printf '%s\n' \
'polkit.addRule(function(action, subject) {' \
' if (subject.user == "archipelago" && action.id.indexOf("org.freedesktop.NetworkManager.") == 0) {' \
' return polkit.Result.YES;' \
' }' \
'});' \
> /etc/polkit-1/rules.d/49-archipelago-networkmanager.rules && \
chmod 644 /etc/polkit-1/rules.d/49-archipelago-networkmanager.rules
2026-02-01 05:42:05 +00:00
# Enable services
RUN systemctl enable NetworkManager || true && \
2026-05-15 18:15:06 -04:00
systemctl enable polkit || systemctl enable polkit.service || true && \
2026-02-01 05:42:05 +00:00
systemctl enable ssh || true && \
systemctl enable nginx || true && \
2026-07-01 10:42:20 -04:00
systemctl enable avahi-daemon || true && \
2026-03-14 03:17:12 +00:00
systemctl enable archipelago || true && \
systemctl enable tor || true && \
2026-03-18 13:53:27 +00:00
systemctl enable tailscaled || true && \
2026-03-25 15:52:26 +00:00
systemctl enable chrony || true && \
2026-03-28 11:31:48 +00:00
systemctl enable archipelago-update.timer || true && \
systemctl enable archipelago-doctor.timer || true && \
2026-04-08 13:15:09 +02:00
systemctl enable archipelago-tor-helper.path || true && \
2026-04-08 17:48:38 +02:00
systemctl enable nostr-relay || true
2026-04-19 16:33:21 -04:00
# archipelago-fips.service + archipelago-wg.service + archipelago-wg-address.service
# stay installed and enabled. They all use `ConditionPathExists=` on their
# respective seed-derived key files, so on a fresh pre-onboarding boot
# systemd quietly skips them with no [FAILED] in the MOTD. Once the user
# completes the seed onboarding flow, archipelago writes the key files,
# the archipelago backend calls `systemctl start archipelago-fips.service`
# (see server.rs post-onboarding auto-activate block) and the WG setup
# path runs `archipelago-wg setup` directly. No masking, no user-facing
# "Activate" button — install → onboard → FIPS + WG are just running.
RUN systemctl enable archipelago-fips.service || true
2026-03-18 13:53:27 +00:00
2026-04-19 16:33:21 -04:00
# nostr-vpn is the legacy nostr-tunnel service — deprecated in favour of
# the upstream FIPS daemon. It still crash-loops on boot if left enabled
# (env file doesn't exist until onboarding) so we mask it outright.
# `systemctl mask` alone doesn't stick because the real .service file is
# already in place — explicit rm + /dev/null symlink is what sticks.
RUN rm -f /etc/systemd/system/nostr-vpn.service && \\
ln -sf /dev/null /etc/systemd/system/nostr-vpn.service
2026-04-19 08:27:22 -04:00
2026-03-18 13:53:27 +00:00
# Remove policy-rc.d so services can start on first boot
RUN rm -f /usr/sbin/policy-rc.d
2026-02-01 05:42:05 +00:00
2026-03-05 08:34:53 +00:00
# Create directories (including Cloud storage for FileBrowser)
2026-04-08 15:43:22 +02:00
RUN mkdir -p /var/lib/archipelago/data /var/lib/archipelago/config /var/lib/archipelago/containers /var/lib/archipelago/nostr-relay /var/lib/archipelago/nostr-vpn && \
2026-02-01 05:42:05 +00:00
mkdir -p /etc/archipelago && \
2026-04-08 15:43:22 +02:00
mkdir -p /opt/archipelago/bin /opt/archipelago/scripts /opt/archipelago/web-ui && \
mkdir -p /var/lib/archipelago/data/cloud/Documents /var/lib/archipelago/data/cloud/Photos /var/lib/archipelago/data/cloud/Music /var/lib/archipelago/data/cloud/Videos /var/lib/archipelago/data/cloud/Downloads && \
2026-04-08 15:06:27 +02:00
cp /etc/archipelago/nostr-relay-config.toml /var/lib/archipelago/nostr-relay/config.toml && \
2026-02-01 05:42:05 +00:00
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
2026-04-19 16:33:21 -04:00
# Persist journalctl across reboots — without /var/log/journal systemd
# journal uses tmpfs and everything before the last boot is lost. We
# need the full history to diagnose first-boot / install / onboarding
2026-07-02 08:00:39 -04:00
# issues after the fact. Size cap keeps it from eating the disk, and the
# explicit rate limit stops a single chatty service (e.g. a container
# spamming conmon->journald during Bitcoin IBD) from drowning the journal.
# Keep this byte-identical to image-recipe/configs/journald-archipelago.conf —
# the backend self-heals the same file onto deployed nodes (bootstrap.rs).
2026-04-19 16:33:21 -04:00
RUN mkdir -p /var/log/journal && \
systemd-tmpfiles --create --prefix /var/log/journal 2>/dev/null || true && \
install -d -m 0755 /etc/systemd/journald.conf.d && \
2026-07-02 08:00:39 -04:00
printf '[Journal]\nStorage=persistent\nSystemMaxUse=500M\nRuntimeMaxUse=100M\nForwardToSyslog=no\nRateLimitIntervalSec=30s\nRateLimitBurst=10000\n' > /etc/systemd/journald.conf.d/10-archipelago-persistent.conf
2026-04-19 16:33:21 -04:00
2026-02-01 05:42:05 +00:00
# Clean up
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
DOCKERFILE
2026-03-05 08:34:53 +00:00
# Copy nginx snippets for HTTPS (PWA, app proxies)
2026-04-28 15:00:58 -04:00
if [ -d " $SCRIPT_DIR /../configs/snippets" ] ; then
2026-03-05 08:34:53 +00:00
mkdir -p " $WORK_DIR /snippets"
2026-04-28 15:00:58 -04:00
cp " $SCRIPT_DIR /../configs/snippets/" *.conf " $WORK_DIR /snippets/" 2>/dev/null || true
2026-03-05 08:34:53 +00:00
echo " Using nginx snippets from configs/snippets/"
else
mkdir -p " $WORK_DIR /snippets"
echo " ⚠ No nginx snippets found, HTTPS features may not work"
fi
2026-03-22 03:30:21 +00:00
# Use nginx config from configs/ (includes app proxies for Nextcloud, Vaultwarden, etc.)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/nginx-archipelago.conf" ] ; then
cp " $SCRIPT_DIR /../configs/nginx-archipelago.conf" " $WORK_DIR /nginx-archipelago.conf"
2026-02-25 18:20:50 +00:00
echo " Using nginx config from configs/nginx-archipelago.conf"
else
echo " ⚠ configs/nginx-archipelago.conf not found, using minimal config"
cat > " $WORK_DIR /nginx-archipelago.conf" <<'NGINXCONF'
2026-02-01 05:42:05 +00:00
server {
listen 80;
server_name _;
root /opt/archipelago/web-ui;
index index.html;
2026-02-25 18:20:50 +00:00
location / { try_files $uri $uri/ /index.html; }
location /archipelago/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
location /rpc/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; }
location /ws { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400s; }
2026-02-01 05:42:05 +00:00
}
NGINXCONF
2026-02-25 18:20:50 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-03-18 10:50:13 +00:00
# Copy udev rule for mesh radio stable naming
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/99-mesh-radio.rules" ] ; then
cp " $SCRIPT_DIR /../configs/99-mesh-radio.rules" " $WORK_DIR /99-mesh-radio.rules"
2026-03-18 10:50:13 +00:00
echo " Using 99-mesh-radio.rules from configs/"
fi
2026-03-25 15:52:26 +00:00
# Copy update service and timer
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-update.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-update.service" " $WORK_DIR /archipelago-update.service"
cp " $SCRIPT_DIR /../configs/archipelago-update.timer" " $WORK_DIR /archipelago-update.timer"
2026-03-25 15:52:26 +00:00
echo " Using archipelago-update.service + timer from configs/"
fi
2026-04-23 03:04:58 -04:00
# Copy container doctor timer + reconcile script (the reconcile systemd
# timer is gone as of Step 8a — BootReconciler replaces it — but the
# reconcile-containers.sh script stays, invoked by the OTA update RPC
# until Step 8b/c ports all manifests to the Rust orchestrator).
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-doctor.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-doctor.service" " $WORK_DIR /archipelago-doctor.service"
cp " $SCRIPT_DIR /../configs/archipelago-doctor.timer" " $WORK_DIR /archipelago-doctor.timer"
2026-04-23 03:04:58 -04:00
# Copy the actual scripts the services / update RPC reference
2026-03-30 13:35:02 +01:00
for s in container-doctor.sh reconcile-containers.sh container-specs.sh tor-helper.sh; do
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/ $s " ] ; then
cp " $SCRIPT_DIR /../../scripts/ $s " " $WORK_DIR / $s "
2026-03-28 23:41:40 +00:00
fi
done
2026-03-30 13:35:02 +01:00
# Copy shared script library (mem_limit etc.)
2026-04-28 15:00:58 -04:00
if [ -d " $SCRIPT_DIR /../../scripts/lib" ] ; then
2026-03-30 13:35:02 +01:00
mkdir -p " $WORK_DIR /lib"
2026-04-28 15:00:58 -04:00
cp " $SCRIPT_DIR /../../scripts/lib/" *.sh " $WORK_DIR /lib/" 2>/dev/null || true
2026-03-30 13:35:02 +01:00
fi
2026-04-23 03:04:58 -04:00
echo " Using container doctor timer from configs/"
2026-03-28 11:31:48 +00:00
fi
2026-03-29 19:26:21 +01:00
# Copy Tor helper path-activated service (allows backend to manage Tor as non-root)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-tor-helper.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-tor-helper.service" " $WORK_DIR /archipelago-tor-helper.service"
cp " $SCRIPT_DIR /../configs/archipelago-tor-helper.path" " $WORK_DIR /archipelago-tor-helper.path"
2026-03-29 19:26:21 +01:00
echo " Using tor-helper path unit from configs/"
fi
2026-04-07 14:40:33 +01:00
# Copy NostrVPN system service (native mesh VPN, not a container)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/nostr-vpn.service" ] ; then
cp " $SCRIPT_DIR /../configs/nostr-vpn.service" " $WORK_DIR /nostr-vpn.service"
2026-04-07 14:40:33 +01:00
echo " Using nostr-vpn.service from configs/"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-wg.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-wg.service" " $WORK_DIR /archipelago-wg.service"
2026-04-08 20:27:38 +02:00
echo " Using archipelago-wg.service from configs/"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-wg-address.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-wg-address.service" " $WORK_DIR /archipelago-wg-address.service"
2026-04-08 13:15:09 +02:00
echo " Using archipelago-wg-address.service from configs/"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago-fips.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago-fips.service" " $WORK_DIR /archipelago-fips.service"
2026-04-18 22:57:51 -04:00
echo " Using archipelago-fips.service from configs/"
fi
2026-04-08 13:15:09 +02:00
2026-04-08 15:06:27 +02:00
# Copy private Nostr relay service (native, for NostrVPN signaling)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/nostr-relay.service" ] ; then
cp " $SCRIPT_DIR /../configs/nostr-relay.service" " $WORK_DIR /nostr-relay.service"
2026-04-08 15:06:27 +02:00
echo " Using nostr-relay.service from configs/"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/nostr-relay-config.toml" ] ; then
cp " $SCRIPT_DIR /../configs/nostr-relay-config.toml" " $WORK_DIR /nostr-relay-config.toml"
2026-04-08 15:06:27 +02:00
echo " Using nostr-relay-config.toml from configs/"
fi
2026-04-08 13:15:09 +02:00
# Copy WireGuard helper script (privileged peer management)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/archipelago-wg" ] ; then
cp " $SCRIPT_DIR /../../scripts/archipelago-wg" " $WORK_DIR /archipelago-wg"
2026-04-08 13:15:09 +02:00
echo " Using archipelago-wg helper from scripts/"
fi
2026-02-25 18:20:50 +00:00
# Use archipelago.service from configs/ (User=root for Podman container access)
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../configs/archipelago.service" ] ; then
cp " $SCRIPT_DIR /../configs/archipelago.service" " $WORK_DIR /archipelago.service"
2026-02-25 18:20:50 +00:00
echo " Using archipelago.service from configs/"
else
cat > " $WORK_DIR /archipelago.service" <<'SYSTEMDSERVICE'
2026-02-01 05:42:05 +00:00
[Unit]
Description=Archipelago Backend
2026-03-27 16:16:57 +00:00
After=network-online.target archipelago-setup-tor.service
2026-02-01 05:42:05 +00:00
Wants=network-online.target
[Service]
Type=simple
2026-03-27 16:16:57 +00:00
User=archipelago
2026-03-19 12:55:31 +00:00
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
2026-03-27 16:16:57 +00:00
Environment="XDG_RUNTIME_DIR=/run/user/1000"
ExecStartPre=/bin/bash -c 'mkdir -p /run/user/1000 && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
2026-02-01 05:42:05 +00:00
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
2026-03-27 16:16:57 +00:00
ProtectHome=no
2026-02-01 05:42:05 +00:00
[Install]
WantedBy=multi-user.target
SYSTEMDSERVICE
2026-02-25 18:20:50 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-02-01 18:46:35 +00:00
echo " Building $CONTAINER_CMD image (this may take a few minutes)..."
2026-03-25 21:31:51 +00:00
$CONTAINER_CMD build --no-cache --platform $CONTAINER_PLATFORM -t archipelago-rootfs -f " $WORK_DIR /Dockerfile.rootfs" " $WORK_DIR "
2026-02-01 05:42:05 +00:00
echo " Exporting filesystem..."
2026-04-02 15:16:24 +01:00
$CONTAINER_CMD rm -f archipelago-rootfs-tmp 2>/dev/null || true
2026-03-12 00:19:30 +00:00
$CONTAINER_CMD create --platform $CONTAINER_PLATFORM --name archipelago-rootfs-tmp archipelago-rootfs
2026-02-01 18:46:35 +00:00
$CONTAINER_CMD export archipelago-rootfs-tmp > " $ROOTFS_TAR "
$CONTAINER_CMD rm archipelago-rootfs-tmp
2026-02-01 05:42:05 +00:00
echo "✅ Root filesystem created: $( du -h " $ROOTFS_TAR " | cut -f1) "
else
echo "✅ Using cached root filesystem: $( du -h " $ROOTFS_TAR " | cut -f1) "
fi
# =============================================================================
2026-03-27 18:19:49 +00:00
# STEP 2: Build minimal installer environment (replaces Debian Live)
2026-02-01 05:42:05 +00:00
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 2: Building minimal installer environment via debootstrap..."
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
INSTALLER_ISO = " $WORK_DIR /installer-iso"
INSTALLER_SQUASHFS = " $WORK_DIR /installer-squashfs"
rm -rf " $INSTALLER_ISO " " $INSTALLER_SQUASHFS "
mkdir -p " $INSTALLER_ISO /live" " $INSTALLER_ISO /archipelago"
mkdir -p " $INSTALLER_ISO /boot/grub" " $INSTALLER_ISO /isolinux"
mkdir -p " $INSTALLER_ISO /EFI/BOOT"
2026-02-14 16:44:20 +00:00
2026-03-27 18:19:49 +00:00
# Build the installer filesystem inside a container
# This creates: vmlinuz, initrd.img, filesystem.squashfs
2026-04-19 14:40:52 -04:00
# NOTE: the installer-env script is written to a file and bind-mounted into the
# container rather than passed via `bash -c '...'`. On some hosts, the inline
# form somehow interferes with debootstrap's dpkg-deb|tar extraction (repro'd
# on this box: bash -c fails at "Extracting apt...", bash /script.sh succeeds).
_INSTALLER_ENV_SCRIPT = " $WORK_DIR /_installer-env.sh"
cat > " $_INSTALLER_ENV_SCRIPT " <<'INSTALLER_ENV_EOF'
2026-03-27 18:19:49 +00:00
set -e
apt-get update -qq
apt-get install -y -qq debootstrap squashfs-tools initramfs-tools dosfstools mtools \
grub-efi-amd64-bin grub-pc-bin grub-common isolinux syslinux-common
echo " [container] Running debootstrap --variant=minbase..."
2026-04-19 09:54:12 -04:00
# ifupdown + isc-dhcp-client added because live-boot's /init writes
# /etc/network/interfaces on the target — without ifupdown, /etc/network/
# doesn't exist and the initramfs throws a non-fatal but noisy
# "can't create /root/etc/network/interfaces: nonexistent directory".
2026-03-27 18:19:49 +00:00
debootstrap --variant=minbase --arch=${DEB_ARCH} \
--include=systemd,systemd-sysv,udev,dbus,bash,coreutils,mount,util-linux,\
kmod,procps,iproute2,ca-certificates,gdisk,\
cryptsetup,cryptsetup-initramfs,parted,dosfstools,e2fsprogs,\
linux-image-${DEB_ARCH},grub-efi-${DEB_ARCH},grub-pc-bin,\
2026-04-19 09:54:12 -04:00
ifupdown,isc-dhcp-client,\
2026-05-15 18:32:14 -04:00
wpasupplicant,wireless-regdb,iw,rfkill,\
2026-03-27 18:19:49 +00:00
pciutils,usbutils,less,nano \
2026-04-09 20:42:09 +02:00
trixie /installer http://deb.debian.org/debian
2026-03-27 18:19:49 +00:00
2026-03-27 22:26:56 +00:00
# Install live-boot via chroot — debootstrap minbase resolver cannot handle it.
# The chroot approach works (confirmed in CI run 90) — just needs proc/sys/dev mounts.
2026-03-27 19:11:54 +00:00
echo " [container] Installing live-boot for squashfs root support..."
2026-03-27 19:45:52 +00:00
cp /etc/resolv.conf /installer/etc/resolv.conf 2>/dev/null || true
2026-03-27 22:26:56 +00:00
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
chroot /installer apt-get update -qq
2026-05-05 11:29:18 -04:00
chroot /installer apt-get -y -qq full-upgrade
2026-03-27 22:26:56 +00:00
chroot /installer apt-get install -y --no-install-recommends live-boot live-boot-initramfs-tools
chroot /installer apt-get clean
umount /installer/dev 2>/dev/null || true
umount /installer/sys 2>/dev/null || true
umount /installer/proc 2>/dev/null || true
2026-03-27 19:45:52 +00:00
2026-03-27 22:26:56 +00:00
# Verify live-boot hooks are in place (scripts/live is a FILE not a directory)
2026-03-27 20:36:39 +00:00
if [ -e /installer/usr/share/initramfs-tools/scripts/live ]; then
2026-03-27 19:11:54 +00:00
echo " [container] live-boot initramfs hooks: OK"
else
2026-03-27 22:26:56 +00:00
echo " [container] FATAL: live-boot hooks not found after install!"
ls -la /installer/usr/share/initramfs-tools/scripts/ 2>/dev/null
exit 1
2026-03-27 19:11:54 +00:00
fi
2026-03-27 18:19:49 +00:00
echo " [container] Configuring installer environment..."
# Set hostname
echo "archipelago-installer" > /installer/etc/hostname
# Set root password
echo "root:archipelago" | chroot /installer chpasswd
# Auto-login on tty1
mkdir -p /installer/etc/systemd/system/getty@tty1.service.d
cat > /installer/etc/systemd/system/getty@tty1.service.d/autologin.conf <<GETTY
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I \$TERM
GETTY
2026-03-28 00:35:34 +00:00
# Auto-start installer via profile.d (runs after auto-login, no getty race)
# This is the same approach the working Debian Live build used.
mkdir -p /installer/etc/profile.d
2026-03-28 18:44:36 +00:00
cat > /installer/etc/profile.d/z99-archipelago-installer.sh <<PROFILE
2026-03-27 18:19:49 +00:00
#!/bin/bash
2026-03-28 00:35:34 +00:00
# Auto-start Archipelago installer on login — only run once
2026-03-28 18:44:36 +00:00
if [ -n "\$INSTALLER_STARTED" ]; then
2026-03-28 00:35:34 +00:00
return 0 2>/dev/null || exit 0
fi
export INSTALLER_STARTED=1
sleep 1
2026-03-27 18:19:49 +00:00
clear
echo ""
2026-03-28 23:58:42 +00:00
echo -e "\033[38;5;208m ▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█\033[0m"
echo -e "\033[38;5;208m █▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █\033[0m"
echo -e "\033[38;5;208m ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀\033[0m"
echo -e " \033[38;5;130mbitcoin node os\033[0m"
2026-03-27 18:19:49 +00:00
echo ""
BOOT_MEDIA=""
2026-03-27 19:08:00 +00:00
for dev in /run/live/medium /lib/live/mount/medium /run/archiso /cdrom /media/cdrom /mnt/iso; do
2026-03-28 18:44:36 +00:00
if [ -f "\$dev/archipelago/auto-install.sh" ]; then
BOOT_MEDIA="\$dev"
2026-03-27 18:19:49 +00:00
break
2026-02-14 16:44:20 +00:00
fi
2026-03-27 18:19:49 +00:00
done
2026-03-28 18:44:36 +00:00
# If standard mount points failed, actively find and mount the boot device
if [ -z "\$BOOT_MEDIA" ]; then
echo -e " \033[37mSearching for boot device...\033[0m"
mkdir -p /run/archiso 2>/dev/null
for blk in /dev/sr0 /dev/sd[a-z] /dev/sd[a-z][0-9] /dev/nvme[0-9]n[0-9]p[0-9]; do
[ -b "\$blk" ] || continue
mount -o ro "\$blk" /run/archiso 2>/dev/null || continue
if [ -f /run/archiso/archipelago/auto-install.sh ]; then
BOOT_MEDIA="/run/archiso"
break
fi
umount /run/archiso 2>/dev/null
done
fi
if [ -n "\$BOOT_MEDIA" ]; then
echo -e " \033[37mFound installer at: \$BOOT_MEDIA\033[0m"
2026-03-27 18:19:49 +00:00
echo ""
2026-03-28 15:15:42 +00:00
echo -e " Press Enter to install | \033[1;37mCtrl+C\033[0m for shell"
2026-03-28 11:31:48 +00:00
read -s
2026-03-28 18:44:36 +00:00
bash "\$BOOT_MEDIA/archipelago/auto-install.sh"
2026-03-27 18:19:49 +00:00
else
2026-03-28 15:15:42 +00:00
echo -e " \033[37mInstaller not found on boot media.\033[0m"
2026-03-28 00:35:34 +00:00
echo ""
2026-03-28 18:44:36 +00:00
echo -e " \033[37mDebug info:\033[0m"
ls -la /run/live/ 2>/dev/null || echo " /run/live/ does not exist"
mount | grep -E "iso9660|squashfs|overlay" 2>/dev/null
echo ""
echo -e " \033[37mTry: mount /dev/sdX /mnt/iso && bash /mnt/iso/archipelago/auto-install.sh\033[0m"
echo ""
2026-02-14 16:44:20 +00:00
fi
2026-03-28 00:35:34 +00:00
PROFILE
chmod +x /installer/etc/profile.d/z99-archipelago-installer.sh
2026-02-14 16:44:20 +00:00
2026-03-27 18:19:49 +00:00
# Custom initramfs hook: mount ISO boot media at /run/archiso
mkdir -p /installer/etc/initramfs-tools/hooks
cat > /installer/etc/initramfs-tools/hooks/archipelago <<HOOK
#!/bin/sh
set -e
PREREQ=""
prereqs() { echo "\$PREREQ"; }
case "\$1" in prereqs) prereqs; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
# Ensure mount helpers and filesystem tools are in initramfs
copy_exec /bin/mount
copy_exec /bin/umount
copy_exec /bin/findfs 2>/dev/null || true
copy_exec /sbin/blkid
manual_add_modules iso9660 vfat squashfs overlay
HOOK
chmod +x /installer/etc/initramfs-tools/hooks/archipelago
mkdir -p /installer/etc/initramfs-tools/scripts/local-bottom
cat > /installer/etc/initramfs-tools/scripts/local-bottom/archipelago-mount <<INITSCRIPT
#!/bin/sh
PREREQ=""
prereqs() { echo "\$PREREQ"; }
case "\$1" in prereqs) prereqs; exit 0;; esac
. /scripts/functions
# Try to find and mount the Archipelago boot media
mkdir -p /run/archiso
log_begin_msg "Searching for Archipelago boot media..."
# Try CD-ROM first, then USB partitions
for dev in /dev/sr0 /dev/sd??* /dev/nvme*p*; do
[ -b "\$dev" ] 2>/dev/null || continue
mount -o ro "\$dev" /run/archiso 2>/dev/null || continue
if [ -d /run/archiso/archipelago ]; then
log_end_msg 0
echo "Found Archipelago media on \$dev"
exit 0
2026-02-14 16:44:20 +00:00
fi
2026-03-27 18:19:49 +00:00
umount /run/archiso 2>/dev/null || true
done
log_end_msg 1
echo "Archipelago boot media not found (will retry from userspace)"
INITSCRIPT
chmod +x /installer/etc/initramfs-tools/scripts/local-bottom/archipelago-mount
# Strip docs and man pages from installer
rm -rf /installer/usr/share/man/* /installer/usr/share/doc/*
rm -rf /installer/var/lib/apt/lists/* /installer/var/cache/apt/*
# Extract kernel
KVER=$(ls /installer/lib/modules/ | sort -V | tail -1)
echo " [container] Kernel version: $KVER"
cp /installer/boot/vmlinuz-$KVER /output/vmlinuz
2026-03-27 19:08:00 +00:00
# Mount virtual filesystems for proper initramfs generation
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
# Build initramfs with live-boot hooks + our custom hooks
2026-03-27 18:19:49 +00:00
chroot /installer update-initramfs -c -k $KVER
cp /installer/boot/initrd.img-$KVER /output/initrd.img
2026-03-27 19:08:00 +00:00
# Cleanup mounts
umount /installer/dev 2>/dev/null || true
umount /installer/sys 2>/dev/null || true
umount /installer/proc 2>/dev/null || true
2026-03-27 18:19:49 +00:00
# Create squashfs
echo " [container] Creating installer squashfs..."
mksquashfs /installer /output/filesystem.squashfs -comp xz -Xbcj x86 -noappend -quiet
2026-03-27 23:33:31 +00:00
# Build GRUB EFI image with embedded bootstrap config (grub-mkstandalone)
2026-03-27 18:19:49 +00:00
echo " [container] Building GRUB EFI image..."
2026-03-28 18:44:36 +00:00
cat > /tmp/grub-embed.cfg <<GRUBEMBED
2026-03-28 03:15:47 +00:00
insmod part_gpt
2026-03-28 23:58:42 +00:00
insmod part_msdos
2026-03-28 03:15:47 +00:00
insmod fat
insmod iso9660
2026-03-28 23:58:42 +00:00
insmod search
2026-03-28 03:15:47 +00:00
insmod search_label
2026-03-28 23:58:42 +00:00
insmod search_fs_file
2026-03-28 03:15:47 +00:00
insmod normal
insmod linux
insmod all_video
2026-03-28 23:58:42 +00:00
# Try label first (standard path)
2026-03-27 23:33:31 +00:00
search --no-floppy --set=root --label ARCHIPELAGO
2026-03-28 23:58:42 +00:00
# Fallback: search for a known file on the ISO
if [ -z "\$root" ]; then
search --no-floppy --set=root --file /archipelago/auto-install.sh
fi
# Fallback: try configfile from the EFI partition path
if [ -z "\$root" ]; then
set root=\$cmdpath
fi
2026-03-28 18:44:36 +00:00
set prefix=(\$root)/boot/grub
2026-03-28 23:58:42 +00:00
configfile (\$root)/boot/grub/grub.cfg
# If configfile fails, try normal
2026-03-28 03:15:47 +00:00
normal
2026-03-27 23:33:31 +00:00
GRUBEMBED
2026-03-27 18:19:49 +00:00
2026-03-27 23:33:31 +00:00
grub-mkstandalone -O x86_64-efi \
2026-03-28 23:58:42 +00:00
--modules="part_gpt part_msdos fat iso9660 search search_label search_fs_file normal linux all_video font gfxterm configfile echo cat ls test true loopback png" \
2026-03-27 23:33:31 +00:00
--locales="" \
--themes="" \
--fonts="" \
--output=/output/BOOTX64.EFI \
"boot/grub/grub.cfg=/tmp/grub-embed.cfg"
# Create EFI FAT image (20MB — includes GRUB binary + grub.cfg)
dd if=/dev/zero of=/output/efi.img bs=1M count=20 2>/dev/null
2026-03-27 18:19:49 +00:00
mkfs.vfat /output/efi.img >/dev/null
mmd -i /output/efi.img ::/EFI ::/EFI/BOOT
mcopy -i /output/efi.img /output/BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI
# Copy ISOLINUX files for legacy BIOS boot
cp /usr/lib/ISOLINUX/isolinux.bin /output/isolinux.bin
cp /usr/lib/syslinux/modules/bios/ldlinux.c32 /output/ldlinux.c32
cp /usr/lib/syslinux/modules/bios/menu.c32 /output/menu.c32 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp /usr/lib/syslinux/modules/bios/vesamenu.c32 /output/vesamenu.c32 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp /usr/lib/syslinux/modules/bios/libutil.c32 /output/libutil.c32 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp /usr/lib/syslinux/modules/bios/libcom32.c32 /output/libcom32.c32 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp /usr/lib/ISOLINUX/isohdpfx.bin /output/isohdpfx.bin
# Generate GRUB fonts for theme
echo " [container] Generating GRUB fonts..."
apt-get install -y -qq fonts-dejavu-core grub-common >/dev/null 2>&1
mkdir -p /output/grub-fonts
2026-03-31 17:35:34 +01:00
grub-mkfont -s 12 -o /output/grub-fonts/dejavu_12.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
grub-mkfont -s 14 -o /output/grub-fonts/dejavu_14.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
grub-mkfont -s 16 -o /output/grub-fonts/dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
2026-03-27 18:19:49 +00:00
grub-mkfont -s 24 -o /output/grub-fonts/dejavu_24.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf
echo " [container] Done!"
2026-04-19 14:40:52 -04:00
INSTALLER_ENV_EOF
$CONTAINER_CMD run --rm --privileged --platform $CONTAINER_PLATFORM \
-v " $WORK_DIR :/output" \
-v " $_INSTALLER_ENV_SCRIPT :/installer-env.sh:ro" \
-e DEB_ARCH = " $DEB_ARCH " \
-e LIB_DIR = " $LIB_DIR " \
debian:trixie bash /installer-env.sh
2026-03-27 18:19:49 +00:00
# Verify artifacts
for artifact in vmlinuz initrd.img filesystem.squashfs BOOTX64.EFI efi.img isolinux.bin isohdpfx.bin; do
if [ ! -f " $WORK_DIR / $artifact " ] ; then
echo " FATAL: Missing build artifact: $artifact "
2026-02-14 16:44:20 +00:00
exit 1
fi
2026-03-27 18:19:49 +00:00
done
# Place artifacts into ISO directory structure
cp " $WORK_DIR /vmlinuz" " $INSTALLER_ISO /live/vmlinuz"
cp " $WORK_DIR /initrd.img" " $INSTALLER_ISO /live/initrd.img"
cp " $WORK_DIR /filesystem.squashfs" " $INSTALLER_ISO /live/filesystem.squashfs"
cp " $WORK_DIR /BOOTX64.EFI" " $INSTALLER_ISO /EFI/BOOT/BOOTX64.EFI"
2026-03-28 00:35:34 +00:00
cp " $WORK_DIR /efi.img" " $INSTALLER_ISO /boot/grub/efi.img"
2026-03-27 18:19:49 +00:00
cp " $WORK_DIR /isolinux.bin" " $INSTALLER_ISO /isolinux/isolinux.bin"
cp " $WORK_DIR /ldlinux.c32" " $INSTALLER_ISO /isolinux/ldlinux.c32"
cp " $WORK_DIR /menu.c32" " $INSTALLER_ISO /isolinux/menu.c32" 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp " $WORK_DIR /vesamenu.c32" " $INSTALLER_ISO /isolinux/vesamenu.c32" 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp " $WORK_DIR /libutil.c32" " $INSTALLER_ISO /isolinux/libutil.c32" 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp " $WORK_DIR /libcom32.c32" " $INSTALLER_ISO /isolinux/libcom32.c32" 2>/dev/null || true
2026-03-27 18:19:49 +00:00
# Install GRUB theme
THEME_SRC = " $SCRIPT_DIR /branding/grub-theme"
THEME_DST = " $INSTALLER_ISO /boot/grub/themes/archipelago"
mkdir -p " $THEME_DST "
if [ -f " $THEME_SRC /theme.txt" ] ; then
cp " $THEME_SRC /theme.txt" " $THEME_DST /"
echo " Installed GRUB theme from branding/grub-theme/"
fi
# Install generated fonts
if [ -d " $WORK_DIR /grub-fonts" ] ; then
cp " $WORK_DIR /grub-fonts/" *.pf2 " $THEME_DST /"
# Also copy unicode font for GRUB to load
cp " $WORK_DIR /grub-fonts/dejavu_16.pf2" " $INSTALLER_ISO /boot/grub/font.pf2"
2026-02-01 05:42:05 +00:00
fi
2026-03-27 22:26:56 +00:00
# Copy GRUB background image (static asset or generate if missing)
GRUB_BG = " $SCRIPT_DIR /branding/grub-theme/background.png"
if [ -f " $GRUB_BG " ] ; then
cp " $GRUB_BG " " $THEME_DST /background.png"
echo " Installed GRUB background"
elif [ -f " $SCRIPT_DIR /branding/generate-grub-background.py" ] ; then
echo " Generating GRUB background..."
python3 " $SCRIPT_DIR /branding/generate-grub-background.py" " $THEME_DST /background.png" 2>/dev/null || \
echo " WARNING: Could not generate GRUB background"
fi
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
echo " Installer squashfs: $( du -h " $INSTALLER_ISO /live/filesystem.squashfs" | cut -f1) "
echo " Kernel: $( du -h " $INSTALLER_ISO /live/vmlinuz" | cut -f1) "
echo " Initrd: $( du -h " $INSTALLER_ISO /live/initrd.img" | cut -f1) "
echo " Step 2 complete (custom minimal base, no Debian Live)"
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 3: Add Archipelago components
# =============================================================================
echo ""
echo "📦 Step 3: Adding Archipelago components..."
ARCH_DIR = " $INSTALLER_ISO /archipelago"
mkdir -p " $ARCH_DIR "
mkdir -p " $ARCH_DIR /bin"
mkdir -p " $ARCH_DIR /scripts"
2026-04-09 21:32:08 +02:00
# netavark + aardvark-dns are installed in the rootfs via Dockerfile.rootfs (Debian 13 packages).
# Do NOT copy from the build host — the host may run a different glibc version.
echo " netavark + aardvark-dns: included in rootfs (Debian 13 packages)"
2026-03-30 20:52:01 +01:00
2026-02-01 05:42:05 +00:00
# Copy the pre-built rootfs
echo " Including root filesystem..."
cp " $ROOTFS_TAR " " $ARCH_DIR /rootfs.tar"
2026-02-03 21:43:33 +00:00
# Capture backend binary from live server
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo " Building backend binary from source..."
else
echo " Capturing backend binary from live server..."
fi
2026-04-10 03:10:49 -04:00
# Try to get backend binary: local release build → local install → remote → container build
2026-02-03 21:43:33 +00:00
BACKEND_CAPTURED = 0
2026-04-10 03:10:49 -04:00
2026-04-19 15:19:56 -04:00
# The captured binary MUST report the same version as the checked-out
# core/archipelago/Cargo.toml, otherwise we're shipping a stale binary
# from an earlier version bump (which is what happened with the 14:40
# ISO — it grabbed an Apr-18 1.4.0 binary and the fleet rejected the
# fips.yaml it wrote out on Activate). The expected version is the one
# compiled into this build run.
EXPECTED_VERSION = " $( grep '^version' " $( cd " $SCRIPT_DIR /.." && pwd ) /core/archipelago/Cargo.toml" | head -1 | sed 's/.*"\(.*\)".*/\1/' ) "
echo " Expected backend version (from Cargo.toml): $EXPECTED_VERSION "
verify_backend_version() {
local bin = " $1 "
2026-04-19 15:41:48 -04:00
# CARGO_PKG_VERSION is compiled into the binary as a string literal.
# `strings` output concatenates adjacent printable bytes, so the
# version rarely sits on its own line — a fixed-string substring
# match is the right tool. The version is specific enough (e.g.
# "1.5.0-alpha") that accidental collisions with unrelated data
# are vanishingly unlikely.
if strings " $bin " 2>/dev/null | grep -qF " $EXPECTED_VERSION " ; then
echo " ✅ Version match: binary contains $EXPECTED_VERSION "
return 0
2026-04-19 15:19:56 -04:00
fi
2026-04-19 15:41:48 -04:00
echo " ⚠️ Captured binary does NOT contain expected version $EXPECTED_VERSION — it is stale"
return 1
2026-04-19 15:19:56 -04:00
}
2026-04-10 03:10:49 -04:00
# Check for local release binary first (works for both BUILD_FROM_SOURCE and normal mode)
LOCAL_RELEASE = " $( cd " $SCRIPT_DIR /.." && pwd ) /core/target/release/archipelago"
if [ -f " $LOCAL_RELEASE " ] ; then
2026-04-19 15:19:56 -04:00
if verify_backend_version " $LOCAL_RELEASE " ; then
cp " $LOCAL_RELEASE " " $ARCH_DIR /bin/archipelago"
chmod +x " $ARCH_DIR /bin/archipelago"
echo " ✅ Backend from local release build ( $( du -h " $ARCH_DIR /bin/archipelago" | cut -f1) )"
BACKEND_CAPTURED = 1
else
echo " Skipping stale local release binary; trying next source"
fi
2026-04-10 03:10:49 -04:00
fi
if [ " $BACKEND_CAPTURED " = "0" ] && [ " $BUILD_FROM_SOURCE " != "1" ] ; then
# Direct copy from ARCHIPELAGO_BIN env or local install
2026-03-26 16:23:19 +00:00
BIN = " ${ ARCHIPELAGO_BIN :- /usr/local/bin/archipelago } "
2026-04-19 15:19:56 -04:00
if [ -f " $BIN " ] && verify_backend_version " $BIN " ; then
2026-03-26 15:28:43 +00:00
cp " $BIN " " $ARCH_DIR /bin/archipelago"
2026-02-17 15:03:34 +00:00
chmod +x " $ARCH_DIR /bin/archipelago"
echo " ✅ Backend captured from local system ( $( du -h " $ARCH_DIR /bin/archipelago" | cut -f1) )"
BACKEND_CAPTURED = 1
fi
# Remote copy via SCP if local failed
if [ " $BACKEND_CAPTURED " = "0" ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
2026-04-19 15:19:56 -04:00
if scp " $DEV_SERVER :/usr/local/bin/archipelago" " $ARCH_DIR /bin/archipelago" 2>/dev/null && verify_backend_version " $ARCH_DIR /bin/archipelago" ; then
2026-02-03 21:43:33 +00:00
chmod +x " $ARCH_DIR /bin/archipelago"
echo " ✅ Backend captured from remote server ( $( du -h " $ARCH_DIR /bin/archipelago" | cut -f1) )"
BACKEND_CAPTURED = 1
2026-04-19 15:19:56 -04:00
else
rm -f " $ARCH_DIR /bin/archipelago"
2026-02-03 21:43:33 +00:00
fi
fi
fi
if [ " $BACKEND_CAPTURED " = "0" ] ; then
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
echo " ⚠️ Could not capture from live server, building from source..."
fi
BACKEND_DOCKERFILE = " $WORK_DIR /Dockerfile.backend"
cat > " $BACKEND_DOCKERFILE " <<'BACKENDFILE'
2026-04-09 20:42:09 +02:00
FROM rust:1.93-trixie as builder
2026-02-01 13:24:03 +00:00
WORKDIR /build
COPY core ./core
2026-05-13 22:59:55 -04:00
COPY scripts ./scripts
COPY image-recipe/configs ./image-recipe/configs
2026-02-01 13:24:03 +00:00
RUN cd core && cargo build --release --bin archipelago
BACKENDFILE
2026-05-13 22:59:55 -04:00
BACKEND_IMAGE = "localhost/archipelago-backend:iso"
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t " $BACKEND_IMAGE " -f " $BACKEND_DOCKERFILE " " $SCRIPT_DIR /.." ; then
2026-02-03 21:43:33 +00:00
echo " Extracting backend binary..."
2026-05-13 22:59:55 -04:00
BACKEND_CONTAINER = $( $CONTAINER_CMD create --platform $CONTAINER_PLATFORM " $BACKEND_IMAGE " )
2026-02-03 21:43:33 +00:00
$CONTAINER_CMD cp " $BACKEND_CONTAINER :/build/core/target/release/archipelago" " $ARCH_DIR /bin/" && \
echo " ✅ Backend binary built ( $( du -h " $ARCH_DIR /bin/archipelago" | cut -f1) )"
$CONTAINER_CMD rm " $BACKEND_CONTAINER "
else
echo " ❌ Backend build failed and server capture failed"
exit 1
2026-02-01 13:24:03 +00:00
fi
2026-02-01 05:42:05 +00:00
fi
2026-04-07 14:40:33 +01:00
# Extract NostrVPN binary from container image (native system service, not a container app)
2026-04-09 21:32:08 +02:00
# NOTE: The container image must be built against Debian 13's GLIBC (2.40).
# If built against a newer GLIBC, the binary will fail at runtime.
# Rebuild with: FROM debian:13 AS builder
2026-04-07 14:40:33 +01:00
echo " Extracting NostrVPN binary..."
2026-07-10 18:55:32 +01:00
_NVPN_IMG = " ${ NOSTR_VPN_IMAGE :- 146 .59.87.168: 3000 /lfg2025/nostr-vpn: v0 .3.7 } "
2026-04-09 20:42:09 +02:00
NVPN_IMAGE_ID = " $( $CONTAINER_CMD images -q " $_NVPN_IMG " 2>/dev/null) "
if [ -z " $NVPN_IMAGE_ID " ] ; then
$CONTAINER_CMD pull " $_NVPN_IMG " 2>/dev/null || true
2026-04-07 14:40:33 +01:00
fi
2026-04-09 20:42:09 +02:00
NVPN_CONTAINER = $( $CONTAINER_CMD create " $_NVPN_IMG " 2>/dev/null) || true
2026-04-07 14:40:33 +01:00
if [ -n " $NVPN_CONTAINER " ] ; then
$CONTAINER_CMD cp " $NVPN_CONTAINER :/usr/local/bin/nvpn" " $ARCH_DIR /bin/nvpn" 2>/dev/null && \
chmod +x " $ARCH_DIR /bin/nvpn" && \
echo " ✅ NostrVPN binary extracted ( $( du -h " $ARCH_DIR /bin/nvpn" | cut -f1) )"
$CONTAINER_CMD rm " $NVPN_CONTAINER " 2>/dev/null || true
2026-04-09 21:32:08 +02:00
# Check GLIBC compatibility — Debian 13 (Trixie) has GLIBC 2.40
2026-04-09 20:42:09 +02:00
if [ -f " $ARCH_DIR /bin/nvpn" ] ; then
NVPN_GLIBC = $( objdump -T " $ARCH_DIR /bin/nvpn" 2>/dev/null | grep -oP 'GLIBC_\K[0-9.]+' | sort -V | tail -1)
if [ -n " $NVPN_GLIBC " ] ; then
2026-04-09 21:32:08 +02:00
# Compare: if required GLIBC > 2.40, warn
if printf '%s\n' "2.40" " $NVPN_GLIBC " | sort -V | tail -1 | grep -qv "^2\.40 $" ; then
echo " ⚠ WARNING: nvpn binary requires GLIBC $NVPN_GLIBC but Debian 13 has 2.40"
echo " ⚠ The nvpn daemon will fail at runtime. Rebuild the container against Debian 13."
2026-04-09 20:42:09 +02:00
echo " ⚠ VPN invite/status will still work via Rust backend config.toml fallback."
else
2026-04-09 21:32:08 +02:00
echo " ✅ nvpn GLIBC compatibility OK (requires $NVPN_GLIBC , target has 2.40)"
2026-04-09 20:42:09 +02:00
fi
fi
fi
2026-04-07 14:40:33 +01:00
else
echo " ⚠ NostrVPN image not available — nvpn binary will be missing"
fi
2026-04-08 15:06:27 +02:00
# Extract nostr-rs-relay binary from container image (native system service for VPN signaling)
echo " Extracting nostr-rs-relay binary..."
2026-07-10 18:55:32 +01:00
RELAY_IMAGE = " $( $CONTAINER_CMD images -q 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null) "
2026-04-08 15:06:27 +02:00
if [ -z " $RELAY_IMAGE " ] ; then
2026-07-10 18:55:32 +01:00
$CONTAINER_CMD pull 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null || true
2026-04-08 15:06:27 +02:00
fi
2026-07-10 18:55:32 +01:00
RELAY_CONTAINER = $( $CONTAINER_CMD create 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null) || true
2026-04-08 15:06:27 +02:00
if [ -n " $RELAY_CONTAINER " ] ; then
$CONTAINER_CMD cp " $RELAY_CONTAINER :/usr/local/bin/nostr-rs-relay" " $ARCH_DIR /bin/nostr-rs-relay" 2>/dev/null && \
chmod +x " $ARCH_DIR /bin/nostr-rs-relay" && \
echo " ✅ nostr-rs-relay binary extracted ( $( du -h " $ARCH_DIR /bin/nostr-rs-relay" | cut -f1) )"
$CONTAINER_CMD rm " $RELAY_CONTAINER " 2>/dev/null || true
else
echo " ⚠ nostr-rs-relay image not available — relay binary will be missing"
fi
2026-04-08 13:15:09 +02:00
# Copy WireGuard helper script
if [ -f " $WORK_DIR /archipelago-wg" ] ; then
cp " $WORK_DIR /archipelago-wg" " $ARCH_DIR /bin/archipelago-wg"
chmod +x " $ARCH_DIR /bin/archipelago-wg"
echo " ✅ WireGuard helper script included"
fi
2026-04-07 14:40:33 +01:00
# Copy NostrVPN UI dashboard for nginx serving
if [ -d " $SCRIPT_DIR /../docker/nostr-vpn-ui" ] ; then
mkdir -p " $ARCH_DIR /web-ui/nostr-vpn"
cp " $SCRIPT_DIR /../docker/nostr-vpn-ui/index.html" " $ARCH_DIR /web-ui/nostr-vpn/"
echo " ✅ NostrVPN UI dashboard included"
fi
2026-02-03 21:43:33 +00:00
# Capture web UI from live server
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo " Building web UI from source..."
2026-02-01 05:42:05 +00:00
else
2026-02-03 21:43:33 +00:00
echo " Capturing web UI from live server..."
fi
mkdir -p " $ARCH_DIR /web-ui"
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
WEBUI_CAPTURED = 0
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
2026-02-17 15:03:34 +00:00
# Direct copy from local filesystem (when running on target with sudo)
if [ -d "/opt/archipelago/web-ui" ] && [ " $( ls -A /opt/archipelago/web-ui 2>/dev/null) " ] ; then
cp -r /opt/archipelago/web-ui/* " $ARCH_DIR /web-ui/"
echo " ✅ Web UI captured from local system ( $( du -sh " $ARCH_DIR /web-ui" | cut -f1) )"
WEBUI_CAPTURED = 1
fi
# Remote copy via rsync if local failed
if [ " $WEBUI_CAPTURED " = "0" ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
2026-02-03 21:43:33 +00:00
if rsync -az " $DEV_SERVER :/opt/archipelago/web-ui/" " $ARCH_DIR /web-ui/" 2>/dev/null && [ " $( ls -A " $ARCH_DIR /web-ui" ) " ] ; then
echo " ✅ Web UI captured from remote server ( $( du -sh " $ARCH_DIR /web-ui" | cut -f1) )"
WEBUI_CAPTURED = 1
fi
2026-02-01 13:24:03 +00:00
fi
2026-02-01 05:42:05 +00:00
fi
2026-02-03 21:43:33 +00:00
if [ " $WEBUI_CAPTURED " = "0" ] ; then
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
echo " ⚠️ Could not capture from live server, building from source..."
fi
cd " $SCRIPT_DIR /../neode-ui"
2026-04-11 13:01:10 -04:00
echo " Installing frontend dependencies..."
npm ci --prefer-offline 2>& 1 | tail -3
2026-02-03 21:43:33 +00:00
if npm run build 2>& 1 | tail -5; then
if [ -d " $SCRIPT_DIR /../web/dist/neode-ui" ] ; then
echo " Including web UI from web/dist/neode-ui..."
cp -r " $SCRIPT_DIR /../web/dist/neode-ui/" * " $ARCH_DIR /web-ui/"
echo " ✅ Web UI built ( $( du -sh " $ARCH_DIR /web-ui" | cut -f1) )"
fi
else
echo " ⚠️ Web UI build failed"
# Try to use existing build
if [ -d " $SCRIPT_DIR /../web/dist/neode-ui" ] ; then
echo " Using existing web UI build..."
cp -r " $SCRIPT_DIR /../web/dist/neode-ui/" * " $ARCH_DIR /web-ui/"
elif [ -d " $SCRIPT_DIR /../neode-ui/dist" ] ; then
echo " Using neode-ui/dist..."
cp -r " $SCRIPT_DIR /../neode-ui/dist/" * " $ARCH_DIR /web-ui/"
else
echo " ❌ No web UI available"
exit 1
fi
fi
cd " $SCRIPT_DIR "
fi
2026-02-01 05:42:05 +00:00
2026-04-02 01:28:11 +01:00
# Include AIUI web app (Claude chat interface)
AIUI_INCLUDED = 0
2026-04-23 13:21:49 -04:00
# Search multiple locations for a pre-built AIUI app.
# demo/aiui is the canonical AIUI bundle checked into the repo and is
# tried first so ISO builds on a fresh clone work without needing any
# external AIUI checkout.
2026-04-02 01:28:11 +01:00
for AIUI_DIR in \
2026-04-23 13:21:49 -04:00
" $SCRIPT_DIR /../demo/aiui" \
2026-04-02 01:28:11 +01:00
" $SCRIPT_DIR /../../AIUI/packages/app/dist" \
" $HOME /AIUI/packages/app/dist" \
"/home/archipelago/AIUI/packages/app/dist" \
"/opt/archipelago/web-ui/aiui" \
"/home/archipelago/archy/AIUI/packages/app/dist" ; do
if [ -d " $AIUI_DIR " ] && [ -f " $AIUI_DIR /index.html" ] ; then
echo " Including AIUI from $AIUI_DIR ..."
mkdir -p " $ARCH_DIR /web-ui/aiui"
2026-04-02 13:04:10 +01:00
# Use rsync to handle same-file (CI workspace == /opt/archipelago) gracefully
if command -v rsync >/dev/null 2>& 1; then
rsync -a " $AIUI_DIR /" " $ARCH_DIR /web-ui/aiui/"
else
cp -r " $AIUI_DIR /" * " $ARCH_DIR /web-ui/aiui/" 2>/dev/null || true
fi
2026-04-02 01:28:11 +01:00
echo " ✅ AIUI included ( $( du -sh " $ARCH_DIR /web-ui/aiui" | cut -f1) )"
AIUI_INCLUDED = 1
break
fi
done
if [ " $AIUI_INCLUDED " = "0" ] ; then
echo " ⚠️ AIUI not found — build it first:"
echo " cd ~/AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build"
2026-04-23 13:21:49 -04:00
echo " Searched: demo/aiui, ~/AIUI, /home/archipelago/AIUI, /opt/archipelago/web-ui/aiui"
2026-04-02 01:28:11 +01:00
fi
2026-02-01 05:42:05 +00:00
# Copy app manifests
if [ -d " $SCRIPT_DIR /../apps" ] ; then
echo " Including app manifests..."
cp -r " $SCRIPT_DIR /../apps" " $ARCH_DIR /"
fi
2026-03-27 22:26:56 +00:00
# Copy Plymouth theme files for installation on target
PLYMOUTH_SRC = " $SCRIPT_DIR /branding/plymouth-theme"
if [ -d " $PLYMOUTH_SRC " ] ; then
mkdir -p " $ARCH_DIR /plymouth-theme"
cp " $PLYMOUTH_SRC /" * " $ARCH_DIR /plymouth-theme/"
echo " Included Plymouth theme"
fi
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 3b: Bundle container images for offline installation
# =============================================================================
echo ""
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-03-26 09:12:16 +00:00
echo "📦 Step 3b: Bundling core containers only (UNBUNDLED mode)"
echo " Optional apps will be downloaded on-demand from the Marketplace after install."
2026-04-10 04:01:35 -04:00
# Marker file: first-boot-containers.sh checks this to skip app creation
touch " $ARCH_DIR /.unbundled"
2026-03-10 23:29:05 +00:00
IMAGES_DIR = " $ARCH_DIR /container-images"
2026-04-11 13:01:10 -04:00
# Clean stale images from previous builds (e.g. bundled build tars leaking into unbundled)
rm -rf " $IMAGES_DIR "
2026-03-10 23:29:05 +00:00
mkdir -p " $IMAGES_DIR "
2026-03-26 09:12:16 +00:00
# FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it
2026-03-26 14:06:21 +00:00
CORE_IMAGE = " ${ FILEBROWSER_IMAGE } "
2026-03-26 09:12:16 +00:00
CORE_FILE = "filebrowser.tar"
if [ -f " $IMAGES_DIR / $CORE_FILE " ] ; then
echo " ✅ Using cached: $CORE_FILE "
else
echo " Pulling $CORE_IMAGE ( $CONTAINER_PLATFORM )..."
2026-05-06 09:23:57 -04:00
if container_pull " $CORE_IMAGE " ; then
2026-03-26 09:12:16 +00:00
$CONTAINER_CMD save " $CORE_IMAGE " -o " $IMAGES_DIR / $CORE_FILE " 2>/dev/null && \
echo " ✅ Saved core: $CORE_FILE ( $( du -h " $IMAGES_DIR / $CORE_FILE " | cut -f1) )" || \
echo " ⚠️ Failed to save $CORE_IMAGE "
else
echo " ⚠️ Failed to pull $CORE_IMAGE — Cloud will not work until installed"
fi
fi
2026-03-10 23:29:05 +00:00
else
2026-02-01 05:42:05 +00:00
echo "📦 Step 3b: Bundling container images for offline use..."
IMAGES_DIR = " $ARCH_DIR /container-images"
mkdir -p " $IMAGES_DIR "
2026-02-14 16:44:20 +00:00
# When DEV_SERVER is set (and not localhost), try to capture images from live server
# so the ISO includes the same set as the dev server (including custom UIs: bitcoin-ui, lnd-ui).
IMAGES_CAPTURED_FROM_SERVER = 0
if [ -n " $DEV_SERVER " ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
echo " Capturing container images from live server ( $DEV_SERVER )..."
2026-03-09 17:09:59 +00:00
# Patterns match against `podman images` repository names (not container names)
2026-04-02 18:20:52 +01:00
CAPTURE_PATTERNS = "bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager adguard"
2026-02-14 16:44:20 +00:00
REMOTE_TMP = "/tmp/archipelago-image-capture- $$ "
2026-03-21 01:11:05 +00:00
SAVED_LIST = $( ssh " $DEV_SERVER " "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS ; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \" $REMOTE_TMP /\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
2026-02-14 16:44:20 +00:00
for p in $SAVED_LIST ; do
if [ -n " $p " ] && scp " $DEV_SERVER : $REMOTE_TMP / $p .tar" " $IMAGES_DIR / $p .tar" 2>/dev/null; then
echo " ✅ Captured from server: $p .tar"
IMAGES_CAPTURED_FROM_SERVER = 1
fi
done
ssh " $DEV_SERVER " "rm -rf $REMOTE_TMP " 2>/dev/null || true
if [ " $IMAGES_CAPTURED_FROM_SERVER " = "0" ] ; then
echo " ⚠️ No images captured from server, will use registry pull fallback"
fi
fi
# Define images to bundle for fallback (when not from server or missing). Includes filebrowser.
# bitcoin-ui and lnd-ui are custom and normally captured from server or built separately.
2026-03-09 17:09:59 +00:00
# Alpha: core Bitcoin/Lightning stack + essential apps. Others pulled on-demand from Marketplace.
2026-02-01 05:42:05 +00:00
CONTAINER_IMAGES = "
2026-03-26 14:06:21 +00:00
${ BITCOIN_KNOTS_IMAGE } bitcoin-knots.tar
${ LND_IMAGE } lnd.tar
${ HOMEASSISTANT_IMAGE } homeassistant.tar
${ BTCPAY_IMAGE } btcpayserver.tar
${ NBXPLORER_IMAGE } nbxplorer.tar
${ POSTGRES_IMAGE } postgres-btcpay.tar
${ MEMPOOL_BACKEND_IMAGE } mempool-backend.tar
${ MEMPOOL_WEB_IMAGE } mempool-frontend.tar
${ ELECTRUMX_IMAGE } electrumx.tar
${ MARIADB_IMAGE } mariadb-mempool.tar
${ FEDIMINT_IMAGE } fedimint.tar
${ FEDIMINT_GATEWAY_IMAGE } fedimint-gateway.tar
2026-06-17 19:22:02 -04:00
${ FMCD_IMAGE } fmcd.tar
2026-03-26 14:06:21 +00:00
${ FILEBROWSER_IMAGE } filebrowser.tar
${ ALPINE_TOR_IMAGE } alpine-tor.tar
${ NGINX_ALPINE_IMAGE } nginx-alpine.tar
${ DWN_SERVER_IMAGE } dwn-server.tar
${ GRAFANA_IMAGE } grafana.tar
${ UPTIME_KUMA_IMAGE } uptime-kuma.tar
${ VAULTWARDEN_IMAGE } vaultwarden.tar
${ SEARXNG_IMAGE } searxng.tar
${ PORTAINER_IMAGE } portainer.tar
${ TAILSCALE_IMAGE } tailscale.tar
${ JELLYFIN_IMAGE } jellyfin.tar
${ PHOTOPRISM_IMAGE } photoprism.tar
${ NEXTCLOUD_IMAGE } nextcloud.tar
${ NPM_IMAGE } nginx-proxy-manager.tar
${ ONLYOFFICE_IMAGE } onlyoffice.tar
${ ADGUARDHOME_IMAGE } adguardhome.tar
2026-02-01 05:42:05 +00:00
"
2026-03-12 00:19:30 +00:00
# Pull and save each image (force target arch) only if not already present
2026-02-01 05:42:05 +00:00
echo " $CONTAINER_IMAGES " | while read -r image filename; do
[ -z " $image " ] && continue
tarpath = " $IMAGES_DIR / $filename "
2026-03-10 23:29:05 +00:00
2026-02-01 05:42:05 +00:00
if [ -f " $tarpath " ] ; then
echo " ✅ Using cached: $filename "
else
2026-03-12 00:19:30 +00:00
echo " Pulling $image ( $CONTAINER_PLATFORM )..."
2026-05-06 09:23:57 -04:00
if container_pull " $image " ; then
2026-02-01 05:42:05 +00:00
echo " Saving $filename ..."
2026-03-06 13:00:28 +00:00
if $CONTAINER_CMD save " $image " -o " $tarpath " 2>/dev/null; then
echo " ✅ Saved: $( du -h " $tarpath " | cut -f1) "
else
echo " ⚠️ Failed to save $image (zstd/format issue) - skipping"
rm -f " $tarpath "
fi
2026-02-01 05:42:05 +00:00
else
echo " ⚠️ Failed to pull $image - skipping"
fi
fi
done
2026-03-10 23:29:05 +00:00
fi # end UNBUNDLED check
2026-02-01 05:42:05 +00:00
# Create first-boot service to load images into Podman
echo " Creating first-boot image loader service..."
cat > " $WORK_DIR /archipelago-load-images.service" <<'LOADSERVICE'
[Unit]
Description=Load Archipelago Container Images
After=network.target podman.service
ConditionPathExists=/opt/archipelago/container-images
ConditionPathExists=!/var/lib/archipelago/.images-loaded
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/load-container-images.sh
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.images-loaded
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
LOADSERVICE
cat > " $WORK_DIR /load-container-images.sh" <<'LOADSCRIPT'
#!/bin/bash
# Load pre-bundled container images into Podman
IMAGES_DIR="/opt/archipelago/container-images"
LOG_FILE="/var/log/archipelago-images.log"
echo "$(date): Starting container image load" >> "$LOG_FILE"
if [ ! -d "$IMAGES_DIR" ]; then
echo "$(date): No images directory found" >> "$LOG_FILE"
exit 0
fi
for tarfile in "$IMAGES_DIR"/*.tar; do
if [ -f "$tarfile" ]; then
echo "$(date): Loading $(basename "$tarfile")..." >> "$LOG_FILE"
podman load -i "$tarfile" >> "$LOG_FILE" 2>&1 && \
echo "$(date): Successfully loaded $(basename "$tarfile")" >> "$LOG_FILE" || \
echo "$(date): Failed to load $(basename "$tarfile")" >> "$LOG_FILE"
fi
done
2026-02-17 15:03:34 +00:00
# Ensure archy-net exists for mempool stack (db, api, frontend)
podman network create archy-net 2>/dev/null || true
2026-02-01 05:42:05 +00:00
echo "$(date): Container image load complete" >> "$LOG_FILE"
echo "$(date): Available images:" >> "$LOG_FILE"
podman images >> "$LOG_FILE" 2>&1
LOADSCRIPT
chmod +x " $WORK_DIR /load-container-images.sh"
# Copy scripts to ISO
mkdir -p " $ARCH_DIR /scripts"
cp " $WORK_DIR /load-container-images.sh" " $ARCH_DIR /scripts/"
cp " $WORK_DIR /archipelago-load-images.service" " $ARCH_DIR /scripts/"
2026-07-13 13:41:16 +01:00
# First-boot per-device secrets: the squashfs bakes one TLS key and one set
# of SSH host keys at build time, so every device flashed from the same image
# would share them. Regenerate both on the installed system's first boot,
# before the network-facing services come up.
echo " Creating first-boot secrets regeneration service..."
cat > " $WORK_DIR /archipelago-first-boot-secrets.service" <<'SECRETSSERVICE'
[Unit]
Description=Regenerate per-device secrets (TLS key, SSH host keys)
DefaultDependencies=no
After=local-fs.target
Before=ssh.service nginx.service archipelago.service
ConditionPathExists=!/var/lib/archipelago/.secrets-regenerated
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/first-boot-secrets.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
SECRETSSERVICE
cat > " $WORK_DIR /first-boot-secrets.sh" <<'SECRETSSCRIPT'
#!/bin/bash
# Replace image-baked secrets with device-unique ones on first boot.
# Never leaves the device without working keys: new material is generated
# to a staging path first and only swapped in on success.
set -u
LOG=/var/log/archipelago-first-boot-secrets.log
MARKER=/var/lib/archipelago/.secrets-regenerated
[ -f "$MARKER" ] && exit 0
mkdir -p /var/lib/archipelago
NODE_NAME=$(hostname 2>/dev/null || echo archipelago)
echo "$(date): regenerating per-device secrets" >> "$LOG"
# 1. Self-signed TLS: fresh keypair with this device's hostname in the SAN
# (server.set-name regenerates again if the node is renamed later)
mkdir -p /etc/archipelago/ssl
if openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/archipelago/ssl/archipelago.key.new \
-out /etc/archipelago/ssl/archipelago.crt.new \
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=${NODE_NAME}" \
-addext "subjectAltName=DNS:${NODE_NAME},DNS:${NODE_NAME}.local,DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" \
>> "$LOG" 2>&1; then
mv /etc/archipelago/ssl/archipelago.key.new /etc/archipelago/ssl/archipelago.key
mv /etc/archipelago/ssl/archipelago.crt.new /etc/archipelago/ssl/archipelago.crt
chmod 600 /etc/archipelago/ssl/archipelago.key
echo "$(date): TLS keypair regenerated (CN=${NODE_NAME})" >> "$LOG"
systemctl try-reload-or-restart nginx >> "$LOG" 2>&1 || true
else
rm -f /etc/archipelago/ssl/archipelago.key.new /etc/archipelago/ssl/archipelago.crt.new
echo "$(date): WARNING: TLS regeneration failed, keeping baked key" >> "$LOG"
fi
# 2. SSH host keys: generate a full fresh set in staging, then swap
STAGING=$(mktemp -d)
mkdir -p "$STAGING/etc/ssh"
if ssh-keygen -A -f "$STAGING" >> "$LOG" 2>&1 && ls "$STAGING"/etc/ssh/ssh_host_*_key >/dev/null 2>&1; then
rm -f /etc/ssh/ssh_host_*
mv "$STAGING"/etc/ssh/ssh_host_* /etc/ssh/
echo "$(date): SSH host keys regenerated" >> "$LOG"
systemctl try-reload-or-restart ssh >> "$LOG" 2>&1 || true
else
echo "$(date): WARNING: ssh-keygen -A failed, keeping baked host keys" >> "$LOG"
fi
rm -rf "$STAGING"
touch "$MARKER"
echo "$(date): per-device secrets done" >> "$LOG"
SECRETSSCRIPT
chmod +x " $WORK_DIR /first-boot-secrets.sh"
cp " $WORK_DIR /first-boot-secrets.sh" " $ARCH_DIR /scripts/"
cp " $WORK_DIR /archipelago-first-boot-secrets.service" " $ARCH_DIR /scripts/"
2026-02-17 15:03:34 +00:00
# Tor setup: copy torrc and create first-boot setup script
mkdir -p " $ARCH_DIR /scripts/tor"
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/tor/torrc.template" ] ; then
cp " $SCRIPT_DIR /../../scripts/tor/torrc.template" " $ARCH_DIR /scripts/tor/torrc"
2026-02-17 15:03:34 +00:00
fi
echo " Creating first-boot Tor setup service..."
cat > " $WORK_DIR /archipelago-setup-tor.service" <<'TORSERVICE'
[Unit]
Description=Setup and start Archipelago Tor hidden services
After=archipelago-load-images.service network.target podman.service
ConditionPathExists=/opt/archipelago/scripts/setup-tor.sh
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/setup-tor.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
TORSERVICE
cat > " $WORK_DIR /setup-tor.sh" <<'TORSCRIPT'
#!/bin/bash
2026-03-14 03:17:12 +00:00
# Setup and start Tor hidden services (autoinstaller first-boot)
# Prefers system Tor (apt package) over container
2026-02-17 15:03:34 +00:00
2026-03-14 03:17:12 +00:00
ARCHY_TOR_DIR="/var/lib/archipelago/tor"
2026-03-31 07:03:57 +01:00
TOR_CONFIG_DIR="/var/lib/archipelago/tor-config"
2026-03-14 03:17:12 +00:00
TOR_DIR="/var/lib/tor"
2026-02-17 15:03:34 +00:00
LOG="/var/log/archipelago-tor.log"
2026-03-31 07:03:57 +01:00
mkdir -p "$ARCHY_TOR_DIR" "$TOR_CONFIG_DIR"
2026-02-17 15:03:34 +00:00
2026-03-14 03:17:12 +00:00
# Write services.json for the backend to read
2026-03-30 16:35:06 +01:00
cat > "$ARCHY_TOR_DIR/services.json" <<TORJSON
2026-03-28 23:41:40 +00:00
{
"services": [
{"name": "archipelago", "local_port": 80, "enabled": true},
{"name": "bitcoin", "local_port": 8333, "enabled": true},
{"name": "electrumx", "local_port": 50001, "enabled": true},
{"name": "lnd", "local_port": 9735, "enabled": true},
{"name": "btcpay", "local_port": 23000, "enabled": true},
{"name": "mempool", "local_port": 4080, "enabled": true},
{"name": "fedimint", "local_port": 8175, "enabled": true}
]
}
TORJSON
echo "services.json created"
2026-03-31 07:03:57 +01:00
# Backend reads from tor-config/, not tor/
cp "$ARCHY_TOR_DIR/services.json" "$TOR_CONFIG_DIR/services.json"
chown -R archipelago:archipelago "$TOR_CONFIG_DIR" 2>/dev/null || true
2026-02-17 15:03:34 +00:00
2026-03-14 03:17:12 +00:00
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
cat > /etc/tor/torrc <<TORRC
2026-03-31 07:03:57 +01:00
SocksPort 0.0.0.0:9050
SocksPolicy accept 10.89.0.0/16
SocksPolicy accept 127.0.0.0/8
SocksPolicy reject *
# ControlPort disabled for security
2026-02-17 15:03:34 +00:00
2026-03-14 03:17:12 +00:00
HiddenServiceDir $TOR_DIR/hidden_service_archipelago
HiddenServicePort 80 127.0.0.1:80
HiddenServiceDir $TOR_DIR/hidden_service_bitcoin
HiddenServicePort 8333 127.0.0.1:8333
2026-04-02 16:15:04 +01:00
HiddenServicePort 8332 127.0.0.1:8332
2026-03-14 03:17:12 +00:00
2026-03-16 12:58:35 +00:00
HiddenServiceDir $TOR_DIR/hidden_service_electrumx
2026-03-14 03:17:12 +00:00
HiddenServicePort 50001 127.0.0.1:50001
HiddenServiceDir $TOR_DIR/hidden_service_lnd
HiddenServicePort 9735 127.0.0.1:9735
2026-03-19 12:44:31 +00:00
HiddenServicePort 8080 127.0.0.1:8080
2026-03-14 03:17:12 +00:00
HiddenServiceDir $TOR_DIR/hidden_service_btcpay
HiddenServicePort 23000 127.0.0.1:23000
HiddenServiceDir $TOR_DIR/hidden_service_mempool
HiddenServicePort 4080 127.0.0.1:4080
HiddenServiceDir $TOR_DIR/hidden_service_fedimint
HiddenServicePort 8175 127.0.0.1:8175
2026-04-08 20:27:38 +02:00
HiddenServiceDir $TOR_DIR/hidden_service_relay
HiddenServicePort 7777 127.0.0.1:7777
2026-03-14 03:17:12 +00:00
TORRC
2026-03-28 02:40:39 +00:00
# Create hidden service dirs with correct ownership and permissions (700, not 750)
# Tor refuses to start if permissions are too permissive
2026-04-08 20:27:38 +02:00
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint relay; do
2026-03-28 02:40:39 +00:00
mkdir -p "$TOR_DIR/hidden_service_$svc"
chown debian-tor:debian-tor "$TOR_DIR/hidden_service_$svc"
chmod 700 "$TOR_DIR/hidden_service_$svc"
done
2026-03-14 03:17:12 +00:00
# Prefer system Tor (installed via apt)
if command -v tor >/dev/null 2>&1; then
echo "$(date): Using system Tor daemon" >> "$LOG"
systemctl enable tor 2>/dev/null
systemctl restart tor@default 2>/dev/null
else
# Fallback: use container
echo "$(date): System Tor not found, using container" >> "$LOG"
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
for c in $(sudo $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'archy-tor|^tor$'); do
[ -n "$c" ] && sudo $DOCKER stop "$c" 2>/dev/null; sudo $DOCKER rm -f "$c" 2>/dev/null
done
if ! sudo $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-tor; then
sudo $DOCKER run -d --name archy-tor --restart unless-stopped --network host \
-v "$TOR_DIR:$TOR_DIR" \
--entrypoint tor \
2026-03-26 14:06:21 +00:00
${ALPINE_TOR_IMAGE} \
2026-03-14 03:17:12 +00:00
-f /etc/tor/torrc >> "$LOG" 2>&1
2026-02-17 15:03:34 +00:00
echo "$(date): Tor container started" >> "$LOG"
fi
fi
2026-03-14 03:17:12 +00:00
2026-02-17 15:03:34 +00:00
# Wait for Tor to create hostname files (~30-60s), then chmod so archipelago user can read
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 6
if [ -f "$TOR_DIR/hidden_service_archipelago/hostname" ]; then
2026-03-14 03:17:12 +00:00
chmod 750 "$TOR_DIR"/hidden_service_*/ 2>/dev/null || true
2026-02-17 15:03:34 +00:00
for f in "$TOR_DIR"/hidden_service_*/hostname; do
2026-03-14 03:17:12 +00:00
[ -f "$f" ] && chmod 640 "$f" && echo "$(date): chmod hostname $f" >> "$LOG"
2026-02-17 15:03:34 +00:00
done
echo "$(date): Tor hostname files readable by archipelago" >> "$LOG"
break
fi
done
2026-03-31 07:03:57 +01:00
# Sync hostnames to backend-readable directory
HOSTNAMES_DIR="/var/lib/archipelago/tor-hostnames"
mkdir -p "$HOSTNAMES_DIR"
2026-04-08 20:27:38 +02:00
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint relay; do
2026-03-31 07:03:57 +01:00
if [ -f "$TOR_DIR/hidden_service_${svc}/hostname" ]; then
cp "$TOR_DIR/hidden_service_${svc}/hostname" "$HOSTNAMES_DIR/$svc"
echo "$(date): Synced hostname: $svc" >> "$LOG"
fi
done
chown -R archipelago:archipelago "$HOSTNAMES_DIR" 2>/dev/null || true
echo "$(date): Hostnames synced: $(ls $HOSTNAMES_DIR 2>/dev/null | tr '\n' ' ')" >> "$LOG"
2026-02-17 15:03:34 +00:00
TORSCRIPT
chmod +x " $WORK_DIR /setup-tor.sh"
cp " $WORK_DIR /setup-tor.sh" " $ARCH_DIR /scripts/"
cp " $WORK_DIR /archipelago-setup-tor.service" " $ARCH_DIR /scripts/"
2026-02-25 18:04:41 +00:00
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
2026-04-01 22:31:45 +01:00
# Both bundled and unbundled builds use the full first-boot script.
# Unbundled mode pulls images from registry; bundled mode loads from tarballs.
if false && [ " $UNBUNDLED " = "1" ] ; then
2026-03-26 09:12:16 +00:00
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
2026-04-01 22:31:45 +01:00
# DISABLED: minimal script doesn't create UI sidecars or write app configs.
# The full first-boot-containers.sh handles both bundled and unbundled modes.
2026-03-26 09:12:16 +00:00
cat > " $WORK_DIR /first-boot-containers-unbundled.sh" <<'FBUNBUNDLED'
#!/bin/bash
# Minimal first-boot: create FileBrowser container only (unbundled ISO)
set -e
LOG="/var/log/archipelago-first-boot.log"
echo "[$(date)] Starting minimal first-boot (unbundled)..." >> "$LOG"
2026-03-27 16:16:57 +00:00
# Source image versions (provides $FILEBROWSER_IMAGE etc.)
for f in /opt/archipelago/scripts/image-versions.sh /home/archipelago/archy/scripts/image-versions.sh; do
if [ -f "$f" ]; then
source "$f"
echo "[$(date)] Sourced image versions from $f" >> "$LOG"
break
fi
done
2026-03-26 09:12:16 +00:00
2026-03-27 16:16:57 +00:00
if [ -z "$FILEBROWSER_IMAGE" ]; then
echo "[$(date)] ERROR: FILEBROWSER_IMAGE not set — image-versions.sh missing or incomplete" >> "$LOG"
exit 1
fi
# Create Cloud storage directories (as root, then fix ownership for rootless podman)
mkdir -p /var/lib/archipelago/filebrowser
mkdir -p /var/lib/archipelago/filebrowser-data
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
# Container UID 0 maps to host UID 100000 under rootless podman (subuid mapping)
chown -R 100000:100000 /var/lib/archipelago/filebrowser
chown -R 100000:100000 /var/lib/archipelago/filebrowser-data
chown -R 100000:100000 /var/lib/archipelago/data
# Enable linger so rootless podman containers survive logout
loginctl enable-linger archipelago 2>/dev/null || true
2026-04-02 15:16:24 +01:00
# Enable podman-restart so containers with --restart=unless-stopped auto-start on boot
runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && systemctl --user enable podman-restart.service' 2>>"$LOG" || true
2026-03-27 16:16:57 +00:00
# Ensure podman socket is active for archipelago user
runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && systemctl --user enable --now podman.socket' 2>>"$LOG" || true
# Create FileBrowser container as archipelago user (rootless podman)
2026-03-29 21:56:38 +01:00
# Generate random FileBrowser password and store for auto-login
FB_PASS_DIR="/var/lib/archipelago/secrets/filebrowser"
mkdir -p "$FB_PASS_DIR"
if [ ! -f "$FB_PASS_DIR/password" ]; then
head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24 > "$FB_PASS_DIR/password"
chmod 600 "$FB_PASS_DIR/password"
chown 1000:1000 "$FB_PASS_DIR/password"
fi
2026-03-27 16:16:57 +00:00
if ! runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && podman ps -a --format "{{.Names}}"' 2>/dev/null | grep -q filebrowser; then
echo "[$(date)] Creating FileBrowser container ($FILEBROWSER_IMAGE)..." >> "$LOG"
runuser -u archipelago -- bash -c "export XDG_RUNTIME_DIR=/run/user/1000 && podman run -d --name filebrowser --restart unless-stopped \
--cap-drop=ALL \
--cap-add=DAC_OVERRIDE \
2026-03-27 17:17:18 +00:00
--cap-add=NET_BIND_SERVICE \
2026-03-27 16:16:57 +00:00
--security-opt=no-new-privileges:true \
--read-only \
--tmpfs=/tmp:rw,noexec,nosuid,size=64m \
--health-cmd='curl -sf http://localhost:80/ || exit 1' \
2026-03-26 09:12:16 +00:00
--health-interval=30s --health-timeout=5s --health-retries=3 \
--memory=256m \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
2026-03-27 16:16:57 +00:00
-v /var/lib/archipelago/filebrowser-data:/data \
-v /var/lib/archipelago/data/cloud:/srv/cloud \
$FILEBROWSER_IMAGE \
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80" 2>>"$LOG" && \
2026-03-26 09:12:16 +00:00
echo "[$(date)] FileBrowser created successfully" >> "$LOG" || \
echo "[$(date)] WARNING: FileBrowser creation failed" >> "$LOG"
2026-03-29 21:56:38 +01:00
# Set FileBrowser password to match the stored random password
sleep 5
FB_PASS=$(cat "$FB_PASS_DIR/password" 2>/dev/null || echo "admin")
runuser -u archipelago -- bash -c "export XDG_RUNTIME_DIR=/run/user/1000 && podman exec filebrowser filebrowser users update admin --password '$FB_PASS' --database /data/database.db" 2>>"$LOG" && \
echo "[$(date)] FileBrowser admin password set" >> "$LOG" || \
echo "[$(date)] WARNING: Could not set FileBrowser password" >> "$LOG"
2026-03-26 09:12:16 +00:00
fi
echo "[$(date)] Minimal first-boot complete" >> "$LOG"
FBUNBUNDLED
chmod +x " $WORK_DIR /first-boot-containers-unbundled.sh"
cp " $WORK_DIR /first-boot-containers-unbundled.sh" " $ARCH_DIR /scripts/first-boot-containers.sh"
2026-04-01 10:38:41 +01:00
# Copy shared script library (TUI animations for installer, shared utils)
2026-04-28 15:00:58 -04:00
if [ -d " $SCRIPT_DIR /../../scripts/lib" ] ; then
2026-04-01 10:38:41 +01:00
mkdir -p " $ARCH_DIR /scripts/lib"
2026-04-28 15:00:58 -04:00
cp " $SCRIPT_DIR /../../scripts/lib/" *.sh " $ARCH_DIR /scripts/lib/" 2>/dev/null || true
2026-04-01 10:38:41 +01:00
echo " Copied scripts/lib/ ( $( ls " $ARCH_DIR /scripts/lib/" 2>/dev/null | wc -l) files)"
fi
2026-03-26 09:12:16 +00:00
cat > " $WORK_DIR /archipelago-first-boot-containers.service" <<'FBCSERVICE'
[Unit]
Description=Create core Archipelago containers on first boot
2026-03-31 18:31:00 +01:00
After=archipelago-load-images.service archipelago-setup-tor.service network-online.target podman.service
Wants=archipelago-load-images.service
2026-03-26 09:12:16 +00:00
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
2026-03-31 18:31:00 +01:00
TimeoutStartSec=900
2026-03-26 09:12:16 +00:00
ExecStart=/opt/archipelago/scripts/first-boot-containers.sh
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
FBCSERVICE
cp " $WORK_DIR /archipelago-first-boot-containers.service" " $ARCH_DIR /scripts/"
2026-03-10 23:29:05 +00:00
else
echo " Creating first-boot container creation service..."
2026-03-21 03:06:29 +00:00
# Copy shared script library
2026-04-28 15:00:58 -04:00
if [ -d " $SCRIPT_DIR /../../scripts/lib" ] ; then
2026-03-21 03:06:29 +00:00
mkdir -p " $ARCH_DIR /scripts/lib"
2026-04-28 15:00:58 -04:00
cp " $SCRIPT_DIR /../../scripts/lib/" *.sh " $ARCH_DIR /scripts/lib/" 2>/dev/null || true
2026-03-21 03:06:29 +00:00
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/first-boot-containers.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/first-boot-containers.sh" " $ARCH_DIR /scripts/"
2026-03-10 23:29:05 +00:00
chmod +x " $ARCH_DIR /scripts/first-boot-containers.sh"
cat > " $WORK_DIR /archipelago-first-boot-containers.service" <<'FBCSERVICE'
2026-02-25 18:04:41 +00:00
[Unit]
Description=Create core Archipelago containers on first boot
2026-03-31 18:31:00 +01:00
After=archipelago-load-images.service archipelago-setup-tor.service network-online.target podman.service
Wants=archipelago-load-images.service
2026-02-25 18:04:41 +00:00
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
2026-03-31 18:31:00 +01:00
TimeoutStartSec=900
2026-02-25 18:04:41 +00:00
ExecStart=/opt/archipelago/scripts/first-boot-containers.sh
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
FBCSERVICE
2026-03-10 23:29:05 +00:00
cp " $WORK_DIR /archipelago-first-boot-containers.service" " $ARCH_DIR /scripts/"
fi
2026-02-25 18:04:41 +00:00
fi
2026-04-02 16:15:04 +01:00
# Bootstrap node config — new installs use this Bitcoin node during IBD
# so ElectrumX/LND/BTCPay/Mempool work immediately while local chain syncs
# Tries LAN first (fast), falls back to Tor (works from anywhere)
BOOTSTRAP_RPC_PASS = ""
BOOTSTRAP_ONION = ""
if [ -f /var/lib/archipelago/secrets/bitcoin-rpc-password ] ; then
BOOTSTRAP_RPC_PASS = $( cat /var/lib/archipelago/secrets/bitcoin-rpc-password 2>/dev/null)
fi
if [ -f /var/lib/archipelago/tor-hostnames/bitcoin ] ; then
BOOTSTRAP_ONION = $( cat /var/lib/archipelago/tor-hostnames/bitcoin 2>/dev/null)
fi
if [ -n " $BOOTSTRAP_RPC_PASS " ] ; then
DEV_IP = " ${ DEV_SERVER :- 192 .168.1.228 } "
cat > " $ARCH_DIR /bootstrap.conf" <<BSTRAP
# Bootstrap Bitcoin node — used during Initial Block Download
# Services connect here until the local node is fully synced
# First-boot tries LAN, then Tor (works from any network)
BOOTSTRAP_LAN_HOST=${DEV_IP}
BOOTSTRAP_ONION=${BOOTSTRAP_ONION}
BOOTSTRAP_RPC_USER=archipelago
BOOTSTRAP_RPC_PASS=${BOOTSTRAP_RPC_PASS}
BSTRAP
chmod 600 " $ARCH_DIR /bootstrap.conf"
echo " ✅ Bootstrap node config embedded (LAN: ${ DEV_IP } , Tor: ${ BOOTSTRAP_ONION :- none } )"
else
echo " ⚠ No bootstrap config — no Bitcoin RPC password found on build host"
fi
# Bundle bootstrap switchover script + systemd timer
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/bootstrap-switchover.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/bootstrap-switchover.sh" " $ARCH_DIR /scripts/"
2026-04-02 16:15:04 +01:00
chmod +x " $ARCH_DIR /scripts/bootstrap-switchover.sh"
echo " ✅ Bundled bootstrap switchover script"
fi
2026-03-09 17:09:59 +00:00
# Bundle E2E test script for post-install validation
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/run-e2e-tests.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/run-e2e-tests.sh" " $ARCH_DIR /scripts/"
2026-03-09 17:09:59 +00:00
chmod +x " $ARCH_DIR /scripts/run-e2e-tests.sh"
echo " ✅ Bundled E2E test script for post-install validation"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/run-post-install-tests.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/run-post-install-tests.sh" " $ARCH_DIR /scripts/"
2026-03-27 16:16:57 +00:00
chmod +x " $ARCH_DIR /scripts/run-post-install-tests.sh"
echo " ✅ Bundled post-install test suite"
fi
2026-03-09 17:09:59 +00:00
2026-03-25 15:52:26 +00:00
# Bundle self-update script and image-versions for update system
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/self-update.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/self-update.sh" " $ARCH_DIR /scripts/"
2026-03-25 15:52:26 +00:00
chmod +x " $ARCH_DIR /scripts/self-update.sh"
echo " ✅ Bundled self-update script"
fi
2026-04-28 15:00:58 -04:00
if [ -f " $SCRIPT_DIR /../../scripts/image-versions.sh" ] ; then
cp " $SCRIPT_DIR /../../scripts/image-versions.sh" " $ARCH_DIR /scripts/"
2026-03-25 15:52:26 +00:00
echo " ✅ Bundled image-versions.sh"
fi
2026-03-29 17:48:13 +01:00
# Bundle docker UI source files for building custom UIs on first boot
# Always bundle — these are tiny HTML/CSS files, not container images
if true; then
2026-03-10 23:29:05 +00:00
DOCKER_UI_DIR = " $SCRIPT_DIR /../docker"
if [ -d " $DOCKER_UI_DIR " ] ; then
echo " Bundling docker UI source files..."
mkdir -p " $ARCH_DIR /docker"
for ui_dir in bitcoin-ui lnd-ui electrs-ui; do
if [ -d " $DOCKER_UI_DIR / $ui_dir " ] ; then
cp -r " $DOCKER_UI_DIR / $ui_dir " " $ARCH_DIR /docker/"
echo " ✅ Bundled $ui_dir source"
fi
done
fi
2026-03-08 02:27:58 +00:00
fi
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
echo " ✅ Unbundled build ready (Tor setup included, no container images)"
else
echo " ✅ Container images bundled (including Tor + first-boot)"
fi
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 4: Create auto-installer script
# =============================================================================
echo ""
echo "📦 Step 4: Creating auto-installer..."
cat > " $ARCH_DIR /auto-install.sh" <<'INSTALLER_SCRIPT'
#!/bin/bash
#
# Archipelago Auto-Installer
# Automatically installs to internal disk (StartOS-like experience)
#
set -e
2026-03-29 12:06:19 +01:00
# Log file — verbose command output goes here, TUI stays on console
2026-03-26 20:54:32 +00:00
INSTALL_LOG="/tmp/archipelago-install.log"
2026-03-29 12:06:19 +01:00
# Run commands quietly: redirect their stdout/stderr to log
# TUI functions (p, step, ok, etc.) print directly to console
run() { "$@" >> "$INSTALL_LOG" 2>&1; }
runq() { "$@" >>"$INSTALL_LOG" 2>&1 || true; }
2026-03-26 20:54:32 +00:00
2026-03-21 01:11:05 +00:00
# Detect architecture at install time
case "$(uname -m)" in
x86_64|amd64)
ARCH="x86_64"
GRUB_TARGET="x86_64-efi"
GRUB_BIOS_TARGET="i386-pc"
;;
aarch64|arm64)
ARCH="arm64"
GRUB_TARGET="arm64-efi"
GRUB_BIOS_TARGET=""
;;
esac
2026-03-28 23:58:42 +00:00
# Colors — 256-color ANSI (works on Linux fbcon console)
ORANGE=$'\033[38;5;208m'
ORANGE_DIM=$'\033[38;5;130m'
ORANGE_BRIGHT=$'\033[38;5;214m'
2026-03-28 11:31:48 +00:00
RED=$'\033[31m'
GREEN=$'\033[32m'
WHITE=$'\033[1;37m'
2026-03-28 23:58:42 +00:00
DIM=$'\033[38;5;242m'
DIMMER=$'\033[38;5;238m'
2026-03-25 17:03:21 +00:00
NC=$'\033[0m'
2026-03-28 23:58:42 +00:00
BOLD=$'\033[1m'
2026-02-01 05:42:05 +00:00
2026-03-29 15:43:57 +01:00
# Left-justified layout — 2-space indent, no centering
PADS=" "
2026-03-28 11:31:48 +00:00
2026-03-28 23:58:42 +00:00
p() { printf "%s%b\n" "$PADS" "$1"; }
2026-03-29 15:43:57 +01:00
hrule() { local hr=""; for i in $(seq 1 48); do hr="${hr}─"; done; p "${ORANGE_DIM}${hr}${NC}"; }
# Typewriter animation for key text
typewrite() {
local text="$1" delay="${2:-0.02}"
printf "%s" "$PADS"
local i=0
while [ $i -lt ${#text} ]; do
printf "%s" "${text:$i:1}"
i=$((i + 1))
sleep "$delay"
done
printf "\n"
}
2026-03-28 11:31:48 +00:00
2026-03-28 23:58:42 +00:00
# Phase display
2026-03-28 13:39:10 +00:00
STEP=0
2026-03-28 23:41:40 +00:00
TOTAL_STEPS=8
2026-03-28 13:39:10 +00:00
step() {
STEP=$((STEP + 1))
echo ""
2026-03-28 23:58:42 +00:00
p "${ORANGE}[$STEP/$TOTAL_STEPS] $1${NC}"
2026-03-28 13:39:10 +00:00
}
2026-03-28 23:58:42 +00:00
ok() { p " ${ORANGE_BRIGHT}✓ $1${NC}"; }
warn() { p " ${ORANGE}⚠ $1${NC}"; }
fail() { p " ${RED}✗ $1${NC}"; }
2026-03-28 13:39:10 +00:00
spinner() {
local pid=$1 msg=$2
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
while kill -0 "$pid" 2>/dev/null; do
2026-03-28 23:58:42 +00:00
printf "\r%s %b%s %s%b" "$PADS" "$ORANGE" "${frames:i%10:1}" "$msg" "$NC"
2026-03-28 13:39:10 +00:00
i=$((i + 1))
sleep 0.1
done
2026-03-28 23:58:42 +00:00
printf "\r%s %b✓ %s%b\n" "$PADS" "$ORANGE_BRIGHT" "$msg" "$NC"
2026-03-28 13:39:10 +00:00
}
2026-03-31 21:23:29 +01:00
# Source TUI library for install animations (graceful fallback if missing)
for _tui_path in \
"$BOOT_MEDIA/archipelago/scripts/lib/install-tui.sh" \
"/opt/archipelago/scripts/lib/install-tui.sh" \
2026-04-28 15:00:58 -04:00
"$(dirname "$0")/../../scripts/lib/install-tui.sh"; do
2026-03-31 21:23:29 +01:00
[ -f "$_tui_path" ] && { source "$_tui_path" 2>/dev/null; break; }
done
if [ "${TUI_AVAILABLE:-}" = "1" ]; then
tui_welcome
tui_enable_progress_spinner
else
clear
echo ""
echo -e "${PADS}${ORANGE}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${NC}"
echo -e "${PADS}${ORANGE}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${NC}"
echo -e "${PADS}${ORANGE}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${NC}"
typewrite "$(echo -e "${ORANGE_DIM}bitcoin node os${NC}")" 0.04
echo ""
fi
2026-02-01 05:42:05 +00:00
# Check required tools are present (should be bundled in ISO)
2026-03-28 13:39:10 +00:00
step "Checking tools"
2026-02-01 05:42:05 +00:00
MISSING=""
command -v parted >/dev/null 2>&1 || MISSING="parted $MISSING"
command -v mkfs.vfat >/dev/null 2>&1 || MISSING="mkfs.vfat $MISSING"
command -v mkfs.ext4 >/dev/null 2>&1 || MISSING="mkfs.ext4 $MISSING"
if [ -n "$MISSING" ]; then
2026-03-28 13:39:10 +00:00
warn "Installing missing: $MISSING"
2026-02-01 05:42:05 +00:00
if apt-get update -qq >/dev/null 2>&1; then
2026-03-28 13:39:10 +00:00
apt-get install -y -qq parted dosfstools e2fsprogs >/dev/null 2>&1 && ok "Tools installed" || {
fail "Failed to install required tools"
2026-02-01 05:42:05 +00:00
exit 1
}
else
2026-03-28 13:39:10 +00:00
fail "No network available and tools not bundled"
2026-02-01 05:42:05 +00:00
exit 1
fi
else
2026-03-28 13:39:10 +00:00
ok "All tools present"
2026-02-01 05:42:05 +00:00
fi
# Find boot media
BOOT_MEDIA=""
2026-03-27 18:19:49 +00:00
for dev in /run/archiso /cdrom /media/cdrom /run/live/medium /lib/live/mount/medium /mnt/iso; do
2026-02-01 05:42:05 +00:00
if [ -d "$dev/archipelago" ]; then
BOOT_MEDIA="$dev"
break
fi
done
if [ -z "$BOOT_MEDIA" ]; then
echo -e "${RED}❌ Boot media not found${NC}"
exit 1
fi
ROOTFS_TAR="$BOOT_MEDIA/archipelago/rootfs.tar"
if [ ! -f "$ROOTFS_TAR" ]; then
echo -e "${RED}❌ Root filesystem not found: $ROOTFS_TAR${NC}"
exit 1
fi
# Find the boot USB device to exclude it
BOOT_DEV=$(findmnt -n -o SOURCE "$BOOT_MEDIA" 2>/dev/null | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//')
BOOT_DEV_NAME=$(basename "$BOOT_DEV" 2>/dev/null || echo "")
2026-03-28 13:39:10 +00:00
step "Detecting disks"
2026-02-01 05:42:05 +00:00
echo ""
# Find internal disk (prefer NVMe, then SATA, skip USB)
TARGET_DISK=""
TARGET_SIZE=""
# Check NVMe drives first
for disk in /dev/nvme*n1; do
if [ -b "$disk" ]; then
disk_name=$(basename "$disk")
if [ "$disk_name" != "$BOOT_DEV_NAME" ]; then
size=$(lsblk -dn -o SIZE "$disk" 2>/dev/null)
model=$(cat /sys/block/$disk_name/device/model 2>/dev/null || echo "NVMe SSD")
echo " Found: $disk ($size) - $model"
TARGET_DISK="$disk"
TARGET_SIZE="$size"
break
fi
fi
done
# If no NVMe, check SATA drives
if [ -z "$TARGET_DISK" ]; then
for disk in /dev/sd[a-z]; do
if [ -b "$disk" ]; then
disk_name=$(basename "$disk")
if [ "$disk_name" != "$BOOT_DEV_NAME" ]; then
# Skip USB drives (check removable flag)
removable=$(cat /sys/block/$disk_name/removable 2>/dev/null || echo "0")
if [ "$removable" = "0" ]; then
size=$(lsblk -dn -o SIZE "$disk" 2>/dev/null)
model=$(cat /sys/block/$disk_name/device/model 2>/dev/null || echo "SATA Drive")
echo " Found: $disk ($size) - $model"
TARGET_DISK="$disk"
TARGET_SIZE="$size"
break
fi
fi
fi
done
fi
if [ -z "$TARGET_DISK" ]; then
echo ""
echo -e "${RED}❌ No suitable internal disk found${NC}"
echo " Please ensure an internal drive is connected."
exit 1
fi
2026-03-28 13:39:10 +00:00
ok "$TARGET_DISK ($TARGET_SIZE)"
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 13:39:10 +00:00
hrule
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p "${ORANGE} ⚠ all data on $TARGET_DISK will be erased${NC}"
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p "${ORANGE_DIM} press enter to install | ctrl+c to cancel${NC}"
2026-03-28 11:31:48 +00:00
read -s
2026-02-01 05:42:05 +00:00
echo ""
# Unmount any existing partitions
umount ${TARGET_DISK}* 2>/dev/null || true
umount ${TARGET_DISK}p* 2>/dev/null || true
2026-03-26 09:12:16 +00:00
# Create partition table — dual BIOS+UEFI boot + LUKS2 encrypted data
2026-03-28 13:39:10 +00:00
step "Creating partitions"
2026-02-01 05:42:05 +00:00
parted -s "$TARGET_DISK" mklabel gpt
2026-03-21 01:11:05 +00:00
# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks)
parted -s "$TARGET_DISK" mkpart bios_boot 1MiB 2MiB
parted -s "$TARGET_DISK" set 1 bios_grub on
# Partition 2: 512MB EFI System Partition (for UEFI boot)
parted -s "$TARGET_DISK" mkpart efi fat32 2MiB 514MiB
parted -s "$TARGET_DISK" set 2 esp on
2026-03-26 09:12:16 +00:00
# Partition 3: Root filesystem (30GB — system, packages, container runtime)
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 30GiB
# Partition 4: Encrypted data (LUKS2 — Bitcoin data, secrets, app volumes)
parted -s "$TARGET_DISK" mkpart data 30GiB 100%
2026-02-01 05:42:05 +00:00
sleep 2
# Determine partition names
if [[ "$TARGET_DISK" == *nvme* ]]; then
2026-03-21 01:11:05 +00:00
BIOS_PART="${TARGET_DISK}p1"
EFI_PART="${TARGET_DISK}p2"
ROOT_PART="${TARGET_DISK}p3"
2026-03-26 09:12:16 +00:00
DATA_PART="${TARGET_DISK}p4"
2026-02-01 05:42:05 +00:00
else
2026-03-21 01:11:05 +00:00
BIOS_PART="${TARGET_DISK}1"
EFI_PART="${TARGET_DISK}2"
ROOT_PART="${TARGET_DISK}3"
2026-03-26 09:12:16 +00:00
DATA_PART="${TARGET_DISK}4"
2026-02-01 05:42:05 +00:00
fi
# Format partitions
2026-03-28 13:39:10 +00:00
step "Formatting partitions"
2026-03-25 18:25:01 +00:00
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
dd if=/dev/zero of="$BIOS_PART" bs=1M count=1 2>/dev/null || true
2026-03-29 12:06:19 +01:00
run mkfs.vfat -F32 -n EFI "$EFI_PART"
run mkfs.ext4 -F -L archipelago "$ROOT_PART"
2026-02-01 05:42:05 +00:00
2026-03-26 09:12:16 +00:00
# Mount root + extract rootfs (need cryptsetup from rootfs for LUKS)
2026-03-28 13:39:10 +00:00
ok "Partitions created"
echo ""
2026-03-28 23:58:42 +00:00
p " ${ORANGE_DIM}Mounting filesystems...${NC}"
2026-02-01 05:42:05 +00:00
mkdir -p /mnt/target
mount "$ROOT_PART" /mnt/target
mkdir -p /mnt/target/boot/efi
mount "$EFI_PART" /mnt/target/boot/efi
2026-03-28 13:39:10 +00:00
step "Installing system"
2026-03-29 12:06:19 +01:00
run tar -xf "$ROOTFS_TAR" -C /mnt/target
2026-02-01 05:42:05 +00:00
2026-03-26 09:12:16 +00:00
# LUKS2 encryption for data partition
2026-03-28 13:39:10 +00:00
step "Encrypting data partition"
2026-03-26 09:12:16 +00:00
# Generate random 4KB key file
dd if=/dev/urandom of=/mnt/target/root/.luks-archipelago.key bs=4096 count=1 2>/dev/null
chmod 600 /mnt/target/root/.luks-archipelago.key
2026-03-26 17:28:08 +00:00
# Load dm_mod kernel module (required for device-mapper / LUKS)
modprobe dm_mod 2>/dev/null || true
modprobe dm_crypt 2>/dev/null || true
# Bind-mount /dev, /proc, /sys so cryptsetup works in chroot
2026-03-26 09:12:16 +00:00
mount --bind /dev /mnt/target/dev
2026-03-26 17:28:08 +00:00
mount --bind /proc /mnt/target/proc
mount --bind /sys /mnt/target/sys
2026-03-26 09:12:16 +00:00
# Detect AES-NI support for cipher selection
if grep -q aes /proc/cpuinfo 2>/dev/null; then
LUKS_CIPHER="aes-xts-plain64"
echo " AES-NI detected — using AES-256-XTS"
else
LUKS_CIPHER="xchacha20,aes-adiantum-plain64"
echo " No AES-NI — using ChaCha20-Adiantum"
fi
# Format LUKS2 partition with key file
2026-03-29 12:06:19 +01:00
run chroot /mnt/target cryptsetup luksFormat --type luks2 \
2026-03-26 09:12:16 +00:00
--key-file /root/.luks-archipelago.key \
--cipher "$LUKS_CIPHER" --key-size 512 \
--pbkdf argon2id --batch-mode \
"$DATA_PART"
# Open the LUKS volume
2026-03-29 12:06:19 +01:00
run chroot /mnt/target cryptsetup open --type luks2 \
2026-03-26 09:12:16 +00:00
--key-file /root/.luks-archipelago.key \
"$DATA_PART" archipelago-data
2026-03-26 17:28:08 +00:00
# Unmount chroot bind mounts (will be re-mounted later for grub-install)
2026-03-29 12:06:19 +01:00
runq umount /mnt/target/sys
runq umount /mnt/target/proc
runq umount /mnt/target/dev
2026-03-26 09:12:16 +00:00
# Format the inner filesystem
2026-03-29 12:06:19 +01:00
run mkfs.ext4 -F -L archipelago-data /dev/mapper/archipelago-data
2026-03-26 09:12:16 +00:00
# Mount encrypted partition
mkdir -p /mnt/target/var/lib/archipelago
mount /dev/mapper/archipelago-data /mnt/target/var/lib/archipelago
# Recreate directory structure on encrypted partition
2026-04-08 20:27:38 +02:00
mkdir -p /mnt/target/var/lib/archipelago/{data,config,containers,secrets,tor,identities,lnd,nostr-relay,nostr-vpn,tor-hostnames,wireguard}
2026-03-29 15:43:57 +01:00
mkdir -p /mnt/target/var/lib/archipelago/containers/storage
2026-03-26 09:12:16 +00:00
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
2026-04-08 17:48:38 +02:00
# Copy relay config from rootfs (LUKS mount hides what the Dockerfile put there)
if [ -f /mnt/target/etc/archipelago/nostr-relay-config.toml ]; then
cp /mnt/target/etc/archipelago/nostr-relay-config.toml /mnt/target/var/lib/archipelago/nostr-relay/config.toml
fi
2026-03-26 09:12:16 +00:00
chown -R 1000:1000 /mnt/target/var/lib/archipelago
echo " ✅ Data partition encrypted with LUKS2 ($LUKS_CIPHER)"
2026-04-29 12:31:45 -04:00
# Allocate swap space on the encrypted data partition. Without swap, large
# container image builds (immich, indeedhub) and brief memory spikes can
# OOM-kill containers or trigger cgroup cascades. Sized to RAM, capped at
# 8GB (above which swap is rarely useful), floored at 2GB so even
# constrained nodes have headroom. Lives on the LUKS partition so it's
# encrypted at rest.
step "Allocating swap"
RAM_MB=$(($(awk '/^MemTotal:/ {print $2}' /proc/meminfo) / 1024))
SWAP_MB=$RAM_MB
[ "$SWAP_MB" -lt 2048 ] && SWAP_MB=2048
[ "$SWAP_MB" -gt 8192 ] && SWAP_MB=8192
SWAPFILE=/mnt/target/var/lib/archipelago/swapfile
echo " Allocating ${SWAP_MB}MB swap at /var/lib/archipelago/swapfile"
run dd if=/dev/zero of="$SWAPFILE" bs=1M count=$SWAP_MB status=none
run chmod 600 "$SWAPFILE"
run mkswap "$SWAPFILE"
echo " ✅ ${SWAP_MB}MB swap allocated"
2026-03-26 09:12:16 +00:00
# Configure auto-unlock via crypttab (key file on root partition)
2026-03-28 13:39:10 +00:00
step "Configuring system"
2026-03-26 09:12:16 +00:00
DATA_UUID=$(blkid -s UUID -o value "$DATA_PART")
echo "# LUKS2 encrypted data — auto-unlock with key file" > /mnt/target/etc/crypttab
echo "archipelago-data UUID=$DATA_UUID /root/.luks-archipelago.key luks,discard" >> /mnt/target/etc/crypttab
2026-04-10 03:10:49 -04:00
# Configure LUKS auto-unlock: three layers to ensure it works
# Layer 1: cryptsetup-initramfs config (tells update-initramfs to embed key)
mkdir -p /mnt/target/etc/cryptsetup-initramfs
cat > /mnt/target/etc/cryptsetup-initramfs/conf <<'CRYPTCONF'
KEYFILE_PATTERN="/root/.luks-*.key"
UMASK=0077
CRYPTCONF
# Layer 2: initramfs hook to force-copy key file
mkdir -p /mnt/target/etc/initramfs-tools/hooks
cat > /mnt/target/etc/initramfs-tools/hooks/archipelago-luks <<'LUKSHOOK'
#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case $1 in prereqs) prereqs; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
if [ -f /root/.luks-archipelago.key ]; then
mkdir -p "${DESTDIR}/root"
cp /root/.luks-archipelago.key "${DESTDIR}/root/.luks-archipelago.key"
chmod 600 "${DESTDIR}/root/.luks-archipelago.key"
fi
if [ -f /etc/crypttab ]; then
mkdir -p "${DESTDIR}/etc"
cp /etc/crypttab "${DESTDIR}/etc/crypttab"
fi
copy_exec /sbin/cryptsetup
LUKSHOOK
chmod +x /mnt/target/etc/initramfs-tools/hooks/archipelago-luks
# Layer 3: systemd service as fallback — unlocks LUKS early if initramfs missed it
cat > /mnt/target/etc/systemd/system/archipelago-luks-unlock.service <<'LUKSUNIT'
[Unit]
Description=Unlock Archipelago LUKS data partition
DefaultDependencies=no
Before=local-fs-pre.target
After=systemd-udevd.service
Wants=systemd-udevd.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c '\
if [ -e /dev/mapper/archipelago-data ]; then exit 0; fi; \
DATA_DEV=$(blkid -t TYPE=crypto_LUKS -o device 2>/dev/null | head -1); \
if [ -z "$DATA_DEV" ]; then exit 0; fi; \
cryptsetup open --type luks2 --key-file /root/.luks-archipelago.key "$DATA_DEV" archipelago-data'
[Install]
WantedBy=local-fs-pre.target
LUKSUNIT
chroot /mnt/target systemctl enable archipelago-luks-unlock.service 2>/dev/null || \
ln -sf /etc/systemd/system/archipelago-luks-unlock.service /mnt/target/etc/systemd/system/local-fs-pre.target.wants/archipelago-luks-unlock.service
2026-02-01 05:42:05 +00:00
# Create fstab
cat > /mnt/target/etc/fstab <<EOF
# Archipelago Bitcoin Node OS
2026-03-26 09:12:16 +00:00
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
2026-04-10 03:10:49 -04:00
/dev/mapper/archipelago-data /var/lib/archipelago ext4 defaults,nofail,x-systemd.device-timeout=60 0 2
2026-04-29 12:31:45 -04:00
# Swap on encrypted data partition — activated after LUKS unlock
/var/lib/archipelago/swapfile none swap sw,nofail 0 0
2026-02-01 05:42:05 +00:00
EOF
# Configure hostname
echo "archipelago" > /mnt/target/etc/hostname
cat > /mnt/target/etc/hosts <<EOF
127.0.0.1 localhost
127.0.1.1 archipelago
::1 localhost ip6-localhost ip6-loopback
EOF
2026-03-27 13:32:54 +00:00
chmod 644 /mnt/target/etc/hosts
2026-02-01 05:42:05 +00:00
2026-03-27 17:17:18 +00:00
# Pre-create container storage dirs (ReadWritePaths needs these to exist)
mkdir -p /mnt/target/home/archipelago/.local/share/containers
2026-03-26 10:46:26 +00:00
mkdir -p /mnt/target/home/archipelago/.config/containers
2026-03-27 17:17:18 +00:00
chown -R 1000:1000 /mnt/target/home/archipelago/.local
2026-03-29 15:43:57 +01:00
# Redirect container storage to encrypted LUKS partition (not root filesystem)
# Without this, pulling images fills the 29GB root partition
cat > /mnt/target/home/archipelago/.config/containers/storage.conf <<'STORAGECONF'
[storage]
driver = "overlay"
graphroot = "/var/lib/archipelago/containers/storage"
runroot = "/run/user/1000/containers"
STORAGECONF
# Symlink for backward compat (some tools look in ~/.local/share/containers)
ln -sf /var/lib/archipelago/containers/storage /mnt/target/home/archipelago/.local/share/containers/storage 2>/dev/null || true
2026-04-12 09:06:12 -04:00
# Configure Archipelago app registries (primary + fallback)
2026-03-26 10:46:26 +00:00
cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF'
2026-04-12 09:06:12 -04:00
unqualified-search-registries = ["docker.io"]
2026-03-26 10:46:26 +00:00
[[registry]]
2026-04-29 12:31:45 -04:00
location = "146.59.87.168:3000"
2026-03-26 10:46:26 +00:00
insecure = true
2026-04-12 09:06:12 -04:00
[[registry]]
2026-07-10 18:55:32 +01:00
location = "146.59.87.168:3000"
2026-04-12 09:06:12 -04:00
insecure = true
2026-03-26 10:46:26 +00:00
REGCONF
chown -R 1000:1000 /mnt/target/home/archipelago/.config
2026-04-12 09:06:12 -04:00
# Pre-create dynamic registry config for the backend (fallback registries)
mkdir -p /mnt/target/var/lib/archipelago/config
cat > /mnt/target/var/lib/archipelago/config/registries.json <<'DYNREG'
{
"registries": [
2026-04-29 12:31:45 -04:00
{"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Primary", "tls_verify": false, "enabled": true, "priority": 0},
2026-07-10 18:55:32 +01:00
{"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Fallback", "tls_verify": true, "enabled": true, "priority": 10}
2026-04-12 09:06:12 -04:00
]
}
DYNREG
chown -R 1000:1000 /mnt/target/var/lib/archipelago/config
2026-04-09 11:47:35 +02:00
# Configure podman to use netavark backend (enables container DNS on archy-net).
2026-04-09 21:32:08 +02:00
# netavark + aardvark-dns binaries come from the rootfs (Debian 13 apt packages).
2026-04-09 11:47:35 +02:00
if [ -f /mnt/target/usr/lib/podman/netavark ]; then
2026-03-30 18:57:17 +01:00
mkdir -p /mnt/target/home/archipelago/.config/containers
cat > /mnt/target/home/archipelago/.config/containers/containers.conf <<'CONTAINERSCONF'
[network]
network_backend = "netavark"
2026-04-10 03:10:49 -04:00
default_rootless_network_cmd = "pasta"
[engine]
image_copy_tmp_dir = "/var/lib/archipelago/containers/tmp"
2026-03-30 18:57:17 +01:00
CONTAINERSCONF
2026-04-10 03:10:49 -04:00
mkdir -p /mnt/target/var/lib/archipelago/containers/tmp
chown -R 1000:1000 /mnt/target/var/lib/archipelago/containers/tmp
2026-03-30 18:57:17 +01:00
chown -R 1000:1000 /mnt/target/home/archipelago/.config/containers
2026-04-09 11:47:35 +02:00
echo " Configured netavark backend (container DNS enabled)"
2026-03-30 18:57:17 +01:00
else
2026-04-09 11:47:35 +02:00
echo " WARNING: netavark not found in rootfs — container DNS will not work"
2026-03-30 18:57:17 +01:00
fi
2026-03-26 15:58:16 +00:00
# Laptop support: ignore lid close so server keeps running
mkdir -p /mnt/target/etc/systemd/logind.conf.d
cat > /mnt/target/etc/systemd/logind.conf.d/lid-ignore.conf <<'LIDCONF'
[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
LIDCONF
2026-02-01 05:42:05 +00:00
# Copy Archipelago binaries and files
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
cp -r "$BOOT_MEDIA/archipelago/bin/"* /mnt/target/usr/local/bin/ 2>/dev/null || true
chmod +x /mnt/target/usr/local/bin/* 2>/dev/null || true
fi
if [ -d "$BOOT_MEDIA/archipelago/web-ui" ]; then
cp -r "$BOOT_MEDIA/archipelago/web-ui" /mnt/target/opt/archipelago/
fi
2026-04-10 04:01:35 -04:00
# Mark unbundled mode so first-boot only creates FileBrowser (user installs apps from Marketplace)
if [ -f "$BOOT_MEDIA/archipelago/.unbundled" ]; then
touch /mnt/target/opt/archipelago/.unbundled
echo " Unbundled mode: apps install on-demand from Marketplace"
fi
2026-02-01 05:42:05 +00:00
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/target/etc/archipelago/
fi
# Copy pre-bundled container images
if [ -d "$BOOT_MEDIA/archipelago/container-images" ]; then
echo " Copying container images (this may take a moment)..."
mkdir -p /mnt/target/opt/archipelago/container-images
cp -r "$BOOT_MEDIA/archipelago/container-images/"*.tar /mnt/target/opt/archipelago/container-images/ 2>/dev/null || true
# Copy first-boot loader script and service
mkdir -p /mnt/target/opt/archipelago/scripts
if [ -f "$BOOT_MEDIA/archipelago/scripts/load-container-images.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/load-container-images.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/load-container-images.sh
fi
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" /mnt/target/etc/systemd/system/
fi
2026-07-13 13:41:16 +01:00
if [ -f "$BOOT_MEDIA/archipelago/scripts/first-boot-secrets.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/first-boot-secrets.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-secrets.sh
fi
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.service" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-secrets.service" /mnt/target/etc/systemd/system/
fi
2026-02-17 15:03:34 +00:00
if [ -f "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
fi
if [ -d "$BOOT_MEDIA/archipelago/scripts/tor" ]; then
mkdir -p /mnt/target/opt/archipelago/scripts/tor
cp -r "$BOOT_MEDIA/archipelago/scripts/tor/"* /mnt/target/opt/archipelago/scripts/tor/ 2>/dev/null || true
fi
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-setup-tor.service" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-setup-tor.service" /mnt/target/etc/systemd/system/
fi
2026-03-21 03:06:29 +00:00
# Copy shared script library
if [ -d "$BOOT_MEDIA/archipelago/scripts/lib" ]; then
mkdir -p /mnt/target/opt/archipelago/scripts/lib
cp -r "$BOOT_MEDIA/archipelago/scripts/lib/"* /mnt/target/opt/archipelago/scripts/lib/ 2>/dev/null || true
fi
2026-02-25 18:04:41 +00:00
if [ -f "$BOOT_MEDIA/archipelago/scripts/first-boot-containers.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/first-boot-containers.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-containers.sh
fi
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-containers.service" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-containers.service" /mnt/target/etc/systemd/system/
fi
2026-03-08 02:27:58 +00:00
# Copy docker UI source files for first-boot container builds
if [ -d "$BOOT_MEDIA/archipelago/docker" ]; then
mkdir -p /mnt/target/opt/archipelago/docker
cp -r "$BOOT_MEDIA/archipelago/docker/"* /mnt/target/opt/archipelago/docker/ 2>/dev/null || true
fi
2026-02-01 05:42:05 +00:00
echo " ✅ Container images staged for first-boot loading"
fi
2026-03-09 17:09:59 +00:00
# Initialize backend data directories for seamless first boot
mkdir -p /mnt/target/var/lib/archipelago/tor-config
mkdir -p /mnt/target/var/lib/archipelago/identities
mkdir -p /mnt/target/var/lib/archipelago/lnd
2026-03-27 16:16:57 +00:00
# Copy test scripts for post-install validation
for test_script in run-e2e-tests.sh run-post-install-tests.sh; do
if [ -f "$BOOT_MEDIA/archipelago/scripts/$test_script" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/$test_script" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/$test_script
fi
done
2026-03-09 17:09:59 +00:00
2026-03-25 15:52:26 +00:00
# Copy self-update script
if [ -f "$BOOT_MEDIA/archipelago/scripts/self-update.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/self-update.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/self-update.sh
# Also place in home for the update timer to find
mkdir -p /mnt/target/home/archipelago/archy/scripts
cp "$BOOT_MEDIA/archipelago/scripts/self-update.sh" /mnt/target/home/archipelago/archy/scripts/
chmod +x /mnt/target/home/archipelago/archy/scripts/self-update.sh
fi
2026-03-27 16:16:57 +00:00
# Copy image-versions.sh (needed by first-boot-containers and updates)
if [ -f "$BOOT_MEDIA/archipelago/scripts/image-versions.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/image-versions.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/image-versions.sh
# Also place in home for container scripts to find
mkdir -p /mnt/target/home/archipelago/archy/scripts
cp "$BOOT_MEDIA/archipelago/scripts/image-versions.sh" /mnt/target/home/archipelago/archy/scripts/
fi
2026-03-25 15:52:26 +00:00
# Clone repo for git-based updates (first-boot will have network)
# Create a script that runs on first boot to clone the repo
cat > /mnt/target/opt/archipelago/scripts/setup-git-updates.sh <<'GITSETUP'
#!/bin/bash
# Clone the Archipelago repo for git-based self-updates
REPO_DIR="/home/archipelago/archy"
if [ -d "$REPO_DIR/.git" ]; then
exit 0 # Already cloned
fi
echo "[update] Cloning Archipelago repo for self-updates..."
2026-07-10 18:55:32 +01:00
su - archipelago -c "git clone https://146.59.87.168:3000/lfg2025/archy $REPO_DIR" 2>/dev/null || {
2026-03-25 15:52:26 +00:00
echo "[update] Git clone failed (network?). Updates will retry on next boot."
exit 0
}
chown -R 1000:1000 "$REPO_DIR"
echo "[update] Repo cloned. Self-updates enabled."
GITSETUP
chmod +x /mnt/target/opt/archipelago/scripts/setup-git-updates.sh
2026-02-01 05:42:05 +00:00
# Ensure correct ownership (use numeric UID:GID 1000:1000 since we're outside chroot)
chown -R 1000:1000 /mnt/target/opt/archipelago 2>/dev/null || true
chown -R 1000:1000 /mnt/target/var/lib/archipelago 2>/dev/null || true
# Create welcome profile (nginx serves on port 80)
cat > /mnt/target/etc/profile.d/archipelago.sh <<'PROFILE'
#!/bin/bash
2026-03-21 01:11:05 +00:00
# Ensure /sbin and /usr/sbin are in PATH (needed for reboot, shutdown, etc.)
case ":$PATH:" in
*:/sbin:*) ;; *) export PATH="$PATH:/sbin:/usr/sbin" ;;
esac
2026-02-01 05:42:05 +00:00
if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
export ARCHIPELAGO_WELCOMED=1
2026-03-28 13:06:34 +00:00
# Wait for network (DHCP may not be ready yet on first boot)
IP=""
for i in 1 2 3 4 5; do
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$IP" ] && break
sleep 2
done
2026-03-28 23:41:40 +00:00
O='\033[38;5;208m'
2026-03-28 23:58:42 +00:00
OD='\033[38;5;130m'
W='\033[1;37m'
2026-03-28 23:41:40 +00:00
N='\033[0m'
2026-03-28 13:06:34 +00:00
clear
2026-03-28 23:58:42 +00:00
echo -e " ${O}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${N}"
echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${N}"
echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
echo -e " ${OD}bitcoin node os${N}"
2026-02-01 05:42:05 +00:00
if [ -n "$IP" ]; then
2026-03-28 23:58:42 +00:00
echo -e " ${W}web ui http://$IP${N}"
echo -e " ${W}ssh archipelago@$IP${N}"
echo -e " ${W}password archipelago (SSH) / password123 (Web)${N}"
2026-03-26 09:12:16 +00:00
else
2026-03-28 23:58:42 +00:00
echo -e " ${OD}Waiting for network...${N}"
2026-03-28 13:06:34 +00:00
fi
2026-03-30 16:35:06 +01:00
if [ -b /dev/mapper/archipelago-data ] || [ -b /dev/mapper/archipelago_crypt ]; then
2026-03-28 23:58:42 +00:00
echo -e " ${OD}storage LUKS2 encrypted${N}"
2026-03-28 13:06:34 +00:00
fi
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
2026-03-28 23:58:42 +00:00
echo -e " ${OD}display Kiosk active (Ctrl+Alt+F1 for terminal)${N}"
2026-03-28 13:06:34 +00:00
else
2026-03-28 23:58:42 +00:00
echo -e " ${OD}display Console (Ctrl+Alt+F7 for kiosk)${N}"
2026-03-26 09:12:16 +00:00
fi
echo ""
2026-02-01 05:42:05 +00:00
fi
PROFILE
chmod +x /mnt/target/etc/profile.d/archipelago.sh
2026-03-30 23:47:15 +01:00
# Force UTF-8 console with Terminus font (supports Unicode block chars for ASCII logo)
cat > /mnt/target/etc/default/console-setup <<'CONSOLESETUP'
ACTIVE_CONSOLES="/dev/tty[1-6]"
CHARMAP="UTF-8"
CODESET="Uni2"
FONTFACE="Terminus"
FONTSIZE="16"
CONSOLESETUP
2026-03-30 16:35:06 +01:00
# Suppress default Debian MOTD (our profile.d script handles the welcome)
echo -n > /mnt/target/etc/motd
rm -f /mnt/target/etc/motd.d/* 2>/dev/null || true
2026-03-29 14:25:13 +01:00
# Ensure reboot/shutdown work without sudo for the archipelago user
# profile.d only runs for login shells; .bashrc handles SSH interactive sessions
if ! grep -q '/sbin' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
echo 'export PATH="$PATH:/sbin:/usr/sbin"' >> /mnt/target/home/archipelago/.bashrc
fi
2026-04-08 17:51:21 +02:00
# Power commands need sudo on SSH sessions (polkit denies without local seat)
if ! grep -q 'alias reboot' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
cat >> /mnt/target/home/archipelago/.bashrc <<'ALIASES'
alias reboot='sudo systemctl reboot'
alias shutdown='sudo shutdown'
alias halt='sudo systemctl halt'
alias poweroff='sudo systemctl poweroff'
ALIASES
fi
2026-03-29 14:25:13 +01:00
2026-03-27 16:16:57 +00:00
# Systemd service: use the production version from rootfs (configs/archipelago.service)
# Do NOT overwrite — the rootfs already has the correct User=archipelago, no DEV_MODE version
if [ ! -f /mnt/target/etc/systemd/system/archipelago.service ]; then
echo " WARNING: archipelago.service missing from rootfs — copying from ISO"
2026-04-28 15:00:58 -04:00
cp "$BOOT_MEDIA/archipelago/../configs/archipelago.service" /mnt/target/etc/systemd/system/archipelago.service 2>/dev/null || true
2026-03-27 16:16:57 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-03-12 22:19:04 +00:00
# Claude API proxy — middleware that injects max_tokens, strips invalid fields
# API key must be set after install via setup-aiui-server.sh or manually
cat > /mnt/target/opt/archipelago/claude-api-proxy.py <<'CLAUDEPROXY'
#!/usr/bin/env python3
import http.server, json, ssl, sys, os, urllib.request, urllib.error
API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
PORT = 3142
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/health":
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
self.wfile.write(b'{"status":"ok"}'); return
cl = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(cl)
try: data = json.loads(body)
except: data = {}
if "max_tokens" not in data: data["max_tokens"] = 8096
for f in ["webSearch","web_search"]: data.pop(f, None)
2026-04-02 11:01:58 +01:00
# Normalize model IDs — map short/dotted names to full API model IDs
MODEL_MAP = {
"claude-haiku-4.5": "claude-haiku-4-5-20251001",
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
"claude-sonnet-4": "claude-sonnet-4-20250514",
"claude-sonnet-4.5": "claude-sonnet-4-5-20250514",
"claude-sonnet-4-5": "claude-sonnet-4-5-20250514",
"claude-opus-4": "claude-opus-4-20250514",
}
m = data.get("model", "")
if m in MODEL_MAP: data["model"] = MODEL_MAP[m]
2026-03-12 22:19:04 +00:00
body = json.dumps(data).encode()
2026-04-02 18:30:34 +01:00
if not API_KEY:
err = json.dumps({"type":"error","error":{"type":"auth_error","message":"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat."}}).encode()
self.send_response(401); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(err))); self.end_headers(); self.wfile.write(err); return
2026-03-12 22:19:04 +00:00
headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}
for h in ["anthropic-version","anthropic-beta"]:
if self.headers.get(h): headers[h] = self.headers[h]
req = urllib.request.Request("https://api.anthropic.com"+self.path, data=body, headers=headers, method="POST")
try:
ctx = ssl.create_default_context()
resp = urllib.request.urlopen(req, context=ctx, timeout=300)
self.send_response(resp.status)
is_stream = "text/event-stream" in (resp.headers.get("Content-Type","") or "")
for k,v in resp.headers.items():
if k.lower() not in ("transfer-encoding","connection"): self.send_header(k,v)
if is_stream: self.send_header("Transfer-Encoding","chunked")
self.end_headers()
if is_stream:
while True:
chunk = resp.read(4096)
if not chunk: break
self.wfile.write(b"%x\r\n" % len(chunk)); self.wfile.write(chunk); self.wfile.write(b"\r\n"); self.wfile.flush()
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
else: self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(e.code); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(e.read())
except Exception as e:
self.send_response(502); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(json.dumps({"error":str(e)}).encode())
def do_GET(self):
if self.path == "/health":
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b'{"status":"ok"}')
else: self.send_response(404); self.end_headers()
def log_message(self, fmt, *args): pass
2026-04-02 18:30:34 +01:00
if not API_KEY: print("WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions")
2026-03-12 22:19:04 +00:00
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
print(f"Claude API proxy on port {PORT}")
server.serve_forever()
CLAUDEPROXY
chmod +x /mnt/target/opt/archipelago/claude-api-proxy.py
# Claude API proxy systemd service (disabled by default — enabled after API key is configured)
cat > /mnt/target/etc/systemd/system/claude-api-proxy.service <<'CLAUDESVC'
[Unit]
Description=Claude API Proxy
After=network.target
[Service]
Type=simple
2026-03-31 05:01:13 +01:00
Environment=ANTHROPIC_API_KEY=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
2026-03-12 22:19:04 +00:00
ExecStart=/usr/bin/python3 /opt/archipelago/claude-api-proxy.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
CLAUDESVC
2026-07-02 08:00:39 -04:00
INSTALLER_SCRIPT
2026-03-26 21:16:07 +00:00
2026-07-02 08:00:39 -04:00
# -----------------------------------------------------------------------------
# Kiosk launcher + systemd service: spliced into auto-install.sh at BUILD time
# from image-recipe/configs/ — the single source of truth (the same files are
# embedded in the Rust binary via include_str! in core/archipelago/src/bootstrap.rs
# and self-healed onto nodes by ensure_kiosk_hardened()).
#
# A previous inline heredoc copy here had silently diverged (unconditional
# --disable-gpu, no CPU/memory limits, VT7 scheme) and shipped stale kiosk
# behavior on fresh ISOs. Never re-inline these payloads.
# -----------------------------------------------------------------------------
KIOSK_LAUNCHER_SRC = " $SCRIPT_DIR /../configs/archipelago-kiosk-launcher.sh"
KIOSK_SERVICE_SRC = " $SCRIPT_DIR /../configs/archipelago-kiosk.service"
for _kiosk_src in " $KIOSK_LAUNCHER_SRC " " $KIOSK_SERVICE_SRC " ; do
if [ ! -f " $_kiosk_src " ] ; then
echo "ERROR: kiosk config file missing: $_kiosk_src " >& 2
echo " The ISO must ship the maintained kiosk launcher/service from" >& 2
echo " image-recipe/configs/ — refusing to build (no stale fallback)." >& 2
exit 1
fi
done
# Guard: payloads must not contain the heredoc terminators we wrap them in.
if grep -qx 'KIOSKLAUNCHER' " $KIOSK_LAUNCHER_SRC " || grep -qx 'KIOSKSVC' " $KIOSK_SERVICE_SRC " ; then
echo "ERROR: kiosk config contains a reserved heredoc terminator line (KIOSKLAUNCHER/KIOSKSVC)" >& 2
2026-03-26 09:12:16 +00:00
exit 1
fi
2026-07-02 08:00:39 -04:00
{
echo "# Kiosk mode — X11 + Chromium fullscreen on attached display"
echo "# Not enabled by default; toggle via: sudo archipelago-kiosk enable/disable"
echo "# Payloads spliced at ISO-build time from image-recipe/configs/ (source of truth)."
echo "cat > /mnt/target/usr/local/bin/archipelago-kiosk-launcher <<'KIOSKLAUNCHER'"
cat " $KIOSK_LAUNCHER_SRC "
echo "KIOSKLAUNCHER"
echo "chmod +x /mnt/target/usr/local/bin/archipelago-kiosk-launcher"
echo ""
echo "cat > /mnt/target/etc/systemd/system/archipelago-kiosk.service <<'KIOSKSVC'"
cat " $KIOSK_SERVICE_SRC "
echo "KIOSKSVC"
} >> " $ARCH_DIR /auto-install.sh"
2026-03-26 09:12:16 +00:00
2026-07-02 08:00:39 -04:00
cat >> " $ARCH_DIR /auto-install.sh" <<'INSTALLER_SCRIPT'
2026-03-26 09:12:16 +00:00
# Toggle script: sudo archipelago-kiosk enable|disable|status
cat > /mnt/target/usr/local/bin/archipelago-kiosk <<'KIOSKTOGGLE'
#!/bin/bash
set -e
case "${1:-status}" in
enable)
echo "Enabling kiosk mode (X11 + Chromium on display)..."
systemctl enable archipelago-kiosk.service
systemctl start archipelago-kiosk.service 2>/dev/null || true
echo "Kiosk mode ENABLED. Console login (tty1) is now disabled."
echo "To access the server, use SSH or the web UI."
;;
disable)
echo "Disabling kiosk mode (restoring console login)..."
systemctl stop archipelago-kiosk.service 2>/dev/null || true
systemctl disable archipelago-kiosk.service
systemctl restart getty@tty1.service 2>/dev/null || true
echo "Kiosk mode DISABLED. Console login restored on tty1."
;;
status)
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
echo "Kiosk mode: ACTIVE (display showing web UI)"
elif systemctl is-enabled archipelago-kiosk.service >/dev/null 2>&1; then
echo "Kiosk mode: ENABLED (will start on next boot)"
else
echo "Kiosk mode: DISABLED (console login on tty1)"
fi
;;
2026-03-26 18:30:13 +00:00
toggle)
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
systemctl stop archipelago-kiosk.service 2>/dev/null || true
systemctl restart getty@tty1.service 2>/dev/null || true
chvt 1 2>/dev/null || true
else
systemctl start archipelago-kiosk.service 2>/dev/null || true
fi
;;
2026-03-26 09:12:16 +00:00
*)
2026-03-26 18:30:13 +00:00
echo "Usage: archipelago-kiosk [enable|disable|status|toggle]"
2026-03-26 09:12:16 +00:00
echo " enable — Start kiosk (fullscreen web UI on display)"
echo " disable — Stop kiosk, restore console login"
2026-03-26 18:30:13 +00:00
echo " toggle — Switch between kiosk and terminal"
2026-03-26 09:12:16 +00:00
echo " status — Show current mode"
2026-03-26 18:30:13 +00:00
echo ""
echo "Keyboard shortcuts (from terminal):"
2026-07-02 08:00:39 -04:00
echo " Ctrl+Alt+F1 — Kiosk display (when enabled; console login when disabled)"
echo " Ctrl+Alt+F2 — Text console"
2026-03-26 09:12:16 +00:00
exit 1
;;
esac
KIOSKTOGGLE
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk
2026-02-01 05:42:05 +00:00
# Install GRUB
2026-03-28 13:39:10 +00:00
step "Installing bootloader"
2026-02-01 05:42:05 +00:00
mount --bind /dev /mnt/target/dev
mount --bind /dev/pts /mnt/target/dev/pts
mount --bind /proc /mnt/target/proc
mount --bind /sys /mnt/target/sys
mount --bind /run /mnt/target/run
2026-03-10 23:29:05 +00:00
# Set passwords reliably by directly editing /etc/shadow
# chpasswd fails silently in chroot due to missing PAM — use sed instead
echo " Setting user passwords..."
# Pre-computed SHA-512 hash of "archipelago"
ARCHY_HASH='$6$archipelago.salt1$QpB5VPzGHOKRVKQ5cTfd4R7PYqmMH5MUx6MxFN7MbZkxWKR3WxFp.RV4tBVbJiv.6iWXfHeq3vDph7G.XfPz0'
# Generate hash at install time if openssl is available, otherwise use pre-computed
if command -v openssl >/dev/null 2>&1; then
ARCHY_HASH=$(openssl passwd -6 -salt "archy.install" "archipelago")
fi
# Direct shadow file manipulation — works without PAM
sed -i "s|^archipelago:[^:]*:|archipelago:${ARCHY_HASH}:|" /mnt/target/etc/shadow
sed -i "s|^root:[^:]*:|root:${ARCHY_HASH}:|" /mnt/target/etc/shadow
# Verify the password was set (not locked/empty)
if grep -q "^archipelago:[!*]" /mnt/target/etc/shadow 2>/dev/null; then
echo " WARNING: Password still locked, trying chpasswd fallback..."
chroot /mnt/target bash -c 'echo "archipelago:archipelago" | chpasswd' 2>/dev/null || true
fi
echo " Passwords set for archipelago and root users"
2026-03-26 17:31:26 +00:00
# Remove shim-signed before grub-install to prevent hooks re-creating shim files
chroot /mnt/target dpkg --purge shim-signed shim-helpers-amd64-signed shim-helpers-arm64-signed 2>/dev/null || true
2026-03-21 01:11:05 +00:00
# UEFI boot: install to fallback path (/EFI/BOOT/BOOTX64.EFI) for maximum compatibility
echo " Installing UEFI bootloader..."
2026-03-29 12:06:19 +01:00
if run chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable; then
ok "UEFI bootloader installed (removable/fallback path)"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
warn "UEFI removable install failed, trying standard..."
if run chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago; then
ok "UEFI bootloader installed (standard)"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
fail "UEFI bootloader installation failed"
2026-03-21 01:11:05 +00:00
fi
fi
2026-03-26 09:12:16 +00:00
# EFI boot: grub-install --removable places unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
# No shim chain — Secure Boot must be disabled. shim-signed was removed from rootfs
# because it installs BOOTX64.CSV + shimx64.efi which cause "Failed to open \EFI\BOOT\"
# errors with garbled filenames on every boot.
2026-03-25 20:47:14 +00:00
echo " Verifying EFI boot files..."
2026-03-21 01:11:05 +00:00
EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT"
2026-03-25 20:47:14 +00:00
if [ "$ARCH" = "x86_64" ]; then
EFI_BOOT_BINARY="BOOTX64.EFI"
else
EFI_BOOT_BINARY="BOOTAA64.EFI"
fi
2026-03-26 09:12:16 +00:00
# Remove any residual shim chain files (from grub-efi-*-signed package hooks)
# These cause firmware to try loading garbled vendor paths before falling back
for shim_file in shimx64.efi mmx64.efi fbx64.efi BOOTX64.CSV shimaa64.efi mmaa64.efi fbaa64.efi BOOTAA64.CSV; do
if [ -f "$EFI_BOOT_DIR/$shim_file" ] && [ "$shim_file" != "$EFI_BOOT_BINARY" ]; then
rm -f "$EFI_BOOT_DIR/$shim_file"
echo " Removed shim artifact: $shim_file"
fi
done
# Also remove vendor-specific EFI directory (shim creates /EFI/archipelago/)
rm -rf "/mnt/target/boot/efi/EFI/archipelago" 2>/dev/null || true
2026-03-26 17:31:26 +00:00
# Nuclear cleanup: remove everything except the GRUB binary from EFI/BOOT
if [ -d "$EFI_BOOT_DIR" ]; then
for f in "$EFI_BOOT_DIR"/*; do
[ "$(basename "$f")" = "$EFI_BOOT_BINARY" ] && continue
[ "$(basename "$f")" = "grub.cfg" ] && continue
rm -f "$f" 2>/dev/null && echo " Removed: $(basename "$f")"
done
fi
2026-03-25 20:47:14 +00:00
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
2026-03-25 19:25:55 +00:00
ls -la "$EFI_BOOT_DIR/"
2026-03-21 01:11:05 +00:00
else
2026-03-25 20:47:14 +00:00
echo " ❌ Missing $EFI_BOOT_DIR/$EFI_BOOT_BINARY — boot will fail!"
2026-03-21 01:11:05 +00:00
fi
# Legacy BIOS boot: only install if the installer booted in Legacy BIOS mode
# (if /sys/firmware/efi exists, the machine supports UEFI — no need for BIOS fallback)
if [ -n "${GRUB_BIOS_TARGET}" ] && [ ! -d /sys/firmware/efi ]; then
echo " Installing Legacy BIOS bootloader (machine booted in BIOS mode)..."
2026-03-29 12:06:19 +01:00
if run chroot /mnt/target grub-install --target=${GRUB_BIOS_TARGET} "${TARGET_DISK}"; then
ok "Legacy BIOS bootloader installed"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
warn "Legacy BIOS bootloader failed (UEFI-only boot)"
2026-03-21 01:11:05 +00:00
fi
elif [ -n "${GRUB_BIOS_TARGET}" ]; then
echo " Skipping Legacy BIOS bootloader (machine supports UEFI)"
fi
2026-03-27 18:19:49 +00:00
# Clean any stale live-boot artifacts (should not exist in the custom rootfs,
# but clean up defensively in case Docker base image pulled them in)
2026-03-25 16:51:14 +00:00
rm -f /mnt/target/etc/initramfs-tools/conf.d/live-boot* 2>/dev/null || true
rm -f /mnt/target/usr/share/initramfs-tools/scripts/live* 2>/dev/null || true
rm -f /mnt/target/usr/share/initramfs-tools/hooks/live* 2>/dev/null || true
2026-04-08 15:19:14 +02:00
# Brand GRUB as Archipelago (default says "Debian GNU/Linux")
sed -i 's/^GRUB_DISTRIBUTOR=.*/GRUB_DISTRIBUTOR="Archipelago"/' /mnt/target/etc/default/grub
grep -q '^GRUB_DISTRIBUTOR' /mnt/target/etc/default/grub || echo 'GRUB_DISTRIBUTOR="Archipelago"' >> /mnt/target/etc/default/grub
2026-03-25 16:56:02 +00:00
# Suppress os-prober warning in GRUB
echo "GRUB_DISABLE_OS_PROBER=true" >> /mnt/target/etc/default/grub
2026-04-02 20:28:53 +01:00
# GFX fallback for hardware without graphical GRUB support
echo 'GRUB_GFXMODE=auto' >> /mnt/target/etc/default/grub
echo 'GRUB_GFXPAYLOAD_LINUX=keep' >> /mnt/target/etc/default/grub
echo 'GRUB_TERMINAL_OUTPUT=gfxterm' >> /mnt/target/etc/default/grub
2026-03-25 16:56:02 +00:00
2026-03-27 18:19:49 +00:00
# Install Archipelago GRUB theme on target system
if [ -d "$BOOT_MEDIA/boot/grub/themes/archipelago" ]; then
mkdir -p /mnt/target/boot/grub/themes/archipelago
cp "$BOOT_MEDIA/boot/grub/themes/archipelago/"* /mnt/target/boot/grub/themes/archipelago/
echo 'GRUB_THEME="/boot/grub/themes/archipelago/theme.txt"' >> /mnt/target/etc/default/grub
echo " Installed Archipelago GRUB theme on target"
fi
2026-03-27 22:26:56 +00:00
# Install Archipelago Plymouth theme on target system
if [ -d "$BOOT_MEDIA/archipelago/plymouth-theme" ]; then
PLYMOUTH_DIR="/mnt/target/usr/share/plymouth/themes/archipelago"
mkdir -p "$PLYMOUTH_DIR"
cp "$BOOT_MEDIA/archipelago/plymouth-theme/"* "$PLYMOUTH_DIR/"
# Set as default Plymouth theme
chroot /mnt/target plymouth-set-default-theme archipelago 2>/dev/null || \
ln -sf /usr/share/plymouth/themes/archipelago/archipelago.plymouth \
/mnt/target/etc/alternatives/default.plymouth 2>/dev/null || true
2026-04-10 03:10:49 -04:00
# Configure clean boot: splash, suppress kernel noise, hide cursor
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT=".*"/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force"/' \
/mnt/target/etc/default/grub 2>/dev/null || true
2026-03-27 22:26:56 +00:00
echo " Installed Archipelago Plymouth theme on target"
fi
2026-03-21 01:11:05 +00:00
# Regenerate initramfs — the one from Docker export is corrupt/incomplete
# (Docker builds have limited /proc, /sys, /dev so initramfs generation fails silently)
echo " Regenerating initramfs..."
2026-03-29 12:06:19 +01:00
run chroot /mnt/target update-initramfs -u -k all
2026-02-01 05:42:05 +00:00
2026-03-29 12:06:19 +01:00
run chroot /mnt/target update-grub
2026-02-01 05:42:05 +00:00
2026-03-28 13:39:10 +00:00
# CRITICAL: Write EFI grub.cfg that finds the root filesystem and loads the full config.
# grub-install --removable creates a BOOTX64.EFI that looks for grub.cfg on the
# EFI FAT partition (/EFI/BOOT/grub.cfg). This stub must search for the root FS
# and then load the full /boot/grub/grub.cfg from ext4.
ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART")
if [ -n "$ROOT_UUID" ] && [ -d "/mnt/target/boot/efi/EFI/BOOT" ]; then
cat > /mnt/target/boot/efi/EFI/BOOT/grub.cfg <<EFICFG
search.fs_uuid $ROOT_UUID root
set prefix=(\$root)/boot/grub
configfile \$prefix/grub.cfg
EFICFG
echo " Wrote EFI grub.cfg (root UUID=$ROOT_UUID)"
else
echo " WARNING: Could not write EFI grub.cfg (UUID=$ROOT_UUID)"
fi
2026-03-18 10:50:13 +00:00
# Install udev rule for mesh radio stable naming (/dev/mesh-radio)
2026-03-27 18:19:49 +00:00
MESH_RULES=""
2026-04-28 15:00:58 -04:00
for p in "$BOOT_MEDIA/99-mesh-radio.rules" /cdrom/99-mesh-radio.rules "$BOOT_MEDIA/archipelago/../configs/99-mesh-radio.rules"; do
2026-03-27 18:19:49 +00:00
[ -f "$p" ] && MESH_RULES="$p" && break
done
if [ -n "$MESH_RULES" ]; then
cp "$MESH_RULES" /mnt/target/etc/udev/rules.d/99-mesh-radio.rules
2026-03-18 10:50:13 +00:00
echo " Installed mesh radio udev rule"
fi
2026-03-26 20:58:34 +00:00
# First-boot diagnostics — runs once, captures system state for debugging
cat > /mnt/target/usr/local/bin/archipelago-diagnostics <<'DIAG'
#!/bin/bash
LOG="/var/log/archipelago-first-boot-diagnostics.log"
echo "=== Archipelago First Boot Diagnostics ===" > "$LOG"
echo "Date: $(date -u)" >> "$LOG"
echo "Kernel: $(uname -r)" >> "$LOG"
echo "Hostname: $(hostname)" >> "$LOG"
echo "IP: $(hostname -I 2>/dev/null)" >> "$LOG"
echo "" >> "$LOG"
echo "=== Disk ===" >> "$LOG"
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE >> "$LOG" 2>&1
df -h >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== LUKS ===" >> "$LOG"
ls -la /dev/mapper/archipelago-data 2>&1 >> "$LOG"
cryptsetup status archipelago-data >> "$LOG" 2>&1 || echo "No LUKS" >> "$LOG"
echo "" >> "$LOG"
echo "=== Services ===" >> "$LOG"
for svc in nginx archipelago archipelago-kiosk archipelago-load-images \
2026-07-13 13:41:16 +01:00
archipelago-first-boot-containers archipelago-first-boot-secrets \
archipelago-setup-tor console-setup; do
2026-03-26 20:58:34 +00:00
STATUS=$(systemctl is-active "$svc" 2>/dev/null || echo "inactive")
ENABLED=$(systemctl is-enabled "$svc" 2>/dev/null || echo "disabled")
printf " %-40s %s / %s\n" "$svc" "$STATUS" "$ENABLED" >> "$LOG"
done
echo "" >> "$LOG"
echo "=== Failed Services ===" >> "$LOG"
systemctl --failed --no-pager >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== Nginx ===" >> "$LOG"
nginx -t >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== EFI Boot ===" >> "$LOG"
ls -laR /boot/efi/EFI/ >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== SSL Cert ===" >> "$LOG"
ls -la /etc/archipelago/ssl/ >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== Podman ===" >> "$LOG"
su - archipelago -c "podman ps -a --format '{{.Names}} {{.Status}}'" >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== Memory ===" >> "$LOG"
free -h >> "$LOG" 2>&1
echo "" >> "$LOG"
echo "=== Journal Errors (last 50) ===" >> "$LOG"
journalctl -p err --no-pager -n 50 >> "$LOG" 2>&1
echo "Diagnostics saved to $LOG"
DIAG
chmod +x /mnt/target/usr/local/bin/archipelago-diagnostics
cat > /mnt/target/etc/systemd/system/archipelago-diagnostics.service <<'DIAGSVC'
[Unit]
Description=Archipelago First Boot Diagnostics
After=multi-user.target archipelago.service nginx.service
ConditionPathExists=!/var/log/archipelago-first-boot-diagnostics.log
[Service]
Type=oneshot
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/bin/archipelago-diagnostics
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
DIAGSVC
2026-03-26 18:30:13 +00:00
# Ensure SSL cert exists for nginx HTTPS (safety net if rootfs build missed it)
if [ ! -f /mnt/target/etc/archipelago/ssl/archipelago.crt ]; then
mkdir -p /mnt/target/etc/archipelago/ssl
chroot /mnt/target openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/archipelago/ssl/archipelago.key \
-out /etc/archipelago/ssl/archipelago.crt \
2026-07-01 10:42:20 -04:00
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" \
-addext "subjectAltName=DNS:archipelago,DNS:archipelago.local,DNS:localhost,IP:127.0.0.1" 2>/dev/null
2026-03-26 18:30:13 +00:00
chmod 600 /mnt/target/etc/archipelago/ssl/archipelago.key
echo " Generated self-signed SSL certificate"
fi
2026-03-27 16:16:57 +00:00
# Enable linger for rootless podman (containers survive logout)
mkdir -p /mnt/target/var/lib/systemd/linger
touch /mnt/target/var/lib/systemd/linger/archipelago
# Enable podman socket for archipelago user (activated on first login/boot)
mkdir -p /mnt/target/home/archipelago/.config/systemd/user/sockets.target.wants
ln -sf /usr/lib/systemd/user/podman.socket /mnt/target/home/archipelago/.config/systemd/user/sockets.target.wants/podman.socket 2>/dev/null || true
chown -R 1000:1000 /mnt/target/home/archipelago/.config 2>/dev/null || true
# Ensure /run/user/1000 is created at boot for podman socket
mkdir -p /mnt/target/etc/tmpfiles.d
echo 'd /run/user/1000 0700 archipelago archipelago -' > /mnt/target/etc/tmpfiles.d/archipelago-runtime.conf
2026-04-23 12:02:46 -04:00
# Pre-create /var/log/archipelago/ and container-installs.log so the
# backend (running as `archipelago`) can append to them without needing
# root. Logrotate rotates files in this directory daily.
cat > /mnt/target/etc/tmpfiles.d/archipelago-logs.conf <<'LOGSTMPFILES'
d /var/log/archipelago 0755 archipelago archipelago - -
f /var/log/archipelago/container-installs.log 0644 archipelago archipelago - -
LOGSTMPFILES
2026-04-02 16:15:04 +01:00
# Bootstrap switchover — checks when local Bitcoin finishes IBD and switches services
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.service <<'BSSERVICE'
[Unit]
Description=Switch Bitcoin-dependent services from bootstrap to local node
After=archipelago-first-boot-containers.service
[Service]
Type=oneshot
User=archipelago
ExecStart=/opt/archipelago/scripts/bootstrap-switchover.sh
BSSERVICE
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.timer <<'BSTIMER'
[Unit]
Description=Periodically check if local Bitcoin is synced and switch from bootstrap
[Timer]
OnBootSec=10min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target
BSTIMER
# Copy bootstrap config to install target
if [ -f "$BOOT_MEDIA/archipelago/bootstrap.conf" ]; then
cp "$BOOT_MEDIA/archipelago/bootstrap.conf" /mnt/target/opt/archipelago/bootstrap.conf
chmod 600 /mnt/target/opt/archipelago/bootstrap.conf
chown root:root /mnt/target/opt/archipelago/bootstrap.conf
fi
# Copy bootstrap switchover script
if [ -f "$BOOT_MEDIA/archipelago/scripts/bootstrap-switchover.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/bootstrap-switchover.sh" /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/bootstrap-switchover.sh
fi
2026-02-01 05:42:05 +00:00
# Enable services
2026-04-02 16:15:04 +01:00
chroot /mnt/target systemctl enable archipelago-bootstrap-switchover.timer 2>/dev/null || true
2026-02-01 05:42:05 +00:00
chroot /mnt/target systemctl enable archipelago.service 2>/dev/null || true
chroot /mnt/target systemctl enable nginx.service 2>/dev/null || true
chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null || true
2026-07-13 13:41:16 +01:00
chroot /mnt/target systemctl enable archipelago-first-boot-secrets.service 2>/dev/null || true
2026-02-17 15:03:34 +00:00
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
2026-02-25 18:04:41 +00:00
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
2026-03-26 19:50:59 +00:00
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
2026-04-10 03:10:49 -04:00
chroot /mnt/target systemctl enable nostr-vpn.service 2>/dev/null || true
2026-04-02 01:28:11 +01:00
# Enable claude-api-proxy (create symlink manually — chroot systemctl can fail)
chroot /mnt/target systemctl enable claude-api-proxy.service 2>/dev/null || \
ln -sf /etc/systemd/system/claude-api-proxy.service /mnt/target/etc/systemd/system/multi-user.target.wants/claude-api-proxy.service 2>/dev/null || true
2026-03-27 11:12:31 +00:00
2026-03-27 17:17:18 +00:00
# Fix console-setup: setupcon needs /tmp writable, add ordering dependency
mkdir -p /mnt/target/etc/systemd/system/console-setup.service.d
cat > /mnt/target/etc/systemd/system/console-setup.service.d/fix-tmp.conf <<'CONSOLEFIX'
[Unit]
After=tmp.mount systemd-tmpfiles-setup.service
Wants=tmp.mount
[Service]
ExecStartPre=/bin/mkdir -p /tmp
CONSOLEFIX
2026-07-01 04:53:52 -04:00
# Raise /tmp tmpfs cap above systemd's 50%-of-RAM default — PyInstaller-based
# daemons (e.g. the Reticulum mesh daemon) self-extract to /tmp on every launch
# and leak their extraction dir on abnormal exit; combined with release-build
# staging dirs this can exhaust the default cap and starve app installs/mesh.
mkdir -p /mnt/target/etc/systemd/system/tmp.mount.d
cat > /mnt/target/etc/systemd/system/tmp.mount.d/override.conf <<'TMPSIZE'
[Mount]
Options=mode=1777,strictatime,nosuid,nodev,size=75%,nr_inodes=1m
TMPSIZE
2026-03-27 11:12:31 +00:00
# Auto-login on tty1 — no password prompt on console
mkdir -p /mnt/target/etc/systemd/system/getty@tty1.service.d
cat > /mnt/target/etc/systemd/system/getty@tty1.service.d/autologin.conf <<'AUTOLOGIN'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin archipelago --noclear %I $TERM
AUTOLOGIN
2026-03-26 20:58:34 +00:00
chroot /mnt/target systemctl enable archipelago-diagnostics.service 2>/dev/null || true
2026-02-01 05:42:05 +00:00
2026-03-27 17:17:18 +00:00
# Post-install smoke test — runs Phase 1 (install verification) only on first boot
# Does NOT run onboarding or create passwords — lets user do that via the UI
2026-03-27 16:19:33 +00:00
cat > /mnt/target/etc/systemd/system/archipelago-post-install-tests.service <<'PITSERVICE'
[Unit]
2026-03-27 17:17:18 +00:00
Description=Archipelago Install Verification (first boot)
2026-03-27 16:19:33 +00:00
After=archipelago.service archipelago-first-boot-containers.service nginx.service
Wants=archipelago.service nginx.service
ConditionPathExists=!/var/lib/archipelago/.post-install-tests-done
[Service]
Type=oneshot
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf http://127.0.0.1:5678/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
2026-03-27 17:17:18 +00:00
ExecStart=/bin/bash -c '/opt/archipelago/scripts/run-post-install-tests.sh --phase1-only 2>&1 | tee /var/log/archipelago-post-install-tests.log; touch /var/lib/archipelago/.post-install-tests-done'
2026-03-27 16:19:33 +00:00
RemainAfterExit=yes
StandardOutput=journal+console
StandardError=journal+console
2026-03-27 17:17:18 +00:00
TimeoutStartSec=120
2026-03-27 16:19:33 +00:00
[Install]
WantedBy=multi-user.target
PITSERVICE
chroot /mnt/target systemctl enable archipelago-post-install-tests.service 2>/dev/null || true
2026-03-26 20:54:32 +00:00
# Install first-boot diagnostic script — runs once after first boot and logs system state
cat > /mnt/target/opt/archipelago/scripts/first-boot-diag.sh <<'DIAGSCRIPT'
#!/bin/bash
LOG="/var/log/archipelago-first-boot-diag.log"
exec > "$LOG" 2>&1
echo "=== Archipelago First Boot Diagnostics ==="
echo "Date: $(date -u)"
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
echo "IP: $(hostname -I 2>/dev/null | awk '{print $1}')"
echo ""
echo "=== Build Info ==="
cat /opt/archipelago/build-info.txt 2>/dev/null || echo "No build-info.txt"
echo ""
echo "=== Services ==="
for svc in nginx archipelago archipelago-kiosk archipelago-load-images archipelago-first-boot-containers; do
STATUS=$(systemctl is-active "$svc" 2>/dev/null || echo "missing")
ENABLED=$(systemctl is-enabled "$svc" 2>/dev/null || echo "missing")
printf " %-45s active=%-10s enabled=%s\n" "$svc" "$STATUS" "$ENABLED"
done
echo ""
echo "=== Nginx Test ==="
nginx -t 2>&1
echo ""
echo "=== SSL Cert ==="
ls -la /etc/archipelago/ssl/ 2>/dev/null || echo " No SSL directory"
echo ""
echo "=== EFI Boot ==="
ls -la /boot/efi/EFI/BOOT/ 2>/dev/null || echo " No EFI/BOOT directory"
echo ""
echo "=== LUKS ==="
ls -la /dev/mapper/archipelago-data 2>/dev/null && echo " LUKS volume open" || echo " No LUKS volume"
cat /etc/crypttab 2>/dev/null
echo ""
echo "=== Podman ==="
podman --version 2>/dev/null || echo " podman not found"
podman ps -a --format "{{.Names}} {{.Status}}" 2>/dev/null | head -20
echo ""
echo "=== Kiosk ==="
systemctl status archipelago-kiosk --no-pager 2>&1 | head -10
echo ""
echo "=== Console Setup ==="
systemctl status console-setup --no-pager 2>&1 | head -5
cat /etc/default/keyboard 2>/dev/null || echo " No keyboard config"
echo ""
echo "=== Logind (Lid) ==="
cat /etc/systemd/logind.conf.d/lid-ignore.conf 2>/dev/null || echo " No lid config"
echo ""
echo "=== Disk ==="
df -h / /boot/efi /var/lib/archipelago 2>/dev/null
echo ""
echo "=== Network ==="
ip addr show | grep -E "inet |link/" | head -10
echo ""
echo "=== Journal Errors (last boot) ==="
journalctl -b -p err --no-pager 2>/dev/null | tail -30
echo ""
echo "=== Done ==="
DIAGSCRIPT
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-diag.sh
# Systemd oneshot service for first-boot diagnostics
cat > /mnt/target/etc/systemd/system/archipelago-diag.service <<'DIAGSVC'
[Unit]
Description=Archipelago First Boot Diagnostics
After=multi-user.target archipelago.service nginx.service
ConditionPathExists=!/var/log/archipelago-first-boot-diag.log
[Service]
Type=oneshot
ExecStartPre=/bin/sleep 30
ExecStart=/opt/archipelago/scripts/first-boot-diag.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
DIAGSVC
chroot /mnt/target systemctl enable archipelago-diag.service 2>/dev/null || true
# Write build info into the installed system
cat > /mnt/target/opt/archipelago/build-info.txt <<BUILDINFO
2026-03-31 23:43:32 +01:00
version=__BUILD_VERSION__
build=__BUILD_NUM__
commit=__GIT_SHORT__
2026-03-26 20:54:32 +00:00
date=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
type=unbundled
BUILDINFO
2026-03-28 11:31:48 +00:00
# Save install log BEFORE unmounting target
cp "$INSTALL_LOG" /mnt/target/var/log/archipelago-install.log 2>/dev/null || true
2026-02-01 05:42:05 +00:00
# Cleanup
sync
umount /mnt/target/run 2>/dev/null || true
umount /mnt/target/sys 2>/dev/null || true
umount /mnt/target/proc 2>/dev/null || true
umount /mnt/target/dev/pts 2>/dev/null || true
umount /mnt/target/dev 2>/dev/null || true
umount /mnt/target/boot/efi 2>/dev/null || true
2026-03-26 09:12:16 +00:00
umount /mnt/target/var/lib/archipelago 2>/dev/null || true
cryptsetup close archipelago-data 2>/dev/null || true
2026-02-01 05:42:05 +00:00
umount /mnt/target 2>/dev/null || true
2026-02-14 16:44:20 +00:00
echo ""
2026-03-29 15:43:57 +01:00
hrule
2026-02-14 16:44:20 +00:00
echo ""
2026-03-31 21:23:29 +01:00
# Celebration animation if TUI available
[ "${TUI_AVAILABLE:-}" = "1" ] && tui_complete
2026-03-28 23:58:42 +00:00
p "${ORANGE_BRIGHT} ✓ Installation Complete${NC}"
2026-02-14 16:44:20 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p "${ORANGE_DIM} After reboot, access from any device:${NC}"
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p "${ORANGE} http://<this machine's IP>${NC}"
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p "${WHITE} SSH ssh archipelago@<IP>${NC}"
p "${WHITE} Password archipelago${NC}"
p "${WHITE} Web Login password123${NC}"
2026-03-25 16:56:02 +00:00
echo ""
2026-03-28 11:31:48 +00:00
hrule
2026-03-25 16:56:02 +00:00
echo ""
2026-03-28 13:14:33 +00:00
# Suppress kernel messages on console (SquashFS errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
2026-03-26 20:54:32 +00:00
2026-03-29 22:42:09 +01:00
# Show completion message, unmount USB, then reboot
# All done inline — no separate script needed (avoids /bin/bash dependency on squashfs)
2026-03-28 23:41:40 +00:00
echo ""
2026-03-31 21:23:29 +01:00
if [ "${TUI_AVAILABLE:-}" = "1" ]; then
tui_flash_remove_usb
else
p "${ORANGE}>>> REMOVE THE USB DRIVE NOW <<<${NC}"
fi
2026-03-29 22:42:09 +01:00
echo ""
p "${ORANGE_DIM}Press Enter to reboot (or wait 30 seconds)${NC}"
2026-03-28 23:41:40 +00:00
2026-03-29 22:42:09 +01:00
# Suppress kernel messages (squashfs errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
# Lazy-unmount live filesystem
2026-03-26 18:30:13 +00:00
exec 2>/dev/null
2026-03-28 13:14:33 +00:00
umount -l /run/live/medium 2>/dev/null || true
umount -l /lib/live/mount/medium 2>/dev/null || true
umount -l /run/archiso 2>/dev/null || true
umount -l /cdrom 2>/dev/null || true
BOOT_DEV=$(findmnt -n -o SOURCE /run/live/medium 2>/dev/null || findmnt -n -o SOURCE /cdrom 2>/dev/null || echo "")
2026-03-25 16:56:02 +00:00
if [ -n "$BOOT_DEV" ]; then
BOOT_DISK=$(lsblk -no PKNAME "$BOOT_DEV" 2>/dev/null | head -1)
2026-03-28 13:14:33 +00:00
[ -n "$BOOT_DISK" ] && eject "/dev/$BOOT_DISK" 2>/dev/null || true
2026-03-25 16:56:02 +00:00
fi
2026-03-28 13:06:34 +00:00
exec 2>&1
2026-03-25 16:56:02 +00:00
2026-03-29 22:42:09 +01:00
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
echo ""
p "${ORANGE_DIM}Rebooting...${NC}"
sleep 1
# Force reboot — multiple methods, first one that works wins
echo b > /proc/sysrq-trigger 2>/dev/null || \
/sbin/reboot -f 2>/dev/null || \
/usr/sbin/reboot -f 2>/dev/null || \
kill -9 1 2>/dev/null
2026-02-01 05:42:05 +00:00
INSTALLER_SCRIPT
2026-03-31 23:43:32 +01:00
# Inject build version into auto-install.sh (heredoc is single-quoted, can't expand variables)
sed -i "s|__BUILD_VERSION__| ${ BUILD_VERSION } |g" " $ARCH_DIR /auto-install.sh"
sed -i "s|__BUILD_NUM__| ${ BUILD_NUM } |g" " $ARCH_DIR /auto-install.sh"
sed -i "s|__GIT_SHORT__| ${ GIT_SHORT } |g" " $ARCH_DIR /auto-install.sh"
2026-03-10 23:29:05 +00:00
# For unbundled builds, patch the completion message to reflect no pre-loaded apps
if [ " $UNBUNDLED " = "1" ] ; then
sed -i 's/Pre-loaded apps (ready to start via Web UI):/Install apps from the Marketplace (internet required):/' " $ARCH_DIR /auto-install.sh"
sed -i 's/• Bitcoin Knots • LND • Home Assistant/ Open the Web UI → Marketplace → Install any app/' " $ARCH_DIR /auto-install.sh"
sed -i 's/• BTCPay Server • Mempool • Nostr Relays/ All apps download automatically via Podman /' " $ARCH_DIR /auto-install.sh"
fi
2026-02-01 05:42:05 +00:00
chmod +x " $ARCH_DIR /auto-install.sh"
# =============================================================================
2026-03-27 18:19:49 +00:00
# STEP 5: Configure boot loader and ISO structure
2026-02-01 05:42:05 +00:00
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 5: Configuring boot loaders..."
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
# The installer squashfs (from Step 2) already contains:
# - systemd service for auto-starting the installer
# - auto-login on tty1
# - custom initramfs hook for mounting boot media at /run/archiso
# - all partitioning tools (parted, mkfs.*, cryptsetup)
#
# Step 5 just needs to create the GRUB and ISOLINUX boot configs.
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
# Create GRUB configuration
echo " Writing GRUB config..."
cat > " $INSTALLER_ISO /boot/grub/grub.cfg" <<'GRUBCFG'
2026-03-27 23:33:31 +00:00
insmod part_gpt
insmod part_msdos
insmod fat
insmod iso9660
insmod all_video
2026-03-28 23:58:42 +00:00
insmod search
insmod search_label
insmod search_fs_file
2026-03-27 23:33:31 +00:00
2026-03-28 23:58:42 +00:00
# Find boot media — try label first, then known file fallback
2026-03-27 23:33:31 +00:00
search --no-floppy --set=root --label ARCHIPELAGO
2026-03-28 23:58:42 +00:00
if [ -z "$root" ]; then
search --no-floppy --set=root --file /archipelago/auto-install.sh
fi
2026-03-27 23:33:31 +00:00
2026-03-27 18:19:49 +00:00
set timeout=5
set default=0
2026-02-01 05:42:05 +00:00
2026-04-10 03:10:49 -04:00
# Serial console for QEMU/headless testing
insmod serial
serial --unit=0 --speed=115200
terminal_input serial console
terminal_output serial console
2026-04-02 20:28:53 +01:00
# Load font for graphical menu — fallback to text mode on hardware without gfxterm
2026-03-27 23:33:31 +00:00
if loadfont ($root)/boot/grub/font.pf2; then
2026-03-30 16:35:06 +01:00
set gfxmode=auto
2026-04-02 20:28:53 +01:00
set gfxpayload=keep
2026-03-27 18:19:49 +00:00
insmod gfxterm
insmod png
2026-04-10 03:10:49 -04:00
terminal_output gfxterm serial
2026-04-02 20:28:53 +01:00
else
2026-04-10 03:10:49 -04:00
terminal_output console serial
2026-02-01 05:42:05 +00:00
fi
2026-03-27 18:19:49 +00:00
# Archipelago GRUB theme
2026-03-27 23:33:31 +00:00
if [ -f ($root)/boot/grub/themes/archipelago/theme.txt ]; then
loadfont ($root)/boot/grub/themes/archipelago/dejavu_12.pf2
loadfont ($root)/boot/grub/themes/archipelago/dejavu_14.pf2
loadfont ($root)/boot/grub/themes/archipelago/dejavu_16.pf2
loadfont ($root)/boot/grub/themes/archipelago/dejavu_24.pf2
set theme=($root)/boot/grub/themes/archipelago/theme.txt
2026-02-01 05:42:05 +00:00
else
2026-03-27 18:19:49 +00:00
set menu_color_normal=light-gray/black
set menu_color_highlight=white/dark-gray
2026-02-01 05:42:05 +00:00
fi
2026-03-27 18:19:49 +00:00
menuentry "Install Archipelago" --hotkey=i {
2026-04-10 03:10:49 -04:00
linux ($root)/live/vmlinuz boot=live components quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force console=ttyS0,115200 console=tty0
2026-03-27 23:33:31 +00:00
initrd ($root)/live/initrd.img
2026-03-27 18:19:49 +00:00
}
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
menuentry "Install Archipelago (verbose)" --hotkey=v {
2026-04-08 13:15:09 +02:00
linux ($root)/live/vmlinuz boot=live components loglevel=4 console=ttyS0,115200 console=tty0 acpi=force
2026-03-27 23:33:31 +00:00
initrd ($root)/live/initrd.img
2026-03-27 18:19:49 +00:00
}
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
menuentry "Boot from local disk" --hotkey=b {
set root=(hd0)
chainloader +1
}
GRUBCFG
2026-02-01 05:42:05 +00:00
2026-03-27 23:33:31 +00:00
# Copy grub.cfg to EFI/BOOT on ISO filesystem AND into the FAT EFI image
# The embedded grub bootstrap does configfile "${cmdpath}/grub.cfg"
cp " $INSTALLER_ISO /boot/grub/grub.cfg" " $INSTALLER_ISO /EFI/BOOT/grub.cfg"
if [ -f " $WORK_DIR /efi.img" ] ; then
mcopy -oi " $WORK_DIR /efi.img" " $INSTALLER_ISO /boot/grub/grub.cfg" ::/EFI/BOOT/grub.cfg 2>/dev/null || \
echo " WARNING: Could not copy grub.cfg into efi.img (mtools required)"
fi
2026-03-27 18:19:49 +00:00
# Create ISOLINUX configuration (legacy BIOS boot)
echo " Writing ISOLINUX config..."
2026-03-28 03:15:47 +00:00
# Copy background image for ISOLINUX graphical menu
ISOLINUX_BG = " $SCRIPT_DIR /branding/grub-theme/background.png"
if [ -f " $ISOLINUX_BG " ] ; then
cp " $ISOLINUX_BG " " $INSTALLER_ISO /isolinux/splash.png"
fi
# Copy vesamenu.c32 for graphical menu (with background support)
if [ -f " $WORK_DIR /vesamenu.c32" ] ; then
cp " $WORK_DIR /vesamenu.c32" " $INSTALLER_ISO /isolinux/vesamenu.c32"
fi
2026-03-27 18:19:49 +00:00
cat > " $INSTALLER_ISO /isolinux/isolinux.cfg" <<'ISOCFG'
2026-03-28 03:15:47 +00:00
UI vesamenu.c32
2026-03-27 18:19:49 +00:00
PROMPT 0
2026-03-28 03:15:47 +00:00
TIMEOUT 0
2026-03-27 18:19:49 +00:00
2026-03-31 17:35:34 +01:00
MENU TITLE
2026-03-28 03:15:47 +00:00
MENU BACKGROUND splash.png
2026-03-28 13:14:33 +00:00
MENU RESOLUTION 1024 768
2026-03-31 17:35:34 +01:00
MENU VSHIFT 20
MENU HSHIFT 6
MENU WIDTH 68
MENU MARGIN 2
2026-03-28 11:31:48 +00:00
MENU ROWS 5
2026-03-28 13:06:34 +00:00
MENU TABMSG press tab to edit | archipelago.sh
2026-03-28 03:15:47 +00:00
MENU COLOR screen 37;40 #00000000 #00000000 none
2026-03-28 00:35:34 +00:00
MENU COLOR border 30;40 #00000000 #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR title 1;37;40 #80888888 #00000000 none
MENU COLOR sel 7;37;40 #ffffffff #c0181818 std
MENU COLOR unsel 37;40 #ffaaaaaa #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR hotkey 1;37;40 #fffb923c #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR hotsel 1;37;40 #fffb923c #c0181818 std
MENU COLOR timeout_msg 37;40 #ff555555 #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR timeout 1;37;40 #fffb923c #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR tabmsg 37;40 #ff444444 #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR cmdmark 37;40 #00000000 #00000000 none
MENU COLOR cmdline 37;40 #00000000 #00000000 none
2026-03-27 18:19:49 +00:00
DEFAULT install
LABEL install
MENU LABEL Install Archipelago
KERNEL /live/vmlinuz
2026-04-08 13:15:09 +02:00
APPEND initrd=/live/initrd.img boot=live components quiet loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force
2026-03-27 18:19:49 +00:00
MENU DEFAULT
LABEL install-verbose
2026-03-28 11:31:48 +00:00
MENU LABEL Install (verbose output)
2026-03-27 18:19:49 +00:00
KERNEL /live/vmlinuz
2026-04-08 13:15:09 +02:00
APPEND initrd=/live/initrd.img boot=live components loglevel=4 acpi=force
2026-03-27 18:19:49 +00:00
LABEL local
MENU LABEL Boot from local disk
LOCALBOOT 0x80
ISOCFG
echo " Step 5 complete (GRUB + ISOLINUX configured)"
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 6: Create final ISO
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 6: Creating bootable ISO..."
2026-02-01 05:42:05 +00:00
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-05-06 09:23:57 -04:00
OUTPUT_ISO = " $OUTPUT_DIR /archipelago-installer- ${ BUILD_VERSION } -unbundled- ${ ARCH } .iso"
2026-03-10 23:29:05 +00:00
else
2026-05-06 09:23:57 -04:00
OUTPUT_ISO = " $OUTPUT_DIR /archipelago-installer- ${ BUILD_VERSION } - ${ ARCH } .iso"
2026-03-10 23:29:05 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-03-27 22:26:56 +00:00
# Use the proven MBR code for hybrid USB boot
# The ISOLINUX package's isohdpfx.bin (33 ed) doesn't boot on all hardware.
# We ship the Debian Live MBR (45 52) which is known to work with Balena Etcher.
ISOHDPFX = " $SCRIPT_DIR /branding/isohdpfx.bin"
if [ ! -f " $ISOHDPFX " ] ; then
ISOHDPFX = " $WORK_DIR /isohdpfx.bin"
fi
2026-03-27 18:19:49 +00:00
if [ ! -f " $ISOHDPFX " ] ; then
# Fallback to system-installed copy
2026-03-09 17:09:59 +00:00
for path in \
"/usr/lib/ISOLINUX/isohdpfx.bin" \
"/usr/share/syslinux/isohdpfx.bin" \
"/usr/local/share/syslinux/isohdpfx.bin" ; do
if [ -f " $path " ] ; then
ISOHDPFX = " $path "
2026-03-27 18:19:49 +00:00
echo " Using system isohdpfx.bin: $path "
2026-03-09 17:09:59 +00:00
break
fi
done
fi
2026-03-28 00:35:34 +00:00
# EFI boot image — embedded inside ISO (same approach as the working main ISO)
# The efi.img must be copied into the ISO directory in Step 2 artifact placement
EFI_IMG = " $INSTALLER_ISO /boot/grub/efi.img"
2026-02-01 05:42:05 +00:00
2026-03-28 00:35:34 +00:00
if [ ! -f " $EFI_IMG " ] ; then
2026-03-27 18:19:49 +00:00
echo " WARNING: No EFI boot image — ISO will only support Legacy BIOS boot"
2026-03-09 17:09:59 +00:00
xorriso -as mkisofs -o " $OUTPUT_ISO " \
-volid "ARCHIPELAGO" \
-iso-level 3 \
2026-03-28 00:35:34 +00:00
-J -joliet-long -R \
2026-03-09 17:09:59 +00:00
-isohybrid-mbr " $ISOHDPFX " \
2026-03-28 00:35:34 +00:00
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-partition_offset 16 \
2026-03-09 17:09:59 +00:00
" $INSTALLER_ISO "
else
2026-03-29 12:44:13 +01:00
# UEFI fix: append efi.img as a real EFI System Partition (ESP) in GPT
# instead of embedding it as "basic data". Strict UEFI firmware requires
# the correct ESP type GUID (C12A7328-F81F-11D2-BA4B-00A0C93EC93B).
# This is the same approach used by Arch Linux ISOs.
2026-03-09 17:09:59 +00:00
xorriso -as mkisofs -o " $OUTPUT_ISO " \
-volid "ARCHIPELAGO" \
-iso-level 3 \
2026-03-28 00:35:34 +00:00
-J -joliet-long -R \
2026-03-09 17:09:59 +00:00
-isohybrid-mbr " $ISOHDPFX " \
2026-03-28 00:35:34 +00:00
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
2026-03-09 17:09:59 +00:00
-eltorito-alt-boot \
2026-03-29 12:44:13 +01:00
-e --interval:appended_partition_2:all:: \
2026-03-28 00:35:34 +00:00
-no-emul-boot \
2026-03-29 12:44:13 +01:00
-appended_part_as_gpt \
-append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B " $WORK_DIR /efi.img" \
2026-03-28 00:35:34 +00:00
-partition_offset 16 \
2026-03-09 17:09:59 +00:00
" $INSTALLER_ISO "
fi
2026-02-01 05:42:05 +00:00
2026-07-13 13:41:16 +01:00
# Checksums: emit next to the ISO so releases can be verified and the
# release ceremony can sign them (publish step signs; the build host
# never holds the release key)
(
cd " $( dirname " $OUTPUT_ISO " ) " || exit 0
ISO_NAME = $( basename " $OUTPUT_ISO " )
sha256sum " $ISO_NAME " > " $ISO_NAME .sha256"
echo " Checksum: $( cat " $ISO_NAME .sha256" ) "
)
2026-02-01 05:42:05 +00:00
echo ""
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-03-27 18:19:49 +00:00
echo "UNBUNDLED AUTO-INSTALLER ISO CREATED"
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Output: $OUTPUT_ISO "
echo " Size: $( du -h " $OUTPUT_ISO " | cut -f1) "
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Lightweight installer -- apps downloaded on-demand from Marketplace"
2026-03-10 23:29:05 +00:00
else
2026-03-27 18:19:49 +00:00
echo "AUTO-INSTALLER ISO CREATED"
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Output: $OUTPUT_ISO "
echo " Size: $( du -h " $OUTPUT_ISO " | cut -f1) "
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Full installer with pre-bundled container apps"
2026-03-10 23:29:05 +00:00
fi
2026-02-01 05:42:05 +00:00
echo "To create USB:"
echo " 1. Flash with: sudo dd if= $OUTPUT_ISO of=/dev/rdiskX bs=4m"
echo " Or use Balena Etcher"
echo " 2. Boot from USB"
echo " 3. Press Enter to install"
echo " 4. Remove USB and reboot"
echo ""