Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d56f4fa
Adding the lowercase module name scheme
victorusu Jul 10, 2017
dc51f55
Merge pull request #3 from victorusu/lowercase-name-scheme
victorusu Jul 10, 2017
54d9722
Merge branch 'master' of https://github.com/easybuilders/easybuild-fr…
victorusu Jan 30, 2019
00d0077
Merge branch 'master' of https://github.com/easybuilders/easybuild-fr…
victorusu Jan 23, 2020
46473d5
Add probe of PREFIX and VERSION variable in external modules
victorusu Jan 23, 2020
9cdf2ed
Fix PR remarks and add support for Lua syntax
victorusu Jan 23, 2020
afe4e11
Improve the design
victorusu Jan 23, 2020
5d87a9a
improve cray support
victorusu Jan 30, 2020
6648542
Fix PR remarks
victorusu Feb 17, 2020
bab80b3
Add Cray specific mapping
victorusu Mar 2, 2020
b3152c7
Address PR remarks
victorusu Mar 2, 2020
e6f3eb7
Address style changes
victorusu Mar 2, 2020
6d3a63e
Quick fix to test external_metadata definition hypothesis
victorusu Mar 2, 2020
1a8cf18
Merge branch 'develop' into cug20
boegel Apr 8, 2020
b80c8d6
Merge pull request #4 from boegel/cug20
victorusu Apr 8, 2020
e36cde2
rename ModulesTool.get_variable_from_modulefile method to ModulesTool…
boegel Apr 8, 2020
1507e62
reimplement handle_external_module_metadata + clean up probe_external…
boegel Apr 8, 2020
0277662
enhance test_external_dependencies to check probing of external modul…
boegel Apr 8, 2020
12573ab
fix broken test_resolve_dependencies test by providing dummy implemen…
boegel Apr 8, 2020
a9facb3
strip off leading/trailing whitespace in get_setenv_value_from_module…
boegel Apr 8, 2020
227a9fd
use prefix value rather than name of environment variable that contai…
boegel Apr 8, 2020
1fbea39
Merge pull request #5 from boegel/cug20
victorusu Apr 9, 2020
3aed793
remove unused imports in test/framework/modules.py
boegel Apr 9, 2020
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
136 changes: 125 additions & 11 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
:author: Alan O'Cais (Juelich Supercomputing Centre)
:author: Bart Oldeman (McGill University, Calcul Quebec, Compute Canada)
:author: Maxime Boissonneault (Universite Laval, Calcul Quebec, Compute Canada)
:author: Victor Holanda (CSCS, ETH Zurich)
"""

import copy
Expand All @@ -62,7 +63,7 @@
from easybuild.tools.config import GENERIC_EASYBLOCK_PKG, LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN
from easybuild.tools.config import Singleton, build_option, get_module_naming_scheme
from easybuild.tools.filetools import copy_file, create_index, decode_class_name, encode_class_name
from easybuild.tools.filetools import convert_name, copy_file, create_index, decode_class_name, encode_class_name
from easybuild.tools.filetools import find_backup_name_candidate, find_easyconfigs, load_index
from easybuild.tools.filetools import read_file, write_file
from easybuild.tools.hooks import PARSE, load_hooks, run_hook
Expand Down Expand Up @@ -1165,20 +1166,133 @@ def _validate(self, attr, values): # private method
if self[attr] and self[attr] not in values:
raise EasyBuildError("%s provided '%s' is not valid: %s", attr, self[attr], values)

def handle_external_module_metadata(self, dep_name):
def probe_external_module_metadata(self, mod_name, existing_metadata=None):
"""
helper function for _parse_dependency
handles metadata for external module dependencies
Helper function for handle_external_module_metadata.

Tries to determine metadata for external module when there is not entry in the metadata file,
by looking at the variables defined by the module file.

This is mainly intended for modules provided in the Cray Programming Environment,
but it could also be useful in other contexts.

The following pairs of variables are considered (in order, first hit wins),
where 'XXX' is the software name in capitals:
1. $CRAY_XXX_PREFIX and $CRAY_XXX_VERSION
1. $CRAY_XXX_PREFIX_DIR and $CRAY_XXX_VERSION
2. $CRAY_XXX_DIR and $CRAY_XXX_VERSION
2. $CRAY_XXX_ROOT and $CRAY_XXX_VERSION
5. $XXX_PREFIX and $XXX_VERSION
4. $XXX_DIR and $XXX_VERSION
5. $XXX_ROOT and $XXX_VERSION
3. $XXX_HOME and $XXX_VERSION

If none of the pairs is found, then an empty dictionary is returned.

:param mod_name: name of the external module
:param metadata: already available metadata for this external module (if any)
"""
dependency = {}
if dep_name in self.external_modules_metadata:
dependency['external_module_metadata'] = self.external_modules_metadata[dep_name]
self.log.info("Updated dependency info with available metadata for external module %s: %s",
dep_name, dependency['external_module_metadata'])
res = {}

if existing_metadata is None:
existing_metadata = {}

soft_name = existing_metadata.get('name')
if soft_name:
# software name is a list of names in metadata, just grab first one
soft_name = soft_name[0]
else:
self.log.info("No metadata available for external module %s", dep_name)
# if the software name is not known yet, use the first part of the module name as software name,
# but strip off the leading 'cray-' part first (examples: cray-netcdf/4.6.1.3, cray-fftw/3.3.8.2)
soft_name = mod_name.split('/')[0]

cray_prefix = 'cray-'
if soft_name.startswith(cray_prefix):
soft_name = soft_name[len(cray_prefix):]

# determine software name to use in names of environment variables (upper case, '-' becomes '_')
soft_name_in_mod_name = convert_name(soft_name.replace('-', '_'), upper=True)

var_name_pairs = [
('CRAY_%s_PREFIX', 'CRAY_%s_VERSION'),
('CRAY_%s_PREFIX_DIR', 'CRAY_%s_VERSION'),
('CRAY_%s_DIR', 'CRAY_%s_VERSION'),
('CRAY_%s_ROOT', 'CRAY_%s_VERSION'),
('%s_PREFIX', '%s_VERSION'),
('%s_DIR', '%s_VERSION'),
('%s_ROOT', '%s_VERSION'),
('%s_HOME', '%s_VERSION'),
]

for prefix_var_name, version_var_name in var_name_pairs:
prefix_var_name = prefix_var_name % soft_name_in_mod_name
version_var_name = version_var_name % soft_name_in_mod_name

prefix = self.modules_tool.get_setenv_value_from_modulefile(mod_name, prefix_var_name)
version = self.modules_tool.get_setenv_value_from_modulefile(mod_name, version_var_name)

# we only have a hit when values for *both* variables are found
if prefix and version:
Copy link
Member

Choose a reason for hiding this comment

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

if prefix is found but not version, and if the version is in dep_name, can't we simply use that one?

Copy link
Member

Choose a reason for hiding this comment

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

The point is to only trust the metadata obtained by probing the module if it provides both the installation prefix and software version.
If it only provides one of both, we could have a "lucky hit", and the obtained metadata may be less trustworthy...

Let's go forward as is with this strict requirement, which can easily be loosened up later (the other way around is more difficult).

The other scenario, where the prefix is already known from the metadata file, but (only) the version is available through the module, may actually be more likely (since the prefix can be specified as the name of the environment variable specifying the installation prefix, not an actual hardcoded prefix, which can be done generically in the metadata file).


if 'name' not in existing_metadata:
res['name'] = [soft_name]

# if a version is already set in the available metadata, we retain it
if 'version' not in existing_metadata:
res['version'] = [version]
self.log.info('setting external module %s version to be %s', mod_name, version)

# if a prefix is already set in the available metadata, we retain it
if 'prefix' not in existing_metadata:
res['prefix'] = prefix
self.log.info('setting external module %s prefix to be %s', mod_name, prefix_var_name)
break

return dependency
return res

def handle_external_module_metadata(self, mod_name):
"""
Helper function for _parse_dependency; collects metadata for external module dependencies.

:param mod_name: name of external module to collect metadata for
"""
partial_mod_name = mod_name.split('/')[0]

# check whether existing metadata for external modules already has metadata for this module;
# first using full module name (as it is provided), for example 'cray-netcdf/4.6.1.3',
# then with partial module name, for example 'cray-netcdf'
metadata = self.external_modules_metadata.get(mod_name, {})
self.log.info("Available metadata for external module %s: %s", mod_name, metadata)

partial_mod_name_metadata = self.external_modules_metadata.get(partial_mod_name, {})
self.log.info("Available metadata for external module using partial module name %s: %s",
partial_mod_name, partial_mod_name_metadata)

for key in partial_mod_name_metadata:
if key not in metadata:
metadata[key] = partial_mod_name_metadata[key]

self.log.info("Combined available metadata for external module %s: %s", mod_name, metadata)

# if not all metadata is available (name/version/prefix), probe external module to collect more metadata;
# first with full module name, and then with partial module name if first probe didn't return anything;
# note: result of probe_external_module_metadata only contains metadata for keys that were not set yet
if not all(key in metadata for key in ['name', 'prefix', 'version']):
self.log.info("Not all metadata found yet for external module %s, probing module...", mod_name)
probed_metadata = self.probe_external_module_metadata(mod_name, existing_metadata=metadata)
if probed_metadata:
self.log.info("Extra metadata found by probing external module %s: %s", mod_name, probed_metadata)
metadata.update(probed_metadata)
else:
self.log.info("No extra metadata found by probing %s, trying with partial module name...", mod_name)
probed_metadata = self.probe_external_module_metadata(partial_mod_name, existing_metadata=metadata)
self.log.info("Extra metadata for external module %s found by probing partial module name %s: %s",
mod_name, partial_mod_name, probed_metadata)
metadata.update(probed_metadata)

self.log.info("Obtained metadata after module probing: %s", metadata)

return {'external_module_metadata': metadata}

def handle_multi_deps(self):
"""
Expand Down
62 changes: 58 additions & 4 deletions easybuild/tools/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,24 +649,28 @@ def show(self, mod_name):

return ans

def get_value_from_modulefile(self, mod_name, regex):
def get_value_from_modulefile(self, mod_name, regex, strict=True):
"""
Get info from the module file for the specified module.

:param mod_name: module name
:param regex: (compiled) regular expression, with one group
"""
value = None

if self.exist([mod_name], skip_avail=True)[0]:
modinfo = self.show(mod_name)
res = regex.search(modinfo)
if res:
return res.group(1)
else:
value = res.group(1)
elif strict:
raise EasyBuildError("Failed to determine value from 'show' (pattern: '%s') in %s",
regex.pattern, modinfo)
else:
elif strict:
raise EasyBuildError("Can't get value from a non-existing module %s", mod_name)

return value

def modulefile_path(self, mod_name, strip_ext=False):
"""
Get the path of the module file for the specified module
Expand Down Expand Up @@ -1086,6 +1090,15 @@ def path_to_top_of_module_tree(self, top_paths, mod_name, full_mod_subdir, deps,
self.log.debug("Path to top of module tree from %s: %s" % (mod_name, path))
return path

def get_setenv_value_from_modulefile(self, mod_name, var_name):
"""
Get value for specific 'setenv' statement from module file for the specified module.

:param mod_name: module name
:param var_name: name of the variable being set for which value should be returned
"""
raise NotImplementedError

def update(self):
"""Update after new modules were added."""
raise NotImplementedError
Expand Down Expand Up @@ -1126,6 +1139,26 @@ def update(self):
"""Update after new modules were added."""
pass

def get_setenv_value_from_modulefile(self, mod_name, var_name):
"""
Get value for specific 'setenv' statement from module file for the specified module.

:param mod_name: module name
:param var_name: name of the variable being set for which value should be returned
"""
# Tcl-based module tools produce "module show" output with setenv statements like:
# "setenv GCC_PATH /opt/gcc/8.3.0"
# - line starts with 'setenv'
# - whitespace (spaces & tabs) around variable name
# - no quotes or parentheses around value (which can contain spaces!)
regex = re.compile(r'^setenv\s+%s\s+(?P<value>.+)' % var_name, re.M)
value = self.get_value_from_modulefile(mod_name, regex, strict=False)

if value:
value = value.strip()

return value


class EnvironmentModulesTcl(EnvironmentModulesC):
"""Interface to (Tcl) environment modules (modulecmd.tcl)."""
Expand Down Expand Up @@ -1390,6 +1423,27 @@ def exist(self, mod_names, skip_avail=False, maybe_partial=True):
return super(Lmod, self).exist(mod_names, mod_exists_regex_template=r'^\s*\S*/%s.*(\.lua)?:\s*$',
skip_avail=skip_avail, maybe_partial=maybe_partial)

def get_setenv_value_from_modulefile(self, mod_name, var_name):
"""
Get value for specific 'setenv' statement from module file for the specified module.

:param mod_name: module name
:param var_name: name of the variable being set for which value should be returned
"""
# Lmod produces "module show" output with setenv statements like:
# setenv("EBROOTBZIP2","/tmp/software/bzip2/1.0.6")
# - line starts with setenv(
# - both variable name and value are enclosed in double quotes, separated by comma
# - value can contain spaces!
# - line ends with )
regex = re.compile(r'^setenv\("%s"\s*,\s*"(?P<value>.+)"\)' % var_name, re.M)
value = self.get_value_from_modulefile(mod_name, regex, strict=False)

if value:
value = value.strip()

return value


def get_software_root_env_var_name(name):
"""Return name of environment variable for software root."""
Expand Down
Loading