Skip to content

Commit a3ee7d4

Browse files
authored
Merge pull request #4653 from lexming/cpath-mod
refactor generation of required environment variables in module files + deprecate `make_module_req_guess` method in `EasyBlock` class
2 parents 3e28cb6 + 678ce99 commit a3ee7d4

File tree

8 files changed

+670
-111
lines changed

8 files changed

+670
-111
lines changed

easybuild/framework/easyblock.py

Lines changed: 132 additions & 97 deletions
Large diffs are not rendered by default.

easybuild/framework/extensioneasyblock.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs):
8989
self.installdir = self.master.installdir
9090
self.modules_tool = self.master.modules_tool
9191
self.module_generator = self.master.module_generator
92+
self.module_load_environment = self.master.module_load_environment
9293
self.robot_path = self.master.robot_path
9394
self.is_extension = True
9495
self.unpack_options = None

easybuild/tools/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,16 @@
168168
LOCAL_VAR_NAMING_CHECK_WARN = WARN
169169
LOCAL_VAR_NAMING_CHECKS = [LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG, LOCAL_VAR_NAMING_CHECK_WARN]
170170

171-
172171
OUTPUT_STYLE_AUTO = 'auto'
173172
OUTPUT_STYLE_BASIC = 'basic'
174173
OUTPUT_STYLE_NO_COLOR = 'no_color'
175174
OUTPUT_STYLE_RICH = 'rich'
176175
OUTPUT_STYLES = (OUTPUT_STYLE_AUTO, OUTPUT_STYLE_BASIC, OUTPUT_STYLE_NO_COLOR, OUTPUT_STYLE_RICH)
177176

177+
SEARCH_PATH_BIN_DIRS = ['bin']
178+
SEARCH_PATH_HEADER_DIRS = ['include']
179+
SEARCH_PATH_LIB_DIRS = ['lib', 'lib64']
180+
178181
PYTHONPATH = 'PYTHONPATH'
179182
EBPYTHONPREFIXES = 'EBPYTHONPREFIXES'
180183
PYTHON_SEARCH_PATH_TYPES = [PYTHONPATH, EBPYTHONPREFIXES]

easybuild/tools/modules.py

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@
4141
import os
4242
import re
4343
import shlex
44+
from enum import Enum
4445

4546
from easybuild.base import fancylogger
4647
from easybuild.tools import LooseVersion
4748
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
5051
from easybuild.tools.config import build_option, get_modules_tool, install_path
5152
from easybuild.tools.environment import ORIG_OS_ENVIRON, restore_env, setvar, unset_env_vars
5253
from easybuild.tools.filetools import convert_name, mkdir, normalize_path, path_matches, read_file, which, write_file
@@ -55,6 +56,7 @@
5556
from easybuild.tools.systemtools import get_shared_lib_ext
5657
from easybuild.tools.utilities import get_subclasses, nub
5758

59+
5860
# software root/version environment variable name prefixes
5961
ROOT_ENV_VAR_NAME_PREFIX = "EBROOT"
6062
VERSION_ENV_VAR_NAME_PREFIX = "EBVERSION"
@@ -131,6 +133,199 @@
131133
_log = fancylogger.getLogger('modules', fname=False)
132134

133135

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+
134329
class ModulesTool(object):
135330
"""An abstract interface to a tool that deals with modules."""
136331
# name of this modules tool (used in log/warning/error messages)

0 commit comments

Comments
 (0)