Implement wrapper workflow and btrfs layout

This commit is contained in:
Stefan Strobl 2025-12-24 14:36:53 +01:00
parent 1fc9fa0f5f
commit a6e5399572
12 changed files with 868 additions and 4 deletions

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# Void Bootstrapp Wrapper
Ein Bash-Wrapper, der die kritischen Vorarbeiten fuer eine verschluesselte Void-Linux-Installation erledigt und danach den offiziellen `void-installer` nutzt.
## Ablauf
1. Sanity Checks (Root, UEFI, Zielplatte)
2. Partitionierung (GPT, ESP + Root)
3. LUKS-Verschluesselung und Mapping
4. Dateisysteme (btrfs oder ext4)
5. Mounts fuer den Installer
6. Installer-Handoff (manuelle Mountpoints, kein Reformat)
7. Post-Install (crypttab, dracut, GRUB, optional Swapfile)
## Usage
```bash
sudo ./src/main.sh [--dry-run] [--skip-installer]
```
## Flags
- `--dry-run`: Zeigt die Konfiguration und bricht ohne Aenderungen ab.
- `--skip-installer`: Laesst den Start von `void-installer` aus (manuell starten).
## Installer Bedienung nach dem Script
Im `void-installer` musst du die vorbereiteten Mounts uebernehmen und darfst nichts formatieren.
1. Waehle die manuelle Partitionierung.
2. Setze nur die Mountpoints, ohne Formatierung.
3. Root:
- Device: `/dev/mapper/cryptroot`
- Label: `void-root` (btrfs und ext4)
- Mountpoint: `/`
- Format: aus
4. ESP:
- Device: erste Partition der Disk (meist `...1`)
- Label: `EFI`
- Mountpoint: `/boot/efi`
- Format: aus
Wenn Labels nicht angezeigt werden, pruefe mit `lsblk -f`, welches Device `void-root` bzw. `EFI` traegt.
Bei btrfs sind Subvolumes bereits angelegt und gemountet: `@`, `@home`, `@var`, `@log`, `@snapshots`, `@swap`.
Der Installer soll keine neuen Subvolumes anlegen oder formatieren, sondern die vorhandenen Mounts unveraendert lassen.
Es reicht, im Installer nur `/` und `/boot/efi` zu setzen; die Subvolumes werden im Post-Install in `/etc/fstab` eingetragen.
Mapping der Subvolumes (gesetzt durch den Wrapper/Post-Install):
- `@` -> `/`
- `@home` -> `/home`
- `@var` -> `/var`
- `@log` -> `/var/log`
- `@snapshots` -> `/.snapshots`
- `@swap` -> `/swap`
## Hinweise
- Der Wrapper ist destruktiv und loescht die Zielplatte nach expliziter Bestaetigung.
- UEFI ist Pflicht; BIOS-Installationen sind nicht unterstuetzt.
- Logs landen unter `/tmp/void-wrapper-YYYY-MM-DD-HHMMSS.log`.

View File

@ -1,13 +1,19 @@
#!/usr/bin/env bash
# === Motivation ===
# Centralize user-provided inputs to avoid scattered magic values.
# === Problem Statement ===
# Disk targets, encryption mode, filesystem choice, and host metadata need a single source of truth.
# === Scope ===
# In scope: definition of configurable parameters and validation expectations.
# Out of scope: parsing implementation and storage mechanism.
# === Concepts ===
# Preset: a named set of config values for repeatable installs.
# Interactive input: prompting the user for values at runtime.
# === Decisions ===
# Keep defaults conservative and require explicit confirmation for destructive values.
# Surface all critical values before any destructive phase begins.
@ -18,14 +24,167 @@
# Swap defaults to a file (works with both btrfs and ext4).
# Mandatory values are DISK, HOSTNAME, and size inputs where no safe default exists.
# Interactive prompts are the default input mechanism; no preset formats are defined yet.
# === Alternatives Considered ===
# Hard-coded values rejected because they do not scale across machines.
# === Constraints ===
# Config must be readable and editable without specialized tools.
# Target boot mode is UEFI only; BIOS is out of scope.
# === Open Questions ===
# Should we support config file presets for batch installations, or keep it fully interactive?
# How should we validate user-provided disk paths to prevent typos?
# Should swap size have intelligent defaults based on RAM size, or always ask?
# === Success Criteria ===
# Users can clearly see and confirm target disk, encryption type, and filesystem choice.
export DISK=""
export HOSTNAME=""
export FS_TYPE="btrfs"
export LUKS_VERSION="2"
export ESP_SIZE="1GiB"
export ROOT_END="100%"
export SWAP_SIZE="4GiB"
export CRYPT_NAME="cryptroot"
export MOUNT_ROOT="/mnt"
export ESP_MOUNT="/mnt/boot/efi"
export ROOT_LABEL="void-root"
export EFI_LABEL="EFI"
prompt_with_default() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
local input
read -r -p "${prompt_text} [${default_value}]: " input
if [[ -z "$input" ]]; then
input="$default_value"
fi
printf -v "$var_name" '%s' "$input"
}
prompt_required() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
local input=""
while [[ -z "$input" ]]; do
read -r -p "${prompt_text} [${default_value}]: " input
if [[ -z "$input" ]]; then
input="$default_value"
fi
if [[ -z "$input" ]]; then
log_warn "Value is required."
fi
done
printf -v "$var_name" '%s' "$input"
}
prompt_choice() {
local var_name="$1"
local prompt_text="$2"
local default_value="$3"
shift 3
local options=("$@")
local input=""
local valid=""
while [[ -z "$valid" ]]; do
read -r -p "${prompt_text} [${default_value}]: " input
if [[ -z "$input" ]]; then
input="$default_value"
fi
for option in "${options[@]}"; do
if [[ "$input" == "$option" ]]; then
valid="yes"
break
fi
done
if [[ -z "$valid" ]]; then
log_warn "Invalid choice. Allowed: ${options[*]}"
fi
done
printf -v "$var_name" '%s' "$input"
}
config_prompt_interactive() {
require_command lsblk
log_info "Available disks:"
lsblk -dpno NAME,SIZE,TYPE | awk '$3=="disk" {printf " %s (%s)\n", $1, $2}'
prompt_required DISK "Target disk (e.g. /dev/sda)" "${DISK}"
prompt_required HOSTNAME "Hostname" "${HOSTNAME}"
prompt_choice FS_TYPE "Filesystem (btrfs/ext4)" "${FS_TYPE}" "btrfs" "ext4"
prompt_choice LUKS_VERSION "LUKS version (2/1)" "${LUKS_VERSION}" "2" "1"
prompt_with_default ESP_SIZE "ESP size (e.g. 1GiB)" "${ESP_SIZE}"
prompt_with_default ROOT_END "Root partition end (e.g. 100% or 200GiB)" "${ROOT_END}"
prompt_with_default SWAP_SIZE "Swap size (e.g. 4GiB, 0 to disable)" "${SWAP_SIZE}"
}
config_validate() {
if [[ -z "$DISK" ]]; then
die "Target disk is required."
fi
if [[ -z "$HOSTNAME" ]]; then
die "Hostname is required."
fi
case "$FS_TYPE" in
btrfs|ext4) ;;
*) die "Unsupported filesystem: $FS_TYPE" ;;
esac
case "$LUKS_VERSION" in
1|2) ;;
*) die "Unsupported LUKS version: $LUKS_VERSION" ;;
esac
if [[ "$SWAP_SIZE" == "0" || "$SWAP_SIZE" == "0G" || "$SWAP_SIZE" == "0GiB" ]]; then
SWAP_SIZE="0"
fi
}
config_print_summary() {
: "${DISK:?Target disk is required}"
: "${HOSTNAME:?Hostname is required}"
: "${FS_TYPE:?Filesystem type is required}"
: "${LUKS_VERSION:?LUKS version is required}"
: "${MOUNT_ROOT:?Mount root is required}"
: "${ESP_MOUNT:?ESP mount path is required}"
log_info "Configuration summary:"
log_info " Disk: $DISK"
log_info " Hostname: $HOSTNAME"
log_info " Filesystem: $FS_TYPE"
log_info " LUKS version: $LUKS_VERSION"
log_info " ESP size: $ESP_SIZE"
log_info " Root end: $ROOT_END"
log_info " Swap size: ${SWAP_SIZE}"
log_info " Mount root: $MOUNT_ROOT"
log_info " ESP mount: $ESP_MOUNT"
}
config_confirm_destructive() {
: "${DISK:?Target disk is required}"
local confirmation
log_warn "All data on $DISK will be destroyed."
read -r -p "Type the target disk to confirm: " confirmation
if [[ "$confirmation" != "$DISK" ]]; then
die "Disk confirmation failed."
fi
read -r -p "Type YES to continue: " confirmation
if [[ "$confirmation" != "YES" ]]; then
die "Confirmation not received. Aborting."
fi
}
swap_enabled() {
[[ "$SWAP_SIZE" != "0" ]]
}

View File

@ -1,13 +1,19 @@
#!/usr/bin/env bash
# === Motivation ===
# Protect user data with full-disk encryption while keeping boot reliable.
# === Problem Statement ===
# The wrapper must create and open an encrypted container for the root filesystem.
# === Scope ===
# In scope: encryption mode selection, key handling policy, and naming conventions.
# Out of scope: specific encryption tool commands.
# === Concepts ===
# LUKS version: compatibility trade-off between bootloader support and features.
# Mapping name: the identifier exposed for the opened container.
# === Decisions ===
# Default to LUKS2 to benefit from improved security features and tooling.
# Never store passphrases in the script or config files.
@ -15,16 +21,37 @@
# Use default LUKS2 settings (PBKDF2 or Argon2i) for key derivation.
# If GRUB fails to unlock LUKS2 after installation, provide clear guidance to recreate with LUKS1.
# Do not auto-downgrade to LUKS1 silently; make the user aware of the compatibility issue.
# === Alternatives Considered ===
# Unencrypted root rejected because it defeats the goal.
# Defaulting to LUKS1 rejected because LUKS2 improves maintainability and security tooling.
# Auto-detecting GRUB LUKS2 support rejected due to complexity and version fragmentation.
# === Constraints ===
# Bootloader support limits available encryption features when /boot is inside encrypted root.
# GRUB versions before 2.06 may have incomplete LUKS2 support depending on distribution patches.
# === Open Questions ===
# Should we allow users to customize LUKS2 PBKDF parameters (iterations, memory), or use defaults?
# How do we handle GRUB boot failures with LUKS2 - provide recovery instructions or force LUKS1?
# Should the wrapper test GRUB's ability to unlock LUKS2 before completing installation?
# === Success Criteria ===
# The encrypted container opens reliably and is ready for filesystem creation.
encrypt_root() {
: "${ROOT_PART:?Root partition is required}"
: "${LUKS_VERSION:?LUKS version is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
local luks_type="luks2"
if [[ "$LUKS_VERSION" == "1" ]]; then
luks_type="luks1"
fi
log_info "Creating LUKS container ($luks_type) on $ROOT_PART"
cryptsetup luksFormat --type "$luks_type" "$ROOT_PART"
log_info "Opening LUKS container as $CRYPT_NAME"
cryptsetup open "$ROOT_PART" "$CRYPT_NAME"
}

View File

@ -1,20 +1,28 @@
#!/usr/bin/env bash
# === Motivation ===
# Provide a stable filesystem base aligned with user intent.
# === Problem Statement ===
# The wrapper must format the root and boot partitions using the selected filesystem.
# === Scope ===
# In scope: filesystem options, defaults, and naming.
# Out of scope: exact formatting command flags.
# === Concepts ===
# Root filesystem: primary data store inside the encrypted container.
# Boot filesystem: unencrypted partition for firmware and bootloader data.
# === Decisions ===
# Default to btrfs with opt-in alternatives for users who want ext4.
# Ensure filesystem choices are explicit and confirmed before formatting.
# Use swap file instead of swap partition to keep layout simple and flexible.
# Swap file lives inside encrypted root for automatic encryption protection.
# Swap file size is chosen interactively based on system needs.
# For btrfs, swap file must reside on a No-COW subvolume to avoid corruption.
# For btrfs, swap file must reside on a No-COW directory to avoid corruption.
# For btrfs, create subvolumes for root, home, var, log, snapshots, and swap.
# === Alternatives Considered ===
# Automatic detection of best filesystem rejected to keep behavior predictable.
# Swap partition rejected because it requires either:
@ -22,14 +30,51 @@
# - Unencrypted swap (security risk: sensitive data in swap)
# - LVM setup (out of scope for this wrapper)
# Swap partition also has fixed size, while swap file can be resized post-install.
# === Constraints ===
# Filesystem choice must be compatible with the initramfs and bootloader.
# Hibernation with swap file requires kernel >= 5.0 and resume_offset parameter.
# For btrfs swap files, COW must be disabled on the swap subvolume.
# For btrfs swap files, COW must be disabled on the swap directory.
# === Open Questions ===
# Should btrfs subvolumes be created for root, home, and swap, or keep it flat?
# How should we handle swap file creation - in this phase or defer to post-install?
# Should we support configurable filesystem options (compression, mount flags)?
# === Success Criteria ===
# Filesystems are prepared in the right places and match the chosen layout.
# Swap file is ready for activation and resides within encrypted root.
format_filesystems() {
: "${ESP_PART:?ESP partition is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
: "${FS_TYPE:?Filesystem type is required}"
: "${ROOT_LABEL:?Root filesystem label is required}"
: "${EFI_LABEL:?EFI filesystem label is required}"
local root_device
root_device="/dev/mapper/${CRYPT_NAME}"
log_info "Formatting ESP on $ESP_PART"
mkfs.vfat -F32 -n "$EFI_LABEL" "$ESP_PART"
if [[ "$FS_TYPE" == "btrfs" ]]; then
log_info "Formatting root as btrfs on $root_device"
mkfs.btrfs -L "$ROOT_LABEL" "$root_device"
log_info "Creating btrfs subvolumes"
local temp_mount
temp_mount="/tmp/void-wrapper-btrfs"
mkdir -p "$temp_mount"
mount "$root_device" "$temp_mount"
btrfs subvolume create "$temp_mount/@"
btrfs subvolume create "$temp_mount/@home"
btrfs subvolume create "$temp_mount/@var"
btrfs subvolume create "$temp_mount/@log"
btrfs subvolume create "$temp_mount/@snapshots"
btrfs subvolume create "$temp_mount/@swap"
umount "$temp_mount"
rmdir "$temp_mount"
else
log_info "Formatting root as ext4 on $root_device"
mkfs.ext4 -L "$ROOT_LABEL" "$root_device"
fi
}

View File

@ -1,22 +1,61 @@
#!/usr/bin/env bash
# === Motivation ===
# Keep the official installer as the configuration authority.
# === Problem Statement ===
# We need a clean handoff so the installer uses existing mounts without reformatting.
# === Scope ===
# In scope: instructions and guardrails for the user during the installer run.
# Out of scope: automated installer configuration.
# === Concepts ===
# Handoff: a pause where the wrapper delegates to the installer.
# === Decisions ===
# Provide clear, minimal guidance to avoid overriding prepared filesystems.
# Support only CLI installer flow to keep guidance consistent.
# === Alternatives Considered ===
# Fully scripted installation rejected for this phase.
# === Constraints ===
# The wrapper must not hide or alter installer behavior.
# === Open Questions ===
# Should we provide a checklist or step-by-step guide during the installer handoff?
# How do we detect if the installer reformatted filesystems against our intent?
# Should we monitor the installer process, or fully delegate control?
# === Success Criteria ===
# The installer completes using the prepared mounts without reformatting.
run_installer() {
: "${MOUNT_ROOT:?Mount root is required}"
: "${ESP_MOUNT:?ESP mount path is required}"
if ! findmnt "$MOUNT_ROOT" >/dev/null 2>&1; then
die "Mount root not found: $MOUNT_ROOT"
fi
if ! findmnt "$ESP_MOUNT" >/dev/null 2>&1; then
die "ESP mount not found: $ESP_MOUNT"
fi
log_info "Installer handoff: use the prepared mounts without formatting."
log_info "In the installer: choose manual partitioning and only set mountpoints."
if [[ "${SKIP_INSTALLER:-0}" -eq 1 ]]; then
log_warn "Skipping installer per request."
return 0
fi
read -r -p "Press Enter to launch void-installer..." _
if command -v void-installer >/dev/null 2>&1; then
void-installer
else
log_warn "void-installer not found. Run it manually in another shell."
fi
read -r -p "Press Enter once the installer is complete..." _
}

View File

@ -1,14 +1,20 @@
#!/usr/bin/env bash
# === Motivation ===
# Provide transparency and auditability during destructive operations.
# === Problem Statement ===
# Users need to see what the wrapper is about to do and what it did.
# === Scope ===
# In scope: logging levels, user prompts, and summary output.
# Out of scope: external logging services.
# === Concepts ===
# Plan summary: a preflight printout of intended actions.
# Log levels: INFO (progress), WARN (recoverable issues), ERROR (fatal failures).
# Secret masking: never log passphrases or cryptographic material.
# === Decisions ===
# Use clear, non-ambiguous wording for destructive steps.
# Capture a local log file for later review and debugging.
@ -17,13 +23,67 @@
# Use plain text format for readability (not JSON).
# Distinguish between stdout (user-facing messages) and log file (detailed trace).
# Prefix log lines with timestamp and level: [2025-01-15 14:23:45] [INFO] message
# === Alternatives Considered ===
# Silent mode rejected because it hides risk.
# === Constraints ===
# Logging must not expose secrets such as passphrases.
# === Open Questions ===
# Should we support a verbose/debug mode for troubleshooting, beyond the standard levels?
# Should the log file path be displayed to the user at the end for manual review?
# How should we handle log rotation if the wrapper is run multiple times in the same session?
# === Success Criteria ===
# Users can review what happened before and after the installer handoff.
LOG_FILE=""
# Initialize the log file early so every phase can append to it.
logging_init() {
local timestamp
timestamp="$(date "+%Y-%m-%d-%H%M%S")"
LOG_FILE="/tmp/void-wrapper-${timestamp}.log"
touch "$LOG_FILE"
}
log_line() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp="$(date "+%Y-%m-%d %H:%M:%S")"
local line="[${timestamp}] [${level}] ${message}"
if [[ -n "${LOG_FILE:-}" ]]; then
if [[ "$level" == "ERROR" ]]; then
echo "$line" | tee -a "$LOG_FILE" >&2
else
echo "$line" | tee -a "$LOG_FILE"
fi
else
if [[ "$level" == "ERROR" ]]; then
echo "$line" >&2
else
echo "$line"
fi
fi
}
log_info() {
log_line "INFO" "$*"
}
log_warn() {
log_line "WARN" "$*"
}
log_error() {
log_line "ERROR" "$*"
}
die() {
log_error "$*"
exit 1
}

View File

@ -1,26 +1,33 @@
#!/usr/bin/env bash
# === Motivation ===
# Provide a reproducible wrapper around the Void installer for encrypted setups.
# Reduce manual, error-prone steps while keeping user control over installer choices.
# === Problem Statement ===
# The stock installer does not automate disk preparation for encrypted layouts.
# We need a guided wrapper that prepares the target disk before the installer runs.
# === Scope ===
# In scope: orchestration of phases, user confirmations, mode selection.
# Out of scope: replacing the official installer UI, package selection, BIOS support.
# === Concepts ===
# Wrapper: a script that executes pre-install tasks then hands off to the installer.
# Phases: sanity, disk layout, encryption, filesystems, mounts, installer, post-install.
# === User Journey ===
# 1. User boots Void live medium and runs the wrapper script.
# 2. Sanity checks validate environment (UEFI mode, root privileges, target disk exists).
# 3. User provides configuration: target disk, encryption mode, filesystem choice, hostname.
# 4. User reviews planned layout, encryption settings, and filesystem choices.
# 5. User confirms destructive operation with explicit disk identifier confirmation.
# 6. Wrapper prepares disk: partition → encrypt → format → mount.
# 6. Wrapper prepares disk: partition -> encrypt -> format -> mount.
# 7. Wrapper pauses and instructs user to run official installer using existing mounts.
# 8. User completes installer, selecting packages and configuring system preferences.
# 9. After installer exits, wrapper performs post-install configuration (crypttab, dracut, GRUB).
# 10. User reboots into encrypted Void system, enters passphrase at boot.
# === Decisions ===
# Keep the official installer in the loop because it handles system configuration choices.
# Prefer a phase-based flow to make recovery and retries understandable.
@ -28,16 +35,129 @@
# Default to a UEFI-first flow with an ESP.
# Start with a fully interactive flow; presets are out of scope for now.
# Provide a dry-run mode to review planned actions before execution.
# === Alternatives Considered ===
# Full automation without the official installer rejected due to maintenance burden.
# Manual instructions only rejected because they remain error-prone.
# === Constraints ===
# Must be safe for destructive operations and require explicit confirmation.
# Must support UEFI boot flow; BIOS is out of scope.
# === Open Questions ===
# Should the wrapper support resuming from a failed phase, or always start from scratch?
# How should the wrapper handle pre-existing partitions on the target disk?
# Should the dry-run mode generate a detailed execution plan, or just show configuration summary?
# === Success Criteria ===
# A user can run the wrapper, confirm disk intent, and reach the installer with correct mounts.
# The narrative of each phase is clear enough to audit and adapt.
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=src/logging.sh
source "$SCRIPT_DIR/logging.sh"
# shellcheck source=src/config.sh
source "$SCRIPT_DIR/config.sh"
# shellcheck source=src/sanity.sh
source "$SCRIPT_DIR/sanity.sh"
# shellcheck source=src/partitioning.sh
source "$SCRIPT_DIR/partitioning.sh"
# shellcheck source=src/encryption.sh
source "$SCRIPT_DIR/encryption.sh"
# shellcheck source=src/filesystems.sh
source "$SCRIPT_DIR/filesystems.sh"
# shellcheck source=src/mounts.sh
source "$SCRIPT_DIR/mounts.sh"
# shellcheck source=src/installer.sh
source "$SCRIPT_DIR/installer.sh"
# shellcheck source=src/postinstall.sh
source "$SCRIPT_DIR/postinstall.sh"
# shellcheck source=src/rollback.sh
source "$SCRIPT_DIR/rollback.sh"
DRY_RUN=0
SKIP_INSTALLER=0
CURRENT_PHASE=""
usage() {
cat <<USAGE
Usage: ./main.sh [--dry-run] [--skip-installer]
--dry-run Print configuration summary and exit
--skip-installer Skip launching void-installer (manual run)
USAGE
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
;;
--skip-installer)
SKIP_INSTALLER=1
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
shift
done
}
run_phase() {
local phase="$1"
shift
CURRENT_PHASE="$phase"
log_info "=== Phase: $phase ==="
"$@"
}
on_error() {
local exit_code=$?
set +e
log_error "Failure in phase '$CURRENT_PHASE'."
rollback_offer
exit "$exit_code"
}
main() {
trap on_error ERR
parse_args "$@"
logging_init
run_phase "sanity" sanity_check_base
config_prompt_interactive
config_validate
run_phase "sanity" sanity_check_disk
sanity_check_commands
config_print_summary
if [[ "$DRY_RUN" -eq 1 ]]; then
log_info "Dry-run mode enabled. No changes were made."
exit 0
fi
config_confirm_destructive
run_phase "partitioning" partition_disk
run_phase "encryption" encrypt_root
run_phase "filesystems" format_filesystems
run_phase "mounts" mount_filesystems
run_phase "installer" run_installer
run_phase "post-install" postinstall_run
log_info "Installation wrapper completed."
log_info "Log file: ${LOG_FILE}"
}
main "$@"

View File

@ -1,24 +1,76 @@
#!/usr/bin/env bash
# === Motivation ===
# Present a clean mount tree to the installer.
# === Problem Statement ===
# The installer expects mounts to exist and should not reformat them.
# === Scope ===
# In scope: mount points, ordering, and verification.
# Out of scope: mount command details.
# === Concepts ===
# Mount tree: the hierarchy rooted at the target installation path.
# === Decisions ===
# Mount root first, then boot-related partitions.
# Verify mounts before handing control to the installer.
# Default to mounting the ESP at /boot/efi for UEFI-first layouts.
# Keep /boot inside the encrypted root (no separate /boot mount by default).
# === Alternatives Considered ===
# Letting the installer mount everything rejected because encryption requires pre-mount.
# === Constraints ===
# Mount paths must align with the installer's expectations.
# === Open Questions ===
# Should we verify mount points are writable before handing off to installer?
# How do we handle mount failures - retry, abort, or offer manual intervention?
# Should we mount with specific options (noatime, relatime) or use defaults?
# === Success Criteria ===
# The installer sees a ready mount tree and skips formatting.
mount_filesystems() {
: "${MOUNT_ROOT:?Mount root is required}"
: "${ESP_MOUNT:?ESP mount path is required}"
: "${ESP_PART:?ESP partition is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
: "${FS_TYPE:?Filesystem type is required}"
local root_device
root_device="/dev/mapper/${CRYPT_NAME}"
log_info "Mounting root filesystem at $MOUNT_ROOT"
mkdir -p "$MOUNT_ROOT"
if [[ "$FS_TYPE" == "btrfs" ]]; then
mount -o subvol=@ "$root_device" "$MOUNT_ROOT"
mkdir -p "$MOUNT_ROOT/home" "$MOUNT_ROOT/var" "$MOUNT_ROOT/.snapshots" "$MOUNT_ROOT/swap"
mount -o subvol=@home "$root_device" "$MOUNT_ROOT/home"
mount -o subvol=@var "$root_device" "$MOUNT_ROOT/var"
mkdir -p "$MOUNT_ROOT/var/log"
mount -o subvol=@log "$root_device" "$MOUNT_ROOT/var/log"
mount -o subvol=@snapshots "$root_device" "$MOUNT_ROOT/.snapshots"
mount -o subvol=@swap "$root_device" "$MOUNT_ROOT/swap"
else
mount "$root_device" "$MOUNT_ROOT"
fi
log_info "Mounting ESP at $ESP_MOUNT"
mkdir -p "$ESP_MOUNT"
mount "$ESP_PART" "$ESP_MOUNT"
if findmnt "$MOUNT_ROOT" >/dev/null 2>&1 && findmnt "$ESP_MOUNT" >/dev/null 2>&1; then
if [[ "$FS_TYPE" == "btrfs" ]]; then
for mount_point in "$MOUNT_ROOT/home" "$MOUNT_ROOT/var" "$MOUNT_ROOT/var/log" "$MOUNT_ROOT/.snapshots" "$MOUNT_ROOT/swap"; do
if ! findmnt "$mount_point" >/dev/null 2>&1; then
die "Mount verification failed for $mount_point."
fi
done
fi
log_info "Mounts verified."
else
die "Mount verification failed."
fi
}

View File

@ -1,14 +1,20 @@
#!/usr/bin/env bash
# === Motivation ===
# Establish a predictable disk layout that supports encrypted root.
# === Problem Statement ===
# We need a partition scheme that works for UEFI and leaves encryption boundaries clear.
# === Scope ===
# In scope: layout choices, partition roles, and size policy.
# Out of scope: exact partitioning commands and tooling.
# === Concepts ===
# EFI System Partition (ESP): unencrypted boot partition for UEFI.
# Root partition: the encrypted container for the main system.
# Separate boot partition: unencrypted /boot used only in alternative layouts.
# === Decisions ===
# Keep ESP unencrypted to meet firmware requirements.
# Make root the only encrypted container by default to keep the flow simple.
@ -16,13 +22,54 @@
# BIOS support is out of scope for this wrapper.
# Do not create a separate unencrypted /boot; /boot lives inside encrypted root.
# Default ESP size is 1G; root size is chosen interactively.
# === Alternatives Considered ===
# Separate encrypted /home rejected for phase 1 due to complexity.
# === Constraints ===
# Layout must remain compatible with the chosen bootloader.
# === Open Questions ===
# Should we support GPT labels for partitions, or use default numeric identifiers?
# What partition alignment should we use for optimal SSD performance?
# Should ESP size be configurable, or keep the 1G default fixed?
# === Success Criteria ===
# The layout supports boot and allows the installer to mount targets cleanly.
partition_path() {
local disk="$1"
local number="$2"
if [[ "$disk" =~ [0-9]$ ]]; then
echo "${disk}p${number}"
else
echo "${disk}${number}"
fi
}
partition_disk() {
: "${DISK:?Target disk is required}"
: "${ESP_SIZE:?ESP size is required}"
: "${ROOT_END:?Root partition end is required}"
log_info "Partitioning disk: $DISK"
if command -v wipefs >/dev/null 2>&1; then
log_warn "Wiping existing signatures on $DISK"
wipefs -a "$DISK"
else
log_warn "wipefs not found; proceeding without signature wipe"
fi
parted -s "$DISK" mklabel gpt
parted -s "$DISK" mkpart ESP fat32 1MiB "$ESP_SIZE"
parted -s "$DISK" set 1 esp on
parted -s "$DISK" mkpart ROOT "$ESP_SIZE" "$ROOT_END"
partprobe "$DISK"
ESP_PART="$(partition_path "$DISK" 1)"
ROOT_PART="$(partition_path "$DISK" 2)"
log_info "ESP partition: $ESP_PART"
log_info "Root partition: $ROOT_PART"
}

View File

@ -1,10 +1,15 @@
#!/usr/bin/env bash
# === Motivation ===
# Ensure the system can boot with encrypted root after installation.
# === Problem Statement ===
# Post-install steps must align initramfs, bootloader, and encryption metadata.
# === Scope ===
# In scope: required configuration updates inside the installed system.
# Out of scope: package selection and user account management.
# === Concepts ===
# Initramfs: early boot image that unlocks encrypted storage.
# Bootloader config: entries that point to the encrypted root.
@ -12,6 +17,7 @@
# Crypttab: /etc/crypttab maps LUKS UUIDs to device names for initramfs.
# GRUB cryptodisk: GRUB_ENABLE_CRYPTODISK=y in /etc/default/grub enables LUKS unlock.
# Kernel parameters: rd.luks.uuid=<UUID> tells dracut which LUKS container to unlock.
# === Decisions ===
# Keep post-install steps explicit and minimal, focused on boot viability.
# Default to GRUB because it is widely supported on Void without systemd dependencies.
@ -21,16 +27,153 @@
# Set GRUB_ENABLE_CRYPTODISK=y in /etc/default/grub to allow GRUB to unlock LUKS.
# Add rd.luks.uuid=<UUID> to GRUB_CMDLINE_LINUX for initramfs LUKS unlock.
# Run grub-install to embed cryptodisk support, then grub-mkconfig to update menu entries.
# === Alternatives Considered ===
# Skipping post-install updates rejected because it risks unbootable systems.
# systemd-boot and rEFInd rejected as defaults due to availability and scope constraints.
# EFISTUB rejected as default because it increases manual UEFI entry management.
# === Constraints ===
# Steps must run in the target system context.
# === Open Questions ===
# Should we generate a rescue initramfs in addition to the default one?
# Should we verify GRUB can unlock LUKS2 before rebooting, or trust the configuration?
# How do we handle future kernel updates - should we document the dracut reconfiguration process?
# Should swap file activation be configured in this phase, or deferred to first boot?
# === Success Criteria ===
# After reboot, the system prompts for decryption and boots successfully.
postinstall_bind_mounts() {
mkdir -p "$MOUNT_ROOT/dev" "$MOUNT_ROOT/proc" "$MOUNT_ROOT/sys" "$MOUNT_ROOT/run"
mount --rbind /dev "$MOUNT_ROOT/dev"
mount --make-rslave "$MOUNT_ROOT/dev"
mount -t proc /proc "$MOUNT_ROOT/proc"
mount --rbind /sys "$MOUNT_ROOT/sys"
mount --make-rslave "$MOUNT_ROOT/sys"
mount --rbind /run "$MOUNT_ROOT/run"
mount --make-rslave "$MOUNT_ROOT/run"
}
postinstall_unbind_mounts() {
umount -R "$MOUNT_ROOT/dev" 2>/dev/null || true
umount -R "$MOUNT_ROOT/proc" 2>/dev/null || true
umount -R "$MOUNT_ROOT/sys" 2>/dev/null || true
umount -R "$MOUNT_ROOT/run" 2>/dev/null || true
}
postinstall_run() {
: "${ROOT_PART:?Root partition is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
: "${MOUNT_ROOT:?Mount root is required}"
: "${FS_TYPE:?Filesystem type is required}"
: "${SWAP_SIZE:?Swap size is required}"
: "${HOSTNAME:?Hostname is required}"
local luks_uuid
local root_uuid
luks_uuid="$(cryptsetup luksUUID "$ROOT_PART")"
root_uuid="$(blkid -s UUID -o value "/dev/mapper/${CRYPT_NAME}")"
log_info "Post-install: configuring target system in chroot"
postinstall_bind_mounts
LUKS_UUID="$luks_uuid" ROOT_UUID="$root_uuid" FS_TYPE="$FS_TYPE" SWAP_SIZE="$SWAP_SIZE" HOSTNAME="$HOSTNAME" \
chroot "$MOUNT_ROOT" /bin/bash -s <<'EOS'
set -euo pipefail
# Ensure required tools are available inside the target system.
for cmd in grub-install grub-mkconfig xbps-reconfigure fallocate mkswap; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Missing required command inside chroot: $cmd" >&2
exit 1
fi
done
# Write the hostname explicitly to match wrapper configuration.
printf '%s\n' "$HOSTNAME" > /etc/hostname
# Ensure crypttab exists with the correct UUID mapping.
mkdir -p /etc
printf 'cryptroot UUID=%s none luks\n' "$LUKS_UUID" > /etc/crypttab
# Ensure dracut loads crypt and optional resume modules.
mkdir -p /etc/dracut.conf.d
if [[ "$SWAP_SIZE" != "0" ]]; then
printf 'add_dracutmodules+=" crypt resume "\n' > /etc/dracut.conf.d/10-crypt.conf
else
printf 'add_dracutmodules+=" crypt "\n' > /etc/dracut.conf.d/10-crypt.conf
fi
# Prepare a swap file inside the encrypted root if enabled.
if [[ "$SWAP_SIZE" != "0" ]]; then
mkdir -p /swap
if [[ "$FS_TYPE" == "btrfs" ]]; then
if command -v chattr >/dev/null 2>&1; then
chattr +C /swap
fi
if command -v btrfs >/dev/null 2>&1; then
btrfs property set /swap compression none || true
fi
fi
fallocate -l "$SWAP_SIZE" /swap/swapfile
chmod 600 /swap/swapfile
mkswap /swap/swapfile
if ! grep -q '^/swap/swapfile' /etc/fstab; then
printf '/swap/swapfile none swap defaults 0 0\n' >> /etc/fstab
fi
fi
# Ensure btrfs subvolumes are reflected in /etc/fstab.
if [[ "$FS_TYPE" == "btrfs" ]]; then
touch /etc/fstab
update_fstab_entry() {
local mount_point="$1"
local options="$2"
local line="UUID=$ROOT_UUID $mount_point btrfs $options 0 0"
if awk -v mp="$mount_point" '$2==mp {found=1} END {exit found?0:1}' /etc/fstab; then
awk -v mp="$mount_point" -v line="$line" 'BEGIN{OFS=" "} $2==mp {$0=line} {print}' /etc/fstab > /etc/fstab.tmp
mv /etc/fstab.tmp /etc/fstab
else
printf '%s\n' "$line" >> /etc/fstab
fi
}
update_fstab_entry / "defaults,subvol=@"
update_fstab_entry /home "defaults,subvol=@home"
update_fstab_entry /var "defaults,subvol=@var"
update_fstab_entry /var/log "defaults,subvol=@log"
update_fstab_entry /.snapshots "defaults,subvol=@snapshots"
update_fstab_entry /swap "defaults,subvol=@swap"
fi
# Ensure GRUB unlocks the encrypted root and passes the LUKS UUID to initramfs.
mkdir -p /etc/default
touch /etc/default/grub
if grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then
sed -i 's/^GRUB_ENABLE_CRYPTODISK=.*/GRUB_ENABLE_CRYPTODISK=y/' /etc/default/grub
else
printf 'GRUB_ENABLE_CRYPTODISK=y\n' >> /etc/default/grub
fi
if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then
if ! grep -q "rd.luks.uuid=$LUKS_UUID" /etc/default/grub; then
sed -i "s/^GRUB_CMDLINE_LINUX=\"/GRUB_CMDLINE_LINUX=\"rd.luks.uuid=$LUKS_UUID /" /etc/default/grub
fi
else
printf 'GRUB_CMDLINE_LINUX="rd.luks.uuid=%s"\n' "$LUKS_UUID" >> /etc/default/grub
fi
# Regenerate initramfs and GRUB configuration.
xbps-reconfigure -fa
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Void --recheck
grub-mkconfig -o /boot/grub/grub.cfg
EOS
postinstall_unbind_mounts
log_info "Post-install complete."
}

View File

@ -1,13 +1,19 @@
#!/usr/bin/env bash
# === Motivation ===
# Reduce fallout if a phase fails mid-way.
# === Problem Statement ===
# The wrapper should leave the system in a predictable state after errors.
# === Scope ===
# In scope: cleanup expectations and guidance for manual recovery.
# Out of scope: full automatic rollback of disk changes.
# === Concepts ===
# Cleanup: unmounting and closing encryption mappings.
# Best-effort rollback: undo reversible operations, leave destructive changes as-is.
# === Decisions ===
# Rollback is limited to reversible operations only:
# - Unmount all filesystems under /mnt
@ -16,13 +22,49 @@
# Only perform cleanup after explicit user confirmation.
# Provide clear manual recovery instructions if automatic cleanup fails.
# Track which phase failed to offer targeted recovery advice.
# === Alternatives Considered ===
# Full rollback rejected due to complexity and risk.
# === Constraints ===
# Cleanup must avoid touching unrelated devices.
# === Open Questions ===
# Should rollback be automatic on failure, or require explicit user confirmation?
# What state should be left after rollback - empty disk, partial setup, or unchanged?
# Should we provide manual recovery commands if automatic rollback fails?
# === Success Criteria ===
# After a failure, mounts and encryption mappings are closed when safe to do so.
rollback_offer() {
: "${MOUNT_ROOT:?Mount root is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
local response
if ! findmnt "$MOUNT_ROOT" >/dev/null 2>&1 && [[ ! -e "/dev/mapper/$CRYPT_NAME" ]]; then
return 0
fi
read -r -p "Cleanup mounts and close LUKS mapping? (yes/no): " response
if [[ "$response" != "yes" ]]; then
log_warn "Skipping cleanup. Manual recovery may be required."
return 0
fi
rollback_cleanup
}
rollback_cleanup() {
: "${MOUNT_ROOT:?Mount root is required}"
: "${CRYPT_NAME:?Crypt mapping name is required}"
if findmnt "$MOUNT_ROOT" >/dev/null 2>&1; then
log_info "Unmounting $MOUNT_ROOT"
umount -R "$MOUNT_ROOT" || log_warn "Failed to unmount $MOUNT_ROOT"
fi
if [[ -e "/dev/mapper/$CRYPT_NAME" ]]; then
log_info "Closing LUKS mapping $CRYPT_NAME"
cryptsetup close "$CRYPT_NAME" || log_warn "Failed to close LUKS mapping"
fi
}

View File

@ -1,25 +1,96 @@
#!/usr/bin/env bash
# === Motivation ===
# Prevent catastrophic mistakes before any disk operations.
# === Problem Statement ===
# The wrapper must ensure it is running in a safe context with the intended target.
# === Scope ===
# In scope: checks for privileges, environment, and target disk presence.
# Out of scope: detailed hardware inventory.
# === Concepts ===
# Preflight: a set of checks that must pass before continuing.
# === Decisions ===
# Fail fast on missing privileges or ambiguous disk selection.
# Require a typed confirmation of the target disk identifier.
# Reject unsupported architectures; x86_64 is the only supported target.
# Reject non-UEFI boot modes; BIOS systems are out of scope.
# Provide a dry-run mode to review planned actions before execution.
# === Alternatives Considered ===
# Proceeding with warnings only rejected due to data loss risk.
# === Constraints ===
# Checks must run without relying on network access.
# === Open Questions ===
# Should we check for minimum disk size requirements before proceeding?
# How do we detect if the system was booted in UEFI mode reliably?
# Should we warn if the target disk contains existing partitions, or just require explicit confirmation?
# === Success Criteria ===
# The wrapper refuses to proceed if the target disk is unclear or unsafe.
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
die "Missing required command: $cmd"
fi
}
sanity_check_base() {
if [[ "$(id -u)" -ne 0 ]]; then
die "Run this script as root."
fi
if [[ "$(uname -m)" != "x86_64" ]]; then
die "Unsupported architecture: $(uname -m)."
fi
if [[ ! -d /sys/firmware/efi ]]; then
die "UEFI mode required. BIOS systems are out of scope."
fi
}
sanity_check_commands() {
require_command lsblk
require_command partprobe
require_command parted
require_command cryptsetup
require_command mkfs.vfat
require_command blkid
require_command mount
require_command umount
require_command findmnt
require_command chroot
if [[ "$FS_TYPE" == "btrfs" ]]; then
require_command mkfs.btrfs
require_command btrfs
else
require_command mkfs.ext4
fi
}
sanity_check_disk() {
: "${DISK:?Target disk is required}"
if [[ ! -b "$DISK" ]]; then
die "Target disk is not a block device: $DISK"
fi
local disk_type
disk_type="$(lsblk -dn -o TYPE "$DISK")"
if [[ "$disk_type" != "disk" ]]; then
die "Target is not a disk device: $DISK"
fi
if lsblk -n -o MOUNTPOINT "$DISK" | grep -q '/'; then
log_warn "Target disk has mounted partitions. Proceed with caution."
fi
if lsblk -n -o NAME,TYPE "$DISK" | grep -q 'part'; then
log_warn "Target disk has existing partitions. They will be erased."
fi
}