#!/usr/bin/env bash # Exit on error. set -e # Exit on undefined variable reference. set -u script_dir="$(dirname "${BASH_SOURCE[0]}")" function script_is_installed(){ # Check if this script is run installed or locally (development setup). [ "${script_dir}" = '/usr/bin' ] } if script_is_installed; then lib_dir='/usr/lib/de-p1st-installer' else lib_dir='./lib' # Print commands before they get executed. set -v fi # Source library files. # shellcheck source=lib/validate-args.sh source "${lib_dir}"/validate-args.sh # shellcheck source=lib/util.sh source "${lib_dir}"/util.sh # shellcheck source=lib/user-input.sh source "${lib_dir}"/user-input.sh # shellcheck source=lib/block-device.sh source "${lib_dir}"/block-device.sh # Get path of configuration file. if [ $# -eq 0 ]; then # No arguments given. if script_is_installed; then cfg_file=/etc/de-p1st-installer/installer.cfg else cfg_file="${script_dir}"/installer.cfg fi else # >= 1 arguments given. # First argument is the configuration file! cfg_file="${1}" fi # Source the configuration file. # shellcheck source=installer.cfg source "${cfg_file}" function main() { # @pre # bash libraries imported # @post # installation finished scrollback check_network system_time repository # in: BOOT_PART_SIZE, BOOT_FIRMWARE, INITRAMFS, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD; (all variables are optional) # out: BOOT_PART_SIZE, BOOT_FIRMWARE, INITRAMFS, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD (if FDE='true') get_user_input # in: CPU_VENDOR (optional) # out: CPU_VENDOR get_cpu_vendor CPU_VENDOR # in: FS # out: FS_DEFAULT_MOUNT_OPTIONS get_default_mount_options # in: FS # out: FS_ADDITIONAL_MOUNT_OPTIONS get_additional_mount_options # out: BOOT_PART, LUKS_PART partition \ "${TARGET_BLOCK_DEVICE}" "${BOOT_FIRMWARE}" "${BOOT_PART_SIZE}" \ BOOT_PART LUKS_PART # out: LUKS_PART_UUID (if FDE='true'), DATA_PART format \ "${BOOT_PART}" "${LUKS_PART}" "${LUKS_PWD}" "${FDE}" "${FS}" \ LUKS_PART_UUID DATA_PART # Join default and additional mount options by ',' # shellcheck disable=SC2034 FS_MOUNT_OPTIONS_ARRAY=("${FS_DEFAULT_MOUNT_OPTIONS[@]}" "${FS_ADDITIONAL_MOUNT_OPTIONS[@]}") join_by ',' FS_MOUNT_OPTIONS_ARRAY FS_MOUNT_OPTIONS mount_partitions # in: BOOT_FIRMWARE, INITRAMFS, PACSTRAP_INTERACTIVE (optional) run_pacstrap # in: FS run_genfstab # in: HOSTNAME, FQDN (optional), STATIC_IP (optional), IPV6_CAPABLE (optional) config_hostname_and_hosts # in: USERNAME, USER_PWD, ROOT_PWD (optional) user_and_pwd # in: INITRAMFS initramfs # in: TARGET_BLOCK_DEVICE, FDE, LUKS_PART_UUID bootloader unmount_partitions echo 'Finished installation without errors!' } function scrollback(){ if ! grep --quiet '^defscrollback' /etc/screenrc; then printf '\n%s\n' "defscrollback 100000" | sudo tee -a /etc/screenrc printf '%s\n' 'Extended screen scrollback history. Pleas create a new screen session and run the installer again.' return 1 fi if [ -z "${STY:-}" ]; then printf '%s\n' 'Pleas run the installer from a screen session.' return 1 fi } function check_network() { echo 'Sending ping to wikipedia.de ...' ping -c 1 wikipedia.de >/dev/null || { echo 'Pleas set up network access and then run the installer again.' return 1 } } function system_time() { # Use timedatectl(1) to ensure the system clock is accurate timedatectl set-ntp true } function repository(){ # Check if the [de-p1st] repository is available. # If not, add it to pacman.conf if ! grep --quiet 'de-p1st' /etc/pacman.conf; then printf '%s\n' 'Enabling [de-p1st] package repository:' printf '\n%s\n' "[de-p1st] SigLevel = Optional TrustAll Server = https://arch.p1st.de" | sudo tee -a /etc/pacman.conf fi } function increase_cow_space() { # May be useful when running 'pacman -Syu' on the live medium. # Usually not necessary! # Make sure that we are on a live medium: findmnt /run/archiso/cowspace >/dev/null || { echo 'Not on live medium, did not increase cowspace!' return 1 } echo 'Increasing cowspace partition of live medium ...' sudo mount -o remount,size=2G /run/archiso/cowspace } function get_user_input() { # @post # BOOT_PART_SIZE # BOOT_FIRMWARE: 'uefi' | 'bios' # INITRAMFS: 'mkinitcpio' | 'dracut' # FS: 'BTRFS' | 'EXT4' | 'F2FS' # FS_BTRFS_SUBVOL_LAYOUT: 'root_only' | '@root@home' # HOSTNAME # USERNAME, USER_PWD # FDE: 'true' | 'false' # LUKS_PWD: only set if FDE='true' # out: BLOCK_DEVICE_SIZES get_block_devices_with_size BLOCK_DEVICE_SIZES single_choice_if_empty TARGET_BLOCK_DEVICE 'Select target device for installation' BLOCK_DEVICE_SIZES ask_user_if_empty BOOT_PART_SIZE 'Enter boot partition size in MiB, at least 260:' if [ "${BOOT_FIRMWARE}" = 'autodetect' ]; then # Detect boot firmware type: https://askubuntu.com/a/162573 # Check exit code; if 0 EFI, else BIOS. if dmesg | grep --quiet --fixed-strings 'EFI v'; then echo 'Detected EFI boot.' BOOT_FIRMWARE='uefi' else echo 'Detected BIOS boot' BOOT_FIRMWARE='bios' fi else # If $BOOT_FIRMWARE is empty: Let user select BIOS type # shellcheck disable=SC2034 BOOT_FIRMWARE_QUESTION=('uefi' 'Newer mainboards' \ 'bios' 'Legacy BIOS on older mainboards') single_choice_if_empty BOOT_FIRMWARE 'Select your bios type' BOOT_FIRMWARE_QUESTION fi # shellcheck disable=SC2034 INITRAMFS_QUESTION=('mkinitcpio' 'Default for Arch Linux' \ 'dracut' 'Testing') single_choice_if_empty INITRAMFS 'Select filesystem to use' INITRAMFS_QUESTION # shellcheck disable=SC2034 FS_QUESTION=('BTRFS' 'Allows snapshots and dynamic extension of the FS' \ 'EXT4' 'Default FS of many distributions' \ 'F2FS' 'Flash-Friendly-FS for SSD or NVMe') single_choice_if_empty FS 'Select filesystem to use' FS_QUESTION if [ "${FS}" = 'BTRFS' ]; then # shellcheck disable=SC2034 FS_QUESTION2=('root_only' 'Just one subvolume for "/".' \ '@root@home' 'Two subvolumes @ and @home. This configuration allows usage of "Timeshift".') single_choice_if_empty FS_BTRFS_SUBVOL_LAYOUT 'Select your preferred subvolume layout' FS_QUESTION2 fi ask_user_if_empty HOSTNAME 'Enter hostname:' ask_user_if_empty USERNAME 'Enter username:' # If USER_PWD is empty if [ -z "${USER_PWD}" ]; then ask_user_if_empty USER_PWD 'Enter a user password:' ask_user_if_empty USER_PWD2 'Please enter the password again:' # shellcheck disable=SC2153 if [ "${USER_PWD}" != "${USER_PWD2}" ]; then echo 'Passwords did not match' exit 1 fi fi # shellcheck disable=SC2034 FDE_QUESTION=('true' 'Yes' 'false' 'No') single_choice_if_empty FDE 'Shall Full-Disk-Encryption be enabled?' FDE_QUESTION # If FDE enabled but LUKS_PWD is empty if [ "${FDE}" = 'true' ] && [ -z "${LUKS_PWD}" ]; then ask_user_if_empty LUKS_PWD 'Enter a disk encryption password:' ask_user_if_empty LUKS_PWD2 'Please enter the password again:' # shellcheck disable=SC2153 if [ "${LUKS_PWD}" != "${LUKS_PWD2}" ]; then echo 'Passwords did not match' exit 1 fi fi # ADDITIONAL_PKGS is an array, but all elements should be non-empty. # Thus, it is ok if we do only check the first element. if [ -z "${ADDITIONAL_PKGS:-}" ]; then ask_user_if_empty ADDITIONAL_PKGS 'Additional packages to install (separated by space):' space_separated_to_array ADDITIONAL_PKGS ADDITIONAL_PKGS || return $? fi } function get_default_mount_options() { # @pre # FS # @post # FS_DEFAULT_MOUNT_OPTIONS (array) FS_DEFAULT_MOUNT_OPTIONS=() case "${FS}" in BTRFS) # "compress=lzo": archwiki -> Btrfs#Compression # "compress=zstd:1": # -> https://btrfs.wiki.kernel.org/index.php/Compression#What_are_the_differences_between_compression_methods.3F # -> https://fedoraproject.org/wiki/Changes/BtrfsTransparentCompression#Q:_Why_use_zstd:1_specifically.3F # # "Enable compression (better performance, longer flash lifespan)" FS_DEFAULT_MOUNT_OPTIONS+=('compress=zstd:1') ;; EXT4) FS_DEFAULT_MOUNT_OPTIONS+=() ;; F2FS) # When mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle). # Using compress_extension=txt will cause all txt files to be compressed by default. FS_DEFAULT_MOUNT_OPTIONS+=('compress_algorithm=lz4') ;; *) echo 'Filesystem '"${FS}"' not yet supported!' return 1 ;; esac } function get_additional_mount_options() { # @pre # FS # @post # FS_ADDITIONAL_MOUNT_OPTIONS (array) case "${FS}" in BTRFS) # noatime, nodiratime: # - `atime` impacts drive performance # - `noatime` implies `nodiratime`, one does not need to specify both # - `noatime` fully disables writing file access times to the drive every time you read a file. # This works well for almost all applications, except for those that need to know if a file has been # read since the last time it was modified. FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on' 'ssd' 'Enable if using SSD/NVMe' 'off') ;; EXT4) FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on') ;; F2FS) # shellcheck disable=SC2034 FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on') ;; *) echo 'Filesystem '"${FS}"' not yet supported!' return 1 ;; esac multi_choice_if_empty FS_ADDITIONAL_MOUNT_OPTIONS 'Select mount options' FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION } function mount_partitions() { # @pre # FS, FS_BTRFS_SUBVOL_LAYOUT, FS_MOUNT_OPTIONS, DATA_PART, BOOT_PART if [ "${FS}" = 'BTRFS' ]; then case "${FS_BTRFS_SUBVOL_LAYOUT}" in 'root_only') # Nothing special; same steps as for a regular FS echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}" sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt ;; '@root@home') # Timeshift BTRFS subvol layout: # https://github.com/teejee2008/timeshift#supported-system-configurations # Mount top level subvolume sudo mount -o subvolid=5 "${DATA_PART}" /mnt # Create subvolumes @ and @home sudo btrfs subvolume create /mnt/@ sudo btrfs subvolume create /mnt/@home # List the created subvolumes sudo btrfs subvolume list /mnt # Umount the top level subvolume sudo umount -R /mnt echo 'Mounting @ and @home subvolumes with options: '"${FS_MOUNT_OPTIONS}" sudo mount -o 'subvol=@,'"${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt sudo mkdir /mnt/home sudo mount -o 'subvol=@home,'"${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt/home ;; *) echo 'BTRFS subvolume layout '"${FS_BTRFS_SUBVOL_LAYOUT}"' not supported!' return 1 ;; esac else echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}" sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt fi echo 'Mounting boot partition ...' sudo mkdir /mnt/boot sudo mount "${BOOT_PART}" /mnt/boot # Print information about partitions. lsblk --tree=PATH -o PATH,TYPE,UUID } function run_pacstrap() { # @pre # BOOT_FIRMWARE # PACSTRAP_INTERACTIVE: optional, 'true' echo 'Running pacstrap ...' PKGS=("${ADDITIONAL_PKGS[@]}") case "${BOOT_FIRMWARE}" in uefi) PKGS+=('de-p1st-base') ;; bios) echo 'Not yet implemented' return 1 ;; *) echo 'Not yet implemented!' return 1 ;; esac case "${INITRAMFS}" in mkinitcpio) PKGS+=('de-p1st-mkinitcpio') ;; dracut) PKGS+=('dracut') # PKGS+=('binutils') # for --uefi (Unified Kernel Image) option PKGS+=('elfutils') # reduced initramfs size PKGS+=('multipath-tools') # dracut module support ;; *) echo 'Invalid option: '"${INITRAMFS}" return 1 ;; esac case "${CPU_VENDOR}" in amd) PKGS+=('de-p1st-ucode-amd') ;; intel) PKGS+=('de-p1st-ucode-intel') ;; none) PKGS+=('de-p1st-ucode-none') ;; *) echo 'Invalid option: '"${CPU_VENDOR}" return 1 ;; esac local args=() if [ "${PACSTRAP_INTERACTIVE}" = 'true' ]; then args+=('-i') # Run interactively fi sudo pacstrap -K "${args[@]}" /mnt "${PKGS[@]}" } function run_genfstab() { # @pre # FS echo 'Generating fstab ...' local fstab fstab="$(genfstab -U /mnt)" case "${FS}" in BTRFS) # Remove "subvolid=..." mount option but leave "subvol=..." mount option fstab=$(printf '%s' "${fstab}" | sed 's/,subvolid=[^,\s]\+//') # Check if fstab does still contain subvolid mount option if printf '%s' "${fstab}" | grep --quiet 'subvolid='; then echo 'This should not happen!' return 1 fi ;; EXT4) true ;; F2FS) true ;; *) printf '%s\n' "Filesystem ${FS} not yet supported!" return 1 ;; esac echo 'Generating fstab' printf '%s' "${fstab}" | sudo tee /mnt/etc/fstab } function config_hostname_and_hosts() { # @pre # HOSTNAME # FQDN: optional, e.g. sub.domain.de # STATIC_IP: optional, e.g. 93.133.433.133 # IPV6_CAPABLE: optional, 'true' echo 'Set hostname ...' echo "${HOSTNAME}" | sudo tee /mnt/etc/hostname >/dev/null echo 'Create hosts file ...' # If the system has a permanent IP address, it should be used instead of 127.0.1.1. # * https://wiki.archlinux.org/index.php/Installation_guide#Network_configuration # Desirable entries IPv4/IPv6: # * https://man.archlinux.org/man/hosts.5#EXAMPLES # If FQDN not given, use $HOSTNAME.localdomain instead FQDN="${FQDN:="${HOSTNAME}.localdomain"}" # If STATIC_IP not given, use 127.0.1.1 instead STATIC_IP="${STATIC_IP:=127.0.1.1}" echo '# 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 '"${STATIC_IP} ${FQDN} ${HOSTNAME}" | sudo tee /mnt/etc/hosts >/dev/null if [ "${IPV6_CAPABLE}" = 'true' ]; then echo ' # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters' | sudo tee -a /mnt/etc/hosts >/dev/null fi } function user_and_pwd() { # @pre # USERNAME # USER_PWD # ROOT_PWD (optional) echo 'Adding user and changing shell to /bin/zsh ...' # -m: Create home # -U: Create a group with the same name as the user, and add the user to this group. sudo arch-chroot /mnt useradd -m -s /usr/bin/zsh -g wheel "${USERNAME}" sudo arch-chroot /mnt chsh -s /usr/bin/zsh if [ -z "${ROOT_PWD:-}" ]; then printf '%s\n' 'No root password given, using the user password for root' ROOT_PWD="${USER_PWD}" fi printf '%s:%s' "${USERNAME}" "${USER_PWD}" | sudo chpasswd --root /mnt printf '%s:%s' "root" "${ROOT_PWD}" | sudo chpasswd --root /mnt } function initramfs(){ # @pre # INITRAMFS case "${INITRAMFS}" in mkinitcpio) sudo arch-chroot /mnt mkinitcpio -P ;; dracut) # configuration # https://man.archlinux.org/man/dracut.conf.5 # https://man.archlinux.org/man/dracut.cmdline.7 install -m0644 /dev/stdin /mnt/etc/dracut.conf.d/de-p1st.conf << 'EOF' # --fstab use_fstab=yes # --show-modules show_modules=yes # https://wiki.archlinux.org/title/Dracut#LVM_/_software_RAID_/_LUKS kernel_cmdline="rd.auto rd.luks=1" EOF # TODO: # Use a hook to do this # on the running system after each # kernel upgrade. # https://wiki.archlinux.org/title/Dracut#Generate_a_new_initramfs_on_kernel_upgrade # - detect kernel version # - install vmlinuz # - run dracut for module in /mnt/lib/modules/*; do kernel="$(basename "${module}")" pkgbase="$(cat "${module}/pkgbase")" install -Dm644 "${module}/vmlinuz" "/mnt/boot/vmlinuz-${pkgbase}" echo 'Kernel command line:' arch-chroot /mnt dracut \ --kver "${kernel}" \ --print-cmdline \ "/boot/initramfs-${pkgbase}.img" echo 'Creating initramfs:' arch-chroot /mnt dracut \ --kver "${kernel}" \ "/boot/initramfs-${pkgbase}.img" done ;; *) echo 'Invalid option: '"${INITRAMFS}" return 1 ;; esac } function bootloader() { # @pre # TARGET_BLOCK_DEVICE # FDE: 'true' | 'false' # LUKS_PART_UUID: required if FDE='true' echo 'Installing grub:' case "${BOOT_FIRMWARE}" in uefi) # Portable fallback efi name for grub: # * https://www.rodsbooks.com/efi-bootloaders/installation.html#alternative-naming # * arch-chroot /mnt cp /boot/EFI/GRUB/grubx64.efi /boot/EFI/BOOT/bootx64.efi sudo arch-chroot /mnt grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot --removable ;; bios) sudo arch-chroot /mnt grub-install --target=i386-pc "${TARGET_BLOCK_DEVICE}" ;; *) echo 'Not yet implemented!' return 1 ;; esac # grub.cfg adjustments if [ "${INITRAMFS}" = 'mkinitcpio' ] && [ "${FDE}" = "true" ]; then # /etc/default/grub is managed by Holo. Therefore we should not manually modify it. # Instead, we create a holosript which writes $LUKS_PART_UUID into GRUB_CMDLINE_LINUX of /etc/default/grub { # Use filename .../20- for the holoscript so that it gets executed after the one from de-p1st-grub local holoScriptDir=/mnt/usr/share/holo/files/20-de-p1st-installer/etc/default/ # The holoscript shall contain one 'sed "..."' command sudo mkdir -p "${holoScriptDir}" sudo echo "#!/bin/sh py-regex-replace -p '^GRUB_CMDLINE_LINUX=\"\"\$' -r 'GRUB_CMDLINE_LINUX=\"cryptdevice=/dev/disk/by-uuid/${LUKS_PART_UUID}:crypt\"' " | sudo tee "${holoScriptDir}"/grub.holoscript sudo chmod 0544 "${holoScriptDir}"/grub.holoscript } # Then we apply the holoscript sudo arch-chroot /mnt holo apply --force file:/etc/default/grub # Print start of grub file containing GRUB_CMDLINE_LINUX head /mnt/etc/default/grub fi echo 'Generating /boot/grub/grub.cfg:' sudo arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg if [ "${INITRAMFS}" = 'mkinitcpio' ] && [ "${FDE}" = "true" ]; then printf '%s\n' 'Occurrence of "cryptdevice" in /boot/grub/grub.cfg:' grep 'cryptdevice' /mnt/boot/grub/grub.cfg fi } function unmount_partitions(){ if [ "${LEAVE_MOUNTED}" = 'true' ]; then echo 'Leaving partitions below /mnt mounted and '"${DATA_PART}"' opened.' else echo 'unmount /mnt' sudo umount -R /mnt if [ "${FDE}" = 'true' ]; then echo "luksClose ${DATA_PART}" sudo cryptsetup luksClose "$(basename "${DATA_PART}")" fi fi } main "$@"