#!/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" </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 --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=1500000000 # ~1.5GB min (Debian 13 Live standard ~1.9GB) # 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 ISO_URL="https://cdimage.debian.org/debian-cd/current-live/${DEB_ARCH}/iso-hybrid/debian-live-13.3.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 creates it with a name like "download" or the actual filename) if [ -f "download" ]; then mv "download" "$BASE_ISO" elif [ -f "debian-live-13.3.0-${DEB_ARCH}-standard.iso" ]; then mv "debian-live-13.3.0-${DEB_ARCH}-standard.iso" "$BASE_ISO" else echo " ❌ Downloaded file not found" 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 </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 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..." 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 < /mnt/target/etc/hostname cat > /mnt/target/etc/hosts </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 # Secure Boot chain: replace unsigned GRUB with signed shim+grub for Secure Boot compatibility # Framework laptops and other Secure Boot-enabled machines need this chain: # BOOTX64.EFI (shimx64, Microsoft-signed) → grubx64.efi (Debian-signed) → kernel echo " Setting up Secure Boot chain..." if [ "$ARCH" = "x86_64" ]; then SHIM_SRC="/mnt/target/usr/lib/shim/shimx64.efi.signed" GRUB_SIGNED_SRC="/mnt/target/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed" EFI_BOOT_BINARY="BOOTX64.EFI" GRUB_EFI_BINARY="grubx64.efi" SHIM_EFI_BINARY="shimx64.efi" else SHIM_SRC="/mnt/target/usr/lib/shim/shimaa64.efi.signed" GRUB_SIGNED_SRC="/mnt/target/usr/lib/grub/arm64-efi-signed/grubaa64.efi.signed" EFI_BOOT_BINARY="BOOTAA64.EFI" GRUB_EFI_BINARY="grubaa64.efi" SHIM_EFI_BINARY="shimaa64.efi" fi EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT" EFI_ARCHY_DIR="/mnt/target/boot/efi/EFI/archipelago" if [ -f "$SHIM_SRC" ] && [ -f "$GRUB_SIGNED_SRC" ]; then # Fallback path — what UEFI firmware checks when no boot entry exists mkdir -p "$EFI_BOOT_DIR" cp "$SHIM_SRC" "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" cp "$GRUB_SIGNED_SRC" "$EFI_BOOT_DIR/$GRUB_EFI_BINARY" # Named entry path — for efibootmgr-registered entries mkdir -p "$EFI_ARCHY_DIR" cp "$SHIM_SRC" "$EFI_ARCHY_DIR/$SHIM_EFI_BINARY" cp "$GRUB_SIGNED_SRC" "$EFI_ARCHY_DIR/$GRUB_EFI_BINARY" echo " ✅ Secure Boot chain installed (shim + signed GRUB)" else echo " ⚠️ Signed shim/GRUB not found — Secure Boot machines must disable Secure Boot" [ ! -f "$SHIM_SRC" ] && echo " Missing: $(basename $SHIM_SRC)" [ ! -f "$GRUB_SIGNED_SRC" ] && echo " Missing: $(basename $GRUB_SIGNED_SRC)" 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 # 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:// ║${NC}" echo -e "${GREEN}║ • SSH: ssh archipelago@ ║${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 "" read -p "Press Enter to reboot..." 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 ""