|
41 | 41 | import os
|
42 | 42 | import re
|
43 | 43 | import shlex
|
| 44 | +from enum import Enum |
44 | 45 |
|
45 | 46 | from easybuild.base import fancylogger
|
46 | 47 | from easybuild.tools import LooseVersion
|
47 | 48 | from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning
|
48 |
| -from easybuild.tools.config import ERROR, IGNORE, PURGE, UNLOAD, UNSET |
49 |
| -from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, LOADED_MODULES_ACTIONS |
| 49 | +from easybuild.tools.config import ERROR, EBROOT_ENV_VAR_ACTIONS, IGNORE, LOADED_MODULES_ACTIONS, PURGE |
| 50 | +from easybuild.tools.config import SEARCH_PATH_BIN_DIRS, SEARCH_PATH_HEADER_DIRS, SEARCH_PATH_LIB_DIRS, UNLOAD, UNSET |
50 | 51 | from easybuild.tools.config import build_option, get_modules_tool, install_path
|
51 | 52 | from easybuild.tools.environment import ORIG_OS_ENVIRON, restore_env, setvar, unset_env_vars
|
52 | 53 | from easybuild.tools.filetools import convert_name, mkdir, normalize_path, path_matches, read_file, which, write_file
|
|
55 | 56 | from easybuild.tools.systemtools import get_shared_lib_ext
|
56 | 57 | from easybuild.tools.utilities import get_subclasses, nub
|
57 | 58 |
|
| 59 | + |
58 | 60 | # software root/version environment variable name prefixes
|
59 | 61 | ROOT_ENV_VAR_NAME_PREFIX = "EBROOT"
|
60 | 62 | VERSION_ENV_VAR_NAME_PREFIX = "EBVERSION"
|
|
131 | 133 | _log = fancylogger.getLogger('modules', fname=False)
|
132 | 134 |
|
133 | 135 |
|
| 136 | +class ModEnvVarType(Enum): |
| 137 | + """ |
| 138 | + Possible types of ModuleEnvironmentVariable: |
| 139 | + - STRING: (list of) strings with no further meaning |
| 140 | + - PATH: (list of) of paths to existing directories or files |
| 141 | + - PATH_WITH_FILES: (list of) of paths to existing directories containing |
| 142 | + one or more files |
| 143 | + - PATH_WITH_TOP_FILES: (list of) of paths to existing directories |
| 144 | + containing one or more files in its top directory |
| 145 | + - """ |
| 146 | + STRING, PATH, PATH_WITH_FILES, PATH_WITH_TOP_FILES = range(0, 4) |
| 147 | + |
| 148 | + |
| 149 | +class ModuleEnvironmentVariable: |
| 150 | + """ |
| 151 | + Environment variable data structure for modules |
| 152 | + Contents of environment variable is a list of unique strings |
| 153 | + """ |
| 154 | + |
| 155 | + def __init__(self, contents, var_type=None, delim=os.pathsep): |
| 156 | + """ |
| 157 | + Initialize new environment variable |
| 158 | + Actual contents of the environment variable are held in self.contents |
| 159 | + By default, environment variable is a (list of) paths with files in them |
| 160 | + Existence of paths and their contents are not checked at init |
| 161 | + """ |
| 162 | + self.contents = contents |
| 163 | + self.delim = delim |
| 164 | + |
| 165 | + if var_type is None: |
| 166 | + var_type = ModEnvVarType.PATH_WITH_FILES |
| 167 | + self.type = var_type |
| 168 | + |
| 169 | + self.log = fancylogger.getLogger(self.__class__.__name__, fname=False) |
| 170 | + |
| 171 | + def __repr__(self): |
| 172 | + return repr(self.contents) |
| 173 | + |
| 174 | + def __str__(self): |
| 175 | + return self.delim.join(self.contents) |
| 176 | + |
| 177 | + def __iter__(self): |
| 178 | + return iter(self.contents) |
| 179 | + |
| 180 | + @property |
| 181 | + def contents(self): |
| 182 | + return self._contents |
| 183 | + |
| 184 | + @contents.setter |
| 185 | + def contents(self, value): |
| 186 | + """Enforce that contents is a list of strings""" |
| 187 | + if isinstance(value, str): |
| 188 | + value = [value] |
| 189 | + |
| 190 | + try: |
| 191 | + str_list = [str(path) for path in value] |
| 192 | + except TypeError as err: |
| 193 | + raise TypeError("ModuleEnvironmentVariable.contents must be a list of strings") from err |
| 194 | + |
| 195 | + self._contents = nub(str_list) # remove duplicates and keep order |
| 196 | + |
| 197 | + @property |
| 198 | + def type(self): |
| 199 | + return self._type |
| 200 | + |
| 201 | + @type.setter |
| 202 | + def type(self, value): |
| 203 | + """Convert type to VarType""" |
| 204 | + if isinstance(value, ModEnvVarType): |
| 205 | + self._type = value |
| 206 | + else: |
| 207 | + try: |
| 208 | + self._type = ModEnvVarType[value] |
| 209 | + except KeyError as err: |
| 210 | + raise EasyBuildError(f"Cannot create ModuleEnvironmentVariable with type {value}") from err |
| 211 | + |
| 212 | + def append(self, item): |
| 213 | + """Shortcut to append to list of contents""" |
| 214 | + self.contents += [item] |
| 215 | + |
| 216 | + def extend(self, item): |
| 217 | + """Shortcut to extend list of contents""" |
| 218 | + self.contents += item |
| 219 | + |
| 220 | + def prepend(self, item): |
| 221 | + """Shortcut to prepend item to list of contents""" |
| 222 | + self.contents = [item] + self.contents |
| 223 | + |
| 224 | + def update(self, item): |
| 225 | + """Shortcut to replace list of contents with item""" |
| 226 | + self.contents = item |
| 227 | + |
| 228 | + def remove(self, *args): |
| 229 | + """Shortcut to remove items from list of contents""" |
| 230 | + try: |
| 231 | + self.contents.remove(*args) |
| 232 | + except ValueError: |
| 233 | + # item is not in the list, move along |
| 234 | + self.log.debug(f"ModuleEnvironmentVariable does not contain item: {' '.join(args)}") |
| 235 | + |
| 236 | + @property |
| 237 | + def is_path(self): |
| 238 | + path_like_types = [ |
| 239 | + ModEnvVarType.PATH, |
| 240 | + ModEnvVarType.PATH_WITH_FILES, |
| 241 | + ModEnvVarType.PATH_WITH_TOP_FILES, |
| 242 | + ] |
| 243 | + return self.type in path_like_types |
| 244 | + |
| 245 | + |
| 246 | +class ModuleLoadEnvironment: |
| 247 | + """Changes to environment variables that should be made when environment module is loaded""" |
| 248 | + |
| 249 | + def __init__(self): |
| 250 | + """ |
| 251 | + Initialize default environment definition |
| 252 | + Paths are relative to root of installation directory |
| 253 | + """ |
| 254 | + self.ACLOCAL_PATH = [os.path.join('share', 'aclocal')] |
| 255 | + self.CLASSPATH = ['*.jar'] |
| 256 | + self.CMAKE_LIBRARY_PATH = ['lib64'] # only needed for installations with standalone lib64 |
| 257 | + self.CMAKE_PREFIX_PATH = [''] |
| 258 | + self.CPATH = SEARCH_PATH_HEADER_DIRS |
| 259 | + self.GI_TYPELIB_PATH = [os.path.join(x, 'girepository-*') for x in SEARCH_PATH_LIB_DIRS] |
| 260 | + self.LD_LIBRARY_PATH = SEARCH_PATH_LIB_DIRS |
| 261 | + self.LIBRARY_PATH = SEARCH_PATH_LIB_DIRS |
| 262 | + self.MANPATH = ['man', os.path.join('share', 'man')] |
| 263 | + self.PATH = SEARCH_PATH_BIN_DIRS + ['sbin'] |
| 264 | + self.PKG_CONFIG_PATH = [os.path.join(x, 'pkgconfig') for x in SEARCH_PATH_LIB_DIRS + ['share']] |
| 265 | + self.XDG_DATA_DIRS = ['share'] |
| 266 | + |
| 267 | + def __setattr__(self, name, value): |
| 268 | + """ |
| 269 | + Specific restrictions for ModuleLoadEnvironment attributes: |
| 270 | + - attribute names are uppercase |
| 271 | + - attributes are instances of ModuleEnvironmentVariable |
| 272 | + """ |
| 273 | + if name != name.upper(): |
| 274 | + raise EasyBuildError(f"Names of ModuleLoadEnvironment attributes must be uppercase, got '{name}'") |
| 275 | + try: |
| 276 | + (contents, kwargs) = value |
| 277 | + except ValueError: |
| 278 | + contents, kwargs = value, {} |
| 279 | + |
| 280 | + if not isinstance(kwargs, dict): |
| 281 | + contents, kwargs = value, {} |
| 282 | + |
| 283 | + # special variables that require files in their top directories |
| 284 | + if name in ('LD_LIBRARY_PATH', 'PATH'): |
| 285 | + kwargs.update({'var_type': ModEnvVarType.PATH_WITH_TOP_FILES}) |
| 286 | + |
| 287 | + return super().__setattr__(name, ModuleEnvironmentVariable(contents, **kwargs)) |
| 288 | + |
| 289 | + def __iter__(self): |
| 290 | + """Make the class iterable""" |
| 291 | + yield from self.__dict__ |
| 292 | + |
| 293 | + def items(self): |
| 294 | + """ |
| 295 | + Return key-value pairs for each attribute that is a ModuleEnvironmentVariable |
| 296 | + - key = attribute name |
| 297 | + - value = its "contents" attribute |
| 298 | + """ |
| 299 | + for attr in self.__dict__: |
| 300 | + yield attr, getattr(self, attr) |
| 301 | + |
| 302 | + def update(self, new_env): |
| 303 | + """Update contents of environment from given dictionary""" |
| 304 | + try: |
| 305 | + for envar_name, envar_contents in new_env.items(): |
| 306 | + setattr(self, envar_name, envar_contents) |
| 307 | + except AttributeError as err: |
| 308 | + raise EasyBuildError("Cannot update ModuleLoadEnvironment from a non-dict variable") from err |
| 309 | + |
| 310 | + @property |
| 311 | + def as_dict(self): |
| 312 | + """ |
| 313 | + Return dict with mapping of ModuleEnvironmentVariables names with their contents |
| 314 | + """ |
| 315 | + return dict(self.items()) |
| 316 | + |
| 317 | + @property |
| 318 | + def environ(self): |
| 319 | + """ |
| 320 | + Return dict with mapping of ModuleEnvironmentVariables names with their contents |
| 321 | + Equivalent in shape to os.environ |
| 322 | + """ |
| 323 | + mapping = {} |
| 324 | + for envar_name, envar_contents in self.items(): |
| 325 | + mapping.update({envar_name: str(envar_contents)}) |
| 326 | + return mapping |
| 327 | + |
| 328 | + |
134 | 329 | class ModulesTool(object):
|
135 | 330 | """An abstract interface to a tool that deals with modules."""
|
136 | 331 | # name of this modules tool (used in log/warning/error messages)
|
|
0 commit comments