3535"""
3636import copy
3737import os
38+ from datetime import datetime
3839
3940import easybuild .tools .environment as env
4041from easybuild .framework .easyblock import EasyBlock
4142from 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
4345from easybuild .framework .easyconfig .easyconfig import get_easyblock_class
4446from easybuild .tools .build_log import EasyBuildError , print_msg
4547from easybuild .tools .config import build_option
4648from easybuild .tools .hooks import TEST_STEP
4749from 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
5163class 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