#!/bin/bash # Whatever comes in on file descriptor (FD) 3 gets redirected to where file # descriptor 1 is pointing. File descriptor 1 points to stdout so when we # output-redirect something into FD 3 it shows up on stdout. We can use this # to produce arbitrary logging output inside a subshell like so: # # function my_func () { # some_command "${1:?}" # >&3 echo 'A log message' # } # # var="$(my_func arg_1)" # # Here "${var}" will only capture the output of some_command "${1:?}". It # will not capture 'echo' which will instead show up on our stdout/FD 1. exec 3>&1 declare this_script_url this_script_url="${SCRIPT_URL:?}" declare zpool_name zfs_arch_dataset_name settings_file zpool_name='zpool' zfs_arch_dataset_name='archlinux' settings_file='archzbm_settings.env' declare -A partition_types partition_types[gpt_zfs]='6a85cf4d-1dd2-11b2-99a6-080020736631' partition_types[gpt_efi]='c12a7328-f81f-11d2-ba4b-00a0c93ec93b' partition_types[mbr_zfs]='0xbf' partition_types[mbr_boot]='0x83' # https://unix.stackexchange.com/a/48550 set -E trap '[ "$?" -ne 77 ] || exit 77' ERR declare zpool_drive efi_drive boot_drive part_schema function calculate_prefix_from_netmask () { # https://stackoverflow.com/a/50419919 c='0' x='0'"$(printf -- '%o' ${1//./ })" while [ "${x}" -gt '0' ]; do # Modulo then bitwise-shift x and store as new x let c+="$(( x % 2 ))" 'x>>=1' done printf -- '%s' '/'"${c}"; } function setup_env_vars () { printf -- '%s\n' \ 'We will go over a series of questions to create an answer file with' \ 'options you want the script to use.' \ '' \ 'Current working directory is:'\ "$(pwd)" \ '' \ 'After we'"'"'re done answer file will be written to current working dir:' \ './'"${settings_file}" \ '' \ 'Press +C to abort this process. No answer file will' \ 'be written to ./'"${settings_file}"' if you abort the script.' \ '' \ 'When done rerun the same command you just did without '"'"'setup'"'"' argument.' \ '' read -u3 -n 1 -s -r -p "Press any key to begin questionnaire" echo echo '----------------------------------------' echo read -u3 -p 'Please type kernel version to use, leave empty for latest, confirm with : ' ARCHZBM_KERNEL_VER echo echo 'Do you want compressed datasets?' select arg_compressed in 'Compressed' 'Uncompressed'; do case "${arg_compressed}" in 'Compressed') break ;; 'Uncompressed') ARCHZBM_ZFSPROPS_NO_COMPRESSION='true' break ;; esac done <&3 && echo echo 'Do you want encrypted datasets?' select arg_encrypted in 'Encrypted' 'Unencrypted'; do case "${arg_encrypted}" in 'Encrypted') break ;; 'Unencrypted') ARCHZBM_ZFSPROPS_NO_ENCRYPTION='true' break ;; esac done <&3 && echo if [[ "${arg_encrypted}" = 'Encrypted' ]]; then echo 'Do you want a custom dataset decryption password?' select arg_custom_dataset_pw in 'Yes' 'No (use '"'"'password'"'"')'; do case "${arg_custom_dataset_pw}" in 'Yes') want_custom_dataset_pw='true' break ;; 'No (use '"'"'password'"'"')') break ;; esac done <&3 && echo if [[ "${want_custom_dataset_pw}" ]]; then read -u3 -p 'Please type password, confirm with : ' -s ARCHZBM_ZPOOL_PASSWORD echo echo fi fi echo 'Do you want a custom '"'"'root'"'"' user password?' select arg_custom_root_pw in 'Yes' 'No (use '"'"'password'"'"')'; do case "${arg_custom_root_pw}" in 'Yes') want_custom_root_pw='true' break ;; 'No (use '"'"'password'"'"')') break ;; esac done <&3 && echo if [[ "${want_custom_root_pw}" ]]; then read -u3 -p 'Please type password, confirm with : ' -s ARCHZBM_ROOT_PASSWORD echo echo fi echo 'Do you want an SSH daemon in ZFSBootMenu?' select arg_ssh_in_zbm in "Yes" "No"; do case "${arg_ssh_in_zbm}" in Yes) want_ssh_in_zbm='true' break ;; No) break ;; esac done <&3 && echo if [[ "${want_ssh_in_zbm}" ]]; then echo 'How do you want to assign an IP address in ZFSBootMenu?' select arg_ip_autoconf_method in 'Statically' 'Dynamically, DHCP' 'Dynamically, BOOTP' 'Dynamically, RARP'; do case "${arg_ip_autoconf_method}" in 'Statically') ARCHZBM_NET_AUTOCONF='none' break ;; 'Dynamically, DHCP') ARCHZBM_NET_AUTOCONF='dhcp' break ;; 'Dynamically, BOOTP') ARCHZBM_NET_AUTOCONF='bootp' break ;; 'Dynamically, RARP') ARCHZBM_NET_AUTOCONF='rarp' break ;; esac done <&3 && echo read -u3 -p 'Which device to configure (eth0 is a good guess): ' ARCHZBM_NET_DEVICE echo if [[ "${arg_ip_autoconf_method}" = 'Statically' ]]; then read -u3 -p 'Interface IP address: ' ARCHZBM_NET_CLIENT_IP echo read -u3 -p 'Netmask: ' ARCHZBM_NET_NETMASK echo read -u3 -p 'Gateway IP address: ' ARCHZBM_NET_GATEWAY_IP echo fi echo 'Do you want a custom SSH listening port?' select arg_custom_ssh_port in 'Yes (let me specify)' 'No (keep port 22)'; do case "${arg_custom_ssh_port}" in 'Yes (let me specify)') want_custom_ssh_port='true' break ;; 'No (keep port 22)') break ;; esac done <&3 && echo if [[ "${want_custom_ssh_port}" ]]; then read -u3 -p 'Please type SSH port, confirm with : ' ARCHZBM_SSH_PORT echo fi echo 'Do you want the SSH daemon to use a custom keepalive send interval?' select arg_custom_ssh_keepalive_intvl in 'Yes (let me specify)' 'No (keep 1)'; do case "${arg_custom_ssh_keepalive_intvl}" in 'Yes (let me specify)') want_custom_keepalive_intvl='true' break ;; 'No (keep 1)') break ;; esac done <&3 && echo if [[ "${want_custom_keepalive_intvl}" ]]; then read -u3 -p 'Please type server keepalive send interval, confirm with : ' ARCHZBM_SSH_KEEPALIVE_INTVL echo fi read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with : ' ARCHZBM_SSH_AUTH_KEYS echo fi if [[ "${want_ssh_in_zbm}" ]]; then echo 'Do you want to define operating system'"'"'s IP address?' select arg_os_ip in 'Yes (let me specify)' 'Yes (use ZBM addresses)' 'No (DHCP is fine)'; do case "${arg_os_ip}" in 'Yes (let me specify)') want_custom_ip_in_os='true' want_dns_and_ntp='true' break ;; 'Yes (use ZBM addresses)') ARCHZBM_OS_CLIENT_IP="${ARCHZBM_NET_CLIENT_IP}" ARCHZBM_NET_CLIENT_IP_PREFIX="$(calculate_prefix_from_netmask "${ARCHZBM_NET_NETMASK}")" ARCHZBM_OS_CLIENT_IP+="${ARCHZBM_NET_CLIENT_IP_PREFIX}" ARCHZBM_OS_GATEWAY_IP="${ARCHZBM_NET_GATEWAY_IP}" want_dns_and_ntp='true' break ;; 'No (DHCP is fine)') break ;; esac done <&3 && echo if [[ "${want_custom_ip_in_os}" ]]; then read -u3 -p 'Interface IP address with CIDR prefix (a.b.c.d/nn): ' ARCHZBM_OS_CLIENT_IP echo read -u3 -p 'Gateway IP address: ' ARCHZBM_OS_GATEWAY_IP echo fi else echo 'Do you want to define operating system'"'"'s IP address?' select arg_os_ip in 'Yes (let me specify)' 'No (DHCP is fine)'; do case "${arg_os_ip}" in 'Yes (let me specify)') want_own_ip_in_os='true' want_dns_and_ntp='true' break ;; 'No (DHCP is fine)') break ;; esac done <&3 && echo if [[ "${want_own_ip_in_os}" ]]; then read -u3 -p 'Interface IP address with CIDR prefix (a.b.c.d/nn): ' ARCHZBM_OS_CLIENT_IP echo read -u3 -p 'Gateway IP address: ' ARCHZBM_OS_GATEWAY_IP echo fi fi if [[ "${want_ssh_in_zbm}" ]]; then echo 'Do you want to define OS '"'"'root'"'"' user'"'"'s SSH pub key?' select arg_root_pub_keys in 'Yes (let me specify)' 'Yes (use ZBM pub keys)' 'No (don'"'"'t enable sshd.service)'; do case "${arg_root_pub_keys}" in 'Yes (let me specify)') want_custom_pub_keys_in_os='true' break ;; 'Yes (use ZBM pub keys)') ARCHZBM_OS_SSH_AUTH_KEYS="${ARCHZBM_SSH_AUTH_KEYS}" break ;; 'No (don'"'"'t enable sshd.service)') break ;; esac done <&3 && echo if [[ "${want_custom_pub_keys_in_os}" ]]; then read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with : ' ARCHZBM_OS_SSH_AUTH_KEYS echo fi else echo 'Do you want to define OS root user'"'"'s SSH pub key?' select arg_root_pub_keys in 'Yes (let me specify)' 'No (don'"'"'t enable sshd.service)'; do case "${arg_root_pub_keys}" in 'Yes (let me specify)') want_own_pub_key_in_os='true' break ;; 'No (don'"'"'t enable sshd.service)') break ;; esac done <&3 && echo if [[ "${want_own_pub_key_in_os}" ]]; then read -u3 -p 'Please type SSH pub keys on one line separated by double-commas (,,) and confirm with : ' ARCHZBM_OS_SSH_AUTH_KEYS echo fi fi if [[ "${want_dns_and_ntp}" ]]; then read -u3 -p 'Specify one or more comma-separated DNS IPs: ' ARCHZBM_OS_DNS_IP echo echo 'Do you want to override Arch Linux'"'"' NTP servers?' select arg_custom_ntp in 'Yes' 'No'; do case "${arg_custom_ntp}" in 'Yes') want_own_ntp='true' break ;; 'No') break ;; esac done <&3 && echo if [[ "${want_own_ntp}" ]]; then read -u3 -p 'Specify one or more comma-separated NTP hostnames or IPs: ' ARCHZBM_OS_NTP_IP echo fi fi for env_var in 'ARCHZBM_KERNEL_VER' 'ARCHZBM_ZFSPROPS_NO_COMPRESSION' 'ARCHZBM_ZFSPROPS_NO_ENCRYPTION' 'ARCHZBM_ZPOOL_PASSWORD' 'ARCHZBM_ROOT_PASSWORD' 'ARCHZBM_NET_AUTOCONF' 'ARCHZBM_NET_DEVICE' 'ARCHZBM_NET_CLIENT_IP' 'ARCHZBM_NET_NETMASK' 'ARCHZBM_NET_GATEWAY_IP' 'ARCHZBM_SSH_PORT' 'ARCHZBM_SSH_KEEPALIVE_INTVL' 'ARCHZBM_SSH_AUTH_KEYS' 'ARCHZBM_OS_CLIENT_IP' 'ARCHZBM_OS_GATEWAY_IP' 'ARCHZBM_OS_SSH_AUTH_KEYS' 'ARCHZBM_OS_DNS_IP' 'ARCHZBM_OS_NTP_IP'; do if [[ "${!env_var}" ]]; then printf -- '%s='"'"'%s'"'"'\n' \ "${env_var}" "${!env_var}" \ >> "${settings_file}" fi done printf -- '%s\n' \ 'Done, please rerun script now with just' \ '... | bash' \ 'so without the '"'"'setup'"'"' argument' exit 77 } function arg_parse () { [[ "${1}" ]] && while :; do case "${1}" in -[[:alnum:]]*) >&3 printf -- '%s\n' \ 'Short options '"'${1}'"' detected. Only known option is' \ 'literal string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \ 'Exiting ...' exit 77 ;; --*) >&3 printf -- '%s\n' \ 'Long-form option '"'${1}'"' detected. Only known option is' \ 'literal string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \ 'Exiting ...' exit 77 ;; setup) setup_env_vars return ;; *) >&3 printf -- '%s\n' \ 'Argument '"'${1}'"' detected. Only known option is literal' \ 'string '"'"'setup'"'"'. No idea what '"'${1}'"' is.' \ 'Exiting ...' exit 77 ;; esac done } function we_are_changerooted () { if [ "$(stat -c '%d:%i' '/')" != "$(stat -c '%d:%i' '/proc/1/root/.')" ]; then return 0 else return 1 fi } function no_kernel_update_in_iso () { #1.1 sed -ri -e 's'$'\x1''#(IgnorePkg)[^\r\n\f]+'$'\x1''\1 = linux linux-headers'$'\x1''g' /etc/pacman.conf } function set_ntp () { #1.2 timedatectl set-ntp true } function resize_cow_space () { #1.3 mount -o remount,size='50%' /run/archiso/cowspace } function update_pacman_db () { #1.4 pacman_dl_parallel printf -- '%s\n' 'Refreshing mirror list ...' printf -- '%s\n' \ '--save /etc/pacman.d/mirrorlist' \ '--protocol https' \ '--latest 5' \ '--sort age' \ > '/etc/xdg/reflector/reflector.conf' systemctl start reflector # In an ISO and for the minimal number of packages we need we do not # care about partial upgrades # # Are we better off not attempting an upgrade inside the ISO? # Let's try and find out. # while ! pacman -Syyuu --needed --noconfirm --downloadonly; do # sleep 5 # done # pacman -Syyuu --needed --noconfirm pacman -Syy } function install_pkgs () { #1.5 printf -- '%s\n' 'Installing packages ...' while ! pacman -S --needed --noconfirm --downloadonly "${@}"; do sleep 5 done pacman -S --needed --noconfirm "${@}" } function install_zfs () { #1.6 declare reset_colors='\033[0m' if modinfo 'zfs' &>/dev/null; then >&3 printf -- '%s\n' \ 'ZFS kernel module is loaded, no need to install ...' else curl -s 'https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init' | bash fi printf -- "${reset_colors}" } function uefi_or_bios () { #1.7 local part_count_linux part_count_efi # Select disks with at least one partition. Among them count how many # with the given partition type code we have. part_count_linux="$(get_partitions | jq --raw-output '[.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${partition_types[mbr_boot]}"'") | .path] | length')" part_count_efi="$(get_partitions | jq --raw-output '[.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${partition_types[gpt_efi]}"'") | .path] | length')" if [[ "${part_count_linux}" -eq '1' ]] && [[ "${part_count_efi}" -eq '0' ]]; then part_schema='mbr' >&3 printf -- '%s\n' \ 'Treating this as an MBR/legacy BIOS machine ...' elif [[ "${part_count_linux}" -eq '0' ]] && [[ "${part_count_efi}" -eq '1' ]]; then part_schema='gpt' >&3 printf -- '%s\n' \ 'Treating this as a GPT/UEFI machine ...' else >&3 printf -- '%s\n' \ 'We are seeing partitions as follows:' \ '- Partition type code '"${partition_types[mbr_boot]}"': '"${part_count_linux}" \ '- Partition type code '"${partition_types[gpt_efi]}"': '"${part_count_efi}" \ '' \ 'We are instead expecting either 1 and 0 indicating a legacy' \ 'BIOS setup or 0 and 1 indicating UEFI. '"${part_count_linux}"' and '"${part_count_efi}"' is' \ 'neither. Cowardly exiting ...' exit 77 fi export part_schema="${part_schema}" } function get_partitions () { declare partitions_json partitions_json="$(lsblk --output PATH,PARTTYPE --json --tree)" || return 1 printf -- '%s' "${partitions_json}" return 0 } function get_part_parent () { local child_partition parent_partition child_partition="${1:?}" parent_partition="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.children[].path=="'"${child_partition:?}"'") | .path')" printf -- '%s' "${parent_partition}" return 0 } function get_parts () { local zfs_install_drive part_type_code declare parttype parts parttype="${1:?}" zfs_install_drive="${2:-}" case "${parttype}" in zfs) if [[ "${part_schema}" = 'mbr' ]]; then part_type_code="${partition_types[mbr_zfs]}" else part_type_code="${partition_types[gpt_zfs]}" fi parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | .children[] | select(.parttype=="'"${part_type_code}"'") | .path')" || exit 1 ;; boot) parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="'"${partition_types[mbr_boot]}"'") | .path')" || exit 1 ;; efi) parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="'"${partition_types[gpt_efi]}"'") | .path')" || exit 1 ;; *) >&3 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...' exit 77 ;; esac printf -- '%s' "${parts}" return 0 } function we_have_exactly_one_part () { local parttype parts_list parts_count parttype="${1:?}" parts_list="${2:?}" parts_count="$(wc -l <<<"${parts_list}")" if [[ "$(wc -c <<<"${parts_list}")" -gt '1' ]]; then case "${parts_count}" in 0) >&3 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...' exit 77 ;; 1) return 0 ;; *) return 1 ;; esac >&3 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...' exit 77 fi } function get_drive_id () { local drive_id_list drive_id_single drive_id_list="$(find -L /dev/disk/by-partuuid -samefile "${1:?}" | sort)" drive_id_single="$(head -n1 <<<"${drive_id_list}")" if [[ "$(wc -l <<<"${drive_id_single}")" -eq '1' ]] && [[ "$(wc -c <<<"${drive_id_single}")" -gt '1' ]]; then printf -- '%s' "${drive_id_single}" return 0 fi >&3 printf -- '%s\n' 'No '"'${1:?}'"' partition entry in /dev/disk/by-partuuid, exiting ...' exit 77 } function select_part () { local parts enriched_parts enriched_parts_count part_number part_type zfs_install_drive part_type_code specific_part_type declare part part_type="${1:?}" # 'boot' or 'zfs' zfs_install_drive="${2:-}" if [[ "${part_type}" = 'boot' ]]; then if [[ "${part_schema}" = 'mbr' ]]; then specific_part_type='boot' else specific_part_type='efi' fi else specific_part_type="${part_type}" fi if [[ "${zfs_install_drive}" ]]; then # This is intended to find correct boot/EFI partition parts="$(get_parts "${specific_part_type}" "${zfs_install_drive}")" else parts="$(get_parts "${specific_part_type}")" fi if [[ ! "${parts}" ]]; then case "${part_type}" in efi) part_type_human_readable='EFI System Partition (ESP) with partition type code EF00' ;; zfs) if [[ "${part_schema}" = 'mbr' ]]; then part_type_code='bf' else part_type_code='BF00' fi part_type_human_readable='ZFS zpool partition with partition type code '"${part_type_code}" ;; esac >&3 printf -- '%s\n' \ 'It looks as if there is no '"${part_type_human_readable}" \ 'on any of the disks. Did you correctly partition a disk before starting?' \ 'Check https://quico.space/quico-os-setup/arch-zbm#prep. Exiting ...' exit 77 fi if we_have_exactly_one_part "${part_type}" "${parts}"; then part="${parts}" else >&3 printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...' exit 77 fi printf -- '%s' "${part}" return 0 } function no_zpool_exists () { declare zpool_list zpool_list="$(zpool list -H)" [[ "$(wc -l <<<"${zpool_list}")" -le '1' ]] && [[ "$(wc -c <<<"${zpool_list}")" -le '1' ]] && return 0 return 1 } function set_zpool_password () { local zpool_password if [[ "${ARCHZBM_ZPOOL_PASSWORD}" ]]; then zpool_password="${ARCHZBM_ZPOOL_PASSWORD}" else zpool_password='password' fi # May or may not have a newline at the end, ZFS doesn't care printf -- '%s' "${zpool_password}" > '/etc/zfs/'"${zpool_name}"'.key' chmod '000' '/etc/zfs/'"${zpool_name}"'.key' } function import_pool () { zpool import -d '/dev/disk/by-partuuid' -R '/mnt' "${zpool_name}" -N -f [[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && zfs load-key "${zpool_name}" } function create_pool () { # Create a temporary pool that is not cached # # Add zfsprops 'compression' unless environment variable # ARCHZBM_ZFSPROPS_NO_COMPRESSION is set to any value. # # Add zfsprops 'encryption' along with 'keyformat' and a 'keylocation' # unless environment variable ARCHZBM_ZFSPROPS_NO_ENCRYPTION is set to # any value. zpool create -f \ -o 'ashift=12' \ -o 'autotrim=on' \ -O 'acltype=posix' \ $([[ ! "${ARCHZBM_ZFSPROPS_NO_COMPRESSION}" ]] && \ printf -- '%s ' \ '-O compression=on') \ -O 'relatime=on' \ -O 'xattr=sa' \ $([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && \ printf -- '%s ' \ '-O encryption=on' \ '-O keyformat=passphrase' \ '-O keylocation=file:///etc/zfs/'"${zpool_name}"'.key') \ -O 'normalization=formD' \ -O 'mountpoint=none' \ -O 'canmount=off' \ -O 'devices=off' \ -R '/mnt' \ "${zpool_name}" "${1:?}" } function create_root_dataset () { zfs create -o 'mountpoint=none' -o 'canmount=off' "${zpool_name}"'/root' # zfs set org.zfsbootmenu:commandline="ro quiet" "${zpool_name}"'/root' zfs set org.zfsbootmenu:commandline="ro" "${zpool_name}"'/root' } function create_system_dataset () { zfs create -o 'mountpoint=/' -o 'canmount=noauto' "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" zgenhostid zpool set bootfs="${zpool_name}"'/root/'"${zfs_arch_dataset_name}" "${zpool_name}" zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" } function create_home_dataset () { zfs create -o 'mountpoint=/' -o 'canmount=off' "${zpool_name}"'/data' zfs create "${zpool_name}"'/data/home' } function export_pool () { zpool export "${zpool_name}" } function load_settings_file () { #1.8 local working_dir settings_abs working_dir="$(pwd)" settings_abs="${working_dir}"'/'"${settings_file}" if [[ -r "${settings_abs}" ]]; then set -a source "${settings_abs}" set +a fi } function setup_zpool () { #1.9 local drive_by_id zpool_drive="$(select_part 'zfs')" drive_by_id="$(get_drive_id "${zpool_drive}")" [[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && set_zpool_password if no_zpool_exists; then create_pool "${drive_by_id}" create_root_dataset create_system_dataset create_home_dataset export_pool import_pool else >&3 printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...' exit 1 fi } function mount_system () { #1.10 zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" zfs mount -a local zfs_parent efi_part boot_part zfs_parent="$(get_part_parent "${zpool_drive:?}")" if [[ "${part_schema}" = 'mbr' ]]; then boot_drive="${zfs_parent}" export boot_drive="${boot_drive}" boot_part="$(select_part 'boot' "${zfs_parent:?}")" mkdir -p '/mnt/boot/syslinux' mount "${boot_part}" '/mnt/boot/syslinux' else efi_drive="${zfs_parent}" export efi_drive="${efi_drive}" efi_part="$(select_part 'boot' "${zfs_parent:?}")" mkdir -p '/mnt/efi' mount "${efi_part}" '/mnt/efi' fi } function copy_zpool_cache () { #1.11 mkdir -p '/mnt/etc/zfs' zpool set 'cachefile=/etc/zfs/'"${zpool_name}"'.cache' "${zpool_name}" } function pacman_dont_check_space () { # See pacman bug comment # https://bugs.archlinux.org/task/45070#comment142712 # # When we pacstrap onto ZFS pacman incorrectly calculates and # overestimates required disk space. We instead assume an installation # gets done with at least a 10 GiB drive which is plenty. Skip pacman's # size check. # # We're setting this in Arch Linux ISO CD while we install proper Arch. # No need to revert this later as it is ephemeral anyway. sed -ri -e 's'$'\x1''^.*?(CheckSpace)([^\r\n\f]*)'$'\x1''#\1\2'$'\x1''g' '/etc/pacman.conf' } function install_archlinux () { #1.12 pacman_dl_parallel pacman_dont_check_space while ! pacstrap /mnt \ base \ base-devel \ linux \ linux-headers \ linux-firmware \ amd-ucode \ efibootmgr \ vim \ git \ iwd \ networkmanager \ network-manager-applet \ dialog \ os-prober \ reflector \ bluez \ bluez-utils \ man-db \ xdg-utils \ xdg-user-dirs; do sleep 5 done } function gen_fstab () { #1.13 genfstab -U /mnt | grep -v "${zpool_name}" | tr -s '\n' | sed -r -e 's/\/mnt//' -e '/./,$!d' > '/mnt/etc/fstab' } function configure_hosts_file () { cat > '/mnt/etc/hosts' < 127.0.0.1 localhost ${1} ::1 localhost ${1} EOF } function set_hostname () { #1.14 declare new_hostname install_pkgs 'pwgen' new_hostname="$(pwgen --no-numerals --no-capitalize --ambiguous 8)" printf -- '%s\n' "${new_hostname}" > '/mnt/etc/hostname' configure_hosts_file "${new_hostname}" } function set_locale () { #1.15 printf -- '%s\n' \ 'KEYMAP=de-latin1' \ 'FONT=Lat2-Terminus16' \ 'FONT_MAP=8859-1' \ > '/mnt/etc/vconsole.conf' sed -ri -e 's'$'\x1''^(#)(en_US.UTF-8[^\r\n\f]*)'$'\x1''\2'$'\x1''g' '/mnt/etc/locale.gen' printf -- '%s\n' 'LANG=en_US.UTF-8' > '/mnt/etc/locale.conf' } function in_file_in_array_insert_n_before_m () { local arg_file arg_array arg_string arg_precede arg_file="${1:?}" arg_array="${2:?}" arg_string="${3:?}" arg_precede="${4:?}" # Look for a line that contains in this order # - String "${arg_array}" and a equals sign (=), assign capture group \1 # - Followed by as few characters as possible followed by either an # opening parenthesis or a space character, assign capture group \2 # - String "${arg_precede}", capture as capture group \3 # - Followed by either a closing parenthesis or a space which are then # followed by as many non-line break characters as possible, capture # as capture group \2 # # For following example text we're assuming that: # - "${arg_array}" equals 'HOOKS' # - "${arg_precede}" equals 'filesystems' # - "${arg_string}" equals 'zfs' # # This finds a 'HOOKS=' array definition that contains the string # 'filesystems' either at the beginning of the 'HOOKS=(...)' opening # parenthesis, at the very end or somewhere in the middle where it may # be preceded or followed by one or more space characters. It saves # 'HOOKS=', it saves whatever precedes 'filesystems' and 'filesystems' # itself plus whatever comes after 'filesystems' until end of line. It # lastly inserts 'zfs' and a space character right in front of # 'filesystems'. sed -ri \ -e 's'$'\x1''('"${arg_array}"'=)(.*?[( ])('"${arg_precede}"')([) ][^\r\n\f]*)'$'\x1''\1\2'"${arg_string}"' \3\4'$'\x1''g' \ "${arg_file}" } function in_file_in_array_insert_n_at_the_end () { local arg_file arg_array arg_string arg_file="${1:?}" arg_array="${2:?}" arg_string="${3:?}" # Look for end of array, insert "${arg_string}" right before # # For following example text we're assuming that: # - "${arg_array}" equals 'HOOKS' # - "${arg_string}" equals 'net' # # This: # - Finds a 'HOOKS=' array definition, saves it as \1 # - Finds as many non-closing parenthesis characters as possible, so all # characters until just before the final ')' character of the line and # saves this as \2 # - Finds the closing parenthesis character plus all non-line break # characters until end of line that come after it and saves this as \3 # - Adds a space character and 'net' after \2 sed -ri \ -e 's'$'\x1''('"${arg_array}"'=)([^)]*)(\)[^\r\n\f]*)'$'\x1''\1\2 '"${arg_string}"'\3'$'\x1''g' \ "${arg_file}" } function in_file_in_array_remove_n () { local arg_file arg_array arg_string arg_file="${1:?}" arg_array="${2:?}" arg_string="${3:?}" # Look for any line that contains "${arg_string}", delete that string # # For following example text we're assuming that: # - "${arg_array}" equals 'HOOKS' # - "${arg_string}" equals 'fsck' # # First -e expression removes 'fsck' wherever it is defined as the one # and only element of any HOOKS=(fsck) should such a line exist. # # Second -e expression finds string 'fsck' where it's preceded by space # character(s) and followed by either space character(s) or a closing # parenthesis. # # Third -e expression finds string 'fsck' where it's preceded by space # character(s) or an opening parenthesis and followed space # character(s). sed -ri \ -e 's'$'\x1''((\()('"${arg_string}"')(\)))'$'\x1''\2\4'$'\x1''g' \ -e 's'$'\x1''('"${arg_array}"'=.*?)([[:space:]]+'"${arg_string}"')([[:space:]]+|\))'$'\x1''\1\3'$'\x1''g' \ -e 's'$'\x1''('"${arg_array}"'=.*?)([[:space:]]+|\()('"${arg_string}"'[[:space:]]+)'$'\x1''\1\2'$'\x1''g' \ "${arg_file}" } function add_zfs_hook_to_initramfs () { #1.16 # Add zfs hook, remove fsck hook from initramfs. in_file_in_array_insert_n_before_m '/mnt/etc/mkinitcpio.conf' 'HOOKS' 'zfs' 'filesystems' in_file_in_array_remove_n '/mnt/etc/mkinitcpio.conf' 'HOOKS' 'fsck' # Also unless encryption's unwanted add plain text key file into # initramfs since it's living inside an encrypted pool anyway. [[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && sed -ri \ -e 's'$'\x1''^(FILES=)[^\r\n\f]*'$'\x1''\1(/etc/zfs/'"${zpool_name}"'.key)'$'\x1''g' \ '/mnt/etc/mkinitcpio.conf' } function set_initramfs_build_list () { #1.17 # No need to build fallback initramfs, our new fallback is ZFS snapshots sed -ri \ -e '/^#/d' \ -e '/^$/d' \ -e '/^fallback/d' \ -e 's'$'\x1''^(PRESETS=)[^\r\n\f]*'$'\x1''\1('"'"'default'"'"')'$'\x1''g' \ '/mnt/etc/mkinitcpio.d/linux.preset' # Remove any existing fallback initramfs files find '/mnt/boot' -type f -regextype posix-extended -iregex '^/mnt/boot/initramfs-.*?-fallback.img' -delete } function add_zfs_files_to_new_os () { #1.18 for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' $([[ ! "${ARCHZBM_ZFSPROPS_NO_ENCRYPTION}" ]] && printf -- '%s' '/etc/zfs/'"${zpool_name}"'.key'); do rsync -av --itemize-changes {'','/mnt'}"${zfs_file}" done } function create_unpriv_user () { account_name="${1:?}" full_name="${2:-${account_name}}" authorized_keys_abs_path='/home/'"${account_name}"'/.ssh/authorized_keys' useradd --create-home --shell '/bin/bash' --comment 'Personal account of '"${full_name}" --user-group "${account_name}" chfn --full-name "${full_name}" "${account_name}" mkdir -p "$(dirname "${authorized_keys_abs_path}")" touch "${authorized_keys_abs_path}" chown -R "${account_name}"':' '/home/'"${account_name}"; chmod -R 'u=rwX,go=' "$(dirname "${authorized_keys_abs_path}")" } function enter_chroot () { #2.1 arch-chroot /mnt /bin/bash -xe < "${path_unit}" <<"EOF" [Unit] Description=chmod initramfs to be root-read-writable only [Path] PathChanged=/boot/initramfs-linux.img [Install] WantedBy=multi-user.target WantedBy=system-update.target EOF cat > "${service_unit}" <<"EOF" [Unit] Description=chmod initramfs to be root-read-writable only [Service] Type=oneshot ExecStart=/usr/bin/chmod 600 /boot/initramfs-linux.img EOF systemctl enable "${path_unit}" } function pacman_dl_parallel () { #2.4 # We're setting this in Arch Linux ISO CD while we install proper Arch. # No need to revert this later as it is ephemeral anyway. sed -ri -e 's'$'\x1''^.*?(ParallelDownloads)[^\r\n\f]*'$'\x1''\1 = 20'$'\x1''g' '/etc/pacman.conf' } function unleash_makepkg () { #2.5 local path_prefix path_prefix='/mnt' if we_are_changerooted; then path_prefix='' fi sed -ri \ -e 's'$'\x1''^(#?(MAKEFLAGS=))[^\r\n\f]*'$'\x1''\2"-j$(nproc --ignore 1)"'$'\x1''g' \ "${path_prefix}"'/etc/makepkg.conf' } function add_motd_getting_started_msg () { #2.6 cat > '/etc/motd' <<"EOF" #################### GUI basics: paru -S xorg plasma-meta kde-applications-meta sddm localectl set-x11-keymap de useradd --create-home --shell /bin/bash --user-group --groups wheel passwd systemctl enable --now sddm.service #################### EOF } function get_aur_helper () { #2.7 create_unpriv_user 'build' usermod --append --groups 'wheel' 'build' printf -- '%s\n' '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > '/etc/sudoers.d/10-wheel-group-no-passwd-prompt' pushd /tmp git clone https://aur.archlinux.org/paru-bin.git chown -R 'build:' 'paru-bin' pushd 'paru-bin' sudo --user 'build' makepkg -si --noconfirm popd rm -rf 'paru-bin' popd } function paru_install () { sudo --user build paru -S --noconfirm "${@}" } function configure_syslinux () { paru_install 'syslinux' cp /usr/lib/syslinux/bios/*.c32 /boot/syslinux extlinux --install /boot/syslinux cat > /boot/syslinux/syslinux.cfg < '/etc/zfsbootmenu/config.yaml' < '/etc/zfsbootmenu/config.yaml' <&3 printf -- '%s\n' \ 'No .Kernel.CommandLine YAML element with content found in '"${zbm_config}"'. Exiting ...' exit 77 else kcl_string="$(yq '.Kernel.CommandLine' "${zbm_config}")" fi mapfile -t kcl < <(<<<"${kcl_string}" tr ' ' '\n' | sed '/^$/d') for kcl_elem in "${!kcl[@]}"; do if grep -Piq -- 'ip=' <<<"${kcl[$kcl_elem]}"; then ip_addr_found='true' kcl["${kcl_elem}"]="${default_ip}" fi done if [[ ! "${ip_addr_found}" ]]; then kcl+=("${default_ip}") fi new_kcl='' first_kcl_elem='true' for kcl_elem in "${kcl[@]}"; do if [[ ! "${first_kcl_elem}" ]]; then new_kcl+=' '"${kcl_elem}" else new_kcl+="${kcl_elem}" unset -v first_kcl_elem fi done yq -i '.Kernel.CommandLine = "'"${new_kcl}"'"' "${zbm_config}" else local zbm_config zbm_config='/boot/syslinux/syslinux.cfg' set_unique_ip_in_syslinux_kcl "${zbm_config}" "${default_ip}" fi } function set_pub_keys () { local authorized_keys_file raw_pub_keys authorized_keys_file="${1:?}" raw_pub_keys="${2:?}" :> "${authorized_keys_file}" while IFS= read -r pub_key_line; do printf -- '%s\n' "${pub_key_line}" >> "${authorized_keys_file}" done < <(<<<"${raw_pub_keys}" sed -r -e 's/,,/\n/g') sed -i '/^$/d' "${authorized_keys_file}" } function we_want_ssh () { #2.10 if [[ "${ARCHZBM_NET_CLIENT_IP}" ]] || \ [[ "${ARCHZBM_NET_SERVER_IP}" ]] || \ [[ "${ARCHZBM_NET_GATEWAY_IP}" ]] || \ [[ "${ARCHZBM_NET_NETMASK}" ]] || \ [[ "${ARCHZBM_NET_HOSTNAME}" ]] || \ [[ "${ARCHZBM_NET_DEVICE}" ]] || \ [[ "${ARCHZBM_NET_AUTOCONF}" ]] || \ [[ "${ARCHZBM_SSH_PORT}" ]] || \ [[ "${ARCHZBM_SSH_KEEPALIVE_INTVL}" ]] || \ [[ "${ARCHZBM_SSH_AUTH_KEYS}" ]]; then >&3 printf -- '%s\n' 'Installing SSH in ZFSBootMenu' return 0 fi >&3 printf -- '%s\n' 'Not installing SSH in ZFSBootMenu' return 1 } function configure_ssh_in_zbm () { #2.11 get_dropbear_hooks customize_dropbear_hooks paru_install 'mkinitcpio-nfs-utils' 'dropbear' 'mkinitcpio-dropbear' in_file_in_array_insert_n_at_the_end '/etc/zfsbootmenu/mkinitcpio.conf' 'HOOKS' 'net' in_file_in_array_insert_n_at_the_end '/etc/zfsbootmenu/mkinitcpio.conf' 'HOOKS' 'dropbear' for key_type in 'dss' 'ecdsa' 'ed25519' 'rsa'; do dropbearkey -t "${key_type}" -f '/etc/dropbear/dropbear_'"${key_type}"'_host_key' done set_pub_keys '/etc/dropbear/root_key' "${ARCHZBM_SSH_AUTH_KEYS}" ensure_ip_in_kcl } function add_syslinux_pacman_hook () { mkdir -p '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main' git -C '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/zbm-syslinux-pacman-hook.git' . chmod +x '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.sh' ln -s '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.sh' '/usr/local/bin/pacman-zbm-syslinux-regen' ln -s '/opt/git/quico.space/quico-os-setup/zbm-syslinux-pacman-hook/branches/main/pacman-zbm-syslinux-regen.hook' '/usr/share/libalpm/hooks/pacman-zbm-syslinux-regen.hook' } function add_zbm_pacman_hook () { mkdir -p '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main' git -C '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main' clone 'https://quico.space/quico-os-setup/zbm-regen-pacman-hook.git' . ln -s '/opt/git/quico.space/quico-os-setup/zbm-regen-pacman-hook/branches/main/pacman-zbm-image-regen.hook' '/usr/share/libalpm/hooks/pacman-zbm-image-regen.hook' } function get_disks_with_one_efipart () { local disks_with_one_efipart # Find disks that have exactly one EFI partition and where that EFI # partition is partition number 1. We expect exactly one disk to meet # these criteria. Anything else and we bail. disks_with_one_efipart="$(lsblk --output PATH,PARTTYPE --json --tree | jq --raw-output '.[][] | select(.children | length > 0) | select( any (.children[]; (.path | test("^[^[:digit:]]+1")) and (.parttype=="'"${partition_types[gpt_efi]}"'")) and ([select(.children[].parttype=="'"${partition_types[gpt_efi]}"'")] | length == 1) ) | .path')" if [[ "$(wc -l <<<"${disks_with_one_efipart}")" -eq '1' ]] && [[ "$(wc -c <<<"${disks_with_one_efipart}")" -gt '1' ]]; then printf -- '%s' "${disks_with_one_efipart}" return 0 fi return 1 } function install_os_in_chroot () { #2.2 dd if='/dev/zero' of='/swapfile' bs='1M' count='2048' losetup '/dev/loop9' '/swapfile' mkswap '/dev/loop9' swapon '/dev/loop9' ### Reinit keyring # As keyring is initialized at boot, and copied to the install dir with pacstrap, and ntp is running # Time changed after keyring initialization, it leads to malfunction # Keyring needs to be reinitialised properly to be able to sign keys. rm -rf '/etc/pacman.d/gnupg' pacman-key --init pacman-key --populate archlinux while ! pacman -S archlinux-keyring --noconfirm --downloadonly; do sleep 5 done pacman -S archlinux-keyring --noconfirm locale-gen source /etc/locale.conf keep_initiramfs_root_only_rw #2.3 pacman_dl_parallel #2.4 unleash_makepkg #2.5 add_motd_getting_started_msg #2.6 get_aur_helper #2.7 if [[ "${ARCHZBM_KERNEL_VER}" ]]; then paru_install 'downgrade' yes | downgrade --ala-only \ 'linux='"${ARCHZBM_KERNEL_VER}" \ 'linux-headers='"${ARCHZBM_KERNEL_VER}" \ --ignore always fi paru_install 'zfs-dkms' 'zfs-utils' 'jq' hwclock --systohc mkinitcpio -P # Install ZFSBootMenu image configure_zfsbootmenu #2.9 if we_want_ssh; then #2.10 configure_ssh_in_zbm #2.11 fi generate-zbm # Yes, we do this twice so we immediately get a functional backup file generate-zbm if [[ "${part_schema}" = 'mbr' ]]; then paru_install 'rsync' add_syslinux_pacman_hook fi add_zbm_pacman_hook swapoff '/dev/loop9' losetup -d '/dev/loop9' rm '/swapfile' } function set_root_pw () { #3.2 local root_password if [[ "${ARCHZBM_ROOT_PASSWORD}" ]]; then root_password="${ARCHZBM_ROOT_PASSWORD}" else root_password='password' fi printf -- '%s\n' 'root:'"${root_password}" | chpasswd --crypt-method 'SHA512' --root '/mnt' } function configure_networking () { #3.3 local -a dns_addresses ntp_addresses # Begin network unit file with a default top section cat > '/mnt/etc/systemd/network/50-wired.network' <<"EOF" [Match] Name=en* [Network] EOF # Decide on what comes next in network unit file if [[ "${ARCHZBM_OS_CLIENT_IP}" ]] || \ [[ "${ARCHZBM_OS_GATEWAY_IP}" ]] || \ [[ "${ARCHZBM_OS_DNS_IP}" ]] || \ [[ "${ARCHZBM_OS_NTP_IP}" ]]; then cat >> '/mnt/etc/systemd/network/50-wired.network' <> '/mnt/etc/systemd/network/50-wired.network' <> '/mnt/etc/systemd/network/50-wired.network' <> '/mnt/etc/systemd/network/50-wired.network' <<"EOF" IPForward=yes Domains=~. EOF else cat >> '/mnt/etc/systemd/network/50-wired.network' <<"EOF" DHCP=ipv4 IPForward=yes [DHCP] UseDNS=yes RouteMetric=10 EOF fi systemctl enable 'systemd-networkd' --root='/mnt' systemctl disable 'systemd-networkd-wait-online' --root='/mnt' } function configure_sshd () { #3.4 local pub_key_line cat >> '/mnt/etc/ssh/sshd_config.d/40-defaults.conf' <<"EOF" PasswordAuthentication no PermitRootLogin yes EOF while IFS= read -r pub_key_line; do printf -- '%s\n' "${pub_key_line}" >> '/mnt/root/.ssh/authorized_keys' done < <(<<<"${ARCHZBM_OS_SSH_AUTH_KEYS}" sed -r -e 's/,,/\n/g') systemctl enable 'sshd.service' --root='/mnt' } function configure_dns () { #3.5 rm '/mnt/etc/resolv.conf' ln -s '/run/systemd/resolve/stub-resolv.conf' '/mnt/etc/resolv.conf' # Optionally you may want /etc/systemd/network/50-wired.network to use # UseDNS=no and hardcode DNS server(s) here: # sed -i 's/^#DNS=.*/DNS=1.1.1.1/' /mnt/etc/systemd/resolved.conf systemctl enable 'systemd-resolved.service' --root='/mnt' } function configure_ntp () { #3.6 systemctl enable 'systemd-timesyncd.service' --root='/mnt' } function configure_reflector () { #3.7 systemctl enable 'reflector.service' 'reflector.timer' --root='/mnt' } function configure_zfs () { #3.8 systemctl enable 'zfs-import-cache.service' 'zfs-mount.service' 'zfs-import.target' 'zfs.target' --root='/mnt' } function configure_zfs_mount_gen () { #3.9 mkdir -p '/mnt/etc/zfs/zfs-list.cache' touch '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}" zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand | sed 's/\/mnt//' > '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}" systemctl enable 'zfs-zed.service' --root='/mnt' } function set_new_uefi_boot_entries () { #3.10 declare -a uefi_images mapfile -t uefi_images < \ <(find '/mnt/efi/EFI/ZBM' -type f -iname '*.efi' -print0 | \ xargs -0 --no-run-if-empty --max-args '1' stat -c '%Y %n' | \ sort -V | \ awk '{print $2}') if efibootmgr | grep -Piq -- 'ZFSBootMenu'; then local -a old_uefi_entries mapfile -t old_uefi_entries < \ <(efibootmgr | \ grep -Pio -- '(?<=^Boot)[^\*[:space:]]+(?=\*? ZFSBootMenu)') for old_uefi_entry in "${old_uefi_entries[@]}"; do efibootmgr --bootnum "${old_uefi_entry}" --delete-bootnum &>/dev/null && { >&3 printf -- '%s\n' \ 'EFI boot entry '"${old_uefi_entry}"' deleted.' } done fi if ! efibootmgr | grep -Piq -- 'ZFSBootMenu'; then local efi_disks_list efi_disks_list="$(get_disks_with_one_efipart)" if grep -Piq -- '^'"${efi_drive}"'$' <<<"${efi_disks_list}"; then for uefi_image in "${uefi_images[@]}"; do uefi_image_version="$(basename "${uefi_image%%.EFI}")" uefi_image_inverted="${uefi_image#/mnt/efi}" uefi_image_inverted="${uefi_image_inverted//\//\\}" efibootmgr --disk "${efi_drive}" \ --part 1 \ --create \ --label 'ZFSBootMenu '"${uefi_image_version}" \ --loader "${uefi_image_inverted}" &>/dev/null && { >&3 printf -- '%s\n' \ 'EFI boot entry ZFSBootMenu '"${uefi_image_version}"' added.' } done fi fi } function umount_all () { #3.11 if [[ "${part_schema}" = 'mbr' ]]; then umount '/mnt/boot/syslinux' else umount '/mnt/efi' fi zfs umount -a zpool export "${zpool_name}" } function finalize_os_setup () { #3.1 set_root_pw #3.2 configure_networking #3.3 if [[ "${ARCHZBM_OS_SSH_AUTH_KEYS}" ]]; then configure_sshd #3.4 fi configure_dns #3.5 configure_ntp #3.6 configure_reflector #3.7 configure_zfs #3.8 configure_zfs_mount_gen #3.9 if [[ "${part_schema}" = 'gpt' ]]; then set_new_uefi_boot_entries #3.10 fi umount_all #3.11 } function main () { if [[ "${#@}" -gt '0' ]]; then arg_parse "${@}" fi if we_are_changerooted; then install_os_in_chroot #2.2 else no_kernel_update_in_iso #1.1 set_ntp #1.2 resize_cow_space #1.3 update_pacman_db #1.4 install_pkgs 'jq' #1.5 install_zfs #1.6 uefi_or_bios #1.7 load_settings_file #1.8 setup_zpool #1.9 mount_system #1.10 copy_zpool_cache #1.11 install_archlinux #1.12 gen_fstab #1.13 set_hostname #1.14 set_locale #1.15 add_zfs_hook_to_initramfs #1.16 set_initramfs_build_list #1.17 add_zfs_files_to_new_os #1.18 enter_chroot #2.1 # We're done in chroot finalize_os_setup #3.1 fi } main "${@}"