#!/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_has_programs dialog ncurses # 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 get_supported_locales() { if [[ -e /usr/share/i18n/SUPPORTED ]]; then echo /usr/share/i18n/SUPPORTED else echo "$GENTOO_INSTALL_REPO_DIR/contrib/i18n_supported" fi } 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() { 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}" ;; "zfs_centric") define_disk_configuration_function "create_zfs_centric_layout swap=$(define_swap) type=${PARTITIONING_BOOT_TYPE@Q} encrypt=${PARTITIONING_ZFS_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 } INIT_SYSTEMS=("systemd" "OpenRC") ALL_GENTOO_ARCHS=("x86" "amd64" "arm" "arm64") ALL_PARTITIONING_SCHEMES=( "classic_single_disk" "Classic single disk layout (boot, swap, root)" "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 (edit the config manually later)" ) PARTITIONING_BOOT_TYPES=("efi" "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_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_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 < "$(get_supported_locales)" 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 [[ "$SYSTEMD" == true ]]; then INIT_SYSTEM="systemd" else INIT_SYSTEM="OpenRC" fi 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.utf8" function disk_configuration() { #create_zfs_centric_layout swap=8GiB type=efi encrypt=true compress=zstd pool_type=standard /dev/sdX create_classic_single_disk_layout swap=8GiB type=efi luks=true root_fs=ext4 /dev/sdX } SYSTEMD=true 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" SELECT_MIRRORS=true SELECT_MIRRORS_LARGE_FILE=false ADDITIONAL_PACKAGES=() INSTALL_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 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 } 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 #