#!/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. # Updated on Sun, 15 Jul 2018 17:22:26 +0200 # Add as "super-fallback" /EFI/startup.nsh if absent? # Internationalize. # Provide a pre-built EFI image # Ship in an initramfs to make a real rescue stick. # Use a rEFInd EFI image instead of a GRUB one. # Allow to use an already formatted USB stick. # Do not accept to write in the already mounted ESP of an USB stick and # warn the user then. # 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, edit and change the order of # their displayed labels, make the menu sound or mute. # 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't distinguish with certainty EFI boot loaders from other EFI # files, hence can display entries for these pother EFI files in the # boot menu. # This script has been tested on a machine with an X86_64 architecture # and, as is, deals only with the X86_64 EFI applications. # Permanent files written: # An EFI application in the form of PE32+ executable that gives access # to EFI boot-loaders, stored in /boot as EFI3Mx64.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, as its md5 checksum. # /var/EFI3M stores one line per boot entry, with fields allowing to # store customized labels and record hidden ones. # /var/EFI3Minstall 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 linking to the PE32+ image written by # this script in an ESP as /EFI/EFI3M/BOOTx64.EFI # /etc/EFI3M/timeout stores the timeout in seconds before starting # the first boot entry, if set by the user. # /etc/EFI3M/sound stores the choice of a sound (y) or mute # (n) boot menu, if set by the user or because at least one of the # applications brltty, espeakup or orca was running when writing the # configuration file # 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/EFI3M # 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 # ALREADY, ALREADY_SORTED, NEW: used to properly update /var/EFI3M, # keeping the exiting customization but updating the partitions' names # case occurring and presenting the already registered entries first # # Commands used by this script are shipped in: # coreutils, efibootmgr, efivar, 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 1 392 1 330 1 262 1" 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/EFI3M ESPLIST=$TMP/ESPLIST DEFAULT_TIMEOUT=10 HELLO_SOUND="play 480 440 1" # 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 features() { 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. 3) 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. " printf "Press Enter to continue. " read dummy clear clear echo \ "Requirements: GRUB version at least 2.02 should be installed, as well as usual utilities found in pretty much any Linux distribution. Sound aid for the visually impaired: After booting, navigate in the menu with the up and down arrow keys. If brltty, espeakup or orca is active when EFI3M is run: 1) A Beep is emitted as soon as the menu is displayed. 2) When pressing Enter on the menu entry n (numbered from the top of the menu), a tune of n sounds is played. The user can then confirm this choice pressing Enter (the same tune is then played again) or go back to the top of the menu pressing the Esc key. 3) The same apply to the the last entries, Halt and Reboot. A descending arpeggio is played for Halt, a descending followed by an ascending arpeggio for Reboot. Again, confirm with Enter or cancel with Esc. The sound can also be set afterwards: type 4 in the main menu of EFI3M, then M for Mute or S for Sound, and validate the setting typing V. To apply the modification to an USB stick you will need to write again the menu on it, typing 3 in the main menu. " printf "Press enter to go back to the main menu " read 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/EFI3Minstall ]; 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 # from an ESP listed in /var/EFI3Minstall continue fi ( cd $MOUNTPOINT$EFIDIR if [ -f EFI3M.md5 ]; then if md5sum -c --quiet EFI3M.md5; then rm BOOTx64.EFI rm EFI3M.md5 fi fi if [ -f EFI3Mgrub.md5 ]; then if md5sum -c --quiet EFI3Mgrub.md5; then rm grub.cfg rm EFI3Mgrub.md5 fi fi ) if [ "$MOUNTED" = "n" ]; then umount $MNT fi done < /var/EFI3Minstall } display_the_menu() { clear if [ ! -f /var/EFI3M ]; then printf %b "The boot menu does not exist yet. We can generate it now, but then don't\n" printf %b "forget to install it afterwards in this computer or an USB stick!\n" printf "Do you want to generate the menu now [y/N]? " read dummy if [ ! "$(to_lower $dummy)" = "y" ]; then return else clear make_boot_menu fi fi printf %b "The boot menu will be displayed like this, if and when installed in the\n" printf %b "computer or an USB stick:\n\n" 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/EFI3M echo "Shut down the computer" echo "Reboot the computer" printf %b "\nPress Enter to continue. " read dummy } display_install_path() { if [ "$1" = "main" ]; then SHOW=$(grep EFI3M /var/EFI3Minstall) else SHOW=$(grep /EFI/BOOT /var/EFI3Minstall) 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) printf %b "/dev/$PARTITION as ${ESP_PATH}.\n" } update_grub() { if [ ! -f $ESPLIST ]; then list_the_ESP fi while read NAME PARTTYPE UUID PARENT; do set_mountpoint ( cd $MOUNTPOINT MD5=$(find -name EFI3Mgrub.md5) if [ ! "$MD5" = "" ]; then for i in $MD5; do ( cd ${i%/EFI3Mgrub.md5}; if md5sum -c --quiet EFI3Mgrub.md5; then cp $TMP/grub.cfg grub.cfg md5sum grub.cfg > EFI3Mgrub.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?" printf "Press Enter to quit." read dummy the_end else grub_mkimage=$grub2_mkimage fi fi # Some distributions like Fedora put the same binary file in two # locations. Pick the first that we find. grub_mkimage=$(echo $grub_mkimage|sed "s/ .*//") GRUB_VERSION=$($(basename $grub_mkimage) -V | sed "s/.* //g") # Fedora pretends that its grub 2.202 is at version 2.03... if ! echo $GRUB_VERSION | grep -q "2.0[2|3|4]"; then echo "GRUB version at least 2.02 is required but version $GRUB_VERSION is installed." printf "Press Enter to quit. " read dummy the_end fi # Some distributions don't ship the x86_64-efi grub modules by # defaut, like Fedora, or at all, like Solus. if [ ! -d /usr/lib64/grub/x86_64-efi ] && [ ! -d /usr/lib/grub/x86_64-efi ] ; then echo "The x86_64 GRUB modules are required but not found." printf "Press Enter to quit. " read dummy the_end fi $(basename $grub_mkimage) \ --format=x86_64-efi \ --output=/boot/EFI3Mx64.efi \ --prefix="" \ --compression=xz \ part_gpt part_msdos fat play chain reboot halt search search_fs_uuid help at_keyboard usb_keyboard usb sleep } # 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/EFI3M/timeout fi if [ ! "$USERSOUND" = "" ]; then echo $USERSOUND > /etc/EFI3M/sound fi } write_config_file_with_sound() { echo "$HELLO_SOUND" >> $TMP/grub.cfg # Write a stanza for each line of /var/EFI3M 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="" PLAYTUNE="$(playtune $NUMENTRY)" cat <<-EOF >> $TMP/grub.cfg submenu "$LABEL" { $PLAYTUNE menuentry "start now $LABEL" { insmod part_gpt insmod part_msdos insmod fat search --fs-uuid --set=root $UUID chainloader $EFIPATH $PLAYTUNE } } EOF done < /var/EFI3M # Add global features. cat <<-EOF >> $TMP/grub.cfg submenu "Shut down computer" { $HALT menuentry "Confirm shut down computer" { $HALT halt } } submenu "Reboot computer" { $REBOOT menuentry "Confirm Reboot computer" { $REBOOT reboot } } EOF } write_config_file_without_sound() { # Write a stanza for each line of /var/EFI3M 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 cat <<-EOF >> $TMP/grub.cfg menuentry "$LABEL" { insmod part_gpt insmod part_msdos insmod fat search --fs-uuid --set=root $UUID chainloader $EFIPATH } EOF done < /var/EFI3M # Add global features. cat <<-EOF >> $TMP/grub.cfg menuentry "Shut down computer" { halt } menuentry "Reboot computer" { reboot } EOF } write_grub_config_file() { if [ -f /etc/EFI3M/timeout ]; then TIMEOUT=$(grep . /etc/EFI3M/timeout) # Just in case /etc/EFI3M/timeout has an unexpected content if echo $TIMEOUT|grep -q "[^[:digit:]]"; then TIMEOUT=$DEFAULT_TIMEOUT fi if [ $TIMEOUT -lt 5 ] || [ $TIMEOUT -gt 60 ]; then TIMEOUT=$DEFAULT_TIMEOUT fi fi if [ "$TIMEOUT" = "" ]; then TIMEOUT=$DEFAULT_TIMEOUT fi if [ -f /etc/EFI3M/sound ]; then USERSOUND=$(grep . /etc/EFI3M/sound) fi # Just in case /etc/EFI3M/sound has an unexpected content [ ! "$USERSOUND" = "n" ] && [ ! "$USERSOUND" = "y" ] && USERSOUND="" if [ "$USERSOUND" = "" ]; then if [ ! "$(ps -C espeakup --noheaders)" = "" ] || \ [ ! "$(ps -C orca --noheaders)" = "" ] || \ [ ! "$(ps -C brltty --noheaders)" = "" ] || \ [ "$USERSOUND" = "y" ]; then SOUND=y else SOUND=n fi else SOUND=$USERSOUND 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 set menu_color_normal=white/black set menu_color_highlight=white/blue EOF if [ "$SOUND" = "y" ]; then write_config_file_with_sound else write_config_file_without_sound fi } 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/EFI3M, 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/EFI3M. # 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 ALREADY_SORTED=$TMP/ALREADY_SORTED NEW=$TMP/NEW touch $ALREADY $ALREADY_SORTED $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 GRUB and rEFInd boot managers # although it be somehow redundant. This could be made optional. # 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'|grep 'x86-64') FALLBACK=$(echo $MOUNTPOINT$EFIPATH|grep -i /EFI/BOOT) TOOLS=$(echo $MOUNTPOINT$EFIPATH|grep -i /EFI/tools) # REFIND=$(echo $MOUNTPOINT$EFIPATH|grep -i /efi/refind/refind_x64.efi) if [ "$EFIAPP" = "" ]; then continue fi # Don't include our own OS loader, wherever it be found if echo $MOUNTPOINT$EFIPATH|grep -q /EFI/EFI3M; then continue fi if echo $MOUNTPOINT$EFIPATH|grep -q /EFI/BOOT/BOOTx64.EFI; then CONTINUE=n ( cd $(dirname $MOUNTPOINT$EFIPATH) if [ -f EFI3M.md5 ]; then if md5sum -c --quiet EFI3M.md5; then rm BOOTx64.EFI rm EFI3M.md5 CONTINUE=y fi fi ) [ "$CONTINUE" = "y" ] && continue fi # /var/EFI3M stores a menu with customized labels # in lines formatted this way: # Lines with a customized label: # $NAME $UUID $EFIPATH LABEL=