diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py
index a786cbea2b..0920fec2fc 100644
--- a/easybuild/framework/easyblock.py
+++ b/easybuild/framework/easyblock.py
@@ -54,6 +54,7 @@
import traceback
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
+from enum import Enum
from textwrap import indent
import easybuild.tools.environment as env
@@ -74,6 +75,7 @@
from easybuild.tools.build_log import print_error, print_msg, print_warning
from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES, PYTHONPATH, EBPYTHONPREFIXES
from easybuild.tools.config import FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES
+from easybuild.tools.config import SEARCH_PATH_BIN_DIRS, SEARCH_PATH_LIB_DIRS
from easybuild.tools.config import EASYBUILD_SOURCES_URL # noqa
from easybuild.tools.config import build_option, build_path, get_log_filename, get_repository, get_repositorypath
from easybuild.tools.config import install_path, log_path, package_path, source_paths
@@ -95,7 +97,8 @@
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl, module_generator, dependencies_for
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
from easybuild.tools.modules import ROOT_ENV_VAR_NAME_PREFIX, VERSION_ENV_VAR_NAME_PREFIX, DEVEL_ENV_VAR_NAME_PREFIX
-from easybuild.tools.modules import Lmod, curr_module_paths, invalidate_module_caches_for, get_software_root
+from easybuild.tools.modules import Lmod, ModuleLoadEnvironment
+from easybuild.tools.modules import curr_module_paths, invalidate_module_caches_for, get_software_root
from easybuild.tools.modules import get_software_root_env_var_name, get_software_version_env_var_name
from easybuild.tools.output import PROGRESS_BAR_DOWNLOAD_ALL, PROGRESS_BAR_EASYCONFIG, PROGRESS_BAR_EXTENSIONS
from easybuild.tools.output import show_progress_bars, start_progress_bar, stop_progress_bar, update_progress_bar
@@ -107,7 +110,7 @@
from easybuild.tools.utilities import remove_unwanted_chars, time2str, trace_msg
from easybuild.tools.version import this_is_easybuild, VERBOSE_VERSION, VERSION
-DEFAULT_BIN_LIB_SUBDIRS = ('bin', 'lib', 'lib64')
+DEFAULT_BIN_LIB_SUBDIRS = SEARCH_PATH_BIN_DIRS + SEARCH_PATH_LIB_DIRS
MODULE_ONLY_STEPS = [MODULE_STEP, PREPARE_STEP, READY_STEP, POSTITER_STEP, SANITYCHECK_STEP]
@@ -120,6 +123,11 @@
_log = fancylogger.getLogger('easyblock')
+class LibSymlink(Enum):
+ """Possible states for symlinking of library directories"""
+ NONE, LIB, LIB64, NEITHER = range(0, 4)
+
+
class EasyBlock(object):
"""Generic support for building and installing software, base class for actual easyblocks."""
@@ -201,9 +209,15 @@ def __init__(self, ec):
if modules_header_path is not None:
self.modules_header = read_file(modules_header_path)
+ # environment variables on module load
+ self.module_load_environment = ModuleLoadEnvironment()
+
# determine install subdirectory, based on module name
self.install_subdir = None
+ # track status of symlink between library directories
+ self.install_lib_symlink = LibSymlink.NONE
+
# indicates whether build should be performed in installation dir
self.build_in_installdir = self.cfg['buildininstalldir']
@@ -1601,108 +1615,113 @@ def make_module_group_check(self):
return txt
- def make_module_req(self):
+ def make_module_req(self, fake=False):
"""
- Generate the environment-variables to run the module.
+ Generate the environment-variables required to run the module.
+ Fake modules can set search paths to empty directories.
"""
- requirements = self.make_module_req_guess()
-
- lines = ['\n']
- if os.path.isdir(self.installdir):
- old_dir = change_dir(self.installdir)
- else:
- old_dir = None
+ mod_lines = ['\n']
if self.dry_run:
self.dry_run_msg("List of paths that would be searched and added to module file:\n")
note = "note: glob patterns are not expanded and existence checks "
note += "for paths are skipped for the statements below due to dry run"
- lines.append(self.module_generator.comment(note))
-
- # For these environment variables, the corresponding directory must include at least one file.
- # The values determine if detection is done recursively, i.e. if it accepts directories where files
- # are only in subdirectories.
- keys_requiring_files = {
- 'PATH': False,
- 'LD_LIBRARY_PATH': False,
- 'LIBRARY_PATH': True,
- 'CPATH': True,
- 'CMAKE_PREFIX_PATH': True,
- 'CMAKE_LIBRARY_PATH': True,
- }
+ mod_lines.append(self.module_generator.comment(note))
- for key, reqs in sorted(requirements.items()):
- if isinstance(reqs, str):
- self.log.warning("Hoisting string value %s into a list before iterating over it", reqs)
- reqs = [reqs]
+ # prefer deprecated make_module_req_guess on custom easyblocks
+ if self.make_module_req_guess.__qualname__ == "EasyBlock.make_module_req_guess":
+ # No custom method in child Easyblock, deprecated method is defined by base EasyBlock class
+ env_var_requirements = self.module_load_environment.environ
+ else:
+ # Custom deprecated method used by child EasyBlock
+ self.log.devel(
+ "make_module_req_guess() is deprecated, use module_load_environment object instead.",
+ "6.0",
+ )
+ env_var_requirements = self.make_module_req_guess()
+ # backward compatibility: manually convert paths defined as string to lists
+ env_var_requirements.update({
+ envar: [path] for envar, path in env_var_requirements.items() if isinstance(path, str)
+ })
+
+ for env_var, search_paths in sorted(env_var_requirements.items()):
if self.dry_run:
- self.dry_run_msg(" $%s: %s" % (key, ', '.join(reqs)))
- # Don't expand globs or do any filtering below for dry run
- paths = reqs
+ self.dry_run_msg(f" ${env_var}:{', '.join(search_paths)}")
+ # Don't expand globs or do any filtering for dry run
+ mod_req_paths = search_paths
else:
- # Expand globs but only if the string is non-empty
- # empty string is a valid value here (i.e. to prepend the installation prefix, cfr $CUDA_HOME)
- paths = sum((glob.glob(path) if path else [path] for path in reqs), []) # sum flattens to list
-
- # If lib64 is just a symlink to lib we fixup the paths to avoid duplicates
- lib64_is_symlink = (all(os.path.isdir(path) for path in ['lib', 'lib64']) and
- os.path.samefile('lib', 'lib64'))
- if lib64_is_symlink:
- fixed_paths = []
- for path in paths:
- if (path + os.path.sep).startswith('lib64' + os.path.sep):
- # We only need CMAKE_LIBRARY_PATH if there is a separate lib64 path, so skip symlink
- if key == 'CMAKE_LIBRARY_PATH':
- continue
- path = path.replace('lib64', 'lib', 1)
- fixed_paths.append(path)
- if fixed_paths != paths:
- self.log.info("Fixed symlink lib64 in paths for %s: %s -> %s", key, paths, fixed_paths)
- paths = fixed_paths
- # remove duplicate paths preserving order
- paths = nub(paths)
- if key in keys_requiring_files:
- # only retain paths that contain at least one file
- recursive = keys_requiring_files[key]
- retained_paths = []
- for pth in paths:
- fullpath = os.path.join(self.installdir, pth)
- if os.path.isdir(fullpath) and dir_contains_files(fullpath, recursive=recursive):
- retained_paths.append(pth)
- if retained_paths != paths:
- self.log.info("Only retaining paths for %s that contain at least one file: %s -> %s",
- key, paths, retained_paths)
- paths = retained_paths
-
- if paths:
- lines.append(self.module_generator.prepend_paths(key, paths))
+ mod_req_paths = []
+ top_level = getattr(self.module_load_environment, env_var).top_level_file
+ for path in search_paths:
+ mod_req_paths.extend(self._expand_module_search_path(path, top_level, fake=fake))
+
+ if mod_req_paths:
+ mod_req_paths = nub(mod_req_paths) # remove duplicates
+ mod_lines.append(self.module_generator.prepend_paths(env_var, mod_req_paths))
+
if self.dry_run:
self.dry_run_msg('')
- if old_dir is not None:
- change_dir(old_dir)
+ return "".join(mod_lines)
- return ''.join(lines)
+ def _expand_module_search_path(self, search_path, top_level, fake=False):
+ """
+ Expand given path glob and return list of suitable paths to be used as search paths:
+ - Paths are relative to installation prefix root
+ - Paths to files must exist and directories be non-empty
+ - Fake modules can set search paths to empty directories
+ - Search paths to a 'lib64' symlinked to 'lib' are discarded to avoid duplicates
+ """
+ # Expand globs but only if the string is non-empty
+ # empty string is a valid value here (i.e. to prepend the installation prefix root directory)
+ abs_glob = os.path.join(self.installdir, search_path)
+ exp_search_paths = [abs_glob] if search_path == "" else glob.glob(abs_glob)
+
+ # Explicitly check symlink state between lib dirs if it is still undefined (e.g. --module-only)
+ if self.install_lib_symlink == LibSymlink.NONE:
+ self.check_install_lib_symlink()
+
+ retained_search_paths = []
+ for abs_path in exp_search_paths:
+ # return relative paths
+ tentative_path = os.path.relpath(abs_path, start=self.installdir)
+ tentative_path = "" if tentative_path == "." else tentative_path # use empty string instead of dot
+
+ # avoid duplicate entries between symlinked library dirs
+ tentative_sep = tentative_path + os.path.sep
+ if self.install_lib_symlink == LibSymlink.LIB64 and tentative_sep.startswith("lib64" + os.path.sep):
+ self.log.debug("Discarded search path to symlinked lib64 directory: %s", tentative_path)
+ break
+ if self.install_lib_symlink == LibSymlink.LIB and tentative_sep.startswith("lib" + os.path.sep):
+ self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path)
+ break
+
+ # only retain paths to directories that contain at least one file
+ if os.path.isdir(abs_path) and not dir_contains_files(abs_path, recursive=not top_level) and not fake:
+ self.log.debug("Discarded search path to empty directory: %s", tentative_path)
+ break
+
+ retained_search_paths.append(tentative_path)
+
+ return retained_search_paths
+
+ def check_install_lib_symlink(self):
+ """Check symlink state between library directories in installation prefix"""
+ lib_dir = os.path.join(self.installdir, 'lib')
+ lib64_dir = os.path.join(self.installdir, 'lib64')
+ if os.path.exists(lib_dir) and os.path.exists(lib64_dir):
+ self.install_lib_symlink = LibSymlink.NEITHER
+ if os.path.islink(lib_dir) and os.path.samefile(lib_dir, lib64_dir):
+ self.install_lib_symlink = LibSymlink.LIB
+ elif os.path.islink(lib64_dir) and os.path.samefile(lib_dir, lib64_dir):
+ self.install_lib_symlink = LibSymlink.LIB64
def make_module_req_guess(self):
"""
- A dictionary of possible directories to look for.
- """
- lib_paths = ['lib', 'lib32', 'lib64']
- return {
- 'PATH': ['bin', 'sbin'],
- 'LD_LIBRARY_PATH': lib_paths,
- 'LIBRARY_PATH': lib_paths,
- 'CPATH': ['include'],
- 'MANPATH': ['man', os.path.join('share', 'man')],
- 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in lib_paths + ['share']],
- 'ACLOCAL_PATH': [os.path.join('share', 'aclocal')],
- 'CLASSPATH': ['*.jar'],
- 'XDG_DATA_DIRS': ['share'],
- 'GI_TYPELIB_PATH': [os.path.join(x, 'girepository-*') for x in lib_paths],
- 'CMAKE_PREFIX_PATH': [''],
- 'CMAKE_LIBRARY_PATH': ['lib64'], # lib and lib32 are searched through the above
- }
+ A dictionary of common search path variables to be loaded by environment modules
+ Each key contains the list of known directories related to the search path
+ """
+ return self.module_load_environment.environ
def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True):
"""
@@ -3105,18 +3124,19 @@ def post_install_step(self):
# However for each
in $LIBRARY_PATH (where is often /lib) it searches /../lib64 first.
# So we create /lib64 as a symlink to /lib to make it prefer EB installed libraries.
# See https://github.com/easybuilders/easybuild-easyconfigs/issues/5776
- if build_option('lib64_lib_symlink'):
- if os.path.exists(lib_dir) and not os.path.exists(lib64_dir):
- # create *relative* 'lib64' symlink to 'lib';
- # see https://github.com/easybuilders/easybuild-framework/issues/3564
- symlink('lib', lib64_dir, use_abspath_source=False)
+ if build_option('lib64_lib_symlink') and os.path.exists(lib_dir) and not os.path.exists(lib64_dir):
+ # create *relative* 'lib64' symlink to 'lib';
+ # see https://github.com/easybuilders/easybuild-framework/issues/3564
+ symlink('lib', lib64_dir, use_abspath_source=False)
# symlink lib to lib64, which is helpful on OpenSUSE;
# see https://github.com/easybuilders/easybuild-framework/issues/3549
- if build_option('lib_lib64_symlink'):
- if os.path.exists(lib64_dir) and not os.path.exists(lib_dir):
- # create *relative* 'lib' symlink to 'lib64';
- symlink('lib64', lib_dir, use_abspath_source=False)
+ if build_option('lib_lib64_symlink') and os.path.exists(lib64_dir) and not os.path.exists(lib_dir):
+ # create *relative* 'lib' symlink to 'lib64';
+ symlink('lib64', lib_dir, use_abspath_source=False)
+
+ # refresh symlink state
+ self.check_install_lib_symlink()
self.run_post_install_commands()
self.apply_post_install_patches()
@@ -3841,7 +3861,7 @@ def make_module_step(self, fake=False):
txt += self.make_module_deppaths()
txt += self.make_module_dep()
txt += self.make_module_extend_modpath()
- txt += self.make_module_req()
+ txt += self.make_module_req(fake=fake)
txt += self.make_module_extra()
txt += self.make_module_footer()
diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py
index 1d69822a7c..20b3826751 100644
--- a/easybuild/tools/config.py
+++ b/easybuild/tools/config.py
@@ -168,13 +168,16 @@
LOCAL_VAR_NAMING_CHECK_WARN = WARN
LOCAL_VAR_NAMING_CHECKS = [LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG, LOCAL_VAR_NAMING_CHECK_WARN]
-
OUTPUT_STYLE_AUTO = 'auto'
OUTPUT_STYLE_BASIC = 'basic'
OUTPUT_STYLE_NO_COLOR = 'no_color'
OUTPUT_STYLE_RICH = 'rich'
OUTPUT_STYLES = (OUTPUT_STYLE_AUTO, OUTPUT_STYLE_BASIC, OUTPUT_STYLE_NO_COLOR, OUTPUT_STYLE_RICH)
+SEARCH_PATH_BIN_DIRS = ["bin"]
+SEARCH_PATH_HEADER_DIRS = ["include"]
+SEARCH_PATH_LIB_DIRS = ["lib", "lib64"]
+
PYTHONPATH = 'PYTHONPATH'
EBPYTHONPREFIXES = 'EBPYTHONPREFIXES'
PYTHON_SEARCH_PATH_TYPES = [PYTHONPATH, EBPYTHONPREFIXES]
diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py
index aa2dc7b7e4..16212f9fd5 100644
--- a/easybuild/tools/modules.py
+++ b/easybuild/tools/modules.py
@@ -43,11 +43,13 @@
import shlex
from easybuild.base import fancylogger
+from easybuild.base.wrapper import create_base_metaclass
from easybuild.tools import LooseVersion
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning
from easybuild.tools.config import ERROR, IGNORE, PURGE, UNLOAD, UNSET
from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, LOADED_MODULES_ACTIONS
-from easybuild.tools.config import build_option, get_modules_tool, install_path
+from easybuild.tools.config import Singleton, build_option, get_modules_tool, install_path
+from easybuild.tools.config import SEARCH_PATH_BIN_DIRS, SEARCH_PATH_HEADER_DIRS, SEARCH_PATH_LIB_DIRS
from easybuild.tools.environment import ORIG_OS_ENVIRON, restore_env, setvar, unset_env_vars
from easybuild.tools.filetools import convert_name, mkdir, normalize_path, path_matches, read_file, which, write_file
from easybuild.tools.module_naming_scheme.mns import DEVEL_MODULE_SUFFIX
@@ -131,6 +133,116 @@
_log = fancylogger.getLogger('modules', fname=False)
+class ModuleEnvironmentVariable:
+ """Environment variable data structure for modules"""
+ def __init__(self, paths, top_level_file=False):
+ self.paths = paths
+ self.top_level_file = bool(top_level_file)
+
+ def __str__(self):
+ return ":".join(self.paths)
+
+ @property
+ def paths(self):
+ return self._paths
+
+ @paths.setter
+ def paths(self, value):
+ """Enforce that paths is a list of strings"""
+ if isinstance(value, str):
+ value = [value]
+ try:
+ self._paths = [str(path) for path in value]
+ except TypeError:
+ raise TypeError("ModuleEnvironmentVariable.paths must be a list of strings") from None
+
+ def append(self, *args):
+ """Shortcut to append to list of paths"""
+ self.paths.append(*args)
+
+ def extend(self, *args):
+ """Shortcut to extend list of paths"""
+ self.paths.extend(*args)
+
+ def prepend(self, item):
+ """Shortcut to append to list of paths"""
+ self.paths.insert(0, item)
+
+
+# singleton metaclass: only one instance is created
+BaseModuleEnvironment = create_base_metaclass('BaseModuleEnvironment', Singleton, object)
+
+
+class ModuleLoadEnvironment(BaseModuleEnvironment):
+ """Environment set by modules on load"""
+
+ def __init__(self):
+ """
+ Initialize default environment definition
+ Paths are relative to root of installation directory
+ """
+
+ self.PATH = (
+ SEARCH_PATH_BIN_DIRS + ['sbin'],
+ {"top_level_file": True},
+ )
+ self.LD_LIBRARY_PATH = (
+ SEARCH_PATH_LIB_DIRS,
+ {"top_level_file": True},
+ )
+ self.LIBRARY_PATH = SEARCH_PATH_LIB_DIRS
+ self.CPATH = SEARCH_PATH_HEADER_DIRS
+ self.MANPATH = ['man', os.path.join('share', 'man')]
+ self.PKG_CONFIG_PATH = [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['share']]
+ self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')]
+ self.CLASSPATH = ['*.jar']
+ self.XDG_DATA_DIRS = ['share']
+ self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS]
+ self.CMAKE_PREFIX_PATH = ['']
+ # only needed for installations whith standalone lib64
+ self.CMAKE_LIBRARY_PATH = ['lib64']
+
+ def __setattr__(self, name, value):
+ """
+ Specific restrictions for ModuleLoadEnvironment attributes:
+ - attribute names are uppercase
+ - attributes are instances of ModuleEnvironmentVariable
+ """
+ try:
+ (paths, kwargs) = value
+ except ValueError:
+ paths, kwargs = value, {}
+ else:
+ if not isinstance(kwargs, dict):
+ paths, kwargs = value, {}
+
+ return super().__setattr__(name.upper(), ModuleEnvironmentVariable(paths, **kwargs))
+
+ def __iter__(self):
+ """Make the class iterable"""
+ yield from self.__dict__
+
+ def items(self):
+ """
+ Return key-value pairs for each attribute that is a ModuleEnvironmentVariable
+ - key = attribute name
+ - value = its "paths" attribute
+ """
+ for attr in self.__dict__:
+ yield attr, getattr(self, attr).paths
+
+ @property
+ def environ(self):
+ """
+ Return dict with mapping of ModuleEnvironmentVariables names with their paths
+ Equivalent in shape to os.environ
+ """
+ mapping = {}
+ for envar_name, envar_paths in self.items():
+ mapping.update({envar_name: envar_paths})
+ return mapping
+
+
class ModulesTool(object):
"""An abstract interface to a tool that deals with modules."""
# name of this modules tool (used in log/warning/error messages)
diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py
index 505a1c1d2f..5c8091be01 100644
--- a/test/framework/easyblock.py
+++ b/test/framework/easyblock.py
@@ -40,7 +40,7 @@
import easybuild.tools.systemtools as st
from easybuild.base import fancylogger
-from easybuild.framework.easyblock import EasyBlock, get_easyblock_instance
+from easybuild.framework.easyblock import LibSymlink, EasyBlock, get_easyblock_instance
from easybuild.framework.easyconfig import CUSTOM
from easybuild.framework.easyconfig.easyconfig import EasyConfig
from easybuild.framework.easyconfig.tools import avail_easyblocks, process_easyconfig
@@ -433,12 +433,15 @@ def test_make_module_req(self):
# create fake directories and files that should be guessed
os.makedirs(eb.installdir)
- write_file(os.path.join(eb.installdir, 'foo.jar'), 'foo.jar')
- write_file(os.path.join(eb.installdir, 'bla.jar'), 'bla.jar')
for path in ('bin', ('bin', 'testdir'), 'sbin', 'share', ('share', 'man'), 'lib', 'lib64'):
if isinstance(path, str):
path = (path, )
os.mkdir(os.path.join(eb.installdir, *path))
+ eb.install_lib_symlink = LibSymlink.NEITHER
+
+ write_file(os.path.join(eb.installdir, 'foo.jar'), 'foo.jar')
+ write_file(os.path.join(eb.installdir, 'bla.jar'), 'bla.jar')
+ write_file(os.path.join(eb.installdir, 'share', 'man', 'pi'), 'Man page')
# this is not a path that should be picked up
os.mkdir(os.path.join(eb.installdir, 'CPATH'))
@@ -500,6 +503,7 @@ def test_make_module_req(self):
write_file(os.path.join(eb.installdir, 'lib', 'libfoo.so'), 'test')
shutil.rmtree(os.path.join(eb.installdir, 'lib64'))
os.symlink('lib', os.path.join(eb.installdir, 'lib64'))
+ eb.install_lib_symlink = LibSymlink.LIB64
with eb.module_generator.start_module_creation():
guess = eb.make_module_req()
if get_module_syntax() == 'Tcl':
diff --git a/test/framework/modules.py b/test/framework/modules.py
index 5cd783694d..bfbb6666a2 100644
--- a/test/framework/modules.py
+++ b/test/framework/modules.py
@@ -1588,6 +1588,59 @@ def test_get_setenv_value_from_modulefile(self):
res = self.modtool.get_setenv_value_from_modulefile('toy/0.0', 'NO_SUCH_VARIABLE_SET')
self.assertEqual(res, None)
+ def test_module_environment_variable(self):
+ """Test for ModuleEnvironmentVariable object"""
+ test_paths = ['lib', 'lib64']
+ mod_envar = mod.ModuleEnvironmentVariable(test_paths)
+ self.assertTrue(hasattr(mod_envar, "paths"))
+ self.assertTrue(hasattr(mod_envar, "top_level_file"))
+ self.assertEqual(mod_envar.paths, test_paths)
+ self.assertEqual(str(mod_envar), "lib:lib64")
+
+ mod_envar.paths = []
+ self.assertEqual(mod_envar.paths, [])
+ self.assertRaises(TypeError, setattr, mod_envar, "paths", None)
+
+ mod_envar.paths = (1, 2, 3)
+ self.assertEqual(mod_envar.paths, ["1", "2", "3"])
+
+ mod_envar.paths = "include"
+ self.assertEqual(mod_envar.paths, ["include"])
+
+ mod_envar.append("share")
+ self.assertEqual(mod_envar.paths, ["include", "share"])
+ mod_envar.extend(test_paths)
+ self.assertEqual(mod_envar.paths, ["include", "share", "lib", "lib64"])
+ mod_envar.prepend("bin")
+ self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib", "lib64"])
+
+ def test_module_load_environment(self):
+ """Test for ModuleLoadEnvironment object"""
+ test_paths = ['lib', 'lib64']
+ mod_load_env = mod.ModuleLoadEnvironment()
+ mod_load_env.TEST_VAR = test_paths
+ self.assertTrue(hasattr(mod_load_env, "TEST_VAR"))
+ self.assertEqual(mod_load_env.TEST_VAR.paths, test_paths)
+
+ ref_load_env = mod_load_env.__dict__.copy()
+ self.assertCountEqual(list(mod_load_env), ref_load_env.keys())
+ ref_load_env_item_list = [(key, value.paths) for key, value in ref_load_env.items()]
+ self.assertCountEqual(list(mod_load_env.items()), ref_load_env_item_list)
+ ref_load_env_environ = {key: value.paths for key, value in ref_load_env.items()}
+ self.assertDictEqual(mod_load_env.environ, ref_load_env_environ)
+
+ mod_load_env.test_lower = test_paths
+ self.assertTrue(hasattr(mod_load_env, "TEST_LOWER"))
+ self.assertEqual(mod_load_env.TEST_LOWER.paths, test_paths)
+ mod_load_env.TEST_STR = "some/path"
+ self.assertTrue(hasattr(mod_load_env, "TEST_STR"))
+ self.assertEqual(mod_load_env.TEST_STR.paths, ["some/path"])
+ mod_load_env.TEST_EXTRA = (test_paths, {"top_level_file": True})
+ self.assertTrue(hasattr(mod_load_env, "TEST_EXTRA"))
+ self.assertEqual(mod_load_env.TEST_EXTRA.paths, test_paths)
+ self.assertEqual(mod_load_env.TEST_EXTRA.top_level_file, True)
+ self.assertRaises(TypeError, setattr, mod_load_env, "TEST_UNKNONW", (test_paths, {"unkown_param": True}))
+
def suite():
""" returns all the testcases in this module """
diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy.py b/test/framework/sandbox/easybuild/easyblocks/t/toy.py
index 5727dcd0e0..552b5d8a37 100644
--- a/test/framework/sandbox/easybuild/easyblocks/t/toy.py
+++ b/test/framework/sandbox/easybuild/easyblocks/t/toy.py
@@ -192,3 +192,10 @@ def make_module_extra(self):
txt = super(EB_toy, self).make_module_extra()
txt += self.module_generator.set_environment('TOY', os.getenv('TOY', ''))
return txt
+
+ def make_module_req_guess(self):
+ """Extra paths for environment variables to consider"""
+ guesses = super(EB_toy, self).make_module_req_guess()
+ if self.name == 'toy':
+ guesses['CPATH'].append('toy-headers')
+ return guesses
diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py
index edd9e94fcc..6edef42c5d 100644
--- a/test/framework/toy_build.py
+++ b/test/framework/toy_build.py
@@ -4302,6 +4302,42 @@ def test_toy_python(self):
self.assertTrue(ebpythonprefixes_regex.search(toy_mod_txt),
f"Pattern '{ebpythonprefixes_regex.pattern}' found in: {toy_mod_txt}")
+ def test_toy_multiple_ecs_module(self):
+ """
+ Verify whether module file is correct when multiple easyconfigs are being installed.
+ """
+ test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
+ toy_ec = os.path.join(test_ecs, 't', 'toy', 'toy-0.0.eb')
+
+ # modify 'toy' easyconfig so toy-headers subdirectory is created,
+ # which is taken into account by EB_toy easyblock for $CPATH
+ test_toy_ec = os.path.join(self.test_prefix, 'test-toy.eb')
+ toy_ec_txt = read_file(toy_ec)
+ toy_ec_txt += "\npostinstallcmds += ['mkdir %(installdir)s/toy-headers']"
+ toy_ec_txt += "\npostinstallcmds += ['touch %(installdir)s/toy-headers/toy.h']"
+ write_file(test_toy_ec, toy_ec_txt)
+
+ # modify 'toy-app' easyconfig so toy-headers subdirectory is created,
+ # which is consider by EB_toy easyblock for $CPATH,
+ # but should *not* be actually used because software name is not 'toy'
+ toy_app_ec = os.path.join(test_ecs, 't', 'toy-app', 'toy-app-0.0.eb')
+ test_toy_app_ec = os.path.join(self.test_prefix, 'test-toy-app.eb')
+ toy_ec_txt = read_file(toy_app_ec)
+ toy_ec_txt += "\npostinstallcmds += ['mkdir %(installdir)s/toy-headers']"
+ toy_ec_txt += "\npostinstallcmds += ['touch %(installdir)s/toy-headers/toy.h']"
+ write_file(test_toy_app_ec, toy_ec_txt)
+
+ self.run_test_toy_build_with_output(ec_file=test_toy_ec, extra_args=[test_toy_app_ec])
+
+ toy_modtxt = read_file(os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0.lua'))
+ regex = re.compile('prepend[-_]path.*CPATH.*toy-headers', re.M)
+ self.assertTrue(regex.search(toy_modtxt),
+ f"Pattern '{regex.pattern}' should be found in: {toy_modtxt}")
+
+ toy_app_modtxt = read_file(os.path.join(self.test_installpath, 'modules', 'all', 'toy-app', '0.0.lua'))
+ self.assertFalse(regex.search(toy_app_modtxt),
+ f"Pattern '{regex.pattern}' should *not* be found in: {toy_app_modtxt}")
+
def suite():
""" return all the tests in this file """