Skip to content

Commit 584de26

Browse files
author
ocaisa
authored
Merge pull request #3655 from boegel/sanity_check_only
add support for --sanity-check-only + also run sanity check for extensions when using --module-only
2 parents 097db73 + 50ce59a commit 584de26

File tree

6 files changed

+233
-48
lines changed

6 files changed

+233
-48
lines changed

easybuild/framework/easyblock.py

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,13 +1504,14 @@ def make_module_req_guess(self):
15041504
'CMAKE_LIBRARY_PATH': ['lib64'], # lib and lib32 are searched through the above
15051505
}
15061506

1507-
def load_module(self, mod_paths=None, purge=True, extra_modules=None):
1507+
def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True):
15081508
"""
15091509
Load module for this software package/version, after purging all currently loaded modules.
15101510
15111511
:param mod_paths: list of (additional) module paths to take into account
15121512
:param purge: boolean indicating whether or not to purge currently loaded modules first
15131513
:param extra_modules: list of extra modules to load (these are loaded *before* loading the 'self' module)
1514+
:param verbose: print modules being loaded when trace mode is enabled
15141515
"""
15151516
# self.full_mod_name might not be set (e.g. during unit tests)
15161517
if self.full_mod_name is not None:
@@ -1532,6 +1533,9 @@ def load_module(self, mod_paths=None, purge=True, extra_modules=None):
15321533
if self.mod_subdir and not self.toolchain.is_system_toolchain():
15331534
mods.insert(0, self.toolchain.det_short_module_name())
15341535

1536+
if verbose:
1537+
trace_msg("loading modules: %s..." % ', '.join(mods))
1538+
15351539
# pass initial environment, to use it for resetting the environment before loading the modules
15361540
self.modules_tool.load(mods, mod_paths=all_mod_paths, purge=purge, init_env=self.initial_environ)
15371541

@@ -1543,7 +1547,7 @@ def load_module(self, mod_paths=None, purge=True, extra_modules=None):
15431547
else:
15441548
self.log.warning("Not loading module, since self.full_mod_name is not set.")
15451549

1546-
def load_fake_module(self, purge=False, extra_modules=None):
1550+
def load_fake_module(self, purge=False, extra_modules=None, verbose=False):
15471551
"""
15481552
Create and load fake module.
15491553
@@ -1558,7 +1562,7 @@ def load_fake_module(self, purge=False, extra_modules=None):
15581562

15591563
# load fake module
15601564
self.modules_tool.prepend_module_path(os.path.join(fake_mod_path, self.mod_subdir), priority=10000)
1561-
self.load_module(purge=purge, extra_modules=extra_modules)
1565+
self.load_module(purge=purge, extra_modules=extra_modules, verbose=verbose)
15621566

15631567
return (fake_mod_path, env)
15641568

@@ -2235,54 +2239,32 @@ def install_step(self):
22352239
"""Install built software (abstract method)."""
22362240
raise NotImplementedError
22372241

2238-
def extensions_step(self, fetch=False, install=True):
2242+
def init_ext_instances(self):
22392243
"""
2240-
After make install, run this.
2241-
- only if variable len(exts_list) > 0
2242-
- optionally: load module that was just created using temp module file
2243-
- find source for extensions, in 'extensions' (and 'packages' for legacy reasons)
2244-
- run extra_extensions
2244+
Create class instances for all extensions.
22452245
"""
2246-
if not self.cfg.get_ref('exts_list'):
2247-
self.log.debug("No extensions in exts_list")
2248-
return
2249-
2250-
# load fake module
2251-
fake_mod_data = None
2252-
if install and not self.dry_run:
2253-
2254-
# load modules for build dependencies as extra modules
2255-
build_dep_mods = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]
2256-
2257-
fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)
2258-
2259-
self.prepare_for_extensions()
2260-
2261-
if fetch:
2262-
self.exts = self.fetch_extension_sources()
2246+
exts_list = self.cfg.get_ref('exts_list')
22632247

2264-
self.exts_all = self.exts[:] # retain a copy of all extensions, regardless of filtering/skipping
2248+
# early exit if there are no extensions
2249+
if not exts_list:
2250+
return
22652251

2266-
# actually install extensions
2267-
self.log.debug("Installing extensions")
2268-
exts_defaultclass = self.cfg['exts_defaultclass']
2252+
self.ext_instances = []
22692253
exts_classmap = self.cfg['exts_classmap']
22702254

2271-
# we really need a default class
2272-
if not exts_defaultclass and fake_mod_data:
2273-
self.clean_up_fake_module(fake_mod_data)
2274-
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)
2255+
if exts_list and not self.exts:
2256+
self.exts = self.fetch_extension_sources()
22752257

22762258
# obtain name and module path for default extention class
2259+
exts_defaultclass = self.cfg['exts_defaultclass']
22772260
if isinstance(exts_defaultclass, string_type):
22782261
# proper way: derive module path from specified class name
22792262
default_class = exts_defaultclass
22802263
default_class_modpath = get_module_path(default_class, generic=True)
22812264
else:
2282-
raise EasyBuildError("Improper default extension class specification, should be string.")
2265+
error_msg = "Improper default extension class specification, should be string: %s (%s)"
2266+
raise EasyBuildError(error_msg, exts_defaultclass, type(exts_defaultclass))
22832267

2284-
# get class instances for all extensions
2285-
self.ext_instances = []
22862268
for ext in self.exts:
22872269
ext_name = ext['name']
22882270
self.log.debug("Creating class instance for extension %s...", ext_name)
@@ -2332,6 +2314,45 @@ def extensions_step(self, fetch=False, install=True):
23322314

23332315
self.ext_instances.append(inst)
23342316

2317+
def extensions_step(self, fetch=False, install=True):
2318+
"""
2319+
After make install, run this.
2320+
- only if variable len(exts_list) > 0
2321+
- optionally: load module that was just created using temp module file
2322+
- find source for extensions, in 'extensions' (and 'packages' for legacy reasons)
2323+
- run extra_extensions
2324+
"""
2325+
if not self.cfg.get_ref('exts_list'):
2326+
self.log.debug("No extensions in exts_list")
2327+
return
2328+
2329+
# load fake module
2330+
fake_mod_data = None
2331+
if install and not self.dry_run:
2332+
2333+
# load modules for build dependencies as extra modules
2334+
build_dep_mods = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]
2335+
2336+
fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)
2337+
2338+
self.prepare_for_extensions()
2339+
2340+
if fetch:
2341+
self.exts = self.fetch_extension_sources()
2342+
2343+
self.exts_all = self.exts[:] # retain a copy of all extensions, regardless of filtering/skipping
2344+
2345+
# actually install extensions
2346+
if install:
2347+
self.log.info("Installing extensions")
2348+
2349+
# we really need a default class
2350+
if not self.cfg['exts_defaultclass'] and fake_mod_data:
2351+
self.clean_up_fake_module(fake_mod_data)
2352+
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)
2353+
2354+
self.init_ext_instances()
2355+
23352356
if self.skip:
23362357
self.skip_extensions()
23372358

@@ -2347,7 +2368,7 @@ def extensions_step(self, fetch=False, install=True):
23472368
print_msg("installing extension %s %s (%d/%d)..." % tup, silent=self.silent)
23482369

23492370
if self.dry_run:
2350-
tup = (ext.name, ext.version, cls.__name__)
2371+
tup = (ext.name, ext.version, ext.__class__.__name__)
23512372
msg = "\n* installing extension %s %s using '%s' easyblock\n" % tup
23522373
self.dry_run_msg(msg)
23532374

@@ -2890,6 +2911,13 @@ def _sanity_check_step_dry_run(self, custom_paths=None, custom_commands=None, **
28902911
def _sanity_check_step_extensions(self):
28912912
"""Sanity check on extensions (if any)."""
28922913
failed_exts = []
2914+
2915+
# class instances for extensions may not be initialized yet here,
2916+
# for example when using --module-only or --sanity-check-only
2917+
if not self.ext_instances:
2918+
self.prepare_for_extensions()
2919+
self.init_ext_instances()
2920+
28932921
for ext in self.ext_instances:
28942922
success, fail_msg = None, None
28952923
res = ext.sanity_check_step()
@@ -2981,12 +3009,16 @@ def xs2str(xs):
29813009

29823010
fake_mod_data = None
29833011

3012+
# skip loading of fake module when using --sanity-check-only, load real module instead
3013+
if build_option('sanity_check_only') and not extension:
3014+
self.load_module(extra_modules=extra_modules)
3015+
29843016
# only load fake module for non-extensions, and not during dry run
2985-
if not (extension or self.dry_run):
3017+
elif not (extension or self.dry_run):
29863018
try:
29873019
# unload all loaded modules before loading fake module
29883020
# this ensures that loading of dependencies is tested, and avoids conflicts with build dependencies
2989-
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules)
3021+
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules, verbose=True)
29903022
except EasyBuildError as err:
29913023
self.sanity_check_fail_msgs.append("loading fake module failed: %s" % err)
29923024
self.log.warning("Sanity check: %s" % self.sanity_check_fail_msgs[-1])
@@ -3270,16 +3302,18 @@ def update_config_template_run_step(self):
32703302

32713303
def skip_step(self, step, skippable):
32723304
"""Dedice whether or not to skip the specified step."""
3273-
module_only = build_option('module_only')
3274-
force = build_option('force')
32753305
skip = False
3306+
force = build_option('force')
3307+
module_only = build_option('module_only')
3308+
sanity_check_only = build_option('sanity_check_only')
3309+
skipsteps = self.cfg['skipsteps']
32763310

32773311
# under --skip, sanity check is not skipped
32783312
cli_skip = self.skip and step != SANITYCHECK_STEP
32793313

32803314
# skip step if specified as individual (skippable) step, or if --skip is used
3281-
if skippable and (cli_skip or step in self.cfg['skipsteps']):
3282-
self.log.info("Skipping %s step (skip: %s, skipsteps: %s)", step, self.skip, self.cfg['skipsteps'])
3315+
if skippable and (cli_skip or step in skipsteps):
3316+
self.log.info("Skipping %s step (skip: %s, skipsteps: %s)", step, self.skip, skipsteps)
32833317
skip = True
32843318

32853319
# skip step when only generating module file
@@ -3294,9 +3328,14 @@ def skip_step(self, step, skippable):
32943328
self.log.info("Skipping %s step because of forced module-only mode", step)
32953329
skip = True
32963330

3331+
elif sanity_check_only and step != SANITYCHECK_STEP:
3332+
self.log.info("Skipping %s step because of sanity-check-only mode", step)
3333+
skip = True
3334+
32973335
else:
3298-
self.log.debug("Not skipping %s step (skippable: %s, skip: %s, skipsteps: %s, module_only: %s, force: %s",
3299-
step, skippable, self.skip, self.cfg['skipsteps'], module_only, force)
3336+
msg = "Not skipping %s step (skippable: %s, skip: %s, skipsteps: %s, module_only: %s, force: %s, "
3337+
msg += "sanity_check_only: %s)"
3338+
self.log.debug(msg, step, skippable, self.skip, skipsteps, module_only, force, sanity_check_only)
33003339

33013340
return skip
33023341

easybuild/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,11 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
404404
forced = options.force or options.rebuild
405405
dry_run_mode = options.dry_run or options.dry_run_short or options.missing_modules
406406

407+
keep_available_modules = forced or dry_run_mode or options.extended_dry_run or pr_options
408+
keep_available_modules = keep_available_modules or options.inject_checksums or options.sanity_check_only
409+
407410
# skip modules that are already installed unless forced, or unless an option is used that warrants not skipping
408-
if not (forced or dry_run_mode or options.extended_dry_run or pr_options or options.inject_checksums):
411+
if not keep_available_modules:
409412
retained_ecs = skip_available(easyconfigs, modtool)
410413
if not testing:
411414
for skipped_ec in [ec for ec in easyconfigs if ec not in retained_ecs]:

easybuild/tools/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
259259
'rebuild',
260260
'robot',
261261
'rpath',
262+
'sanity_check_only',
262263
'search_paths',
263264
'sequential',
264265
'set_gid_bit',

easybuild/tools/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ def override_options(self):
460460
'rpath-filter': ("List of regex patterns to use for filtering out RPATH paths", 'strlist', 'store', None),
461461
'rpath-override-dirs': ("Path(s) to be prepended when linking with RPATH (string, colon-separated)",
462462
None, 'store', None),
463+
'sanity-check-only': ("Only run sanity check (module is expected to be installed already",
464+
None, 'store_true', False),
463465
'set-default-module': ("Set the generated module as default", None, 'store_true', False),
464466
'set-gid-bit': ("Set group ID bit on newly created directories", None, 'store_true', False),
465467
'silence-deprecation-warnings': ("Silence specified deprecation warnings", 'strlist', 'extend', None),

test/framework/options.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5806,6 +5806,93 @@ def test_tmp_logdir(self):
58065806
logtxt = read_file(os.path.join(tmp_logdir, tmp_logs[0]))
58075807
self.assertTrue("COMPLETED: Installation ended successfully" in logtxt)
58085808

5809+
def test_sanity_check_only(self):
5810+
"""Test use of --sanity-check-only."""
5811+
topdir = os.path.abspath(os.path.dirname(__file__))
5812+
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
5813+
5814+
test_ec = os.path.join(self.test_prefix, 'test.ec')
5815+
test_ec_txt = read_file(toy_ec)
5816+
test_ec_txt += '\n' + '\n'.join([
5817+
"sanity_check_commands = ['barbar', 'toy']",
5818+
"sanity_check_paths = {'files': ['bin/barbar', 'bin/toy'], 'dirs': ['bin']}",
5819+
"exts_list = [",
5820+
" ('barbar', '0.0', {",
5821+
" 'start_dir': 'src',",
5822+
" 'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''),",
5823+
" })",
5824+
"]",
5825+
])
5826+
write_file(test_ec, test_ec_txt)
5827+
5828+
# sanity check fails if software was not installed yet
5829+
outtxt, error_thrown = self.eb_main([test_ec, '--sanity-check-only'], do_build=True, return_error=True)
5830+
self.assertTrue("Sanity check failed" in str(error_thrown))
5831+
5832+
# actually install, then try --sanity-check-only again;
5833+
# need to use --force to install toy because module already exists (but installation doesn't)
5834+
self.eb_main([test_ec, '--force'], do_build=True, raise_error=True)
5835+
5836+
args = [test_ec, '--sanity-check-only']
5837+
5838+
self.mock_stdout(True)
5839+
self.mock_stderr(True)
5840+
self.eb_main(args + ['--trace'], do_build=True, raise_error=True, testing=False)
5841+
stdout = self.get_stdout().strip()
5842+
stderr = self.get_stderr().strip()
5843+
self.mock_stdout(False)
5844+
self.mock_stderr(False)
5845+
5846+
self.assertFalse(stderr)
5847+
skipped = [
5848+
"fetching files",
5849+
"creating build dir, resetting environment",
5850+
"unpacking",
5851+
"patching",
5852+
"preparing",
5853+
"configuring",
5854+
"building",
5855+
"testing",
5856+
"installing",
5857+
"taking care of extensions",
5858+
"restore after iterating",
5859+
"postprocessing",
5860+
"cleaning up",
5861+
"creating module",
5862+
"permissions",
5863+
"packaging"
5864+
]
5865+
for skip in skipped:
5866+
self.assertTrue("== %s [skipped]" % skip)
5867+
5868+
self.assertTrue("== sanity checking..." in stdout)
5869+
self.assertTrue("COMPLETED: Installation ended successfully" in stdout)
5870+
msgs = [
5871+
" >> file 'bin/barbar' found: OK",
5872+
" >> file 'bin/toy' found: OK",
5873+
" >> (non-empty) directory 'bin' found: OK",
5874+
" >> loading modules: toy/0.0...",
5875+
" >> result for command 'toy': OK",
5876+
"ls -l lib/libbarbar.a", # sanity check for extension barbar (via exts_filter)
5877+
]
5878+
for msg in msgs:
5879+
self.assertTrue(msg in stdout, "'%s' found in: %s" % (msg, stdout))
5880+
5881+
# check if sanity check for extension fails if a file provided by that extension,
5882+
# which is checked by the sanity check for that extension, is removed
5883+
libbarbar = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'lib', 'libbarbar.a')
5884+
remove_file(libbarbar)
5885+
5886+
outtxt, error_thrown = self.eb_main(args + ['--debug'], do_build=True, return_error=True)
5887+
error_msg = str(error_thrown)
5888+
error_patterns = [
5889+
r"Sanity check failed",
5890+
r'command "ls -l lib/libbarbar\.a" failed',
5891+
]
5892+
for error_pattern in error_patterns:
5893+
regex = re.compile(error_pattern)
5894+
self.assertTrue(regex.search(error_msg), "Pattern '%s' should be found in: %s" % (regex.pattern, error_msg))
5895+
58095896
def test_fake_vsc_include(self):
58105897
"""Test whether fake 'vsc' namespace is triggered for modules included via --include-*."""
58115898

0 commit comments

Comments
 (0)