-
Notifications
You must be signed in to change notification settings - Fork 202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
interpret statements that extend $MODULEPATH in modpath_extensions_for method #2104
interpret statements that extend $MODULEPATH in modpath_extensions_for method #2104
Conversation
This way lmod can do some parsing of the lua file, when the MODULEPATH extension uses environment variables.
(we expand MODULEPATH based on an "ARCH" environment variable) |
The "if" logic works correctly now but it really tested for the modules to exist in $HOME during the testcase which is not appropriate. The reason the test worked previously is that the MODULEPATH extension was taken unconditionally -- however that does not work if the syntax is more complex as in e.g. prepend_path("MODULEPATH", pathJoin(os.getenv("HOME"), "Public/easybuild/modules/Compiler/GCCcore/6.3.0")) without parsing this was literally taken as the string 'pathJoin(os.getenv("HOME"), "Public/easybuild/modules/Compiler/GCCcore/6.3.0")'
@bartoldeman Still one failing test:
|
easybuild/tools/modules.py
Outdated
@@ -711,15 +711,6 @@ def loaded_modules(self): | |||
|
|||
return loaded_modules | |||
|
|||
def read_module_file(self, mod_name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bartoldeman I'm not sure if we should remove this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is dead code now so it could be removed in principle.
easybuild/tools/modules.py
Outdated
@@ -747,7 +738,9 @@ def modpath_extensions_for(self, mod_names): | |||
|
|||
modpath_exts = {} | |||
for mod_name in mod_names: | |||
modtxt = self.read_module_file(mod_name) | |||
if not self.exist([mod_name], skip_avail=True)[0]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will this (still) work for hidden modules?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes: read_module_name() called modulefile_path() which calls get_value_from_modulefile() which also called exist() in the same way.
test/framework/modules.py
Outdated
# we cannot use mod_dir = os.environ['HOME'] here since that may not exist in the test | ||
# environment | ||
"if { [ file isdirectory %s/Compiler/GCC/4.7.2 ] } {" % mod_dir, | ||
" module use %s/Compiler/GCC/4.7.2" % mod_dir, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can redefine $HOME
in the test to os.path.join(self.test_prefix, 'HOME')
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Will do that later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
We can redefine $HOME in the test to os.path.join(self.test_prefix, 'HOME') as suggested by @boegel.
…extensions Travis reported that modulecmd.tcl does not include the extensions in its module show output. Therefore the module show output is only valid if there is at least one match.
I am hitting an issue where "module show" does not show "module use" statements for modulecmd.tcl (it does show prepend-path MODULEPATH ... however). So somehow the code needs to look at the actual modulefile (to scan module use) and at the parsed output of "module show" and somehow merge those. |
... or only consider "module show" if Lmod is used so we get parsed lua syntax. |
easybuild/tools/modules.py
Outdated
@@ -750,6 +750,14 @@ def modpath_extensions_for(self, mod_names): | |||
modtxt = self.read_module_file(mod_name) | |||
exts = [ext for tup in modpath_ext_regex.findall(modtxt) for ext in tup if ext] | |||
self.log.debug("Found $MODULEPATH extensions for %s: %s", mod_name, exts) | |||
if exts: | |||
modtxt = self.show(mod_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
include comment to explain why you're going through show
, since you already have the list of $MODULEPATH
extensions?
test/framework/modules.py
Outdated
@@ -404,6 +404,10 @@ def test_modpath_extensions_for(self): | |||
error_pattern = "Can't get value from a non-existing module" | |||
self.assertErrorRegex(EasyBuildError, error_pattern, self.modtool.modpath_extensions_for, ['nosuchmodule/1.2']) | |||
|
|||
realhome = os.environ.get('HOME') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to keep track of original $HOME
test/framework/modules.py
Outdated
@@ -404,6 +404,10 @@ def test_modpath_extensions_for(self): | |||
error_pattern = "Can't get value from a non-existing module" | |||
self.assertErrorRegex(EasyBuildError, error_pattern, self.modtool.modpath_extensions_for, ['nosuchmodule/1.2']) | |||
|
|||
realhome = os.environ.get('HOME') | |||
os.environ['HOME'] = os.path.join(self.test_prefix, 'HOME') | |||
os.makedirs("%s/modules/Compiler/GCC/4.7.2" % os.environ['HOME']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use mkdir(..., parents=True)
from filetools (which will print out a decent error rather than a traceback should a problem occur)
why did you have to make this change btw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because with "module show" the if statement in the module file is parsed, so for
if isDir("/home/foo/modules/Compiler/GCC/4.7.2") then
prepend_path("MODULEPATH", "/home/foo/modules/Compiler/GCC/4.7.2")
end
module show
will output just:
prepend_path("MODULEPATH", "/home/foo/modules/Compiler/GCC/4.7.2")
only if that directory exists, and nothing if it does not exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... so the test only passes if that directory exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, but what if the directory is created later? You still want to exclude the module that may start extending $MODULEPATH
with .../Compiler/GCC/4.7.2
in the future when the corresponding directory is created, no?
test/framework/modules.py
Outdated
if realhome is not None: | ||
os.environ['HOME'] = realhome | ||
else: | ||
del os.environ['HOME'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for this, the environment is restored after each test automatically
I will install modulecmd.tcl before making further changes, so I can see exactly what it does without having to rely on Travis only... May be a bit slow today though (need to watch the kids since we had 36cm of snow here and schools are closed) |
For future reference: given this input file:
"module show" gives this Lmod:
C-modules 3.2.9 (different system)
TCL modules:
so one could argue that TCL modules is buggy here and we need to workaround this bug by reading the modulefile directly and grepping for "module use" statements. Or perhaps a newer TCL modules version fixes the bug and one could up the minimum version of TCL modules required? on the other hand all module flavours correctly parse the if statement and expand environment variables in the show ouput. |
Note: TCL modules only very recently fixed this issue (Dec 2016): https://sourceforge.net/p/modules/modules-tcl/ci/2d7654383ee586732e3272b49c93e47d8093e369/ |
This is backwards compatible and avoids bugs in TCL modules < 1.661.
This matches the actual syntax used for subdir-user-modules.
I wonder if easybuild should use "prepend-path MODULEPATH " in generated TCL modulefiles instead of "module use", at least for the guarded extensions, to avoid the modulecmd.tcl issue? |
The pure Tcl environment modules implementation is a bit of a mess, since there are several variants around. Reliably checking the version is going to be quite difficult. We don't have any version checks right now for the Tcl-only env mods tool...
Maybe, but then what about modules that are already there and do have This was probably the initial reason why we are reading the module files themselves rather than using You mention that the motivation for this change is that you are extending |
test/framework/modules.py
Outdated
"if { [ file isdirectory %s/modules/Compiler/GCC/4.7.2 ] } {" % os.environ['HOME'], | ||
" module use %s/modules/Compiler/GCC/4.7.2" % os.environ['HOME'], | ||
"if { [ file isdirectory $env(HOME)/modules/Compiler/GCC/4.7.2 ] } {", | ||
' prepend-path MODULEPATH "$env(HOME)/modules/Compiler/GCC/4.7.2"', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with changing the test to dynamically rely on $HOME
, that's a better test indeed (especially with your $ARCH
use case in mind), but why change module use
to prepend-path
?
Is that required to make the test pass (for modulecmd.tcl
)? Because this doesn't match with how EasyBuild currently generates modules, it'll use module use
instead in Tcl modules...
easybuild/tools/modules.py
Outdated
modpath_ext_regex = re.compile(modpath_ext_regex, re.M) | ||
|
||
modpath_exts = {} | ||
for mod_name in mod_names: | ||
# we must include parsed MODULEPATH extensions where the module tool expands | ||
# environment variables, concatenates strings, etc. | ||
# The show output is actually sufficient except for modulecmd.tcl < 1.661 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is correct, I think we still want to include $MODULEPATH
extensions that are guarded, cfr. my other comment: if the directory is created at a later point, all of a sudden we'll have a module A again that loads a module B that extend the $MODULEPATH
with the location where module A resides, which doesn't make sense...
easybuild/tools/modules.py
Outdated
modtxt = self.read_module_file(mod_name) | ||
exts = [ext for tup in modpath_ext_regex.findall(modtxt) for ext in tup if ext] | ||
use_exts = modpath_ext_use_regex.findall(modtxt) | ||
modtxt = self.show(mod_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename this modtxt
to show_out
or something, to make the distinction clear?
easybuild/tools/modules.py
Outdated
use_exts = modpath_ext_use_regex.findall(modtxt) | ||
modtxt = self.show(mod_name) | ||
parsed_exts = [ext for tup in modpath_ext_regex.findall(modtxt) for ext in tup if ext] | ||
exts = use_exts + [ext for ext in parsed_exts if ext not in use_exts] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this, you'll sort of still have duplicates, just one with resolved variables and one without (for example), e.g. $env(HOME)/MPI/GCC/4.7.2/OpenMPI/1.6.4.2
and /home/example/MPI/GCC/4.7.2/OpenMPI/1.6.4.2
...
This may be OK, but it depends for what the result of modpath_extensions_for
is used.
In any case, we need to extend the dedicated for modpath_extensions_for
further to highlight this, if we go through with this change.
This avoids the issue if the directory does not exist and the issues with modulecmd.tcl and "module use".
To avoid all this issues I switched the code to read the original modulefile and manually expand environment variables / join paths for Lua. I might as well open a new PR since the diff versus the original code is smaller than before now? |
easybuild/tools/modules.py
Outdated
|
||
# join paths (Lua) | ||
if matches[i][3]: | ||
ext = os.path.join(*ext.replace(",","").replace('"','').split()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bartoldeman There's quite a lot going on here, and it looks quite tedious to me as well.
If we go through with this, we should flesh this out in a separate function, so we can test in isolation...
def interpret_lua_statement(txt):
"""Interpret raw Lua statement: resolve environment variables, join paths wher `pathJoin` is specified"""
return ...
...
exts = []
for raw_ext in raw_exts:
if lua:
ext = process_raw_lua(raw_ext)
else:
ext = process_raw_tcl(raw_ext)
exts.append(ext)
That being said, this looks... adventurous.
easybuild/tools/modules.py
Outdated
# Need to expand environment variables and join paths, e.g. when --subdir-user-modules is used | ||
for i, ext in enumerate(exts): | ||
|
||
if not matches[i][2]: # can skip plain literal Lua match |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's going on here... How can you use the indexes of exts
for matches
? This looks wrong, since exts
is basically a flattened list of the entries in matches
(which is a list of tuples with several empty entries), or I'm missing something...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It works here because the matches are mutually exclusive. So the matches list looks like this:
[('','','','match'),('match','','',''),...]
only one of the 4 is filled in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, but wouldn't it be clearer to just loop over matches
then?
for tcl_use, tcl_prepend, lua_prepend_str, lua_prepend_pathjoin in matches:
if tcl_use or tcl_prepend:
# Tcl statement
elif lua_prepend_pathjoin:
# Lua statement with pathJoin included
easybuild/tools/modules.py
Outdated
envlocend = ext.find(")", envlocstart+len(envstr)) | ||
if envlocend == -1: | ||
break | ||
envvar = ext[envlocstart+len(envstr):envlocend].strip('"') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just use a regular expression to extract the environment variable name?
(totally untested code below):
if lua:
env_regex = re.compile('os.getenv\("(.*)"\)')
else:
env_regex = re.compile('\$env\("(.*)"\)'
res = env_regex.search(ext)
while res:
envval = os.getenv(res.group(1), '')
ext = env_regex.sub(envval, ext)
res = env_regex.search(ext)
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok let me try with regex
easybuild/tools/modules.py
Outdated
@@ -742,14 +742,44 @@ def modpath_extensions_for(self, mod_names): | |||
r'^\s*module\s+use\s+"?([^"\s]+)"?', # 'module use' in Tcl module files | |||
r'^\s*prepend-path\s+MODULEPATH\s+"?([^"\s]+)"?', # prepend to $MODULEPATH in Tcl modules | |||
r'^\s*prepend_path\(\"MODULEPATH\",\s*\"(\S+)\"', # prepend to $MODULEPATH in Lua modules | |||
r'^\s*prepend_path\(\"MODULEPATH\",\s*pathJoin\((.+)\)\)', # prepend to $MODULEPATH in Lua modules using pathJoin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we're handling prepend_path("MODULEPATH", os.getenv("HOME"))
correctly yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no ... however the generated modulefiles won't have that.
Would be a nice to have though for consistency.
@bartoldeman No need to open a new PR, having the history of how we reached this point may be relevant in the future. |
Using shlex, regular expressions for environment variables, and named matches this is now more clearly and compactly written.
…d tests for them + fix some bugs
… in modpath_extensions_for
rename parse_raw_path_* methods to interpret_raw_path_*, add dedicated tests for them + fix some bugs
…se' actually exist
fix broken test, modulecmd.tcl requires that paths used for 'module use' actually exist
also interpret '[ file join x y ]' statements in interpret_raw_path_tcl
Going in to be included in EasyBuild v3.1.2, thanks @bartoldeman! |
This way lmod can do some parsing of the lua file, when the MODULEPATH
extension uses environment variables.