#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## ERR path:
##   exception_handler_general | exception_handler_unchroot_unmount | exception_handler_unmount
##   -> exception_handler_process_shared -> function_trace -> process_backtrace -> exception_handler_shell -> exception_handler_retry -> exception_handler_maybe_exit
## INT/TERM path:
##   exception_handler_signal -> variant cleanup -> exit 128+signum

#set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
## Make '$( ... )' command substitutions inherit errexit. Without this
## shopt, bash 5.x disables errexit inside '$()' by default, so
## 'x="$(false; echo later)"' silently sets x to "later" with no error.
## With it, the 'false' triggers errexit inside the substitution and
## the outer assignment fails. ( ... ) parenthesized subshells already
## inherit errexit - this shopt covers the '$()' case only. Only has
## any effect while errexit itself is on; in pre's default (retry)
## mode errexit is disabled further down anyway.
shopt -s inherit_errexit

true "${BASH_SOURCE[0]} INFO: begin"

## Source libraries from package 'helper-scripts' that this 'pre' script
## depends on.
##
## 'xtrace.bsh'  must be loaded first because the colors source below is
## wrapped in functions 'xtrace_off' and 'xtrace_restore' (and the 'trap'
## functions further down reach for 'benchmark_format', 'function_trace',
## 'process_backtrace').
##
## Environment variable 'HELPER_SCRIPTS_PATH' lets 'helper-scripts' resolve
## their internal
## 'source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/..."'
## references against the in-tree submodule rather than the not-yet-
## installed system path '/usr/libexec/helper-scripts'.
HELPER_SCRIPTS_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../packages/kicksecure/helper-scripts"
export HELPER_SCRIPTS_PATH

test -d "$HELPER_SCRIPTS_PATH"
if ! test -e "${HELPER_SCRIPTS_PATH}/.git"; then
   error "Folder '${HELPER_SCRIPTS_PATH}/.git' does not exist! Need to run...?

git submodule update --init --recursive --progress --jobs=4"
fi

# shellcheck source=../packages/kicksecure/helper-scripts/usr/libexec/helper-scripts/xtrace.bsh
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/xtrace.bsh"

## Debugging.
#xtrace_off

## help-steps/pre uses color variables (e.g. $red, $bold, $reset) in its
## error messages and in root_check (called at end of this file). Source
## colors here so callers don't have to worry about source order.
## Sourcing it again later (from the caller) is idempotent.
# shellcheck source=../packages/kicksecure/helper-scripts/usr/libexec/helper-scripts/get_colors.sh
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/get_colors.sh"
# shellcheck source=../packages/kicksecure/helper-scripts/usr/libexec/helper-scripts/benchmark.bsh
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/benchmark.bsh"
# shellcheck source=../packages/kicksecure/helper-scripts/usr/libexec/helper-scripts/trace.bsh
source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/trace.bsh"

## Debugging.
#xtrace_restore

## Keep xtrace on if '--debug' was passed; otherwise silence it. Cannot be
## moved to help-steps/parse-cmd because parse-cmd runs later.
if ! printf '%s\n' "${@:-}" | grep --fixed-strings -- "--debug" &>/dev/null; then
   if test -o xtrace ; then
      true "\
${BASH_SOURCE[0]} INFO: Disabling verbose/debug output (xtrace) (set +x). For more verbose output add the following build option: --debug"
      xtrace_off
   fi
fi

## Resolve build interactivity once, with precedence:
##   0. retry-max 0:   fail-fast, always non-interactive (the continue/retry
##                     menu cannot resume under errexit anyway).
##   1. command line:  --interactive true|false
##   2. environment:   dist_build_interactive (export before sourcing pre)
##   3. auto-detect:   interactive only with a TTY on both stdin and stdout
##                     and not under CI; otherwise non-interactive.
## help-steps/parse-cmd is the authoritative validator of the --interactive
## value; this early scan (mirroring the --debug handling above) makes the
## value available before parse-cmd runs, e.g. for the error-handler menu.
## Sub-steps that source pre without the flag inherit the exported value.
dist_build_interactive_cli=""
dist_build_interactive_prev=""
for dist_build_interactive_arg in "${@}"; do
   if [ "${dist_build_interactive_prev}" = "--interactive" ]; then
      dist_build_interactive_cli="${dist_build_interactive_arg}"
   fi
   dist_build_interactive_prev="${dist_build_interactive_arg}"
done
unset dist_build_interactive_prev dist_build_interactive_arg

if [ "${dist_build_auto_retry:-}" = "0" ]; then
   dist_build_interactive=false
   true "${BASH_SOURCE[0]} INFO: retry-max 0 (fail-fast); dist_build_interactive=false (non-interactive)."
elif [ -n "${dist_build_interactive_cli}" ]; then
   dist_build_interactive="${dist_build_interactive_cli}"
   true "${BASH_SOURCE[0]} INFO: dist_build_interactive from command line: ${dist_build_interactive}"
elif [ -n "${dist_build_interactive:-}" ]; then
   true "${BASH_SOURCE[0]} INFO: dist_build_interactive from environment: ${dist_build_interactive}"
elif [ "${CI:-}" = "true" ]; then
   dist_build_interactive=false
   true "${BASH_SOURCE[0]} INFO: CI detected; dist_build_interactive=false (non-interactive)."
elif [ ! -t 0 ] || [ ! -t 1 ]; then
   dist_build_interactive=false
   true "${BASH_SOURCE[0]} INFO: stdin or stdout is not a TTY; dist_build_interactive=false (non-interactive)."
else
   dist_build_interactive=true
   true "${BASH_SOURCE[0]} INFO: TTY on stdin and stdout and not CI; dist_build_interactive=true (interactive)."
fi
unset dist_build_interactive_cli
export dist_build_interactive

if [ "${CI:-}" = "true" ]; then
   true "${BASH_SOURCE[0]} INFO: CI detected."
   [ -n "${rsync_cmd:-}" ] || rsync_cmd="echo simulate-only rsync"
fi

## The error-handler trap functions below pass "${args[@]}" to the cleanup
## scripts. 'args' is normally set by parse-cmd (args=("$@") in
## dist_build_one_parse_cmd), but the trap can fire before parse-cmd has
## run - in that case "${args[@]}" would be unbound under 'set -o nounset'.
## Initialize 'args' to an empty array here so a later 'args=("$@")' from
## parse-cmd still wins.
args=()

## output_cmd_set is sourced from helper-scripts (xtrace.bsh) at the
## top of this file.

build_run_function() {
   case "${build_skip_run_functions:-}" in
   *"$1"*)
      true "${cyan}INFO: ${bold}Skipping${bold} function ${under}$1${eunder}, because variable build_skip_run_functions includes it.${reset}"
      return 0
      ;;
   esac

   true "INFO: ${under}Running${eunder} function ${under}$1${eunder}, because variable build_skip_run_functions does not include it..."
   "$@"
   true "INFO: ${under}End${eunder} of function ${under}$1${eunder} reached, ok."
}

error() {
   output_cmd_set
   $output_cmd "${blue}############################################################${reset}"
   $output_cmd "${bold}${under}ERROR:${eunder}${reset}"
   $output_cmd "          ${bold}\$0:${reset} ${under}$0${eunder}"
   $output_cmd "${bold}\${BASH_SOURCE[0]}:${reset} ${under}${BASH_SOURCE[0]}${eunder}${reset}"
   $output_cmd "      ${bold}${red}message:${reset} ${under}$*${reset}"
   $output_cmd "${blue}############################################################${reset}"
   error_reason="${bold}message:${reset} $*
"
   error_ "See above! (There should be a bold, red message surrounded by blue hashtags (#).)"
   true
}

## function_trace and process_backtrace are sourced from
## helper-scripts (trace.bsh) at the top of this file. They print
## their result to stdout; callers capture into a local variable.

exception_handler_shell() {
   $output_cmd "${cyan}${bold}INFO: Opening interactive shell...${reset}"
   $output_cmd "${cyan}${bold}INFO: When you have finished, please enter \"exit 0\" to \
continue (Recommended against!) or \"exit 1\" to cleanup and exit. (Recommended.)${reset}"
   interactive_chroot_shell_bash_exit_code="0"
   chroot_run /bin/bash || { interactive_chroot_shell_bash_exit_code="${PIPESTATUS[0]}" ; true; };
   if [ "${interactive_chroot_shell_bash_exit_code:-}" = "0" ]; then
      $output_cmd "${cyan}${bold}INFO: Interactive shell terminated with \
exit code $interactive_chroot_shell_bash_exit_code, will ignore this error and continue...${reset}"
      ignore_error="true"
   else
      $output_cmd "${cyan}${bold}INFO: Interactive shell terminated with \
exit code $interactive_chroot_shell_bash_exit_code, cleanup and exit as requested...${reset}"
      ignore_error="false"
   fi
}

exception_handler_retry() {
   if [ ! "${dist_build_dispatch_before_retry:-}" = "" ]; then
      $output_cmd "${cyan}${bold}INFO: dispatch before retry (--retry-before)...: $dist_build_dispatch_before_retry ${reset}"
      dist_build_dispatch_before_retry_exit_code="0"
      eval $dist_build_dispatch_before_retry || { dist_build_dispatch_before_retry_exit_code="$?" ; true; };
      if [ "${dist_build_dispatch_before_retry_exit_code:-}" = "0" ]; then
         $output_cmd "${cyan}${bold}INFO: dispatch before retry (--retry-before) exit code was 0, ok. ${reset}"
      else
         $output_cmd "${red}${bold}INFO: dispatch before retry (--retry-before) non-zero exit code: $dist_build_dispatch_before_retry_exit_code ${reset}"
      fi
   else
      $output_cmd "INFO: Skipping dist_build_dispatch_before_retry (--retry-before), because empty, ok."
   fi

   $output_cmd "${cyan}${bold}INFO: Retrying last_failed_bash_command...: $last_failed_bash_command ${reset}"
   retry_last_failed_bash_command_exit_code="0"
   $last_failed_bash_command || { retry_last_failed_bash_command_exit_code="$?" ; true; };
   if [ "${retry_last_failed_bash_command_exit_code:-}" = "0" ]; then
      $output_cmd "${cyan}${bold}INFO: Retry succeeded. exit code of last_failed_bash_command: $retry_last_failed_bash_command_exit_code ${reset}"
      unset dist_build_auto_retry_counter
   else
      $output_cmd "${red}${bold}INFO: Retry failed. exit code of last_failed_bash_command: $retry_last_failed_bash_command_exit_code ${reset}"
      last_failed_exit_code="$retry_last_failed_bash_command_exit_code"
   fi

   if [ ! "${dist_build_dispatch_after_retry:-}" = "" ]; then
      $output_cmd "${cyan}${bold}INFO: dispatch after retry (--retry-after)...: $dist_build_dispatch_after_retry ${reset}"
      dist_build_dispatch_after_retry_exit_code="0"
      eval $dist_build_dispatch_after_retry || { dist_build_dispatch_after_retry_exit_code="$?" ; true; };
      if [ "${dist_build_dispatch_after_retry_exit_code:-}" = "0" ]; then
         $output_cmd "${cyan}${bold}INFO: dispatch after retry (--retry-after) exit code was 0, ok. ${reset}"
      else
         $output_cmd "${red}${bold}INFO: dispatch after retry (--retry-after) non-zero exit code: $dist_build_dispatch_after_retry_exit_code ${reset}"
      fi
   else
      $output_cmd "INFO: Skipping dist_build_dispatch_after_retry (--retry-after), because empty, ok."
   fi

   if [ "${retry_last_failed_bash_command_exit_code:-}" = "0" ]; then
      return 0
   else
      exception_handler_process_shared "NONE_(called_by_exception_handler_retry)"
   fi
}

exception_handler_process_shared() {
   last_script="$0"
   ## $1 contains trap signal type.
   trap_signal_type_previous="${trap_signal_type_last:-}"
   if [ "${trap_signal_type_previous:-}" = "" ]; then
      trap_signal_type_previous="unset"
   fi
   trap_signal_type_last="$1"
   dist_build_error_counter="$(( dist_build_error_counter + 1 ))"
   benchmark_took_time="$(benchmark_format "$SECONDS")"
   local first
   read -r first _ <<< "$last_failed_bash_command" || true

   ## process_backtrace and function_trace print their result to
   ## stdout; capture into a local before referencing in the report.
   local process_backtrace_result function_trace_result
   process_backtrace_result="$(process_backtrace)"
   function_trace_result="$(function_trace)"
   output_cmd_set
   $output_cmd "
${cyan}${bold}############################################################${reset}
${red}${bold}ERROR detected in script!: ${under}$last_script${eunder}${reset}

${blue}${bold}#####${reset}
${blue}${bold}User Help Message 2/2:${reset}

${cyan}Please READ this message carefully.${reset}

Copying/pasting/screenshotting this box alone will not be insightful, and no help can be provided with it alone as it may not contain sufficient information by itself.

In many instances, providing a longer segment above this box or the entire log may be necessary for an effective diagnosis.
${blue}${bold}#####${reset}

dist_build_version: ${under}${dist_build_version:-}${eunder}
dist_build_error_counter: $dist_build_error_counter
benchmark: $benchmark_took_time
last_failed_exit_code: $last_failed_exit_code
trap_signal_type_previous: $trap_signal_type_previous
trap_signal_type_last    : $trap_signal_type_last

${bold}process_backtrace_result${reset}:
$process_backtrace_result

${bold}function_trace_result${reset}:
$function_trace_result

${error_reason:-}${red}${bold}last_failed_bash_command${reset}: ${bold}$last_failed_bash_command${reset}
${cyan}${bold}############################################################${reset}
"

   unset error_reason

   ## INT and TERM no longer reach this function - exception_handler_signal
   ## (installed via exception_handler_setup) handles them directly. The
   ## only signal types that can reach here are "ERR" (from the trap) and
   ## "NONE_(called_by_exception_handler_retry)" (from the retry recursion).

   #$output_cmd "INFO: trap_signal_type_last: $trap_signal_type_last, considering auto retry..."
   if [ ! "${dist_build_auto_retry:-}" = "0" ]; then
      if [ "${dist_build_auto_retry_counter:-}" = "" ] \
         || [[ "${dist_build_auto_retry_counter:-}" = *[!0-9]* ]]; then
         dist_build_auto_retry_counter="1"
      fi
      [ -n "${dist_build_auto_retry:-}" ] || dist_build_auto_retry="2"
      [ -n "${dist_build_wait_auto_retry:-}" ] || dist_build_wait_auto_retry="5"
      if [ "${first:-}" = "error_" ]; then
         $output_cmd 'INFO: No auto retry because first item of last_failed_bash_command is "error_".'
      elif [ "${dist_build_auto_retry_counter:-0}" -gt "$dist_build_auto_retry" ]; then
         $output_cmd "${cyan}${bold}INFO: Auto retried (--retry-max) already $dist_build_auto_retry times. No more auto retry. ${reset}"
      else
         $output_cmd "${cyan}${bold}INFO: Auto retry attempt number: $dist_build_auto_retry_counter. Max retry attempts: $dist_build_auto_retry (--retry-max). Auto retry... ${reset}"
         dist_build_auto_retry_counter="$(( $dist_build_auto_retry_counter + 1 ))"
         if [ ! "${dist_build_wait_auto_retry:-}" = "0" ]; then
            $output_cmd "${cyan}${bold}INFO: Waiting (--retry-wait) $dist_build_wait_auto_retry seconds before auto retry... ${reset}"
            sleep "$dist_build_wait_auto_retry" &
            wait "$!"
         fi
         ignore_error="true"
         error_handler_do_retry="true"
         exception_handler_retry
         return 0
      fi
   else
      $output_cmd "INFO: dist_build_auto_retry set to $dist_build_auto_retry (--retry-max). No auto retry."
   fi
   unset dist_build_auto_retry_counter

   while true; do
      ## Default.
      ignore_error="false"

      answer=""
      ## Only ERR-family signal types reach this function (see note above),
      ## so no separate "non ERR signal" branch is needed here.
      if [ "${dist_build_interactive}" = "false" ]; then
         $output_cmd "INFO: using non-interactive error handler."
         break
      else
         if [ -t "0" ] && [ -t "1" ]; then
            $output_cmd "INFO: stdin and stdout connected to terminal, using interactive error handler."
            $output_cmd "\
${red}${bold}ERROR: An issue in ${under}$0${eunder} has been detected!${reset}

${blue}${bold}#####${reset}
${blue}${bold}User Help Message 1/2:${reset}

${cyan}Please READ this message carefully.${reset}

Copying/pasting/screenshotting this error message alone will not be insightful, and no help can be provided with it alone as it does not contain comprehensive information by itself. Instead, please scroll up and review the block encapsulated within ${cyan}${bold}###${reset} for more detailed information.

For support queries, it is essential to, at minimum, provide the portion of the log located above this message containing the actual error details. In many instances, providing a longer segment or the entire log may be necessary for an effective diagnosis.

Regrettably, assistance cannot be provided without the aforementioned details.

Options:

Choose ${under}either${eunder} option A), B), C) ${under}or${eunder} D).

 - A) Press ${blue}${bold}c${reset} and press enter to bypass this error and continue with the build. (For Developers Only!) (Strongly discouraged for regular users as it may lead to unstable builds! Please refrain from reporting any issues encountered subsequently!)
 - B) Press ${blue}${bold}r${reset} and enter to retry.
 - C) Press ${blue}${bold}s${reset} and enter to initiate a chroot interactive shell. (For Developers Only!)
 - D) Press ${blue}${bold}a${reset} and enter to abort.${reset}
${blue}${bold}#####${reset}"

            read -r -p "Answer? " answer
         else
            $output_cmd "INFO: stdin or stdout not connected to terminal, using non-interactive error handler."
            break
         fi
      fi
      error_handler_do_retry=""
      if [ "${answer:-}" = "continue" ] || [ "${answer:-}" = "c" ]; then
         ignore_error="true"
         break
      elif [ "${answer:-}" = "s" ] || [ "${answer:-}" = "shell" ]; then
         exception_handler_shell
         break
      elif [ "${answer:-}" = "r" ] || [ "${answer:-}" = "retry" ]; then
         ignore_error="true"
         error_handler_do_retry="true"
         exception_handler_retry
         break
      elif [ "${answer:-}" = "a" ]; then
         ignore_error="false"
         break
      else
         $output_cmd "${red}${bold}${under}ERROR: Invalid answer!${reset}"
         continue
      fi
   done
}

if [ "${dist_build_error_counter:-}" = "" ] \
   || [[ "${dist_build_error_counter:-}" = *[!0-9]* ]]; then
   dist_build_error_counter="0"
fi

exception_handler_maybe_exit() {
   if [ "${error_handler_do_retry:-}" = "true" ]; then
      if [ "${retry_last_failed_bash_command_exit_code:-}" = "0" ]; then
         true "INFO (${FUNCNAME[0]}): auto retry succeeded. Will not exit. Continue."
         return 0
      fi
   fi
   if [ "${ignore_error:-}" != "true" ]; then
      ## Remove lockfile for systemcheck
      rm --force "/run/package_manager_lock"
   fi
   if [ "${ignore_error:-}" = "true" ]; then
      $output_cmd "INFO: dist_build_interactive: ${dist_build_interactive}"
      if [ "${dist_build_interactive}" = "false" ]; then
         $output_cmd "INFO: using non-interactive error handler."
      else
         $output_cmd "${red}${bold}You have chosen to ignore this error. Your build may be unstable!
This is recommended against unless you know what you are doing. Do not report
bugs, that are a result of this! Please press enter to continue. ${reset}"
      fi
   else
      ## INT/TERM are handled by exception_handler_signal and never reach
      ## this function, so only ERR unwinds here.
      trap - EXIT
      $output_cmd "${red}${bold}INFO: Now exiting from $last_script (because error was detected, see above) with exit code ${under}1${eunder}.${reset}"
      exit 1
   fi
}

exception_handler_general() {
   last_failed_exit_code="$?"
   last_failed_bash_command="$BASH_COMMAND"
   output_cmd_set
   $output_cmd "${cyan}INFO: Middle of function ${FUNCNAME[0]} of $0.${reset}"
   ## $1 contains trap signal type.
   exception_handler_process_shared "$1"
   exception_handler_maybe_exit "$1"
   $output_cmd "${cyan}INFO: End of function ${FUNCNAME[0]} of $0.${reset}"
}

exception_handler_unchroot_unmount() {
   last_failed_exit_code="$?"
   last_failed_bash_command="$BASH_COMMAND"
   output_cmd_set
   ## $1 contains trap signal type.
   exception_handler_process_shared "$1"
   if [ "${ignore_error:-}" = "false" ]; then
      "$dist_source_help_steps_folder"/remove-local-temp-apt-repo "${args[@]}"
      "$dist_source_help_steps_folder"/unchroot-raw "${args[@]}"
      "$dist_source_help_steps_folder"/unprevent-daemons-from-starting "${args[@]}"
      "$dist_source_help_steps_folder"/unmount-raw "${args[@]}"
   fi
   exception_handler_maybe_exit "$1"
}

exception_handler_unmount() {
   last_failed_exit_code="$?"
   last_failed_bash_command="$BASH_COMMAND"
   output_cmd_set
   ## $1 contains trap signal type.
   exception_handler_process_shared "$1"
   if [ "${ignore_error:-}" = "false" ]; then
      "$dist_source_help_steps_folder"/unmount-raw "${args[@]}"
   fi
   exception_handler_maybe_exit "$1"
}

## Simple signal (INT/TERM) handler. Unlike the ERR path this does not
## retry, does not open an interactive menu, and does not recurse: we were
## asked to stop, so we run the variant-specific cleanup and exit with
## 128+signum. The ERR path remains untouched and keeps its auto-retry and
## interactive-menu machinery.
##
## $1 = signal name (INT or TERM)
## $2 = cleanup kind, derived from the ERR handler name by exception_handler_setup:
##        general          -> no cleanup
##        unchroot_unmount -> remove-local-temp-apt-repo, unchroot-raw,
##                            unprevent-daemons-from-starting, unmount-raw
##        unmount          -> unmount-raw
exception_handler_signal() {
   local signal kind signum exit_code
   signal="$1"
   kind="$2"
   case "$signal" in
      INT)  signum=2 ;;
      TERM) signum=15 ;;
      *)    signum=1 ;;
   esac
   exit_code="$(( 128 + signum ))"
   output_cmd_set
   $output_cmd "${cyan}${bold}############################################################${reset}"
   $output_cmd "${red}${bold}INFO: Signal $signal received in ${under}$0${eunder}. Running cleanup (kind: $kind)...${reset}"
   case "$kind" in
      unchroot_unmount)
         "$dist_source_help_steps_folder"/remove-local-temp-apt-repo "${args[@]}" || true
         "$dist_source_help_steps_folder"/unchroot-raw "${args[@]}" || true
         "$dist_source_help_steps_folder"/unprevent-daemons-from-starting "${args[@]}" || true
         "$dist_source_help_steps_folder"/unmount-raw "${args[@]}" || true
         ;;
      unmount)
         "$dist_source_help_steps_folder"/unmount-raw "${args[@]}" || true
         ;;
      general|*)
         ## No cleanup for the general variant.
         ;;
   esac
   $output_cmd "${red}${bold}INFO: Exiting $0 due to signal $signal with exit code $exit_code.${reset}"
   $output_cmd "${cyan}${bold}############################################################${reset}"
   ## Remove the EXIT trap so exithandler's generic "non-zero exit code"
   ## summary does not overwrite the signal-specific message above.
   trap - EXIT
   exit "$exit_code"
}

## Thanks to:
## camh
## http://stackoverflow.com/a/2183063/2605155
exception_handler_setup() {
   local handler signal kind
   ## $1 contains the name of the ERR handler function.
   handler="$1" ; shift
   ## Derive the signal-handler cleanup kind from the ERR handler name,
   ## e.g. exception_handler_unchroot_unmount -> unchroot_unmount.
   kind="${handler#exception_handler_}"
   for signal ; do
      ## SC2064: $handler/$signal/$kind are intentionally expanded NOW so
      ## each trap captures its specific handler and signal name, rather
      ## than the value at trap-fire time.
      if [ "${signal:-}" = "ERR" ]; then
         # shellcheck disable=SC2064
         trap "$handler $signal" "$signal"
      else
         # shellcheck disable=SC2064
         trap "exception_handler_signal $signal $kind" "$signal"
      fi
   done
}

output_cmd_set

exception_handler_setup "exception_handler_general" ERR INT TERM

## 'set +e' is only needed when the ERR trap wants to RESUME execution
## after a failure - i.e. the auto-retry or the interactive "continue/retry"
## paths in exception_handler_process_shared. Under 'set -o errexit', bash
## still runs the ERR trap but exits the script afterwards regardless, so
## neither path can put the script back on its feet.
if [ "${dist_build_auto_retry:-}" = "0" ]; then
   true "${BASH_SOURCE[0]} INFO: fail-fast mode (retry-max 0); keeping errexit on."
else
   set +o errexit
fi

## Benchmark. $SECONDS is a bash builtin - integer seconds since the
## shell started or since the last assignment to it - so we get script
## timing without spawning date(1). Reset here to make 'seconds since
## this script was sourced' the baseline.
##
## benchmark_format / benchmark_since are sourced from helper-scripts
## (benchmark.bsh) at the top of this file.
SECONDS=0

exithandler() {
   local exit_code=$?
   local elapsed
   elapsed="$(benchmark_format "$SECONDS")"
   output_cmd_set
   if [ "$exit_code" = "0" ]; then
      $output_cmd "${bold}INFO: Script ${under}$0${eunder} completed.${reset} Exit Code: ${under}$exit_code${eunder}. Errors Detected: ${under}$dist_build_error_counter${eunder}. Execution Time: $elapsed${reset}"
   else
      $output_cmd "${bold}${red}ERROR: Exiting ${under}$0${eunder} with non-zero exit code ${under}$exit_code${eunder}. Errors Detected: ${under}$dist_build_error_counter${eunder}. Execution Time: $elapsed${reset}."
   fi
   exit "$exit_code"
}

trap "exithandler" EXIT

root_check() {
   if [ "$EUID" = "0" ]; then
      ## Escape hatch for environments where we legitimately run as root
      ## (e.g. CI runners, containers without a non-root user). Opt in by
      ## exporting 'dist_build_allow_root=true' before sourcing pre. This
      ## also skips the sudo probes below - when we are already root,
      ## sudo is both moot and potentially unavailable.
      if [ "${dist_build_allow_root:-}" = "true" ]; then
         true "INFO: root_check: running as root but dist_build_allow_root=true, allowing."
         return 0
      fi
      $output_cmd "${red}${bold}ERROR: This must NOT be run as root (sudo)!${reset}"
      $output_cmd "${red}${bold}       Export 'dist_build_allow_root=true' to override (CI / container use only).${reset}"
      exit 1
   fi
   true "INFO: Script running as non-root, ok."
   ## Not using 'sudo --non-interactive --validate' because that caused an
   ## error on an ansible server "sudo: a password is required". Potential bug in sudo?
   ## https://forums.whonix.org/t/derivative-maker-automated-ci-builder/14468/14
   true "INFO: Running 'sudo --non-interactive -- test -d /usr' to test if sudo password entry prompt is needed..."
   if sudo --non-interactive -- test -d /usr ; then
      true "INFO: sudo password already previously cached (entered) or this system has passwordless sudo, ok."
   else
      $output_cmd "INFO: Going to run 'sudo --validate' to prompt for password..."
      $output_cmd "${bold}INFO: Please enter sudo password.${reset}"
      ## sudo password prompt
      ## overwrite with '|| true' so sudo prompts only once (with the sudo default allowed retries).
      ## Avoiding the built-in auto retry in this script. Validation happens below.
      sudo --validate || true
      $output_cmd "INFO: Running 'sudo --non-interactive -- test -d /usr' to test if sudo password prompt succeeded..."
      if sudo --non-interactive -- test -d /usr ; then
         $output_cmd "INFO: sudo password prompt success."
      else
         $output_cmd "${red}${bold}ERROR: sudo password prompt entry failure!${reset}"
         error "sudo password prompt entry failure"
      fi
   fi
   true "INFO: Running 'sudo --non-interactive -- test -d /usr 2>&1' to test if output (stdout, stderr) is empty as expected (no warnings or error messages shown)..."
   ## SUDO_TO_ROOT unavailable here because set in file: help-steps/variables
   local sudo_test_output
   sudo_test_output="$(sudo --non-interactive -- test -d /usr 2>&1)" || true
   if [ ! "${sudo_test_output:-}" = "" ]; then
      $output_cmd "${red}${bold}ERROR: Unexpected non-empty sudo command output!${reset}"
      $output_cmd "${red}${bold}ERROR: General system sudo issue. Most likely unrelated to this derivative-maker. Please manually run 'sudo --non-interactive -- test -d /usr' and fix your system issue.${reset}"
      $output_cmd "${red}${bold}sudo_test_output:${reset}"
      $output_cmd "${red}${bold}$sudo_test_output${reset}"
      error "sudo command output is non-empty!"
      return 0
   fi
   true "INFO: root_check ok."
}

nothing_to_commit_test() {
   ## Use --porcelain (locale-independent; empty output means clean tree)
   ## rather than matching the English "nothing to commit, working tree clean"
   ## line of `git status`, which breaks on any non-English locale.
   if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
      return 0
   fi
   return 1
}

root_check

xtrace_restore

true "${BASH_SOURCE[0]} INFO: End of script, ok."
