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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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}"