Add nostr-rs-relay as native system service (port 7777) for VPN signaling. Every node runs its own private relay from first boot. Update nvpn binary from v0.3.4 to v0.3.7 (fixes mesh event processing). Add WireGuard helper and address service for peer VPN. First-boot script configures relay, nvpn identity, relay URLs (direct + Tor onion), and syncs daemon config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3248 lines
130 KiB
Bash
Executable File
3248 lines
130 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}"
|
|
|
|
# ── Sequential build numbering ─────────────────────────────────────────
|
|
# Increments on each build. Users see this in UI (Settings, sidebar).
|
|
# Counter persists in /opt/archipelago/build-counter (on build machine).
|
|
BUILD_COUNTER_FILE="/opt/archipelago/build-counter"
|
|
if [ -f "$BUILD_COUNTER_FILE" ]; then
|
|
BUILD_NUM=$(( $(cat "$BUILD_COUNTER_FILE") + 1 ))
|
|
else
|
|
BUILD_NUM=1
|
|
fi
|
|
echo "$BUILD_NUM" | sudo tee "$BUILD_COUNTER_FILE" > /dev/null 2>/dev/null || BUILD_NUM=1
|
|
GIT_SHORT=$(cd "$SCRIPT_DIR/.." && git rev-parse --short HEAD 2>/dev/null || echo "dev")
|
|
# Version format: major.minor.patch-prerelease (semver)
|
|
# Read version from Cargo.toml (single source of truth)
|
|
BUILD_VERSION=$(grep '^version' "$SCRIPT_DIR/../core/archipelago/Cargo.toml" 2>/dev/null | head -1 | sed 's/version = "//;s/"//' || echo "0.0.0")
|
|
echo "Build #${BUILD_NUM} (${BUILD_VERSION}, commit ${GIT_SHORT})"
|
|
|
|
# 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 Uni2" | debconf-set-selections && \
|
|
echo "console-setup console-setup/fontface47 select Terminus" | debconf-set-selections && \
|
|
echo "console-setup console-setup/fontsize-fb47 select 16" | debconf-set-selections
|
|
|
|
# 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 \
|
|
python3 \
|
|
curl \
|
|
git \
|
|
vim-tiny \
|
|
ca-certificates \
|
|
openssl \
|
|
chrony \
|
|
locales \
|
|
console-setup \
|
|
keyboard-configuration \
|
|
cryptsetup \
|
|
firmware-realtek \
|
|
firmware-iwlwifi \
|
|
firmware-misc-nonfree \
|
|
firmware-linux-nonfree \
|
|
intel-microcode \
|
|
amd64-microcode \
|
|
xorg \
|
|
xdotool \
|
|
chromium \
|
|
unclutter \
|
|
fonts-liberation \
|
|
xfonts-base \
|
|
plymouth \
|
|
plymouth-themes \
|
|
zstd \
|
|
socat \
|
|
python3 \
|
|
apache2-utils \
|
|
wireguard-tools \
|
|
acpid \
|
|
acpi-support-base \
|
|
&& 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 archipelago-tor-helper.service /etc/systemd/system/archipelago-tor-helper.service
|
|
COPY archipelago-tor-helper.path /etc/systemd/system/archipelago-tor-helper.path
|
|
COPY nostr-vpn.service /etc/systemd/system/nostr-vpn.service
|
|
COPY archipelago-wg-address.service /etc/systemd/system/archipelago-wg-address.service
|
|
COPY nostr-relay.service /etc/systemd/system/nostr-relay.service
|
|
COPY nostr-relay-config.toml /etc/archipelago/nostr-relay-config.toml
|
|
|
|
# WireGuard kernel module auto-load on boot
|
|
RUN echo "wireguard" >> /etc/modules-load.d/wireguard.conf
|
|
|
|
# Copy container doctor + reconcile scripts (referenced by the services above)
|
|
RUN mkdir -p /home/archipelago/archy/scripts/lib
|
|
COPY container-doctor.sh /home/archipelago/archy/scripts/container-doctor.sh
|
|
COPY reconcile-containers.sh /home/archipelago/archy/scripts/reconcile-containers.sh
|
|
COPY container-specs.sh /home/archipelago/archy/scripts/container-specs.sh
|
|
COPY tor-helper.sh /opt/archipelago/scripts/tor-helper.sh
|
|
COPY lib/ /home/archipelago/archy/scripts/lib/
|
|
RUN chmod +x /home/archipelago/archy/scripts/*.sh /home/archipelago/archy/scripts/lib/*.sh /opt/archipelago/scripts/*.sh && \
|
|
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 && \
|
|
systemctl enable archipelago-tor-helper.path || true && \
|
|
systemctl enable nostr-relay || true && \
|
|
systemctl enable nostr-vpn || true && \
|
|
systemctl enable archipelago-wg-address || 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,nostr-relay,nostr-vpn} && \
|
|
mkdir -p /etc/archipelago && \
|
|
mkdir -p /opt/archipelago/{bin,scripts,web-ui} && \
|
|
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads} && \
|
|
cp /etc/archipelago/nostr-relay-config.toml /var/lib/archipelago/nostr-relay/config.toml && \
|
|
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 container-specs.sh tor-helper.sh; do
|
|
if [ -f "$SCRIPT_DIR/../scripts/$s" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/$s" "$WORK_DIR/$s"
|
|
fi
|
|
done
|
|
# Copy shared script library (mem_limit etc.)
|
|
if [ -d "$SCRIPT_DIR/../scripts/lib" ]; then
|
|
mkdir -p "$WORK_DIR/lib"
|
|
cp "$SCRIPT_DIR/../scripts/lib/"*.sh "$WORK_DIR/lib/" 2>/dev/null || true
|
|
fi
|
|
echo " Using container doctor + reconcile timers from configs/"
|
|
fi
|
|
|
|
# Copy Tor helper path-activated service (allows backend to manage Tor as non-root)
|
|
if [ -f "$SCRIPT_DIR/configs/archipelago-tor-helper.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/archipelago-tor-helper.service" "$WORK_DIR/archipelago-tor-helper.service"
|
|
cp "$SCRIPT_DIR/configs/archipelago-tor-helper.path" "$WORK_DIR/archipelago-tor-helper.path"
|
|
echo " Using tor-helper path unit from configs/"
|
|
fi
|
|
|
|
# Copy NostrVPN system service (native mesh VPN, not a container)
|
|
if [ -f "$SCRIPT_DIR/configs/nostr-vpn.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/nostr-vpn.service" "$WORK_DIR/nostr-vpn.service"
|
|
echo " Using nostr-vpn.service from configs/"
|
|
fi
|
|
|
|
if [ -f "$SCRIPT_DIR/configs/archipelago-wg-address.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/archipelago-wg-address.service" "$WORK_DIR/archipelago-wg-address.service"
|
|
echo " Using archipelago-wg-address.service from configs/"
|
|
fi
|
|
|
|
# Copy private Nostr relay service (native, for NostrVPN signaling)
|
|
if [ -f "$SCRIPT_DIR/configs/nostr-relay.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/nostr-relay.service" "$WORK_DIR/nostr-relay.service"
|
|
echo " Using nostr-relay.service from configs/"
|
|
fi
|
|
if [ -f "$SCRIPT_DIR/configs/nostr-relay-config.toml" ]; then
|
|
cp "$SCRIPT_DIR/configs/nostr-relay-config.toml" "$WORK_DIR/nostr-relay-config.toml"
|
|
echo " Using nostr-relay-config.toml from configs/"
|
|
fi
|
|
|
|
# Copy WireGuard helper script (privileged peer management)
|
|
if [ -f "$SCRIPT_DIR/../scripts/archipelago-wg" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/archipelago-wg" "$WORK_DIR/archipelago-wg"
|
|
echo " Using archipelago-wg helper from scripts/"
|
|
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 rm -f archipelago-rootfs-tmp 2>/dev/null || true
|
|
$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-Bold.ttf
|
|
grub-mkfont -s 14 -o /output/grub-fonts/dejavu_14.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
|
|
grub-mkfont -s 16 -o /output/grub-fonts/dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
|
|
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"
|
|
|
|
# Embed netavark + aardvark-dns for container DNS (podman CNI lacks DNS)
|
|
if [ -f /usr/lib/podman/netavark ] && [ -f /usr/lib/podman/aardvark-dns ]; then
|
|
cp /usr/lib/podman/netavark "$ARCH_DIR/bin/netavark"
|
|
cp /usr/lib/podman/aardvark-dns "$ARCH_DIR/bin/aardvark-dns"
|
|
echo " Embedded netavark + aardvark-dns in ISO"
|
|
else
|
|
echo " WARNING: netavark/aardvark-dns not found — install with: apt install aardvark-dns netavark"
|
|
fi
|
|
|
|
# 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
|
|
|
|
# Extract NostrVPN binary from container image (native system service, not a container app)
|
|
echo " Extracting NostrVPN binary..."
|
|
NVPN_IMAGE="$($CONTAINER_CMD images -q 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null)"
|
|
if [ -z "$NVPN_IMAGE" ]; then
|
|
$CONTAINER_CMD pull 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null || true
|
|
fi
|
|
NVPN_CONTAINER=$($CONTAINER_CMD create 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null) || true
|
|
if [ -n "$NVPN_CONTAINER" ]; then
|
|
$CONTAINER_CMD cp "$NVPN_CONTAINER:/usr/local/bin/nvpn" "$ARCH_DIR/bin/nvpn" 2>/dev/null && \
|
|
chmod +x "$ARCH_DIR/bin/nvpn" && \
|
|
echo " ✅ NostrVPN binary extracted ($(du -h "$ARCH_DIR/bin/nvpn" | cut -f1))"
|
|
$CONTAINER_CMD rm "$NVPN_CONTAINER" 2>/dev/null || true
|
|
else
|
|
echo " ⚠ NostrVPN image not available — nvpn binary will be missing"
|
|
fi
|
|
|
|
# Extract nostr-rs-relay binary from container image (native system service for VPN signaling)
|
|
echo " Extracting nostr-rs-relay binary..."
|
|
RELAY_IMAGE="$($CONTAINER_CMD images -q 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null)"
|
|
if [ -z "$RELAY_IMAGE" ]; then
|
|
$CONTAINER_CMD pull 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null || true
|
|
fi
|
|
RELAY_CONTAINER=$($CONTAINER_CMD create 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null) || true
|
|
if [ -n "$RELAY_CONTAINER" ]; then
|
|
$CONTAINER_CMD cp "$RELAY_CONTAINER:/usr/local/bin/nostr-rs-relay" "$ARCH_DIR/bin/nostr-rs-relay" 2>/dev/null && \
|
|
chmod +x "$ARCH_DIR/bin/nostr-rs-relay" && \
|
|
echo " ✅ nostr-rs-relay binary extracted ($(du -h "$ARCH_DIR/bin/nostr-rs-relay" | cut -f1))"
|
|
$CONTAINER_CMD rm "$RELAY_CONTAINER" 2>/dev/null || true
|
|
else
|
|
echo " ⚠ nostr-rs-relay image not available — relay binary will be missing"
|
|
fi
|
|
|
|
# Copy WireGuard helper script
|
|
if [ -f "$WORK_DIR/archipelago-wg" ]; then
|
|
cp "$WORK_DIR/archipelago-wg" "$ARCH_DIR/bin/archipelago-wg"
|
|
chmod +x "$ARCH_DIR/bin/archipelago-wg"
|
|
echo " ✅ WireGuard helper script included"
|
|
fi
|
|
|
|
# Copy NostrVPN UI dashboard for nginx serving
|
|
if [ -d "$SCRIPT_DIR/../docker/nostr-vpn-ui" ]; then
|
|
mkdir -p "$ARCH_DIR/web-ui/nostr-vpn"
|
|
cp "$SCRIPT_DIR/../docker/nostr-vpn-ui/index.html" "$ARCH_DIR/web-ui/nostr-vpn/"
|
|
echo " ✅ NostrVPN UI dashboard included"
|
|
fi
|
|
|
|
# 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
|
|
|
|
# Include AIUI web app (Claude chat interface)
|
|
AIUI_INCLUDED=0
|
|
# Search multiple locations for a pre-built AIUI app
|
|
for AIUI_DIR in \
|
|
"$SCRIPT_DIR/../../AIUI/packages/app/dist" \
|
|
"$HOME/AIUI/packages/app/dist" \
|
|
"/home/archipelago/AIUI/packages/app/dist" \
|
|
"/opt/archipelago/web-ui/aiui" \
|
|
"/home/archipelago/archy/AIUI/packages/app/dist"; do
|
|
if [ -d "$AIUI_DIR" ] && [ -f "$AIUI_DIR/index.html" ]; then
|
|
echo " Including AIUI from $AIUI_DIR..."
|
|
mkdir -p "$ARCH_DIR/web-ui/aiui"
|
|
# Use rsync to handle same-file (CI workspace == /opt/archipelago) gracefully
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rsync -a "$AIUI_DIR/" "$ARCH_DIR/web-ui/aiui/"
|
|
else
|
|
cp -r "$AIUI_DIR/"* "$ARCH_DIR/web-ui/aiui/" 2>/dev/null || true
|
|
fi
|
|
echo " ✅ AIUI included ($(du -sh "$ARCH_DIR/web-ui/aiui" | cut -f1))"
|
|
AIUI_INCLUDED=1
|
|
break
|
|
fi
|
|
done
|
|
if [ "$AIUI_INCLUDED" = "0" ]; then
|
|
echo " ⚠️ AIUI not found — build it first:"
|
|
echo " cd ~/AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build"
|
|
echo " Searched: ~/AIUI, /home/archipelago/AIUI, /opt/archipelago/web-ui/aiui"
|
|
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 vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager 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_CONFIG_DIR="/var/lib/archipelago/tor-config"
|
|
TOR_DIR="/var/lib/tor"
|
|
LOG="/var/log/archipelago-tor.log"
|
|
|
|
mkdir -p "$ARCHY_TOR_DIR" "$TOR_CONFIG_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"
|
|
# Backend reads from tor-config/, not tor/
|
|
cp "$ARCHY_TOR_DIR/services.json" "$TOR_CONFIG_DIR/services.json"
|
|
chown -R archipelago:archipelago "$TOR_CONFIG_DIR" 2>/dev/null || true
|
|
|
|
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
|
|
cat > /etc/tor/torrc <<TORRC
|
|
SocksPort 0.0.0.0:9050
|
|
SocksPolicy accept 10.89.0.0/16
|
|
SocksPolicy accept 127.0.0.0/8
|
|
SocksPolicy reject *
|
|
# ControlPort disabled for security
|
|
|
|
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
|
|
HiddenServicePort 8332 127.0.0.1:8332
|
|
|
|
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
|
|
|
|
# Sync hostnames to backend-readable directory
|
|
HOSTNAMES_DIR="/var/lib/archipelago/tor-hostnames"
|
|
mkdir -p "$HOSTNAMES_DIR"
|
|
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint; do
|
|
if [ -f "$TOR_DIR/hidden_service_${svc}/hostname" ]; then
|
|
cp "$TOR_DIR/hidden_service_${svc}/hostname" "$HOSTNAMES_DIR/$svc"
|
|
echo "$(date): Synced hostname: $svc" >> "$LOG"
|
|
fi
|
|
done
|
|
chown -R archipelago:archipelago "$HOSTNAMES_DIR" 2>/dev/null || true
|
|
echo "$(date): Hostnames synced: $(ls $HOSTNAMES_DIR 2>/dev/null | tr '\n' ' ')" >> "$LOG"
|
|
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)
|
|
# Both bundled and unbundled builds use the full first-boot script.
|
|
# Unbundled mode pulls images from registry; bundled mode loads from tarballs.
|
|
if false && [ "$UNBUNDLED" = "1" ]; then
|
|
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
|
|
# DISABLED: minimal script doesn't create UI sidecars or write app configs.
|
|
# The full first-boot-containers.sh handles both bundled and unbundled modes.
|
|
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
|
|
|
|
# Enable podman-restart so containers with --restart=unless-stopped auto-start on boot
|
|
runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && systemctl --user enable podman-restart.service' 2>>"$LOG" || true
|
|
|
|
# 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)
|
|
# Generate random FileBrowser password and store for auto-login
|
|
FB_PASS_DIR="/var/lib/archipelago/secrets/filebrowser"
|
|
mkdir -p "$FB_PASS_DIR"
|
|
if [ ! -f "$FB_PASS_DIR/password" ]; then
|
|
head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24 > "$FB_PASS_DIR/password"
|
|
chmod 600 "$FB_PASS_DIR/password"
|
|
chown 1000:1000 "$FB_PASS_DIR/password"
|
|
fi
|
|
|
|
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"
|
|
# Set FileBrowser password to match the stored random password
|
|
sleep 5
|
|
FB_PASS=$(cat "$FB_PASS_DIR/password" 2>/dev/null || echo "admin")
|
|
runuser -u archipelago -- bash -c "export XDG_RUNTIME_DIR=/run/user/1000 && podman exec filebrowser filebrowser users update admin --password '$FB_PASS' --database /data/database.db" 2>>"$LOG" && \
|
|
echo "[$(date)] FileBrowser admin password set" >> "$LOG" || \
|
|
echo "[$(date)] WARNING: Could not set FileBrowser password" >> "$LOG"
|
|
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"
|
|
|
|
# Copy shared script library (TUI animations for installer, shared utils)
|
|
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
|
|
echo " Copied scripts/lib/ ($(ls "$ARCH_DIR/scripts/lib/" 2>/dev/null | wc -l) files)"
|
|
fi
|
|
|
|
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
|
|
[Unit]
|
|
Description=Create core Archipelago containers on first boot
|
|
After=archipelago-load-images.service archipelago-setup-tor.service network-online.target podman.service
|
|
Wants=archipelago-load-images.service
|
|
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
|
|
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
TimeoutStartSec=900
|
|
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-load-images.service archipelago-setup-tor.service network-online.target podman.service
|
|
Wants=archipelago-load-images.service
|
|
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
|
|
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
TimeoutStartSec=900
|
|
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
|
|
|
|
# Bootstrap node config — new installs use this Bitcoin node during IBD
|
|
# so ElectrumX/LND/BTCPay/Mempool work immediately while local chain syncs
|
|
# Tries LAN first (fast), falls back to Tor (works from anywhere)
|
|
BOOTSTRAP_RPC_PASS=""
|
|
BOOTSTRAP_ONION=""
|
|
if [ -f /var/lib/archipelago/secrets/bitcoin-rpc-password ]; then
|
|
BOOTSTRAP_RPC_PASS=$(cat /var/lib/archipelago/secrets/bitcoin-rpc-password 2>/dev/null)
|
|
fi
|
|
if [ -f /var/lib/archipelago/tor-hostnames/bitcoin ]; then
|
|
BOOTSTRAP_ONION=$(cat /var/lib/archipelago/tor-hostnames/bitcoin 2>/dev/null)
|
|
fi
|
|
if [ -n "$BOOTSTRAP_RPC_PASS" ]; then
|
|
DEV_IP="${DEV_SERVER:-192.168.1.228}"
|
|
cat > "$ARCH_DIR/bootstrap.conf" <<BSTRAP
|
|
# Bootstrap Bitcoin node — used during Initial Block Download
|
|
# Services connect here until the local node is fully synced
|
|
# First-boot tries LAN, then Tor (works from any network)
|
|
BOOTSTRAP_LAN_HOST=${DEV_IP}
|
|
BOOTSTRAP_ONION=${BOOTSTRAP_ONION}
|
|
BOOTSTRAP_RPC_USER=archipelago
|
|
BOOTSTRAP_RPC_PASS=${BOOTSTRAP_RPC_PASS}
|
|
BSTRAP
|
|
chmod 600 "$ARCH_DIR/bootstrap.conf"
|
|
echo " ✅ Bootstrap node config embedded (LAN: ${DEV_IP}, Tor: ${BOOTSTRAP_ONION:-none})"
|
|
else
|
|
echo " ⚠ No bootstrap config — no Bitcoin RPC password found on build host"
|
|
fi
|
|
|
|
# Bundle bootstrap switchover script + systemd timer
|
|
if [ -f "$SCRIPT_DIR/../scripts/bootstrap-switchover.sh" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/bootstrap-switchover.sh" "$ARCH_DIR/scripts/"
|
|
chmod +x "$ARCH_DIR/scripts/bootstrap-switchover.sh"
|
|
echo " ✅ Bundled bootstrap switchover script"
|
|
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
|
|
# Always bundle — these are tiny HTML/CSS files, not container images
|
|
if true; 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 file — verbose command output goes here, TUI stays on console
|
|
INSTALL_LOG="/tmp/archipelago-install.log"
|
|
# Run commands quietly: redirect their stdout/stderr to log
|
|
# TUI functions (p, step, ok, etc.) print directly to console
|
|
run() { "$@" >> "$INSTALL_LOG" 2>&1; }
|
|
runq() { "$@" >>"$INSTALL_LOG" 2>&1 || true; }
|
|
|
|
# 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'
|
|
|
|
# Left-justified layout — 2-space indent, no centering
|
|
PADS=" "
|
|
|
|
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}"; }
|
|
|
|
# Typewriter animation for key text
|
|
typewrite() {
|
|
local text="$1" delay="${2:-0.02}"
|
|
printf "%s" "$PADS"
|
|
local i=0
|
|
while [ $i -lt ${#text} ]; do
|
|
printf "%s" "${text:$i:1}"
|
|
i=$((i + 1))
|
|
sleep "$delay"
|
|
done
|
|
printf "\n"
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# Source TUI library for install animations (graceful fallback if missing)
|
|
for _tui_path in \
|
|
"$BOOT_MEDIA/archipelago/scripts/lib/install-tui.sh" \
|
|
"/opt/archipelago/scripts/lib/install-tui.sh" \
|
|
"$(dirname "$0")/../scripts/lib/install-tui.sh"; do
|
|
[ -f "$_tui_path" ] && { source "$_tui_path" 2>/dev/null; break; }
|
|
done
|
|
|
|
if [ "${TUI_AVAILABLE:-}" = "1" ]; then
|
|
tui_welcome
|
|
tui_enable_progress_spinner
|
|
else
|
|
clear
|
|
echo ""
|
|
echo -e "${PADS}${ORANGE}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${NC}"
|
|
echo -e "${PADS}${ORANGE}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${NC}"
|
|
echo -e "${PADS}${ORANGE}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${NC}"
|
|
typewrite "$(echo -e "${ORANGE_DIM}bitcoin node os${NC}")" 0.04
|
|
echo ""
|
|
fi
|
|
|
|
# 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
|
|
run mkfs.vfat -F32 -n EFI "$EFI_PART"
|
|
run 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"
|
|
run 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
|
|
run 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
|
|
run 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)
|
|
runq umount /mnt/target/sys
|
|
runq umount /mnt/target/proc
|
|
runq umount /mnt/target/dev
|
|
|
|
# Format the inner filesystem
|
|
run 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/containers/storage
|
|
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
|
|
|
|
# Redirect container storage to encrypted LUKS partition (not root filesystem)
|
|
# Without this, pulling images fills the 29GB root partition
|
|
cat > /mnt/target/home/archipelago/.config/containers/storage.conf <<'STORAGECONF'
|
|
[storage]
|
|
driver = "overlay"
|
|
graphroot = "/var/lib/archipelago/containers/storage"
|
|
runroot = "/run/user/1000/containers"
|
|
STORAGECONF
|
|
|
|
# Symlink for backward compat (some tools look in ~/.local/share/containers)
|
|
ln -sf /var/lib/archipelago/containers/storage /mnt/target/home/archipelago/.local/share/containers/storage 2>/dev/null || true
|
|
|
|
# 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
|
|
|
|
# Install netavark + aardvark-dns for container DNS resolution on archy-net.
|
|
# Debian 12's podman defaults to CNI which lacks DNS. Netavark provides built-in DNS.
|
|
# Binaries are embedded in the ISO at build time (archipelago/bin/).
|
|
if [ -f "$BOOT_MEDIA/archipelago/bin/netavark" ] && [ -f "$BOOT_MEDIA/archipelago/bin/aardvark-dns" ]; then
|
|
mkdir -p /mnt/target/usr/lib/podman
|
|
cp "$BOOT_MEDIA/archipelago/bin/netavark" /mnt/target/usr/lib/podman/netavark
|
|
cp "$BOOT_MEDIA/archipelago/bin/aardvark-dns" /mnt/target/usr/lib/podman/aardvark-dns
|
|
chmod +x /mnt/target/usr/lib/podman/netavark /mnt/target/usr/lib/podman/aardvark-dns
|
|
# Configure podman to use netavark backend (enables container DNS)
|
|
mkdir -p /mnt/target/home/archipelago/.config/containers
|
|
cat > /mnt/target/home/archipelago/.config/containers/containers.conf <<'CONTAINERSCONF'
|
|
[network]
|
|
network_backend = "netavark"
|
|
CONTAINERSCONF
|
|
chown -R 1000:1000 /mnt/target/home/archipelago/.config/containers
|
|
echo " Installed netavark + aardvark-dns (container DNS enabled)"
|
|
else
|
|
echo " WARNING: netavark/aardvark-dns not found in ISO — container DNS will not work"
|
|
fi
|
|
|
|
# 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 ] || [ -b /dev/mapper/archipelago_crypt ]; 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
|
|
|
|
# Force UTF-8 console with Terminus font (supports Unicode block chars for ASCII logo)
|
|
cat > /mnt/target/etc/default/console-setup <<'CONSOLESETUP'
|
|
ACTIVE_CONSOLES="/dev/tty[1-6]"
|
|
CHARMAP="UTF-8"
|
|
CODESET="Uni2"
|
|
FONTFACE="Terminus"
|
|
FONTSIZE="16"
|
|
CONSOLESETUP
|
|
|
|
# Suppress default Debian MOTD (our profile.d script handles the welcome)
|
|
echo -n > /mnt/target/etc/motd
|
|
rm -f /mnt/target/etc/motd.d/* 2>/dev/null || true
|
|
|
|
# Ensure reboot/shutdown work without sudo for the archipelago user
|
|
# profile.d only runs for login shells; .bashrc handles SSH interactive sessions
|
|
if ! grep -q '/sbin' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
|
|
echo 'export PATH="$PATH:/sbin:/usr/sbin"' >> /mnt/target/home/archipelago/.bashrc
|
|
fi
|
|
|
|
# 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)
|
|
# Normalize model IDs — map short/dotted names to full API model IDs
|
|
MODEL_MAP = {
|
|
"claude-haiku-4.5": "claude-haiku-4-5-20251001",
|
|
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
|
"claude-sonnet-4": "claude-sonnet-4-20250514",
|
|
"claude-sonnet-4.5": "claude-sonnet-4-5-20250514",
|
|
"claude-sonnet-4-5": "claude-sonnet-4-5-20250514",
|
|
"claude-opus-4": "claude-opus-4-20250514",
|
|
}
|
|
m = data.get("model", "")
|
|
if m in MODEL_MAP: data["model"] = MODEL_MAP[m]
|
|
body = json.dumps(data).encode()
|
|
if not API_KEY:
|
|
err = json.dumps({"type":"error","error":{"type":"auth_error","message":"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat."}}).encode()
|
|
self.send_response(401); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(err))); self.end_headers(); self.wfile.write(err); return
|
|
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("WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions")
|
|
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=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
|
|
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,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \
|
|
--disable-session-crashed-bubble \
|
|
--disable-save-password-bubble \
|
|
--disable-suggestions-service \
|
|
--password-store=basic \
|
|
--disable-component-update \
|
|
--credentials_enable_service=false \
|
|
--disable-gpu \
|
|
--disable-breakpad \
|
|
--disable-metrics \
|
|
--disable-metrics-reporting \
|
|
--metrics-recording-only \
|
|
--disable-domain-reliability \
|
|
--disable-background-networking \
|
|
--disable-background-timer-throttling \
|
|
--disable-backgrounding-occluded-windows \
|
|
--user-data-dir=/var/lib/archipelago/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 30); 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=90
|
|
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 run chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable; then
|
|
ok "UEFI bootloader installed (removable/fallback path)"
|
|
else
|
|
warn "UEFI removable install failed, trying standard..."
|
|
if run chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago; then
|
|
ok "UEFI bootloader installed (standard)"
|
|
else
|
|
fail "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 run chroot /mnt/target grub-install --target=${GRUB_BIOS_TARGET} "${TARGET_DISK}"; then
|
|
ok "Legacy BIOS bootloader installed"
|
|
else
|
|
warn "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
|
|
# GFX fallback for hardware without graphical GRUB support
|
|
echo 'GRUB_GFXMODE=auto' >> /mnt/target/etc/default/grub
|
|
echo 'GRUB_GFXPAYLOAD_LINUX=keep' >> /mnt/target/etc/default/grub
|
|
echo 'GRUB_TERMINAL_OUTPUT=gfxterm' >> /mnt/target/etc/default/grub
|
|
|
|
# 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 and ACPI 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
|
|
if ! grep -q "acpi=force" /mnt/target/etc/default/grub; then
|
|
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 acpi=force"/' \
|
|
/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..."
|
|
run chroot /mnt/target update-initramfs -u -k all
|
|
|
|
run 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
|
|
|
|
# Bootstrap switchover — checks when local Bitcoin finishes IBD and switches services
|
|
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.service <<'BSSERVICE'
|
|
[Unit]
|
|
Description=Switch Bitcoin-dependent services from bootstrap to local node
|
|
After=archipelago-first-boot-containers.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
User=archipelago
|
|
ExecStart=/opt/archipelago/scripts/bootstrap-switchover.sh
|
|
BSSERVICE
|
|
|
|
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.timer <<'BSTIMER'
|
|
[Unit]
|
|
Description=Periodically check if local Bitcoin is synced and switch from bootstrap
|
|
|
|
[Timer]
|
|
OnBootSec=10min
|
|
OnUnitActiveSec=5min
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
BSTIMER
|
|
|
|
# Copy bootstrap config to install target
|
|
if [ -f "$BOOT_MEDIA/archipelago/bootstrap.conf" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/bootstrap.conf" /mnt/target/opt/archipelago/bootstrap.conf
|
|
chmod 600 /mnt/target/opt/archipelago/bootstrap.conf
|
|
chown root:root /mnt/target/opt/archipelago/bootstrap.conf
|
|
fi
|
|
|
|
# Copy bootstrap switchover script
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/bootstrap-switchover.sh" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/bootstrap-switchover.sh" /mnt/target/opt/archipelago/scripts/
|
|
chmod +x /mnt/target/opt/archipelago/scripts/bootstrap-switchover.sh
|
|
fi
|
|
|
|
# Enable services
|
|
chroot /mnt/target systemctl enable archipelago-bootstrap-switchover.timer 2>/dev/null || true
|
|
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
|
|
# Enable claude-api-proxy (create symlink manually — chroot systemctl can fail)
|
|
chroot /mnt/target systemctl enable claude-api-proxy.service 2>/dev/null || \
|
|
ln -sf /etc/systemd/system/claude-api-proxy.service /mnt/target/etc/systemd/system/multi-user.target.wants/claude-api-proxy.service 2>/dev/null || true
|
|
|
|
# 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
|
|
version=__BUILD_VERSION__
|
|
build=__BUILD_NUM__
|
|
commit=__GIT_SHORT__
|
|
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 ""
|
|
hrule
|
|
echo ""
|
|
|
|
# Celebration animation if TUI available
|
|
[ "${TUI_AVAILABLE:-}" = "1" ] && tui_complete
|
|
|
|
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
|
|
|
|
# Show completion message, unmount USB, then reboot
|
|
# All done inline — no separate script needed (avoids /bin/bash dependency on squashfs)
|
|
|
|
echo ""
|
|
if [ "${TUI_AVAILABLE:-}" = "1" ]; then
|
|
tui_flash_remove_usb
|
|
else
|
|
p "${ORANGE}>>> REMOVE THE USB DRIVE NOW <<<${NC}"
|
|
fi
|
|
echo ""
|
|
p "${ORANGE_DIM}Press Enter to reboot (or wait 30 seconds)${NC}"
|
|
|
|
# Suppress kernel messages (squashfs errors when USB is pulled)
|
|
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
|
|
|
|
# Lazy-unmount live filesystem
|
|
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
|
|
|
|
# Wait for Enter or timeout
|
|
read -t 30 -s 2>/dev/null || true
|
|
|
|
echo ""
|
|
p "${ORANGE_DIM}Rebooting...${NC}"
|
|
sleep 1
|
|
|
|
# Force reboot — multiple methods, first one that works wins
|
|
echo b > /proc/sysrq-trigger 2>/dev/null || \
|
|
/sbin/reboot -f 2>/dev/null || \
|
|
/usr/sbin/reboot -f 2>/dev/null || \
|
|
kill -9 1 2>/dev/null
|
|
INSTALLER_SCRIPT
|
|
|
|
# Inject build version into auto-install.sh (heredoc is single-quoted, can't expand variables)
|
|
sed -i "s|__BUILD_VERSION__|${BUILD_VERSION}|g" "$ARCH_DIR/auto-install.sh"
|
|
sed -i "s|__BUILD_NUM__|${BUILD_NUM}|g" "$ARCH_DIR/auto-install.sh"
|
|
sed -i "s|__GIT_SHORT__|${GIT_SHORT}|g" "$ARCH_DIR/auto-install.sh"
|
|
|
|
# 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 — fallback to text mode on hardware without gfxterm
|
|
if loadfont ($root)/boot/grub/font.pf2; then
|
|
set gfxmode=auto
|
|
set gfxpayload=keep
|
|
insmod gfxterm
|
|
insmod png
|
|
terminal_output gfxterm
|
|
else
|
|
terminal_output console
|
|
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 acpi=force
|
|
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 acpi=force
|
|
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
|
|
MENU BACKGROUND splash.png
|
|
MENU RESOLUTION 1024 768
|
|
MENU VSHIFT 20
|
|
MENU HSHIFT 6
|
|
MENU WIDTH 68
|
|
MENU MARGIN 2
|
|
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 acpi=force
|
|
MENU DEFAULT
|
|
|
|
LABEL install-verbose
|
|
MENU LABEL Install (verbose output)
|
|
KERNEL /live/vmlinuz
|
|
APPEND initrd=/live/initrd.img boot=live components loglevel=4 acpi=force
|
|
|
|
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
|
|
# UEFI fix: append efi.img as a real EFI System Partition (ESP) in GPT
|
|
# instead of embedding it as "basic data". Strict UEFI firmware requires
|
|
# the correct ESP type GUID (C12A7328-F81F-11D2-BA4B-00A0C93EC93B).
|
|
# This is the same approach used by Arch Linux ISOs.
|
|
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 --interval:appended_partition_2:all:: \
|
|
-no-emul-boot \
|
|
-appended_part_as_gpt \
|
|
-append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B "$WORK_DIR/efi.img" \
|
|
-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 ""
|