Skip to content

Commit 1f8b68e

Browse files
authored
Merge branch 'develop' into 20250813131209_new_pr_bundle
2 parents 59d358f + cae1c39 commit 1f8b68e

File tree

28 files changed

+629
-239
lines changed

28 files changed

+629
-239
lines changed

easybuild/easyblocks/a/amber.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -88,35 +88,37 @@ def extract_step(self):
8888
def patch_step(self, *args, **kwargs):
8989
"""Patch Amber using 'update_amber' tool, prior to applying listed patch files (if any)."""
9090

91-
# figure out which Python command to use to run the update_amber script;
92-
# by default it uses 'python', but this may not be available (on CentOS 8 for example);
93-
# note that the dependencies are not loaded yet at this point, so we're at the mercy of the OS here...
94-
python_cmd = None
95-
for cand_python_cmd in ['python', 'python3', 'python2']:
96-
if which(cand_python_cmd):
97-
python_cmd = cand_python_cmd
98-
break
99-
100-
if python_cmd is None:
101-
raise EasyBuildError("No suitable Python command found to run update_amber script!")
102-
103-
if self.cfg['patchlevels'] == "latest":
104-
cmd = "%s ./update_amber --update" % python_cmd
105-
# Run as many times as specified. It is the responsibility
106-
# of the easyconfig author to get this right, especially if
107-
# he or she selects "latest". (Note: "latest" is not
108-
# recommended for this reason and others.)
109-
for _ in range(self.cfg['patchruns']):
110-
run_shell_cmd(cmd)
111-
else:
112-
for (tree, patch_level) in zip(['AmberTools', 'Amber'], self.cfg['patchlevels']):
113-
if patch_level == 0:
114-
continue
115-
cmd = "%s ./update_amber --update-to %s/%s" % (python_cmd, tree, patch_level)
91+
# Use the update_amber script if patchlevels is defined - if not then the easyconfig should apply the patches
92+
if self.cfg['patchlevels']:
93+
# figure out which Python command to use to run the update_amber script;
94+
# by default it uses 'python', but this may not be available (on CentOS 8 for example);
95+
# note that the dependencies are not loaded yet at this point, so we're at the mercy of the OS here...
96+
python_cmd = None
97+
for cand_python_cmd in ['python', 'python3', 'python2']:
98+
if which(cand_python_cmd):
99+
python_cmd = cand_python_cmd
100+
break
101+
102+
if python_cmd is None:
103+
raise EasyBuildError("No suitable Python command found to run update_amber script!")
104+
105+
if self.cfg['patchlevels'] == "latest":
106+
cmd = "%s ./update_amber --update" % python_cmd
116107
# Run as many times as specified. It is the responsibility
117-
# of the easyconfig author to get this right.
108+
# of the easyconfig author to get this right, especially if
109+
# he or she selects "latest". (Note: "latest" is not
110+
# recommended for this reason and others.)
118111
for _ in range(self.cfg['patchruns']):
119112
run_shell_cmd(cmd)
113+
else:
114+
for (tree, patch_level) in zip(['AmberTools', 'Amber'], self.cfg['patchlevels']):
115+
if patch_level == 0:
116+
continue
117+
cmd = "%s ./update_amber --update-to %s/%s" % (python_cmd, tree, patch_level)
118+
# Run as many times as specified. It is the responsibility
119+
# of the easyconfig author to get this right.
120+
for _ in range(self.cfg['patchruns']):
121+
run_shell_cmd(cmd)
120122

121123
super().patch_step(*args, **kwargs)
122124

easybuild/easyblocks/e/elpa.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ def configure_step(self):
206206
self.cfg.update('configopts', '--with-NVIDIA-GPU-compute-capability=sm_%s' % cuda_cc_string)
207207
self.log.info("Enabling nvidia GPU support for compute capability: %s", cuda_cc_string)
208208
# There is a dedicated kernel for sm80, but only from version 2021.11.001 onwards
209-
if float(cuda_cc) >= 8.0 and LooseVersion(self.version) >= LooseVersion('2021.11.001'):
209+
# Trying to use these kernels for GPUs newer than sm80 will fail ELPA configure
210+
if float(cuda_cc) == 8.0 and LooseVersion(self.version) >= LooseVersion('2021.11.001'):
210211
self.cfg.update('configopts', '--enable-nvidia-sm80-gpu')
211212

212213
# From v2022.05.001 onwards, the config complains if CPP is not set, resulting in non-zero exit of configure

easybuild/easyblocks/g/ghc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class EB_GHC(ConfigureMake):
3737
Support for building and installing applications with configure/make/make install
3838
"""
3939

40-
def build_step(self, verbose=False):
40+
def build_step(self, *args, **kwargs):
4141
"""
4242
Support for a binary 6.12.x installation. Starting there,
4343
later GHC versions are build from source and thus require
@@ -46,4 +46,4 @@ def build_step(self, verbose=False):
4646
if LooseVersion(self.version) < LooseVersion("7.0"):
4747
pass
4848
else:
49-
super().build_step(verbose=verbose)
49+
super().build_step(*args, **kwargs)

easybuild/easyblocks/generic/buildenv.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
2929
@author: Alan O'Cais (Juelich Supercomputing Centre)
3030
"""
31+
import glob
3132
import os
3233
import stat
3334

3435
from easybuild.easyblocks.generic.bundle import Bundle
35-
from easybuild.tools.filetools import adjust_permissions, copy_dir
36+
from easybuild.framework.easyconfig import CUSTOM
37+
from easybuild.tools.build_log import EasyBuildError
38+
from easybuild.tools.filetools import adjust_permissions, apply_regex_substitutions, copy_dir
3639
from easybuild.tools.toolchain.toolchain import RPATH_WRAPPERS_SUBDIR
3740

3841

@@ -41,6 +44,15 @@ class BuildEnv(Bundle):
4144
Build environment of toolchain: only generate module file
4245
"""
4346

47+
@staticmethod
48+
def extra_options():
49+
"""Add extra easyconfig parameters for Boost."""
50+
extra_vars = {
51+
'python_executable': ['python3', "Python executable to use for the wrappers (use None to use path to "
52+
"Python executable used by EasyBuild).", CUSTOM],
53+
}
54+
return Bundle.extra_options(extra_vars)
55+
4456
def prepare_step(self, *args, **kwargs):
4557
"""
4658
Custom prepare step for buildenv: export rpath wrappers if they are being used
@@ -60,7 +72,16 @@ def install_step(self, *args, **kwargs):
6072
wrappers_dir = os.path.join(self.rpath_wrappers_dir, RPATH_WRAPPERS_SUBDIR)
6173
if os.path.exists(wrappers_dir):
6274
self.rpath_wrappers_dir = os.path.join(self.installdir, 'bin')
63-
copy_dir(wrappers_dir, os.path.join(self.rpath_wrappers_dir, RPATH_WRAPPERS_SUBDIR))
75+
rpath_wrappers_path = os.path.join(self.rpath_wrappers_dir, RPATH_WRAPPERS_SUBDIR)
76+
copy_dir(wrappers_dir, rpath_wrappers_path)
77+
py_exe = self.cfg['python_executable']
78+
if py_exe is not None:
79+
if not isinstance(py_exe, str) or not py_exe:
80+
raise EasyBuildError(f"python_executable should be None or non-empty string, got {py_exe}")
81+
wrapper_files = list(filter(os.path.isfile, glob.glob(os.path.join(rpath_wrappers_path, '*', '*'))))
82+
# replace path to Python executable with python_executable in the wrappers,
83+
# as this is the executable that runs EasyBuild and may be unavailable when using the buildenv wrappers.
84+
apply_regex_substitutions(wrapper_files, [(r'^PYTHON_EXE=.*$', f'PYTHON_EXE={py_exe}')], backup=False)
6485
# Make sure wrappers are readable/executable by everyone
6586
adjust_permissions(
6687
self.rpath_wrappers_dir,

easybuild/easyblocks/generic/bundle.py

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,29 @@
3535
"""
3636
import copy
3737
import os
38+
from datetime import datetime
3839

3940
import easybuild.tools.environment as env
4041
from easybuild.framework.easyblock import EasyBlock
4142
from easybuild.framework.easyconfig import CUSTOM
42-
from easybuild.framework.easyconfig.default import DEFAULT_CONFIG
43+
from easybuild.framework.easyconfig.default import get_easyconfig_parameter_default
44+
from easybuild.framework.easyconfig.default import is_easyconfig_parameter_default_value
4345
from easybuild.framework.easyconfig.easyconfig import get_easyblock_class
4446
from easybuild.tools.build_log import EasyBuildError, print_msg
4547
from easybuild.tools.config import build_option
4648
from easybuild.tools.hooks import TEST_STEP
4749
from easybuild.tools.modules import get_software_root, get_software_version
48-
from easybuild.tools.utilities import nub
50+
from easybuild.tools.utilities import nub, time2str
51+
52+
53+
# Description and step name run during component installation
54+
COMPONENT_INSTALL_STEPS = [
55+
('patching', 'patch'),
56+
('configuring', 'configure'),
57+
('building', 'build'),
58+
('testing', 'test'),
59+
('installing', 'install'),
60+
]
4961

5062

5163
class Bundle(EasyBlock):
@@ -125,24 +137,6 @@ def __init__(self, *args, **kwargs):
125137
if len(comp) == 3:
126138
comp_specs = comp[2]
127139

128-
comp_cfg = self.cfg.copy()
129-
130-
comp_cfg['name'] = comp_name
131-
comp_cfg['version'] = comp_version
132-
133-
# The copy above may include unexpected settings for common values.
134-
# In particular for a Pythonbundle we have seen a component inheriting
135-
# runtest = True
136-
# which is not a valid value for many easyblocks.
137-
# Reset runtest to the original default, if people want the test step
138-
# they can set it explicitly, in default_component_specs or by the component easyblock
139-
if comp_cfg._config['runtest'] != DEFAULT_CONFIG["runtest"]:
140-
self.log.warning(
141-
"Resetting runtest to default value for component easyblock "
142-
f"(from {comp_cfg._config['runtest']})."
143-
)
144-
comp_cfg._config['runtest'] = DEFAULT_CONFIG["runtest"]
145-
146140
# determine easyblock to use for this component
147141
# - if an easyblock is specified explicitly, that will be used
148142
# - if not, a software-specific easyblock will be considered by get_easyblock_class
@@ -164,28 +158,45 @@ def __init__(self, *args, **kwargs):
164158
if easyblock == 'Bundle':
165159
raise EasyBuildError("The Bundle easyblock can not be used to install components in a bundle")
166160

161+
comp_cfg = self.cfg.copy()
167162
comp_cfg.easyblock = easyblock_class
168163

169164
# make sure that extra easyconfig parameters are known, so they can be set
170165
extra_opts = comp_cfg.easyblock.extra_options()
171166
comp_cfg.extend_params(copy.deepcopy(extra_opts))
172167

173-
comp_cfg.generate_template_values()
168+
# The copy above may include unexpected settings for common values.
169+
# In particular for a Pythonbundle we have seen a component inheriting
170+
# runtest = True
171+
# which is not a valid value for many easyblocks.
172+
# Reset runtest to the original default, if people want the test step
173+
# they can set it explicitly, in default_component_specs or by the component easyblock
174+
if not is_easyconfig_parameter_default_value('runtest', comp_cfg.get('runtest', resolve=False)):
175+
self.log.warning(
176+
"Resetting runtest to default value for component easyblock "
177+
f"(from {comp_cfg.get('runtest', resolve=False)})."
178+
)
179+
comp_cfg['runtest'] = get_easyconfig_parameter_default('runtest')
174180

175-
# do not inherit easyblock to use from parent
176-
# (since that would result in an infinite loop in install_step)
177-
comp_cfg['easyblock'] = None
181+
# Reset others to their default value
182+
# Inheriting easyblock would lead to an infinite loop in the install step
183+
for var in ('easyblock',
184+
'sources', 'source_urls', 'checksums',
185+
'patches', 'postinstallpatches',
186+
'modextravars', 'modextrapaths'):
187+
comp_cfg[var] = copy.deepcopy(get_easyconfig_parameter_default(var))
178188

179-
# reset list of sources/source_urls/checksums
180-
comp_cfg['sources'] = comp_cfg['source_urls'] = comp_cfg['checksums'] = []
181-
comp_cfg['patches'] = comp_cfg['postinstallpatches'] = []
189+
comp_cfg['name'] = comp_name
190+
comp_cfg['version'] = comp_version
182191

183192
for key in self.cfg['default_component_specs']:
184193
comp_cfg[key] = self.cfg['default_component_specs'][key]
185194

186195
for key in comp_specs:
187196
comp_cfg[key] = comp_specs[key]
188197

198+
comp_cfg.generate_template_values()
199+
189200
# Don't require that all template values can be resolved at this point but still resolve them.
190201
# This is important to ensure that template values like %(name)s and %(version)s
191202
# are correctly resolved with the component name/version before values are copied over to self.cfg
@@ -274,14 +285,39 @@ def build_step(self):
274285
"""Do nothing."""
275286
pass
276287

288+
def _install_component(self, comp):
289+
"""Run the installation steps for a single component"""
290+
# run relevant steps
291+
for descr, step_name in COMPONENT_INSTALL_STEPS:
292+
if step_name in comp.cfg['skipsteps']:
293+
comp.log.info("Skipping '%s' step for component %s v%s", step_name, comp.name, comp.version)
294+
elif build_option('skip_test_step') and step_name == TEST_STEP:
295+
comp.log.info("Skipping %s step for component %s v%s, as requested via skip-test-step", step_name,
296+
comp.name, comp.version)
297+
else:
298+
msg = f' {descr} component {comp.name}...'
299+
if self.dry_run:
300+
self.dry_run_msg("%s [DRY RUN]\n", msg)
301+
else:
302+
print_msg(msg, log=self.log, silent=self.silent)
303+
start_time = datetime.now()
304+
try:
305+
comp.run_step(step_name, [lambda x: getattr(x, '%s_step' % step_name)])
306+
finally:
307+
if not self.dry_run:
308+
step_duration = datetime.now() - start_time
309+
if step_duration.total_seconds() >= 1:
310+
print_msg(" ... (took %s)", time2str(step_duration), log=self.log, silent=self.silent)
311+
elif self.logdebug or build_option('trace'):
312+
print_msg(" ... (took < 1 sec)", log=self.log, silent=self.silent)
313+
277314
def install_step(self):
278315
"""Install components, if specified."""
279316
comp_cnt = len(self.cfg['components'])
280317
for idx, (cfg, comp) in enumerate(self.comp_instances):
281-
282318
print_msg("installing bundle component %s v%s (%d/%d)..." %
283-
(cfg['name'], cfg['version'], idx + 1, comp_cnt))
284-
self.log.info("Installing component %s v%s using easyblock %s", cfg['name'], cfg['version'], cfg.easyblock)
319+
(comp.name, comp.version, idx + 1, comp_cnt))
320+
self.log.info("Installing component %s v%s using easyblock %s", comp.name, comp.version, cfg.easyblock)
285321

286322
# correct build/install dirs
287323
comp.builddir = self.builddir
@@ -326,18 +362,10 @@ def install_step(self):
326362
comp.src[-1]['finalpath'] = comp.cfg['start_dir']
327363

328364
# check if sanity checks are enabled for the component
329-
if self.cfg['sanity_check_all_components'] or comp.cfg['name'] in self.cfg['sanity_check_components']:
365+
if self.cfg['sanity_check_all_components'] or comp.name in self.cfg['sanity_check_components']:
330366
self.comp_cfgs_sanity_check.append(comp)
331367

332-
# run relevant steps
333-
for step_name in ['patch', 'configure', 'build', 'test', 'install']:
334-
if step_name in cfg['skipsteps']:
335-
comp.log.info("Skipping '%s' step for component %s v%s", step_name, cfg['name'], cfg['version'])
336-
elif build_option('skip_test_step') and step_name == TEST_STEP:
337-
comp.log.info("Skipping %s step for component %s v%s, as requested via skip-test-step", step_name,
338-
cfg['name'], cfg['version'])
339-
else:
340-
comp.run_step(step_name, [lambda x: getattr(x, '%s_step' % step_name)])
368+
self._install_component(comp)
341369

342370
if comp.make_module_req_guess.__qualname__ != 'EasyBlock.make_module_req_guess':
343371
depr_msg = f"Easyblock used to install component {comp.name} still uses make_module_req_guess"
@@ -390,13 +418,13 @@ def make_module_step(self, *args, **kwargs):
390418
as this is done in the generic EasyBlock while creating
391419
the module file already.
392420
"""
393-
for cfg, comp in self.comp_instances:
394-
self.log.info("Gathering module paths for component %s v%s", cfg['name'], cfg['version'])
421+
for _, comp in self.comp_instances:
422+
self.log.info("Gathering module paths for component %s v%s", comp.name, comp.version)
395423

396424
# take into account that easyblock used for component may not be migrated yet to module_load_environment
397425
if comp.make_module_req_guess.__qualname__ != 'EasyBlock.make_module_req_guess':
398426

399-
depr_msg = f"Easyblock used to install component {cfg['name']} still uses make_module_req_guess"
427+
depr_msg = f"Easyblock used to install component {comp.name} still uses make_module_req_guess"
400428
self.log.deprecated(depr_msg, '6.0')
401429

402430
reqs = comp.make_module_req_guess()
@@ -412,7 +440,7 @@ def make_module_step(self, *args, **kwargs):
412440
setattr(self.module_load_environment, key, value)
413441
except AttributeError:
414442
raise EasyBuildError("Cannot process module requirements of bundle component %s v%s",
415-
cfg['name'], cfg['version'])
443+
comp.name, comp.version)
416444
else:
417445
# Explicit call required as adding step to 'install_step' is not sufficient
418446
# for module-only build. Set fake arg to True, as module components should
@@ -453,9 +481,23 @@ def sanity_check_step(self, *args, **kwargs):
453481

454482
# run sanity checks for specific components
455483
cnt = len(self.comp_cfgs_sanity_check)
456-
for idx, comp in enumerate(self.comp_cfgs_sanity_check):
457-
comp_name, comp_ver = comp.cfg['name'], comp.cfg['version']
458-
print_msg("sanity checking bundle component %s v%s (%i/%i)...", comp_name, comp_ver, idx + 1, cnt)
459-
self.log.info("Starting sanity check step for component %s v%s", comp_name, comp_ver)
460-
461-
comp.run_step('sanity_check', [lambda x: x.sanity_check_step])
484+
if cnt > 0:
485+
if self.sanity_check_module_loaded:
486+
loaded_module = False
487+
else:
488+
self.sanity_check_load_module(extension=kwargs.get('extension', False),
489+
extra_modules=kwargs.get('extra_modules', None))
490+
loaded_module = self.sanity_check_module_loaded
491+
for idx, comp in enumerate(self.comp_cfgs_sanity_check):
492+
print_msg("sanity checking bundle component %s v%s (%i/%i)...", comp.name, comp.version, idx + 1, cnt)
493+
self.log.info("Starting sanity check step for component %s v%s", comp.name, comp.version)
494+
495+
# Avoid loading the module in components again
496+
comp.sanity_check_module_loaded = True
497+
comp.run_step('sanity_check', [lambda x: x.sanity_check_step])
498+
comp.sanity_check_module_loaded = False
499+
if loaded_module:
500+
if self.fake_mod_data:
501+
self.clean_up_fake_module(self.fake_mod_data)
502+
self.fake_mod_data = None
503+
self.sanity_check_module_loaded = False

0 commit comments

Comments
 (0)