#!/bin/sh # Copyright 2018 Didier Spaier # # All rights reserved. # # Redistribution and use of this script, with or without modification, is # permitted provided that the following conditions are met: # # 1. Redistributions of this script must retain the above copyright # notice, this list of conditions and the following disclaimer. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Last updated on Sunday 17 June 2018 21.46 Paris time. # TODO: # add as "super-fallback" /EFI/startup.nsh if absent? # Internationalize. # This script provides boot menu entries for all accessible EFI OS # loaders. # Features: # Write an EFI application displaying an menu entry for each EFI boot # loader located in a device connected to the computer. # Install the EFI application in a mass storage device, in a specific # location and optionally as a fall back as /BOOT/EFI/BOOTx64.EFI # Install the EFI application in an USB stick. # Make an entry for the EFI application menu in the firmware. # Allow the user to hide menu entries and edit and change the order of # their displayed labels. # When installing in a mass storage device, remove the EFI applications # previously installed by this script. # Limitations: this script only handles EFI images accessible at time of # running it, stored in an ESP with a FAT file system, with a partition # table labeled either msdos or gpt. # It can need adaptation to Linux distribution not based on Slackware. # It can't distinguish with certainty EFI boot loaders from other EFI # files, hence can display entries for these pother EFI files in the # boot menu. # Permanent files written: # An EFI application in the form of PE32+ executable that gives access # to EFI boot-loaders, stored in /boot as efimultibootmenux64.efi and # copied in its installation locations as BOOTx64.EFI, written by # grub.mkimage # A configuration file grub.cfg stored alongside and read by BOOTx64.EFI # in the target ESP, and its md5 chacksum # A md5 checksum efibootmenu.md5 of BOOTx64.EFI stored alongside it. # /var/efibootmenu stores one line per boot entry, with as format # allowing to store customized labels and record hidden ones. # /var/efibootmenuinstall records the UUID of the ESP where the # PE+32 image and associated grub.cfg are installed and the path to # these files inside the ESP # A NVRAM variable in the firmware to link to the PE32+ executable # written by this script. # /etc/efibootmenu/timeout stores the timeout in seconds before starting # the first boot entry, if set by the user. # /etc/efibootmenu/usersound stores the choice of a sound (y) or mute # (n) boot menu if set by the user. # Purpose of temporary files used: # TMP: main temporary dir. All other temporary files are in $TMP # MENU: menu as will be displayed when running the PE32+ executable # NEWMENU: temporary MENU while reordered # LINE: a line of $MENU being edited # ESPLIST: list of EFI System Partitions accessible at time of running # this script # ACCESSIBLE: lists the EFI loaders accessible at time of running this # script will be copied to /var/efibootmenu # MNT: directory used as mount point for the ESP listed in ESPLIST. We # assume that if an ESP is already mounted, probably on /boot/efi, it is # with read and write permissions for root. Also used as mount point for # the USB stick on which write the PE32+ image and grub.cfg # BEFORE, AFTER: register the lists of USB sticks resp. before and after # the user should have plugged in the one on which to write the PE32+ # image # grub.cfg that will be written in the target ESP # # Commands that have been used used to test this script are shipped in: # coreutils, efibootmgr, file, grep, gptfdisk, grub-2.02, parted, # util-linux in a Slint or Slackware distribution. A run-time dependency # to grub could be avoided shipping a pre-built PE32+ image. This will # be proposed in a further release. # Initialization if [ $(id -u) -ne 0 ]; then echo "Only root may run this script." exit fi if [ ! -d /sys/firmware/efi ]; then echo "EFI booting is not enabled, game over." exit fi # The UEFI specification states that an EFI System partition has # a GUID of C12A7328-F81F-11D2-BA4B-00A0C93EC93B for a GPT layout. # In case of a DOS layout instead, an ESP should have an OS type of # 0xEF. lsblk writes these values in the same field PARTTYPE. ESPPARTTYPE=C12A7328-F81F-11D2-BA4B-00A0C93EC93B OSTYPE=0xEF # Ref: https://fr.wikipedia.org/wiki/Note_de_musique SCALE="26163 29366 32963 34923 39200 44000 49388" # do ré mi fa sol la si HALT="play 240 523 3 392 3 330 3 262 3" REBOOT="play 240 523 1 392 1 330 1 262 1 330 1 392 1 523 1" TMP=$(mktemp -d) MNT=$TMP/MNT mkdir -p $MNT mkdir -p /etc/efibootmenu ESPLIST=$TMP/ESPLIST # Main functions: # # install_on_system_ESP: makes and install the EFI boot manager on a # mass storage device permanently attached to the computer. # # install_on_USB_stick: wipes out and format an USB stick with an ESP # and installs the EFI boot manager there. # # install_in_firmware: insert an entry for the boot manager in the # firmware's boot menu. # # edit_the_menu: allow the user to hide or show a menu entry, modify # the label of a menu entry or change the order of the boot entries About_EFI3M() { clear echo \ "The EFI Multi Boot Menu Maker (EFI3M) allows booting any installed system for which an EFI boot loader is found on the computer. It comes handy if you can't boot otherwise some installed system. Features: 1) Build a boot menu and install it in an EFI system partition of your computer, in a specific location and, if not already busy, optionally in a fall back location where the firmware should look at priority. 2) Install the boot menu on an USB stick. Then you can boot off the USB stick, which in turn will present you with the boot menu allowing to boot any of the installed systems. This helps in case for some reason the internal boot menu be not displayed. 2) Customize the boot menu: hide a menu entry, edit its displayed label, modify the order in which the entries are displayed, set the delay before auto boot, mute or sound the menu. All modifications will be automatically applied to the internal boot menu; to apply them to the USB stick you will need to write again the menu on it. Caveat: the menu built by EFI3M can include entries for EFI files that are not boot loaders, or not working ones. Just hide them editing it. " read -p "Press Enter to continue. " dummy } to_lower() { echo "$1"|tr '[:upper:]' '[:lower:]' } frequency() { # We won't play a tune for more than 35 entries... ENTRYNUM=$1 if [ $ENTRYNUM -lt 8 ]; then OCTAVE=1 elif [ $ENTRYNUM -lt 15 ]; then OCTAVE=2 elif [ $ENTRYNUM -lt 22 ]; then OCTAVE=3 elif [ $ENTRYNUM -lt 29 ]; then OCTAVE=4 elif [ $ENTRYNUM -lt 36 ]; then OCTAVE=5 else echo "" return fi NUMNOTE=$(($ENTRYNUM-(${OCTAVE}-1)*7)) CENTIHERZ=$(($(echo $SCALE|cut -d" " -f$NUMNOTE)*${OCTAVE})) REMAIN=0 if [ $(($CENTIHERZ % 100)) -gt 49 ]; then REMAIN=1 fi FREQUENCY=$((($CENTIHERZ/100)+$REMAIN)) echo $FREQUENCY } playtune() { TUNE="play 480 " i=1 while [ $i -le $1 ]; do TUNE="$TUNE $(frequency $i) 2" i=$((${i}+1)) done echo $TUNE } one_space() { echo "$1"|sed "s/ \{1,\}/ /g" } set_mountpoint() { ESP_NAME=$(one_space "$(lsblk -l -o uuid,name)"|grep $UUID|cut -d" " -f 2) MOUNTPOINT=$(one_space "$(df -h)"|grep /dev/$ESP_NAME|cut -d" " -f 6) ESP=/dev/$ESP_NAME MOUNTED=y if [ "$MOUNTPOINT" = "" ]; then MOUNTPOINT=$MNT mount $ESP $MNT MOUNTED=n fi } remove_previous_files() { if [ ! -f /var/efibootmenuinstall ]; then return fi while read UUID EFIPATH; do set_mountpoint EFIDIR=$(dirname $EFIPATH) if [ ! -d $MOUNTPOINT$EFIDIR ]; then # Don't try to cd to a directory that have been removed # after previous installation in system ESP continue fi ( cd $MOUNTPOINT$EFIDIR if [ -f efibootmenu.md5 ]; then if md5sum -c --quiet efibootmenu.md5; then rm BOOTx64.EFI rm efibootmenu.md5 fi fi if [ -f efibootgrub.md5 ]; then if md5sum -c --quiet efibootgrub.md5; then rm grub.cfg rm efibootgrub.md5 fi fi ) if [ "$MOUNTED" = "n" ]; then umount $MNT fi done < /var/efibootmenuinstall } display_the_menu() { while read name uuid path label; do if [ "$label" = "LABEL=hidden" ]; then continue elif [ "$label" = "LABEL=defaultscheme" ]; then echo " ($name)$path" else echo " ${label#LABEL=}" fi done < /var/efibootmenu } display_install_path() { if [ "$1" = "main" ]; then SHOW=$(grep efibootmenu /var/efibootmenuinstall) else SHOW=$(grep /EFI/BOOT /var/efibootmenuinstall) fi UUID=$(echo $SHOW|cut -d" " -f1) ESP_PATH=$(echo $SHOW|cut -d" " -f2) PARTITION=$(one_space "$(lsblk -l -o UUID,NAME)"|grep $UUID|cut -d" " -f2) echo "/dev/$PARTITION as ${ESP_PATH}. " } update_grub() { if [ ! -f $ESPLIST ]; then list_the_ESP fi while read NAME PARTTYPE UUID PARENT; do set_mountpoint ( cd $MOUNTPOINT MD5=$(find -name efibootgrub.md5) if [ ! "$MD5" = "" ]; then for i in $MD5; do ( cd ${i%/efibootgrub.md5}; if md5sum -c --quiet efibootgrub.md5; then cp $TMP/grub.cfg grub.cfg md5sum grub.cfg > efibootgrub.md5 fi ) done fi ) if [ "$MOUNTED" = "n" ]; then umount $MNT fi done < $ESPLIST } build_boot_loader() { grub_mkimage="$(find /bin/ /usr/bin /sbin /usr/sbin -type f -name grub-mkimage -perm -u+x)" grub2_mkimage="$(find /bin/ /usr/bin /sbin /usr/sbin -type f -name grub2-mkimage -perm -u+x)" if [ "$grub_mkimage" = "" ]; then if [ "$grub2_mkimage" = "" ]; then echo "Neither grub-mkimage nor grub2-mkimage have been found." echo "Maybe grub (I mean grub2) is not installed?" read -p "Press Enter to quit." the_end else grub_mkimage=$grub2_mkimage fi fi if [ ! "$(echo "$grub_mkimage"|wc -l)" = "1" ]; then grub_mkimage=$(echo $grub_mkimage|sed -n 1p) fi $(basename $grub_mkimage) \ --format=x86_64-efi \ --output=/boot/efimultibootmenux64.efi \ --prefix="" \ --compression=xz \ part_gpt part_msdos fat play chain reboot halt search search_fs_uuid efi_gop efi_uga all_video loadbios help at_keyboard usb_keyboard usb sleep extcmd normal } store_default_settings() { if [ ! "$USERTIMEOUT" = "" ]; then echo $USERTIMEOUT > /etc/efibootmenu/timeout fi if [ ! "$USERSOUND" = "" ]; then echo $USERSOUND > /etc/efibootmenu/sound fi } write_grub_config_file() { if [ -f /etc/efibootmenu/timeout ]; then TIMEOUT=$(grep . /etc/efibootmenu/timeout) fi if [ "$TIMEOUT" = "" ]; then TIMEOUT=15 fi if [ -f /etc/efibootmenu/sound ]; then USERSOUND=$(grep . /etc/efibootmenu/sound) fi # Just in case the file be empty if [ "$USERSOUND" = "" ]; then USERSOUND=notknown fi if [ ! "ps -C espeakup --noheaders" = "" ] || \ [ ! "ps -C orca --noheaders" = "" ] \ [ "$USERSOUND" = "y" ]; then SOUND=y fi if [ "$USERSOUND" = "n" ]; then SOUND=n fi if [ "$SOUND" = "y" ]; then PLAY="play 480 440 1" fi NUMENTRY=0 # Write our specific default settings cat <<-EOF >$TMP/grub.cfg # Configuration file written by EFI3M, the EFI Multi-boot Manager Maker. set timeout=$TIMEOUT $PLAY set menu_color_normal=white/black set menu_color_highlight=white/blue EOF # Write a stanza for each line of /var/efibootmenu while read NAME UUID EFIPATH LABEL; do if echo $LABEL|grep -q "^LABEL=hidden"; then continue elif echo $LABEL|grep -q "^LABEL=defaultscheme"; then LABEL="($NAME)$EFIPATH" else LABEL=$(echo $LABEL|sed "s^LABEL=") fi NUMENTRY=$((${NUMENTRY}+1)) PLAYTUNE="" if [ "$SOUND" = "y" ] && [ $NUMENTRY -lt 36 ]; then PLAYTUNE="$(playtune $NUMENTRY)" fi cat <<-EOF >> $TMP/grub.cfg menuentry "$LABEL" { insmod part_gpt insmod part_msdos insmod fat search --fs-uuid --set=root $UUID chainloader $EFIPATH $PLAYTUNE } EOF done < /var/efibootmenu # Add global features. if [ ! "$SOUND" = "y" ];then HALT="" REBOOT="" fi cat <<-EOF >> $TMP/grub.cfg menuentry "Shut down computer" { $HALT halt } menuentry "Reboot computer" { $REBOOT reboot } EOF } list_the_ESP() { # List the accessible EFI system partitions or ESPs lsblk -l -o name,parttype,uuid,pkname|\ grep -i -F -e "$ESPPARTTYPE" -e "$OSTYPE"|sed "s/ \{1,\}/ /g" > $ESPLIST if [ ! -s $ESPLIST ]; then # No EFI partitions echo "No EFI partition found, game over." return fi } make_boot_menu() { # Write a line for each menu entry in the file $ACCESSIBLE that will # be moved to /var/efibootmenu, creating or updating it. # We remove the stuff we previously installed in all accessible ESP # thus they won't be included in grub.cfg, which will be written # from /var/efibootmenu. # For this reason this function will only be called by # install_on_system_ESP, implying that install_on_USB_stick will # call install_on_system_ESP, not directly this function. But then # we won't suggest at the end of install_on_system_ESP to also # install on firmware. This won't hurt as anyway installing on # firmware needs to have already written EFI file in an ESP of which # the path will be written in the firmware menu boot entry. list_the_ESP ALREADY=$TMP/ALREADY NEW=$TMP/NEW touch $ALREADY $NEW # # We successively mount each ESP to find the boot loaders, and # (re)build the menu accordingly. # We take into account the previous customization of the menu. while read NAME PARTTYPE UUID PARENT; do # We discard the ESP on USB sticks. TRANSPORT=$(one_space "$(lsblk -l -o name,tran)"|grep "^$PARENT "|cut -d" " -f 2) HOTPLUG=$(one_space "$(lsblk -l -o name,hotplug)"|grep "^$PARENT "|cut -d" " -f 2) if [ "$TRANSPORT" = "usb" ] && [ "$HOTPLUG" = "1" ]; then continue fi # We mount successively each ESP to find the EFI boot loaders # that they could contain. set_mountpoint ( cd $MOUNTPOINT # EFIPATHS will list all paths to efi boot loaders in this ESP EFIPATHS=$(find -iname "*.efi"|sed s/.//) # Select the EFI boot loaders and write a line for each. # For now we don't exclude the rEFInd boot manager although # it be somehow redundant. # We include all files ending in .efi not written by this script # and fulfilling at least one of the following conditions: # "file" says they are 'PE32+ executable' and 'EFI application' # They lie in the "fallback" directory /EFI/BOOT # They lie in /EFI/tools. for EFIPATH in $EFIPATHS; do EFIAPP=$(file $MOUNTPOINT$EFIPATH|grep 'PE32+ executable'|grep 'EFI application') FALLBACK=$(echo $MOUNPOINT$EFIPATH|grep -i /EFI/BOOT) TOOLS=$(echo $MOUNTPOINT$EFIPATH|grep -i /EFI/tools) # REFIND=$(echo $MOUNTPOINT$EFIPATH|grep -i /efi/refind/refind_x64.efi) INCLUDE="n" if [ ! "$EFIAPP" = "" ] || [ ! "$FALLBACK" = "" ] || [ ! "$TOOLS" = "" ]; then INCLUDE="y" fi if [ "$INCLUDE" = "n" ]; then continue fi # /var/efibootmenu stores a menu with customized labels # in lines formatted this way: # Lines with a customized label: # $NAME $UUID $EFIPATH LABEL=