Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5751312
add support for multi_deps easyconfig paramter (syntactic sugar for i…
boegel Mar 18, 2019
a655336
also consider builddependencies to define template values like %(pyve…
boegel Mar 19, 2019
4670776
move extensions step into list of iterative steps
boegel Mar 23, 2019
97d884a
add post_iter step to restore after iterating (rather than doing that…
boegel Mar 23, 2019
3e77c22
add support to pass extra modules to load in sanity_check_step
boegel Mar 23, 2019
7c1d4a3
add postiter step to list of steps run when using --module-only
boegel Mar 23, 2019
626e319
also run extensions step in first iteration (oopsie)
boegel Mar 23, 2019
aa8fd33
also restore easyconfig parameters over which was iterated for extens…
boegel Mar 23, 2019
63372d7
make sanity check aware of multi_deps
boegel Mar 23, 2019
6382776
move iterated sanity check for multi-deps into dedicated method _sani…
boegel Mar 24, 2019
f43136d
flesh out get_multi_deps method from _sanity_check_step_multi_deps, s…
boegel Mar 24, 2019
59f47cd
make ExtensionEasyBlock.sanity_check_step aware of multi_deps
boegel Mar 24, 2019
cd2d06e
add trace/log message for loading extra modules for extension sanity …
boegel Mar 24, 2019
35b41ee
only run iterative sanity check for extension in ExtensionEasyBlock f…
boegel Mar 24, 2019
c3cb4f5
print message when starting new iteration
boegel Mar 24, 2019
18e0d6e
move EasyBlock.get_multi_deps to EasyConfig.get_parsed_multi_deps + e…
boegel Mar 24, 2019
23b28da
Merge branch 'develop' into multi_deps
boegel Mar 24, 2019
85f17b6
only run multi-deps sanity check for non-extensions
boegel Mar 25, 2019
3c34722
fix remarks + test for incorrect multi_deps spec with different # dep…
boegel Mar 26, 2019
6d8e369
avoid keeping multiple copies of each extension in self.ext_instances…
boegel Mar 26, 2019
62afc42
Delete rogue comment.
bartoldeman Mar 27, 2019
219849d
Fix dumping of iterated builddependencies (regression from fcdc90)
bartoldeman Mar 27, 2019
77d1d84
Fix dump of easyconfig with multi_deps for reprod.
bartoldeman Mar 27, 2019
e8ecb32
Merge pull request #43 from ComputeCanada/multi_deps_dump_fixes
boegel Mar 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 91 additions & 12 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
from easybuild.tools.filetools import move_file, move_logs, read_file, remove_file, rmtree2, verify_checksum, weld_paths
from easybuild.tools.filetools import write_file
from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP
from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTPROC_STEP, PREPARE_STEP
from easybuild.tools.hooks import READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP
from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP
from easybuild.tools.hooks import PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP
from easybuild.tools.hooks import load_hooks, run_hook
from easybuild.tools.run import run_cmd
from easybuild.tools.jenkins import write_to_xml
Expand All @@ -96,7 +96,7 @@
from easybuild.tools.version import this_is_easybuild, VERBOSE_VERSION, VERSION


MODULE_ONLY_STEPS = [MODULE_STEP, PREPARE_STEP, READY_STEP, SANITYCHECK_STEP]
MODULE_ONLY_STEPS = [MODULE_STEP, PREPARE_STEP, READY_STEP, POSTITER_STEP, SANITYCHECK_STEP]

# string part of URL for Python packages on PyPI that indicates needs to be rewritten (see derive_alt_pypi_url)
PYPI_PKG_URL_PATTERN = 'pypi.python.org/packages/source/'
Expand Down Expand Up @@ -158,6 +158,10 @@ def __init__(self, ec):
self.skip = None
self.module_extra_extensions = '' # extra stuff for module file required by extensions

# indicates whether or not this instance represents an extension or not;
# may be set to True by ExtensionEasyBlock
self.is_extension = False

# easyconfig for this application
if isinstance(ec, EasyConfig):
self.cfg = ec
Expand Down Expand Up @@ -1484,7 +1488,9 @@ def handle_iterate_opts(self):
prev_enable_templating = self.cfg.enable_templating
self.cfg.enable_templating = False

self.cfg.start_iterating()
# start iterative mode (only need to do this once)
if self.iter_idx == 0:
self.cfg.start_iterating()

# handle configure/build/install options that are specified as lists (+ perhaps builddependencies)
# set first element to be used, keep track of list in self.iter_opts
Expand All @@ -1496,6 +1502,7 @@ def handle_iterate_opts(self):
self.log.debug("Found list for %s: %s", opt, self.iter_opts[opt])

if self.iter_opts:
print_msg("starting iteration #%s ..." % self.iter_idx, log=self.log, silent=self.silent)
self.log.info("Current iteration index: %s", self.iter_idx)

# pop first element from all iterative easyconfig parameters as next value to use
Expand All @@ -1512,14 +1519,19 @@ def handle_iterate_opts(self):
# prepare for next iteration (if any)
self.iter_idx += 1

def restore_iterate_opts(self):
def post_iter_step(self):
"""Restore options that were iterated over"""
# disable templating, since we're messing about with values in self.cfg
prev_enable_templating = self.cfg.enable_templating
self.cfg.enable_templating = False

for opt in self.iter_opts:
self.cfg[opt] = self.iter_opts[opt]

# also need to take into account extensions, since those were iterated over as well
for ext in self.ext_instances:
ext.cfg[opt] = self.iter_opts[opt]

self.log.debug("Restored value of '%s' that was iterated over: %s", opt, self.cfg[opt])

self.cfg.stop_iterating()
Expand Down Expand Up @@ -1976,6 +1988,7 @@ def extensions_step(self, fetch=False):
raise EasyBuildError("Improper default extension class specification, should be list/tuple or string.")

# get class instances for all extensions
self.ext_instances = []
exts_cnt = len(self.exts)
for idx, ext in enumerate(self.exts):
self.log.debug("Starting extension %s" % ext['name'])
Expand Down Expand Up @@ -2088,6 +2101,7 @@ def post_install_step(self):
Do some postprocessing
- run post install commands if any were specified
"""

if self.cfg['postinstallcmds'] is not None:
# make sure we have a list of commands
if not isinstance(self.cfg['postinstallcmds'], (list, tuple)):
Expand All @@ -2105,9 +2119,50 @@ def sanity_check_step(self, *args, **kwargs):
"""
if self.dry_run:
self._sanity_check_step_dry_run(*args, **kwargs)

# handling of extensions that were installed for multiple dependency versions is done in ExtensionEasyBlock
elif self.cfg['multi_deps'] and not self.is_extension:
self._sanity_check_step_multi_deps(*args, **kwargs)

else:
self._sanity_check_step(*args, **kwargs)

def _sanity_check_step_multi_deps(self, *args, **kwargs):
"""Perform sanity check for installations that iterate over a list a versions for particular dependencies."""

# take into account provided list of extra modules (if any)
common_extra_modules = kwargs.get('extra_modules') or []

# if multi_deps was used to do an iterative installation over multiple sets of dependencies,
# we need to perform the sanity check for each one of these;
# this implies iterating over the list of lists of build dependencies again...

# get list of (lists of) builddependencies, without templating values
builddeps = self.cfg.get_ref('builddependencies')

# start iterating again;
# required to ensure build dependencies are taken into account to resolve templates like %(pyver)s
self.cfg.iterating = True

for iter_deps in self.cfg.get_parsed_multi_deps():

# need to re-generate template values to get correct values for %(pyver)s and %(pyshortver)s
self.cfg['builddependencies'] = iter_deps
self.cfg.generate_template_values()

extra_modules = common_extra_modules + [d['short_mod_name'] for d in iter_deps]

info_msg = "Running sanity check with extra modules: %s" % ', '.join(extra_modules)
trace_msg(info_msg)
self.log.info(info_msg)

kwargs['extra_modules'] = extra_modules
self._sanity_check_step(*args, **kwargs)

# restore list of lists of build dependencies & stop iterating again
self.cfg['builddependencies'] = builddeps
self.cfg.iterating = False

def sanity_check_rpath(self, rpath_dirs=None):
"""Sanity check binaries/libraries w.r.t. RPATH linking."""

Expand Down Expand Up @@ -2180,7 +2235,12 @@ def sanity_check_rpath(self, rpath_dirs=None):
return fails

def _sanity_check_step_common(self, custom_paths, custom_commands):
"""Determine sanity check paths and commands to use."""
"""
Determine sanity check paths and commands to use.

:param custom_paths: custom sanity check paths to check existence for
:param custom_commands: custom sanity check commands to run
"""

# supported/required keys in for sanity check paths, along with function used to check the paths
path_keys_and_check = {
Expand Down Expand Up @@ -2248,7 +2308,12 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
return paths, path_keys_and_check, commands

def _sanity_check_step_dry_run(self, custom_paths=None, custom_commands=None, **_):
"""Dry run version of sanity_check_step method."""
"""
Dry run version of sanity_check_step method.

:param custom_paths: custom sanity check paths to check existence for
:param custom_commands: custom sanity check commands to run
"""
paths, path_keys_and_check, commands = self._sanity_check_step_common(custom_paths, custom_commands)

for key, (typ, _) in path_keys_and_check.items():
Expand Down Expand Up @@ -2299,8 +2364,15 @@ def _sanity_check_step_extensions(self):
self.sanity_check_fail_msgs.append(overall_fail_msg + ', '.join(x[0] for x in failed_exts))
self.sanity_check_fail_msgs.extend(x[1] for x in failed_exts)

def _sanity_check_step(self, custom_paths=None, custom_commands=None, extension=False):
"""Real version of sanity_check_step method."""
def _sanity_check_step(self, custom_paths=None, custom_commands=None, extension=False, extra_modules=None):
"""
Real version of sanity_check_step method.

:param custom_paths: custom sanity check paths to check existence for
:param custom_commands: custom sanity check commands to run
:param extension: indicates whether or not sanity check is run for an extension
:param extra_modules: extra modules to load before running sanity check commands
"""
paths, path_keys_and_check, commands = self._sanity_check_step_common(custom_paths, custom_commands)

# helper function to sanity check (alternatives for) one particular path
Expand Down Expand Up @@ -2355,6 +2427,7 @@ def xs2str(xs):
trace_msg("%s %s found: %s" % (typ, xs2str(xs), ('FAILED', 'OK')[found]))

fake_mod_data = None

# only load fake module for non-extensions, and not during dry run
if not (extension or self.dry_run):
try:
Expand All @@ -2365,6 +2438,11 @@ def xs2str(xs):
self.sanity_check_fail_msgs.append("loading fake module failed: %s" % err)
self.log.warning("Sanity check: %s" % self.sanity_check_fail_msgs[-1])

# also load specificed additional modules, if any
if extra_modules:
self.log.info("Loading extra modules for sanity check: %s", ', '.join(extra_modules))
self.modules_tool.load(extra_modules)

# chdir to installdir (better environment for running tests)
if os.path.isdir(self.installdir):
change_dir(self.installdir)
Expand Down Expand Up @@ -2449,8 +2527,6 @@ def cleanup_step(self):

env.restore_env_vars(self.cfg['unwanted_env_vars'])

self.restore_iterate_opts()

def invalidate_module_caches(self, modpath):
"""Helper method to invalidate module caches for specified module path."""
# invalidate relevant 'module avail'/'module show' cache entries
Expand Down Expand Up @@ -2729,6 +2805,7 @@ def install_step_spec(initial):
configure_step_spec = (CONFIGURE_STEP, 'configuring', [lambda x: x.configure_step], True)
build_step_spec = (BUILD_STEP, 'building', [lambda x: x.build_step], True)
test_step_spec = (TEST_STEP, 'testing', [lambda x: x.test_step], True)
extensions_step_spec = (EXTENSIONS_STEP, 'taking care of extensions', [lambda x: x.extensions_step], False)

# part 1: pre-iteration + first iteration
steps_part1 = [
Expand All @@ -2741,6 +2818,7 @@ def install_step_spec(initial):
build_step_spec,
test_step_spec,
install_step_spec(True),
extensions_step_spec,
]
# part 2: iterated part, from 2nd iteration onwards
# repeat core procedure again depending on specified iteration count
Expand All @@ -2754,10 +2832,11 @@ def install_step_spec(initial):
build_step_spec,
test_step_spec,
install_step_spec(False),
extensions_step_spec,
] * (iteration_count - 1)
# part 3: post-iteration part
steps_part3 = [
(EXTENSIONS_STEP, 'taking care of extensions', [lambda x: x.extensions_step], False),
(POSTITER_STEP, 'restore after iterating', [lambda x: x.post_iter_step], False),
(POSTPROC_STEP, 'postprocessing', [lambda x: x.post_install_step], True),
(SANITYCHECK_STEP, 'sanity checking', [lambda x: x.sanity_check_step], True),
(CLEANUP_STEP, 'cleaning up', [lambda x: x.cleanup_step], False),
Expand Down
1 change: 1 addition & 0 deletions easybuild/framework/easyconfig/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
'builddependencies': [[], "List of build dependencies", DEPENDENCIES],
'dependencies': [[], "List of dependencies", DEPENDENCIES],
'hiddendependencies': [[], "List of dependencies available as hidden modules", DEPENDENCIES],
'multi_deps': [{}, "Dict of lists of dependency versions over which to iterate", DEPENDENCIES],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this only makes for the case of build_deps but I still think that there's no harm in making that connection explicit here (for those who haven't thought so deeply about it), how about multi_builddeps?

Copy link
Member Author

@boegel boegel Mar 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that could make sense, but it's kind of double...

What we're doing is installing something for multiple versions of a particular dependency (e.g. Python) in a single installation prefix.
Those dependencies are generally runtime dependencies, but during the installation process we only use them as build dependencies, to avoid that they are included as a runtime dep in the generated module file.
That's the only way we can iterate over dependencies without introducing conflicts in the generated module file.
That this is done by injecting stuff into builddependencies doesn't have to be explicit imho, it's kind of an implementation detail...

One concern with the multi-Python installations (raised by @Micket) is that users will have to know to load a Python module as well for things to work.
This could be remedied by loading one of the Python versions (e.g. the first listed in multi_deps) by default, if no other Python version is loaded already (perhaps with an accompanying message). The logical choice there would be the Python 3.x version. Note that this still leaves the door open to use another Python version, which is just a module swap away...

I haven't made up my mind how to implement that yet (and it should probably be part of this PR)...
It could be done in general in the framework, or only for Python packages (but then we'll need to touch multiple generic easyblock like PythonPackage, PythonBundle, etc.).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that makes sense.

Regarding letting users know a Python module is required is something that can be handled with depends_on('Python') (for Lmod) without providing a version. That way the default is chosen by the site, without them doing something explicit it would be the latest version of Python 3. This could also work in general, depends_on('%(name)s'). Of course it would mean that if they had multiple subversions of Python the dep tree might change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ocaisa That won't work imho, since you'd be in trouble when a new Python 3.x module is installed with a different subtoolchain...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There you go with your flat naming scheme ruining the party again!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for loading a default module is not implemented yet in this PR, I'll do that in a follow-up PR to avoid making this PR too big/complicated...

It's a nice enhancement to have, but not a strict requirement to make this work...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to the depends_on support in the future. One could have a guard, to check if the MNS is hierarchical or not, and use depends_on selectively on those cases. But the guard might not be trivial to make it general for custom MNS

'osdependencies': [[], "OS dependencies that should be present on the system", DEPENDENCIES],

# LICENSE easyconfig parameters
Expand Down
84 changes: 74 additions & 10 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ def __init__(self, path, extra_options=None, build_specs=None, validate=True, hi

self.external_modules_metadata = build_option('external_modules_metadata')

# list of all options to iterate over
self.iterate_options = []
self.iterating = False

# parse easyconfig file
self.build_specs = build_specs
self.parse()
Expand Down Expand Up @@ -527,26 +531,23 @@ def parse(self):
else:
self.log.debug("Ignoring unknown easyconfig parameter %s (value: %s)" % (key, local_vars[key]))

# trigger parse hook
# templating is disabled when parse_hook is called to allow for easy updating of mutable easyconfig parameters
# (see also comment in resolve_template)
hooks = load_hooks(build_option('hooks'))
prev_enable_templating = self.enable_templating
self.enable_templating = False

# if any lists of dependency versions are specified over which we should iterate,
# deal with them now, before calling parse hook, parsing of dependencies & iterative easyconfig parameters...
self.handle_multi_deps()

parse_hook_msg = None
if self.path:
parse_hook_msg = "Running %s hook for %s..." % (PARSE, os.path.basename(self.path))

# trigger parse hook
hooks = load_hooks(build_option('hooks'))
run_hook(PARSE, hooks, args=[self], msg=parse_hook_msg)

# create a list of all options that are actually going to be iterated over
# builddependencies are always a list, need to look deeper down below

# list of all options to iterate over
self.iterate_options = []
self.iterating = False

# parse dependency specifications
# it's important that templating is still disabled at this stage!
self.log.info("Parsing dependency specifications...")
Expand Down Expand Up @@ -731,7 +732,7 @@ def filter_hidden_deps(self):
deps = dict([(key, self.get_ref(key)) for key in ['dependencies', 'builddependencies', 'hiddendependencies']])

if 'builddependencies' in self.iterate_options:
deplists = deps['builddependencies']
deplists = copy.deepcopy(deps['builddependencies'])
else:
deplists = [deps['builddependencies']]

Expand Down Expand Up @@ -1032,6 +1033,69 @@ def handle_external_module_metadata(self, dep_name):

return dependency

def handle_multi_deps(self):
"""
Handle lists of dependency versions of which we should iterate specified in 'multi_deps' easyconfig parameter.

This is basically just syntactic sugar to prevent having to specify a list of lists in 'builddependencies'.
"""

multi_deps = self['multi_deps']
if multi_deps:

# first, make sure all lists have same length, otherwise we're dealing with invalid input...
multi_dep_cnts = nub([len(dep_vers) for dep_vers in multi_deps.values()])
if len(multi_dep_cnts) == 1:
multi_dep_cnt = multi_dep_cnts[0]
else:
raise EasyBuildError("Not all the dependencies listed in multi_deps have the same number of versions!")

self.log.info("Found %d lists of %d dependency versions to iterate over", len(multi_deps), multi_dep_cnt)

# make sure that build dependencies is not a list of lists to iterate over already...
if self['builddependencies'] and all(isinstance(bd, list) for bd in self['builddependencies']):
raise EasyBuildError("Can't combine multi_deps with builddependencies specified as list of lists")

# now make builddependencies a list of lists to iterate over
builddeps = self['builddependencies']
self['builddependencies'] = []

keys = sorted(multi_deps.keys())
for idx in range(multi_dep_cnt):
self['builddependencies'].append(builddeps + [(key, multi_deps[key][idx]) for key in keys])

self.log.info("Original list of build dependencies: %s", builddeps)
self.log.info("List of lists of build dependencies to iterate over: %s", self['builddependencies'])

def get_parsed_multi_deps(self):
"""Get list of lists of parsed dependencies that correspond with entries in multi_deps easyconfig parameter."""

multi_deps = []

builddeps = self['builddependencies']

# all multi_deps entries should be listed in builddependencies (if not, something is very wrong)
if isinstance(builddeps, list) and all(isinstance(x, list) for x in builddeps):

for iter_id in range(len(builddeps)):

# only build dependencies that correspond to multi_deps entries should be loaded as extra modules
# (other build dependencies should not be required to make sanity check pass for this iteration)
iter_deps = []
for key in self['multi_deps']:
hits = [d for d in builddeps[iter_id] if d['name'] == key]
if len(hits) == 1:
iter_deps.append(hits[0])
else:
raise EasyBuildError("Failed to isolate %s dep during iter #%d: %s", key, iter_id, hits)

multi_deps.append(iter_deps)
else:
error_msg = "builddependencies should be a list of lists when calling get_multi_deps(), but it's not: %s"
raise EasyBuildError(error_msg, builddeps)

return multi_deps

# private method
def _parse_dependency(self, dep, hidden=False, build_only=False):
"""
Expand Down
4 changes: 2 additions & 2 deletions easybuild/framework/easyconfig/format/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@

# values for these keys will not be templated in dump()
EXCLUDED_KEYS_REPLACE_TEMPLATES = ['description', 'easyblock', 'exts_list', 'homepage', 'name', 'toolchain',
'version'] + DEPENDENCY_PARAMETERS
'version', 'multi_deps'] + DEPENDENCY_PARAMETERS

# ordered groups of keys to obtain a nice looking easyconfig file
GROUPED_PARAMS = [
Expand All @@ -63,7 +63,7 @@
['homepage', 'description'],
['toolchain', 'toolchainopts'],
['source_urls', 'sources', 'patches', 'checksums'],
DEPENDENCY_PARAMETERS,
DEPENDENCY_PARAMETERS + ['multi_deps'],
['osdependencies'],
['preconfigopts', 'configopts'],
['prebuildopts', 'buildopts'],
Expand Down
Loading