#!/bin/bash # Build a custom Void Linux live ISO with the installer baked in. # # Strategy: # 1. Stage everything possible AS THE HOST USER (overlay, mklive clone, patches, xbps-static). # 2. Run void-mklive's mklive.sh AS ROOT INSIDE A DOCKER CONTAINER. # Avoids the user-namespace CAP_MKNOD/CAP_SYS_ADMIN walls. # # Requires (host): bash, git, curl, docker. set -Eeuo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CACHE_DIR="${CACHE_DIR:-$PROJECT_DIR/cache}" OUT_DIR="${OUT_DIR:-$PROJECT_DIR/out}" BUILD_DIR="${BUILD_DIR:-$PROJECT_DIR/build}" INCLUDE_DIR="$BUILD_DIR/includes" MKLIVE_DIR="$CACHE_DIR/void-mklive" MKLIVE_REPO="${MKLIVE_REPO:-https://github.com/void-linux/void-mklive.git}" MKLIVE_REF="${MKLIVE_REF:-master}" PATCH_DIR="$PROJECT_DIR/iso/patches" DOCKER_IMAGE="${DOCKER_IMAGE:-void-installer-builder:latest}" DOCKER="${DOCKER:-docker}" # shellcheck disable=SC1091 source "$PROJECT_DIR/config/install.conf" SECRETS_FILE="${SECRETS_FILE:-$PROJECT_DIR/secrets.env}" [[ -r "$SECRETS_FILE" ]] || { echo "missing $SECRETS_FILE"; exit 1; } # shellcheck disable=SC1090 source "$SECRETS_FILE" : "${USER_PASSWORD:?}"; : "${ROOT_PASSWORD:?}" SSH_SRC_DIR="${SSH_SRC_DIR:-$HOME/.ssh}" [[ -d "$SSH_SRC_DIR" ]] || { echo "no SSH dir at $SSH_SRC_DIR"; exit 1; } command -v "$DOCKER" >/dev/null \ || { echo "ERROR: '$DOCKER' not in PATH"; exit 1; } "$DOCKER" info >/dev/null 2>&1 \ || { echo "ERROR: '$DOCKER' daemon unreachable"; exit 1; } mkdir -p "$CACHE_DIR" "$OUT_DIR" "$BUILD_DIR" # 1) clone+patch mklive if [[ ! -d "$MKLIVE_DIR/.git" ]]; then echo ">>> cloning void-mklive" git clone --depth=1 --branch "$MKLIVE_REF" "$MKLIVE_REPO" "$MKLIVE_DIR" fi if compgen -G "$PATCH_DIR/*.patch" >/dev/null; then echo ">>> resetting + applying iso/patches/" ( cd "$MKLIVE_DIR" && git checkout -- . ) for p in "$PATCH_DIR"/*.patch; do echo " $(basename "$p")" ( cd "$MKLIVE_DIR" && patch -p1 --silent < "$p" ) done fi # 2) xbps-static XBPS_STATIC_DIR="$CACHE_DIR/xbps-static" if [[ ! -x "$XBPS_STATIC_DIR/usr/bin/xbps-install.static" ]]; then echo ">>> downloading xbps-static" mkdir -p "$XBPS_STATIC_DIR" curl -fsSL "https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz" \ | tar xJf - -C "$XBPS_STATIC_DIR" fi # 3) overlay echo ">>> staging includes overlay at $INCLUDE_DIR" rm -rf "$INCLUDE_DIR" mkdir -p "$INCLUDE_DIR" install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/lib" install -m 0644 "$PROJECT_DIR/config/install.conf" "$INCLUDE_DIR/usr/local/share/installer/install.conf" # Default package list = stable-cinnamon profile (kept in /share for back-compat). install -m 0644 "$PROJECT_DIR/config/profiles/stable-cinnamon/packages.list" \ "$INCLUDE_DIR/usr/local/share/installer/packages.list" install -m 0755 "$PROJECT_DIR/installer/install.sh" "$INCLUDE_DIR/usr/local/share/installer/install.sh" for f in "$PROJECT_DIR"/installer/lib/*.sh; do install -m 0755 "$f" "$INCLUDE_DIR/usr/local/share/installer/lib/$(basename "$f")" done # Ship every profile so the live ISO can install any of them via PROFILE=. install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/profiles" cp -a "$PROJECT_DIR/config/profiles/." "$INCLUDE_DIR/usr/local/share/installer/profiles/" install -d -m 0755 "$INCLUDE_DIR/usr/local/sbin" ln -sf /usr/local/share/installer/install.sh "$INCLUDE_DIR/usr/local/sbin/install-void" install -d -m 0700 "$INCLUDE_DIR/etc" { printf "USER_PASSWORD=%q\n" "$USER_PASSWORD" printf "ROOT_PASSWORD=%q\n" "$ROOT_PASSWORD" } > "$INCLUDE_DIR/etc/installer-secrets.env" chmod 0600 "$INCLUDE_DIR/etc/installer-secrets.env" install -d -m 0700 "$INCLUDE_DIR/etc/installer-ssh" cp -a "$SSH_SRC_DIR"/. "$INCLUDE_DIR/etc/installer-ssh/" find "$INCLUDE_DIR/etc/installer-ssh" -type f -exec chmod 0600 {} + find "$INCLUDE_DIR/etc/installer-ssh" -type d -exec chmod 0700 {} + install -d -m 0755 "$INCLUDE_DIR/etc/sv/installer" cat > "$INCLUDE_DIR/etc/sv/installer/run" <<'SV_EOF' #!/bin/sh exec /sbin/agetty --autologin root --noclear tty1 linux SV_EOF chmod 0755 "$INCLUDE_DIR/etc/sv/installer/run" install -d -m 0755 "$INCLUDE_DIR/etc/sv/agetty-tty1" : > "$INCLUDE_DIR/etc/sv/agetty-tty1/down" install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default" ln -sf /etc/sv/installer "$INCLUDE_DIR/etc/runit/runsvdir/default/installer" install -d -m 0700 "$INCLUDE_DIR/root" cat > "$INCLUDE_DIR/root/.bash_profile" <<'PROFILE_EOF' case "$(tty)" in /dev/tty1) if [ ! -f /tmp/.installer-done ]; then touch /tmp/.installer-done clear echo echo " Void Linux Installer (xps9700)" echo " Press ENTER to start, or Ctrl-C within 5s for a shell." sleep 5 || true /usr/local/sbin/install-void || { echo "Installer exited with $?. Dropping to shell." exec /bin/bash } echo "Install complete. Type 'reboot' or 'poweroff'." exec /bin/bash fi ;; esac PROFILE_EOF chmod 0644 "$INCLUDE_DIR/root/.bash_profile" cat > "$INCLUDE_DIR/etc/motd" <>> merging EXTRA_INCLUDE_DIR=$EXTRA_INCLUDE_DIR" cp -a "$EXTRA_INCLUDE_DIR/." "$INCLUDE_DIR/" fi # 3b) Customizations overlay (themes / icons / wallpapers / dotfiles / vscode) echo ">>> staging user customizations overlay" OVERLAY="$INCLUDE_DIR/etc/installer-overlay" install -d -m 0755 "$OVERLAY" "$OVERLAY/wallpapers" \ "$OVERLAY/themes" "$OVERLAY/icons" \ "$OVERLAY/skel" "$OVERLAY/vscode-user" # Wallpapers from ~/Scaricati/pxfuel*.jpg (literal parens in filenames) WP_SRC="${WALLPAPERS_SRC:-$HOME/Scaricati}" shopt -s nullglob for f in "$WP_SRC"/pxfuel*.jpg; do install -m 0644 "$f" "$OVERLAY/wallpapers/$(basename "$f")" done shopt -u nullglob echo " wallpapers: $(ls "$OVERLAY/wallpapers" | wc -l) file(s)" # Theme: clone Gruvbox-GTK-Theme and run install.sh into a staging dir. THEME_CACHE="$CACHE_DIR/gruvbox-gtk-theme" if [[ ! -d "$THEME_CACHE/.git" ]]; then git clone --depth=1 https://github.com/Fausto-Korpsvart/Gruvbox-GTK-Theme.git "$THEME_CACHE" || \ echo " (warning: could not clone theme repo)" fi THEME_BUILD="$CACHE_DIR/gruvbox-gtk-built" if [[ -x "$THEME_CACHE/themes/install.sh" && ! -d "$THEME_BUILD" ]]; then echo " building gruvbox themes -> $THEME_BUILD (via docker)" install -d -m 0755 "$THEME_BUILD" "$DOCKER" run --rm \ -v "$THEME_CACHE":/src \ -v "$THEME_BUILD":/out \ debian:stable-slim sh -c ' export DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null apt-get install -y --no-install-recommends sassc bash >/dev/null cd /src/themes && bash install.sh -d /out -t default -c dark -s standard ' || echo " (warning: theme build failed; continuing without)" fi if [[ -d "$THEME_BUILD" ]]; then for d in "$THEME_BUILD"/Gruvbox-Dark*; do [[ -d "$d" ]] && cp -a "$d" "$OVERLAY/themes/$(basename "$d")" done echo " themes: $(ls "$OVERLAY/themes" 2>/dev/null | wc -l) variant(s)" fi # Icons: clone gruvbox-plus-icon-pack ICON_CACHE="$CACHE_DIR/gruvbox-plus-icons" if [[ ! -d "$ICON_CACHE/.git" ]]; then git clone --depth=1 https://github.com/SylEleuth/gruvbox-plus-icon-pack.git "$ICON_CACHE" || \ echo " (warning: could not clone icon repo)" fi if [[ -d "$ICON_CACHE/Gruvbox-Plus-Dark" ]]; then cp -a "$ICON_CACHE/Gruvbox-Plus-Dark" "$OVERLAY/icons/Gruvbox-Plus-Dark" echo " icons: Gruvbox-Plus-Dark deployed" fi # Bibata cursor — copied from host (no Void package). BIBATA_SRC="${BIBATA_SRC:-/usr/share/icons/Bibata-Modern-Ice}" if [[ -d "$BIBATA_SRC" ]]; then cp -a "$BIBATA_SRC" "$OVERLAY/icons/Bibata-Modern-Ice" echo " cursor: Bibata-Modern-Ice deployed" else echo " (warning: $BIBATA_SRC missing on host; cursor theme will be skipped)" fi # Dotfiles (skel) DOTFILES_SRC="${DOTFILES_SRC:-$HOME}" for f in .bashrc .bash_aliases .gitconfig; do [[ -r "$DOTFILES_SRC/$f" ]] && install -m 0644 "$DOTFILES_SRC/$f" "$OVERLAY/skel/$f" done echo " dotfiles: $(ls -A "$OVERLAY/skel" 2>/dev/null | wc -l) file(s)" # VS Code user config VSCODE_SRC="${VSCODE_USER_SRC:-$HOME/.config/Code/User}" if [[ -d "$VSCODE_SRC" ]]; then for f in settings.json keybindings.json mcp.json tasks.json; do [[ -r "$VSCODE_SRC/$f" ]] && install -m 0644 "$VSCODE_SRC/$f" "$OVERLAY/vscode-user/$f" done [[ -d "$VSCODE_SRC/snippets" ]] && cp -a "$VSCODE_SRC/snippets" "$OVERLAY/vscode-user/snippets" # Copy the whole globalStorage so Copilot/extensions keep their state # (note: GitHub OAuth tokens live in libsecret, NOT here — user re-signs once). [[ -d "$VSCODE_SRC/globalStorage" ]] && cp -a "$VSCODE_SRC/globalStorage" "$OVERLAY/vscode-user/globalStorage" echo " vscode-user: $(ls -A "$OVERLAY/vscode-user" 2>/dev/null | wc -l) item(s)" fi # VS Code extensions list (so first-login can re-install them). if command -v code >/dev/null 2>&1; then code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true echo " vscode extensions: $(wc -l < "$OVERLAY/vscode-extensions.txt" 2>/dev/null) listed" fi # Claude Code config + auth (~/.claude lives in $HOME on Linux). CLAUDE_SRC="${CLAUDE_SRC:-$HOME/.claude}" if [[ -d "$CLAUDE_SRC" ]]; then cp -a "$CLAUDE_SRC" "$OVERLAY/claude" echo " claude: ~/.claude bundled" fi CLAUDE_JSON="${CLAUDE_JSON:-$HOME/.claude.json}" if [[ -r "$CLAUDE_JSON" ]]; then install -m 0600 "$CLAUDE_JSON" "$OVERLAY/claude.json" fi # VS Code extensions list (host) if command -v code >/dev/null 2>&1; then code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true echo " vscode extensions: $(wc -l < "$OVERLAY/vscode-extensions.txt") to install" fi # First-login one-shot [[ -r "$PROJECT_DIR/installer/first-login.sh" ]] && \ install -m 0755 "$PROJECT_DIR/installer/first-login.sh" "$OVERLAY/first-login.sh" # 4) packages, output filename ISO_PKGS=$(grep -vE '^\s*(#|$)' "$PROJECT_DIR/config/packages.live.list" | tr '\n' ' ') TS="$(date -u +%Y%m%d)" OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-install-${HOSTNAME}-${TS}.iso}" # 5) docker echo ">>> building docker image $DOCKER_IMAGE (cached)" # Force the legacy builder if buildx is missing (Docker 29 removed it from # the default `docker build` path; users without docker-buildx-plugin # detected on PATH will otherwise see "unknown command: docker buildx"). if "$DOCKER" buildx version >/dev/null 2>&1; then "$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso" else DOCKER_BUILDKIT=0 "$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso" fi echo ">>> running mklive.sh inside docker — output: $OUT_ISO" "$DOCKER" run --rm --privileged \ -v "$PROJECT_DIR:/work:rw" \ -v "$CACHE_DIR:/cache:rw" \ -e ARCH="$ARCH" \ -e REPO_URL="$REPO_URL" \ -e KEYMAP="$KEYMAP" \ -e LOCALE="$LOCALE" \ -e ISO_PKGS="$ISO_PKGS" \ -e ISO_TITLE="Void Installer ($HOSTNAME)" \ -e OUT_ISO_REL="${OUT_ISO#$PROJECT_DIR/}" \ -e BOOT_CMDLINE="${BOOT_CMDLINE:-}" \ -e HOST_UID="$(id -u)" \ -e HOST_GID="$(id -g)" \ "$DOCKER_IMAGE" \ bash /work/iso/_inner-build.sh echo echo ">>> ISO built: $OUT_ISO" sha256sum "$OUT_ISO" | tee "${OUT_ISO}.sha256"