feat: initial commit — void-installer multi-profile (stable-cinnamon + mainline-niri)
This commit is contained in:
123
installer/lib/bootstrap.sh
Executable file
123
installer/lib/bootstrap.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
# Bootstrap base system into $TARGET via xbps-install.
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
bootstrap_base() {
|
||||
step "Bootstrapping base system into $TARGET (this takes a while)"
|
||||
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
local pkg_list=()
|
||||
mapfile -t pkg_list < <(grep -vE '^\s*(#|$)' "${PKG_LIST_FILE:-/usr/local/share/installer/packages.list}")
|
||||
|
||||
mkdir -p "$TARGET/var/db/xbps/keys"
|
||||
cp -a /var/db/xbps/keys/* "$TARGET/var/db/xbps/keys/" 2>/dev/null || \
|
||||
warn "could not copy xbps keys (running outside Void live env?)"
|
||||
|
||||
# Use the caching proxy during install if available; fall back to real repo.
|
||||
# The proxy URL (if set) is only used during installation — the installed
|
||||
# system's xbps.d always gets the real REPO_URL.
|
||||
local _repo="${INSTALL_REPO_URL:-$REPO_URL}"
|
||||
local _nonfree="${_repo%/current}/current/nonfree"
|
||||
local _multilib="${_repo%/current}/current/multilib"
|
||||
local _multilib_nonfree="${_repo%/current}/current/multilib/nonfree"
|
||||
# Normalise: if _repo doesn't end in /current the above substitution
|
||||
# leaves it unchanged; append nonfree/multilib paths directly.
|
||||
[[ "$_repo" == */current ]] || {
|
||||
_nonfree="${_repo}/nonfree"
|
||||
_multilib="${_repo}/multilib"
|
||||
_multilib_nonfree="${_repo}/multilib/nonfree"
|
||||
}
|
||||
log "install repo: $_repo (proxy=${INSTALL_REPO_URL:-none})"
|
||||
|
||||
# Bootstrap base-system via proxy/repo.
|
||||
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" -R "$_repo" base-system
|
||||
ok "base-system installed"
|
||||
|
||||
# Enable extra repos in target xbps.d — always write the REAL URLs so the
|
||||
# installed system never depends on the proxy.
|
||||
mkdir -p "$TARGET/etc/xbps.d"
|
||||
cat > "$TARGET/etc/xbps.d/00-repository-main.conf" <<EOF
|
||||
repository=$REPO_URL
|
||||
EOF
|
||||
|
||||
log "enabling extra repos: ${EXTRA_REPOS[*]}"
|
||||
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" \
|
||||
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree" \
|
||||
void-repo-nonfree void-repo-multilib void-repo-multilib-nonfree
|
||||
|
||||
# Sync from all newly enabled repos.
|
||||
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" \
|
||||
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree"
|
||||
|
||||
log "installing extra packages from $PKG_LIST_FILE"
|
||||
XBPS_ARCH="$ARCH" xbps-install -y -r "$TARGET" \
|
||||
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree" \
|
||||
"${pkg_list[@]}"
|
||||
|
||||
# Ensure the installed system's xbps config points only at real repos.
|
||||
cat > "$TARGET/etc/xbps.d/00-repository-main.conf" <<EOF
|
||||
repository=$REPO_URL
|
||||
EOF
|
||||
|
||||
ok "all xbps packages installed (proxy removed from target xbps.d)"
|
||||
}
|
||||
|
||||
generate_fstab() {
|
||||
step "Generating /etc/fstab"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
if command -v xgenfstab >/dev/null 2>&1; then
|
||||
xgenfstab -U "$TARGET" > "$TARGET/etc/fstab"
|
||||
else
|
||||
# fallback minimal generator (uses BTRFS_SUBVOLS array from install.conf)
|
||||
: > "$TARGET/etc/fstab"
|
||||
local uuid entry sv mp pass
|
||||
uuid=$(blkid -s UUID -o value "$ROOT_PART")
|
||||
for entry in "${BTRFS_SUBVOLS[@]}"; do
|
||||
sv="${entry%%:*}"
|
||||
mp="${entry##*:}"
|
||||
# root gets fsck pass 1, others 2
|
||||
pass=2; [[ "$mp" == "/" ]] && pass=1
|
||||
printf 'UUID=%s %-12s btrfs %s,subvol=%s 0 %d\n' \
|
||||
"$uuid" "$mp" "$BTRFS_MOUNT_OPTS" "$sv" "$pass" >> "$TARGET/etc/fstab"
|
||||
done
|
||||
local efi_uuid
|
||||
efi_uuid=$(blkid -s UUID -o value "$EFI_PART")
|
||||
printf 'UUID=%s /boot/efi vfat defaults,noatime,umask=0077 0 2\n' "$efi_uuid" >> "$TARGET/etc/fstab"
|
||||
printf 'tmpfs /tmp tmpfs defaults,nosuid,nodev 0 0\n' >> "$TARGET/etc/fstab"
|
||||
fi
|
||||
ok "fstab generated"
|
||||
}
|
||||
|
||||
mount_pseudo_fs() {
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
step "Mounting pseudo-filesystems for chroot"
|
||||
for d in dev proc sys run; do
|
||||
mkdir -p "$TARGET/$d"
|
||||
mount --rbind "/$d" "$TARGET/$d"
|
||||
mount --make-rslave "$TARGET/$d"
|
||||
done
|
||||
# /dev/shm must be a real tmpfs for python multiprocessing (sem_open)
|
||||
# used by xbps-reconfigure → compileall. Without it the byte-compile
|
||||
# step fails with FileNotFoundError on every python package.
|
||||
mkdir -p "$TARGET/dev/shm"
|
||||
mountpoint -q "$TARGET/dev/shm" || mount -t tmpfs -o nosuid,nodev tmpfs "$TARGET/dev/shm" 2>/dev/null || true
|
||||
if is_uefi; then
|
||||
mkdir -p "$TARGET/sys/firmware/efi/efivars"
|
||||
mount -t efivarfs efivarfs "$TARGET/sys/firmware/efi/efivars" 2>/dev/null || true
|
||||
fi
|
||||
# Make DNS work inside the chroot (needed for VS Code download, etc.).
|
||||
if [[ -r /etc/resolv.conf ]]; then
|
||||
install -Dm644 /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null \
|
||||
|| cp /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
unmount_target() {
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
step "Unmounting $TARGET"
|
||||
sync
|
||||
umount -R "$TARGET" 2>/dev/null || warn "lazy umount fallback"
|
||||
umount -lR "$TARGET" 2>/dev/null || true
|
||||
}
|
||||
98
installer/lib/common.sh
Executable file
98
installer/lib/common.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# Common helpers: logging, error handling, run-as-root checks.
|
||||
# Sourced by all installer modules.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# --- colors ---
|
||||
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
||||
C_RED="$(tput setaf 1)"; C_GREEN="$(tput setaf 2)"
|
||||
C_YEL="$(tput setaf 3)"; C_BLUE="$(tput setaf 4)"
|
||||
C_BOLD="$(tput bold)"; C_RESET="$(tput sgr0)"
|
||||
else
|
||||
C_RED=""; C_GREEN=""; C_YEL=""; C_BLUE=""; C_BOLD=""; C_RESET=""
|
||||
fi
|
||||
|
||||
LOG_FILE="${LOG_FILE:-/var/log/void-installer.log}"
|
||||
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||
: > "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/void-installer.log"
|
||||
|
||||
log() { printf '%s [%s] %s\n' "$(date +%H:%M:%S)" "INFO" "$*" | tee -a "$LOG_FILE" >&2; }
|
||||
warn() { printf '%s%s [%s] %s%s\n' "$C_YEL" "$(date +%H:%M:%S)" "WARN" "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||
err() { printf '%s%s [%s] %s%s\n' "$C_RED" "$(date +%H:%M:%S)" "ERR " "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||
ok() { printf '%s%s [%s] %s%s\n' "$C_GREEN" "$(date +%H:%M:%S)" " OK " "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||
step() { printf '\n%s%s==> %s%s\n' "$C_BOLD" "$C_BLUE" "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
trap 'err "Installer aborted at line $LINENO (exit=$?). Log: $LOG_FILE"' ERR
|
||||
|
||||
require_root() {
|
||||
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "must run as root"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
# confirm "Question" -> returns 0 on yes
|
||||
local prompt="${1:-Proceed?}"
|
||||
if [[ "${UNATTENDED:-0}" == "1" ]]; then
|
||||
log "[unattended] auto-yes: $prompt"
|
||||
return 0
|
||||
fi
|
||||
local ans
|
||||
read -r -p "$prompt [y/N] " ans
|
||||
[[ "$ans" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
run_chroot() {
|
||||
# run a command inside the target chroot
|
||||
local target="${TARGET:-/mnt}"
|
||||
chroot "$target" /usr/bin/env -i \
|
||||
HOME=/root TERM="${TERM:-linux}" \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
LANG="${LANG:-en_US.UTF-8}" \
|
||||
/bin/bash -c "$*"
|
||||
}
|
||||
|
||||
is_uefi() {
|
||||
[[ -d /sys/firmware/efi/efivars ]]
|
||||
}
|
||||
|
||||
# Set a password inside the chroot without exposing it to the shell argv
|
||||
# or to ${} expansion in command strings (bash injection-safe).
|
||||
# Uses openssl to pre-hash, then usermod -p, because chpasswd on Void can
|
||||
# silently no-op for freshly-created (locked) accounts depending on the
|
||||
# default crypt method in /etc/login.defs.
|
||||
set_chroot_password() {
|
||||
local user="$1" password="$2"
|
||||
local target="${TARGET:-/mnt}"
|
||||
# Generate a SHA-512 crypt hash on the host (openssl is in the live ISO).
|
||||
local hash
|
||||
hash="$(openssl passwd -6 "$password")" || {
|
||||
warn "openssl passwd failed for $user; falling back to chpasswd"
|
||||
chroot "$target" /usr/bin/env -i \
|
||||
HOME=/root TERM="${TERM:-linux}" \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
chpasswd <<EOF
|
||||
$user:$password
|
||||
EOF
|
||||
return
|
||||
}
|
||||
# usermod -p writes the hash directly into /etc/shadow and unlocks.
|
||||
chroot "$target" /usr/bin/env -i \
|
||||
HOME=/root TERM="${TERM:-linux}" \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
usermod -p "$hash" "$user"
|
||||
}
|
||||
|
||||
load_secrets() {
|
||||
local f="${1:-/etc/installer-secrets.env}"
|
||||
if [[ -r "$f" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$f"
|
||||
log "loaded secrets from $f"
|
||||
else
|
||||
warn "no secrets file at $f; passwords must be set in env"
|
||||
fi
|
||||
: "${USER_PASSWORD:?USER_PASSWORD missing}"
|
||||
: "${ROOT_PASSWORD:?ROOT_PASSWORD missing}"
|
||||
}
|
||||
432
installer/lib/customizations.sh
Normal file
432
installer/lib/customizations.sh
Normal file
@@ -0,0 +1,432 @@
|
||||
#!/bin/bash
|
||||
# User-environment customizations: themes, icons, wallpapers, dotfiles,
|
||||
# vscode config, cinnamon dconf defaults, default terminal, keymap.
|
||||
# Reads pre-baked overlay from /etc/installer-overlay/ on the live ISO.
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
OVERLAY_SRC="${OVERLAY_SRC:-/etc/installer-overlay}"
|
||||
|
||||
install_customizations() {
|
||||
step "Installing customizations (themes / icons / wallpapers / dotfiles)"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
|
||||
if [[ ! -d "$OVERLAY_SRC" ]]; then
|
||||
warn "no overlay at $OVERLAY_SRC; skipping customizations"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_deploy_themes "$TARGET"
|
||||
_deploy_icons "$TARGET"
|
||||
_deploy_wallpapers "$TARGET"
|
||||
_deploy_user_dotfiles "$TARGET"
|
||||
_deploy_vscode_config "$TARGET"
|
||||
_deploy_first_login "$TARGET"
|
||||
_set_default_terminal "$TARGET"
|
||||
_install_gestures "$TARGET"
|
||||
_install_snapshot_hook "$TARGET"
|
||||
_install_upgrade_applet "$TARGET"
|
||||
|
||||
# Cinnamon/X11-specific helpers — only run on the cinnamon profile.
|
||||
if [[ "${DESKTOP:-cinnamon}" == "cinnamon" ]]; then
|
||||
_write_dconf_defaults "$TARGET"
|
||||
_install_nemo_actions "$TARGET"
|
||||
fi
|
||||
|
||||
# Profile-specific hooks (e.g. niri KDL config, waybar, greetd).
|
||||
if declare -F run_profile_customizations >/dev/null 2>&1; then
|
||||
run_profile_customizations
|
||||
fi
|
||||
|
||||
ok "customizations installed"
|
||||
}
|
||||
|
||||
_deploy_themes() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/themes"
|
||||
[[ -d "$src" ]] || { log "no themes overlay; skipping"; return 0; }
|
||||
|
||||
install -d -m 0755 "$TARGET/usr/share/themes"
|
||||
# Each subdir of overlay/themes is a complete theme dir; just rsync them.
|
||||
cp -a "$src"/. "$TARGET/usr/share/themes/" 2>/dev/null || true
|
||||
log "themes deployed -> /usr/share/themes/"
|
||||
}
|
||||
|
||||
_deploy_icons() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/icons"
|
||||
[[ -d "$src" ]] || { log "no icons overlay; skipping"; return 0; }
|
||||
|
||||
install -d -m 0755 "$TARGET/usr/share/icons"
|
||||
cp -a "$src"/. "$TARGET/usr/share/icons/" 2>/dev/null || true
|
||||
# Refresh icon cache (best-effort).
|
||||
for d in "$TARGET"/usr/share/icons/*/; do
|
||||
[[ -d "$d" ]] || continue
|
||||
run_chroot "gtk-update-icon-cache -f -t /usr/share/icons/$(basename "$d") 2>/dev/null || true"
|
||||
done
|
||||
log "icons deployed -> /usr/share/icons/"
|
||||
}
|
||||
|
||||
_deploy_wallpapers() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/wallpapers"
|
||||
[[ -d "$src" ]] || { log "no wallpapers overlay; skipping"; return 0; }
|
||||
|
||||
local dst="$TARGET/usr/share/backgrounds/void-installer"
|
||||
install -d -m 0755 "$dst"
|
||||
cp -a "$src"/. "$dst/"
|
||||
chmod 0644 "$dst"/* 2>/dev/null || true
|
||||
log "wallpapers -> /usr/share/backgrounds/void-installer/"
|
||||
}
|
||||
|
||||
_deploy_user_dotfiles() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/skel"
|
||||
[[ -d "$src" ]] || { log "no skel overlay; skipping dotfiles"; return 0; }
|
||||
|
||||
local home="$TARGET/home/$USERNAME"
|
||||
install -d -m 0755 "$home"
|
||||
|
||||
# Copy dotfiles, preserving structure. Don't clobber .ssh (already set).
|
||||
(
|
||||
cd "$src" && find . -mindepth 1 -maxdepth 1 \
|
||||
! -name '.ssh' -print0
|
||||
) | while IFS= read -r -d '' rel; do
|
||||
cp -a "$src/$rel" "$home/" 2>/dev/null || true
|
||||
done
|
||||
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME"
|
||||
log "dotfiles deployed -> /home/$USERNAME/"
|
||||
}
|
||||
|
||||
_deploy_vscode_config() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/vscode-user"
|
||||
[[ -d "$src" ]] || { log "no vscode-user overlay; skipping"; return 0; }
|
||||
|
||||
local dst="$TARGET/home/$USERNAME/.config/Code/User"
|
||||
install -d -m 0755 "$dst"
|
||||
cp -a "$src"/. "$dst/"
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config"
|
||||
log "vscode user config -> ~/.config/Code/User/"
|
||||
}
|
||||
|
||||
_deploy_first_login() {
|
||||
local TARGET="$1"
|
||||
local src="$OVERLAY_SRC/first-login.sh"
|
||||
local ext_list="$OVERLAY_SRC/vscode-extensions.txt"
|
||||
|
||||
install -d -m 0755 "$TARGET/usr/local/libexec"
|
||||
[[ -r "$src" ]] && {
|
||||
install -m 0755 "$src" "$TARGET/usr/local/libexec/first-login.sh"
|
||||
log "first-login script staged"
|
||||
}
|
||||
[[ -r "$ext_list" ]] && {
|
||||
install -m 0644 "$ext_list" "$TARGET/etc/installer-vscode-extensions.txt"
|
||||
}
|
||||
|
||||
# Inject one-shot trigger into ~/.bash_profile (appended; idempotent guard
|
||||
# in the script itself via /var/lib/first-login.done).
|
||||
local home="$TARGET/home/$USERNAME"
|
||||
install -d -m 0755 "$home"
|
||||
if [[ -x "$TARGET/usr/local/libexec/first-login.sh" ]]; then
|
||||
if ! grep -q "first-login.sh" "$home/.bash_profile" 2>/dev/null; then
|
||||
cat >> "$home/.bash_profile" <<'EOF'
|
||||
|
||||
# Auto-run user environment setup on first interactive login.
|
||||
if [[ -z "$_FIRST_LOGIN_RAN" && -x /usr/local/libexec/first-login.sh \
|
||||
&& ! -f "$HOME/.first-login-done" ]]; then
|
||||
export _FIRST_LOGIN_RAN=1
|
||||
/usr/local/libexec/first-login.sh 2>&1 | tee -a "$HOME/.first-login.log"
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
run_chroot "chown $USERNAME:$USERNAME /home/$USERNAME/.bash_profile"
|
||||
fi
|
||||
|
||||
# Also autostart it from the desktop session so GUI-only users get it.
|
||||
if [[ -x "$TARGET/usr/local/libexec/first-login.sh" ]]; then
|
||||
local autostart="$TARGET/etc/xdg/autostart"
|
||||
install -d -m 0755 "$autostart"
|
||||
cat > "$autostart/void-installer-first-login.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Void Installer First-Login Setup
|
||||
Exec=/usr/local/libexec/first-login.sh
|
||||
NoDisplay=true
|
||||
X-GNOME-Autostart-enabled=true
|
||||
OnlyShowIn=X-Cinnamon;GNOME;XFCE;KDE;
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Ensure ~/.local/bin is on PATH for every shell (login + non-login).
|
||||
install -d -m 0755 "$TARGET/etc/profile.d"
|
||||
cat > "$TARGET/etc/profile.d/local-bin.sh" <<'EOF'
|
||||
# Prepend the per-user bin dir so first-login symlinks (claude, node, npm)
|
||||
# are visible to every interactive shell.
|
||||
case ":$PATH:" in
|
||||
*":$HOME/.local/bin:"*) ;;
|
||||
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
# Claude Code config (auth tokens) bundled from host overlay.
|
||||
local claude_src="$OVERLAY_SRC/claude"
|
||||
if [[ -d "$claude_src" ]]; then
|
||||
local cdst="$TARGET/home/$USERNAME/.claude"
|
||||
install -d -m 0700 "$cdst"
|
||||
cp -a "$claude_src"/. "$cdst/"
|
||||
log "claude config -> ~/.claude/"
|
||||
fi
|
||||
if [[ -r "$OVERLAY_SRC/claude.json" ]]; then
|
||||
install -m 0600 "$OVERLAY_SRC/claude.json" "$TARGET/home/$USERNAME/.claude.json"
|
||||
fi
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.claude /home/$USERNAME/.claude.json 2>/dev/null || true"
|
||||
}
|
||||
|
||||
_write_dconf_defaults() {
|
||||
local TARGET="$1"
|
||||
install -d -m 0755 "$TARGET/etc/dconf/db/local.d" \
|
||||
"$TARGET/etc/dconf/profile"
|
||||
|
||||
cat > "$TARGET/etc/dconf/profile/user" <<'EOF'
|
||||
user-db:user
|
||||
system-db:local
|
||||
EOF
|
||||
|
||||
local wallpaper="${INITIAL_WALLPAPER:-pxfuel.jpg}"
|
||||
cat > "$TARGET/etc/dconf/db/local.d/00-cinnamon" <<EOF
|
||||
# Cinnamon system-wide defaults — generated by void-installer.
|
||||
[org/cinnamon/desktop/interface]
|
||||
gtk-theme='${GTK_THEME}'
|
||||
icon-theme='${ICON_THEME}'
|
||||
cursor-theme='${CURSOR_THEME}'
|
||||
|
||||
[org/cinnamon/desktop/wm/preferences]
|
||||
theme='${GTK_THEME}'
|
||||
|
||||
[org/cinnamon/theme]
|
||||
name='${GTK_THEME}'
|
||||
|
||||
[org/cinnamon/desktop/background]
|
||||
picture-uri='file:///usr/share/backgrounds/void-installer/${wallpaper}'
|
||||
picture-options='zoom'
|
||||
|
||||
[org/gnome/desktop/background]
|
||||
picture-uri='file:///usr/share/backgrounds/void-installer/${wallpaper}'
|
||||
picture-options='zoom'
|
||||
|
||||
[org/gnome/desktop/input-sources]
|
||||
xkb-options=['']
|
||||
sources=[('xkb', 'ch+fr')]
|
||||
|
||||
[org/cinnamon/desktop/keybindings]
|
||||
custom-list=['custom0', 'custom1']
|
||||
|
||||
[org/cinnamon/desktop/keybindings/custom-keybindings/custom0]
|
||||
name='Open Terminal'
|
||||
command='${DEFAULT_TERMINAL:-alacritty}'
|
||||
binding=['<Primary><Alt>t']
|
||||
|
||||
[org/cinnamon/desktop/keybindings/custom-keybindings/custom1]
|
||||
name='Flameshot'
|
||||
command='flameshot gui'
|
||||
binding=['Print']
|
||||
|
||||
[org/cinnamon/desktop/keybindings/media-keys]
|
||||
screenshot=['']
|
||||
area-screenshot=['']
|
||||
window-screenshot=['']
|
||||
|
||||
[org/cinnamon]
|
||||
enabled-applets=['panel1:left:0:menu@cinnamon.org', 'panel1:left:1:show-desktop@cinnamon.org', 'panel1:left:2:grouped-window-list@cinnamon.org', 'panel1:right:0:systray@cinnamon.org', 'panel1:right:1:xapp-status@cinnamon.org', 'panel1:right:2:notifications@cinnamon.org', 'panel1:right:3:printers@cinnamon.org', 'panel1:right:4:removable-drives@cinnamon.org', 'panel1:right:5:keyboard@cinnamon.org', 'panel1:right:6:favorites@cinnamon.org', 'panel1:right:7:network@cinnamon.org', 'panel1:right:8:sound@cinnamon.org', 'panel1:right:9:power@cinnamon.org', 'panel1:right:10:calendar@cinnamon.org', 'panel1:right:11:user@cinnamon.org']
|
||||
|
||||
[org/cinnamon/desktop/default-applications/terminal]
|
||||
exec='${DEFAULT_TERMINAL:-alacritty}'
|
||||
exec-arg='-e'
|
||||
EOF
|
||||
|
||||
# Compile dconf db inside the chroot.
|
||||
run_chroot "dconf update 2>/dev/null || true"
|
||||
log "dconf cinnamon defaults written"
|
||||
}
|
||||
|
||||
_set_default_terminal() {
|
||||
local TARGET="$1"
|
||||
local term="${DEFAULT_TERMINAL:-alacritty}"
|
||||
|
||||
# x-terminal-emulator-style alternative — Void doesn't ship update-alternatives
|
||||
# for terminals, so just symlink in /usr/local/bin.
|
||||
if [[ -x "$TARGET/usr/bin/$term" ]]; then
|
||||
ln -sf "/usr/bin/$term" "$TARGET/usr/local/bin/x-terminal-emulator"
|
||||
log "default terminal set to $term"
|
||||
else
|
||||
warn "$term not found in target; skipping default-terminal symlink"
|
||||
fi
|
||||
|
||||
# Persistent X keymap (works for non-cinnamon login too).
|
||||
install -d -m 0755 "$TARGET/etc/X11/xorg.conf.d"
|
||||
cat > "$TARGET/etc/X11/xorg.conf.d/00-keyboard.conf" <<EOF
|
||||
Section "InputClass"
|
||||
Identifier "system-keyboard"
|
||||
MatchIsKeyboard "on"
|
||||
Option "XkbLayout" "ch"
|
||||
Option "XkbVariant" "fr"
|
||||
EndSection
|
||||
EOF
|
||||
log "X11 keymap pinned: ch(fr)"
|
||||
}
|
||||
|
||||
_install_gestures() {
|
||||
local TARGET="$1"
|
||||
# System-wide default libinput-gestures config (workspace switching, overview, etc.)
|
||||
install -d -m 0755 "$TARGET/etc/skel/.config"
|
||||
cat > "$TARGET/etc/skel/.config/libinput-gestures.conf" <<'EOF'
|
||||
# void-installer defaults
|
||||
gesture swipe up 3 wmctrl -k on
|
||||
gesture swipe down 3 wmctrl -k off
|
||||
gesture swipe left 3 xdotool key super+Right
|
||||
gesture swipe right 3 xdotool key super+Left
|
||||
gesture swipe left 4 xdotool key ctrl+alt+Right
|
||||
gesture swipe right 4 xdotool key ctrl+alt+Left
|
||||
gesture swipe up 4 xdotool key super
|
||||
gesture swipe down 4 xdotool key super+d
|
||||
gesture pinch in xdotool key ctrl+minus
|
||||
gesture pinch out xdotool key ctrl+plus
|
||||
EOF
|
||||
# Ensure user is in 'input' group (libinput-gestures needs /dev/input/event*).
|
||||
run_chroot "groupadd -f input; usermod -aG input $USERNAME" || true
|
||||
|
||||
# Autostart for cinnamon session.
|
||||
install -d -m 0755 "$TARGET/etc/skel/.config/autostart"
|
||||
cat > "$TARGET/etc/skel/.config/autostart/libinput-gestures.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Libinput Gestures
|
||||
Exec=libinput-gestures-setup start
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=false
|
||||
EOF
|
||||
|
||||
# Mirror into existing user's home (skel only applies to NEW users).
|
||||
if id "$USERNAME" >/dev/null 2>&1 || [[ -d "$TARGET/home/$USERNAME" ]]; then
|
||||
install -d -m 0755 "$TARGET/home/$USERNAME/.config/autostart"
|
||||
cp -a "$TARGET/etc/skel/.config/libinput-gestures.conf" \
|
||||
"$TARGET/home/$USERNAME/.config/libinput-gestures.conf" 2>/dev/null || true
|
||||
cp -a "$TARGET/etc/skel/.config/autostart/libinput-gestures.desktop" \
|
||||
"$TARGET/home/$USERNAME/.config/autostart/" 2>/dev/null || true
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config" || true
|
||||
fi
|
||||
log "trackpad gestures configured (libinput-gestures)"
|
||||
}
|
||||
|
||||
_install_snapshot_hook() {
|
||||
local TARGET="$1"
|
||||
# Pre-upgrade btrfs snapshot via xbps-install hook.
|
||||
install -d -m 0755 "$TARGET/etc/xbps.d"
|
||||
install -d -m 0755 "$TARGET/usr/local/sbin"
|
||||
|
||||
cat > "$TARGET/usr/local/sbin/xbps-pre-upgrade-snapshot.sh" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Take a read-only btrfs snapshot of @ before an xbps upgrade/install.
|
||||
# Snapshots live in /.snapshots/auto-YYYYmmdd-HHMMSS so timeshift can manage them.
|
||||
set -e
|
||||
ROOT_SUBVOL=/run/btrfs-root/@
|
||||
SNAP_DIR=/.snapshots
|
||||
[ -d "$SNAP_DIR" ] || mkdir -p "$SNAP_DIR"
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
NAME="auto-pre-xbps-$TS"
|
||||
# Mount the toplevel of the btrfs FS once so we can snapshot @.
|
||||
ROOT_DEV=$(findmnt -no SOURCE /)
|
||||
mkdir -p /run/btrfs-root
|
||||
if ! mountpoint -q /run/btrfs-root; then
|
||||
mount -o subvolid=5 "$ROOT_DEV" /run/btrfs-root 2>/dev/null || exit 0
|
||||
fi
|
||||
btrfs subvolume snapshot -r /run/btrfs-root/@ "/run/btrfs-root/.snapshots/$NAME" 2>/dev/null \
|
||||
|| mkdir -p /run/btrfs-root/.snapshots && \
|
||||
btrfs subvolume snapshot -r /run/btrfs-root/@ "/run/btrfs-root/.snapshots/$NAME"
|
||||
echo "[snapshot] created $NAME"
|
||||
EOF
|
||||
chmod 0755 "$TARGET/usr/local/sbin/xbps-pre-upgrade-snapshot.sh"
|
||||
|
||||
# xbps doesn't have native pre-hooks; wrap xbps-install via /usr/local/bin shim.
|
||||
cat > "$TARGET/usr/local/bin/xbps-install" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Wrapper that snapshots @ before any state-changing xbps-install run.
|
||||
case " $* " in
|
||||
*" -S "*|*" --sync "*|*" -u "*|*" --update "*) /usr/local/sbin/xbps-pre-upgrade-snapshot.sh || true ;;
|
||||
*) [ -n "$1" ] && /usr/local/sbin/xbps-pre-upgrade-snapshot.sh || true ;;
|
||||
esac
|
||||
exec /usr/bin/xbps-install "$@"
|
||||
EOF
|
||||
chmod 0755 "$TARGET/usr/local/bin/xbps-install"
|
||||
log "btrfs pre-upgrade snapshot hook installed"
|
||||
}
|
||||
|
||||
_install_upgrade_applet() {
|
||||
local TARGET="$1"
|
||||
install -d -m 0755 "$TARGET/usr/local/bin" \
|
||||
"$TARGET/usr/share/applications" \
|
||||
"$TARGET/etc/skel/.config/autostart"
|
||||
|
||||
# Tiny GUI wrapper: uses zenity if available, else xterm.
|
||||
cat > "$TARGET/usr/local/bin/void-upgrade-gui" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Check for upgrades, prompt user, run xbps-install -Su (with snapshot via wrapper).
|
||||
set -e
|
||||
PENDING=$(xbps-install -Sun 2>/dev/null | wc -l)
|
||||
if [ "$PENDING" -eq 0 ]; then
|
||||
notify-send "Void Upgrade" "System is up to date." 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
MSG="There are $PENDING package updates available.\nRun system upgrade now?\n(A btrfs snapshot will be taken automatically.)"
|
||||
if command -v zenity >/dev/null 2>&1; then
|
||||
zenity --question --title="Void Upgrade" --text="$MSG" || exit 0
|
||||
fi
|
||||
pkexec xbps-install -Suy 2>&1 | tee /tmp/void-upgrade.log
|
||||
notify-send "Void Upgrade" "Upgrade finished. See /tmp/void-upgrade.log" 2>/dev/null || true
|
||||
EOF
|
||||
chmod 0755 "$TARGET/usr/local/bin/void-upgrade-gui"
|
||||
|
||||
cat > "$TARGET/usr/share/applications/void-upgrade.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Void Upgrade
|
||||
Comment=Check and apply system upgrades (with btrfs snapshot)
|
||||
Exec=void-upgrade-gui
|
||||
Icon=system-software-update
|
||||
Categories=System;PackageManager;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
# Daily check on login (autostart, runs once per day).
|
||||
cat > "$TARGET/etc/skel/.config/autostart/void-upgrade-check.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Void Upgrade Check
|
||||
Exec=sh -c '[ "$(date +%F)" != "$(cat ~/.cache/void-upgrade.last 2>/dev/null)" ] && void-upgrade-gui && date +%F > ~/.cache/void-upgrade.last'
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=true
|
||||
EOF
|
||||
cp -a "$TARGET/etc/skel/.config/autostart/void-upgrade-check.desktop" \
|
||||
"$TARGET/home/$USERNAME/.config/autostart/" 2>/dev/null || true
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config" || true
|
||||
log "void-upgrade GUI applet installed"
|
||||
}
|
||||
|
||||
_install_nemo_actions() {
|
||||
local TARGET="$1"
|
||||
install -d -m 0755 "$TARGET/usr/share/nemo/actions"
|
||||
cat > "$TARGET/usr/share/nemo/actions/open-vscode.nemo_action" <<'EOF'
|
||||
[Nemo Action]
|
||||
Active=true
|
||||
Name=Open with VS Code
|
||||
Comment=Open the selected file or folder in Visual Studio Code
|
||||
Exec=code %F
|
||||
Icon-Name=com.visualstudio.code
|
||||
Selection=Any
|
||||
Extensions=any;
|
||||
EOF
|
||||
log "Nemo 'Open with VS Code' action installed"
|
||||
}
|
||||
56
installer/lib/grub.sh
Executable file
56
installer/lib/grub.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# GRUB UEFI install with dual-boot (Windows on /dev/nvme0n1p3 via os-prober).
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
install_grub() {
|
||||
step "Installing GRUB (UEFI, bootloader-id=$BOOTLOADER_ID)"
|
||||
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
|
||||
if ! is_uefi; then
|
||||
die "non-UEFI boot detected; this installer only supports UEFI"
|
||||
fi
|
||||
|
||||
# Configure /etc/default/grub
|
||||
cat > "$TARGET/etc/default/grub" <<'GRUBEOF'
|
||||
GRUB_DEFAULT=0
|
||||
GRUB_TIMEOUT=5
|
||||
GRUB_DISTRIBUTOR="Void"
|
||||
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=4 nvidia-drm.modeset=1"
|
||||
GRUB_CMDLINE_LINUX=""
|
||||
GRUB_DISABLE_OS_PROBER=false
|
||||
GRUB_TERMINAL_OUTPUT="gfxterm"
|
||||
GRUB_GFXMODE=auto
|
||||
GRUBEOF
|
||||
|
||||
# Make sure os-prober can see the NTFS partitions to enumerate Windows.
|
||||
run_chroot "modprobe efivarfs 2>/dev/null || true"
|
||||
run_chroot "xbps-install -y os-prober ntfs-3g >/dev/null 2>&1 || true"
|
||||
|
||||
run_chroot "grub-install \
|
||||
--target=x86_64-efi \
|
||||
--efi-directory=/boot/efi \
|
||||
--bootloader-id='$BOOTLOADER_ID' \
|
||||
--recheck"
|
||||
|
||||
# Ensure os-prober actually runs (some hosts skip it without this).
|
||||
mkdir -p "$TARGET/etc/grub.d"
|
||||
|
||||
# Generate config
|
||||
run_chroot "grub-mkconfig -o /boot/grub/grub.cfg"
|
||||
|
||||
# Verify Windows entry was found (best-effort, non-fatal in test mode)
|
||||
if grep -q -i 'windows\|microsoft' "$TARGET/boot/grub/grub.cfg"; then
|
||||
ok "Windows boot entry detected in grub.cfg"
|
||||
else
|
||||
if [[ "${TEST_MODE:-0}" == "1" ]]; then
|
||||
log "no Windows entry (expected in test mode)"
|
||||
else
|
||||
warn "no Windows entry in grub.cfg — os-prober may have failed; you can re-run grub-mkconfig later"
|
||||
fi
|
||||
fi
|
||||
|
||||
ok "GRUB installed"
|
||||
}
|
||||
130
installer/lib/partition.sh
Executable file
130
installer/lib/partition.sh
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
# Partition / filesystem setup.
|
||||
# - Reformats ROOT_PART as btrfs with subvolumes
|
||||
# - Mounts everything under TARGET (default /mnt)
|
||||
# - Mounts existing EFI partition read-write at $TARGET/boot/efi (NEVER reformatted)
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
setup_filesystems() {
|
||||
step "Filesystem setup on $ROOT_PART (btrfs) + EFI share $EFI_PART"
|
||||
|
||||
[[ -b "$ROOT_PART" ]] || die "ROOT_PART $ROOT_PART not a block device"
|
||||
[[ -b "$EFI_PART" ]] || die "EFI_PART $EFI_PART not a block device"
|
||||
|
||||
# Sanity: EFI must already be vfat. We never format it.
|
||||
local efi_fs
|
||||
efi_fs=$(lsblk -no FSTYPE "$EFI_PART")
|
||||
[[ "$efi_fs" == "vfat" ]] \
|
||||
|| die "EFI partition $EFI_PART is '$efi_fs', expected vfat — refusing"
|
||||
|
||||
# Force unmount anything currently using ROOT_PART (live ISO, prior run).
|
||||
local mp
|
||||
while read -r mp; do
|
||||
[[ -n "$mp" ]] && umount -R "$mp" 2>/dev/null || true
|
||||
done < <(lsblk -nro MOUNTPOINTS "$ROOT_PART" 2>/dev/null | grep -v '^$' || true)
|
||||
umount -R "$ROOT_PART" 2>/dev/null || true
|
||||
swapoff -a 2>/dev/null || true
|
||||
|
||||
# If the live ISO's initramfs already auto-discovered a btrfs on this
|
||||
# partition (mklive runs `btrfs device scan` early), the device is
|
||||
# registered with the in-kernel btrfs module and any later mkfs sees EBUSY.
|
||||
# Forget the registration BEFORE wiping so it doesn't get re-claimed.
|
||||
btrfs device scan --forget 2>/dev/null || true
|
||||
|
||||
log "wiping filesystem signatures on $ROOT_PART"
|
||||
wipefs -af "$ROOT_PART" 2>/dev/null || true
|
||||
# Zero the first 64 MiB and last 4 MiB to obliterate btrfs primary AND
|
||||
# backup superblocks. Without this, a half-written btrfs from an
|
||||
# interrupted prior run can be auto-mounted between commands.
|
||||
dd if=/dev/zero of="$ROOT_PART" bs=1M count=64 conv=fsync 2>/dev/null || true
|
||||
local part_size_b
|
||||
part_size_b=$(blockdev --getsize64 "$ROOT_PART" 2>/dev/null || echo 0)
|
||||
if [[ "$part_size_b" -gt $((8 * 1024 * 1024)) ]]; then
|
||||
local seek_mb=$(( part_size_b / 1024 / 1024 - 4 ))
|
||||
dd if=/dev/zero of="$ROOT_PART" bs=1M count=4 seek="$seek_mb" \
|
||||
conv=fsync 2>/dev/null || true
|
||||
fi
|
||||
sync
|
||||
udevadm settle 2>/dev/null || true
|
||||
btrfs device scan --forget 2>/dev/null || true
|
||||
|
||||
log "lsblk view of $ROOT_PART:"
|
||||
lsblk -fno NAME,FSTYPE,LABEL,MOUNTPOINTS "$ROOT_PART" 2>&1 \
|
||||
| while read -r l; do log " $l"; done
|
||||
|
||||
log "creating btrfs on $ROOT_PART"
|
||||
# Stop udev from auto-claiming the device mid-format (race that causes
|
||||
# mkfs.btrfs to fail its final O_EXCL reopen with EBUSY even though the
|
||||
# superblock was written successfully).
|
||||
udevadm control --stop-exec-queue 2>/dev/null || true
|
||||
local mkfs_rc=0
|
||||
mkfs.btrfs -f -L void "$ROOT_PART" || mkfs_rc=$?
|
||||
sync
|
||||
udevadm control --start-exec-queue 2>/dev/null || true
|
||||
udevadm settle 2>/dev/null || true
|
||||
if [[ $mkfs_rc -ne 0 ]]; then
|
||||
# Tolerate nonzero exit if a valid btrfs was in fact written
|
||||
# (EBUSY-on-close race observed with btrfs-progs 6.11).
|
||||
local actual_fs
|
||||
actual_fs=$(blkid -o value -s TYPE "$ROOT_PART" 2>/dev/null || echo "")
|
||||
if [[ "$actual_fs" != "btrfs" ]]; then
|
||||
die "mkfs.btrfs failed on $ROOT_PART (rc=$mkfs_rc, fs='$actual_fs')"
|
||||
fi
|
||||
log "mkfs.btrfs exited rc=$mkfs_rc but blkid reports btrfs — continuing"
|
||||
fi
|
||||
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
mkdir -p "$TARGET"
|
||||
mount -o "${BTRFS_MOUNT_OPTS}" "$ROOT_PART" "$TARGET"
|
||||
|
||||
log "creating subvolumes: ${BTRFS_SUBVOLS[*]%%:*}"
|
||||
local entry sv
|
||||
for entry in "${BTRFS_SUBVOLS[@]}"; do
|
||||
sv="${entry%%:*}"
|
||||
# Idempotent: skip if a stale subvolume already exists from a
|
||||
# previous interrupted run (mkfs -f recreates the FS but on a fresh
|
||||
# mount the directory listing should be empty; this is defensive).
|
||||
if [[ -e "$TARGET/$sv" ]]; then
|
||||
log " subvolume $sv already present — skipping"
|
||||
continue
|
||||
fi
|
||||
btrfs subvolume create "$TARGET/$sv"
|
||||
done
|
||||
umount "$TARGET"
|
||||
|
||||
log "remounting subvolumes"
|
||||
# First mount the root subvolume (must be the first entry, conventionally @).
|
||||
local root_entry="${BTRFS_SUBVOLS[0]}"
|
||||
local root_sv="${root_entry%%:*}"
|
||||
mount -o "${BTRFS_MOUNT_OPTS},subvol=${root_sv}" "$ROOT_PART" "$TARGET"
|
||||
|
||||
# Pre-create mountpoints + EFI dir.
|
||||
mkdir -p "$TARGET/boot/efi"
|
||||
for entry in "${BTRFS_SUBVOLS[@]:1}"; do
|
||||
local mp="${entry##*:}"
|
||||
mkdir -p "$TARGET$mp"
|
||||
done
|
||||
|
||||
for entry in "${BTRFS_SUBVOLS[@]:1}"; do
|
||||
sv="${entry%%:*}"
|
||||
local mp="${entry##*:}"
|
||||
mount -o "${BTRFS_MOUNT_OPTS},subvol=${sv}" "$ROOT_PART" "$TARGET$mp"
|
||||
done
|
||||
|
||||
# Mount shared EFI WITHOUT formatting.
|
||||
mount "$EFI_PART" "$TARGET/boot/efi"
|
||||
|
||||
# Make sure we're not about to clobber Windows' EFI loader.
|
||||
if [[ -d "$TARGET/boot/efi/EFI/Microsoft" ]]; then
|
||||
log "Windows EFI files detected — will preserve EFI/Microsoft/* untouched"
|
||||
else
|
||||
warn "no EFI/Microsoft dir found on $EFI_PART — proceeding anyway"
|
||||
fi
|
||||
|
||||
ok "filesystems mounted under $TARGET"
|
||||
findmnt -R "$TARGET" >> "$LOG_FILE"
|
||||
|
||||
export TARGET
|
||||
}
|
||||
351
installer/lib/postinstall.sh
Executable file
351
installer/lib/postinstall.sh
Executable file
@@ -0,0 +1,351 @@
|
||||
#!/bin/bash
|
||||
# Post-install configuration:
|
||||
# - hostname / locale / keyboard / timezone / hwclock
|
||||
# - users (root + moze) + sudo
|
||||
# - services (NetworkManager, lightdm, dbus, polkitd, docker, bluetoothd,
|
||||
# acpid, tlp, sshd[disabled], dhcpcd[disabled in favor of NM])
|
||||
# - zram swap (zramen)
|
||||
# - NVIDIA PRIME render-offload setup
|
||||
# - SSH config copy
|
||||
# - Nix bootstrap (nix-daemon service + first-boot user package install)
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
configure_system() {
|
||||
step "System configuration"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
|
||||
# ----- hostname -----
|
||||
echo "$HOSTNAME" > "$TARGET/etc/hostname"
|
||||
cat > "$TARGET/etc/hosts" <<EOF
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
127.0.1.1 $HOSTNAME.localdomain $HOSTNAME
|
||||
EOF
|
||||
|
||||
# ----- rc.conf -----
|
||||
cat > "$TARGET/etc/rc.conf" <<EOF
|
||||
KEYMAP="$KEYMAP"
|
||||
HARDWARECLOCK="$HARDWARECLOCK"
|
||||
TIMEZONE="$TIMEZONE"
|
||||
EOF
|
||||
|
||||
# ----- timezone (also create symlink for systems checking it) -----
|
||||
run_chroot "ln -sf /usr/share/zoneinfo/$TIMEZONE /etc/localtime"
|
||||
|
||||
# ----- locales (glibc) -----
|
||||
if [[ -f "$TARGET/etc/default/libc-locales" ]]; then
|
||||
sed -i "s/^#\($LOCALE.*\)/\1/" "$TARGET/etc/default/libc-locales"
|
||||
run_chroot "xbps-reconfigure -f glibc-locales"
|
||||
fi
|
||||
cat > "$TARGET/etc/locale.conf" <<EOF
|
||||
LANG=$LANG
|
||||
LC_ALL=$LANG
|
||||
EOF
|
||||
|
||||
# ----- vconsole (for early TTY keymap) -----
|
||||
cat > "$TARGET/etc/vconsole.conf" <<EOF
|
||||
KEYMAP=$KEYMAP
|
||||
EOF
|
||||
|
||||
ok "locale / keymap / tz configured"
|
||||
}
|
||||
|
||||
configure_users() {
|
||||
step "Creating users"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
|
||||
# ----- root password (chpasswd via stdin — no shell expansion of $) -----
|
||||
set_chroot_password root "$ROOT_PASSWORD"
|
||||
|
||||
# ----- ensure groups exist -----
|
||||
run_chroot "groupadd -f docker"
|
||||
run_chroot "groupadd -f plugdev"
|
||||
|
||||
# ----- create user (idempotent: tolerate 'already exists', fail on real errors) -----
|
||||
if ! run_chroot "id -u $USERNAME >/dev/null 2>&1"; then
|
||||
run_chroot "useradd -m -u $USER_UID -G $USER_GROUPS -s $DEFAULT_SHELL -c '$USER_FULLNAME' $USERNAME"
|
||||
else
|
||||
log "user $USERNAME already exists — skipping useradd"
|
||||
run_chroot "usermod -G $USER_GROUPS -s $DEFAULT_SHELL $USERNAME"
|
||||
fi
|
||||
set_chroot_password "$USERNAME" "$USER_PASSWORD"
|
||||
|
||||
# ----- sudoers: wheel group -----
|
||||
mkdir -p "$TARGET/etc/sudoers.d"
|
||||
if [[ "${TEST_MODE:-0}" == "1" ]]; then
|
||||
# Test harness needs passwordless sudo to run smoke checks via SSH.
|
||||
cat > "$TARGET/etc/sudoers.d/10-wheel" <<'EOF'
|
||||
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
Defaults env_keep += "EDITOR"
|
||||
EOF
|
||||
else
|
||||
cat > "$TARGET/etc/sudoers.d/10-wheel" <<'EOF'
|
||||
%wheel ALL=(ALL:ALL) ALL
|
||||
Defaults env_keep += "EDITOR"
|
||||
EOF
|
||||
fi
|
||||
chmod 440 "$TARGET/etc/sudoers.d/10-wheel"
|
||||
|
||||
ok "user '$USERNAME' created and added to: $USER_GROUPS"
|
||||
}
|
||||
|
||||
configure_ssh_config() {
|
||||
step "Installing SSH config for $USERNAME"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
local src="$SSH_SOURCE_DIR"
|
||||
local dst="$TARGET/home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||
|
||||
if [[ ! -d "$src" ]]; then
|
||||
warn "no SSH source dir at $src — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
install -d -m 0700 "$dst"
|
||||
cp -a "$src"/. "$dst/"
|
||||
# Tighten perms.
|
||||
find "$dst" -type d -exec chmod 700 {} +
|
||||
find "$dst" -type f -exec chmod 600 {} +
|
||||
find "$dst" -type f -name '*.pub' -exec chmod 644 {} +
|
||||
[[ -f "$dst/known_hosts" ]] && chmod 644 "$dst/known_hosts"
|
||||
[[ -f "$dst/known_hosts.old" ]] && chmod 644 "$dst/known_hosts.old"
|
||||
[[ -f "$dst/config" ]] && chmod 600 "$dst/config"
|
||||
|
||||
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||
ok "SSH config installed at /home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||
}
|
||||
|
||||
configure_nvidia_prime() {
|
||||
step "Configuring NVIDIA PRIME render-offload"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
|
||||
# 1) Xorg config: Intel as primary, NVIDIA as PRIME provider.
|
||||
install -d -m 0755 "$TARGET/etc/X11/xorg.conf.d"
|
||||
cat > "$TARGET/etc/X11/xorg.conf.d/10-intel.conf" <<'EOF'
|
||||
Section "OutputClass"
|
||||
Identifier "intel"
|
||||
MatchDriver "i915"
|
||||
Driver "modesetting"
|
||||
EndSection
|
||||
EOF
|
||||
cat > "$TARGET/etc/X11/xorg.conf.d/20-nvidia.conf" <<'EOF'
|
||||
Section "OutputClass"
|
||||
Identifier "nvidia"
|
||||
MatchDriver "nvidia-drm"
|
||||
Driver "nvidia"
|
||||
Option "AllowEmptyInitialConfiguration"
|
||||
Option "PrimaryGPU" "no"
|
||||
ModulePath "/usr/lib/nvidia/xorg"
|
||||
ModulePath "/usr/lib/xorg/modules"
|
||||
EndSection
|
||||
EOF
|
||||
|
||||
# 2) Modules to load early for KMS.
|
||||
install -d -m 0755 "$TARGET/etc/modules-load.d"
|
||||
cat > "$TARGET/etc/modules-load.d/nvidia.conf" <<'EOF'
|
||||
nvidia
|
||||
nvidia_modeset
|
||||
nvidia_uvm
|
||||
nvidia_drm
|
||||
EOF
|
||||
|
||||
# 3) Wrapper script + desktop file: run any app on NVIDIA via `prime-run`.
|
||||
install -d -m 0755 "$TARGET/usr/local/bin"
|
||||
cat > "$TARGET/usr/local/bin/prime-run" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Run a program on the NVIDIA dGPU via PRIME render offload.
|
||||
exec env __NV_PRIME_RENDER_OFFLOAD=1 \
|
||||
__VK_LAYER_NV_optimus=NVIDIA_only \
|
||||
__GLX_VENDOR_LIBRARY_NAME=nvidia \
|
||||
"$@"
|
||||
EOF
|
||||
chmod 0755 "$TARGET/usr/local/bin/prime-run"
|
||||
|
||||
# 4) Make sure dracut picks up nvidia modules.
|
||||
install -d -m 0755 "$TARGET/etc/dracut.conf.d"
|
||||
cat > "$TARGET/etc/dracut.conf.d/10-nvidia.conf" <<'EOF'
|
||||
add_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "
|
||||
EOF
|
||||
|
||||
ok "NVIDIA PRIME offload configured (use 'prime-run <app>')"
|
||||
}
|
||||
|
||||
configure_zram() {
|
||||
[[ "${ZRAM_ENABLE:-yes}" == "yes" ]] || return 0
|
||||
step "Configuring zram (zramen)"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
install -d -m 0755 "$TARGET/etc/default"
|
||||
cat > "$TARGET/etc/default/zramen" <<EOF
|
||||
ALGO=zstd
|
||||
PERCENT=$ZRAM_SIZE_PCT
|
||||
PRIORITY=100
|
||||
EOF
|
||||
ok "zram configured at $ZRAM_SIZE_PCT% RAM"
|
||||
}
|
||||
|
||||
configure_nix() {
|
||||
[[ "${ENABLE_NIX:-yes}" == "yes" ]] || return 0
|
||||
step "Configuring nix multi-user (daemon mode)"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
# Void's `nix` xbps package installs to /usr and ships a runit service.
|
||||
install -d -m 0755 "$TARGET/etc/nix"
|
||||
cat > "$TARGET/etc/nix/nix.conf" <<'EOF'
|
||||
experimental-features = nix-command flakes
|
||||
build-users-group = nixbld
|
||||
auto-optimise-store = true
|
||||
sandbox = true
|
||||
EOF
|
||||
|
||||
# First-boot script: as $USERNAME, install user packages.
|
||||
install -d -m 0755 "$TARGET/usr/local/libexec"
|
||||
cat > "$TARGET/usr/local/libexec/first-boot-nix.sh" <<EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
mark=/var/lib/first-boot-nix.done
|
||||
[[ -f "\$mark" ]] && exit 0
|
||||
|
||||
# Wait for nix-daemon to be available.
|
||||
# The Void package puts the socket at /var/nix/daemon-socket/nix-daemon.sock
|
||||
# (NOT /nix/var/nix/...).
|
||||
for _ in \$(seq 1 30); do
|
||||
[[ -S /var/nix/daemon-socket/nix-daemon.sock ]] && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ ! -S /var/nix/daemon-socket/nix-daemon.sock ]]; then
|
||||
echo "nix-daemon not available; aborting first-boot nix install" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
su - $USERNAME -c '
|
||||
set -e
|
||||
. /etc/profile.d/nix.sh 2>/dev/null || true
|
||||
# google-chrome / spotify / discord are unfree -> need allow-unfree + --impure.
|
||||
export NIXPKGS_ALLOW_UNFREE=1
|
||||
nix profile install --impure ${NIX_USER_PACKAGES[*]} || true
|
||||
'
|
||||
|
||||
mkdir -p "\$(dirname "\$mark")"
|
||||
touch "\$mark"
|
||||
EOF
|
||||
chmod 0755 "$TARGET/usr/local/libexec/first-boot-nix.sh"
|
||||
|
||||
# runit one-shot service.
|
||||
install -d -m 0755 "$TARGET/etc/sv/first-boot-nix"
|
||||
cat > "$TARGET/etc/sv/first-boot-nix/run" <<'EOF'
|
||||
#!/bin/sh
|
||||
exec 2>&1
|
||||
/usr/local/libexec/first-boot-nix.sh
|
||||
exec chpst -b first-boot-nix pause
|
||||
EOF
|
||||
chmod 0755 "$TARGET/etc/sv/first-boot-nix/run"
|
||||
cat > "$TARGET/etc/sv/first-boot-nix/finish" <<'EOF'
|
||||
#!/bin/sh
|
||||
sv down first-boot-nix
|
||||
EOF
|
||||
chmod 0755 "$TARGET/etc/sv/first-boot-nix/finish"
|
||||
|
||||
ok "Nix configured; user packages will install on first boot"
|
||||
}
|
||||
|
||||
install_vscode_real() {
|
||||
# Install official Microsoft VS Code (the real proprietary build), NOT the
|
||||
# `vscode` xbps package which is actually code-oss and ships `code-oss`.
|
||||
step "Installing official Microsoft VS Code"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
local url="https://update.code.visualstudio.com/latest/linux-x64/stable"
|
||||
local tmp="$TARGET/tmp/vscode.tar.gz"
|
||||
|
||||
install -d -m 0755 "$TARGET/opt" "$TARGET/usr/local/bin"
|
||||
|
||||
if ! run_chroot "curl -fsSL --retry 3 -o /tmp/vscode.tar.gz '$url'"; then
|
||||
warn "failed to download VS Code; skipping (install manually later)"
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Tarball extracts to VSCode-linux-x64/. Move it to /opt/vscode.
|
||||
rm -rf "$TARGET/opt/vscode"
|
||||
if ! run_chroot "tar -xzf /tmp/vscode.tar.gz -C /opt && mv /opt/VSCode-linux-x64 /opt/vscode"; then
|
||||
warn "failed to extract VS Code tarball; skipping"
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
# `code` shim on PATH.
|
||||
ln -sf /opt/vscode/bin/code "$TARGET/usr/local/bin/code"
|
||||
|
||||
# Desktop entry so it shows up in the Cinnamon menu.
|
||||
install -d -m 0755 "$TARGET/usr/local/share/applications"
|
||||
cat > "$TARGET/usr/local/share/applications/code.desktop" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Visual Studio Code
|
||||
Comment=Code Editing. Redefined.
|
||||
GenericName=Text Editor
|
||||
Exec=/opt/vscode/bin/code %F
|
||||
Icon=/opt/vscode/resources/app/resources/linux/code.png
|
||||
Type=Application
|
||||
StartupNotify=false
|
||||
StartupWMClass=Code
|
||||
Categories=TextEditor;Development;IDE;
|
||||
MimeType=text/plain;inode/directory;application/x-code-workspace;
|
||||
Actions=new-empty-window;
|
||||
Keywords=vscode;
|
||||
|
||||
[Desktop Action new-empty-window]
|
||||
Name=New Empty Window
|
||||
Exec=/opt/vscode/bin/code --new-window %F
|
||||
Icon=/opt/vscode/resources/app/resources/linux/code.png
|
||||
EOF
|
||||
|
||||
ok "VS Code installed at /opt/vscode (use 'code' on PATH)"
|
||||
}
|
||||
|
||||
enable_services() {
|
||||
step "Enabling services (runit)"
|
||||
local TARGET="${TARGET:-/mnt}"
|
||||
local svdir="$TARGET/etc/runit/runsvdir/default"
|
||||
install -d -m 0755 "$svdir"
|
||||
|
||||
local svc
|
||||
local enabled=(
|
||||
dbus
|
||||
NetworkManager
|
||||
lightdm
|
||||
polkitd
|
||||
docker
|
||||
bluetoothd
|
||||
acpid
|
||||
tlp
|
||||
elogind
|
||||
chronyd
|
||||
nix-daemon
|
||||
first-boot-nix
|
||||
zramen
|
||||
cupsd
|
||||
cups-browsed
|
||||
)
|
||||
[[ "${SSHD_ENABLE:-no}" == "yes" ]] && enabled+=(sshd)
|
||||
|
||||
for svc in "${enabled[@]}"; do
|
||||
if [[ -d "$TARGET/etc/sv/$svc" ]]; then
|
||||
ln -sf "/etc/sv/$svc" "$svdir/$svc"
|
||||
log "enabled $svc"
|
||||
else
|
||||
warn "no service dir /etc/sv/$svc — skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
# Disable dhcpcd if it's there (NetworkManager handles it).
|
||||
rm -f "$svdir/dhcpcd" 2>/dev/null || true
|
||||
|
||||
ok "services enabled"
|
||||
}
|
||||
|
||||
reconfigure_all() {
|
||||
step "Reconfiguring all packages (initramfs + grub artifacts)"
|
||||
run_chroot "xbps-reconfigure -fa"
|
||||
ok "reconfigure complete"
|
||||
}
|
||||
53
installer/lib/profiles.sh
Normal file
53
installer/lib/profiles.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Profile loading. PROFILE env var (default: stable-cinnamon) selects which
|
||||
# config/profiles/<name>/profile.conf is sourced and which packages.list is
|
||||
# used. Every variable defined in profile.conf overrides install.conf.
|
||||
|
||||
PROFILES_DIR="${PROFILES_DIR:-${PROJECT_DIR:-/usr/local/share/installer}/profiles}"
|
||||
|
||||
load_profile() {
|
||||
local profile="${PROFILE:-stable-cinnamon}"
|
||||
local pdir="$PROFILES_DIR/$profile"
|
||||
|
||||
if [[ ! -d "$pdir" ]]; then
|
||||
echo "[ERR] profile '$profile' not found at $pdir" >&2
|
||||
echo "Available profiles:" >&2
|
||||
ls -1 "$PROFILES_DIR" 2>/dev/null | sed 's/^/ - /' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -r "$pdir/profile.conf" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$pdir/profile.conf"
|
||||
fi
|
||||
|
||||
export PROFILE="$profile"
|
||||
export PROFILE_DIR="$pdir"
|
||||
# Resolve packages list path (profile.conf may set it relative or absolute).
|
||||
if [[ -n "$PROFILE_PACKAGES_FILE" && ! -r "$PROFILE_PACKAGES_FILE" ]]; then
|
||||
# Try resolving relative to profile dir, then project root.
|
||||
if [[ -r "$pdir/$(basename "$PROFILE_PACKAGES_FILE")" ]]; then
|
||||
PROFILE_PACKAGES_FILE="$pdir/$(basename "$PROFILE_PACKAGES_FILE")"
|
||||
elif [[ -r "${PROJECT_DIR:-.}/$PROFILE_PACKAGES_FILE" ]]; then
|
||||
PROFILE_PACKAGES_FILE="${PROJECT_DIR:-.}/$PROFILE_PACKAGES_FILE"
|
||||
fi
|
||||
fi
|
||||
[[ -z "$PROFILE_PACKAGES_FILE" ]] && PROFILE_PACKAGES_FILE="$pdir/packages.list"
|
||||
export PROFILE_PACKAGES_FILE
|
||||
|
||||
echo "[INFO] profile loaded: $PROFILE ($PROFILE_DESC)"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_profile_customizations() {
|
||||
# Source every *.sh under <profile>/customizations/ in name order.
|
||||
local cdir="$PROFILE_DIR/customizations"
|
||||
[[ -d "$cdir" ]] || { echo "[INFO] no profile customizations dir at $cdir"; return 0; }
|
||||
local hook
|
||||
for hook in "$cdir"/*.sh; do
|
||||
[[ -r "$hook" ]] || continue
|
||||
echo "[INFO] running profile hook: $(basename "$hook")"
|
||||
# shellcheck disable=SC1090
|
||||
source "$hook"
|
||||
done
|
||||
}
|
||||
77
installer/lib/tui.sh
Executable file
77
installer/lib/tui.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# TUI disk selection. Uses `dialog` to show detected disks/partitions
|
||||
# and require explicit user confirmation before any destructive action.
|
||||
# Sets globals: TARGET_DISK, ROOT_PART, EFI_PART
|
||||
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
tui_select_install_target() {
|
||||
step "Disk selection"
|
||||
|
||||
local default_root="${DEFAULT_ROOT_PART:-}"
|
||||
local default_efi="${DEFAULT_EFI_PART:-}"
|
||||
|
||||
# Build a human menu of partitions (skip loop/ram/zram, only TYPE=part).
|
||||
local menu_items=()
|
||||
local dev type fstype size label
|
||||
while read -r dev type fstype size label; do
|
||||
[[ "$type" == "part" ]] || continue
|
||||
[[ "$dev" =~ ^/dev/(sd|nvme|vd|mmcblk|hd|xvd) ]] || continue
|
||||
local marker=""
|
||||
[[ "$dev" == "$default_root" ]] && marker=" (DEFAULT ROOT)"
|
||||
[[ "$fstype" == "vfat" ]] && marker+=" [EFI?]"
|
||||
[[ "$fstype" == "ntfs" ]] && marker+=" [WINDOWS - DO NOT TOUCH]"
|
||||
menu_items+=("$dev" "${fstype:-?} ${size} '${label:-}'${marker}")
|
||||
done < <(lsblk -lnpo NAME,TYPE,FSTYPE,SIZE,LABEL 2>/dev/null || true)
|
||||
|
||||
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
||||
log "lsblk output for diagnosis:"
|
||||
lsblk -lnpo NAME,TYPE,FSTYPE,SIZE,LABEL 2>&1 | while read -r l; do log " $l"; done
|
||||
die "no candidate partitions found"
|
||||
fi
|
||||
|
||||
local choice
|
||||
if [[ "${UNATTENDED:-0}" == "1" ]]; then
|
||||
choice="$default_root"
|
||||
log "[unattended] target root partition = $choice"
|
||||
else
|
||||
choice=$(dialog --stdout --title "Void Installer — SELECT ROOT PARTITION" \
|
||||
--backtitle "WARNING: the chosen partition will be WIPED. Windows partitions show [WINDOWS]." \
|
||||
--default-item "$default_root" \
|
||||
--menu "Choose the partition to install Void Linux onto.\nDefault highlights $default_root (current Linux Mint).\nAbsolutely DO NOT pick a partition labelled [WINDOWS]." \
|
||||
25 90 14 "${menu_items[@]}") \
|
||||
|| die "user cancelled disk selection"
|
||||
fi
|
||||
|
||||
[[ -b "$choice" ]] || die "selected device $choice is not a block device"
|
||||
local fstype
|
||||
fstype=$(lsblk -no FSTYPE "$choice" 2>/dev/null | head -1)
|
||||
if [[ "$fstype" == "ntfs" ]]; then
|
||||
die "REFUSING to wipe NTFS partition $choice (looks like Windows)"
|
||||
fi
|
||||
|
||||
ROOT_PART="$choice"
|
||||
EFI_PART="${default_efi}"
|
||||
# Resolve the parent block device robustly (works for nvme, mmcblk, sd*, vd*).
|
||||
TARGET_DISK="/dev/$(lsblk -no PKNAME "$choice" 2>/dev/null | head -1)"
|
||||
[[ "$TARGET_DISK" == "/dev/" ]] && TARGET_DISK="$DEFAULT_DISK"
|
||||
|
||||
# Confirmation step: must type the partition device name verbatim.
|
||||
if [[ "${UNATTENDED:-0}" != "1" ]]; then
|
||||
local typed
|
||||
typed=$(dialog --stdout --title "FINAL CONFIRMATION" \
|
||||
--backtitle "Type the device name to confirm WIPE" \
|
||||
--inputbox "About to:\n - WIPE : $ROOT_PART (will become btrfs)\n - SHARE : $EFI_PART (kept intact, only adds /EFI/Void)\n - LEAVE : everything else (Windows, recovery)\n\nType the FULL device path of the partition to wipe to continue:" \
|
||||
18 75) \
|
||||
|| die "user cancelled confirmation"
|
||||
[[ "$typed" == "$ROOT_PART" ]] \
|
||||
|| die "confirmation mismatch (typed '$typed' != '$ROOT_PART')"
|
||||
fi
|
||||
|
||||
ok "target root : $ROOT_PART"
|
||||
ok "shared EFI : $EFI_PART"
|
||||
ok "parent disk : $TARGET_DISK"
|
||||
|
||||
export TARGET_DISK ROOT_PART EFI_PART
|
||||
}
|
||||
Reference in New Issue
Block a user