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:
co-authored by
Claude Opus 4.6
parent
e8c80263f3
commit
f3f7b8b72f
@@ -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)')
|
||||
Reference in New Issue
Block a user