Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for --sanity-check-only + also run sanity check for extensions when using --module-only #3655

Merged
merged 11 commits into from
May 26, 2021
Merged
131 changes: 85 additions & 46 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1504,13 +1504,14 @@ def make_module_req_guess(self):
'CMAKE_LIBRARY_PATH': ['lib64'], # lib and lib32 are searched through the above
}

def load_module(self, mod_paths=None, purge=True, extra_modules=None):
def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True):
"""
Load module for this software package/version, after purging all currently loaded modules.

:param mod_paths: list of (additional) module paths to take into account
:param purge: boolean indicating whether or not to purge currently loaded modules first
:param extra_modules: list of extra modules to load (these are loaded *before* loading the 'self' module)
:param verbose: print modules being loaded when trace mode is enabled
"""
# self.full_mod_name might not be set (e.g. during unit tests)
if self.full_mod_name is not None:
Expand All @@ -1532,6 +1533,9 @@ def load_module(self, mod_paths=None, purge=True, extra_modules=None):
if self.mod_subdir and not self.toolchain.is_system_toolchain():
mods.insert(0, self.toolchain.det_short_module_name())

if verbose:
trace_msg("loading modules: %s..." % ', '.join(mods))

# pass initial environment, to use it for resetting the environment before loading the modules
self.modules_tool.load(mods, mod_paths=all_mod_paths, purge=purge, init_env=self.initial_environ)

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

def load_fake_module(self, purge=False, extra_modules=None):
def load_fake_module(self, purge=False, extra_modules=None, verbose=False):
"""
Create and load fake module.

Expand All @@ -1558,7 +1562,7 @@ def load_fake_module(self, purge=False, extra_modules=None):

# load fake module
self.modules_tool.prepend_module_path(os.path.join(fake_mod_path, self.mod_subdir), priority=10000)
self.load_module(purge=purge, extra_modules=extra_modules)
self.load_module(purge=purge, extra_modules=extra_modules, verbose=verbose)

return (fake_mod_path, env)

Expand Down Expand Up @@ -2235,54 +2239,32 @@ def install_step(self):
"""Install built software (abstract method)."""
raise NotImplementedError

def extensions_step(self, fetch=False, install=True):
def init_ext_instances(self):
"""
After make install, run this.
- only if variable len(exts_list) > 0
- optionally: load module that was just created using temp module file
- find source for extensions, in 'extensions' (and 'packages' for legacy reasons)
- run extra_extensions
Create class instances for all extensions.
"""
if not self.cfg.get_ref('exts_list'):
self.log.debug("No extensions in exts_list")
return

# load fake module
fake_mod_data = None
if install and not self.dry_run:

# load modules for build dependencies as extra modules
build_dep_mods = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]

fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)

self.prepare_for_extensions()

if fetch:
self.exts = self.fetch_extension_sources()
exts_list = self.cfg.get_ref('exts_list')

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

# actually install extensions
self.log.debug("Installing extensions")
exts_defaultclass = self.cfg['exts_defaultclass']
self.ext_instances = []
exts_classmap = self.cfg['exts_classmap']

# we really need a default class
if not exts_defaultclass and fake_mod_data:
self.clean_up_fake_module(fake_mod_data)
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)
if exts_list and not self.exts:
self.exts = self.fetch_extension_sources()

# obtain name and module path for default extention class
exts_defaultclass = self.cfg['exts_defaultclass']
if isinstance(exts_defaultclass, string_type):
# proper way: derive module path from specified class name
default_class = exts_defaultclass
default_class_modpath = get_module_path(default_class, generic=True)
else:
raise EasyBuildError("Improper default extension class specification, should be string.")
error_msg = "Improper default extension class specification, should be string: %s (%s)"
raise EasyBuildError(error_msg, exts_defaultclass, type(exts_defaultclass))

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

self.ext_instances.append(inst)

def extensions_step(self, fetch=False, install=True):
"""
After make install, run this.
- only if variable len(exts_list) > 0
- optionally: load module that was just created using temp module file
- find source for extensions, in 'extensions' (and 'packages' for legacy reasons)
- run extra_extensions
"""
if not self.cfg.get_ref('exts_list'):
self.log.debug("No extensions in exts_list")
return

# load fake module
fake_mod_data = None
if install and not self.dry_run:

# load modules for build dependencies as extra modules
build_dep_mods = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]

fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)

self.prepare_for_extensions()

if fetch:
self.exts = self.fetch_extension_sources()

self.exts_all = self.exts[:] # retain a copy of all extensions, regardless of filtering/skipping

# actually install extensions
if install:
self.log.info("Installing extensions")

# we really need a default class
if not self.cfg['exts_defaultclass'] and fake_mod_data:
self.clean_up_fake_module(fake_mod_data)
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)

self.init_ext_instances()

if self.skip:
self.skip_extensions()

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

if self.dry_run:
tup = (ext.name, ext.version, cls.__name__)
tup = (ext.name, ext.version, ext.__class__.__name__)
msg = "\n* installing extension %s %s using '%s' easyblock\n" % tup
self.dry_run_msg(msg)

Expand Down Expand Up @@ -2877,6 +2898,13 @@ def _sanity_check_step_dry_run(self, custom_paths=None, custom_commands=None, **
def _sanity_check_step_extensions(self):
"""Sanity check on extensions (if any)."""
failed_exts = []

# class instances for extensions may not be initialized yet here,
# for example when using --module-only or --sanity-check-only
if not self.ext_instances:
self.prepare_for_extensions()
self.init_ext_instances()

for ext in self.ext_instances:
success, fail_msg = None, None
res = ext.sanity_check_step()
Expand Down Expand Up @@ -2968,12 +2996,16 @@ def xs2str(xs):

fake_mod_data = None

# skip loading of fake module when using --sanity-check-only, load real module instead
if build_option('sanity_check_only') and not extension:
self.load_module(extra_modules=extra_modules)

# only load fake module for non-extensions, and not during dry run
if not (extension or self.dry_run):
elif not (extension or self.dry_run):
try:
# unload all loaded modules before loading fake module
# this ensures that loading of dependencies is tested, and avoids conflicts with build dependencies
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules)
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules, verbose=True)
except EasyBuildError as err:
self.sanity_check_fail_msgs.append("loading fake module failed: %s" % err)
self.log.warning("Sanity check: %s" % self.sanity_check_fail_msgs[-1])
Expand Down Expand Up @@ -3257,16 +3289,18 @@ def update_config_template_run_step(self):

def skip_step(self, step, skippable):
"""Dedice whether or not to skip the specified step."""
module_only = build_option('module_only')
force = build_option('force')
skip = False
force = build_option('force')
module_only = build_option('module_only')
sanity_check_only = build_option('sanity_check_only')
skipsteps = self.cfg['skipsteps']

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

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

# skip step when only generating module file
Expand All @@ -3281,9 +3315,14 @@ def skip_step(self, step, skippable):
self.log.info("Skipping %s step because of forced module-only mode", step)
skip = True

elif sanity_check_only and step != SANITYCHECK_STEP:
self.log.info("Skipping %s step because of sanity-check-only mode", step)
skip = True
boegel marked this conversation as resolved.
Show resolved Hide resolved

else:
self.log.debug("Not skipping %s step (skippable: %s, skip: %s, skipsteps: %s, module_only: %s, force: %s",
step, skippable, self.skip, self.cfg['skipsteps'], module_only, force)
msg = "Not skipping %s step (skippable: %s, skip: %s, skipsteps: %s, module_only: %s, force: %s, "
boegel marked this conversation as resolved.
Show resolved Hide resolved
msg += "sanity_check_only: %s"
boegel marked this conversation as resolved.
Show resolved Hide resolved
self.log.debug(msg, step, skippable, self.skip, skipsteps, module_only, force, sanity_check_only)

return skip

Expand Down
5 changes: 4 additions & 1 deletion easybuild/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,11 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
forced = options.force or options.rebuild
dry_run_mode = options.dry_run or options.dry_run_short or options.missing_modules

keep_available_modules = forced or dry_run_mode or options.extended_dry_run or pr_options
keep_available_modules = keep_available_modules or options.inject_checksums or options.sanity_check_only

# skip modules that are already installed unless forced, or unless an option is used that warrants not skipping
if not (forced or dry_run_mode or options.extended_dry_run or pr_options or options.inject_checksums):
if not keep_available_modules:
retained_ecs = skip_available(easyconfigs, modtool)
if not testing:
for skipped_ec in [ec for ec in easyconfigs if ec not in retained_ecs]:
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'rebuild',
'robot',
'rpath',
'sanity_check_only',
'search_paths',
'sequential',
'set_gid_bit',
Expand Down
2 changes: 2 additions & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ def override_options(self):
'rpath-filter': ("List of regex patterns to use for filtering out RPATH paths", 'strlist', 'store', None),
'rpath-override-dirs': ("Path(s) to be prepended when linking with RPATH (string, colon-separated)",
None, 'store', None),
'sanity-check-only': ("Only run sanity check (module is expected to be installed already",
None, 'store_true', False),
'set-default-module': ("Set the generated module as default", None, 'store_true', False),
'set-gid-bit': ("Set group ID bit on newly created directories", None, 'store_true', False),
'silence-deprecation-warnings': ("Silence specified deprecation warnings", 'strlist', 'extend', None),
Expand Down
87 changes: 87 additions & 0 deletions test/framework/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5770,6 +5770,93 @@ def test_tmp_logdir(self):
logtxt = read_file(os.path.join(tmp_logdir, tmp_logs[0]))
self.assertTrue("COMPLETED: Installation ended successfully" in logtxt)

def test_sanity_check_only(self):
"""Test use of --sanity-check-only."""
topdir = os.path.abspath(os.path.dirname(__file__))
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')

test_ec = os.path.join(self.test_prefix, 'test.ec')
test_ec_txt = read_file(toy_ec)
test_ec_txt += '\n' + '\n'.join([
"sanity_check_commands = ['barbar', 'toy']",
"sanity_check_paths = {'files': ['bin/barbar', 'bin/toy'], 'dirs': ['bin']}",
"exts_list = [",
" ('barbar', '0.0', {",
" 'start_dir': 'src',",
" 'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''),",
" })",
"]",
])
write_file(test_ec, test_ec_txt)

# sanity check fails if software was not installed yet
outtxt, error_thrown = self.eb_main([test_ec, '--sanity-check-only'], do_build=True, return_error=True)
self.assertTrue("Sanity check failed" in str(error_thrown))

# actually install, then try --sanity-check-only again;
# need to use --force to install toy because module already exists (but installation doesn't)
self.eb_main([test_ec, '--force'], do_build=True, raise_error=True)

args = [test_ec, '--sanity-check-only']

self.mock_stdout(True)
self.mock_stderr(True)
self.eb_main(args + ['--trace'], do_build=True, raise_error=True, testing=False)
stdout = self.get_stdout().strip()
stderr = self.get_stderr().strip()
self.mock_stdout(False)
self.mock_stderr(False)

self.assertFalse(stderr)
skipped = [
"fetching files",
"creating build dir, resetting environment",
"unpacking",
"patching",
"preparing",
"configuring",
"building",
"testing",
"installing",
"taking care of extensions",
"restore after iterating",
"postprocessing",
"cleaning up",
"creating module",
"permissions",
"packaging"
]
for skip in skipped:
self.assertTrue("== %s [skipped]" % skip)

self.assertTrue("== sanity checking..." in stdout)
self.assertTrue("COMPLETED: Installation ended successfully" in stdout)
msgs = [
" >> file 'bin/barbar' found: OK",
" >> file 'bin/toy' found: OK",
" >> (non-empty) directory 'bin' found: OK",
" >> loading modules: toy/0.0...",
" >> result for command 'toy': OK",
"ls -l lib/libbarbar.a", # sanity check for extension barbar (via exts_filter)
]
for msg in msgs:
self.assertTrue(msg in stdout, "'%s' found in: %s" % (msg, stdout))

# check if sanity check for extension fails if a file provided by that extension,
# which is checked by the sanity check for that extension, is removed
libbarbar = os.path.join(self.test_installpath, 'software', 'toy', '0.0', 'lib', 'libbarbar.a')
remove_file(libbarbar)

outtxt, error_thrown = self.eb_main(args + ['--debug'], do_build=True, return_error=True)
error_msg = str(error_thrown)
error_patterns = [
r"Sanity check failed",
r'command "ls -l lib/libbarbar\.a" failed',
]
for error_pattern in error_patterns:
regex = re.compile(error_pattern)
self.assertTrue(regex.search(error_msg), "Pattern '%s' should be found in: %s" % (regex.pattern, error_msg))

def test_fake_vsc_include(self):
"""Test whether fake 'vsc' namespace is triggered for modules included via --include-*."""

Expand Down
Loading