#!/usr/bin/env bash # ## Wizard Kit: UFD Build Tool # # Based on a template by BASH3 Boilerplate v2.3.0 # http://bash3boilerplate.sh/#authors # # The MIT License (MIT) # Copyright (c) 2013 Kevin van Zonneveld and contributors # You are not obligated to bundle the LICENSE file with your b3bp projects as long # as you leave these references intact in the header comments of your source files. # Exit on error. Append "|| true" if you expect an error. set -o errexit # Exit on error inside any functions or subshells. set -o errtrace # Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR set -o nounset # Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip` set -o pipefail # Turn on traces, useful while debugging but commented out by default # set -o xtrace if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then __i_am_main_script="0" # false if [[ "${__usage+x}" ]]; then if [[ "${BASH_SOURCE[1]}" = "${0}" ]]; then __i_am_main_script="1" # true fi __b3bp_external_usage="true" __b3bp_tmp_source_idx=1 fi else __i_am_main_script="1" # true [[ "${__usage+x}" ]] && unset -v __usage [[ "${__helptext+x}" ]] && unset -v __helptext fi # Set magic variables for current file, directory, os, etc. __dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)" __file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" __base="$(basename "${__file}" .sh)" __wd="$(pwd)" __usage_example="Usage: ${0} --ufd-device [device] --linux-iso [path] --main-kit [path] --winpe-iso [path]" __all_args="" for a in "${@}"; do if [[ "${a:0:1}" == "-" ]]; then __all_args="${__all_args} ${a}" else __all_args="${__all_args} \"${a}\"" fi done # Define the environment variables (and their defaults) that this script depends on LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected ### Functions ############################################################################## function __b3bp_log () { local log_level="${1}" shift # shellcheck disable=SC2034 local color_debug="\x1b[35m" # shellcheck disable=SC2034 local color_info="\x1b[32m" # shellcheck disable=SC2034 local color_notice="\x1b[34m" # shellcheck disable=SC2034 local color_warning="\x1b[33m" # shellcheck disable=SC2034 local color_error="\x1b[31m" # shellcheck disable=SC2034 local color_critical="\x1b[1;31m" # shellcheck disable=SC2034 local color_alert="\x1b[1;33;41m" # shellcheck disable=SC2034 local color_emergency="\x1b[1;4;5;33;41m" local colorvar="color_${log_level}" local color="${!colorvar:-${color_error}}" local color_reset="\x1b[0m" if [[ "${NO_COLOR:-}" = "true" ]] || ( [[ "${TERM:-}" != *"256color"* ]] && [[ "${TERM:-}" != "xterm"* ]] && [[ "${TERM:-}" != "screen"* ]] ) || [[ ! -t 2 ]]; then if [[ "${NO_COLOR:-}" != "false" ]]; then # Don't use colors on pipes or non-recognized terminals color=""; color_reset="" fi fi # all remaining arguments are to be printed local log_line="" while IFS=$'\n' read -r log_line; do echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" 1>&2 done <<< "${@:-}" } function emergency () { __b3bp_log emergency "${@}"; exit 1; } function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; } function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; } function error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}"; true; } function warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}"; true; } function notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}"; true; } function info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}"; true; } function debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}"; true; } function help () { echo "" 1>&2 echo " ${*}" 1>&2 echo "" 1>&2 echo " ${__usage:-No usage available}" 1>&2 echo "" 1>&2 if [[ "${__helptext:-}" ]]; then echo " ${__helptext}" 1>&2 echo "" 1>&2 fi exit 1 } ### Parse commandline options ############################################################################## # Commandline options. This defines the usage page, and is used to parse cli # opts & defaults from. The parsing is unforgiving so be precise in your syntax # - A short option must be preset for every long option; but every short option # need not have a long option # - `--` is respected as the separator between options and arguments # - We do not bash-expand defaults, so setting '~/app' as a default will not resolve to ${HOME}. # you can use bash variables to work around this (so use ${HOME} instead) # shellcheck disable=SC2015 [[ "${__usage+x}" ]] || read -r -d '' __usage <<-'EOF' || true # exits non-zero when EOF encountered OPTIONS: -u --ufd-device [arg] Device to which the kit will be applied -m --main-kit [arg] Path to the Main Kit -l --linux-iso [arg] Path to the Linux ISO -w --winpe-iso [arg] Path to the WinPE ISO -e --extra-dir [arg] Path to the Extra folder (optional) -h --help This page ADVANCED: -d --debug Enable debug mode -v --verbose Enable verbose mode -F --force Bypass all confirmation messages. USE WITH EXTREME CAUTION! EOF # shellcheck disable=SC2015 [[ "${__helptext+x}" ]] || read -r -d '' __helptext <<-'EOF' || true # exits non-zero when EOF encountered Paths can be relative to the current working directory or absolute EOF # Translate usage string -> getopts arguments, and set $arg_ defaults while read -r __b3bp_tmp_line; do if [[ "${__b3bp_tmp_line}" =~ ^- ]]; then # fetch single character version of option string __b3bp_tmp_opt="${__b3bp_tmp_line%% *}" __b3bp_tmp_opt="${__b3bp_tmp_opt:1}" # fetch long version if present __b3bp_tmp_long_opt="" if [[ "${__b3bp_tmp_line}" = *"--"* ]]; then __b3bp_tmp_long_opt="${__b3bp_tmp_line#*--}" __b3bp_tmp_long_opt="${__b3bp_tmp_long_opt%% *}" fi # map opt long name to+from opt short name printf -v "__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" '%s' "${__b3bp_tmp_opt}" printf -v "__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" '%s' "${__b3bp_tmp_long_opt//-/_}" # check if option takes an argument if [[ "${__b3bp_tmp_line}" =~ \[.*\] ]]; then __b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg __b3bp_tmp_init="" # it has an arg. init with "" printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "1" elif [[ "${__b3bp_tmp_line}" =~ \{.*\} ]]; then __b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg __b3bp_tmp_init="" # it has an arg. init with "" # remember that this option requires an argument printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2" else __b3bp_tmp_init="0" # it's a flag. init with 0 printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "0" fi __b3bp_tmp_opts="${__b3bp_tmp_opts:-}${__b3bp_tmp_opt}" fi [[ "${__b3bp_tmp_opt:-}" ]] || continue if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Default= ]]; then # ignore default value if option does not have an argument __b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" if [[ "${!__b3bp_tmp_varname}" != "0" ]]; then __b3bp_tmp_init="${__b3bp_tmp_line##*Default=}" __b3bp_tmp_re='^"(.*)"$' if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then __b3bp_tmp_init="${BASH_REMATCH[1]}" else __b3bp_tmp_re="^'(.*)'$" if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then __b3bp_tmp_init="${BASH_REMATCH[1]}" fi fi fi fi if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Required\. ]]; then # remember that this option requires an argument printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2" fi printf -v "arg_${__b3bp_tmp_opt:0:1}" '%s' "${__b3bp_tmp_init}" done <<< "${__usage:-}" # run getopts only if options were specified in __usage if [[ "${__b3bp_tmp_opts:-}" ]]; then # Allow long options like --this __b3bp_tmp_opts="${__b3bp_tmp_opts}-:" # Reset in case getopts has been used previously in the shell. OPTIND=1 # start parsing command line set +o nounset # unexpected arguments will cause unbound variables # to be dereferenced # Overwrite $arg_ defaults with the actual CLI options while getopts "${__b3bp_tmp_opts}" __b3bp_tmp_opt; do [[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} " if [[ "${__b3bp_tmp_opt}" = "-" ]]; then # OPTARG is long-option-name or long-option=value if [[ "${OPTARG}" =~ .*=.* ]]; then # --key=value format __b3bp_tmp_long_opt=${OPTARG/=*/} # Set opt to the short option corresponding to the long option __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" OPTARG=${OPTARG#*=} else # --key value format # Map long name to short version of option __b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${OPTARG//-/_}" printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}" # Only assign OPTARG if option takes an argument __b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}" printf -v "OPTARG" '%s' "${@:OPTIND:${!__b3bp_tmp_varname}}" # shift over the argument if argument is expected ((OPTIND+=__b3bp_tmp_has_arg_${__b3bp_tmp_opt})) fi # we have set opt/OPTARG to the short value and the argument as OPTARG if it exists fi __b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}" __b3bp_tmp_default="${!__b3bp_tmp_varname}" __b3bp_tmp_value="${OPTARG}" if [[ -z "${OPTARG}" ]] && [[ "${__b3bp_tmp_default}" = "0" ]]; then __b3bp_tmp_value="1" fi printf -v "${__b3bp_tmp_varname}" '%s' "${__b3bp_tmp_value}" debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}" done set -o nounset # no more unbound variable references expected shift $((OPTIND-1)) if [[ "${1:-}" = "--" ]] ; then shift fi fi ### Automatic validation of required option arguments ############################################################################## for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do # validate only options which required an argument [[ "${!__b3bp_tmp_varname}" = "2" ]] || continue __b3bp_tmp_opt_short="${__b3bp_tmp_varname##*_}" __b3bp_tmp_varname="arg_${__b3bp_tmp_opt_short}" [[ "${!__b3bp_tmp_varname}" ]] && continue __b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt_short}" printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname}" [[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})" help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long:-} requires an argument" done ### Cleanup Environment variables ############################################################################## for __tmp_varname in ${!__b3bp_tmp_*}; do unset -v "${__tmp_varname}" done unset -v __tmp_varname ### Externally supplied __usage. Nothing else to do here ############################################################################## if [[ "${__b3bp_external_usage:-}" = "true" ]]; then unset -v __b3bp_external_usage return fi ### Signal trapping and backtracing ############################################################################## function __b3bp_cleanup_before_exit () { if [[ "${?}" != "0" ]]; then info "Sources unmounted" fi if [[ ${arg_F:-} == 0 && "${SILENT:-False}" == "False" ]]; then read -r -p "Press Enter to exit... " ignored_var 2>&1 fi } trap __b3bp_cleanup_before_exit EXIT # requires `set -o errtrace` __b3bp_err_report() { local error_code error_code=${?} error "Error in ${__file} in function ${1} on line ${2}" exit ${error_code} } # Uncomment the following line for always providing an error backtrace trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR ### Command-line argument switches (like -d for debugmode, -h for showing helppage) ############################################################################## # debug mode if [[ "${arg_d:?}" = "1" ]]; then set -o xtrace LOG_LEVEL="7" # Enable error backtracing trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR fi # verbose mode if [[ "${arg_v:?}" = "1" ]]; then set -o verbose fi # help mode if [[ "${arg_h:?}" = "1" ]]; then # Help exists with code 1 help "Help using ${0}" fi ### Validation. Error out if the things required for your script are not present ############################################################################## if [[ "${arg_F:?}" == 1 ]]; then SILENT="True" else SILENT="False" fi if [[ "${arg_h:?}" == 1 ]]; then help "${__usage_example}" else # Print warning line [[ "${arg_u:-}" ]] || echo " -u or --ufd-device is required" [[ "${arg_m:-}" ]] || echo " -m or --main-kit is required" [[ "${arg_l:-}" ]] || echo " -l or --linux-iso is required" [[ "${arg_w:-}" ]] || echo " -w or --winpe-iso is required" # Bail if necessary [[ "${arg_u:-}" ]] || help "${__usage_example}" [[ "${arg_l:-}" ]] || help "${__usage_example}" [[ "${arg_m:-}" ]] || help "${__usage_example}" [[ "${arg_w:-}" ]] || help "${__usage_example}" fi [[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. " ### More functions ############################################################################## function abort () { local abort_message="Aborted" [[ "${1:-}" ]] && abort_message="${1}" || true error "${abort_message}" #echo -e "${YELLOW}${abort_message}${CLEAR}" exit 1 } function ask() { if [[ "${SILENT}" == "True" ]]; then echo -e "${1:-} Yes ${BLUE}(Silent)${CLEAR}" return 0 fi while :; do read -p "${1:-} [Y/N] " -r answer if echo "$answer" | egrep -iq '^(y|yes|sure)$'; then return 0 elif echo "$answer" | egrep -iq '^(n|no|nope)$'; then return 1 fi done } ### Runtime ############################################################################## # VARIABLES DEST_DEV="${arg_u}" DEST_PAR="${DEST_DEV}1" LOG_FILE="${HOME}/build-ufd_${DEST_DEV##*/}_$(date +%Y-%m-%d_%H%M_%z).log" MAIN_PY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/settings/main.py" RSYNC_ARGS="-hrtuvS --modify-window=1 --progress" MAIN_KIT="$(realpath "${arg_m:-}" 2>/dev/null || true)" LINUX_ISO="$(realpath "${arg_l:-}" 2>/dev/null || true)" WINPE_ISO="$(realpath "${arg_w:-}" 2>/dev/null || true)" EXTRA_DIR="$(realpath "${arg_e:-}" 2>/dev/null || true)" # COLORS CLEAR="\e[0m" RED="\e[31m" GREEN="\e[32m" YELLOW="\e[33m" BLUE="\e[34m" # Load main.py settings if [ ! -f "$MAIN_PY" ]; then echo -e "${RED}ERROR${CLEAR}: $MAIN_PY not found." abort fi while read line; do if echo "$line" | egrep -q "^\w+='"; then line="$(echo "$line" | sed -r 's/[\r\n]+//')" eval "$line" fi done < "$MAIN_PY" if [ -z ${KIT_NAME_FULL+x} ]; then # KIT_NAME_FULL is not set, assume main.py missing or malformatted echo -e "${RED}ERROR${CLEAR}: failed to load settings from $MAIN_PY" abort fi UFD_LABEL="${KIT_NAME_SHORT}_LINUX" # Check if root if [[ "$EUID" -ne 0 ]]; then echo -e "${RED}ERROR${CLEAR}: This script must be run as root." abort fi # Check if in tmux if ! tmux list-session 2>/dev/null | grep -q "build-ufd"; then # Reload in tmux eval tmux new-session -s "build-ufd" "${0:-}" ${__all_args} SILENT="True" # avoid two "Press Enter to exit..." prompts exit 0 fi # Header echo -e "${GREEN}$KIT_NAME_FULL${CLEAR}: UFD Build Tool" echo "" # Verify sources [[ -b "${DEST_DEV}" ]] || abort "${DEST_DEV} is not a valid device." [[ -d "${MAIN_KIT}/.bin" ]] || abort "Invalid Main Kit, ${MAIN_KIT}/.bin not found." [[ -e "${LINUX_ISO}" ]] || abort "Linux ISO not found." [[ -e "${WINPE_ISO}" ]] || abort "WinPE ISO not found." if [[ ! -z "${arg_e:-}" ]]; then [[ -d "${EXTRA_DIR}" ]] || abort "Extra Dir not found." fi # Print Info echo -e "${BLUE}Sources${CLEAR}" echo "Main Kit: ${MAIN_KIT}" echo "Linux ISO: ${LINUX_ISO}" echo "WinPE ISO: ${WINPE_ISO}" echo "Extra Dir: ${EXTRA_DIR:-(Not Specified)}" echo "" echo -e "${BLUE}Destination${CLEAR}" lsblk -n -o NAME,LABEL,SIZE,MODEL,SERIAL "${DEST_DEV}" echo "" # Ask before starting job echo "" if ask "Is the above information correct?"; then echo "" echo -e "${YELLOW}SAFETY CHECK${CLEAR}" echo "All data will be DELETED from the disk and partition(s) listed above." echo -e "This is irreversible and will lead to ${RED}DATA LOSS.${CLEAR}" if ! ask "Asking again to confirm, is this correct?"; then abort fi else abort fi # Debug info "arg_u: ${arg_u}" info "arg_m: ${arg_m}" info "arg_l: ${arg_l}" info "arg_w: ${arg_w}" info "arg_e: ${arg_e}" info "arg_h: ${arg_h}" info "arg_d: ${arg_d}" info "arg_v: ${arg_v}" info "arg_F: ${arg_F}" read -r -p "Done? " meh