From c32f1ecaf6c0424c31d40700d2fd07aa92af19f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Thu, 26 Jun 2025 17:19:18 +0200 Subject: [PATCH 01/17] add hooks for improving reproducibility --- eb_hooks.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index e20ef2e4..b2a33cc5 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1,5 +1,6 @@ # Hooks to customize how EasyBuild installs software in EESSI # see https://docs.easybuild.io/en/latest/Hooks.html +import datetime import glob import os import re @@ -7,9 +8,10 @@ import easybuild.tools.environment as env from easybuild.easyblocks.generic.configuremake import obtain_config_guess from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS +from easybuild.tools import config from easybuild.tools.build_log import EasyBuildError, print_msg -from easybuild.tools.config import build_option, update_build_option -from easybuild.tools.filetools import apply_regex_substitutions, copy_file, remove_file, symlink, which +from easybuild.tools.config import build_option, install_path, update_build_option +from easybuild.tools.filetools import apply_regex_substitutions, copy_dir, copy_file, remove_file, symlink, which from easybuild.tools.run import run_cmd from easybuild.tools.systemtools import AARCH64, POWER, X86_64, get_cpu_architecture, get_cpu_features from easybuild.tools.toolchain.compiler import OPTARCH_GENERIC @@ -46,6 +48,8 @@ # Make sure a single environment variable name is used for this throughout the hooks EESSI_IGNORE_ZEN4_GCC1220_ENVVAR="EESSI_IGNORE_LMOD_ERROR_ZEN4_GCC1220" +STACK_REPROD_SUBDIR = 'reprod' + def is_gcccore_1220_based(**kwargs): # ecname, ecversion, tcname, tcversion): """ @@ -516,6 +520,26 @@ def post_module_hook_zen4_gcccore1220(self, *args, **kwargs): del self.initial_environ[EESSI_IGNORE_ZEN4_GCC1220_ENVVAR] +def post_easyblock_hook_copy_exts_patches_to_easybuild_subdir(self, *args, **kwargs): + """Copy the patches of extensions to the easybuild subdir of the installation directory.""" + + # Temporary fix for https://github.com/easybuilders/easybuild-framework/issues/4864: + # make sure that the patches of extensions are copied to the easybuild subdirectory + for ext in self.exts: + for patch in ext.get('patches', []): + new_log_dir = os.path.join(self.installdir, config.log_path(ec=self.cfg)) + target = os.path.join(new_log_dir, os.path.basename(patch['path'])) + copy_file(patch['path'], target) + + # Now we simply copy the entire "easybuild" subdirectory of the installed application + # to a timestamped subdirectory of the stack's central reprod directory, e.g.: + # + stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR) + now_utc_timestamp = datetime.datetime.now(datetime.UTC).isoformat('T', 'seconds').replace('+00:00', 'Z') + app_reprod_dir = os.path.join(stack_reprod_dir, self.install_subdir, now_utc_timestamp) + copy_dir(new_log_dir, app_reprod_dir) + + # Modules for dependencies are loaded in the prepare step. Thus, that's where we need this variable to be set # so that the modules can be succesfully loaded without printing the error (so that we can create a module # _with_ the warning for the current software being installed) @@ -1297,6 +1321,31 @@ def post_module_hook(self, *args, **kwargs): post_module_hook_zen4_gcccore1220(self, *args, **kwargs) +def post_easyblock_hook(self, *args, **kwargs): + """Main post easyblock hook: trigger custom functions based on software name.""" + if self.name in POST_EASYBLOCK_HOOKS: + POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) + + # Always trigger this one, regardless of self.name + post_easyblock_hook_copy_exts_patches_to_easybuild_subdir(self, *args, **kwargs) + + +def post_build_and_install_loop_hook(ecs, *args, **kwargs): + """ + Post build and install loop hook that copies the easybuild subdirectory of every installed application + to a central location in the root of the software stack, e.g.: + /path/to/stack/reprod/20250102T12:34:56Z/MyApp/1.2-foss-2025a + """ + + stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR) + for (ec, status) in ecs: + if status['success']: + now_timestamp = datetime.datetime.now(datetime.UTC).isoformat('T', 'seconds').replace('+00:00', 'Z') + app_easybuild_dir = os.path.dirname(status['log_file']) + app_reprod_dir = os.path.join(stack_reprod_dir, ec['short_mod_name'], now_timestamp, 'easybuild') + copy_dir(app_easybuild_dir, app_reprod_dir) + + PARSE_HOOKS = { 'casacore': parse_hook_casacore_disable_vectorize, 'CGAL': parse_hook_cgal_toolchainopts_precise, @@ -1365,6 +1414,8 @@ def post_module_hook(self, *args, **kwargs): POST_MODULE_HOOKS = {} +POST_EASYBLOCK_HOOKS = {} + # Define parallelism limit operations def divide_by_factor(parallel, factor): """Divide parallelism by given factor""" From ce0e0feac5ed5a502686c34a8738ef4c2e277481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Fri, 27 Jun 2025 09:43:36 +0200 Subject: [PATCH 02/17] remove temporary workaround that copies extension patches, clean up, only copy easybuild subdir with post_easyblock_hook --- eb_hooks.py | 44 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index b2a33cc5..a279729e 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -520,24 +520,18 @@ def post_module_hook_zen4_gcccore1220(self, *args, **kwargs): del self.initial_environ[EESSI_IGNORE_ZEN4_GCC1220_ENVVAR] -def post_easyblock_hook_copy_exts_patches_to_easybuild_subdir(self, *args, **kwargs): - """Copy the patches of extensions to the easybuild subdir of the installation directory.""" - - # Temporary fix for https://github.com/easybuilders/easybuild-framework/issues/4864: - # make sure that the patches of extensions are copied to the easybuild subdirectory - for ext in self.exts: - for patch in ext.get('patches', []): - new_log_dir = os.path.join(self.installdir, config.log_path(ec=self.cfg)) - target = os.path.join(new_log_dir, os.path.basename(patch['path'])) - copy_file(patch['path'], target) - - # Now we simply copy the entire "easybuild" subdirectory of the installed application - # to a timestamped subdirectory of the stack's central reprod directory, e.g.: - # +def post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs): + """ + Post easyblock hook that copies the easybuild subdirectory of every installed application + to a central and timestamped location in the root of the software stack, e.g.: + /path/to/stack/reprod/20250102T12:34:56Z/MyApp/1.2-foss-2025a + """ + stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR) now_utc_timestamp = datetime.datetime.now(datetime.UTC).isoformat('T', 'seconds').replace('+00:00', 'Z') - app_reprod_dir = os.path.join(stack_reprod_dir, self.install_subdir, now_utc_timestamp) - copy_dir(new_log_dir, app_reprod_dir) + app_easybuild_dir = os.path.join(self.installdir, config.log_path(ec=self.cfg)) + app_reprod_dir = os.path.join(stack_reprod_dir, self.install_subdir, now_utc_timestamp, 'easybuild') + copy_dir(app_easybuild_dir, app_reprod_dir) # Modules for dependencies are loaded in the prepare step. Thus, that's where we need this variable to be set @@ -1327,23 +1321,7 @@ def post_easyblock_hook(self, *args, **kwargs): POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) # Always trigger this one, regardless of self.name - post_easyblock_hook_copy_exts_patches_to_easybuild_subdir(self, *args, **kwargs) - - -def post_build_and_install_loop_hook(ecs, *args, **kwargs): - """ - Post build and install loop hook that copies the easybuild subdirectory of every installed application - to a central location in the root of the software stack, e.g.: - /path/to/stack/reprod/20250102T12:34:56Z/MyApp/1.2-foss-2025a - """ - - stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR) - for (ec, status) in ecs: - if status['success']: - now_timestamp = datetime.datetime.now(datetime.UTC).isoformat('T', 'seconds').replace('+00:00', 'Z') - app_easybuild_dir = os.path.dirname(status['log_file']) - app_reprod_dir = os.path.join(stack_reprod_dir, ec['short_mod_name'], now_timestamp, 'easybuild') - copy_dir(app_easybuild_dir, app_reprod_dir) + post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) PARSE_HOOKS = { From 98b9f9006423578bea682734b458be34cd45411c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 14:33:57 +0200 Subject: [PATCH 03/17] only trigger post_easyblock_hook_copy_easybuild_subdir for EESSI cvmfs and site installations --- eb_hooks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index a279729e..56bc2f6c 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1320,8 +1320,9 @@ def post_easyblock_hook(self, *args, **kwargs): if self.name in POST_EASYBLOCK_HOOKS: POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) - # Always trigger this one, regardless of self.name - post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) + # Always trigger this one for EESSI CVMFS/site installations, regardless of self.name + if os.getenv('EESSI_CVMFS_INSTALL') or os.getenv('EESSI_SITE_INSTALL'): + post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) PARSE_HOOKS = { From c5c7abc598c4e6e18ad9b14504e46e038e6bd818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 14:34:12 +0200 Subject: [PATCH 04/17] add an EB version check for the entire hooks file --- eb_hooks.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eb_hooks.py b/eb_hooks.py index 56bc2f6c..fa08b95e 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -50,6 +50,14 @@ STACK_REPROD_SUBDIR = 'reprod' +# The post_easyblock_hook was introduced in EB 5.1.1 +MIN_EASYBUILD_VERSION_REQUIRED = '5.1.1' +if EASYBUILD_VERSION < MIN_EASYBUILD_VERSION_REQUIRED: + raise EasyBuildError( + f"This hooks file requires at least EasyBuild version {MIN_EASYBUILD_VERSION_REQUIRED}, you are using {EASYBUILD_VERSION}." + ) + + def is_gcccore_1220_based(**kwargs): # ecname, ecversion, tcname, tcversion): """ From 27c588330cc151e471bd2a5720ef0fb5819f8191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 15:34:46 +0200 Subject: [PATCH 05/17] only define post_easyblock_hook function if for EB 5.1.1 and newer --- eb_hooks.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index fa08b95e..ded53ae9 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -50,13 +50,6 @@ STACK_REPROD_SUBDIR = 'reprod' -# The post_easyblock_hook was introduced in EB 5.1.1 -MIN_EASYBUILD_VERSION_REQUIRED = '5.1.1' -if EASYBUILD_VERSION < MIN_EASYBUILD_VERSION_REQUIRED: - raise EasyBuildError( - f"This hooks file requires at least EasyBuild version {MIN_EASYBUILD_VERSION_REQUIRED}, you are using {EASYBUILD_VERSION}." - ) - def is_gcccore_1220_based(**kwargs): # ecname, ecversion, tcname, tcversion): @@ -1323,14 +1316,19 @@ def post_module_hook(self, *args, **kwargs): post_module_hook_zen4_gcccore1220(self, *args, **kwargs) -def post_easyblock_hook(self, *args, **kwargs): - """Main post easyblock hook: trigger custom functions based on software name.""" - if self.name in POST_EASYBLOCK_HOOKS: - POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) +if EASYBUILD_VERSION >= '5.1.1': + def post_easyblock_hook(self, *args, **kwargs): + """Main post easyblock hook: trigger custom functions based on software name.""" + if self.name in POST_EASYBLOCK_HOOKS: + POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) - # Always trigger this one for EESSI CVMFS/site installations, regardless of self.name - if os.getenv('EESSI_CVMFS_INSTALL') or os.getenv('EESSI_SITE_INSTALL'): - post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) + # Always trigger this one for EESSI CVMFS/site installations, regardless of self.name + if os.getenv('EESSI_CVMFS_INSTALL') or os.getenv('EESSI_SITE_INSTALL'): + post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) + else: + self.log.debug("No CVMFS/site installation requested, not running post_easyblock_hook_copy_easybuild_subdir.") +else: + print_msg(f"Not enabling the post_easybuild_hook, as it requires EasyBuild 5.1.1 or newer.") PARSE_HOOKS = { From 1eb3cc282ea33388288d1a433fd873a46fa64b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 15:38:59 +0200 Subject: [PATCH 06/17] add comment about post_easyblock_hook --- eb_hooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eb_hooks.py b/eb_hooks.py index ded53ae9..cd2e4f4e 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1316,6 +1316,8 @@ def post_module_hook(self, *args, **kwargs): post_module_hook_zen4_gcccore1220(self, *args, **kwargs) +# The post_easyblock_hook was introduced in EasyBuild 5.1.1. +# Older versions would fail if the function is defined anyway, as EasyBuild performs some checks on function names in hooks files. if EASYBUILD_VERSION >= '5.1.1': def post_easyblock_hook(self, *args, **kwargs): """Main post easyblock hook: trigger custom functions based on software name.""" From 3dbdf52294e72f3bc32774e0511fbca4a7c39aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 15:43:51 +0200 Subject: [PATCH 07/17] only trigger post_easyblock_hook_copy_easybuild_subdir for EESSI 2025.06 and newer --- eb_hooks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index cd2e4f4e..f02c9668 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1324,9 +1324,10 @@ def post_easyblock_hook(self, *args, **kwargs): if self.name in POST_EASYBLOCK_HOOKS: POST_EASYBLOCK_HOOKS[self.name](self, *args, **kwargs) - # Always trigger this one for EESSI CVMFS/site installations, regardless of self.name + # Always trigger this one for EESSI CVMFS/site installations and version 2025.06 or newer, regardless of self.name if os.getenv('EESSI_CVMFS_INSTALL') or os.getenv('EESSI_SITE_INSTALL'): - post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) + if os.getenv('EESSI_VERSION') and LooseVersion(os.getenv('EESSI_VERSION')) >= '2025.06': + post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs) else: self.log.debug("No CVMFS/site installation requested, not running post_easyblock_hook_copy_easybuild_subdir.") else: From ad765673feef7c6ae1cdc62ebd67c9867a678d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 17:37:19 +0200 Subject: [PATCH 08/17] Update eb_hooks.py Co-authored-by: ocaisa --- eb_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eb_hooks.py b/eb_hooks.py index f02c9668..e195dbbe 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -525,7 +525,7 @@ def post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs): """ Post easyblock hook that copies the easybuild subdirectory of every installed application to a central and timestamped location in the root of the software stack, e.g.: - /path/to/stack/reprod/20250102T12:34:56Z/MyApp/1.2-foss-2025a + /path/to/stack/reprod/MyApp/1.2-foss-2025a/20250102T12:34:56Z """ stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR) From 53f16af845769ad486adc72b2ba681b819f99789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 8 Jul 2025 17:49:22 +0200 Subject: [PATCH 09/17] add dummy easystack --- .../2023.06/eessi-2023.06-eb-5.1.1-001-system.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml diff --git a/easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml b/easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml new file mode 100644 index 00000000..44fe4d92 --- /dev/null +++ b/easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml @@ -0,0 +1,2 @@ +easyconfigs: + - cowsay-3.04.eb From a6b494c83361e2285941a5d822137278bcd95751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Mon, 14 Jul 2025 23:17:11 +0200 Subject: [PATCH 10/17] move test easystack to 2025.06 --- .../eessi-2025.06-eb-5.1.1-001-system.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename easystacks/software.eessi.io/{2023.06/eessi-2023.06-eb-5.1.1-001-system.yml => 2025.06/eessi-2025.06-eb-5.1.1-001-system.yml} (100%) diff --git a/easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml b/easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml similarity index 100% rename from easystacks/software.eessi.io/2023.06/eessi-2023.06-eb-5.1.1-001-system.yml rename to easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml From 549080f27ae1b4e33c07bf3e31e745e16ee9ff66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 15 Jul 2025 14:10:51 +0200 Subject: [PATCH 11/17] include reprod dirs --- create_tarball.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/create_tarball.sh b/create_tarball.sh index 84956d14..d104e210 100755 --- a/create_tarball.sh +++ b/create_tarball.sh @@ -90,8 +90,13 @@ for subdir in ${sw_subdirs}; do for package_version in $(cat ${module_files_list}); do echo "handling ${package_version}" find ${eessi_version}/software/${os}/${subdir}/software/${package_version} -maxdepth 0 -type d \! -name '.wh.*' >> ${files_list} + # if there is a directory for this installation in the stack's reprod directory, include that too + if [ -d ${eessi_version}/software/${os}/${subdir}/reprod ]; then + find ${eessi_version}/software/${os}/${subdir}/reprod/${package_version} -maxdepth 0 -type d \! -name '.wh.*' >> ${files_list} + fi done fi + done # add a bit debug output From a6ea05bcdd8446a1838757889c8095271a118b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 15 Jul 2025 15:17:10 +0200 Subject: [PATCH 12/17] show reprod directories in tarball overview --- bot/check-build.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bot/check-build.sh b/bot/check-build.sh index a3c3a3a1..7c7be0ff 100755 --- a/bot/check-build.sh +++ b/bot/check-build.sh @@ -502,7 +502,9 @@ if [[ $USE_CHECK_BUILD_ARTEFACTS_SCRIPT -eq 0 ]]; then # extract directories/entries from tarball content modules_entries=$(grep "${prefix}/modules" ${tmpfile}) software_entries=$(grep "${prefix}/software" ${tmpfile}) - other_entries=$(cat ${tmpfile} | grep -v "${prefix}/modules" | grep -v "${prefix}/software") + reprod_entries=$(grep "${prefix}/reprod" ${tmpfile}) + reprod_shortened=$(echo "{reprod_entries}" | sed -e "s@${prefix}/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u) + other_entries=$(cat ${tmpfile} | grep -v "${prefix}/modules" | grep -v "${prefix}/software" | grep -v "${prefix}/reprod") other_shortened=$(echo "${other_entries}" | sed -e "s@^.*${prefix}/@@" | sort -u) modules=$(echo "${modules_entries}" | grep "/all/.*/.*lua$" | sed -e 's@^.*/\([^/]*/[^/]*.lua\)$@\1@' | sort -u) software_pkgs=$(echo "${software_entries}" | sed -e "s@${prefix}/software/@@" | awk -F/ '{if (NR >= 2) {print $1 "/" $2}}' | sort -u) @@ -531,6 +533,16 @@ if [[ $USE_CHECK_BUILD_ARTEFACTS_SCRIPT -eq 0 ]]; then comment_artifacts_list="${comment_artifacts_list}$(print_br_item '__ITEM__' 'no software packages in tarball')" fi comment_artifacts_list="${comment_artifacts_list}" + comment_artifacts_list="${comment_artifacts_list}$(print_br_item 'reprod directories under ___ITEM___' ${prefix}/reprod)" + comment_artifacts_list="${comment_artifacts_list}
"
+        if [[ ! -z ${reprod_shortened} ]]; then
+            while IFS= read -r reprod ; do
+                comment_artifacts_list="${comment_artifacts_list}$(print_br_item '__ITEM__' ${reprod})"
+            done <<< "${reprod_shortened}"
+        else
+            comment_artifacts_list="${comment_artifacts_list}$(print_br_item '__ITEM__' 'no reprod directories in tarball')"
+        fi
+        comment_artifacts_list="${comment_artifacts_list}
" comment_artifacts_list="${comment_artifacts_list}$(print_br_item 'other under ___ITEM___' ${prefix})" comment_artifacts_list="${comment_artifacts_list}
"
         if [[ ! -z ${other_shortened} ]]; then

From 0899e98509265823ceeceb5958c38177a5bc5ffa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= 
Date: Tue, 15 Jul 2025 15:25:59 +0200
Subject: [PATCH 13/17] add missing dollar sign

---
 bot/check-build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bot/check-build.sh b/bot/check-build.sh
index 7c7be0ff..696cb5d6 100755
--- a/bot/check-build.sh
+++ b/bot/check-build.sh
@@ -503,7 +503,7 @@ if [[ $USE_CHECK_BUILD_ARTEFACTS_SCRIPT -eq 0 ]]; then
         modules_entries=$(grep "${prefix}/modules" ${tmpfile})
         software_entries=$(grep "${prefix}/software" ${tmpfile})
         reprod_entries=$(grep "${prefix}/reprod" ${tmpfile})
-        reprod_shortened=$(echo "{reprod_entries}" | sed -e "s@${prefix}/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u)
+        reprod_shortened=$(echo "${reprod_entries}" | sed -e "s@${prefix}/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u)
         other_entries=$(cat ${tmpfile} | grep -v "${prefix}/modules" | grep -v "${prefix}/software" | grep -v "${prefix}/reprod")
         other_shortened=$(echo "${other_entries}" | sed -e "s@^.*${prefix}/@@" | sort -u)
         modules=$(echo "${modules_entries}" | grep "/all/.*/.*lua$" | sed -e 's@^.*/\([^/]*/[^/]*.lua\)$@\1@' | sort -u)

From c4f84678eca9ef6c31f658666522989668298495 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= 
Date: Tue, 15 Jul 2025 15:33:50 +0200
Subject: [PATCH 14/17] remove leading reprod dir

---
 bot/check-build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bot/check-build.sh b/bot/check-build.sh
index 696cb5d6..428ecb49 100755
--- a/bot/check-build.sh
+++ b/bot/check-build.sh
@@ -503,7 +503,7 @@ if [[ $USE_CHECK_BUILD_ARTEFACTS_SCRIPT -eq 0 ]]; then
         modules_entries=$(grep "${prefix}/modules" ${tmpfile})
         software_entries=$(grep "${prefix}/software" ${tmpfile})
         reprod_entries=$(grep "${prefix}/reprod" ${tmpfile})
-        reprod_shortened=$(echo "${reprod_entries}" | sed -e "s@${prefix}/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u)
+        reprod_shortened=$(echo "${reprod_entries}" | sed -e "s@${prefix}/reprod/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u)
         other_entries=$(cat ${tmpfile} | grep -v "${prefix}/modules" | grep -v "${prefix}/software" | grep -v "${prefix}/reprod")
         other_shortened=$(echo "${other_entries}" | sed -e "s@^.*${prefix}/@@" | sort -u)
         modules=$(echo "${modules_entries}" | grep "/all/.*/.*lua$" | sed -e 's@^.*/\([^/]*/[^/]*.lua\)$@\1@' | sort -u)

From 8135225f68e7e6e51f9f997a26126ce5f1ef20a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= 
Date: Tue, 15 Jul 2025 15:59:18 +0200
Subject: [PATCH 15/17] use YYYYMMDD_HHMMSSUTC formatting for reprod dirnames

---
 eb_hooks.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/eb_hooks.py b/eb_hooks.py
index e195dbbe..baa7958d 100644
--- a/eb_hooks.py
+++ b/eb_hooks.py
@@ -529,7 +529,7 @@ def post_easyblock_hook_copy_easybuild_subdir(self, *args, **kwargs):
     """
 
     stack_reprod_dir = os.path.join(os.path.dirname(install_path()), STACK_REPROD_SUBDIR)
-    now_utc_timestamp = datetime.datetime.now(datetime.UTC).isoformat('T', 'seconds').replace('+00:00', 'Z')
+    now_utc_timestamp = datetime.datetime.now(datetime.UTC).strftime('%Y%m%d_%H%M%S%Z')
     app_easybuild_dir = os.path.join(self.installdir, config.log_path(ec=self.cfg))
     app_reprod_dir = os.path.join(stack_reprod_dir, self.install_subdir, now_utc_timestamp, 'easybuild')
     copy_dir(app_easybuild_dir, app_reprod_dir)

From b5cad334bb1170be083e5997db86203ff3390b5e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= 
Date: Tue, 15 Jul 2025 16:53:15 +0200
Subject: [PATCH 16/17] remove 4th element of reprod path

---
 bot/check-build.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bot/check-build.sh b/bot/check-build.sh
index 428ecb49..507d962b 100755
--- a/bot/check-build.sh
+++ b/bot/check-build.sh
@@ -503,7 +503,7 @@ if [[ $USE_CHECK_BUILD_ARTEFACTS_SCRIPT -eq 0 ]]; then
         modules_entries=$(grep "${prefix}/modules" ${tmpfile})
         software_entries=$(grep "${prefix}/software" ${tmpfile})
         reprod_entries=$(grep "${prefix}/reprod" ${tmpfile})
-        reprod_shortened=$(echo "${reprod_entries}" | sed -e "s@${prefix}/reprod/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3 "/" $4}}' | sort -u)
+        reprod_shortened=$(echo "${reprod_entries}" | sed -e "s@${prefix}/reprod/@@" | awk -F/ '{if (NR >= 4) {print $1 "/" $2 "/" $3}}' | sort -u)
         other_entries=$(cat ${tmpfile} | grep -v "${prefix}/modules" | grep -v "${prefix}/software" | grep -v "${prefix}/reprod")
         other_shortened=$(echo "${other_entries}" | sed -e "s@^.*${prefix}/@@" | sort -u)
         modules=$(echo "${modules_entries}" | grep "/all/.*/.*lua$" | sed -e 's@^.*/\([^/]*/[^/]*.lua\)$@\1@' | sort -u)

From 269cca8b4c1ccb0872d377e0add9340ed9003550 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= 
Date: Tue, 15 Jul 2025 19:40:44 +0200
Subject: [PATCH 17/17] remove dummy easystack used for testing

---
 .../2025.06/eessi-2025.06-eb-5.1.1-001-system.yml               | 2 --
 1 file changed, 2 deletions(-)
 delete mode 100644 easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml

diff --git a/easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml b/easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml
deleted file mode 100644
index 44fe4d92..00000000
--- a/easystacks/software.eessi.io/2025.06/eessi-2025.06-eb-5.1.1-001-system.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-easyconfigs:
-  - cowsay-3.04.eb