From 8a76e474fe2f91bb6186ec2fb727d2bb5da93468 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Wed, 7 Oct 2026 13:02:09 +0200 Subject: [PATCH 01/17] See if we can do interactive debuggin in an identical environment --- bot/build.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bot/build.sh b/bot/build.sh index a60054fe..082dfbd2 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -322,6 +322,17 @@ for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do echo "Executing command to build software:" echo "$software_layer_dir/eessi_container.sh ${COMMON_ARGS[@]} ${BUILD_STEP_ARGS_ACCEL[@]}" echo " -- $software_layer_dir/install_software_layer.sh \"${INSTALL_SCRIPT_ARGS[@]}\" \"$@\" 2>&1 | tee -a ${build_outerr}" + if [[ -n "${EESSI_BOT_INTERACTIVE}" ]]; then + # Interactive debugging: same setup as above, but drop into a shell in the build container + # instead of running the install script (later --mode overrides the --mode exec in COMMON_ARGS). + # No 'tee' here as it would break the interactive terminal; no resume/tarball handling either. + echo "bot/build.sh: EESSI_BOT_INTERACTIVE is set, starting an interactive shell instead of the build" + echo "bot/build.sh: to run the build step manually from within the shell, use:" + echo " $software_layer_dir/install_software_layer.sh ${INSTALL_SCRIPT_ARGS[@]} $@" + $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" --mode shell + # only a single (the first) accelerator target is supported in interactive mode + exit 0 + fi $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" \ -- $software_layer_dir/install_software_layer.sh "${INSTALL_SCRIPT_ARGS[@]}" "$@" 2>&1 | tee -a ${build_outerr} From 51f82efc0348e31f92c84c4386aae95887a6a4eb Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Wed, 7 Oct 2026 17:22:09 +0200 Subject: [PATCH 02/17] Implement proper interactive debugging, with options of being dropped in a container shell right away, or with hooks that drop you at the start of the loop over easystack files, or after an eb command has finished with non-zero exit. Also support debugging of accelerator builds by selecting a single accelerator - even if the initial build was multi-accelerator. Finally, make sure that we can resume from a build tarball, so that if a lot of dependencies have already been built, we don't have to redo it. --- EESSI-install-software.sh | 48 ++++++++++++++++++++ bot/build.sh | 91 +++++++++++++++++++++++++++++++++----- run_in_compat_layer_env.sh | 3 ++ 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/EESSI-install-software.sh b/EESSI-install-software.sh index 6ed3985a..ff14e0b0 100755 --- a/EESSI-install-software.sh +++ b/EESSI-install-software.sh @@ -84,6 +84,41 @@ TOPDIR=$(dirname $(realpath $0)) source $TOPDIR/scripts/utils.sh +# Debugging aid, only active if EESSI_INTERACTIVE_BUILD is set (see bot/build.sh): start an interactive shell +# in the current environment. We are running inside 'startprefix' with stdin consumed by a here-string, so we +# talk to /dev/tty directly. The new shell inherits all exported variables (Prefix, Lmod state, ...); we +# additionally dump the non-exported variables, functions, aliases and shell options of this script into an +# rcfile, so that e.g. ${EB} and our helper functions are available too. +debug_shell() { + local reason="$1" + local rcfile + rcfile=$(mktemp "${TMPDIR:-/tmp}/debug_shell_rc.XXXXXX") + { + # 'grep -v' filters out read-only variables (e.g. BASHOPTS, EUID), as trying to load those in a debug shell + # would cause errors. If grep -v returns a non-zero output (i.e. the only variables are read-only variables) + # the '|| true' ensures this still passes if 'set -e' is ever set + declare -p 2>/dev/null | grep -v '^declare -[a-zA-Z-]*r[a-zA-Z-]* ' || true + declare -f + alias -p + shopt -p + # the debugging shell should not abort on errors, nor re-trigger our hooks + echo "set +e +u; trap - EXIT" + echo "PS1='[EESSI debug] \\w \\$ '" + echo "cd '${PWD}'" + } > "${rcfile}" 2>/dev/null + echo_yellow ">> Dropping into an interactive debug shell: ${reason}" + echo_yellow ">> Working directory: ${PWD}; exit the shell (Ctrl-D) to continue/finish" + bash --rcfile "${rcfile}" -i < /dev/tty > /dev/tty 2>&1 + rm -f "${rcfile}" +} + +# If EESSI_INTERACTIVE_BUILD=onfail, then define a trap so that if an error is encounted that would cause the shell to exit +# (e.g. fatal_error) we get dropped into the debug_shell as well. That will allow us to not only debug EasyBuild build +# failures interactively, but anything resulting in fatal_error as well. +if [[ "${EESSI_INTERACTIVE_BUILD}" == "onfail" ]]; then + trap 'ec_exit=$?; if [[ ${ec_exit} -ne 0 ]]; then debug_shell "script exiting with exit code ${ec_exit}"; fi' EXIT +fi + # honor $TMPDIR if it is already defined, use /tmp otherwise if [ -z $TMPDIR ]; then export WORKDIR=/tmp/$USER @@ -425,6 +460,14 @@ else echo_green "All set, let's start installing some software with EasyBuild v${eb_version} in ${EASYBUILD_INSTALLPATH}..." + # If EESSI_INTERACTIVE_BUILD=before, you'll get a debug shell right before easybuild starts + # You can execute the ${EB} --easystack ${easystack_file} manually, or just exist the shell again + # to let the script continue (e.g. because the item you want to debug is in the 2nd iteration of this for-loop over + # easystack files) + if [[ "${EESSI_INTERACTIVE_BUILD}" == "before" ]]; then + debug_shell "environment is ready to run: ${EB} --easystack ${easystack_file} --robot" + fi + if [ -f ${easystack_file} ]; then echo_green "Feeding easystack file ${easystack_file} to EasyBuild..." @@ -437,6 +480,11 @@ else # copy EasyBuild log file if EasyBuild exited with an error if [ ${ec} -ne 0 ]; then + # If easybuild had a non-zero exit code, and EESSI_INTERACTIVE_BUILD=onfail, you'll get a debug + # shell so you can debug the failure + if [[ "${EESSI_INTERACTIVE_BUILD}" == "onfail" ]]; then + debug_shell "EasyBuild failed with exit code ${ec} for ${easystack_file}" + fi eb_last_log=$(eb --last-log | grep ^/.*\.log) # copy to current working directory if file exhists if [ -f ${eb_last_log} ]; then diff --git a/bot/build.sh b/bot/build.sh index 082dfbd2..bf9803ae 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -18,6 +18,21 @@ # file 'job.cfg' has been deposited # - the directory may contain any additional files referenced in job.cfg +# To debug a bot build interactively, re-run this script from the bot's job directory (the one containing +# cfg/job.cfg), where the bot's wrapper has already cloned software-layer-scripts, from an interactive +# allocation (a terminal is needed), with: +# EESSI_INTERACTIVE_BUILD= ./software-layer-scripts/bot/build.sh [args the bot passed] +# Call it directly (not via the wrapper bot/build.sh of the software-layer checkout, which would try to +# clone software-layer-scripts again and fail). Supported modes: +# shell - start a plain shell in the build container instead of running the build +# before - run the build, but start a shell (inside the compat layer, EasyBuild loaded) right before +# EasyBuild is called +# onfail - run the build, and start a shell when EasyBuild fails (or the build script exits non-zero) +# The state of the bot's build step is resumed from EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball), +# by default the newest tarball in previous_tmp/build_step. Nothing is saved, no tarball is created. +# Only one accelerator target is handled: by default the last one in job.cfg (matching the newest tarball), +# EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80) selects another one, in which case also set EESSI_INTERACTIVE_BUILD_RESUME_FROM. + # stop as soon as something fails set -e @@ -260,9 +275,27 @@ mkdir -p ${TARBALL_TMP_BUILD_STEP_DIR} # prepare arguments to eessi_container.sh specific to build step declare -a BUILD_STEP_ARGS=() -BUILD_STEP_ARGS+=("--save" "${TARBALL_TMP_BUILD_STEP_DIR}") +# We don't want interactive debugging sessions to save tarballs, so only save if EESSI_INTERACTIVE_BUILD isn't set +if [[ -z "${EESSI_INTERACTIVE_BUILD}" ]]; then + BUILD_STEP_ARGS+=("--save" "${TARBALL_TMP_BUILD_STEP_DIR}") +fi BUILD_STEP_ARGS+=("--storage" "${STORAGE}") +# interactive debugging: resume the state of the bot's build step (so that already installed software is available) +# Default will be to resume from the latest build-step tarball, but this can be overriden by pointing EESSI_INTERACTIVE_BUILD_RESUME_FROM +# to another tarball before running bot/build.sh +if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then + if [[ ! "${EESSI_INTERACTIVE_BUILD}" =~ ^(shell|before|onfail)$ ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD must be one of: shell, before, onfail (got '${EESSI_INTERACTIVE_BUILD}')" + fi + EESSI_INTERACTIVE_BUILD_RESUME_FROM=${EESSI_INTERACTIVE_BUILD_RESUME_FROM:-$(ls -t ${TARBALL_TMP_BUILD_STEP_DIR}/tmp_storage-* 2>/dev/null | head -n 1)} + if [[ -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" || ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + fatal_error "no state of the bot's build step to resume from found in ${TARBALL_TMP_BUILD_STEP_DIR}; set EESSI_INTERACTIVE_BUILD_RESUME_FROM" + fi + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" + BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") +fi + # Retain location for host injections so we don't reinstall CUDA # (Always need to run the driver installation as available driver may change) if [[ ! -z ${SHARED_FS_PATH} ]]; then @@ -296,6 +329,42 @@ else fi RESUME_DIR="" +# interactive debugging only handles a single accelerator target: by default the last one, as the default (newest) +# tarball is from its build; EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80 or accel/nvidia/cc80) selects another one +if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then + selected_accel="${EESSI_ACCELERATOR_TARGET_OVERRIDES[-1]}" + if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then + selected_accel="" + for candidate in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do + if [[ "${candidate}" == "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" || "${candidate}" == "accel/${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then + selected_accel="${candidate}" + fi + done + if [[ -z "${selected_accel}" ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${EESSI_ACCELERATOR_TARGET_OVERRIDES[*]}" + fi + fi + + # show what is available, so it is easy to check that the accelerator and tarball selection belong together + mapfile -t available_tarballs < <(ls -tr ${TARBALL_TMP_BUILD_STEP_DIR}/tmp_storage-* 2>/dev/null) + echo "bot/build.sh: interactive debugging, available state of the bot's build step:" + echo " accelerator targets (in the order the bot built them):" + for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do + echo " [$((i+1))] '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$i]}'" + done + echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" + for i in "${!available_tarballs[@]}"; do + echo " [$((i+1))] $(basename ${available_tarballs[$i]}) ($(date -r ${available_tarballs[$i]} '+%Y-%m-%d %H:%M'), $(du -h ${available_tarballs[$i]} | cut -f1))" + done + echo " note: tarball [n] was written after the n-th accelerator build; to debug another target set" + echo " EESSI_INTERACTIVE_BUILD_ACCELERATOR= and EESSI_INTERACTIVE_BUILD_RESUME_FROM=" + if [[ ${#available_tarballs[@]} -ne ${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} ]]; then + echo " WARNING: ${#available_tarballs[@]} tarball(s) vs ${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} accelerator target(s), so the pairing above is unreliable" + fi + echo " selected: accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" + EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel}") +fi + for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do # copy the common build step arguments to a a BUILD_STEP_ARGS_ACCEL=("${BUILD_STEP_ARGS[@]}") @@ -322,15 +391,17 @@ for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do echo "Executing command to build software:" echo "$software_layer_dir/eessi_container.sh ${COMMON_ARGS[@]} ${BUILD_STEP_ARGS_ACCEL[@]}" echo " -- $software_layer_dir/install_software_layer.sh \"${INSTALL_SCRIPT_ARGS[@]}\" \"$@\" 2>&1 | tee -a ${build_outerr}" - if [[ -n "${EESSI_BOT_INTERACTIVE}" ]]; then - # Interactive debugging: same setup as above, but drop into a shell in the build container - # instead of running the install script (later --mode overrides the --mode exec in COMMON_ARGS). - # No 'tee' here as it would break the interactive terminal; no resume/tarball handling either. - echo "bot/build.sh: EESSI_BOT_INTERACTIVE is set, starting an interactive shell instead of the build" - echo "bot/build.sh: to run the build step manually from within the shell, use:" - echo " $software_layer_dir/install_software_layer.sh ${INSTALL_SCRIPT_ARGS[@]} $@" - $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" --mode shell - # only a single (the first) accelerator target is supported in interactive mode + if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then + # Interactive debugging: same setup as above. No 'tee' as it would break the interactive terminal. + if [[ "${EESSI_INTERACTIVE_BUILD}" == "shell" ]]; then + echo "bot/build.sh: starting a shell in the build container instead of the build; to run the build step manually use:" + echo " $software_layer_dir/install_software_layer.sh ${INSTALL_SCRIPT_ARGS[@]} $@" + # a later --mode overrides the --mode exec in COMMON_ARGS + $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" --mode shell + else + $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" \ + -- $software_layer_dir/install_software_layer.sh "${INSTALL_SCRIPT_ARGS[@]}" "$@" + fi exit 0 fi $software_layer_dir/eessi_container.sh "${COMMON_ARGS[@]}" "${BUILD_STEP_ARGS_ACCEL[@]}" \ diff --git a/run_in_compat_layer_env.sh b/run_in_compat_layer_env.sh index 07501a7c..989f1d6b 100755 --- a/run_in_compat_layer_env.sh +++ b/run_in_compat_layer_env.sh @@ -59,6 +59,9 @@ fi if [ ! -z ${EESSI_INIT_PREFIX} ]; then INPUT="export EESSI_INIT_PREFIX=${EESSI_INIT_PREFIX}; ${INPUT}" fi +if [ ! -z ${EESSI_INTERACTIVE_BUILD} ]; then + INPUT="export EESSI_INTERACTIVE_BUILD=${EESSI_INTERACTIVE_BUILD}; ${INPUT}" +fi if [ ! -z ${EESSI_SITE_INSTALL_FORCE} ]; then INPUT="export EESSI_SITE_INSTALL_FORCE=${EESSI_SITE_INSTALL_FORCE}; ${INPUT}" if [ ! -z ${EESSI_SITE_SOFTWARE_PREFIX} ]; then From 4fdc9d0574577e03c485ca7f32f3ce92e245b790 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Thu, 8 Oct 2026 16:50:05 +0200 Subject: [PATCH 03/17] Make sure the debug shell prints the command that the user should run to reproduce the failure --- EESSI-install-software.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/EESSI-install-software.sh b/EESSI-install-software.sh index ff14e0b0..52b8968b 100755 --- a/EESSI-install-software.sh +++ b/EESSI-install-software.sh @@ -460,22 +460,25 @@ else echo_green "All set, let's start installing some software with EasyBuild v${eb_version} in ${EASYBUILD_INSTALLPATH}..." + # full EasyBuild command for this easystack file (also shown in the debug shell messages) + if [[ ${easystack_file} == *"/rebuilds/"* ]]; then + eb_cmd="${EB} --easystack ${easystack_file} --rebuild" + else + eb_cmd="${EB} --easystack ${easystack_file} --robot" + fi + # If EESSI_INTERACTIVE_BUILD=before, you'll get a debug shell right before easybuild starts - # You can execute the ${EB} --easystack ${easystack_file} manually, or just exist the shell again + # You can execute ${eb_cmd} manually, or just exit the shell again # to let the script continue (e.g. because the item you want to debug is in the 2nd iteration of this for-loop over # easystack files) if [[ "${EESSI_INTERACTIVE_BUILD}" == "before" ]]; then - debug_shell "environment is ready to run: ${EB} --easystack ${easystack_file} --robot" + debug_shell "environment is ready to run: ${eb_cmd}" fi if [ -f ${easystack_file} ]; then echo_green "Feeding easystack file ${easystack_file} to EasyBuild..." - if [[ ${easystack_file} == *"/rebuilds/"* ]]; then - ${EB} --easystack ${easystack_file} --rebuild - else - ${EB} --easystack ${easystack_file} --robot - fi + ${eb_cmd} ec=$? # copy EasyBuild log file if EasyBuild exited with an error @@ -483,7 +486,7 @@ else # If easybuild had a non-zero exit code, and EESSI_INTERACTIVE_BUILD=onfail, you'll get a debug # shell so you can debug the failure if [[ "${EESSI_INTERACTIVE_BUILD}" == "onfail" ]]; then - debug_shell "EasyBuild failed with exit code ${ec} for ${easystack_file}" + debug_shell "EasyBuild failed with exit code ${ec} for command: ${eb_cmd}" fi eb_last_log=$(eb --last-log | grep ^/.*\.log) # copy to current working directory if file exhists From 3e4067dd5e92eeb490756ff61cb1fa5c8018c830 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 14:18:02 +0200 Subject: [PATCH 04/17] Change the comment about interactive debugging, in order to make clear that the preferred way of debugging is to start at the bot-build.slurm level --- bot/build.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index bf9803ae..a4eee5cf 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -18,12 +18,15 @@ # file 'job.cfg' has been deposited # - the directory may contain any additional files referenced in job.cfg -# To debug a bot build interactively, re-run this script from the bot's job directory (the one containing -# cfg/job.cfg), where the bot's wrapper has already cloned software-layer-scripts, from an interactive -# allocation (a terminal is needed), with: -# EESSI_INTERACTIVE_BUILD= ./software-layer-scripts/bot/build.sh [args the bot passed] -# Call it directly (not via the wrapper bot/build.sh of the software-layer checkout, which would try to -# clone software-layer-scripts again and fail). Supported modes: +# Debugging interactive builds +# This script respects the environment variable EESSI_INTERACTIVE_BUILD= which may be set if one wants to debug interactively +# To debug interactively, run: +# EESSI_INTERACTIVE_BUILD= +# where the path-to-bot-build-jobscript is the job script referred to as build_job_script by the app.cfg of the build bot. +# That is typically eessi-bot-software-layer/scripts/bot-build.slurm, which then calls bot/build.sh (i.e. this script) with +# EESSI_INTERACTIVE_BUILD= set. This way, environment setup that is part of the bot-build.slurm script (including e.g. +# sourcing of the site_config_script as pointed to in the bot's app.cfg) is included. Then, this script sets up more of the +# build environment, before eventually running the build, dropping you in an interactive debugging shell. Supported modes: # shell - start a plain shell in the build container instead of running the build # before - run the build, but start a shell (inside the compat layer, EasyBuild loaded) right before # EasyBuild is called From 8c336b00664cfecba8b4d6852d632d55bb8797e6 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 15:15:15 +0200 Subject: [PATCH 05/17] Prompt for a selection, if the user didn't specify the accelerator target or the tarball to resume from up front --- bot/build.sh | 143 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 33 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index a4eee5cf..f8e41029 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -31,10 +31,13 @@ # before - run the build, but start a shell (inside the compat layer, EasyBuild loaded) right before # EasyBuild is called # onfail - run the build, and start a shell when EasyBuild fails (or the build script exits non-zero) -# The state of the bot's build step is resumed from EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball), -# by default the newest tarball in previous_tmp/build_step. Nothing is saved, no tarball is created. -# Only one accelerator target is handled: by default the last one in job.cfg (matching the newest tarball), -# EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80) selects another one, in which case also set EESSI_INTERACTIVE_BUILD_RESUME_FROM. +# The state of the bot's build step (a tarball in previous_tmp/build_step) is resumed, and one accelerator target is +# handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball) and +# EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are used as given. If one or both are +# unset and there is a terminal, the available tarballs and accelerator targets are listed and you are asked for the +# missing one(s). Without a terminal, the missing one(s) are inferred (default: the last accelerator target and the newest +# tarball, or the accelerator target / tarball that belongs to the other one, if there are as many tarballs as +# accelerator targets), and a warning is printed. Nothing is saved, no tarball is created. # stop as soon as something fails set -e @@ -284,19 +287,12 @@ if [[ -z "${EESSI_INTERACTIVE_BUILD}" ]]; then fi BUILD_STEP_ARGS+=("--storage" "${STORAGE}") -# interactive debugging: resume the state of the bot's build step (so that already installed software is available) -# Default will be to resume from the latest build-step tarball, but this can be overriden by pointing EESSI_INTERACTIVE_BUILD_RESUME_FROM -# to another tarball before running bot/build.sh +# interactive debugging: check the requested mode. The state of the bot's build step to resume from (tarball) and the +# accelerator target are selected further below, once the list of accelerator targets is known if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then if [[ ! "${EESSI_INTERACTIVE_BUILD}" =~ ^(shell|before|onfail)$ ]]; then fatal_error "EESSI_INTERACTIVE_BUILD must be one of: shell, before, onfail (got '${EESSI_INTERACTIVE_BUILD}')" fi - EESSI_INTERACTIVE_BUILD_RESUME_FROM=${EESSI_INTERACTIVE_BUILD_RESUME_FROM:-$(ls -t ${TARBALL_TMP_BUILD_STEP_DIR}/tmp_storage-* 2>/dev/null | head -n 1)} - if [[ -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" || ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then - fatal_error "no state of the bot's build step to resume from found in ${TARBALL_TMP_BUILD_STEP_DIR}; set EESSI_INTERACTIVE_BUILD_RESUME_FROM" - fi - echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" - BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") fi # Retain location for host injections so we don't reinstall CUDA @@ -332,24 +328,36 @@ else fi RESUME_DIR="" -# interactive debugging only handles a single accelerator target: by default the last one, as the default (newest) -# tarball is from its build; EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80 or accel/nvidia/cc80) selects another one -if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then - selected_accel="${EESSI_ACCELERATOR_TARGET_OVERRIDES[-1]}" - if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - selected_accel="" - for candidate in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do - if [[ "${candidate}" == "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" || "${candidate}" == "accel/${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - selected_accel="${candidate}" - fi - done - if [[ -z "${selected_accel}" ]]; then - fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${EESSI_ACCELERATOR_TARGET_OVERRIDES[*]}" +# interactive debugging only handles a single accelerator target, and resumes from a single tarball of the bot's build step. +# - If both EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM are set, we use them as given. +# - If one or both are unset and we have a terminal, we list the options and ask for the missing one(s). +# - If one or both are unset and there is no terminal, we cannot ask: we infer the missing one(s) and print a warning. +# The suggested (default) accelerator target is the last one, and the default tarball the newest one. If the other one is +# known and there are as many tarballs as accelerator targets, the default is the one that belongs to it instead (the +# n-th tarball was written after the n-th accelerator build). +# Ask the user to choose a number between 1 and $2 for $1, default $3, result in INTERACTIVE_CHOICE +interactive_choice() { + local what="$1" n="$2" default="$3" answer + while true; do + if ! read -r -p "bot/build.sh: select ${what} [1-${n}, default ${default}]: " answer < /dev/tty; then + INTERACTIVE_CHOICE=${default} + return fi - fi + answer=${answer:-${default}} + if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= n )); then + INTERACTIVE_CHOICE=${answer} + return + fi + echo "bot/build.sh: invalid choice '${answer}'" + done +} - # show what is available, so it is easy to check that the accelerator and tarball selection belong together +if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then mapfile -t available_tarballs < <(ls -tr ${TARBALL_TMP_BUILD_STEP_DIR}/tmp_storage-* 2>/dev/null) + n_accel=${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} + n_tar=${#available_tarballs[@]} + + # show what is available, so it is easy to check that the accelerator and tarball selection belong together echo "bot/build.sh: interactive debugging, available state of the bot's build step:" echo " accelerator targets (in the order the bot built them):" for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do @@ -359,13 +367,82 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then for i in "${!available_tarballs[@]}"; do echo " [$((i+1))] $(basename ${available_tarballs[$i]}) ($(date -r ${available_tarballs[$i]} '+%Y-%m-%d %H:%M'), $(du -h ${available_tarballs[$i]} | cut -f1))" done - echo " note: tarball [n] was written after the n-th accelerator build; to debug another target set" - echo " EESSI_INTERACTIVE_BUILD_ACCELERATOR= and EESSI_INTERACTIVE_BUILD_RESUME_FROM=" - if [[ ${#available_tarballs[@]} -ne ${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} ]]; then - echo " WARNING: ${#available_tarballs[@]} tarball(s) vs ${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} accelerator target(s), so the pairing above is unreliable" + echo " note: tarball [n] was written after the n-th accelerator build" + if [[ ${n_tar} -ne ${n_accel} ]]; then + echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so this pairing is unreliable" + fi + + can_prompt=0 + { : < /dev/tty; } 2>/dev/null && can_prompt=1 + + # index (1-based) of the tarball given in EESSI_INTERACTIVE_BUILD_RESUME_FROM in the list of tarballs (0 if not in there) + given_tar_idx=0 + if [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + for i in "${!available_tarballs[@]}"; do + if [[ "$(realpath -m ${available_tarballs[$i]})" == "$(realpath -m ${EESSI_INTERACTIVE_BUILD_RESUME_FROM})" ]]; then + given_tar_idx=$((i+1)) + fi + done fi - echo " selected: accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" + + # select accelerator target + if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then + accel_idx=0 + for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do + candidate="${EESSI_ACCELERATOR_TARGET_OVERRIDES[$i]}" + if [[ "${candidate}" == "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" || "${candidate}" == "accel/${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then + accel_idx=$((i+1)) + fi + done + if [[ ${accel_idx} -eq 0 ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${EESSI_ACCELERATOR_TARGET_OVERRIDES[*]}" + fi + else + accel_idx=${n_accel} + if [[ ${given_tar_idx} -gt 0 && ${n_tar} -eq ${n_accel} ]]; then + accel_idx=${given_tar_idx} + fi + if [[ ${n_accel} -gt 1 ]]; then + if [[ ${can_prompt} -eq 1 ]]; then + interactive_choice "accelerator target" ${n_accel} ${accel_idx} + accel_idx=${INTERACTIVE_CHOICE} + else + msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which accelerator target to use." + msg="${msg} Assuming '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}'. To choose explicitly, set EESSI_INTERACTIVE_BUILD_ACCELERATOR." + echo_yellow "${msg}" + fi + fi + fi + selected_accel="${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}" + + # select tarball to resume from + if [[ -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + if [[ ${n_tar} -eq 0 ]]; then + fatal_error "no state of the bot's build step to resume from found in ${TARBALL_TMP_BUILD_STEP_DIR}; set EESSI_INTERACTIVE_BUILD_RESUME_FROM" + fi + tar_idx=${n_tar} + if [[ ${n_tar} -eq ${n_accel} ]]; then + tar_idx=${accel_idx} + fi + if [[ ${n_tar} -gt 1 ]]; then + if [[ ${can_prompt} -eq 1 ]]; then + interactive_choice "tarball to resume from" ${n_tar} ${tar_idx} + tar_idx=${INTERACTIVE_CHOICE} + else + msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from." + msg="${msg} Assuming '$(basename ${available_tarballs[$((tar_idx-1))]})'. To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM." + echo_yellow "${msg}" + fi + fi + EESSI_INTERACTIVE_BUILD_RESUME_FROM="${available_tarballs[$((tar_idx-1))]}" + fi + if [[ ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + fatal_error "state to resume from does not exist: ${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" + fi + + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel}") + BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") fi for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do From de767ba0e3e1d92bd672403327211bf1b65fffdd Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 15:53:36 +0200 Subject: [PATCH 06/17] Be more conservative in assuming defaults --- bot/build.sh | 92 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index f8e41029..e584450c 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -32,12 +32,14 @@ # EasyBuild is called # onfail - run the build, and start a shell when EasyBuild fails (or the build script exits non-zero) # The state of the bot's build step (a tarball in previous_tmp/build_step) is resumed, and one accelerator target is -# handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball) and -# EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are used as given. If one or both are +# handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball, or 'none' to not resume from +# anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are +# used as given. If one or both are # unset and there is a terminal, the available tarballs and accelerator targets are listed and you are asked for the -# missing one(s). Without a terminal, the missing one(s) are inferred (default: the last accelerator target and the newest -# tarball, or the accelerator target / tarball that belongs to the other one, if there are as many tarballs as -# accelerator targets), and a warning is printed. Nothing is saved, no tarball is created. +# missing one(s). Without a terminal, the missing one(s) are inferred and a warning is printed. If there are as many +# tarballs as accelerator targets, the accelerator target / tarball that belongs to the other one is inferred (the n-th +# tarball was written after the n-th accelerator build). Otherwise they cannot be paired: the default accelerator target is +# the last one, and the default is not to resume from a tarball. Nothing is saved, no tarball is created. # stop as soon as something fails set -e @@ -329,22 +331,24 @@ fi RESUME_DIR="" # interactive debugging only handles a single accelerator target, and resumes from a single tarball of the bot's build step. -# - If both EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM are set, we use them as given. +# - If both EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM are set, we use them as given +# (EESSI_INTERACTIVE_BUILD_RESUME_FROM=none means: do not resume from a tarball, i.e. don't pass --resume). # - If one or both are unset and we have a terminal, we list the options and ask for the missing one(s). # - If one or both are unset and there is no terminal, we cannot ask: we infer the missing one(s) and print a warning. -# The suggested (default) accelerator target is the last one, and the default tarball the newest one. If the other one is -# known and there are as many tarballs as accelerator targets, the default is the one that belongs to it instead (the -# n-th tarball was written after the n-th accelerator build). -# Ask the user to choose a number between 1 and $2 for $1, default $3, result in INTERACTIVE_CHOICE +# The suggested (default) accelerator target is the last one. The default tarball is the one that belongs to the selected +# accelerator target (the n-th tarball was written after the n-th accelerator build), which we can only tell if there are +# as many tarballs as accelerator targets; otherwise the default is not to resume from a tarball. Likewise, if a tarball +# from the list is given and the counts are equal, the default accelerator target is the one that belongs to it. +# Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE interactive_choice() { - local what="$1" n="$2" default="$3" answer + local what="$1" n="$2" default="$3" min="${4:-1}" answer while true; do - if ! read -r -p "bot/build.sh: select ${what} [1-${n}, default ${default}]: " answer < /dev/tty; then + if ! read -r -p "bot/build.sh: select ${what} [${min}-${n}, default ${default}]: " answer < /dev/tty; then INTERACTIVE_CHOICE=${default} return fi answer=${answer:-${default}} - if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= n )); then + if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then INTERACTIVE_CHOICE=${answer} return fi @@ -364,12 +368,13 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then echo " [$((i+1))] '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$i]}'" done echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" + echo " [0] none (do not resume from a tarball, start from scratch)" for i in "${!available_tarballs[@]}"; do echo " [$((i+1))] $(basename ${available_tarballs[$i]}) ($(date -r ${available_tarballs[$i]} '+%Y-%m-%d %H:%M'), $(du -h ${available_tarballs[$i]} | cut -f1))" done echo " note: tarball [n] was written after the n-th accelerator build" if [[ ${n_tar} -ne ${n_accel} ]]; then - echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so this pairing is unreliable" + echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" fi can_prompt=0 @@ -377,7 +382,10 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then # index (1-based) of the tarball given in EESSI_INTERACTIVE_BUILD_RESUME_FROM in the list of tarballs (0 if not in there) given_tar_idx=0 - if [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + resume_none=0 + if [[ "${EESSI_INTERACTIVE_BUILD_RESUME_FROM,,}" == "none" ]]; then + resume_none=1 + elif [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then for i in "${!available_tarballs[@]}"; do if [[ "$(realpath -m ${available_tarballs[$i]})" == "$(realpath -m ${EESSI_INTERACTIVE_BUILD_RESUME_FROM})" ]]; then given_tar_idx=$((i+1)) @@ -415,34 +423,50 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then fi selected_accel="${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}" - # select tarball to resume from - if [[ -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then - if [[ ${n_tar} -eq 0 ]]; then - fatal_error "no state of the bot's build step to resume from found in ${TARBALL_TMP_BUILD_STEP_DIR}; set EESSI_INTERACTIVE_BUILD_RESUME_FROM" - fi - tar_idx=${n_tar} + # select tarball to resume from (or none) + # The default is the tarball that belongs to the selected accelerator target, which we can only tell if there are as many + # tarballs as accelerator targets. If not, there is no sensible default tarball, so the default is not to resume (0). + if [[ ${resume_none} -eq 0 && -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then if [[ ${n_tar} -eq ${n_accel} ]]; then tar_idx=${accel_idx} + else + tar_idx=0 fi - if [[ ${n_tar} -gt 1 ]]; then - if [[ ${can_prompt} -eq 1 ]]; then - interactive_choice "tarball to resume from" ${n_tar} ${tar_idx} - tar_idx=${INTERACTIVE_CHOICE} - else - msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from." - msg="${msg} Assuming '$(basename ${available_tarballs[$((tar_idx-1))]})'. To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM." - echo_yellow "${msg}" - fi + if [[ ${n_tar} -eq 0 ]]; then + echo_yellow "bot/build.sh: WARNING: no state of the bot's build step found in ${TARBALL_TMP_BUILD_STEP_DIR}, so not resuming from a tarball." + elif [[ ${can_prompt} -eq 1 ]]; then + interactive_choice "tarball to resume from (0 = none)" ${n_tar} ${tar_idx} 0 + tar_idx=${INTERACTIVE_CHOICE} + elif [[ ${tar_idx} -eq 0 ]]; then + msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from, and the ${n_tar} tarball(s)" + msg="${msg} cannot be paired with the ${n_accel} accelerator target(s). Assuming not to resume from a tarball." + msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." + echo_yellow "${msg}" + elif [[ ${n_tar} -gt 1 ]]; then + msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from." + msg="${msg} Assuming '$(basename ${available_tarballs[$((tar_idx-1))]})', the one that belongs to accelerator target '${selected_accel}'." + msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." + echo_yellow "${msg}" + fi + if [[ ${tar_idx} -eq 0 ]]; then + resume_none=1 + else + EESSI_INTERACTIVE_BUILD_RESUME_FROM="${available_tarballs[$((tar_idx-1))]}" fi - EESSI_INTERACTIVE_BUILD_RESUME_FROM="${available_tarballs[$((tar_idx-1))]}" fi - if [[ ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + if [[ ${resume_none} -eq 1 ]]; then + EESSI_INTERACTIVE_BUILD_RESUME_FROM="" + elif [[ ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then fatal_error "state to resume from does not exist: ${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" fi - echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" + if [[ ${resume_none} -eq 1 ]]; then + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', not resuming from a tarball" + else + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" + BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") + fi EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel}") - BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") fi for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do From 1c6ed23951454b82fd46abe81ff60743319ba29d Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen <33718780+casparvl@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:08:40 +0200 Subject: [PATCH 07/17] Apply batched suggestions from code review Co-authored-by: Caspar van Leeuwen <33718780+casparvl@users.noreply.github.com> --- bot/build.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/build.sh b/bot/build.sh index e584450c..bf02b507 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -342,12 +342,15 @@ RESUME_DIR="" # Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE interactive_choice() { local what="$1" n="$2" default="$3" min="${4:-1}" answer + # loop until we get a valid answer while true; do + # Make sure to use defautl & exist loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal disappears (avoid infinite loop) if ! read -r -p "bot/build.sh: select ${what} [${min}-${n}, default ${default}]: " answer < /dev/tty; then INTERACTIVE_CHOICE=${default} return fi answer=${answer:-${default}} + # Check answer is valid and return if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then INTERACTIVE_CHOICE=${answer} return From 1c4cc81fd5641fb5414412ebc746aaccd33a2eb6 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 16:23:46 +0200 Subject: [PATCH 08/17] Update to use stdin instead of tty --- bot/build.sh | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index bf02b507..3f37576a 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -35,8 +35,9 @@ # handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball, or 'none' to not resume from # anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are # used as given. If one or both are -# unset and there is a terminal, the available tarballs and accelerator targets are listed and you are asked for the -# missing one(s). Without a terminal, the missing one(s) are inferred and a warning is printed. If there are as many +# unset and stdin is a terminal (or answers are piped in, e.g. printf '2\n0\n' | ...), the available tarballs and +# accelerator targets are listed and you are asked for the missing one(s), one answer per line. Without that (e.g. in a +# batch job), the missing one(s) are inferred and a warning is printed. If there are as many # tarballs as accelerator targets, the accelerator target / tarball that belongs to the other one is inferred (the n-th # tarball was written after the n-th accelerator build). Otherwise they cannot be paired: the default accelerator target is # the last one, and the default is not to resume from a tarball. Nothing is saved, no tarball is created. @@ -333,8 +334,10 @@ RESUME_DIR="" # interactive debugging only handles a single accelerator target, and resumes from a single tarball of the bot's build step. # - If both EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM are set, we use them as given # (EESSI_INTERACTIVE_BUILD_RESUME_FROM=none means: do not resume from a tarball, i.e. don't pass --resume). -# - If one or both are unset and we have a terminal, we list the options and ask for the missing one(s). -# - If one or both are unset and there is no terminal, we cannot ask: we infer the missing one(s) and print a warning. +# - If one or both are unset and stdin is a terminal (or answers are piped in), we list the options and ask for the missing +# one(s), reading answers from stdin (one per line). +# - If one or both are unset and stdin is neither (e.g. /dev/null in a batch job), we cannot ask: we infer the missing +# one(s) and print a warning. # The suggested (default) accelerator target is the last one. The default tarball is the one that belongs to the selected # accelerator target (the n-th tarball was written after the n-th accelerator build), which we can only tell if there are # as many tarballs as accelerator targets; otherwise the default is not to resume from a tarball. Likewise, if a tarball @@ -345,10 +348,15 @@ interactive_choice() { # loop until we get a valid answer while true; do # Make sure to use defautl & exist loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal disappears (avoid infinite loop) - if ! read -r -p "bot/build.sh: select ${what} [${min}-${n}, default ${default}]: " answer < /dev/tty; then + # (we print the prompt ourselves: 'read -p' only shows it if stdin is a terminal, not if answers are piped in) + printf "bot/build.sh: select %s [%s-%s, default %s]: " "${what}" "${min}" "${n}" "${default}" >&2 + if ! read -r answer; then + echo >&2 INTERACTIVE_CHOICE=${default} return fi + # if the answer was piped in, it isn't echoed by a terminal, so show it in the output + [[ -t 0 ]] || echo "${answer}" >&2 answer=${answer:-${default}} # Check answer is valid and return if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then @@ -380,8 +388,12 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" fi + # we can ask if stdin is a terminal, or if answers are piped/redirected in (a here-string, pipe or file); not if stdin is + # /dev/null or closed (e.g. in a batch job) can_prompt=0 - { : < /dev/tty; } 2>/dev/null && can_prompt=1 + if [[ -t 0 || -p /dev/stdin || -f /dev/stdin ]]; then + can_prompt=1 + fi # index (1-based) of the tarball given in EESSI_INTERACTIVE_BUILD_RESUME_FROM in the list of tarballs (0 if not in there) given_tar_idx=0 @@ -418,7 +430,7 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then interactive_choice "accelerator target" ${n_accel} ${accel_idx} accel_idx=${INTERACTIVE_CHOICE} else - msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which accelerator target to use." + msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which accelerator target to use." msg="${msg} Assuming '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}'. To choose explicitly, set EESSI_INTERACTIVE_BUILD_ACCELERATOR." echo_yellow "${msg}" fi @@ -441,12 +453,12 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then interactive_choice "tarball to resume from (0 = none)" ${n_tar} ${tar_idx} 0 tar_idx=${INTERACTIVE_CHOICE} elif [[ ${tar_idx} -eq 0 ]]; then - msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from, and the ${n_tar} tarball(s)" + msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which tarball to resume from, and the ${n_tar} tarball(s)" msg="${msg} cannot be paired with the ${n_accel} accelerator target(s). Assuming not to resume from a tarball." msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." echo_yellow "${msg}" elif [[ ${n_tar} -gt 1 ]]; then - msg="bot/build.sh: WARNING: no interactive terminal, so could not ask which tarball to resume from." + msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which tarball to resume from." msg="${msg} Assuming '$(basename ${available_tarballs[$((tar_idx-1))]})', the one that belongs to accelerator target '${selected_accel}'." msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." echo_yellow "${msg}" From 05e6daa2036f3c64099c56f635d82af4ec6e18db Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen <33718780+casparvl@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:49:40 +0200 Subject: [PATCH 09/17] Apply batched suggestions from code review Co-authored-by: Caspar van Leeuwen <33718780+casparvl@users.noreply.github.com> --- bot/build.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index 3f37576a..c3cb7699 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -383,7 +383,7 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then for i in "${!available_tarballs[@]}"; do echo " [$((i+1))] $(basename ${available_tarballs[$i]}) ($(date -r ${available_tarballs[$i]} '+%Y-%m-%d %H:%M'), $(du -h ${available_tarballs[$i]} | cut -f1))" done - echo " note: tarball [n] was written after the n-th accelerator build" + echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" if [[ ${n_tar} -ne ${n_accel} ]]; then echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" fi @@ -391,6 +391,7 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then # we can ask if stdin is a terminal, or if answers are piped/redirected in (a here-string, pipe or file); not if stdin is # /dev/null or closed (e.g. in a batch job) can_prompt=0 + # Allow for the user to input answers via stdin, or preselect answers using bot/build.sh <<< "2" or bot/build.sh < answers.txt type of redirects / pipes if [[ -t 0 || -p /dev/stdin || -f /dev/stdin ]]; then can_prompt=1 fi @@ -398,8 +399,10 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then # index (1-based) of the tarball given in EESSI_INTERACTIVE_BUILD_RESUME_FROM in the list of tarballs (0 if not in there) given_tar_idx=0 resume_none=0 + # do case insensative match through lower-case conversion in bash expansion if [[ "${EESSI_INTERACTIVE_BUILD_RESUME_FROM,,}" == "none" ]]; then resume_none=1 + # If EESSI_INTERACTIVE_BUILD_RESUME_FROM is set, find the matching item in the available_tarballs list and set that as index `given_tar_idx` elif [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then for i in "${!available_tarballs[@]}"; do if [[ "$(realpath -m ${available_tarballs[$i]})" == "$(realpath -m ${EESSI_INTERACTIVE_BUILD_RESUME_FROM})" ]]; then @@ -409,6 +412,7 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then fi # select accelerator target + # If EESSI_INTERACTIVE_BUILD_ACCELERATOR is set, find the matching item in the EESSI_ACCELERATOR_TARGET_OVERRIDES list and set the matching index as `accel_idx`. if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then accel_idx=0 for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do @@ -421,11 +425,15 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${EESSI_ACCELERATOR_TARGET_OVERRIDES[*]}" fi else + # default to last accelerator accel_idx=${n_accel} + # if the number of tarballs and accelerators is the same, default to the same index as for the tarballs instead + # Since 1 tarball is produced per accelerator build, the n-th tarball should match the n-th accelerator target if [[ ${given_tar_idx} -gt 0 && ${n_tar} -eq ${n_accel} ]]; then accel_idx=${given_tar_idx} fi if [[ ${n_accel} -gt 1 ]]; then + # Now either prompt the user (with the default set above) if possible, otherwise, just use the default directly if [[ ${can_prompt} -eq 1 ]]; then interactive_choice "accelerator target" ${n_accel} ${accel_idx} accel_idx=${INTERACTIVE_CHOICE} @@ -453,13 +461,13 @@ if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then interactive_choice "tarball to resume from (0 = none)" ${n_tar} ${tar_idx} 0 tar_idx=${INTERACTIVE_CHOICE} elif [[ ${tar_idx} -eq 0 ]]; then - msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which tarball to resume from, and the ${n_tar} tarball(s)" - msg="${msg} cannot be paired with the ${n_accel} accelerator target(s). Assuming not to resume from a tarball." + msg="bot/build.sh: WARNING: cannot get user input on which tarball to resume from (no interactive terminal or piped input). Also, numbef of tarball(s) (${n_tar})" + msg="${msg} does not match the number of accelerators (${n_accel}) so we cannot pick a tarball automatically. Thus, we won't resume from a tarball." msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." echo_yellow "${msg}" elif [[ ${n_tar} -gt 1 ]]; then - msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which tarball to resume from." - msg="${msg} Assuming '$(basename ${available_tarballs[$((tar_idx-1))]})', the one that belongs to accelerator target '${selected_accel}'." + msg="bot/build.sh: WARNING: cannot get user input on which tarball to resume from (no interactive terminal or piped input)." + msg="${msg} Assuming that tarball '$(basename ${available_tarballs[$((tar_idx-1))]})' belongs to accelerator target '${selected_accel}' and resuming from there." msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." echo_yellow "${msg}" fi From a410e8ac6a775d8eb6ae6de90b56418a34954748 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 17:22:10 +0200 Subject: [PATCH 10/17] Refactor, the logic had become a bit of a spaghetti that was hard to trace --- bot/build.sh | 161 +++------------------------------------------------ 1 file changed, 7 insertions(+), 154 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index c3cb7699..58b83e22 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -331,163 +331,16 @@ else fi RESUME_DIR="" -# interactive debugging only handles a single accelerator target, and resumes from a single tarball of the bot's build step. -# - If both EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM are set, we use them as given -# (EESSI_INTERACTIVE_BUILD_RESUME_FROM=none means: do not resume from a tarball, i.e. don't pass --resume). -# - If one or both are unset and stdin is a terminal (or answers are piped in), we list the options and ask for the missing -# one(s), reading answers from stdin (one per line). -# - If one or both are unset and stdin is neither (e.g. /dev/null in a batch job), we cannot ask: we infer the missing -# one(s) and print a warning. -# The suggested (default) accelerator target is the last one. The default tarball is the one that belongs to the selected -# accelerator target (the n-th tarball was written after the n-th accelerator build), which we can only tell if there are -# as many tarballs as accelerator targets; otherwise the default is not to resume from a tarball. Likewise, if a tarball -# from the list is given and the counts are equal, the default accelerator target is the one that belongs to it. -# Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE -interactive_choice() { - local what="$1" n="$2" default="$3" min="${4:-1}" answer - # loop until we get a valid answer - while true; do - # Make sure to use defautl & exist loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal disappears (avoid infinite loop) - # (we print the prompt ourselves: 'read -p' only shows it if stdin is a terminal, not if answers are piped in) - printf "bot/build.sh: select %s [%s-%s, default %s]: " "${what}" "${min}" "${n}" "${default}" >&2 - if ! read -r answer; then - echo >&2 - INTERACTIVE_CHOICE=${default} - return - fi - # if the answer was piped in, it isn't echoed by a terminal, so show it in the output - [[ -t 0 ]] || echo "${answer}" >&2 - answer=${answer:-${default}} - # Check answer is valid and return - if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then - INTERACTIVE_CHOICE=${answer} - return - fi - echo "bot/build.sh: invalid choice '${answer}'" - done -} - +# interactive debugging only handles a single accelerator target, and resumes from a single tarball of the bot's build step +# (or from none). Select them as described at the top of this script, see scripts/interactive_build.sh if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then - mapfile -t available_tarballs < <(ls -tr ${TARBALL_TMP_BUILD_STEP_DIR}/tmp_storage-* 2>/dev/null) - n_accel=${#EESSI_ACCELERATOR_TARGET_OVERRIDES[@]} - n_tar=${#available_tarballs[@]} - - # show what is available, so it is easy to check that the accelerator and tarball selection belong together - echo "bot/build.sh: interactive debugging, available state of the bot's build step:" - echo " accelerator targets (in the order the bot built them):" - for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do - echo " [$((i+1))] '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$i]}'" - done - echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" - echo " [0] none (do not resume from a tarball, start from scratch)" - for i in "${!available_tarballs[@]}"; do - echo " [$((i+1))] $(basename ${available_tarballs[$i]}) ($(date -r ${available_tarballs[$i]} '+%Y-%m-%d %H:%M'), $(du -h ${available_tarballs[$i]} | cut -f1))" - done - echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" - if [[ ${n_tar} -ne ${n_accel} ]]; then - echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" - fi - - # we can ask if stdin is a terminal, or if answers are piped/redirected in (a here-string, pipe or file); not if stdin is - # /dev/null or closed (e.g. in a batch job) - can_prompt=0 - # Allow for the user to input answers via stdin, or preselect answers using bot/build.sh <<< "2" or bot/build.sh < answers.txt type of redirects / pipes - if [[ -t 0 || -p /dev/stdin || -f /dev/stdin ]]; then - can_prompt=1 - fi - - # index (1-based) of the tarball given in EESSI_INTERACTIVE_BUILD_RESUME_FROM in the list of tarballs (0 if not in there) - given_tar_idx=0 - resume_none=0 - # do case insensative match through lower-case conversion in bash expansion - if [[ "${EESSI_INTERACTIVE_BUILD_RESUME_FROM,,}" == "none" ]]; then - resume_none=1 - # If EESSI_INTERACTIVE_BUILD_RESUME_FROM is set, find the matching item in the available_tarballs list and set that as index `given_tar_idx` - elif [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then - for i in "${!available_tarballs[@]}"; do - if [[ "$(realpath -m ${available_tarballs[$i]})" == "$(realpath -m ${EESSI_INTERACTIVE_BUILD_RESUME_FROM})" ]]; then - given_tar_idx=$((i+1)) - fi - done - fi - - # select accelerator target - # If EESSI_INTERACTIVE_BUILD_ACCELERATOR is set, find the matching item in the EESSI_ACCELERATOR_TARGET_OVERRIDES list and set the matching index as `accel_idx`. - if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - accel_idx=0 - for i in "${!EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do - candidate="${EESSI_ACCELERATOR_TARGET_OVERRIDES[$i]}" - if [[ "${candidate}" == "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" || "${candidate}" == "accel/${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - accel_idx=$((i+1)) - fi - done - if [[ ${accel_idx} -eq 0 ]]; then - fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${EESSI_ACCELERATOR_TARGET_OVERRIDES[*]}" - fi - else - # default to last accelerator - accel_idx=${n_accel} - # if the number of tarballs and accelerators is the same, default to the same index as for the tarballs instead - # Since 1 tarball is produced per accelerator build, the n-th tarball should match the n-th accelerator target - if [[ ${given_tar_idx} -gt 0 && ${n_tar} -eq ${n_accel} ]]; then - accel_idx=${given_tar_idx} - fi - if [[ ${n_accel} -gt 1 ]]; then - # Now either prompt the user (with the default set above) if possible, otherwise, just use the default directly - if [[ ${can_prompt} -eq 1 ]]; then - interactive_choice "accelerator target" ${n_accel} ${accel_idx} - accel_idx=${INTERACTIVE_CHOICE} - else - msg="bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which accelerator target to use." - msg="${msg} Assuming '${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}'. To choose explicitly, set EESSI_INTERACTIVE_BUILD_ACCELERATOR." - echo_yellow "${msg}" - fi - fi - fi - selected_accel="${EESSI_ACCELERATOR_TARGET_OVERRIDES[$((accel_idx-1))]}" - - # select tarball to resume from (or none) - # The default is the tarball that belongs to the selected accelerator target, which we can only tell if there are as many - # tarballs as accelerator targets. If not, there is no sensible default tarball, so the default is not to resume (0). - if [[ ${resume_none} -eq 0 && -z "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then - if [[ ${n_tar} -eq ${n_accel} ]]; then - tar_idx=${accel_idx} - else - tar_idx=0 - fi - if [[ ${n_tar} -eq 0 ]]; then - echo_yellow "bot/build.sh: WARNING: no state of the bot's build step found in ${TARBALL_TMP_BUILD_STEP_DIR}, so not resuming from a tarball." - elif [[ ${can_prompt} -eq 1 ]]; then - interactive_choice "tarball to resume from (0 = none)" ${n_tar} ${tar_idx} 0 - tar_idx=${INTERACTIVE_CHOICE} - elif [[ ${tar_idx} -eq 0 ]]; then - msg="bot/build.sh: WARNING: cannot get user input on which tarball to resume from (no interactive terminal or piped input). Also, numbef of tarball(s) (${n_tar})" - msg="${msg} does not match the number of accelerators (${n_accel}) so we cannot pick a tarball automatically. Thus, we won't resume from a tarball." - msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." - echo_yellow "${msg}" - elif [[ ${n_tar} -gt 1 ]]; then - msg="bot/build.sh: WARNING: cannot get user input on which tarball to resume from (no interactive terminal or piped input)." - msg="${msg} Assuming that tarball '$(basename ${available_tarballs[$((tar_idx-1))]})' belongs to accelerator target '${selected_accel}' and resuming from there." - msg="${msg} To choose explicitly, set EESSI_INTERACTIVE_BUILD_RESUME_FROM (to a tarball, or to 'none' to start from scratch)." - echo_yellow "${msg}" - fi - if [[ ${tar_idx} -eq 0 ]]; then - resume_none=1 - else - EESSI_INTERACTIVE_BUILD_RESUME_FROM="${available_tarballs[$((tar_idx-1))]}" - fi - fi - if [[ ${resume_none} -eq 1 ]]; then - EESSI_INTERACTIVE_BUILD_RESUME_FROM="" - elif [[ ! -e "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then - fatal_error "state to resume from does not exist: ${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" - fi - - if [[ ${resume_none} -eq 1 ]]; then + source $software_layer_dir/scripts/interactive_build.sh + select_interactive_state + if [[ "${selected_tar}" == "none" ]]; then echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', not resuming from a tarball" else - echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${EESSI_INTERACTIVE_BUILD_RESUME_FROM}'" - BUILD_STEP_ARGS+=("--resume" "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}") + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${selected_tar}'" + BUILD_STEP_ARGS+=("--resume" "${selected_tar}") fi EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel}") fi From fe6eb17014bfa00a8e386d063a6c1a597e58412d Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 17:22:40 +0200 Subject: [PATCH 11/17] Forgot to add this... now needed from bot/build.sh if an interactive build is done --- scripts/interactive_build.sh | 146 +++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 scripts/interactive_build.sh diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh new file mode 100644 index 00000000..bd5c2d1a --- /dev/null +++ b/scripts/interactive_build.sh @@ -0,0 +1,146 @@ +# Functions for debugging a bot build interactively, sourced by bot/build.sh if EESSI_INTERACTIVE_BUILD is set. +# See the comments at the top of bot/build.sh for the user-facing description. +# +# Requires fatal_error and echo_yellow (scripts/utils.sh). + +# We can ask the user something if stdin is a terminal, or if answers are piped/redirected in (a here-string, pipe or +# file); not if stdin is /dev/null or closed (e.g. in a batch job) +can_ask() { + [[ -t 0 || -p /dev/stdin || -f /dev/stdin ]] +} + +# Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE +interactive_choice() { + local what="$1" n="$2" default="$3" min="${4:-1}" answer + # loop until we get a valid answer + while true; do + # (we print the prompt ourselves: 'read -p' only shows it if stdin is a terminal, not if answers are piped in) + printf "bot/build.sh: select %s [%s-%s, default %s]: " "${what}" "${min}" "${n}" "${default}" >&2 + # Use the default & exit the loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal + # disappears (avoid infinite loop) + if ! read -r answer; then + echo >&2 + INTERACTIVE_CHOICE=${default} + return + fi + # if the answer was piped in, it isn't echoed by a terminal, so show it in the output + [[ -t 0 ]] || echo "${answer}" >&2 + answer=${answer:-${default}} + # Check answer is valid and return + if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then + INTERACTIVE_CHOICE=${answer} + return + fi + echo "bot/build.sh: invalid choice '${answer}'" + done +} + +# Print the number (position in the array, starting at 1) of the first entry of the array named $1 that equals one of +# the other arguments, or 0 if none does +index_of() { + local -n haystack="$1" + shift + local i needle + for i in "${!haystack[@]}"; do + for needle in "$@"; do + if [[ "${haystack[$i]}" == "${needle}" ]]; then + echo $((i+1)) + return + fi + done + done + echo 0 +} + +# Choose one entry of the array named $5, and return its number in CHOSEN: N for entry N of the array (counting from 1), +# and 0 for 'none' (only allowed if $4, the lowest allowed number, is 0). +# $1 what is being chosen, $2 environment variable with which the user can set it, $3 default number, $4 lowest number +# If there is only one option, we don't need to choose. Otherwise we ask the user, if we can, or use the default and warn +choose_option() { + local what="$1" var="$2" default="$3" min="$4" + local -n options="$5" + local max=${#options[@]} + if [[ ${max} -eq ${min} ]]; then + CHOSEN=${max} + elif can_ask; then + interactive_choice "${what}" ${max} ${default} ${min} + CHOSEN=${INTERACTIVE_CHOICE} + else + CHOSEN=${default} + local assumed="none" + [[ ${CHOSEN} -gt 0 ]] && assumed="'${options[$((CHOSEN-1))]}'" + echo_yellow "bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which ${what}. Assuming ${assumed}. To choose explicitly, set ${var}." + fi +} + +# Select the accelerator target and the tarball of the bot's build step to resume from, based on +# EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM, asking for what isn't set (see bot/build.sh). +# Needs EESSI_ACCELERATOR_TARGET_OVERRIDES (the accelerator targets of the build) and TARBALL_TMP_BUILD_STEP_DIR. +# Returns the accelerator target in selected_accel, and the tarball (or directory) in selected_tar, which is the word +# 'none' if we shouldn't resume from anything. +select_interactive_state() { + local -a accels=("${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}") + local -a tarballs + # the n-th tarball was written after the n-th accelerator build, as each build produces one tarball + mapfile -t tarballs < <(ls -tr "${TARBALL_TMP_BUILD_STEP_DIR}"/tmp_storage-* 2>/dev/null) + local i + for i in "${!tarballs[@]}"; do + tarballs[$i]=$(realpath -m "${tarballs[$i]}") + done + local n_accel=${#accels[@]} n_tar=${#tarballs[@]} + + # show what is available, so it is easy to check that the accelerator and tarball selection belong together + echo "bot/build.sh: interactive debugging, available state of the bot's build step:" + echo " accelerator targets (in the order the bot built them):" + for i in "${!accels[@]}"; do + echo " [$((i+1))] '${accels[$i]}'" + done + echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" + echo " [0] none (do not resume from a tarball, start from scratch)" + for i in "${!tarballs[@]}"; do + echo " [$((i+1))] $(basename "${tarballs[$i]}") ($(date -r "${tarballs[$i]}" '+%Y-%m-%d %H:%M'), $(du -h "${tarballs[$i]}" | cut -f1))" + done + echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" + if [[ ${n_tar} -ne ${n_accel} ]]; then + echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" + fi + + # 1. resolve what the user gave: the number of the accelerator target / tarball in the lists above (0 if not given or, + # for a tarball, not in the list). EESSI_INTERACTIVE_BUILD_RESUME_FROM=none (in any case) means: don't resume + local accel_num=0 tar_num=0 given_tar="${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" + local given_accel="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" + [[ "${given_tar,,}" == "none" ]] && given_tar="none" + if [[ -n "${given_accel}" ]]; then + accel_num=$(index_of accels "${given_accel}" "accel/${given_accel}") + if [[ ${accel_num} -eq 0 ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${given_accel}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accels[*]}" + fi + fi + if [[ -n "${given_tar}" && "${given_tar}" != "none" ]]; then + tar_num=$(index_of tarballs "$(realpath -m "${given_tar}")") + fi + + # 2. choose what the user didn't give. If there are as many tarballs as accelerator targets, we can pair them (tarball n + # with accelerator target n) and suggest the one belonging to the other choice. If not, we suggest the last + # accelerator target and no tarball + local paired=0 + [[ ${n_tar} -eq ${n_accel} ]] && paired=1 + if [[ -z "${given_accel}" ]]; then + local default=${n_accel} + [[ ${paired} -eq 1 && ${tar_num} -gt 0 ]] && default=${tar_num} + choose_option "accelerator target" EESSI_INTERACTIVE_BUILD_ACCELERATOR ${default} 1 accels + accel_num=${CHOSEN} + fi + selected_accel="${accels[$((accel_num-1))]}" + if [[ -z "${given_tar}" ]]; then + local default=0 + [[ ${paired} -eq 1 ]] && default=${accel_num} + choose_option "tarball to resume from (0 = none)" EESSI_INTERACTIVE_BUILD_RESUME_FROM ${default} 0 tarballs + given_tar="none" + [[ ${CHOSEN} -gt 0 ]] && given_tar="${tarballs[$((CHOSEN-1))]}" + fi + selected_tar="${given_tar}" + if [[ "${selected_tar}" != "none" && ! -e "${selected_tar}" ]]; then + fatal_error "state to resume from does not exist: ${selected_tar}" + fi +} From d6da7d4dde31b84a12a3c8d488fb132ab9cd8375 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 17:55:12 +0200 Subject: [PATCH 12/17] Drop support for piping in answers, just keep it simple and require an interactive shell. Then, use read -p, and since this prints to stdout, do the whole selection conversation on stdout to avoid spreading it out --- bot/build.sh | 13 ++++++------- scripts/interactive_build.sh | 22 +++++++--------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index 58b83e22..5996e87d 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -34,13 +34,12 @@ # The state of the bot's build step (a tarball in previous_tmp/build_step) is resumed, and one accelerator target is # handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball, or 'none' to not resume from # anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are -# used as given. If one or both are -# unset and stdin is a terminal (or answers are piped in, e.g. printf '2\n0\n' | ...), the available tarballs and -# accelerator targets are listed and you are asked for the missing one(s), one answer per line. Without that (e.g. in a -# batch job), the missing one(s) are inferred and a warning is printed. If there are as many -# tarballs as accelerator targets, the accelerator target / tarball that belongs to the other one is inferred (the n-th -# tarball was written after the n-th accelerator build). Otherwise they cannot be paired: the default accelerator target is -# the last one, and the default is not to resume from a tarball. Nothing is saved, no tarball is created. +# used as given. If one or both are unset and stdin is a terminal, the available tarballs and accelerator targets are +# listed and you are asked for the missing one(s). Without a terminal (e.g. in a batch job), the missing one(s) are +# inferred and a warning is printed. If there are as many tarballs as accelerator targets, the accelerator target / +# tarball that belongs to the other one is inferred (the n-th tarball was written after the n-th accelerator build). +# Otherwise they cannot be paired: the default accelerator target is the last one, and the default is not to resume from +# a tarball. Nothing is saved, no tarball is created. # stop as soon as something fails set -e diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh index bd5c2d1a..6ad5f647 100644 --- a/scripts/interactive_build.sh +++ b/scripts/interactive_build.sh @@ -3,35 +3,26 @@ # # Requires fatal_error and echo_yellow (scripts/utils.sh). -# We can ask the user something if stdin is a terminal, or if answers are piped/redirected in (a here-string, pipe or -# file); not if stdin is /dev/null or closed (e.g. in a batch job) -can_ask() { - [[ -t 0 || -p /dev/stdin || -f /dev/stdin ]] -} - # Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE +# Note that all output of the dialog goes to stderr, like the prompt of 'read -p' does (bash's own convention for prompts) interactive_choice() { local what="$1" n="$2" default="$3" min="${4:-1}" answer # loop until we get a valid answer while true; do - # (we print the prompt ourselves: 'read -p' only shows it if stdin is a terminal, not if answers are piped in) - printf "bot/build.sh: select %s [%s-%s, default %s]: " "${what}" "${min}" "${n}" "${default}" >&2 # Use the default & exit the loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal # disappears (avoid infinite loop) - if ! read -r answer; then + if ! read -r -p "bot/build.sh: select ${what} [${min}-${n}, default ${default}]: " answer; then echo >&2 INTERACTIVE_CHOICE=${default} return fi - # if the answer was piped in, it isn't echoed by a terminal, so show it in the output - [[ -t 0 ]] || echo "${answer}" >&2 answer=${answer:-${default}} # Check answer is valid and return if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then INTERACTIVE_CHOICE=${answer} return fi - echo "bot/build.sh: invalid choice '${answer}'" + echo "bot/build.sh: invalid choice '${answer}'" >&2 done } @@ -55,21 +46,22 @@ index_of() { # Choose one entry of the array named $5, and return its number in CHOSEN: N for entry N of the array (counting from 1), # and 0 for 'none' (only allowed if $4, the lowest allowed number, is 0). # $1 what is being chosen, $2 environment variable with which the user can set it, $3 default number, $4 lowest number -# If there is only one option, we don't need to choose. Otherwise we ask the user, if we can, or use the default and warn +# If there is only one option, we don't need to choose. Otherwise we ask the user, if stdin is a terminal, or else use the +# default and warn choose_option() { local what="$1" var="$2" default="$3" min="$4" local -n options="$5" local max=${#options[@]} if [[ ${max} -eq ${min} ]]; then CHOSEN=${max} - elif can_ask; then + elif [[ -t 0 ]]; then interactive_choice "${what}" ${max} ${default} ${min} CHOSEN=${INTERACTIVE_CHOICE} else CHOSEN=${default} local assumed="none" [[ ${CHOSEN} -gt 0 ]] && assumed="'${options[$((CHOSEN-1))]}'" - echo_yellow "bot/build.sh: WARNING: no interactive terminal or piped input, so could not ask which ${what}. Assuming ${assumed}. To choose explicitly, set ${var}." + echo_yellow "bot/build.sh: WARNING: stdin is not a terminal, so could not ask which ${what}. Assuming ${assumed}. To choose explicitly, set ${var}." fi } From 34aa0906c51bea21dce5b5cafbdab6a000439c99 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 18:06:26 +0200 Subject: [PATCH 13/17] Rename things to be more descriptive --- bot/build.sh | 10 +-- scripts/interactive_build.sh | 151 ++++++++++++++++++----------------- 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index 5996e87d..72e4b2f7 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -335,13 +335,13 @@ RESUME_DIR="" if [[ -n "${EESSI_INTERACTIVE_BUILD}" ]]; then source $software_layer_dir/scripts/interactive_build.sh select_interactive_state - if [[ "${selected_tar}" == "none" ]]; then - echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', not resuming from a tarball" + if [[ "${selected_tarball}" == "none" ]]; then + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel_target}', not resuming from a tarball" else - echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel}', resuming from '${selected_tar}'" - BUILD_STEP_ARGS+=("--resume" "${selected_tar}") + echo "bot/build.sh: EESSI_INTERACTIVE_BUILD='${EESSI_INTERACTIVE_BUILD}', accelerator '${selected_accel_target}', resuming from '${selected_tarball}'" + BUILD_STEP_ARGS+=("--resume" "${selected_tarball}") fi - EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel}") + EESSI_ACCELERATOR_TARGET_OVERRIDES=("${selected_accel_target}") fi for ACCEL_OVERRIDE in "${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}"; do diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh index 6ad5f647..e1b6a227 100644 --- a/scripts/interactive_build.sh +++ b/scripts/interactive_build.sh @@ -3,38 +3,39 @@ # # Requires fatal_error and echo_yellow (scripts/utils.sh). -# Ask the user to choose a number between $4 (default 1) and $2 for $1, default $3, result in INTERACTIVE_CHOICE +# Ask the user to choose a number between $4 (lowest number, default 1) and $2 (highest number) for $1 (description of +# what is chosen), default $3 (default number). The result is returned in CHOSEN_NUMBER. # Note that all output of the dialog goes to stderr, like the prompt of 'read -p' does (bash's own convention for prompts) interactive_choice() { - local what="$1" n="$2" default="$3" min="${4:-1}" answer + local description="$1" highest_number="$2" default_number="$3" lowest_number="${4:-1}" answer # loop until we get a valid answer while true; do # Use the default & exit the loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal # disappears (avoid infinite loop) - if ! read -r -p "bot/build.sh: select ${what} [${min}-${n}, default ${default}]: " answer; then + if ! read -r -p "bot/build.sh: select ${description} [${lowest_number}-${highest_number}, default ${default_number}]: " answer; then echo >&2 - INTERACTIVE_CHOICE=${default} + CHOSEN_NUMBER=${default_number} return fi - answer=${answer:-${default}} + answer=${answer:-${default_number}} # Check answer is valid and return - if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= min && answer <= n )); then - INTERACTIVE_CHOICE=${answer} + if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= lowest_number && answer <= highest_number )); then + CHOSEN_NUMBER=${answer} return fi echo "bot/build.sh: invalid choice '${answer}'" >&2 done } -# Print the number (position in the array, starting at 1) of the first entry of the array named $1 that equals one of -# the other arguments, or 0 if none does -index_of() { - local -n haystack="$1" +# Print the position (counting from 1) in the array named $1 of the first entry that equals one of the other arguments +# (the search values), or 0 if none does +position_of() { + local -n array="$1" shift - local i needle - for i in "${!haystack[@]}"; do - for needle in "$@"; do - if [[ "${haystack[$i]}" == "${needle}" ]]; then + local i search_value + for i in "${!array[@]}"; do + for search_value in "$@"; do + if [[ "${array[$i]}" == "${search_value}" ]]; then echo $((i+1)) return fi @@ -43,96 +44,98 @@ index_of() { echo 0 } -# Choose one entry of the array named $5, and return its number in CHOSEN: N for entry N of the array (counting from 1), -# and 0 for 'none' (only allowed if $4, the lowest allowed number, is 0). -# $1 what is being chosen, $2 environment variable with which the user can set it, $3 default number, $4 lowest number +# Choose one entry of the array named $5, and return its number in CHOSEN_NUMBER: N for entry N of the array (counting from +# 1), and 0 for 'none' (only allowed if $4, the lowest number, is 0). +# $1 description of what is chosen, $2 name of the environment variable with which the user can set it, $3 default +# number, $4 lowest number # If there is only one option, we don't need to choose. Otherwise we ask the user, if stdin is a terminal, or else use the # default and warn choose_option() { - local what="$1" var="$2" default="$3" min="$4" - local -n options="$5" - local max=${#options[@]} - if [[ ${max} -eq ${min} ]]; then - CHOSEN=${max} + local description="$1" env_var_name="$2" default_number="$3" lowest_number="$4" + local -n option_array="$5" + local highest_number=${#option_array[@]} + if [[ ${highest_number} -eq ${lowest_number} ]]; then + CHOSEN_NUMBER=${highest_number} elif [[ -t 0 ]]; then - interactive_choice "${what}" ${max} ${default} ${min} - CHOSEN=${INTERACTIVE_CHOICE} + interactive_choice "${description}" ${highest_number} ${default_number} ${lowest_number} else - CHOSEN=${default} - local assumed="none" - [[ ${CHOSEN} -gt 0 ]] && assumed="'${options[$((CHOSEN-1))]}'" - echo_yellow "bot/build.sh: WARNING: stdin is not a terminal, so could not ask which ${what}. Assuming ${assumed}. To choose explicitly, set ${var}." + CHOSEN_NUMBER=${default_number} + local assumed_description="none" + [[ ${CHOSEN_NUMBER} -gt 0 ]] && assumed_description="'${option_array[$((CHOSEN_NUMBER-1))]}'" + echo_yellow "bot/build.sh: WARNING: stdin is not a terminal, so could not ask which ${description}. Assuming ${assumed_description}. To choose explicitly, set ${env_var_name}." fi } # Select the accelerator target and the tarball of the bot's build step to resume from, based on # EESSI_INTERACTIVE_BUILD_ACCELERATOR and EESSI_INTERACTIVE_BUILD_RESUME_FROM, asking for what isn't set (see bot/build.sh). # Needs EESSI_ACCELERATOR_TARGET_OVERRIDES (the accelerator targets of the build) and TARBALL_TMP_BUILD_STEP_DIR. -# Returns the accelerator target in selected_accel, and the tarball (or directory) in selected_tar, which is the word -# 'none' if we shouldn't resume from anything. +# Returns the accelerator target in selected_accel_target, and the tarball (or directory) in selected_tarball, which is +# the word 'none' if we shouldn't resume from anything. select_interactive_state() { - local -a accels=("${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}") - local -a tarballs + local -a accel_targets=("${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}") + local -a tarball_paths # the n-th tarball was written after the n-th accelerator build, as each build produces one tarball - mapfile -t tarballs < <(ls -tr "${TARBALL_TMP_BUILD_STEP_DIR}"/tmp_storage-* 2>/dev/null) + mapfile -t tarball_paths < <(ls -tr "${TARBALL_TMP_BUILD_STEP_DIR}"/tmp_storage-* 2>/dev/null) local i - for i in "${!tarballs[@]}"; do - tarballs[$i]=$(realpath -m "${tarballs[$i]}") + for i in "${!tarball_paths[@]}"; do + tarball_paths[$i]=$(realpath -m "${tarball_paths[$i]}") done - local n_accel=${#accels[@]} n_tar=${#tarballs[@]} + local num_accel_targets=${#accel_targets[@]} num_tarballs=${#tarball_paths[@]} # show what is available, so it is easy to check that the accelerator and tarball selection belong together echo "bot/build.sh: interactive debugging, available state of the bot's build step:" echo " accelerator targets (in the order the bot built them):" - for i in "${!accels[@]}"; do - echo " [$((i+1))] '${accels[$i]}'" + for i in "${!accel_targets[@]}"; do + echo " [$((i+1))] '${accel_targets[$i]}'" done echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" echo " [0] none (do not resume from a tarball, start from scratch)" - for i in "${!tarballs[@]}"; do - echo " [$((i+1))] $(basename "${tarballs[$i]}") ($(date -r "${tarballs[$i]}" '+%Y-%m-%d %H:%M'), $(du -h "${tarballs[$i]}" | cut -f1))" + for i in "${!tarball_paths[@]}"; do + echo " [$((i+1))] $(basename "${tarball_paths[$i]}") ($(date -r "${tarball_paths[$i]}" '+%Y-%m-%d %H:%M'), $(du -h "${tarball_paths[$i]}" | cut -f1))" done echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" - if [[ ${n_tar} -ne ${n_accel} ]]; then - echo " WARNING: ${n_tar} tarball(s) vs ${n_accel} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" + if [[ ${num_tarballs} -ne ${num_accel_targets} ]]; then + echo " WARNING: ${num_tarballs} tarball(s) vs ${num_accel_targets} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" fi - # 1. resolve what the user gave: the number of the accelerator target / tarball in the lists above (0 if not given or, - # for a tarball, not in the list). EESSI_INTERACTIVE_BUILD_RESUME_FROM=none (in any case) means: don't resume - local accel_num=0 tar_num=0 given_tar="${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" - local given_accel="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" - [[ "${given_tar,,}" == "none" ]] && given_tar="none" - if [[ -n "${given_accel}" ]]; then - accel_num=$(index_of accels "${given_accel}" "accel/${given_accel}") - if [[ ${accel_num} -eq 0 ]]; then - fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${given_accel}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accels[*]}" + # 1. resolve what the user requested: the number of the accelerator target / tarball in the lists above (0 if not + # requested or, for a tarball, not in the list). EESSI_INTERACTIVE_BUILD_RESUME_FROM=none (in any case) means: don't resume + local requested_accel="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" requested_tarball="${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" + local accel_number=0 tarball_number=0 + if [[ -n "${requested_accel}" ]]; then + accel_number=$(position_of accel_targets "${requested_accel}" "accel/${requested_accel}") + if [[ ${accel_number} -eq 0 ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${requested_accel}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accel_targets[*]}" fi fi - if [[ -n "${given_tar}" && "${given_tar}" != "none" ]]; then - tar_num=$(index_of tarballs "$(realpath -m "${given_tar}")") + if [[ -n "${requested_tarball}" && "${requested_tarball,,}" != "none" ]]; then + tarball_number=$(position_of tarball_paths "$(realpath -m "${requested_tarball}")") fi - # 2. choose what the user didn't give. If there are as many tarballs as accelerator targets, we can pair them (tarball n - # with accelerator target n) and suggest the one belonging to the other choice. If not, we suggest the last - # accelerator target and no tarball - local paired=0 - [[ ${n_tar} -eq ${n_accel} ]] && paired=1 - if [[ -z "${given_accel}" ]]; then - local default=${n_accel} - [[ ${paired} -eq 1 && ${tar_num} -gt 0 ]] && default=${tar_num} - choose_option "accelerator target" EESSI_INTERACTIVE_BUILD_ACCELERATOR ${default} 1 accels - accel_num=${CHOSEN} + # 2. choose what the user didn't request. If there are as many tarballs as accelerator targets, we can pair them + # (tarball n with accelerator target n) and suggest the one belonging to the other choice. If not, we suggest the + # last accelerator target and no tarball + local counts_match=0 + [[ ${num_tarballs} -eq ${num_accel_targets} ]] && counts_match=1 + if [[ -z "${requested_accel}" ]]; then + local default_accel_number=${num_accel_targets} + [[ ${counts_match} -eq 1 && ${tarball_number} -gt 0 ]] && default_accel_number=${tarball_number} + choose_option "accelerator target" EESSI_INTERACTIVE_BUILD_ACCELERATOR ${default_accel_number} 1 accel_targets + accel_number=${CHOSEN_NUMBER} fi - selected_accel="${accels[$((accel_num-1))]}" - if [[ -z "${given_tar}" ]]; then - local default=0 - [[ ${paired} -eq 1 ]] && default=${accel_num} - choose_option "tarball to resume from (0 = none)" EESSI_INTERACTIVE_BUILD_RESUME_FROM ${default} 0 tarballs - given_tar="none" - [[ ${CHOSEN} -gt 0 ]] && given_tar="${tarballs[$((CHOSEN-1))]}" + selected_accel_target="${accel_targets[$((accel_number-1))]}" + if [[ -n "${requested_tarball}" ]]; then + selected_tarball="${requested_tarball}" + else + local default_tarball_number=0 + [[ ${counts_match} -eq 1 ]] && default_tarball_number=${accel_number} + choose_option "tarball to resume from (0 = none)" EESSI_INTERACTIVE_BUILD_RESUME_FROM ${default_tarball_number} 0 tarball_paths + selected_tarball="none" + [[ ${CHOSEN_NUMBER} -gt 0 ]] && selected_tarball="${tarball_paths[$((CHOSEN_NUMBER-1))]}" fi - selected_tar="${given_tar}" - if [[ "${selected_tar}" != "none" && ! -e "${selected_tar}" ]]; then - fatal_error "state to resume from does not exist: ${selected_tar}" + # accept 'none' in any capitalisation + [[ "${selected_tarball,,}" == "none" ]] && selected_tarball="none" + if [[ "${selected_tarball}" != "none" && ! -e "${selected_tarball}" ]]; then + fatal_error "state to resume from does not exist: ${selected_tarball}" fi } From 0c26819fcc5154bbdc5bca9b48d3e98b881861ef Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 18:23:53 +0200 Subject: [PATCH 14/17] Interactive build: don't assume defaults when selecting accelerator target / tarball Simplify the selection of the accelerator target and the tarball to resume from in interactive builds (EESSI_INTERACTIVE_BUILD): don't infer anything. - If EESSI_INTERACTIVE_BUILD_ACCELERATOR / EESSI_INTERACTIVE_BUILD_RESUME_FROM is set, use it as given. - If not, ask the user to choose from the available options if stdin is a terminal, and stop with an error naming the variable to set if it is not. - Only choices with a single option are not asked: a single accelerator target, or no tarballs to resume from. We still ask whether to resume from a tarball or start from scratch if there is at least one tarball. This removes the pairing of tarballs with accelerator targets, the suggested defaults and the warnings about assumptions, position_of() and the canonicalization of the tarball paths that was only needed for lookups. The options are now only listed when we need to ask for them. Co-Authored-By: Claude Sonnet 5.5 --- bot/build.sh | 13 ++-- scripts/interactive_build.sh | 138 +++++++++++++---------------------- 2 files changed, 55 insertions(+), 96 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index 72e4b2f7..5b1ec367 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -33,13 +33,12 @@ # onfail - run the build, and start a shell when EasyBuild fails (or the build script exits non-zero) # The state of the bot's build step (a tarball in previous_tmp/build_step) is resumed, and one accelerator target is # handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball, or 'none' to not resume from -# anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If both are set, they are -# used as given. If one or both are unset and stdin is a terminal, the available tarballs and accelerator targets are -# listed and you are asked for the missing one(s). Without a terminal (e.g. in a batch job), the missing one(s) are -# inferred and a warning is printed. If there are as many tarballs as accelerator targets, the accelerator target / -# tarball that belongs to the other one is inferred (the n-th tarball was written after the n-th accelerator build). -# Otherwise they cannot be paired: the default accelerator target is the last one, and the default is not to resume from -# a tarball. Nothing is saved, no tarball is created. +# anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If one of them is set, it +# is used as given. If it is not set, you are asked to choose from the available accelerator targets / tarballs (the n-th +# tarball is normally the one written after the n-th accelerator build), which needs stdin to be a terminal. Nothing is +# assumed: without a terminal the script stops with an error asking you to set the variable(s). The only choices that +# are not asked are those with just one option: a single accelerator target, or no tarballs to resume from. +# Nothing is saved, no tarball is created. # stop as soon as something fails set -e diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh index e1b6a227..ac1c471a 100644 --- a/scripts/interactive_build.sh +++ b/scripts/interactive_build.sh @@ -1,23 +1,20 @@ # Functions for debugging a bot build interactively, sourced by bot/build.sh if EESSI_INTERACTIVE_BUILD is set. # See the comments at the top of bot/build.sh for the user-facing description. # -# Requires fatal_error and echo_yellow (scripts/utils.sh). +# Requires fatal_error (scripts/utils.sh). -# Ask the user to choose a number between $4 (lowest number, default 1) and $2 (highest number) for $1 (description of -# what is chosen), default $3 (default number). The result is returned in CHOSEN_NUMBER. +# Ask the user to choose a number between $2 (lowest number) and $3 (highest number) for $1 (description of what is +# chosen). The result is returned in CHOSEN_NUMBER. There is no default: an empty answer is invalid. Returns 1 if no +# answer can be read (e.g. at EOF because user pressed Ctrl+D / terminal disappears, which also avoids an infinite loop). # Note that all output of the dialog goes to stderr, like the prompt of 'read -p' does (bash's own convention for prompts) interactive_choice() { - local description="$1" highest_number="$2" default_number="$3" lowest_number="${4:-1}" answer + local description="$1" lowest_number="$2" highest_number="$3" answer # loop until we get a valid answer while true; do - # Use the default & exit the loop if read returns non-zero, e.g. at EOF because user pressed Ctrl+D / terminal - # disappears (avoid infinite loop) - if ! read -r -p "bot/build.sh: select ${description} [${lowest_number}-${highest_number}, default ${default_number}]: " answer; then + if ! read -r -p "bot/build.sh: select ${description} [${lowest_number}-${highest_number}]: " answer; then echo >&2 - CHOSEN_NUMBER=${default_number} - return + return 1 fi - answer=${answer:-${default_number}} # Check answer is valid and return if [[ "${answer}" =~ ^[0-9]+$ ]] && (( answer >= lowest_number && answer <= highest_number )); then CHOSEN_NUMBER=${answer} @@ -27,42 +24,22 @@ interactive_choice() { done } -# Print the position (counting from 1) in the array named $1 of the first entry that equals one of the other arguments -# (the search values), or 0 if none does -position_of() { - local -n array="$1" - shift - local i search_value - for i in "${!array[@]}"; do - for search_value in "$@"; do - if [[ "${array[$i]}" == "${search_value}" ]]; then - echo $((i+1)) - return - fi - done - done - echo 0 -} - -# Choose one entry of the array named $5, and return its number in CHOSEN_NUMBER: N for entry N of the array (counting from -# 1), and 0 for 'none' (only allowed if $4, the lowest number, is 0). -# $1 description of what is chosen, $2 name of the environment variable with which the user can set it, $3 default -# number, $4 lowest number -# If there is only one option, we don't need to choose. Otherwise we ask the user, if stdin is a terminal, or else use the -# default and warn +# Choose one entry of the array named $4, and return its number in CHOSEN_NUMBER: N for entry N of the array (counting from +# 1), and 0 for 'none' (only allowed if $3, the lowest number, is 0). +# $1 description of what is chosen, $2 name of the environment variable with which the user can set it +# If there is only one option, we don't need to choose. Otherwise we ask the user, if stdin is a terminal, or else stop +# with an error: we don't assume anything. choose_option() { - local description="$1" env_var_name="$2" default_number="$3" lowest_number="$4" - local -n option_array="$5" + local description="$1" env_var_name="$2" lowest_number="$3" + local -n option_array="$4" local highest_number=${#option_array[@]} if [[ ${highest_number} -eq ${lowest_number} ]]; then CHOSEN_NUMBER=${highest_number} elif [[ -t 0 ]]; then - interactive_choice "${description}" ${highest_number} ${default_number} ${lowest_number} + interactive_choice "${description}" ${lowest_number} ${highest_number} || + fatal_error "no answer given for ${description}, set ${env_var_name} to choose it without being asked" else - CHOSEN_NUMBER=${default_number} - local assumed_description="none" - [[ ${CHOSEN_NUMBER} -gt 0 ]] && assumed_description="'${option_array[$((CHOSEN_NUMBER-1))]}'" - echo_yellow "bot/build.sh: WARNING: stdin is not a terminal, so could not ask which ${description}. Assuming ${assumed_description}. To choose explicitly, set ${env_var_name}." + fatal_error "stdin is not a terminal, so cannot ask which ${description}, set ${env_var_name} to choose it" fi } @@ -74,62 +51,45 @@ choose_option() { select_interactive_state() { local -a accel_targets=("${EESSI_ACCELERATOR_TARGET_OVERRIDES[@]}") local -a tarball_paths - # the n-th tarball was written after the n-th accelerator build, as each build produces one tarball mapfile -t tarball_paths < <(ls -tr "${TARBALL_TMP_BUILD_STEP_DIR}"/tmp_storage-* 2>/dev/null) local i - for i in "${!tarball_paths[@]}"; do - tarball_paths[$i]=$(realpath -m "${tarball_paths[$i]}") - done - local num_accel_targets=${#accel_targets[@]} num_tarballs=${#tarball_paths[@]} - - # show what is available, so it is easy to check that the accelerator and tarball selection belong together - echo "bot/build.sh: interactive debugging, available state of the bot's build step:" - echo " accelerator targets (in the order the bot built them):" - for i in "${!accel_targets[@]}"; do - echo " [$((i+1))] '${accel_targets[$i]}'" - done - echo " build step tarballs in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" - echo " [0] none (do not resume from a tarball, start from scratch)" - for i in "${!tarball_paths[@]}"; do - echo " [$((i+1))] $(basename "${tarball_paths[$i]}") ($(date -r "${tarball_paths[$i]}" '+%Y-%m-%d %H:%M'), $(du -h "${tarball_paths[$i]}" | cut -f1))" - done - echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" - if [[ ${num_tarballs} -ne ${num_accel_targets} ]]; then - echo " WARNING: ${num_tarballs} tarball(s) vs ${num_accel_targets} accelerator target(s), so tarballs cannot be paired with accelerator targets and the default is not to resume from a tarball" - fi - # 1. resolve what the user requested: the number of the accelerator target / tarball in the lists above (0 if not - # requested or, for a tarball, not in the list). EESSI_INTERACTIVE_BUILD_RESUME_FROM=none (in any case) means: don't resume - local requested_accel="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" requested_tarball="${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" - local accel_number=0 tarball_number=0 - if [[ -n "${requested_accel}" ]]; then - accel_number=$(position_of accel_targets "${requested_accel}" "accel/${requested_accel}") - if [[ ${accel_number} -eq 0 ]]; then - fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${requested_accel}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accel_targets[*]}" + # select the accelerator target + if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then + # accept the target with or without the 'accel/' prefix + selected_accel_target="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" + [[ "${selected_accel_target}" == accel/* ]] || selected_accel_target="accel/${selected_accel_target}" + local found=0 accel_target + for accel_target in "${accel_targets[@]}"; do + [[ "${accel_target}" == "${selected_accel_target}" ]] && found=1 + done + if [[ ${found} -eq 0 ]]; then + fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accel_targets[*]}" fi - fi - if [[ -n "${requested_tarball}" && "${requested_tarball,,}" != "none" ]]; then - tarball_number=$(position_of tarball_paths "$(realpath -m "${requested_tarball}")") + else + # (no need to list or ask if there is only one) + if [[ ${#accel_targets[@]} -gt 1 ]]; then + echo "bot/build.sh: accelerator targets of this build (in the order the bot built them):" + for i in "${!accel_targets[@]}"; do + echo " [$((i+1))] '${accel_targets[$i]}'" + done + fi + choose_option "accelerator target" EESSI_INTERACTIVE_BUILD_ACCELERATOR 1 accel_targets + selected_accel_target="${accel_targets[$((CHOSEN_NUMBER-1))]}" fi - # 2. choose what the user didn't request. If there are as many tarballs as accelerator targets, we can pair them - # (tarball n with accelerator target n) and suggest the one belonging to the other choice. If not, we suggest the - # last accelerator target and no tarball - local counts_match=0 - [[ ${num_tarballs} -eq ${num_accel_targets} ]] && counts_match=1 - if [[ -z "${requested_accel}" ]]; then - local default_accel_number=${num_accel_targets} - [[ ${counts_match} -eq 1 && ${tarball_number} -gt 0 ]] && default_accel_number=${tarball_number} - choose_option "accelerator target" EESSI_INTERACTIVE_BUILD_ACCELERATOR ${default_accel_number} 1 accel_targets - accel_number=${CHOSEN_NUMBER} - fi - selected_accel_target="${accel_targets[$((accel_number-1))]}" - if [[ -n "${requested_tarball}" ]]; then - selected_tarball="${requested_tarball}" + # select the tarball to resume from, or none + if [[ -n "${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" ]]; then + selected_tarball="${EESSI_INTERACTIVE_BUILD_RESUME_FROM}" else - local default_tarball_number=0 - [[ ${counts_match} -eq 1 ]] && default_tarball_number=${accel_number} - choose_option "tarball to resume from (0 = none)" EESSI_INTERACTIVE_BUILD_RESUME_FROM ${default_tarball_number} 0 tarball_paths + echo "bot/build.sh: tarballs of the bot's build step in ${TARBALL_TMP_BUILD_STEP_DIR} (oldest first):" + echo " [0] none (do not resume from a tarball, start from scratch)" + for i in "${!tarball_paths[@]}"; do + echo " [$((i+1))] $(basename "${tarball_paths[$i]}") ($(date -r "${tarball_paths[$i]}" '+%Y-%m-%d %H:%M'), $(du -h "${tarball_paths[$i]}" | cut -f1))" + done + echo " note: normally, tarball [n] is written after the n-th accelerator build, since each build should produce one tarball" + [[ ${#tarball_paths[@]} -gt 0 ]] || echo " no tarballs found, so not resuming from a tarball" + choose_option "tarball to resume from (0 = none)" EESSI_INTERACTIVE_BUILD_RESUME_FROM 0 tarball_paths selected_tarball="none" [[ ${CHOSEN_NUMBER} -gt 0 ]] && selected_tarball="${tarball_paths[$((CHOSEN_NUMBER-1))]}" fi From c7fdd8a0f1891d6003d3a96d0a81365f76ae9915 Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 19:53:37 +0200 Subject: [PATCH 15/17] Interactive build: require full accelerator name, like EESSI_ACCELERATOR_TARGET_OVERRIDE EESSI_INTERACTIVE_BUILD_ACCELERATOR now takes the full name (e.g. accel/nvidia/cc80), consistent with EESSI_ACCELERATOR_TARGET_OVERRIDE and with the names listed when asking. Co-Authored-By: Claude Sonnet 5.5 --- bot/build.sh | 2 +- scripts/interactive_build.sh | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/build.sh b/bot/build.sh index 5b1ec367..45cf4e4b 100755 --- a/bot/build.sh +++ b/bot/build.sh @@ -33,7 +33,7 @@ # onfail - run the build, and start a shell when EasyBuild fails (or the build script exits non-zero) # The state of the bot's build step (a tarball in previous_tmp/build_step) is resumed, and one accelerator target is # handled. Select them with EESSI_INTERACTIVE_BUILD_RESUME_FROM (a tmp directory or tarball, or 'none' to not resume from -# anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. nvidia/cc80). If one of them is set, it +# anything and start from scratch) and EESSI_INTERACTIVE_BUILD_ACCELERATOR (e.g. accel/nvidia/cc80). If one of them is set, it # is used as given. If it is not set, you are asked to choose from the available accelerator targets / tarballs (the n-th # tarball is normally the one written after the n-th accelerator build), which needs stdin to be a terminal. Nothing is # assumed: without a terminal the script stops with an error asking you to set the variable(s). The only choices that diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh index ac1c471a..4caf636d 100644 --- a/scripts/interactive_build.sh +++ b/scripts/interactive_build.sh @@ -56,9 +56,8 @@ select_interactive_state() { # select the accelerator target if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - # accept the target with or without the 'accel/' prefix + # (same full name as EESSI_ACCELERATOR_TARGET_OVERRIDE, e.g. accel/nvidia/cc80) selected_accel_target="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" - [[ "${selected_accel_target}" == accel/* ]] || selected_accel_target="accel/${selected_accel_target}" local found=0 accel_target for accel_target in "${accel_targets[@]}"; do [[ "${accel_target}" == "${selected_accel_target}" ]] && found=1 From c5074a756f8c837c511dd01731bdaf9ada6d90aa Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Mon, 12 Oct 2026 20:18:26 +0200 Subject: [PATCH 16/17] Clarify some docstrings --- scripts/interactive_build.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/interactive_build.sh b/scripts/interactive_build.sh index 4caf636d..fa19e820 100644 --- a/scripts/interactive_build.sh +++ b/scripts/interactive_build.sh @@ -56,7 +56,8 @@ select_interactive_state() { # select the accelerator target if [[ -n "${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" ]]; then - # (same full name as EESSI_ACCELERATOR_TARGET_OVERRIDE, e.g. accel/nvidia/cc80) + # Format for EESSI_INTERACTIVE_BUILD_ACCELERATOR is similar to EESSI_ACCELERATOR_TARGET_OVERRIDE, + # e.g. accel/nvidia/cc80 selected_accel_target="${EESSI_INTERACTIVE_BUILD_ACCELERATOR}" local found=0 accel_target for accel_target in "${accel_targets[@]}"; do @@ -66,7 +67,8 @@ select_interactive_state() { fatal_error "EESSI_INTERACTIVE_BUILD_ACCELERATOR='${EESSI_INTERACTIVE_BUILD_ACCELERATOR}' not found in accelerator targets of ${JOB_CFG_FILE}: ${accel_targets[*]}" fi else - # (no need to list or ask if there is only one) + # List accelerators from the job.cfg file + # (skip listing if there is only one - choose_option will select it by default) if [[ ${#accel_targets[@]} -gt 1 ]]; then echo "bot/build.sh: accelerator targets of this build (in the order the bot built them):" for i in "${!accel_targets[@]}"; do @@ -92,7 +94,7 @@ select_interactive_state() { selected_tarball="none" [[ ${CHOSEN_NUMBER} -gt 0 ]] && selected_tarball="${tarball_paths[$((CHOSEN_NUMBER-1))]}" fi - # accept 'none' in any capitalisation + # accept 'none' in any capitalisation (and normalize to lower case for easier subsequent checks) [[ "${selected_tarball,,}" == "none" ]] && selected_tarball="none" if [[ "${selected_tarball}" != "none" && ! -e "${selected_tarball}" ]]; then fatal_error "state to resume from does not exist: ${selected_tarball}" From ec9035c0c0f6a2ab3f672589091368adb101400a Mon Sep 17 00:00:00 2001 From: Caspar van Leeuwen Date: Thu, 15 Oct 2026 17:06:47 +0200 Subject: [PATCH 17/17] Interactive build: give the terminal back after the debug shell exits The debug shell has job control, so it makes its own process group the terminal's foreground group. That group was left behind as the foreground group after the shell exited, so Ctrl+C no longer reached the build (observed: TPGID pointing at an empty process group). Explicitly set our own process group as the foreground group again. Co-Authored-By: Claude Sonnet 5.5 --- EESSI-install-software.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EESSI-install-software.sh b/EESSI-install-software.sh index 52b8968b..f621cf4c 100755 --- a/EESSI-install-software.sh +++ b/EESSI-install-software.sh @@ -110,6 +110,12 @@ debug_shell() { echo_yellow ">> Working directory: ${PWD}; exit the shell (Ctrl-D) to continue/finish" bash --rcfile "${rcfile}" -i < /dev/tty > /dev/tty 2>&1 rm -f "${rcfile}" + # The debug shell has job control, so it made its own process group the terminal's foreground group, and we don't + # get the terminal back reliably when it exits. Without this, Ctrl+C no longer reaches the build afterwards. + # (SIGTTOU is ignored, as changing the foreground group from a background group would otherwise stop us.) + python3 -c 'import os, signal +signal.signal(signal.SIGTTOU, signal.SIG_IGN) +os.tcsetpgrp(os.open("/dev/tty", os.O_RDWR), os.getpgrp())' 2>/dev/null || true } # If EESSI_INTERACTIVE_BUILD=onfail, then define a trap so that if an error is encounted that would cause the shell to exit