From 03a03f7f46cbd1b46aa412c81429a825b6858aa0 Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:30:13 +0200 Subject: [PATCH 1/8] Implement pre-prepare hook for uv builds Add a pre-prepare hook to set SOURCE_DATE_EPOCH for uv builds to ensure a timestamp is available in A64FX builds --- eb_hooks.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/eb_hooks.py b/eb_hooks.py index 2e7597d9..c4d9b897 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1203,6 +1203,29 @@ def pre_configure_hook(self, *args, **kwargs): if self.name in ['FreeXL', 'libspatialite', 'VSEARCH']: self.cfg.update('configopts', 'CPPFLAGS="-DOF=_Z_OF ${CPPFLAGS}"') + def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): + """ + Set SOURCE_DATE_EPOCH for uv builds. + + This ensures that maturin uses an explicit timestamp while creating the + wheel rather than relying on the ZIP writer's default timestamp handling. + Otherwise the builds fail on A64FX. + """ + if self.name != 'uv': + raise EasyBuildError( + "uv-specific hook triggered for non-uv easyconfig?!" + ) + + source_date_epoch = str(int(datetime.datetime.now().timestamp())) + + env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) + + print_msg( + "Set SOURCE_DATE_EPOCH=%s for %s %s", + source_date_epoch, + self.name, + self.version, + ) def pre_configure_hook_BLIS(self, *args, **kwargs): """ @@ -2455,6 +2478,7 @@ def post_easyblock_hook(self, *args, **kwargs): 'LLVM': pre_prepare_hook_llvm_a64fx, 'PyTorch': pre_prepare_hook_pytorch, 'Rust': pre_prepare_hook_llvm_a64fx, + 'uv': pre_prepare_hook_uv_source_date_epoch, } POST_PREPARE_HOOKS = { From 7f4b90ca258c34692bcbb0e03b644d0fb594a43c Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:52:24 +0200 Subject: [PATCH 2/8] Update eb_hooks.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bob Dröge --- eb_hooks.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index c4d9b897..4a8a6a0b 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1204,28 +1204,28 @@ def pre_configure_hook(self, *args, **kwargs): self.cfg.update('configopts', 'CPPFLAGS="-DOF=_Z_OF ${CPPFLAGS}"') def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): - """ - Set SOURCE_DATE_EPOCH for uv builds. - - This ensures that maturin uses an explicit timestamp while creating the - wheel rather than relying on the ZIP writer's default timestamp handling. - Otherwise the builds fail on A64FX. - """ - if self.name != 'uv': - raise EasyBuildError( - "uv-specific hook triggered for non-uv easyconfig?!" - ) + """ + Set SOURCE_DATE_EPOCH for uv builds. + + This ensures that maturin uses an explicit timestamp while creating the + wheel rather than relying on the ZIP writer's default timestamp handling. + Otherwise the builds fail on A64FX. + """ + if self.name != 'uv': + raise EasyBuildError( + "uv-specific hook triggered for non-uv easyconfig?!" + ) - source_date_epoch = str(int(datetime.datetime.now().timestamp())) + source_date_epoch = str(int(datetime.datetime.now().timestamp())) - env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) + env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) - print_msg( - "Set SOURCE_DATE_EPOCH=%s for %s %s", - source_date_epoch, - self.name, - self.version, - ) + print_msg( + "Set SOURCE_DATE_EPOCH=%s for %s %s", + source_date_epoch, + self.name, + self.version, + ) def pre_configure_hook_BLIS(self, *args, **kwargs): """ From addbb8ff0dd6266b18a0961af2186d316503b8ec Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:10:51 +0200 Subject: [PATCH 3/8] Move the pre prepare hook to the right location Add pre-prepare hook to set SOURCE_DATE_EPOCH for uv builds. --- eb_hooks.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index 4a8a6a0b..0fdfbfc4 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1182,6 +1182,30 @@ def pre_prepare_hook_LAMMPS_kokkos_CUDA_families(self, *args, **kwargs): updated_cuda_cc = [v.replace('12.0f', '12.0') for v in cuda_cc] update_build_option('cuda_compute_capabilities', updated_cuda_cc) + +def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): + """ + Set SOURCE_DATE_EPOCH for uv builds. + + This ensures that maturin uses an explicit timestamp while creating the + wheel rather than relying on the ZIP writer's default timestamp handling. + Otherwise the builds fail on A64FX. + """ + if self.name != 'uv': + raise EasyBuildError( + "uv-specific hook triggered for non-uv easyconfig?!" + ) + + source_date_epoch = str(int(datetime.datetime.now().timestamp())) + + env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) + + print_msg( + "Set SOURCE_DATE_EPOCH=%s for %s %s", + source_date_epoch, + self.name, + self.version, + ) def post_prepare_hook_llvm_a64fx(self, *args, **kwargs): """ @@ -1203,30 +1227,6 @@ def pre_configure_hook(self, *args, **kwargs): if self.name in ['FreeXL', 'libspatialite', 'VSEARCH']: self.cfg.update('configopts', 'CPPFLAGS="-DOF=_Z_OF ${CPPFLAGS}"') - def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): - """ - Set SOURCE_DATE_EPOCH for uv builds. - - This ensures that maturin uses an explicit timestamp while creating the - wheel rather than relying on the ZIP writer's default timestamp handling. - Otherwise the builds fail on A64FX. - """ - if self.name != 'uv': - raise EasyBuildError( - "uv-specific hook triggered for non-uv easyconfig?!" - ) - - source_date_epoch = str(int(datetime.datetime.now().timestamp())) - - env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) - - print_msg( - "Set SOURCE_DATE_EPOCH=%s for %s %s", - source_date_epoch, - self.name, - self.version, - ) - def pre_configure_hook_BLIS(self, *args, **kwargs): """ Pre-configure hook for BLIS From f26df1f5b5a344c29b1103f3af310cd9e83beec2 Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:36:14 +0200 Subject: [PATCH 4/8] Use source timestamp for SOURCE_DATE_EPOCH Update pre_prepare_hook_uv_source_date_epoch to derive SOURCE_DATE_EPOCH from the source archive's timestamp, ensuring deterministic builds for uv. --- eb_hooks.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index 0fdfbfc4..ef2fb471 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1185,26 +1185,66 @@ def pre_prepare_hook_LAMMPS_kokkos_CUDA_families(self, *args, **kwargs): def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): """ - Set SOURCE_DATE_EPOCH for uv builds. + Set SOURCE_DATE_EPOCH for uv based on the timestamp recorded in the + upstream source archive. - This ensures that maturin uses an explicit timestamp while creating the - wheel rather than relying on the ZIP writer's default timestamp handling. - Otherwise the builds fail on A64FX. + maturin uses SOURCE_DATE_EPOCH when setting timestamps on wheel entries. + Using a timestamp derived from the source archive makes this deterministic + across builds. This value is missing in A64FX builds so we need to handle + it explicilty here when building uv. """ if self.name != 'uv': raise EasyBuildError( "uv-specific hook triggered for non-uv easyconfig?!" ) - source_date_epoch = str(int(datetime.datetime.now().timestamp())) + source_name = f"{self.name}-{self.version}.tar.gz" + + sources = [ + src for src in self.src + if src['name'] == source_name + ] + + if len(sources) != 1: + raise EasyBuildError( + "Expected exactly one source archive named %s, found %d", + source_name, + len(sources), + ) + + source_path = sources[0]['path'] + + try: + with tarfile.open(source_path, 'r:*') as archive: + mtimes = [ + member.mtime + for member in archive.getmembers() + if member.isfile() + ] + except (OSError, tarfile.TarError) as err: + raise EasyBuildError( + "Failed to determine SOURCE_DATE_EPOCH from %s: %s", + source_path, + err, + ) + + if not mtimes: + raise EasyBuildError( + "Could not determine SOURCE_DATE_EPOCH: " + "no files found in source archive %s", + source_path, + ) + + source_date_epoch = str(max(mtimes)) env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) print_msg( - "Set SOURCE_DATE_EPOCH=%s for %s %s", + "Set SOURCE_DATE_EPOCH=%s for %s %s based on source archive %s", source_date_epoch, self.name, self.version, + source_name, ) def post_prepare_hook_llvm_a64fx(self, *args, **kwargs): From 1fbddb70d0f9d43fb085c4c0301179143323af78 Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:12:35 +0200 Subject: [PATCH 5/8] Add import for tarfile module --- eb_hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eb_hooks.py b/eb_hooks.py index ef2fb471..20b2d691 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -7,6 +7,7 @@ import os import re import socket +import tarfile from typing import NamedTuple import easybuild.tools.environment as env From b1c87b8d487701dc4719d8346d964503350b4ab4 Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:39:34 +0200 Subject: [PATCH 6/8] Only work around SOURCE_DATE_EPOCH for A64FX --- eb_hooks.py | 96 +++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index 20b2d691..acaaa929 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1198,56 +1198,58 @@ def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): raise EasyBuildError( "uv-specific hook triggered for non-uv easyconfig?!" ) - - source_name = f"{self.name}-{self.version}.tar.gz" - - sources = [ - src for src in self.src - if src['name'] == source_name - ] - - if len(sources) != 1: - raise EasyBuildError( - "Expected exactly one source archive named %s, found %d", + + cpu_target = get_eessi_envvar('EESSI_SOFTWARE_SUBDIR') + if cpu_target == CPU_TARGET_A64FX: + source_name = f"{self.name}-{self.version}.tar.gz" + + sources = [ + src for src in self.src + if src['name'] == source_name + ] + + if len(sources) != 1: + raise EasyBuildError( + "Expected exactly one source archive named %s, found %d", + source_name, + len(sources), + ) + + source_path = sources[0]['path'] + + try: + with tarfile.open(source_path, 'r:*') as archive: + mtimes = [ + member.mtime + for member in archive.getmembers() + if member.isfile() + ] + except (OSError, tarfile.TarError) as err: + raise EasyBuildError( + "Failed to determine SOURCE_DATE_EPOCH from %s: %s", + source_path, + err, + ) + + if not mtimes: + raise EasyBuildError( + "Could not determine SOURCE_DATE_EPOCH: " + "no files found in source archive %s", + source_path, + ) + + source_date_epoch = str(max(mtimes)) + + env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) + + print_msg( + "Set SOURCE_DATE_EPOCH=%s for %s %s based on source archive %s", + source_date_epoch, + self.name, + self.version, source_name, - len(sources), ) - source_path = sources[0]['path'] - - try: - with tarfile.open(source_path, 'r:*') as archive: - mtimes = [ - member.mtime - for member in archive.getmembers() - if member.isfile() - ] - except (OSError, tarfile.TarError) as err: - raise EasyBuildError( - "Failed to determine SOURCE_DATE_EPOCH from %s: %s", - source_path, - err, - ) - - if not mtimes: - raise EasyBuildError( - "Could not determine SOURCE_DATE_EPOCH: " - "no files found in source archive %s", - source_path, - ) - - source_date_epoch = str(max(mtimes)) - - env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) - - print_msg( - "Set SOURCE_DATE_EPOCH=%s for %s %s based on source archive %s", - source_date_epoch, - self.name, - self.version, - source_name, - ) - def post_prepare_hook_llvm_a64fx(self, *args, **kwargs): """ Post-prepare hook for LLVM 14 and 15 on A64FX to reset optarch build option. From 82c4f6761144de676f8b1b59c350e1072b515a5e Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:05:59 +0200 Subject: [PATCH 7/8] Also guard for particular uv version --- eb_hooks.py | 98 +++++++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/eb_hooks.py b/eb_hooks.py index acaaa929..7843fdfa 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1199,56 +1199,58 @@ def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): "uv-specific hook triggered for non-uv easyconfig?!" ) - cpu_target = get_eessi_envvar('EESSI_SOFTWARE_SUBDIR') - if cpu_target == CPU_TARGET_A64FX: - source_name = f"{self.name}-{self.version}.tar.gz" - - sources = [ - src for src in self.src - if src['name'] == source_name - ] - - if len(sources) != 1: - raise EasyBuildError( - "Expected exactly one source archive named %s, found %d", + + if self.version == "0.10.9": + if cpu_target == CPU_TARGET_A64FX: + cpu_target = get_eessi_envvar('EESSI_SOFTWARE_SUBDIR') + source_name = f"{self.name}-{self.version}.tar.gz" + + sources = [ + src for src in self.src + if src['name'] == source_name + ] + + if len(sources) != 1: + raise EasyBuildError( + "Expected exactly one source archive named %s, found %d", + source_name, + len(sources), + ) + + source_path = sources[0]['path'] + + try: + with tarfile.open(source_path, 'r:*') as archive: + mtimes = [ + member.mtime + for member in archive.getmembers() + if member.isfile() + ] + except (OSError, tarfile.TarError) as err: + raise EasyBuildError( + "Failed to determine SOURCE_DATE_EPOCH from %s: %s", + source_path, + err, + ) + + if not mtimes: + raise EasyBuildError( + "Could not determine SOURCE_DATE_EPOCH: " + "no files found in source archive %s", + source_path, + ) + + source_date_epoch = str(max(mtimes)) + + env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) + + print_msg( + "Set SOURCE_DATE_EPOCH=%s for %s %s based on source archive %s", + source_date_epoch, + self.name, + self.version, source_name, - len(sources), - ) - - source_path = sources[0]['path'] - - try: - with tarfile.open(source_path, 'r:*') as archive: - mtimes = [ - member.mtime - for member in archive.getmembers() - if member.isfile() - ] - except (OSError, tarfile.TarError) as err: - raise EasyBuildError( - "Failed to determine SOURCE_DATE_EPOCH from %s: %s", - source_path, - err, ) - - if not mtimes: - raise EasyBuildError( - "Could not determine SOURCE_DATE_EPOCH: " - "no files found in source archive %s", - source_path, - ) - - source_date_epoch = str(max(mtimes)) - - env.setvar('SOURCE_DATE_EPOCH', source_date_epoch) - - print_msg( - "Set SOURCE_DATE_EPOCH=%s for %s %s based on source archive %s", - source_date_epoch, - self.name, - self.version, - source_name, - ) def post_prepare_hook_llvm_a64fx(self, *args, **kwargs): """ From b36bb5953ab1d49238b78f5d98695d7bf3bc8c69 Mon Sep 17 00:00:00 2001 From: Pedro Santos Neves <10762799+Neves-P@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:07:14 +0200 Subject: [PATCH 8/8] Need to define cpu_target before using it... --- eb_hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/eb_hooks.py b/eb_hooks.py index 7843fdfa..e94b2873 100644 --- a/eb_hooks.py +++ b/eb_hooks.py @@ -1201,6 +1201,7 @@ def pre_prepare_hook_uv_source_date_epoch(self, *args, **kwargs): if self.version == "0.10.9": + cpu_target = get_eessi_envvar('EESSI_SOFTWARE_SUBDIR') if cpu_target == CPU_TARGET_A64FX: cpu_target = get_eessi_envvar('EESSI_SOFTWARE_SUBDIR') source_name = f"{self.name}-{self.version}.tar.gz"