#!/bin/bash set -e [ -n "$DEBUG" ] && set -x . /lib/gentoo/functions.sh EFI_PARTITION=/boot/efi EFI_DIR=/efi/gentoo _() { if [ "$NOOP" ]; then echo "+" "$*" else echo "+" "$*" "$@" fi } mount_efi() { mount "${EFI_PARTITION}" || true mount -o remount,rw /sys/firmware/efi/efivars || true trap '{ umount ${EFI_PARTITION}; mount -o remount,ro /sys/firmware/efi/efivars; }' EXIT } path_from_efi() { sed -e 's,\\,/,g' -e "s,^,${EFI_PARTITION}," } path_to_efi() { sed -e "s,${EFI_PARTITION},," -e 's,/,\\,g' } path_to_boot() { sed -e "s,${EFI_PARTITION}${EFI_DIR},/boot," } hashsum() { sha1sum | cut -d' ' -f1 } parse_efi() ( local bootorder bootcurrent bootorder=$(efibootmgr -u | grep '^BootOrder' | sed -e 's,BootOrder: ,,' -e 's/,/\n/g' | awk '{print $0 "\t" NR}') bootcurrent=$(efibootmgr -u | grep '^BootCurrent' | sed -e 's,BootCurrent: ,,') efibootmgr -u | grep -E '^Boot[0-9]' | while read -r ln; do local hash entry loader id hash=$(hashsum <<<"$ln") ln=$(sed -e 's|Boot\([^\*]*\)\*\s\(.*\)\sHD(\([0-9]*\),[^,]*,\([^,]*\),.*)/File(\([^)]*\))\(.*\)$|\1\t\2\t\3\t\4\t\5\t\6|' <<<"$ln") entry=$(cut -d $'\t' -f1 <<<"$ln") loader=$(cut -d $'\t' -f5 <<<"$ln") version=$(sed -e 's,^.*vmlinuz-\(.*\)\.efi$,\1,' <<<"$loader") if [ -f "$(path_from_efi <<<"$loader")" ]; then jq -cn -S \ --arg hash "$hash" \ --arg entry "$entry" \ --arg loader "$loader" \ --arg version "$version" \ --argjson bootorder "$(grep "$entry" <<<"$bootorder" | cut -d $'\t' -f2)" \ --argjson current "$(test "$entry" = "$bootcurrent" && echo true || echo false)" \ --arg disk "$(cut -d $'\t' -f4 <<<"$ln")" \ --arg label "$(cut -d $'\t' -f2 <<<"$ln")" \ --argjson part "$(cut -d $'\t' -f3 <<<"$ln")" \ --arg args "$(cut -d $'\t' -f6 <<<"$ln")" \ '{version:$version,entry:$entry,label:$label,loader:$loader,args:$args,bootorder:$bootorder,current:$current,disk:$disk,part:$part,hash:$hash}' fi done | jq -s 'sort_by(.bootorder)' ) create_efi() ( jq -rc '.[]' | while read -r ln; do local entry hash entry=$(jq -r '.entry' <<<"$ln" || echo none) hash=$(jq -r '.hash' <<<"$ln" || echo none) loader=$(jq -r '.loader' <<<"$ln") bootorder=$(jq -r '.bootorder' <<<"$ln") label=$(jq -r '.label' <<<"$ln") args=$(jq -r '.args' <<<"$ln") disk=$(findmnt -no SOURCE --target "$(path_from_efi <<<"$loader")") part=$(sed -e 's,^.*p\([0-9]\)$,\1,' <<<"$disk") disk=${disk//p${part}/} _ efibootmgr -u --remove-dups --create --disk "$disk" --part "$part" --label "$label" --loader "$loader" -u "$args" --index "$bootorder" done ) delete_efi() ( local entry=$1 _ efibootmgr -u --delete-bootnum --bootnum "$entry" ) bootnext_efi() ( local entry=$1 _ efibootmgr -u --bootnext "$entry" ) activate_efi() ( local entry=$1 _ efibootmgr -u --active --bootnum "$entry" ) deactivate_efi() ( local entry=$1 _ efibootmgr -u --inactive --bootnum "$entry" ) parse_kernels() ( local kernel_dir=${1:-/boot} # test -n "$kernel_dir" || eerror "need kernel directory argument" local current_kernel current_hash current_kernel=$(parse_efi | jq -rc '.[]|select(.current == false)|.loader' | path_from_efi) current_hash=$(hashsum <"$current_kernel") find "$kernel_dir" -maxdepth 1 -type f -name 'vmlinuz*' -print | while read -r ln; do local file mtime ver ext hash file=$ln hash=$(hashsum <"$file") mtime=$(stat -c '%Y' "$file") ln=$(sed -e "s,^${kernel_dir}/\([^-]*\)-\([0-9]*\.[0-9]*\.[0-9]*[^\.]*\)\(.*\)\$,\1\t\2\t\3," <<<"$ln") ver=$(cut -d $'\t' -f2 <<<"$ln") ext=$(cut -d $'\t' -f3 <<<"$ln") jq -cn -S \ --arg file "$file" \ --argjson old "$(grep -q old <<<"$ext" && echo true || echo false)" \ --argjson current "$(test "$hash" = "$current_hash" && echo true || echo false)" \ --arg version "$ver" \ --argjson mtime "$mtime" \ --arg hash "$hash" \ '{file:$file,old:$old,version:$version,mtime:$mtime,hash:$hash,current:$current}' done | jq -s 'sort_by(.mtime)|reverse' ) kernel_to_initramfs() { local file file=$(dirname "$1")/$(basename "$1" | sed -e 's,^vmlinuz,initramfs,') if grep -Eq '\.efi$' <<<"$file"; then sed -e 's,\.efi$,.img,' <<<"$file" else sed -e 's,$,.img,' <<<"$file" fi } install_kernel() { local file=$1 target=${EFI_PARTITION}${EFI_DIR} newfile newfile=$target/$(basename "$file").efi _ cp -vf "$file" "$newfile" >&2 _ cp -vf "$(kernel_to_initramfs "$file")" "$target/" >&2 echo "$newfile" } remove_kernel() { local file=$1 initramfs initramfs=$(kernel_to_initramfs "$file") _ rm -vf "$file" >&2 _ rm -vf "$initramfs" >&2 # _ rm -vf "$(path_to_boot <<<"$file")" >&2 # _ rm -vf "$(path_to_boot <<<"$file").old" >&2 # # _ rm -vf "$(path_to_boot <<<"$initramfs")" >&2 # _ rm -vf "$(path_to_boot <<<"$initramfs").old" >&2 # # _ rm -vf "$(path_to_boot <<<"$file" | sed -e 's,vmlinuz,System.map,')" >&2 # _ rm -vf "$(path_to_boot <<<"$file" | sed -e 's,vmlinuz,System.map,').old" >&2 # # _ rm -vf "$(path_to_boot <<<"$file" | sed -e 's,vmlinuz,config,')" >&2 # _ rm -vf "$(path_to_boot <<<"$file" | sed -e 's,vmlinuz,config,').old" >&2 } # $1 : new kernel version, e.g. 6.1.27-gentoo-dist # $2 : new kernel path, e.g. /boot/vmlinuz-6.1.27-gentoo-dist new_kernel() { local version=$1 kernel=$2 local kernel_dir efi_kernel efi_entries test -f "$kernel" || eerror "kernel needs to be a file" einfo "New kernel version ${version}" kernel_dir=$(dirname "$kernel") efi_kernel=${EFI_PARTITION}${EFI_DIR}/$(basename "$kernel").efi efi_entries=$(parse_efi | jq -rc) efi_entries_dup=$(jq -rc --arg version "$version" '.[]|select(.version==$version)|{entry:.entry,loader:.loader}' <<<"$efi_entries") ebegin "Remove duplicate kernel and boot entry with version $version" for efi in $efi_entries_dup; do delete_efi "$(jq -rc '.entry' <<<"$efi")" remove_kernel "$(jq -rc '.loader' <<<"$efi" | path_from_efi)" done eend # ebegin "Create fallback boot entry" # local bootcurrent_entry bootcurrent # bootcurrent_entry=$(efibootmgr -u | grep '^BootCurrent' | sed -e 's,BootCurrent: ,,') # bootcurrent=$(jq -rc --arg current "$bootcurrent_entry" '.[]|select(.entry == $current)' <<<"$efi_entries") # if [ -f "$(jq -r '.loader' <<<"$bootcurrent" | path_from_efi)" ]; then # if [ "$efi_kernel" = "$(jq -r '.loader' <<<"$bootcurrent" | path_from_efi)" ]; then # efibootmgr -u --bootnum "$bootcurrent_entry" --delete-bootnum # # local bootcurrent_kernel bootcurrent_initramfs # bootcurrent_kernel=$(jq -rc '.loader' <<<"$bootcurrent" | path_from_efi) # bootcurrent_initramfs=$(dirname "$bootcurrent_kernel")/$(basename "$bootcurrent_kernel" | sed -e 's,^vmlinuz,initramfs,' -e 's,efi$,img,') # mv -v "$bootcurrent_kernel" "$bootcurrent_kernel.old" >&2 # mv -v "$bootcurrent_initramfs" "$bootcurrent_initramfs.old" >&2 # # jq -cnS \ # --arg loader "$(path_to_efi <<<"$bootcurrent_kernel")" \ # --argjson bootorder 1 \ # --arg label "Gentoo Linux (old)" \ # --arg args "initrd=$(path_to_efi <<<"$bootcurrent_initramfs")" \ # '[{label:$label,loader:$loader,args:$args,bootorder:$bootorder}]' | # create_efi # else # eerror "can't demote boot entry" # exit 2 # #efibootmgr -u --bootnum "$bootcurrent_entry" --index 2 # fi # else # local oldkernel # oldkernel=$(parse_kernels "/boot" | jq -rc '[.[]|select(.current != true)]|first|.file') # efi_oldkernel=$(install_kernel "$oldkernel") # jq -cnS \ # --arg loader "$(path_to_efi <<<"$efi_oldkernel")" \ # --argjson bootorder 1 \ # --arg label "Gentoo Linux (old)" \ # --arg args "initrd=$(kernel_to_initramfs "$efi_oldkernel" | path_to_efi)" \ # '[{label:$label,loader:$loader,args:$args,bootorder:$bootorder}]' | # create_efi # fi # eend ebegin "Install new kernel" efi_kernel=$(install_kernel "$kernel") if [ ! -e "$efi_kernel" ]; then eerror "Installing kernel to $efi_kernel failed!" fi eend ebegin "Create new boot entry (debug)" jq -cnS \ --arg loader "$(path_to_efi <<<"$efi_kernel")" \ --arg label "Gentoo Linux $version (debug)" \ --arg args "initrd=$(kernel_to_initramfs "$efi_kernel" | path_to_efi) rd.cmdline=ask" \ '[{label:$label,loader:$loader,args:$args}]' | create_efi eend ebegin "Create new boot entry (default)" jq -cnS \ --arg loader "$(path_to_efi <<<"$efi_kernel")" \ --arg label "Gentoo Linux $version" \ --arg args "initrd=$(kernel_to_initramfs "$efi_kernel" | path_to_efi)" \ '[{label:$label,loader:$loader,args:$args}]' | create_efi eend } # check for cli usage efi=1 kernel= interactive= list= json= while getopts KEhlij opt; do case ${opt} in E) efi=1 kernel= ;; K) kernel=1 efi= ;; i) interactive=1 ;; l) list=1 ;; j) json=1 ;; h | ?) printf "usage: %s [-hilj]\n" "$(basename "$0")" printf "\n" exit 2 ;; esac done shift $((OPTIND - 1)) if [ "$(id -u)" -ne 0 ]; then eerror "need to be root" fi # always mount efivarfs first mount_efi # run list if [ -n "$list" ]; then [ -n "$efi" ] && parse_efi | jq '[.[] | {boot:([(if .current then "*" else " " end), .entry]|join("")),label:.label}]' | /home/robert/bin/jq-table [ -n "$kernel" ] && parse_kernels | jq '[.[] | {hash:([(if .current then "*" else " " end), .hash[:6]]|join("")),version:.version}]' | /home/robert/bin/jq-table exit $? fi if [ -n "$json" ]; then [ -n "$efi" ] && parse_efi [ -n "$kernel" ] && parse_kernels exit $? fi # run interactive if [ -n "$interactive" ]; then export BEMENU_BACKEND=curses entry=$(parse_efi | jq '[.[] | {boot:([(if .current then "*" else " " end), .entry]|join("")),label:.label}]' | /home/robert/bin/jq-table | tail -n+2 | bemenu -p ' efi: select boot entry ' | cut -c2-) [ -z "$entry" ] && exit bootnum=$(cut -d' ' -f1 <<<"$entry") [ -z "$bootnum" ] && exit echo ">>> $(cut -d' ' -f2 <<<"$entry")" action=$(cat <<-EOF | bemenu -p " efi: $(cut -d' ' -f2 <<<"$entry") " delete activate deactivate bootnext EOF ) [ -z "$action" ] && exit case "$action" in delete) delete_efi "$bootnum" ;; activate) activate_efi "$bootnum" ;; deactivate) deactivate_efi "$bootnum" ;; bootnext) bootnext_efi "$bootnum" ;; esac exit $? fi # run install routine, used by installkernel test -n "$1" || eerror "need version argument" test -n "$2" || eerror "need kernel argument" new_kernel "$1" "$2" exit $?