2
\$\begingroup\$

I wrote a bash script that installs Arch Linux. I wanted to simplify the installation of the Arch Linux system by automating things like setting locale, hostname, networking, etc. This is my second bigger script right to the two scripts that automate setting the ZFS filesystem. Any code review of what can I improve is appreciated.

lib/utils

#!/usr/bin/env bash

die() {
    echo "Error: $1. Exiting..." 1>&2
    exit 1
}

require_binaries() {
    local binaries=( "$@" )
    for binary in "${binaries[@]}"; do
        if ! command -v "${binary}" &> /dev/null; then
            die "${binary} binary does not exist"
        fi
    done
}

is_mounted() {  # Copied from https://www.baeldung.com/linux/bash-is-directory-mounted
    mount | awk -v DIR="$1" '{if ($3 == DIR) { exit 0}} ENDFILE{exit -1}'
}

insist_root() {
    if [ "$EUID" -ne 0 ]; then
        echo "Please run as root"
        exit 1
    fi
}

array_contains_element() {
    local array=$1
    local str=$2
    for elem in "${array[@]}"; do
        if [[ "${elem}" == "${str}" ]]; then
            return 0
        fi
    done
    return 1
}

read_password() {
    local password confirm_password
    until
        stty -echo
        read -r -p "Password: " password
        echo
        read -r -p "Confirm password: " confirm_password
        echo
        stty echo
        [[ "${password}" = "${confirm_password}" ]]
    do
        echo "Error: passwords don't match." >&2
    done
    echo "${password}"
}

_another_instance() {
    echo "There is another instance running, exiting"
    exit 1
}

create_lock() {
    LOCKFILE=/var/lock/$(basename "$0")
    LOCKFD="100"
    eval "exec ${LOCKFD}>\"${LOCKFILE}\"" || exit 1
    flock -xn ${LOCKFD} || _another_instance
    trap 'flock -u ${LOCKFD}; flock -xn ${LOCKFD} && rm -f ${LOCKFILE}' EXIT
}

install_arch

#!/usr/bin/env bash

source ./lib/utils

insist_root
create_lock
require_binaries "pacman" "pacstrap" "genfstab" "arch-chroot"

ARCHZFS_REPO_KEY="DDF7DB817396A49B2A2723F7403BD972F75D9D76"
CWD="$(pwd)"
ARCH_AVAILABLE_PROFILES=( "grub" "uefi" "devel" "firmware" "parabola_firmware" "zfs" "kernel_headers" "python"
                          "zfs_force_dkms" "!dkms" "genfstab" "genfstab_uuid" "genfstab_label" "zfs_snapshot_install"
                          "swap_encrypt" "raid" )
ARCH_ENABLED_PROFILES=${ARCH_ENABLED_PROFILES:-"grub uefi firmware genfstab genfstab_uuid"}

usage() {
    cat <<EOF
$0 - Install Arch Linux base system
Usage:
    $0 [ options ]
    $0 --help
Available profiles:
    ${ARCH_AVAILABLE_PROFILES[*]}
Enabled profiles by default:
    ${ARCH_ENABLED_PROFILES[*]}
Options:
    -m|--mnt value - rootfs mount path, required
    -u|--user value - target user name, required (default: user)
    -p|--profiles "quoted list of values" - instructs script how to configure Arch 
    -e|--extra-software "quoted list of additional software to install"
    -t|--timezone "value" - target system timezone (default: UTC)
    --hwclock-timezone (localtime|UTC) - hardware clock timezone (default: UTC)
    -l|--enabled-locales "quoted list of values" - locales to enable in /etc/locale.gen (default: "en_US.UTF-8")
    -L|--locale value - default system locale (default: en_US.UTF-8)
    -H|--hostname value - system hostname (default: host)
    -D|--domain value - system domain (default: localdomain)
    --bootstrap-opts "quotes list of values" - bootstrap options used by initial call of pacstrap (default: -K)
    -k|--keymap - system keymap (default: en)
    -K|--kernels - system kernels (default: linux)
    --kernels-headers "quoted list of values" - system kernels' headers, not required
    -b|--boot-mode - boot mode (default: uefi)
    --efi-bootloader-id value - uefi bootloade id (default: arch)
    -B|--boot-devices "quoted list of values" - boot devices used by GRUB, required
    --initrd-modules "quoted list of modules" - initcpio modules
    --initrd-binaries "quoted list of binaries" - inicpio binaries
    --initrd-files "quoted list of files" - initcpio files
    --initrd-hooks "quoted list of hooks" - inicpio hooks
    --initrd-compression value - initcpio compression algorithm
    --initrd-compression-opts "quoted list of opts" - inicpio compression options
    --initrd-decompress value - specifies whether kernel modules should be decompressed during initramfs creation (default: no)
    --initrd-custom-hooks-files "quoted list of files" - initcpio custom hooks files
    --initrd-custom-install-files "quoted list of files" - initcpio custom hook install files
    --grub-boot-dir value - grub boot directory, only needed in uefi installation (default: /boot/efi/arch)
    --grub-efi-dir value - grub efi directory, only needed in uefi installation (default: /boot/efi)
    --zfs-boot-pool|--zfs-bpool - zfs boot pool (default: bpool)
    --zfs-root-pool|--zfs-rpool - zfs root pool (default: rpool)
    --zfs-home-dataset - zfs home dataset (default: rpool/home/${ARCH_USER})
    --zfs-snapshot-name value - specifies initial installation snapshot name (default: install)
    --zfs-module-pkgname value - specifies zfs kernel module package name if dkms is globally disabled, not required
    --swap-cryptdev value - specifies swap crypt device when swap_encrypt profile is enabled (default: none)
    --config-dir - config directory (default: ${CWD}/install_arch.d)
    -c|--config - script config file, required
    --apply - tells script that changes should be applied, use with caution
    -h|--help - displays this help
EOF
    exit 0
}

[ $# -eq 0 ] && usage

# parse script arguments
while [ "$#" -gt 0 ]; do
    case $1 in
        -m|--mnt)
            ARCH_MNT_PATH="$2"
            shift
            shift
            ;;
        -u|--user)
            ARCH_USER="$2"
            shift
            shift
            ;;
        -p|--profiles)
            ARCH_ENABLED_PROFILES="$2"
            shift
            shift
            ;;
        -e|--extra-software)
            ARCH_EXTRA_SOFTWARE="$2"
            shift
            shift
            ;;
        -t|--timezone)
            ARCH_TIMEZONE="$2"
            shift
            shift
            ;;
        --hwclock-timezone)
            ARCH_HWCLOCK_TIMEZONE="$2"
            shift
            shift
            ;;
        -l|--enabled-locales)
            ARCH_ENABLED_LOCALES="$2"
            shift
            shift
            ;;
        -L|--locale)
            ARCH_LOCALE="$2"
            shift
            shift
            ;;
        -H|--hostname)
            ARCH_HOSTNAME="$2"
            shift
            shift
            ;;
        -D|--domain)
            ARCH_DOMAIN="$2"
            shift
            shift
            ;;
        --bootstrap-opts)
            ARCH_BOOTSTRAP_OPTS="$2"
            shift
            shift
            ;;
        -k|--keymap)
            ARCH_KEYMAP="$2"
            shift
            shift
            ;;
        -K|--kernels)
            ARCH_KERNELS="$2"
            shift
            shift
            ;;
        --kernels-headers)
            ARCH_KERNELS_HEADERS="$2"
            shift
            shift
            ;;
        -b|--boot-mode)
            ARCH_BOOT_MODE="$2"
            shift
            shift
            ;;
        --efi-bootloader-id)
            ARCH_EFI_BOOTLOADER_ID="$2"
            shift
            shift
            ;;
        -B|--boot-devices)
            ARCH_BOOT_DEVICES="$2"
            shift
            shift
            ;;
        --initrd-modules)
            ARCH_INITRD_MODULES="$2"
            shift
            shift
            ;;
        --initrd-binaries)
            ARCH_INITRD_BINARIES="$2"
            shift
            shift
            ;;
        --initrd-files)
            ARCH_INITRD_FILES="$2"
            shift
            shift
            ;;
        --initrd-hooks)
            ARCH_INITRD_HOOKS="$2"
            shift
            shift
            ;;
        --initrd-compression)
            ARCH_INITRD_COMPRESSION="$2"
            shift
            shift
            ;;
        --initrd-compression-opts)
            ARCH_INITRD_COMPRESSION_OPTS="$2"
            shift
            shift
            ;;
        --initrd-modules-decompress)
            ARCH_INITRD_MODULES_DECOMPRESS="y"
            shift
            shift
            ;;
        --initrd-custom-hooks-files)
            ARCH_INITRD_CUSTOM_HOOKS_FILES="$2"
            shift
            shift
            ;;
        --initrd-custom-install-files)
            ARCH_INITRD_CUSTOM_INSTALL_FILES="$2"
            shift
            shift
            ;;
        --grub-boot-dir)
            ARCH_GRUB_BOOT_DIR="$2"
            shift
            shift
            ;;
        --grub-efi-dir)
            ARCH_GRUB_EFI_DIR="$2"
            shift
            shift
            ;;
        --zfs-boot-pool|--zfs-bpool)
            ARCH_ZFS_BOOT_POOL="$2"
            shift
            shift
            ;;
        --zfs-root-pool|--zfs-rpool)
            ARCH_ZFS_ROOT_POOL="$2"
            shift
            shift
            ;;
        --zfs-home-dataset)
            ARCH_ZFS_HOME_DATASET="$2"
            shift
            shift
            ;;
        --zfs-snapshot-name)
            ARCH_ZFS_SNAPSHOT_NAME="$2"
            shift
            shift
            ;;
        --zfs-module-pkgname)
            ARCH_ZFS_MODULE_PKGNAME="$2"
            shift
            shift
            ;;
        --swap-cryptdev)
            ARCH_SWAP_CRYPTDEV="$2"
            shift
            shift
            ;;
        --config-dir)
            CONFIG_DIR="$2"
            shift
            shift
            ;;
        -c|--config)
            CONFIG_FILE="$2"
            shift
            shift
            ;;
        --apply)
            APPLY="y"
            shift
            ;;
        -h|--help)
            usage
            ;;
        *)
            echo "Error: command '$1' not recognized."
            usage
            ;;
    esac
done

# shellcheck source=/dev/null
if [[ -f "${CONFIG_FILE}" ]]; then
    source "${CONFIG_FILE}"
else
    die "config file does not exist"
fi

# assign default values to variables
ARCH_MNT_PATH=${ARCH_MNT_PATH:-}
ARCH_USER=${ARCH_USER:-"user"}
ARCH_EXTRA_SOFTWARE=${ARCH_EXTRA_SOFTWARE:-}
ARCH_TIMEZONE=${ARCH_TIMEZONE:-"UTC"}
ARCH_HWCLOCK_TIMEZONE=${ARCH_HWCLOCK_TIMEZONE:-"UTC"}
ARCH_ENABLED_LOCALES=${ARCH_ENABLED_LOCALES:-"en_US.UTF-8 UTF-8"}
ARCH_LOCALE=${ARCH_LOCALE:-"en_US.UTF-8"}
ARCH_HOSTNAME=${ARCH_HOSTNAME:-"host"}
ARCH_DOMAIN=${ARCH_DOMAIN:-"localdomain"}
ARCH_BOOTSTRAP_OPTS=${ARCH_BOOTSTRAP_OPTS:-"-K"}
ARCH_KEYMAP=${ARCH_KEYMAP:-"en"}
ARCH_KERNELS=${ARCH_KERNELS:-"linux"}
ARCH_KERNELS_HEADERS=${ARCH_KERNELS_HEADERS:-}
ARCH_BOOT_MODE=${ARCH_BOOT_MODE:-"uefi"}
ARCH_EFI_BOOTLOADER_ID=${ARCH_EFI_BOOTLOADER_ID:-"arch"}
ARCH_BOOT_DEVICES=${ARCH_BOOT_DEVICES:-}
ARCH_INITRD_MODULES=${ARCH_INITRD_MODULES:-"ext4"}
ARCH_INITRD_BINARIES=${ARCH_INITRD_BINARIES:-}
ARCH_INITRD_FILES=${ARCH_INITRD_FILES:-}
ARCH_INITRD_HOOKS=${ARCH_INITRD_HOOKS:-"base udev autodetect modconf kms keyboard keymap consolefont block filesystems fsck"}
ARCH_INITRD_COMPRESSION=${ARCH_INITRD_COMPRESSION:-}
ARCH_INITRD_COMPRESSION_OPTS=${ARCH_INITRD_COMPRESSION_OPTS:-}
ARCH_INITRD_MODULES_DECOMPRESS=${ARCH_INITRD_MODULES_DECOMPRESS:-}
ARCH_INITRD_CUSTOM_HOOKS_FILES=${ARCH_INITRD_CUSTOM_HOOKS_FILES:-}
ARCH_INITRD_CUSTOM_INSTALL_FILES=${ARCH_INITRD_CUSTOM_INSTALL_FILES:-}
ARCH_GRUB_BOOT_DIR=${ARCH_GRUB_BOOT_DIR:-"/boot"}
ARCH_GRUB_EFI_DIR=${ARCH_GRUB_EFI_DIR:-"/boot/efi"}
ARCH_ZFS_BOOT_POOL=${ARCH_ZFS_BOOT_POOL-"bpool"}
ARCH_ZFS_ROOT_POOL=${ARCH_ZFS_ROOT_POOL:-"rpool"}
ARCH_ZFS_HOME_DATASET=${ARCH_ZFS_HOME_DATASET:-"${ARCH_ZFS_ROOT_POOL}/home/${ARCH_USER}"}
ARCH_ZFS_SNAPSHOT_NAME=${ARCH_ZFS_SNAPSHOT_NAME:-"install"}
ARCH_ZFS_MODULE_PKGNAME=${ARCH_ZFS_MODULE_PKGNAME:-}
ARCH_SWAP_CRYPTDEV=${ARCH_SWAP_CRYPTDEV:-}
ARCH_POSTINST_SERVICES=${ARCH_POSTINST_SERVICES:-}
ARCH_POSTINST_CMDS=${ARCH_POSTINST_CMDS:-}
ARCH_ROOT_PASSWD=${ARCH_ROOT_PASSWD:-"\$1\$HNeYMtER\$0VoteufFxJBfPM1YkkNpk."} # default password: changeme
ARCH_USER_PASSWD=${ARCH_USER_PASSWD:-"\$1\$1ajoaeQC\$lubTSvAeoWn8kNvXIPoKi/"} # default password: changeme
CONFIG_DIR=${CONFIG_DIR:-"${CWD}/install_arch.d"}
APPLY=${APPLY:-}

validate_args() {
    if [[ -n "${ARCH_MNT_PATH}" ]]; then
        if [[ ! -d "${ARCH_MNT_PATH}" ]]; then
            die "directory '${ARCH_MNT_PATH}' does not exist"
        else
            if ! is_mounted "${ARCH_MNT_PATH}"; then
                die "directory '${ARCH_MNT_PATH}' is not mounted"
            fi
        fi
    else
        die "mount path not specified"
    fi

    [[ -z "${ARCH_USER}" ]] && die "target user name is not specified"

    if [[ -n "${ARCH_ENABLED_PROFILES[*]}" ]]; then
        for profile in "${ARCH_ENABLED_PROFILES[@]}"; do
            if [[ ! ${ARCH_AVAILABLE_PROFILES[*]} =~ ${profile} ]]; then
                die "unknown profile '${profile}'"
            fi
        done
    else
        die "profile list is empty"
    fi

    [[ -z "${ARCH_TIMEZONE}" ]] && die "timezone value is empty"

    if [[ "${ARCH_HWCLOCK_TIMEZONE}" != "UTC" && "${ARCH_HWCLOCK_TIMEZONE}" != "localtime" ]]; then
        die "valid options for --hwclock-timezone are 'UTC' and 'localtime'"
    fi

    if [[ -z "${ARCH_LOCALE[*]}" ]] || [[ -z "${ARCH_ENABLED_LOCALES[*]}" ]]; then
        die "locale not specified"
    fi

    [[ -z "${ARCH_HOSTNAME}" ]] && die "hostname is empty"
    [[ -z "${ARCH_DOMAIN}" ]] && die "domain name is empty"

    [[ -z "${ARCH_KEYMAP}" ]] && die "keymap info isn't specified"

    [[ -z "${ARCH_KERNELS[*]}" ]] && die "kernel list is empty"

    if [[ -n "${ARCH_BOOT_MODE}" ]]; then
        case "${ARCH_BOOT_MODE}" in
            "uefi") ;;
            "legacy_bios") ;;
            *) die "boot mode must be 'uefi' or 'legacy_bios'" ;;
        esac
    else
        die "boot mode value is empty"
    fi

    for device in "${ARCH_BOOT_DEVICES[@]}"; do
        if [[ ! -b "${device}" ]]; then
            die "${device}: not a block device"
        fi
    done
}

is_profile_enabled() {
    for profile in "${ARCH_ENABLED_PROFILES[@]}"; do
        if [[ "${profile}" == "$1" ]]; then
            return 0
        fi
    done
    return 1
}

check_profiles_deps() {
    if ! is_profile_enabled "zfs"; then
        is_profile_enabled "zfs_force_dkms" && die "profile 'zfs_force_dkms' depends on the profile 'zfs'"
        is_profile_enabled "zfs_snapshot_install" && die "profile 'zfs_snapshot_install' depends on the profile 'zfs'"
    fi
    if ! is_profile_enabled "genfstab"; then
        is_profile_enabled "genfstab_uuid" && die "profile 'genfstab_uuid' depends on the profile 'genfstab'"
        is_profile_enabled "genfstab_label" && die "profile 'genfstab_label' depends on the profile 'genfstab'"
    fi
    if is_profile_enabled "zfs_force_dkms" && is_profile_enabled "!dkms"; then
        die "mutually exclusive profiles: 'zfs_force_dkms' and '!dkms'"
    fi
    if is_profile_enabled "kernel_headers" && ! is_profile_enabled "devel"; then
        die "profile 'kernel_headers' depends on the profile 'devel'"
    fi
    if is_profile_enabled "python" && ! is_profile_enabled "devel"; then
        die "profile 'python' depends on the profile 'devel'"
    fi
    if is_profile_enabled "genfstab_uuid" && ! is_profile_enabled "genfstab"; then
        die "profile 'genfstab_uuid' depends on the profile 'genfstab'"
    fi
    if is_profile_enabled "genfstab_label" && ! is_profile_enabled "genfstab"; then
        die "profile 'genfstab_label' depends on the profile 'genfstab'"
    fi
}

install_base_system() {
    echo "Installing base system"
    pacstrap "${ARCH_BOOTSTRAP_OPTS[*]}" "${ARCH_MNT_PATH}" base
}

generate_fstab() {
    local genfstab_args
    genfstab_args=""
    echo "Generating /etc/fstab"
    is_profile_enabled "uuid" && genfstab_args="-U"
    is_profile_enabled "label" && genfstab_args="-L"
    genfstab "${genfstab_args}" "${ARCH_MNT_PATH}" >> "${ARCH_MNT_PATH}"/etc/fstab
}

set_timezone() {
    echo "Setting timezone"
    arch-chroot "${ARCH_MNT_PATH}" ln -sf /usr/share/zoneinfo/"${ARCH_TIMEZONE}" /etc/localtime
    case "${ARCH_HWCLOCK_TIMEZONE}" in
        "UTC")
            arch-chroot "${ARCH_MNT_PATH}" hwclock --systohc --utc
            ;;
        "localtime")
            arch-chroot "${ARCH_MNT_PATH}" hwclock --systohc --localtime
            ;;
    esac
}

set_locale() {
    local IFS
    IFS=":"
    echo "Setting locale"
    for locale in "${ARCH_ENABLED_LOCALES[@]}"; do
        sed -i "s/#${locale}/${locale}/g" "${ARCH_MNT_PATH}"/etc/locale.gen
    done
    echo "LANG=${ARCH_LOCALE}" > "${ARCH_MNT_PATH}"/etc/locale.conf
    arch-chroot "${ARCH_MNT_PATH}" locale-gen
}

set_keymap() {
    echo "Setting keymap"
    echo "KEYMAP=${ARCH_KEYMAP}" > "${ARCH_MNT_PATH}"/etc/vconsole.conf
}

set_hostname() {
    echo "Setting hostname"
    echo "${ARCH_HOSTNAME}" > "${ARCH_MNT_PATH}"/etc/hostname
    cat <<EOF > "${ARCH_MNT_PATH}"/etc/hosts
# Static table lookup for hostnames.
# See hosts(5) for details.

# The following lines are desirable for IPv4 capable hosts
127.0.0.1               localhost

# 127.0.1.1 is often used for the FQDN of the machine
127.0.1.1               ${ARCH_HOSTNAME}.${ARCH_DOMAIN} ${ARCH_HOSTNAME}

# The following lines are desirable for IPv6 capable hosts
::1                     localhost ip6-localhost ip6-loopback
ff02::1                 ip6-allnodes
ff02::2                 ip6-allrouters
EOF
}

enable_dhcpcd() {
    echo "Installing dhcpcd"
    pacstrap "${ARCH_MNT_PATH}" dhcpcd
    echo "Enabling dhcpcd"
    arch-chroot "${ARCH_MNT_PATH}" systemctl enable dhcpcd
}

install_devel_pkgs() {
    echo "Installing base development packages"
    pacstrap "${ARCH_MNT_PATH}" base-devel
    if is_profile_enabled "python"; then
        echo "Installing python development packages"
        pacstrap "${ARCH_MNT_PATH}" python python-pip python-setuptools
    fi
}

# shellcheck disable=SC2048
# shellcheck disable=SC2086
install_kernel() {
    echo "Installing kernel"
    pacstrap "${ARCH_MNT_PATH}" ${ARCH_KERNELS[*]}
    is_profile_enabled "firmware" && pacstrap "${ARCH_MNT_PATH}" linux-firmware
    is_profile_enabled "parabola_firmware" && pacstrap "${ARCH_MNT_PATH}" linux-libre-firmware
    if is_profile_enabled "kernel_headers"; then
        for kernel in "${ARCH_KERNELS[@]}"; do
            case "${kernel}" in
                "linux") pacstrap "${ARCH_MNT_PATH}" linux-headers ;;
                "linux-lts") pacstrap "${ARCH_MNT_PATH}" linux-lts-headers ;;
                "linux-hardened") pacstrap "${ARCH_MNT_PATH}" linux-hardened-headers ;;
                "linux-zen") pacstrap "${ARCH_MNT_PATH}" linux-zen-headers ;;
                "linux-libre") pacstrap "${ARCH_MNT_PATH}" linux-libre-headers ;;
                "linux-libre-hardened") pacstrap "${ARCH_MNT_PATH}" linux-libre-hardened-headers ;;
                *)
                    if [[ -z "${ARCH_KERNELS_HEADERS[*]}" ]]; then
                        pacstrap "${ARCH_MNT_PATH}" "${kernel}-headers"
                    else
                        for kernel_headers in "${ARCH_KERNELS_HEADERS[@]}"; do
                            pacstrap "${ARCH_MNT_PATH}" "${kernel_headers}"
                        done
                    fi
                    ;;
            esac
        done
    fi
}

install_zfs() {
    cat <<EOF >> "${ARCH_MNT_PATH}"/etc/pacman.conf
[archzfs]
# Origin Server - France
Server = http://archzfs.com/\$repo/x86_64
# Mirror - Germany
Server = http://mirror.sum7.eu/archlinux/archzfs/\$repo/x86_64
# Mirror - Germany
Server = https://mirror.biocrafting.net/archlinux/archzfs/\$repo/x86_64
# Mirror - India
Server = https://mirror.in.themindsmaze.com/archzfs/\$repo/x86_64
# Mirror - US
Server = https://zxcvfdsa.com/archzfs/\$repo/\$arch
EOF
    arch-chroot "${ARCH_MNT_PATH}" <<EOF
pacman-key --recv-keys ${ARCHZFS_REPO_KEY}
pacman-key --lsign-key ${ARCHZFS_REPO_KEY}
pacman -Syu --noconfirm
EOF
    if is_profile_enabled "zfs_force_dkms"; then
        arch-chroot "${ARCH_MNT_PATH}" <<EOF
pacman -S --noconfirm zfs-dkms
depmod
modprobe zfs
EOF
    else
        for kernel in "${ARCH_KERNELS[@]}"; do
            case "${kernel}" in
                "linux") pacstrap "${ARCH_MNT_PATH}" zfs-linux ;;
                "linux-lts") pacstrap "${ARCH_MNT_PATH}" zfs-linux-lts ;;
                "linux-hardened") pacstrap "${ARCH_MNT_PATH}" zfs-linux-hardened ;;
                "linux-zen") pacstrap "${ARCH_MNT_PATH}" zfs-linux-zen ;;
                *)
                    if is_profile_enabled "!dkms"; then
                        pacstrap "${ARCH_MNT_PATH}" "${ARCH_ZFS_MODULE_PKGNAME}"
                    else
                        arch-chroot "${ARCH_MNT_PATH}" <<EOF
pacman -S --noconfirm zfs-dkms
depmod
modprobe zfs
EOF
                    fi
                    ;;
            esac
        done
    fi
}

setup_mdadm() {
    mdadm --detail --scan >> "${ARCH_MNT_PATH}"/etc/mdadm.conf
    pacstrap "${ARCH_MNT_PATH}" mdadm
}

# shellcheck disable=SC2048
# shellcheck disable=SC2086
install_extra_software() {
    if [[ -n "${ARCH_EXTRA_SOFTWARE[*]}" ]]; then
        echo "Installing extra software"
        pacstrap "${ARCH_MNT_PATH}" ${ARCH_EXTRA_SOFTWARE[*]}
    fi
}

change_root_password() {
    echo "Setting root password"
    echo "root:${ARCH_ROOT_PASSWD}" | arch-chroot "${ARCH_MNT_PATH}" chpasswd --encrypted
}

create_regular_user() {
    arch-chroot "${ARCH_MNT_PATH}" useradd -m -g users -G wheel,games,power,optical,storage,scanner,lp,audio,video -s /bin/bash "${ARCH_USER}"
    echo "Setting ${ARCH_USER} password"
    echo "${ARCH_USER}:${ARCH_USER_PASSWD}" | arch-chroot "${ARCH_MNT_PATH}" chpasswd --encrypted
    sed -i "s/#\s*%wheel ALL=\(ALL:ALL\) ALL/%wheel ALL=\(ALL:ALL\) ALL/g" "${ARCH_MNT_PATH}"/etc/sudoers
    if is_profile_enabled "zfs"; then
        arch-chroot "${ARCH_MNT_PATH}" <<EOF
zfs allow ${ARCH_USER} create,mount,mountpoint,snapshot ${ARCH_ZFS_HOME_DATASET}
chown -R ${ARCH_USER} /home/${ARCH_USER}
EOF
    fi
}

setup_crypttab() {
    if is_profile_enabled "swap_encrypt"; then
        cat <<EOF >> /etc/crypttab
swap           ${ARCH_SWAP_CRYPTDEV}                                 /dev/urandom            swap,cipher=aes-xts-plain64:sha256,size=512
EOF
        cat <<EOF >> /etc/fstab
/dev/mapper/swap        none            swap            defaults        0 0
EOF
    fi
}

copy_custom_files() {
    echo "Copying custom files to the target system"
    cp -r "${CONFIG_DIR}"/custom_files/* "${ARCH_MNT_PATH}"
}

generate_initramfs() {
    local initcpio_conf
    initcpio_conf="${ARCH_MNT_PATH}"/etc/mkinitcpio.conf
    echo "Setting up mkinitcpio options"
    if [[ -n "${ARCH_INITRD_MODULES[*]}" ]]; then
        sed -ri "s/#?MODULES=\(.*\)/MODULES=\(${ARCH_INITRD_MODULES[*]}\)/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_BINARIES[*]}" ]]; then
        sed -ri "s/#?BINARIES=\(.*\)/BINARIES=\(${ARCH_INITRD_BINARIES[*]}\)/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_FILES[*]}" ]]; then
        sed -ri "s/#?FILES=\(.*\)/FILES=\(${ARCH_INITRD_FILES[*]}\)/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_HOOKS[*]}" ]]; then
        if [[ -d "${CONFIG_DIR}/initcpio_hooks" ]] && [[ -d "${CONFIG_DIR}/initcpio_install" ]]; then
            echo "Copying mkinitcpio custom hooks files"
            cp "${CONFIG_DIR}"/initcpio_hooks/* "${ARCH_MNT_PATH}"/etc/initcpio/hooks
            cp "${CONFIG_DIR}"/initcpio_install/* "${ARCH_MNT_PATH}"/etc/initcpio/install
        fi
        sed -ri "s/#?HOOKS=\(.*\)/HOOKS=\(${ARCH_INITRD_HOOKS[*]}\)/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_COMPRESSION[*]}" ]]; then
        sed -ri "s/#?COMPRESSION=\"${ARCH_INITRD_COMPRESSION}\"/COMPRESSION=\"${ARCH_INITRD_COMPRESSION}\"/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_COMPRESSION_OPTS[*]}" ]]; then
        sed -ri "s/#?COMPRESSION_OPTIONS=\(.*\)/COMPRESSION_OPTIONS=\(${ARCH_INITRD_COMPRESSION_OPTS[*]}\)/g" "${initcpio_conf}"
    fi
    if [[ -n "${ARCH_INITRD_MODULES_DECOMPRESS}" ]]; then
        sed -ri "s/#?MODULES_DECOMPRESS=\".*\"/MODULES_DECOMPRESS=\"yes\"/g" "${initcpio_conf}"
    fi
    is_profile_enabled "zfs" && arch-chroot "${ARCH_MNT_PATH}" zgenhostid
    echo "Generating initramfs"
    arch-chroot "${ARCH_MNT_PATH}" mkinitcpio -P
}

install_grub() {
    if is_profile_enabled "zfs"; then
        echo "ZPOOL_VDEV_NAME_PATH=YES" >> "${ARCH_MNT_PATH}"/etc/environment
    fi
    pacstrap "${ARCH_MNT_PATH}" grub
    case "${ARCH_BOOT_MODE}" in
        "uefi")
            pacstrap "${ARCH_MNT_PATH}" dosfstools efibootmgr mtools
            mkdir -p "${ARCH_MNT_PATH%%/}${ARCH_GRUB_BOOT_DIR}"
            mkdir -p "${ARCH_MNT_PATH%%/}${ARCH_GRUB_EFI_DIR}"
            arch-chroot "${ARCH_MNT_PATH}" bash -c "ZPOOL_VDEV_NAME_PATH=YES grub-install --target x86_64-efi \
                --boot-directory ${ARCH_GRUB_BOOT_DIR} --efi-directory ${ARCH_GRUB_EFI_DIR} \
                --bootloader-id ${ARCH_EFI_BOOTLOADER_ID} --removable"
            ;;
        "legacy_bios")
            for boot_dev in "${ARCH_BOOT_DEVICES[@]}"; do
                arch-chroot "${ARCH_MNT_PATH}" bash -c "ZPOOL_VDEV_NAME_PATH=YES grub-install ${boot_dev}"
            done
            ;;
        *)
            die "install_grub: unknown boot mode '${ARCH_BOOT_MODE}'"
            ;;
    esac
    [[ -f "${CONFIG_DIR}/grub_default.conf" ]] && cp "${CONFIG_DIR}"/grub_default.conf "${ARCH_MNT_PATH}"/etc/default/grub
    sleep 1
    arch-chroot "${ARCH_MNT_PATH}" bash -c "ZPOOL_VDEV_NAME_PATH=YES grub-mkconfig -o /boot/grub/grub.cfg"
}

enable_extra_services() {
    echo "Enabling extra services"
    for systemd_service in "${ARCH_POSTINST_SERVICES[@]}"; do
        arch-chroot "${ARCH_MNT_PATH}" systemctl enable "${systemd_service}"
    done
}

run_extra_commands() {
    echo "Running post-install extra commands"
    arch-chroot "${ARCH_MNT_PATH}" "${ARCH_POSTINST_CMDS}"
}

snapshot_installation() {
    echo "Taking snapshot of the ZFS datasets"
    zfs snapshot -r "${ARCH_ZFS_BOOT_POOL}@${ARCH_ZFS_SNAPSHOT_NAME}"
    zfs snapshot -r "${ARCH_ZFS_ROOT_POOL}@${ARCH_ZFS_SNAPSHOT_NAME}"
}

validate_args
check_profiles_deps
if [[ -n "${APPLY}" ]]; then
    install_base_system
    sleep 1; is_profile_enabled "genfstab" && generate_fstab
    set_timezone
    set_locale
    set_keymap
    set_hostname
    sleep 2; enable_dhcpcd
    sleep 2; is_profile_enabled "devel" && install_devel_pkgs
    sleep 2; install_kernel
    sleep 2; is_profile_enabled "zfs" && install_zfs
    sleep 2; is_profile_enabled "raid" && setup_mdadm
    [[ -n "${ARCH_EXTRA_SOFTWARE[*]}" ]] && install_extra_software
    sleep 2; change_root_password
    sleep 2; create_regular_user
    sleep 2; setup_crypttab
    [[ -d "${CONFIG_DIR}/custom_files" ]] && copy_custom_files
    sleep 2; generate_initramfs
    sleep 2; install_grub
    sleep 2; [[ -n "${ARCH_POSTINST_SERVICES[*]}" ]] && enable_extra_services
    sleep 2; [[ -n "${ARCH_POSTINST_CMDS}" ]] && run_extra_commands
    if is_profile_enabled "zfs" && is_profile_enabled "zfs_snapshot_install"; then
        sleep 2; snapshot_installation
    fi
    echo "Done!"
else
    echo "Execute script with --apply option to confirm destructive action"
fi
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I think this message should go to the error stream (>&2):

if [ "$EUID" -ne 0 ]; then
    echo "Please run as root"
    exit 1
fi

Bash has read -s for reading passwords without echo. So we can replace

    stty -echo
    read -r -p "Password: " password
    echo
    read -r -p "Confirm password: " confirm_password
    echo
    stty echo

with

    read -srp "Password: " password
    echo
    read -srp "Confirm password: " confirm_password
    echo

case $1 in

Missing quotes: "$1"


shift; shift can be replaced with shift 2.


        echo "Error: command '$1' not recognized."
        usage

This error message and the usage should go to standard error. And we should exit with a failure status here (usage currently contains exit 0 which is harmful to this case).


if [[ -n "${ARCH_EXTRA_SOFTWARE[*]}" ]]; then
    echo "Installing extra software"
    pacstrap "${ARCH_MNT_PATH}" ${ARCH_EXTRA_SOFTWARE[*]}
fi

For testing an array is non-empty, consider [ ${#ARCH_EXTRA_SOFTWARE[*]} -ne 0 ]. And use $@ expansion ("${ARCH_EXTRA_SOFTWARE[@]}"). That should avoid the need to suppress shellcheck messages.


generate_initramfs repeatedly edits "$initcpio_conf" using sed -i. It's probably better to build a single sed program and run it just once. E.g.

    local transform=()
    if [ -n "${ARCH_INITRD_MODULES[*]}" ]
    then transform+=(-e "s/^#?(MODULES=)\(.*\)/\1\(${ARCH_INITRD_MODULES[*]}\)/")
    fi
    if [ -n "${ARCH_INITRD_BINARIES[*]}" ]
    then transform+=(-e "s/^#?(BINARIES=)\(.*\)/\1\(${ARCH_INITRD_BINARIES[*]}\)/")
    fi
    ⋮
    sed -ri "$(transform[@])" "$initcpio_conf"

What are all these sleeps for?

    sleep 2; enable_dhcpcd
    sleep 2; is_profile_enabled "devel" && install_devel_pkgs
    sleep 2; install_kernel
    sleep 2; is_profile_enabled "zfs" && install_zfs
    sleep 2; is_profile_enabled "raid" && setup_mdadm

If there is some race to be avoided, it's probably worth a comment to explain where the value of 2 seconds came from.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.