#!/usr/bin/env bash # shellcheck disable=SC1090 ######################################################### # ARCH OS INSTALLER | Automated Arch Linux Installer TUI ######################################################### # SOURCE: https://github.com/murkl/arch-os # AUTOR: murkl # ORIGIN: Germany # LICENCE: GPL 3.0 # CONFIG set -o pipefail # A pipeline error results in the error status of the entire pipeline set -e # Terminate if any command exits with a non-zero set -E # ERR trap inherited by shell functions (errtrace) # ENVIRONMENT : "${DEBUG:=false}" # DEBUG=true ./installer.sh : "${FORCE:=false}" # FORCE=true ./installer.sh : "${GUM:=/usr/local/bin/gum}" # GUM=/usr/bin/gum ./installer.sh # SCRIPT VERSION='1.9.6' # VERSION [ "$*" = "--version" ] && echo "$VERSION" && exit 0 # GUM GUM_VERSION="0.13.0" # FILES SCRIPT_CONFIG="./installer.conf" SCRIPT_LOG="./installer.log" # TEMP (everything volatile lives here, so a single rm -rf "$SCRIPT_TMP_DIR" cleans it all up; # installer.conf/installer.log are the only persistent files and stay outside on purpose) SCRIPT_TMP_DIR="$(mktemp -d "./.tmp.XXXXX")" ERROR_MSG_TMP_FILE="${SCRIPT_TMP_DIR}/installer.err" PROCESS_LOG_TMP_FILE="${SCRIPT_TMP_DIR}/process.log" PROCESS_RET_TMP_FILE="${SCRIPT_TMP_DIR}/process.ret" SCRIPT_CONFIG_TMP_FILE="${SCRIPT_TMP_DIR}/installer.conf.new" # Traps (error & exit) - registered as early as possible, right after SCRIPT_TMP_DIR exists, so the # tmp dir is guaranteed to be removed by trap_exit no matter where the script later fails (e.g. the # root check or gum_init below, both of which can exit before main() would otherwise set this up). trap 'trap_exit' EXIT trap 'trap_error ${FUNCNAME} ${LINENO}' ERR # COLORS COLOR_BLACK=0 # #000000 COLOR_RED=9 # #ff0000 COLOR_GREEN=10 # #00ff00 COLOR_YELLOW=11 # #ffff00 COLOR_BLUE=12 # #0000ff COLOR_PURPLE=13 # #ff00ff COLOR_CYAN=14 # #00ffff COLOR_WHITE=15 # #ffffff COLOR_FOREGROUND="${COLOR_BLUE}" COLOR_BACKGROUND="${COLOR_WHITE}" # //////////////////////////////////////////////////////////////////////////////////////////////////// # MAIN # //////////////////////////////////////////////////////////////////////////////////////////////////// main() { # Show short debug warning if [ "$DEBUG" = "true" ]; then clear && echo "!!! DEBUG MODE IS ENABLED !!!" && sleep 1 fi # Must run as root (except in debug mode) if [ "$DEBUG" = "false" ] && [ "$(id -u)" -ne 0 ]; then echo "Error: Arch OS Installer must be run as root" >&2 && exit 1 fi # Clear logfile [ -f "$SCRIPT_LOG" ] && mv -f "$SCRIPT_LOG" "${SCRIPT_LOG}.old" # Check gum binary or download gum_init # Print version to logfile log_info "Arch OS ${VERSION}" # Offer self update and restart with new version if a newer release is available update_installer # --------------------------------------------------------------------------------------------------- # Loop properties step to update screen if user edit properties while (true); do print_header "Arch OS Installer" # Show landig page gum_white 'Please make sure you have:' && echo gum_white '• Backed up your important data' gum_white '• A stable internet connection' gum_white '• Secure Boot disabled' gum_white '• Boot Mode set to UEFI' # Ask for load & remove existing config file if [ "$FORCE" = "false" ] && [ -f "$SCRIPT_CONFIG" ] && ! gum_confirm "Load existing installer.conf?"; then gum_confirm "Remove existing installer.conf?" || trap_gum_exit # If not want remove config > exit script echo && gum_title "Properties File" mv -f "$SCRIPT_CONFIG" "${SCRIPT_CONFIG}.old" && gum_info "installer.conf was moved to installer.conf.old" gum_warn "Please restart Arch OS Installer..." echo && exit 0 fi echo # Print new line # Source installer.conf if exists or select preset until properties_preset_source; do :; done # Selectors echo && gum_title "Core Setup" until select_username; do :; done until select_password; do :; done until select_timezone; do :; done until select_language; do :; done until select_keyboard; do :; done until select_filesystem; do :; done until select_bootloader; do :; done until select_disk; do :; done until select_enable_encryption; do :; done echo && gum_title "Desktop Setup" until select_enable_desktop_environment; do :; done until select_enable_desktop_driver; do :; done until select_enable_desktop_slim; do :; done until select_enable_desktop_keyboard; do :; done echo && gum_title "Feature Setup" until select_enable_core_tweaks; do :; done until select_enable_bootsplash; do :; done until select_enable_multilib; do :; done until select_enable_aur; do :; done until select_enable_housekeeping; do :; done until select_enable_shell_enhancement; do :; done until select_enable_manager; do :; done # Finish & show Advanced Properties echo && gum_title "Properties" # Open Advanced Properties? if [ "$FORCE" = "false" ] && gum_confirm --default=false --negative="Skip" "Open Advanced Setup Editor?"; then print_header "Arch OS Installer" # Show landig page gum_title "Advanced Setup Editor" local header_txt="• Save with CTRL + D or ESC and cancel with CTRL + C" if gum_write --show-line-numbers --prompt "" --height=18 --width=180 --char-limit=0 --header="${header_txt}" --value="$(cat "$SCRIPT_CONFIG")" >"$SCRIPT_CONFIG_TMP_FILE"; then mv "$SCRIPT_CONFIG_TMP_FILE" "$SCRIPT_CONFIG" && properties_source gum_info "Properties successfully saved" gum_confirm "Change Password?" && until select_password --change && properties_source; do :; done else rm -f "$SCRIPT_CONFIG_TMP_FILE" # Remove tmp properties gum_warn "Advanced Setup canceled" fi echo && ! gum_spin --title="Reload Properties in 3 seconds..." -- sleep 3 && trap_gum_exit continue # Restart properties step to refresh properties screen fi # Hard safety gates right at the init of the installation, before touching any disk and # before the Summary/confirm below - by the time the user confirms, the install is already # known to work. All read-only checks (string/regex tests, blkid/lsblk queries) so they run # in DEBUG/FORCE mode too. On failure, validate_properties offers to fix it in the Advanced # Setup Editor; afterwards it restarts this whole properties step from the top so the # corrected values are shown again before the Summary. validate_properties || continue gum_info "Successfully validated" ###################################################### break # Exit properties step and continue installation ###################################################### done # --------------------------------------------------------------------------------------------------- # Start installation in 5 seconds? if [ "$FORCE" = "false" ]; then echo && gum_title "Summary" && print_summary gum_confirm "Start Arch OS Installation?" || trap_gum_exit fi echo && gum_title "Arch OS Installation" local spin_title="Arch OS Installation starts in 5 seconds. Press CTRL + C to cancel..." ! gum_spin --title="$spin_title" -- sleep 5 && trap_gum_exit # CTRL + C pressed SECONDS=0 # Messure execution time of installation # Executors exec_init_installation exec_prepare_disk exec_pacstrap_core exec_enable_multilib exec_install_aur_helper exec_install_bootsplash exec_install_housekeeping exec_install_shell_enhancement exec_install_desktop exec_install_graphics_driver exec_install_archos_manager exec_install_vm_support exec_finalize_arch_os # Calc installation duration duration=$SECONDS # This is set before install starts duration_min="$((duration / 60))" duration_sec="$((duration % 60))" # Print duration time info local finish_txt="Installation successful in ${duration_min} minutes and ${duration_sec} seconds" echo && gum_green --bold "$finish_txt" log_info "$finish_txt" # Copy installer files to users home if [ "$DEBUG" = "false" ]; then cp -f "$SCRIPT_CONFIG" "/mnt/home/${ARCH_OS_USERNAME}/installer.conf" sed -i "1i\# Arch OS Version: ${VERSION}" "/mnt/home/${ARCH_OS_USERNAME}/installer.conf" cp -f "$SCRIPT_LOG" "/mnt/home/${ARCH_OS_USERNAME}/installer.log" arch-chroot /mnt chown -R "$ARCH_OS_USERNAME":"$ARCH_OS_USERNAME" "/home/${ARCH_OS_USERNAME}/installer.conf" arch-chroot /mnt chown -R "$ARCH_OS_USERNAME":"$ARCH_OS_USERNAME" "/home/${ARCH_OS_USERNAME}/installer.log" fi wait # Wait for sub processes # --------------------------------------------------------------------------------------------------- # Show reboot & unmount promt local do_reboot do_unmount do_chroot # Default values do_reboot="false" do_chroot="false" do_unmount="false" # Force values if [ "$FORCE" = "true" ]; then do_reboot="false" do_chroot="false" do_unmount="true" fi # Reboot promt [ "$FORCE" = "false" ] && gum_confirm "Reboot to Arch OS now?" && do_reboot="true" && do_unmount="true" # Unmount [ "$FORCE" = "false" ] && [ "$do_reboot" = "false" ] && gum_confirm "Unmount Arch OS from /mnt?" && do_unmount="true" [ "$do_unmount" = "true" ] && echo && gum_warn "Unmounting Arch OS from /mnt..." if [ "$DEBUG" = "false" ] && [ "$do_unmount" = "true" ]; then swapoff -a umount -A -R /mnt [ "$ARCH_OS_ENCRYPTION_ENABLED" = "true" ] && cryptsetup close cryptroot fi # Do reboot [ "$FORCE" = "false" ] && [ "$do_reboot" = "true" ] && gum_warn "Rebooting to Arch OS..." && [ "$DEBUG" = "false" ] && reboot # Chroot [ "$FORCE" = "false" ] && [ "$do_unmount" = "false" ] && gum_confirm "Chroot to new Arch OS?" && do_chroot="true" if [ "$do_chroot" = "true" ] && echo && gum_warn "Chrooting Arch OS at /mnt..."; then gum_warn "!! YOU ARE NOW ON YOUR NEW ARCH OS SYSTEM !!" gum_warn ">> Leave with command 'exit'" if [ "$DEBUG" = "false" ]; then arch-chroot /mnt storage -> # regional -> kernel -> system features -> desktop (mirrors the interactive TUI order) echo "ARCH_OS_USERNAME='${ARCH_OS_USERNAME}' # User" echo "ARCH_OS_HOSTNAME='${ARCH_OS_HOSTNAME}' # Hostname" echo "ARCH_OS_DISK='${ARCH_OS_DISK}' # Disk" echo "ARCH_OS_BOOT_PARTITION='${ARCH_OS_BOOT_PARTITION}' # Boot partition" echo "ARCH_OS_ROOT_PARTITION='${ARCH_OS_ROOT_PARTITION}' # Root partition" echo "ARCH_OS_FILESYSTEM='${ARCH_OS_FILESYSTEM}' # Filesystem | Available: btrfs, ext4" echo "ARCH_OS_BTRFS_SNAPPER_ENABLED='${ARCH_OS_BTRFS_SNAPPER_ENABLED}' # BTRFS Snapper enabled | Disable: false" echo "ARCH_OS_BTRFS_ASSISTANT_ENABLED='${ARCH_OS_BTRFS_ASSISTANT_ENABLED}' # BTRFS Desktop Assistant enabled | Disable: false" echo "ARCH_OS_BOOTLOADER='${ARCH_OS_BOOTLOADER}' # Bootloader | Available: grub, systemd" echo "ARCH_OS_DUAL_BOOT_ENABLED='${ARCH_OS_DUAL_BOOT_ENABLED}' # Dual boot: install alongside existing OS (no disk wipe, reuse ESP, add boot entry only) | Default: false | Enable: true" echo "ARCH_OS_ENCRYPTION_ENABLED='${ARCH_OS_ENCRYPTION_ENABLED}' # Disk encryption | Disable: false" echo "ARCH_OS_TIMEZONE='${ARCH_OS_TIMEZONE}' # Timezone | Show available: ls /usr/share/zoneinfo/** | Example: Europe/Berlin" echo "ARCH_OS_LOCALE_LANG='${ARCH_OS_LOCALE_LANG}' # Locale | Show available: ls /usr/share/i18n/locales | Example: de_DE" echo "ARCH_OS_LOCALE_GEN_LIST=(${ARCH_OS_LOCALE_GEN_LIST[*]@Q}) # Locale List | Show available: cat /etc/locale.gen" echo "ARCH_OS_REFLECTOR_COUNTRY='${ARCH_OS_REFLECTOR_COUNTRY}' # Country used by reflector | Default: null | Example: Germany,France" echo "ARCH_OS_VCONSOLE_KEYMAP='${ARCH_OS_VCONSOLE_KEYMAP}' # Console keymap | Show available: localectl list-keymaps | Example: de-latin1-nodeadkeys" echo "ARCH_OS_VCONSOLE_FONT='${ARCH_OS_VCONSOLE_FONT}' # Console font | Default: null | Show available: find /usr/share/kbd/consolefonts/*.psfu.gz | Example: eurlatgr" echo "ARCH_OS_KERNEL='${ARCH_OS_KERNEL}' # Kernel | Default: linux-zen | Recommended: linux, linux-lts linux-zen, linux-hardened" echo "ARCH_OS_KERNEL_ARGS='${ARCH_OS_KERNEL_ARGS}' # Additional kernel parameters (space separated) | Default: null | Example: amd_pstate=active mitigations=off" echo "ARCH_OS_MICROCODE='${ARCH_OS_MICROCODE}' # Microcode | Disable: none | Available: intel-ucode, amd-ucode" echo "ARCH_OS_CORE_TWEAKS_ENABLED='${ARCH_OS_CORE_TWEAKS_ENABLED}' # Arch OS Core Tweaks | Disable: false" echo "ARCH_OS_BOOTSPLASH_ENABLED='${ARCH_OS_BOOTSPLASH_ENABLED}' # Bootsplash | Disable: false" echo "ARCH_OS_MULTILIB_ENABLED='${ARCH_OS_MULTILIB_ENABLED}' # MultiLib 32 Bit Support | Disable: false" echo "ARCH_OS_AUR_HELPER='${ARCH_OS_AUR_HELPER}' # AUR Helper | Default: paru | Disable: none | Recommended: paru, yay, trizen, pikaur" echo "ARCH_OS_HOUSEKEEPING_ENABLED='${ARCH_OS_HOUSEKEEPING_ENABLED}' # Housekeeping | Disable: false" echo "ARCH_OS_SHELL_ENHANCEMENT_ENABLED='${ARCH_OS_SHELL_ENHANCEMENT_ENABLED}' # Shell Enhancement | Disable: false" echo "ARCH_OS_SHELL_ENHANCEMENT_FISH_ENABLED='${ARCH_OS_SHELL_ENHANCEMENT_FISH_ENABLED}' # Enable fish shell | Default: true | Disable: false" echo "ARCH_OS_MANAGER_ENABLED='${ARCH_OS_MANAGER_ENABLED}' # Arch OS Manager | Disable: false" echo "ARCH_OS_VM_SUPPORT_ENABLED='${ARCH_OS_VM_SUPPORT_ENABLED}' # VM Support | Default: true | Disable: false" echo "ARCH_OS_ECN_ENABLED='${ARCH_OS_ECN_ENABLED}' # Disable ECN support for legacy routers | Default: true | Disable: false" echo "ARCH_OS_DESKTOP_ENABLED='${ARCH_OS_DESKTOP_ENABLED}' # Arch OS Desktop (caution: if disabled, only a minimal tty will be provied)| Disable: false" echo "ARCH_OS_DESKTOP_GRAPHICS_DRIVER='${ARCH_OS_DESKTOP_GRAPHICS_DRIVER}' # Graphics Driver | Disable: none | Available: mesa, intel_i915, nvidia, amd, ati" echo "ARCH_OS_DESKTOP_EXTRAS_ENABLED='${ARCH_OS_DESKTOP_EXTRAS_ENABLED}' # Enable desktop extra packages (caution: if disabled, only core + gnome + git packages will be installed) | Disable: false" echo "ARCH_OS_DESKTOP_SLIM_ENABLED='${ARCH_OS_DESKTOP_SLIM_ENABLED}' # Enable Sim Desktop (only GNOME Core Apps) | Default: false" echo "ARCH_OS_DESKTOP_AUTOLOGIN_ENABLED='${ARCH_OS_DESKTOP_AUTOLOGIN_ENABLED}' # Enable GNOME autologin | Default: matches Disk Encryption (on -> autologin on, avoids a 2nd password prompt) | Override: true, false" echo "ARCH_OS_DESKTOP_KEYBOARD_MODEL='${ARCH_OS_DESKTOP_KEYBOARD_MODEL}' # GNOME keyboard model | Default: pc105 | Show available: localectl list-x11-keymap-models" echo "ARCH_OS_DESKTOP_KEYBOARD_LAYOUT='${ARCH_OS_DESKTOP_KEYBOARD_LAYOUT}' # GNOME keyboard layout | Show available: localectl list-x11-keymap-layouts | Example: de" echo "ARCH_OS_DESKTOP_KEYBOARD_VARIANT='${ARCH_OS_DESKTOP_KEYBOARD_VARIANT}' # GNOME keyboard variant | Default: null | Show available: localectl list-x11-keymap-variants | Example: nodeadkeys" echo "ARCH_OS_SAMBA_SHARE_ENABLED='${ARCH_OS_SAMBA_SHARE_ENABLED}' # Enable Samba public (anonymous) & home share (user) | Disable: false" } >"$SCRIPT_CONFIG" # Write properties to file } properties_preset_source() { # Default presets [ -z "$ARCH_OS_HOSTNAME" ] && ARCH_OS_HOSTNAME="arch-os" [ -z "$ARCH_OS_KERNEL" ] && ARCH_OS_KERNEL="linux-zen" [ -z "$ARCH_OS_BTRFS_SNAPPER_ENABLED" ] && ARCH_OS_BTRFS_SNAPPER_ENABLED='true' [ -z "$ARCH_OS_BTRFS_ASSISTANT_ENABLED" ] && ARCH_OS_BTRFS_ASSISTANT_ENABLED='true' [ -z "$ARCH_OS_SHELL_ENHANCEMENT_FISH_ENABLED" ] && ARCH_OS_SHELL_ENHANCEMENT_FISH_ENABLED="true" [ -z "$ARCH_OS_DESKTOP_EXTRAS_ENABLED" ] && ARCH_OS_DESKTOP_EXTRAS_ENABLED='true' [ -z "$ARCH_OS_DESKTOP_KEYBOARD_MODEL" ] && ARCH_OS_DESKTOP_KEYBOARD_MODEL="pc105" [ -z "$ARCH_OS_SAMBA_SHARE_ENABLED" ] && ARCH_OS_SAMBA_SHARE_ENABLED="true" [ -z "$ARCH_OS_ECN_ENABLED" ] && ARCH_OS_ECN_ENABLED="true" [ -z "$ARCH_OS_VM_SUPPORT_ENABLED" ] && ARCH_OS_VM_SUPPORT_ENABLED="true" [ -z "$ARCH_OS_DUAL_BOOT_ENABLED" ] && ARCH_OS_DUAL_BOOT_ENABLED="false" # Set microcode [ -z "$ARCH_OS_MICROCODE" ] && grep -E "GenuineIntel" &>/dev/null <<<"$(lscpu)" && ARCH_OS_MICROCODE="intel-ucode" [ -z "$ARCH_OS_MICROCODE" ] && grep -E "AuthenticAMD" &>/dev/null <<<"$(lscpu)" && ARCH_OS_MICROCODE="amd-ucode" # Load properties or select preset if [ -f "$SCRIPT_CONFIG" ]; then properties_source gum join "$(gum_green --bold "• ")" "$(gum_white "Setup preset loaded from: ")" "$(gum_white --bold "installer.conf")" else # Select preset local preset options options=("desktop - GNOME Desktop Environment (default)" "core - Minimal Arch Linux TTY Environment" "none - No pre-selection") preset=$(gum_choose --header "+ Choose Setup Preset" "${options[@]}") || trap_gum_exit_confirm [ -z "$preset" ] && return 1 # Check if new value is null preset="$(echo "$preset" | awk '{print $1}')" # Core preset if [[ $preset == core* ]]; then ARCH_OS_BTRFS_SNAPPER_ENABLED='false' ARCH_OS_BTRFS_ASSISTANT_ENABLED='false' ARCH_OS_DESKTOP_ENABLED='false' ARCH_OS_MULTILIB_ENABLED='false' ARCH_OS_HOUSEKEEPING_ENABLED='false' ARCH_OS_SHELL_ENHANCEMENT_ENABLED='false' ARCH_OS_BOOTSPLASH_ENABLED='false' ARCH_OS_MANAGER_ENABLED='false' ARCH_OS_DESKTOP_GRAPHICS_DRIVER="none" ARCH_OS_AUR_HELPER='none' fi # --------------------------------------------------------------------------------------------------- # Desktop preset if [[ $preset == desktop* ]]; then ARCH_OS_BTRFS_SNAPPER_ENABLED='true' ARCH_OS_BTRFS_ASSISTANT_ENABLED='true' ARCH_OS_DESKTOP_EXTRAS_ENABLED='true' ARCH_OS_SAMBA_SHARE_ENABLED='true' ARCH_OS_CORE_TWEAKS_ENABLED="true" ARCH_OS_BOOTSPLASH_ENABLED='true' ARCH_OS_DESKTOP_ENABLED='true' ARCH_OS_MULTILIB_ENABLED='true' ARCH_OS_HOUSEKEEPING_ENABLED='true' ARCH_OS_SHELL_ENHANCEMENT_ENABLED='true' ARCH_OS_MANAGER_ENABLED='true' ARCH_OS_AUR_HELPER='paru' fi # Write properties properties_source gum join "$(gum_green --bold "• ")" "$(gum_white "Setup preset loaded for: ")" "$(gum_white --bold "$preset")" fi return 0 } # //////////////////////////////////////////////////////////////////////////////////////////////////// # VALIDATORS # //////////////////////////////////////////////////////////////////////////////////////////////////// # Shared error format for all validators below: a "Property: / Issue:" pair instead of one long # sentence per validator - gum_fail renders this as an aligned two-line bullet (see gum_fail). validate_fail() { printf 'Property: %s\nIssue: %s\n' "$1" "$2" >"$ERROR_MSG_TMP_FILE"; } # --------------------------------------------------------------------------------------------------- # ARCH_OS_USERNAME can be set directly via the Advanced Setup Editor, bypassing the interactive # check in select_username (which only runs while the value is still empty); re-check here so a # bad value can never reach useradd. validate_username() { if [[ ! "$ARCH_OS_USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || [ "${#ARCH_OS_USERNAME}" -gt 32 ]; then validate_fail "ARCH_OS_USERNAME" "invalid value '${ARCH_OS_USERNAME}' (allowed: a-z, 0-9, '-', '_'; must not start with a digit; max 32 chars)" return 1 fi return 0 } # --------------------------------------------------------------------------------------------------- # ARCH_OS_HOSTNAME has no interactive selector at all (always advanced-editor-only, see # properties_preset_source); an invalid value would silently end up in /etc/hostname validate_hostname() { if [[ ! "$ARCH_OS_HOSTNAME" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then validate_fail "ARCH_OS_HOSTNAME" "invalid value '${ARCH_OS_HOSTNAME}' (allowed: a-z, 0-9, '-'; must not start/end with '-'; max 63 chars)" return 1 fi return 0 } # --------------------------------------------------------------------------------------------------- # Disk must already exist, and the enum-like properties below fall through their case/if-chains # silently when misspelled (no default branch anywhere) - that would otherwise produce a # half-installed, unbootable, or unaccelerated system with no visible error at all validate_install_properties() { if [ ! -b "$ARCH_OS_DISK" ]; then validate_fail "ARCH_OS_DISK" "disk '${ARCH_OS_DISK}' does not exist" return 1 fi case "$ARCH_OS_FILESYSTEM" in btrfs | ext4) ;; *) validate_fail "ARCH_OS_FILESYSTEM" "must be 'btrfs' or 'ext4' (got '${ARCH_OS_FILESYSTEM}')" return 1 ;; esac case "$ARCH_OS_BOOTLOADER" in systemd | grub) ;; *) validate_fail "ARCH_OS_BOOTLOADER" "must be 'systemd' or 'grub' (got '${ARCH_OS_BOOTLOADER}')" return 1 ;; esac if [ -n "$ARCH_OS_MICROCODE" ]; then case "$ARCH_OS_MICROCODE" in none | intel-ucode | amd-ucode) ;; *) validate_fail "ARCH_OS_MICROCODE" "must be 'none', 'intel-ucode' or 'amd-ucode' (got '${ARCH_OS_MICROCODE}')" return 1 ;; esac fi if [ "$ARCH_OS_DESKTOP_ENABLED" = "true" ] && [ -n "$ARCH_OS_DESKTOP_GRAPHICS_DRIVER" ]; then case "$ARCH_OS_DESKTOP_GRAPHICS_DRIVER" in none | mesa | intel_i915 | nvidia | amd | ati) ;; *) validate_fail "ARCH_OS_DESKTOP_GRAPHICS_DRIVER" "must be one of: none, mesa, intel_i915, nvidia, amd, ati (got '${ARCH_OS_DESKTOP_GRAPHICS_DRIVER}')" return 1 ;; esac fi return 0 } # --------------------------------------------------------------------------------------------------- # Hard safety gate for dual boot: refuse to continue if the configured partitions look wrong # (e.g. still pointing at the disk-derived defaults instead of an existing ESP + a real target partition). # The reason is written to ERROR_MSG_TMP_FILE so trap_exit shows this exact text instead of its # generic "An error occurred" fallback - one clear message instead of two stacked ones. validate_dual_boot_partitions() { local boot_type root_bytes boot_type="$(blkid -s TYPE -o value "$ARCH_OS_BOOT_PARTITION" 2>/dev/null)" if [ "$boot_type" != "vfat" ]; then validate_fail "ARCH_OS_BOOT_PARTITION" "'${ARCH_OS_BOOT_PARTITION}' is not an existing vfat/EFI partition (detected: ${boot_type:-none})" return 1 fi root_bytes="$(lsblk -bdn -o SIZE "$ARCH_OS_ROOT_PARTITION" 2>/dev/null)" if [ -z "$root_bytes" ] || [ "$root_bytes" -lt $((8 * 1024 * 1024 * 1024)) ]; then validate_fail "ARCH_OS_ROOT_PARTITION" "'${ARCH_OS_ROOT_PARTITION}' is smaller than 8 GiB, refusing to format it (looks like the wrong partition)" return 1 fi return 0 } # --------------------------------------------------------------------------------------------------- # ARCH_OS_KERNEL_ARGS is appended into a ',' delimited sed expression (see exec_pacstrap_core); # reject characters that would break or hijack that substitution instead of trying to escape them. # See validate_dual_boot_partitions above for why this writes to ERROR_MSG_TMP_FILE instead of gum_fail. validate_kernel_args() { case "$ARCH_OS_KERNEL_ARGS" in *[,\&\\]*) validate_fail "ARCH_OS_KERNEL_ARGS" "contains an unsupported character (, & or \\)" return 1 ;; esac return 0 } # --------------------------------------------------------------------------------------------------- # Runs all pre-flight validators above, part of the "Properties" step (see main) rather than its # own section. On failure, shows the specific reason directly (no log dump) and offers to fix it # right away in the Advanced Setup Editor instead of a hard exit. Call as: validate_properties || # continue - a fix (or a cancel) makes the caller restart the whole properties step from the top so # the corrected values are shown again before the Summary. validate_properties() { if validate_username && validate_hostname && validate_install_properties \ && { [ -z "$ARCH_OS_KERNEL_ARGS" ] || validate_kernel_args; } \ && { [ "$ARCH_OS_DUAL_BOOT_ENABLED" != "true" ] || validate_dual_boot_partitions; }; then return 0 fi # Show the specific reason directly and consume the file so trap_exit's generic "An error # occurred" + log pointer can't also fire below (exit 130 takes the short "Exit..." path instead) local error && [ -f "$ERROR_MSG_TMP_FILE" ] && error="$(<"$ERROR_MSG_TMP_FILE")" && rm -f "$ERROR_MSG_TMP_FILE" gum_fail "${error:-Validation failed}" [ "$FORCE" = "true" ] && exit 130 # Unattended: no prompt to hang on, just exit gum_confirm --affirmative="Fix" --negative="Exit" "Open Advanced Setup Editor?" || exit 130 print_header "Arch OS Installer" # Show landing page gum_title "Advanced Setup Editor" local header_txt="• Save with CTRL + D or ESC and cancel with CTRL + C" if gum_write --show-line-numbers --prompt "" --height=18 --width=180 --char-limit=0 --header="${header_txt}" --value="$(cat "$SCRIPT_CONFIG")" >"$SCRIPT_CONFIG_TMP_FILE"; then mv "$SCRIPT_CONFIG_TMP_FILE" "$SCRIPT_CONFIG" && properties_source gum_info "Properties saved" else rm -f "$SCRIPT_CONFIG_TMP_FILE" # Remove tmp properties gum_warn "Advanced Setup canceled" fi echo && ! gum_spin --title="Reload Properties in 3 seconds..." -- sleep 3 && trap_gum_exit return 1 # Restart the whole properties step from the top (see main) } # //////////////////////////////////////////////////////////////////////////////////////////////////// # SELECTORS # //////////////////////////////////////////////////////////////////////////////////////////////////// select_username() { if [ -z "$ARCH_OS_USERNAME" ]; then local user_input user_input=$(gum_input --header "+ Enter Username") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null # Validate against Linux username rules early (before any destructive step like disk wipe) if [[ ! "$user_input" =~ ^[a-z_][a-z0-9_-]*$ ]] || [ "${#user_input}" -gt 32 ]; then gum_confirm --affirmative="Ok" --negative="" "Invalid username '${user_input}' (allowed: a-z, 0-9, '-', '_'; must not start with a digit; max 32 chars)" return 1 fi ARCH_OS_USERNAME="$user_input" && properties_generate # Set value and generate properties file fi gum_property "Username" "$ARCH_OS_USERNAME" return 0 } # --------------------------------------------------------------------------------------------------- select_password() { # --change if [ "$1" = "--change" ] || [ -z "$ARCH_OS_PASSWORD" ]; then local user_password user_password_check user_password=$(gum_input --password --header "+ Enter Password") || trap_gum_exit_confirm if [ -z "$user_password" ]; then gum_confirm --affirmative="Ok" --negative="" "Password must not be empty" return 1 fi user_password_check=$(gum_input --password --header "+ Enter Password again") || trap_gum_exit_confirm if [ "$user_password" != "$user_password_check" ]; then gum_confirm --affirmative="Ok" --negative="" "The passwords are not identical" return 1 fi # Warn (but allow) on weak passwords; user keeps the final say if [ "${#user_password}" -lt 8 ] && ! gum_confirm $'Password is shorter than 8 chars.\nUse anyway?'; then return 1 fi ARCH_OS_PASSWORD="$user_password" && properties_generate # Set value and generate properties file fi [ "$1" = "--change" ] && gum_info "Password successfully changed" [ "$1" != "--change" ] && gum_property "Password" "*******" return 0 } # --------------------------------------------------------------------------------------------------- select_timezone() { if [ -z "$ARCH_OS_TIMEZONE" ]; then local tz_auto user_input # Auto-detect timezone via IP (best-effort) to pre-fill the filter tz_auto="$(curl -s --connect-timeout 5 --max-time 5 http://ip-api.com/line?fields=timezone)" user_input=$(timedatectl list-timezones | gum_filter --value="$tz_auto" --header "+ Choose Timezone") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_TIMEZONE="$user_input" && properties_generate # Set property and generate properties file fi gum_property "Timezone" "$ARCH_OS_TIMEZONE" return 0 } # --------------------------------------------------------------------------------------------------- # shellcheck disable=SC2001 select_language() { if [ -z "$ARCH_OS_LOCALE_LANG" ] || [ -z "${ARCH_OS_LOCALE_GEN_LIST[*]}" ]; then local user_input items options filter # Fetch available options (list all from /usr/share/i18n/locales and check if entry exists in /etc/locale.gen) mapfile -t items < <(basename -a /usr/share/i18n/locales/* | grep -v "@") # Create array without @ files # Add only available locales (!!! intense command !!!) options=() && for item in "${items[@]}"; do grep -q -e "^$item" -e "^#$item" /etc/locale.gen && options+=("$item"); done # shellcheck disable=SC2002 [ -r /root/.zsh_history ] && filter=$(cat /root/.zsh_history | grep 'loadkeys' | head -n 2 | tail -n 1 | cut -d';' -f2 | cut -d' ' -f2 | cut -d'-' -f1) # Select locale user_input=$(gum_filter --value="$filter" --header "+ Choose Language" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_LOCALE_LANG="$user_input" # Set property # Set locale.gen properties (auto generate ARCH_OS_LOCALE_GEN_LIST) ARCH_OS_LOCALE_GEN_LIST=() && while read -r locale_entry; do ARCH_OS_LOCALE_GEN_LIST+=("$locale_entry") # Remove leading # from matched lang in /etc/locale.gen and add entry to array done < <(sed "/^#${ARCH_OS_LOCALE_LANG}/s/^#//" /etc/locale.gen | grep "$ARCH_OS_LOCALE_LANG") # Add en_US fallback (every language) if not already exists in list [[ "${ARCH_OS_LOCALE_GEN_LIST[*]}" != *'en_US.UTF-8 UTF-8'* ]] && ARCH_OS_LOCALE_GEN_LIST+=('en_US.UTF-8 UTF-8') properties_generate # Generate properties file (for ARCH_OS_LOCALE_LANG & ARCH_OS_LOCALE_GEN_LIST) fi gum_property "Language" "$ARCH_OS_LOCALE_LANG" return 0 } # --------------------------------------------------------------------------------------------------- select_keyboard() { if [ -z "$ARCH_OS_VCONSOLE_KEYMAP" ]; then local user_input items options filter mapfile -t items < <(command localectl list-keymaps) options=() && for item in "${items[@]}"; do options+=("$item"); done # shellcheck disable=SC2002 [ -r /root/.zsh_history ] && filter=$(cat /root/.zsh_history | grep 'loadkeys' | head -n 2 | tail -n 1 | cut -d';' -f2 | cut -d' ' -f2 | cut -d'-' -f1) user_input=$(gum_filter --value="$filter" --header "+ Choose Keyboard" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_VCONSOLE_KEYMAP="$user_input" && properties_generate # Set value and generate properties file fi gum_property "Keyboard" "$ARCH_OS_VCONSOLE_KEYMAP" return 0 } # --------------------------------------------------------------------------------------------------- select_disk() { if [ -z "$ARCH_OS_DISK" ] || [ -z "$ARCH_OS_BOOT_PARTITION" ] || [ -z "$ARCH_OS_ROOT_PARTITION" ]; then local user_input items options mapfile -t items < <(lsblk -I 8,259,254 -d -o KNAME,SIZE -n) # size: $(lsblk -d -n -o SIZE "/dev/${item}") options=() && for item in "${items[@]}"; do options+=("/dev/${item}"); done user_input=$(gum_choose --header "+ Choose Disk" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null user_input=$(echo "$user_input" | awk -F' ' '{print $1}') # Remove size from input [ ! -e "$user_input" ] && log_fail "Disk does not exist" && return 1 ARCH_OS_DISK="$user_input" # Set property # Append 'p' when the device name ends in a digit (covers nvme*, mmcblk*, loop*) local part_sep="" && [[ "$ARCH_OS_DISK" =~ [0-9]$ ]] && part_sep="p" ARCH_OS_BOOT_PARTITION="${ARCH_OS_DISK}${part_sep}1" ARCH_OS_ROOT_PARTITION="${ARCH_OS_DISK}${part_sep}2" properties_generate # Generate properties file fi gum_property "Disk" "$ARCH_OS_DISK" return 0 } # --------------------------------------------------------------------------------------------------- select_filesystem() { if [ -z "$ARCH_OS_FILESYSTEM" ]; then local user_input options options=("btrfs" "ext4") user_input=$(gum_choose --header "+ Choose Filesystem (snapshot support: btrfs)" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_FILESYSTEM="$user_input" && properties_generate # Set value and generate properties file fi gum_property "Filesystem" "${ARCH_OS_FILESYSTEM}" return 0 } # --------------------------------------------------------------------------------------------------- select_bootloader() { if [ -z "$ARCH_OS_BOOTLOADER" ]; then local user_input options options=("systemd" "grub") user_input=$(gum_choose --header "+ Choose Bootloader (snapshot menu: grub)" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_BOOTLOADER="$user_input" && properties_generate # Set value and generate properties file fi gum_property "Bootloader" "${ARCH_OS_BOOTLOADER}" return 0 } # --------------------------------------------------------------------------------------------------- select_enable_desktop_driver() { if [ "$ARCH_OS_DESKTOP_ENABLED" = "true" ]; then if [ -z "$ARCH_OS_DESKTOP_GRAPHICS_DRIVER" ] || [ "$ARCH_OS_DESKTOP_GRAPHICS_DRIVER" = "none" ]; then local user_input options options=("mesa" "intel_i915" "nvidia" "amd" "ati") user_input=$(gum_choose --header "+ Choose Desktop Graphics Driver (default: mesa)" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_DESKTOP_GRAPHICS_DRIVER="$user_input" && properties_generate # Set value and generate properties file fi gum_property "Desktop Graphics Driver" "$ARCH_OS_DESKTOP_GRAPHICS_DRIVER" fi return 0 } # --------------------------------------------------------------------------------------------------- # variant_map (layout -> valid variants) is static reference data, see STATIC INPUT VALUES below select_enable_desktop_keyboard() { [ "$ARCH_OS_DESKTOP_ENABLED" != "true" ] && return 0 if [ -z "$ARCH_OS_DESKTOP_KEYBOARD_LAYOUT" ]; then local layout variant variants local layouts=() variant_list=() read -ra layouts <<<"$(gnome_keymap_layouts)" # Layout filter, pre-filled from the chosen console keymap (e.g. 'de-latin1-...' -> 'de') layout=$(gum_filter --value="${ARCH_OS_VCONSOLE_KEYMAP%%-*}" --header "+ Choose Desktop Keyboard Layout" "${layouts[@]}") || trap_gum_exit_confirm [ -z "$layout" ] && return 1 # Check if new value is null ARCH_OS_DESKTOP_KEYBOARD_LAYOUT="$layout" ARCH_OS_DESKTOP_KEYBOARD_VARIANT="" gum_property "Desktop Keyboard" "$ARCH_OS_DESKTOP_KEYBOARD_LAYOUT" # Variant filter restricted to the chosen layout; '(none)' = no variant variants="${variant_map[$layout]:-}" if [ -n "$variants" ]; then read -ra variant_list <<<"$variants" variant=$(gum_filter --header "+ Choose Desktop Keyboard Variant" "(none)" "${variant_list[@]}") || trap_gum_exit_confirm [ "$variant" != "(none)" ] && ARCH_OS_DESKTOP_KEYBOARD_VARIANT="$variant" fi properties_generate else gum_property "Desktop Keyboard" "$ARCH_OS_DESKTOP_KEYBOARD_LAYOUT" fi [ -n "$ARCH_OS_DESKTOP_KEYBOARD_VARIANT" ] && gum_property "Desktop Keyboard Variant" "$ARCH_OS_DESKTOP_KEYBOARD_VARIANT" return 0 } # --------------------------------------------------------------------------------------------------- select_enable_aur() { if [ -z "$ARCH_OS_AUR_HELPER" ]; then local user_input options options=("paru" "paru-bin" "paru-git" "none") user_input=$(gum_choose --header "+ Choose AUR Helper (default: paru)" "${options[@]}") || trap_gum_exit_confirm [ -z "$user_input" ] && return 1 # Check if new value is null ARCH_OS_AUR_HELPER="$user_input" && properties_generate # Set value and generate properties file fi gum_property "AUR Helper" "$ARCH_OS_AUR_HELPER" return 0 } # //////////////////////////////////////////////////////////////////////////////////////////////////// # SELECTORS (TOGGLER) # //////////////////////////////////////////////////////////////////////////////////////////////////// # Generic yes/no selector: select_toggle "" "