1. registries.conf includes docker.io search + fallback 23.182.128.160 2. First-boot pull_with_fallback() tries primary then fallback registry 3. FileBrowser created with noauth config on persistent volume 4. Backend dynamic registries.json pre-created in ISO 5. Filebrowser password secret created for token flow Fixes: apps stuck at 0% download, filebrowser not working, dynamic catalog not loading on fresh installs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3387 lines
136 KiB
Bash
Executable File
3387 lines
136 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:trixie 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 = "git.tx1138.com"
|
|
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:trixie
|
|
|
|
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 trixie main non-free-firmware" > /etc/apt/sources.list && \
|
|
echo "deb http://deb.debian.org/debian trixie-updates main non-free-firmware" >> /etc/apt/sources.list && \
|
|
echo "deb http://deb.debian.org/debian-security trixie-security main non-free-firmware" >> /etc/apt/sources.list && \
|
|
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 \
|
|
passt \
|
|
aardvark-dns \
|
|
netavark \
|
|
nftables \
|
|
fuse-overlayfs \
|
|
tor \
|
|
python3 \
|
|
curl \
|
|
git \
|
|
vim-tiny \
|
|
ca-certificates \
|
|
openssl \
|
|
chrony \
|
|
locales \
|
|
console-setup \
|
|
keyboard-configuration \
|
|
cryptsetup \
|
|
cryptsetup-initramfs \
|
|
e2fsprogs \
|
|
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/trixie.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
|
|
curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list && \
|
|
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.service /etc/systemd/system/archipelago-wg.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
|
|
# archipelago-wg + wg-address: enabled by first-boot after WG key is generated
|
|
# nostr-vpn: enabled by first-boot after Nostr identity is generated
|
|
# (env file doesn't exist until onboarding, so pre-enabling causes crash-loop)
|
|
|
|
# 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 /var/lib/archipelago/config /var/lib/archipelago/containers /var/lib/archipelago/nostr-relay /var/lib/archipelago/nostr-vpn && \
|
|
mkdir -p /etc/archipelago && \
|
|
mkdir -p /opt/archipelago/bin /opt/archipelago/scripts /opt/archipelago/web-ui && \
|
|
mkdir -p /var/lib/archipelago/data/cloud/Documents /var/lib/archipelago/data/cloud/Photos /var/lib/archipelago/data/cloud/Music /var/lib/archipelago/data/cloud/Videos /var/lib/archipelago/data/cloud/Downloads && \
|
|
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.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/archipelago-wg.service" "$WORK_DIR/archipelago-wg.service"
|
|
echo " Using archipelago-wg.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:trixie 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 \
|
|
trixie /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"
|
|
|
|
# netavark + aardvark-dns are installed in the rootfs via Dockerfile.rootfs (Debian 13 packages).
|
|
# Do NOT copy from the build host — the host may run a different glibc version.
|
|
echo " netavark + aardvark-dns: included in rootfs (Debian 13 packages)"
|
|
|
|
# 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 backend binary: local release build → local install → remote → container build
|
|
BACKEND_CAPTURED=0
|
|
|
|
# Check for local release binary first (works for both BUILD_FROM_SOURCE and normal mode)
|
|
LOCAL_RELEASE="$(cd "$SCRIPT_DIR/.." && pwd)/core/target/release/archipelago"
|
|
if [ -f "$LOCAL_RELEASE" ]; then
|
|
cp "$LOCAL_RELEASE" "$ARCH_DIR/bin/archipelago"
|
|
chmod +x "$ARCH_DIR/bin/archipelago"
|
|
echo " ✅ Backend from local release build ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
|
|
BACKEND_CAPTURED=1
|
|
fi
|
|
|
|
if [ "$BACKEND_CAPTURED" = "0" ] && [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
|
# Direct copy from ARCHIPELAGO_BIN env or local install
|
|
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-trixie 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)
|
|
# NOTE: The container image must be built against Debian 13's GLIBC (2.40).
|
|
# If built against a newer GLIBC, the binary will fail at runtime.
|
|
# Rebuild with: FROM debian:13 AS builder
|
|
echo " Extracting NostrVPN binary..."
|
|
_NVPN_IMG="${NOSTR_VPN_IMAGE:-git.tx1138.com/lfg2025/nostr-vpn:v0.3.7}"
|
|
NVPN_IMAGE_ID="$($CONTAINER_CMD images -q "$_NVPN_IMG" 2>/dev/null)"
|
|
if [ -z "$NVPN_IMAGE_ID" ]; then
|
|
$CONTAINER_CMD pull "$_NVPN_IMG" 2>/dev/null || true
|
|
fi
|
|
NVPN_CONTAINER=$($CONTAINER_CMD create "$_NVPN_IMG" 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
|
|
# Check GLIBC compatibility — Debian 13 (Trixie) has GLIBC 2.40
|
|
if [ -f "$ARCH_DIR/bin/nvpn" ]; then
|
|
NVPN_GLIBC=$(objdump -T "$ARCH_DIR/bin/nvpn" 2>/dev/null | grep -oP 'GLIBC_\K[0-9.]+' | sort -V | tail -1)
|
|
if [ -n "$NVPN_GLIBC" ]; then
|
|
# Compare: if required GLIBC > 2.40, warn
|
|
if printf '%s\n' "2.40" "$NVPN_GLIBC" | sort -V | tail -1 | grep -qv "^2\.40$"; then
|
|
echo " ⚠ WARNING: nvpn binary requires GLIBC $NVPN_GLIBC but Debian 13 has 2.40"
|
|
echo " ⚠ The nvpn daemon will fail at runtime. Rebuild the container against Debian 13."
|
|
echo " ⚠ VPN invite/status will still work via Rust backend config.toml fallback."
|
|
else
|
|
echo " ✅ nvpn GLIBC compatibility OK (requires $NVPN_GLIBC, target has 2.40)"
|
|
fi
|
|
fi
|
|
fi
|
|
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 git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null)"
|
|
if [ -z "$RELAY_IMAGE" ]; then
|
|
$CONTAINER_CMD pull git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null || true
|
|
fi
|
|
RELAY_CONTAINER=$($CONTAINER_CMD create git.tx1138.com/lfg2025/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"
|
|
echo " Installing frontend dependencies..."
|
|
npm ci --prefer-offline 2>&1 | tail -3
|
|
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."
|
|
# Marker file: first-boot-containers.sh checks this to skip app creation
|
|
touch "$ARCH_DIR/.unbundled"
|
|
IMAGES_DIR="$ARCH_DIR/container-images"
|
|
# Clean stale images from previous builds (e.g. bundled build tars leaking into unbundled)
|
|
rm -rf "$IMAGES_DIR"
|
|
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
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_relay
|
|
HiddenServicePort 7777 127.0.0.1:7777
|
|
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 relay; 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 relay; 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,nostr-relay,nostr-vpn,tor-hostnames,wireguard}
|
|
mkdir -p /mnt/target/var/lib/archipelago/containers/storage
|
|
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
|
|
# Copy relay config from rootfs (LUKS mount hides what the Dockerfile put there)
|
|
if [ -f /mnt/target/etc/archipelago/nostr-relay-config.toml ]; then
|
|
cp /mnt/target/etc/archipelago/nostr-relay-config.toml /mnt/target/var/lib/archipelago/nostr-relay/config.toml
|
|
fi
|
|
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
|
|
|
|
# Configure LUKS auto-unlock: three layers to ensure it works
|
|
# Layer 1: cryptsetup-initramfs config (tells update-initramfs to embed key)
|
|
mkdir -p /mnt/target/etc/cryptsetup-initramfs
|
|
cat > /mnt/target/etc/cryptsetup-initramfs/conf <<'CRYPTCONF'
|
|
KEYFILE_PATTERN="/root/.luks-*.key"
|
|
UMASK=0077
|
|
CRYPTCONF
|
|
|
|
# Layer 2: initramfs hook to force-copy key file
|
|
mkdir -p /mnt/target/etc/initramfs-tools/hooks
|
|
cat > /mnt/target/etc/initramfs-tools/hooks/archipelago-luks <<'LUKSHOOK'
|
|
#!/bin/sh
|
|
PREREQ=""
|
|
prereqs() { echo "$PREREQ"; }
|
|
case $1 in prereqs) prereqs; exit 0;; esac
|
|
. /usr/share/initramfs-tools/hook-functions
|
|
if [ -f /root/.luks-archipelago.key ]; then
|
|
mkdir -p "${DESTDIR}/root"
|
|
cp /root/.luks-archipelago.key "${DESTDIR}/root/.luks-archipelago.key"
|
|
chmod 600 "${DESTDIR}/root/.luks-archipelago.key"
|
|
fi
|
|
if [ -f /etc/crypttab ]; then
|
|
mkdir -p "${DESTDIR}/etc"
|
|
cp /etc/crypttab "${DESTDIR}/etc/crypttab"
|
|
fi
|
|
copy_exec /sbin/cryptsetup
|
|
LUKSHOOK
|
|
chmod +x /mnt/target/etc/initramfs-tools/hooks/archipelago-luks
|
|
|
|
# Layer 3: systemd service as fallback — unlocks LUKS early if initramfs missed it
|
|
cat > /mnt/target/etc/systemd/system/archipelago-luks-unlock.service <<'LUKSUNIT'
|
|
[Unit]
|
|
Description=Unlock Archipelago LUKS data partition
|
|
DefaultDependencies=no
|
|
Before=local-fs-pre.target
|
|
After=systemd-udevd.service
|
|
Wants=systemd-udevd.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=/bin/bash -c '\
|
|
if [ -e /dev/mapper/archipelago-data ]; then exit 0; fi; \
|
|
DATA_DEV=$(blkid -t TYPE=crypto_LUKS -o device 2>/dev/null | head -1); \
|
|
if [ -z "$DATA_DEV" ]; then exit 0; fi; \
|
|
cryptsetup open --type luks2 --key-file /root/.luks-archipelago.key "$DATA_DEV" archipelago-data'
|
|
|
|
[Install]
|
|
WantedBy=local-fs-pre.target
|
|
LUKSUNIT
|
|
chroot /mnt/target systemctl enable archipelago-luks-unlock.service 2>/dev/null || \
|
|
ln -sf /etc/systemd/system/archipelago-luks-unlock.service /mnt/target/etc/systemd/system/local-fs-pre.target.wants/archipelago-luks-unlock.service
|
|
|
|
# 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,nofail,x-systemd.device-timeout=60 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 registries (primary + fallback)
|
|
cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF'
|
|
unqualified-search-registries = ["docker.io"]
|
|
|
|
[[registry]]
|
|
location = "git.tx1138.com"
|
|
insecure = true
|
|
|
|
[[registry]]
|
|
location = "23.182.128.160:3000"
|
|
insecure = true
|
|
REGCONF
|
|
chown -R 1000:1000 /mnt/target/home/archipelago/.config
|
|
|
|
# Pre-create dynamic registry config for the backend (fallback registries)
|
|
mkdir -p /mnt/target/var/lib/archipelago/config
|
|
cat > /mnt/target/var/lib/archipelago/config/registries.json <<'DYNREG'
|
|
{
|
|
"registries": [
|
|
{"url": "git.tx1138.com/lfg2025", "name": "Archipelago Primary", "tls_verify": true, "enabled": true, "priority": 0},
|
|
{"url": "23.182.128.160:3000/lfg2025", "name": "Archipelago Fallback", "tls_verify": false, "enabled": true, "priority": 10}
|
|
]
|
|
}
|
|
DYNREG
|
|
chown -R 1000:1000 /mnt/target/var/lib/archipelago/config
|
|
|
|
# Configure podman to use netavark backend (enables container DNS on archy-net).
|
|
# netavark + aardvark-dns binaries come from the rootfs (Debian 13 apt packages).
|
|
if [ -f /mnt/target/usr/lib/podman/netavark ]; then
|
|
mkdir -p /mnt/target/home/archipelago/.config/containers
|
|
cat > /mnt/target/home/archipelago/.config/containers/containers.conf <<'CONTAINERSCONF'
|
|
[network]
|
|
network_backend = "netavark"
|
|
default_rootless_network_cmd = "pasta"
|
|
|
|
[engine]
|
|
image_copy_tmp_dir = "/var/lib/archipelago/containers/tmp"
|
|
CONTAINERSCONF
|
|
mkdir -p /mnt/target/var/lib/archipelago/containers/tmp
|
|
chown -R 1000:1000 /mnt/target/var/lib/archipelago/containers/tmp
|
|
chown -R 1000:1000 /mnt/target/home/archipelago/.config/containers
|
|
echo " Configured netavark backend (container DNS enabled)"
|
|
else
|
|
echo " WARNING: netavark not found in rootfs — 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
|
|
|
|
# Mark unbundled mode so first-boot only creates FileBrowser (user installs apps from Marketplace)
|
|
if [ -f "$BOOT_MEDIA/archipelago/.unbundled" ]; then
|
|
touch /mnt/target/opt/archipelago/.unbundled
|
|
echo " Unbundled mode: apps install on-demand from Marketplace"
|
|
fi
|
|
|
|
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
|
|
# Power commands need sudo on SSH sessions (polkit denies without local seat)
|
|
if ! grep -q 'alias reboot' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
|
|
cat >> /mnt/target/home/archipelago/.bashrc <<'ALIASES'
|
|
alias reboot='sudo systemctl reboot'
|
|
alias shutdown='sudo shutdown'
|
|
alias halt='sudo systemctl halt'
|
|
alias poweroff='sudo systemctl poweroff'
|
|
ALIASES
|
|
fi
|
|
|
|
# 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
|
|
|
|
# Brand GRUB as Archipelago (default says "Debian GNU/Linux")
|
|
sed -i 's/^GRUB_DISTRIBUTOR=.*/GRUB_DISTRIBUTOR="Archipelago"/' /mnt/target/etc/default/grub
|
|
grep -q '^GRUB_DISTRIBUTOR' /mnt/target/etc/default/grub || echo 'GRUB_DISTRIBUTOR="Archipelago"' >> /mnt/target/etc/default/grub
|
|
|
|
# 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
|
|
# Configure clean boot: splash, suppress kernel noise, hide cursor
|
|
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT=".*"/GRUB_CMDLINE_LINUX_DEFAULT="quiet splash loglevel=0 rd.systemd.show_status=false vt.global_cursor_default=0 acpi=force"/' \
|
|
/mnt/target/etc/default/grub 2>/dev/null || true
|
|
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
|
|
chroot /mnt/target systemctl enable nostr-vpn.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
|
|
|
|
# Serial console for QEMU/headless testing
|
|
insmod serial
|
|
serial --unit=0 --speed=115200
|
|
terminal_input serial console
|
|
terminal_output serial console
|
|
|
|
# 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 serial
|
|
else
|
|
terminal_output console serial
|
|
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 console=ttyS0,115200 console=tty0
|
|
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 ""
|