Podman was caching the rootfs Docker layers, meaning firmware packages and sources.list changes were never picked up on rebuild. Force fresh build every time since the rootfs tar is the real cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1977 lines
84 KiB
Bash
Executable File
1977 lines
84 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: Download/extract Debian Live base ISO (~80 lines)
|
|
# components.sh — Step 3: Add Archipelago components (binary, configs, web UI) (~120 lines)
|
|
# container-images.sh — Step 3b: Bundle container images for offline install (~330 lines)
|
|
# auto-install-script.sh — Step 4: Generate the embedded auto-install.sh (~615 lines)
|
|
# boot-config.sh — Step 5: Configure live boot auto-start + overlay squashfs (~215 lines)
|
|
# create-iso.sh — Step 6: Build final bootable ISO with xorriso/grub (~140 lines)
|
|
# Each lib/ script exports functions; main script sources them and calls in sequence.
|
|
# DO NOT split until tested on the build server — this is critical infrastructure.
|
|
# ---
|
|
#
|
|
|
|
set -e
|
|
|
|
# Source pinned image versions (single source of truth)
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
[ -f "$SCRIPT_DIR/../scripts/image-versions.sh" ] && . "$SCRIPT_DIR/../scripts/image-versions.sh"
|
|
|
|
# Configuration
|
|
DEV_SERVER="${DEV_SERVER:-archipelago@192.168.1.228}"
|
|
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}"
|
|
UNBUNDLED="${UNBUNDLED:-0}"
|
|
ARCH="${ARCH:-x86_64}"
|
|
|
|
# Architecture-dependent variables
|
|
case "$ARCH" in
|
|
x86_64|amd64)
|
|
ARCH="x86_64"
|
|
DEB_ARCH="amd64"
|
|
LINUX_IMAGE_PKG="linux-image-amd64"
|
|
GRUB_EFI_PKG="grub-efi-amd64"
|
|
GRUB_EFI_SIGNED_PKG="grub-efi-amd64-signed"
|
|
GRUB_PC_PKG="grub-pc-bin"
|
|
GRUB_TARGET="x86_64-efi"
|
|
GRUB_BIOS_TARGET="i386-pc"
|
|
CONTAINER_PLATFORM="linux/amd64"
|
|
LIB_DIR="${LIB_DIR}"
|
|
;;
|
|
arm64|aarch64)
|
|
ARCH="arm64"
|
|
DEB_ARCH="arm64"
|
|
LINUX_IMAGE_PKG="linux-image-arm64"
|
|
GRUB_EFI_PKG="grub-efi-arm64"
|
|
GRUB_EFI_SIGNED_PKG="grub-efi-arm64-signed"
|
|
GRUB_PC_PKG=""
|
|
GRUB_TARGET="arm64-efi"
|
|
GRUB_BIOS_TARGET=""
|
|
CONTAINER_PLATFORM="linux/arm64"
|
|
LIB_DIR="aarch64-linux-gnu"
|
|
;;
|
|
*)
|
|
echo "❌ Unsupported architecture: $ARCH (use x86_64 or arm64)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
WORK_DIR="$SCRIPT_DIR/build/auto-installer"
|
|
OUTPUT_DIR="$SCRIPT_DIR/results"
|
|
ROOTFS_DIR="$WORK_DIR/rootfs"
|
|
INSTALLER_DIR="$WORK_DIR/installer"
|
|
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ Building Archipelago UNBUNDLED ISO (no pre-loaded apps) ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
else
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ Building Archipelago Auto-Installer ISO (StartOS-like) ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
fi
|
|
echo ""
|
|
if [ "$BUILD_FROM_SOURCE" = "1" ]; then
|
|
echo "📦 Mode: Building from SOURCE CODE"
|
|
elif [ "$UNBUNDLED" = "1" ]; then
|
|
echo "📦 Mode: UNBUNDLED (apps downloaded on-demand from Marketplace)"
|
|
echo " Server: $DEV_SERVER (backend + web UI only)"
|
|
else
|
|
echo "📦 Mode: Capturing LIVE SERVER state"
|
|
echo " Server: $DEV_SERVER"
|
|
fi
|
|
echo "🏗️ Architecture: $ARCH ($DEB_ARCH)"
|
|
echo ""
|
|
|
|
# Check for required tools
|
|
check_tools() {
|
|
local missing=""
|
|
local can_install=false
|
|
|
|
# Check if we can auto-install (running as root on Debian/Ubuntu)
|
|
if [ "$EUID" -eq 0 ] && [ -f /etc/debian_version ]; then
|
|
can_install=true
|
|
fi
|
|
|
|
# Check for docker or podman
|
|
if command -v docker >/dev/null 2>&1; then
|
|
CONTAINER_CMD="docker"
|
|
elif command -v podman >/dev/null 2>&1; then
|
|
CONTAINER_CMD="podman"
|
|
else
|
|
missing="$missing docker-or-podman"
|
|
fi
|
|
|
|
if ! command -v xorriso >/dev/null 2>&1; then
|
|
missing="$missing xorriso"
|
|
fi
|
|
if ! command -v 7z >/dev/null 2>&1 && ! command -v 7za >/dev/null 2>&1; then
|
|
missing="$missing p7zip-full"
|
|
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" == *"p7zip-full"* ]]; then
|
|
apt-get install -y p7zip-full
|
|
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 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"
|
|
}
|
|
|
|
check_tools
|
|
|
|
mkdir -p "$WORK_DIR"
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
# =============================================================================
|
|
# STEP 1: Build complete root filesystem using Docker
|
|
# =============================================================================
|
|
echo "📦 Step 1: Building root filesystem..."
|
|
|
|
ROOTFS_TAR="$WORK_DIR/archipelago-rootfs.tar"
|
|
|
|
if [ ! -f "$ROOTFS_TAR" ] || [ "$1" == "--rebuild" ]; then
|
|
echo " Using Docker to create Debian root filesystem..."
|
|
|
|
# Create a Dockerfile for building the rootfs
|
|
cat > "$WORK_DIR/Dockerfile.rootfs" <<DOCKERFILE
|
|
FROM debian:bookworm
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Enable non-free-firmware repo — replace DEB822 sources with traditional format
|
|
# (DEB822 sed was silently failing, so just overwrite with known-good sources.list)
|
|
RUN echo "deb http://deb.debian.org/debian bookworm main non-free-firmware" > /etc/apt/sources.list && \
|
|
echo "deb http://deb.debian.org/debian bookworm-updates main non-free-firmware" >> /etc/apt/sources.list && \
|
|
echo "deb http://deb.debian.org/debian-security bookworm-security main non-free-firmware" >> /etc/apt/sources.list && \
|
|
rm -f /etc/apt/sources.list.d/debian.sources
|
|
|
|
# Install all packages we need including nginx, podman, tor, and openssl (for self-signed certs)
|
|
RUN apt-get update && apt-get install -y \
|
|
${LINUX_IMAGE_PKG} \
|
|
${GRUB_EFI_PKG} \
|
|
${GRUB_EFI_SIGNED_PKG} \
|
|
${GRUB_PC_PKG} shim-signed \
|
|
systemd \
|
|
systemd-sysv \
|
|
dbus \
|
|
sudo \
|
|
network-manager \
|
|
openssh-server \
|
|
nginx \
|
|
podman \
|
|
tor \
|
|
curl \
|
|
wget \
|
|
git \
|
|
htop \
|
|
vim-tiny \
|
|
ca-certificates \
|
|
openssl \
|
|
chrony \
|
|
locales \
|
|
console-setup \
|
|
keyboard-configuration \
|
|
firmware-realtek \
|
|
firmware-iwlwifi \
|
|
firmware-misc-nonfree \
|
|
intel-microcode \
|
|
amd64-microcode \
|
|
&& apt-get clean \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install Tailscale from official repo
|
|
RUN curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
|
|
curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list && \
|
|
apt-get update && apt-get install -y 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
|
|
|
|
# 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
|
|
|
|
# Remove policy-rc.d so services can start on first boot
|
|
RUN rm -f /usr/sbin/policy-rc.d
|
|
|
|
# Create directories (including Cloud storage for FileBrowser)
|
|
RUN mkdir -p /var/lib/archipelago/{data,config,containers} && \
|
|
mkdir -p /etc/archipelago && \
|
|
mkdir -p /opt/archipelago/{bin,scripts,web-ui} && \
|
|
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads} && \
|
|
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
|
|
|
|
# Clean up
|
|
RUN apt-get clean && \
|
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
DOCKERFILE
|
|
|
|
# Copy nginx snippets for HTTPS (PWA, app proxies)
|
|
if [ -d "$SCRIPT_DIR/configs/snippets" ]; then
|
|
mkdir -p "$WORK_DIR/snippets"
|
|
cp "$SCRIPT_DIR/configs/snippets/"*.conf "$WORK_DIR/snippets/" 2>/dev/null || true
|
|
echo " Using nginx snippets from configs/snippets/"
|
|
else
|
|
mkdir -p "$WORK_DIR/snippets"
|
|
echo " ⚠ No nginx snippets found, HTTPS features may not work"
|
|
fi
|
|
|
|
# Use nginx config from configs/ (includes app proxies for Nextcloud, Vaultwarden, etc.)
|
|
if [ -f "$SCRIPT_DIR/configs/nginx-archipelago.conf" ]; then
|
|
cp "$SCRIPT_DIR/configs/nginx-archipelago.conf" "$WORK_DIR/nginx-archipelago.conf"
|
|
echo " Using nginx config from configs/nginx-archipelago.conf"
|
|
else
|
|
echo " ⚠ configs/nginx-archipelago.conf not found, using minimal config"
|
|
cat > "$WORK_DIR/nginx-archipelago.conf" <<'NGINXCONF'
|
|
server {
|
|
listen 80;
|
|
server_name _;
|
|
root /opt/archipelago/web-ui;
|
|
index index.html;
|
|
location / { try_files $uri $uri/ /index.html; }
|
|
location /archipelago/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
|
location /rpc/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; }
|
|
location /ws { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400s; }
|
|
}
|
|
NGINXCONF
|
|
fi
|
|
|
|
# Copy udev rule for mesh radio stable naming
|
|
if [ -f "$SCRIPT_DIR/configs/99-mesh-radio.rules" ]; then
|
|
cp "$SCRIPT_DIR/configs/99-mesh-radio.rules" "$WORK_DIR/99-mesh-radio.rules"
|
|
echo " Using 99-mesh-radio.rules from configs/"
|
|
fi
|
|
|
|
# Copy update service and timer
|
|
if [ -f "$SCRIPT_DIR/configs/archipelago-update.service" ]; then
|
|
cp "$SCRIPT_DIR/configs/archipelago-update.service" "$WORK_DIR/archipelago-update.service"
|
|
cp "$SCRIPT_DIR/configs/archipelago-update.timer" "$WORK_DIR/archipelago-update.timer"
|
|
echo " Using archipelago-update.service + timer from configs/"
|
|
fi
|
|
|
|
# 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
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
|
Environment="ARCHIPELAGO_DEV_MODE=true"
|
|
ExecStart=/usr/local/bin/archipelago
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SYSTEMDSERVICE
|
|
fi
|
|
|
|
echo " Building $CONTAINER_CMD image (this may take a few minutes)..."
|
|
$CONTAINER_CMD build --no-cache --platform $CONTAINER_PLATFORM -t archipelago-rootfs -f "$WORK_DIR/Dockerfile.rootfs" "$WORK_DIR"
|
|
|
|
echo " Exporting filesystem..."
|
|
$CONTAINER_CMD create --platform $CONTAINER_PLATFORM --name archipelago-rootfs-tmp archipelago-rootfs
|
|
$CONTAINER_CMD export archipelago-rootfs-tmp > "$ROOTFS_TAR"
|
|
$CONTAINER_CMD rm archipelago-rootfs-tmp
|
|
|
|
echo "✅ Root filesystem created: $(du -h "$ROOTFS_TAR" | cut -f1)"
|
|
else
|
|
echo "✅ Using cached root filesystem: $(du -h "$ROOTFS_TAR" | cut -f1)"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# STEP 2: Create installer environment
|
|
# =============================================================================
|
|
echo ""
|
|
echo "📦 Step 2: Creating installer environment..."
|
|
|
|
# Download Debian Live as our installer base
|
|
BASE_ISO="$WORK_DIR/debian-live-installer.iso"
|
|
EXPECTED_SIZE=350000000 # ~350MB min (Debian 12 Live standard ~600MB)
|
|
|
|
# Check if file exists and is complete
|
|
if [ -f "$BASE_ISO" ]; then
|
|
CURRENT_SIZE=$(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0)
|
|
if [ "$CURRENT_SIZE" -ge "$EXPECTED_SIZE" ]; then
|
|
echo " ✅ Debian Live base already downloaded"
|
|
else
|
|
echo " Found incomplete download ($(($CURRENT_SIZE / 1024 / 1024))MB), removing..."
|
|
rm -f "$BASE_ISO"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -f "$BASE_ISO" ]; then
|
|
echo " Downloading Debian Live base (352MB)..."
|
|
echo " (This may take 5-10 minutes depending on network speed)"
|
|
|
|
# Use wget without -O so --continue actually works
|
|
# Download with the ugly SourceForge filename, then rename
|
|
# Use Debian 12 (Bookworm) to match the rootfs — NOT Debian 13 (Trixie)
|
|
ISO_URL="https://cdimage.debian.org/cdimage/archive/12.10.0-live/${DEB_ARCH}/iso-hybrid/debian-live-12.10.0-${DEB_ARCH}-standard.iso"
|
|
|
|
if command -v wget >/dev/null 2>&1; then
|
|
cd "$WORK_DIR"
|
|
wget --tries=10 --read-timeout=120 --continue --progress=bar:force \
|
|
--no-check-certificate \
|
|
"$ISO_URL" || {
|
|
echo " ❌ Download failed or incomplete"
|
|
echo " Partial file kept - run script again to continue"
|
|
exit 1
|
|
}
|
|
|
|
# Find the downloaded file — wget may use URL filename or follow redirects
|
|
FOUND_ISO=$(find "$WORK_DIR" -maxdepth 1 -name "debian-live-12*.iso" -o -name "download" 2>/dev/null | head -1)
|
|
if [ -n "$FOUND_ISO" ] && [ -f "$FOUND_ISO" ]; then
|
|
mv "$FOUND_ISO" "$BASE_ISO"
|
|
else
|
|
echo " ❌ Downloaded file not found in $WORK_DIR"
|
|
echo " Files present:"
|
|
ls -la "$WORK_DIR"/*.iso 2>/dev/null || echo " (no .iso files)"
|
|
exit 1
|
|
fi
|
|
else
|
|
# Fallback to curl (no resume support)
|
|
curl -L --location-trusted --retry 10 --retry-delay 5 \
|
|
--connect-timeout 60 --max-time 1800 \
|
|
-o "$BASE_ISO" \
|
|
"$ISO_URL" || {
|
|
echo " ❌ Download failed"
|
|
rm -f "$BASE_ISO"
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# Verify download size
|
|
FINAL_SIZE=$(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0)
|
|
if [ "$FINAL_SIZE" -lt "$EXPECTED_SIZE" ]; then
|
|
echo " ❌ Download incomplete: got $(($FINAL_SIZE / 1024 / 1024))MB, expected 352MB"
|
|
exit 1
|
|
fi
|
|
|
|
echo " ✅ Download complete ($(($FINAL_SIZE / 1024 / 1024))MB)"
|
|
fi
|
|
|
|
echo " Extracting installer base..."
|
|
INSTALLER_ISO="$WORK_DIR/installer-iso"
|
|
rm -rf "$INSTALLER_ISO"
|
|
mkdir -p "$INSTALLER_ISO"
|
|
cd "$INSTALLER_ISO"
|
|
# 7z returns exit code 2 for warnings (symlinks in ISO) — check for key files instead
|
|
7z x -y "$BASE_ISO" >/dev/null 2>&1 || 7za x -y "$BASE_ISO" >/dev/null 2>&1 || bsdtar -xf "$BASE_ISO" 2>/dev/null || true
|
|
if [ ! -d "$INSTALLER_ISO/live" ] || [ ! -f "$INSTALLER_ISO/live/vmlinuz" ]; then
|
|
echo " ❌ Failed to extract ISO. Install p7zip-full: sudo apt install p7zip-full"
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# STEP 3: Add Archipelago components
|
|
# =============================================================================
|
|
echo ""
|
|
echo "📦 Step 3: Adding Archipelago components..."
|
|
|
|
ARCH_DIR="$INSTALLER_ISO/archipelago"
|
|
mkdir -p "$ARCH_DIR"
|
|
mkdir -p "$ARCH_DIR/bin"
|
|
mkdir -p "$ARCH_DIR/scripts"
|
|
|
|
# Copy the pre-built rootfs
|
|
echo " Including root filesystem..."
|
|
cp "$ROOTFS_TAR" "$ARCH_DIR/rootfs.tar"
|
|
|
|
# Capture backend binary from live server
|
|
if [ "$BUILD_FROM_SOURCE" = "1" ]; then
|
|
echo " Building backend binary from source..."
|
|
else
|
|
echo " Capturing backend binary from live server..."
|
|
fi
|
|
|
|
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
|
|
BACKEND_CAPTURED=0
|
|
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
|
# Direct copy from local filesystem (when running on target with sudo)
|
|
if [ -f "/usr/local/bin/archipelago" ]; then
|
|
cp "/usr/local/bin/archipelago" "$ARCH_DIR/bin/archipelago"
|
|
chmod +x "$ARCH_DIR/bin/archipelago"
|
|
echo " ✅ Backend captured from local system ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
|
|
BACKEND_CAPTURED=1
|
|
fi
|
|
# Remote copy via SCP if local failed
|
|
if [ "$BACKEND_CAPTURED" = "0" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
|
if scp "$DEV_SERVER:/usr/local/bin/archipelago" "$ARCH_DIR/bin/archipelago" 2>/dev/null; then
|
|
chmod +x "$ARCH_DIR/bin/archipelago"
|
|
echo " ✅ Backend captured from remote server ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
|
|
BACKEND_CAPTURED=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "$BACKEND_CAPTURED" = "0" ]; then
|
|
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
|
echo " ⚠️ Could not capture from live server, building from source..."
|
|
fi
|
|
BACKEND_DOCKERFILE="$WORK_DIR/Dockerfile.backend"
|
|
cat > "$BACKEND_DOCKERFILE" <<'BACKENDFILE'
|
|
FROM rust:1.93-bookworm as builder
|
|
WORKDIR /build
|
|
COPY core ./core
|
|
RUN cd core && cargo build --release --bin archipelago
|
|
BACKENDFILE
|
|
|
|
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t archipelago-backend -f "$BACKEND_DOCKERFILE" "$SCRIPT_DIR/.." 2>&1 | tail -20; then
|
|
echo " Extracting backend binary..."
|
|
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform $CONTAINER_PLATFORM archipelago-backend)
|
|
$CONTAINER_CMD cp "$BACKEND_CONTAINER:/build/core/target/release/archipelago" "$ARCH_DIR/bin/" && \
|
|
echo " ✅ Backend binary built ($(du -h "$ARCH_DIR/bin/archipelago" | cut -f1))"
|
|
$CONTAINER_CMD rm "$BACKEND_CONTAINER"
|
|
else
|
|
echo " ❌ Backend build failed and server capture failed"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Capture web UI from live server
|
|
if [ "$BUILD_FROM_SOURCE" = "1" ]; then
|
|
echo " Building web UI from source..."
|
|
else
|
|
echo " Capturing web UI from live server..."
|
|
fi
|
|
mkdir -p "$ARCH_DIR/web-ui"
|
|
|
|
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
|
|
WEBUI_CAPTURED=0
|
|
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
|
# Direct copy from local filesystem (when running on target with sudo)
|
|
if [ -d "/opt/archipelago/web-ui" ] && [ "$(ls -A /opt/archipelago/web-ui 2>/dev/null)" ]; then
|
|
cp -r /opt/archipelago/web-ui/* "$ARCH_DIR/web-ui/"
|
|
echo " ✅ Web UI captured from local system ($(du -sh "$ARCH_DIR/web-ui" | cut -f1))"
|
|
WEBUI_CAPTURED=1
|
|
fi
|
|
# Remote copy via rsync if local failed
|
|
if [ "$WEBUI_CAPTURED" = "0" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
|
if rsync -az "$DEV_SERVER:/opt/archipelago/web-ui/" "$ARCH_DIR/web-ui/" 2>/dev/null && [ "$(ls -A "$ARCH_DIR/web-ui")" ]; then
|
|
echo " ✅ Web UI captured from remote server ($(du -sh "$ARCH_DIR/web-ui" | cut -f1))"
|
|
WEBUI_CAPTURED=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "$WEBUI_CAPTURED" = "0" ]; then
|
|
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
|
echo " ⚠️ Could not capture from live server, building from source..."
|
|
fi
|
|
cd "$SCRIPT_DIR/../neode-ui"
|
|
if npm run build 2>&1 | tail -5; then
|
|
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
|
|
echo " Including web UI from web/dist/neode-ui..."
|
|
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$ARCH_DIR/web-ui/"
|
|
echo " ✅ Web UI built ($(du -sh "$ARCH_DIR/web-ui" | cut -f1))"
|
|
fi
|
|
else
|
|
echo " ⚠️ Web UI build failed"
|
|
# Try to use existing build
|
|
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
|
|
echo " Using existing web UI build..."
|
|
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$ARCH_DIR/web-ui/"
|
|
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
|
|
echo " Using neode-ui/dist..."
|
|
cp -r "$SCRIPT_DIR/../neode-ui/dist/"* "$ARCH_DIR/web-ui/"
|
|
else
|
|
echo " ❌ No web UI available"
|
|
exit 1
|
|
fi
|
|
fi
|
|
cd "$SCRIPT_DIR"
|
|
fi
|
|
|
|
# Copy app manifests
|
|
if [ -d "$SCRIPT_DIR/../apps" ]; then
|
|
echo " Including app manifests..."
|
|
cp -r "$SCRIPT_DIR/../apps" "$ARCH_DIR/"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# STEP 3b: Bundle container images for offline installation
|
|
# =============================================================================
|
|
echo ""
|
|
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
echo "📦 Step 3b: SKIPPING container image bundling (UNBUNDLED mode)"
|
|
echo " Apps will be downloaded on-demand from the Marketplace after install."
|
|
IMAGES_DIR="$ARCH_DIR/container-images"
|
|
mkdir -p "$IMAGES_DIR"
|
|
else
|
|
echo "📦 Step 3b: Bundling container images for offline use..."
|
|
|
|
IMAGES_DIR="$ARCH_DIR/container-images"
|
|
mkdir -p "$IMAGES_DIR"
|
|
|
|
# When DEV_SERVER is set (and not localhost), try to capture images from live server
|
|
# so the ISO includes the same set as the dev server (including custom UIs: bitcoin-ui, lnd-ui).
|
|
IMAGES_CAPTURED_FROM_SERVER=0
|
|
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
|
echo " Capturing container images from live server ($DEV_SERVER)..."
|
|
# Patterns match against `podman images` repository names (not container names)
|
|
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server grafana uptime-kuma jellyfin vaultwarden searxng mariadb valkey nginx-alpine portainer photoprism nextcloud nginx-proxy-manager onlyoffice adguard"
|
|
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
|
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
|
for p in $SAVED_LIST; do
|
|
if [ -n "$p" ] && scp "$DEV_SERVER:$REMOTE_TMP/$p.tar" "$IMAGES_DIR/$p.tar" 2>/dev/null; then
|
|
echo " ✅ Captured from server: $p.tar"
|
|
IMAGES_CAPTURED_FROM_SERVER=1
|
|
fi
|
|
done
|
|
ssh "$DEV_SERVER" "rm -rf $REMOTE_TMP" 2>/dev/null || true
|
|
if [ "$IMAGES_CAPTURED_FROM_SERVER" = "0" ]; then
|
|
echo " ⚠️ No images captured from server, will use registry pull fallback"
|
|
fi
|
|
fi
|
|
|
|
# Define images to bundle for fallback (when not from server or missing). Includes filebrowser.
|
|
# bitcoin-ui and lnd-ui are custom and normally captured from server or built separately.
|
|
# Alpha: core Bitcoin/Lightning stack + essential apps. Others pulled on-demand from Marketplace.
|
|
CONTAINER_IMAGES="
|
|
${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:28.1} bitcoin-knots.tar
|
|
${LND_IMAGE:-docker.io/lightninglabs/lnd:v0.18.5-beta} lnd.tar
|
|
${HOMEASSISTANT_IMAGE:-ghcr.io/home-assistant/home-assistant:2024.12} homeassistant.tar
|
|
${BTCPAY_IMAGE:-docker.io/btcpayserver/btcpayserver:1.13.7} btcpayserver.tar
|
|
${NBXPLORER_IMAGE:-docker.io/nicolasdorier/nbxplorer:2.5.13} nbxplorer.tar
|
|
${POSTGRES_IMAGE:-docker.io/library/postgres:16} postgres-btcpay.tar
|
|
${MEMPOOL_BACKEND_IMAGE:-docker.io/mempool/backend:v3.0.0} mempool-backend.tar
|
|
${MEMPOOL_WEB_IMAGE:-docker.io/mempool/frontend:v3.0.0} mempool-frontend.tar
|
|
${ELECTRUMX_IMAGE:-docker.io/lukechilds/electrumx:v1.16.0} electrumx.tar
|
|
${MARIADB_IMAGE:-docker.io/library/mariadb:11.4} mariadb-mempool.tar
|
|
${FEDIMINT_IMAGE:-docker.io/fedimint/fedimintd:v0.5.1} fedimint.tar
|
|
${FEDIMINT_GATEWAY_IMAGE:-docker.io/fedimint/gatewayd:v0.5.1} fedimint-gateway.tar
|
|
${FILEBROWSER_IMAGE:-docker.io/filebrowser/filebrowser:v2} filebrowser.tar
|
|
${ALPINE_TOR_IMAGE:-docker.io/andrius/alpine-tor:0.4.8.13} alpine-tor.tar
|
|
${NGINX_ALPINE_IMAGE:-docker.io/library/nginx:alpine} nginx-alpine.tar
|
|
${DWN_SERVER_IMAGE:-ghcr.io/tbd54566975/dwn-server:main} dwn-server.tar
|
|
${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.4.0} grafana.tar
|
|
${UPTIME_KUMA_IMAGE:-docker.io/louislam/uptime-kuma:1} uptime-kuma.tar
|
|
${VAULTWARDEN_IMAGE:-docker.io/vaultwarden/server:1.32.5} vaultwarden.tar
|
|
${SEARXNG_IMAGE:-docker.io/searxng/searxng:2026.3.20-6c7e9c197} searxng.tar
|
|
${PORTAINER_IMAGE:-docker.io/portainer/portainer-ce:2.21.5} portainer.tar
|
|
${TAILSCALE_IMAGE:-docker.io/tailscale/tailscale:v1.78.3} tailscale.tar
|
|
${JELLYFIN_IMAGE:-docker.io/jellyfin/jellyfin:10.10.3} jellyfin.tar
|
|
${PHOTOPRISM_IMAGE:-docker.io/photoprism/photoprism:240915} photoprism.tar
|
|
${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:30} nextcloud.tar
|
|
${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2} nginx-proxy-manager.tar
|
|
${ONLYOFFICE_IMAGE:-docker.io/onlyoffice/documentserver:8.2} onlyoffice.tar
|
|
${ADGUARDHOME_IMAGE:-docker.io/adguard/adguardhome:v0.107.55} adguardhome.tar
|
|
"
|
|
|
|
# Pull and save each image (force target arch) only if not already present
|
|
echo "$CONTAINER_IMAGES" | while read -r image filename; do
|
|
[ -z "$image" ] && continue
|
|
tarpath="$IMAGES_DIR/$filename"
|
|
|
|
if [ -f "$tarpath" ]; then
|
|
echo " ✅ Using cached: $filename"
|
|
else
|
|
echo " Pulling $image ($CONTAINER_PLATFORM)..."
|
|
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$image"; then
|
|
echo " Saving $filename..."
|
|
if $CONTAINER_CMD save "$image" -o "$tarpath" 2>/dev/null; then
|
|
echo " ✅ Saved: $(du -h "$tarpath" | cut -f1)"
|
|
else
|
|
echo " ⚠️ Failed to save $image (zstd/format issue) - skipping"
|
|
rm -f "$tarpath"
|
|
fi
|
|
else
|
|
echo " ⚠️ Failed to pull $image - skipping"
|
|
fi
|
|
fi
|
|
done
|
|
fi # end UNBUNDLED check
|
|
|
|
# Create first-boot service to load images into Podman
|
|
echo " Creating first-boot image loader service..."
|
|
cat > "$WORK_DIR/archipelago-load-images.service" <<'LOADSERVICE'
|
|
[Unit]
|
|
Description=Load Archipelago Container Images
|
|
After=network.target podman.service
|
|
ConditionPathExists=/opt/archipelago/container-images
|
|
ConditionPathExists=!/var/lib/archipelago/.images-loaded
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/opt/archipelago/scripts/load-container-images.sh
|
|
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.images-loaded
|
|
RemainAfterExit=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
LOADSERVICE
|
|
|
|
cat > "$WORK_DIR/load-container-images.sh" <<'LOADSCRIPT'
|
|
#!/bin/bash
|
|
# Load pre-bundled container images into Podman
|
|
|
|
IMAGES_DIR="/opt/archipelago/container-images"
|
|
LOG_FILE="/var/log/archipelago-images.log"
|
|
|
|
echo "$(date): Starting container image load" >> "$LOG_FILE"
|
|
|
|
if [ ! -d "$IMAGES_DIR" ]; then
|
|
echo "$(date): No images directory found" >> "$LOG_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
for tarfile in "$IMAGES_DIR"/*.tar; do
|
|
if [ -f "$tarfile" ]; then
|
|
echo "$(date): Loading $(basename "$tarfile")..." >> "$LOG_FILE"
|
|
podman load -i "$tarfile" >> "$LOG_FILE" 2>&1 && \
|
|
echo "$(date): Successfully loaded $(basename "$tarfile")" >> "$LOG_FILE" || \
|
|
echo "$(date): Failed to load $(basename "$tarfile")" >> "$LOG_FILE"
|
|
fi
|
|
done
|
|
|
|
# Ensure archy-net exists for mempool stack (db, api, frontend)
|
|
podman network create archy-net 2>/dev/null || true
|
|
|
|
echo "$(date): Container image load complete" >> "$LOG_FILE"
|
|
echo "$(date): Available images:" >> "$LOG_FILE"
|
|
podman images >> "$LOG_FILE" 2>&1
|
|
LOADSCRIPT
|
|
|
|
chmod +x "$WORK_DIR/load-container-images.sh"
|
|
|
|
# Copy scripts to ISO
|
|
mkdir -p "$ARCH_DIR/scripts"
|
|
cp "$WORK_DIR/load-container-images.sh" "$ARCH_DIR/scripts/"
|
|
cp "$WORK_DIR/archipelago-load-images.service" "$ARCH_DIR/scripts/"
|
|
|
|
# Tor setup: copy torrc and create first-boot setup script
|
|
mkdir -p "$ARCH_DIR/scripts/tor"
|
|
if [ -f "$SCRIPT_DIR/../scripts/tor/torrc.template" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/tor/torrc.template" "$ARCH_DIR/scripts/tor/torrc"
|
|
fi
|
|
|
|
echo " Creating first-boot Tor setup service..."
|
|
cat > "$WORK_DIR/archipelago-setup-tor.service" <<'TORSERVICE'
|
|
[Unit]
|
|
Description=Setup and start Archipelago Tor hidden services
|
|
After=archipelago-load-images.service network.target podman.service
|
|
ConditionPathExists=/opt/archipelago/scripts/setup-tor.sh
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/opt/archipelago/scripts/setup-tor.sh
|
|
RemainAfterExit=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
TORSERVICE
|
|
|
|
cat > "$WORK_DIR/setup-tor.sh" <<'TORSCRIPT'
|
|
#!/bin/bash
|
|
# Setup and start Tor hidden services (autoinstaller first-boot)
|
|
# Prefers system Tor (apt package) over container
|
|
|
|
ARCHY_TOR_DIR="/var/lib/archipelago/tor"
|
|
TOR_DIR="/var/lib/tor"
|
|
LOG="/var/log/archipelago-tor.log"
|
|
|
|
mkdir -p "$ARCHY_TOR_DIR"
|
|
|
|
# Write services.json for the backend to read
|
|
python3 -c '
|
|
import json
|
|
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}
|
|
]
|
|
with open("'"$ARCHY_TOR_DIR"'/services.json", "w") as f:
|
|
json.dump({"services": services}, f, indent=2)
|
|
print("services.json created")
|
|
'
|
|
|
|
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
|
|
cat > /etc/tor/torrc <<TORRC
|
|
SocksPort 9050
|
|
ControlPort 0
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_archipelago
|
|
HiddenServicePort 80 127.0.0.1:80
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_bitcoin
|
|
HiddenServicePort 8333 127.0.0.1:8333
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_electrumx
|
|
HiddenServicePort 50001 127.0.0.1:50001
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_lnd
|
|
HiddenServicePort 9735 127.0.0.1:9735
|
|
HiddenServicePort 8080 127.0.0.1:8080
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_btcpay
|
|
HiddenServicePort 23000 127.0.0.1:23000
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_mempool
|
|
HiddenServicePort 4080 127.0.0.1:4080
|
|
|
|
HiddenServiceDir $TOR_DIR/hidden_service_fedimint
|
|
HiddenServicePort 8175 127.0.0.1:8175
|
|
TORRC
|
|
|
|
# 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:-docker.io/andrius/alpine-tor:0.4.8.13} \
|
|
-f /etc/tor/torrc >> "$LOG" 2>&1
|
|
echo "$(date): Tor container started" >> "$LOG"
|
|
fi
|
|
fi
|
|
|
|
# Wait for Tor to create hostname files (~30-60s), then chmod so archipelago user can read
|
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
|
sleep 6
|
|
if [ -f "$TOR_DIR/hidden_service_archipelago/hostname" ]; then
|
|
chmod 750 "$TOR_DIR"/hidden_service_*/ 2>/dev/null || true
|
|
for f in "$TOR_DIR"/hidden_service_*/hostname; do
|
|
[ -f "$f" ] && chmod 640 "$f" && echo "$(date): chmod hostname $f" >> "$LOG"
|
|
done
|
|
echo "$(date): Tor hostname files readable by archipelago" >> "$LOG"
|
|
break
|
|
fi
|
|
done
|
|
TORSCRIPT
|
|
|
|
chmod +x "$WORK_DIR/setup-tor.sh"
|
|
cp "$WORK_DIR/setup-tor.sh" "$ARCH_DIR/scripts/"
|
|
cp "$WORK_DIR/archipelago-setup-tor.service" "$ARCH_DIR/scripts/"
|
|
|
|
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
|
|
# Skip for unbundled builds — no images pre-loaded, users install from Marketplace
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
echo " Skipping first-boot containers (UNBUNDLED: apps installed from Marketplace)"
|
|
else
|
|
echo " Creating first-boot container creation service..."
|
|
# Copy shared script library
|
|
if [ -d "$SCRIPT_DIR/../scripts/lib" ]; then
|
|
mkdir -p "$ARCH_DIR/scripts/lib"
|
|
cp "$SCRIPT_DIR/../scripts/lib/"*.sh "$ARCH_DIR/scripts/lib/" 2>/dev/null || true
|
|
fi
|
|
if [ -f "$SCRIPT_DIR/../scripts/first-boot-containers.sh" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/first-boot-containers.sh" "$ARCH_DIR/scripts/"
|
|
chmod +x "$ARCH_DIR/scripts/first-boot-containers.sh"
|
|
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
|
|
[Unit]
|
|
Description=Create core Archipelago containers on first boot
|
|
After=archipelago-setup-tor.service network-online.target podman.service
|
|
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
|
|
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/opt/archipelago/scripts/first-boot-containers.sh
|
|
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
|
|
RemainAfterExit=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
FBCSERVICE
|
|
cp "$WORK_DIR/archipelago-first-boot-containers.service" "$ARCH_DIR/scripts/"
|
|
fi
|
|
fi
|
|
|
|
# Bundle E2E test script for post-install validation
|
|
if [ -f "$SCRIPT_DIR/../scripts/run-e2e-tests.sh" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/run-e2e-tests.sh" "$ARCH_DIR/scripts/"
|
|
chmod +x "$ARCH_DIR/scripts/run-e2e-tests.sh"
|
|
echo " ✅ Bundled E2E test script for post-install validation"
|
|
fi
|
|
|
|
# Bundle self-update script and image-versions for update system
|
|
if [ -f "$SCRIPT_DIR/../scripts/self-update.sh" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/self-update.sh" "$ARCH_DIR/scripts/"
|
|
chmod +x "$ARCH_DIR/scripts/self-update.sh"
|
|
echo " ✅ Bundled self-update script"
|
|
fi
|
|
if [ -f "$SCRIPT_DIR/../scripts/image-versions.sh" ]; then
|
|
cp "$SCRIPT_DIR/../scripts/image-versions.sh" "$ARCH_DIR/scripts/"
|
|
echo " ✅ Bundled image-versions.sh"
|
|
fi
|
|
|
|
# Bundle docker UI source files for building custom UIs on first boot (fallback if images not captured)
|
|
# Skip for unbundled builds
|
|
if [ "$UNBUNDLED" != "1" ]; then
|
|
DOCKER_UI_DIR="$SCRIPT_DIR/../docker"
|
|
if [ -d "$DOCKER_UI_DIR" ]; then
|
|
echo " Bundling docker UI source files..."
|
|
mkdir -p "$ARCH_DIR/docker"
|
|
for ui_dir in bitcoin-ui lnd-ui electrs-ui; do
|
|
if [ -d "$DOCKER_UI_DIR/$ui_dir" ]; then
|
|
cp -r "$DOCKER_UI_DIR/$ui_dir" "$ARCH_DIR/docker/"
|
|
echo " ✅ Bundled $ui_dir source"
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
echo " ✅ Unbundled build ready (Tor setup included, no container images)"
|
|
else
|
|
echo " ✅ Container images bundled (including Tor + first-boot)"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# STEP 4: Create auto-installer script
|
|
# =============================================================================
|
|
echo ""
|
|
echo "📦 Step 4: Creating auto-installer..."
|
|
|
|
cat > "$ARCH_DIR/auto-install.sh" <<'INSTALLER_SCRIPT'
|
|
#!/bin/bash
|
|
#
|
|
# Archipelago Auto-Installer
|
|
# Automatically installs to internal disk (StartOS-like experience)
|
|
#
|
|
|
|
set -e
|
|
|
|
# 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 (use $'...' syntax for reliable escape code interpretation)
|
|
RED=$'\033[0;31m'
|
|
GREEN=$'\033[0;32m'
|
|
YELLOW=$'\033[1;33m'
|
|
BLUE=$'\033[0;34m'
|
|
NC=$'\033[0m'
|
|
|
|
clear
|
|
echo ""
|
|
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║ ║${NC}"
|
|
echo -e "${BLUE}║ ${GREEN}🏝️ ARCHIPELAGO BITCOIN NODE OS${BLUE} ║${NC}"
|
|
echo -e "${BLUE}║ ║${NC}"
|
|
echo -e "${BLUE}║ ${NC}Automatic Installation${BLUE} ║${NC}"
|
|
echo -e "${BLUE}║ ║${NC}"
|
|
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# Check required tools are present (should be bundled in ISO)
|
|
echo -e "${YELLOW}🔧 Checking installer tools...${NC}"
|
|
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
|
|
echo " Missing tools: $MISSING"
|
|
echo " Attempting to install (requires network)..."
|
|
if apt-get update -qq >/dev/null 2>&1; then
|
|
apt-get install -y -qq parted dosfstools e2fsprogs >/dev/null 2>&1 && echo " ✅ Tools installed" || {
|
|
echo -e "${RED}❌ Failed to install required tools. No network?${NC}"
|
|
echo " Required: parted, mkfs.vfat, mkfs.ext4"
|
|
exit 1
|
|
}
|
|
else
|
|
echo -e "${RED}❌ No network available and tools not bundled.${NC}"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo " ✅ All tools present"
|
|
fi
|
|
echo ""
|
|
|
|
# Find boot media
|
|
BOOT_MEDIA=""
|
|
for dev in /run/live/medium /lib/live/mount/medium /cdrom; 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 "")
|
|
|
|
echo -e "${YELLOW}📋 Detecting disks...${NC}"
|
|
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
|
|
|
|
echo ""
|
|
echo -e "${GREEN}✅ Target disk: $TARGET_DISK ($TARGET_SIZE)${NC}"
|
|
echo ""
|
|
echo -e "${RED}⚠️ WARNING: ALL DATA ON $TARGET_DISK WILL BE ERASED${NC}"
|
|
echo ""
|
|
echo "Press Enter to install Archipelago, or Ctrl+C to cancel..."
|
|
read
|
|
|
|
echo ""
|
|
echo -e "${YELLOW}🔧 Installing Archipelago...${NC}"
|
|
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 support
|
|
echo " [1/6] 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 (remaining space)
|
|
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 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"
|
|
else
|
|
BIOS_PART="${TARGET_DISK}1"
|
|
EFI_PART="${TARGET_DISK}2"
|
|
ROOT_PART="${TARGET_DISK}3"
|
|
fi
|
|
|
|
# Format partitions
|
|
echo " [2/6] Formatting partitions..."
|
|
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
|
|
dd if=/dev/zero of="$BIOS_PART" bs=1M count=1 2>/dev/null || true
|
|
mkfs.vfat -F32 -n EFI "$EFI_PART"
|
|
mkfs.ext4 -F -L archipelago "$ROOT_PART"
|
|
|
|
# Mount
|
|
echo " [3/6] Mounting filesystems..."
|
|
mkdir -p /mnt/target
|
|
mount "$ROOT_PART" /mnt/target
|
|
mkdir -p /mnt/target/boot/efi
|
|
mount "$EFI_PART" /mnt/target/boot/efi
|
|
|
|
# Extract rootfs
|
|
echo " [4/6] Installing system (this may take a few minutes)..."
|
|
tar -xf "$ROOTFS_TAR" -C /mnt/target
|
|
|
|
# Create fstab
|
|
echo " [5/6] Configuring system..."
|
|
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
|
|
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
|
|
|
|
# Copy Archipelago binaries and files
|
|
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
|
|
cp -r "$BOOT_MEDIA/archipelago/bin/"* /mnt/target/usr/local/bin/ 2>/dev/null || true
|
|
chmod +x /mnt/target/usr/local/bin/* 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -d "$BOOT_MEDIA/archipelago/web-ui" ]; then
|
|
cp -r "$BOOT_MEDIA/archipelago/web-ui" /mnt/target/opt/archipelago/
|
|
fi
|
|
|
|
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
|
|
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/target/etc/archipelago/
|
|
fi
|
|
|
|
# Copy pre-bundled container images
|
|
if [ -d "$BOOT_MEDIA/archipelago/container-images" ]; then
|
|
echo " Copying container images (this may take a moment)..."
|
|
mkdir -p /mnt/target/opt/archipelago/container-images
|
|
cp -r "$BOOT_MEDIA/archipelago/container-images/"*.tar /mnt/target/opt/archipelago/container-images/ 2>/dev/null || true
|
|
|
|
# Copy first-boot loader script and service
|
|
mkdir -p /mnt/target/opt/archipelago/scripts
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/load-container-images.sh" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/load-container-images.sh" /mnt/target/opt/archipelago/scripts/
|
|
chmod +x /mnt/target/opt/archipelago/scripts/load-container-images.sh
|
|
fi
|
|
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-load-images.service" /mnt/target/etc/systemd/system/
|
|
fi
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/setup-tor.sh" /mnt/target/opt/archipelago/scripts/
|
|
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
|
|
fi
|
|
if [ -d "$BOOT_MEDIA/archipelago/scripts/tor" ]; then
|
|
mkdir -p /mnt/target/opt/archipelago/scripts/tor
|
|
cp -r "$BOOT_MEDIA/archipelago/scripts/tor/"* /mnt/target/opt/archipelago/scripts/tor/ 2>/dev/null || true
|
|
fi
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-setup-tor.service" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-setup-tor.service" /mnt/target/etc/systemd/system/
|
|
fi
|
|
# Copy shared script library
|
|
if [ -d "$BOOT_MEDIA/archipelago/scripts/lib" ]; then
|
|
mkdir -p /mnt/target/opt/archipelago/scripts/lib
|
|
cp -r "$BOOT_MEDIA/archipelago/scripts/lib/"* /mnt/target/opt/archipelago/scripts/lib/ 2>/dev/null || true
|
|
fi
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/first-boot-containers.sh" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/first-boot-containers.sh" /mnt/target/opt/archipelago/scripts/
|
|
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-containers.sh
|
|
fi
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-containers.service" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/archipelago-first-boot-containers.service" /mnt/target/etc/systemd/system/
|
|
fi
|
|
# Copy docker UI source files for first-boot container builds
|
|
if [ -d "$BOOT_MEDIA/archipelago/docker" ]; then
|
|
mkdir -p /mnt/target/opt/archipelago/docker
|
|
cp -r "$BOOT_MEDIA/archipelago/docker/"* /mnt/target/opt/archipelago/docker/ 2>/dev/null || true
|
|
fi
|
|
|
|
echo " ✅ Container images staged for first-boot loading"
|
|
fi
|
|
|
|
# Initialize backend data directories for seamless first boot
|
|
mkdir -p /mnt/target/var/lib/archipelago/tor-config
|
|
mkdir -p /mnt/target/var/lib/archipelago/identities
|
|
mkdir -p /mnt/target/var/lib/archipelago/lnd
|
|
|
|
# Copy E2E test script for post-install validation
|
|
if [ -f "$BOOT_MEDIA/archipelago/scripts/run-e2e-tests.sh" ]; then
|
|
cp "$BOOT_MEDIA/archipelago/scripts/run-e2e-tests.sh" /mnt/target/opt/archipelago/scripts/
|
|
chmod +x /mnt/target/opt/archipelago/scripts/run-e2e-tests.sh
|
|
fi
|
|
|
|
# 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
|
|
|
|
# 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
|
|
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
echo ""
|
|
echo " _"
|
|
echo " ,--.\\\`-. __"
|
|
echo " _,.\\\`. \\:/,\" \\\`-._"
|
|
echo " ,-*\" _,.-;-*\\\`-.+\"*._ )"
|
|
echo " ( ,.\"* ,-\" / \\\`. \\\\. \\\`."
|
|
echo " ,\" ,;\" ,\"\\../\\ \\: \\"
|
|
echo " ( ,\"/ / \\\\.,' : )) /"
|
|
echo " \\ |/ / \\\\.,' / // ,'"
|
|
echo " \\_)\\ ,' \\\\.,' ( / )/"
|
|
echo " \\\` \\._,' \\\`\""
|
|
echo " \\..\/"
|
|
echo " \\..\/"
|
|
echo " ~ ~\\..\/ ~~ ~~"
|
|
echo " ~~ ~~ \\..\/ ~~ ~ ~~"
|
|
echo " ~~ ~ ~~ __...---\\../-...__ ~~~ ~~"
|
|
echo " ~~~~ ~_,--' \\..\/ \\\`--.__ ~~ ~~"
|
|
echo " ~~~ __,--' \\\`\" \\\`--.__ ~~~"
|
|
echo "~~ ,--' \\\`--."
|
|
echo " '------......______ ______......------\\\` ~~"
|
|
echo " ~~~ ~ ~~ ~ \\\`\\\`\\\`\\\`\\\`---\"\"\"\"\" ~~ ~ ~~"
|
|
echo " ~~~~ ~~ ~~~~ ~~~~~~ ~ ~~ ~~ ~~~ ~"
|
|
echo ""
|
|
echo " █████╗ ██████╗ ██████╗██╗ ██╗██╗██████╗ ███████╗██╗ █████╗ ██████╗ ██████╗ "
|
|
echo " ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝ ██╔═══██╗"
|
|
echo " ███████║██████╔╝██║ ███████║██║██████╔╝█████╗ ██║ ███████║██║ ███╗██║ ██║"
|
|
echo " ██╔══██║██╔══██╗██║ ██╔══██║██║██╔═══╝ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║"
|
|
echo " ██║ ██║██║ ██║╚██████╗██║ ██║██║██║ ███████╗███████╗██║ ██║╚██████╔╝╚██████╔╝"
|
|
echo " ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ "
|
|
echo ""
|
|
echo " 🏝️ BITCOIN NODE OS 🏝️"
|
|
echo ""
|
|
if [ -n "$IP" ]; then
|
|
echo " 🌐 Web UI: http://$IP"
|
|
echo " 📡 SSH: ssh archipelago@$IP"
|
|
echo " 🔑 Password: archipelago (SSH) / password123 (Web UI)"
|
|
echo ""
|
|
fi
|
|
fi
|
|
PROFILE
|
|
chmod +x /mnt/target/etc/profile.d/archipelago.sh
|
|
|
|
# Systemd service: User=root required for Podman container access
|
|
cat > /mnt/target/etc/systemd/system/archipelago.service <<'SERVICE'
|
|
[Unit]
|
|
Description=Archipelago Backend
|
|
After=network-online.target archipelago-setup-tor.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
|
Environment="ARCHIPELAGO_DEV_MODE=true"
|
|
ExecStartPre=/bin/bash -c 'mkdir -p /etc/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk \"{print \$1}\")" > /etc/archipelago/host-ip.env'
|
|
ExecStart=/usr/local/bin/archipelago
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVICE
|
|
|
|
# Claude API proxy — middleware that injects max_tokens, strips invalid fields
|
|
# API key must be set after install via setup-aiui-server.sh or manually
|
|
cat > /mnt/target/opt/archipelago/claude-api-proxy.py <<'CLAUDEPROXY'
|
|
#!/usr/bin/env python3
|
|
import http.server, json, ssl, sys, os, urllib.request, urllib.error
|
|
API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
PORT = 3142
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
if self.path == "/health":
|
|
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
|
|
self.wfile.write(b'{"status":"ok"}'); return
|
|
cl = int(self.headers.get("Content-Length", 0))
|
|
body = self.rfile.read(cl)
|
|
try: data = json.loads(body)
|
|
except: data = {}
|
|
if "max_tokens" not in data: data["max_tokens"] = 8096
|
|
for f in ["webSearch","web_search"]: data.pop(f, None)
|
|
body = json.dumps(data).encode()
|
|
headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}
|
|
for h in ["anthropic-version","anthropic-beta"]:
|
|
if self.headers.get(h): headers[h] = self.headers[h]
|
|
req = urllib.request.Request("https://api.anthropic.com"+self.path, data=body, headers=headers, method="POST")
|
|
try:
|
|
ctx = ssl.create_default_context()
|
|
resp = urllib.request.urlopen(req, context=ctx, timeout=300)
|
|
self.send_response(resp.status)
|
|
is_stream = "text/event-stream" in (resp.headers.get("Content-Type","") or "")
|
|
for k,v in resp.headers.items():
|
|
if k.lower() not in ("transfer-encoding","connection"): self.send_header(k,v)
|
|
if is_stream: self.send_header("Transfer-Encoding","chunked")
|
|
self.end_headers()
|
|
if is_stream:
|
|
while True:
|
|
chunk = resp.read(4096)
|
|
if not chunk: break
|
|
self.wfile.write(b"%x\r\n" % len(chunk)); self.wfile.write(chunk); self.wfile.write(b"\r\n"); self.wfile.flush()
|
|
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
|
|
else: self.wfile.write(resp.read())
|
|
except urllib.error.HTTPError as e:
|
|
self.send_response(e.code); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(e.read())
|
|
except Exception as e:
|
|
self.send_response(502); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(json.dumps({"error":str(e)}).encode())
|
|
def do_GET(self):
|
|
if self.path == "/health":
|
|
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b'{"status":"ok"}')
|
|
else: self.send_response(404); self.end_headers()
|
|
def log_message(self, fmt, *args): pass
|
|
if not API_KEY: print("ERROR: ANTHROPIC_API_KEY not set — configure via setup-aiui-server.sh"); sys.exit(1)
|
|
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
|
|
print(f"Claude API proxy on port {PORT}")
|
|
server.serve_forever()
|
|
CLAUDEPROXY
|
|
chmod +x /mnt/target/opt/archipelago/claude-api-proxy.py
|
|
|
|
# Claude API proxy systemd service (disabled by default — enabled after API key is configured)
|
|
cat > /mnt/target/etc/systemd/system/claude-api-proxy.service <<'CLAUDESVC'
|
|
[Unit]
|
|
Description=Claude API Proxy
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
Environment=ANTHROPIC_API_KEY=
|
|
ExecStart=/usr/bin/python3 /opt/archipelago/claude-api-proxy.py
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
CLAUDESVC
|
|
|
|
# Install GRUB
|
|
echo " [6/6] 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"
|
|
|
|
# UEFI boot: install to fallback path (/EFI/BOOT/BOOTX64.EFI) for maximum compatibility
|
|
echo " Installing UEFI bootloader..."
|
|
if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable; then
|
|
echo " ✅ UEFI bootloader installed (removable/fallback path)"
|
|
else
|
|
echo " ⚠️ UEFI removable install failed, trying standard..."
|
|
if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago; then
|
|
echo " ✅ UEFI bootloader installed (standard)"
|
|
else
|
|
echo " ❌ UEFI bootloader installation failed"
|
|
fi
|
|
fi
|
|
|
|
# EFI boot: grub-install --removable already placed unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
|
|
# This works on all machines without Secure Boot. For Secure Boot, users must disable it.
|
|
# The shim chain was causing "Failed to open \EFI\BOOT\" errors with garbled filenames
|
|
# on machines where Secure Boot is disabled — the shim tries to verify signatures and fails.
|
|
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
|
|
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
|
|
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
|
|
ls -la "$EFI_BOOT_DIR/"
|
|
else
|
|
echo " ❌ Missing $EFI_BOOT_DIR/$EFI_BOOT_BINARY — boot will fail!"
|
|
fi
|
|
|
|
# Legacy BIOS boot: only install if the installer booted in Legacy BIOS mode
|
|
# (if /sys/firmware/efi exists, the machine supports UEFI — no need for BIOS fallback)
|
|
if [ -n "${GRUB_BIOS_TARGET}" ] && [ ! -d /sys/firmware/efi ]; then
|
|
echo " Installing Legacy BIOS bootloader (machine booted in BIOS mode)..."
|
|
if chroot /mnt/target grub-install --target=${GRUB_BIOS_TARGET} "${TARGET_DISK}"; then
|
|
echo " ✅ Legacy BIOS bootloader installed"
|
|
else
|
|
echo " ⚠️ Legacy BIOS bootloader failed (UEFI-only boot)"
|
|
fi
|
|
elif [ -n "${GRUB_BIOS_TARGET}" ]; then
|
|
echo " Skipping Legacy BIOS bootloader (machine supports UEFI)"
|
|
fi
|
|
|
|
# Remove any live-boot artifacts that could cause squashfs errors on boot
|
|
# These can sneak in from Docker base image or package dependencies
|
|
echo " Cleaning live-boot artifacts..."
|
|
chroot /mnt/target apt-get remove -y --purge live-boot live-boot-initramfs-tools live-config 2>/dev/null || true
|
|
rm -f /mnt/target/etc/initramfs-tools/conf.d/live-boot* 2>/dev/null || true
|
|
rm -f /mnt/target/usr/share/initramfs-tools/scripts/live* 2>/dev/null || true
|
|
rm -f /mnt/target/usr/share/initramfs-tools/hooks/live* 2>/dev/null || true
|
|
|
|
# Suppress os-prober warning in GRUB
|
|
echo "GRUB_DISABLE_OS_PROBER=true" >> /mnt/target/etc/default/grub
|
|
|
|
# Regenerate initramfs — the one from Docker export is corrupt/incomplete
|
|
# (Docker builds have limited /proc, /sys, /dev so initramfs generation fails silently)
|
|
echo " Regenerating initramfs..."
|
|
chroot /mnt/target update-initramfs -u -k all
|
|
|
|
chroot /mnt/target update-grub
|
|
|
|
# Install udev rule for mesh radio stable naming (/dev/mesh-radio)
|
|
if [ -f /cdrom/99-mesh-radio.rules ]; then
|
|
cp /cdrom/99-mesh-radio.rules /mnt/target/etc/udev/rules.d/99-mesh-radio.rules
|
|
echo " Installed mesh radio udev rule"
|
|
fi
|
|
|
|
# Enable services
|
|
chroot /mnt/target systemctl enable archipelago.service 2>/dev/null || true
|
|
chroot /mnt/target systemctl enable nginx.service 2>/dev/null || true
|
|
chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null || true
|
|
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
|
|
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
|
|
|
|
# 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 2>/dev/null || true
|
|
|
|
echo ""
|
|
echo -e "${GREEN} _${NC}"
|
|
echo -e "${GREEN} ,--.\\\`-. __${NC}"
|
|
echo -e "${GREEN} _,.\\\`. \\:/,\" \\\`-._${NC}"
|
|
echo -e "${GREEN} ,-*\" _,.-;-*\\\`-.+\"*._ )${NC}"
|
|
echo -e "${GREEN} ( ,.\"* ,-\" / \\\`. \\\\. \\\`.${NC}"
|
|
echo -e "${GREEN} ,\" ,;\" ,\"\\../\\ \\: \\${NC}"
|
|
echo -e "${GREEN} ( ,\"/ / \\\\.,' : )) /${NC}"
|
|
echo -e "${GREEN} \\ |/ / \\\\.,' / // ,'${NC}"
|
|
echo -e "${GREEN} \\_)\\ ,' \\\\.,' ( / )/${NC}"
|
|
echo -e "${GREEN} \\\` \\._,' \\\`\"${NC}"
|
|
echo -e "${GREEN} \\../${NC}"
|
|
echo -e "${GREEN} \\../${NC}"
|
|
echo -e "${GREEN} ~ ~\\../ ~~ ~~${NC}"
|
|
echo -e "${GREEN} ~~ ~~ \\../ ~~ ~ ~~${NC}"
|
|
echo -e "${GREEN} ~~ ~ ~~ __...---\\../-...__ ~~~ ~~${NC}"
|
|
echo -e "${GREEN} ~~~~ ~_,--' \\../ \\\`--.__ ~~ ~~${NC}"
|
|
echo -e "${GREEN} ~~~ __,--' \\\`\" \\\`--.__ ~~~${NC}"
|
|
echo -e "${GREEN}~~ ,--' \\\`--.${NC}"
|
|
echo -e "${GREEN} '------......______ ______......------\\\` ~~${NC}"
|
|
echo -e "${GREEN} ~~~ ~ ~~ ~ \\\`\\\`\\\`\\\`\\\`---\"\"\"\"\" ~~ ~ ~~${NC}"
|
|
echo -e "${GREEN} ~~~~ ~~ ~~~~ ~~~~~~ ~ ~~ ~~ ~~~ ~${NC}"
|
|
echo ""
|
|
echo -e "${GREEN} █████╗ ██████╗ ██████╗██╗ ██╗██╗██████╗ ███████╗██╗ █████╗ ██████╗ ██████╗ ${NC}"
|
|
echo -e "${GREEN} ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝ ██╔═══██╗${NC}"
|
|
echo -e "${GREEN} ███████║██████╔╝██║ ███████║██║██████╔╝█████╗ ██║ ███████║██║ ███╗██║ ██║${NC}"
|
|
echo -e "${GREEN} ██╔══██║██╔══██╗██║ ██╔══██║██║██╔═══╝ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║${NC}"
|
|
echo -e "${GREEN} ██║ ██║██║ ██║╚██████╗██║ ██║██║██║ ███████╗███████╗██║ ██║╚██████╔╝╚██████╔╝${NC}"
|
|
echo -e "${GREEN} ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ${NC}"
|
|
echo ""
|
|
echo -e "${GREEN} 🏝️ BITCOIN NODE OS 🏝️${NC}"
|
|
echo ""
|
|
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ ✅ INSTALLATION COMPLETE! ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Remove the USB drive and press Enter to reboot. ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ After reboot: ║${NC}"
|
|
echo -e "${GREEN}║ • Web UI: http://<IP> ║${NC}"
|
|
echo -e "${GREEN}║ • SSH: ssh archipelago@<IP> ║${NC}"
|
|
echo -e "${GREEN}║ • SSH Password: archipelago ║${NC}"
|
|
echo -e "${GREEN}║ • Web Password: password123 ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Pre-loaded apps (ready to start via Web UI): ║${NC}"
|
|
echo -e "${GREEN}║ • Bitcoin Knots • LND • Home Assistant ║${NC}"
|
|
echo -e "${GREEN}║ • BTCPay Server • Mempool • Nostr Relays ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Validate: bash /opt/archipelago/scripts/run-e2e-tests.sh ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo ""
|
|
echo -e "${YELLOW} >>> REMOVE THE USB DRIVE NOW <<<${NC}"
|
|
echo ""
|
|
read -p "Press Enter to reboot (make sure USB is removed)..."
|
|
|
|
# Try to eject the USB boot media to prevent booting back into installer
|
|
BOOT_DEV=$(findmnt -n -o SOURCE /run/live/medium 2>/dev/null || findmnt -n -o SOURCE /lib/live/mount/medium 2>/dev/null || echo "")
|
|
if [ -n "$BOOT_DEV" ]; then
|
|
# Get the parent disk device (e.g., /dev/sdb1 -> /dev/sdb)
|
|
BOOT_DISK=$(lsblk -no PKNAME "$BOOT_DEV" 2>/dev/null | head -1)
|
|
if [ -n "$BOOT_DISK" ]; then
|
|
echo " Ejecting USB (/dev/$BOOT_DISK)..."
|
|
umount -l /run/live/medium 2>/dev/null || true
|
|
umount -l /lib/live/mount/medium 2>/dev/null || true
|
|
eject "/dev/$BOOT_DISK" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
reboot
|
|
INSTALLER_SCRIPT
|
|
|
|
# For unbundled builds, patch the completion message to reflect no pre-loaded apps
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
sed -i 's/Pre-loaded apps (ready to start via Web UI):/Install apps from the Marketplace (internet required):/' "$ARCH_DIR/auto-install.sh"
|
|
sed -i 's/• Bitcoin Knots • LND • Home Assistant/ Open the Web UI → Marketplace → Install any app/' "$ARCH_DIR/auto-install.sh"
|
|
sed -i 's/• BTCPay Server • Mempool • Nostr Relays/ All apps download automatically via Podman /' "$ARCH_DIR/auto-install.sh"
|
|
fi
|
|
|
|
chmod +x "$ARCH_DIR/auto-install.sh"
|
|
|
|
# =============================================================================
|
|
# STEP 5: Configure auto-start installer on boot
|
|
# =============================================================================
|
|
echo ""
|
|
echo "📦 Step 5: Configuring auto-start..."
|
|
|
|
# Create a squashfs overlay module that Debian Live will automatically load
|
|
# This is the proper way to add files to the live filesystem
|
|
OVERLAY_DIR="$WORK_DIR/overlay-root"
|
|
rm -rf "$OVERLAY_DIR"
|
|
mkdir -p "$OVERLAY_DIR/etc/profile.d"
|
|
mkdir -p "$OVERLAY_DIR/etc/skel"
|
|
mkdir -p "$OVERLAY_DIR/usr/local/bin"
|
|
mkdir -p "$OVERLAY_DIR/usr/sbin"
|
|
mkdir -p "$OVERLAY_DIR/sbin"
|
|
|
|
# Download and extract required tools AND their libraries for offline use
|
|
echo " Downloading partitioning tools for offline use..."
|
|
TOOLS_DIR="$WORK_DIR/tools-extract"
|
|
rm -rf "$TOOLS_DIR"
|
|
mkdir -p "$TOOLS_DIR"
|
|
|
|
$CONTAINER_CMD run --rm --platform $CONTAINER_PLATFORM \
|
|
-v "$TOOLS_DIR:/output" \
|
|
debian:bookworm \
|
|
bash -c '
|
|
apt-get update -qq
|
|
apt-get install -y -qq parted dosfstools e2fsprogs
|
|
|
|
# Copy binaries
|
|
cp /usr/sbin/parted /output/
|
|
cp /usr/sbin/mkfs.vfat /output/ 2>/dev/null || cp /sbin/mkfs.vfat /output/ 2>/dev/null || true
|
|
cp /usr/sbin/mkfs.ext4 /output/ 2>/dev/null || cp /sbin/mkfs.ext4 /output/ 2>/dev/null || true
|
|
cp /usr/sbin/mke2fs /output/ 2>/dev/null || cp /sbin/mke2fs /output/ 2>/dev/null || true
|
|
cp /sbin/mkfs.fat /output/ 2>/dev/null || true
|
|
|
|
# Copy required shared libraries for parted
|
|
mkdir -p /output/lib
|
|
cp /lib/${LIB_DIR}/libparted.so* /output/lib/ 2>/dev/null || true
|
|
cp /usr/lib/${LIB_DIR}/libparted.so* /output/lib/ 2>/dev/null || true
|
|
cp /lib/${LIB_DIR}/libreadline.so* /output/lib/ 2>/dev/null || true
|
|
cp /usr/lib/${LIB_DIR}/libreadline.so* /output/lib/ 2>/dev/null || true
|
|
cp /lib/${LIB_DIR}/libdevmapper.so* /output/lib/ 2>/dev/null || true
|
|
cp /usr/lib/${LIB_DIR}/libdevmapper.so* /output/lib/ 2>/dev/null || true
|
|
|
|
# List what parted actually needs
|
|
ldd /usr/sbin/parted 2>/dev/null | grep "=>" | awk "{print \$3}" | while read lib; do
|
|
[ -f "$lib" ] && cp "$lib" /output/lib/ 2>/dev/null || true
|
|
done
|
|
|
|
echo "Libraries bundled:"
|
|
ls -la /output/lib/
|
|
'
|
|
|
|
# Copy tools to overlay
|
|
cp "$TOOLS_DIR/parted" "$OVERLAY_DIR/usr/sbin/" 2>/dev/null || true
|
|
cp "$TOOLS_DIR/mkfs.vfat" "$OVERLAY_DIR/usr/sbin/" 2>/dev/null || true
|
|
cp "$TOOLS_DIR/mkfs.fat" "$OVERLAY_DIR/sbin/" 2>/dev/null || true
|
|
cp "$TOOLS_DIR/mkfs.ext4" "$OVERLAY_DIR/usr/sbin/" 2>/dev/null || true
|
|
cp "$TOOLS_DIR/mke2fs" "$OVERLAY_DIR/usr/sbin/" 2>/dev/null || true
|
|
|
|
# Copy shared libraries
|
|
mkdir -p "$OVERLAY_DIR/usr/lib/${LIB_DIR}"
|
|
cp "$TOOLS_DIR/lib/"*.so* "$OVERLAY_DIR/usr/lib/${LIB_DIR}/" 2>/dev/null || true
|
|
|
|
chmod +x "$OVERLAY_DIR/usr/sbin/"* 2>/dev/null || true
|
|
chmod +x "$OVERLAY_DIR/sbin/"* 2>/dev/null || true
|
|
echo " ✅ Partitioning tools and libraries bundled"
|
|
|
|
# Create the auto-start profile script
|
|
cat > "$OVERLAY_DIR/etc/profile.d/z99-archipelago-installer.sh" <<'AUTOSTART'
|
|
#!/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
|
|
|
|
# Give system a moment to settle
|
|
sleep 1
|
|
|
|
clear
|
|
echo ""
|
|
echo " ╔═══════════════════════════════════════════════════════════════════╗"
|
|
echo " ║ ║"
|
|
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS - INSTALLER ║"
|
|
echo " ║ ║"
|
|
echo " ╚═══════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
|
|
# Find boot media
|
|
BOOT_MEDIA=""
|
|
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
|
|
if [ -f "$dev/archipelago/auto-install.sh" ]; then
|
|
BOOT_MEDIA="$dev"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -n "$BOOT_MEDIA" ]; then
|
|
echo " Found installer at: $BOOT_MEDIA"
|
|
echo ""
|
|
echo " Press Enter to start installation, or Ctrl+C for shell..."
|
|
read
|
|
sudo bash "$BOOT_MEDIA/archipelago/auto-install.sh"
|
|
else
|
|
echo " ⚠️ Installer not found on boot media."
|
|
echo ""
|
|
echo " Checked: /run/live/medium, /lib/live/mount/medium, /cdrom"
|
|
echo ""
|
|
echo " You can try manually:"
|
|
echo " sudo bash /path/to/archipelago/auto-install.sh"
|
|
echo ""
|
|
fi
|
|
AUTOSTART
|
|
chmod +x "$OVERLAY_DIR/etc/profile.d/z99-archipelago-installer.sh"
|
|
|
|
# Also create .bashrc that sources the profile script (belt and suspenders)
|
|
cat > "$OVERLAY_DIR/etc/skel/.bashrc" <<'BASHRC'
|
|
# ~/.bashrc: executed by bash(1) for non-login shells.
|
|
|
|
# If not running interactively, don't do anything
|
|
case $- in
|
|
*i*) ;;
|
|
*) return;;
|
|
esac
|
|
|
|
# Source profile.d scripts
|
|
if [ -d /etc/profile.d ]; then
|
|
for i in /etc/profile.d/*.sh; do
|
|
if [ -r "$i" ]; then
|
|
. "$i"
|
|
fi
|
|
done
|
|
fi
|
|
BASHRC
|
|
|
|
# Create a wrapper script that can be called directly
|
|
cat > "$OVERLAY_DIR/usr/local/bin/archipelago-install" <<'WRAPPER'
|
|
#!/bin/bash
|
|
# Archipelago installer wrapper
|
|
|
|
BOOT_MEDIA=""
|
|
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
|
|
if [ -f "$dev/archipelago/auto-install.sh" ]; then
|
|
BOOT_MEDIA="$dev"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -n "$BOOT_MEDIA" ]; then
|
|
exec sudo bash "$BOOT_MEDIA/archipelago/auto-install.sh"
|
|
else
|
|
echo "Installer not found. Searched:"
|
|
echo " /run/live/medium, /lib/live/mount/medium, /cdrom, /media/cdrom"
|
|
exit 1
|
|
fi
|
|
WRAPPER
|
|
chmod +x "$OVERLAY_DIR/usr/local/bin/archipelago-install"
|
|
|
|
# Create the squashfs module
|
|
# Debian Live automatically loads all .squashfs files from live/ directory
|
|
echo " Creating overlay squashfs module..."
|
|
LIVE_DIR="$INSTALLER_ISO/live"
|
|
mkdir -p "$LIVE_DIR"
|
|
|
|
# Check if mksquashfs is available (may need to use Docker on macOS)
|
|
if command -v mksquashfs >/dev/null 2>&1; then
|
|
mksquashfs "$OVERLAY_DIR" "$LIVE_DIR/99-archipelago.squashfs" -comp xz -noappend
|
|
else
|
|
# Use $CONTAINER_CMD to create squashfs on macOS
|
|
echo " Using $CONTAINER_CMD to create squashfs..."
|
|
$CONTAINER_CMD run --rm --platform $CONTAINER_PLATFORM \
|
|
-v "$OVERLAY_DIR:/overlay:ro" \
|
|
-v "$LIVE_DIR:/output" \
|
|
debian:bookworm \
|
|
bash -c "apt-get update && apt-get install -y squashfs-tools && mksquashfs /overlay /output/99-archipelago.squashfs -comp xz -noappend"
|
|
fi
|
|
|
|
echo " ✅ Created overlay module: 99-archipelago.squashfs"
|
|
|
|
# Modify GRUB config - update branding and ensure components are loaded
|
|
if [ -f "$INSTALLER_ISO/boot/grub/grub.cfg" ]; then
|
|
echo " Configuring GRUB..."
|
|
sed -i.bak \
|
|
-e 's/Debian GNU\/Linux/Archipelago Installer/g' \
|
|
-e 's/Live system/Install Archipelago/g' \
|
|
"$INSTALLER_ISO/boot/grub/grub.cfg"
|
|
|
|
# Ensure 'components' parameter is present (loads additional squashfs modules)
|
|
# Also add 'username=user' to ensure consistent username
|
|
if ! grep -q "components" "$INSTALLER_ISO/boot/grub/grub.cfg"; then
|
|
sed -i 's/boot=live/boot=live components/' "$INSTALLER_ISO/boot/grub/grub.cfg"
|
|
fi
|
|
fi
|
|
|
|
if [ -f "$INSTALLER_ISO/isolinux/live.cfg" ]; then
|
|
echo " Configuring ISOLINUX..."
|
|
sed -i.bak \
|
|
-e 's/Debian GNU\/Linux/Archipelago Installer/g' \
|
|
-e 's/Live system/Install Archipelago/g' \
|
|
"$INSTALLER_ISO/isolinux/live.cfg"
|
|
|
|
# Add components parameter
|
|
if ! grep -q "components" "$INSTALLER_ISO/isolinux/live.cfg"; then
|
|
sed -i 's/boot=live/boot=live components/' "$INSTALLER_ISO/isolinux/live.cfg"
|
|
fi
|
|
fi
|
|
|
|
if [ -f "$INSTALLER_ISO/isolinux/menu.cfg" ]; then
|
|
sed -i.bak \
|
|
-e 's/Debian GNU\/Linux/Archipelago Installer/g' \
|
|
"$INSTALLER_ISO/isolinux/menu.cfg"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 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
|
|
|
|
# Extract MBR from original Debian Live ISO (most reliable for hybrid boot)
|
|
# This preserves the exact MBR that makes the ISO work as a USB drive in Balena Etcher
|
|
echo " Extracting hybrid MBR from original Debian Live ISO..."
|
|
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
|
dd if="$BASE_ISO" bs=1 count=432 of="$ISOHDPFX" 2>/dev/null
|
|
|
|
# Verify we got a valid MBR (should be 432 bytes)
|
|
ISOHDPFX_SIZE=$(stat -c%s "$ISOHDPFX" 2>/dev/null || stat -f%z "$ISOHDPFX" 2>/dev/null || echo 0)
|
|
if [ "$ISOHDPFX_SIZE" -ne 432 ]; then
|
|
echo " ⚠️ MBR extraction unexpected size ($ISOHDPFX_SIZE), trying syslinux paths..."
|
|
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 $path"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Find the EFI boot image — 7z may extract it to different locations
|
|
EFI_IMG=""
|
|
for efi_path in \
|
|
"$INSTALLER_ISO/boot/grub/efi.img" \
|
|
"$INSTALLER_ISO/EFI/boot/efi.img" \
|
|
"$INSTALLER_ISO/efi.img"; do
|
|
if [ -f "$efi_path" ]; then
|
|
EFI_IMG="$efi_path"
|
|
break
|
|
fi
|
|
done
|
|
|
|
# If no standalone efi.img, check for [BOOT] directory from 7z extraction
|
|
if [ -z "$EFI_IMG" ] && [ -d "$INSTALLER_ISO/[BOOT]" ]; then
|
|
# 7z extracts El Torito boot images into [BOOT]/ — the EFI image is usually entry 2
|
|
for entry in "$INSTALLER_ISO/[BOOT]/"*; do
|
|
# EFI images are typically > 1MB FAT filesystems
|
|
if [ -f "$entry" ]; then
|
|
entry_size=$(stat -c%s "$entry" 2>/dev/null || stat -f%z "$entry" 2>/dev/null || echo 0)
|
|
if [ "$entry_size" -gt 1048576 ]; then
|
|
mkdir -p "$INSTALLER_ISO/boot/grub"
|
|
cp "$entry" "$INSTALLER_ISO/boot/grub/efi.img"
|
|
EFI_IMG="$INSTALLER_ISO/boot/grub/efi.img"
|
|
echo " Recovered EFI image from [BOOT] directory"
|
|
break
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -z "$EFI_IMG" ]; then
|
|
echo " ⚠️ No EFI boot image found — 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
|
|
# Make EFI path relative to INSTALLER_ISO for xorriso
|
|
EFI_REL="${EFI_IMG#$INSTALLER_ISO/}"
|
|
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 "$EFI_REL" \
|
|
-no-emul-boot \
|
|
-isohybrid-gpt-basdat \
|
|
-partition_offset 16 \
|
|
"$INSTALLER_ISO"
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$UNBUNDLED" = "1" ]; then
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ ✅ UNBUNDLED AUTO-INSTALLER ISO CREATED! ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo "📀 Output: $OUTPUT_ISO"
|
|
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
|
|
echo ""
|
|
echo "🔥 Lightweight installer — apps downloaded on-demand!"
|
|
echo ""
|
|
echo "Features:"
|
|
echo " • Pre-built system (no internet needed during install)"
|
|
echo " • Auto-detects internal disk"
|
|
echo " • One-button installation"
|
|
echo " • Boots directly to Archipelago after install"
|
|
echo " • NO pre-bundled apps (smaller ISO)"
|
|
echo " • Install any app from the Marketplace (internet required)"
|
|
echo ""
|
|
else
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ ✅ AUTO-INSTALLER ISO CREATED! ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
echo ""
|
|
echo "📀 Output: $OUTPUT_ISO"
|
|
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
|
|
echo ""
|
|
echo "🔥 This is a StartOS-like automatic installer!"
|
|
echo ""
|
|
echo "Features:"
|
|
echo " • Pre-built system (no internet needed during install)"
|
|
echo " • Auto-detects internal disk"
|
|
echo " • One-button installation"
|
|
echo " • Boots directly to Archipelago after install"
|
|
echo " • Pre-bundled container apps:"
|
|
echo " - Bitcoin Knots v29"
|
|
echo " - LND v0.18.4"
|
|
echo " - Home Assistant"
|
|
echo ""
|
|
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 ""
|