archy/image-recipe/build-auto-installer-iso.sh
Dorian e9903e7b4b fix: UEFI boot fallback — search by file when label fails
The embedded GRUB EFI config only searched by volume label ARCHIPELAGO.
Some UEFI firmware presents USB devices differently, causing the search
to fail and GRUB to stall.

Added fallbacks:
1. search --file /archipelago/auto-install.sh (known ISO file)
2. Fall back to $cmdpath (EFI partition itself)
3. Use configfile before normal for explicit config loading
4. Added search_fs_file module to grub-mkstandalone

Also added same fallback to the main ISO grub.cfg.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:58:42 +00:00

2825 lines
110 KiB
Bash
Executable File

#!/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.
#
# 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
#
# 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
#
# 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)
# installer-env.sh — Step 2: Build minimal installer via debootstrap (~80 lines)
# 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.
# ---
#
set -e
# Source pinned image versions (single source of truth)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
[ -f "$SCRIPT_DIR/../scripts/image-versions.sh" ] && . "$SCRIPT_DIR/../scripts/image-versions.sh"
# Configuration
DEV_SERVER="${DEV_SERVER:-archipelago@192.168.1.228}"
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}"
UNBUNDLED="${UNBUNDLED:-0}"
ARCH="${ARCH:-x86_64}"
# 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"
GRUB_PC_PKG="grub-pc-bin"
GRUB_TARGET="x86_64-efi"
GRUB_BIOS_TARGET="i386-pc"
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"
GRUB_PC_PKG=""
GRUB_TARGET="arm64-efi"
GRUB_BIOS_TARGET=""
CONTAINER_PLATFORM="linux/arm64"
LIB_DIR="aarch64-linux-gnu"
;;
*)
echo "❌ Unsupported architecture: $ARCH (use x86_64 or arm64)"
exit 1
;;
esac
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"
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
echo ""
if [ "$BUILD_FROM_SOURCE" = "1" ]; then
echo "📦 Mode: Building from SOURCE CODE"
elif [ "$UNBUNDLED" = "1" ]; then
echo "📦 Mode: UNBUNDLED (apps downloaded on-demand from Marketplace)"
echo " Server: $DEV_SERVER (backend + web UI only)"
else
echo "📦 Mode: Capturing LIVE SERVER state"
echo " Server: $DEV_SERVER"
fi
echo "🏗️ Architecture: $ARCH ($DEB_ARCH)"
echo ""
# Check for required tools
check_tools() {
local missing=""
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
# 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
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"
fi
if [ -n "$missing" ]; then
echo "Missing required tools:$missing"
if [ "$can_install" = true ]; then
echo " Auto-installing missing dependencies..."
apt-get update -qq
if [[ "$missing" == *"xorriso"* ]]; then
apt-get install -y xorriso
fi
if [[ "$missing" == *"mksquashfs"* ]]; then
apt-get install -y squashfs-tools
fi
if [[ "$missing" == *"isolinux"* ]]; then
apt-get install -y isolinux syslinux-common
fi
if [[ "$missing" == *"docker-or-podman"* ]]; then
echo " Installing podman..."
apt-get install -y podman
CONTAINER_CMD="podman"
fi
echo " Dependencies installed successfully!"
else
echo " Install with: sudo apt install xorriso squashfs-tools isolinux podman"
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"
exit 1
fi
echo "Using container runtime: $CONTAINER_CMD"
# 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
if ! $CONTAINER_CMD run --rm debian:bookworm true 2>/dev/null; then
echo " Root podman D-Bus issue detected, using cgroupfs manager"
CONTAINER_CMD="podman --cgroup-manager=cgroupfs"
fi
fi
# Ensure insecure registry config for Archipelago app registry (HTTP)
if [ "$CONTAINER_CMD" = "podman" ]; then
mkdir -p /etc/containers/registries.conf.d
cat > /etc/containers/registries.conf.d/archipelago.conf <<'REGCONF'
[[registry]]
location = "80.71.235.15:3000"
insecure = true
REGCONF
fi
}
check_tools
mkdir -p "$WORK_DIR"
mkdir -p "$OUTPUT_DIR"
# =============================================================================
# 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
cat > "$WORK_DIR/Dockerfile.rootfs" <<DOCKERFILE
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive
# 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 && \
echo "console-setup console-setup/codeset47 select Guess optimal character set" | debconf-set-selections
# Enable non-free-firmware repo — replace DEB822 sources with traditional format
# (DEB822 sed was silently failing, so just overwrite with known-good sources.list)
RUN echo "deb http://deb.debian.org/debian bookworm main non-free-firmware" > /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian bookworm-updates main non-free-firmware" >> /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian-security bookworm-security main non-free-firmware" >> /etc/apt/sources.list && \
rm -f /etc/apt/sources.list.d/debian.sources
# Install all packages we need including nginx, podman, tor, and openssl (for self-signed certs)
RUN apt-get update && apt-get install -y --no-install-recommends \
${LINUX_IMAGE_PKG} \
${GRUB_EFI_PKG} \
${GRUB_EFI_SIGNED_PKG} \
${GRUB_PC_PKG} \
systemd \
systemd-sysv \
dbus \
sudo \
network-manager \
openssh-server \
nginx \
podman \
uidmap \
slirp4netns \
fuse-overlayfs \
tor \
curl \
git \
vim-tiny \
ca-certificates \
openssl \
chrony \
locales \
console-setup \
keyboard-configuration \
cryptsetup \
firmware-realtek \
firmware-iwlwifi \
firmware-misc-nonfree \
intel-microcode \
amd64-microcode \
xorg \
chromium \
unclutter \
fonts-liberation \
xfonts-base \
plymouth \
plymouth-themes \
zstd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 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
# Install Tailscale from official repo
RUN curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list && \
apt-get update && apt-get install -y --no-install-recommends tailscale && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Configure locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
# Create archipelago user with password "archipelago"
RUN useradd -m -s /bin/bash -G sudo archipelago && \
echo "archipelago:archipelago" | chpasswd && \
echo "root:archipelago" | chpasswd && \
echo "archipelago ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/archipelago
# Verify password hash was set (not locked)
RUN grep -q "^archipelago:\$" /etc/shadow && echo "Password set OK" || echo "WARNING: password may not be set"
# 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
# Install nginx snippets (PWA config, HTTPS app proxies)
COPY snippets/ /etc/nginx/snippets/
# Generate self-signed SSL certificate for HTTPS (PWA install requires secure context)
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 \
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" && \
chmod 600 /etc/archipelago/ssl/archipelago.key
# Create archipelago systemd service
COPY archipelago.service /etc/systemd/system/archipelago.service
COPY archipelago-update.service /etc/systemd/system/archipelago-update.service
COPY archipelago-update.timer /etc/systemd/system/archipelago-update.timer
COPY archipelago-doctor.service /etc/systemd/system/archipelago-doctor.service
COPY archipelago-doctor.timer /etc/systemd/system/archipelago-doctor.timer
COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.service
COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer
# Copy container doctor + reconcile scripts (referenced by the services above)
RUN mkdir -p /home/archipelago/archy/scripts
COPY container-doctor.sh /home/archipelago/archy/scripts/container-doctor.sh
COPY reconcile-containers.sh /home/archipelago/archy/scripts/reconcile-containers.sh
RUN chmod +x /home/archipelago/archy/scripts/*.sh && \
chown -R archipelago:archipelago /home/archipelago/archy
# Enable services
RUN systemctl enable NetworkManager || true && \
systemctl enable ssh || true && \
systemctl enable nginx || true && \
systemctl enable archipelago || true && \
systemctl enable tor || true && \
systemctl enable tailscaled || true && \
systemctl enable chrony || true && \
systemctl enable archipelago-update.timer || true && \
systemctl enable archipelago-doctor.timer || true && \
systemctl enable archipelago-reconcile.timer || true
# Remove policy-rc.d so services can start on first boot
RUN rm -f /usr/sbin/policy-rc.d
# Create directories (including Cloud storage for FileBrowser)
RUN mkdir -p /var/lib/archipelago/{data,config,containers} && \
mkdir -p /etc/archipelago && \
mkdir -p /opt/archipelago/{bin,scripts,web-ui} && \
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads} && \
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
# Clean up
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
DOCKERFILE
# Copy nginx snippets for HTTPS (PWA, app proxies)
if [ -d "$SCRIPT_DIR/configs/snippets" ]; then
mkdir -p "$WORK_DIR/snippets"
cp "$SCRIPT_DIR/configs/snippets/"*.conf "$WORK_DIR/snippets/" 2>/dev/null || true
echo " Using nginx snippets from configs/snippets/"
else
mkdir -p "$WORK_DIR/snippets"
echo " ⚠ No nginx snippets found, HTTPS features may not work"
fi
# Use nginx config from configs/ (includes app proxies for Nextcloud, Vaultwarden, etc.)
if [ -f "$SCRIPT_DIR/configs/nginx-archipelago.conf" ]; then
cp "$SCRIPT_DIR/configs/nginx-archipelago.conf" "$WORK_DIR/nginx-archipelago.conf"
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'
server {
listen 80;
server_name _;
root /opt/archipelago/web-ui;
index index.html;
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; }
}
NGINXCONF
fi
# Copy udev rule for mesh radio stable naming
if [ -f "$SCRIPT_DIR/configs/99-mesh-radio.rules" ]; then
cp "$SCRIPT_DIR/configs/99-mesh-radio.rules" "$WORK_DIR/99-mesh-radio.rules"
echo " Using 99-mesh-radio.rules from configs/"
fi
# Copy update service and timer
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"
echo " Using archipelago-update.service + timer from configs/"
fi
# Copy container doctor and reconciliation timers + scripts
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"
cp "$SCRIPT_DIR/configs/archipelago-reconcile.service" "$WORK_DIR/archipelago-reconcile.service"
cp "$SCRIPT_DIR/configs/archipelago-reconcile.timer" "$WORK_DIR/archipelago-reconcile.timer"
# Copy the actual scripts the services reference
for s in container-doctor.sh reconcile-containers.sh; do
if [ -f "$SCRIPT_DIR/../scripts/$s" ]; then
cp "$SCRIPT_DIR/../scripts/$s" "$WORK_DIR/$s"
fi
done
echo " Using container doctor + reconcile timers from configs/"
fi
# Use archipelago.service from configs/ (User=root for Podman container access)
if [ -f "$SCRIPT_DIR/configs/archipelago.service" ]; then
cp "$SCRIPT_DIR/configs/archipelago.service" "$WORK_DIR/archipelago.service"
echo " Using archipelago.service from configs/"
else
cat > "$WORK_DIR/archipelago.service" <<'SYSTEMDSERVICE'
[Unit]
Description=Archipelago Backend
After=network-online.target archipelago-setup-tor.service
Wants=network-online.target
[Service]
Type=simple
User=archipelago
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
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'
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
ProtectHome=no
[Install]
WantedBy=multi-user.target
SYSTEMDSERVICE
fi
echo " Building $CONTAINER_CMD image (this may take a few minutes)..."
$CONTAINER_CMD build --no-cache --platform $CONTAINER_PLATFORM -t archipelago-rootfs -f "$WORK_DIR/Dockerfile.rootfs" "$WORK_DIR"
echo " Exporting filesystem..."
$CONTAINER_CMD create --platform $CONTAINER_PLATFORM --name archipelago-rootfs-tmp archipelago-rootfs
$CONTAINER_CMD export archipelago-rootfs-tmp > "$ROOTFS_TAR"
$CONTAINER_CMD rm archipelago-rootfs-tmp
echo "✅ Root filesystem created: $(du -h "$ROOTFS_TAR" | cut -f1)"
else
echo "✅ Using cached root filesystem: $(du -h "$ROOTFS_TAR" | cut -f1)"
fi
# =============================================================================
# STEP 2: Build minimal installer environment (replaces Debian Live)
# =============================================================================
echo ""
echo "Step 2: Building minimal installer environment via debootstrap..."
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"
# Build the installer filesystem inside a container
# This creates: vmlinuz, initrd.img, filesystem.squashfs
echo " Building installer rootfs with debootstrap (this takes a few minutes)..."
$CONTAINER_CMD run --rm --privileged --platform $CONTAINER_PLATFORM \
-v "$WORK_DIR:/output" \
-e DEB_ARCH="$DEB_ARCH" \
-e LIB_DIR="$LIB_DIR" \
debian:bookworm bash -c '
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..."
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,\
pciutils,usbutils,less,nano \
bookworm /installer http://deb.debian.org/debian
# 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.
echo " [container] Installing live-boot for squashfs root support..."
cp /etc/resolv.conf /installer/etc/resolv.conf 2>/dev/null || true
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
chroot /installer apt-get update -qq
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
# Verify live-boot hooks are in place (scripts/live is a FILE not a directory)
if [ -e /installer/usr/share/initramfs-tools/scripts/live ]; then
echo " [container] live-boot initramfs hooks: OK"
else
echo " [container] FATAL: live-boot hooks not found after install!"
ls -la /installer/usr/share/initramfs-tools/scripts/ 2>/dev/null
exit 1
fi
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
# 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
cat > /installer/etc/profile.d/z99-archipelago-installer.sh <<PROFILE
#!/bin/bash
# Auto-start Archipelago installer on login — only run once
if [ -n "\$INSTALLER_STARTED" ]; then
return 0 2>/dev/null || exit 0
fi
export INSTALLER_STARTED=1
sleep 1
clear
echo ""
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"
echo ""
BOOT_MEDIA=""
for dev in /run/live/medium /lib/live/mount/medium /run/archiso /cdrom /media/cdrom /mnt/iso; do
if [ -f "\$dev/archipelago/auto-install.sh" ]; then
BOOT_MEDIA="\$dev"
break
fi
done
# 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"
echo ""
echo -e " Press Enter to install | \033[1;37mCtrl+C\033[0m for shell"
read -s
bash "\$BOOT_MEDIA/archipelago/auto-install.sh"
else
echo -e " \033[37mInstaller not found on boot media.\033[0m"
echo ""
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 ""
fi
PROFILE
chmod +x /installer/etc/profile.d/z99-archipelago-installer.sh
# 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
fi
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
# 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
chroot /installer update-initramfs -c -k $KVER
cp /installer/boot/initrd.img-$KVER /output/initrd.img
# Cleanup mounts
umount /installer/dev 2>/dev/null || true
umount /installer/sys 2>/dev/null || true
umount /installer/proc 2>/dev/null || true
# Create squashfs
echo " [container] Creating installer squashfs..."
mksquashfs /installer /output/filesystem.squashfs -comp xz -Xbcj x86 -noappend -quiet
# Build GRUB EFI image with embedded bootstrap config (grub-mkstandalone)
echo " [container] Building GRUB EFI image..."
cat > /tmp/grub-embed.cfg <<GRUBEMBED
insmod part_gpt
insmod part_msdos
insmod fat
insmod iso9660
insmod search
insmod search_label
insmod search_fs_file
insmod normal
insmod linux
insmod all_video
# Try label first (standard path)
search --no-floppy --set=root --label ARCHIPELAGO
# 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
set prefix=(\$root)/boot/grub
configfile (\$root)/boot/grub/grub.cfg
# If configfile fails, try normal
normal
GRUBEMBED
grub-mkstandalone -O x86_64-efi \
--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" \
--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
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
cp /usr/lib/syslinux/modules/bios/vesamenu.c32 /output/vesamenu.c32 2>/dev/null || true
cp /usr/lib/syslinux/modules/bios/libutil.c32 /output/libutil.c32 2>/dev/null || true
cp /usr/lib/syslinux/modules/bios/libcom32.c32 /output/libcom32.c32 2>/dev/null || true
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
grub-mkfont -s 12 -o /output/grub-fonts/dejavu_12.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
grub-mkfont -s 14 -o /output/grub-fonts/dejavu_14.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
grub-mkfont -s 16 -o /output/grub-fonts/dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
grub-mkfont -s 24 -o /output/grub-fonts/dejavu_24.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf
echo " [container] Done!"
'
# 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"
exit 1
fi
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"
cp "$WORK_DIR/efi.img" "$INSTALLER_ISO/boot/grub/efi.img"
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
cp "$WORK_DIR/vesamenu.c32" "$INSTALLER_ISO/isolinux/vesamenu.c32" 2>/dev/null || true
cp "$WORK_DIR/libutil.c32" "$INSTALLER_ISO/isolinux/libutil.c32" 2>/dev/null || true
cp "$WORK_DIR/libcom32.c32" "$INSTALLER_ISO/isolinux/libcom32.c32" 2>/dev/null || true
# 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"
fi
# 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
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)"
# =============================================================================
# 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"
# Copy the pre-built rootfs
echo " Including root filesystem..."
cp "$ROOTFS_TAR" "$ARCH_DIR/rootfs.tar"
# 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
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
BACKEND_CAPTURED=0
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
# Direct copy from ARCHIPELAGO_BIN env, local install, or remote
BIN="${ARCHIPELAGO_BIN:-/usr/local/bin/archipelago}"
if [ -f "$BIN" ]; then
cp "$BIN" "$ARCH_DIR/bin/archipelago"
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
if scp "$DEV_SERVER:/usr/local/bin/archipelago" "$ARCH_DIR/bin/archipelago" 2>/dev/null; then
chmod +x "$ARCH_DIR/bin/archipelago"
echo " ✅ Backend captured from remote server ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
BACKEND_CAPTURED=1
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'
FROM rust:1.93-bookworm as builder
WORKDIR /build
COPY core ./core
RUN cd core && cargo build --release --bin archipelago
BACKENDFILE
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t archipelago-backend -f "$BACKEND_DOCKERFILE" "$SCRIPT_DIR/.." 2>&1 | tail -20; then
echo " Extracting backend binary..."
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform $CONTAINER_PLATFORM archipelago-backend)
$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
fi
fi
# Capture web UI from live server
if [ "$BUILD_FROM_SOURCE" = "1" ]; then
echo " Building web UI from source..."
else
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
# 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
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
fi
fi
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"
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
# Copy app manifests
if [ -d "$SCRIPT_DIR/../apps" ]; then
echo " Including app manifests..."
cp -r "$SCRIPT_DIR/../apps" "$ARCH_DIR/"
fi
# 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
# =============================================================================
# STEP 3b: Bundle container images for offline installation
# =============================================================================
echo ""
if [ "$UNBUNDLED" = "1" ]; then
echo "📦 Step 3b: Bundling core containers only (UNBUNDLED mode)"
echo " Optional apps will be downloaded on-demand from the Marketplace after install."
IMAGES_DIR="$ARCH_DIR/container-images"
mkdir -p "$IMAGES_DIR"
# FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it
CORE_IMAGE="${FILEBROWSER_IMAGE}"
CORE_FILE="filebrowser.tar"
if [ -f "$IMAGES_DIR/$CORE_FILE" ]; then
echo " ✅ Using cached: $CORE_FILE"
else
echo " Pulling $CORE_IMAGE ($CONTAINER_PLATFORM)..."
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$CORE_IMAGE"; then
$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
else
echo "📦 Step 3b: Bundling container images for offline use..."
IMAGES_DIR="$ARCH_DIR/container-images"
mkdir -p "$IMAGES_DIR"
# 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)..."
# Patterns match against `podman images` repository names (not container names)
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 grafana uptime-kuma jellyfin vaultwarden searxng mariadb valkey nginx-alpine portainer photoprism nextcloud nginx-proxy-manager onlyoffice adguard"
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
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
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.
# Alpha: core Bitcoin/Lightning stack + essential apps. Others pulled on-demand from Marketplace.
CONTAINER_IMAGES="
${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
${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
"
# Pull and save each image (force target arch) only if not already present
echo "$CONTAINER_IMAGES" | while read -r image filename; do
[ -z "$image" ] && continue
tarpath="$IMAGES_DIR/$filename"
if [ -f "$tarpath" ]; then
echo " ✅ Using cached: $filename"
else
echo " Pulling $image ($CONTAINER_PLATFORM)..."
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$image"; then
echo " Saving $filename..."
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
else
echo " ⚠️ Failed to pull $image - skipping"
fi
fi
done
fi # end UNBUNDLED check
# 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
# Ensure archy-net exists for mempool stack (db, api, frontend)
podman network create archy-net 2>/dev/null || true
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/"
# Tor setup: copy torrc and create first-boot setup script
mkdir -p "$ARCH_DIR/scripts/tor"
if [ -f "$SCRIPT_DIR/../scripts/tor/torrc.template" ]; then
cp "$SCRIPT_DIR/../scripts/tor/torrc.template" "$ARCH_DIR/scripts/tor/torrc"
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
# Setup and start Tor hidden services (autoinstaller first-boot)
# Prefers system Tor (apt package) over container
ARCHY_TOR_DIR="/var/lib/archipelago/tor"
TOR_DIR="/var/lib/tor"
LOG="/var/log/archipelago-tor.log"
mkdir -p "$ARCHY_TOR_DIR"
# Write services.json for the backend to read
cat > "\$ARCHY_TOR_DIR/services.json" <<TORJSON
{
"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"
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
cat > /etc/tor/torrc <<TORRC
SocksPort 9050
ControlPort 0
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
HiddenServiceDir $TOR_DIR/hidden_service_electrumx
HiddenServicePort 50001 127.0.0.1:50001
HiddenServiceDir $TOR_DIR/hidden_service_lnd
HiddenServicePort 9735 127.0.0.1:9735
HiddenServicePort 8080 127.0.0.1:8080
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
TORRC
# Create hidden service dirs with correct ownership and permissions (700, not 750)
# Tor refuses to start if permissions are too permissive
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint; do
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
# 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 \
${ALPINE_TOR_IMAGE} \
-f /etc/tor/torrc >> "$LOG" 2>&1
echo "$(date): Tor container started" >> "$LOG"
fi
fi
# 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
chmod 750 "$TOR_DIR"/hidden_service_*/ 2>/dev/null || true
for f in "$TOR_DIR"/hidden_service_*/hostname; do
[ -f "$f" ] && chmod 640 "$f" && echo "$(date): chmod hostname $f" >> "$LOG"
done
echo "$(date): Tor hostname files readable by archipelago" >> "$LOG"
break
fi
done
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/"
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
# Unbundled builds only create FileBrowser (core dependency for Cloud)
if [ "$UNBUNDLED" = "1" ]; then
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
# Create a minimal first-boot script that only starts FileBrowser
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"
# 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
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
# 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)
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 \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \
--read-only \
--tmpfs=/tmp:rw,noexec,nosuid,size=64m \
--health-cmd='curl -sf http://localhost:80/ || exit 1' \
--health-interval=30s --health-timeout=5s --health-retries=3 \
--memory=256m \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
-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" && \
echo "[$(date)] FileBrowser created successfully" >> "$LOG" || \
echo "[$(date)] WARNING: FileBrowser creation failed" >> "$LOG"
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"
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
[Unit]
Description=Create core Archipelago containers on first boot
After=archipelago-setup-tor.service network-online.target podman.service
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
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/"
else
echo " Creating first-boot container creation service..."
# Copy shared script library
if [ -d "$SCRIPT_DIR/../scripts/lib" ]; then
mkdir -p "$ARCH_DIR/scripts/lib"
cp "$SCRIPT_DIR/../scripts/lib/"*.sh "$ARCH_DIR/scripts/lib/" 2>/dev/null || true
fi
if [ -f "$SCRIPT_DIR/../scripts/first-boot-containers.sh" ]; then
cp "$SCRIPT_DIR/../scripts/first-boot-containers.sh" "$ARCH_DIR/scripts/"
chmod +x "$ARCH_DIR/scripts/first-boot-containers.sh"
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
[Unit]
Description=Create core Archipelago containers on first boot
After=archipelago-setup-tor.service network-online.target podman.service
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
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/"
fi
fi
# Bundle E2E test script for post-install validation
if [ -f "$SCRIPT_DIR/../scripts/run-e2e-tests.sh" ]; then
cp "$SCRIPT_DIR/../scripts/run-e2e-tests.sh" "$ARCH_DIR/scripts/"
chmod +x "$ARCH_DIR/scripts/run-e2e-tests.sh"
echo " ✅ Bundled E2E test script for post-install validation"
fi
if [ -f "$SCRIPT_DIR/../scripts/run-post-install-tests.sh" ]; then
cp "$SCRIPT_DIR/../scripts/run-post-install-tests.sh" "$ARCH_DIR/scripts/"
chmod +x "$ARCH_DIR/scripts/run-post-install-tests.sh"
echo " ✅ Bundled post-install test suite"
fi
# Bundle self-update script and image-versions for update system
if [ -f "$SCRIPT_DIR/../scripts/self-update.sh" ]; then
cp "$SCRIPT_DIR/../scripts/self-update.sh" "$ARCH_DIR/scripts/"
chmod +x "$ARCH_DIR/scripts/self-update.sh"
echo " ✅ Bundled self-update script"
fi
if [ -f "$SCRIPT_DIR/../scripts/image-versions.sh" ]; then
cp "$SCRIPT_DIR/../scripts/image-versions.sh" "$ARCH_DIR/scripts/"
echo " ✅ Bundled image-versions.sh"
fi
# Bundle docker UI source files for building custom UIs on first boot (fallback if images not captured)
# Skip for unbundled builds
if [ "$UNBUNDLED" != "1" ]; then
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
fi
if [ "$UNBUNDLED" = "1" ]; then
echo " ✅ Unbundled build ready (Tor setup included, no container images)"
else
echo " ✅ Container images bundled (including Tor + first-boot)"
fi
# =============================================================================
# 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
# Log everything to a file on the target disk (after mount) and to console
INSTALL_LOG="/tmp/archipelago-install.log"
exec > >(tee -a "$INSTALL_LOG") 2>&1
# 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
# 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'
RED=$'\033[31m'
GREEN=$'\033[32m'
WHITE=$'\033[1;37m'
DIM=$'\033[38;5;242m'
DIMMER=$'\033[38;5;238m'
NC=$'\033[0m'
BOLD=$'\033[1m'
# Fixed left-margin layout (no more mixed centering)
TW=$(tput cols 2>/dev/null || echo 60)
[ "$TW" -gt 100 ] && TW=100
PAD=$(( (TW - 50) / 2 ))
[ "$PAD" -lt 0 ] && PAD=0
PADS=$(printf "%*s" "$PAD" "")
p() { printf "%s%b\n" "$PADS" "$1"; }
hrule() { local hr=""; for i in $(seq 1 48); do hr="${hr}*"; done; p "${ORANGE_DIM}${hr}${NC}"; }
# Phase display
STEP=0
TOTAL_STEPS=8
step() {
STEP=$((STEP + 1))
echo ""
p "${ORANGE}[$STEP/$TOTAL_STEPS] $1${NC}"
}
ok() { p " ${ORANGE_BRIGHT}✓ $1${NC}"; }
warn() { p " ${ORANGE}⚠ $1${NC}"; }
fail() { p " ${RED}✗ $1${NC}"; }
spinner() {
local pid=$1 msg=$2
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r%s %b%s %s%b" "$PADS" "$ORANGE" "${frames:i%10:1}" "$msg" "$NC"
i=$((i + 1))
sleep 0.1
done
printf "\r%s %b✓ %s%b\n" "$PADS" "$ORANGE_BRIGHT" "$msg" "$NC"
}
clear
echo -e " ${ORANGE}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${NC}"
echo -e " ${ORANGE}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${NC}"
echo -e " ${ORANGE}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${NC}"
echo -e " ${ORANGE_DIM}bitcoin node os${NC}"
echo ""
# Check required tools are present (should be bundled in ISO)
step "Checking tools"
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
warn "Installing missing: $MISSING"
if apt-get update -qq >/dev/null 2>&1; then
apt-get install -y -qq parted dosfstools e2fsprogs >/dev/null 2>&1 && ok "Tools installed" || {
fail "Failed to install required tools"
exit 1
}
else
fail "No network available and tools not bundled"
exit 1
fi
else
ok "All tools present"
fi
# Find boot media
BOOT_MEDIA=""
for dev in /run/archiso /cdrom /media/cdrom /run/live/medium /lib/live/mount/medium /mnt/iso; do
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 "")
step "Detecting disks"
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
ok "$TARGET_DISK ($TARGET_SIZE)"
echo ""
hrule
echo ""
p "${ORANGE} ⚠ all data on $TARGET_DISK will be erased${NC}"
echo ""
p "${ORANGE_DIM} press enter to install | ctrl+c to cancel${NC}"
read -s
echo ""
# Unmount any existing partitions
umount ${TARGET_DISK}* 2>/dev/null || true
umount ${TARGET_DISK}p* 2>/dev/null || true
# Create partition table — dual BIOS+UEFI boot + LUKS2 encrypted data
step "Creating partitions"
parted -s "$TARGET_DISK" mklabel gpt
# 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
# 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%
sleep 2
# Determine partition names
if [[ "$TARGET_DISK" == *nvme* ]]; then
BIOS_PART="${TARGET_DISK}p1"
EFI_PART="${TARGET_DISK}p2"
ROOT_PART="${TARGET_DISK}p3"
DATA_PART="${TARGET_DISK}p4"
else
BIOS_PART="${TARGET_DISK}1"
EFI_PART="${TARGET_DISK}2"
ROOT_PART="${TARGET_DISK}3"
DATA_PART="${TARGET_DISK}4"
fi
# Format partitions
step "Formatting partitions"
# 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
mkfs.vfat -F32 -n EFI "$EFI_PART"
mkfs.ext4 -F -L archipelago "$ROOT_PART"
# Mount root + extract rootfs (need cryptsetup from rootfs for LUKS)
ok "Partitions created"
echo ""
p " ${ORANGE_DIM}Mounting filesystems...${NC}"
mkdir -p /mnt/target
mount "$ROOT_PART" /mnt/target
mkdir -p /mnt/target/boot/efi
mount "$EFI_PART" /mnt/target/boot/efi
step "Installing system"
tar -xf "$ROOTFS_TAR" -C /mnt/target
# LUKS2 encryption for data partition
step "Encrypting data partition"
# 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
# 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
mount --bind /dev /mnt/target/dev
mount --bind /proc /mnt/target/proc
mount --bind /sys /mnt/target/sys
# 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
chroot /mnt/target cryptsetup luksFormat --type luks2 \
--key-file /root/.luks-archipelago.key \
--cipher "$LUKS_CIPHER" --key-size 512 \
--pbkdf argon2id --batch-mode \
"$DATA_PART"
# Open the LUKS volume
chroot /mnt/target cryptsetup open --type luks2 \
--key-file /root/.luks-archipelago.key \
"$DATA_PART" archipelago-data
# Unmount chroot bind mounts (will be re-mounted later for grub-install)
umount /mnt/target/sys 2>/dev/null || true
umount /mnt/target/proc 2>/dev/null || true
umount /mnt/target/dev
# Format the inner filesystem
mkfs.ext4 -F -L archipelago-data /dev/mapper/archipelago-data
# 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
mkdir -p /mnt/target/var/lib/archipelago/{data,config,containers,secrets,tor,identities,lnd}
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
chown -R 1000:1000 /mnt/target/var/lib/archipelago
echo " ✅ Data partition encrypted with LUKS2 ($LUKS_CIPHER)"
# Configure auto-unlock via crypttab (key file on root partition)
step "Configuring system"
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
# Create fstab
cat > /mnt/target/etc/fstab <<EOF
# Archipelago Bitcoin Node OS
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
/dev/mapper/archipelago-data /var/lib/archipelago ext4 defaults 0 2
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
chmod 644 /mnt/target/etc/hosts
# Pre-create container storage dirs (ReadWritePaths needs these to exist)
mkdir -p /mnt/target/home/archipelago/.local/share/containers
mkdir -p /mnt/target/home/archipelago/.config/containers
chown -R 1000:1000 /mnt/target/home/archipelago/.local
# Configure Archipelago app registry (HTTP, insecure)
cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF'
[[registry]]
location = "80.71.235.15:3000"
insecure = true
REGCONF
chown -R 1000:1000 /mnt/target/home/archipelago/.config
# 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
# 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
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
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
# 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
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
# 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
echo " ✅ Container images staged for first-boot loading"
fi
# 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
# 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
# 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
# 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
# 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..."
su - archipelago -c "git clone https://git.tx1138.com/lfg2025/archy $REPO_DIR" 2>/dev/null || {
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
# 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
# Ensure /sbin and /usr/sbin are in PATH (needed for reboot, shutdown, etc.)
case ":$PATH:" in
*:/sbin:*) ;; *) export PATH="$PATH:/sbin:/usr/sbin" ;;
esac
if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
export ARCHIPELAGO_WELCOMED=1
# 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
O='\033[38;5;208m'
OD='\033[38;5;130m'
W='\033[1;37m'
N='\033[0m'
clear
echo -e " ${O}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${N}"
echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${N}"
echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
echo -e " ${OD}bitcoin node os${N}"
if [ -n "$IP" ]; then
echo -e " ${W}web ui http://$IP${N}"
echo -e " ${W}ssh archipelago@$IP${N}"
echo -e " ${W}password archipelago (SSH) / password123 (Web)${N}"
else
echo -e " ${OD}Waiting for network...${N}"
fi
if [ -b /dev/mapper/archipelago-data ]; then
echo -e " ${OD}storage LUKS2 encrypted${N}"
fi
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
echo -e " ${OD}display Kiosk active (Ctrl+Alt+F1 for terminal)${N}"
else
echo -e " ${OD}display Console (Ctrl+Alt+F7 for kiosk)${N}"
fi
echo ""
fi
PROFILE
chmod +x /mnt/target/etc/profile.d/archipelago.sh
# 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"
cp "$BOOT_MEDIA/archipelago/configs/archipelago.service" /mnt/target/etc/systemd/system/archipelago.service 2>/dev/null || true
fi
# 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)
body = json.dumps(data).encode()
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
if not API_KEY: print("ERROR: ANTHROPIC_API_KEY not set — configure via setup-aiui-server.sh"); sys.exit(1)
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
Environment=ANTHROPIC_API_KEY=
ExecStart=/usr/bin/python3 /opt/archipelago/claude-api-proxy.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
CLAUDESVC
# Kiosk mode — X11 + Chromium fullscreen on attached display
# Not enabled by default; toggle via: sudo archipelago-kiosk enable/disable
cat > /mnt/target/usr/local/bin/archipelago-kiosk-launcher <<'KIOSKLAUNCHER'
#!/bin/bash
# Start X server on VT7 (VT1 stays on MOTD/console)
/usr/bin/Xorg :0 vt7 -nolisten tcp -keeptty &
XPID=$!
sleep 3
# Switch to kiosk display
chvt 7 2>/dev/null || true
if ! kill -0 $XPID 2>/dev/null; then
echo 'ERROR: Xorg failed to start'
exit 1
fi
export DISPLAY=:0
export HOME=/home/archipelago
xhost +SI:localuser:archipelago 2>/dev/null
xset s off 2>/dev/null
xset -dpms 2>/dev/null
xset s noblank 2>/dev/null
unclutter -idle 3 -root &
while true; do
# Get screen resolution for window sizing
SCREEN_RES=$(xdpyinfo 2>/dev/null | awk '/dimensions:/{print $2}')
SCREEN_RES=${SCREEN_RES:-1920x1080}
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium \
--kiosk \
--start-fullscreen \
--start-maximized \
--window-position=0,0 \
--window-size=${SCREEN_RES/x/,} \
--app=http://localhost/kiosk \
--noerrdialogs \
--disable-infobars \
--disable-translate \
--no-first-run \
--check-for-update-interval=31536000 \
--disable-features=TranslateUI \
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
--password-store=basic \
--disable-features=TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \
--disable-component-update \
--credentials_enable_service=false \
--user-data-dir=/home/archipelago/.config/chromium-kiosk
sleep 3
done
kill $XPID 2>/dev/null
KIOSKLAUNCHER
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk-launcher
cat > /mnt/target/etc/systemd/system/archipelago-kiosk.service <<'KIOSKSVC'
[Unit]
Description=Archipelago Kiosk (X11 + Chromium)
After=archipelago.service
Wants=archipelago.service
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'for i in $(seq 1 15); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
TimeoutStartSec=60
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
KIOSKSVC
# 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
;;
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
;;
*)
echo "Usage: archipelago-kiosk [enable|disable|status|toggle]"
echo " enable — Start kiosk (fullscreen web UI on display)"
echo " disable — Stop kiosk, restore console login"
echo " toggle — Switch between kiosk and terminal"
echo " status — Show current mode"
echo ""
echo "Keyboard shortcuts (from terminal):"
echo " Ctrl+Alt+F7 — Switch to kiosk display"
echo " Ctrl+Alt+F1 — Switch to terminal"
exit 1
;;
esac
KIOSKTOGGLE
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk
# Install GRUB
step "Installing bootloader"
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
# 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"
# 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
# UEFI boot: install to fallback path (/EFI/BOOT/BOOTX64.EFI) for maximum compatibility
echo " Installing UEFI bootloader..."
if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable; then
echo " ✅ UEFI bootloader installed (removable/fallback path)"
else
echo " ⚠️ UEFI removable install failed, trying standard..."
if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago; then
echo " ✅ UEFI bootloader installed (standard)"
else
echo " ❌ UEFI bootloader installation failed"
fi
fi
# 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.
echo " Verifying EFI boot files..."
EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT"
if [ "$ARCH" = "x86_64" ]; then
EFI_BOOT_BINARY="BOOTX64.EFI"
else
EFI_BOOT_BINARY="BOOTAA64.EFI"
fi
# 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
# 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
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
ls -la "$EFI_BOOT_DIR/"
else
echo " ❌ Missing $EFI_BOOT_DIR/$EFI_BOOT_BINARY — boot will fail!"
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)..."
if chroot /mnt/target grub-install --target=${GRUB_BIOS_TARGET} "${TARGET_DISK}"; then
echo " ✅ Legacy BIOS bootloader installed"
else
echo " ⚠️ Legacy BIOS bootloader failed (UEFI-only boot)"
fi
elif [ -n "${GRUB_BIOS_TARGET}" ]; then
echo " Skipping Legacy BIOS bootloader (machine supports UEFI)"
fi
# 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)
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
# Suppress os-prober warning in GRUB
echo "GRUB_DISABLE_OS_PROBER=true" >> /mnt/target/etc/default/grub
# 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
# 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
# Enable splash in GRUB
if ! grep -q "splash" /mnt/target/etc/default/grub; then
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 splash"/' \
/mnt/target/etc/default/grub 2>/dev/null || true
fi
echo " Installed Archipelago Plymouth theme on target"
fi
# 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..."
chroot /mnt/target update-initramfs -u -k all 2>&1 | grep -v "Possible missing firmware"
chroot /mnt/target update-grub
# 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
# Install udev rule for mesh radio stable naming (/dev/mesh-radio)
MESH_RULES=""
for p in "$BOOT_MEDIA/99-mesh-radio.rules" /cdrom/99-mesh-radio.rules "$BOOT_MEDIA/archipelago/configs/99-mesh-radio.rules"; do
[ -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
echo " Installed mesh radio udev rule"
fi
# 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 \
archipelago-first-boot-containers archipelago-setup-tor \
console-setup; do
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
# 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 \
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" 2>/dev/null
chmod 600 /mnt/target/etc/archipelago/ssl/archipelago.key
echo " Generated self-signed SSL certificate"
fi
# 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
# Enable services
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
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
# 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
# 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
chroot /mnt/target systemctl enable archipelago-diagnostics.service 2>/dev/null || true
# 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
cat > /mnt/target/etc/systemd/system/archipelago-post-install-tests.service <<'PITSERVICE'
[Unit]
Description=Archipelago Install Verification (first boot)
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'
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'
RemainAfterExit=yes
StandardOutput=journal+console
StandardError=journal+console
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
PITSERVICE
chroot /mnt/target systemctl enable archipelago-post-install-tests.service 2>/dev/null || true
# 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
commit=$(cd /tmp 2>/dev/null && git -C "$BOOT_MEDIA/../" rev-parse --short HEAD 2>/dev/null || echo "unknown")
date=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
type=unbundled
BUILDINFO
# Save install log BEFORE unmounting target
cp "$INSTALL_LOG" /mnt/target/var/log/archipelago-install.log 2>/dev/null || true
# 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
umount /mnt/target/var/lib/archipelago 2>/dev/null || true
cryptsetup close archipelago-data 2>/dev/null || true
umount /mnt/target 2>/dev/null || true
echo ""
echo -e " ${ORANGE}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${NC}"
echo -e " ${ORANGE}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${NC}"
echo -e " ${ORANGE}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${NC}"
echo -e " ${ORANGE_DIM}bitcoin node os${NC}"
echo ""
p "${ORANGE_BRIGHT} ✓ Installation Complete${NC}"
echo ""
p "${ORANGE_DIM} After reboot, access from any device:${NC}"
echo ""
p "${ORANGE} http://<this machine's IP>${NC}"
echo ""
p "${WHITE} SSH ssh archipelago@<IP>${NC}"
p "${WHITE} Password archipelago${NC}"
p "${WHITE} Web Login password123${NC}"
echo ""
hrule
echo ""
# Suppress kernel messages on console (SquashFS errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
# Copy reboot script to tmpfs so it survives USB removal
cat > /tmp/archipelago-reboot.sh <<'REBOOTSCRIPT'
#!/bin/bash
# This script runs from tmpfs — safe after USB removal
O=$'\033[38;5;208m'
OD=$'\033[38;5;130m'
N=$'\033[0m'
echo -e " ${O}>>> REMOVE THE USB DRIVE NOW <<<${N}"
echo ""
echo -e " ${OD}Press Enter to reboot (or wait 30 seconds)${N}"
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
echo ""
echo -e " ${OD}Rebooting...${N}"
sleep 1
reboot -f
REBOOTSCRIPT
chmod +x /tmp/archipelago-reboot.sh
# Lazy-unmount live filesystem BEFORE telling user to pull USB
exec 2>/dev/null
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 "")
if [ -n "$BOOT_DEV" ]; then
BOOT_DISK=$(lsblk -no PKNAME "$BOOT_DEV" 2>/dev/null | head -1)
[ -n "$BOOT_DISK" ] && eject "/dev/$BOOT_DISK" 2>/dev/null || true
fi
exec 2>&1
# Hand off to tmpfs-based script — survives USB removal
exec /bin/bash /tmp/archipelago-reboot.sh
INSTALLER_SCRIPT
# 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
chmod +x "$ARCH_DIR/auto-install.sh"
# =============================================================================
# STEP 5: Configure boot loader and ISO structure
# =============================================================================
echo ""
echo "Step 5: Configuring boot loaders..."
# 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.
# Create GRUB configuration
echo " Writing GRUB config..."
cat > "$INSTALLER_ISO/boot/grub/grub.cfg" <<'GRUBCFG'
insmod part_gpt
insmod part_msdos
insmod fat
insmod iso9660
insmod all_video
insmod search
insmod search_label
insmod search_fs_file
# Find boot media — try label first, then known file fallback
search --no-floppy --set=root --label ARCHIPELAGO
if [ -z "$root" ]; then
search --no-floppy --set=root --file /archipelago/auto-install.sh
fi
set timeout=5
set default=0
# Load font for graphical menu
if loadfont ($root)/boot/grub/font.pf2; then
set gfxmode=1024x768,auto
insmod gfxterm
insmod png
terminal_output gfxterm
fi
# Archipelago GRUB theme
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
else
set menu_color_normal=light-gray/black
set menu_color_highlight=white/dark-gray
fi
menuentry "Install Archipelago" --hotkey=i {
linux ($root)/live/vmlinuz boot=live components quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0
initrd ($root)/live/initrd.img
}
menuentry "Install Archipelago (verbose)" --hotkey=v {
linux ($root)/live/vmlinuz boot=live components loglevel=4 console=ttyS0,115200 console=tty0
initrd ($root)/live/initrd.img
}
menuentry "Boot from local disk" --hotkey=b {
set root=(hd0)
chainloader +1
}
GRUBCFG
# 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
# Create ISOLINUX configuration (legacy BIOS boot)
echo " Writing ISOLINUX config..."
# 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
cat > "$INSTALLER_ISO/isolinux/isolinux.cfg" <<'ISOCFG'
UI vesamenu.c32
PROMPT 0
TIMEOUT 0
MENU TITLE bitcoin node os
MENU BACKGROUND splash.png
MENU RESOLUTION 1024 768
MENU VSHIFT 18
MENU HSHIFT 32
MENU WIDTH 18
MENU MARGIN 1
MENU ROWS 5
MENU TABMSG press tab to edit | archipelago.sh
MENU COLOR screen 37;40 #00000000 #00000000 none
MENU COLOR border 30;40 #00000000 #00000000 none
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
MENU COLOR hotkey 1;37;40 #fffb923c #00000000 none
MENU COLOR hotsel 1;37;40 #fffb923c #c0181818 std
MENU COLOR timeout_msg 37;40 #ff555555 #00000000 none
MENU COLOR timeout 1;37;40 #fffb923c #00000000 none
MENU COLOR tabmsg 37;40 #ff444444 #00000000 none
MENU COLOR cmdmark 37;40 #00000000 #00000000 none
MENU COLOR cmdline 37;40 #00000000 #00000000 none
DEFAULT install
LABEL install
MENU LABEL Install Archipelago
KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components quiet loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0
MENU DEFAULT
LABEL install-verbose
MENU LABEL Install (verbose output)
KERNEL /live/vmlinuz
APPEND initrd=/live/initrd.img boot=live components loglevel=4
LABEL local
MENU LABEL Boot from local disk
LOCALBOOT 0x80
ISOCFG
echo " Step 5 complete (GRUB + ISOLINUX configured)"
# =============================================================================
# STEP 6: Create final ISO
# =============================================================================
echo ""
echo "Step 6: Creating bootable ISO..."
if [ "$UNBUNDLED" = "1" ]; then
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-unbundled-${ARCH}.iso"
else
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-${ARCH}.iso"
fi
# 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
if [ ! -f "$ISOHDPFX" ]; then
# Fallback to system-installed copy
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"
echo " Using system isohdpfx.bin: $path"
break
fi
done
fi
# 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"
if [ ! -f "$EFI_IMG" ]; then
echo " WARNING: No EFI boot image — ISO will only support Legacy BIOS boot"
xorriso -as mkisofs -o "$OUTPUT_ISO" \
-volid "ARCHIPELAGO" \
-iso-level 3 \
-J -joliet-long -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-partition_offset 16 \
"$INSTALLER_ISO"
else
xorriso -as mkisofs -o "$OUTPUT_ISO" \
-volid "ARCHIPELAGO" \
-iso-level 3 \
-J -joliet-long -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
-partition_offset 16 \
"$INSTALLER_ISO"
fi
echo ""
if [ "$UNBUNDLED" = "1" ]; then
echo "UNBUNDLED AUTO-INSTALLER ISO CREATED"
echo ""
echo " Output: $OUTPUT_ISO"
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
echo ""
echo " Lightweight installer -- apps downloaded on-demand from Marketplace"
else
echo "AUTO-INSTALLER ISO CREATED"
echo ""
echo " Output: $OUTPUT_ISO"
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
echo ""
echo " Full installer with pre-bundled container apps"
fi
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 ""