5252from easybuild .tools .modules import ROOT_ENV_VAR_NAME_PREFIX , EnvironmentModulesC , Lmod , modules_tool
5353from 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 """
0 commit comments