From bce90bf7d8ee870e3ce5096c6c4e0440de2988f2 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Tue, 21 Mar 2023 14:11:10 +0100 Subject: [PATCH] v3.0 mkinitcpio or dracut for initramfs generation --- PKGBUILD | 2 +- README.md | 2 +- de-p1st-installer.sh | 356 ++++++++++++++++++++++++------------ example-headless-docker.cfg | 16 +- example-vbox.cfg | 57 ++---- lib/block-device.sh | 206 ++++++++++++--------- lib/user-input.sh | 144 +++++++-------- lib/util.sh | 103 ++++++----- lib/validate-args.sh | 15 ++ name-reference-test.sh | 39 ---- 10 files changed, 531 insertions(+), 409 deletions(-) create mode 100644 lib/validate-args.sh delete mode 100644 name-reference-test.sh diff --git a/PKGBUILD b/PKGBUILD index e6d77d6..0fb668b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ _pkgname=installer _reponame=arch-installer pkgname="de-p1st-$_pkgname" -pkgver=0.2.4 +pkgver=0.3.0 pkgrel=1 pkgdesc="Installer for Arch Linux written in Bash" arch=('any') diff --git a/README.md b/README.md index 029a84e..2dfdd0e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ls out/out_dir/*.iso 2) Boot into the live medium 3) Connect to the Internet - * [wifi instructions](https://wiki.archlinux.org/title/Iwd#iwctl) + * [Wi-Fi instructions](https://wiki.archlinux.org/title/Iwd#iwctl) **On some other computer:** diff --git a/de-p1st-installer.sh b/de-p1st-installer.sh index c533e28..07ec9e7 100755 --- a/de-p1st-installer.sh +++ b/de-p1st-installer.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Exit on error. set -e # Exit on undefined variable reference. @@ -20,6 +20,8 @@ else 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 @@ -51,40 +53,42 @@ function main() { # @post # installation finished + scrollback check_network system_time - # in: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD; (all variables are optional) - # out: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD (if FDE='true') - get_user_input - # in: CPU_VENDOR (optional) - # out: CPU_VENDOR - get_cpu_vendor + repository - # in: FS + # 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 + # in: FS # out: FS_ADDITIONAL_MOUNT_OPTIONS get_additional_mount_options - # in: TARGET_BLOCK_DEVICE, BOOT_FIRMWARE # out: BOOT_PART, LUKS_PART - partition - # in: BOOT_FIRMWARE, BOOT_PART, LUKS_PART, FDE, LUKS_PWD, FS + partition \ + "${TARGET_BLOCK_DEVICE}" "${BOOT_FIRMWARE}" "${BOOT_PART_SIZE}" \ + BOOT_PART LUKS_PART # out: LUKS_PART_UUID (if FDE='true'), DATA_PART - format + format \ + "${BOOT_PART}" "${LUKS_PART}" "${LUKS_PWD}" "${FDE}" "${FS}" \ + LUKS_PART_UUID DATA_PART - # Combine default and additional mount options - # out: FS_MOUNT_OPTIONS - { - TMP1=("${FS_DEFAULT_MOUNT_OPTIONS[@]}" "${FS_ADDITIONAL_MOUNT_OPTIONS[@]}") - # Join array elements by "," - join_by ',' TMP1 FS_MOUNT_OPTIONS - } + # 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, PACSTRAP_INTERACTIVE (optional) + # in: BOOT_FIRMWARE, INITRAMFS, PACSTRAP_INTERACTIVE (optional) run_pacstrap # in: FS run_genfstab @@ -94,25 +98,33 @@ function main() { # in: USERNAME, USER_PWD, ROOT_PWD (optional) user_and_pwd - sudo arch-chroot /mnt mkinitcpio -P + # in: INITRAMFS + initramfs # in: TARGET_BLOCK_DEVICE, FDE, LUKS_PART_UUID bootloader - if [ "${LEAVE_MOUNTED}" = 'true' ]; then - echo 'Leaving partitions below /mnt mounted and '"${DATA_PART}"' opened.' - else - sudo umount -R /mnt - if [ "${FDE}" = 'true' ]; then - sudo cryptsetup luksClose "$(basename "${DATA_PART}")" - fi - fi + 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.' + echo 'Pleas set up network access and then run the installer again.' return 1 } } @@ -122,6 +134,19 @@ function system_time() { 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! @@ -138,7 +163,9 @@ function increase_cow_space() { 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 @@ -146,15 +173,17 @@ function get_user_input() { # FDE: 'true' | 'false' # LUKS_PWD: only set if FDE='true' - get_block_devices_with_size + # 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. - # "-q": tell grep to output nothing - if dmesg | grep -q 'EFI v'; then + if dmesg | grep --quiet --fixed-strings 'EFI v'; then echo 'Detected EFI boot.' BOOT_FIRMWARE='uefi' else @@ -163,46 +192,64 @@ function get_user_input() { fi else # If $BOOT_FIRMWARE is empty: Let user select BIOS type - TMP1=('uefi' 'Newer mainboards' \ - 'bios' 'Legacy BIOS on older mainboards') - single_choice_if_empty BOOT_FIRMWARE 'Select your bios type' TMP1 + # 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 - TMP1=('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' TMP1 + # 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 - TMP1=('root_only' 'Just one subvolume for "/".' \ + # 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' TMP1 + 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 - [[ "${USER_PWD}" == "${USER_PWD2}" ]] || { + if [ "${USER_PWD}" != "${USER_PWD2}" ]; then echo 'Passwords did not match' exit 1 - } + fi fi - TMP1=('true' 'Yes' 'false' 'No') - single_choice_if_empty FDE 'Shall Full-Disk-Encryption be enabled?' TMP1 + # 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 - [[ "${LUKS_PWD}" == "${LUKS_PWD2}" ]] || { + 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 } @@ -225,9 +272,7 @@ function get_default_mount_options() { FS_DEFAULT_MOUNT_OPTIONS+=('compress=zstd:1') ;; EXT4) - # https://wiki.archlinux.org/index.php/Ext4#Enabling_metadata_checksums - # If the CPU supports SSE 4.2, make sure the crc32c_intel kernel module is loaded - FS_DEFAULT_MOUNT_OPTIONS+=('metadata_csum') + FS_DEFAULT_MOUNT_OPTIONS+=() ;; F2FS) # When mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle). @@ -250,19 +295,19 @@ function get_additional_mount_options() { case "${FS}" in BTRFS) # noatime, nodiratime: - # - The atime options do impact drive performance; - # - noatime implies nodiratime, one does not need to specify both; - # - The noatime option fully disables writing file access times to the drive every time you read a file. + # - `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. - TMP1=('noatime' 'Don'\''t write file/folder access times' 'on' 'ssd' 'Enable if using SSD/NVMe' 'off') + FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on' 'ssd' 'Enable if using SSD/NVMe' 'off') ;; EXT4) - TMP1=('noatime' 'Don'\''t write file/folder access times' 'on') + FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on') ;; F2FS) # shellcheck disable=SC2034 - TMP1=('noatime' 'Don'\''t write file/folder access times' 'on') + FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION=('noatime' 'Don'\''t write file/folder access times' 'on') ;; *) echo 'Filesystem '"${FS}"' not yet supported!' @@ -270,7 +315,7 @@ function get_additional_mount_options() { ;; esac - multi_choice_if_empty FS_ADDITIONAL_MOUNT_OPTIONS 'Select mount options' TMP1 + multi_choice_if_empty FS_ADDITIONAL_MOUNT_OPTIONS 'Select mount options' FS_ADDITIONAL_MOUNT_OPTIONS_QUESTION } function mount_partitions() { @@ -316,6 +361,9 @@ function mount_partitions() { 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() { @@ -340,24 +388,37 @@ function run_pacstrap() { ;; esac - # If CPU_VENDOR is not empty, then - if [ -n "${CPU_VENDOR}" ]; then - case "${CPU_VENDOR}" in - amd) - PKGS+=('de-p1st-ucode-amd') - ;; - intel) - PKGS+=('de-p1st-ucode-intel') - ;; - none) - PKGS+=('de-p1st-ucode-placeholder') - ;; - *) - echo 'Invalid option: '"${CPU_VENDOR}" - return 1 - ;; - esac - fi + 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 @@ -380,7 +441,7 @@ function run_genfstab() { # 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 -q 'subvolid='; then + if printf '%s' "${fstab}" | grep --quiet 'subvolid='; then echo 'This should not happen!' return 1 fi @@ -392,12 +453,13 @@ function run_genfstab() { true ;; *) - echo 'Filesystem '"${FS}"' not yet supported!' + printf '%s\n' "Filesystem ${FS} not yet supported!" return 1 ;; esac - printf '%s' "${fstab}" | sudo tee /mnt/etc/fstab >/dev/null + echo 'Generating fstab' + printf '%s' "${fstab}" | sudo tee /mnt/etc/fstab } function config_hostname_and_hosts() { @@ -448,20 +510,77 @@ function user_and_pwd() { sudo arch-chroot /mnt useradd -m -s /usr/bin/zsh -g wheel "${USERNAME}" sudo arch-chroot /mnt chsh -s /usr/bin/zsh - # If ROOT_PWD is not given, the use USER_PWD for root user - ROOT_PWD="${ROOT_PWD:="${USER_PWD}"}" + 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 + 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 ...' + echo 'Installing grub:' case "${BOOT_FIRMWARE}" in uefi) # Portable fallback efi name for grub: @@ -478,40 +597,47 @@ function bootloader() { ;; esac - echo 'Generating /boot/grub/grub.cfg ...' - { - case "${FDE}" in - true) - # /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 - { - # Assert - grep --quiet '^GRUB_CMDLINE_LINUX=""$' < /mnt/etc/default/grub || return + # 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 - # 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 -sed "s|^GRUB_CMDLINE_LINUX=\"\"\$|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 - ;; - false) - true - ;; - *) - echo 'Invalid option: '"${FDE}" - return 1 - ;; - esac + # Print start of grub file containing GRUB_CMDLINE_LINUX + head /mnt/etc/default/grub + fi - # And finally run grub-mkconfig - sudo arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg - } + 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 "$@" diff --git a/example-headless-docker.cfg b/example-headless-docker.cfg index 87d37ad..71fb687 100644 --- a/example-headless-docker.cfg +++ b/example-headless-docker.cfg @@ -3,26 +3,30 @@ FQDN=v2202012136854137045.happysrv.de STATIC_IP=45.83.105.88 IPV6_CAPABLE=true -HOSTNAME=p1st-arch-1 +HOSTNAME=netcup-example USERNAME=yoda +FDE=false + TARGET_BLOCK_DEVICE=/dev/sda -BOOT_PART_SIZE=500 # MiB +BOOT_PART_SIZE=260 + FS=BTRFS FS_ADDITIONAL_MOUNT_OPTIONS=('noatime') CPU_VENDOR=none BOOT_FIRMWARE=uefi +INITRAMFS=dracut LEAVE_MOUNTED=false PACSTRAP_INTERACTIVE=true +############## ADDITIONAL_PKGS from here on ############## ADDITIONAL_PKGS=() -# to skip pacman selections -ADDITIONAL_PKGS+=('mkinitcpio' 'de-p1st-kernel-lts' 'de-p1st-ucode-placeholder' 'qemu-guest-agent') -# remote ssh access + +# Remote SSH access ADDITIONAL_PKGS+=('de-p1st-ssh' 'de-p1st-ssh-key-yoda') -# docker and docker-compose +# Docker and Docker Compose ADDITIONAL_PKGS+=('de-p1st-docker') # cronie ADDITIONAL_PKGS+=('de-p1st-cronie') diff --git a/example-vbox.cfg b/example-vbox.cfg index 7390dc7..c4bb2ec 100644 --- a/example-vbox.cfg +++ b/example-vbox.cfg @@ -1,64 +1,47 @@ # Example config for testing in VirtualBox -# FQDN=domain.name.of.this.host.de -# STATIC_IP=123.123.123.123 -# IPV6_CAPABLE=true +#FQDN=domain.name.of.this.host.de +#STATIC_IP=123.123.123.123 +# IPV6_CAPABLE: 'true' | 'false' +IPV6_CAPABLE=true HOSTNAME=yodaTest USERNAME=yoda -# One should rather enter these interactively than saving in this cfg. USER_PWD='test' -FDE=false # enable FULL-DISK-ENCRYPTION +#ROOT_PWD=test + +# FDE: 'true' | 'false' +FDE=true LUKS_PWD='test' -# If unset, then USER_PWD will be used for ROOT_PWD -# ROOT_PWD=test - TARGET_BLOCK_DEVICE=/dev/sda # /dev/vda for Virtual Machine Manager -BOOT_PART_SIZE=500 # MiB +# BOOT_PART_SIZE: in MiB, at least 260 +BOOT_PART_SIZE=260 + +# FS: 'BTRFS' | 'EXT4' | 'F2FS' FS=BTRFS -FS_BTRFS_SUBVOL_LAYOUT='@root@home' # Subvolume layout that is supported by Timeshift +# FS_BTRFS_SUBVOL_LAYOUT: 'root_only' | '@root@home' +FS_BTRFS_SUBVOL_LAYOUT='@root@home' FS_ADDITIONAL_MOUNT_OPTIONS=('noatime') # If not booted into the target system, these values should be set: # CPU_VENDOR: 'autodetect' | 'amd' | 'intel' | 'none' -CPU_VENDOR=autodetect +CPU_VENDOR=none # BOOT_FIRMWARE: 'autodetect' | 'uefi' | 'bios' BOOT_FIRMWARE=uefi +# INITRAMFS: 'mkinitcpio' | 'dracut' +INITRAMFS=dracut # If set to 'true', then the data, boot and luks partitions # will be left mounted/opened for manual inspection # after the installation -LEAVE_MOUNTED=true +# LEAVE_MOUNTED: 'true' | 'false' +LEAVE_MOUNTED=false +# PACSTRAP_INTERACTIVE: 'true' | 'false' PACSTRAP_INTERACTIVE=true ############## ADDITIONAL_PKGS from here on ############## ADDITIONAL_PKGS=() -# Specify the preferred providers for initramfs, de-p1st-kernel, de-p1st-ucode, de-p1st-sddm-theme -ADDITIONAL_PKGS+=('mkinitcpio' 'de-p1st-kernel-lts' 'de-p1st-ucode-placeholder' 'de-p1st-sddm-theme-nordic') # VBox guest additions ADDITIONAL_PKGS+=('virtualbox-guest-utils') -# XFCE4 desktop with HiDPI -ADDITIONAL_PKGS+=('de-p1st-gpu-generic' 'de-p1st-xfce4-hidpi' 'de-p1st-sddm-tablet') -# To be able to install ungoogled-chromium -ADDITIONAL_PKGS+=('de-p1st-pacman-repo-chromium') - -# If your network is unsafe, e.g. if you live in Germany, then include a VPN client -ADDITIONAL_PKGS+=('riseup-vpn') -# Smartcard (e.g. Nitrokey or Yubikey) -ADDITIONAL_PKGS+=('de-p1st-smartcard') - - -# Other programs -ADDITIONAL_PKGS+=('firefox' 'signal-desktop' 'nextcloud-client' 'keepassxc' 'xournalpp' 'zotero') -# Development -ADDITIONAL_PKGS+=('intellij-idea-ultimate-edition' 'intellij-idea-ultimate-edition-jre') -# Document viewer -ADDITIONAL_PKGS+=('evince') -# Gallery / image viewer -ADDITIONAL_PKGS+=('nomacs' 'qt5-imageformats') -# LibreOffice with spell checking (de, us) and hyphenation rules (de, us) -ADDITIONAL_PKGS+=('libreoffice-fresh') -ADDITIONAL_PKGS+=('hunspell-de' 'hunspell-en_US') -ADDITIONAL_PKGS+=('hyphen' 'hyphen-de' 'hyphen-en') diff --git a/lib/block-device.sh b/lib/block-device.sh index f338caf..838fbf5 100644 --- a/lib/block-device.sh +++ b/lib/block-device.sh @@ -6,87 +6,103 @@ # -l, --list Produce output in the form of a list. # -n, --noheadings # -x, --sort column +# +# lsblk --tree=PATH -o PATH,TYPE,PARTUUID function get_uuid() { # arg $1: Partition # arg $2: Variable name to store UUID + validate_args 2 "$@" || return $? + local partition="${1}" + local -n uuid_ptr=$2 - local -n ptr=$2 || return $? # shellcheck disable=SC2034 - ptr="$(blkid -o value -s UUID "${1}")" || return $? + uuid_ptr="$(blkid -o value -s UUID "${partition}")" || return $? } function get_partitions(){ - # arg $1: block-device - # @post - # PARTITIONS (array with one partition per entry) + # arg $1: block device + # arg $2: name of variable to store partitions + validate_args 2 "$@" || return $? + local block_device="$1" + local -n partitions_ptr=$2 - # Remove first line of output (which is just the block device $1 itself) - # with sed: sed 1d - PARTITIONS="$(lsblk -pln -o name "${1}" | sed 1d)" || return $? + # Remove first line of output (which is just the block device itself) + # with sed: `sed 1d` + # shellcheck disable=SC2034 + partitions_ptr="$(lsblk -pln -o name "${block_device}" | sed 1d)" || return $? - newline_separated_to_array PARTITIONS PARTITIONS || return $? + newline_separated_to_array partitions_ptr partitions_ptr || return $? } function get_block_devices() { - # @post - # BLOCK_DEVICES (array with one entry for each block device) + # arg $1: name of variable to store block devices + validate_args 1 "$@" || return $? + local -n block_devices_ptr=$1 # Get list of devices, one per line - BLOCK_DEVICES="$(lsblk -dplnx size -o name | grep -Ev 'boot|rpmb|loop' | tac)" || return $? + # shellcheck disable=SC2034 + block_devices_ptr="$(lsblk -dplnx size -o name | grep -Ev 'boot|rpmb|loop' | tac)" || return $? - newline_separated_to_array BLOCK_DEVICES BLOCK_DEVICES || return $? + newline_separated_to_array block_devices_ptr block_devices_ptr || return $? } function get_block_devices_with_size() { - # @post - # BLOCK_DEVICE_SIZES (array with two entries for each block device: device path and device size) + # Example: variable with name $1 has content ('/dev/nvme0n1' '1,9T') + # + # arg $1: name of variable to store block device paths and their sizes + validate_args 1 "$@" || return $? + local -n block_device_sizes_ptr=$1 # Get list of devices and their sizes, one pair per line. # Use sed to remove multiple white spaces: sed 's|\s\s*| |' - BLOCK_DEVICE_SIZES="$(lsblk -dplnx size -o name,size | grep -Ev 'boot|rpmb|loop' | sed 's|\s\s*| |' | tac)" || return $? + # shellcheck disable=SC2034 + block_device_sizes_ptr="$(lsblk -dplnx size -o name,size | grep -Ev 'boot|rpmb|loop' | sed 's|\s\s*| |' | tac)" || return $? - newline_to_space BLOCK_DEVICE_SIZES || return $? - space_separated_to_array BLOCK_DEVICE_SIZES BLOCK_DEVICE_SIZES || return $? + newline_to_space block_device_sizes_ptr || return $? + space_separated_to_array block_device_sizes_ptr block_device_sizes_ptr || return $? } - function partition() { # Creates two partitions: - # - BOOT_PART by default 261mb - # - LUKS_PART rest of the device for (encrypted) data + # - boot partition, by default 260MiB + # - luks partition, rest of the device for (encrypted) data # - # @pre - # TARGET_BLOCK_DEVICE - # BOOT_FIRMWARE - # @post - # BOOT_PART - # LUKS_PART + # arg $1: target block device + # arg $2: boot firmware: 'uefi' | 'bios' + # arg $3: boot partition size in MiB: >=260 | 'none' + # arg $4: name of variable to store boot partition + # arg $5: name of variable to store luks partition + validate_args 5 "$@" || return $? + local target_block_device="${1}" + local boot_firmware="${2}" + local boot_part_size="${3}" + local -n boot_part=$4 + local -n luks_part=$5 - # If BOOT_PART_SIZE not given, set to default value - BOOT_PART_SIZE="${BOOT_PART_SIZE:='261'}" - # If too small, print warning and exit - if [ "${BOOT_PART_SIZE}" -lt '261' ]; then - echo 'BOOT_PART_SIZE should be larger than 260!'; - return 1; + local minimum=260 + # Check if too small + if [ "${boot_part_size}" -lt "${minimum}" ]; then + printf '%s\n' "boot_part_size should be >= ${minimum}" + return 1 fi # As our data partition is encrypted, # we need a separate boot partition! - case "${BOOT_FIRMWARE}" in + case "${boot_firmware}" in uefi) # EFI boot partition # # Create a partition with fat32 as the file system type and set the # esp flag on it. - sudo parted --script "${TARGET_BLOCK_DEVICE}" -- mklabel gpt \ - mkpart ESP fat32 2Mib "${BOOT_PART_SIZE}MiB" \ + sudo parted --script "${target_block_device}" -- mklabel gpt \ + mkpart ESP fat32 2Mib "${boot_part_size}MiB" \ set 1 esp on \ - mkpart primary "${BOOT_PART_SIZE}MiB" 100% || return $? + mkpart primary "${boot_part_size}MiB" 100% || return $? - get_partitions "${TARGET_BLOCK_DEVICE}" || return $? - BOOT_PART="${PARTITIONS[0]}" - LUKS_PART="${PARTITIONS[1]}" + get_partitions "${target_block_device}" PARTITIONS || return $? + boot_part="${PARTITIONS[0]}" + luks_part="${PARTITIONS[1]}" ;; bios) # > On a BIOS/GPT configuration, a BIOS boot partition is required. GRUB embeds its `core.img` @@ -95,97 +111,111 @@ function partition() { # # archwiki -> GRUB#GUID_Partition_Table_(GPT)_specific_instructions # https://www.gnu.org/software/grub/manual/grub/html_node/BIOS-installation.html#BIOS-installation - sudo parted --script "${TARGET_BLOCK_DEVICE}" -- mklabel gpt \ + sudo parted --script "${target_block_device}" -- mklabel gpt \ mkpart primary 1MiB 2MiB \ set 1 bios_grub on \ - mkpart primary 2MiB "${BOOT_PART_SIZE}MiB" \ - mkpart primary "${BOOT_PART_SIZE}MiB" 100% || return $? + mkpart primary 2MiB "${boot_part_size}MiB" \ + mkpart primary "${boot_part_size}MiB" 100% || return $? - get_partitions "${TARGET_BLOCK_DEVICE}" || return $? - BOOT_PART="${PARTITIONS[2]}" - LUKS_PART="${PARTITIONS[3]}" + get_partitions "${target_block_device}" PARTITIONS || return $? + boot_part="${PARTITIONS[2]}" + luks_part="${PARTITIONS[3]}" ;; *) - echo 'Expected uefi or bios but got '"${BOOT_FIRMWARE}"' instead!' + printf '%s\n' "Expected uefi or bios but got ${boot_firmware} instead!" return 1 ;; esac - echo 'boot partition: '"${BOOT_PART}" - echo 'luks partition: '"${LUKS_PART}" + printf '%s\n' "boot partition: ${boot_part}" + printf '%s\n' "luks partition: ${luks_part}" } function format() { - # Formats BOOT_PART and LUKS_PART. - # If encryption is enabled (FDE='true'), then an encrypted partition will be created at LUKS_PART - # which can be accessed under DATA_PART. - # If encryption is disabled, then DATA_PART is identical to LUKS_PART. + # Formats the `boot` and `luks` partitions. + # + # If FDE is enabled ('true'), + # then $2 will be encrypted with password $3 and opened at $7. + # + # If FDE is disabled, + # then $7 is identical with $2. + # + # arg $1: boot partition + # arg $2: (encrypted) luks partition + # arg $3: password of luks partition + # arg $4: full-disk-encryption (FDE): 'true' | 'false' + # arg $5: filesystem: 'BTRFS' | 'EXT4' | 'F2FS' + # arg $6: name of variable to store partition of encrypted luks partition (if FDE='true'). + # Set to /dev/mapper/$(basename $LUKS_PART) + # arg $7: name of variable to store data partition # - # @pre - # BOOT_FIRMWARE - # BOOT_PART - # LUKS_PART - # LUKS_PWD - # FDE: 'true' | 'false' - # FS # @post - # LUKS_PART_UUID: only set if FDE='true' - # DATA_PART: if FDE='true', then set to /dev/mapper/$(basename "${LUKS_PART}") # boot partition formatted - # luks partition formatted and accessible under DATA_PART + # luks partition formatted and accessible under $7 + validate_args 7 "$@" || return $? + local boot_part="${1}" + local luks_part="${2}" + local luks_pwd="${3}" + local fde="${4}" + local fs="${5}" + # shellcheck disable=SC2034 + local -n luks_part_uuid=$6 + local -n data_part=$7 - echo 'Wiping old signatures from partitions ...' - sudo wipefs "${BOOT_PART}" || return $? - sudo wipefs "${LUKS_PART}" || return $? + printf '%s\n' 'Wiping old signatures from partitions ...' + sudo wipefs --all "${boot_part}" || return $? + sudo wipefs --all "${luks_part}" || return $? - echo 'Formatting boot partition '"${BOOT_PART}"' ...' - sudo mkfs.fat -F32 "${BOOT_PART}" || return $? + printf '%s\n' "Formatting boot partition ${boot_part} ..." + sudo mkfs.fat -F32 "${boot_part}" || return $? - case "${FDE}" in + case "${fde}" in true) - # Note: - # FDE: GRUB does support LUKS2 since this commit: https://git.savannah.gnu.org/cgit/grub.git/commit/?id=365e0cc3e7e44151c14dd29514c2f870b49f9755 - # -> Using "--type luks1" is no longer required. - echo 'Creating encrypted luks partition '"${LUKS_PART}"' ...' - printf '%s' "${LUKS_PWD}" | sudo cryptsetup luksFormat \ + # FDE: + # GRUB does support LUKS2 since this commit: + # https://git.savannah.gnu.org/cgit/grub.git/commit/?id=365e0cc3e7e44151c14dd29514c2f870b49f9755 + # Using "--type luks1" is no longer required. + printf '%s\n' "Creating encrypted luks partition ${luks_part} ..." + printf '%s' "${luks_pwd}" | sudo cryptsetup luksFormat \ --cipher aes-xts-plain64 --key-size 512 --hash sha512 \ - --iter-time 10000 --use-random "${LUKS_PART}" || return $? + --iter-time 10000 --use-random "${luks_part}" || return $? - get_uuid "${LUKS_PART}" LUKS_PART_UUID || return $? + get_uuid "${luks_part}" luks_part_uuid || return $? local luks_name - luks_name=$(basename "${LUKS_PART}") - DATA_PART="/dev/mapper/${luks_name}" + luks_name="$(basename "${luks_part}")" + data_part="/dev/mapper/${luks_name}" # Open luks partition - printf '%s' "${LUKS_PWD}" | sudo cryptsetup luksOpen "${LUKS_PART}" "${luks_name}" || return $? + printf '%s' "${luks_pwd}" | sudo cryptsetup luksOpen "${luks_part}" "${luks_name}" || return $? ;; false) - DATA_PART="${LUKS_PART}" + data_part="${luks_part}" ;; *) - echo 'Invalid option: '"${FDE}" + printf '%s\n' "Invalid option: ${fde}" return 1 ;; esac - echo 'Formatting the data partition '"${DATA_PART}"' ...' - case "${FS}" in + printf '%s\n' "Formatting the data partition ${data_part} ..." + case "${fs}" in BTRFS) - sudo mkfs.btrfs "${DATA_PART}" || return $? + sudo mkfs.btrfs "${data_part}" || return $? ;; EXT4) - # archwiki -> Ext4#Enabling_metadata_checksums - sudo mkfs.ext4 -O metadata_csum "${DATA_PART}" || return $? + # https://wiki.archlinux.org/index.php/Ext4 + # If the CPU supports SSE 4.2, make sure the crc32c_intel kernel module is loaded + sudo mkfs.ext4 -O metadata_csum "${data_part}" || return $? ;; F2FS) # archwiki -> F2FS#Creating_a_F2FS_file_system # - requires f2fs-tools # - compression: "-O compression" and when mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle) - sudo mkfs.f2fs -O extra_attr,inode_checksum,sb_checksum,compression "${DATA_PART}" || return $? + sudo mkfs.f2fs -O extra_attr,inode_checksum,sb_checksum,compression "${data_part}" || return $? ;; *) - echo 'Filesystem '"${FS}"' is not yet supported!' + printf '%s\n' "Filesystem ${fs} is not yet supported!" return 1 ;; esac diff --git a/lib/user-input.sh b/lib/user-input.sh index 544d82f..2f08e5a 100644 --- a/lib/user-input.sh +++ b/lib/user-input.sh @@ -1,102 +1,84 @@ function ask_user_if_empty { - # If variable with name $1 is empty, then ask for user input. + # If variable named $1 is empty, then ask for user input. # - # Only one line of user input is allowed. - # The input must not be empty. + # One line of user input is read. + # The user input must not be empty. # # arg $1: Name of variable to store user input # arg $2: Text to display (e.g. "Enter hostname:") - if [ "$#" -ne 2 ]; then - echo 'ask_user_if_empty requires two args!'; + validate_args 2 "$@" || return $? + local -n user_input_ptr=$1 + local display_text="${2}" + + # If user_input_ptr is not empty, return + if [ -n "${user_input_ptr:-}" ]; then + return 0 + fi + # Else, ask for user input! + + printf '%s\n' "${display_text}" + read -r user_input_ptr || return $? + + # User input must not be empty + if [ -z "${user_input_ptr}" ]; then + echo 'The input must not be empty!' return 1 fi - for i in "$@"; do - if [ -z "${i}" ]; then - echo 'ask_user_if_empty: all given args must not be empty'; - return 1; - fi - done - - local -n ptr=$1 || return $? - if [ -z "${ptr}" ]; then - # If ptr has no value yet, ask user for input! - echo "${2}" - read -r ptr || return $? - fi - - # Check string length to be greater than zero! - if [ "${#ptr}" -lt 1 ]; then - echo 'The input must not be empty!'; - return 1; - fi } function single_choice_if_empty { - # If variable with name $1 is empty, then let user select one of the given options. + # If variable named $1 is empty, + # then let user select one of the given options. # - # arg $1: Name of variable to store the selected option + # arg $1: Name of variable to store the selected option-name # arg $2: Text to display - # arg $3: Name of variable with array of options to display (for each option there must be two entries in the array: Item and description) - if [ "$#" -ne 3 ]; then - echo 'single_choice_if_empty requires three args!'; + # arg $3: Name of variable with array of options to display (for each option there must be two entries in the array: name and description) + validate_args 3 "$@" || return $? + local -n selected_option_ptr=$1 + local display_text="${2}" + local -n menu_options_ptr=$3 + + # If selected_option_ptr is not empty, return. + if [ -n "${selected_option_ptr:-}" ]; then + return 0 + fi + # Else, ask for user input! + + selected_option_ptr=$(dialog --stdout --menu "${display_text}" 0 0 0 "${menu_options_ptr[@]}") || { + echo 'Error during menu selection!' return 1 - fi - for i in "$@"; do - if [ -z "${i}" ]; then - echo 'single_choice_if_empty: all given args must not be empty'; - return 1; - fi - done - - - local -n ptr=$1 || return $? - if [ -z "${ptr}" ]; then - # If ptr has no value yet, ask user for input! - - local -n MENU_OPTIONS=$3 || return $? - ptr=$(dialog --stdout --menu "${2}" 0 0 0 "${MENU_OPTIONS[@]}") || { - echo 'Error during menu selection!' - exit 1 - } - clear - fi + } + clear } function multi_choice_if_empty { - # If variable with name $1 is empty, then let user select one or more of the given options. + # If variable named $1 is empty, + # then let user select one or more of the given options. # - # arg $1: Name of variable to store array of selected options + # arg $1: Name of variable to store the selected option-names as array # arg $2: Text to display - # arg $3: Name of variable with array of options to display (for each option there must be three entries in the array: Item, description, on/off) - if [ "$#" -ne 3 ]; then - echo 'multi_choice_if_empty requires three args!'; + # arg $3: Name of variable with array of options to display (for each option there must be three entries in the array: name, description, on/off) + validate_args 3 "$@" || return $? + local -n selected_option_ptr=$1 + local display_text="${2}" + local -n menu_options_ptr=$3 + + # If selected_option_ptr is not empty, return. + if [ -n "${selected_option_ptr:-}" ]; then + return 0 + fi + # Else, ask for user input! + + selected_option_ptr=$(dialog --stdout --checklist "${display_text}" 0 0 0 "${menu_options_ptr[@]}") || { + echo 'Error during menu selection!' return 1 - fi - for i in "$@"; do - if [ -z "${i}" ]; then - echo 'multi_choice_if_empty: all given args must not be empty'; - return 1; - fi - done + } + clear - - local -n ptr=$1 || return $? - if [ -z "${ptr}" ]; then - # If ptr has no value yet, ask user for input! - - local -n MENU_OPTIONS=$3 || return $? - # shellcheck disable=SC2034 # Variable name used below. - TMP1=$(dialog --stdout --checklist "${2}" 0 0 0 "${MENU_OPTIONS[@]}") || { - echo 'Error during menu selection!' - exit 1 - } - clear - - # Result of dialog is a space separated list. - # Store this as an array. - # Without newline at last array element: https://unix.stackexchange.com/a/519917/315162 - # readarray -d " " -t ptr < <(printf '%s' "$TMP1") - # - space_separated_to_array TMP1 "$1" - fi + # Result of dialog is a space separated list. + # Store this as an array. + # Without newline at last array element: https://unix.stackexchange.com/a/519917/315162 + # readarray -d " " -t ptr < <(printf '%s' "selected_option_ptr") + # + space_separated_to_array selected_option_ptr selected_option_ptr || return $? } diff --git a/lib/util.sh b/lib/util.sh index 2dff9ce..508c9a8 100644 --- a/lib/util.sh +++ b/lib/util.sh @@ -1,83 +1,104 @@ function join_by() { # Join array elements with character $1 # - # arg $1: Delimiter + # arg $1: A single-character delimiter, e.g. ',' # arg $2: Name of source array # arg $3: Variable name to store result + validate_args 3 "$@" || return $? + local delimiter="${1}" + local -n source_array_ptr=$2 + local -n joined_ptr=$3 - local -n ptr=$2 || return $? - local -n ptr2=$3 || return $? - ptr2=$(printf ',%s' "${ptr[@]}") - ptr2=${ptr2:1} + # https://stackoverflow.com/a/2317171 + joined_ptr=$(printf "${delimiter}%s" "${source_array_ptr[@]}") || return $? + joined_ptr=${joined_ptr:1} || return $? } function newline_to_space() { # Replaces all newlines with spaces # # arg $1: Name of variable + validate_args 1 "$@" || return $? + local -n ptr=$1 - local -n ptr=$1 || return $? # Replace newlines with spaces. # See bash string substitution: https://gist.github.com/JPvRiel/b279524d3e56229a896477cb8082a72b - - # echo "replacing newlines in str: -->$ptr<--" ptr="${ptr//$'\n'/' '}" - # echo "new str is: -->$ptr<--" } function newline_separated_to_array() { - # Watch out for tailing newlines as these will get an empty array entry at the end. + # Watch out for tailing newlines as these result in additional elements "" at the end of the array. # $1 and $2 can be equal (if the result shall be written to the input variable). # # arg $1: Name of variable with newline separated list # arg $2: Name of array to store values into + validate_args 2 "$@" || return $? + local -n source_ptr=$1 + local -n array1_ptr=$2 - local -n ptr=$1 || return $? - local -n ptr2=$2 || return $? - # ptr newline separated list. - # Store this as array ptr2. - readarray -t ptr2 <<<"${ptr}" + # source_ptr is a newline separated list. + # Store this as array array1_ptr. + # shellcheck disable=SC2034 + readarray -t array1_ptr <<<"${source_ptr}" } function space_separated_to_array() { # arg $1: Name of variable with space separated list # arg $2: Name of array to store values into - - local -n ptr=$1 || return $? + validate_args 2 "$@" || return $? + local -n source_ptr=$1 # shellcheck disable=SC2178 - local -n ptr2=$2 || return $? - # ptr space separated list. - # Store this as array ptr2. + local -n array2_ptr=$2 + + # source_ptr space separated list. + # Store this as array array2_ptr. # Without newline at last array element: https://unix.stackexchange.com/a/519917/315162 - readarray -d ' ' -t ptr2 < <(printf '%s' "${ptr}") + # shellcheck disable=SC2034 + readarray -d ' ' -t array2_ptr < <(printf '%s' "${source_ptr}") } function get_cpu_vendor() { + # If $1 does already contain a known CPU vendor, nothing is done. + # + # Otherwise, we try to detect the CPU vendor and save it into $1. + # + # arg $1: Name of variable to store CPU vendor into. + # # @pre - # CPU_VENDOR ("", "autodetect") + # $1 ("amd", "intel", "none", *) # @post - # CPU_VENDOR ("amd", "intel", "none", "") + # $1 ("amd", "intel", "none") + validate_args 1 "$@" || return $? + local -n cpu_vendor_ptr=$1 - if [[ -z "${CPU_VENDOR}" ]] || [[ "${CPU_VENDOR}" == 'autodetect' ]]; then + # Check if variable does already contain known CPU vendor. + case "${cpu_vendor_ptr}" in + amd|intel|none) + return 0 + ;; + *) + : + ;; + esac - # If run virtual environment (e.g. VirtualBox) then no CPU microcode is required. - if grep '^flags.*hypervisor' /proc/cpuinfo >/dev/null; then - echo 'Detected virtual environment.' - CPU_VENDOR='none' + # If in virtual environment (e.g. VirtualBox), then no CPU microcode is required. + if grep --quiet '^flags.*hypervisor' /proc/cpuinfo; then + echo 'Detected virtual environment.' + cpu_vendor_ptr='none' + return 0 + fi - else - local vendor_id - vendor_id=$(grep vendor_id /proc/cpuinfo | head -1 | sed 's|vendor_id\s*:\s*||') - - if [ "${vendor_id}" = 'AuthenticAMD' ]; then - CPU_VENDOR='amd' - elif [ "${vendor_id}" = 'GenuineIntel' ]; then - CPU_VENDOR='intel' - else - echo 'Unknown CPU vendor' - return 1 - fi - fi + local vendor_id + vendor_id="$(grep vendor_id /proc/cpuinfo | head -1 | sed 's|vendor_id\s*:\s*||')" + if [ "${vendor_id}" = 'AuthenticAMD' ]; then + cpu_vendor_ptr='amd' + return 0 + elif [ "${vendor_id}" = 'GenuineIntel' ]; then + cpu_vendor_ptr='intel' + return 0 + else + echo 'Unknown CPU vendor' + return 1 fi } diff --git a/lib/validate-args.sh b/lib/validate-args.sh new file mode 100644 index 0000000..54e8bcc --- /dev/null +++ b/lib/validate-args.sh @@ -0,0 +1,15 @@ +function validate_args(){ + local num_args="${1}" + shift + + if [ "$#" -ne "${num_args}" ]; then + printf '%s\n' "ERROR: ${num_args} arguments required" + return 1 + fi + for i in "$@"; do + if [ -z "${i}" ]; then + printf '%s\n' 'ERROR: All given args must not be empty' + return 1 + fi + done +} diff --git a/name-reference-test.sh b/name-reference-test.sh deleted file mode 100644 index 86d2f6b..0000000 --- a/name-reference-test.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# source: https://stackoverflow.com/a/52678279/6334421 - -# A variable can be assigned the nameref attribute using the -n option -# to the declare or local builtin commands (see Bash Builtins) to create -# a nameref, or a reference to another variable. This allows variables to -# be manipulated indirectly. - -function text_input { - # If variable of name $1 is zero, then ask user for input. - # Only one line user input is allowed. - # User input must not be empty. - # - # $1: variable name to store input to - # $2: text to display (e.g. "Enter hostname:") - if [ -z "${1}" ] || [ -z "${2}" ]; then - echo 'text_input requires two args!'; - return 1 - fi - - local -n ptr=$1 || return $? - if [ -z "${ptr}" ]; then - echo "${2}" - read -r ptr || return $? - fi - - # check string length to be greater than zero! - if [ "${#ptr}" -lt 1 ]; then - echo 'text_input must not be empty!'; - return 1; - fi -} - -text_input foo 'Enter something funny:' -echo "Input: ${foo}" - -bar='' -text_input bar 'Enter something even funnier:' -echo "Input: ${bar}"