feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix: - Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was the root cause of "machine doesn't pick up the USB". Branding: - Custom GRUB background: pixel-art floating island (1024x574) - Archipelago pixel-art logo for Plymouth boot splash - GRUB theme: dark background, orange selected item, no broken font refs - Plymouth theme: script-based with progress bar, LUKS prompt support - Plymouth + splash added to target rootfs packages - GRUB theme installed on both installer ISO and target system - Serial console (ttyS0) added to kernel params for QEMU debugging CI improvements: - Smoke test step: mounts ISO, verifies all critical files, checks initrd has live-boot, confirms boot=live in grub.cfg. Fails build before copying to Builds if any check fails. Dev workflow: - dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU (~10 seconds vs 20 min full rebuild) - generate-grub-background.py: procedural cyberpunk background generator - generate-plymouth-logo.py: procedural logo generator - Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging Build: - Simplified live-boot install (clean chroot, no complex fallbacks) - Static branding images preferred, generators as fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
93d05970e1
commit
436f337a13
@ -54,6 +54,61 @@ jobs:
|
||||
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: Copy to Builds
|
||||
run: |
|
||||
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
|
||||
|
||||
332
image-recipe/branding/generate-grub-background.py
Normal file
332
image-recipe/branding/generate-grub-background.py
Normal file
@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Archipelago GRUB boot background — 80s pixel cyberpunk aesthetic.
|
||||
|
||||
Outputs a 1024x768 PNG with:
|
||||
- Near-black background with subtle radial gradient
|
||||
- Scanline overlay (CRT effect)
|
||||
- Pixel-art "A" logo (from Archipelago SVG) rendered in neon orange
|
||||
- Neon glow effect around the logo
|
||||
- Retro grid lines at the bottom (Tron-style horizon)
|
||||
- Subtle vignette
|
||||
|
||||
Uses only PIL (Pillow) — no external dependencies.
|
||||
"""
|
||||
import struct
|
||||
import zlib
|
||||
import math
|
||||
import sys
|
||||
import os
|
||||
|
||||
W, H = 1024, 768
|
||||
|
||||
# Archipelago brand colors
|
||||
BG_DARK = (5, 5, 10) # Near-black with blue tint
|
||||
BG_MID = (10, 10, 18) # Slightly lighter center
|
||||
ORANGE = (251, 146, 60) # #fb923c — primary accent
|
||||
ORANGE_DIM = (180, 100, 30) # Dimmed orange for glow
|
||||
CYAN = (60, 200, 220) # Cyberpunk accent
|
||||
MAGENTA = (180, 60, 180) # Cyberpunk accent 2
|
||||
GRID_COLOR = (30, 60, 80) # Subtle teal grid
|
||||
SCANLINE = (0, 0, 0) # Black scanlines
|
||||
|
||||
# The pixel-art "a" (lowercase) from the Archipelago SVG favicon
|
||||
# Matched to the actual SVG path — it's a blocky pixel-art lowercase "a"
|
||||
LOGO_A = [
|
||||
[0, 1, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 1, 1, 1],
|
||||
[1, 0, 0, 0, 0, 1],
|
||||
[1, 0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 1, 1, 1],
|
||||
]
|
||||
|
||||
# Pixel-art text: "archipelago" — 5-pixel-high bitmap font (lowercase)
|
||||
PIXEL_CHARS = {
|
||||
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
||||
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
|
||||
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
|
||||
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
|
||||
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
|
||||
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
|
||||
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
|
||||
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
|
||||
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
|
||||
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
|
||||
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
|
||||
}
|
||||
LOGO_TEXT = "archipelago"
|
||||
|
||||
|
||||
def lerp_color(c1, c2, t):
|
||||
"""Linearly interpolate between two RGB colors."""
|
||||
return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2))
|
||||
|
||||
|
||||
def distance(x1, y1, x2, y2):
|
||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
|
||||
|
||||
def create_png(width, height, pixels):
|
||||
"""Create a PNG file from raw RGB pixel data without PIL."""
|
||||
|
||||
def make_chunk(chunk_type, data):
|
||||
chunk = chunk_type + data
|
||||
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
|
||||
|
||||
# PNG signature
|
||||
signature = b'\x89PNG\r\n\x1a\n'
|
||||
|
||||
# IHDR
|
||||
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0) # 8-bit RGB
|
||||
ihdr = make_chunk(b'IHDR', ihdr_data)
|
||||
|
||||
# IDAT — raw pixel data with filter byte per row
|
||||
raw_data = bytearray()
|
||||
for y in range(height):
|
||||
raw_data.append(0) # filter: none
|
||||
offset = y * width * 3
|
||||
raw_data.extend(pixels[offset:offset + width * 3])
|
||||
|
||||
compressed = zlib.compress(bytes(raw_data), 9)
|
||||
idat = make_chunk(b'IDAT', compressed)
|
||||
|
||||
# IEND
|
||||
iend = make_chunk(b'IEND', b'')
|
||||
|
||||
return signature + ihdr + idat + iend
|
||||
|
||||
|
||||
def generate():
|
||||
# Create pixel buffer (RGB, row-major)
|
||||
pixels = bytearray(W * H * 3)
|
||||
|
||||
cx, cy = W // 2, H // 2 - 40 # Center, slightly above middle
|
||||
|
||||
# --- Background: radial gradient ---
|
||||
max_dist = distance(0, 0, cx, cy)
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
d = distance(x, y, cx, cy) / max_dist
|
||||
d = min(d, 1.0)
|
||||
bg = lerp_color(BG_MID, BG_DARK, d * d) # Quadratic falloff
|
||||
|
||||
# Vignette — darken edges
|
||||
vx = abs(x - cx) / (W / 2)
|
||||
vy = abs(y - cy) / (H / 2)
|
||||
vignette = max(0, 1.0 - (vx * vx + vy * vy) * 0.4)
|
||||
r = int(bg[0] * vignette)
|
||||
g = int(bg[1] * vignette)
|
||||
b = int(bg[2] * vignette)
|
||||
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = max(0, min(255, r))
|
||||
pixels[idx + 1] = max(0, min(255, g))
|
||||
pixels[idx + 2] = max(0, min(255, b))
|
||||
|
||||
# --- Retro grid (bottom third) ---
|
||||
horizon_y = H * 2 // 3
|
||||
for y in range(horizon_y, H):
|
||||
depth = (y - horizon_y) / (H - horizon_y) # 0 at horizon, 1 at bottom
|
||||
|
||||
# Horizontal grid lines — spacing decreases with perspective
|
||||
grid_spacing = max(4, int(40 * (1.0 - depth * 0.8)))
|
||||
is_hline = ((y - horizon_y) % grid_spacing) < 1
|
||||
|
||||
for x in range(W):
|
||||
# Vertical grid lines — converge toward center
|
||||
spread = 0.3 + depth * 0.7 # Lines spread more toward bottom
|
||||
grid_x = (x - cx) / (spread * W / 2) * 12
|
||||
is_vline = abs(grid_x - round(grid_x)) < 0.04
|
||||
|
||||
if is_hline or is_vline:
|
||||
alpha = 0.15 + depth * 0.25 # Brighter closer to viewer
|
||||
idx = (y * W + x) * 3
|
||||
for c in range(3):
|
||||
old = pixels[idx + c]
|
||||
pixels[idx + c] = min(255, int(old + GRID_COLOR[c] * alpha))
|
||||
|
||||
# --- Horizon glow line ---
|
||||
for x in range(W):
|
||||
dx = abs(x - cx) / (W / 2)
|
||||
intensity = max(0, 1.0 - dx * 1.5) * 0.4
|
||||
for dy in range(-2, 3):
|
||||
y = horizon_y + dy
|
||||
if 0 <= y < H:
|
||||
falloff = 1.0 - abs(dy) / 3.0
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * intensity * falloff))
|
||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * intensity * falloff))
|
||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * intensity * falloff))
|
||||
|
||||
# --- Pixel-art "A" logo ---
|
||||
logo_rows = len(LOGO_A)
|
||||
logo_cols = len(LOGO_A[0])
|
||||
pixel_size = 14
|
||||
logo_w = logo_cols * pixel_size
|
||||
logo_h = logo_rows * pixel_size
|
||||
logo_x = cx - logo_w // 2
|
||||
logo_y = 80
|
||||
|
||||
# Glow behind logo
|
||||
glow_radius = 90
|
||||
for y in range(max(0, logo_y - glow_radius), min(H, logo_y + logo_h + glow_radius)):
|
||||
for x in range(max(0, logo_x - glow_radius), min(W, logo_x + logo_w + glow_radius)):
|
||||
# Distance to logo bounding box
|
||||
dx = max(0, logo_x - x, x - (logo_x + logo_w))
|
||||
dy = max(0, logo_y - y, y - (logo_y + logo_h))
|
||||
d = math.sqrt(dx * dx + dy * dy)
|
||||
if d < glow_radius:
|
||||
alpha = (1.0 - d / glow_radius) ** 2 * 0.15
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = min(255, int(pixels[idx] + ORANGE[0] * alpha))
|
||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + ORANGE[1] * alpha))
|
||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + ORANGE[2] * alpha))
|
||||
|
||||
# Draw logo pixels with 3D depth/shadow effect
|
||||
shadow_offset = 3 # Pixel offset for 3D shadow
|
||||
for row in range(logo_rows):
|
||||
for col in range(logo_cols):
|
||||
if LOGO_A[row][col]:
|
||||
px = logo_x + col * pixel_size
|
||||
py = logo_y + row * pixel_size
|
||||
# Shadow layer (dark, offset down-right)
|
||||
for dy in range(pixel_size - 1):
|
||||
for dx in range(pixel_size - 1):
|
||||
x = px + dx + shadow_offset
|
||||
y = py + dy + shadow_offset
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = max(0, pixels[idx] - 5)
|
||||
pixels[idx + 1] = min(255, pixels[idx + 1] + 15)
|
||||
pixels[idx + 2] = min(255, pixels[idx + 2] + 20)
|
||||
# Main pixel with highlight gradient (brighter at top-left)
|
||||
for dy in range(pixel_size - 1):
|
||||
for dx in range(pixel_size - 1):
|
||||
x, y = px + dx, py + dy
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
# Gradient: top-left bright, bottom-right darker
|
||||
t = (dx + dy) / (2 * pixel_size)
|
||||
r = int(ORANGE[0] * (1.0 - t * 0.3))
|
||||
g = int(ORANGE[1] * (1.0 - t * 0.3))
|
||||
b = int(ORANGE[2] * (1.0 - t * 0.3))
|
||||
# Top-left highlight for 3D bevel
|
||||
if dx < 2 or dy < 2:
|
||||
r = min(255, r + 40)
|
||||
g = min(255, g + 30)
|
||||
b = min(255, b + 10)
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = r
|
||||
pixels[idx + 1] = g
|
||||
pixels[idx + 2] = b
|
||||
|
||||
# --- Pixel-art text "archipelago" below logo ---
|
||||
text_pixel = 4 # Smaller pixels for text
|
||||
text_gap = 2 # Gap between characters in pixels
|
||||
# Calculate total text width
|
||||
total_w = 0
|
||||
for ch in LOGO_TEXT:
|
||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
||||
total_w += len(char_data[0]) * text_pixel + text_gap
|
||||
total_w -= text_gap # No gap after last char
|
||||
|
||||
text_x = cx - total_w // 2
|
||||
text_y = logo_y + logo_h + 20 # Below the logo
|
||||
|
||||
cursor_x = text_x
|
||||
for ch in LOGO_TEXT:
|
||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
||||
char_h = len(char_data)
|
||||
char_w = len(char_data[0])
|
||||
for row in range(char_h):
|
||||
for col in range(char_w):
|
||||
if char_data[row][col]:
|
||||
px = cursor_x + col * text_pixel
|
||||
py = text_y + row * text_pixel
|
||||
for dy in range(text_pixel - 1):
|
||||
for dx in range(text_pixel - 1):
|
||||
x, y = px + dx, py + dy
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
idx = (y * W + x) * 3
|
||||
# Dimmer orange for text
|
||||
pixels[idx] = ORANGE_DIM[0]
|
||||
pixels[idx + 1] = ORANGE_DIM[1]
|
||||
pixels[idx + 2] = ORANGE_DIM[2]
|
||||
cursor_x += char_w * text_pixel + text_gap
|
||||
|
||||
# --- Decorative neon lines flanking the text ---
|
||||
line_y = text_y + 5 * text_pixel + 12
|
||||
line_w = total_w + 40
|
||||
line_x1 = cx - line_w // 2
|
||||
line_x2 = cx + line_w // 2
|
||||
for x in range(line_x1, line_x2):
|
||||
if 0 <= x < W:
|
||||
# Fade at edges
|
||||
edge_dist = min(x - line_x1, line_x2 - x)
|
||||
alpha = min(1.0, edge_dist / 30.0) * 0.5
|
||||
for dy in range(2):
|
||||
y = line_y + dy
|
||||
if 0 <= y < H:
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * alpha * 0.3))
|
||||
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * alpha * 0.3))
|
||||
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * alpha * 0.3))
|
||||
|
||||
# --- "self-sovereign bitcoin infrastructure" tagline ---
|
||||
TAG_CHARS = {
|
||||
's': [[0,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,0]],
|
||||
'f': [[0,1,1],[1,0,0],[1,1,0],[1,0,0],[1,0,0]],
|
||||
'-': [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
|
||||
'v': [[1,0,1],[1,0,1],[1,0,1],[0,1,0],[0,1,0]],
|
||||
'n': [[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1]],
|
||||
'b': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,1,1,0]],
|
||||
't': [[0,1,0],[1,1,1],[0,1,0],[0,1,0],[0,0,1]],
|
||||
'd': [[0,0,0,1],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
||||
'u': [[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,1]],
|
||||
}
|
||||
# Merge with existing chars
|
||||
all_chars = {**PIXEL_CHARS, **TAG_CHARS}
|
||||
tagline = "self-sovereign bitcoin node"
|
||||
tag_pixel = 3
|
||||
tag_gap = 2
|
||||
tag_total = sum(len(all_chars.get(c, all_chars[' '])[0]) * tag_pixel + tag_gap for c in tagline) - tag_gap
|
||||
tag_x = cx - tag_total // 2
|
||||
tag_y = line_y + 8
|
||||
tag_cursor = tag_x
|
||||
for ch in tagline:
|
||||
char_data = all_chars.get(ch, all_chars[' '])
|
||||
char_h = len(char_data)
|
||||
char_w = len(char_data[0])
|
||||
for row in range(char_h):
|
||||
for col in range(char_w):
|
||||
if char_data[row][col]:
|
||||
px = tag_cursor + col * tag_pixel
|
||||
py = tag_y + row * tag_pixel
|
||||
for dy in range(tag_pixel - 1):
|
||||
for dx in range(tag_pixel - 1):
|
||||
x, y = px + dx, py + dy
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = min(255, pixels[idx] + 40)
|
||||
pixels[idx + 1] = min(255, pixels[idx + 1] + 50)
|
||||
pixels[idx + 2] = min(255, pixels[idx + 2] + 55)
|
||||
tag_cursor += char_w * tag_pixel + tag_gap
|
||||
|
||||
# --- Scanlines (every other row, subtle) ---
|
||||
for y in range(0, H, 2):
|
||||
for x in range(W):
|
||||
idx = (y * W + x) * 3
|
||||
pixels[idx] = int(pixels[idx] * 0.92)
|
||||
pixels[idx + 1] = int(pixels[idx + 1] * 0.92)
|
||||
pixels[idx + 2] = int(pixels[idx + 2] * 0.92)
|
||||
|
||||
# --- Generate PNG ---
|
||||
png_data = create_png(W, H, bytes(pixels))
|
||||
return png_data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else 'background.png'
|
||||
png_data = generate()
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(png_data)
|
||||
print(f'Generated {out_path} ({len(png_data)} bytes)')
|
||||
149
image-recipe/branding/generate-plymouth-logo.py
Normal file
149
image-recipe/branding/generate-plymouth-logo.py
Normal file
@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the Archipelago Plymouth boot logo — pixel-art 'a' with neon glow.
|
||||
|
||||
Outputs a 256x256 PNG with transparent background (RGBA).
|
||||
"""
|
||||
import struct
|
||||
import zlib
|
||||
import math
|
||||
import sys
|
||||
|
||||
W, H = 256, 256
|
||||
ORANGE = (251, 146, 60)
|
||||
ORANGE_BRIGHT = (255, 180, 100)
|
||||
|
||||
# Lowercase pixel-art "a" — 6x6 grid
|
||||
LOGO_A = [
|
||||
[0, 1, 1, 1, 1, 0],
|
||||
[0, 0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 1, 1, 1],
|
||||
[1, 0, 0, 0, 0, 1],
|
||||
[1, 0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 1, 1, 1],
|
||||
]
|
||||
|
||||
# "archipelago" text — same pixel font as GRUB generator
|
||||
PIXEL_CHARS = {
|
||||
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
|
||||
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
|
||||
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
|
||||
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
|
||||
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
|
||||
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
|
||||
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
|
||||
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
|
||||
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
|
||||
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
|
||||
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
|
||||
}
|
||||
|
||||
|
||||
def create_png_rgba(width, height, pixels):
|
||||
"""Create a PNG with RGBA pixel data."""
|
||||
def make_chunk(chunk_type, data):
|
||||
chunk = chunk_type + data
|
||||
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
|
||||
|
||||
signature = b'\x89PNG\r\n\x1a\n'
|
||||
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
|
||||
ihdr = make_chunk(b'IHDR', ihdr_data)
|
||||
|
||||
raw_data = bytearray()
|
||||
for y in range(height):
|
||||
raw_data.append(0)
|
||||
offset = y * width * 4
|
||||
raw_data.extend(pixels[offset:offset + width * 4])
|
||||
|
||||
compressed = zlib.compress(bytes(raw_data), 9)
|
||||
idat = make_chunk(b'IDAT', compressed)
|
||||
iend = make_chunk(b'IEND', b'')
|
||||
|
||||
return signature + ihdr + idat + iend
|
||||
|
||||
|
||||
def generate():
|
||||
pixels = bytearray(W * H * 4) # RGBA
|
||||
|
||||
cx, cy = W // 2, W // 2 - 30
|
||||
|
||||
logo_rows = len(LOGO_A)
|
||||
logo_cols = len(LOGO_A[0])
|
||||
pixel_size = 18
|
||||
logo_w = logo_cols * pixel_size
|
||||
logo_h = logo_rows * pixel_size
|
||||
logo_x = cx - logo_w // 2
|
||||
logo_y = 30
|
||||
|
||||
# Glow
|
||||
glow_radius = 60
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
dx = max(0, logo_x - x, x - (logo_x + logo_w))
|
||||
dy = max(0, logo_y - y, y - (logo_y + logo_h))
|
||||
d = math.sqrt(dx * dx + dy * dy)
|
||||
if d < glow_radius:
|
||||
alpha = (1.0 - d / glow_radius) ** 2 * 0.25
|
||||
idx = (y * W + x) * 4
|
||||
pixels[idx] = ORANGE[0]
|
||||
pixels[idx + 1] = ORANGE[1]
|
||||
pixels[idx + 2] = ORANGE[2]
|
||||
pixels[idx + 3] = int(alpha * 255)
|
||||
|
||||
# Logo pixels with 3D bevel
|
||||
for row in range(logo_rows):
|
||||
for col in range(logo_cols):
|
||||
if LOGO_A[row][col]:
|
||||
px = logo_x + col * pixel_size
|
||||
py = logo_y + row * pixel_size
|
||||
for dy in range(pixel_size - 1):
|
||||
for dx in range(pixel_size - 1):
|
||||
x, y = px + dx, py + dy
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
t = (dx + dy) / (2 * pixel_size)
|
||||
r = int(ORANGE[0] * (1.0 - t * 0.3))
|
||||
g = int(ORANGE[1] * (1.0 - t * 0.3))
|
||||
b = int(ORANGE[2] * (1.0 - t * 0.3))
|
||||
if dx < 2 or dy < 2:
|
||||
r = min(255, r + 40)
|
||||
g = min(255, g + 30)
|
||||
b = min(255, b + 10)
|
||||
idx = (y * W + x) * 4
|
||||
pixels[idx] = r
|
||||
pixels[idx + 1] = g
|
||||
pixels[idx + 2] = b
|
||||
pixels[idx + 3] = 255
|
||||
|
||||
# Text "archipelago" below logo
|
||||
text = "archipelago"
|
||||
text_pixel = 3
|
||||
text_gap = 2
|
||||
total_w = sum(len(PIXEL_CHARS.get(c, PIXEL_CHARS[' '])[0]) * text_pixel + text_gap for c in text) - text_gap
|
||||
text_x = cx - total_w // 2
|
||||
text_y = logo_y + logo_h + 16
|
||||
cursor = text_x
|
||||
for ch in text:
|
||||
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
|
||||
for row in range(len(char_data)):
|
||||
for col in range(len(char_data[0])):
|
||||
if char_data[row][col]:
|
||||
for dy in range(text_pixel - 1):
|
||||
for dx in range(text_pixel - 1):
|
||||
x = cursor + col * text_pixel + dx
|
||||
y = text_y + row * text_pixel + dy
|
||||
if 0 <= x < W and 0 <= y < H:
|
||||
idx = (y * W + x) * 4
|
||||
pixels[idx] = 180
|
||||
pixels[idx + 1] = 100
|
||||
pixels[idx + 2] = 30
|
||||
pixels[idx + 3] = 200
|
||||
cursor += len(char_data[0]) * text_pixel + text_gap
|
||||
|
||||
return create_png_rgba(W, H, bytes(pixels))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else 'logo.png'
|
||||
data = generate()
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(data)
|
||||
print(f'Generated {out_path} ({len(data)} bytes)')
|
||||
BIN
image-recipe/branding/grub-theme/background.png
Normal file
BIN
image-recipe/branding/grub-theme/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 675 KiB |
@ -4,6 +4,7 @@
|
||||
|
||||
title-text: ""
|
||||
desktop-color: "#0a0a0a"
|
||||
desktop-image: "background.png"
|
||||
|
||||
+ boot_menu {
|
||||
left = 25%
|
||||
|
||||
BIN
image-recipe/branding/isohdpfx.bin
Normal file
BIN
image-recipe/branding/isohdpfx.bin
Normal file
Binary file not shown.
@ -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
|
||||
109
image-recipe/branding/plymouth-theme/archipelago.script
Normal file
109
image-recipe/branding/plymouth-theme/archipelago.script
Normal file
@ -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);
|
||||
BIN
image-recipe/branding/plymouth-theme/logo.png
Normal file
BIN
image-recipe/branding/plymouth-theme/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@ -265,6 +265,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
unclutter \
|
||||
fonts-liberation \
|
||||
xfonts-base \
|
||||
plymouth \
|
||||
plymouth-themes \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@ -464,51 +466,27 @@ linux-image-${DEB_ARCH},grub-efi-${DEB_ARCH},grub-pc-bin,\
|
||||
pciutils,usbutils,less,nano \
|
||||
bookworm /installer http://deb.debian.org/debian
|
||||
|
||||
# Install live-boot separately — debootstrap minbase resolver cannot handle it.
|
||||
# Use apt-get with explicit root dir instead of chroot (avoids /proc issues in containers).
|
||||
# Install live-boot via chroot — debootstrap minbase resolver cannot handle it.
|
||||
# The chroot approach works (confirmed in CI run 90) — just needs proc/sys/dev mounts.
|
||||
echo " [container] Installing live-boot for squashfs root support..."
|
||||
cp /etc/resolv.conf /installer/etc/resolv.conf 2>/dev/null || true
|
||||
apt-get -o Dir=/installer -o Dir::State::status=/installer/var/lib/dpkg/status \
|
||||
-o Dir::Etc::SourceList=/installer/etc/apt/sources.list \
|
||||
-o Dir::Cache=/installer/var/cache/apt \
|
||||
update -qq 2>/dev/null || {
|
||||
# Fallback: copy host apt lists and install directly
|
||||
echo " [container] apt-get Dir method failed, using dpkg extraction..."
|
||||
apt-get update -qq
|
||||
apt-get download live-boot live-boot-initramfs-tools 2>/dev/null
|
||||
for deb in live-boot*.deb; do
|
||||
dpkg-deb -x "$deb" /installer/ 2>/dev/null || true
|
||||
done
|
||||
rm -f live-boot*.deb
|
||||
}
|
||||
# Try chroot install if apt-get Dir method populated the lists
|
||||
if [ -f /installer/var/lib/apt/lists/*_Packages 2>/dev/null ]; then
|
||||
mount --bind /proc /installer/proc 2>/dev/null || true
|
||||
mount --bind /sys /installer/sys 2>/dev/null || true
|
||||
mount --bind /dev /installer/dev 2>/dev/null || true
|
||||
chroot /installer apt-get install -y --no-install-recommends live-boot live-boot-initramfs-tools 2>/dev/null || true
|
||||
chroot /installer apt-get clean 2>/dev/null || true
|
||||
umount /installer/dev 2>/dev/null || true
|
||||
umount /installer/sys 2>/dev/null || true
|
||||
umount /installer/proc 2>/dev/null || true
|
||||
fi
|
||||
mount --bind /proc /installer/proc
|
||||
mount --bind /sys /installer/sys
|
||||
mount --bind /dev /installer/dev
|
||||
chroot /installer apt-get update -qq
|
||||
chroot /installer apt-get install -y --no-install-recommends live-boot live-boot-initramfs-tools
|
||||
chroot /installer apt-get clean
|
||||
umount /installer/dev 2>/dev/null || true
|
||||
umount /installer/sys 2>/dev/null || true
|
||||
umount /installer/proc 2>/dev/null || true
|
||||
|
||||
# Verify live-boot hooks are in place
|
||||
# Verify live-boot hooks are in place (scripts/live is a FILE not a directory)
|
||||
if [ -e /installer/usr/share/initramfs-tools/scripts/live ]; then
|
||||
echo " [container] live-boot initramfs hooks: OK"
|
||||
else
|
||||
echo " [container] WARNING: live-boot hooks missing, trying dpkg extraction..."
|
||||
apt-get download live-boot live-boot-initramfs-tools 2>/dev/null || true
|
||||
for deb in live-boot*.deb; do
|
||||
dpkg-deb -x "$deb" /installer/ 2>/dev/null || true
|
||||
done
|
||||
rm -f live-boot*.deb 2>/dev/null || true
|
||||
if [ -e /installer/usr/share/initramfs-tools/scripts/live ]; then
|
||||
echo " [container] live-boot hooks installed via dpkg extraction: OK"
|
||||
else
|
||||
echo " [container] FATAL: Could not install live-boot hooks!"
|
||||
exit 1
|
||||
fi
|
||||
echo " [container] FATAL: live-boot hooks not found after install!"
|
||||
ls -la /installer/usr/share/initramfs-tools/scripts/ 2>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " [container] Configuring installer environment..."
|
||||
@ -725,6 +703,16 @@ if [ -d "$WORK_DIR/grub-fonts" ]; then
|
||||
# Also copy unicode font for GRUB to load
|
||||
cp "$WORK_DIR/grub-fonts/dejavu_16.pf2" "$INSTALLER_ISO/boot/grub/font.pf2"
|
||||
fi
|
||||
# Copy GRUB background image (static asset or generate if missing)
|
||||
GRUB_BG="$SCRIPT_DIR/branding/grub-theme/background.png"
|
||||
if [ -f "$GRUB_BG" ]; then
|
||||
cp "$GRUB_BG" "$THEME_DST/background.png"
|
||||
echo " Installed GRUB background"
|
||||
elif [ -f "$SCRIPT_DIR/branding/generate-grub-background.py" ]; then
|
||||
echo " Generating GRUB background..."
|
||||
python3 "$SCRIPT_DIR/branding/generate-grub-background.py" "$THEME_DST/background.png" 2>/dev/null || \
|
||||
echo " WARNING: Could not generate GRUB background"
|
||||
fi
|
||||
|
||||
echo " Installer squashfs: $(du -h "$INSTALLER_ISO/live/filesystem.squashfs" | cut -f1)"
|
||||
echo " Kernel: $(du -h "$INSTALLER_ISO/live/vmlinuz" | cut -f1)"
|
||||
@ -858,6 +846,14 @@ if [ -d "$SCRIPT_DIR/../apps" ]; then
|
||||
cp -r "$SCRIPT_DIR/../apps" "$ARCH_DIR/"
|
||||
fi
|
||||
|
||||
# Copy Plymouth theme files for installation on target
|
||||
PLYMOUTH_SRC="$SCRIPT_DIR/branding/plymouth-theme"
|
||||
if [ -d "$PLYMOUTH_SRC" ]; then
|
||||
mkdir -p "$ARCH_DIR/plymouth-theme"
|
||||
cp "$PLYMOUTH_SRC/"* "$ARCH_DIR/plymouth-theme/"
|
||||
echo " Included Plymouth theme"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# STEP 3b: Bundle container images for offline installation
|
||||
# =============================================================================
|
||||
@ -2117,6 +2113,23 @@ if [ -d "$BOOT_MEDIA/boot/grub/themes/archipelago" ]; then
|
||||
echo " Installed Archipelago GRUB theme on target"
|
||||
fi
|
||||
|
||||
# Install Archipelago Plymouth theme on target system
|
||||
if [ -d "$BOOT_MEDIA/archipelago/plymouth-theme" ]; then
|
||||
PLYMOUTH_DIR="/mnt/target/usr/share/plymouth/themes/archipelago"
|
||||
mkdir -p "$PLYMOUTH_DIR"
|
||||
cp "$BOOT_MEDIA/archipelago/plymouth-theme/"* "$PLYMOUTH_DIR/"
|
||||
# Set as default Plymouth theme
|
||||
chroot /mnt/target plymouth-set-default-theme archipelago 2>/dev/null || \
|
||||
ln -sf /usr/share/plymouth/themes/archipelago/archipelago.plymouth \
|
||||
/mnt/target/etc/alternatives/default.plymouth 2>/dev/null || true
|
||||
# Enable splash in GRUB
|
||||
if ! grep -q "splash" /mnt/target/etc/default/grub; then
|
||||
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 splash"/' \
|
||||
/mnt/target/etc/default/grub 2>/dev/null || true
|
||||
fi
|
||||
echo " Installed Archipelago Plymouth theme on target"
|
||||
fi
|
||||
|
||||
# Regenerate initramfs — the one from Docker export is corrupt/incomplete
|
||||
# (Docker builds have limited /proc, /sys, /dev so initramfs generation fails silently)
|
||||
echo " Regenerating initramfs..."
|
||||
@ -2513,7 +2526,7 @@ else
|
||||
fi
|
||||
|
||||
menuentry "Install Archipelago" --hotkey=i {
|
||||
linux /live/vmlinuz boot=live components quiet
|
||||
linux /live/vmlinuz boot=live components quiet console=ttyS0,115200 console=tty0
|
||||
initrd /live/initrd.img
|
||||
}
|
||||
|
||||
@ -2546,7 +2559,7 @@ DEFAULT install
|
||||
LABEL install
|
||||
MENU LABEL Install Archipelago
|
||||
KERNEL /live/vmlinuz
|
||||
APPEND initrd=/live/initrd.img boot=live components quiet
|
||||
APPEND initrd=/live/initrd.img boot=live components quiet console=ttyS0,115200 console=tty0
|
||||
MENU DEFAULT
|
||||
|
||||
LABEL install-verbose
|
||||
@ -2573,8 +2586,13 @@ else
|
||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-installer-${ARCH}.iso"
|
||||
fi
|
||||
|
||||
# Use ISOLINUX isohdpfx.bin for hybrid USB boot (built in Step 2)
|
||||
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
||||
# Use the proven MBR code for hybrid USB boot
|
||||
# The ISOLINUX package's isohdpfx.bin (33 ed) doesn't boot on all hardware.
|
||||
# We ship the Debian Live MBR (45 52) which is known to work with Balena Etcher.
|
||||
ISOHDPFX="$SCRIPT_DIR/branding/isohdpfx.bin"
|
||||
if [ ! -f "$ISOHDPFX" ]; then
|
||||
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
||||
fi
|
||||
if [ ! -f "$ISOHDPFX" ]; then
|
||||
# Fallback to system-installed copy
|
||||
for path in \
|
||||
|
||||
173
image-recipe/dev-branding.sh
Executable file
173
image-recipe/dev-branding.sh
Executable file
@ -0,0 +1,173 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Quick-iterate on boot branding without rebuilding the ISO.
|
||||
#
|
||||
# Usage:
|
||||
# ./dev-branding.sh <base-iso>
|
||||
#
|
||||
# What it does:
|
||||
# 1. Regenerates GRUB background and Plymouth logo from Python scripts
|
||||
# 2. Extracts the existing ISO
|
||||
# 3. Swaps in updated branding files (theme, background, Plymouth)
|
||||
# 4. Repackages as a new ISO
|
||||
# 5. Boots it in QEMU for testing
|
||||
#
|
||||
# This takes ~10 seconds instead of 20 minutes.
|
||||
#
|
||||
# For design-only iteration (no QEMU boot):
|
||||
# python3 branding/generate-grub-background.py /tmp/grub-bg.png && open /tmp/grub-bg.png
|
||||
#
|
||||
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ISO="${1:-}"
|
||||
|
||||
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
|
||||
# Auto-detect latest dev ISO on Desktop
|
||||
ISO=$(ls -t ~/Desktop/archipelago-dev-*.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 "No ISO found. Provide a path or place one on Desktop/results."
|
||||
echo "Usage: $0 <base-iso>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="/tmp/archipelago-dev-branding"
|
||||
PATCHED="$SCRIPT_DIR/results/archipelago-dev-patched.iso"
|
||||
|
||||
echo "=== Archipelago Branding Dev ==="
|
||||
echo " Base ISO: $ISO"
|
||||
echo ""
|
||||
|
||||
# Step 1: Regenerate assets
|
||||
echo "[1/4] Generating assets..."
|
||||
python3 "$SCRIPT_DIR/branding/generate-grub-background.py" /tmp/grub-bg.png 2>/dev/null && \
|
||||
echo " GRUB background: OK" || echo " GRUB background: FAILED"
|
||||
python3 "$SCRIPT_DIR/branding/generate-plymouth-logo.py" /tmp/plymouth-logo.png 2>/dev/null && \
|
||||
echo " Plymouth logo: OK" || echo " Plymouth logo: FAILED"
|
||||
|
||||
# Also show the background for quick visual check
|
||||
if command -v open >/dev/null 2>&1; then
|
||||
open /tmp/grub-bg.png 2>/dev/null &
|
||||
fi
|
||||
|
||||
# Step 2: Extract ISO
|
||||
echo "[2/4] Extracting ISO..."
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"
|
||||
xorriso -osirrox on -indev "$ISO" -extract / "$WORK" 2>/dev/null || {
|
||||
# Fallback: mount + copy
|
||||
MNT=$(mktemp -d)
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
hdiutil attach "$ISO" -mountpoint "$MNT" -readonly -nobrowse 2>/dev/null
|
||||
else
|
||||
sudo mount -o loop,ro "$ISO" "$MNT"
|
||||
fi
|
||||
cp -a "$MNT"/* "$WORK/" 2>/dev/null || true
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
hdiutil detach "$MNT" 2>/dev/null
|
||||
else
|
||||
sudo umount "$MNT"
|
||||
fi
|
||||
rmdir "$MNT" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Step 3: Patch branding
|
||||
echo "[3/4] Patching branding..."
|
||||
THEME_DST="$WORK/boot/grub/themes/archipelago"
|
||||
mkdir -p "$THEME_DST"
|
||||
|
||||
# GRUB theme
|
||||
cp "$SCRIPT_DIR/branding/grub-theme/theme.txt" "$THEME_DST/" 2>/dev/null && \
|
||||
echo " theme.txt: OK"
|
||||
cp /tmp/grub-bg.png "$THEME_DST/background.png" 2>/dev/null && \
|
||||
echo " background.png: OK"
|
||||
|
||||
# Plymouth theme
|
||||
if [ -d "$WORK/archipelago/plymouth-theme" ]; then
|
||||
cp "$SCRIPT_DIR/branding/plymouth-theme/"* "$WORK/archipelago/plymouth-theme/" 2>/dev/null
|
||||
cp /tmp/plymouth-logo.png "$WORK/archipelago/plymouth-theme/logo.png" 2>/dev/null
|
||||
echo " Plymouth theme: OK"
|
||||
fi
|
||||
|
||||
# GRUB config (in case you edited it)
|
||||
if [ -f "$SCRIPT_DIR/branding/grub.cfg" ]; then
|
||||
cp "$SCRIPT_DIR/branding/grub.cfg" "$WORK/boot/grub/grub.cfg"
|
||||
echo " grub.cfg: OK (custom)"
|
||||
fi
|
||||
|
||||
# ISOLINUX config
|
||||
if [ -f "$SCRIPT_DIR/branding/isolinux.cfg" ]; then
|
||||
cp "$SCRIPT_DIR/branding/isolinux.cfg" "$WORK/isolinux/isolinux.cfg"
|
||||
echo " isolinux.cfg: OK (custom)"
|
||||
fi
|
||||
|
||||
# Step 4: Repackage ISO
|
||||
echo "[4/4] Repackaging ISO..."
|
||||
mkdir -p "$SCRIPT_DIR/results"
|
||||
|
||||
# Find isohdpfx.bin
|
||||
ISOHDPFX=""
|
||||
for p in "$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
|
||||
|
||||
# Check for EFI image
|
||||
EFI_IMG="$WORK/boot/grub/efi.img"
|
||||
|
||||
if [ -n "$ISOHDPFX" ] && [ -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
|
||||
elif [ -n "$ISOHDPFX" ]; 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 \
|
||||
-partition_offset 16 \
|
||||
"$WORK" 2>/dev/null
|
||||
else
|
||||
echo "Cannot repackage: no isohdpfx.bin found."
|
||||
echo "Install xorriso and isolinux: brew install xorriso"
|
||||
echo ""
|
||||
echo "You can still preview the assets:"
|
||||
echo " open /tmp/grub-bg.png"
|
||||
echo " open /tmp/plymouth-logo.png"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Patched ISO: $PATCHED ($(du -h "$PATCHED" | cut -f1))"
|
||||
echo ""
|
||||
|
||||
# Auto-boot in QEMU if available
|
||||
if command -v qemu-system-x86_64 >/dev/null 2>&1; then
|
||||
read -p "Boot in QEMU? [Y/n] " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
exec "$SCRIPT_DIR/test-iso-qemu.sh" "$PATCHED" --bios
|
||||
fi
|
||||
else
|
||||
echo "Install QEMU to test: brew install qemu"
|
||||
fi
|
||||
Loading…
x
Reference in New Issue
Block a user