#!/bin/bash set -uo pipefail ################################################ # Initialize script environment # Find the directory this script is stored in. (from: http://stackoverflow.com/questions/59895) function get_source_dir() { local source="${BASH_SOURCE[0]}" while [[ -h $source ]]; do local tmp tmp="$(cd -P "$(dirname "${source}")" && pwd)" source="$(readlink "${source}")" [[ $source != /* ]] && source="${tmp}/${source}" done echo -n "$(realpath "$(dirname "${source}")")" } GENTOO_INSTALL_REPO_DIR="$(get_source_dir)" export GENTOO_INSTALL_REPO_DIR export GENTOO_INSTALL_REPO_SCRIPT_ACTIVE=true # shellcheck source=./scripts/utils.sh source "$GENTOO_INSTALL_REPO_DIR/scripts/utils.sh" # Remember actual config file path CONFIG_FILE="$(realpath "${1-"gentoo.conf"}" 2>/dev/null)" RELA_CONFIG_FILE="$(realpath --relative-to="$(pwd)" "$CONFIG_FILE" 2>/dev/null)" # Check if help is requested while [[ $# -gt 0 ]]; do case "$1" in ""|"help"|"--help"|"-help"|"-h") echo "Usage: $0 [config]" echo "Starts the gentoo configurator. If no file is given, it defaults to 'gentoo.conf'." echo "If the file doesn't exist, the configurator loads default values, otherwise, the" echo "the configuration is loaded into the configurator." exit 0 ;; esac shift done check_wanted_programs dialog ncurses=ncursesw6-config # Determine whether EFI is available HAS_EFI_SUPPORT=$([[ -d /sys/firmware/efi ]] && echo -n "true" || echo -n "false") DEFAULT_BOOT_TYPE=$([[ $HAS_EFI_SUPPORT == true ]] && echo -n "efi" || echo -n "bios") EFI_UNSUPPORTED_MESSAGE_SHORT="Missing EFI support on this system!" EFI_UNSUPPORTED_MESSAGE="Apparently, your system does NOT support efi. Double-check before proceeding!" # Wrap dialog in two functions to prevent it from cluttering stderr. function dialog_wrapper() { dialog_out=$(command dialog --colors "$@" 3>&2 2>&1 1>&3 3>&-); } function dialog() { dialog_wrapper "$@" 2>&1; } ################################################ # Configuration helper functions function get_timezone() { local file if file="$(readlink /etc/localtime)"; then # /etc/localtime is a symlink as expected local timezone timezone=${file#*zoneinfo/} if [[ $timezone == "$file" ]]; then # not pointing to expected location or not Region/City echo "Europe/London" else echo "$timezone" fi else # compare files by contents find /usr/share/zoneinfo -type f -exec cmp -s {} /etc/localtime \; -print \ | sed -e 's,.*/zoneinfo/,,' \ | head -1 fi } function get_default_keymap() { local keymap keymap="$(grep KEYMAP /etc/vconsole.conf 2>/dev/null || echo "KEYMAP=us")" keymap="${keymap#KEYMAP=}" local map for map in "${ALL_KEYMAPS[@]}"; do if [[ $map == "$keymap" ]]; then echo -n "${keymap}" return fi done # Fallback to us echo -n "us" } function get_all_keymaps() { ALL_KEYMAPS=() local map for map in $(find /usr/share/keymaps/ /usr/share/kbd/keymaps/ -type f -iname '*.map.gz' -printf "%f\n" 2>/dev/null | sort -u); do ALL_KEYMAPS+=("${map%%.map.gz}") done } function get_all_timezones() { readarray -t ALL_TIMEZONES < <(find /usr/share/zoneinfo -type f -printf "%P\n" | sort -u) } function recalculate_locales() { LOCALES="" N_LOCALES=0 local selected_index_list="$SELECTED_LOCALES" local next_selected next_selected="${selected_index_list%% *}" selected_index_list="${selected_index_list#* }" local i=0 for item in "${SUPPORTED_LOCALES[@]}"; do if [[ "$i" == "$next_selected" ]]; then LOCALES="$LOCALES"$'\n'"$item" next_selected="${selected_index_list%% *}" selected_index_list="${selected_index_list#* }" ((++N_LOCALES)) fi ((++i)) done LOCALES="${LOCALES:1}" } function define_disk_configuration_function() { cat << EOF function disk_configuration() { $* ; } EOF } function define_swap() { if [[ $PARTITIONING_USE_SWAP == "true" ]]; then echo -n "${PARTITIONING_SWAP@Q}" else echo -n "false" fi } function define_zfs_compression() { if [[ $PARTITIONING_ZFS_USE_COMPRESSION != "false" ]]; then echo -n "${PARTITIONING_ZFS_COMPRESSION@Q}" else echo -n "false" fi } function define_disk_layout() { local swapdev case "$PARTITIONING_SCHEME" in "classic_single_disk") define_disk_configuration_function "create_classic_single_disk_layout swap=$(define_swap) type=${PARTITIONING_BOOT_TYPE@Q} luks=${PARTITIONING_USE_LUKS@Q} root_fs=${PARTITIONING_ROOT_FS@Q}" "${PARTITIONING_DEVICE@Q}" ;; "existing_partitions") if [[ "$PARTITIONING_USE_SWAP" ]]; then swapdev=${PARTITIONING_SWAP_DEVICE:-false} else swapdev=false fi define_disk_configuration_function "create_existing_partitions_layout boot=${PARTITIONING_BOOT_DEVICE@Q} swap=${swapdev@Q} type=${PARTITIONING_BOOT_TYPE@Q}" "${PARTITIONING_DEVICE@Q}" ;; "zfs_centric") define_disk_configuration_function "create_zfs_centric_layout swap=$(define_swap) type=${PARTITIONING_BOOT_TYPE@Q} encrypt=${PARTITIONING_ZFS_USE_ENCRYPTION@Q} compress=$(define_zfs_compression) pool_type=${PARTITIONING_ZFS_POOL_TYPE@Q}" "${PARTITIONING_DEVICES[@]@Q}" ;; "btrfs_centric") define_disk_configuration_function "create_btrfs_centric_layout swap=$(define_swap) type=${PARTITIONING_BOOT_TYPE@Q} raid_type=${PARTITIONING_BTRFS_RAID_TYPE@Q} luks=${PARTITIONING_USE_LUKS@Q}" "${PARTITIONING_DEVICES[@]@Q}" ;; "raid0_luks") define_disk_configuration_function "create_raid0_luks_layout swap=$(define_swap) type=${PARTITIONING_BOOT_TYPE@Q} root_fs=${PARTITIONING_ROOT_FS@Q}" "${PARTITIONING_DEVICES[@]@Q}" ;; "custom") # Show current function declaration, trim trailing whitespace declare -f disk_configuration \ | sed -e 's/\s*$//' ;; esac } ALL_GENTOO_ARCHS=("x86" "amd64" "arm" "arm64") ALL_STAGE3_VARIANTS=( "openrc" "openrc | Minimal OpenRC base (recommended)" "desktop-openrc" "openrc-desktop | OpenRC, desktop profile, might have blockers" "systemd" "systemd | Minimal systemd base (recommended)" "systemd-mergedusr" "systemd-mergedusr | Minimal systemd base with merged filesystem layout" "desktop-systemd" "systemd-desktop | systemd, desktop profile, might have blockers" "nomultilib-openrc" "nomultilib-openrc | Minimal OpenRC base without 32bits support (Experimental)" "nomultilib-systemd" "nomultilib-systemd | Minimal systemd base without 32bits support (Experimental)" "nomultilib-systemd-mergedusr" "nomultilib-systemd-mergedusr | Minimal systemd base with merged filesystem layout and without 32bits support (Experimental)" "x32-openrc" "x32-openrc | Minimal OpenRC base without 64bits support (Experimental)" "x32-systemd" "x32-systemd | Minimal systemd base without 64bits support (Experimental)" "x32-systemd-mergedusr" "x32-systemd-mergedusr | Minimal systemd base with merged filesystem layout and without 64bits support (Experimental)" "llvm-openrc" "llvm-openrc | Minimal OpenRC base compiled with LLVM (Experimental)" "llvm-systemd" "llvm-systemd | Minimal systemd base compiled with LLVM (Experimental)" "llvm-systemd-mergedusr" "llvm-systemd-mergedusr | Minimal systemd base with merged filesystem layout compiled with LLVM (Experimental)" "hardened-openrc" "hardened-openrc | Hardened OpenRC base (Experimental)" "hardened-nomultilib-openrc" "hardened-nomultilib-openrc | Hardened OpenRC base without 32bits support (Experimental)" "hardened-selinux-openrc" "hardened-selinux-openrc | Hardened OpenRC base with SELinux (Experimental)" "hardened-nomultilib-selinux-openrc" "hardened-nomultilib-selinux-openrc | Hardened OpenRC base with SELinux and without 32bits support (Experimental)" "musl" "musl-openrc | Minimal OpenRC base using musl (Experimental)" "musl-llvm" "musl-llvm-openrc | Minimal OpenRC base using musl compiled with LLVM (Experimental)" "musl-hardened" "musl-hardened-openrc | Hardened OpenRC base using musl (Experimental)" ) ALL_PARTITIONING_SCHEMES=( "classic_single_disk" "Classic single disk layout (boot/efi, swap?, root)" "existing_partitions" "Skip partitioning, use existing pre-formatted partitions" "zfs_centric" "ZFS centric (optional ZFS compression and encryption)" "btrfs_centric" "Btrfs centric (optional raid0/1 via btrfs)" "raid0_luks" "Raid0 (N>=2 disks) and luks for root" "custom" "Custom (expert option; edit the config manually later)" ) PARTITIONING_BOOT_TYPES=( "efi" "efi$([[ $HAS_EFI_SUPPORT == true ]] && echo -n "" || echo -n " (!! $EFI_UNSUPPORTED_MESSAGE_SHORT !!)")" "bios" "bios" ) PARTITIONING_ROOT_FS_TYPES=("ext4" "btrfs") PARTITIONING_BTRFS_RAID_TYPES=("raid0" "raid1") PARTITIONING_ZFS_POOL_TYPES=("standard" "custom") PARTITIONING_ZFS_COMPRESSIONS=("on" "gzip" "lz4" "lzjb" "zle" "zstd" "zstd-fast") PORTAGE_SYNC_TYPES=("git" "rsync") function create_single_disk_layout() { create_classic_single_disk_layout "$@" } function parse_swap() { if [[ $1 == "false" ]]; then PARTITIONING_USE_SWAP=false PARTITIONING_SWAP=8GiB else PARTITIONING_USE_SWAP=true PARTITIONING_SWAP="$1" fi } function parse_zfs_compression() { if [[ $1 == "false" ]]; then PARTITIONING_ZFS_USE_COMPRESSION=false PARTITIONING_ZFS_COMPRESSION=zstd else PARTITIONING_ZFS_USE_COMPRESSION=true PARTITIONING_ZFS_COMPRESSION="$1" fi } function create_classic_single_disk_layout() { PARTITIONING_SCHEME="classic_single_disk" local known_arguments=('+swap' '?type' '?luks' '?root_fs') local extra_arguments=() declare -A arguments; parse_arguments "$@" PARTITIONING_DEVICE="${extra_arguments[0]}" parse_swap "${arguments[swap]}" PARTITIONING_BOOT_TYPE="${arguments[type]}" PARTITIONING_USE_LUKS="${arguments[luks]:-false}" PARTITIONING_ROOT_FS="${arguments[root_fs]:-ext4}" } function create_existing_partitions_layout() { PARTITIONING_SCHEME="existing_partitions" local known_arguments=('+swap' '+boot' '?type') local extra_arguments=() declare -A arguments; parse_arguments "$@" PARTITIONING_DEVICE="${extra_arguments[0]}" if [[ "${arguments[swap]}" == "false" ]]; then PARTITIONING_USE_SWAP=false PARTITIONING_SWAP_DEVICE="" else PARTITIONING_USE_SWAP=true PARTITIONING_SWAP_DEVICE="${arguments[swap]}" fi PARTITIONING_BOOT_TYPE="${arguments[type]}" PARTITIONING_BOOT_DEVICE="${arguments[boot]}" } function create_raid0_luks_layout() { PARTITIONING_SCHEME="raid0_luks" local known_arguments=('+swap' '?type' '?root_fs') local extra_arguments=() declare -A arguments; parse_arguments "$@" PARTITIONING_DEVICES=("${extra_arguments[@]}") parse_swap "${arguments[swap]}" PARTITIONING_BOOT_TYPE="${arguments[type]}" PARTITIONING_ROOT_FS="${arguments[root_fs]:-ext4}" } function create_zfs_centric_layout() { PARTITIONING_SCHEME="zfs_centric" local known_arguments=('+swap' '?type' '?pool_type' '?encrypt' '?compress') local extra_arguments=() declare -A arguments; parse_arguments "$@" PARTITIONING_DEVICES=("${extra_arguments[@]}") parse_swap "${arguments[swap]}" PARTITIONING_BOOT_TYPE="${arguments[type]}" PARTITIONING_ZFS_POOL_TYPE="${arguments[pool_type]:-standard}" PARTITIONING_ZFS_USE_ENCRYPTION="${arguments[encrypt]:-false}" parse_zfs_compression "${arguments[compress]:-false}" } function create_btrfs_raid_layout() { create_btrfs_centric_layout "$@" } function create_btrfs_centric_layout() { PARTITIONING_SCHEME="btrfs_centric" # shellcheck disable=SC2034 local known_arguments=('+swap' '?type' '?raid_type' '?luks') local extra_arguments=() declare -A arguments; parse_arguments "$@" PARTITIONING_DEVICES=("${extra_arguments[@]}") parse_swap "${arguments[swap]}" PARTITIONING_BOOT_TYPE="${arguments[type]}" PARTITIONING_USE_LUKS="${arguments[luks]:-false}" PARTITIONING_BTRFS_RAID_TYPE="${arguments[raid_type]:-raid0}" } ################################################ # Configuration constants get_all_keymaps get_all_timezones readarray -t SUPPORTED_LOCALES < "$GENTOO_INSTALL_REPO_DIR/contrib/i18n_supported" readarray -t LOCALE_A < <(locale -a) ################################################ # Load/Default configuration function load_selected_locales() { local sel_locales=() local IFS=$'\n' declare -A selected_by_name for i in $LOCALES; do selected_by_name["$i"]=true done local i=0 for item in "${SUPPORTED_LOCALES[@]}"; do [[ "${selected_by_name[$item]:-}" == true ]] \ && sel_locales+=("$i") ((++i)) done local IFS=" " SELECTED_LOCALES="${sel_locales[*]}" } function process_config() { disk_configuration if [[ "$KEYMAP" == "$KEYMAP_INITRAMFS" ]]; then KEYMAP_INITRAMFS_OTHER=false else KEYMAP_INITRAMFS_OTHER=true fi load_selected_locales recalculate_locales } function load_config() { # First load defaults, then replace by sourcing config. load_default_config # Fallback to custom partitioning scheme if it isn't set in the actual config PARTITIONING_SCHEME="custom" # Load settings # shellcheck disable=SC1090 source "$1" || die "Could not load given configuration." # After loading a config no unsaved changes exist. UNSAVED_CHANGES=false } function load_default_config() { HOSTNAME="gentoo" TIMEZONE="$(get_timezone)" KEYMAP="$(get_default_keymap)" KEYMAP_INITRAMFS="$KEYMAP" LOCALES="C.UTF-8 UTF-8" LOCALE="C.UTF-8" SYSTEMD_NETWORKD=true SYSTEMD_NETWORKD_INTERFACE_NAME="en*" SYSTEMD_NETWORKD_DHCP=true SYSTEMD_NETWORKD_ADDRESSES=("192.168.1.100/32" "fd00::1/64") SYSTEMD_NETWORKD_GATEWAY="192.168.1.1" SYSTEMD_INITRAMFS_SSHD=false function disk_configuration() { #create_zfs_centric_layout swap=8GiB type="$DEFAULT_BOOT_TYPE" encrypt=true compress=zstd pool_type=standard /dev/sdX create_classic_single_disk_layout swap=8GiB type="$DEFAULT_BOOT_TYPE" luks=true root_fs=ext4 /dev/sdX } STAGE3_VARIANT="systemd" PORTAGE_SYNC_TYPE="git" PORTAGE_GIT_FULL_HISTORY=false PORTAGE_GIT_MIRROR="https://anongit.gentoo.org/git/repo/sync/gentoo.git" GENTOO_MIRROR="https://mirror.eu.oneandone.net/linux/distributions/gentoo/gentoo" GENTOO_ARCH="amd64" USE_PORTAGE_TESTING=true SELECT_MIRRORS=false SELECT_MIRRORS_LARGE_FILE=false ADDITIONAL_PACKAGES=() ENABLE_SSHD=true ROOT_SSH_AUTHORIZED_KEYS="" # All settings are unsaved. UNSAVED_CHANGES=true } SAVE_AS_FILENAME="$RELA_CONFIG_FILE" if [[ -e "$CONFIG_FILE" ]]; then load_config "$CONFIG_FILE" else load_default_config fi process_config ################################################ # Menu helpers and constants # $1: exit code function clear_and_exit() { dialog --clear clear -x exit "$1" } function ellipsis() { local len="$1" shift local str="$*" if [[ "${#str}" -gt "$len" ]]; then echo "${str:0:$len}…" else echo "$str" fi } function on_off_toggle() { if [[ "${!1}" == true ]]; then declare -g "$1"=false else declare -g "$1"=true fi } function on_off_str() { if [[ "$1" == true ]]; then echo -n "$2" else echo -n "$3" fi } function on_off_label() { local prefix="${2:-}" on_off_str "$1" "${prefix}[*]" "${prefix}[ ]" } function on_off_label_inverted() { local var=$1 shift on_off_label "$(is_on "$var" && echo false || echo true)" "$@" } function is_on() { [[ "$1" == true ]] } function is_off() { [[ "$1" != true ]] } # if $1 is in $2.. function one_of() { local what="$1" shift for i in "$@"; do [[ "$i" == "$what" ]] \ && return 0 done return 1 } # $1: title # $2: description # $3: space separated index list of selected items (e.g. "0 1 5 6") # $@: all items function menu_splitlist() { local title="$1" local description="$2" local selected_index_list="$3" shift 3 # Build option array local items=() local item local i=0 local next_selected="${selected_index_list%% *}" local selected_index_list="${selected_index_list#* }" for item in "$@"; do if [[ "$i" == "$next_selected" ]]; then items+=("$i" "$item" "on") next_selected="${selected_index_list%% *}" selected_index_list="${selected_index_list#* }" else items+=("$i" "$item" "off") fi ((++i)) done # Show selection dialog dialog \ --title "$title" \ --buildlist "$description\nUse ^ to focus the list of unselected items and $ to focus the list of selected items. Use to select/deselect an item and select by pressing when finished." \ "${BUILDLIST_SIZE[@]}" "${items[@]}" local diag_exit=$? if [[ $diag_exit == 0 ]]; then # return 0 elif [[ $diag_exit == 1 ]]; then # return 1 else # return 1 fi } # $1: title # $2: description # $3: default item # $@: [tag label]... function menu_radiolist_labeled() { local title="$1" local description="$2" local default_item="$3" shift 3 # Build option array local items=() local tag local label while [[ "$#" -gt 0 ]]; do tag="$1" label="$2" shift 2 if [[ $tag == "$default_item" ]]; then items+=("$tag" "$label" "on") else items+=("$tag" "$label" "off") fi done # Show selection dialog dialog \ --no-tags \ --title "$title" \ --help-button \ --help-label "Select" \ --help-status \ --ok-label "OK" \ --default-item "$default_item" \ --default-button help \ --radiolist "$description\nUse local sel="${dialog_out#HELP }" local sel_cur="${sel% *}" #local sel_radio="${sel#* }" dialog_out="$sel_cur" return 0 else # return 1 fi } # $1: title # $2: description # $3: default item # $@: items function menu_radiolist() { local title="$1" local description="$2" local default_item="$3" shift 3 # Build option array local items=() local item for item in "$@"; do if [[ $item == "$default_item" ]]; then items+=("$item" "on") else items+=("$item" "off") fi done # Show selection dialog dialog \ --no-items \ --title "$title" \ --help-button \ --help-label "Select" \ --help-status \ --ok-label "OK" \ --default-item "$default_item" \ --default-button help \ --radiolist "$description\nUse local sel="${dialog_out#HELP }" local sel_cur="${sel% *}" #local sel_radio="${sel#* }" dialog_out="$sel_cur" return 0 else # return 1 fi } # $1: title # $2: description # $3: current device (will be canonicalized if possible) function menu_select_device() { local title="$1" local desc="$2" local prev_selected prev_selected=$(canonicalize_device "$3") while true; do local all_devices=() for dev in /dev/disk/by-id/*; do all_devices+=("$dev" "$(basename "$dev")") done all_devices+=("/dev/null" "") if menu_radiolist_labeled "$title" "$desc" "$prev_selected" "${all_devices[@]}"; then if [[ "$dialog_out" == "/dev/null" ]]; then while true; do if dialog \ --title "$1" \ --inputbox "$([[ -e $prev_selected ]] || echo -n "\Z1The previously selected device $prev_selected does not exist!\Zn ")Enter the path of the desired device." \ "${INPUTBOX_SIZE[@]}" "$prev_selected" then # Repeat until selected device exists or cancelled prev_selected="$dialog_out" [[ ! -e "$dialog_out" ]] && continue return 0 else # -> return to previous menu break fi done # Return to radiolist continue else return 0 fi else # return 1 fi done } function msgbox_help() { dialog --msgbox "$1" "${HELP_POPUP_SIZE[@]}" } function menu_exit() { if [[ $UNSAVED_CHANGES == "true" ]]; then dialog \ --help-button --help-label "Back" \ --yes-label "Save" --no-label "Discard" \ --yesno "Do you want to save your configuration?\n(Press , or choose to continue gentoo configuration)." \ "${CONFIRM_SIZE[@]}" local diag_exit="$?" if [[ $diag_exit == 0 ]]; then # save "$CONFIG_FILE" clear_and_exit 0 elif [[ $diag_exit == 1 ]]; then # clear_and_exit 0 else # Back to menu (, ) true fi else # Nothing was changed. Exit immediately. clear_and_exit 0 fi } function menu_save_as() { dialog \ --ok-label "Save" \ --inputbox "Enter a filename to which this configuration should be saved.\n(Press , or choose to abort)." \ "${INPUTBOX_SIZE[@]}" "$SAVE_AS_FILENAME" local diag_exit="$?" if [[ $diag_exit == 0 ]]; then # SAVE_AS_FILENAME="$dialog_out" save "$SAVE_AS_FILENAME" UNSAVED_CHANGES=false else # Back to menu (, ) true fi } function menu() { local item local item_tag local tag_item_list=() declare -A reverse_lookup # Create menu list for item in "${MENU_ITEMS[@]}"; do # Only if item is visible "${item}_show" || continue item_tag="$("${item}_tag")" tag_item_list+=("$item_tag" "$("${item}_label")") reverse_lookup["$item_tag"]="$item" done dialog --colors \ --title "Gentoo configuration ($RELA_CONFIG_FILE)" \ --extra-button --extra-label "Exit" \ --help-button \ --default-item "$SELECTED_MENU_ITEM" \ --ok-label "Select" --cancel-label "Save" \ --menu "This is the gentoo configuration menu. Read and adjust all options below carefully. Save your desired configuration and run ./install afterwards. Use if you want further information on any option." \ "${MENU_SIZE[@]}" "${tag_item_list[@]}" local diag_exit="$?" if [[ $diag_exit == 0 ]]; then #