From 734cd9915926ab7a0ace0ef736aa09fd26046aea Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 23 Sep 2024 16:43:43 +0200 Subject: [PATCH 01/59] refactor generation of required environment variables in module files --- easybuild/framework/easyblock.py | 146 ++++++++++++++----------------- easybuild/tools/config.py | 5 +- 2 files changed, 72 insertions(+), 79 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 88da255d1d..d2977cede4 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -74,6 +74,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 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_HEADER_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 @@ -107,10 +108,13 @@ 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] +# search paths that require some file in their top directory +NON_RECURSIVE_SEARCH_PATHS = ["PATH", "LD_LIBRARY_PATH"] + # 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/' @@ -1557,107 +1561,93 @@ def make_module_group_check(self): def make_module_req(self): """ - Generate the environment-variables to run the module. + Generate the environment-variables required to run the module. """ - 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)) + + env_var_requirements = self.make_module_req_guess() + for env_var, search_paths in sorted(env_var_requirements.items()): + if isinstance(search_paths, str): + self.log.warning("Hoisting string value %s into a list before iterating over it", search_paths) + search_paths = [search_paths] - 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] + mod_env_paths = [] + recursive = env_var not in NON_RECURSIVE_SEARCH_PATHS 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_env_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)) + for sp in search_paths: + mod_env_paths.extend(self._expand_module_search_path(sp, recursive)) + + if mod_env_paths: + mod_env_paths = nub(mod_env_paths) # remove duplicates + mod_lines.append(self.module_generator.prepend_paths(env_var, mod_env_paths)) + if self.dry_run: self.dry_run_msg('') - if old_dir is not None: - change_dir(old_dir) - - return ''.join(lines) + return "".join(mod_lines) def make_module_req_guess(self): """ - A dictionary of possible directories to look for. + 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 """ - lib_paths = ['lib', 'lib32', 'lib64'] return { - 'PATH': ['bin', 'sbin'], - 'LD_LIBRARY_PATH': lib_paths, - 'LIBRARY_PATH': lib_paths, - 'CPATH': ['include'], + 'PATH': SEARCH_PATH_BIN_DIRS + ['sbin'], + 'LD_LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, + 'LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, + 'CPATH': SEARCH_PATH_HEADER_DIRS, 'MANPATH': ['man', os.path.join('share', 'man')], - 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in lib_paths + ['share']], + 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['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], + 'GI_TYPELIB_PATH': [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS], 'CMAKE_PREFIX_PATH': [''], - 'CMAKE_LIBRARY_PATH': ['lib64'], # lib and lib32 are searched through the above + 'CMAKE_LIBRARY_PATH': ['lib64'], # only needed for installations whith standalone lib64 } + def _expand_module_search_path(self, search_path, recursive): + """ + Expand given path glob and return list of paths that are suitable to be + used as search paths in environment module + """ + # 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_search_path = os.path.join(self.installdir, search_path) + exp_search_paths = [abs_search_path] if search_path == "" else glob.glob(abs_search_path) + + retained_search_paths = [] + for abs_path in exp_search_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 if lib64 is just a symlink to lib + if (tentative_path + os.path.sep).startswith("lib64" + os.path.sep): + abs_lib_path = os.path.join(self.installdir, "lib") + abs_lib64_path = os.path.join(self.installdir, "lib64") + if os.path.islink(abs_lib64_path) and os.path.samefile(abs_lib_path, abs_lib64_path): + self.log.debug("Discarded search path to symlink lib64: %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=recursive): + self.log.debug("Discarded search path to empty directory: %s", tentative_path) + break + + retained_search_paths.append(tentative_path) + + return retained_search_paths + 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. diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py index 13f9722bce..c9a914d478 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"] + class Singleton(ABCMeta): """Serves as metaclass for classes that should implement the Singleton pattern. From df505cf7da212f6d81dd3ca1933789c5fe64e6bc Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 23 Sep 2024 16:45:08 +0200 Subject: [PATCH 02/59] update test_make_module_req to add a file into MANPATH of test installation --- test/framework/easyblock.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 505a1c1d2f..bdf15504f4 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -433,12 +433,13 @@ 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)) + 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')) From 56800386935a56b23bb024c104d4c209a0a4d31f Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 24 Sep 2024 01:58:59 +0200 Subject: [PATCH 03/59] disable non-empty check on search path drectories for fake module files --- easybuild/framework/easyblock.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index d2977cede4..559c6630df 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1559,9 +1559,10 @@ def make_module_group_check(self): return txt - def make_module_req(self): + def make_module_req(self, fake=False): """ Generate the environment-variables required to run the module. + Fake modules can set search paths to empty directories. """ mod_lines = ['\n'] @@ -1585,7 +1586,7 @@ def make_module_req(self): mod_env_paths = search_paths else: for sp in search_paths: - mod_env_paths.extend(self._expand_module_search_path(sp, recursive)) + mod_env_paths.extend(self._expand_module_search_path(sp, recursive, fake=fake)) if mod_env_paths: mod_env_paths = nub(mod_env_paths) # remove duplicates @@ -1616,10 +1617,12 @@ def make_module_req_guess(self): 'CMAKE_LIBRARY_PATH': ['lib64'], # only needed for installations whith standalone lib64 } - def _expand_module_search_path(self, search_path, recursive): + def _expand_module_search_path(self, search_path, recursive, fake=False): """ - Expand given path glob and return list of paths that are suitable to be - used as search paths in environment module + Expand given path glob and return list of suitable paths to be used as search paths: + - Files must exist and directories be non-empty + - Fake modules can set search paths to empty directories + - Search paths to 'lib64' symlinked to 'lib' are discarded """ # 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) @@ -1628,6 +1631,7 @@ def _expand_module_search_path(self, search_path, recursive): 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 @@ -1640,7 +1644,7 @@ def _expand_module_search_path(self, search_path, recursive): 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=recursive): + if os.path.isdir(abs_path) and not dir_contains_files(abs_path, recursive=recursive) and not fake: self.log.debug("Discarded search path to empty directory: %s", tentative_path) break @@ -3785,7 +3789,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() From 5b17f2e8e85b0abd822c70c792db253074dc7e70 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 24 Sep 2024 23:17:18 +0200 Subject: [PATCH 04/59] change position of easyblock methods make_module_req_guess and _expand_module_search_path --- easybuild/framework/easyblock.py | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 559c6630df..660af36ccf 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1597,26 +1597,6 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def make_module_req_guess(self): - """ - 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 { - 'PATH': SEARCH_PATH_BIN_DIRS + ['sbin'], - 'LD_LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, - 'LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, - 'CPATH': SEARCH_PATH_HEADER_DIRS, - 'MANPATH': ['man', os.path.join('share', 'man')], - 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['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 SEARCH_PATH_LIB_DIRS], - 'CMAKE_PREFIX_PATH': [''], - 'CMAKE_LIBRARY_PATH': ['lib64'], # only needed for installations whith standalone lib64 - } - def _expand_module_search_path(self, search_path, recursive, fake=False): """ Expand given path glob and return list of suitable paths to be used as search paths: @@ -1652,6 +1632,26 @@ def _expand_module_search_path(self, search_path, recursive, fake=False): return retained_search_paths + def make_module_req_guess(self): + """ + 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 { + 'PATH': SEARCH_PATH_BIN_DIRS + ['sbin'], + 'LD_LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, + 'LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, + 'CPATH': SEARCH_PATH_HEADER_DIRS, + 'MANPATH': ['man', os.path.join('share', 'man')], + 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['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 SEARCH_PATH_LIB_DIRS], + 'CMAKE_PREFIX_PATH': [''], + 'CMAKE_LIBRARY_PATH': ['lib64'], # only needed for installations whith standalone lib64 + } + 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. From 9205c3b5731d4b678d68b5904ee62eed6703253f Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:03:31 +0200 Subject: [PATCH 05/59] add new class ModuleEnvironmentVariable to hold definitions of environment variables for module files --- easybuild/tools/modules.py | 36 ++++++++++++++++++++++++++++++++++++ test/framework/modules.py | 26 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index a4e1f565ae..e4699941de 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -130,6 +130,42 @@ _log = fancylogger.getLogger('modules', fname=False) +class ModuleEnvironmentVariable: + """Environment variable data structure for modules""" + def __init__(self, paths, empty=False, top_level_file=False): + self.paths = paths + self.empty = bool(empty) + 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) + class ModulesTool(object): """An abstract interface to a tool that deals with modules.""" diff --git a/test/framework/modules.py b/test/framework/modules.py index 5cd783694d..1d847db6b1 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1588,6 +1588,32 @@ 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, "empty")) + 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 suite(): """ returns all the testcases in this module """ From 29affc984bda000780fc6f8b91d83c2d519eb1bb Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:05:06 +0200 Subject: [PATCH 06/59] add new class ModuleLoadEnvironment to hold environment definition for modules on load --- easybuild/tools/modules.py | 84 +++++++++++++++++++++++++++++++++++++- test/framework/modules.py | 21 ++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index e4699941de..5bef0e82bb 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 @@ -167,6 +169,86 @@ def prepend(self, item): 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 = ModuleEnvironmentVariable( + SEARCH_PATH_BIN_DIRS + ['sbin'], + top_level_file=True, + ) + self.LD_LIBRARY_PATH = ModuleEnvironmentVariable( + SEARCH_PATH_LIB_DIRS, + top_level_file=True, + ) + self.LIBRARY_PATH = ModuleEnvironmentVariable( + SEARCH_PATH_LIB_DIRS, + ) + self.CPATH = ModuleEnvironmentVariable( + SEARCH_PATH_HEADER_DIRS, + ) + self.MANPATH = ModuleEnvironmentVariable( + ['man', os.path.join('share', 'man')], + ) + self.PKG_CONFIG_PATH = ModuleEnvironmentVariable( + [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['share']], + ) + self.ACLOCAL_PATH = ModuleEnvironmentVariable( + [os.path.join('share', 'aclocal')], + ) + self.CLASSPATH = ModuleEnvironmentVariable( + ['*.jar'], + ) + self.XDG_DATA_DIRS = ModuleEnvironmentVariable( + ['share'], + ) + self.GI_TYPELIB_PATH = ModuleEnvironmentVariable( + [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] + ) + self.CMAKE_PREFIX_PATH = ModuleEnvironmentVariable( + [''], + ) + # only needed for installations whith standalone lib64 + self.CMAKE_LIBRARY_PATH = ModuleEnvironmentVariable( + ['lib64'], + ) + + def __iter__(self): + """Make the class iterable""" + yield from (envar for envar in self.__dict__ if isinstance(getattr(self, envar), ModuleEnvironmentVariable)) + + 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__: + envar = getattr(self, attr) + if isinstance(envar, ModuleEnvironmentVariable): + yield attr, envar.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/modules.py b/test/framework/modules.py index 1d847db6b1..4437360359 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1615,6 +1615,27 @@ def test_module_environment_variable(self): mod_envar.prepend("bin") self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib", "lib64"]) + def test_module_load_environment(self): + """Test for ModuleLoadEnvironment object""" + mod_load_env = mod.ModuleLoadEnvironment() + mod_load_env.TEST_VAR = mod.ModuleEnvironmentVariable(["lib", "lib64"]) + self.assertTrue(hasattr(mod_load_env, "TEST_VAR")) + self.assertEqual(mod_load_env.TEST_VAR.paths, ["lib", "lib64"]) + + # copy current state as reference + ref_load_env = mod_load_env.__dict__.copy() + # and add some garbage + mod_load_env.garbage = "not_an_env_var" + self.assertTrue(hasattr(mod_load_env, "garbage")) + self.assertEqual(mod_load_env.garbage, "not_an_env_var") + + 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) + + def suite(): """ returns all the testcases in this module """ return TestLoaderFiltered().loadTestsFromTestCase(ModulesTest, sys.argv[1:]) From 8d3a0511dc9dac7f6dee1d69d2e8104234142fed Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:06:43 +0200 Subject: [PATCH 07/59] add LibSymlink enum to easyblock to define possible states of symlinked library directories --- easybuild/framework/easyblock.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 660af36ccf..3c13b45866 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 @@ -124,6 +125,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.""" From ee37ae744b930dda932ea7a60cf11aa650b2ce27 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:07:30 +0200 Subject: [PATCH 08/59] set library symlink state at the end of post_install_step --- easybuild/framework/easyblock.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 3c13b45866..a267cc86c1 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -214,6 +214,9 @@ def __init__(self, ec): # 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'] @@ -3059,18 +3062,24 @@ 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) + + # check symlink state + 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 self.run_post_install_commands() self.apply_post_install_patches() From b6e1821d38637a202a7aa214e34b98bd60dcd7b7 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:10:37 +0200 Subject: [PATCH 09/59] use environment definition from ModuleLoadEnvironment in make_module_req_guess --- easybuild/framework/easyblock.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index a267cc86c1..afef6c2c53 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -97,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 @@ -211,6 +212,9 @@ 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 @@ -1646,20 +1650,7 @@ def make_module_req_guess(self): 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 { - 'PATH': SEARCH_PATH_BIN_DIRS + ['sbin'], - 'LD_LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, - 'LIBRARY_PATH': SEARCH_PATH_LIB_DIRS, - 'CPATH': SEARCH_PATH_HEADER_DIRS, - 'MANPATH': ['man', os.path.join('share', 'man')], - 'PKG_CONFIG_PATH': [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['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 SEARCH_PATH_LIB_DIRS], - 'CMAKE_PREFIX_PATH': [''], - 'CMAKE_LIBRARY_PATH': ['lib64'], # only needed for installations whith standalone lib64 - } + return self.module_load_environment.environ def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True): """ From 544a3165ba22f340f53c976495d767f9e075d1a7 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:12:27 +0200 Subject: [PATCH 10/59] deprecate make_module_req_guess in favor of directly using ModuleLoadEnvironment --- easybuild/framework/easyblock.py | 44 +++++++++++++++++++------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index afef6c2c53..5a27fae724 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -75,7 +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 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_HEADER_DIRS, SEARCH_PATH_LIB_DIRS +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 @@ -114,9 +114,6 @@ MODULE_ONLY_STEPS = [MODULE_STEP, PREPARE_STEP, READY_STEP, POSTITER_STEP, SANITYCHECK_STEP] -# search paths that require some file in their top directory -NON_RECURSIVE_SEARCH_PATHS = ["PATH", "LD_LIBRARY_PATH"] - # 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/' @@ -1585,25 +1582,36 @@ def make_module_req(self, fake=False): note += "for paths are skipped for the statements below due to dry run" mod_lines.append(self.module_generator.comment(note)) - env_var_requirements = self.make_module_req_guess() - for env_var, search_paths in sorted(env_var_requirements.items()): - if isinstance(search_paths, str): - self.log.warning("Hoisting string value %s into a list before iterating over it", search_paths) - search_paths = [search_paths] + # 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) + }) - mod_env_paths = [] - recursive = env_var not in NON_RECURSIVE_SEARCH_PATHS + for env_var, search_paths in sorted(env_var_requirements.items()): if self.dry_run: self.dry_run_msg(f" ${env_var}:{', '.join(search_paths)}") # Don't expand globs or do any filtering for dry run - mod_env_paths = search_paths + mod_req_paths = search_paths else: - for sp in search_paths: - mod_env_paths.extend(self._expand_module_search_path(sp, recursive, fake=fake)) - - if mod_env_paths: - mod_env_paths = nub(mod_env_paths) # remove duplicates - mod_lines.append(self.module_generator.prepend_paths(env_var, mod_env_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('') From dc09eeeb7716af644e96992530f8bf7cf256fb19 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:13:33 +0200 Subject: [PATCH 11/59] consider both possible symplink states between lib and lib64 in expanding paths for environment of modules --- easybuild/framework/easyblock.py | 28 +++++++++++++++------------- test/framework/easyblock.py | 5 ++++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 5a27fae724..245f45e0e1 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1618,17 +1618,18 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def _expand_module_search_path(self, search_path, recursive, fake=False): + 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: - - Files must exist and directories be non-empty + - 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 'lib64' symlinked to 'lib' are discarded + - 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_search_path = os.path.join(self.installdir, search_path) - exp_search_paths = [abs_search_path] if search_path == "" else glob.glob(abs_search_path) + abs_glob = os.path.join(self.installdir, search_path) + exp_search_paths = [abs_glob] if search_path == "" else glob.glob(abs_glob) retained_search_paths = [] for abs_path in exp_search_paths: @@ -1636,16 +1637,17 @@ def _expand_module_search_path(self, search_path, recursive, fake=False): 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 if lib64 is just a symlink to lib - if (tentative_path + os.path.sep).startswith("lib64" + os.path.sep): - abs_lib_path = os.path.join(self.installdir, "lib") - abs_lib64_path = os.path.join(self.installdir, "lib64") - if os.path.islink(abs_lib64_path) and os.path.samefile(abs_lib_path, abs_lib64_path): - self.log.debug("Discarded search path to symlink lib64: %s", tentative_path) - break + # 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=recursive) and not fake: + 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 diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index bdf15504f4..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 @@ -437,6 +437,8 @@ def test_make_module_req(self): 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') @@ -501,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': From 562c98d08918bff0722c938b0127224a2f1e924a Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 00:17:23 +0200 Subject: [PATCH 12/59] fix code style around ModuleEnvironmentVariable --- easybuild/tools/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 5bef0e82bb..4efc78f696 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -132,6 +132,7 @@ _log = fancylogger.getLogger('modules', fname=False) + class ModuleEnvironmentVariable: """Environment variable data structure for modules""" def __init__(self, paths, empty=False, top_level_file=False): From 17cc73f81772fe7b589615f72c25654260acfce8 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 01:04:10 +0200 Subject: [PATCH 13/59] add check_install_lib_symlink method to EasyBlock to be able to trigger explicit checks in the module step --- easybuild/framework/easyblock.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 245f45e0e1..fcae1fc3bd 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1631,6 +1631,10 @@ def _expand_module_search_path(self, search_path, top_level, fake=False): 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 @@ -1655,6 +1659,17 @@ def _expand_module_search_path(self, search_path, top_level, fake=False): 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 common search path variables to be loaded by environment modules @@ -3074,13 +3089,8 @@ def post_install_step(self): # create *relative* 'lib' symlink to 'lib64'; symlink('lib64', lib_dir, use_abspath_source=False) - # check symlink state - 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 + # refresh symlink state + self.check_install_lib_symlink() self.run_post_install_commands() self.apply_post_install_patches() From 16e47436d11af407d503a50a3b122e829bbb0211 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 01:59:49 +0200 Subject: [PATCH 14/59] remove unused empty attribute from ModuleEnvironmentVariable --- easybuild/tools/modules.py | 3 +-- test/framework/modules.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 4efc78f696..582137f85c 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -135,9 +135,8 @@ class ModuleEnvironmentVariable: """Environment variable data structure for modules""" - def __init__(self, paths, empty=False, top_level_file=False): + def __init__(self, paths, top_level_file=False): self.paths = paths - self.empty = bool(empty) self.top_level_file = bool(top_level_file) def __str__(self): diff --git a/test/framework/modules.py b/test/framework/modules.py index 4437360359..8e7b1962e9 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1593,7 +1593,6 @@ def test_module_environment_variable(self): test_paths = ['lib', 'lib64'] mod_envar = mod.ModuleEnvironmentVariable(test_paths) self.assertTrue(hasattr(mod_envar, "paths")) - self.assertTrue(hasattr(mod_envar, "empty")) self.assertTrue(hasattr(mod_envar, "top_level_file")) self.assertEqual(mod_envar.paths, test_paths) self.assertEqual(str(mod_envar), "lib:lib64") From f68f383cdea1726f55f98585e1b2da987e36f992 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 1 Oct 2024 02:00:44 +0200 Subject: [PATCH 15/59] attributes in ModuleLoadEnvironment can only be instances of ModuleEnvironmentVariable --- easybuild/tools/modules.py | 70 +++++++++++++++++--------------------- test/framework/modules.py | 23 ++++++++----- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 582137f85c..a0bc842d41 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -182,49 +182,45 @@ def __init__(self): Paths are relative to root of installation directory """ - self.PATH = ModuleEnvironmentVariable( + self.PATH = ( SEARCH_PATH_BIN_DIRS + ['sbin'], - top_level_file=True, + {"top_level_file": True}, ) - self.LD_LIBRARY_PATH = ModuleEnvironmentVariable( + self.LD_LIBRARY_PATH = ( SEARCH_PATH_LIB_DIRS, - top_level_file=True, - ) - self.LIBRARY_PATH = ModuleEnvironmentVariable( - SEARCH_PATH_LIB_DIRS, - ) - self.CPATH = ModuleEnvironmentVariable( - SEARCH_PATH_HEADER_DIRS, - ) - self.MANPATH = ModuleEnvironmentVariable( - ['man', os.path.join('share', 'man')], - ) - self.PKG_CONFIG_PATH = ModuleEnvironmentVariable( - [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['share']], - ) - self.ACLOCAL_PATH = ModuleEnvironmentVariable( - [os.path.join('share', 'aclocal')], - ) - self.CLASSPATH = ModuleEnvironmentVariable( - ['*.jar'], - ) - self.XDG_DATA_DIRS = ModuleEnvironmentVariable( - ['share'], - ) - self.GI_TYPELIB_PATH = ModuleEnvironmentVariable( - [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] - ) - self.CMAKE_PREFIX_PATH = ModuleEnvironmentVariable( - [''], + {"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 = ModuleEnvironmentVariable( - ['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 (envar for envar in self.__dict__ if isinstance(getattr(self, envar), ModuleEnvironmentVariable)) + yield from self.__dict__ def items(self): """ @@ -233,9 +229,7 @@ def items(self): - value = its "paths" attribute """ for attr in self.__dict__: - envar = getattr(self, attr) - if isinstance(envar, ModuleEnvironmentVariable): - yield attr, envar.paths + yield attr, getattr(self, attr).paths @property def environ(self): diff --git a/test/framework/modules.py b/test/framework/modules.py index 8e7b1962e9..bfbb6666a2 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1616,24 +1616,31 @@ def test_module_environment_variable(self): def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" + test_paths = ['lib', 'lib64'] mod_load_env = mod.ModuleLoadEnvironment() - mod_load_env.TEST_VAR = mod.ModuleEnvironmentVariable(["lib", "lib64"]) + mod_load_env.TEST_VAR = test_paths self.assertTrue(hasattr(mod_load_env, "TEST_VAR")) - self.assertEqual(mod_load_env.TEST_VAR.paths, ["lib", "lib64"]) + self.assertEqual(mod_load_env.TEST_VAR.paths, test_paths) - # copy current state as reference ref_load_env = mod_load_env.__dict__.copy() - # and add some garbage - mod_load_env.garbage = "not_an_env_var" - self.assertTrue(hasattr(mod_load_env, "garbage")) - self.assertEqual(mod_load_env.garbage, "not_an_env_var") - 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 """ From 36d4f9274ffd429dfd935be3d1b35a3767ec4ff2 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Mon, 7 Oct 2024 10:18:54 +0200 Subject: [PATCH 16/59] add test to verify that environment variables don't leak into module file of subsequent installations --- .../sandbox/easybuild/easyblocks/t/toy.py | 7 ++++ test/framework/toy_build.py | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+) 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 """ From d560ce20e22c7750934127d9ad5ed35c6a2e643d Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Wed, 6 Nov 2024 09:36:53 +0100 Subject: [PATCH 17/59] convert ModuleLoadEnvironment to regular class instead of singleton --- easybuild/tools/modules.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 16212f9fd5..e3868807af 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -43,12 +43,11 @@ 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 Singleton, build_option, get_modules_tool, install_path +from easybuild.tools.config import 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 @@ -169,11 +168,7 @@ def prepend(self, item): self.paths.insert(0, item) -# singleton metaclass: only one instance is created -BaseModuleEnvironment = create_base_metaclass('BaseModuleEnvironment', Singleton, object) - - -class ModuleLoadEnvironment(BaseModuleEnvironment): +class ModuleLoadEnvironment: """Environment set by modules on load""" def __init__(self): From 9a230cf27967228091dbac34a60c8ced732200ff Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 6 Nov 2024 13:52:06 +0100 Subject: [PATCH 18/59] enable raising of errors when running toy build in test_toy_multiple_ecs_module --- test/framework/toy_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index 6385880fef..c5df316bab 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -4345,7 +4345,7 @@ def test_toy_multiple_ecs_module(self): 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]) + self.run_test_toy_build_with_output(ec_file=test_toy_ec, extra_args=[test_toy_app_ec], raise_error=True) 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) From f43ae679517e3b8f342c056406c4f2f493617321 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Thu, 7 Nov 2024 08:14:20 +0100 Subject: [PATCH 19/59] fix test_toy_multiple_ecs_module when using Tcl as module syntax --- test/framework/toy_build.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index c5df316bab..821999b30d 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -4347,12 +4347,18 @@ def test_toy_multiple_ecs_module(self): self.run_test_toy_build_with_output(ec_file=test_toy_ec, extra_args=[test_toy_app_ec], raise_error=True) - toy_modtxt = read_file(os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0.lua')) + toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0') + if get_module_syntax() == 'Lua': + toy_mod += '.lua' + toy_modtxt = read_file(toy_mod) 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')) + toy_app_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy-app', '0.0') + if get_module_syntax() == 'Lua': + toy_app_mod += '.lua' + toy_app_modtxt = read_file(toy_app_mod) self.assertFalse(regex.search(toy_app_modtxt), f"Pattern '{regex.pattern}' should *not* be found in: {toy_app_modtxt}") From fad0926164bdb03284be440e3df69c1928711576 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Wed, 13 Nov 2024 11:44:11 +0100 Subject: [PATCH 20/59] fix deprecation warning about make_module_req_guess --- easybuild/framework/easyblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 9a8652c740..0b78bb81b8 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1638,8 +1638,8 @@ def make_module_req(self, fake=False): 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.", + self.log.deprecated( + "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment instead.", "6.0", ) env_var_requirements = self.make_module_req_guess() From 5f04fc34b7c049f165bacd55b9cd71d36c4941ea Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Wed, 13 Nov 2024 13:39:47 +0100 Subject: [PATCH 21/59] make ModuleEnvironmentVariable iterable --- easybuild/tools/modules.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index e3868807af..65273db7d1 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -141,6 +141,9 @@ def __init__(self, paths, top_level_file=False): def __str__(self): return ":".join(self.paths) + def __iter__(self): + return iter(self.paths) + @property def paths(self): return self._paths @@ -207,9 +210,9 @@ def __setattr__(self, name, value): (paths, kwargs) = value except ValueError: paths, kwargs = value, {} - else: - if not isinstance(kwargs, dict): - paths, kwargs = value, {} + + if not isinstance(kwargs, dict): + paths, kwargs = value, {} return super().__setattr__(name.upper(), ModuleEnvironmentVariable(paths, **kwargs)) From a45783e0fa86c92d713dc071ba3f73d893e030ad Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 19 Nov 2024 01:40:59 +0100 Subject: [PATCH 22/59] ModuleEnvironmentVariable repr corresponds to its list of paths --- easybuild/tools/modules.py | 3 +++ test/framework/modules.py | 1 + 2 files changed, 4 insertions(+) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 65273db7d1..8d5a2bf421 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -138,6 +138,9 @@ def __init__(self, paths, top_level_file=False): self.paths = paths self.top_level_file = bool(top_level_file) + def __repr__(self): + return repr(self.paths) + def __str__(self): return ":".join(self.paths) diff --git a/test/framework/modules.py b/test/framework/modules.py index bfbb6666a2..e89521c908 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1595,6 +1595,7 @@ def test_module_environment_variable(self): self.assertTrue(hasattr(mod_envar, "paths")) self.assertTrue(hasattr(mod_envar, "top_level_file")) self.assertEqual(mod_envar.paths, test_paths) + self.assertEqual(repr(mod_envar), repr(test_paths)) self.assertEqual(str(mod_envar), "lib:lib64") mod_envar.paths = [] From b330d84a53f4a58b40dc9f542f44c149d293ed0c Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Tue, 19 Nov 2024 01:41:31 +0100 Subject: [PATCH 23/59] add update method to ModuleEnvironmentVariable to replace its list of paths --- easybuild/tools/modules.py | 3 +++ test/framework/modules.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 8d5a2bf421..031c5154c6 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -173,6 +173,9 @@ def prepend(self, item): """Shortcut to append to list of paths""" self.paths.insert(0, item) + def update(self, item): + """Shortcut to update list of paths""" + self.paths = item class ModuleLoadEnvironment: """Environment set by modules on load""" diff --git a/test/framework/modules.py b/test/framework/modules.py index e89521c908..b74183001d 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1614,6 +1614,10 @@ def test_module_environment_variable(self): self.assertEqual(mod_envar.paths, ["include", "share", "lib", "lib64"]) mod_envar.prepend("bin") self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib", "lib64"]) + mod_envar.update("new_path") + self.assertEqual(mod_envar.paths, ["new_path"]) + mod_envar.update(["new_path_1", "new_path_2"]) + self.assertEqual(mod_envar.paths, ["new_path_1", "new_path_2"]) def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" From c67afd4ae1cf83764cf4da7dbab9620aed98235a Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 21 Nov 2024 12:33:10 +0100 Subject: [PATCH 24/59] add logging facility to ModuleEnvironmentVariable --- easybuild/tools/modules.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 031c5154c6..266b1ff35d 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -134,10 +134,17 @@ class ModuleEnvironmentVariable: """Environment variable data structure for modules""" + def __init__(self, paths, top_level_file=False): + """ + Initialize new environment variable + Actual contents of the variable are held in "self.contents" + """ self.paths = paths self.top_level_file = bool(top_level_file) + self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) + def __repr__(self): return repr(self.paths) @@ -185,7 +192,6 @@ 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}, From 6048010c2ab42cbe0b05be6617ac538435e9fb83 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 21 Nov 2024 12:33:34 +0100 Subject: [PATCH 25/59] add remove method to ModuleEnvironmentVariable --- easybuild/tools/modules.py | 12 ++++++++++-- test/framework/modules.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 266b1ff35d..898325e89b 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -177,13 +177,21 @@ def extend(self, *args): self.paths.extend(*args) def prepend(self, item): - """Shortcut to append to list of paths""" + """Shortcut to prepend item to list of paths""" self.paths.insert(0, item) def update(self, item): - """Shortcut to update list of paths""" + """Shortcut to replace list of paths with item""" self.paths = item + def remove(self, *args): + """Shortcut to remove items from list of paths""" + try: + self.paths.remove(*args) + except ValueError: + # item is not in the list, move along + self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") + class ModuleLoadEnvironment: """Environment set by modules on load""" diff --git a/test/framework/modules.py b/test/framework/modules.py index b74183001d..4e827015ba 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1610,10 +1610,21 @@ def test_module_environment_variable(self): mod_envar.append("share") self.assertEqual(mod_envar.paths, ["include", "share"]) + self.assertRaises(TypeError, mod_envar.append, "arg1" , "arg2") + mod_envar.extend(test_paths) self.assertEqual(mod_envar.paths, ["include", "share", "lib", "lib64"]) + self.assertRaises(TypeError, mod_envar.append, ["list1"], ["list2"]) + mod_envar.prepend("bin") self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib", "lib64"]) + + mod_envar.remove("lib") + self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib64"]) + mod_envar.remove("nonexistent") + self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib64"]) + self.assertRaises(TypeError, mod_envar.remove, "arg1", "arg2") + mod_envar.update("new_path") self.assertEqual(mod_envar.paths, ["new_path"]) mod_envar.update(["new_path_1", "new_path_2"]) From de3fa38c559b50b0e74a89ab7d13f9d8737ffb08 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 21 Nov 2024 12:43:09 +0100 Subject: [PATCH 26/59] rename ModuleEnvironmentVariable.paths to ModuleEnvironmentVariable.contents --- easybuild/tools/modules.py | 69 ++++++++++++++++++++------------------ test/framework/modules.py | 54 ++++++++++++++--------------- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 898325e89b..b247435aa1 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -133,61 +133,64 @@ class ModuleEnvironmentVariable: - """Environment variable data structure for modules""" + """ + Environment variable data structure for modules + Contents of environment variable is a list of unique strings + """ - def __init__(self, paths, top_level_file=False): + def __init__(self, contents, top_level_file=False): """ Initialize new environment variable - Actual contents of the variable are held in "self.contents" + Actual contents of the environment variable are held in self.contents """ - self.paths = paths + self.contents = contents self.top_level_file = bool(top_level_file) self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) def __repr__(self): - return repr(self.paths) + return repr(self.contents) def __str__(self): - return ":".join(self.paths) + return ":".join(self.contents) def __iter__(self): - return iter(self.paths) + return iter(self.contents) @property - def paths(self): - return self._paths + def contents(self): + return self._contents - @paths.setter - def paths(self, value): - """Enforce that paths is a list of strings""" + @contents.setter + def contents(self, value): + """Enforce that contents is a list of strings""" if isinstance(value, str): value = [value] try: - self._paths = [str(path) for path in value] + self._contents = [str(path) for path in value] except TypeError: - raise TypeError("ModuleEnvironmentVariable.paths must be a list of strings") from None + raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from None def append(self, *args): - """Shortcut to append to list of paths""" - self.paths.append(*args) + """Shortcut to append to list of contents""" + self.contents.append(*args) def extend(self, *args): - """Shortcut to extend list of paths""" - self.paths.extend(*args) + """Shortcut to extend list of contents""" + self.contents.extend(*args) def prepend(self, item): - """Shortcut to prepend item to list of paths""" - self.paths.insert(0, item) + """Shortcut to prepend item to list of contents""" + self.contents.insert(0, item) def update(self, item): - """Shortcut to replace list of paths with item""" - self.paths = item + """Shortcut to replace list of contents with item""" + self.contents = item def remove(self, *args): - """Shortcut to remove items from list of paths""" + """Shortcut to remove items from list of contents""" try: - self.paths.remove(*args) + self.contents.remove(*args) except ValueError: # item is not in the list, move along self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") @@ -227,14 +230,14 @@ def __setattr__(self, name, value): - attributes are instances of ModuleEnvironmentVariable """ try: - (paths, kwargs) = value + (contents, kwargs) = value except ValueError: - paths, kwargs = value, {} + contents, kwargs = value, {} if not isinstance(kwargs, dict): - paths, kwargs = value, {} + contents, kwargs = value, {} - return super().__setattr__(name.upper(), ModuleEnvironmentVariable(paths, **kwargs)) + return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) def __iter__(self): """Make the class iterable""" @@ -244,20 +247,20 @@ def items(self): """ Return key-value pairs for each attribute that is a ModuleEnvironmentVariable - key = attribute name - - value = its "paths" attribute + - value = its "contents" attribute """ for attr in self.__dict__: - yield attr, getattr(self, attr).paths + yield attr, getattr(self, attr).contents @property def environ(self): """ - Return dict with mapping of ModuleEnvironmentVariables names with their paths + Return dict with mapping of ModuleEnvironmentVariables names with their contents Equivalent in shape to os.environ """ mapping = {} - for envar_name, envar_paths in self.items(): - mapping.update({envar_name: envar_paths}) + for envar_name, envar_contents in self.items(): + mapping.update({envar_name: envar_contents}) return mapping diff --git a/test/framework/modules.py b/test/framework/modules.py index 4e827015ba..596d4479b7 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1592,70 +1592,70 @@ 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, "contents")) self.assertTrue(hasattr(mod_envar, "top_level_file")) - self.assertEqual(mod_envar.paths, test_paths) + self.assertEqual(mod_envar.contents, test_paths) self.assertEqual(repr(mod_envar), repr(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.contents = [] + self.assertEqual(mod_envar.contents, []) + self.assertRaises(TypeError, setattr, mod_envar, "contents", None) - mod_envar.paths = (1, 2, 3) - self.assertEqual(mod_envar.paths, ["1", "2", "3"]) + mod_envar.contents = (1, 2, 3) + self.assertEqual(mod_envar.contents, ["1", "2", "3"]) - mod_envar.paths = "include" - self.assertEqual(mod_envar.paths, ["include"]) + mod_envar.contents = "include" + self.assertEqual(mod_envar.contents, ["include"]) mod_envar.append("share") - self.assertEqual(mod_envar.paths, ["include", "share"]) + self.assertEqual(mod_envar.contents, ["include", "share"]) self.assertRaises(TypeError, mod_envar.append, "arg1" , "arg2") mod_envar.extend(test_paths) - self.assertEqual(mod_envar.paths, ["include", "share", "lib", "lib64"]) + self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) self.assertRaises(TypeError, mod_envar.append, ["list1"], ["list2"]) mod_envar.prepend("bin") - self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib", "lib64"]) + self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib", "lib64"]) mod_envar.remove("lib") - self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib64"]) + self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib64"]) mod_envar.remove("nonexistent") - self.assertEqual(mod_envar.paths, ["bin", "include", "share", "lib64"]) + self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib64"]) self.assertRaises(TypeError, mod_envar.remove, "arg1", "arg2") mod_envar.update("new_path") - self.assertEqual(mod_envar.paths, ["new_path"]) + self.assertEqual(mod_envar.contents, ["new_path"]) mod_envar.update(["new_path_1", "new_path_2"]) - self.assertEqual(mod_envar.paths, ["new_path_1", "new_path_2"]) + self.assertEqual(mod_envar.contents, ["new_path_1", "new_path_2"]) def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" - test_paths = ['lib', 'lib64'] + test_contents = ['lib', 'lib64'] mod_load_env = mod.ModuleLoadEnvironment() - mod_load_env.TEST_VAR = test_paths + mod_load_env.TEST_VAR = test_contents self.assertTrue(hasattr(mod_load_env, "TEST_VAR")) - self.assertEqual(mod_load_env.TEST_VAR.paths, test_paths) + self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) 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()] + ref_load_env_item_list = [(key, value.contents) 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()} + ref_load_env_environ = {key: value.contents 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 + mod_load_env.test_lower = test_contents self.assertTrue(hasattr(mod_load_env, "TEST_LOWER")) - self.assertEqual(mod_load_env.TEST_LOWER.paths, test_paths) + self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) 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.assertEqual(mod_load_env.TEST_STR.contents, ["some/path"]) + mod_load_env.TEST_EXTRA = (test_contents, {"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.contents, test_contents) 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})) + self.assertRaises(TypeError, setattr, mod_load_env, "TEST_UNKNONW", (test_contents, {"unkown_param": True})) def suite(): From 0274faf16348e1072c2ffe0780fe10c75031a53d Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 21 Nov 2024 14:22:50 +0100 Subject: [PATCH 27/59] ensure ModuleEnvironmentVariable is a list of unique strings --- easybuild/tools/modules.py | 16 ++++++++++------ test/framework/modules.py | 25 +++++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index b247435aa1..9021619407 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -56,6 +56,7 @@ from easybuild.tools.systemtools import get_shared_lib_ext from easybuild.tools.utilities import get_subclasses, nub + # software root/version environment variable name prefixes ROOT_ENV_VAR_NAME_PREFIX = "EBROOT" VERSION_ENV_VAR_NAME_PREFIX = "EBVERSION" @@ -166,22 +167,25 @@ def contents(self, value): """Enforce that contents is a list of strings""" if isinstance(value, str): value = [value] + try: - self._contents = [str(path) for path in value] + str_list = [str(path) for path in value] except TypeError: raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from None - def append(self, *args): + self._contents = nub(str_list) # remove duplicates and keep order + + def append(self, item): """Shortcut to append to list of contents""" - self.contents.append(*args) + self.contents += [item] - def extend(self, *args): + def extend(self, item): """Shortcut to extend list of contents""" - self.contents.extend(*args) + self.contents += item def prepend(self, item): """Shortcut to prepend item to list of contents""" - self.contents.insert(0, item) + self.contents = [item] + self.contents def update(self, item): """Shortcut to replace list of contents with item""" diff --git a/test/framework/modules.py b/test/framework/modules.py index 596d4479b7..a5cf070f81 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1601,34 +1601,39 @@ def test_module_environment_variable(self): mod_envar.contents = [] self.assertEqual(mod_envar.contents, []) self.assertRaises(TypeError, setattr, mod_envar, "contents", None) - - mod_envar.contents = (1, 2, 3) - self.assertEqual(mod_envar.contents, ["1", "2", "3"]) - + mod_envar.contents = (1, 3, 2, 3) + self.assertEqual(mod_envar.contents, ["1", "3", "2"]) mod_envar.contents = "include" self.assertEqual(mod_envar.contents, ["include"]) + mod_envar.append("share") + self.assertEqual(mod_envar.contents, ["include", "share"]) mod_envar.append("share") self.assertEqual(mod_envar.contents, ["include", "share"]) self.assertRaises(TypeError, mod_envar.append, "arg1" , "arg2") mod_envar.extend(test_paths) self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) + mod_envar.extend(test_paths) + self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) + mod_envar.extend(test_paths + ["lib128"]) + self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64", "lib128"]) self.assertRaises(TypeError, mod_envar.append, ["list1"], ["list2"]) - mod_envar.prepend("bin") - self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib", "lib64"]) - - mod_envar.remove("lib") - self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib64"]) + mod_envar.remove("lib128") + self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) mod_envar.remove("nonexistent") - self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib64"]) + self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) self.assertRaises(TypeError, mod_envar.remove, "arg1", "arg2") + mod_envar.prepend("bin") + self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib", "lib64"]) + mod_envar.update("new_path") self.assertEqual(mod_envar.contents, ["new_path"]) mod_envar.update(["new_path_1", "new_path_2"]) self.assertEqual(mod_envar.contents, ["new_path_1", "new_path_2"]) + self.assertRaises(TypeError, mod_envar.update, "arg1", "arg2") def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" From 5b4372e2f8c50d4fe7c60ee4493caec6fbc8181e Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 00:15:49 +0100 Subject: [PATCH 28/59] make EasyBlock._expand_module_search_path a public method --- easybuild/framework/easyblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 0b78bb81b8..35328637ec 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1657,7 +1657,7 @@ def make_module_req(self, fake=False): 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)) + 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 @@ -1668,7 +1668,7 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def _expand_module_search_path(self, search_path, top_level, fake=False): + 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 From f20adb8de91d24109d818c7b94d5e57895eee91f Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 01:39:00 +0100 Subject: [PATCH 29/59] fix codestyle issues --- easybuild/tools/modules.py | 1 + test/framework/modules.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 9021619407..2d39c4ccdc 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -199,6 +199,7 @@ def remove(self, *args): # item is not in the list, move along self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") + class ModuleLoadEnvironment: """Environment set by modules on load""" diff --git a/test/framework/modules.py b/test/framework/modules.py index a5cf070f81..20150d03ff 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1610,7 +1610,7 @@ def test_module_environment_variable(self): self.assertEqual(mod_envar.contents, ["include", "share"]) mod_envar.append("share") self.assertEqual(mod_envar.contents, ["include", "share"]) - self.assertRaises(TypeError, mod_envar.append, "arg1" , "arg2") + self.assertRaises(TypeError, mod_envar.append, "arg1", "arg2") mod_envar.extend(test_paths) self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) From 03c6c46ee5417f54a8956000357e72e1d06dcd1f Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 09:19:13 +0100 Subject: [PATCH 30/59] replace make_module_req_guess with module_load_environment in Toy easyblock --- test/framework/sandbox/easybuild/easyblocks/t/toy.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/framework/sandbox/easybuild/easyblocks/t/toy.py b/test/framework/sandbox/easybuild/easyblocks/t/toy.py index 552b5d8a37..2957973d83 100644 --- a/test/framework/sandbox/easybuild/easyblocks/t/toy.py +++ b/test/framework/sandbox/easybuild/easyblocks/t/toy.py @@ -73,6 +73,10 @@ def __init__(self, *args, **kwargs): setvar('TOY', '%s-%s' % (self.name, self.version)) + # extra paths for environment variables to consider + if self.name == 'toy': + self.module_load_environment.CPATH.append('toy-headers') + def prepare_for_extensions(self): """ Prepare for installing toy extensions. @@ -192,10 +196,3 @@ 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 From d43acaedcf65c44464f5b3bc83bdcfe6c6926d44 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 09:47:49 +0100 Subject: [PATCH 31/59] ExtensionEasyBlock pulls module load environment from its master --- easybuild/framework/extensioneasyblock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/easybuild/framework/extensioneasyblock.py b/easybuild/framework/extensioneasyblock.py index dca2d587cc..9c836aa992 100644 --- a/easybuild/framework/extensioneasyblock.py +++ b/easybuild/framework/extensioneasyblock.py @@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs): self.installdir = self.master.installdir self.modules_tool = self.master.modules_tool self.module_generator = self.master.module_generator + self.module_load_environment = self.master.module_load_environment self.robot_path = self.master.robot_path self.is_extension = True self.unpack_options = None From c692e66b2de5ac8eae57be045f5356e786314f59 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 12:28:51 +0100 Subject: [PATCH 32/59] automatically enforce top level files for PATH and LD_LIBRARY_PATH in module load environment --- easybuild/tools/modules.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 2d39c4ccdc..84e1efc380 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -208,14 +208,8 @@ 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.PATH = SEARCH_PATH_BIN_DIRS + ['sbin'] + self.LD_LIBRARY_PATH = SEARCH_PATH_LIB_DIRS self.LIBRARY_PATH = SEARCH_PATH_LIB_DIRS self.CPATH = SEARCH_PATH_HEADER_DIRS self.MANPATH = ['man', os.path.join('share', 'man')] @@ -242,6 +236,10 @@ def __setattr__(self, name, value): if not isinstance(kwargs, dict): contents, kwargs = value, {} + # special variables that require top level files + if name in ["PATH", "LD_LIBRARY_PATH"]: + kwargs.update({"top_level_file": True}) + return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) def __iter__(self): From 561d89ef0a138699690ee9cbdd317e787fb7d31c Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 12:29:19 +0100 Subject: [PATCH 33/59] replace make_module_req_guess with module_load_environment in test_make_module_req --- test/framework/easyblock.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 912fd08961..084e4a6fdc 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -522,8 +522,18 @@ def test_make_module_req(self): self.assertEqual(len(re.findall(r'^prepend_path\("%s", pathJoin\(root, "lib"\)\)$' % var, guess, re.M)), 1) - # check for behavior when a string value is used as dict value by make_module_req_guesses - eb.make_module_req_guess = lambda: {'PATH': 'bin'} + # nuke default module load environment + default_mod_load_vars = [ + "PATH", "LD_LIBRARY_PATH", "LIBRARY_PATH", "CPATH", "MANPATH", "PKG_CONFIG_PATH", "ACLOCAL_PATH", + "CLASSPATH", "XDG_DATA_DIRS", "GI_TYPELIB_PATH", "CMAKE_PREFIX_PATH", "CMAKE_LIBRARY_PATH", + ] + for env_var in default_mod_load_vars: + delattr(eb.module_load_environment, env_var) + + self.assertEqual(len(vars(eb.module_load_environment)), 0) + + # check for behavior when a string value is used as value of module_load_environment + eb.module_load_environment.PATH = 'bin' with eb.module_generator.start_module_creation(): txt = eb.make_module_req() if get_module_syntax() == 'Tcl': @@ -535,7 +545,7 @@ def test_make_module_req(self): # check for correct behaviour if empty string is specified as one of the values # prepend-path statements should be included for both the 'bin' subdir and the install root - eb.make_module_req_guess = lambda: {'PATH': ['bin', '']} + eb.module_load_environment.PATH = ['bin', ''] with eb.module_generator.start_module_creation(): txt = eb.make_module_req() if get_module_syntax() == 'Tcl': @@ -548,7 +558,7 @@ def test_make_module_req(self): self.fail("Unknown module syntax: %s" % get_module_syntax()) # check for correct order of prepend statements when providing a list (and that no duplicates are allowed) - eb.make_module_req_guess = lambda: {'LD_LIBRARY_PATH': ['lib/pathC', 'lib/pathA', 'lib/pathB', 'lib/pathA']} + eb.module_load_environment.LD_LIBRARY_PATH = ['lib/pathC', 'lib/pathA', 'lib/pathB', 'lib/pathA'] for path in ['pathA', 'pathB', 'pathC']: os.mkdir(os.path.join(eb.installdir, 'lib', path)) write_file(os.path.join(eb.installdir, 'lib', path, 'libfoo.so'), 'test') @@ -576,7 +586,8 @@ def test_make_module_req(self): # If PATH or LD_LIBRARY_PATH contain only folders, do not add an entry sub_lib_path = os.path.join('lib', 'path_folders') sub_path_path = os.path.join('bin', 'path_folders') - eb.make_module_req_guess = lambda: {'LD_LIBRARY_PATH': sub_lib_path, 'PATH': sub_path_path} + eb.module_load_environment.LD_LIBRARY_PATH = sub_lib_path + eb.module_load_environment.PATH = sub_path_path for path in (sub_lib_path, sub_path_path): full_path = os.path.join(eb.installdir, path, 'subpath') os.makedirs(full_path) From a514a4231ad4c5f64348ca3a7d1f101e7443087a Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 22 Nov 2024 12:34:11 +0100 Subject: [PATCH 34/59] allow to customize delimiter in ModuleEnvironmentVariable --- easybuild/tools/modules.py | 5 +++-- test/framework/modules.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 84e1efc380..dfd7e8ce03 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -139,13 +139,14 @@ class ModuleEnvironmentVariable: Contents of environment variable is a list of unique strings """ - def __init__(self, contents, top_level_file=False): + def __init__(self, contents, top_level_file=False, delim=os.pathsep): """ Initialize new environment variable Actual contents of the environment variable are held in self.contents """ self.contents = contents self.top_level_file = bool(top_level_file) + self.delim = delim self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) @@ -153,7 +154,7 @@ def __repr__(self): return repr(self.contents) def __str__(self): - return ":".join(self.contents) + return self.delim.join(self.contents) def __iter__(self): return iter(self.contents) diff --git a/test/framework/modules.py b/test/framework/modules.py index 20150d03ff..31222e40d5 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1594,10 +1594,16 @@ def test_module_environment_variable(self): mod_envar = mod.ModuleEnvironmentVariable(test_paths) self.assertTrue(hasattr(mod_envar, "contents")) self.assertTrue(hasattr(mod_envar, "top_level_file")) + self.assertTrue(hasattr(mod_envar, "delim")) self.assertEqual(mod_envar.contents, test_paths) self.assertEqual(repr(mod_envar), repr(test_paths)) self.assertEqual(str(mod_envar), "lib:lib64") + mod_envar_custom_delim = mod.ModuleEnvironmentVariable(test_paths, delim="|") + self.assertEqual(mod_envar_custom_delim.contents, test_paths) + self.assertEqual(repr(mod_envar_custom_delim), repr(test_paths)) + self.assertEqual(str(mod_envar_custom_delim), "lib|lib64") + mod_envar.contents = [] self.assertEqual(mod_envar.contents, []) self.assertRaises(TypeError, setattr, mod_envar, "contents", None) From 7f96240823599ad94df6cc1fca5943d56d6af9d9 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Sat, 7 Dec 2024 15:32:36 +0100 Subject: [PATCH 35/59] rework LibSymlink to make it less confusing --- easybuild/framework/easyblock.py | 22 ++++++++++++++-------- test/framework/easyblock.py | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 35328637ec..a48ad7dd8a 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -124,8 +124,14 @@ class LibSymlink(Enum): - """Possible states for symlinking of library directories""" - NONE, LIB, LIB64, NEITHER = range(0, 4) + """ + Possible states for symlinking of lib/lib64 subdirectories: + - UNKNOWN: has not been determined yet + - LIB_TO_LIB64: 'lib' is a symlink to 'lib64' + - LIB64_TO_LIB: 'lib64' is a symlink to 'lib' + - NEITHER: neither 'lib' is a symlink to 'lib64', nor 'lib64 is a symlink to 'lib' + - """ + UNKNOWN, LIB_TO_LIB64, LIB64_TO_LIB, NEITHER = range(0, 4) class EasyBlock(object): @@ -216,7 +222,7 @@ def __init__(self, ec): self.install_subdir = None # track status of symlink between library directories - self.install_lib_symlink = LibSymlink.NONE + self.install_lib_symlink = LibSymlink.UNKNOWN # indicates whether build should be performed in installation dir self.build_in_installdir = self.cfg['buildininstalldir'] @@ -1682,7 +1688,7 @@ def expand_module_search_path(self, search_path, top_level, fake=False): 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: + if self.install_lib_symlink == LibSymlink.UNKNOWN: self.check_install_lib_symlink() retained_search_paths = [] @@ -1693,10 +1699,10 @@ def expand_module_search_path(self, search_path, top_level, fake=False): # 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): + if self.install_lib_symlink == LibSymlink.LIB64_TO_LIB 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): + if self.install_lib_symlink == LibSymlink.LIB_TO_LIB64 and tentative_sep.startswith('lib' + os.path.sep): self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) break @@ -1716,9 +1722,9 @@ def check_install_lib_symlink(self): 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 + self.install_lib_symlink = LibSymlink.LIB_TO_LIB64 elif os.path.islink(lib64_dir) and os.path.samefile(lib_dir, lib64_dir): - self.install_lib_symlink = LibSymlink.LIB64 + self.install_lib_symlink = LibSymlink.LIB64_TO_LIB def make_module_req_guess(self): """ diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 084e4a6fdc..70c95b1809 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 LibSymlink, EasyBlock, get_easyblock_instance +from easybuild.framework.easyblock import EasyBlock, LibSymlink, 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 @@ -503,7 +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 + eb.install_lib_symlink = LibSymlink.LIB64_TO_LIB with eb.module_generator.start_module_creation(): guess = eb.make_module_req() if get_module_syntax() == 'Tcl': From d3fdd716e90ca5871ee96f346282386d362963a6 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 14:41:18 +0100 Subject: [PATCH 36/59] fix order of import statement from tools.config in easyblock.py --- easybuild/framework/easyblock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index a48ad7dd8a..5ea604612d 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -73,10 +73,10 @@ from easybuild.tools.build_details import get_build_stats from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, dry_run_msg, dry_run_warning, dry_run_set_dirs 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 CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES +from easybuild.tools.config import EASYBUILD_SOURCES_URL, EBPYTHONPREFIXES # noqa 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 PYTHONPATH, SEARCH_PATH_BIN_DIRS, SEARCH_PATH_LIB_DIRS 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 from easybuild.tools.environment import restore_env, sanitize_env From 084467ffe12540413e8caf67a99de9bf3637329f Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 14:44:31 +0100 Subject: [PATCH 37/59] make sure that install_lib_symlink always gets set when calling check_install_lib_symlink --- easybuild/framework/easyblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 5ea604612d..7a0f1b2e2f 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -129,7 +129,7 @@ class LibSymlink(Enum): - UNKNOWN: has not been determined yet - LIB_TO_LIB64: 'lib' is a symlink to 'lib64' - LIB64_TO_LIB: 'lib64' is a symlink to 'lib' - - NEITHER: neither 'lib' is a symlink to 'lib64', nor 'lib64 is a symlink to 'lib' + - NEITHER: neither 'lib' is a symlink to 'lib64', nor 'lib64' is a symlink to 'lib' - """ UNKNOWN, LIB_TO_LIB64, LIB64_TO_LIB, NEITHER = range(0, 4) @@ -1717,10 +1717,10 @@ def expand_module_search_path(self, search_path, top_level, fake=False): def check_install_lib_symlink(self): """Check symlink state between library directories in installation prefix""" + self.install_lib_symlink = LibSymlink.NEITHER 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_TO_LIB64 elif os.path.islink(lib64_dir) and os.path.samefile(lib_dir, lib64_dir): From ef446e36664a54955353f728236a4f78e96c6f1d Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 14:51:30 +0100 Subject: [PATCH 38/59] rename 'top_level_file' attribute of ModuleEnvironmentVariable to 'requires_files' --- easybuild/framework/easyblock.py | 8 ++++---- easybuild/tools/modules.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 7a0f1b2e2f..698758c341 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1661,9 +1661,9 @@ def make_module_req(self, fake=False): mod_req_paths = search_paths else: mod_req_paths = [] - top_level = getattr(self.module_load_environment, env_var).top_level_file + requires_files = getattr(self.module_load_environment, env_var).requires_files for path in search_paths: - mod_req_paths.extend(self.expand_module_search_path(path, top_level, fake=fake)) + mod_req_paths.extend(self.expand_module_search_path(path, requires_files, fake=fake)) if mod_req_paths: mod_req_paths = nub(mod_req_paths) # remove duplicates @@ -1674,7 +1674,7 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def expand_module_search_path(self, search_path, top_level, fake=False): + def expand_module_search_path(self, search_path, requires_files, 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 @@ -1707,7 +1707,7 @@ def expand_module_search_path(self, search_path, top_level, fake=False): 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: + if os.path.isdir(abs_path) and not dir_contains_files(abs_path, recursive=not requires_files) and not fake: self.log.debug("Discarded search path to empty directory: %s", tentative_path) break diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index dfd7e8ce03..fd62f2ebe5 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -139,13 +139,13 @@ class ModuleEnvironmentVariable: Contents of environment variable is a list of unique strings """ - def __init__(self, contents, top_level_file=False, delim=os.pathsep): + def __init__(self, contents, requires_files=False, delim=os.pathsep): """ Initialize new environment variable Actual contents of the environment variable are held in self.contents """ self.contents = contents - self.top_level_file = bool(top_level_file) + self.requires_files = bool(requires_files) self.delim = delim self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) @@ -237,9 +237,9 @@ def __setattr__(self, name, value): if not isinstance(kwargs, dict): contents, kwargs = value, {} - # special variables that require top level files - if name in ["PATH", "LD_LIBRARY_PATH"]: - kwargs.update({"top_level_file": True}) + # special variables that files to be present in the specified paths + if name in ['PATH', 'LD_LIBRARY_PATH']: + kwargs.update({'requires_files': True}) return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) From f50639dd459fdfb7cb7f721bb21a086f1f7922b3 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 15:03:21 +0100 Subject: [PATCH 39/59] deprecate EasyBlock.make_module_req_guess method --- easybuild/framework/easyblock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 698758c341..96ea07f888 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1640,7 +1640,7 @@ def make_module_req(self, fake=False): # 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 + # No custom method in child Easyblock, so make_module_req_guess is the one defined by base EasyBlock class env_var_requirements = self.module_load_environment.environ else: # Custom deprecated method used by child EasyBlock @@ -1731,6 +1731,10 @@ def make_module_req_guess(self): 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 """ + self.log.deprecated( + "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment instead", + '6.0', + ) return self.module_load_environment.environ def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True): From b38ff58bbe5f27c8f1b34467bbcc0da8ab45b167 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 15:12:13 +0100 Subject: [PATCH 40/59] use 'continue' rather than 'break' in EasyBlock.expand_module_search_path to consider all paths and not give up on first symlink --- easybuild/framework/easyblock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 96ea07f888..6feef63e08 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1695,21 +1695,21 @@ def expand_module_search_path(self, search_path, requires_files, fake=False): 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 + 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_TO_LIB and tentative_sep.startswith('lib64' + os.path.sep): self.log.debug("Discarded search path to symlinked lib64 directory: %s", tentative_path) - break + continue if self.install_lib_symlink == LibSymlink.LIB_TO_LIB64 and tentative_sep.startswith('lib' + os.path.sep): self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) - break + continue # 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 requires_files) and not fake: self.log.debug("Discarded search path to empty directory: %s", tentative_path) - break + continue retained_search_paths.append(tentative_path) @@ -3160,7 +3160,7 @@ def post_install_step(self): # create *relative* 'lib' symlink to 'lib64'; symlink('lib64', lib_dir, use_abspath_source=False) - # refresh symlink state + # refresh symlink state in install_lib_symlink class variable self.check_install_lib_symlink() self.run_post_install_commands() From 7972f2e1d9f1abf59a667a98e687e4aabcc1196c Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 11 Dec 2024 15:22:49 +0100 Subject: [PATCH 41/59] minor style changes --- easybuild/tools/config.py | 6 +-- easybuild/tools/modules.py | 24 +++++----- test/framework/easyblock.py | 4 +- test/framework/modules.py | 88 ++++++++++++++++++------------------- 4 files changed, 60 insertions(+), 62 deletions(-) diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py index ab2ddbe141..b63e864c59 100644 --- a/easybuild/tools/config.py +++ b/easybuild/tools/config.py @@ -174,9 +174,9 @@ 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"] +SEARCH_PATH_BIN_DIRS = ['bin'] +SEARCH_PATH_HEADER_DIRS = ['include'] +SEARCH_PATH_LIB_DIRS = ['lib', 'lib64'] PYTHONPATH = 'PYTHONPATH' EBPYTHONPREFIXES = 'EBPYTHONPREFIXES' diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index fd62f2ebe5..cc8f4fdf3c 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -45,10 +45,9 @@ from easybuild.base import fancylogger 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 ERROR, EBROOT_ENV_VAR_ACTIONS, IGNORE, LOADED_MODULES_ACTIONS, PURGE +from easybuild.tools.config import SEARCH_PATH_BIN_DIRS, SEARCH_PATH_HEADER_DIRS, SEARCH_PATH_LIB_DIRS, UNLOAD, UNSET from easybuild.tools.config import 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 @@ -209,19 +208,19 @@ def __init__(self): Initialize default environment definition Paths are relative to root of installation directory """ - self.PATH = SEARCH_PATH_BIN_DIRS + ['sbin'] + self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')] + self.CLASSPATH = ['*.jar'] + # only needed for installations whith standalone lib64 + self.CMAKE_LIBRARY_PATH = ['lib64'] + self.CMAKE_PREFIX_PATH = [''] + self.CPATH = SEARCH_PATH_HEADER_DIRS + self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] self.LD_LIBRARY_PATH = SEARCH_PATH_LIB_DIRS self.LIBRARY_PATH = SEARCH_PATH_LIB_DIRS - self.CPATH = SEARCH_PATH_HEADER_DIRS self.MANPATH = ['man', os.path.join('share', 'man')] + self.PATH = SEARCH_PATH_BIN_DIRS + ['sbin'] 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): """ @@ -238,7 +237,7 @@ def __setattr__(self, name, value): contents, kwargs = value, {} # special variables that files to be present in the specified paths - if name in ['PATH', 'LD_LIBRARY_PATH']: + if name in ('LD_LIBRARY_PATH', 'PATH'): kwargs.update({'requires_files': True}) return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) @@ -260,7 +259,6 @@ def items(self): def environ(self): """ Return dict with mapping of ModuleEnvironmentVariables names with their contents - Equivalent in shape to os.environ """ mapping = {} for envar_name, envar_contents in self.items(): diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 70c95b1809..ebd2e25ceb 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -524,8 +524,8 @@ def test_make_module_req(self): # nuke default module load environment default_mod_load_vars = [ - "PATH", "LD_LIBRARY_PATH", "LIBRARY_PATH", "CPATH", "MANPATH", "PKG_CONFIG_PATH", "ACLOCAL_PATH", - "CLASSPATH", "XDG_DATA_DIRS", "GI_TYPELIB_PATH", "CMAKE_PREFIX_PATH", "CMAKE_LIBRARY_PATH", + 'ACLOCAL_PATH', 'CLASSPATH', 'CMAKE_PREFIX_PATH', 'CMAKE_LIBRARY_PATH', 'CPATH', 'GI_TYPELIB_PATH', + 'LD_LIBRARY_PATH', 'LIBRARY_PATH', 'MANPATH', 'PATH', 'PKG_CONFIG_PATH', 'XDG_DATA_DIRS', ] for env_var in default_mod_load_vars: delattr(eb.module_load_environment, env_var) diff --git a/test/framework/modules.py b/test/framework/modules.py index 31222e40d5..6aaafeb480 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1592,61 +1592,61 @@ 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, "contents")) - self.assertTrue(hasattr(mod_envar, "top_level_file")) - self.assertTrue(hasattr(mod_envar, "delim")) + self.assertTrue(hasattr(mod_envar, 'contents')) + self.assertTrue(hasattr(mod_envar, 'requires_files')) + self.assertTrue(hasattr(mod_envar, 'delim')) self.assertEqual(mod_envar.contents, test_paths) self.assertEqual(repr(mod_envar), repr(test_paths)) - self.assertEqual(str(mod_envar), "lib:lib64") + self.assertEqual(str(mod_envar), 'lib:lib64') - mod_envar_custom_delim = mod.ModuleEnvironmentVariable(test_paths, delim="|") + mod_envar_custom_delim = mod.ModuleEnvironmentVariable(test_paths, delim='|') self.assertEqual(mod_envar_custom_delim.contents, test_paths) self.assertEqual(repr(mod_envar_custom_delim), repr(test_paths)) - self.assertEqual(str(mod_envar_custom_delim), "lib|lib64") + self.assertEqual(str(mod_envar_custom_delim), 'lib|lib64') mod_envar.contents = [] self.assertEqual(mod_envar.contents, []) - self.assertRaises(TypeError, setattr, mod_envar, "contents", None) + self.assertRaises(TypeError, setattr, mod_envar, 'contents', None) mod_envar.contents = (1, 3, 2, 3) - self.assertEqual(mod_envar.contents, ["1", "3", "2"]) - mod_envar.contents = "include" - self.assertEqual(mod_envar.contents, ["include"]) + self.assertEqual(mod_envar.contents, ['1', '3', '2']) + mod_envar.contents = 'include' + self.assertEqual(mod_envar.contents, ['include']) - mod_envar.append("share") - self.assertEqual(mod_envar.contents, ["include", "share"]) - mod_envar.append("share") - self.assertEqual(mod_envar.contents, ["include", "share"]) - self.assertRaises(TypeError, mod_envar.append, "arg1", "arg2") + mod_envar.append('share') + self.assertEqual(mod_envar.contents, ['include', 'share']) + mod_envar.append('share') + self.assertEqual(mod_envar.contents, ['include', 'share']) + self.assertRaises(TypeError, mod_envar.append, 'arg1', 'arg2') mod_envar.extend(test_paths) - self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) + self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64']) mod_envar.extend(test_paths) - self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) - mod_envar.extend(test_paths + ["lib128"]) - self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64", "lib128"]) - self.assertRaises(TypeError, mod_envar.append, ["list1"], ["list2"]) - - mod_envar.remove("lib128") - self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) - mod_envar.remove("nonexistent") - self.assertEqual(mod_envar.contents, ["include", "share", "lib", "lib64"]) - self.assertRaises(TypeError, mod_envar.remove, "arg1", "arg2") - - mod_envar.prepend("bin") - self.assertEqual(mod_envar.contents, ["bin", "include", "share", "lib", "lib64"]) - - mod_envar.update("new_path") - self.assertEqual(mod_envar.contents, ["new_path"]) - mod_envar.update(["new_path_1", "new_path_2"]) - self.assertEqual(mod_envar.contents, ["new_path_1", "new_path_2"]) - self.assertRaises(TypeError, mod_envar.update, "arg1", "arg2") + self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64']) + mod_envar.extend(test_paths + ['lib128']) + self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64', 'lib128']) + self.assertRaises(TypeError, mod_envar.append, ['list1'], ['list2']) + + mod_envar.remove('lib128') + self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64']) + mod_envar.remove('nonexistent') + self.assertEqual(mod_envar.contents, ['include', 'share', 'lib', 'lib64']) + self.assertRaises(TypeError, mod_envar.remove, 'arg1', 'arg2') + + mod_envar.prepend('bin') + self.assertEqual(mod_envar.contents, ['bin', 'include', 'share', 'lib', 'lib64']) + + mod_envar.update('new_path') + self.assertEqual(mod_envar.contents, ['new_path']) + mod_envar.update(['new_path_1', 'new_path_2']) + self.assertEqual(mod_envar.contents, ['new_path_1', 'new_path_2']) + self.assertRaises(TypeError, mod_envar.update, 'arg1', 'arg2') def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" test_contents = ['lib', 'lib64'] mod_load_env = mod.ModuleLoadEnvironment() mod_load_env.TEST_VAR = test_contents - self.assertTrue(hasattr(mod_load_env, "TEST_VAR")) + self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) ref_load_env = mod_load_env.__dict__.copy() @@ -1657,16 +1657,16 @@ def test_module_load_environment(self): self.assertDictEqual(mod_load_env.environ, ref_load_env_environ) mod_load_env.test_lower = test_contents - self.assertTrue(hasattr(mod_load_env, "TEST_LOWER")) + self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) - mod_load_env.TEST_STR = "some/path" - self.assertTrue(hasattr(mod_load_env, "TEST_STR")) - self.assertEqual(mod_load_env.TEST_STR.contents, ["some/path"]) - mod_load_env.TEST_EXTRA = (test_contents, {"top_level_file": True}) - self.assertTrue(hasattr(mod_load_env, "TEST_EXTRA")) + mod_load_env.TEST_STR = 'some/path' + self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) + self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) + mod_load_env.TEST_EXTRA = (test_contents, {'requires_files': True}) + self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) - self.assertEqual(mod_load_env.TEST_EXTRA.top_level_file, True) - self.assertRaises(TypeError, setattr, mod_load_env, "TEST_UNKNONW", (test_contents, {"unkown_param": True})) + self.assertEqual(mod_load_env.TEST_EXTRA.requires_files, True) + self.assertRaises(TypeError, setattr, mod_load_env, 'TEST_UNKNONW', (test_contents, {'unkown_param': True})) def suite(): From 9eaed27483cc4d79b28935ae106271c7e6ef9632 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 14:06:27 +0100 Subject: [PATCH 42/59] add test_expand_module_search_path to easyblock unit tests --- easybuild/framework/easyblock.py | 5 +- test/framework/easyblock.py | 100 ++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 6feef63e08..6b8ed59a76 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1717,14 +1717,17 @@ def expand_module_search_path(self, search_path, requires_files, fake=False): def check_install_lib_symlink(self): """Check symlink state between library directories in installation prefix""" - self.install_lib_symlink = LibSymlink.NEITHER lib_dir = os.path.join(self.installdir, 'lib') lib64_dir = os.path.join(self.installdir, 'lib64') + + self.install_lib_symlink = LibSymlink.UNKNONWN if os.path.exists(lib_dir) and os.path.exists(lib64_dir): if os.path.islink(lib_dir) and os.path.samefile(lib_dir, lib64_dir): self.install_lib_symlink = LibSymlink.LIB_TO_LIB64 elif os.path.islink(lib64_dir) and os.path.samefile(lib_dir, lib64_dir): self.install_lib_symlink = LibSymlink.LIB64_TO_LIB + else: + self.install_lib_symlink = LibSymlink.NEITHER def make_module_req_guess(self): """ diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index ebd2e25ceb..f6debcd231 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -434,9 +434,8 @@ def test_make_module_req(self): # create fake directories and files that should be guessed os.makedirs(eb.installdir) 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)) + path_components = (path, ) if isinstance(path, str) else path + os.mkdir(os.path.join(eb.installdir, *path_components)) eb.install_lib_symlink = LibSymlink.NEITHER write_file(os.path.join(eb.installdir, 'foo.jar'), 'foo.jar') @@ -2989,6 +2988,101 @@ def run_sanity_check_step(sanity_check_paths, enhance_sanity_check): run_sanity_check_step({}, False) run_sanity_check_step({}, True) + def test_expand_module_search_path(self): + """Testcase for expand_module_search_path""" + top_dir = os.path.abspath(os.path.dirname(__file__)) + toy_ec = os.path.join(top_dir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb') + eb = EasyBlock(EasyConfig(toy_ec)) + eb.installdir = config.install_path() + + # create test directories and files + os.makedirs(eb.installdir) + test_directories = ( + 'empty_dir', + 'dir_with_empty_subdir', + ('dir_with_empty_subdir', 'empty_subdir'), + 'dir_with_file', + 'dir_with_full_subdirs', + ('dir_with_full_subdirs', 'subdir1'), + ('dir_with_full_subdirs', 'subdir2'), + ) + for path in test_directories: + path_components = (path, ) if isinstance(path, str) else path + os.mkdir(os.path.join(eb.installdir, *path_components)) + + write_file(os.path.join(eb.installdir, 'dir_with_file', 'file.txt'), 'test file') + write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir1', 'file11.txt'), 'test file 1.1') + write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2') + write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1') + + eb.install_lib_symlink = LibSymlink.NONE + + self.assertEqual(eb.expand_module_search_path("nonexistent", True), []) + self.assertEqual(eb.expand_module_search_path("nonexistent", False), []) + self.assertEqual(eb.expand_module_search_path("empty_dir", True), []) + self.assertEqual(eb.expand_module_search_path("empty_dir", False), []) + self.assertEqual(eb.expand_module_search_path("dir_with_empty_subdir", True), []) + self.assertEqual(eb.expand_module_search_path("dir_with_empty_subdir", False), []) + self.assertEqual(eb.expand_module_search_path("dir_with_file", True), ["dir_with_file"]) + self.assertEqual(eb.expand_module_search_path("dir_with_file", False), ["dir_with_file"]) + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs", True), []) + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs", False), ["dir_with_full_subdirs"]) + # test globs + ref_expanded_paths = ["dir_with_full_subdirs/subdir1", "dir_with_full_subdirs/subdir2"] + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/*", True), ref_expanded_paths) + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/*", False), ref_expanded_paths) + ref_expanded_paths = ["dir_with_full_subdirs/subdir2/file21.txt"] + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/subdir2/*", True), ref_expanded_paths) + self.assertEqual(eb.expand_module_search_path("nonexistent/*", True), []) + self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/nonexistent/*", True), []) + # state of install_lib_symlink should not have changed + self.assertEqual(eb.install_lib_symlink, LibSymlink.NONE) + + # test both lib and lib64 directories + os.mkdir(os.path.join(eb.installdir, "lib")) + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.NONE) + self.assertEqual(eb.expand_module_search_path("lib", True), []) + self.assertEqual(eb.expand_module_search_path("lib", False), []) + write_file(os.path.join(eb.installdir, "lib", "libtest.so"), "not actually a lib") + self.assertEqual(eb.expand_module_search_path("lib", True), ["lib"]) + self.assertEqual(eb.expand_module_search_path("lib", False), ["lib"]) + + os.mkdir(os.path.join(eb.installdir, "lib64")) + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) + self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib"]) + self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib"]) + write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib") + self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib", "lib64"]) + self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib", "lib64"]) + + # test lib64 symlinked to lib + remove_dir(os.path.join(eb.installdir, "lib64")) + os.symlink("lib", os.path.join(eb.installdir, "lib64")) + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB64) + self.assertEqual(eb.expand_module_search_path("lib", True), ["lib"]) + self.assertEqual(eb.expand_module_search_path("lib", False), ["lib"]) + self.assertEqual(eb.expand_module_search_path("lib64", True), []) + self.assertEqual(eb.expand_module_search_path("lib64", False), []) + self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib"]) + self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib"]) + + # test lib symlinked to lib64 + remove_dir(os.path.join(eb.installdir, "lib")) + remove_file(os.path.join(eb.installdir, "lib64")) + os.mkdir(os.path.join(eb.installdir, "lib64")) + write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib") + os.symlink("lib64", os.path.join(eb.installdir, "lib")) + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB) + self.assertEqual(eb.expand_module_search_path("lib", True), []) + self.assertEqual(eb.expand_module_search_path("lib", False), []) + self.assertEqual(eb.expand_module_search_path("lib64", True), ["lib64"]) + self.assertEqual(eb.expand_module_search_path("lib64", False), ["lib64"]) + self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib64"]) + self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib64"]) def suite(): """ return all the tests in this file """ From 55ae588ee473af658ed2f805ecb18bba2a93cce1 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 14:17:05 +0100 Subject: [PATCH 43/59] fix typo in EasyBlock.check_install_lib_symlink --- easybuild/framework/easyblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 8ac9947f7e..c4c31235e0 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1729,7 +1729,7 @@ def check_install_lib_symlink(self): lib_dir = os.path.join(self.installdir, 'lib') lib64_dir = os.path.join(self.installdir, 'lib64') - self.install_lib_symlink = LibSymlink.UNKNONWN + self.install_lib_symlink = LibSymlink.UNKNOWN if os.path.exists(lib_dir) and os.path.exists(lib64_dir): if os.path.islink(lib_dir) and os.path.samefile(lib_dir, lib64_dir): self.install_lib_symlink = LibSymlink.LIB_TO_LIB64 From f761d348fa23e009414e4e327f823fd99eb05f00 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 14:34:53 +0100 Subject: [PATCH 44/59] fix symlink checking in test_expand_module_search_path --- test/framework/easyblock.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 767b508b36..5a57b8ce66 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -3077,7 +3077,8 @@ def test_expand_module_search_path(self): write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2') write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1') - eb.install_lib_symlink = LibSymlink.NONE + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) self.assertEqual(eb.expand_module_search_path("nonexistent", True), []) self.assertEqual(eb.expand_module_search_path("nonexistent", False), []) @@ -3098,12 +3099,12 @@ def test_expand_module_search_path(self): self.assertEqual(eb.expand_module_search_path("nonexistent/*", True), []) self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/nonexistent/*", True), []) # state of install_lib_symlink should not have changed - self.assertEqual(eb.install_lib_symlink, LibSymlink.NONE) + self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) # test both lib and lib64 directories os.mkdir(os.path.join(eb.installdir, "lib")) eb.check_install_lib_symlink() - self.assertEqual(eb.install_lib_symlink, LibSymlink.NONE) + self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) self.assertEqual(eb.expand_module_search_path("lib", True), []) self.assertEqual(eb.expand_module_search_path("lib", False), []) write_file(os.path.join(eb.installdir, "lib", "libtest.so"), "not actually a lib") @@ -3123,7 +3124,7 @@ def test_expand_module_search_path(self): remove_dir(os.path.join(eb.installdir, "lib64")) os.symlink("lib", os.path.join(eb.installdir, "lib64")) eb.check_install_lib_symlink() - self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB64) + self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB64_TO_LIB) self.assertEqual(eb.expand_module_search_path("lib", True), ["lib"]) self.assertEqual(eb.expand_module_search_path("lib", False), ["lib"]) self.assertEqual(eb.expand_module_search_path("lib64", True), []) @@ -3138,7 +3139,7 @@ def test_expand_module_search_path(self): write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib") os.symlink("lib64", os.path.join(eb.installdir, "lib")) eb.check_install_lib_symlink() - self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB) + self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB_TO_LIB64) self.assertEqual(eb.expand_module_search_path("lib", True), []) self.assertEqual(eb.expand_module_search_path("lib", False), []) self.assertEqual(eb.expand_module_search_path("lib64", True), ["lib64"]) From 9297094a38986313fd610645eaaefc6607878fe3 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 14:55:15 +0100 Subject: [PATCH 45/59] ModuleLoadEnvironment.environ returns dict with string formatted values --- easybuild/tools/modules.py | 5 +++-- test/framework/modules.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 772d88dcf5..3cce6635e5 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -253,16 +253,17 @@ def items(self): - value = its "contents" attribute """ for attr in self.__dict__: - yield attr, getattr(self, attr).contents + yield attr, getattr(self, attr) @property def environ(self): """ Return dict with mapping of ModuleEnvironmentVariables names with their contents + Equivalent in shape to os.environ """ mapping = {} for envar_name, envar_contents in self.items(): - mapping.update({envar_name: envar_contents}) + mapping.update({envar_name: str(envar_contents)}) return mapping diff --git a/test/framework/modules.py b/test/framework/modules.py index b12ff5e036..050c232c5f 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1662,9 +1662,9 @@ def test_module_load_environment(self): 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.contents) for key, value in ref_load_env.items()] + ref_load_env_item_list = list(ref_load_env.items()) self.assertCountEqual(list(mod_load_env.items()), ref_load_env_item_list) - ref_load_env_environ = {key: value.contents for key, value in ref_load_env.items()} + ref_load_env_environ = {key: str(value) for key, value in ref_load_env.items()} self.assertDictEqual(mod_load_env.environ, ref_load_env_environ) mod_load_env.test_lower = test_contents From 87afba73a57d0cb1db3c61347ad52e3bdbc20680 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 15:34:06 +0100 Subject: [PATCH 46/59] add update and as_dict methods to ModuleLoadEnvironment class --- easybuild/tools/modules.py | 18 ++++++++++++++++-- test/framework/modules.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 3cce6635e5..bff746c66b 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -210,8 +210,7 @@ def __init__(self): """ self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')] self.CLASSPATH = ['*.jar'] - # only needed for installations whith standalone lib64 - self.CMAKE_LIBRARY_PATH = ['lib64'] + self.CMAKE_LIBRARY_PATH = ['lib64'] # only needed for installations whith standalone lib64 self.CMAKE_PREFIX_PATH = [''] self.CPATH = SEARCH_PATH_HEADER_DIRS self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] @@ -255,6 +254,21 @@ def items(self): for attr in self.__dict__: yield attr, getattr(self, attr) + def update(self, new_env): + """Update contents of environment from given dictionary""" + try: + for envar_name, envar_contents in new_env.items(): + setattr(self, envar_name, envar_contents) + except AttributeError: + raise EasyBuildError("Cannot update ModuleLoadEnvironment, new environment variables must be in a dict.") + + @property + def as_dict(self): + """ + Return dict with mapping of ModuleEnvironmentVariables names with their contents + """ + return dict(self.items()) + @property def environ(self): """ diff --git a/test/framework/modules.py b/test/framework/modules.py index 050c232c5f..3b87ddec4e 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1654,31 +1654,50 @@ def test_module_environment_variable(self): def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" + # test setting attributes test_contents = ['lib', 'lib64'] mod_load_env = mod.ModuleLoadEnvironment() mod_load_env.TEST_VAR = test_contents self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) - + mod_load_env.test_lower = test_contents + self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) + self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) + mod_load_env.TEST_STR = 'some/path' + self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) + self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) + mod_load_env.TEST_EXTRA = (test_contents, {'requires_files': True}) + self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) + self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) + self.assertEqual(mod_load_env.TEST_EXTRA.requires_files, True) + self.assertRaises(TypeError, setattr, mod_load_env, 'TEST_UNKNONW', (test_contents, {'unkown_param': True})) + # test retrieving environment ref_load_env = mod_load_env.__dict__.copy() self.assertCountEqual(list(mod_load_env), ref_load_env.keys()) ref_load_env_item_list = list(ref_load_env.items()) self.assertCountEqual(list(mod_load_env.items()), ref_load_env_item_list) + ref_load_env_item_list = dict(ref_load_env.items()) + self.assertCountEqual(mod_load_env.as_dict, ref_load_env_item_list) ref_load_env_environ = {key: str(value) for key, value in ref_load_env.items()} self.assertDictEqual(mod_load_env.environ, ref_load_env_environ) - - mod_load_env.test_lower = test_contents + # test updating environment + new_test_env = { + 'TEST_VAR': 'replaced_path', + 'TEST_NEW_VAR': ['new_path1', 'new_path2'], + } + mod_load_env.update(new_test_env) + self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) + self.assertEqual(mod_load_env.TEST_VAR.contents, ['replaced_path']) + self.assertTrue(hasattr(mod_load_env, 'TEST_NEW_VAR')) + self.assertEqual(mod_load_env.TEST_NEW_VAR.contents, ['new_path1', 'new_path2']) + self.assertEqual(mod_load_env.TEST_NEW_VAR.requires_files, False) + # check that existing variables still exist self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) - mod_load_env.TEST_STR = 'some/path' self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) - mod_load_env.TEST_EXTRA = (test_contents, {'requires_files': True}) self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) - self.assertEqual(mod_load_env.TEST_EXTRA.requires_files, True) - self.assertRaises(TypeError, setattr, mod_load_env, 'TEST_UNKNONW', (test_contents, {'unkown_param': True})) - def suite(): """ returns all the testcases in this module """ From 38774ac183f52f30f4960e07eeb8adf189353589 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 15:35:40 +0100 Subject: [PATCH 47/59] make_module_req updates module_load_environment if a deprecated make_module_req_guess is detected --- easybuild/framework/easyblock.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index c4c31235e0..50ce734619 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1647,23 +1647,16 @@ def make_module_req(self, fake=False): note += "for paths are skipped for the statements below due to dry run" mod_lines.append(self.module_generator.comment(note)) - # 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, so make_module_req_guess is the one defined by base EasyBlock class - env_var_requirements = self.module_load_environment.environ - else: - # Custom deprecated method used by child EasyBlock + if self.make_module_req_guess.__qualname__ != "EasyBlock.make_module_req_guess": + # Deprecated make_module_req_guess method used in child Easyblock + # Update environment with custom make_module_req_guess self.log.deprecated( "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment 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) - }) + self.module_load_environment.update(self.make_module_req_guess()) - for env_var, search_paths in sorted(env_var_requirements.items()): + for env_var, search_paths in sorted(self.module_load_environment.items()): if self.dry_run: self.dry_run_msg(f" ${env_var}:{', '.join(search_paths)}") # Don't expand globs or do any filtering for dry run @@ -1747,7 +1740,7 @@ def make_module_req_guess(self): "make_module_req_guess() is deprecated, use EasyBlock.module_load_environment instead", '6.0', ) - return self.module_load_environment.environ + return self.module_load_environment.as_dict def load_module(self, mod_paths=None, purge=True, extra_modules=None, verbose=True): """ From 5fb04671c178d970a7809315d4d306f9d72d4982 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 15:44:22 +0100 Subject: [PATCH 48/59] fix codestyle in test.framework.modules --- test/framework/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/framework/modules.py b/test/framework/modules.py index 3b87ddec4e..ab4bc3af59 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1699,6 +1699,7 @@ def test_module_load_environment(self): self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) + def suite(): """ returns all the testcases in this module """ return TestLoaderFiltered().loadTestsFromTestCase(ModulesTest, sys.argv[1:]) From 516d6e659df0482ea4ac75a4a8a379d71b2ebf60 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 23:28:17 +0100 Subject: [PATCH 49/59] improve control on ModuleEnvironmentVariable behaviour by adding type property based on new ModEnvVarType enum --- easybuild/framework/easyblock.py | 32 ++++++++++++------- easybuild/tools/modules.py | 54 +++++++++++++++++++++++++++----- test/framework/modules.py | 43 ++++++++++++++++++------- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 50ce734619..1721e7aef5 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -99,7 +99,7 @@ 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, ModuleLoadEnvironment +from easybuild.tools.modules import Lmod, ModEnvVarType, 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 @@ -1656,16 +1656,22 @@ def make_module_req(self, fake=False): ) self.module_load_environment.update(self.make_module_req_guess()) - for env_var, search_paths in sorted(self.module_load_environment.items()): + # Only inject path-like environment variables into module file + env_var_requirements = sorted([ + (envar_name, envar_val) for envar_name, envar_val in self.module_load_environment.items() + if envar_val.is_path + ]) + + for env_var, search_paths in env_var_requirements: if self.dry_run: 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: mod_req_paths = [] - requires_files = getattr(self.module_load_environment, env_var).requires_files + requires_top_files = search_paths.type == ModEnvVarType.PATH_WITH_TOP_FILES for path in search_paths: - mod_req_paths.extend(self.expand_module_search_path(path, requires_files, fake=fake)) + mod_req_paths.extend(self.expand_module_search_path(path, requires_top_files, fake=fake)) if mod_req_paths: mod_req_paths = nub(mod_req_paths) # remove duplicates @@ -1676,13 +1682,15 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def expand_module_search_path(self, search_path, requires_files, fake=False): + def expand_module_search_path(self, search_path, requires_top_files, 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 + - Paths must point to existing files/directories - Search paths to a 'lib64' symlinked to 'lib' are discarded to avoid duplicates + - Directories must contain at least one file in them (empty folders are ignored) + - requires_top_files: increases stricness to require files in top level directory + - fake: fake modules can set search paths to empty directories """ # 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) @@ -1708,10 +1716,12 @@ def expand_module_search_path(self, search_path, requires_files, fake=False): self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) continue - # 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 requires_files) and not fake: - self.log.debug("Discarded search path to empty directory: %s", tentative_path) - continue + if os.path.isdir(abs_path) and not fake: + # only retain paths to directories that contain at least one file + recursive = not requires_top_files + if not dir_contains_files(abs_path, recursive=recursive): + self.log.debug("Discarded search path to empty directory: %s", tentative_path) + continue retained_search_paths.append(tentative_path) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index bff746c66b..97275d61a5 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -41,6 +41,7 @@ import os import re import shlex +from enum import Enum from easybuild.base import fancylogger from easybuild.tools import LooseVersion @@ -132,21 +133,38 @@ _log = fancylogger.getLogger('modules', fname=False) +class ModEnvVarType(Enum): + """ + Possible types of ModuleEnvironmentVariable: + - STRING: (list of) strings with no further meaning + - PATH: (list of) of paths to existing directories or files + - PATH_WITH_FILES: (list of) of paths to existing directories containing + one or more files + - PATH_WITH_TOP_FILES: (list of) of paths to existing directories + containing one or more files in its top directory + - """ + STRING, PATH, PATH_WITH_FILES, PATH_WITH_TOP_FILES = range(0, 4) + class ModuleEnvironmentVariable: """ Environment variable data structure for modules Contents of environment variable is a list of unique strings """ - def __init__(self, contents, requires_files=False, delim=os.pathsep): + def __init__(self, contents, var_type=None, delim=os.pathsep): """ Initialize new environment variable Actual contents of the environment variable are held in self.contents + By default, environment variable is a (list of) paths with files in them + Existence of paths and their contents are not checked at init """ self.contents = contents - self.requires_files = bool(requires_files) self.delim = delim + if var_type is None: + var_type = "PATH_WITH_FILES" + self.type = var_type + self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) def __repr__(self): @@ -170,11 +188,23 @@ def contents(self, value): try: str_list = [str(path) for path in value] - except TypeError: - raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from None + except TypeError as err: + raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from err self._contents = nub(str_list) # remove duplicates and keep order + @property + def type(self): + return self._type + + @type.setter + def type(self, value): + """Convert type to VarType""" + try: + self._type = ModEnvVarType[value] + except KeyError as err: + raise EasyBuildError(f"Cannot create ModuleEnvironmentVariable with type {value}") from err + def append(self, item): """Shortcut to append to list of contents""" self.contents += [item] @@ -199,6 +229,14 @@ def remove(self, *args): # item is not in the list, move along self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") + @property + def is_path(self): + path_like_types = [ + ModEnvVarType.PATH, + ModEnvVarType.PATH_WITH_FILES, + ModEnvVarType.PATH_WITH_TOP_FILES, + ] + return self.type in path_like_types class ModuleLoadEnvironment: """Environment set by modules on load""" @@ -235,9 +273,9 @@ def __setattr__(self, name, value): if not isinstance(kwargs, dict): contents, kwargs = value, {} - # special variables that files to be present in the specified paths + # special variables that require files in their top directories if name in ('LD_LIBRARY_PATH', 'PATH'): - kwargs.update({'requires_files': True}) + kwargs.update({'var_type': 'PATH_WITH_TOP_FILES'}) return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) @@ -259,8 +297,8 @@ def update(self, new_env): try: for envar_name, envar_contents in new_env.items(): setattr(self, envar_name, envar_contents) - except AttributeError: - raise EasyBuildError("Cannot update ModuleLoadEnvironment, new environment variables must be in a dict.") + except AttributeError as err: + raise EasyBuildError("Cannot update ModuleLoadEnvironment from a non-dict variable") from err @property def as_dict(self): diff --git a/test/framework/modules.py b/test/framework/modules.py index ab4bc3af59..a457eede62 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1604,7 +1604,7 @@ def test_module_environment_variable(self): test_paths = ['lib', 'lib64'] mod_envar = mod.ModuleEnvironmentVariable(test_paths) self.assertTrue(hasattr(mod_envar, 'contents')) - self.assertTrue(hasattr(mod_envar, 'requires_files')) + self.assertTrue(hasattr(mod_envar, 'type')) self.assertTrue(hasattr(mod_envar, 'delim')) self.assertEqual(mod_envar.contents, test_paths) self.assertEqual(repr(mod_envar), repr(test_paths)) @@ -1615,6 +1615,22 @@ def test_module_environment_variable(self): self.assertEqual(repr(mod_envar_custom_delim), repr(test_paths)) self.assertEqual(str(mod_envar_custom_delim), 'lib|lib64') + mod_envar_custom_type = mod.ModuleEnvironmentVariable(test_paths, var_type='STRING') + self.assertEqual(mod_envar_custom_type.contents, test_paths) + self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.STRING) + self.assertEqual(mod_envar_custom_type.is_path, False) + mod_envar_custom_type.type = 'PATH' + self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH) + self.assertEqual(mod_envar_custom_type.is_path, True) + mod_envar_custom_type.type = 'PATH_WITH_FILES' + self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_FILES) + self.assertEqual(mod_envar_custom_type.is_path, True) + mod_envar_custom_type.type = 'PATH_WITH_TOP_FILES' + self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_TOP_FILES) + self.assertEqual(mod_envar_custom_type.is_path, True) + self.assertRaises(EasyBuildError, setattr, mod_envar_custom_type, 'type', 'NONEXISTENT') + self.assertRaises(EasyBuildError, mod.ModuleEnvironmentVariable, test_paths, 'NONEXISTENT') + mod_envar.contents = [] self.assertEqual(mod_envar.contents, []) self.assertRaises(TypeError, setattr, mod_envar, 'contents', None) @@ -1666,10 +1682,12 @@ def test_module_load_environment(self): mod_load_env.TEST_STR = 'some/path' self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) - mod_load_env.TEST_EXTRA = (test_contents, {'requires_files': True}) - self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) - self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) - self.assertEqual(mod_load_env.TEST_EXTRA.requires_files, True) + mod_load_env.TEST_VARTYPE = (test_contents, {'var_type': "STRING"}) + self.assertTrue(hasattr(mod_load_env, 'TEST_VARTYPE')) + self.assertEqual(mod_load_env.TEST_VARTYPE.contents, test_contents) + self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.STRING) + mod_load_env.TEST_VARTYPE.type = "PATH" + self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.PATH) self.assertRaises(TypeError, setattr, mod_load_env, 'TEST_UNKNONW', (test_contents, {'unkown_param': True})) # test retrieving environment ref_load_env = mod_load_env.__dict__.copy() @@ -1682,22 +1700,23 @@ def test_module_load_environment(self): self.assertDictEqual(mod_load_env.environ, ref_load_env_environ) # test updating environment new_test_env = { - 'TEST_VAR': 'replaced_path', + 'TEST_VARTYPE': 'replaced_path', 'TEST_NEW_VAR': ['new_path1', 'new_path2'], } mod_load_env.update(new_test_env) - self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) - self.assertEqual(mod_load_env.TEST_VAR.contents, ['replaced_path']) + self.assertTrue(hasattr(mod_load_env, 'TEST_VARTYPE')) + self.assertEqual(mod_load_env.TEST_VARTYPE.contents, ['replaced_path']) + self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.PATH_WITH_FILES) self.assertTrue(hasattr(mod_load_env, 'TEST_NEW_VAR')) self.assertEqual(mod_load_env.TEST_NEW_VAR.contents, ['new_path1', 'new_path2']) - self.assertEqual(mod_load_env.TEST_NEW_VAR.requires_files, False) - # check that existing variables still exist + self.assertEqual(mod_load_env.TEST_NEW_VAR.type, mod.ModEnvVarType.PATH_WITH_FILES) + # check that previous variables still exist + self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) + self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) - self.assertTrue(hasattr(mod_load_env, 'TEST_EXTRA')) - self.assertEqual(mod_load_env.TEST_EXTRA.contents, test_contents) def suite(): From de961fe4a3bdba341ec5257cbb76fe8d8b7d1759 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Thu, 12 Dec 2024 23:29:59 +0100 Subject: [PATCH 50/59] fix codestyle in easybuild.tools.modules --- easybuild/tools/modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 97275d61a5..3763fe8f48 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -145,6 +145,7 @@ class ModEnvVarType(Enum): - """ STRING, PATH, PATH_WITH_FILES, PATH_WITH_TOP_FILES = range(0, 4) + class ModuleEnvironmentVariable: """ Environment variable data structure for modules @@ -238,6 +239,7 @@ def is_path(self): ] return self.type in path_like_types + class ModuleLoadEnvironment: """Environment set by modules on load""" From d4a9df04bac44d7cb5067a8ffa7395f89ce54481 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Fri, 13 Dec 2024 10:17:05 +0100 Subject: [PATCH 51/59] EasyBlock.expand_module_search_path accepts ModEnvVarType as path_type to control requirements of paths to directories --- easybuild/framework/easyblock.py | 21 ++++-- test/framework/easyblock.py | 123 ++++++++++++++++++------------- 2 files changed, 86 insertions(+), 58 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 1721e7aef5..dec10ad562 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1668,10 +1668,13 @@ def make_module_req(self, fake=False): # Don't expand globs or do any filtering for dry run mod_req_paths = search_paths else: + path_type = search_paths.type + if fake: + path_type = ModEnvVarType.PATH + mod_req_paths = [] - requires_top_files = search_paths.type == ModEnvVarType.PATH_WITH_TOP_FILES for path in search_paths: - mod_req_paths.extend(self.expand_module_search_path(path, requires_top_files, fake=fake)) + mod_req_paths.extend(self.expand_module_search_path(path, path_type=path_type)) if mod_req_paths: mod_req_paths = nub(mod_req_paths) # remove duplicates @@ -1682,15 +1685,16 @@ def make_module_req(self, fake=False): return "".join(mod_lines) - def expand_module_search_path(self, search_path, requires_top_files, fake=False): + def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WITH_FILES): """ Expand given path glob and return list of suitable paths to be used as search paths: - Paths are relative to installation prefix root - Paths must point to existing files/directories - Search paths to a 'lib64' symlinked to 'lib' are discarded to avoid duplicates - - Directories must contain at least one file in them (empty folders are ignored) - - requires_top_files: increases stricness to require files in top level directory - - fake: fake modules can set search paths to empty directories + - :path_type: ModEnvVarType that controls requirements for population of directories + - PATH: no requirements, can be empty + - PATH_WITH_FILES: must contain at least one file in them (default) + - PATH_WITH_TOP_FILES: increase stricness to require files in top level directory """ # 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) @@ -1716,9 +1720,10 @@ def expand_module_search_path(self, search_path, requires_top_files, fake=False) self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) continue - if os.path.isdir(abs_path) and not fake: + check_dir_files = path_type in [ModEnvVarType.PATH_WITH_FILES, ModEnvVarType.PATH_WITH_TOP_FILES] + if os.path.isdir(abs_path) and check_dir_files: # only retain paths to directories that contain at least one file - recursive = not requires_top_files + recursive = path_type == ModEnvVarType.PATH_WITH_FILES if not dir_contains_files(abs_path, recursive=recursive): self.log.debug("Discarded search path to empty directory: %s", tentative_path) continue diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 5a57b8ce66..2221ca9306 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -52,7 +52,7 @@ from easybuild.tools.filetools import change_dir, copy_dir, copy_file, mkdir, read_file, remove_dir, remove_file from easybuild.tools.filetools import verify_checksum, write_file from easybuild.tools.module_generator import module_generator -from easybuild.tools.modules import EnvironmentModules, Lmod, reset_module_caches +from easybuild.tools.modules import EnvironmentModules, Lmod, ModEnvVarType, reset_module_caches from easybuild.tools.version import get_git_revision, this_is_easybuild @@ -3056,82 +3056,102 @@ def test_expand_module_search_path(self): toy_ec = os.path.join(top_dir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb') eb = EasyBlock(EasyConfig(toy_ec)) eb.installdir = config.install_path() + test_emsp = eb.expand_module_search_path # shortcut # create test directories and files os.makedirs(eb.installdir) test_directories = ( 'empty_dir', - 'dir_with_empty_subdir', - ('dir_with_empty_subdir', 'empty_subdir'), + 'dir_empty_subdir', + ('dir_empty_subdir', 'empty_subdir'), 'dir_with_file', - 'dir_with_full_subdirs', - ('dir_with_full_subdirs', 'subdir1'), - ('dir_with_full_subdirs', 'subdir2'), + 'dir_full_subdirs', + ('dir_full_subdirs', 'subdir1'), + ('dir_full_subdirs', 'subdir2'), ) for path in test_directories: path_components = (path, ) if isinstance(path, str) else path os.mkdir(os.path.join(eb.installdir, *path_components)) write_file(os.path.join(eb.installdir, 'dir_with_file', 'file.txt'), 'test file') - write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir1', 'file11.txt'), 'test file 1.1') - write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2') - write_file(os.path.join(eb.installdir, 'dir_with_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1') + write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir1', 'file11.txt'), 'test file 1.1') + write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2') + write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1') eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) - self.assertEqual(eb.expand_module_search_path("nonexistent", True), []) - self.assertEqual(eb.expand_module_search_path("nonexistent", False), []) - self.assertEqual(eb.expand_module_search_path("empty_dir", True), []) - self.assertEqual(eb.expand_module_search_path("empty_dir", False), []) - self.assertEqual(eb.expand_module_search_path("dir_with_empty_subdir", True), []) - self.assertEqual(eb.expand_module_search_path("dir_with_empty_subdir", False), []) - self.assertEqual(eb.expand_module_search_path("dir_with_file", True), ["dir_with_file"]) - self.assertEqual(eb.expand_module_search_path("dir_with_file", False), ["dir_with_file"]) - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs", True), []) - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs", False), ["dir_with_full_subdirs"]) + self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH), []) + self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH_WITH_TOP_FILES), []) + self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH), ["empty_dir"]) + self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("empty_dir", ModEnvVarType.PATH_WITH_TOP_FILES), []) + self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH), ["dir_empty_subdir"]) + self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("dir_empty_subdir", ModEnvVarType.PATH_WITH_TOP_FILES), []) + self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH), ["dir_with_file"]) + self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH_WITH_FILES), ["dir_with_file"]) + self.assertEqual(test_emsp("dir_with_file", ModEnvVarType.PATH_WITH_TOP_FILES), ["dir_with_file"]) + self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH), ["dir_full_subdirs"]) + self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_FILES), ["dir_full_subdirs"]) + self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_TOP_FILES), []) # test globs - ref_expanded_paths = ["dir_with_full_subdirs/subdir1", "dir_with_full_subdirs/subdir2"] - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/*", True), ref_expanded_paths) - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/*", False), ref_expanded_paths) - ref_expanded_paths = ["dir_with_full_subdirs/subdir2/file21.txt"] - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/subdir2/*", True), ref_expanded_paths) - self.assertEqual(eb.expand_module_search_path("nonexistent/*", True), []) - self.assertEqual(eb.expand_module_search_path("dir_with_full_subdirs/nonexistent/*", True), []) + ref_expanded_paths = ["dir_empty_subdir/empty_subdir"] + self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH), ref_expanded_paths) + self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_TOP_FILES), []) + ref_expanded_paths = ["dir_full_subdirs/subdir1", "dir_full_subdirs/subdir2"] + self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH), ref_expanded_paths) + self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_FILES), ref_expanded_paths) + self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_TOP_FILES), ref_expanded_paths) + ref_expanded_paths = ["dir_full_subdirs/subdir2/file21.txt"] + self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH), ref_expanded_paths) + self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH_WITH_FILES), ref_expanded_paths) + self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH_WITH_TOP_FILES), ref_expanded_paths) + self.assertEqual(test_emsp("nonexistent/*", True), []) + self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH), []) + self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_TOP_FILES), []) # state of install_lib_symlink should not have changed self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) - # test both lib and lib64 directories + # test just one lib directory os.mkdir(os.path.join(eb.installdir, "lib")) eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) - self.assertEqual(eb.expand_module_search_path("lib", True), []) - self.assertEqual(eb.expand_module_search_path("lib", False), []) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), []) write_file(os.path.join(eb.installdir, "lib", "libtest.so"), "not actually a lib") - self.assertEqual(eb.expand_module_search_path("lib", True), ["lib"]) - self.assertEqual(eb.expand_module_search_path("lib", False), ["lib"]) - + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) + # test both lib and lib64 directories os.mkdir(os.path.join(eb.installdir, "lib64")) eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) - self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib"]) - self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib", "lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib") - self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib", "lib64"]) - self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib", "lib64"]) - + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib", "lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib", "lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib", "lib64"]) # test lib64 symlinked to lib remove_dir(os.path.join(eb.installdir, "lib64")) os.symlink("lib", os.path.join(eb.installdir, "lib64")) eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB64_TO_LIB) - self.assertEqual(eb.expand_module_search_path("lib", True), ["lib"]) - self.assertEqual(eb.expand_module_search_path("lib", False), ["lib"]) - self.assertEqual(eb.expand_module_search_path("lib64", True), []) - self.assertEqual(eb.expand_module_search_path("lib64", False), []) - self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib"]) - self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib"]) - + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH), []) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_TOP_FILES), []) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) # test lib symlinked to lib64 remove_dir(os.path.join(eb.installdir, "lib")) remove_file(os.path.join(eb.installdir, "lib64")) @@ -3140,12 +3160,15 @@ def test_expand_module_search_path(self): os.symlink("lib64", os.path.join(eb.installdir, "lib")) eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.LIB_TO_LIB64) - self.assertEqual(eb.expand_module_search_path("lib", True), []) - self.assertEqual(eb.expand_module_search_path("lib", False), []) - self.assertEqual(eb.expand_module_search_path("lib64", True), ["lib64"]) - self.assertEqual(eb.expand_module_search_path("lib64", False), ["lib64"]) - self.assertEqual(eb.expand_module_search_path("lib*", True), ["lib64"]) - self.assertEqual(eb.expand_module_search_path("lib*", False), ["lib64"]) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), []) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), []) + self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), []) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH), ["lib64"]) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_FILES), ["lib64"]) + self.assertEqual(test_emsp("lib64", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib64"]) + self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib64"]) def suite(): From fcd21b88d460b0f8c605e15a669bf399f8b75549 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 8 Jan 2025 20:26:49 +0100 Subject: [PATCH 52/59] require that ModuleLoadEnvironment attributes being set have uppercase name --- easybuild/tools/modules.py | 8 +++++--- test/framework/modules.py | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 3763fe8f48..5c53f0d336 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -241,7 +241,7 @@ def is_path(self): class ModuleLoadEnvironment: - """Environment set by modules on load""" + """Changes to environment variables that should be made when environment module is loaded""" def __init__(self): """ @@ -250,7 +250,7 @@ def __init__(self): """ self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')] self.CLASSPATH = ['*.jar'] - self.CMAKE_LIBRARY_PATH = ['lib64'] # only needed for installations whith standalone lib64 + self.CMAKE_LIBRARY_PATH = ['lib64'] # only needed for installations with standalone lib64 self.CMAKE_PREFIX_PATH = [''] self.CPATH = SEARCH_PATH_HEADER_DIRS self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] @@ -267,6 +267,8 @@ def __setattr__(self, name, value): - attribute names are uppercase - attributes are instances of ModuleEnvironmentVariable """ + if name != name.upper(): + raise EasyBuildError(f"Names of ModuleLoadEnvironment attributes must be uppercase, got '{name}'") try: (contents, kwargs) = value except ValueError: @@ -279,7 +281,7 @@ def __setattr__(self, name, value): if name in ('LD_LIBRARY_PATH', 'PATH'): kwargs.update({'var_type': 'PATH_WITH_TOP_FILES'}) - return super().__setattr__(name.upper(), ModuleEnvironmentVariable(contents, **kwargs)) + return super().__setattr__(name, ModuleEnvironmentVariable(contents, **kwargs)) def __iter__(self): """Make the class iterable""" diff --git a/test/framework/modules.py b/test/framework/modules.py index a457eede62..7f6d728edf 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1670,34 +1670,43 @@ def test_module_environment_variable(self): def test_module_load_environment(self): """Test for ModuleLoadEnvironment object""" + mod_load_env = mod.ModuleLoadEnvironment() + # test setting attributes test_contents = ['lib', 'lib64'] - mod_load_env = mod.ModuleLoadEnvironment() mod_load_env.TEST_VAR = test_contents self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) - mod_load_env.test_lower = test_contents - self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) - self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) + + error_pattern = "Names of ModuleLoadEnvironment attributes must be uppercase, got 'test_lower'" + self.assertErrorRegex(EasyBuildError, error_pattern, setattr, mod_load_env, 'test_lower', test_contents) + mod_load_env.TEST_STR = 'some/path' self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) + mod_load_env.TEST_VARTYPE = (test_contents, {'var_type': "STRING"}) self.assertTrue(hasattr(mod_load_env, 'TEST_VARTYPE')) self.assertEqual(mod_load_env.TEST_VARTYPE.contents, test_contents) self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.STRING) + mod_load_env.TEST_VARTYPE.type = "PATH" self.assertEqual(mod_load_env.TEST_VARTYPE.type, mod.ModEnvVarType.PATH) self.assertRaises(TypeError, setattr, mod_load_env, 'TEST_UNKNONW', (test_contents, {'unkown_param': True})) + # test retrieving environment ref_load_env = mod_load_env.__dict__.copy() self.assertCountEqual(list(mod_load_env), ref_load_env.keys()) + ref_load_env_item_list = list(ref_load_env.items()) self.assertCountEqual(list(mod_load_env.items()), ref_load_env_item_list) + ref_load_env_item_list = dict(ref_load_env.items()) self.assertCountEqual(mod_load_env.as_dict, ref_load_env_item_list) + ref_load_env_environ = {key: str(value) for key, value in ref_load_env.items()} self.assertDictEqual(mod_load_env.environ, ref_load_env_environ) + # test updating environment new_test_env = { 'TEST_VARTYPE': 'replaced_path', @@ -1710,11 +1719,10 @@ def test_module_load_environment(self): self.assertTrue(hasattr(mod_load_env, 'TEST_NEW_VAR')) self.assertEqual(mod_load_env.TEST_NEW_VAR.contents, ['new_path1', 'new_path2']) self.assertEqual(mod_load_env.TEST_NEW_VAR.type, mod.ModEnvVarType.PATH_WITH_FILES) + # check that previous variables still exist self.assertTrue(hasattr(mod_load_env, 'TEST_VAR')) self.assertEqual(mod_load_env.TEST_VAR.contents, test_contents) - self.assertTrue(hasattr(mod_load_env, 'TEST_LOWER')) - self.assertEqual(mod_load_env.TEST_LOWER.contents, test_contents) self.assertTrue(hasattr(mod_load_env, 'TEST_STR')) self.assertEqual(mod_load_env.TEST_STR.contents, ['some/path']) From 2ee964db43e58ca2e75591977e9a15bd79d8c55a Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 8 Jan 2025 20:39:24 +0100 Subject: [PATCH 53/59] fix failures in test_expand_module_search_path by not assuming particular order of list entries --- easybuild/framework/easyblock.py | 6 +++--- test/framework/easyblock.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index dec10ad562..8e8e3bd1f6 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1712,11 +1712,11 @@ def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WI 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_TO_LIB and tentative_sep.startswith('lib64' + os.path.sep): + tent_path_sep = tentative_path + os.path.sep + if self.install_lib_symlink == LibSymlink.LIB64_TO_LIB and tent_path_sep.startswith('lib64' + os.path.sep): self.log.debug("Discarded search path to symlinked lib64 directory: %s", tentative_path) continue - if self.install_lib_symlink == LibSymlink.LIB_TO_LIB64 and tentative_sep.startswith('lib' + os.path.sep): + if self.install_lib_symlink == LibSymlink.LIB_TO_LIB64 and tent_path_sep.startswith('lib' + os.path.sep): self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) continue diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 2221ca9306..5c92b2d298 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -3102,9 +3102,9 @@ def test_expand_module_search_path(self): self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_FILES), []) self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH_WITH_TOP_FILES), []) ref_expanded_paths = ["dir_full_subdirs/subdir1", "dir_full_subdirs/subdir2"] - self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH), ref_expanded_paths) - self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_FILES), ref_expanded_paths) - self.assertEqual(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_TOP_FILES), ref_expanded_paths) + self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH)), ref_expanded_paths) + self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_FILES)), ref_expanded_paths) + self.assertEqual(sorted(test_emsp("dir_full_subdirs/*", ModEnvVarType.PATH_WITH_TOP_FILES)), ref_expanded_paths) ref_expanded_paths = ["dir_full_subdirs/subdir2/file21.txt"] self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH), ref_expanded_paths) self.assertEqual(test_emsp("dir_full_subdirs/subdir2/*", ModEnvVarType.PATH_WITH_FILES), ref_expanded_paths) @@ -3131,13 +3131,13 @@ def test_expand_module_search_path(self): os.mkdir(os.path.join(eb.installdir, "lib64")) eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) - self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib", "lib64"]) + self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["lib", "lib64"]) self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib"]) self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) write_file(os.path.join(eb.installdir, "lib64", "libtest.so"), "not actually a lib") - self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib", "lib64"]) - self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib", "lib64"]) - self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib", "lib64"]) + self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["lib", "lib64"]) + self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES)), ["lib", "lib64"]) + self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES)), ["lib", "lib64"]) # test lib64 symlinked to lib remove_dir(os.path.join(eb.installdir, "lib64")) os.symlink("lib", os.path.join(eb.installdir, "lib64")) From f69f23f3db0d87e9381f9e774bb28960d4d907b5 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 8 Jan 2025 20:45:30 +0100 Subject: [PATCH 54/59] make sure that check_install_lib_symlink updates self.install_lib_symlink to something else than LibSymlink.UNKNOWN --- easybuild/framework/easyblock.py | 6 ++---- test/framework/easyblock.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 8e8e3bd1f6..c82dadbbe6 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1720,7 +1720,7 @@ def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WI self.log.debug("Discarded search path to symlinked lib directory: %s", tentative_path) continue - check_dir_files = path_type in [ModEnvVarType.PATH_WITH_FILES, ModEnvVarType.PATH_WITH_TOP_FILES] + check_dir_files = path_type in (ModEnvVarType.PATH_WITH_FILES, ModEnvVarType.PATH_WITH_TOP_FILES) if os.path.isdir(abs_path) and check_dir_files: # only retain paths to directories that contain at least one file recursive = path_type == ModEnvVarType.PATH_WITH_FILES @@ -1737,14 +1737,12 @@ def check_install_lib_symlink(self): lib_dir = os.path.join(self.installdir, 'lib') lib64_dir = os.path.join(self.installdir, 'lib64') - self.install_lib_symlink = LibSymlink.UNKNOWN + self.install_lib_symlink = LibSymlink.NEITHER if os.path.exists(lib_dir) and os.path.exists(lib64_dir): if os.path.islink(lib_dir) and os.path.samefile(lib_dir, lib64_dir): self.install_lib_symlink = LibSymlink.LIB_TO_LIB64 elif os.path.islink(lib64_dir) and os.path.samefile(lib_dir, lib64_dir): self.install_lib_symlink = LibSymlink.LIB64_TO_LIB - else: - self.install_lib_symlink = LibSymlink.NEITHER def make_module_req_guess(self): """ diff --git a/test/framework/easyblock.py b/test/framework/easyblock.py index 5c92b2d298..009f1ae281 100644 --- a/test/framework/easyblock.py +++ b/test/framework/easyblock.py @@ -3078,8 +3078,9 @@ def test_expand_module_search_path(self): write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir1', 'file12.txt'), 'test file 1.2') write_file(os.path.join(eb.installdir, 'dir_full_subdirs', 'subdir2', 'file21.txt'), 'test file 2.1') - eb.check_install_lib_symlink() self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) + eb.check_install_lib_symlink() + self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH), []) self.assertEqual(test_emsp("nonexistent", ModEnvVarType.PATH_WITH_FILES), []) @@ -3096,6 +3097,7 @@ def test_expand_module_search_path(self): self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH), ["dir_full_subdirs"]) self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_FILES), ["dir_full_subdirs"]) self.assertEqual(test_emsp("dir_full_subdirs", ModEnvVarType.PATH_WITH_TOP_FILES), []) + # test globs ref_expanded_paths = ["dir_empty_subdir/empty_subdir"] self.assertEqual(test_emsp("dir_empty_subdir/*", ModEnvVarType.PATH), ref_expanded_paths) @@ -3113,13 +3115,14 @@ def test_expand_module_search_path(self): self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH), []) self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_FILES), []) self.assertEqual(test_emsp("nonexistent/*", ModEnvVarType.PATH_WITH_TOP_FILES), []) + # state of install_lib_symlink should not have changed - self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) + self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) # test just one lib directory os.mkdir(os.path.join(eb.installdir, "lib")) eb.check_install_lib_symlink() - self.assertEqual(eb.install_lib_symlink, LibSymlink.UNKNOWN) + self.assertEqual(eb.install_lib_symlink, LibSymlink.NEITHER) self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"]) self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), []) self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), []) @@ -3127,6 +3130,7 @@ def test_expand_module_search_path(self): self.assertEqual(test_emsp("lib", ModEnvVarType.PATH), ["lib"]) self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_FILES), ["lib"]) self.assertEqual(test_emsp("lib", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) + # test both lib and lib64 directories os.mkdir(os.path.join(eb.installdir, "lib64")) eb.check_install_lib_symlink() @@ -3138,6 +3142,7 @@ def test_expand_module_search_path(self): self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH)), ["lib", "lib64"]) self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES)), ["lib", "lib64"]) self.assertEqual(sorted(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES)), ["lib", "lib64"]) + # test lib64 symlinked to lib remove_dir(os.path.join(eb.installdir, "lib64")) os.symlink("lib", os.path.join(eb.installdir, "lib64")) @@ -3152,6 +3157,7 @@ def test_expand_module_search_path(self): self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH), ["lib"]) self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_FILES), ["lib"]) self.assertEqual(test_emsp("lib*", ModEnvVarType.PATH_WITH_TOP_FILES), ["lib"]) + # test lib symlinked to lib64 remove_dir(os.path.join(eb.installdir, "lib")) remove_file(os.path.join(eb.installdir, "lib64")) From d27e1cbbbe4c663fc4f17f7176de60e08d8c9510 Mon Sep 17 00:00:00 2001 From: Kenneth Hoste Date: Wed, 8 Jan 2025 20:57:16 +0100 Subject: [PATCH 55/59] always use ModEnvVarType.PATH_WITH_FILES and ModEnvVarType.PATH_WITH_TOP_FILES rather than string values --- easybuild/tools/modules.py | 15 +++++++++------ test/framework/modules.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 5c53f0d336..e063c29b15 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -163,7 +163,7 @@ def __init__(self, contents, var_type=None, delim=os.pathsep): self.delim = delim if var_type is None: - var_type = "PATH_WITH_FILES" + var_type = ModEnvVarType.PATH_WITH_FILES self.type = var_type self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) @@ -201,10 +201,13 @@ def type(self): @type.setter def type(self, value): """Convert type to VarType""" - try: - self._type = ModEnvVarType[value] - except KeyError as err: - raise EasyBuildError(f"Cannot create ModuleEnvironmentVariable with type {value}") from err + if isinstance(value, ModEnvVarType): + self._type = value + else: + try: + self._type = ModEnvVarType[value] + except KeyError as err: + raise EasyBuildError(f"Cannot create ModuleEnvironmentVariable with type {value}") from err def append(self, item): """Shortcut to append to list of contents""" @@ -279,7 +282,7 @@ def __setattr__(self, name, value): # special variables that require files in their top directories if name in ('LD_LIBRARY_PATH', 'PATH'): - kwargs.update({'var_type': 'PATH_WITH_TOP_FILES'}) + kwargs.update({'var_type': ModEnvVarType.PATH_WITH_TOP_FILES}) return super().__setattr__(name, ModuleEnvironmentVariable(contents, **kwargs)) diff --git a/test/framework/modules.py b/test/framework/modules.py index 7f6d728edf..ef3d986c06 100644 --- a/test/framework/modules.py +++ b/test/framework/modules.py @@ -1622,12 +1622,24 @@ def test_module_environment_variable(self): mod_envar_custom_type.type = 'PATH' self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH) self.assertEqual(mod_envar_custom_type.is_path, True) + + mod_envar_custom_type.type = mod.ModEnvVarType.PATH + self.assertEqual(mod_envar_custom_type.is_path, True) + mod_envar_custom_type.type = 'PATH_WITH_FILES' self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_FILES) self.assertEqual(mod_envar_custom_type.is_path, True) + + mod_envar_custom_type.type = mod.ModEnvVarType.PATH_WITH_FILES + self.assertEqual(mod_envar_custom_type.is_path, True) + mod_envar_custom_type.type = 'PATH_WITH_TOP_FILES' self.assertEqual(mod_envar_custom_type.type, mod.ModEnvVarType.PATH_WITH_TOP_FILES) self.assertEqual(mod_envar_custom_type.is_path, True) + + mod_envar_custom_type.type = mod.ModEnvVarType.PATH_WITH_TOP_FILES + self.assertEqual(mod_envar_custom_type.is_path, True) + self.assertRaises(EasyBuildError, setattr, mod_envar_custom_type, 'type', 'NONEXISTENT') self.assertRaises(EasyBuildError, mod.ModuleEnvironmentVariable, test_paths, 'NONEXISTENT') From f8203587e1db29ea43d02acf551a83499caa6e83 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 13 Jan 2025 01:34:25 +0100 Subject: [PATCH 56/59] revert: disable non-empty check on search path drectories for fake module files --- easybuild/framework/easyblock.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index dec10ad562..fd91cf8eda 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1634,10 +1634,9 @@ def make_module_group_check(self): return txt - def make_module_req(self, fake=False): + def make_module_req(self): """ Generate the environment-variables required to run the module. - Fake modules can set search paths to empty directories. """ mod_lines = ['\n'] @@ -1668,13 +1667,9 @@ def make_module_req(self, fake=False): # Don't expand globs or do any filtering for dry run mod_req_paths = search_paths else: - path_type = search_paths.type - if fake: - path_type = ModEnvVarType.PATH - mod_req_paths = [] for path in search_paths: - mod_req_paths.extend(self.expand_module_search_path(path, path_type=path_type)) + mod_req_paths.extend(self.expand_module_search_path(path, path_type=search_paths.type)) if mod_req_paths: mod_req_paths = nub(mod_req_paths) # remove duplicates @@ -3929,7 +3924,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(fake=fake) + txt += self.make_module_req() txt += self.make_module_extra() txt += self.make_module_footer() From a9e5267b752c773d8a9a7e621c69749efbfbc629 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 13 Jan 2025 10:15:16 +0100 Subject: [PATCH 57/59] log at debug level the environment targeted by make_module_req before path expansion --- easybuild/framework/easyblock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index fd91cf8eda..69867b1614 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -1660,6 +1660,7 @@ def make_module_req(self): (envar_name, envar_val) for envar_name, envar_val in self.module_load_environment.items() if envar_val.is_path ]) + self.log.debug(f"Tentative module environment requirements before path expansion: {env_var_requirements}") for env_var, search_paths in env_var_requirements: if self.dry_run: From 13f735c00644a0111599f1b28c2277be15d4f418 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 13 Jan 2025 13:36:35 +0100 Subject: [PATCH 58/59] fix passing of arguments in ModuleEnvironmentVariable.remove --- easybuild/tools/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 3763fe8f48..e589778931 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -225,7 +225,7 @@ def update(self, item): def remove(self, *args): """Shortcut to remove items from list of contents""" try: - self.contents.remove(*args) + self.contents.remove(args) except ValueError: # item is not in the list, move along self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") From 678ce994055ff4ab65423d4bfabf0bf030319797 Mon Sep 17 00:00:00 2001 From: Alex Domingo Date: Mon, 13 Jan 2025 15:28:10 +0100 Subject: [PATCH 59/59] Revert "fix passing of arguments in ModuleEnvironmentVariable.remove" This reverts commit 13f735c00644a0111599f1b28c2277be15d4f418. --- easybuild/tools/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py index 45ada0f9cb..e063c29b15 100644 --- a/easybuild/tools/modules.py +++ b/easybuild/tools/modules.py @@ -228,7 +228,7 @@ def update(self, item): def remove(self, *args): """Shortcut to remove items from list of contents""" try: - self.contents.remove(args) + self.contents.remove(*args) except ValueError: # item is not in the list, move along self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}")