Skip to content

Commit 76d4e97

Browse files
authored
Merge pull request #4855 from lexming/modextra-envars
add support for environment variables in `set_environment` of module generators
2 parents 4b57587 + 3ce7693 commit 76d4e97

File tree

4 files changed

+150
-57
lines changed

4 files changed

+150
-57
lines changed

easybuild/tools/module_generator.py

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
from easybuild.tools.modules import ROOT_ENV_VAR_NAME_PREFIX, EnvironmentModulesC, Lmod, modules_tool
5353
from easybuild.tools.utilities import get_subclasses, nub, quote_str
5454

55-
5655
_log = fancylogger.getLogger('module_generator', fname=False)
5756

5857

@@ -133,6 +132,15 @@ class ModuleGenerator:
133132
# a single level of indentation
134133
INDENTATION = ' ' * 4
135134

135+
# shell environment variable name: ${__}VAR_NAME_00_SUFFIX
136+
REGEX_SHELL_VAR_PATTERN = r'[A-Z_]+[A-Z0-9_]+'
137+
REGEX_SHELL_VAR = re.compile(rf'\$({REGEX_SHELL_VAR_PATTERN})')
138+
REGEX_QUOTE_SHELL_VAR = re.compile(rf'[\"\']\$({REGEX_SHELL_VAR_PATTERN})[\"\']')
139+
140+
# default options for modextravars
141+
DEFAULT_MODEXTRAVARS_USE_PUSHENV = False
142+
DEFAULT_MODEXTRAVARS_RESOLVE_ENV_VARS = True
143+
136144
def __init__(self, application, fake=False):
137145
"""ModuleGenerator constructor."""
138146
self.app = application
@@ -422,28 +430,38 @@ def det_installdir(self, modfile):
422430

423431
return res
424432

425-
def unpack_setenv_value(self, env_var_name, env_var_val):
433+
def unpack_setenv_value(self, *args, **kwargs):
434+
"""
435+
DEPRECATED method, should not be used.
436+
Replaced with (internal) _unpack_setenv_value method.
437+
"""
438+
self.log.deprecated("unpack_setenv_value should not be used directly (replaced by internal method)", '6.0')
439+
value, use_pushenv, _ = self._unpack_setenv_value(*args, **kwargs)
440+
return value, use_pushenv
441+
442+
def _unpack_setenv_value(self, env_var_name, env_var_val):
426443
"""
427444
Unpack value that specifies how to define an environment variable with specified name.
428445
"""
429-
use_pushenv = False
446+
use_pushenv = self.DEFAULT_MODEXTRAVARS_USE_PUSHENV
447+
resolve_env_vars = self.DEFAULT_MODEXTRAVARS_RESOLVE_ENV_VARS
430448

431449
# value may be specified as a string, or as a dict for special cases
432450
if isinstance(env_var_val, str):
433451
value = env_var_val
434-
435452
elif isinstance(env_var_val, dict):
436-
use_pushenv = env_var_val.get('pushenv', False)
453+
use_pushenv = env_var_val.get('pushenv', self.DEFAULT_MODEXTRAVARS_USE_PUSHENV)
454+
resolve_env_vars = env_var_val.get('resolve_env_vars', self.DEFAULT_MODEXTRAVARS_RESOLVE_ENV_VARS)
437455
try:
438456
value = env_var_val['value']
439-
except KeyError:
457+
except KeyError as err:
440458
raise EasyBuildError("Required key 'value' is missing in dict that specifies how to set $%s: %s",
441-
env_var_name, env_var_val)
459+
env_var_name, env_var_val) from err
442460
else:
443461
raise EasyBuildError("Incorrect value type for setting $%s environment variable (%s): %s",
444462
env_var_name, type(env_var_val), env_var_val)
445463

446-
return value, use_pushenv
464+
return value, use_pushenv, resolve_env_vars
447465

448466
# From this point on just not implemented methods
449467

@@ -1056,19 +1074,19 @@ def set_environment(self, key, value, relpath=False):
10561074
self.log.info("Not including statement to define environment variable $%s, as specified", key)
10571075
return ''
10581076

1059-
value, use_pushenv = self.unpack_setenv_value(key, value)
1077+
set_value, use_pushenv, resolve_env_vars = self._unpack_setenv_value(key, value)
10601078

1061-
# quotes are needed, to ensure smooth working of EBDEVEL* modulefiles
10621079
if relpath:
1063-
if value:
1064-
val = quote_str(os.path.join('$root', value), tcl=True)
1065-
else:
1066-
val = '"$root"'
1067-
else:
1068-
val = quote_str(value, tcl=True)
1080+
set_value = os.path.join('$root', set_value) if set_value else '$root'
1081+
1082+
if resolve_env_vars:
1083+
set_value = self.REGEX_SHELL_VAR.sub(r'$::env(\1)', set_value)
1084+
1085+
# quotes are needed, to ensure smooth working of EBDEVEL* modulefiles
1086+
set_value = quote_str(set_value, tcl=True)
10691087

10701088
env_setter = 'pushenv' if use_pushenv else 'setenv'
1071-
return '%s\t%s\t\t%s\n' % (env_setter, key, val)
1089+
return f'{env_setter}\t{key}\t\t{set_value}\n'
10721090

10731091
def swap_module(self, mod_name_out, mod_name_in, guarded=True):
10741092
"""
@@ -1152,12 +1170,14 @@ class ModuleGeneratorLua(ModuleGenerator):
11521170
LOAD_TEMPLATE_DEPENDS_ON = 'depends_on("%(mod_name)s")'
11531171
IS_LOADED_TEMPLATE = 'isloaded("%s")'
11541172

1173+
OS_GETENV_TEMPLATE = r'os.getenv("%s")'
11551174
PATH_JOIN_TEMPLATE = 'pathJoin(root, "%s")'
11561175
UPDATE_PATH_TEMPLATE = '%s_path("%s", %s)'
11571176
UPDATE_PATH_TEMPLATE_DELIM = '%s_path("%s", %s, "%s")'
11581177

11591178
START_STR = '[==['
11601179
END_STR = ']==]'
1180+
CONCAT_STR = ' .. '
11611181

11621182
def __init__(self, *args, **kwargs):
11631183
"""ModuleGeneratorLua constructor."""
@@ -1167,6 +1187,20 @@ def __init__(self, *args, **kwargs):
11671187
if self.modules_tool.version and LooseVersion(self.modules_tool.version) >= LooseVersion('7.7.38'):
11681188
self.DOT_MODULERC = '.modulerc.lua'
11691189

1190+
@staticmethod
1191+
def _path_join_cmd(path):
1192+
"Return 'pathJoin' command for given path string"
1193+
path_components = [quote_str(p) for p in path.split(os.path.sep) if p]
1194+
1195+
path_root = quote_str(os.path.sep) if os.path.isabs(path) else 'root'
1196+
path_components.insert(0, path_root)
1197+
1198+
if len(path_components) > 1:
1199+
return 'pathJoin(' + ', '.join(path_components) + ')'
1200+
1201+
# no need for a pathJoin for single component paths
1202+
return path_components[0]
1203+
11701204
def check_version(self, minimal_version_maj, minimal_version_min, minimal_version_patch='0'):
11711205
"""
11721206
Check the minimal version of the moduletool in the module file
@@ -1292,10 +1326,9 @@ def getenv_cmd(self, envvar, default=None):
12921326
"""
12931327
Return module-syntax specific code to get value of specific environment variable.
12941328
"""
1295-
if default is None:
1296-
cmd = 'os.getenv("%s")' % envvar
1297-
else:
1298-
cmd = 'os.getenv("%s") or "%s"' % (envvar, default)
1329+
cmd = self.OS_GETENV_TEMPLATE % envvar
1330+
if default is not None:
1331+
cmd += f' or "{default}"'
12991332
return cmd
13001333

13011334
def load_module(self, mod_name, recursive_unload=None, depends_on=None, unload_modules=None, multi_dep_mods=None):
@@ -1448,7 +1481,7 @@ def update_paths(self, key, paths, prepend=True, allow_abs=False, expand_relpath
14481481
# use pathJoin for (non-empty) relative paths
14491482
if path:
14501483
if expand_relpaths:
1451-
abspaths.append(self.PATH_JOIN_TEMPLATE % path)
1484+
abspaths.append(self._path_join_cmd(path))
14521485
else:
14531486
abspaths.append(quote_str(path))
14541487
else:
@@ -1513,19 +1546,28 @@ def set_environment(self, key, value, relpath=False):
15131546
self.log.info("Not including statement to define environment variable $%s, as specified", key)
15141547
return ''
15151548

1516-
value, use_pushenv = self.unpack_setenv_value(key, value)
1549+
set_value, use_pushenv, resolve_env_vars = self._unpack_setenv_value(key, value)
15171550

15181551
if relpath:
1519-
if value:
1520-
val = self.PATH_JOIN_TEMPLATE % value
1521-
else:
1522-
val = 'root'
1552+
set_value = self._path_join_cmd(set_value)
1553+
if resolve_env_vars:
1554+
# replace quoted substring with env var with os.getenv statement
1555+
# example: pathJoin(root, "$HOME") -> pathJoin(root, os.getenv("HOME"))
1556+
set_value = self.REGEX_QUOTE_SHELL_VAR.sub(self.OS_GETENV_TEMPLATE % r"\1", set_value)
15231557
else:
1524-
val = quote_str(value)
1558+
if resolve_env_vars:
1559+
# replace env var with os.getenv statement
1560+
# example: $HOME -> os.getenv("HOME")
1561+
concat_getenv = self.CONCAT_STR + self.OS_GETENV_TEMPLATE % r"\1" + self.CONCAT_STR
1562+
set_value = self.REGEX_SHELL_VAR.sub(concat_getenv, set_value)
1563+
set_value = self.CONCAT_STR.join([
1564+
# quote any substrings that are not a os.getenv Lua statement
1565+
x if x.startswith(self.OS_GETENV_TEMPLATE[:10]) else quote_str(x)
1566+
for x in set_value.strip(self.CONCAT_STR).split(self.CONCAT_STR)
1567+
])
15251568

15261569
env_setter = 'pushenv' if use_pushenv else 'setenv'
1527-
1528-
return '%s("%s", %s)\n' % (env_setter, key, val)
1570+
return f'{env_setter}("{key}", {set_value})\n'
15291571

15301572
def swap_module(self, mod_name_out, mod_name_in, guarded=True):
15311573
"""

test/framework/easyblock.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def test_make_module_req(self):
462462
elif get_module_syntax() == 'Lua':
463463
self.assertTrue(re.search(r'^prepend_path\("CLASSPATH", pathJoin\(root, "bla.jar"\)\)$', guess, re.M))
464464
self.assertTrue(re.search(r'^prepend_path\("CLASSPATH", pathJoin\(root, "foo.jar"\)\)$', guess, re.M))
465-
self.assertTrue(re.search(r'^prepend_path\("MANPATH", pathJoin\(root, "share/man"\)\)$', guess, re.M))
465+
self.assertTrue(re.search(r'^prepend_path\("MANPATH", pathJoin\(root, "share", "man"\)\)$', guess, re.M))
466466
self.assertIn('prepend_path("CMAKE_PREFIX_PATH", root)', guess)
467467
# bin/ is not added to $PATH if it doesn't include files
468468
self.assertFalse(re.search(r'^prepend_path\("PATH", pathJoin\(root, "bin"\)\)$', guess, re.M))
@@ -579,12 +579,12 @@ def test_make_module_req(self):
579579
r"prepend-path\s+LD_LIBRARY_PATH\s+\$root/lib/pathA\n",
580580
txt, re.M))
581581
elif get_module_syntax() == 'Lua':
582-
self.assertTrue(re.search(r'\nprepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib/pathC"\)\)\n' +
583-
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib/pathA"\)\)\n' +
584-
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib/pathB"\)\)\n',
582+
self.assertTrue(re.search(r'\nprepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib", "pathC"\)\)\n' +
583+
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib", "pathA"\)\)\n' +
584+
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib", "pathB"\)\)\n',
585585
txt, re.M))
586-
self.assertFalse(re.search(r'\nprepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib/pathB"\)\)\n' +
587-
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib/pathA"\)\)\n',
586+
self.assertFalse(re.search(r'\nprepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib", "pathB"\)\)\n' +
587+
r'prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "lib", "pathA"\)\)\n',
588588
txt, re.M))
589589
else:
590590
self.fail("Unknown module syntax: %s" % get_module_syntax())
@@ -657,10 +657,12 @@ def test_make_module_req(self):
657657
self.assertTrue(re.search(r"^prepend-path\s+LD_LIBRARY_PATH\s+\$root/libraries/intel64_lin$", txt, re.M))
658658
self.assertTrue(re.search(r"^prepend-path\s+LIBRARY_PATH\s+\$root/libraries/intel64_lin\n$", txt, re.M))
659659
elif get_module_syntax() == 'Lua':
660-
self.assertTrue(re.search(r'^prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "libraries/intel64_lin"\)\)$',
661-
txt, re.M))
662-
self.assertTrue(re.search(r'^prepend_path\("LIBRARY_PATH", pathJoin\(root, "libraries/intel64_lin"\)\)$',
663-
txt, re.M))
660+
self.assertTrue(re.search(
661+
r'^prepend_path\("LD_LIBRARY_PATH", pathJoin\(root, "libraries", "intel64_lin"\)\)$', txt, re.M
662+
))
663+
self.assertTrue(re.search(
664+
r'^prepend_path\("LIBRARY_PATH", pathJoin\(root, "libraries", "intel64_lin"\)\)$', txt, re.M
665+
))
664666
else:
665667
self.fail("Unknown module syntax: %s" % get_module_syntax())
666668

@@ -718,7 +720,7 @@ def test_make_module_req(self):
718720
expected_patterns = [
719721
r"^append[-_]path.*TEST_VAR_CUSTOM.*root.*foo.*",
720722
r"^prepend[-_]path.*CPATH.*root.*include.*",
721-
r"^prepend[-_]path.*CPATH.*root.*include/foo.*",
723+
r"^prepend[-_]path.*CPATH.*root.*include.*foo.*",
722724
r"^prepend[-_]path.*LD_LIBRARY_PATH.*root.*lib",
723725
r"^prepend[-_]path.*LD_LIBRARY_PATH.*root.*foo",
724726
r"^prepend[-_]path.*TEST_VAR.*root.*foo",
@@ -874,12 +876,12 @@ def test_make_module_extra(self):
874876
expected_default = re.compile(r'\n'.join([
875877
r'setenv\("EBROOTPI", root\)',
876878
r'setenv\("EBVERSIONPI", "3.14"\)',
877-
r'setenv\("EBDEVELPI", pathJoin\(root, "easybuild/pi-3.14-gompi-2018a-easybuild-devel"\)\)',
879+
r'setenv\("EBDEVELPI", pathJoin\(root, "easybuild", "pi-3.14-gompi-2018a-easybuild-devel"\)\)',
878880
]))
879881
expected_alt = re.compile(r'\n'.join([
880882
r'setenv\("EBROOTPI", "/opt/software/tau/6.28"\)',
881883
r'setenv\("EBVERSIONPI", "6.28"\)',
882-
r'setenv\("EBDEVELPI", pathJoin\(root, "easybuild/pi-3.14-gompi-2018a-easybuild-devel"\)\)',
884+
r'setenv\("EBDEVELPI", pathJoin\(root, "easybuild", "pi-3.14-gompi-2018a-easybuild-devel"\)\)',
883885
]))
884886
else:
885887
self.fail("Unknown module syntax: %s" % get_module_syntax())
@@ -1665,7 +1667,7 @@ def test_make_module_step(self):
16651667
if val == '':
16661668
full_val = 'root'
16671669
else:
1668-
full_val = fr'pathJoin\(root, "{val}"\)'
1670+
full_val = fr'pathJoin\(root, "{val.replace("/", ".*")}"\)'
16691671
regex = re.compile(fr'^{placement}_path\("{key}", {full_val}{delim_lua}\)$', re.M)
16701672
else:
16711673
self.fail(f"Unknown module syntax: {get_module_syntax()}")

0 commit comments

Comments
 (0)