diff --git a/zbm_set_new_uefi_boot_entries.sh b/zbm_set_new_uefi_boot_entries.sh new file mode 100644 index 0000000..bbf27ed --- /dev/null +++ b/zbm_set_new_uefi_boot_entries.sh @@ -0,0 +1,177 @@ +#!/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 + +# https://unix.stackexchange.com/a/48550 +set -E +trap '[ "$?" -ne 77 ] || exit 77' ERR + +function get_partitions () { + declare partitions_json + partitions_json="$(lsblk --output PATH,PARTTYPE --json --tree)" || return 1 + printf -- '%s' "${partitions_json}" + return 0 +} + +function get_parts () { + local zfs_install_drive + declare parttype parts + parttype="${1:?}" + zfs_install_drive="${2:-}" + case "${parttype}" in + zfs) + parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | .children[] | select(.parttype=="6a85cf4d-1dd2-11b2-99a6-080020736631") | .path')" || exit 1 + ;; + efi) + parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b") | .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 select_part () { + local parts enriched_parts enriched_parts_count part_number part_type zfs_install_drive + declare part + part_type="${1:?}" # 'efi' or 'zfs' + zfs_install_drive="${2:-}" + if [[ "${zfs_install_drive}" ]]; then + # This is intended to find correct EFI partition + parts="$(get_parts "${part_type}" "${zfs_install_drive}")" + else + parts="$(get_parts "${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) + part_type_human_readable='ZFS zpool partition with partition type code BF00' + ;; + 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 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_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=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")) and ([select(.children[].parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b")] | 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 set_new_uefi_boot_entries () { + declare -a uefi_images + mapfile -t uefi_images < \ + <(find '/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}') + zpool_drive="$(select_part 'zfs')" + zfs_parent="$(get_part_parent "${zpool_drive:?}")" + efi_drive="${zfs_parent}" + + 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##*-}")" + uefi_image_version="${uefi_image_version%%.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 +}