#!/bin/bash # shellcheck disable=SC2181 # shellcheck disable=SC2236 # # Version 4.1.1 # # bootiso - create a bootable USB drive from an image file # Copyright (C) 2018-2020 jules randolph # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details # You should have received a copy of the GNU General Public License # along with this program. If not, see . # CODE STRUCTURE # # This file is organized in "pseudo-modules". # Refer to the style.md file for a detailed definition. set -o pipefail set -E version="4.1.1" scriptName=$(basename "$0") bashVersion=$(echo "$BASH_VERSION" | cut -d. -f1) if [ -z "$BASH_VERSION" ] || [ "$bashVersion" -lt 4 ]; then echo >&2 "You need bash v4+ to run this script. Aborting..." exit 1 fi # . # .o8 # .ooooo. .o888oo # d88' `"Y8 888 # 888 888 # 888 .o8 888 . # `Y8bod8P' "888" # # CONSTANT GLOBAL VARIABLES (0) # # Readonly (constant) declarations are prefixed with 'ct_'. # Modules can have their own constant declarations, prefixed with # the module name + '_'. typeset -r ct_syslinuxLibRoot=${BOOTISO_SYSLINUX_LIB_ROOT:-'/usr/lib/syslinux'} typeset -r ct_tempRoot=/var/tmp/bootiso typeset -r ct_cacheRoot=/var/cache/bootiso typeset -r ct_mountRoot=/var/tmp/bootiso/mnt typeset -r ct_shortOptions='bydJahlMftLpD' typeset -r ct_ticketsURL="https://github.com/jsamr/bootiso/issues/new/choose" typeset -r ct_dependenciesURL="https://github.com/jsamr/bootiso/blob/master/install.md#dependencies" typeset -r ct_kernelOrgSyslinuxURL="https://mirrors.edge.kernel.org/pub/linux/utils/boot/syslinux/Testing" typeset -r ct_openBugReportMessage="This is not expected: please open a 'Bug Report' ticket at $ct_ticketsURL." typeset -r ct_openImageSupportRequestMessage="You can open an 'Image Support' request here: $ct_ticketsURL" typeset -r ct_architecture=$(getconf LONG_BIT) # . # .o8 # .oooo.o .o888oo # d88( "8 888 # `"Y88b. 888 # o. )88b 888 . # 8""888P' "888" # # STATE GLOBAL VARIABLES (0) typeset st_targetPartition typeset st_elToritoMountPoint typeset st_usbMountPoint typeset st_execStartTime typeset st_execEndTime typeset st_shouldInstallSyslinux=false typeset st_targetSyslinuxVersion typeset st_completedAction typeset st_expectingISOFile typeset st_hasActionDuration typeset st_foundSyslinuxMbrBinary typeset st_foundSyslinuxBiosFolder typeset st_shouldMakePartition typeset st_backgroundProcess='' typeset st_packageManager typeset st_hasLegacyColumn typeset -A st_syslinuxBinaries typeset -a st_temporaryAssets=() typeset -a st_devicesList=() typeset -A st_isoInspections=( [syslinuxBin]='' [syslinuxVer]='' [syslinuxConf]='' [isHybrid]=false [supportsEFIBoot]=false [supportsBIOSBoot]=false [hasWimFile]=false ) typeset -A st_userFlags=( # Actions ['help']='' ['version']='' ['list-usb-drives']='' ['format']='' ['install-image-copy']='' ['install-mount-rsync']='' ['inspect']='' ['probe']='' # Options ['local-bootloader']='' ['assume-yes']='' ['device']='' ['no-eject']='' ['gpt']='' ['autoselect']='' ['no-mime-check']='' ['no-usb-check']='' ['no-size-check']='' ['no-hash-check']='' ['force-hash-check']='' ['no-wimsplit']='' ['data-part']='' ) typeset -A st_userVars=( ['iso-file']='' ['hash-file']='' ['device']='' ['fs']='' ['label']='' ['remote-bootloader']='' ['part-type']='' ['data-part-fs']='' ) # oo.ooooo. .oooo. oooo d8b # 888' `88b `P )88b `888""8P # 888 888 .oP"888 888 # 888 888 d8( 888 888 # 888bod8P' `Y888""8o d888b # 888 # o888o # # PARAMETER GLOBAL VARIABLES (0) # # Global variables derived from program arguments or configuration. # These are the only unprefixed global variables. typeset sourceImageFile typeset sourceHashFile typeset targetDevice typeset targetPartitionLabel typeset targetFilesystem typeset targetPartitionScheme typeset targetAction='install-auto' typeset targetBootloaderVersion typeset targetDDBusSize typeset targetDataPartFstype typeset enableForceHashCheck typeset enableAutoselect typeset enableForceLocalBootloader typeset enableGPT typeset enableDataPart typeset disableMimeCheck typeset disableUSBCheck typeset disableSizeCheck typeset disableConfirmation typeset disableHashCheck typeset disableWimsplit typeset disableDeviceEjection # oooo # `888 # .oooo.o 888 .oo. # d88( "8 888P"Y88b # `"Y88b. 888 888 # o. )88b 888 888 # 8""888P' o888o o888o # # # SH MODULE (1) # $@ - The expression piped to bc function sh_compute() { local _answer _answer=$(echo "$@" | bc) if ((_answer == 0)); then return 1 else return 0 fi } # $1 - The string by which elements will be joined. # $2+ - The elements to join function sh_joinBy() { local -r IFS=$1 shift echo "$*" } # $1 - The element to check. # $2+ - The list to check against. function sh_elementIsInList() { local -r _match="$1" local _arg shift for _arg in "$@"; do [[ "$_arg" == "$_match" ]] && return 0; done return 1 } # $1: fsType function sh_normalizeFSType() { local -r _fsType="${1:-vfat}" if [ "${_fsType,,}" == fat32 ]; then echo vfat else echo "$_fsType" fi } # . # .o8 # .o888oo .ooooo. oooo d8b ooo. .oo. .oo. # 888 d88' `88b `888""8P `888P"Y88bP"Y88b # 888 888ooo888 888 888 888 888 # 888 . 888 .o 888 888 888 888 # "888" `Y8bod8P' d888b o888o o888o o888o # # # TERMINAL MODULE (1) # See console_codes GNU-Linux man page typeset -r term_setRed="\e[31m" typeset -r term_setGreen="\e[32m" typeset -r term_setYellow="\e[33m" typeset -r term_unsetColor="\e[39m" typeset -r term_setUnderline="\e[4m" typeset -r term_unsetUnderline="\e[24m" typeset -r term_setBold="\e[1m" typeset -r term_unsetBold="\e[22m" # Spacing typeset -r term_logPrefix="$scriptName: " typeset -r term_logPrefixLength="${#term_logPrefix}" typeset -r term_logPrefixEmpty="$(printf "%${term_logPrefixLength}s")" function term_boldify() { echo -e "$term_setBold$1$term_unsetBold" } function term_underline() { echo -e "$term_setUnderline$1$term_unsetUnderline" } # $1: The text to colorify. function term_redify() { echo -e "$term_setRed$1$term_unsetColor" } # $1: The text to colorify. function term_greenify() { echo -e "$term_setGreen$1$term_unsetColor" } # $1: The text to colorify. function term_yellowify() { echo -e "$term_setYellow$1$term_unsetColor" } function term_printColumn() { local -r _prefix=$1 local -r _prefixLength=$2 local _termWidth local _sep='\t' _termWidth="$(tput cols)" shift 2 local _rawInput="$*" echo -n -e "$_prefix$_sep$_prefixLength$_sep$_termWidth$_sep$_rawInput" | awk -F $_sep ' { prefix = $1 prefixlen = $2 termwidth = $3 len = prefixlen printargfill = sprintf("%s%s%s", "%-", prefixlen, "s") printf printargfill, prefix for(j=4;j<=NF;j++) { n = split($j,x," ") for(i=1;i<=n;i++){ if(len+length(x[i])>=termwidth){ print "" printf printargfill, " " len = prefixlen } printf "%s ",x[i] len += 1+length(x[i]) } } print "" }' } function term_printLog() { term_printColumn "$term_logPrefix" "$term_logPrefixLength" "$*" } # shellcheck disable=SC2120 function term_indentAll() { while read -r line; do term_printColumn " " "$term_logPrefixLength" "$line" done < "${1:-/dev/stdin}" } # $*: The message to print. function term_echoerr() { term_redify "$(term_printLog "$*")" } # $*: The message to print. function term_echowarn() { term_yellowify "$(term_printLog "$*")" } # $*: The message to print. function term_echogood() { term_greenify "$(term_printLog "$*")" } # $*: The message to print. function term_echoinfo() { term_printLog "$*" } # Print an ASCII "spin" character # depending on the modulo of variable _i in scope, # and sleep for 250ms. function term_updateProgress() { local _sp="/-\\|" # print when launched from terminal if tty -s; then printf "\\b%s" "${_sp:_i++%${#_sp}:1}" fi sleep 0.25 } function term_cleanProgress() { # print when launched from terminal if tty -s; then printf "\\b%s\\n" " " fi } # .o88o. # 888 `" # o888oo .oooo.o # 888 d88( "8 # 888 `"Y88b. # 888 o. )88b # o888o 8""888P' # # # FILESYSTEM MODULE (1) typeset -Ar fs_gptPartitionCodes=( [efi]="C12A7328-F81F-11D2-BA4B-00A0C93EC93B" # Windows Data partition [wdp]="EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" # Linux Filesystem Data [lfd]="0FC63DAF-8483-4772-8E79-3D69D8477DE4" ) typeset -Ar fs_mbrPartitionCodes=( [efi]=ef ) function fs_firstMatchInFolder() { local -r _path=$1 local -r _pattern=$2 find "$_path" -type f -iname "$_pattern" -print -quit } # $1 - From path # $2+ - Patterns (see find -iname) function fs_matchFirstExpression() { local -r _path=$1 local _pattern local _match shift for _pattern in "$@"; do _match=$(fs_firstMatchInFolder "$_path" "$_pattern") if [ ! -z "$_match" ]; then echo "$_match" break fi done } # $1 - From path # $2+ - Path segments (see find -path) function fs_findFileFromPatterns() { local -r _path=$1 shift local _pathSegment local _found local _candidate for _pathSegment in "$@"; do if [ -f "${_path}/$_pathSegment" ]; then _found="${_path}/$_pathSegment" break fi done if [ -z "$_found" ]; then for _pathSegment in "$@"; do _candidate=$(find "$_path" -type f -path "*/$_pathSegment" -print -quit) if [ ! -z "$_candidate" ]; then _found="$_candidate" break fi done fi echo "$_found" } function fs_createTempFile() { local -r _prefix=$1 local _tmpFileTemplate="$ct_tempRoot/$_prefix-XXX" mktemp "$_tmpFileTemplate" || ps_failAndExit IO_ERROR "Failed to create temporary file." } # Print the name of the new folder if operation # succeeded, fails otherwise. # # $1 - The folder name prefix function fs_createMountFolder() { mktemp -d "$ct_mountRoot/$1-XXX" || ps_failAndExit IO_ERROR "Failed to create temporary mount point with pattern '$_tmpFileTemplate'." } function fs_syncdev() { sync } function fs_isMounted() { local -r _partitionBlock=$1 if [ ! -z "$_partitionBlock" ] && grep -q -e "$_partitionBlock" /etc/mtab; then return 0 else return 1 fi } function fs_umountUSB() { if fs_isMounted "$st_usbMountPoint"; then if umount "$st_usbMountPoint" |& term_indentAll; then term_echogood "USB device partition succesfully unmounted." else term_echowarn "Could not unmount USB mount point." fi fi } # $1 - mountPoint function fs_umountPartition() { local -r _mountPoint="$1" if fs_isMounted "$_mountPoint"; then if ! umount "$_mountPoint" |& term_indentAll; then term_echowarn "Could not unmount image mount point." fi fi } function fs_umountElTorito() { if fs_isMounted "$st_elToritoMountPoint"; then if ! umount "$st_elToritoMountPoint" |& term_indentAll; then term_echowarn "Could not unmount image mount point." fi fi } function fs_mountUSB() { local _type="$targetFilesystem" st_usbMountPoint=$(fs_createMountFolder usb) || exit "$?" st_temporaryAssets+=("$st_usbMountPoint") term_echoinfo "Created USB device mount point at '$st_usbMountPoint'" if ! mount -t "$_type" "$st_targetPartition" "$st_usbMountPoint" > /dev/null; then ps_failAndExit IO_ERROR "Could not mount USB device." fi } # $1 - mountPoint function fs_mountElToritoFile() { local -r _mountPoint="$1" if ! mount -r -o loop -- "$sourceImageFile" "$_mountPoint" > /dev/null; then ps_failAndExit IO_ERROR "Could not mount image file." else st_temporaryAssets+=("$_mountPoint") fi } # $1 - fstype # $2 - target partition full path # $3 - partition label function fs_formatPartition() { local -r _fstype=$1 local -r _targetPart=$2 local -r _partLabel=$3 # These options always end up with the label flag setter local -Ar _mkfsOpts=( ['vfat']="-v -F 32 -n" # Fat32 mode ['exfat']="-n" ['ntfs']="-Q -c 4096 -L" # Quick mode + cluster size = 4096 for syslinux support ['ext2']="-O ^64bit -L" # Disabling pure 64 bits compression for syslinux compatibility ['ext3']="-O ^64bit -L" # see https://www.syslinux.org/wiki/index.php?title=Filesystem#ext ['ext4']="-O ^64bit -L" ['f2fs']="-l" ) # format term_echogood "Creating $_fstype partition on '$_targetPart'..." # shellcheck disable=SC2086 "mkfs.$targetFilesystem" ${_mkfsOpts[$_fstype]} "$_partLabel" "$_targetPart" |& term_indentAll || ps_failAndExit IO_ERROR "Failed to create $_fstype partition on USB device." } # Given a partition scheme (1), output a partition type # that would be a good fit for filesystem type (2). # # $1 - partScheme # $2 - fsType function fs_inferFSType() { local -r _partScheme=$1 local -r _fsType=$2 local -Ar _gptTypeCodes=( ['vfat']="${fs_gptPartitionCodes[wdp]}" ['exfat']="${fs_gptPartitionCodes[wdp]}" ['ntfs']="${fs_gptPartitionCodes[wdp]}" ['ext2']="${fs_gptPartitionCodes[lfd]}" ['ext3']="${fs_gptPartitionCodes[lfd]}" ['ext4']="${fs_gptPartitionCodes[lfd]}" ['f2fs']="${fs_gptPartitionCodes[lfd]}" ) local -Ar _mbrTypeCodes=( ['vfat']='c' ['exfat']='7' ['ntfs']='7' ['ext2']='83' ['ext3']='83' ['ext4']='83' ['f2fs']='83' ) case "$_partScheme" in dos | mbr) echo "${_mbrTypeCodes[$_fsType]}" ;; gpt) echo "${_gptTypeCodes[$_fsType]}" ;; *) ps_failAndExit STATE_ERROR "(fs_inferFSType) unhandled partition scheme: $_partScheme" ;; esac } function fs_syncWithProgress() { # _i defined for term_updateProgress local -i _i=1 local -i _status local _syncPid local _statusFile _statusFile=$(fs_createTempFile "bootiso-sync-status") st_temporaryAssets+=("$_statusFile") fs_syncdev & _syncPid=$! echo -n "$scriptName: Synchronizing writes on device '${targetDevice}' " while [ -e "/proc/$_syncPid" ]; do term_updateProgress done term_cleanProgress _status=$(cat "$_statusFile") if [ ! "$_status" -eq 0 ]; then ps_failAndExit IO_ERROR "Sync call failed." fi } # .oooo.o oooo ooo .oooo.o # d88( "8 `88. .8' d88( "8 # `"Y88b. `88..8' `"Y88b. # o. )88b `888' o. )88b # 8""888P' .8' 8""888P' # .o..P' # `Y8P' # # # OPERATING SYSTEM MODULE (1) # $1 - The name of the command to check in $PATH. function sys_hasCommand() { command -v "$1" &> /dev/null return $? } # $1 - The name of the package command to check. function sys_checkCommand() { local -r _command=$1 local _answer if ! sys_hasCommand "$_command"; then term_echowarn "Command '$_command' not found!" if ((EUID != 0)) || [[ -z ${ct_commandPackages["$_command"]} ]]; then ps_failAndExit MISSING_DEPENDENCY \ "Please install the missing package providing this command manually." fi term_echowarn "Should be in package '${ct_commandPackages["$_command"]}'." if [ -n "$st_packageManager" ]; then read -r -n1 -p "${term_logPrefixEmpty}Attempt installation? (y/n)> " _answer echo case $_answer in y | Y) if ! $st_packageManager "${ct_commandPackages["$_command"]}"; then ps_failAndExit MISSING_DEPENDENCY "Installation of dependency '$_command' failed." \ "Perhaps this dependency has a slightly different name on your distribution." \ "Find it and install manually." else if ! sys_hasCommand "$_command"; then ps_failAndExit MISSING_DEPENDENCY "Program '$_command' is not accessible in the \$PATH environment even though the package ${ct_commandPackages["$_command"]} has just been installed." fi fi ;; *) ps_failAndExit MISSING_DEPENDENCY "Missing dependency '$_command'." ;; esac else ps_failAndExit MISSING_DEPENDENCY "Missing dependency '$_command'." fi fi } # $1 - a device block # Returns "usb", "ata", "nvme" ... or fallback to "other" function sys_getDeviceType() { local -r _deviceBlock=$1 local _deviceType _deviceType=$(lsblk -dlno TRAN "$_deviceBlock") echo "${_deviceType:-other}" } # $1 - a device block function sys_isDeviceDisk() { local -r _deviceBlock="$1" lsblk -lno TYPE "$_deviceBlock" | grep -q disk return $? } # oo.ooooo. .oooo.o # 888' `88b d88( "8 # 888 888 `"Y88b. # 888 888 o. )88b # 888bod8P' 8""888P' # 888 # o888o # # # PROCESS STATE MODULE (2) typeset -Ar ps_exitStatus=( # Exceptions [ASSERTION_FAILED]=1 [SYNOPSIS_NONCOMPL]=2 [MISSING_BOOT_CAP]=3 [FILE_NOEXIST]=4 [BADFILE]=5 [DEVICE_NOEXIST]=6 [BAD_DEVICE]=7 [NO_DEVICES]=8 [MISSING_DEPENDENCY]=9 [HOST_UNREACHABLE]=10 [USER_ABORTED]=11 [MISSING_PRIVILEGE]=12 [FAILED_POSTULATE]=13 # Errors [IO_ERROR]=64 [STATE_ERROR]=65 [THIRD_PARTY_ERROR]=66 ) # Normalize PATH on some distro which don't export system binary paths # by default, such as Debian. The normalization follows FHS 3.0 defined paths # for system binaries. function ps_normalizePath() { local -a _paths mapfile -t _paths < <(echo "$PATH" | tr ':' '\n') if ! sh_elementIsInList /sbin "${_paths[@]}"; then export PATH="$PATH:/sbin" fi if ! sh_elementIsInList /usr/sbin "${_paths[@]}"; then export PATH="$PATH:/usr/sbin" fi if ! sh_elementIsInList /usr/local/sbin "${_paths[@]}"; then export PATH="$PATH:/usr/local/sbin" fi } function ps_cleanupOnExit() { local _asset function _removeTempAsset() { if [[ "$1" =~ ^$ct_tempRoot ]] || [[ "$1" =~ ^$ct_mountRoot ]]; then if [ -d "$1" ]; then rm -rf "$1" elif [ -f "$1" ]; then rm "$1" fi else term_echowarn "Skipping deletion of unexpected temporary asset at '$1'." fi } function _ejectDevice() { if [[ "$st_completedAction" =~ ^install ]]; then if [[ "$disableDeviceEjection" == false ]]; then if eject "$targetDevice" |& term_indentAll; then term_echogood "USB device succesfully ejected." \ "You can safely remove it!" else term_echowarn "Failed to eject device '$targetDevice'." fi else term_echoinfo "USB device ejection skipped with $(term_boldify '-J, --no-eject')." fi fi } if ((EUID == 0)); then fs_umountElTorito fs_umountUSB for _asset in "${st_temporaryAssets[@]}"; do _removeTempAsset "$_asset" done _ejectDevice fi } function ps_cleanupOnInterrupt() { function _waitBackgroundProcess() { if [[ -n "$st_backgroundProcess" ]]; then wait "$st_backgroundProcess" fi } if ((EUID == 0)); then echo term_echowarn "Received INT or TERM signal!..." \ "Synchronizing pending writes before unmounting..." \ "PLEASE WAIT SYNC PROCEDURE COMPLETION TO AVOID DAMAGING DEVICES!" _waitBackgroundProcess fs_syncdev ps_cleanupOnExit exit "${ps_exitStatus[USER_ABORTED]}" fi } function ps_startTimer() { st_execStartTime=$(date +%s) } function ps_stopTimerAndPrintLapsed() { st_execEndTime=$(date +%s) term_echogood "Took $((st_execEndTime - st_execStartTime)) seconds to perform $(term_boldify "$targetAction") action." } # $1 - The status code, see ps_exitStatus keys # $2+ - The message to print. function ps_failAndExit() { local -r _statusCode=$1 _status if ! sh_elementIsInList "$_statusCode" "${!ps_exitStatus[@]}"; then term_echoerr "(ps_failAndExit) Internal state error. Unknown status code '$_statusCode'" _statusCode=STATE_ERROR else shift if [ "$_statusCode" == SYNOPSIS_NONCOMPL ]; then [ $# -ne 0 ] && term_echoerr "$@" term_echoerr "Check $(term_boldify 'man 1 bootiso')." else term_echoerr "$@" "Exiting..." fi fi if [ "$_statusCode" == STATE_ERROR ]; then term_echoerr "Provide a bug report at $ct_openBugReportMessage." fi if [ "$_statusCode" == MISSING_DEPENDENCY ]; then term_echoerr "See $ct_dependenciesURL" fi exit "${ps_exitStatus[$_statusCode]}" } # . # .o8 # .oooo. .oooo.o oooo d8b .o888oo # `P )88b d88( "8 `888""8P 888 # .oP"888 `"Y88b. 888 888 # d8( 888 o. )88b 888 888 . # `Y888""8o 8""888P' d888b "888" # # # ASSERTION MODULE (2) typeset -ar asrt_supportedFS=('vfat' 'exfat' 'ntfs' 'ext2' 'ext3' 'ext4' 'f2fs') typeset -Ar asrt_userVarsCompatibilityMatrix=( ['iso-file']='install-auto install-mount-rsync install-image-copy inspect probe' ['hash-file']='install-auto install-mount-rsync install-image-copy inspect probe' [device]='install-auto install-mount-rsync install-image-copy format' [type]='install-mount-rsync format' [label]='install-mount-rsync format' ['remote-bootloader']='install-mount-rsync' ['part-type']='format install-mount-rsync' ['dd-bs']='install-image-copy' ['data-part-fs']='install-image-copy' ) typeset -Ar ct_userFlagsCompatibilityMatrix=( ['assume-yes']='install-auto install-mount-rsync install-image-copy format' ['no-eject']='install-auto install-mount-rsync install-image-copy format' ['autoselect']='install-auto install-mount-rsync install-image-copy format' ['no-mime-check']='install-auto install-mount-rsync install-image-copy probe inspect' ['no-hash-check']='install-auto install-mount-rsync install-image-copy probe inspect' ['force-hash-check']='install-auto install-mount-rsync install-image-copy probe inspect' ['no-usb-check']='install-auto install-mount-rsync install-image-copy list-usb-drives probe format' ['no-size-check']='install-auto install-mount-rsync install-image-copy' [gpt]='format install-mount-rsync' ['data-part']='install-image-copy' ['local-bootloader']='install-mount-rsync' ['no-wimsplit']='install-mount-rsync' ) typeset -ar asrt_commandDependencies=( awk basename bc blkid blockdev cat chmod column curl cut date dd dirname du eject file find fmt getconf grep jq lsblk md5sum mkdir mktemp mount mv numfmt partx rm rsync sed sfdisk sha1sum sha256sum sha512sum sleep sort strings sync syslinux tail tar tput tr tty umount wimlib-imagex wipefs xargs ) typeset -Ar ct_commandPackages=( [awk]='gawk' [basename]='coreutils' [bc]='bc' [blkid]='util-linux' [blockdev]='util-linux' [cat]='coreutils' [chmod]='coreutils' [column]='util-linux' [curl]='curl' [cut]='coreutils' [date]='coreutils' [dd]='coreutils' [dirname]='coreutils' [du]='coreutils' [eject]='util-linux' [file]='file' [find]='findutils' [fmt]='coreutils' [getconf]='glibc' [grep]='grep' [jq]='jq' [lsblk]='util-linux' [md5sum]='coreutils' [mkdir]='coreutils' [mktemp]='coreutils' [mount]='util-linux' [mv]='coreutils' [numfmt]='coreutils' [partx]='util-linux' [rsync]='rsync' [rm]='coreutils' [sed]='sed' [sfdisk]='' # Distro-dependent [sha1sum]='coreutils' [sha256sum]='coreutils' [sha512sum]='coreutils' [sleep]='coreutils' [sort]='coreutils' [strings]='binutils' [sync]='coreutils' [syslinux]='syslinux' [tail]='coreutils' [tar]='tar' [tput]='' # Distro-dependent [tr]='coreutils' [tty]='coreutils' [umount]='util-linux' ['wimlib-imagex']='' # Distro-dependent [wipefs]='util-linux' [xargs]='findutils' ) function asrt_checkSudo() { if ((EUID != 0)); then if [[ -t 1 ]] && sys_hasCommand sudo; then sudo --preserve-env "$0" "$@" elif sys_hasCommand gksu; then exec 1> output_file gksu --preserve-env "$0" "$@" else ps_failAndExit MISSING_PRIVILEGE "You must run $scriptName as root." fi exit fi } function asrt_checkFileIsImage() { local _mimeType if [ -z "$sourceImageFile" ]; then term_echoerr "Missing argument 'iso-file'." exit 2 fi if [ -d "$sourceImageFile" ]; then ps_failAndExit BADFILE "Provided file '$sourceImageFile' is a directory." fi if [ ! -f "$sourceImageFile" ]; then ps_failAndExit FILE_NOEXIST "Provided iso file '$sourceImageFile' does not exist." fi if [ "$disableMimeCheck" == false ]; then _mimeType=$(file --mime-type -b -- "$sourceImageFile") if [[ "$_mimeType" != "application/octet-stream" && "$_mimeType" != "application/x-iso9660-image" ]]; then term_echoerr "Provided file '$sourceImageFile' doesn't seem to be an image file (wrong mime-type: '$_mimeType')." \ "You can bypass this policy with $(term_boldify '-M, --no-mime-check')." ps_failAndExit BADFILE fi fi } function asrt_checkImageHash() { local _lHash local _numValidHashes=0 local _isoDirectory local _isoFileName local -ar _hashes=("md5sum" "sha1sum" "sha256sum" "sha512sum") function _computeHashWithProgress() { local _hashName="$1" local _imageName="$2" # _i defined for term_updateProgress local _hashStoreFile _i _hashStoreFile=$(fs_createTempFile "bootiso-file-_hash") term_echoinfo "Checking _hash for '$_imageName'..." printf "%s" \ "You can disable this check with $(term_boldify "-H, --no-hash-check") flags." | term_indentAll st_temporaryAssets+=("$_hashStoreFile") ( local _hash local -i _hash=$($_hashName "$_imageName" | awk "{print \$1; exit }") if (($? == 0)); then printf "%s" "$_hash" > "$_hashStoreFile" else printf "%s" 1 > "$_hashStoreFile" fi ) & st_backgroundProcess=$! while [ -e "/proc/$st_backgroundProcess" ]; do term_updateProgress done st_backgroundProcess='' term_cleanProgress _lHash=$(cat "$_hashStoreFile") if [ "$_lHash" == "1" ]; then return 1 fi } function _checkHash() { local -r _hashPath=$1 # Path to file containing _hashes local -r _imageName=$2 # File to be checked local -r _hashName=$3 # Name of command of _hash local _answer # Hash from _hash file local _gHash _gHash=$(awk -v pattern="$_imageName$" '$0 ~ pattern { print $1; exit }' "$_hashPath") if [ -z "$_gHash" ]; then term_echoerr "No matching filename found in hash file '$_hashPath'" return elif [ -z "$_hashName" ]; then case ${#_gHash} in 32) _hashName="md5sum" ;; 40) _hashName="sha1sum" ;; 64) _hashName="sha256sum" ;; 128) _hashName="sha512sum" ;; *) ps_failAndExit BADFILE "Matching line in '$_hashPath' has an unexpected hash format." ;; esac fi # Hash from iso _computeHashWithProgress $_hashName "$_imageName" || ps_failAndExit THIRD_PARTY_ERROR "$_hashName command failed with status $?" if [ "$_gHash" != "$_lHash" ]; then if [ "$enableForceHashCheck" == 'true' ]; then ps_failAndExit ASSERTION_FAILED "Hash mismatch in '$_hashPath' (${_hashName%sum})." else term_echowarn "Hash mismatch in '$_hashPath' (${_hashName%sum})." read -r -n1 -p "${term_logPrefixEmpty}Do you still want to continue? (y/n)> " _answer echo case $_answer in y | Y) return ;; *) ps_failAndExit USER_ABORTED ;; esac term_echoinfo "Ignoring mismatching _hash." fi else term_echogood "Matching ${_hashName%sum} hash found in '$_hashPath'" _numValidHashes=$((_numValidHashes + 1)) fi } _isoDirectory=$(dirname "$sourceImageFile") _isoFileName=$(basename "$sourceImageFile") if [ -n "$sourceHashFile" ]; then if [ -f "$sourceHashFile" ]; then _checkHash "$sourceHashFile" "$_isoFileName" else ps_failAndExit FILE_NOEXIST "Specified hash file '$sourceHashFile' does not exist." fi else shopt -s nullglob nocaseglob for _hash in "${_hashes[@]}"; do for file in "$_isoDirectory/$_hash"*; do _checkHash "$file" "$_isoFileName" "$_hash" done if [ -f "$sourceImageFile.${_hash%sum}" ]; then _checkHash "$sourceImageFile.${_hash%sum}" "$_isoFileName" "$_hash" fi done shopt -u nullglob nocaseglob fi if [ "$enableForceHashCheck" == 'true' ] && [ $_numValidHashes == 0 ]; then ps_failAndExit ASSERTION_FAILED "No matching hashes found. Assert forced by $(term_boldify '--force-_hash-check')" fi } function asrt_checkPackages() { local _pkg for _pkg in "${asrt_commandDependencies[@]}"; do sys_checkCommand "$_pkg" done # test grep supports -P option if ! echo 1 | grep -P '1' &> /dev/null; then ps_failAndExit MISSING_DEPENDENCY \ "You're using an old version of grep which does not support perl regular expression (-P option)." fi if echo "" | column -t -N t -W t &> /dev/null; then st_hasLegacyColumn=false else # Old BSD command, see https://git.io/JfauE st_hasLegacyColumn=true fi } function asrt_checkDeviceIsOK() { local -r _deviceBlock=$1 _failDevice() { term_echoerr "$1" exec_listUSBDrives term_echoerr "Exiting..." exit "${ps_exitStatus[DEVICE_NOEXIST]}" } if [ ! -e "$_deviceBlock" ]; then _failDevice "The selected device '$_deviceBlock' does not exist." fi if [ ! -b "$_deviceBlock" ]; then _failDevice "The selected device '$_deviceBlock' is not a valid block file." fi if [ ! -d "/sys/block/$(basename "$_deviceBlock")" ] || ! sys_isDeviceDisk "$_deviceBlock"; then ps_failAndExit BAD_DEVICE \ "The selected device '$_deviceBlock' is either unmounted or not a disk (might be a partition or loop)." \ "Select a disk instead or reconnect the USB deviceBlock." \ "You can check the availability of USB drives with $(term_boldify "$scriptName -l")." fi } function asrt_checkDeviceIsUSB() { local _deviceType if [ "$disableUSBCheck" == 'true' ]; then term_echowarn "USB check has been disabled. Skipping." return 0 fi _deviceType=$(sys_getDeviceType "$targetDevice") if [ "$_deviceType" != "usb" ]; then term_echoerr "The device you selected is not connected via USB (found TRAN: '$_deviceType') and the operation was therefore canceled." term_echowarn "Use $(term_boldify '--no-usb-check') to bypass this policy at your own risk." term_echoerr "Exiting..." exit 1 fi term_echogood "The selected device '$targetDevice' is connected through USB." } function asrt_checkImageSize() { local -r _deviceBlock=$1 local -r _imageFile=$2 if [ "$disableSizeCheck" == 'true' ]; then term_echowarn "Size check has been disabled. Skipping." return 0 fi if [ "$(blockdev --getsz "$_imageFile")" -gt "$(blockdev --getsz "$_deviceBlock")" ]; then term_echoerr "The image is larger than the selected _deviceBlock '$_deviceBlock' and the operation was therefore canceled." term_echowarn "Use $(term_boldify '--no-size-check') to bypass this policy at your own risk." term_echoerr "Exiting..." exit 1 fi } function asrt_checkAction() { local -ra _actions=('help' 'version' 'format' 'install-image-copy' 'install-mount-rsync' 'list-usb-drives' 'inspect' 'probe') local -a _enabledActions=() local _act for _act in "${_actions[@]}"; do if [ "${st_userFlags[$_act]}" == 'true' ]; then _enabledActions+=("$_act") fi done if ((${#_enabledActions[@]} == 0)); then targetAction='install-auto' elif ((${#_enabledActions[@]} == 1)); then targetAction=${_enabledActions[0]} else ps_failAndExit SYNOPSIS_NONCOMPL \ "You cannot invoke multiple actions at once: $(sh_joinBy '+' "${_enabledActions[@]}")." fi } # $1 - fsType function asrt_checkFSType() { local -r _fsType="$1" if ! sh_elementIsInList "$_fsType" "${asrt_supportedFS[@]}"; then ps_failAndExit SYNOPSIS_NONCOMPL "Filesystem type '$_fsType' not supported." \ "Supported filesystem types: $(sh_joinBy "," "${asrt_supportedFS[*]}")." fi if ! command -v "mkfs.$_fsType" &> /dev/null; then ps_failAndExit MISSING_DEPENDENCY \ "Program 'mkfs.$_fsType' could not be found on your system." \ "Please install it and retry." fi } function asrt_checkUserVars() { # check partition types if [ -n "${st_userVars[fs]}" ]; then st_userVars[fs]=$(sh_normalizeFSType "${st_userVars[fs]}") asrt_checkFSType "${st_userVars[fs]}" fi if [ -n "${st_userVars['data-part-fs']}" ]; then st_userVars['data-part-fs']=$(sh_normalizeFSType "${st_userVars['data-part-fs']}") asrt_checkFSType "${st_userVars['data-part-fs']}" fi # check device if [ -n "${st_userVars[device]}" ]; then if [[ ! "${st_userVars[device]}" =~ '/dev/' ]] && [ -e "/dev/${st_userVars[device]}" ]; then st_userVars[device]="/dev/${st_userVars[device]}" fi asrt_checkDeviceIsOK "${st_userVars[device]}" fi if [ -n "${st_userVars['remote-bootloader']}" ] && [[ ! "${st_userVars['remote-bootloader']}" =~ ^[0-9]+\.[0-9]+$ ]]; then ps_failAndExit SYNOPSIS_NONCOMPL \ "Remote bootloader version '${st_userVars['remote-bootloader']}' set with $(term_boldify '--remote-bootloader') doesn't follow MAJOR.MINOR pattern." \ "Valid examples are 4.10, 6.02" fi # Check dd-bs if [[ -n "${st_userVars['dd-bs']}" && ! ${st_userVars['dd-bs']} =~ ^[0-9]+[kMGT]?$ ]]; then ps_failAndExit SYNOPSIS_NONCOMPL "$(term_boldify '--dd-bs') argument must be a valid block size quantifier, e.g. 512k, 2M." fi } function asrt_checkUserFlags() { # Autoselect security if [ "${st_userFlags['autoselect']}" == 'true' ] && [ "${st_userFlags['no-usb-check']}" == 'true' ]; then ps_failAndExit MISSING_DEPENDENCY \ "You cannot set $(term_boldify '-a, --autoselect') while disabling USB check with $(term_boldify '--no-usb-check')" fi if [ "${st_userFlags['no-hash-check']}" == 'true' ] && [ "${st_userFlags['force-hash-check']}" == 'true' ]; then ps_failAndExit SYNOPSIS_NONCOMPL \ "You cannot combine $(term_boldify '--no-hash-check') and $(term_boldify '--force-hash-check')" elif [ "${st_userFlags['no-hash-check']}" == 'true' ] && [ -n "${st_userVars['hash-file']}" ]; then ps_failAndExit SYNOPSIS_NONCOMPL \ "You cannot combine $(term_boldify '--no-hash-check') and $(term_boldify '--hash-file')" fi # warnings (only with sudo) if ((EUID == 0)); then # Eject format if [ "${st_userFlags['no-eject']}" == true ] && [ "$targetAction" == format ]; then term_echowarn "You don't need to prevent device ejection through $(term_boldify '-J') flag with 'format'." fi # Warn autoselecting while assume yes is false if [ "${st_userFlags[autoselect]}" == true ] && [ "${st_userFlags['assume-yes']}" == false ]; then term_echowarn "$(term_boldify '-a, --autoselect') is enabled by default when $(term_boldify '-y, --asume-yes') is not set." fi if [[ -n "${st_userVars['data-part-fs']}" && -z "${st_userFlags['data-part']}" ]]; then term_echowarn "You set $(term_boldify '--data-part-fs') option but didn't set $(term_boldify -D). No data partition will be added." fi fi } #shellcheck disable=SC1087 #shellcheck disable=SC2086 function asrt_checkFlagMatrix() { local _key function _failAndHintUser() { local _matrixName=$1 local _allowedActionsRef="$_matrixName[$_key]" if [[ "$targetAction" == 'install-auto' ]] && sh_elementIsInList install-mount-rsync ${!_allowedActionsRef}; then ps_failAndExit SYNOPSIS_NONCOMPL \ "$(term_boldify "--$_key") modifier requires $(term_boldify '--mrsync') to assert Mount-Rsync mode." elif [[ "$targetAction" == 'install-auto' ]] && sh_elementIsInList install-image-copy ${!_allowedActionsRef}; then ps_failAndExit SYNOPSIS_NONCOMPL \ "$(term_boldify "--$_key") modifier requires $(term_boldify '--icopy') to assert Image-Copy mode." else ps_failAndExit SYNOPSIS_NONCOMPL \ "$(term_boldify "$targetAction") action doesn't support $(term_boldify "--$_key") modifier." fi } for _key in "${!asrt_userVarsCompatibilityMatrix[@]}"; do if [ ! -z "${st_userVars[$_key]}" ]; then if ! sh_elementIsInList "$targetAction" ${asrt_userVarsCompatibilityMatrix[$_key]}; then if [ "$_key" == "iso-file" ]; then ps_failAndExit SYNOPSIS_NONCOMPL \ "$(term_boldify $targetAction) doesn't require any positional arguments." else _failAndHintUser asrt_userVarsCompatibilityMatrix fi fi fi done for _key in "${!ct_userFlagsCompatibilityMatrix[@]}"; do if [ ! -z "${st_userFlags[$_key]}" ]; then if ! sh_elementIsInList "$targetAction" ${ct_userFlagsCompatibilityMatrix[$_key]}; then _failAndHintUser ct_userFlagsCompatibilityMatrix fi fi done } function asrt_inspectImageBootCapabilities() { local _uefiCompatible=${st_isoInspections[supportsEFIBoot]} local _syslinuxCompatible=false if [ "${st_isoInspections[supportsEFIBoot]}" == true ]; then if [ "$targetFilesystem" != "vfat" ]; then term_echowarn "Found UEFI boot capabilities but you selected '$targetFilesystem' type, which is not compatible with UEFI boot." \ "Be warned that only legacy boot might work, if any." else _uefiCompatible=true term_echogood "UEFI boot check validated. Your USB will work with UEFI boot." fi fi if [[ "$enableGPT" == true && "${st_isoInspections[supportsEFIBoot]}" == false ]]; then term_echowarn "$(term_boldify '--gpt') option ignored because image file solely supports legacy BIOS boot, which requires MBR." fi if [ ! -z "${st_isoInspections[syslinuxConf]}" ]; then _syslinuxCompatible=true st_shouldInstallSyslinux=true if [ ! -z "${st_isoInspections[syslinuxVer]}" ]; then term_echogood "Found SYSLINUX config file and binary at version ${st_isoInspections[syslinuxVer]}." elif [ ! -z "${st_isoInspections[syslinuxBin]}" ]; then term_echogood "Found SYSLINUX config file and binary with unknown version." else term_echogood "Found SYSLINUX config file." fi term_echogood "A SYSLINUX booloader will be installed on your USB device." fi if [[ "$_syslinuxCompatible" == false && "$_uefiCompatible" == false ]]; then ps_failAndExit MISSING_BOOT_CAP \ "The selected image is not hybrid, doesn't support UEFI or legacy booting with SYSLINUX." \ "Therefore, it cannot result in any successful booting with $scriptName." \ "$ct_openImageSupportRequestMessage" \ "In the meantime, consider following the instructions provided with this image file." fi } function asrt_checkSyslinuxInstall() { sys_checkCommand 'syslinux' if ! sys_hasCommand extlinux; then ps_failAndExit MISSING_DEPENDENCY \ "Your distribution doesn't ship 'extlinux' with the 'syslinux' package." \ "Please install 'extlinux' and try again." fi st_foundSyslinuxBiosFolder=$(find "$ct_syslinuxLibRoot" -type d -path '*/bios' -print -quit) st_foundSyslinuxMbrBinary=$(fs_findFileFromPatterns "$ct_syslinuxLibRoot" 'bios/mbr.bin' 'mbr.bin') if [ -z "$st_foundSyslinuxBiosFolder" ]; then ps_failAndExit MISSING_DEPENDENCY \ "Could not find a SYSLINUX bios folder containing c32 bios module files on this system." fi if [ -z "$st_foundSyslinuxMbrBinary" ]; then ps_failAndExit MISSING_DEPENDENCY "Could not find a SYSLINUX MBR binary on this system." fi } # # .o8 o8o # "888 `"' # .oooo888 .ooooo. oooo ooo oooo # d88' `888 d88' `88b `88. .8' `888 # 888 888 888ooo888 `88..8' 888 # 888 888 888 .o `888' 888 # `Y8bod88P" `Y8bod8P' `8' o888o # # # DEVICE AND IMAGES MODULE (3) function devi_configureLabel() { local _user _vendor targetPartitionLabel=${targetPartitionLabel:-$(blkid -o value -s LABEL -- "$sourceImageFile")} case $targetFilesystem in vfat) # Label to uppercase, otherwise some DOS systems won't work properly targetPartitionLabel=${targetPartitionLabel^^} # FAT32 labels have maximum 11 chars targetPartitionLabel=${targetPartitionLabel:0:11} ;; exfat) # EXFAT labels have maximum 15 chars targetPartitionLabel=${targetPartitionLabel:0:15} ;; ntfs) # NTFS labels have maximum 32 chars targetPartitionLabel=${targetPartitionLabel:0:32} ;; ext2 | ext3 | ext4) # EXT labels have maximum 16 chars targetPartitionLabel=${targetPartitionLabel:0:16} ;; f2fs) # F2FS labels have maximum 512 glyphs # approximated with 512 chars targetPartitionLabel=${targetPartitionLabel:0:512} ;; *) term_echowarn "Unexpected partition type '$targetFilesystem'." "$ct_openBugReportMessage" ;; esac # Fallback to "USER_VENDOR" if format if [[ "$targetAction" == format ]]; then _user=${SUDO_USER:-$USER} _vendor=$(lsblk -ldno VENDOR "$targetDevice" 2> /dev/null) _vendor=${_vendor:-FLASH} targetPartitionLabel=${targetPartitionLabel:-"${_user^^}_${_vendor^^}"} else targetPartitionLabel=${targetPartitionLabel:-''} fi if [[ -z "${st_userVars['label']}" && -n "$targetPartitionLabel" ]]; then term_echogood "Partition label automatically set to '$targetPartitionLabel'." \ "You can explicitly set the label with $(term_boldify '-L, --label')." elif [[ -n "$targetPartitionLabel" ]]; then term_echogood "Partition label manually set to '$targetPartitionLabel'." fi } function devi_selectDevice() { function _chooseDevice() { local _answer term_echoinfo "Select the device corresponding to the USB device you want to make bootable: $(sh_joinBy ',' "${st_devicesList[@]}")" \ "\n${term_logPrefixEmpty}Type CTRL+D to quit." read -r -p "${term_logPrefixEmpty}Select device id> " _answer echo if sh_elementIsInList "$_answer" "${st_devicesList[@]}"; then targetDevice="/dev/$_answer" else if sh_elementIsInList "$_answer" "" "exit"; then term_echoinfo "Exiting on user request." exit 0 else ps_failAndExit DEVICE_NOEXIST "The drive $_answer does not exist." fi fi } function _handleDeviceSelection() { local _selectedDeviceBlock if [ ${#st_devicesList[@]} -eq 1 ] && [ "$disableUSBCheck" == false ]; then # autoselect if [ "$disableConfirmation" == false ] || { [ "$disableConfirmation" == 'true' ] && [ "$enableAutoselect" == 'true' ] }; then _selectedDeviceBlock="${st_devicesList[0]}" term_echogood "Autoselecting '$_selectedDeviceBlock' (only USB device candidate)" targetDevice="/dev/$_selectedDeviceBlock" else _chooseDevice fi else _chooseDevice fi } if [ -z "$targetDevice" ]; then # List all hard disk drives if exec_listUSBDrives; then _handleDeviceSelection else ps_failAndExit NO_DEVICES fi fi st_targetPartition="${targetDevice}1" } # return 0 - OK # return 1 - failed appending partition table # return 2 - failed formatting partition function devi_addDataPartition() { local _dataPartition _diskReport _sectorSize _partSize _humanSize _partScheme _partType _diskReport=$(sfdisk -lJ "$targetDevice") _partScheme=$(echo "$_diskReport" | jq -r '.partitiontable.label') _partType=$(fs_inferFSType "$_partScheme" "$targetDataPartFstype") sfdisk --append "$targetDevice" < <(echo "-,-,$_partType") |& term_indentAll || return 1 _diskReport=$(sfdisk -lJ "$targetDevice") _dataPartition=$(echo "$_diskReport" | jq -r '.partitiontable.partitions[-1].node') fs_formatPartition "$targetDataPartFstype" "$_dataPartition" "DATA" || return 2 _sectorSize=$(echo "$_diskReport" | jq -r '.partitiontable.sectorsize') _partSize=$(echo "$_diskReport" | jq -r '.partitiontable.partitions[-1].size') _humanSize=$(numfmt --to=iec-i --suffix=B $((_sectorSize * _partSize))) term_echogood "Created $targetDataPartFstype data partition $_dataPartition of size $_humanSize" } # $1: partScheme - MBR or GPT # $2: notBootable - true or false function devi_createPartitionTable() { local -r _partScheme=$1 local -r _notBootable=$2 local _partitionOptions local _sfdiskCommand='sfdisk' local _sfdiskVersion _sfdiskVersion=$(sfdisk -v | grep -Po '\d+\.\d+') function _makeSfdiskCommand() { # Retro compatibility for 'old' sfdisk versions if sh_compute "$_sfdiskVersion >= 2.28"; then _sfdiskCommand='sfdisk -W always' fi } function _initGPT() { local _partitionType=${targetPartitionScheme:-$(fs_inferFSType gpt "$targetFilesystem")} _partitionOptions="label: gpt\n" if [ "$_notBootable" == false ]; then # Windows UEFI boot partitions must be of type "Windows Data Partition", # otherwise bug with "Drivers not found" if [[ "${st_isoInspections[supportsEFIBoot]}" == true && "${st_isoInspections[hasWimFile]}" == false ]]; then # Set typecode to EFI System Partition _partitionType="${fs_gptPartitionCodes[efi]}" else ps_failAndExit STATE_ERROR "(devi_createPartitionTable) GPT partition tables are not compatible with legacy BIOS boot." fi fi _partitionOptions+="type=${_partitionType}" } function _initMBR() { local _partitionType=${targetPartitionScheme:-$(fs_inferFSType mbr "$targetFilesystem")} _partitionOptions="label: dos\n" _partitionOptions+="$st_targetPartition : start=2048, type=$_partitionType" } _makeSfdiskCommand case "$_partScheme" in GPT) _initGPT ;; MBR) _initMBR ;; *) ps_failAndExit FAILED_POSTULATE "(devi_createPartitionTable) Unexpected partition scheme '$1'" ;; esac if [[ "$_notBootable" == false && "$_partScheme" == MBR && "${st_isoInspections[supportsBIOSBoot]}" == true ]]; then _partitionOptions+=", bootable" fi term_echogood "Creating $_partScheme partition table with 'sfdisk' v$_sfdiskVersion..." echo -e "$_partitionOptions" | $_sfdiskCommand "$targetDevice" |& term_indentAll || ps_failAndExit IO_ERROR \ "Failed to write USB device $_partScheme partition table." partx -u "$targetDevice" # Refresh partition table fs_syncdev } # $1: notBootable - true or false, default false function devi_partitionUSB() { local -r _notBootable=${1:-false} local _partScheme function _shouldWipeUSBKey() { local _answer='y' term_echowarn "About to wipe the content of device '$targetDevice'." if [ "$disableConfirmation" == false ]; then read -r -p "${term_logPrefixEmpty}Are you sure you want to proceed? (y/n)> " _answer else term_echowarn "Bypassing confirmation with $(term_boldify '-y, --assume-yes')." fi if [ "$_answer" == 'y' ]; then return 0 else return 1 fi } function _unmountPartitions() { local _partition # unmount any partition on selected device mapfile -t devicePartitions < <(grep -oP "^\\K$targetDevice\\S*" /proc/mounts) for _partition in "${devicePartitions[@]}"; do if ! umount "$_partition" > /dev/null; then ps_failAndExit IO_ERROR \ "Failed to unmount $_partition. It's likely that the partition is busy." fi done } function _eraseDevice() { term_echoinfo "Erasing contents of '$targetDevice'..." # clean signature from selected device wipefs --all --force "$targetDevice" &> /dev/null # erase drive dd if=/dev/zero of="$targetDevice" bs=512 count=1 conv=notrunc status=none |& term_indentAll || ps_failAndExit IO_ERROR "Failed to erase USB device." \ "It's likely that the device has been ejected and needs to be reconnected." \ "You can check the availability of USB drives with $(term_boldify "$scriptName -l")." fs_syncdev } if _shouldWipeUSBKey; then _unmountPartitions _eraseDevice if [ "$st_shouldMakePartition" == 'true' ]; then if [[ "$enableGPT" == true && (${st_isoInspections[supportsEFIBoot]} == true || "$_notBootable" == true) ]]; then _partScheme=GPT else _partScheme=MBR fi devi_createPartitionTable $_partScheme "$_notBootable" fs_formatPartition "$targetFilesystem" "$st_targetPartition" "$targetPartitionLabel" fi else ps_failAndExit USER_ABORTED "Canceling operation." fi } function devi_installSyslinuxVersion() { local -r _desiredSyslinuxVersion=$1 local _versions local _minor local _filename local _syslinuxArchive local _assetURL local _status local _abortingMessage="Aborting SYSLINUX installation and resuming with local install." function _checkConnexion() { _status=$(curl -sLIo /dev/null -w "%{http_code}" "$ct_kernelOrgSyslinuxURL") if [ "$_status" != 200 ]; then if [ "$_status" == 000 ]; then ps_failAndExit HOST_UNREACHABLE "kernel.org is unreachable. You don't seem to have an internet connection." \ "Please try again later or use $(term_boldify '--local-bootloader') to force usage of the local SYSLINUX version." return 9 else term_echowarn "Couldn't GET $ct_kernelOrgSyslinuxURL." \ "Received status code '$_status'." \ "$ct_openBugReportMessage" \ "$_abortingMessage" return 10 fi fi return 0 } function _findMinorVersions() { _versions="$(curl -sL "$ct_kernelOrgSyslinuxURL" | grep -oP 'href="\K\d+\.\d+(?=/")' | sort --version-sort)" if (($? != 0)); then term_echowarn "Couldn't GET $ct_kernelOrgSyslinuxURL." \ "Aborting syslinux installation and resuming with local install." return 10 elif [ -z "$_versions" ]; then term_echoerr "Couldn't parse the result of $ct_kernelOrgSyslinuxURL." \ "This is not expected: please open a ticket at $ct_ticketsURL." \ "$_abortingMessage" return 11 fi _minor=$(echo "$_versions" | grep -E "^$_desiredSyslinuxVersion" | grep "^${_desiredSyslinuxVersion%.}" | tail -n 1) if [ -z "$_minor" ]; then term_echoerr "Version '$_desiredSyslinuxVersion' is not available at kernel.org." return 8 fi return 0 } function _findMatchedRelease() { _filename=$(curl -sL "$ct_kernelOrgSyslinuxURL/$_minor/" | grep -oP 'href="\Ksyslinux-\d+\.\d+-\w+\d+\.tar\.gz(?=")' | sort --version-sort | tail -n1) if [ -z "$_filename" ]; then term_echoerr "Couldn't find '$_filename'." return 11 fi _assetURL="$ct_kernelOrgSyslinuxURL/$_minor/$_filename" _syslinuxArchive=$ct_cacheRoot/$_filename return 0 } function _downloadMatchedVersion() { if [ -e "$_syslinuxArchive" ]; then term_echogood "Found '$_syslinuxArchive' in cache." return 0 fi if curl -sL -o "$_syslinuxArchive" "$_assetURL"; then if [ -f "$_syslinuxArchive" ]; then term_echogood "Download of '$_syslinuxArchive' completed ($(du -h "$_syslinuxArchive" | awk '{print $1}'))" else term_echowarn "Missing file '$_syslinuxArchive'." \ "This is not expected: please open a ticket at $ct_ticketsURL." \ "$_abortingMessage" return 10 fi else term_echowarn "Couldn't get '$_assetURL'." \ "This is not expected: please open a ticket at $ct_ticketsURL." \ "$_abortingMessage" return 10 fi return 0 } function _extractMatchedVersion() { if tar -xf "$_syslinuxArchive" -C "$ct_tempRoot"; then syslinuxInstallDir="$ct_tempRoot/$(basename "${_syslinuxArchive%.tar.gz}")" st_temporaryAssets+=("$syslinuxInstallDir") else rm "$_syslinuxArchive" return 11 fi } function _configureSyslinuxInstall() { local _extlinuxBin local _mbrBin _extlinuxBin=$(fs_findFileFromPatterns "$syslinuxInstallDir" 'bios/extlinux/extlinux' 'extlinux/extlinux' 'extlinux') _mbrBin=$(fs_findFileFromPatterns "$syslinuxInstallDir" 'bios/mbr/mbr.bin' 'mbr/mbr.bin' 'mbr.bin') if [ -z "$_extlinuxBin" ]; then term_echowarn "Couldn't find 'extlinux' binary in installation folder." \ "$_abortingMessage" return 10 fi if [ -z "$_mbrBin" ]; then term_echowarn "Couldn't find 'mbr.bin' in installation folder." \ "$_abortingMessage" return 10 fi st_syslinuxBinaries['mbrBin']="$_mbrBin" st_syslinuxBinaries['extBin']="$_extlinuxBin" return 0 } sys_checkCommand 'curl' _inferSyslinuxVersion _checkConnexion || return "$?" _findMinorVersions || return "$?" _findMatchedRelease || return "$?" _downloadMatchedVersion || return "$?" _extractMatchedVersion || return "$?" _configureSyslinuxInstall || return "$?" term_echogood "SYSLINUX version '$_minor' temporarily set for installation." } # $1 - mountPoint # $2 - inspect syslinux? true or false function devi_inspectPartition() { local -r _mountPoint="$1" local -r _shouldInspectSyslinux=$2 local _supportsEFIBoot _hasWimFile _syslinuxBin _syslinuxConf _syslinuxVer _supportsBIOSBoot _supportsEFIBoot=${st_isoInspections[supportsEFIBoot]:-false} _hasWimFile=${st_isoInspections[hasWimFile]:-false} _syslinuxBin=${st_isoInspections[syslinuxBin]} _syslinuxConf=${st_isoInspections[syslinuxConf]} _syslinuxVer=${st_isoInspections[syslinuxVer]} _supportsBIOSBoot=${st_isoInspections[supportsBIOSBoot]} local -a _sysLinuxLocations=('boot/syslinux/syslinux.cfg' 'syslinux/syslinux.cfg' 'syslinux.cfg' 'boot/syslinux/extlinux.conf' 'boot/syslinux/extlinux.cfg' 'boot/extlinux/extlinux.conf' 'boot/extlinux/extlinux.cfg' 'syslinux/extlinux.conf' 'syslinux/extlinux.cfg' 'extlinux/extlinux.conf' 'extlinux/extlinux.cfg' 'extlinux.conf' 'extlinux.cfg') local -a _isoLinuxLocations=('boot/isolinux/isolinux.cfg' 'isolinux/isolinux.cfg' 'isolinux.cfg' 'boot/syslinux/isolinux.cfg' 'syslinux/isolinux.cfg') function _inspectSyslinux() { _syslinuxBin=$(fs_matchFirstExpression "$_mountPoint" 'syslinux.bin' 'isolinux.bin' 'extlinux.bin' 'boot.bin' 'extlinux' 'syslinux' 'isolinux') if [ ! -z "$_syslinuxBin" ]; then _syslinuxVer=$(strings "$_syslinuxBin" | grep -E 'ISOLINUX|SYSLINUX|EXTLINUX' | grep -oP '(\d+\.\d+)' | awk 'NR==1{print $1}') fi _syslinuxConf=$(fs_findFileFromPatterns "$_mountPoint" "${_sysLinuxLocations[@]}") if [ -z "$_syslinuxConf" ]; then _syslinuxConf=$(fs_matchFirstExpression "$_mountPoint" 'syslinux.cfg' 'extlinux.conf' 'extlinux.cfg') fi if [ -z "$_syslinuxConf" ]; then _syslinuxConf=$(fs_findFileFromPatterns "$_mountPoint" "${_isoLinuxLocations[@]}") _syslinuxConf=${_syslinuxConf:-$(fs_firstMatchInFolder "$_mountPoint" 'isolinux.cfg')} fi if [ -n "$_syslinuxConf" ]; then _supportsBIOSBoot=true fi } function _inspectEFICapabilities() { local _hasEfiRoot local _hasEfiFile _hasEfiRoot=$(find "$_mountPoint" -type d -iname 'efi' -print -quit) _hasEfiFile=$(find "$_mountPoint" -type f -ipath '*/efi/*.efi' -prune -print -quit) if [ ! -z "$_hasEfiFile" ] && [ ! -z "$_hasEfiRoot" ]; then _supportsEFIBoot=true fi } function _inspectWindows() { if [[ -e "$_mountPoint/sources/install.wim" ]]; then _hasWimFile=true fi } _inspectEFICapabilities _inspectWindows st_isoInspections[supportsEFIBoot]=$_supportsEFIBoot st_isoInspections[hasWimFile]=$_hasWimFile if [[ $_shouldInspectSyslinux == true ]]; then _inspectSyslinux st_isoInspections[syslinuxConf]="${_syslinuxConf#$_mountPoint}" st_isoInspections[syslinuxBin]="${_syslinuxBin#$_mountPoint}" st_isoInspections[syslinuxVer]="$_syslinuxVer" st_isoInspections[supportsBIOSBoot]="$_supportsBIOSBoot" fi } function devi_inspectHybridImage() { local _diskReport _partScheme local _supportsEFIBoot _supportsBIOSBoot function _inspectMBRPartTable() { local _startSector # Look for the first ESP partition _startSector=$(echo "$_diskReport" | jq ".partitiontable.partitions | map(select(.type==\"${fs_mbrPartitionCodes[efi]}\"))[0].start | select (.!=null)") # If ESP found, look for first bootable if [[ -n "$_startSector" ]]; then _supportsEFIBoot=true else _supportsEFIBoot=false fi _startSector=$(echo "$_diskReport" | jq '.partitiontable.partitions | map(select(.bootable==true))[0].start | select (.!=null)') if [[ -n "$_startSector" ]]; then _supportsBIOSBoot=true else _supportsBIOSBoot=false fi } function _inspectGPTPartTable() { local _startSector # In GPT mode, UEFI is theoretically the only possible boot mode _startSector=$(echo "$_diskReport" | jq ".partitiontable.partitions | map(select(.type==\"${fs_gptPartitionCodes[efi]}\"))[0].start | select (.!=null)") _supportsBIOSBoot=false if [[ -n "$_startSector" ]]; then _supportsEFIBoot=true else _supportsEFIBoot=false fi } _diskReport=$(sfdisk -lJ -- "$sourceImageFile" 2> /dev/null) \ || ps_failAndExit IO_ERROR "sfdisk couldn't read the partition table on the image file, which is likely corrupted." _partScheme=$(echo "$_diskReport" | jq -r '.partitiontable.label') case $_partScheme in dos) _inspectMBRPartTable ;; gpt) _inspectGPTPartTable ;; esac st_isoInspections[supportsBIOSBoot]="$_supportsBIOSBoot" st_isoInspections[supportsEFIBoot]="$_supportsEFIBoot" } function devi_inspectElToritoImage() { local _mountPoint _mountPoint=$(fs_createMountFolder iso) || exit "$?" fs_mountElToritoFile "$_mountPoint" devi_inspectPartition "$_mountPoint" true fs_umountPartition "$_mountPoint" } # . # .o8 # .oooo.o .o888oo .ooooo. oo.ooooo. # d88( "8 888 d88' `88b 888' `88b # `"Y88b. 888 888ooo888 888 888 # o. )88b 888 . 888 .o 888 888 # 8""888P' "888" `Y8bod8P' 888bod8P' # 888 # o888o # # STEPS MODULE (3) function step_initProcess() { ps_normalizePath } function step_configureFolders() { local _defaultMode=775 if [ ! -e "$ct_tempRoot" ]; then mkdir -m $_defaultMode "$ct_tempRoot" elif [ -d "$ct_tempRoot" ]; then chmod -R $_defaultMode "$ct_tempRoot" else ps_failAndExit FAILED_POSTULATE "'$ct_tempRoot' is not a folder." \ "Remove this file and try again." fi if [ ! -e "$ct_cacheRoot" ]; then mkdir -m $_defaultMode "$ct_cacheRoot" elif [ -d "$ct_cacheRoot" ]; then chmod -R $_defaultMode "$ct_cacheRoot" else ps_failAndExit FAILED_POSTULATE "'$ct_cacheRoot' is not a folder." \ "Remove this file and try again." fi if [ ! -e "$ct_mountRoot" ]; then # shellcheck disable=SC2174 mkdir -pm $_defaultMode "$ct_mountRoot" elif [ ! -d "$ct_mountRoot" ]; then ps_failAndExit FAILED_POSTULATE "'$ct_mountRoot' is not a folder." \ "Remove this file and try again." fi } function step_initDevicesList() { local -a _devices local _device mapfile -t _devices < <(lsblk -dno NAME) st_devicesList=() for _device in "${_devices[@]}"; do if [ "$(sys_getDeviceType "/dev/$_device")" == "usb" ] || [ "$disableUSBCheck" == 'true' ]; then st_devicesList+=("$_device") fi done } function step_inspectImageFile() { local _isHybrid function _inspectImageFilesystem() { file -b -- "$sourceImageFile" | grep -q '^ISO 9660 CD-ROM filesystem' if (($? == 0)); then _isHybrid=false else _isHybrid=true fi } _inspectImageFilesystem if [[ $_isHybrid == true ]]; then devi_inspectHybridImage else devi_inspectElToritoImage fi st_isoInspections[isHybrid]=$_isHybrid } function step_installBootloader() { local _syslinuxFolder local _syslinuxConfig local _localSyslinuxVersion function _inferSyslinuxVersion() { _localSyslinuxVersion=$(syslinux --version |& grep -oP '(\d+\.\d+)') if [ "$targetBootloaderVersion" != 'auto' ]; then st_targetSyslinuxVersion="$targetBootloaderVersion" else st_targetSyslinuxVersion=${st_isoInspections[syslinuxVer]:-"$_localSyslinuxVersion"} fi } # return 0 - install from kernel.org # return 1+ - install from local function _checkSyslinuxVersion() { local -i _versionsMatch=0 if [ "$enableForceLocalBootloader" == true ]; then term_echogood "Enforced local SYSLINUX bootloader at version '$_localSyslinuxVersion'." return 1 fi if [ -z "$st_targetSyslinuxVersion" ]; then return 1 fi if [ "$targetBootloaderVersion" != 'auto' ]; then term_echoinfo "Searching for SYSLINUX V$st_targetSyslinuxVersion remotely." if ! devi_installSyslinuxVersion "$st_targetSyslinuxVersion"; then if [ ! -z "${st_isoInspections[syslinuxVer]}" ]; then term_echowarn "Falling back to image SYSLINUX version '${st_isoInspections[syslinuxVer]}'" st_targetSyslinuxVersion="${st_isoInspections[syslinuxVer]}" devi_installSyslinuxVersion "${st_isoInspections[syslinuxVer]}" return $? else return 1 fi else return 0 fi fi term_echoinfo "Found local SYSLINUX version '$_localSyslinuxVersion'" sh_compute "$_localSyslinuxVersion == ${st_isoInspections[syslinuxVer]}" > /dev/null _versionsMatch=$? if ((_versionsMatch == 0)); then term_echogood "image SYSLINUX version matches local version." return 1 else term_echowarn "image SYSLINUX version doesn't match local version." \ "Scheduling download of version $st_targetSyslinuxVersion..." if ! devi_installSyslinuxVersion "$st_targetSyslinuxVersion"; then term_echowarn "Falling back to local SYSLINUX version '$_localSyslinuxVersion'." st_targetSyslinuxVersion="$_localSyslinuxVersion" return 1 fi fi } function _setSyslinuxLocation() { local _isoFolder local _isolinuxConfig if [[ "${st_isoInspections[syslinuxConf]}" =~ isolinux.cfg ]]; then _isolinuxConfig="$st_usbMountPoint${st_isoInspections[syslinuxConf]}" _isoFolder=$(dirname "$_isolinuxConfig") _syslinuxConfig="$_isoFolder/syslinux.cfg" mv "$_isolinuxConfig" "$_syslinuxConfig" term_echoinfo "Found ISOLINUX config file at '$_isolinuxConfig'." \ "Moving to '$_syslinuxConfig'." else _syslinuxConfig="$st_usbMountPoint${st_isoInspections[syslinuxConf]}" fi _syslinuxFolder=$(dirname "$_syslinuxConfig") } function _installWtLocalExtlinux() { st_syslinuxBinaries=(['mbrBin']="$st_foundSyslinuxMbrBinary" ['extBin']='extlinux') st_targetSyslinuxVersion="$_localSyslinuxVersion" term_echoinfo "Installing SYSLINUX bootloader in '$_syslinuxFolder' with local version '$st_targetSyslinuxVersion'..." rsync --no-links --no-perms --no-owner --no-group -I "$st_foundSyslinuxBiosFolder"/*.c32 "$_syslinuxFolder" |& term_indentAll || term_echowarn "SYSLINUX could not install C32 BIOS modules." fs_syncdev term_echogood "C32 BIOS modules successfully installed." ${st_syslinuxBinaries['extBin']} --stupid --install "$_syslinuxFolder" |& term_indentAll || ps_failAndExit THIRD_PARTY_ERROR \ "SYSLINUX bootloader could not be installed." fs_syncdev } function _installWtKernelOrgExtlinux() { local _isExt32 _isHost32 _fallbackToLocal=false term_echoinfo "Installing SYSLINUX bootloader in '$_syslinuxFolder' with kernel.org version '$st_targetSyslinuxVersion'..." file -b -- "${st_syslinuxBinaries['extBin']}" | awk '{print $2}' | grep -e '^32' &> /dev/null _isExt32=$? echo "$ct_architecture" | grep -e '32|i386' _isHost32=$? if ((_isExt32 == _isHost32)); then _fallbackToLocal=false elif ((_isExt32 == 0 && _isHost32 > 0)); then term_echowarn "The SYSLINUX binary from kernel.org is for 32 bits architectures," \ " but your system is 64 bits." \ "If the install fails, you might need to install 32 bits support on your system." _fallbackToLocal=false else # _isExt32 > 0 && _isHost32 == 0 term_echowarn "The SYSLINUX binary from kernel.org is for 64 bits architectures." \ "Your system is 32 bits. Falling back to local syslinux installation." _fallbackToLocal=true fi if [[ "$_fallbackToLocal" == false ]]; then if ! ${st_syslinuxBinaries['extBin']} --stupid --install "$_syslinuxFolder" |& term_indentAll; then term_echowarn "Could not run SYSLINUX '$st_targetSyslinuxVersion' from kernel.org." \ "Attempting with local SYSLINUX install..." _fallbackToLocal=true fi fi if [[ "$_fallbackToLocal" == true ]]; then _installWtLocalExtlinux > /dev/null fi fs_syncdev } function _installMasterBootRecordProg() { dd bs=440 count=1 conv=notrunc status=none if="${st_syslinuxBinaries['mbrBin']}" of="$targetDevice" |& term_indentAll || ps_failAndExit IO_ERROR "Failed to install Master Boot Record program." fs_syncdev term_echogood "Successfully installed Master Boot Record program." } _inferSyslinuxVersion _setSyslinuxLocation _checkSyslinuxVersion case $? in 0) _installWtKernelOrgExtlinux ;; *) _installWtLocalExtlinux ;; esac term_echogood "Successfully installed SYSLINUX bootloader at version '$st_targetSyslinuxVersion'." _installMasterBootRecordProg } function step_copyWithRsync() { function _rsyncWithProgress() { # _i defined for term_updateProgress local -i _i=1 local _statusFile local _wimFile="$st_elToritoMountPoint/sources/install.wim" local _rsyncOptions="" local _status _statusFile=$(fs_createTempFile "bootiso-rsync-status") st_temporaryAssets+=("$_statusFile") if [ -f "$_wimFile" ]; then if [ "$disableWimsplit" == false ]; then _rsyncOptions="--exclude sources/install.wim" term_echogood "Detected a Windows install.wim file, which will be handled by 'wimlib-imagex' utility." else term_echowarn "Detected a Windows install.wim file but wimsplit has been disabled with $(term_boldify '--no-wim-split') option." fi fi ( # shellcheck disable=SC2086 rsync -r -q -I --no-links --no-perms --no-owner --no-group $_rsyncOptions "$st_elToritoMountPoint"/. "$st_usbMountPoint" _status=$? term_cleanProgress if ((_status == 0)) && [ -f "$_wimFile" ] && [ "$disableWimsplit" == false ]; then echo wimlib-imagex split "$_wimFile" "$st_usbMountPoint/sources/install.swm" 1024 |& term_indentAll fi echo "$_status" > "$_statusFile" ) & st_backgroundProcess=$! echo -n "$scriptName: Copying files from image to USB device with 'rsync' " while [ -e "/proc/$st_backgroundProcess" ]; do term_updateProgress done st_backgroundProcess='' term_cleanProgress _status=$(cat "$_statusFile") if [ ! "$_status" -eq 0 ]; then ps_failAndExit IO_ERROR "Copy command with 'rsync' failed." fi } sys_checkCommand 'rsync' _rsyncWithProgress fs_syncWithProgress } function step_copyWithDD() { function _ddWithProgress() { local -i _i=1 local _statusFile local _status _statusFile=$(fs_createTempFile "bootiso-status") st_temporaryAssets+=("$_statusFile") ( dd if="$sourceImageFile" of="$targetDevice" bs="$targetDDBusSize" status=none echo "$?" > "$_statusFile" ) & st_backgroundProcess=$! echo -n "$scriptName: Copying files from image to USB device with 'dd' " while [ -e "/proc/$st_backgroundProcess" ]; do term_updateProgress done st_backgroundProcess='' term_cleanProgress _status=$(cat "$_statusFile") if [ ! "$_status" -eq 0 ]; then ps_failAndExit IO_ERROR "Copy command with 'dd' failed." fi } _ddWithProgress fs_syncWithProgress } function step_initPckgManager() { if sys_hasCommand apt-get; then # Debian st_packageManager="apt-get install" return 0 fi if sys_hasCommand dnf; then # Fedora st_packageManager="dnf install" return 0 fi if sys_hasCommand yum; then # Fedora st_packageManager="yum install" return 0 fi if sys_hasCommand pacman; then # Arch st_packageManager="pacman -S" return 0 fi if sys_hasCommand zypper; then # OpenSuse st_packageManager="zypper install" return 0 fi if sys_hasCommand emerge; then # Gentoo st_packageManager="emerge" return 0 fi if sys_hasCommand xbps-install; then # Void st_packageManager="xbps-install" return 0 fi if sys_hasCommand eopkg; then # Solus st_packageManager="eopkg install" return 0 fi return 1 } function step_parseArguments() { local _key local _isEndOfOptions=false local _wrongOptions local _options local -a _extractedOptions function _enableUserFlag() { st_userFlags["$1"]=true } function _setUserVar() { st_userVars["$1"]=$2 } while [[ $# -gt 0 ]]; do _key="$1" if [ "$_isEndOfOptions" == false ]; then case $_key in # ACTIONS -h | --help | help) _enableUserFlag 'help' shift ;; -v | --version) _enableUserFlag 'version' shift ;; -l | --list-usb-drives) _enableUserFlag 'list-usb-drives' shift ;; -p | --probe) _enableUserFlag 'probe' shift ;; -f | --format) _enableUserFlag 'format' shift ;; -i | --inspect) _enableUserFlag 'inspect' shift ;; --dd | --icopy) _enableUserFlag 'install-image-copy' shift ;; --mrsync) _enableUserFlag 'install-mount-rsync' shift ;; # OPTIONS --dd-bs) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL \ "Missing value for '$1' flag. Please provide a number of bytes, see dd(1)." fi _setUserVar 'dd-bs' "$2" shift 2 ;; -D | --data-part) _enableUserFlag 'data-part' shift ;; --local-bootloader) _enableUserFlag 'local-bootloader' shift ;; --remote-bootloader) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL \ "Missing value for '$1' flag. Please provide a version following MAJOR.MINOR pattern. ex: '4.10'." fi _setUserVar 'remote-bootloader' "$2" shift 2 ;; --gpt) _enableUserFlag 'gpt' shift ;; -y | --assume-yes) _enableUserFlag 'assume-yes' shift ;; -d | --device) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a device." fi _setUserVar 'device' "$2" shift 2 ;; --part-type) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a partition type." fi _setUserVar 'part-type' "${2}" shift 2 ;; -t | --type | -F | --fs) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a filesystem type." fi _setUserVar 'fs' "${2,,}" #lowercased shift 2 ;; --data-part-fs) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a filesystem type." fi _setUserVar 'data-part-fs' "${2,,}" #lowercased shift 2 ;; -L | --label) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a label." fi _setUserVar 'label' "$2" shift 2 ;; --hash-file) if (($# < 2)); then ps_failAndExit SYNOPSIS_NONCOMPL "Missing value for '$1' flag. Please provide a hash file." fi _setUserVar 'hash-file' "$2" shift 2 ;; -J | --no-eject) _enableUserFlag 'no-eject' shift ;; -H | --no-hash-check) _enableUserFlag 'no-hash-check' shift ;; -a | --autoselect) _enableUserFlag 'autoselect' shift ;; -M | --no-mime-check) _enableUserFlag 'no-mime-check' shift ;; --no-usb-check) _enableUserFlag 'no-usb-check' shift ;; --no-size-check) _enableUserFlag 'no-size-check' shift ;; --force-hash-check) _enableUserFlag 'force-hash-check' shift ;; --no-wimsplit) _enableUserFlag 'no-wimsplit' shift ;; --) _isEndOfOptions=true shift ;; -*) if [[ "$_isEndOfOptions" == false ]]; then if [[ "$_key" =~ ^-- ]]; then ps_failAndExit SYNOPSIS_NONCOMPL "Unknown option: $(term_boldify "$_key")." # Handle stacked options elif [[ "$_key" =~ ^-["$ct_shortOptions"]{2,}$ ]]; then shift _options=${_key#*-} mapfile -t _extractedOptions < <(echo "$_options" | grep -o . | xargs -d '\n' -n1 printf '-%s\n') set -- "${_extractedOptions[@]}" "$@" else printf "\\e[0;31m%s\\e[m" "$scriptName: Unknown options: " printf '%s.' "$_key" | GREP_COLORS='mt=00;32:sl=00;31' grep --color=always -P "[$ct_shortOptions]" if [[ "$_key" =~ ^-[a-zA-Z0-9]+$ ]]; then _wrongOptions=$(printf '%s' "${_key#*-}" | grep -Po "[^$ct_shortOptions]" | tr -d '\n') if [ ${#_key} -eq 2 ]; then term_echowarn "$(term_boldify "$_wrongOptions") flag were not recognized." else term_echowarn "$(term_boldify "$_wrongOptions") flags were not recognized." fi fi ps_failAndExit SYNOPSIS_NONCOMPL fi else _setUserVar 'iso-file' "$1" shift fi ;; *) _setUserVar 'iso-file' "$1" shift ;; esac else _setUserVar 'iso-file' "$1" break fi done } function step_assignInternalVariables() { # Command argument sourceImageFile=${st_userVars['iso-file']:-''} # Option flags disableConfirmation=${st_userFlags['assume-yes']:-'false'} enableAutoselect=${st_userFlags[autoselect]:-'false'} enableGPT=${st_userFlags[gpt]:-'false'} enableForceLocalBootloader=${st_userFlags['local-bootloader']:-'false'} disableMimeCheck=${st_userFlags['no-mime-check']:-'false'} disableUSBCheck=${st_userFlags['no-usb-check']:-'false'} disableSizeCheck=${st_userFlags['no-size-check']:-'false'} disableHashCheck=${st_userFlags['no-hash-check']:-'false'} enableForceHashCheck=${st_userFlags['force-hash-check']:-'false'} disableWimsplit=${st_userFlags['no-wimsplit']:-'false'} enableDataPart=${st_userFlags['data-part']:-'false'} # Vars flags targetFilesystem=${st_userVars[fs]:-'vfat'} targetPartitionScheme=${st_userVars['part-type']} sourceHashFile=${st_userVars['hash-file']:-''} targetDevice=${st_userVars[device]:-''} targetPartitionLabel=${st_userVars[label]:-''} targetBootloaderVersion=${st_userVars['remote-bootloader']:-'auto'} targetDDBusSize=${st_userVars['dd-bs']:-'4M'} targetDataPartFstype=${st_userVars['data-part-fs']:-'vfat'} # Action-dependent flags case $targetAction in install-*) st_hasActionDuration='true' st_expectingISOFile='true' requiresRoot='true' disableDeviceEjection=${st_userFlags['no-eject']:-'false'} ;; format) st_hasActionDuration='true' st_expectingISOFile='false' requiresRoot='true' ;; version) st_hasActionDuration='false' st_expectingISOFile='false' requiresRoot='false' ;; help | list-usb-drives) st_hasActionDuration='false' st_expectingISOFile='false' requiresRoot='false' ;; inspect | probe) st_hasActionDuration='false' st_expectingISOFile='true' requiresRoot='true' ;; *) ps_failAndExit STATE_ERROR "Unhandled action $(term_boldify "$targetAction")." ;; esac } function step_runSecurityAssessments() { devi_configureLabel devi_selectDevice ps_startTimer asrt_checkDeviceIsOK "$targetDevice" asrt_checkDeviceIsUSB asrt_checkImageSize "$targetDevice" "$sourceImageFile" } function step_checkArguments() { asrt_checkAction asrt_checkUserVars asrt_checkUserFlags } function step_finalize() { if [ "$st_hasActionDuration" == 'true' ]; then ps_stopTimerAndPrintLapsed fi st_completedAction=$targetAction } # .ooooo. oooo ooo .ooooo. .ooooo. # d88' `88b `88b..8P' d88' `88b d88' `"Y8 # 888ooo888 Y888' 888ooo888 888 # 888 .o .o8"'88b 888 .o 888 .o8 # `Y8bod8P' o88' 888o `Y8bod8P' `Y8bod8P' # # ACTION EXEC MODULE (4) # Print stdin properly # $1 : terminal width function term_formatFlagDescription() { local -r _termWidth=$1 if [[ "$st_hasLegacyColumn" == false ]]; then column -c "$_termWidth" --table -d -N flag,desc -W desc -s \| else column -c "$_termWidth" -t -s \| fi } function exec_help() { local _termWidth local -r _actionFlagsTable=$(printf "%s\n" \ "-f, --format|Format selected USB drive and exit." \ "-h, --help|Display this help message and exit." \ "-i, --inspect|Inspect boot capabilities." \ "-l, --list-usb-drives|List available USB drives and exit." \ "-p, --probe|Equivalent to -i followed by -l actions.") local -r _modifierFlagsTable=$(printf "%s\n" \ "-a, --autoselect|In combination with -y, autoselect USB drive when only one is connected." \ "-d, --device |Pick block file as target USB drive." \ "-D, --data-part|Add a data partition." \ "-F, --fstype |Format to ." \ "-H, --no-hash-check|Don't search for hash files and check integrity." \ "-J, --no-eject|Don't eject drive after unmounting." \ "-L, --label