Archipelago companion-v0.5.10
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
results/
|
||||
*.deb
|
||||
@@ -0,0 +1,195 @@
|
||||
# Live Server to ISO Build Integration Guide
|
||||
|
||||
This document explains how to keep the ISO build synchronized with the live development server.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Develop and Test on Live Server
|
||||
|
||||
```bash
|
||||
# Make changes locally
|
||||
vim core/archipelago/src/...
|
||||
|
||||
# Deploy to live server for testing
|
||||
./scripts/deploy-to-target.sh --live
|
||||
|
||||
# Test at http://192.168.1.228
|
||||
# Check logs: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago -f'
|
||||
```
|
||||
|
||||
### 2. Capture System Changes
|
||||
|
||||
When you make system-level changes on the live server (nginx config, systemd service, etc.):
|
||||
|
||||
```bash
|
||||
cd image-recipe
|
||||
./sync-from-live.sh
|
||||
```
|
||||
|
||||
This automatically captures:
|
||||
- `/etc/systemd/system/archipelago.service` → `configs/archipelago.service`
|
||||
- `/etc/nginx/sites-available/archipelago` → `configs/nginx-archipelago.conf`
|
||||
- `/etc/logrotate.d/archipelago` → `configs/logrotate.conf`
|
||||
|
||||
### 3. Build New ISO
|
||||
|
||||
```bash
|
||||
# Build backend and frontend
|
||||
./scripts/build-backend.sh
|
||||
./scripts/build-frontend.sh
|
||||
|
||||
# Build ISO with latest changes
|
||||
./build-debian-iso.sh
|
||||
|
||||
# Test in QEMU
|
||||
./test-iso-qemu.sh
|
||||
```
|
||||
|
||||
### 4. Verify Integration
|
||||
|
||||
The ISO build script should:
|
||||
1. Copy `configs/archipelago.service` to `/etc/systemd/system/`
|
||||
2. Copy `configs/nginx-archipelago.conf` to `/etc/nginx/sites-available/archipelago`
|
||||
3. Create symlink: `/etc/nginx/sites-enabled/archipelago`
|
||||
4. Enable the service: `systemctl enable archipelago`
|
||||
5. Install backend to `/usr/local/bin/archipelago`
|
||||
6. Install frontend to `/opt/archipelago/web-ui/`
|
||||
|
||||
## Critical Configuration Settings
|
||||
|
||||
### Backend Service (archipelago.service)
|
||||
|
||||
**Must-have settings**:
|
||||
```ini
|
||||
[Service]
|
||||
User=root # Required for root Podman access
|
||||
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678" # Backend API port
|
||||
Environment="ARCHIPELAGO_DEV_MODE=true" # Enable container auto-detection
|
||||
```
|
||||
|
||||
**Why root?**: The backend must run as root to access containers started with `sudo podman`. Containers in root Podman context are invisible to rootless Podman.
|
||||
|
||||
### Nginx Configuration (nginx-archipelago.conf)
|
||||
|
||||
**Must-have proxies**:
|
||||
```nginx
|
||||
location /rpc/ {
|
||||
proxy_pass http://127.0.0.1:5678; # Backend RPC endpoint
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:5678; # WebSocket for real-time updates
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
## File Paths Reference
|
||||
|
||||
### Build Artifacts
|
||||
- `build/backend/archipelago` - Compiled Rust backend
|
||||
- `build/frontend/` - Built Vue.js frontend
|
||||
- `configs/` - System configuration files
|
||||
- `results/` - Built ISO images
|
||||
|
||||
### Live Server Paths
|
||||
- `/usr/local/bin/archipelago` - Backend binary
|
||||
- `/opt/archipelago/web-ui/` - Frontend files
|
||||
- `/etc/systemd/system/archipelago.service` - Service definition
|
||||
- `/etc/nginx/sites-available/archipelago` - Nginx config
|
||||
- `/var/lib/archipelago/` - Application data
|
||||
|
||||
### ISO Installation Paths
|
||||
Same as live server (above) - the ISO must replicate the exact file structure.
|
||||
|
||||
## Container Management
|
||||
|
||||
### Root vs Rootless Podman
|
||||
|
||||
**Current approach**: Root Podman
|
||||
- Containers started with: `sudo podman run ...`
|
||||
- Backend runs as: `root` user (in systemd)
|
||||
- Container detection: Works automatically in dev mode
|
||||
|
||||
**Why not rootless?**
|
||||
- Would require `User=archipelago` in systemd service
|
||||
- All containers must be started as `archipelago` user
|
||||
- More complex permission management
|
||||
|
||||
### Container Detection
|
||||
|
||||
The backend automatically detects running containers when:
|
||||
1. `ARCHIPELAGO_DEV_MODE=true` is set
|
||||
2. Backend runs with same privileges as container runtime
|
||||
3. Containers exist in accessible Podman context
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Containers not detected in ISO
|
||||
|
||||
**Cause**: Backend not running as root, or dev mode disabled
|
||||
|
||||
**Fix**:
|
||||
1. Check `configs/archipelago.service` has `User=root`
|
||||
2. Check `Environment="ARCHIPELAGO_DEV_MODE=true"` is set
|
||||
3. Rebuild ISO and test
|
||||
|
||||
### Issue: UI not loading
|
||||
|
||||
**Cause**: Nginx config not copied or frontend files missing
|
||||
|
||||
**Fix**:
|
||||
1. Verify `configs/nginx-archipelago.conf` exists
|
||||
2. Check frontend built to `build/frontend/`
|
||||
3. Verify ISO build script copies these files
|
||||
|
||||
### Issue: Backend won't start
|
||||
|
||||
**Cause**: Binary permissions or missing dependencies
|
||||
|
||||
**Fix**:
|
||||
1. Check backend binary is executable: `chmod +x /usr/local/bin/archipelago`
|
||||
2. Check dependencies installed (Podman, nginx)
|
||||
3. Review systemd logs: `journalctl -u archipelago`
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before releasing an ISO, verify:
|
||||
|
||||
- [ ] Boot ISO in QEMU
|
||||
- [ ] Systemd service starts: `systemctl status archipelago`
|
||||
- [ ] Backend responds: `curl http://localhost:5678/health`
|
||||
- [ ] UI accessible: Open browser to `http://localhost`
|
||||
- [ ] Container detection: `sudo podman run -d --name test nginx` → Shows in UI
|
||||
- [ ] RPC works: Test login and API calls
|
||||
- [ ] WebSocket connects: Check browser console
|
||||
|
||||
## Automated Build Pipeline (Future)
|
||||
|
||||
To automate this workflow:
|
||||
|
||||
1. **CI/CD Integration**
|
||||
- Trigger on main branch commits
|
||||
- Run `sync-from-live.sh` with credentials
|
||||
- Build backend and frontend
|
||||
- Build ISO
|
||||
- Upload to releases
|
||||
|
||||
2. **Version Management**
|
||||
- Tag releases with semantic versions
|
||||
- Include git commit hash in ISO metadata
|
||||
- Track which configs were included
|
||||
|
||||
3. **Testing Automation**
|
||||
- Boot ISO in headless QEMU
|
||||
- Run API tests
|
||||
- Verify container detection
|
||||
- Generate test report
|
||||
|
||||
## Resources
|
||||
|
||||
- Development Workflow Rules: `.cursor/rules/Development-Workflow.mdc`
|
||||
- Build Checklist: `ISO-BUILD-CHECKLIST.md`
|
||||
- Architecture Docs: `.cursor/rules/Architecture.mdc`
|
||||
- Deployment Scripts: `scripts/deploy-to-target.sh`
|
||||
@@ -0,0 +1,88 @@
|
||||
# Archipelago OS Image Recipes
|
||||
|
||||
Build scripts for creating bootable Debian Linux OS images for Archipelago Bitcoin Node OS.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build the ISO
|
||||
|
||||
```bash
|
||||
# 1. Sync latest configs from live dev server
|
||||
./sync-from-live.sh
|
||||
|
||||
# 2. Build components
|
||||
./scripts/build-backend.sh
|
||||
./scripts/build-frontend.sh
|
||||
|
||||
# 3. Build the ISO
|
||||
./build-debian-iso.sh
|
||||
```
|
||||
|
||||
This creates a bootable Debian Live ISO with Archipelago pre-installed.
|
||||
|
||||
### Write to USB
|
||||
|
||||
```bash
|
||||
# Using dd (recommended)
|
||||
./write-usb-dd.sh /dev/diskN
|
||||
|
||||
# Or use Balena Etcher to flash the ISO
|
||||
```
|
||||
|
||||
See the **ISO-BUILD-CHECKLIST.md** for a comprehensive build workflow.
|
||||
|
||||
See the Architecture documentation for detailed system information.
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Debian Linux Base**: Debian 13 (Trixie) with security updates applied during ISO/install creation
|
||||
- **Podman**: Container runtime for apps (rootless by default)
|
||||
- **Archipelago Backend**: Rust-based API server
|
||||
- **Archipelago Frontend**: Vue.js web interface
|
||||
- **Systemd Services**: Automatic service management
|
||||
- **Network Configuration**: NetworkManager for easy setup
|
||||
|
||||
## Build Output
|
||||
|
||||
- `results/archipelago-installer-x86_64.iso` - Bootable hybrid ISO image
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
- **x86_64**: Dell OptiPlex, HP ProDesk 400 G4 DM, Start9 Server Pure, and other x86_64 machines
|
||||
- **Build Systems**: macOS (requires Docker) and Linux (native or Docker)
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### 1. Live USB Boot
|
||||
Boot from USB, run in live mode to test, or install to disk.
|
||||
|
||||
### 2. Full Disk Installation
|
||||
From the live environment, run:
|
||||
```bash
|
||||
sudo /archipelago/install-to-disk.sh
|
||||
```
|
||||
|
||||
This installs Archipelago to a target disk using debootstrap.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
image-recipe/
|
||||
├── build-debian-iso.sh # Main ISO builder
|
||||
├── write-usb-dd.sh # Write ISO to USB with dd
|
||||
├── create-fat32-usb.sh # Alternative USB creation
|
||||
├── archipelago-scripts/ # Scripts included in ISO
|
||||
│ ├── install-to-disk.sh # Disk installer
|
||||
│ └── setup-bitcoin.sh # Bitcoin Core setup
|
||||
├── scripts/ # Build helper scripts
|
||||
│ ├── build-backend.sh # Compile Rust backend
|
||||
│ ├── build-frontend.sh # Build Vue.js frontend
|
||||
│ └── check-dependencies.sh # Verify build requirements
|
||||
└── results/ # Built ISO output
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker (for macOS builds)
|
||||
- xorriso (for ISO creation): `brew install xorriso`
|
||||
- 7zip (for ISO extraction): `brew install p7zip`
|
||||
@@ -0,0 +1,352 @@
|
||||
name: Build Archipelago ISO (dev)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev-iso]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-iso:
|
||||
runs-on: iso-builder
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
# Direct fetch + sync (actions/checkout token is broken on this Gitea)
|
||||
REPO_DIR="$HOME/archy"
|
||||
cd "$REPO_DIR" && git fetch origin main && git reset --hard origin/main
|
||||
echo "=== Source at commit: $(git log --oneline -1) ==="
|
||||
rsync -a --delete \
|
||||
--exclude '.git' --exclude 'node_modules' --exclude 'target' \
|
||||
--exclude 'image-recipe/build' --exclude 'image-recipe/results' \
|
||||
--exclude 'web/dist' \
|
||||
"$REPO_DIR/" "$GITHUB_WORKSPACE/"
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
echo "=== Workspace version: $(grep '^version' core/archipelago/Cargo.toml) ==="
|
||||
[ -f "scripts/first-boot-containers.sh" ] && echo " first-boot-containers.sh: PRESENT" || echo " first-boot-containers.sh: MISSING"
|
||||
grep -q 'network-alias' scripts/first-boot-containers.sh 2>/dev/null && echo " network-alias fix: PRESENT" || echo " network-alias fix: MISSING"
|
||||
|
||||
- name: Install ISO build dependencies
|
||||
run: |
|
||||
# Skip apt if packages already installed (persistent runner)
|
||||
if dpkg -s debootstrap squashfs-tools xorriso isolinux syslinux-common mtools \
|
||||
grub-efi-amd64-bin grub-pc-bin grub-common musl-tools >/dev/null 2>&1; then
|
||||
echo "ISO build deps already installed, skipping apt"
|
||||
else
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
debootstrap squashfs-tools xorriso \
|
||||
isolinux syslinux-common mtools \
|
||||
grub-efi-amd64-bin grub-pc-bin grub-common \
|
||||
musl-tools
|
||||
fi
|
||||
# Ensure musl Rust target is available
|
||||
source $HOME/.cargo/env 2>/dev/null || true
|
||||
rustup target add x86_64-unknown-linux-musl 2>/dev/null || true
|
||||
|
||||
- name: Build backend (incremental, musl static)
|
||||
run: |
|
||||
source $HOME/.cargo/env 2>/dev/null || true
|
||||
# Build in persistent repo dir to reuse target/ cache
|
||||
cd "$HOME/archy"
|
||||
export GIT_HASH=$(git rev-parse --short HEAD)
|
||||
# Static musl build for portability — ensures binary runs regardless
|
||||
# of glibc version differences between build host and ISO rootfs.
|
||||
cargo build --release --target x86_64-unknown-linux-musl --manifest-path core/Cargo.toml
|
||||
# Copy binary to workspace for downstream steps
|
||||
mkdir -p "$GITHUB_WORKSPACE/core/target/release"
|
||||
cp core/target/x86_64-unknown-linux-musl/release/archipelago "$GITHUB_WORKSPACE/core/target/release/"
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
source $HOME/.nvm/nvm.sh 2>/dev/null || true
|
||||
cd neode-ui && npm ci && npm run build
|
||||
|
||||
- name: Type check frontend
|
||||
run: |
|
||||
source $HOME/.nvm/nvm.sh 2>/dev/null || true
|
||||
cd neode-ui && npx vue-tsc -b --noEmit
|
||||
|
||||
- name: Run frontend tests
|
||||
run: |
|
||||
source $HOME/.nvm/nvm.sh 2>/dev/null || true
|
||||
cd neode-ui && npx vitest run
|
||||
|
||||
- name: Include AIUI if available
|
||||
run: |
|
||||
# AIUI (the Claude chat sidebar) lives outside the Vue build
|
||||
# and must be copied into the frontend dist BEFORE packaging,
|
||||
# otherwise OTA-tarball upgrades silently strip it from nodes
|
||||
# in the field. Try in order: cached on runner, then the
|
||||
# newest release tarball in this repo's releases/ dir as a
|
||||
# fallback so a freshly-provisioned runner still gets AIUI.
|
||||
AIUI_SRC=""
|
||||
if [ -f "/opt/archipelago/web-ui/aiui/index.html" ]; then
|
||||
AIUI_SRC="/opt/archipelago/web-ui/aiui"
|
||||
elif [ -f "$HOME/archy/web/dist/neode-ui/aiui/index.html" ]; then
|
||||
AIUI_SRC="$HOME/archy/web/dist/neode-ui/aiui"
|
||||
else
|
||||
LATEST_FRONTEND=$(ls -t releases/v*/archipelago-frontend-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST_FRONTEND" ]; then
|
||||
echo "Extracting AIUI from $LATEST_FRONTEND (runner cache miss)"
|
||||
TMP=$(mktemp -d)
|
||||
tar xzf "$LATEST_FRONTEND" -C "$TMP" ./aiui 2>/dev/null || true
|
||||
if [ -f "$TMP/aiui/index.html" ]; then
|
||||
AIUI_SRC="$TMP/aiui"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -n "$AIUI_SRC" ]; then
|
||||
mkdir -p web/dist/neode-ui/aiui
|
||||
cp -r "$AIUI_SRC/"* web/dist/neode-ui/aiui/
|
||||
echo "AIUI included from $AIUI_SRC ($(du -sh web/dist/neode-ui/aiui | cut -f1))"
|
||||
else
|
||||
echo "FAIL: AIUI not found anywhere (runner cache + release tarballs)"
|
||||
echo " checked: /opt/archipelago/web-ui/aiui"
|
||||
echo " \$HOME/archy/web/dist/neode-ui/aiui"
|
||||
echo " releases/v*/archipelago-frontend-*.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure root podman for insecure registry
|
||||
run: |
|
||||
sudo mkdir -p /etc/containers/registries.conf.d
|
||||
echo '[[registry]]
|
||||
location = "146.59.87.168:3000"
|
||||
insecure = true' | sudo tee /etc/containers/registries.conf.d/archipelago.conf
|
||||
|
||||
- name: Build unbundled ISO
|
||||
run: |
|
||||
cd image-recipe
|
||||
export ARCHIPELAGO_BIN="$(pwd)/../core/target/release/archipelago"
|
||||
if [ ! -x "$ARCHIPELAGO_BIN" ]; then
|
||||
echo "FAIL: backend binary missing or not executable at $ARCHIPELAGO_BIN"
|
||||
exit 1
|
||||
fi
|
||||
BIN_VERSION=$(strings "$ARCHIPELAGO_BIN" | grep -oE 'archipelago [0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?' | head -1 || true)
|
||||
EXPECTED=$(grep '^version' ../core/archipelago/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
echo "Binary: $ARCHIPELAGO_BIN ($(du -h "$ARCHIPELAGO_BIN" | cut -f1))"
|
||||
echo "Embedded version string: ${BIN_VERSION:-unknown}"
|
||||
echo "Expected version (Cargo.toml): $EXPECTED"
|
||||
sudo -E UNBUNDLED=1 DEV_SERVER=localhost BUILD_FROM_SOURCE=0 \
|
||||
ARCHIPELAGO_BIN="$ARCHIPELAGO_BIN" \
|
||||
./build-auto-installer-iso.sh
|
||||
|
||||
- name: Smoke test ISO
|
||||
run: |
|
||||
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
|
||||
if [ -z "$ISO" ]; then
|
||||
echo "FAIL: No ISO produced"
|
||||
exit 1
|
||||
fi
|
||||
echo "ISO: $ISO ($(du -h "$ISO" | cut -f1))"
|
||||
|
||||
# Mount and verify structure
|
||||
MNT=$(mktemp -d)
|
||||
sudo mount -o loop,ro "$ISO" "$MNT"
|
||||
|
||||
FAIL=0
|
||||
for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \
|
||||
isolinux/isolinux.bin isolinux/isolinux.cfg \
|
||||
boot/grub/grub.cfg EFI/BOOT/BOOTX64.EFI \
|
||||
archipelago/auto-install.sh archipelago/rootfs.tar; do
|
||||
if [ -e "$MNT/$f" ]; then
|
||||
echo " OK: $f ($(sudo du -h "$MNT/$f" 2>/dev/null | cut -f1))"
|
||||
else
|
||||
echo " MISSING: $f"
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify initrd has live-boot
|
||||
INITRD_DIR=$(mktemp -d)
|
||||
sudo unmkinitramfs "$MNT/live/initrd.img" "$INITRD_DIR" 2>/dev/null
|
||||
if [ -e "$INITRD_DIR/scripts/live" ] || [ -e "$INITRD_DIR/main/scripts/live" ]; then
|
||||
echo " OK: initrd has live-boot scripts"
|
||||
else
|
||||
echo " MISSING: live-boot scripts in initrd!"
|
||||
echo " initrd scripts/: $(ls "$INITRD_DIR/scripts/" 2>/dev/null || ls "$INITRD_DIR/main/scripts/" 2>/dev/null)"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# Check GRUB config has boot=live
|
||||
if grep -q "boot=live" "$MNT/boot/grub/grub.cfg"; then
|
||||
echo " OK: grub.cfg has boot=live"
|
||||
else
|
||||
echo " MISSING: boot=live in grub.cfg"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
sudo umount "$MNT" 2>/dev/null
|
||||
rmdir "$MNT" 2>/dev/null
|
||||
sudo rm -r "$INITRD_DIR" 2>/dev/null
|
||||
|
||||
if [ "$FAIL" = "1" ]; then
|
||||
echo "SMOKE TEST FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "SMOKE TEST PASSED"
|
||||
|
||||
- name: QEMU boot test
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
run: |
|
||||
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
|
||||
if [ -n "$ISO" ] && command -v qemu-system-x86_64 >/dev/null 2>&1; then
|
||||
echo "Running headless QEMU boot test..."
|
||||
bash image-recipe/test-iso-qemu.sh "$ISO" 120
|
||||
else
|
||||
echo "Skipping QEMU test (no ISO or QEMU not available)"
|
||||
fi
|
||||
|
||||
- name: Copy to Builds
|
||||
run: |
|
||||
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
|
||||
if [ -n "$ISO" ]; then
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
DEST="/var/lib/archipelago/filebrowser/Builds/archipelago-dev-unbundled-${DATE}.iso"
|
||||
sudo cp "$ISO" "$DEST"
|
||||
sudo chown 1000:1000 "$DEST"
|
||||
echo "ISO: archipelago-dev-unbundled-${DATE}.iso"
|
||||
echo "Size: $(du -h "$DEST" | cut -f1)"
|
||||
echo "SHA256: $(sha256sum "$DEST" | cut -d' ' -f1)"
|
||||
fi
|
||||
|
||||
- name: Publish release artifacts and manifest
|
||||
run: |
|
||||
VERSION=$(grep '^version' core/archipelago/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
RELEASE_DIR="/var/lib/archipelago/filebrowser/Builds/releases/v${VERSION}"
|
||||
sudo mkdir -p "$RELEASE_DIR"
|
||||
|
||||
# Copy backend binary
|
||||
BINARY="core/target/release/archipelago"
|
||||
if [ -f "$BINARY" ]; then
|
||||
sudo cp "$BINARY" "$RELEASE_DIR/archipelago"
|
||||
sudo chmod 755 "$RELEASE_DIR/archipelago"
|
||||
echo "Backend: $(du -h "$RELEASE_DIR/archipelago" | cut -f1)"
|
||||
fi
|
||||
|
||||
# Create frontend archive
|
||||
if [ -d "web/dist/neode-ui" ]; then
|
||||
FRONTEND_ARCHIVE="$RELEASE_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||
sudo tar -czf "$FRONTEND_ARCHIVE" -C web/dist neode-ui
|
||||
echo "Frontend: $(du -h "$FRONTEND_ARCHIVE" | cut -f1)"
|
||||
fi
|
||||
|
||||
# Generate manifest with SHA256 hashes
|
||||
BACKEND_HASH=$(sha256sum "$RELEASE_DIR/archipelago" 2>/dev/null | awk '{print $1}')
|
||||
BACKEND_SIZE=$(stat -c%s "$RELEASE_DIR/archipelago" 2>/dev/null || echo 0)
|
||||
FRONTEND_NAME="archipelago-frontend-${VERSION}.tar.gz"
|
||||
FRONTEND_HASH=$(sha256sum "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null | awk '{print $1}')
|
||||
FRONTEND_SIZE=$(stat -c%s "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null || echo 0)
|
||||
|
||||
# Build download base URL (FileBrowser serves from /Builds/)
|
||||
HOST=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
BASE_URL="http://${HOST:-192.168.1.228}:8083/Builds/releases/v${VERSION}"
|
||||
|
||||
# Generate manifest JSON
|
||||
python3 -c "
|
||||
import json
|
||||
manifest = {
|
||||
'version': '$VERSION',
|
||||
'release_date': '$DATE',
|
||||
'changelog': ['Update to version $VERSION'],
|
||||
'components': []
|
||||
}
|
||||
if '$BACKEND_HASH':
|
||||
manifest['components'].append({
|
||||
'name': 'archipelago',
|
||||
'current_version': '$VERSION',
|
||||
'new_version': '$VERSION',
|
||||
'download_url': '$BASE_URL/archipelago',
|
||||
'sha256': '$BACKEND_HASH',
|
||||
'size_bytes': int('$BACKEND_SIZE' or '0')
|
||||
})
|
||||
if '$FRONTEND_HASH':
|
||||
manifest['components'].append({
|
||||
'name': '$FRONTEND_NAME',
|
||||
'current_version': '$VERSION',
|
||||
'new_version': '$VERSION',
|
||||
'download_url': '$BASE_URL/$FRONTEND_NAME',
|
||||
'sha256': '$FRONTEND_HASH',
|
||||
'size_bytes': int('$FRONTEND_SIZE' or '0')
|
||||
})
|
||||
print(json.dumps(manifest, indent=2))
|
||||
" | sudo tee "$RELEASE_DIR/manifest.json" > /dev/null
|
||||
|
||||
# Also copy manifest to repo releases/ dir for git-based serving
|
||||
cp "$RELEASE_DIR/manifest.json" releases/manifest.json 2>/dev/null || true
|
||||
|
||||
sudo chown -R 1000:1000 "$RELEASE_DIR"
|
||||
echo ""
|
||||
echo "Release manifest:"
|
||||
cat "$RELEASE_DIR/manifest.json"
|
||||
echo ""
|
||||
echo "Artifacts published to: $RELEASE_DIR"
|
||||
|
||||
- name: Build report
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set +eo pipefail
|
||||
echo "══════════════════════════════════════════"
|
||||
echo "DEV ISO BUILD REPORT"
|
||||
echo "══════════════════════════════════════════"
|
||||
echo "Commit: $(git -C "$HOME/archy" rev-parse --short HEAD 2>/dev/null || echo 'unknown') ($(git -C "$HOME/archy" log -1 --format=%s 2>/dev/null || echo 'unknown'))"
|
||||
echo "Branch: ${GITHUB_REF_NAME:-dev-iso}"
|
||||
echo "Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "Runner: $(hostname)"
|
||||
echo ""
|
||||
echo "── Artifacts ──"
|
||||
ls -lh image-recipe/results/*.iso 2>/dev/null || echo " No ISO produced"
|
||||
ls -lh /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso 2>/dev/null | tail -3
|
||||
echo ""
|
||||
echo "── Rootfs contents check ──"
|
||||
ROOTFS=$(ls image-recipe/build/auto-installer/archipelago-rootfs.tar 2>/dev/null) || true
|
||||
if [ -n "$ROOTFS" ]; then
|
||||
echo " rootfs.tar: $(sudo du -h "$ROOTFS" 2>/dev/null | cut -f1 || echo 'unknown')"
|
||||
# List key paths once (podman export omits ./ prefix, so match without it)
|
||||
ROOTFS_LIST=$(sudo tar tf "$ROOTFS" 2>/dev/null | grep -E '(etc/nginx/sites-available/archipelago|etc/archipelago/ssl/archipelago.crt|usr/local/bin/archipelago-kiosk-launcher|usr/local/bin/archipelago|opt/archipelago/web-ui/index.html)' || true)
|
||||
for item in \
|
||||
"nginx config:etc/nginx/sites-available/archipelago" \
|
||||
"SSL cert:etc/archipelago/ssl/archipelago.crt" \
|
||||
"kiosk launcher:usr/local/bin/archipelago-kiosk-launcher" \
|
||||
"backend binary:usr/local/bin/archipelago" \
|
||||
"web-ui index:opt/archipelago/web-ui/index.html"; do
|
||||
label="${item%%:*}"; path="${item#*:}"
|
||||
echo "$ROOTFS_LIST" | grep -q "$path" && echo " $label: PRESENT" || echo " $label: MISSING"
|
||||
done
|
||||
else
|
||||
echo " rootfs.tar not found in workspace"
|
||||
fi
|
||||
echo ""
|
||||
echo "── ISO contents check ──"
|
||||
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1) || true
|
||||
if [ -n "$ISO" ]; then
|
||||
echo " ISO size: $(sudo du -h "$ISO" 2>/dev/null | cut -f1 || echo 'unknown')"
|
||||
ISO_MOUNT=$(mktemp -d)
|
||||
if sudo mount -o loop,ro "$ISO" "$ISO_MOUNT" 2>/dev/null; then
|
||||
echo " auto-install.sh: $([ -f "$ISO_MOUNT/archipelago/auto-install.sh" ] && echo 'PRESENT' || echo 'MISSING')"
|
||||
echo " rootfs.tar: $([ -f "$ISO_MOUNT/archipelago/rootfs.tar" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/archipelago/rootfs.tar" 2>/dev/null | cut -f1))" || echo 'MISSING')"
|
||||
echo " backend bin: $([ -f "$ISO_MOUNT/archipelago/bin/archipelago" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/archipelago/bin/archipelago" 2>/dev/null | cut -f1))" || echo 'MISSING')"
|
||||
echo " frontend: $([ -f "$ISO_MOUNT/archipelago/web-ui/index.html" ] && echo 'PRESENT' || echo 'MISSING')"
|
||||
echo " vmlinuz: $([ -f "$ISO_MOUNT/live/vmlinuz" ] && echo 'PRESENT' || echo 'MISSING')"
|
||||
echo " initrd: $([ -f "$ISO_MOUNT/live/initrd.img" ] && echo 'PRESENT' || echo 'MISSING')"
|
||||
echo " squashfs: $([ -f "$ISO_MOUNT/live/filesystem.squashfs" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/live/filesystem.squashfs" 2>/dev/null | cut -f1))" || echo 'MISSING')"
|
||||
echo " grub theme: $([ -d "$ISO_MOUNT/boot/grub/themes/archipelago" ] && echo 'PRESENT' || echo 'MISSING')"
|
||||
sudo umount "$ISO_MOUNT" 2>/dev/null || true
|
||||
else
|
||||
echo " Could not mount ISO for inspection"
|
||||
fi
|
||||
rmdir "$ISO_MOUNT" 2>/dev/null || true
|
||||
fi
|
||||
echo "══════════════════════════════════════════"
|
||||
|
||||
- name: Fix workspace permissions
|
||||
if: always()
|
||||
run: |
|
||||
sudo chown -R $(id -u):$(id -g) . 2>/dev/null || true
|
||||
sudo chmod -R u+rwX . 2>/dev/null || true
|
||||
sudo chown -R $(id -u):$(id -g) "$HOME/.cache/act" 2>/dev/null || true
|
||||
sudo chmod -R u+rwX "$HOME/.cache/act" 2>/dev/null || true
|
||||
@@ -0,0 +1,86 @@
|
||||
# Archipelago ISO Build - Quick Guide
|
||||
|
||||
## TL;DR - Build ISO with Live Server State
|
||||
|
||||
```bash
|
||||
cd ~/archy/image-recipe
|
||||
sudo bash build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
The script will automatically:
|
||||
1. Try to capture backend from `/usr/local/bin/archipelago`
|
||||
2. Try to capture frontend from `/opt/archipelago/web-ui`
|
||||
3. Fall back to building from source if capture fails
|
||||
|
||||
## Build Modes
|
||||
|
||||
### Default: Capture from Dev Server (RECOMMENDED)
|
||||
```bash
|
||||
# From your Mac (captures from remote dev server):
|
||||
cd image-recipe
|
||||
DEV_SERVER=archipelago@192.168.1.228 sudo bash build-auto-installer-iso.sh
|
||||
|
||||
# From the dev server itself:
|
||||
cd ~/archy/image-recipe
|
||||
sudo bash build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
### Alternative: Build from Source
|
||||
```bash
|
||||
BUILD_FROM_SOURCE=1 sudo bash build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
## Known Issues & Workarounds
|
||||
|
||||
### Issue: Can't capture from localhost via SCP
|
||||
|
||||
**Problem**: When running on the server itself, `scp localhost:/path` doesn't work.
|
||||
|
||||
**Workaround**: Use direct file copy instead:
|
||||
```bash
|
||||
# Instead of building on the server, build from your Mac:
|
||||
cd ~/Projects/archy/image-recipe
|
||||
DEV_SERVER=archipelago@192.168.1.228 sudo bash build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
### Issue: Podman registry not configured
|
||||
|
||||
**Problem**: Podman can't pull images because `/etc/containers/registries.conf` has no unqualified-search registries.
|
||||
|
||||
**Fix**:
|
||||
```bash
|
||||
ssh archipelago@192.168.1.228
|
||||
sudo tee -a /etc/containers/registries.conf <<EOF
|
||||
[registries.search]
|
||||
registries = ['docker.io']
|
||||
EOF
|
||||
```
|
||||
|
||||
## Flash ISO to USB
|
||||
|
||||
```bash
|
||||
cd ~/Projects/archy/image-recipe
|
||||
./write-usb-dd.sh /dev/diskX
|
||||
```
|
||||
|
||||
## What Gets Captured
|
||||
|
||||
From your dev server (192.168.1.228):
|
||||
- ✅ Backend binary: `/usr/local/bin/archipelago` (6.2M)
|
||||
- ✅ Frontend: `/opt/archipelago/web-ui` (~64M)
|
||||
- ✅ Nginx config: `/etc/nginx/sites-available/default`
|
||||
- ✅ Systemd service: `/etc/systemd/system/archipelago.service`
|
||||
- ✅ App manifests: `~/archy/apps/`
|
||||
|
||||
## Current Status
|
||||
|
||||
**Latest Working ISO**: `archipelago-debian-13-x86_64.iso` (469M, built 18:28)
|
||||
- This ISO was built earlier today
|
||||
- Contains the auto-installer
|
||||
- **Should be tested** - might already have your live server state
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Flash the existing ISO** and test it on the Dell OptiPlex
|
||||
2. **Fix the build script** to properly capture from localhost (use `cp` instead of `scp`)
|
||||
3. **Configure Podman registries** on dev server for fallback source builds
|
||||
@@ -0,0 +1,155 @@
|
||||
# ISO Build Checklist
|
||||
|
||||
This checklist ensures that all changes from the live development server are properly integrated into the ISO build.
|
||||
|
||||
## Pre-Build Steps
|
||||
|
||||
### 1. Sync System Configurations from Live Server
|
||||
|
||||
```bash
|
||||
cd image-recipe
|
||||
./sync-from-live.sh
|
||||
```
|
||||
|
||||
This captures:
|
||||
- [ ] Systemd service configuration (`archipelago.service`) - **User=root** required for Podman
|
||||
- [ ] Nginx configuration (`nginx-archipelago.conf`) - includes app proxies (Nextcloud, Vaultwarden, Immich, Penpot)
|
||||
- [ ] Logrotate configuration (if exists)
|
||||
- [ ] Any custom scripts in `/opt/archipelago/scripts/`
|
||||
|
||||
**Critical**: `build-auto-installer-iso.sh` uses `configs/` for nginx and archipelago.service. Ensure these are synced before building.
|
||||
|
||||
### 2. Verify Code Changes
|
||||
|
||||
Ensure all code changes are committed:
|
||||
- [ ] Backend changes in `core/`
|
||||
- [ ] Frontend changes in `neode-ui/`
|
||||
- [ ] Script changes in `scripts/`
|
||||
|
||||
### 3. Build Components
|
||||
|
||||
```bash
|
||||
cd image-recipe
|
||||
|
||||
# Build backend
|
||||
./scripts/build-backend.sh
|
||||
|
||||
# Build frontend
|
||||
./scripts/build-frontend.sh
|
||||
```
|
||||
|
||||
Verify builds:
|
||||
- [ ] Backend binary exists: `build/backend/archipelago`
|
||||
- [ ] Frontend files exist: `build/frontend/index.html`
|
||||
|
||||
## Integration Check
|
||||
|
||||
### 4. Update Build Scripts
|
||||
|
||||
Review and update if needed:
|
||||
- [ ] `integrate-archipelago.sh` - Includes all config files
|
||||
- [ ] `build-debian-iso.sh` - Installs to correct paths
|
||||
|
||||
### 5. Critical Configuration Values
|
||||
|
||||
Verify in `configs/archipelago.service`:
|
||||
- [ ] `User=root` (required for Podman root context)
|
||||
- [ ] `Environment="ARCHIPELAGO_DEV_MODE=true"` (enables container detection)
|
||||
- [ ] `Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"`
|
||||
|
||||
Verify in `configs/nginx-archipelago.conf`:
|
||||
- [ ] Root path: `/opt/archipelago/web-ui`
|
||||
- [ ] RPC proxy: `/rpc/` → `http://127.0.0.1:5678`
|
||||
- [ ] WebSocket proxy: `/ws` → `http://127.0.0.1:5678`
|
||||
|
||||
## Build Process
|
||||
|
||||
### 6. Build the ISO
|
||||
|
||||
```bash
|
||||
./build-debian-iso.sh
|
||||
```
|
||||
|
||||
Expected output:
|
||||
- [ ] ISO created in `results/` directory
|
||||
- [ ] No build errors
|
||||
- [ ] File size reasonable (~500MB - 2GB)
|
||||
|
||||
### 7. Test in QEMU
|
||||
|
||||
```bash
|
||||
./test-iso-qemu.sh
|
||||
```
|
||||
|
||||
Test checklist:
|
||||
- [ ] ISO boots successfully
|
||||
- [ ] Backend service starts: `systemctl status archipelago`
|
||||
- [ ] Nginx serves frontend
|
||||
- [ ] Can access UI at `http://localhost:8080` (or mapped port)
|
||||
- [ ] Container detection works: Check logs for "Detected container"
|
||||
|
||||
## App Stack Hardening (Immich, Penpot, etc.)
|
||||
|
||||
The first-boot script and deploy script ensure:
|
||||
- [ ] **Immich**: Old single-container `immich` (wrong port) is removed before creating `immich_server` stack
|
||||
- [ ] **First-boot**: Waits for postgres (pg_isready) before starting Immich server
|
||||
- [ ] **Backend**: `package.install` for Immich removes old container before creating stack
|
||||
- [ ] **Deploy**: Ensures Immich stack on every deploy, cleans up conflicts
|
||||
|
||||
## Post-Build
|
||||
|
||||
### 8. Write to USB (Optional)
|
||||
|
||||
```bash
|
||||
./write-usb-dd.sh /dev/diskN
|
||||
```
|
||||
|
||||
Or use Balena Etcher to flash the ISO.
|
||||
|
||||
### 9. Test on Real Hardware
|
||||
|
||||
- [ ] Boot from USB
|
||||
- [ ] Network configuration works
|
||||
- [ ] All services start automatically
|
||||
- [ ] Can access web UI
|
||||
- [ ] Containers are detected and managed
|
||||
|
||||
## Deployment Paths Reference
|
||||
|
||||
The ISO build must install to these paths:
|
||||
|
||||
| Component | Path | Source |
|
||||
|-----------|------|--------|
|
||||
| Backend binary | `/usr/local/bin/archipelago` | `build/backend/archipelago` |
|
||||
| Frontend files | `/opt/archipelago/web-ui/` | `build/frontend/*` |
|
||||
| Systemd service | `/etc/systemd/system/archipelago.service` | `configs/archipelago.service` |
|
||||
| Nginx config | `/etc/nginx/sites-available/archipelago` | `configs/nginx-archipelago.conf` |
|
||||
| Nginx symlink | `/etc/nginx/sites-enabled/archipelago` | Link to sites-available |
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Backend Not Detecting Containers
|
||||
- Verify service runs as `root` user
|
||||
- Check Podman context: `sudo podman ps` should show containers
|
||||
- Enable dev mode: `ARCHIPELAGO_DEV_MODE=true`
|
||||
|
||||
### UI Not Loading
|
||||
- Check nginx configuration paths
|
||||
- Verify frontend files deployed to `/opt/archipelago/web-ui/`
|
||||
- Check nginx error logs: `/var/log/nginx/error.log`
|
||||
|
||||
### Service Fails to Start
|
||||
- Check binary permissions: Should be executable
|
||||
- Check systemd logs: `journalctl -u archipelago`
|
||||
- Test binary manually: `sudo /usr/local/bin/archipelago`
|
||||
|
||||
## Version Tracking
|
||||
|
||||
When building a new ISO, document:
|
||||
- Date: _______________
|
||||
- Git commit: _______________
|
||||
- Backend version: _______________
|
||||
- Frontend version: _______________
|
||||
- ISO filename: _______________
|
||||
- Tested on hardware: _______________
|
||||
- Issues found: _______________
|
||||
@@ -0,0 +1,37 @@
|
||||
# Archived ISO build recipes
|
||||
|
||||
These scripts built the Archipelago auto-installer ISO (bundled and
|
||||
unbundled variants). As of v1.7.43-alpha, ISOs are no longer part of the
|
||||
release deliverable. Releases ship as tarballs consumed by
|
||||
`scripts/self-update.sh` on existing nodes.
|
||||
|
||||
Archived here rather than deleted so they can be resurrected if ISO
|
||||
distribution is reintroduced.
|
||||
|
||||
## Contents
|
||||
|
||||
- `build-auto-installer-iso.sh` — orchestrator, bundles container images into squashfs
|
||||
- `build-unbundled-iso.sh` — thin wrapper that sets BUNDLE_IMAGES=0 and delegates
|
||||
- `test-iso-qemu.sh` — smoke-tests a built ISO under QEMU
|
||||
- `scripts/convert-iso-to-disk.sh` — converts an ISO to a raw disk image
|
||||
- `BUILD-ISO-STATUS.md`, `ISO-BUILD-CHECKLIST.md` — contributor guides
|
||||
- `branding/isohdpfx.bin` — isolinux MBR hybrid image
|
||||
- `.gitea-workflows/build-iso-dev.yml` — CI workflow that ran the build+smoke-test
|
||||
|
||||
## To resurrect
|
||||
|
||||
1. `git mv image-recipe/_archived/* image-recipe/` (adjust paths back)
|
||||
2. Restore `.gitea/workflows/build-iso-dev.yml`
|
||||
3. Re-add release-process references (see `scripts/create-release.sh`,
|
||||
`docs/BETA-RELEASE-CHECKLIST.md`, `docs/hotfix-process.md`, `README.md`).
|
||||
|
||||
## Why archived
|
||||
|
||||
The release flow is simpler and faster as tarball-only:
|
||||
- `releases/vX.Y.Z-alpha/archipelago` (backend binary)
|
||||
- `releases/vX.Y.Z-alpha/archipelago-frontend-X.Y.Z-alpha.tar.gz` (frontend + AIUI + filebrowser UI assets)
|
||||
- `releases/manifest.json` (pointers + changelog)
|
||||
|
||||
Nodes pull these via `scripts/self-update.sh` from either Gitea mirror.
|
||||
Filebrowser and AIUI remain bundled inside the frontend tarball and deployed
|
||||
atomically by `self-update.sh`.
|
||||
Binary file not shown.
+3838
File diff suppressed because it is too large
Load Diff
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Archipelago UNBUNDLED Auto-Installer ISO
|
||||
#
|
||||
# Same as build-auto-installer-iso.sh but WITHOUT pre-bundled container images.
|
||||
# Users install all apps on-demand from the Marketplace (requires internet).
|
||||
#
|
||||
# Benefits:
|
||||
# - Much smaller ISO (~1-2GB vs ~8-10GB)
|
||||
# - Faster build (no image pulling/saving)
|
||||
# - Faster install (no image copying/loading)
|
||||
#
|
||||
# Trade-offs:
|
||||
# - Internet required after first boot to install apps
|
||||
# - No apps pre-loaded — everything comes from Marketplace
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./build-unbundled-iso.sh
|
||||
# DEV_SERVER=archipelago@192.168.1.228 sudo ./build-unbundled-iso.sh
|
||||
# BUILD_FROM_SOURCE=1 sudo ./build-unbundled-iso.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
DEV_SERVER="${DEV_SERVER:-archipelago@192.168.1.228}"
|
||||
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Delegate to the main build script with UNBUNDLED mode
|
||||
export UNBUNDLED=1
|
||||
exec "$SCRIPT_DIR/build-auto-installer-iso.sh" "$@"
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# Convert ISO image to bootable disk image
|
||||
# Creates a raw disk image that can be flashed directly
|
||||
|
||||
set -e
|
||||
|
||||
OUTPUT_DIR="${1:-../results}"
|
||||
ARCHIPELAGO_VERSION="${ARCHIPELAGO_VERSION:-0.1.0}"
|
||||
ARCH="${ARCH:-x86_64}"
|
||||
|
||||
echo "💾 Converting ISO to disk image..."
|
||||
|
||||
# Find ISO file
|
||||
ISO_FILE=$(ls "$OUTPUT_DIR"/*.iso 2>/dev/null | head -1)
|
||||
if [ -z "$ISO_FILE" ]; then
|
||||
echo "❌ No ISO file found in $OUTPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Source ISO: $ISO_FILE"
|
||||
|
||||
# Create disk image (4GB minimum)
|
||||
DISK_SIZE=4096 # 4GB in MB
|
||||
DISK_IMG="$OUTPUT_DIR/archipelago-${ARCHIPELAGO_VERSION}-${ARCH}.img"
|
||||
|
||||
echo " Creating disk image: $DISK_IMG"
|
||||
|
||||
# Check if we have required tools
|
||||
if ! command -v dd >/dev/null 2>&1; then
|
||||
echo "❌ dd not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create empty disk image
|
||||
dd if=/dev/zero of="$DISK_IMG" bs=1M count=$DISK_SIZE 2>/dev/null || {
|
||||
echo "❌ Failed to create disk image"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Note: Full disk image creation with partitions requires:
|
||||
# - parted or fdisk
|
||||
# - mkfs.vfat, mkfs.ext4
|
||||
# - losetup (Linux only)
|
||||
# - grub-install
|
||||
|
||||
# For now, we'll create a simple approach:
|
||||
# The ISO can be used directly, or users can use tools like:
|
||||
# - balenaEtcher (macOS/Linux GUI)
|
||||
# - Rufus (Windows)
|
||||
# - dd (command line)
|
||||
|
||||
echo "⚠️ Full disk image conversion requires additional tools"
|
||||
echo " For now, use the ISO file directly with:"
|
||||
echo " - balenaEtcher (recommended)"
|
||||
echo " - dd command (see docs)"
|
||||
echo ""
|
||||
echo " ISO file: $ISO_FILE"
|
||||
echo " Size: $(du -h "$ISO_FILE" | cut -f1)"
|
||||
|
||||
# Clean up empty image file
|
||||
rm -f "$DISK_IMG"
|
||||
|
||||
echo ""
|
||||
echo "💡 Tip: Use the ISO file with a USB flashing tool"
|
||||
echo " The ISO is bootable and can be flashed directly"
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
# Test Archipelago ISO in QEMU
|
||||
#
|
||||
# Usage:
|
||||
# ./test-iso-qemu.sh [path-to-iso] [--bios] [--nographic]
|
||||
#
|
||||
# Options:
|
||||
# --bios Force legacy BIOS mode (default: UEFI)
|
||||
# --nographic No GUI window, serial console only (great for logging)
|
||||
#
|
||||
# Serial log is always written to /tmp/archipelago-qemu-serial.log
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SERIAL_LOG="/tmp/archipelago-qemu-serial.log"
|
||||
FORCE_BIOS=false
|
||||
NOGRAPHIC=false
|
||||
TIMEOUT=0
|
||||
ISO=""
|
||||
|
||||
# Simple arg parsing. First non-flag positional is the ISO path. A bare
|
||||
# numeric (e.g. `120`) is taken as a boot-test timeout in seconds so CI
|
||||
# can call `test-iso-qemu.sh <iso> 120` without hanging the job. The
|
||||
# pre-fix version used `case *) ISO=...`, which silently overwrote ISO
|
||||
# with the timeout value and sent QEMU looking for a file literally
|
||||
# named "120".
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--bios) FORCE_BIOS=true ;;
|
||||
--nographic) NOGRAPHIC=true ;;
|
||||
--timeout=*) TIMEOUT="${arg#--timeout=}" ;;
|
||||
[0-9]*) TIMEOUT="$arg" ;;
|
||||
*) ISO="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# A positive TIMEOUT implies headless (no DISPLAY in CI anyway) and keeps
|
||||
# the entire script wrapped in `timeout` to guarantee the job returns.
|
||||
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
|
||||
NOGRAPHIC=true
|
||||
fi
|
||||
|
||||
# Auto-detect ISO
|
||||
if [ -z "$ISO" ]; then
|
||||
ISO=$(ls -t "$SCRIPT_DIR"/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
|
||||
ISO=$(ls -t "$SCRIPT_DIR"/results/archipelago-*.iso 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
|
||||
echo "ISO not found."
|
||||
echo ""
|
||||
echo "Usage: $0 [path-to-iso] [--bios] [--nographic]"
|
||||
echo ""
|
||||
echo "Or place an ISO in: $SCRIPT_DIR/results/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing Archipelago ISO in QEMU"
|
||||
echo " ISO: $ISO"
|
||||
echo " Size: $(du -h "$ISO" | cut -f1)"
|
||||
echo " RAM: 4GB"
|
||||
echo " CPU: 2 cores"
|
||||
echo " Serial: $SERIAL_LOG"
|
||||
echo ""
|
||||
|
||||
# Create test disk if it doesn't exist
|
||||
DISK="/tmp/archipelago-test-disk.qcow2"
|
||||
if [ ! -f "$DISK" ]; then
|
||||
echo "Creating 20GB test disk..."
|
||||
qemu-img create -f qcow2 "$DISK" 20G
|
||||
fi
|
||||
|
||||
# Common QEMU args
|
||||
QEMU_ARGS=(
|
||||
-m 4G
|
||||
-smp 2
|
||||
-boot d
|
||||
-cdrom "$ISO"
|
||||
-drive if=virtio,format=qcow2,file="$DISK"
|
||||
-net nic,model=virtio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8100-:80
|
||||
-serial file:"$SERIAL_LOG"
|
||||
)
|
||||
|
||||
# Display mode
|
||||
if [ "$NOGRAPHIC" = true ]; then
|
||||
QEMU_ARGS+=(-nographic -append "console=ttyS0")
|
||||
else
|
||||
QEMU_ARGS+=(-vga virtio -display default)
|
||||
fi
|
||||
|
||||
echo "Starting VM..."
|
||||
echo "(Serial console logging to $SERIAL_LOG)"
|
||||
echo "(Press Ctrl+Alt+G to release mouse, Ctrl+C to stop VM)"
|
||||
echo ""
|
||||
|
||||
# Detect UEFI firmware
|
||||
OVMF=""
|
||||
if [ "$FORCE_BIOS" = false ]; then
|
||||
if [ -f "/opt/homebrew/share/qemu/edk2-x86_64-code.fd" ]; then
|
||||
OVMF="/opt/homebrew/share/qemu/edk2-x86_64-code.fd"
|
||||
elif [ -f "/usr/share/OVMF/OVMF_CODE.fd" ]; then
|
||||
OVMF="/usr/share/OVMF/OVMF_CODE.fd"
|
||||
fi
|
||||
fi
|
||||
|
||||
run_qemu() {
|
||||
if [ -n "$OVMF" ]; then
|
||||
echo " Boot: UEFI ($OVMF)"
|
||||
qemu-system-x86_64 \
|
||||
-machine q35 \
|
||||
-drive if=pflash,format=raw,readonly=on,file="$OVMF" \
|
||||
"${QEMU_ARGS[@]}"
|
||||
else
|
||||
echo " Boot: Legacy BIOS"
|
||||
qemu-system-x86_64 \
|
||||
-machine pc \
|
||||
"${QEMU_ARGS[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrap the QEMU invocation in `timeout` when a CI caller passed one so
|
||||
# the script always returns instead of hanging on a VM that never exits
|
||||
# its boot loop. Exit 124 from coreutils' timeout is treated as "VM
|
||||
# reached the timeout", which for a CI boot test is success as long as
|
||||
# the serial log shows a kernel reaching userspace — we inspect that
|
||||
# after the QEMU process ends.
|
||||
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
|
||||
timeout --foreground --preserve-status "${TIMEOUT}s" bash -c "$(declare -f run_qemu); run_qemu"
|
||||
rc=$?
|
||||
if [ $rc -eq 124 ] || [ $rc -eq 137 ]; then
|
||||
echo "(QEMU terminated after ${TIMEOUT}s boot-test window)"
|
||||
rc=0
|
||||
fi
|
||||
else
|
||||
run_qemu
|
||||
rc=$?
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "VM stopped. Serial log: $SERIAL_LOG"
|
||||
echo "Last 20 lines:"
|
||||
tail -20 "$SERIAL_LOG" 2>/dev/null
|
||||
|
||||
# Boot-sanity check: the CI wrapper wants a non-zero exit only when the
|
||||
# kernel never reached userspace. Look for a well-known marker emitted
|
||||
# by live-boot/systemd early in the sequence. If the marker never
|
||||
# appeared, surface the real failure; otherwise treat "timeout reached
|
||||
# with a live kernel" as a pass.
|
||||
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null && [ -f "$SERIAL_LOG" ]; then
|
||||
if grep -qE "Welcome to Debian|Reached target|systemd\[1\]:" "$SERIAL_LOG"; then
|
||||
echo " Boot sanity: OK (systemd reached in serial log)"
|
||||
exit 0
|
||||
fi
|
||||
echo " Boot sanity: FAIL — no systemd markers in serial log within ${TIMEOUT}s"
|
||||
exit 1
|
||||
fi
|
||||
exit "${rc:-0}"
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# archipelago main menu
|
||||
# interactive setup for archipelago bitcoin node os
|
||||
#
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Colors (256-color — works on Linux console with fbcon)
|
||||
O=$'\033[38;5;208m' # Orange
|
||||
W=$'\033[1;37m' # Bold white
|
||||
D=$'\033[38;5;242m' # Dim
|
||||
C=$'\033[38;5;37m' # Cyan
|
||||
G=$'\033[38;5;35m' # Green
|
||||
R=$'\033[38;5;196m' # Red
|
||||
Y=$'\033[38;5;220m' # Yellow
|
||||
N=$'\033[0m' # Reset
|
||||
|
||||
# Adaptive centering
|
||||
get_width() { TW=$(tput cols 2>/dev/null || echo 60); [ "$TW" -gt 120 ] && TW=120; }
|
||||
get_width
|
||||
cc() { local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local p=$(( (TW - ${#s}) / 2 )); [ $p -lt 0 ] && p=0; printf "%*s" "$p" ""; echo -e "$1"; }
|
||||
|
||||
# Box helpers (Claude-style rounded corners)
|
||||
bw() { echo $((TW > 52 ? 52 : TW - 4)); }
|
||||
btop() { local w=$(bw); local t="╭"; for i in $(seq 1 $((w-2))); do t="${t}─"; done; cc "${D}${t}╮${N}"; }
|
||||
bbox() { local w=$(bw); local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local pad=$((w - 2 - ${#s})); [ $pad -lt 0 ] && pad=0; local r=""; for i in $(seq 1 $pad); do r="${r} "; done; cc "${D}│${N} $1${r}${D}│${N}"; }
|
||||
bbot() { local w=$(bw); local b="╰"; for i in $(seq 1 $((w-2))); do b="${b}─"; done; cc "${D}${b}╯${N}"; }
|
||||
hrule() { local len=$((TW > 50 ? 50 : TW - 4)); local hr=""; for i in $(seq 1 $len); do hr="${hr}─"; done; cc "${D}${hr}${N}"; }
|
||||
|
||||
# Install required tools on first run (for live mode)
|
||||
install_required_tools() {
|
||||
if [ -f /tmp/.archipelago-tools-installed ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local NEED_TOOLS=0
|
||||
for tool in parted debootstrap mkfs.ext4 mkfs.vfat; do
|
||||
if ! command -v $tool >/dev/null 2>&1; then
|
||||
NEED_TOOLS=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $NEED_TOOLS -eq 1 ]; then
|
||||
echo ""
|
||||
cc "${D}installing required tools...${N}"
|
||||
echo ""
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
|
||||
cc "${G}tools installed${N}"
|
||||
echo ""
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
touch /tmp/.archipelago-tools-installed
|
||||
}
|
||||
|
||||
install_required_tools
|
||||
|
||||
show_banner() {
|
||||
get_width
|
||||
clear
|
||||
echo ""
|
||||
echo -e " ${O}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${N}"
|
||||
echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${N}"
|
||||
echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
|
||||
echo -e " ${D}bitcoin node os${N}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
show_status() {
|
||||
if [ -d /run/live ]; then
|
||||
cc "${R}live mode${N} ${D}(changes won't persist)${N}"
|
||||
else
|
||||
cc "${G}installed${N}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
local podman_ok=0
|
||||
command -v podman >/dev/null 2>&1 && podman_ok=1
|
||||
|
||||
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q bitcoind; then
|
||||
local blocks=$(podman exec bitcoind bitcoin-cli getblockcount 2>/dev/null || echo "syncing")
|
||||
cc "${G}bitcoin${N} ${D}running ($blocks blocks)${N}"
|
||||
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q bitcoind; then
|
||||
cc "${Y}bitcoin${N} ${D}stopped${N}"
|
||||
fi
|
||||
|
||||
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q lnd; then
|
||||
cc "${G}lightning${N} ${D}running${N}"
|
||||
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q lnd; then
|
||||
cc "${Y}lightning${N} ${D}stopped${N}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
main_menu() {
|
||||
while true; do
|
||||
show_banner
|
||||
show_status
|
||||
|
||||
# Connection info
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
if [ -n "$IP" ]; then
|
||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||
cc "${C}web ui${N} ${W}http://$IP${N}"
|
||||
else
|
||||
cc "${C}web ui${N} ${D}http://$IP${N} ${Y}(not started)${N}"
|
||||
fi
|
||||
cc "${C}ssh${N} ${D}archipelago@$IP${N}"
|
||||
else
|
||||
cc "${D}no network detected${N}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
hrule
|
||||
echo ""
|
||||
cc "${D}r${N} refresh status ${D}w${N} start web ui"
|
||||
echo ""
|
||||
cc "${O}1${N} install to disk ${O}5${N} view logs"
|
||||
cc "${O}2${N} setup bitcoin core ${O}6${N} network settings"
|
||||
cc "${O}3${N} setup lightning ${O}7${N} system info"
|
||||
cc "${O}4${N} setup btcpay server"
|
||||
echo ""
|
||||
cc "${D}q quit${N}"
|
||||
echo ""
|
||||
|
||||
local pad=$(( (TW - 18) / 2 ))
|
||||
[ $pad -lt 0 ] && pad=0
|
||||
printf "%*s" "$pad" ""
|
||||
read -p "select option: " choice
|
||||
|
||||
case $choice in
|
||||
r|R)
|
||||
;;
|
||||
w|W)
|
||||
echo ""
|
||||
if command -v archipelago >/dev/null 2>&1; then
|
||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||
cc "${G}backend already running${N}"
|
||||
else
|
||||
cc "${D}starting backend on port 5678...${N}"
|
||||
nohup archipelago >/tmp/archipelago.log 2>&1 &
|
||||
sleep 2
|
||||
if pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||
cc "${G}backend started${N}"
|
||||
else
|
||||
cc "${R}failed — see /tmp/archipelago.log${N}"
|
||||
fi
|
||||
fi
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
echo ""
|
||||
cc "open in browser: ${W}http://$IP${N}"
|
||||
else
|
||||
cc "${R}binary not found at /usr/local/bin/archipelago${N}"
|
||||
fi
|
||||
echo ""
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
1)
|
||||
if [ -f "$SCRIPT_DIR/install-to-disk.sh" ]; then
|
||||
sudo bash "$SCRIPT_DIR/install-to-disk.sh"
|
||||
else
|
||||
echo " installer not found at: $SCRIPT_DIR"
|
||||
fi
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
2)
|
||||
if [ -f "$SCRIPT_DIR/setup-bitcoin.sh" ]; then
|
||||
bash "$SCRIPT_DIR/setup-bitcoin.sh"
|
||||
else
|
||||
echo " bitcoin setup script not found."
|
||||
fi
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
3)
|
||||
if [ -f "$SCRIPT_DIR/setup-lnd.sh" ]; then
|
||||
bash "$SCRIPT_DIR/setup-lnd.sh"
|
||||
else
|
||||
echo " lnd setup script not found."
|
||||
fi
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
4)
|
||||
setup_btcpay
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
5)
|
||||
view_logs
|
||||
;;
|
||||
6)
|
||||
network_settings
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
7)
|
||||
system_info
|
||||
read -sp " press enter to continue..."
|
||||
;;
|
||||
q|Q)
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
sleep 0.5
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
setup_btcpay() {
|
||||
show_banner
|
||||
cc "${W}btcpay server setup${N}"
|
||||
cc "${D}self-hosted bitcoin payment processor${N}"
|
||||
echo ""
|
||||
|
||||
if ! podman ps | grep -q bitcoind; then
|
||||
cc "${R}bitcoin core must be running first${N}"
|
||||
return
|
||||
fi
|
||||
|
||||
local pad=$(( (TW - 30) / 2 ))
|
||||
[ $pad -lt 0 ] && pad=0
|
||||
printf "%*s" "$pad" ""
|
||||
read -p "setup btcpay server? [y/N]: " SETUP
|
||||
if [[ ! "$SETUP" =~ ^[Yy]$ ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
cc "${D}pulling btcpay server image...${N}"
|
||||
podman pull "${BTCPAY_IMAGE}"
|
||||
mkdir -p ~/.btcpay
|
||||
|
||||
echo ""
|
||||
cc "${D}full setup: https://docs.btcpayserver.org${N}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
view_logs() {
|
||||
show_banner
|
||||
cc "${W}view logs${N}"
|
||||
echo ""
|
||||
cc "${O}1${N} ${D}bitcoin core${N}"
|
||||
cc "${O}2${N} ${D}lnd${N}"
|
||||
cc "${O}3${N} ${D}system journal${N}"
|
||||
cc "${D}b back${N}"
|
||||
echo ""
|
||||
|
||||
local pad=$(( (TW - 10) / 2 ))
|
||||
[ $pad -lt 0 ] && pad=0
|
||||
printf "%*s" "$pad" ""
|
||||
read -p "select: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
if podman ps -a | grep -q bitcoind; then
|
||||
podman logs -f --tail 50 bitcoind
|
||||
else
|
||||
cc "${D}bitcoin core not running${N}"
|
||||
read -sp " press enter..."
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
if podman ps -a | grep -q lnd; then
|
||||
podman logs -f --tail 50 lnd
|
||||
else
|
||||
cc "${D}lnd not running${N}"
|
||||
read -sp " press enter..."
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
journalctl -f
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
network_settings() {
|
||||
show_banner
|
||||
cc "${W}network settings${N}"
|
||||
echo ""
|
||||
|
||||
IP=$(hostname -I | awk '{print $1}')
|
||||
cc "${C}ip${N} ${W}$IP${N}"
|
||||
echo ""
|
||||
|
||||
cc "${D}interfaces:${N}"
|
||||
ip -br addr | grep -v "^lo" | while read line; do
|
||||
cc " ${D}$line${N}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
cc "${D}service ports:${N}"
|
||||
cc " ${D}8332 bitcoin rpc 9735 lightning p2p${N}"
|
||||
cc " ${D}8333 bitcoin p2p 10009 lightning grpc${N}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
system_info() {
|
||||
show_banner
|
||||
cc "${W}system information${N}"
|
||||
echo ""
|
||||
|
||||
cc "${C}host${N} ${D}$(hostname)${N}"
|
||||
cc "${C}kernel${N} ${D}$(uname -r)${N}"
|
||||
cc "${C}uptime${N} ${D}$(uptime -p 2>/dev/null || echo 'unknown')${N}"
|
||||
echo ""
|
||||
|
||||
local cpu=$(grep "model name" /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs)
|
||||
[ -n "$cpu" ] && cc "${C}cpu${N} ${D}${cpu}${N}"
|
||||
|
||||
local mem_total=$(free -h 2>/dev/null | grep Mem | awk '{print $2}')
|
||||
local mem_used=$(free -h 2>/dev/null | grep Mem | awk '{print $3}')
|
||||
[ -n "$mem_total" ] && cc "${C}memory${N} ${D}${mem_used} / ${mem_total}${N}"
|
||||
echo ""
|
||||
|
||||
cc "${D}disk:${N}"
|
||||
df -h / | tail -1 | awk '{printf " root: %s / %s (%s used)\n", $3, $2, $5}' | while read line; do
|
||||
cc "${D}${line}${N}"
|
||||
done
|
||||
|
||||
if [ -d ~/.bitcoin ]; then
|
||||
local btc_size=$(du -sh ~/.bitcoin 2>/dev/null | cut -f1)
|
||||
cc " ${D}bitcoin: $btc_size${N}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
cc "${D}containers:${N}"
|
||||
podman ps --format " {{.Names}}: {{.Status}}" 2>/dev/null | while read line; do
|
||||
cc "${D}${line}${N}"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
main_menu
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Configure Nginx to listen on Tailscale IP address
|
||||
# This script should be run after Tailscale is set up and connected
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔍 Detecting Tailscale IP..."
|
||||
|
||||
# Get Tailscale IP from tailscale0 interface
|
||||
TAILSCALE_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "")
|
||||
|
||||
if [ -z "$TAILSCALE_IP" ]; then
|
||||
echo "❌ Tailscale interface not found. Is Tailscale running with host networking?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found Tailscale IP: $TAILSCALE_IP"
|
||||
|
||||
NGINX_CONFIG="/etc/nginx/sites-available/archipelago"
|
||||
|
||||
# Check if Tailscale IP is already in the config
|
||||
if grep -q "listen $TAILSCALE_IP:80" "$NGINX_CONFIG"; then
|
||||
echo "✅ Nginx already configured for Tailscale IP $TAILSCALE_IP"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Adding Tailscale IP to Nginx configuration..."
|
||||
|
||||
# Backup the config
|
||||
sudo cp "$NGINX_CONFIG" "$NGINX_CONFIG.backup.$(date +%s)"
|
||||
|
||||
# Add Tailscale IP to listen directive (after the first "listen 80;")
|
||||
sudo sed -i "0,/listen 80;/s//listen 80;\n listen $TAILSCALE_IP:80;/" "$NGINX_CONFIG"
|
||||
|
||||
echo "🔍 Testing Nginx configuration..."
|
||||
sudo nginx -t
|
||||
|
||||
echo "🔄 Reloading Nginx..."
|
||||
sudo systemctl reload nginx
|
||||
|
||||
echo "✅ Nginx configured to accept connections from Tailscale!"
|
||||
echo " Access your Archipelago UI via Tailscale at:"
|
||||
echo " http://$(hostname).tail<your-tailnet>.ts.net/"
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Disk Installer
|
||||
# Installs Archipelago Bitcoin Node OS to a disk
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🏝️ ARCHIPELAGO DISK INSTALLER ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "❌ Please run as root: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install required tools if missing
|
||||
echo "📦 Checking required tools..."
|
||||
NEED_INSTALL=""
|
||||
for tool in parted debootstrap; do
|
||||
if ! command -v $tool >/dev/null 2>&1; then
|
||||
NEED_INSTALL="$NEED_INSTALL $tool"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$NEED_INSTALL" ]; then
|
||||
echo "📥 Installing required tools:$NEED_INSTALL"
|
||||
apt-get update
|
||||
apt-get install -y $NEED_INSTALL dosfstools e2fsprogs
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# List available disks
|
||||
echo "📋 Available disks:"
|
||||
echo ""
|
||||
lsblk -d -o NAME,SIZE,MODEL | grep -v loop | grep -v sr
|
||||
echo ""
|
||||
|
||||
# Get target disk
|
||||
read -p "Enter target disk (e.g., sda, nvme0n1): " TARGET_DISK
|
||||
TARGET_DEVICE="/dev/$TARGET_DISK"
|
||||
|
||||
if [ ! -b "$TARGET_DEVICE" ]; then
|
||||
echo "❌ Device $TARGET_DEVICE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Confirm
|
||||
echo ""
|
||||
echo "⚠️ WARNING: This will ERASE ALL DATA on $TARGET_DEVICE"
|
||||
echo ""
|
||||
read -p "Type 'yes' to continue: " CONFIRM
|
||||
|
||||
if [ "$CONFIRM" != "yes" ]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔧 Partitioning $TARGET_DEVICE..."
|
||||
|
||||
# Unmount any existing partitions
|
||||
umount ${TARGET_DEVICE}* 2>/dev/null || true
|
||||
|
||||
# Create GPT partition table
|
||||
parted -s "$TARGET_DEVICE" mklabel gpt
|
||||
|
||||
# Create partitions:
|
||||
# 1. EFI System Partition (512MB)
|
||||
# 2. Root partition (remaining space)
|
||||
parted -s "$TARGET_DEVICE" mkpart primary fat32 1MiB 513MiB
|
||||
parted -s "$TARGET_DEVICE" set 1 esp on
|
||||
parted -s "$TARGET_DEVICE" mkpart primary ext4 513MiB 100%
|
||||
|
||||
# Wait for partitions to appear
|
||||
sleep 2
|
||||
|
||||
# Determine partition names (handle both /dev/sdX and /dev/nvmeXnYpZ naming)
|
||||
if [[ "$TARGET_DISK" == nvme* ]]; then
|
||||
EFI_PART="${TARGET_DEVICE}p1"
|
||||
ROOT_PART="${TARGET_DEVICE}p2"
|
||||
else
|
||||
EFI_PART="${TARGET_DEVICE}1"
|
||||
ROOT_PART="${TARGET_DEVICE}2"
|
||||
fi
|
||||
|
||||
echo "🗂️ Formatting partitions..."
|
||||
|
||||
# Format EFI partition
|
||||
mkfs.vfat -F32 -n ARCHIPELAGO "$EFI_PART"
|
||||
|
||||
# Format root partition
|
||||
mkfs.ext4 -L archipelago-root "$ROOT_PART"
|
||||
|
||||
echo "📦 Mounting partitions..."
|
||||
|
||||
# Create mount points
|
||||
mkdir -p /mnt/archipelago
|
||||
mount "$ROOT_PART" /mnt/archipelago
|
||||
mkdir -p /mnt/archipelago/boot/efi
|
||||
mount "$EFI_PART" /mnt/archipelago/boot/efi
|
||||
|
||||
echo "📋 Installing base system..."
|
||||
|
||||
# Install base system using debootstrap
|
||||
if command -v debootstrap >/dev/null 2>&1; then
|
||||
debootstrap --arch=amd64 trixie /mnt/archipelago http://deb.debian.org/debian
|
||||
else
|
||||
echo "❌ debootstrap not found. Installing..."
|
||||
apt-get update && apt-get install -y debootstrap
|
||||
debootstrap --arch=amd64 trixie /mnt/archipelago http://deb.debian.org/debian
|
||||
fi
|
||||
|
||||
echo "⚙️ Configuring system..."
|
||||
|
||||
# Mount virtual filesystems for chroot
|
||||
mount --bind /dev /mnt/archipelago/dev
|
||||
mount --bind /dev/pts /mnt/archipelago/dev/pts
|
||||
mount --bind /proc /mnt/archipelago/proc
|
||||
mount --bind /sys /mnt/archipelago/sys
|
||||
mount --bind /sys/firmware/efi/efivars /mnt/archipelago/sys/firmware/efi/efivars 2>/dev/null || true
|
||||
mount --bind /run /mnt/archipelago/run
|
||||
|
||||
# Create fstab
|
||||
cat > /mnt/archipelago/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
|
||||
|
||||
# Set hostname
|
||||
echo "archipelago" > /mnt/archipelago/etc/hostname
|
||||
|
||||
# Configure hosts
|
||||
cat > /mnt/archipelago/etc/hosts <<EOF
|
||||
127.0.0.1 localhost
|
||||
127.0.1.1 archipelago
|
||||
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
EOF
|
||||
chmod 644 /mnt/archipelago/etc/hosts
|
||||
|
||||
# Install bootloader and essential packages in chroot
|
||||
echo "📦 Configuring package sources..."
|
||||
|
||||
# Create sources.list
|
||||
cat > /mnt/archipelago/etc/apt/sources.list <<EOF
|
||||
deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware
|
||||
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
|
||||
EOF
|
||||
|
||||
echo "📥 Updating package lists..."
|
||||
chroot /mnt/archipelago apt-get update
|
||||
|
||||
echo "🔒 Applying Debian security updates..."
|
||||
chroot /mnt/archipelago apt-get -y full-upgrade
|
||||
|
||||
echo "📦 Installing kernel and bootloader..."
|
||||
chroot /mnt/archipelago apt-get install -y linux-image-amd64 grub-efi-amd64 grub-efi-amd64-signed shim-signed
|
||||
|
||||
echo "📦 Installing essential packages..."
|
||||
chroot /mnt/archipelago apt-get install -y \
|
||||
sudo \
|
||||
network-manager \
|
||||
wpasupplicant \
|
||||
wireless-regdb \
|
||||
iw \
|
||||
rfkill \
|
||||
pciutils \
|
||||
usbutils \
|
||||
polkitd \
|
||||
openssh-server \
|
||||
curl \
|
||||
wget \
|
||||
htop \
|
||||
vim-tiny \
|
||||
nano \
|
||||
ca-certificates \
|
||||
chrony
|
||||
|
||||
echo "📦 Installing container tools..."
|
||||
chroot /mnt/archipelago apt-get install -y podman catatonit || echo "⚠️ Podman/catatonit not available in base repos, will use containers.io later"
|
||||
|
||||
echo "🔧 Installing GRUB bootloader..."
|
||||
# Need to run grub-install inside chroot with proper environment
|
||||
chroot /mnt/archipelago /bin/bash -c "
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=archipelago --recheck 2>&1 || \
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=archipelago --removable 2>&1 || \
|
||||
echo '⚠️ GRUB install had issues, trying alternative...'
|
||||
"
|
||||
chroot /mnt/archipelago update-grub
|
||||
|
||||
echo "👤 Creating archipelago user..."
|
||||
# Create user first, then add to groups that exist
|
||||
chroot /mnt/archipelago useradd -m -s /bin/bash archipelago || echo "User may already exist"
|
||||
chroot /mnt/archipelago usermod -aG sudo archipelago || true
|
||||
chroot /mnt/archipelago usermod -aG podman archipelago 2>/dev/null || true
|
||||
|
||||
# Set password using chpasswd
|
||||
echo "archipelago:archipelago" | chroot /mnt/archipelago chpasswd
|
||||
|
||||
echo "⚙️ Enabling services..."
|
||||
chroot /mnt/archipelago systemctl enable NetworkManager || true
|
||||
chroot /mnt/archipelago systemctl enable polkit || chroot /mnt/archipelago systemctl enable polkit.service || true
|
||||
chroot /mnt/archipelago systemctl enable ssh || chroot /mnt/archipelago systemctl enable sshd || true
|
||||
chroot /mnt/archipelago systemctl enable chrony || true
|
||||
|
||||
mkdir -p /mnt/archipelago/etc/polkit-1/rules.d
|
||||
cat > /mnt/archipelago/etc/polkit-1/rules.d/49-archipelago-networkmanager.rules <<'EOF'
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (subject.user == "archipelago" && action.id.indexOf("org.freedesktop.NetworkManager.") == 0) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
EOF
|
||||
chmod 644 /mnt/archipelago/etc/polkit-1/rules.d/49-archipelago-networkmanager.rules
|
||||
|
||||
# Remove policy-rc.d so services can start on first boot
|
||||
rm -f /mnt/archipelago/usr/sbin/policy-rc.d
|
||||
|
||||
echo "💾 Creating swap file..."
|
||||
TOTAL_MEM_KB=$(chroot /mnt/archipelago grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}')
|
||||
SWAP_GB=${TOTAL_MEM_KB:+$((TOTAL_MEM_KB / 1024 / 1024))}
|
||||
SWAP_GB=${SWAP_GB:-4}
|
||||
[ "$SWAP_GB" -gt 8 ] && SWAP_GB=8
|
||||
[ "$SWAP_GB" -lt 2 ] && SWAP_GB=2
|
||||
fallocate -l ${SWAP_GB}G /mnt/archipelago/swapfile 2>/dev/null || dd if=/dev/zero of=/mnt/archipelago/swapfile bs=1G count=$SWAP_GB status=progress
|
||||
chmod 600 /mnt/archipelago/swapfile
|
||||
chroot /mnt/archipelago mkswap /swapfile
|
||||
echo '/swapfile none swap sw 0 0' >> /mnt/archipelago/etc/fstab
|
||||
echo "✅ Created ${SWAP_GB}G swap"
|
||||
|
||||
echo "📁 Creating Archipelago directories..."
|
||||
chroot /mnt/archipelago mkdir -p /var/lib/archipelago/{data,config,containers}
|
||||
chroot /mnt/archipelago mkdir -p /etc/archipelago
|
||||
chroot /mnt/archipelago chown -R archipelago:archipelago /var/lib/archipelago
|
||||
|
||||
echo "✅ Base system configured"
|
||||
|
||||
# Copy Archipelago files
|
||||
echo "📋 Installing Archipelago components..."
|
||||
|
||||
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 [ -n "$BOOT_MEDIA" ]; then
|
||||
echo " Copying from: $BOOT_MEDIA/archipelago"
|
||||
|
||||
# Copy entire archipelago directory
|
||||
mkdir -p /mnt/archipelago/opt/archipelago
|
||||
cp -r "$BOOT_MEDIA/archipelago/"* /mnt/archipelago/opt/archipelago/ 2>/dev/null || true
|
||||
|
||||
# Install binaries
|
||||
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
|
||||
cp "$BOOT_MEDIA/archipelago/bin/"* /mnt/archipelago/usr/local/bin/ 2>/dev/null || true
|
||||
chmod +x /mnt/archipelago/usr/local/bin/* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install scripts
|
||||
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
|
||||
mkdir -p /mnt/archipelago/opt/archipelago/scripts
|
||||
cp "$BOOT_MEDIA/archipelago/scripts/"* /mnt/archipelago/opt/archipelago/scripts/ 2>/dev/null || true
|
||||
chmod +x /mnt/archipelago/opt/archipelago/scripts/*.sh 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install app manifests
|
||||
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
|
||||
mkdir -p /mnt/archipelago/etc/archipelago
|
||||
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/archipelago/etc/archipelago/
|
||||
fi
|
||||
|
||||
# Mesh radio udev rule: stable /dev/mesh-radio symlink + dialout perms
|
||||
# for known LoRa USB-serial chips (this installer never shipped it)
|
||||
for p in "$BOOT_MEDIA/99-mesh-radio.rules" "$BOOT_MEDIA/archipelago/configs/99-mesh-radio.rules"; do
|
||||
if [ -f "$p" ]; then
|
||||
mkdir -p /mnt/archipelago/etc/udev/rules.d
|
||||
cp "$p" /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules
|
||||
chmod 644 /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Create profile.d script for welcome message on login
|
||||
mkdir -p /mnt/archipelago/etc/profile.d
|
||||
cat > /mnt/archipelago/etc/profile.d/archipelago.sh <<'PROFILE_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago welcome message
|
||||
if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
|
||||
export ARCHIPELAGO_WELCOMED=1
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 🌐 Web UI: http://$IP:5678 │"
|
||||
echo " │ 📡 SSH: ssh archipelago@$IP │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
fi
|
||||
echo " Commands:"
|
||||
echo " archipelago - Start backend server"
|
||||
echo " archipelago-menu - Open setup menu"
|
||||
echo ""
|
||||
fi
|
||||
PROFILE_EOF
|
||||
chmod +x /mnt/archipelago/etc/profile.d/archipelago.sh
|
||||
|
||||
# Create systemd service to auto-start archipelago backend
|
||||
mkdir -p /mnt/archipelago/etc/systemd/system
|
||||
cat > /mnt/archipelago/etc/systemd/system/archipelago.service <<'SERVICE_EOF'
|
||||
[Unit]
|
||||
Description=Archipelago Backend Server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
# Never start before the data volume (if one exists) is mounted — without this
|
||||
# the service races the mount on cold boot and "[FAILED]"-loops until it lands (B17)
|
||||
RequiresMountsFor=/var/lib/archipelago
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=archipelago
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE_EOF
|
||||
|
||||
# Enable the service
|
||||
chroot /mnt/archipelago systemctl enable archipelago.service 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "🧹 Cleaning up..."
|
||||
|
||||
# Unmount in reverse order
|
||||
sync
|
||||
umount /mnt/archipelago/run 2>/dev/null || true
|
||||
umount /mnt/archipelago/sys/firmware/efi/efivars 2>/dev/null || true
|
||||
umount /mnt/archipelago/sys 2>/dev/null || true
|
||||
umount /mnt/archipelago/proc 2>/dev/null || true
|
||||
umount /mnt/archipelago/dev/pts 2>/dev/null || true
|
||||
umount /mnt/archipelago/dev 2>/dev/null || true
|
||||
umount /mnt/archipelago/boot/efi 2>/dev/null || true
|
||||
umount /mnt/archipelago 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ INSTALLATION COMPLETE! ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Remove the USB drive and reboot to start Archipelago."
|
||||
echo ""
|
||||
echo "Default login:"
|
||||
echo " Username: archipelago"
|
||||
echo " Password: archipelago"
|
||||
echo ""
|
||||
echo "⚠️ Please change the password after first login!"
|
||||
echo ""
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Bitcoin Core Setup Script for Archipelago
|
||||
# Sets up Bitcoin Core in a Podman container
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ₿ BITCOIN CORE SETUP ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "⚠️ Running as root. For rootless Podman, run as regular user."
|
||||
fi
|
||||
|
||||
# Check for Podman
|
||||
if ! command -v podman >/dev/null 2>&1; then
|
||||
echo "📦 Installing Podman..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y podman podman-compose
|
||||
fi
|
||||
|
||||
# Create data directory
|
||||
BITCOIN_DATA="${HOME}/.bitcoin"
|
||||
mkdir -p "$BITCOIN_DATA"
|
||||
|
||||
echo "📍 Bitcoin data directory: $BITCOIN_DATA"
|
||||
echo ""
|
||||
|
||||
# Ask for configuration
|
||||
echo "Bitcoin Core Configuration:"
|
||||
echo ""
|
||||
read -p "Enable pruning? (saves disk space) [y/N]: " PRUNE
|
||||
read -p "Enable txindex? (required for some apps) [y/N]: " TXINDEX
|
||||
read -p "RPC username [bitcoin]: " RPC_USER
|
||||
RPC_USER=${RPC_USER:-bitcoin}
|
||||
read -p "RPC password [randomly generated]: " RPC_PASS
|
||||
|
||||
if [ -z "$RPC_PASS" ]; then
|
||||
RPC_PASS=$(openssl rand -hex 16)
|
||||
echo " Generated RPC password: $RPC_PASS"
|
||||
fi
|
||||
|
||||
# Create bitcoin.conf
|
||||
cat > "$BITCOIN_DATA/bitcoin.conf" <<EOF
|
||||
# Archipelago Bitcoin Core Configuration
|
||||
|
||||
# Network
|
||||
server=1
|
||||
listen=1
|
||||
|
||||
# RPC
|
||||
rpcuser=$RPC_USER
|
||||
rpcpassword=$RPC_PASS
|
||||
rpcallowip=10.0.0.0/8
|
||||
rpcallowip=172.16.0.0/12
|
||||
rpcallowip=192.168.0.0/16
|
||||
|
||||
# Performance
|
||||
dbcache=450
|
||||
maxmempool=300
|
||||
|
||||
EOF
|
||||
|
||||
if [[ "$PRUNE" =~ ^[Yy]$ ]]; then
|
||||
echo "prune=550" >> "$BITCOIN_DATA/bitcoin.conf"
|
||||
echo " Pruning enabled (550MB)"
|
||||
fi
|
||||
|
||||
if [[ "$TXINDEX" =~ ^[Yy]$ ]]; then
|
||||
echo "txindex=1" >> "$BITCOIN_DATA/bitcoin.conf"
|
||||
echo " Transaction index enabled"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📋 Created bitcoin.conf"
|
||||
echo ""
|
||||
|
||||
# Pull Bitcoin Core image
|
||||
echo "🐳 Pulling Bitcoin Core container image..."
|
||||
podman pull docker.io/lncm/bitcoind:v27.0
|
||||
|
||||
# Create systemd user service for Bitcoin Core
|
||||
mkdir -p ~/.config/systemd/user
|
||||
|
||||
cat > ~/.config/systemd/user/bitcoind.service <<EOF
|
||||
[Unit]
|
||||
Description=Bitcoin Core (Podman)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStartPre=-/usr/bin/podman stop bitcoind
|
||||
ExecStartPre=-/usr/bin/podman rm bitcoind
|
||||
ExecStart=/usr/bin/podman run --name bitcoind \\
|
||||
--rm \\
|
||||
-v ${BITCOIN_DATA}:/data/.bitcoin:Z \\
|
||||
-p 8332:8332 \\
|
||||
-p 8333:8333 \\
|
||||
docker.io/lncm/bitcoind:v27.0
|
||||
ExecStop=/usr/bin/podman stop bitcoind
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
echo "📋 Created systemd service"
|
||||
echo ""
|
||||
|
||||
# Enable and start service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable bitcoind.service
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ BITCOIN CORE SETUP COMPLETE! ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Start: systemctl --user start bitcoind"
|
||||
echo " Stop: systemctl --user stop bitcoind"
|
||||
echo " Status: systemctl --user status bitcoind"
|
||||
echo " Logs: podman logs -f bitcoind"
|
||||
echo ""
|
||||
echo "Bitcoin CLI:"
|
||||
echo " podman exec bitcoind bitcoin-cli -getinfo"
|
||||
echo ""
|
||||
echo "RPC Credentials (save these!):"
|
||||
echo " User: $RPC_USER"
|
||||
echo " Pass: $RPC_PASS"
|
||||
echo ""
|
||||
|
||||
read -p "Start Bitcoin Core now? [Y/n]: " START_NOW
|
||||
if [[ ! "$START_NOW" =~ ^[Nn]$ ]]; then
|
||||
echo ""
|
||||
echo "🚀 Starting Bitcoin Core..."
|
||||
systemctl --user start bitcoind
|
||||
echo ""
|
||||
echo "Bitcoin Core is syncing. This will take several hours/days."
|
||||
echo "Monitor with: podman logs -f bitcoind"
|
||||
fi
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# LND (Lightning Network Daemon) Setup Script for Archipelago
|
||||
# Sets up LND in a Podman container
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚡ LND (LIGHTNING) SETUP ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check for Podman
|
||||
if ! command -v podman >/dev/null 2>&1; then
|
||||
echo "❌ Podman not found. Please run setup-bitcoin.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Bitcoin Core is running
|
||||
if ! podman ps | grep -q bitcoind; then
|
||||
echo "⚠️ Bitcoin Core is not running."
|
||||
echo " LND requires a synced Bitcoin Core node."
|
||||
read -p "Continue anyway? [y/N]: " CONTINUE
|
||||
if [[ ! "$CONTINUE" =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create data directory
|
||||
LND_DATA="${HOME}/.lnd"
|
||||
mkdir -p "$LND_DATA"
|
||||
|
||||
echo "📍 LND data directory: $LND_DATA"
|
||||
echo ""
|
||||
|
||||
# Get Bitcoin RPC credentials
|
||||
BITCOIN_CONF="${HOME}/.bitcoin/bitcoin.conf"
|
||||
if [ -f "$BITCOIN_CONF" ]; then
|
||||
RPC_USER=$(grep "^rpcuser=" "$BITCOIN_CONF" | cut -d= -f2)
|
||||
RPC_PASS=$(grep "^rpcpassword=" "$BITCOIN_CONF" | cut -d= -f2)
|
||||
else
|
||||
read -p "Bitcoin RPC username: " RPC_USER
|
||||
read -p "Bitcoin RPC password: " RPC_PASS
|
||||
fi
|
||||
|
||||
# Ask for LND configuration
|
||||
echo "LND Configuration:"
|
||||
echo ""
|
||||
read -p "Node alias (public name): " LND_ALIAS
|
||||
LND_ALIAS=${LND_ALIAS:-archipelago-node}
|
||||
|
||||
# Create lnd.conf
|
||||
cat > "$LND_DATA/lnd.conf" <<EOF
|
||||
# Archipelago LND Configuration
|
||||
|
||||
[Application Options]
|
||||
alias=$LND_ALIAS
|
||||
color=#FF9900
|
||||
listen=0.0.0.0:9735
|
||||
rpclisten=0.0.0.0:10009
|
||||
restlisten=0.0.0.0:8080
|
||||
|
||||
# Automatically unlock wallet (create password file after first run)
|
||||
# wallet-unlock-password-file=/data/.lnd/password.txt
|
||||
|
||||
[Bitcoin]
|
||||
bitcoin.active=true
|
||||
bitcoin.mainnet=true
|
||||
bitcoin.node=bitcoind
|
||||
|
||||
[Bitcoind]
|
||||
bitcoind.rpchost=host.containers.internal:8332
|
||||
bitcoind.rpcuser=$RPC_USER
|
||||
bitcoind.rpcpass=$RPC_PASS
|
||||
bitcoind.zmqpubrawblock=tcp://host.containers.internal:28332
|
||||
bitcoind.zmqpubrawtx=tcp://host.containers.internal:28333
|
||||
|
||||
[tor]
|
||||
tor.active=false
|
||||
|
||||
[wtclient]
|
||||
wtclient.active=true
|
||||
EOF
|
||||
|
||||
echo "📋 Created lnd.conf"
|
||||
echo ""
|
||||
|
||||
# Pull LND image
|
||||
echo "🐳 Pulling LND container image..."
|
||||
podman pull docker.io/lightninglabs/lnd:v0.18.0-beta
|
||||
|
||||
# Create systemd user service for LND
|
||||
mkdir -p ~/.config/systemd/user
|
||||
|
||||
cat > ~/.config/systemd/user/lnd.service <<EOF
|
||||
[Unit]
|
||||
Description=LND Lightning Network Daemon (Podman)
|
||||
After=bitcoind.service
|
||||
Requires=bitcoind.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStartPre=-/usr/bin/podman stop lnd
|
||||
ExecStartPre=-/usr/bin/podman rm lnd
|
||||
ExecStart=/usr/bin/podman run --name lnd \\
|
||||
--rm \\
|
||||
--add-host=host.containers.internal:host-gateway \\
|
||||
-v ${LND_DATA}:/data/.lnd:Z \\
|
||||
-p 9735:9735 \\
|
||||
-p 10009:10009 \\
|
||||
-p 18080:8080 \\
|
||||
docker.io/lightninglabs/lnd:v0.18.0-beta \\
|
||||
--configfile=/data/.lnd/lnd.conf
|
||||
ExecStop=/usr/bin/podman stop lnd
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
echo "📋 Created systemd service"
|
||||
echo ""
|
||||
|
||||
# Enable service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable lnd.service
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ LND SETUP COMPLETE! ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Start: systemctl --user start lnd"
|
||||
echo " Stop: systemctl --user stop lnd"
|
||||
echo " Status: systemctl --user status lnd"
|
||||
echo " Logs: podman logs -f lnd"
|
||||
echo ""
|
||||
echo "LND CLI:"
|
||||
echo " podman exec lnd lncli --network=mainnet getinfo"
|
||||
echo ""
|
||||
echo "⚠️ IMPORTANT: On first start, you need to create a wallet:"
|
||||
echo " 1. Start LND: systemctl --user start lnd"
|
||||
echo " 2. Create wallet: podman exec -it lnd lncli create"
|
||||
echo " 3. Save your seed phrase securely!"
|
||||
echo ""
|
||||
|
||||
read -p "Start LND now? [Y/n]: " START_NOW
|
||||
if [[ ! "$START_NOW" =~ ^[Nn]$ ]]; then
|
||||
echo ""
|
||||
echo "🚀 Starting LND..."
|
||||
systemctl --user start lnd
|
||||
sleep 3
|
||||
echo ""
|
||||
echo "LND is starting. Create your wallet with:"
|
||||
echo " podman exec -it lnd lncli create"
|
||||
fi
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple API server for Archipelago Web UI
|
||||
Serves static files and handles basic API endpoints
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
PORT = 80
|
||||
WEB_DIR = None
|
||||
|
||||
# Find web UI directory
|
||||
for path in ['/opt/archipelago/web-ui', '/run/live/medium/archipelago/web-ui', '/lib/live/mount/medium/archipelago/web-ui']:
|
||||
if os.path.isdir(path):
|
||||
WEB_DIR = path
|
||||
break
|
||||
|
||||
if not WEB_DIR:
|
||||
print("Web UI directory not found!")
|
||||
exit(1)
|
||||
|
||||
os.chdir(WEB_DIR)
|
||||
|
||||
class ArchipelagoHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
"""Handle POST requests with mock responses"""
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length) if content_length > 0 else b''
|
||||
|
||||
path = urlparse(self.path).path
|
||||
|
||||
# Mock API responses
|
||||
response = {"success": True}
|
||||
|
||||
if '/api/auth' in path or '/auth' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"token": "mock-token-12345",
|
||||
"user": "archipelago"
|
||||
}
|
||||
elif '/api/setup' in path or '/setup' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"status": "complete"
|
||||
}
|
||||
elif '/api/status' in path or '/status' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"status": "running",
|
||||
"version": "0.1.0",
|
||||
"hostname": "archipelago"
|
||||
}
|
||||
elif '/api/apps' in path or '/apps' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"apps": []
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""Handle CORS preflight"""
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle GET requests - serve files or API"""
|
||||
path = urlparse(self.path).path
|
||||
|
||||
if path.startswith('/api/'):
|
||||
# Mock API GET endpoints
|
||||
response = {"success": True}
|
||||
|
||||
if '/status' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"status": "running",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
elif '/apps' in path:
|
||||
response = {
|
||||
"success": True,
|
||||
"apps": [
|
||||
{"id": "bitcoin", "name": "Bitcoin Core", "status": "available"},
|
||||
{"id": "lnd", "name": "Lightning (LND)", "status": "available"}
|
||||
]
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(response).encode())
|
||||
else:
|
||||
# Serve static files, default to index.html for SPA routing
|
||||
if not os.path.exists(self.translate_path(self.path)) or path == '/':
|
||||
self.path = '/index.html'
|
||||
super().do_GET()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Quieter logging"""
|
||||
if '404' in str(args) or 'error' in str(args).lower():
|
||||
print(f" {args[0]}")
|
||||
|
||||
print(f"""
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ 🏝️ ARCHIPELAGO WEB UI ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
|
||||
Serving from: {WEB_DIR}
|
||||
|
||||
🌐 Open in your browser: http://localhost:{PORT}
|
||||
|
||||
Press Ctrl+C to stop
|
||||
""")
|
||||
|
||||
with socketserver.TCPServer(("0.0.0.0", PORT), ArchipelagoHandler) as httpd:
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Start Archipelago Backend Server
|
||||
# Serves the Vue.js UI and handles all API calls
|
||||
#
|
||||
|
||||
# Find archipelago binary
|
||||
ARCHIPELAGO_BIN=""
|
||||
for path in /usr/local/bin/archipelago /opt/archipelago/bin/archipelago /run/live/medium/archipelago/bin/archipelago; do
|
||||
if [ -x "$path" ]; then
|
||||
ARCHIPELAGO_BIN="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Get IP address
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
if [ -z "$IP" ]; then
|
||||
IP="localhost"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Starting Archipelago server..."
|
||||
echo ""
|
||||
echo " ┌─────────────────────────────────────────────────────────┐"
|
||||
echo " │ │"
|
||||
echo " │ 🌐 OPEN IN YOUR BROWSER: │"
|
||||
echo " │ │"
|
||||
echo " │ http://$IP "
|
||||
echo " │ │"
|
||||
echo " └─────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
|
||||
if [ -n "$ARCHIPELAGO_BIN" ]; then
|
||||
echo " Using backend: $ARCHIPELAGO_BIN"
|
||||
echo " Press Ctrl+C to stop"
|
||||
echo ""
|
||||
|
||||
# Set environment for dev mode
|
||||
export ARCHIPELAGO_DEV_MODE=true
|
||||
export ARCHIPELAGO_DATA_DIR=/var/lib/archipelago
|
||||
export RUST_LOG=info
|
||||
|
||||
# Create data directory
|
||||
sudo mkdir -p /var/lib/archipelago 2>/dev/null
|
||||
|
||||
exec "$ARCHIPELAGO_BIN"
|
||||
else
|
||||
echo " ⚠️ Backend binary not found, using static file server"
|
||||
echo ""
|
||||
|
||||
# Fallback to static file server
|
||||
WEB_UI_DIR=""
|
||||
for path in /opt/archipelago/web-ui /run/live/medium/archipelago/web-ui; do
|
||||
if [ -d "$path" ]; then
|
||||
WEB_UI_DIR="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$WEB_UI_DIR" ]; then
|
||||
cd "$WEB_UI_DIR"
|
||||
exec python3 -m http.server 80 --bind 0.0.0.0
|
||||
else
|
||||
echo "❌ Neither backend nor web UI found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 837 KiB |
@@ -0,0 +1,52 @@
|
||||
# Archipelago GRUB Theme
|
||||
# Dark background with Bitcoin orange accents
|
||||
|
||||
title-text: ""
|
||||
desktop-color: "#0a0a0a"
|
||||
desktop-image: "background.png"
|
||||
desktop-image-scale-method: "stretch"
|
||||
|
||||
+ boot_menu {
|
||||
left = 10%
|
||||
top = 35%
|
||||
width = 80%
|
||||
height = 40%
|
||||
item_font = "DejaVu Sans Bold 16"
|
||||
item_color = "#aaaaaa"
|
||||
selected_item_font = "DejaVu Sans Bold 16"
|
||||
selected_item_color = "#f7931a"
|
||||
item_height = 36
|
||||
item_spacing = 8
|
||||
item_padding = 16
|
||||
scrollbar = false
|
||||
}
|
||||
|
||||
+ label {
|
||||
left = 10%
|
||||
top = 18%
|
||||
width = 80%
|
||||
font = "DejaVu Sans Mono Bold 24"
|
||||
text = "a r c h i p e l a g o"
|
||||
color = "#f7931a"
|
||||
align = "center"
|
||||
}
|
||||
|
||||
+ label {
|
||||
left = 10%
|
||||
top = 26%
|
||||
width = 80%
|
||||
font = "DejaVu Sans Bold 14"
|
||||
text = "bitcoin node os"
|
||||
color = "#888888"
|
||||
align = "center"
|
||||
}
|
||||
|
||||
+ label {
|
||||
left = 10%
|
||||
top = 90%
|
||||
width = 80%
|
||||
font = "DejaVu Sans Bold 12"
|
||||
text = "press tab to edit | use arrow keys to select | enter to boot"
|
||||
color = "#555555"
|
||||
align = "center"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[Plymouth Theme]
|
||||
Name=Archipelago
|
||||
Description=Archipelago Bitcoin Node OS — cyberpunk boot splash
|
||||
ModuleName=script
|
||||
|
||||
[script]
|
||||
ImageDir=/usr/share/plymouth/themes/archipelago
|
||||
ScriptFile=/usr/share/plymouth/themes/archipelago/archipelago.script
|
||||
@@ -0,0 +1,109 @@
|
||||
// Archipelago Plymouth Theme — cyberpunk boot splash
|
||||
// Dark background, neon orange pixel-art logo, animated progress bar
|
||||
|
||||
// Screen dimensions
|
||||
screen_w = Window.GetWidth();
|
||||
screen_h = Window.GetHeight();
|
||||
|
||||
// Background — solid near-black (the GRUB background handles the fancy stuff)
|
||||
Window.SetBackgroundTopColor(0.02, 0.02, 0.04);
|
||||
Window.SetBackgroundBottomColor(0.01, 0.01, 0.02);
|
||||
|
||||
// Load logo image (generated during build)
|
||||
logo_image = Image("logo.png");
|
||||
logo_sprite = Sprite(logo_image);
|
||||
logo_w = logo_image.GetWidth();
|
||||
logo_h = logo_image.GetHeight();
|
||||
logo_sprite.SetX(screen_w / 2 - logo_w / 2);
|
||||
logo_sprite.SetY(screen_h / 2 - logo_h / 2 - 60);
|
||||
logo_sprite.SetOpacity(1.0);
|
||||
|
||||
// --- Progress bar ---
|
||||
// Neon orange bar with glow, centered below logo
|
||||
bar_w = 300;
|
||||
bar_h = 4;
|
||||
bar_x = screen_w / 2 - bar_w / 2;
|
||||
bar_y = screen_h / 2 + logo_h / 2;
|
||||
|
||||
// Progress bar background (dark glass)
|
||||
bar_bg = Image(bar_w, bar_h);
|
||||
for (x = 0; x < bar_w; x++) {
|
||||
for (y = 0; y < bar_h; y++) {
|
||||
bar_bg.SetPixel(x, y, 0.1, 0.1, 0.12, 0.8);
|
||||
}
|
||||
}
|
||||
bar_bg_sprite = Sprite(bar_bg);
|
||||
bar_bg_sprite.SetX(bar_x);
|
||||
bar_bg_sprite.SetY(bar_y);
|
||||
|
||||
// Progress bar fill (neon orange)
|
||||
progress_val = 0;
|
||||
|
||||
fun refresh_callback() {
|
||||
// Animate progress smoothly
|
||||
if (Plymouth.GetMode() == "boot") {
|
||||
progress_val = progress_val + 0.002;
|
||||
if (progress_val > 1.0) progress_val = 1.0;
|
||||
}
|
||||
|
||||
fill_w = Math.Int(bar_w * progress_val);
|
||||
if (fill_w > 0) {
|
||||
bar_fill = Image(fill_w, bar_h);
|
||||
for (x = 0; x < fill_w; x++) {
|
||||
for (y = 0; y < bar_h; y++) {
|
||||
// Orange: rgb(251, 146, 60) = 0.984, 0.573, 0.235
|
||||
bar_fill.SetPixel(x, y, 0.984, 0.573, 0.235, 1.0);
|
||||
}
|
||||
}
|
||||
bar_fill_sprite = Sprite(bar_fill);
|
||||
bar_fill_sprite.SetX(bar_x);
|
||||
bar_fill_sprite.SetY(bar_y);
|
||||
bar_fill_sprite.SetZ(1);
|
||||
}
|
||||
}
|
||||
|
||||
Plymouth.SetRefreshFunction(refresh_callback);
|
||||
|
||||
// --- Boot progress callback ---
|
||||
fun boot_progress_callback(duration, progress) {
|
||||
progress_val = progress;
|
||||
}
|
||||
Plymouth.SetBootProgressFunction(boot_progress_callback);
|
||||
|
||||
// --- Status message (below progress bar) ---
|
||||
msg_sprite = Sprite();
|
||||
msg_sprite.SetPosition(screen_w / 2, bar_y + 30, 2);
|
||||
|
||||
fun message_callback(text) {
|
||||
// Plymouth passes boot messages here
|
||||
// We could render them but keeping it clean — just the logo and bar
|
||||
}
|
||||
Plymouth.SetMessageFunction(message_callback);
|
||||
|
||||
// --- Password prompt (for LUKS) ---
|
||||
fun display_password_callback(prompt, bullets) {
|
||||
// LUKS unlock prompt
|
||||
pass_image = Image.Text(prompt, 0.984, 0.573, 0.235);
|
||||
pass_sprite = Sprite(pass_image);
|
||||
pass_sprite.SetX(screen_w / 2 - pass_image.GetWidth() / 2);
|
||||
pass_sprite.SetY(screen_h / 2 + 80);
|
||||
|
||||
// Bullet dots for password
|
||||
if (bullets > 0) {
|
||||
bullet_text = "";
|
||||
for (i = 0; i < bullets; i++) {
|
||||
bullet_text = bullet_text + "* ";
|
||||
}
|
||||
bullet_image = Image.Text(bullet_text, 0.984, 0.573, 0.235);
|
||||
bullet_sprite = Sprite(bullet_image);
|
||||
bullet_sprite.SetX(screen_w / 2 - bullet_image.GetWidth() / 2);
|
||||
bullet_sprite.SetY(screen_h / 2 + 110);
|
||||
}
|
||||
}
|
||||
Plymouth.SetDisplayPasswordFunction(display_password_callback);
|
||||
|
||||
// --- Quit callback ---
|
||||
fun quit_callback() {
|
||||
logo_sprite.SetOpacity(0);
|
||||
}
|
||||
Plymouth.SetQuitFunction(quit_callback);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Archipelago Debian installer ISO.
|
||||
#
|
||||
# The historical ISO builder remains archived because OTA tarballs are the
|
||||
# normal release path. This wrapper keeps the documented ISO command working
|
||||
# by running a temporary active-layout copy of that builder with fixed paths.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ARCHIVED_BUILDER="$SCRIPT_DIR/_archived/build-auto-installer-iso.sh"
|
||||
TMP_DIR="$(mktemp -d -t archipelago-iso-builder.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ ! -f "$ARCHIVED_BUILDER" ]; then
|
||||
echo "Archived ISO builder not found: $ARCHIVED_BUILDER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_BUILDER="$TMP_DIR/build-auto-installer-iso.sh"
|
||||
cp "$ARCHIVED_BUILDER" "$TMP_BUILDER"
|
||||
|
||||
# The archived builder lived one directory deeper at image-recipe/_archived/.
|
||||
# Rewrite only path expressions that were relative to that old location.
|
||||
perl -0pi -e 's#SCRIPT_DIR="\$\(cd "\$\(dirname "\$0"\)" && pwd\)"#SCRIPT_DIR="__ARCHIPELAGO_IMAGE_RECIPE_DIR__"#g' "$TMP_BUILDER"
|
||||
# Repo-root references: _archived/../../X becomes image-recipe/../X.
|
||||
perl -0pi -e 's#\$SCRIPT_DIR/\.\./\.\./#\$SCRIPT_DIR/../#g' "$TMP_BUILDER"
|
||||
perl -0pi -e 's#\$SCRIPT_DIR/\.\./\.\."#\$SCRIPT_DIR/.."#g' "$TMP_BUILDER"
|
||||
# configs/ lives inside image-recipe/ itself: _archived/../configs becomes image-recipe/configs.
|
||||
perl -0pi -e 's#\$SCRIPT_DIR/\.\./configs#\$SCRIPT_DIR/configs#g' "$TMP_BUILDER"
|
||||
perl -0pi -e 's#"\$\(dirname "\$0"\)/\.\./\.\./scripts#"$(dirname "$0")/../scripts#g' "$TMP_BUILDER"
|
||||
|
||||
perl -0pi -e "s#__ARCHIPELAGO_IMAGE_RECIPE_DIR__#${SCRIPT_DIR}#g" "$TMP_BUILDER"
|
||||
|
||||
chmod +x "$TMP_BUILDER"
|
||||
exec bash "$TMP_BUILDER" "$@"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
# Build Archipelago Vue.js frontend for production
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "🎨 Building Archipelago Frontend (Vue.js)"
|
||||
echo ""
|
||||
|
||||
# Navigate to frontend directory
|
||||
cd "$PROJECT_ROOT/neode-ui"
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install || {
|
||||
echo "❌ npm install failed"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Build for production
|
||||
echo "🔨 Building production bundle (skipping type check for speed)..."
|
||||
npm run build:docker || {
|
||||
echo "❌ Build failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create output directory
|
||||
BUILD_DIR="$SCRIPT_DIR/build/frontend"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Copy dist to build directory (check both possible output locations)
|
||||
echo "📋 Copying build artifacts..."
|
||||
if [ -d "../web/dist/neode-ui" ]; then
|
||||
cp -r ../web/dist/neode-ui/* "$BUILD_DIR/" || {
|
||||
echo "❌ Failed to copy build artifacts"
|
||||
exit 1
|
||||
}
|
||||
elif [ -d "dist" ]; then
|
||||
cp -r dist/* "$BUILD_DIR/" || {
|
||||
echo "❌ Failed to copy build artifacts"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "❌ Build output not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Frontend built successfully!"
|
||||
echo " Output: $BUILD_DIR"
|
||||
echo " Files:"
|
||||
ls -lh "$BUILD_DIR" | head -10
|
||||
echo ""
|
||||
@@ -0,0 +1,9 @@
|
||||
# Stable symlink for USB serial adapters used as mesh radios.
|
||||
# Creates /dev/mesh-radio pointing to the underlying ttyUSB device.
|
||||
# Supports MeshCore and Meshtastic radios using CP2102 (Heltec V3),
|
||||
# CH340 (T-Beam), FTDI (RAK WisBlock), and known USB CDC ACM radios.
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="2e8a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
|
||||
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Archipelago audio router (HDMI hot-plug follow + ELD boot-race heal)
|
||||
# Talks to the archipelago user's PipeWire (running under the lingering user
|
||||
# manager) and pokes the kiosk X server for the ELD re-modeset nudge; start
|
||||
# after both are plausibly up. Missing/inactive units here are harmless.
|
||||
After=user@1000.service archipelago-kiosk.service
|
||||
Wants=user@1000.service
|
||||
ConditionPathExists=/usr/local/bin/archipelago-audio-router
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=archipelago
|
||||
ExecStart=/usr/local/bin/archipelago-audio-router
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
# Polls a few pactl calls every 5s — keep it invisible to the scheduler.
|
||||
Nice=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# Archipelago audio router — keep audio flowing out of the display the user is
|
||||
# actually looking at.
|
||||
#
|
||||
# The kiosk's Chromium plays through PipeWire-Pulse, but WirePlumber's stock
|
||||
# profile priorities rank the laptop's analog output above HDMI, so a node
|
||||
# driving a TV plays video sound out of its own tiny speakers (or nowhere).
|
||||
# ALSA jack detection (ELD) tells us when an HDMI/DP sink has a listening
|
||||
# monitor; this daemon polls that via the card's profile availability and:
|
||||
#
|
||||
# - switches the card to the best *available* HDMI stereo profile (the
|
||||
# "+input:analog-stereo" combined variant when offered, so the mic keeps
|
||||
# working), and back to analog when HDMI is unplugged;
|
||||
# - keeps the default sink pointed at the routed output and migrates any
|
||||
# live streams so playback follows a hot-plug without a page reload;
|
||||
# - unmutes the routed sink (HDMI additionally forced to 100% — the TV owns
|
||||
# the real volume control; analog volume is left where the user set it).
|
||||
#
|
||||
# Runs as the archipelago user (systemd system unit with User=archipelago),
|
||||
# talks only to the user-session PipeWire; polling is a few pactl calls every
|
||||
# 5s — negligible. Surround profiles are deliberately ignored: stereo is the
|
||||
# lowest-common-denominator every TV decodes.
|
||||
|
||||
# - re-modesets an external output once when it is connected but no ELD
|
||||
# reports a monitor: the kiosk's boot-time Xorg modeset can beat the
|
||||
# i915→HDA audio-component bind, the ELD notify is lost, and every HDMI
|
||||
# profile stays "available: no" forever (no sound, no error). One
|
||||
# off/on cycle re-delivers the ELD (verified on Framework PT / LG TV).
|
||||
|
||||
RUNTIME_DIR="/run/user/$(id -u)"
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-$RUNTIME_DIR}"
|
||||
export DISPLAY="${DISPLAY:-:0}"
|
||||
|
||||
# The user manager socket-activates pipewire, but after a live package install
|
||||
# (bootstrap self-heal) nothing has poked it yet — start best-effort.
|
||||
systemctl --user start pipewire.socket pipewire-pulse.socket 2>/dev/null || true
|
||||
systemctl --user start wireplumber.service 2>/dev/null || true
|
||||
|
||||
LAST_SINK=""
|
||||
NUDGED_OUTPUTS=""
|
||||
|
||||
# Re-deliver a lost ELD by cycling the connected external output once. Guarded:
|
||||
# only when X answers, only when NO ELD anywhere reports a monitor, and only
|
||||
# once per connector while it stays connected (a display with no audio support
|
||||
# never produces an ELD — without the flag we would blank it every pass).
|
||||
eld_nudge_once() {
|
||||
# Any ELD already valid → audio path is live; reset the nudge memory.
|
||||
if grep -q "monitor_present[[:space:]]*1" /proc/asound/card*/eld* 2>/dev/null; then
|
||||
NUDGED_OUTPUTS=""
|
||||
return 0
|
||||
fi
|
||||
|
||||
local xrandr_out conn name output mode
|
||||
xrandr_out=$(xrandr --query 2>/dev/null) || return 0
|
||||
|
||||
for conn in /sys/class/drm/card*-*/status; do
|
||||
[ -e "$conn" ] || continue
|
||||
[ "$(cat "$conn" 2>/dev/null)" = "connected" ] || continue
|
||||
name=${conn%/status}; name=${name##*/card?-}
|
||||
case "$name" in eDP*|LVDS*) continue ;; esac
|
||||
case " $NUDGED_OUTPUTS " in *" $name "*) continue ;; esac
|
||||
|
||||
# DRM connector names match the modesetting driver's output names.
|
||||
output=$(printf '%s\n' "$xrandr_out" | awk -v n="$name" '$1 == n && $2 == "connected" {print $1; exit}')
|
||||
[ -n "$output" ] || continue
|
||||
|
||||
# Keep the mode the kiosk chose (it may have capped a 4K panel);
|
||||
# --auto only as a fallback.
|
||||
mode=$(printf '%s\n' "$xrandr_out" | awk -v out="$output" '
|
||||
$1 == out { active = 1; next }
|
||||
active && /^[[:space:]]+[0-9]+x[0-9]+/ { if ($0 ~ /\*/) { print $1; exit } }
|
||||
active && /^[^[:space:]]/ { active = 0 }')
|
||||
|
||||
NUDGED_OUTPUTS="$NUDGED_OUTPUTS $name"
|
||||
xrandr --output "$output" --off 2>/dev/null || true
|
||||
sleep 1
|
||||
if [ -n "$mode" ]; then
|
||||
xrandr --output "$output" --mode "$mode" 2>/dev/null \
|
||||
|| xrandr --output "$output" --auto 2>/dev/null || true
|
||||
else
|
||||
xrandr --output "$output" --auto 2>/dev/null || true
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
route_once() {
|
||||
local cards_dump card active want sink default_sink
|
||||
cards_dump=$(pactl list cards 2>/dev/null) || return 0
|
||||
[ -n "$cards_dump" ] || return 0
|
||||
|
||||
# One line per card: "<card>\t<active>\t<wanted-profile>"
|
||||
# wanted = highest-priority available output:hdmi-stereo* profile, else
|
||||
# highest-priority available output:analog* profile.
|
||||
while IFS=$'\t' read -r card active want; do
|
||||
[ -n "$card" ] && [ -n "$want" ] || continue
|
||||
if [ "$active" != "$want" ]; then
|
||||
pactl set-card-profile "$card" "$want" 2>/dev/null || true
|
||||
fi
|
||||
done < <(printf '%s\n' "$cards_dump" | awk '
|
||||
function flush() {
|
||||
if (card != "") print card "\t" active "\t" (hdmi != "" ? hdmi : analog)
|
||||
card=""; active=""; hdmi=""; analog=""; hdmi_p=-1; analog_p=-1
|
||||
}
|
||||
/^Card #/ { flush() }
|
||||
/^\tName: / { card=$2 }
|
||||
/^\t\toutput:/ {
|
||||
line=$0; sub(/^\t\t/, "", line)
|
||||
prof=line; sub(/: .*/, "", prof)
|
||||
prio=0
|
||||
if (match(line, /priority: [0-9]+/)) prio=substr(line, RSTART+10, RLENGTH-10)+0
|
||||
avail = (line ~ /available: yes/ || line ~ /availability unknown/)
|
||||
if (!avail) next
|
||||
if (prof ~ /^output:hdmi-stereo/) { if (prio > hdmi_p) { hdmi=prof; hdmi_p=prio } }
|
||||
else if (prof ~ /^output:analog/) { if (prio > analog_p) { analog=prof; analog_p=prio } }
|
||||
}
|
||||
/^\tActive Profile: / { active=$3 }
|
||||
END { flush() }
|
||||
')
|
||||
|
||||
# Point the default sink at HDMI when one exists, else the first sink.
|
||||
sink=$(pactl list short sinks 2>/dev/null | awk '/hdmi/{print $2; exit}')
|
||||
[ -n "$sink" ] || sink=$(pactl list short sinks 2>/dev/null | awk 'NR==1{print $2}')
|
||||
[ -n "$sink" ] || return 0
|
||||
|
||||
default_sink=$(pactl get-default-sink 2>/dev/null)
|
||||
if [ "$sink" != "$default_sink" ] || [ "$sink" != "$LAST_SINK" ]; then
|
||||
pactl set-default-sink "$sink" 2>/dev/null || true
|
||||
pactl set-sink-mute "$sink" 0 2>/dev/null || true
|
||||
case "$sink" in
|
||||
*hdmi*) pactl set-sink-volume "$sink" 100% 2>/dev/null || true ;;
|
||||
esac
|
||||
# Migrate live streams so playing audio follows the hot-plug.
|
||||
pactl list short sink-inputs 2>/dev/null | while read -r id _; do
|
||||
pactl move-sink-input "$id" "$sink" 2>/dev/null || true
|
||||
done
|
||||
LAST_SINK="$sink"
|
||||
fi
|
||||
}
|
||||
|
||||
while true; do
|
||||
eld_nudge_once
|
||||
route_once
|
||||
sleep 5
|
||||
done
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Archipelago Container Doctor
|
||||
After=archipelago.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Runs as root: needs to kill orphaned conmon processes, fix permissions
|
||||
User=root
|
||||
ExecStart=/home/archipelago/archy/scripts/container-doctor.sh --local
|
||||
TimeoutStartSec=300
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Archipelago container doctor (periodic)
|
||||
|
||||
[Timer]
|
||||
# First run 2 minutes after boot, then every 5 minutes. The doctor is
|
||||
# idempotent and exits quickly when no drift exists; this keeps vanished
|
||||
# rootless port listeners and stopped containers from remaining broken.
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
# Jitter to avoid load spikes
|
||||
RandomizedDelaySec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Archipelago FIPS mesh transport (wraps upstream fips daemon)
|
||||
# Stay dark until onboarding materialises the seed-derived key. Archipelago
|
||||
# backend unmasks + starts this unit via `sudo systemctl` once the key is
|
||||
# present; pre-onboarding the unit must be masked so no traffic is sent
|
||||
# from an ephemeral identity.
|
||||
ConditionPathExists=/var/lib/archipelago/identity/fips_key
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStartPre=/bin/sh -c 'test -x /usr/bin/fips || { echo "fips daemon not installed — run fips.install from dashboard" >&2; exit 1; }'
|
||||
ExecStart=/usr/bin/fips --config /etc/fips/fips.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
# UDP 8668 is reachable on all interfaces by default; the daemon does its
|
||||
# own Noise authentication so no firewall gate is added here.
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Archipelago gamepad→keyboard bridge (kiosk nodes).
|
||||
|
||||
Reads every attached game controller via evdev and mirrors it as a virtual
|
||||
uinput KEYBOARD, so gamepad input works in every app — including cross-origin
|
||||
iframes (IndeeHub, Jellyfin, …) where the web shell can never inject events.
|
||||
The browser just sees arrow/Enter/Escape keys from a real-looking keyboard;
|
||||
X autorepeat handles held directions. Design: docs/tv-input-iframe-apps.md.
|
||||
|
||||
Mapping (standard pad):
|
||||
D-pad / left stick -> arrow keys
|
||||
A (BTN_SOUTH) -> Enter B (BTN_EAST) -> Escape
|
||||
X (BTN_NORTH/WEST*) -> Space Y -> f (player fullscreen)
|
||||
LB (BTN_TL) -> Shift+Tab RB (BTN_TR) -> Tab
|
||||
Start -> Enter Select -> Escape
|
||||
|
||||
*Controllers disagree on NORTH/WEST for X/Y; both map to Space/f — either way
|
||||
one is play/pause and one is fullscreen, which is fine for a TV.
|
||||
|
||||
Pure stdlib (struct/fcntl/select) — no python3-evdev dependency on the node.
|
||||
Runs as root (uinput + /dev/input need it); hotplug via 5s rescans.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
|
||||
# ---- kernel constants ------------------------------------------------------
|
||||
EV_SYN, EV_KEY, EV_ABS = 0x00, 0x01, 0x03
|
||||
SYN_REPORT = 0
|
||||
|
||||
KEY_ESC, KEY_TAB, KEY_ENTER, KEY_SPACE = 1, 15, 28, 57
|
||||
KEY_LEFTSHIFT, KEY_F = 42, 33
|
||||
KEY_UP, KEY_LEFT, KEY_RIGHT, KEY_DOWN = 103, 105, 106, 108
|
||||
|
||||
BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST = 0x130, 0x131, 0x133, 0x134
|
||||
BTN_TL, BTN_TR, BTN_SELECT, BTN_START = 0x136, 0x137, 0x13A, 0x13B
|
||||
BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT = 0x220, 0x221, 0x222, 0x223
|
||||
|
||||
ABS_X, ABS_Y, ABS_HAT0X, ABS_HAT0Y = 0x00, 0x01, 0x10, 0x11
|
||||
|
||||
EVIOCGBIT_EV_KEY = 0x80604521 # EVIOCGBIT(EV_KEY, 96) — enough for BTN range
|
||||
UI_SET_EVBIT, UI_SET_KEYBIT = 0x40045564, 0x40045565
|
||||
UI_DEV_CREATE, UI_DEV_DESTROY = 0x5501, 0x5502
|
||||
|
||||
INPUT_EVENT = struct.Struct("llHHi") # timeval sec/usec, type, code, value
|
||||
|
||||
BUTTON_MAP = {
|
||||
BTN_SOUTH: (KEY_ENTER,),
|
||||
BTN_EAST: (KEY_ESC,),
|
||||
BTN_NORTH: (KEY_SPACE,),
|
||||
BTN_WEST: (KEY_F,),
|
||||
BTN_TL: (KEY_LEFTSHIFT, KEY_TAB),
|
||||
BTN_TR: (KEY_TAB,),
|
||||
BTN_START: (KEY_ENTER,),
|
||||
BTN_SELECT: (KEY_ESC,),
|
||||
BTN_DPAD_UP: (KEY_UP,),
|
||||
BTN_DPAD_DOWN: (KEY_DOWN,),
|
||||
BTN_DPAD_LEFT: (KEY_LEFT,),
|
||||
BTN_DPAD_RIGHT: (KEY_RIGHT,),
|
||||
}
|
||||
EMITTED_KEYS = sorted({k for keys in BUTTON_MAP.values() for k in keys}
|
||||
| {KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT})
|
||||
|
||||
STICK_THRESHOLD = 0.55 # fraction of full deflection before a stick "presses"
|
||||
|
||||
|
||||
def is_gamepad(fd) -> bool:
|
||||
buf = bytearray(96)
|
||||
try:
|
||||
fcntl.ioctl(fd, EVIOCGBIT_EV_KEY, buf)
|
||||
except OSError:
|
||||
return False
|
||||
def has(code):
|
||||
return bool(buf[code // 8] & (1 << (code % 8)))
|
||||
return has(BTN_SOUTH) or has(BTN_START)
|
||||
|
||||
|
||||
class VirtualKeyboard:
|
||||
def __init__(self):
|
||||
self.fd = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK)
|
||||
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_KEY)
|
||||
for key in EMITTED_KEYS:
|
||||
fcntl.ioctl(self.fd, UI_SET_KEYBIT, key)
|
||||
# Legacy uinput_user_dev setup struct: name[80] + input_id + ff_effects
|
||||
# + absmax/absmin/absfuzz/absflat (64 ints each) — works on every kernel.
|
||||
name = b"Archipelago Gamepad Keys"
|
||||
setup = name.ljust(80, b"\0") + struct.pack("HHHHi", 0x06, 0x1, 0x1, 1, 0)
|
||||
setup += b"\0" * (64 * 4 * 4)
|
||||
os.write(self.fd, setup)
|
||||
fcntl.ioctl(self.fd, UI_DEV_CREATE)
|
||||
|
||||
def _emit(self, etype, code, value):
|
||||
os.write(self.fd, INPUT_EVENT.pack(0, 0, etype, code, value))
|
||||
|
||||
def set_key(self, key, pressed):
|
||||
self._emit(EV_KEY, key, 1 if pressed else 0)
|
||||
self._emit(EV_SYN, SYN_REPORT, 0)
|
||||
|
||||
def chord(self, keys, pressed):
|
||||
seq = keys if pressed else tuple(reversed(keys))
|
||||
for k in seq:
|
||||
self._emit(EV_KEY, k, 1 if pressed else 0)
|
||||
self._emit(EV_SYN, SYN_REPORT, 0)
|
||||
|
||||
|
||||
class PadState:
|
||||
"""Per-device axis state → synthetic arrow presses."""
|
||||
|
||||
def __init__(self):
|
||||
self.axis_keys = {} # axis -> currently-pressed arrow key (or None)
|
||||
self.abs_range = {} # axis -> (min, max) for sticks
|
||||
|
||||
def arrow_for(self, axis, value):
|
||||
if axis in (ABS_HAT0X, ABS_HAT0Y):
|
||||
if value < 0:
|
||||
return KEY_LEFT if axis == ABS_HAT0X else KEY_UP
|
||||
if value > 0:
|
||||
return KEY_RIGHT if axis == ABS_HAT0X else KEY_DOWN
|
||||
return None
|
||||
lo, hi = self.abs_range.get(axis, (-32768, 32767))
|
||||
span = (hi - lo) or 1
|
||||
norm = (2 * (value - lo) / span) - 1
|
||||
if norm <= -STICK_THRESHOLD:
|
||||
return KEY_LEFT if axis == ABS_X else KEY_UP
|
||||
if norm >= STICK_THRESHOLD:
|
||||
return KEY_RIGHT if axis == ABS_X else KEY_DOWN
|
||||
return None
|
||||
|
||||
|
||||
def stick_range(fd, axis):
|
||||
# EVIOCGABS(axis): struct input_absinfo { value, min, max, fuzz, flat, res }
|
||||
buf = bytearray(24)
|
||||
try:
|
||||
fcntl.ioctl(fd, 0x80184540 + axis, buf)
|
||||
_, lo, hi = struct.unpack("iii", bytes(buf[:12]))
|
||||
if hi > lo:
|
||||
return (lo, hi)
|
||||
except OSError:
|
||||
pass
|
||||
return (-32768, 32767)
|
||||
|
||||
|
||||
def main():
|
||||
os.system("modprobe uinput 2>/dev/null")
|
||||
kbd = VirtualKeyboard()
|
||||
pads = {} # path -> (fd, PadState)
|
||||
last_scan = 0.0
|
||||
|
||||
while True:
|
||||
now = time.monotonic()
|
||||
if now - last_scan > 5:
|
||||
last_scan = now
|
||||
try:
|
||||
names = sorted(os.listdir("/dev/input"))
|
||||
except FileNotFoundError:
|
||||
names = []
|
||||
for name in names:
|
||||
if not name.startswith("event"):
|
||||
continue
|
||||
path = "/dev/input/" + name
|
||||
if path in pads:
|
||||
continue
|
||||
try:
|
||||
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
except OSError:
|
||||
continue
|
||||
if is_gamepad(fd):
|
||||
state = PadState()
|
||||
for axis in (ABS_X, ABS_Y):
|
||||
state.abs_range[axis] = stick_range(fd, axis)
|
||||
pads[path] = (fd, state)
|
||||
print(f"gamepad attached: {path}", flush=True)
|
||||
else:
|
||||
os.close(fd)
|
||||
|
||||
if not pads:
|
||||
time.sleep(2)
|
||||
continue
|
||||
|
||||
readable, _, _ = select.select([fd for fd, _ in pads.values()], [], [], 2.0)
|
||||
gone = []
|
||||
for path, (fd, state) in list(pads.items()):
|
||||
if fd not in readable:
|
||||
continue
|
||||
try:
|
||||
data = os.read(fd, INPUT_EVENT.size * 64)
|
||||
except OSError:
|
||||
gone.append(path)
|
||||
continue
|
||||
for off in range(0, len(data) - INPUT_EVENT.size + 1, INPUT_EVENT.size):
|
||||
_, _, etype, code, value = INPUT_EVENT.unpack_from(data, off)
|
||||
if etype == EV_KEY and code in BUTTON_MAP and value in (0, 1):
|
||||
keys = BUTTON_MAP[code]
|
||||
if len(keys) == 1:
|
||||
kbd.set_key(keys[0], value == 1)
|
||||
else:
|
||||
kbd.chord(keys, value == 1)
|
||||
elif etype == EV_ABS and code in (ABS_X, ABS_Y, ABS_HAT0X, ABS_HAT0Y):
|
||||
want = state.arrow_for(code, value)
|
||||
held = state.axis_keys.get(code)
|
||||
if want != held:
|
||||
if held is not None:
|
||||
kbd.set_key(held, False)
|
||||
if want is not None:
|
||||
kbd.set_key(want, True)
|
||||
state.axis_keys[code] = want
|
||||
for path in gone:
|
||||
fd, state = pads.pop(path)
|
||||
for held in state.axis_keys.values():
|
||||
if held is not None:
|
||||
kbd.set_key(held, False)
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
print(f"gamepad detached: {path}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Archipelago gamepad->keyboard bridge (TV/kiosk input in every app iframe)
|
||||
# Only meaningful where a display UI runs; the kiosk unit is the marker.
|
||||
ConditionPathExists=/etc/systemd/system/archipelago-kiosk.service
|
||||
ConditionPathExists=/usr/local/bin/archipelago-gamepad-keys
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Root: /dev/uinput device creation + raw /dev/input readers.
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/archipelago-gamepad-keys
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Nice=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start a dedicated X server for the attached kiosk display.
|
||||
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
|
||||
XPID=$!
|
||||
|
||||
export DISPLAY=:0
|
||||
export HOME=/home/archipelago
|
||||
|
||||
X_READY=false
|
||||
for _ in $(seq 1 30); do
|
||||
if kill -0 "$XPID" 2>/dev/null && xrandr --query >/tmp/archipelago-kiosk-xrandr.txt 2>/dev/null; then
|
||||
X_READY=true
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [ "$X_READY" != "true" ]; then
|
||||
echo 'ERROR: Xorg failed to become ready'
|
||||
kill "$XPID" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Settings-managed display overrides (system.kiosk-display.set writes this
|
||||
# file, then restarts the kiosk): may set ARCHIPELAGO_KIOSK_SCALE,
|
||||
# ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH or ARCHIPELAGO_KIOSK_MAX_WIDTH.
|
||||
[ -f /etc/archipelago/kiosk-display.conf ] && . /etc/archipelago/kiosk-display.conf
|
||||
|
||||
KIOSK_SAFE_AREA_X_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_X_PX:-}
|
||||
KIOSK_SAFE_AREA_Y_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_Y_PX:-}
|
||||
|
||||
# Actual mode of the kiosk output — configure_display overwrites these so
|
||||
# Chromium's window matches the panel instead of assuming 1080p (a 1366x768
|
||||
# laptop panel otherwise gets a clipped oversized window).
|
||||
KIOSK_MODE_W=1920
|
||||
KIOSK_MODE_H=1080
|
||||
|
||||
configure_display() {
|
||||
command -v xrandr >/dev/null 2>&1 || return 0
|
||||
|
||||
local output mode internal width height
|
||||
output=$(awk '/ connected/ && $1 !~ /^eDP|^LVDS/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
|
||||
[ -n "$output" ] || output=$(awk '/ connected/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
|
||||
[ -n "$output" ] || return 0
|
||||
|
||||
# Pick the EDID-preferred ("+") mode, falling back to the first-listed
|
||||
# mode (EDID lists native first). Deliberately ignore "*" (currently
|
||||
# active) — trusting "active" lets a bad clone/mirror state from a
|
||||
# previous boot perpetuate itself forever instead of self-healing.
|
||||
mode=$(awk -v out="$output" '
|
||||
$1 == out { active = 1; next }
|
||||
active && /^[[:space:]]+[0-9]+x[0-9]+/ {
|
||||
if ($0 ~ /\+/ && !preferred) { preferred = $1 }
|
||||
if (!first) first = $1
|
||||
}
|
||||
active && /^[^[:space:]]/ { active = 0 }
|
||||
END { if (preferred) print preferred; else if (first) print first }
|
||||
' /tmp/archipelago-kiosk-xrandr.txt)
|
||||
[ -n "$mode" ] || mode=1920x1080
|
||||
|
||||
# Optional resolution cap for weak hardware: ARCHIPELAGO_KIOSK_MAX_WIDTH=1920
|
||||
# drops a 4K panel to its best <=1920-wide mode (the TV upscales) so the
|
||||
# software rasterizer paints 1/4 the pixels. Off by default — native mode
|
||||
# is sharp and fits the tile budget now that --window-size is in DIPs.
|
||||
local max_w=${ARCHIPELAGO_KIOSK_MAX_WIDTH:-0}
|
||||
if [ "$max_w" -gt 0 ] 2>/dev/null && [ "${mode%x*}" -gt "$max_w" ] 2>/dev/null; then
|
||||
local capped
|
||||
capped=$(awk -v out="$output" -v maxw="$max_w" '
|
||||
$1 == out { active = 1; next }
|
||||
active && /^[[:space:]]+[0-9]+x[0-9]+/ {
|
||||
split($1, wh, "x")
|
||||
if (wh[1] + 0 <= maxw && wh[1] + 0 > best_w) { best_w = wh[1] + 0; best = $1 }
|
||||
}
|
||||
active && /^[^[:space:]]/ { active = 0 }
|
||||
END { if (best) print best }
|
||||
' /tmp/archipelago-kiosk-xrandr.txt)
|
||||
[ -n "$capped" ] && mode=$capped
|
||||
fi
|
||||
|
||||
# Kiosk should use one native output. A spanning desktop makes Chromium land
|
||||
# on the laptop panel or stretch across both outputs.
|
||||
for internal in $(awk '/ connected/ && $1 ~ /^eDP|^LVDS/{print $1}' /tmp/archipelago-kiosk-xrandr.txt); do
|
||||
[ "$internal" = "$output" ] || xrandr --output "$internal" --off 2>/dev/null || true
|
||||
done
|
||||
|
||||
xrandr --output "$output" \
|
||||
--primary \
|
||||
--mode "$mode" \
|
||||
--pos 0x0 \
|
||||
--scale 1x1 \
|
||||
--panning 0x0 \
|
||||
--transform none 2>/dev/null || true
|
||||
|
||||
width=${mode%x*}
|
||||
height=${mode#*x}
|
||||
case "$width:$height" in *[!0-9:]*|:*) width=1920; height=1080 ;; esac
|
||||
KIOSK_MODE_W=$width
|
||||
KIOSK_MODE_H=$height
|
||||
|
||||
# Browser safe-area fallback for TVs that crop edges. Driver underscan is
|
||||
# preferable, but many Intel HDMI outputs do not expose that property.
|
||||
KIOSK_SAFE_AREA_X_PX=${KIOSK_SAFE_AREA_X_PX:-$((width * 3 / 100))}
|
||||
KIOSK_SAFE_AREA_Y_PX=${KIOSK_SAFE_AREA_Y_PX:-$((height * 3 / 100))}
|
||||
}
|
||||
|
||||
configure_display
|
||||
|
||||
# --- Kiosk UI scaling for large / high-res displays -----------------------
|
||||
# REVERT: set env ARCHIPELAGO_KIOSK_SCALE=1 (per-node, no rebuild), or restore
|
||||
# the hardcoded --force-device-scale-factor=1 below to disable entirely.
|
||||
#
|
||||
# A big TV reports its full native resolution as the CSS viewport, so a 4K
|
||||
# panel becomes a 3840px-wide viewport and the UI renders tiny — and a
|
||||
# keyboard-less kiosk can't zoom. Derive Chromium's device-scale-factor from
|
||||
# the detected panel width so the *effective* CSS viewport lands near a
|
||||
# comfortable target. Panels >=2560 wide get a 1920-wide layout (4K -> scale
|
||||
# 2.0 — user-validated on a 72" TV: spacious desktop layout, 2x sharp);
|
||||
# smaller panels get a 1280-wide layout (1920 -> 1.50, laptops -> 1.0).
|
||||
if [ -z "${ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH:-}" ]; then
|
||||
if [ "$KIOSK_MODE_W" -ge 2560 ] 2>/dev/null; then
|
||||
ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920
|
||||
else
|
||||
ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280
|
||||
fi
|
||||
fi
|
||||
KIOSK_TARGET_CSS_WIDTH=$ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH
|
||||
if [ -n "${ARCHIPELAGO_KIOSK_SCALE:-}" ]; then
|
||||
KIOSK_SCALE=$ARCHIPELAGO_KIOSK_SCALE
|
||||
else
|
||||
KIOSK_SCALE=$(awk -v w="$KIOSK_MODE_W" -v t="$KIOSK_TARGET_CSS_WIDTH" \
|
||||
'BEGIN{if(t<=0)t=1280; s=w/t; s=int(s*4+0.5)/4; if(s<1)s=1; if(s>3)s=3; printf "%.2f", s}')
|
||||
fi
|
||||
|
||||
# Chromium's --window-size is in DIPs (CSS px), not physical pixels: at scale S
|
||||
# the window paints S x larger. This X session has no window manager, so
|
||||
# --kiosk/--start-fullscreen cannot snap an oversized window back to the panel
|
||||
# — it just hangs off the right/bottom edges (content cropped, background art
|
||||
# offscreen). Size the window in DIPs so DIPs x scale = the panel exactly.
|
||||
KIOSK_WIN_W=$(awk -v w="$KIOSK_MODE_W" -v s="$KIOSK_SCALE" 'BEGIN{printf "%d", w/s}')
|
||||
KIOSK_WIN_H=$(awk -v h="$KIOSK_MODE_H" -v s="$KIOSK_SCALE" 'BEGIN{printf "%d", h/s}')
|
||||
|
||||
xhost +SI:localuser:archipelago 2>/dev/null || true
|
||||
xsetroot -solid black 2>/dev/null || true
|
||||
xset s off 2>/dev/null || true
|
||||
xset -dpms 2>/dev/null || true
|
||||
xset s noblank 2>/dev/null || true
|
||||
|
||||
pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization
|
||||
# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500)
|
||||
# because under X11 it falls back to software compositing anyway — that CPU
|
||||
# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the
|
||||
# separate process; GpuRasterization is also disabled via --disable-features below.
|
||||
# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead.
|
||||
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
|
||||
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
|
||||
else
|
||||
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
|
||||
fi
|
||||
|
||||
ARCHIPELAGO_UID=$(id -u archipelago)
|
||||
|
||||
while true; do
|
||||
# A profile lock left by a previous boot encodes <hostname>-<pid>; after a
|
||||
# hostname change (node rename) Chromium reads it as another computer
|
||||
# holding the profile and refuses to start — with --noerrdialogs that is an
|
||||
# invisible failure and the kiosk black-screens forever. Any Chromium that
|
||||
# owned the lock is dead by now (pkill above / previous loop iteration).
|
||||
rm -f /var/lib/archipelago/chromium-kiosk/Singleton{Lock,Cookie,Socket}
|
||||
# XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio
|
||||
# backend can't find PipeWire-Pulse's socket at /run/user/<uid>/pulse/native,
|
||||
# falls back to raw ALSA "default", fails to connect, and produces no audio
|
||||
# at all with no visible error (--noerrdialogs suppresses it).
|
||||
# OverlayScrollbar: thin auto-hiding scrollbars (the Chrome-on-a-remote-
|
||||
# device look) instead of classic X11 scrollbar chrome — a kiosk TV showed
|
||||
# a permanent fat scrollbar on scrollable views (Peers).
|
||||
# Force a DARK color-scheme preference. The main UI hardcodes its dark
|
||||
# theme, but the bundled AIUI app themes via `@media (prefers-color-scheme)`
|
||||
# and defaults to its LIGHT variant (white panels) when the browser reports
|
||||
# no preference — which a minimal kiosk X session does. Two independent dark
|
||||
# signals so this survives a Chromium enum change: GTK_THEME is version-
|
||||
# independent and can never force light (worst case: no effect), and the
|
||||
# blink-settings flag (0 = kDark in modern Chromium) reinforces it. Neither
|
||||
# can regress the always-dark main UI.
|
||||
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago GTK_THEME=Adwaita:dark XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \
|
||||
--app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \
|
||||
--blink-settings=preferredColorScheme=0 \
|
||||
--noerrdialogs \
|
||||
--disable-infobars \
|
||||
--disable-translate \
|
||||
--no-first-run \
|
||||
--check-for-update-interval=31536000 \
|
||||
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \
|
||||
--enable-features=OverlayScrollbar \
|
||||
--disable-session-crashed-bubble \
|
||||
--disable-save-password-bubble \
|
||||
--disable-suggestions-service \
|
||||
--disable-component-update \
|
||||
$GPU_FLAGS \
|
||||
--renderer-process-limit=2 \
|
||||
--window-size=${KIOSK_WIN_W},${KIOSK_WIN_H} \
|
||||
--window-position=0,0 \
|
||||
--start-fullscreen \
|
||||
--force-device-scale-factor=${KIOSK_SCALE} \
|
||||
--disable-background-networking \
|
||||
--disable-background-timer-throttling \
|
||||
--disable-backgrounding-occluded-windows \
|
||||
--disable-breakpad \
|
||||
--disable-metrics \
|
||||
--disable-metrics-reporting \
|
||||
--disable-domain-reliability \
|
||||
--js-flags="--max-old-space-size=256" \
|
||||
--user-data-dir=/var/lib/archipelago/chromium-kiosk
|
||||
sleep 3
|
||||
done
|
||||
|
||||
kill "$XPID" 2>/dev/null || true
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Archipelago Kiosk Watchdog
|
||||
After=archipelago.service
|
||||
Wants=archipelago.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/local/bin/archipelago-kiosk-watchdog
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,48 @@
|
||||
[Unit]
|
||||
Description=Archipelago Kiosk (X11 + Chromium)
|
||||
After=archipelago.service systemd-user-sessions.service network-online.target
|
||||
Wants=archipelago.service network-online.target
|
||||
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
|
||||
Conflicts=getty@tty1.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Wait up to 5 min for archipelago to serve /health. On slow hardware
|
||||
# first-boot is dominated by the FileBrowser pull (unbundled ISO),
|
||||
# initial archipelago state sync, and frontend settle — .198 took
|
||||
# longer than 120s and chromium launched against an empty backend,
|
||||
# producing a white window that only recovered on reboot. 300s gives
|
||||
# slow-but-functional hardware enough headroom; TimeoutStartSec is
|
||||
# bumped in lockstep so systemd doesn't kill us mid-wait.
|
||||
ExecStartPre=/bin/bash -c 'for i in $(seq 1 150); do curl -sf http://localhost/health >/dev/null 2>&1 && break; sleep 2; done'
|
||||
# Also wait for the web-ui asset swap to finish. The first-boot rsync into
|
||||
# /opt/archipelago/web-ui is non-atomic and writes the large bg-*.webp images
|
||||
# last — a kiosk launched mid-swap rendered the UI with blank backgrounds
|
||||
# (CSS background-image 404s are never refetched). A representative large
|
||||
# asset answering 200 means the swap is effectively done. Bounded 60s so a
|
||||
# renamed asset can never block kiosk startup.
|
||||
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf -o /dev/null http://localhost/assets/img/bg-home.webp && break; sleep 2; done; exit 0'
|
||||
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
|
||||
TimeoutStartSec=360
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
# Resource guardrail (#36). On GPU-less / headless hardware chromium could spin
|
||||
# software compositing at ~92% of a core, saturating the node and starving the
|
||||
# backend (it caused the .198 receive timeout + deploy storms). Cap CPU + memory
|
||||
# so a runaway kiosk can never take the whole machine down; Delegate so the cap
|
||||
# also binds the chromium/Xorg children in this unit's cgroup.
|
||||
# CPUQuota=75% (0.75 cores) was too tight even for normal playback — the kiosk
|
||||
# was throttled ~40% of the time, which is what caused choppy HDMI audio on
|
||||
# archy-x250-exp (2026-06-28 incident). 200% (2 cores) gives enough headroom.
|
||||
Delegate=yes
|
||||
CPUQuota=200%
|
||||
# Raised from 1500M/1200M: a Framework (Tiger Lake) kiosk sat at 806M used /
|
||||
# 1.1G peak, riding the old MemoryHigh reclaim-throttle line — the throttling
|
||||
# itself was the perceived UI lag. Keep Max well above real peaks; High stays
|
||||
# the soft reclaim line so a runaway kiosk still can't take the machine down.
|
||||
MemoryMax=2800M
|
||||
MemoryHigh=2200M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,10 @@
|
||||
# Archipelago persistent log directory and files
|
||||
# Runtime log destination. Backend runs as `archipelago`, but /var/log/
|
||||
# is root-owned, so we pre-create the directory and log files with the
|
||||
# right ownership at boot / install-time.
|
||||
#
|
||||
# Logrotate (image-recipe/configs/logrotate.conf) rotates files in this
|
||||
# directory daily, keeping 30 compressed copies.
|
||||
|
||||
d /var/log/archipelago 0755 archipelago archipelago - -
|
||||
f /var/log/archipelago/container-installs.log 0644 archipelago archipelago - -
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Watch for Archipelago Tor management actions
|
||||
|
||||
[Path]
|
||||
PathExists=/var/lib/archipelago/tor-config/tor-action
|
||||
MakeDirectory=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Process Archipelago Tor management action
|
||||
After=tor.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/archipelago/scripts/tor-helper.sh
|
||||
# Runs as root — needs to write /etc/tor/torrc and restart tor.service
|
||||
User=root
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Archipelago Self-Update
|
||||
After=network-online.target archipelago.service
|
||||
Wants=network-online.target
|
||||
ConditionPathExists=/home/archipelago/archy/.git
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=archipelago
|
||||
ExecStart=/home/archipelago/archy/scripts/self-update.sh
|
||||
TimeoutStartSec=600
|
||||
Environment="HOME=/home/archipelago"
|
||||
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/archipelago/.cargo/bin"
|
||||
|
||||
# Allow sudo for service restart and file install
|
||||
# Requires archipelago user in sudoers for specific commands
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Check for Archipelago updates daily
|
||||
ConditionPathExists=/home/archipelago/archy/.git
|
||||
|
||||
[Timer]
|
||||
# Check at 3 AM daily (low-activity window)
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
# Randomize within 30 min window to avoid thundering herd
|
||||
RandomizedDelaySec=1800
|
||||
# Run once on boot if last check was missed
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Assign WireGuard server address to wg0
|
||||
After=archipelago-wg.service
|
||||
Wants=archipelago-wg.service
|
||||
ConditionPathExists=/sys/class/net/wg0
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/bash -c 'ip address show dev wg0 | grep -q "10.44.0.1" || ip address add 10.44.0.1/16 dev wg0'
|
||||
ExecStart=/bin/bash -c 'iptables -t nat -C POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Archipelago Standalone WireGuard (wg0)
|
||||
After=network.target
|
||||
ConditionPathExists=/var/lib/archipelago/wireguard/private.key
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/bin/archipelago-wg setup /var/lib/archipelago/wireguard/private.key
|
||||
ExecStop=/bin/bash -c 'ip link del wg0 2>/dev/null || true'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,82 @@
|
||||
[Unit]
|
||||
Description=Archipelago Backend
|
||||
After=network-online.target archipelago-setup-tor.service
|
||||
Wants=network-online.target
|
||||
# The data dir AND podman's graphroot (containers/storage) both live on the
|
||||
# separate /var/lib/archipelago volume. Without this, on a cold boot the service
|
||||
# (and its ExecStartPre) can start BEFORE var-lib-archipelago.mount, write to the
|
||||
# bare mountpoint on rootfs, fail every podman call, exit, and get restarted every
|
||||
# 5s until the volume mounts (~5 min of "[FAILED] Failed to start" on boot — B17).
|
||||
# RequiresMountsFor adds both Requires= and After= on the mount unit so we never
|
||||
# start until the data volume is mounted.
|
||||
RequiresMountsFor=/var/lib/archipelago
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=archipelago
|
||||
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
||||
Environment="ARCHIPELAGO_USE_QUADLET_BACKENDS=true"
|
||||
EnvironmentFile=-/var/lib/archipelago/telemetry.env
|
||||
# DEV_MODE disabled in production — enabled via override.conf on dev servers
|
||||
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||
# + prefix runs these as root (needed for chown/mkdir outside ReadWritePaths)
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/user/1000 /var/lib/containers && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
|
||||
# Host IP from the main-table default route — hostname -I token order breaks
|
||||
# once a VPN/bridge interface exists (netbird's wg tunnel sorted first and
|
||||
# poisoned every host_ip consumer). Falls back to hostname -I when routeless.
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /var/lib/archipelago && chown archipelago:archipelago /var/lib/archipelago && IP=$(ip -4 route show default 2>/dev/null | sed -n "s/.* src \([0-9.]*\).*/\1/p" | head -1); [ -n "$$IP" ] || IP=$(hostname -I 2>/dev/null | awk "{print $$1}"); echo "ARCHIPELAGO_HOST_IP=$$IP" > /var/lib/archipelago/host-ip.env && chown archipelago:archipelago /var/lib/archipelago/host-ip.env'
|
||||
# OTA crash-loop guard: if a just-applied binary can't start (SEGV loop), the
|
||||
# in-binary post-OTA probe never runs — this restores the update-backup binary
|
||||
# after 5 failed start attempts while the pending-verify marker exists.
|
||||
# "-" so a missing/failed guard can never block the service itself.
|
||||
ExecStartPre=+-/opt/archipelago/scripts/ota-crash-guard.sh
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
WatchdogSec=300
|
||||
TimeoutStartSec=300
|
||||
# Backend shuts down in <1s; 15s is generous for any cleanup
|
||||
TimeoutStopSec=15
|
||||
|
||||
# Filesystem protection
|
||||
ProtectSystem=strict
|
||||
# ProtectHome=no: rootless podman needs writable ~/.local/share/containers
|
||||
ProtectHome=no
|
||||
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
|
||||
# and must be shared between the service and SSH-created containers
|
||||
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/user /tmp /home/archipelago/.local/share/containers /home/archipelago/.config/containers /etc
|
||||
|
||||
# Privilege restriction — NoNewPrivileges=no required for sudo archipelago-wg
|
||||
# (WireGuard peer management). Scoped via sudoers to only archipelago-wg.
|
||||
NoNewPrivileges=no
|
||||
PrivateDevices=no
|
||||
SupplementaryGroups=dialout debian-tor fips
|
||||
|
||||
# Syscall and network restrictions — safe on Debian 13 (systemd 256+)
|
||||
# which respects NoNewPrivileges=no as an explicit override for seccomp filters
|
||||
SystemCallArchitectures=native
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
RestrictRealtime=yes
|
||||
|
||||
# MemoryDenyWriteExecute removed: ring (rustls) and secp256k1 (bitcoin/nostr)
|
||||
# use assembly code that requires executable memory mappings on some platforms
|
||||
|
||||
# Resource limits
|
||||
MemoryMax=4G
|
||||
LimitNOFILE=65535
|
||||
TasksMax=2048
|
||||
|
||||
# Delegate cgroup controllers so rootless podman (run from this system service
|
||||
# as user=archipelago, not user@1000.service) can create transient libpod-*.scope
|
||||
# units with --memory / --cpus / --pids-limit. Without this, podman create fails
|
||||
# at start time with: "MemoryMax is out of range" because systemd rejects resource
|
||||
# limits on undelegated cgroup subtrees. Required for the ProdContainerOrchestrator
|
||||
# code path (see core/archipelago/src/container/prod_orchestrator.rs).
|
||||
Delegate=memory pids cpu io
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,7 @@
|
||||
pcm.!default {
|
||||
type pulse
|
||||
hint.description "Default ALSA Device (via PulseAudio)"
|
||||
}
|
||||
ctl.!default {
|
||||
type pulse
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
SystemMaxUse=500M
|
||||
RuntimeMaxUse=100M
|
||||
ForwardToSyslog=no
|
||||
RateLimitIntervalSec=30s
|
||||
RateLimitBurst=10000
|
||||
@@ -0,0 +1,24 @@
|
||||
# Log rotation configuration for Archipelago
|
||||
/var/log/archipelago/*.log {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0644 root root
|
||||
sharedscripts
|
||||
postrotate
|
||||
/usr/bin/systemctl reload archipelago > /dev/null 2>&1 || true
|
||||
endscript
|
||||
}
|
||||
|
||||
/var/lib/archipelago/logs/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0644 archipelago archipelago
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
# Gitea iframe proxy — strips X-Frame-Options so Gitea works in Archipelago iframe.
|
||||
# Gitea container binds to port 3001, this proxy listens on port 3000 (the public port).
|
||||
# Deployed to /etc/nginx/conf.d/gitea-iframe.conf
|
||||
server {
|
||||
listen 3000;
|
||||
server_name _;
|
||||
client_max_body_size 1G;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[info]
|
||||
relay_url = "ws://0.0.0.0:7777/"
|
||||
name = "Archipelago Private Relay"
|
||||
description = "Private Nostr relay for Archipelago mesh VPN peer discovery"
|
||||
|
||||
[database]
|
||||
data_directory = "/var/lib/archipelago/nostr-relay"
|
||||
in_memory = true
|
||||
|
||||
[network]
|
||||
address = "0.0.0.0"
|
||||
port = 7777
|
||||
ping_interval = 120
|
||||
|
||||
[limits]
|
||||
messages_per_sec = 50
|
||||
max_event_bytes = 65536
|
||||
max_ws_message_bytes = 65536
|
||||
max_ws_frame_bytes = 65536
|
||||
@@ -0,0 +1,27 @@
|
||||
[Unit]
|
||||
Description=Archipelago Private Nostr Relay
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=nostr-vpn.service
|
||||
# An ISO built without the relay binary (registry unreachable at build time)
|
||||
# must not crash-loop every 3s forever — skip cleanly instead.
|
||||
ConditionPathExists=/usr/local/bin/nostr-rs-relay
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=archipelago
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago/nostr-relay'
|
||||
ExecStart=/usr/local/bin/nostr-rs-relay --config /var/lib/archipelago/nostr-relay/config.toml
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
TimeoutStopSec=10
|
||||
|
||||
# Resource limits — relay is lightweight (in-memory mode)
|
||||
MemoryMax=512M
|
||||
LimitNOFILE=4096
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,35 @@
|
||||
[Unit]
|
||||
Description=Nostr VPN - Mesh VPN with Nostr identity
|
||||
After=network-online.target tor.service archipelago.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
# An ISO built without the nvpn binary (registry unreachable at build time)
|
||||
# must not restart-loop — skip cleanly instead.
|
||||
ConditionPathExists=/usr/local/bin/nvpn
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=HOME=/var/lib/archipelago/nostr-vpn
|
||||
EnvironmentFile=-/var/lib/archipelago/nostr-vpn/env
|
||||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/nostr-vpn /var/lib/archipelago/nostr-vpn/.config/nvpn'
|
||||
ExecStartPre=/bin/bash -c 'test -f /var/lib/archipelago/nostr-vpn/env || { echo "NostrVPN not configured — waiting for onboarding"; exit 1; }'
|
||||
ExecStart=/usr/local/bin/nvpn daemon
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
TimeoutStartSec=30
|
||||
TimeoutStopSec=10
|
||||
|
||||
# No sandbox — runs as root for TUN/WireGuard, needs unrestricted filesystem
|
||||
|
||||
# Resource limits
|
||||
MemoryMax=256M
|
||||
TasksMax=64
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,366 @@
|
||||
# App proxies for HTTPS - avoids mixed content when embedding apps from HTTPS page
|
||||
# Complete list for all apps that may be launched from the UI
|
||||
location /app/grafana/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location = /app/uptime-kuma/ {
|
||||
return 302 /app/uptime-kuma/dashboard;
|
||||
}
|
||||
location /app/uptime-kuma/ {
|
||||
proxy_pass http://127.0.0.1:3002/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Prefix /app/uptime-kuma;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect / /app/uptime-kuma/;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/gitea/ {
|
||||
proxy_pass http://127.0.0.1:3001/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
}
|
||||
location /app/searxng/ {
|
||||
proxy_pass http://127.0.0.1:8888/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/portainer/ {
|
||||
proxy_pass http://127.0.0.1:9000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/filebrowser/ {
|
||||
client_max_body_size 10G;
|
||||
proxy_pass http://127.0.0.1:8083/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_request_buffering off;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/endurain/ {
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/lnd/ {
|
||||
proxy_pass http://127.0.0.1:18083/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/jellyfin/ {
|
||||
proxy_pass http://127.0.0.1:8096/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/photoprism/ {
|
||||
proxy_pass http://127.0.0.1:2342/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/mempool/ {
|
||||
proxy_pass http://127.0.0.1:4080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/fedimint/ {
|
||||
proxy_pass http://127.0.0.1:8175/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_types text/css application/javascript application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter 'href="/' 'href="/app/fedimint/';
|
||||
sub_filter 'src="/' 'src="/app/fedimint/';
|
||||
sub_filter "href='/" "href='/app/fedimint/";
|
||||
sub_filter "src='/" "src='/app/fedimint/";
|
||||
sub_filter 'url("/' 'url("/app/fedimint/';
|
||||
sub_filter "url('/" "url('/app/fedimint/";
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/fedimint-gateway/ {
|
||||
proxy_pass http://127.0.0.1:8176/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/tailscale/ {
|
||||
# Tailscale has no web UI — managed via CLI/Tailscale app
|
||||
default_type application/json;
|
||||
return 503 '{"error":{"code":"NO_WEB_UI","message":"Tailscale is managed via CLI"}}';
|
||||
}
|
||||
location /app/routstr/ {
|
||||
proxy_pass http://127.0.0.1:8200/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/nostr-vpn/ {
|
||||
proxy_pass http://127.0.0.1:8201/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
}
|
||||
location /app/fips/ {
|
||||
proxy_pass http://127.0.0.1:8202/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
}
|
||||
location /app/ollama/ {
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/bitcoin-ui/ {
|
||||
proxy_pass http://127.0.0.1:8334/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/botfights/api/ {
|
||||
proxy_pass http://127.0.0.1:9100/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
location /app/botfights/ {
|
||||
proxy_pass http://127.0.0.1:9100/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_hide_header Cross-Origin-Embedder-Policy;
|
||||
proxy_hide_header Cross-Origin-Opener-Policy;
|
||||
proxy_hide_header Cross-Origin-Resource-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_types text/css application/javascript application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter 'href="/' 'href="/app/botfights/';
|
||||
sub_filter 'src="/' 'src="/app/botfights/';
|
||||
sub_filter "href='/" "href='/app/botfights/";
|
||||
sub_filter "src='/" "src='/app/botfights/";
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script><script>window.addEventListener("message",function(e){var d=e.data;if(d&&d.type==="arcade-input"&&d.key){var t=d.action==="up"?"keyup":"keydown";document.dispatchEvent(new KeyboardEvent(t,{key:d.key,bubbles:true}))}})</script></head>';
|
||||
}
|
||||
location /app/electrumx/ {
|
||||
proxy_pass http://127.0.0.1:50002/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/indeedhub/_next/ {
|
||||
proxy_pass http://127.0.0.1:7777/_next/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_valid 200 30d;
|
||||
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||
}
|
||||
location /app/indeedhub/ws/ {
|
||||
proxy_pass http://127.0.0.1:7777/ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
location /app/indeedhub/ {
|
||||
proxy_pass http://127.0.0.1:7777/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_types text/css application/javascript application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter 'href="/' 'href="/app/indeedhub/';
|
||||
sub_filter 'src="/' 'src="/app/indeedhub/';
|
||||
sub_filter "href='/" "href='/app/indeedhub/";
|
||||
sub_filter "src='/" "src='/app/indeedhub/";
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/nginx-proxy-manager/ {
|
||||
proxy_pass http://127.0.0.1:8081/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_hide_header X-Frame-Options;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# PWA installability - required for Install (not just Add to Home Screen) on Android
|
||||
# Manifest MUST be served with application/manifest+json - Chrome rejects otherwise
|
||||
location = /manifest.webmanifest {
|
||||
default_type application/manifest+json;
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
# Service worker - no cache so updates apply
|
||||
location ~ ^/(sw\.js|workbox-.*\.js|registerSW\.js)$ {
|
||||
add_header Content-Type application/javascript;
|
||||
add_header Service-Worker-Allowed /;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
# index.html - avoid aggressive cache for PWA updates
|
||||
location = /index.html {
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Create bootable FAT32 USB from Archipelago ISO
|
||||
# This extracts the ISO contents to FAT32 for UEFI boot
|
||||
#
|
||||
# Usage: ./create-fat32-usb.sh /dev/diskN
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Get absolute path of script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /dev/diskN"
|
||||
echo ""
|
||||
echo "Available disks:"
|
||||
diskutil list external
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USB_DISK="$1"
|
||||
ISO_FILE="${ARCHIPELAGO_ISO:-}"
|
||||
if [ -z "$ISO_FILE" ]; then
|
||||
ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-x86_64.iso"
|
||||
[ -f "$ISO_FILE" ] || ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-unbundled-x86_64.iso"
|
||||
fi
|
||||
WORK_DIR="$SCRIPT_DIR/build/usb-extract"
|
||||
|
||||
if [ ! -f "$ISO_FILE" ]; then
|
||||
echo "❌ ISO not found: $ISO_FILE"
|
||||
echo ""
|
||||
echo "Build the ISO first with: ./build-debian-iso.sh"
|
||||
echo "Or set ARCHIPELAGO_ISO=/path/to/archipelago-installer-x86_64.iso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ Create FAT32 Bootable USB ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "⚠️ WARNING: This will COMPLETELY ERASE $USB_DISK"
|
||||
echo ""
|
||||
echo "📀 ISO: $(basename "$ISO_FILE")"
|
||||
echo "💾 USB: $USB_DISK"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to cancel, or Enter to continue..."
|
||||
read
|
||||
|
||||
# Clean up work directory
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
echo "🔓 Unmounting USB..."
|
||||
diskutil unmountDisk "$USB_DISK" || true
|
||||
|
||||
echo ""
|
||||
echo "🗂️ Formatting USB as FAT32 with MBR..."
|
||||
diskutil eraseDisk FAT32 ARCHIPELAGO MBR "$USB_DISK"
|
||||
|
||||
echo ""
|
||||
echo "📦 Extracting ISO contents..."
|
||||
cd "$WORK_DIR"
|
||||
7z x -y "$ISO_FILE"
|
||||
|
||||
echo ""
|
||||
echo "📋 Copying files to USB (this may take a few minutes)..."
|
||||
USB_MOUNT="/Volumes/ARCHIPELAGO"
|
||||
if [ ! -d "$USB_MOUNT" ]; then
|
||||
echo "❌ USB not mounted at $USB_MOUNT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy all extracted files
|
||||
cp -Rv "$WORK_DIR"/* "$USB_MOUNT/" 2>/dev/null | tail -5
|
||||
echo " (showing last 5 files copied)"
|
||||
|
||||
echo ""
|
||||
echo "🔄 Syncing..."
|
||||
sync
|
||||
|
||||
echo ""
|
||||
echo "✅ USB created successfully!"
|
||||
echo ""
|
||||
echo "Now:"
|
||||
echo " 1. Eject: diskutil eject $USB_DISK"
|
||||
echo " 2. Insert into target machine"
|
||||
echo " 3. Boot from USB (F12 or similar for boot menu)"
|
||||
echo ""
|
||||
echo "Default login (live mode):"
|
||||
echo " Username: user"
|
||||
echo " Password: live"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$WORK_DIR"
|
||||
Executable
+207
@@ -0,0 +1,207 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Boot branding dev — iterate on GRUB theme, Plymouth, and installer visuals
|
||||
# without rebuilding the ISO. Patches an existing ISO and boots in QEMU.
|
||||
#
|
||||
# Usage:
|
||||
# ./dev-branding.sh [path-to-iso]
|
||||
#
|
||||
# If no ISO is found locally, downloads the latest from the build server.
|
||||
# Edit files in branding/, re-run, see changes in ~10 seconds.
|
||||
#
|
||||
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
WORK="/tmp/archipelago-dev-branding"
|
||||
PATCHED="$SCRIPT_DIR/results/archipelago-dev-patched.iso"
|
||||
CACHED_ISO="$SCRIPT_DIR/results/archipelago-dev-base.iso"
|
||||
DEV_SERVER="archipelago@192.168.1.228"
|
||||
SSH_KEY="$HOME/.ssh/archipelago-deploy"
|
||||
|
||||
echo ""
|
||||
echo " Archipelago Boot Branding Dev"
|
||||
echo ""
|
||||
|
||||
# --- Find or download an ISO ---
|
||||
ISO="${1:-}"
|
||||
|
||||
# Search locally
|
||||
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
|
||||
for pattern in \
|
||||
"$HOME/Desktop/archipelago-dev-"*.iso \
|
||||
"$HOME/Desktop/archipelago-unbundled-"*.iso \
|
||||
"$HOME/Desktop/archipelago-"*.iso \
|
||||
"$SCRIPT_DIR/results/archipelago-dev-base.iso" \
|
||||
"$SCRIPT_DIR/results/archipelago-"*.iso; do
|
||||
found=$(ls -t $pattern 2>/dev/null | head -1)
|
||||
if [ -n "$found" ] && [ -f "$found" ]; then
|
||||
ISO="$found"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Download from server if not found
|
||||
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
|
||||
echo " No ISO found locally. Downloading latest from build server..."
|
||||
REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \
|
||||
"ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso 2>/dev/null | head -1" 2>/dev/null)
|
||||
if [ -z "$REMOTE_ISO" ]; then
|
||||
REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \
|
||||
"ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-unbundled-*.iso 2>/dev/null | head -1" 2>/dev/null)
|
||||
fi
|
||||
if [ -n "$REMOTE_ISO" ]; then
|
||||
mkdir -p "$SCRIPT_DIR/results"
|
||||
echo " Downloading: $(basename "$REMOTE_ISO")..."
|
||||
scp -i "$SSH_KEY" "$DEV_SERVER:$REMOTE_ISO" "$CACHED_ISO"
|
||||
ISO="$CACHED_ISO"
|
||||
echo " Saved to: $ISO"
|
||||
else
|
||||
echo " No ISO on server either. Run a CI build first."
|
||||
echo " Or place an ISO on your Desktop."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " Base ISO: $(basename "$ISO") ($(du -h "$ISO" | cut -f1))"
|
||||
echo ""
|
||||
|
||||
# --- Extract ISO ---
|
||||
echo " [1/3] Extracting ISO..."
|
||||
if [ -d "$WORK" ]; then
|
||||
chmod -R u+w "$WORK" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
xorriso -osirrox on -indev "$ISO" -extract / "$WORK" 2>/dev/null || {
|
||||
echo " xorriso extraction failed, trying hdiutil..."
|
||||
MNT=$(mktemp -d)
|
||||
hdiutil attach "$ISO" -mountpoint "$MNT" -readonly -nobrowse 2>/dev/null || {
|
||||
echo " Could not mount ISO. Is it corrupt?"
|
||||
exit 1
|
||||
}
|
||||
cp -a "$MNT"/* "$WORK/" 2>/dev/null || true
|
||||
hdiutil detach "$MNT" 2>/dev/null || true
|
||||
rmdir "$MNT" 2>/dev/null || true
|
||||
}
|
||||
# Ensure files are writable after extraction
|
||||
chmod -R u+w "$WORK" 2>/dev/null || true
|
||||
|
||||
# --- Patch branding ---
|
||||
echo " [2/3] Patching branding..."
|
||||
THEME_DST="$WORK/boot/grub/themes/archipelago"
|
||||
mkdir -p "$THEME_DST"
|
||||
|
||||
# GRUB theme.txt
|
||||
if [ -f "$SCRIPT_DIR/branding/grub-theme/theme.txt" ]; then
|
||||
cp "$SCRIPT_DIR/branding/grub-theme/theme.txt" "$THEME_DST/"
|
||||
echo " theme.txt"
|
||||
fi
|
||||
|
||||
# GRUB background — use static file from branding dir
|
||||
if [ -f "$SCRIPT_DIR/branding/grub-theme/background.png" ]; then
|
||||
cp "$SCRIPT_DIR/branding/grub-theme/background.png" "$THEME_DST/background.png"
|
||||
echo " background.png (static)"
|
||||
elif [ -f "$SCRIPT_DIR/branding/generate-grub-background.py" ]; then
|
||||
python3 "$SCRIPT_DIR/branding/generate-grub-background.py" "$THEME_DST/background.png" 2>/dev/null
|
||||
echo " background.png (generated)"
|
||||
fi
|
||||
|
||||
# Plymouth theme
|
||||
PLYMOUTH_DST="$WORK/archipelago/plymouth-theme"
|
||||
mkdir -p "$PLYMOUTH_DST"
|
||||
if [ -d "$SCRIPT_DIR/branding/plymouth-theme" ]; then
|
||||
cp "$SCRIPT_DIR/branding/plymouth-theme/"* "$PLYMOUTH_DST/" 2>/dev/null || true
|
||||
echo " plymouth theme"
|
||||
fi
|
||||
|
||||
# --- Repackage ISO ---
|
||||
echo " [3/3] Repackaging ISO..."
|
||||
mkdir -p "$SCRIPT_DIR/results"
|
||||
|
||||
# Find isohdpfx.bin — project copy first, then system
|
||||
ISOHDPFX=""
|
||||
for p in "$SCRIPT_DIR/branding/isohdpfx.bin" \
|
||||
"$WORK/isolinux/isohdpfx.bin" \
|
||||
/usr/lib/ISOLINUX/isohdpfx.bin \
|
||||
/usr/share/syslinux/isohdpfx.bin \
|
||||
/opt/homebrew/share/syslinux/isohdpfx.bin; do
|
||||
[ -f "$p" ] && ISOHDPFX="$p" && break
|
||||
done
|
||||
|
||||
if [ -z "$ISOHDPFX" ]; then
|
||||
echo " ERROR: No isohdpfx.bin found. Cannot create bootable ISO."
|
||||
echo " Preview only — open the background:"
|
||||
open "$THEME_DST/background.png" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EFI_IMG="$WORK/boot/grub/efi.img"
|
||||
if [ -f "$EFI_IMG" ]; then
|
||||
xorriso -as mkisofs -o "$PATCHED" \
|
||||
-volid "ARCHIPELAGO" \
|
||||
-iso-level 3 -J -joliet-long -R \
|
||||
-isohybrid-mbr "$ISOHDPFX" \
|
||||
-c isolinux/boot.cat \
|
||||
-b isolinux/isolinux.bin \
|
||||
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
||||
-eltorito-alt-boot \
|
||||
-e boot/grub/efi.img \
|
||||
-no-emul-boot -isohybrid-gpt-basdat \
|
||||
-partition_offset 16 \
|
||||
"$WORK" 2>/dev/null
|
||||
else
|
||||
xorriso -as mkisofs -o "$PATCHED" \
|
||||
-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 \
|
||||
"$WORK" 2>/dev/null
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Patched: $PATCHED ($(du -h "$PATCHED" | cut -f1))"
|
||||
echo ""
|
||||
|
||||
# --- Boot in QEMU ---
|
||||
if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then
|
||||
echo " QEMU not found. Install: brew install qemu"
|
||||
echo " Opening background preview instead..."
|
||||
open "$THEME_DST/background.png" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " Booting in QEMU (BIOS mode — shows ISOLINUX menu)..."
|
||||
echo " Press Ctrl+C to stop."
|
||||
echo ""
|
||||
|
||||
# Create test disk (use separate disk from other QEMU instances)
|
||||
DISK="/tmp/archipelago-branding-test.qcow2"
|
||||
# Kill any leftover QEMU from previous branding test
|
||||
pkill -f "archipelago-branding-test" 2>/dev/null || true
|
||||
sleep 1
|
||||
if [ ! -f "$DISK" ]; then
|
||||
qemu-img create -f qcow2 "$DISK" 20G 2>/dev/null
|
||||
fi
|
||||
|
||||
# Boot with BIOS to see the ISOLINUX/GRUB menu
|
||||
qemu-system-x86_64 \
|
||||
-machine pc \
|
||||
-m 4G \
|
||||
-smp 2 \
|
||||
-boot d \
|
||||
-cdrom "$PATCHED" \
|
||||
-drive if=virtio,format=qcow2,file="$DISK" \
|
||||
-net nic,model=virtio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8100-:80 \
|
||||
-vga virtio \
|
||||
-display default \
|
||||
-serial file:/tmp/archipelago-qemu-serial.log
|
||||
|
||||
echo ""
|
||||
echo " QEMU stopped. Serial log: /tmp/archipelago-qemu-serial.log"
|
||||
echo " Re-run to test again after editing branding files."
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Quick fix to enable Archipelago auto-start on existing booted system
|
||||
# Run this on the Dell OptiPlex after boot
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Fixing Archipelago auto-start..."
|
||||
|
||||
# Create the auto-start script in .bashrc
|
||||
cat >> ~/.bashrc << 'EOF'
|
||||
|
||||
# Archipelago Auto-Start
|
||||
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -n "$PS1" ]; then
|
||||
export ARCHIPELAGO_STARTED=1
|
||||
|
||||
# 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 [ -n "$BOOT_MEDIA" ]; then
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Get IP
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -n "$IP" ] && echo " 🌐 Web UI: http://$IP:5678"
|
||||
echo ""
|
||||
|
||||
# Run the setup
|
||||
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
|
||||
echo "✅ Auto-start added to .bashrc"
|
||||
echo ""
|
||||
echo "Now logout and login again, or run:"
|
||||
echo " source ~/.bashrc"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Integrate Archipelago backend and frontend into custom ISO
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "🔗 Integrating Archipelago Components into ISO"
|
||||
echo ""
|
||||
|
||||
# Check if backend exists
|
||||
BACKEND_BIN="$SCRIPT_DIR/build/backend/archipelago"
|
||||
if [ ! -f "$BACKEND_BIN" ]; then
|
||||
echo "❌ Backend binary not found at $BACKEND_BIN"
|
||||
echo " Run: ./build-backend.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if frontend exists
|
||||
FRONTEND_DIR="$SCRIPT_DIR/build/frontend"
|
||||
if [ ! -d "$FRONTEND_DIR" ] || [ ! -f "$FRONTEND_DIR/index.html" ]; then
|
||||
echo "❌ Frontend build not found at $FRONTEND_DIR"
|
||||
echo " Run: ./build-frontend.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found backend binary: $(du -h "$BACKEND_BIN" | cut -f1)"
|
||||
echo "✅ Found frontend files"
|
||||
echo ""
|
||||
|
||||
# Now rebuild the ISO with the integrated components
|
||||
export INCLUDE_BACKEND="$BACKEND_BIN"
|
||||
export INCLUDE_FRONTEND="$FRONTEND_DIR"
|
||||
|
||||
echo "🔨 Rebuilding ISO with Archipelago components..."
|
||||
./build-custom-iso.sh
|
||||
|
||||
echo ""
|
||||
echo "✅ Integration complete!"
|
||||
echo ""
|
||||
@@ -0,0 +1,2 @@
|
||||
/dev/mmcblk0p1 /boot vfat umask=0077 0 2
|
||||
/dev/mmcblk0p2 / ext4 defaults 0 1
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
|
||||
get_variables () {
|
||||
ROOT_PART_DEV=$(findmnt / -o source -n)
|
||||
ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3)
|
||||
ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4)
|
||||
ROOT_DEV="/dev/${ROOT_DEV_NAME}"
|
||||
ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition")
|
||||
|
||||
BOOT_PART_DEV=$(findmnt /boot -o source -n)
|
||||
BOOT_PART_NAME=$(echo "$BOOT_PART_DEV" | cut -d "/" -f 3)
|
||||
BOOT_DEV_NAME=$(echo /sys/block/*/"${BOOT_PART_NAME}" | cut -d "/" -f 4)
|
||||
BOOT_PART_NUM=$(cat "/sys/block/${BOOT_DEV_NAME}/${BOOT_PART_NAME}/partition")
|
||||
|
||||
OLD_DISKID=$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p')
|
||||
|
||||
ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size")
|
||||
if [ "$ROOT_DEV_SIZE" -le 67108864 ]; then
|
||||
TARGET_END=$((ROOT_DEV_SIZE - 1))
|
||||
else
|
||||
TARGET_END=$((33554432 - 1))
|
||||
DATA_PART_START=33554432
|
||||
DATA_PART_END=$((ROOT_DEV_SIZE - 1))
|
||||
fi
|
||||
|
||||
PARTITION_TABLE=$(parted -m "$ROOT_DEV" unit s print | tr -d 's')
|
||||
|
||||
LAST_PART_NUM=$(echo "$PARTITION_TABLE" | tail -n 1 | cut -d ":" -f 1)
|
||||
|
||||
ROOT_PART_LINE=$(echo "$PARTITION_TABLE" | grep -e "^${ROOT_PART_NUM}:")
|
||||
ROOT_PART_START=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 2)
|
||||
ROOT_PART_END=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 3)
|
||||
}
|
||||
|
||||
check_variables () {
|
||||
if [ "$BOOT_DEV_NAME" != "$ROOT_DEV_NAME" ]; then
|
||||
FAIL_REASON="Boot and root partitions are on different devices"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$ROOT_PART_NUM" -ne "$LAST_PART_NUM" ]; then
|
||||
FAIL_REASON="Root partition should be last partition"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$ROOT_PART_END" -gt "$TARGET_END" ]; then
|
||||
FAIL_REASON="Root partition runs past the end of device"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -b "$ROOT_DEV" ] || [ ! -b "$ROOT_PART_DEV" ] || [ ! -b "$BOOT_PART_DEV" ] ; then
|
||||
FAIL_REASON="Could not determine partitions"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main () {
|
||||
get_variables
|
||||
|
||||
if ! check_variables; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# if [ "$ROOT_PART_END" -eq "$TARGET_END" ]; then
|
||||
# reboot_pi
|
||||
# fi
|
||||
|
||||
if ! echo Yes | parted -m --align=optimal "$ROOT_DEV" ---pretend-input-tty u s resizepart "$ROOT_PART_NUM" "$TARGET_END" ; then
|
||||
FAIL_REASON="Root partition resize failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$DATA_PART_START" ]; then
|
||||
if ! parted -ms --align=optimal "$ROOT_DEV" u s mkpart primary "$DATA_PART_START" "$DATA_PART_END"; then
|
||||
FAIL_REASON="Data partition creation failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
(
|
||||
echo x
|
||||
echo i
|
||||
echo "0xcb15ae4d"
|
||||
echo r
|
||||
echo w
|
||||
) | fdisk $ROOT_DEV
|
||||
|
||||
mount / -o remount,rw
|
||||
|
||||
resize2fs $ROOT_PART_DEV
|
||||
|
||||
if ! systemd-machine-id-setup; then
|
||||
FAIL_REASON="systemd-machine-id-setup failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! ssh-keygen -A; then
|
||||
FAIL_REASON="ssh host key generation failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo start > /etc/hostname
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sys /sys
|
||||
mount -t tmpfs tmp /run
|
||||
mkdir -p /run/systemd
|
||||
mount /boot
|
||||
mount / -o remount,ro
|
||||
|
||||
beep
|
||||
|
||||
if main; then
|
||||
sed -i 's| init=/usr/lib/startos/scripts/init_resize\.sh| boot=embassy|' /boot/cmdline.txt
|
||||
echo "Resized root filesystem. Rebooting in 5 seconds..."
|
||||
sleep 5
|
||||
else
|
||||
echo -e "Could not expand filesystem.\n${FAIL_REASON}"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
sync
|
||||
|
||||
umount /boot
|
||||
|
||||
reboot -f
|
||||
@@ -0,0 +1 @@
|
||||
usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=embassy
|
||||
@@ -0,0 +1,86 @@
|
||||
# For more options and information see
|
||||
# http://rpf.io/configtxt
|
||||
# Some settings may impact device functionality. See link above for details
|
||||
|
||||
# uncomment if you get no picture on HDMI for a default "safe" mode
|
||||
#hdmi_safe=1
|
||||
|
||||
# uncomment the following to adjust overscan. Use positive numbers if console
|
||||
# goes off screen, and negative if there is too much border
|
||||
#overscan_left=16
|
||||
#overscan_right=16
|
||||
#overscan_top=16
|
||||
#overscan_bottom=16
|
||||
|
||||
# uncomment to force a console size. By default it will be display's size minus
|
||||
# overscan.
|
||||
#framebuffer_width=1280
|
||||
#framebuffer_height=720
|
||||
|
||||
# uncomment if hdmi display is not detected and composite is being output
|
||||
#hdmi_force_hotplug=1
|
||||
|
||||
# uncomment to force a specific HDMI mode (this will force VGA)
|
||||
#hdmi_group=1
|
||||
#hdmi_mode=1
|
||||
|
||||
# uncomment to force a HDMI mode rather than DVI. This can make audio work in
|
||||
# DMT (computer monitor) modes
|
||||
#hdmi_drive=2
|
||||
|
||||
# uncomment to increase signal to HDMI, if you have interference, blanking, or
|
||||
# no display
|
||||
#config_hdmi_boost=4
|
||||
|
||||
# uncomment for composite PAL
|
||||
#sdtv_mode=2
|
||||
|
||||
#uncomment to overclock the arm. 700 MHz is the default.
|
||||
#arm_freq=800
|
||||
|
||||
# Uncomment some or all of these to enable the optional hardware interfaces
|
||||
#dtparam=i2c_arm=on
|
||||
#dtparam=i2s=on
|
||||
#dtparam=spi=on
|
||||
|
||||
# Uncomment this to enable infrared communication.
|
||||
#dtoverlay=gpio-ir,gpio_pin=17
|
||||
#dtoverlay=gpio-ir-tx,gpio_pin=18
|
||||
|
||||
# Additional overlays and parameters are documented /boot/overlays/README
|
||||
|
||||
# Enable audio (loads snd_bcm2835)
|
||||
dtparam=audio=on
|
||||
|
||||
# Automatically load overlays for detected cameras
|
||||
camera_auto_detect=1
|
||||
|
||||
# Automatically load overlays for detected DSI displays
|
||||
display_auto_detect=1
|
||||
|
||||
# Enable DRM VC4 V3D driver
|
||||
dtoverlay=vc4-kms-v3d
|
||||
max_framebuffers=2
|
||||
|
||||
# Run in 64-bit mode
|
||||
arm_64bit=1
|
||||
|
||||
# Disable compensation for displays with overscan
|
||||
disable_overscan=1
|
||||
|
||||
[cm4]
|
||||
# Enable host mode on the 2711 built-in XHCI USB controller.
|
||||
# This line should be removed if the legacy DWC2 controller is required
|
||||
# (e.g. for USB device mode) or if USB support is not required.
|
||||
otg_mode=1
|
||||
|
||||
[all]
|
||||
|
||||
[pi4]
|
||||
# Run as fast as firmware / board allows
|
||||
arm_boost=1
|
||||
|
||||
[all]
|
||||
gpu_mem=16
|
||||
dtoverlay=pwm-2chan,disable-bt
|
||||
initramfs initrd.img-6.1.21-v8+
|
||||
@@ -0,0 +1,6 @@
|
||||
os-partitions:
|
||||
boot: /dev/mmcblk0p1
|
||||
root: /dev/mmcblk0p2
|
||||
ethernet-interface: end0
|
||||
wifi-interface: wlan0
|
||||
disable-encryption: true
|
||||
@@ -0,0 +1 @@
|
||||
options cfg80211 ieee80211_regdom=US
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------
|
||||
# extract-ikconfig - Extract the .config file from a kernel image
|
||||
#
|
||||
# This will only work when the kernel was compiled with CONFIG_IKCONFIG.
|
||||
#
|
||||
# The obscure use of the "tr" filter is to work around older versions of
|
||||
# "grep" that report the byte offset of the line instead of the pattern.
|
||||
#
|
||||
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
|
||||
# Licensed under the terms of the GNU General Public License.
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
cf1='IKCFG_ST\037\213\010'
|
||||
cf2='0123456789'
|
||||
|
||||
dump_config()
|
||||
{
|
||||
if pos=`tr "$cf1\n$cf2" "\n$cf2=" < "$1" | grep -abo "^$cf2"`
|
||||
then
|
||||
pos=${pos%%:*}
|
||||
tail -c+$(($pos+8)) "$1" | zcat > $tmp1 2> /dev/null
|
||||
if [ $? != 1 ]
|
||||
then # exit status must be 0 or 2 (trailing garbage warning)
|
||||
cat $tmp1
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
try_decompress()
|
||||
{
|
||||
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
|
||||
do
|
||||
pos=${pos%%:*}
|
||||
tail -c+$pos "$img" | $3 > $tmp2 2> /dev/null
|
||||
dump_config $tmp2
|
||||
done
|
||||
}
|
||||
|
||||
# Check invocation:
|
||||
me=${0##*/}
|
||||
img=$1
|
||||
if [ $# -ne 1 -o ! -s "$img" ]
|
||||
then
|
||||
echo "Usage: $me <kernel-image>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Prepare temp files:
|
||||
tmp1=/tmp/ikconfig$$.1
|
||||
tmp2=/tmp/ikconfig$$.2
|
||||
trap "rm -f $tmp1 $tmp2" 0
|
||||
|
||||
# Initial attempt for uncompressed images or objects:
|
||||
dump_config "$img"
|
||||
|
||||
# That didn't work, so retry after decompression.
|
||||
try_decompress '\037\213\010' xy gunzip
|
||||
try_decompress '\3757zXZ\000' abcde unxz
|
||||
try_decompress 'BZh' xy bunzip2
|
||||
try_decompress '\135\0\0\0' xxx unlzma
|
||||
try_decompress '\211\114\132' xy 'lzop -d'
|
||||
try_decompress '\002\041\114\030' xyy 'lz4 -d -l'
|
||||
try_decompress '\050\265\057\375' xxx unzstd
|
||||
|
||||
# Bail out:
|
||||
echo "$me: Cannot find kernel config." >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,49 @@
|
||||
# Build Scripts
|
||||
|
||||
Helper scripts for building Archipelago OS images.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `build-backend.sh`
|
||||
Compiles the Archipelago Rust backend binary.
|
||||
- Output: `../build/backend/archipelago`
|
||||
- Requires: Rust toolchain (or Docker)
|
||||
- Builds for Linux x86_64
|
||||
|
||||
### `build-frontend.sh`
|
||||
Builds the Vue.js frontend for production.
|
||||
- Output: `../build/frontend/`
|
||||
- Requires: Node.js 18+, npm
|
||||
- Builds static files for serving
|
||||
|
||||
### `convert-iso-to-disk.sh`
|
||||
Converts ISO image to raw disk image.
|
||||
- Input: ISO file
|
||||
- Output: `.img` file ready for `dd`
|
||||
- Creates partition layout (EFI + root)
|
||||
|
||||
### `check-dependencies.sh`
|
||||
Checks if all build dependencies are available.
|
||||
- Checks: Rust, Node.js, Docker, xorriso
|
||||
- Provides installation instructions
|
||||
- Non-blocking (warns but continues)
|
||||
|
||||
### `install-podman.sh`
|
||||
Installs Podman container runtime.
|
||||
- For use inside the target system
|
||||
- Configures rootless Podman
|
||||
|
||||
## Usage
|
||||
|
||||
These scripts are called automatically by the main build process. You can also run them manually for testing:
|
||||
|
||||
```bash
|
||||
# Build just the backend
|
||||
./scripts/build-backend.sh
|
||||
|
||||
# Build just the frontend
|
||||
./scripts/build-frontend.sh
|
||||
|
||||
# Check dependencies
|
||||
./scripts/check-dependencies.sh
|
||||
```
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Build Archipelago backend binary for Debian Linux
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BACKEND_DIR="$PROJECT_ROOT/core/archipelago"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/../build/backend"
|
||||
|
||||
echo "🔨 Building Archipelago backend..."
|
||||
echo " Source: $BACKEND_DIR"
|
||||
echo " Output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Check if we should use Docker
|
||||
USE_DOCKER=false
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
USE_DOCKER=true
|
||||
echo "🍎 macOS detected - using Docker for Linux build"
|
||||
elif ! command -v rustc >/dev/null 2>&1; then
|
||||
USE_DOCKER=true
|
||||
echo "⚠️ Rust not found - using Docker"
|
||||
fi
|
||||
|
||||
if [ "$USE_DOCKER" = true ]; then
|
||||
echo "🐳 Building in Docker container..."
|
||||
docker run --rm \
|
||||
-v "$PROJECT_ROOT:/workspace" \
|
||||
-v "$OUTPUT_DIR:/output" \
|
||||
-w /workspace/core/archipelago \
|
||||
rust:trixie \
|
||||
sh -c '
|
||||
echo "📦 Installing build dependencies..."
|
||||
apt-get update && apt-get install -y pkg-config libssl-dev
|
||||
|
||||
echo "🔨 Building Archipelago backend..."
|
||||
cargo build --release
|
||||
|
||||
echo "📋 Copying binary to output..."
|
||||
cp ../target/release/archipelago /output/
|
||||
|
||||
echo "✅ Build complete!"
|
||||
ls -lh /output/archipelago
|
||||
'
|
||||
else
|
||||
# Native Linux build
|
||||
echo "🐧 Building natively..."
|
||||
cd "$BACKEND_DIR"
|
||||
cargo build --release
|
||||
|
||||
cp "../target/release/archipelago" "$OUTPUT_DIR/archipelago"
|
||||
fi
|
||||
|
||||
# Strip binary for smaller size
|
||||
if [ -f "$OUTPUT_DIR/archipelago" ]; then
|
||||
if command -v strip >/dev/null 2>&1 && [[ "$OSTYPE" != "darwin"* ]]; then
|
||||
strip "$OUTPUT_DIR/archipelago"
|
||||
fi
|
||||
echo ""
|
||||
echo "✅ Backend built: $OUTPUT_DIR/archipelago"
|
||||
ls -lh "$OUTPUT_DIR/archipelago"
|
||||
else
|
||||
echo "❌ Build failed - binary not found"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
# Build Archipelago frontend for production
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
FRONTEND_DIR="$PROJECT_ROOT/neode-ui"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/../build/frontend"
|
||||
|
||||
echo "🎨 Building Archipelago frontend..."
|
||||
echo " Source: $FRONTEND_DIR"
|
||||
echo " Output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Check if Node.js is installed
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "❌ Node.js not found. Please install Node.js 18+"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Install dependencies if needed
|
||||
cd "$FRONTEND_DIR"
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing frontend dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Build frontend
|
||||
echo "🔨 Building frontend..."
|
||||
DOCKER_BUILD=true npm run build || npm run build
|
||||
|
||||
# Copy built files
|
||||
if [ -d "dist" ]; then
|
||||
cp -r dist/* "$OUTPUT_DIR/"
|
||||
elif [ -d "../web/dist/neode-ui" ]; then
|
||||
cp -r ../web/dist/neode-ui/* "$OUTPUT_DIR/"
|
||||
else
|
||||
echo "❌ Build output not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Frontend built: $OUTPUT_DIR"
|
||||
du -sh "$OUTPUT_DIR"
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Check build dependencies and provide installation instructions
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking build dependencies..."
|
||||
echo ""
|
||||
|
||||
MISSING_DEPS=0
|
||||
|
||||
# Check Rust
|
||||
if command -v rustc >/dev/null 2>&1; then
|
||||
echo "✅ Rust: $(rustc --version)"
|
||||
else
|
||||
echo "❌ Rust: Not found"
|
||||
echo " Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
fi
|
||||
|
||||
# Check Node.js
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
NODE_VERSION=$(node --version)
|
||||
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1 | tr -d 'v')
|
||||
if [ "$NODE_MAJOR" -ge 18 ]; then
|
||||
echo "✅ Node.js: $NODE_VERSION"
|
||||
else
|
||||
echo "⚠️ Node.js: $NODE_VERSION (need 18+)"
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
fi
|
||||
else
|
||||
echo "❌ Node.js: Not found"
|
||||
echo " Install: https://nodejs.org/"
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
fi
|
||||
|
||||
# Check Docker
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo "✅ Docker: $(docker --version)"
|
||||
else
|
||||
echo "⚠️ Docker: Installed but daemon not running"
|
||||
echo " Start Docker Desktop or: sudo systemctl start docker"
|
||||
fi
|
||||
else
|
||||
echo "❌ Docker: Not found"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo " Install: https://www.docker.com/products/docker-desktop"
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
else
|
||||
echo " Install: https://docs.docker.com/get-docker/"
|
||||
echo " (Optional on Linux if building natively)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check xorriso (for ISO creation)
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
echo "✅ xorriso: Installed"
|
||||
else
|
||||
echo "❌ xorriso: Not found (needed for ISO creation)"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo " Install: brew install xorriso"
|
||||
else
|
||||
echo " Install: apt-get install xorriso"
|
||||
fi
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
fi
|
||||
|
||||
# Check 7z (for ISO extraction)
|
||||
if command -v 7z >/dev/null 2>&1; then
|
||||
echo "✅ 7z: Installed"
|
||||
else
|
||||
echo "❌ 7z: Not found (needed for ISO extraction)"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo " Install: brew install p7zip"
|
||||
else
|
||||
echo " Install: apt-get install p7zip-full"
|
||||
fi
|
||||
MISSING_DEPS=$((MISSING_DEPS + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ $MISSING_DEPS -eq 0 ]; then
|
||||
echo "✅ All dependencies satisfied!"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Missing $MISSING_DEPS dependency/dependencies"
|
||||
echo " Please install missing dependencies before building"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# Podman Installation and Configuration Script for Archipelago
|
||||
# Configures Podman for rootless operation
|
||||
|
||||
set -e
|
||||
|
||||
echo "🐳 Configuring Podman for rootless operation..."
|
||||
|
||||
if ! command -v catatonit >/dev/null 2>&1; then
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update || true
|
||||
apt-get install -y catatonit || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y catatonit || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add catatonit || true
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v catatonit >/dev/null 2>&1 || echo "WARNING: catatonit not installed; Podman init-enabled containers may fail"
|
||||
|
||||
# Ensure archipelago user exists
|
||||
if ! id "archipelago" &>/dev/null; then
|
||||
echo "Creating archipelago user..."
|
||||
adduser -D -s /bin/bash archipelago
|
||||
fi
|
||||
|
||||
# Create Podman configuration directories
|
||||
mkdir -p /home/archipelago/.config/containers
|
||||
mkdir -p /home/archipelago/.local/share/containers/storage
|
||||
|
||||
# Configure storage
|
||||
cat > /home/archipelago/.config/containers/storage.conf <<EOF
|
||||
[storage]
|
||||
driver = "overlay"
|
||||
runroot = "/run/user/$(id -u archipelago)/containers"
|
||||
graphroot = "/home/archipelago/.local/share/containers/storage"
|
||||
EOF
|
||||
|
||||
# Configure registries (use Docker Hub and quay.io)
|
||||
mkdir -p /home/archipelago/.config/containers/registries.conf.d
|
||||
cat > /home/archipelago/.config/containers/registries.conf <<EOF
|
||||
unqualified-search-registries = ["docker.io", "ghcr.io", "quay.io", "146.59.87.168:3000"]
|
||||
|
||||
[[registry]]
|
||||
location = "146.59.87.168:3000"
|
||||
insecure = true
|
||||
EOF
|
||||
|
||||
# Set up subuid and subgid for rootless containers
|
||||
if ! grep -q "^archipelago:" /etc/subuid; then
|
||||
echo "archipelago:100000:65536" >> /etc/subuid
|
||||
fi
|
||||
|
||||
if ! grep -q "^archipelago:" /etc/subgid; then
|
||||
echo "archipelago:100000:65536" >> /etc/subgid
|
||||
fi
|
||||
|
||||
# Create systemd user service directory
|
||||
mkdir -p /home/archipelago/.config/systemd/user
|
||||
|
||||
# Enable lingering for archipelago user (allows user services to run without login)
|
||||
loginctl enable-linger archipelago || true
|
||||
|
||||
# Ensure /run/user/1000 exists for podman socket
|
||||
mkdir -p /run/user/1000
|
||||
chown archipelago:archipelago /run/user/1000
|
||||
chmod 700 /run/user/1000
|
||||
|
||||
# Enable podman API socket for archipelago user (backend connects via this)
|
||||
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user enable podman.socket" || true
|
||||
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start podman.socket" || true
|
||||
|
||||
# Set proper permissions
|
||||
chown -R archipelago:archipelago /home/archipelago/.config
|
||||
chown -R archipelago:archipelago /home/archipelago/.local
|
||||
|
||||
echo "✅ Podman configuration complete!"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Sync configuration files from live server to ISO build
|
||||
#
|
||||
# Usage: ./sync-from-live.sh [target-host]
|
||||
#
|
||||
# This script captures system configuration from the live development
|
||||
# server and saves it to the image-recipe/configs/ directory for
|
||||
# inclusion in future ISO builds.
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
TARGET_HOST="${1:-archipelago@192.168.1.228}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="$SCRIPT_DIR/configs"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Syncing Configurations from Live Server ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Target: $TARGET_HOST"
|
||||
echo "Output: $CONFIG_DIR"
|
||||
echo ""
|
||||
|
||||
# Ensure configs directory exists
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Sync systemd service
|
||||
echo "📋 Capturing systemd service..."
|
||||
ssh "$TARGET_HOST" 'sudo cat /etc/systemd/system/archipelago.service' > "$CONFIG_DIR/archipelago.service"
|
||||
echo " ✅ Saved to configs/archipelago.service"
|
||||
|
||||
# Sync nginx configuration
|
||||
echo "📋 Capturing nginx configuration..."
|
||||
ssh "$TARGET_HOST" 'sudo cat /etc/nginx/sites-available/archipelago' > "$CONFIG_DIR/nginx-archipelago.conf"
|
||||
echo " ✅ Saved to configs/nginx-archipelago.conf"
|
||||
|
||||
# Sync logrotate if it exists
|
||||
if ssh "$TARGET_HOST" 'sudo test -f /etc/logrotate.d/archipelago'; then
|
||||
echo "📋 Capturing logrotate configuration..."
|
||||
ssh "$TARGET_HOST" 'sudo cat /etc/logrotate.d/archipelago' > "$CONFIG_DIR/logrotate.conf"
|
||||
echo " ✅ Saved to configs/logrotate.conf"
|
||||
fi
|
||||
|
||||
# Check for custom scripts
|
||||
echo ""
|
||||
echo "📋 Checking for custom scripts..."
|
||||
if ssh "$TARGET_HOST" 'sudo test -d /opt/archipelago/scripts'; then
|
||||
SCRIPT_COUNT=$(ssh "$TARGET_HOST" 'sudo ls /opt/archipelago/scripts/ 2>/dev/null | wc -l' | tr -d ' ')
|
||||
if [ "$SCRIPT_COUNT" -gt 0 ]; then
|
||||
echo " ⚠️ Found $SCRIPT_COUNT script(s) in /opt/archipelago/scripts/"
|
||||
echo " Review and manually sync if needed"
|
||||
ssh "$TARGET_HOST" 'sudo ls -lh /opt/archipelago/scripts/'
|
||||
else
|
||||
echo " ✅ No custom scripts found"
|
||||
fi
|
||||
else
|
||||
echo " ✅ No custom scripts directory"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Sync Complete! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Configuration files captured:"
|
||||
ls -lh "$CONFIG_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review the captured configurations"
|
||||
echo " 2. Build backend: ./scripts/build-backend.sh"
|
||||
echo " 3. Build frontend: ./scripts/build-frontend.sh"
|
||||
echo " 4. Update integration script to use these configs"
|
||||
echo " 5. Build ISO: ./build-debian-iso.sh"
|
||||
echo ""
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Write Archipelago ISO to USB using dd
|
||||
#
|
||||
# Usage: ./write-usb-dd.sh /dev/diskN
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /dev/diskN"
|
||||
echo ""
|
||||
echo "Available disks:"
|
||||
diskutil list external
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USB_DISK="$1"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ISO_FILE="${ARCHIPELAGO_ISO:-}"
|
||||
if [ -z "$ISO_FILE" ]; then
|
||||
ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-x86_64.iso"
|
||||
[ -f "$ISO_FILE" ] || ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-unbundled-x86_64.iso"
|
||||
fi
|
||||
|
||||
if [ ! -f "$ISO_FILE" ]; then
|
||||
echo "❌ ISO not found: $ISO_FILE"
|
||||
echo ""
|
||||
echo "Build the ISO first with: ./build-debian-iso.sh"
|
||||
echo "Or set ARCHIPELAGO_ISO=/path/to/archipelago-installer-x86_64.iso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get raw disk for faster writes
|
||||
RAW_DISK="${USB_DISK/disk/rdisk}"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ Write Archipelago ISO to USB ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "⚠️ WARNING: This will COMPLETELY ERASE $USB_DISK"
|
||||
echo ""
|
||||
echo "📀 ISO: $(basename "$ISO_FILE")"
|
||||
echo "💾 USB: $USB_DISK (raw: $RAW_DISK)"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to cancel, or Enter to continue..."
|
||||
read
|
||||
|
||||
echo "🔓 Unmounting USB..."
|
||||
diskutil unmountDisk "$USB_DISK" || true
|
||||
|
||||
echo ""
|
||||
echo "📝 Writing ISO with dd (this may take a few minutes)..."
|
||||
echo " Using raw disk $RAW_DISK for faster write..."
|
||||
echo ""
|
||||
|
||||
# Use dd to write the ISO directly
|
||||
sudo dd if="$ISO_FILE" of="$RAW_DISK" bs=4m status=progress
|
||||
|
||||
echo ""
|
||||
echo "🔄 Syncing..."
|
||||
sync
|
||||
|
||||
echo ""
|
||||
echo "✅ Done! USB is ready."
|
||||
echo ""
|
||||
echo "Now:"
|
||||
echo " 1. Eject the USB safely: diskutil eject $USB_DISK"
|
||||
echo " 2. Insert into target machine"
|
||||
echo " 3. Boot from USB (F12 or similar for boot menu)"
|
||||
echo ""
|
||||
echo "Default login (live mode):"
|
||||
echo " Username: user"
|
||||
echo " Password: live"
|
||||
Reference in New Issue
Block a user