Skip to content

Commit 4cde629

Browse files
authored
Merge pull request #3770 from Flamefire/duplicate_path_module
Filter out duplicate paths added to module files
2 parents 64b26cd + 146773e commit 4cde629

File tree

4 files changed

+144
-62
lines changed

4 files changed

+144
-62
lines changed

easybuild/framework/easyblock.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2451,7 +2451,8 @@ def extensions_step(self, fetch=False, install=True):
24512451
if install:
24522452
try:
24532453
ext.prerun()
2454-
txt = ext.run()
2454+
with self.module_generator.start_module_creation():
2455+
txt = ext.run()
24552456
if txt:
24562457
self.module_extra_extensions += txt
24572458
ext.postrun()
@@ -3222,21 +3223,18 @@ def make_module_step(self, fake=False):
32223223
else:
32233224
trace_msg("generating module file @ %s" % self.mod_filepath)
32243225

3225-
txt = self.module_generator.MODULE_SHEBANG
3226-
if txt:
3227-
txt += '\n'
3228-
3229-
if self.modules_header:
3230-
txt += self.modules_header + '\n'
3231-
3232-
txt += self.make_module_description()
3233-
txt += self.make_module_group_check()
3234-
txt += self.make_module_deppaths()
3235-
txt += self.make_module_dep()
3236-
txt += self.make_module_extend_modpath()
3237-
txt += self.make_module_req()
3238-
txt += self.make_module_extra()
3239-
txt += self.make_module_footer()
3226+
with self.module_generator.start_module_creation() as txt:
3227+
if self.modules_header:
3228+
txt += self.modules_header + '\n'
3229+
3230+
txt += self.make_module_description()
3231+
txt += self.make_module_group_check()
3232+
txt += self.make_module_deppaths()
3233+
txt += self.make_module_dep()
3234+
txt += self.make_module_extend_modpath()
3235+
txt += self.make_module_req()
3236+
txt += self.make_module_extra()
3237+
txt += self.make_module_footer()
32403238

32413239
hook_txt = run_hook(MODULE_WRITE, self.hooks, args=[self, mod_filepath, txt])
32423240
if hook_txt is not None:

easybuild/tools/module_generator.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@
3737
import os
3838
import re
3939
import tempfile
40+
from contextlib import contextmanager
4041
from distutils.version import LooseVersion
4142
from textwrap import wrap
4243

4344
from easybuild.base import fancylogger
44-
from easybuild.tools.build_log import EasyBuildError
45+
from easybuild.tools.build_log import EasyBuildError, print_warning
4546
from easybuild.tools.config import build_option, get_module_syntax, install_path
4647
from easybuild.tools.filetools import convert_name, mkdir, read_file, remove_file, resolve_path, symlink, write_file
4748
from easybuild.tools.modules import ROOT_ENV_VAR_NAME_PREFIX, EnvironmentModulesC, Lmod, modules_tool
@@ -136,17 +137,29 @@ def __init__(self, application, fake=False):
136137
self.fake_mod_path = tempfile.mkdtemp()
137138

138139
self.modules_tool = modules_tool()
140+
self.added_paths_per_key = None
139141

140-
def append_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
142+
@contextmanager
143+
def start_module_creation(self):
141144
"""
142-
Generate append-path statements for the given list of paths.
145+
Prepares creating a module and returns the file header (shebang) if any including the newline
143146
144-
:param key: environment variable to append paths to
145-
:param paths: list of paths to append
146-
:param allow_abs: allow providing of absolute paths
147-
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
147+
Meant to be used in a with statement:
148+
with generator.start_module_creation() as txt:
149+
# Write txt
148150
"""
149-
return self.update_paths(key, paths, prepend=False, allow_abs=allow_abs, expand_relpaths=expand_relpaths)
151+
if self.added_paths_per_key is not None:
152+
raise EasyBuildError('Module creation already in process. '
153+
'You cannot create multiple modules at the same time!')
154+
# Mapping of keys/env vars to paths already added
155+
self.added_paths_per_key = dict()
156+
txt = self.MODULE_SHEBANG
157+
if txt:
158+
txt += '\n'
159+
try:
160+
yield txt
161+
finally:
162+
self.added_paths_per_key = None
150163

151164
def create_symlinks(self, mod_symlink_paths, fake=False):
152165
"""Create moduleclass symlink(s) to actual module file."""
@@ -191,6 +204,49 @@ def get_modules_path(self, fake=False, mod_path_suffix=None):
191204

192205
return os.path.join(mod_path, mod_path_suffix)
193206

207+
def _filter_paths(self, key, paths):
208+
"""Filter out paths already added to key and return the remaining ones"""
209+
if self.added_paths_per_key is None:
210+
# For compatibility this is only a warning for now and we don't filter any paths
211+
print_warning('Module creation has not been started. Call start_module_creation first!')
212+
return paths
213+
214+
added_paths = self.added_paths_per_key.setdefault(key, set())
215+
# paths can be a string
216+
if isinstance(paths, string_type):
217+
if paths in added_paths:
218+
filtered_paths = None
219+
else:
220+
added_paths.add(paths)
221+
filtered_paths = paths
222+
else:
223+
# Coerce any iterable/generator into a list
224+
if not isinstance(paths, list):
225+
paths = list(paths)
226+
filtered_paths = [x for x in paths if x not in added_paths and not added_paths.add(x)]
227+
if filtered_paths != paths:
228+
removed_paths = paths if filtered_paths is None else [x for x in paths if x not in filtered_paths]
229+
print_warning("Supressed adding the following path(s) to $%s of the module as they were already added: %s",
230+
key, removed_paths,
231+
log=self.log)
232+
if not filtered_paths:
233+
filtered_paths = None
234+
return filtered_paths
235+
236+
def append_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
237+
"""
238+
Generate append-path statements for the given list of paths.
239+
240+
:param key: environment variable to append paths to
241+
:param paths: list of paths to append
242+
:param allow_abs: allow providing of absolute paths
243+
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
244+
"""
245+
paths = self._filter_paths(key, paths)
246+
if paths is None:
247+
return ''
248+
return self.update_paths(key, paths, prepend=False, allow_abs=allow_abs, expand_relpaths=expand_relpaths)
249+
194250
def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
195251
"""
196252
Generate prepend-path statements for the given list of paths.
@@ -200,6 +256,9 @@ def prepend_paths(self, key, paths, allow_abs=False, expand_relpaths=True):
200256
:param allow_abs: allow providing of absolute paths
201257
:param expand_relpaths: expand relative paths into absolute paths (by prefixing install dir)
202258
"""
259+
paths = self._filter_paths(key, paths)
260+
if paths is None:
261+
return ''
203262
return self.update_paths(key, paths, prepend=True, allow_abs=allow_abs, expand_relpaths=expand_relpaths)
204263

205264
def _modulerc_check_module_version(self, module_version):

test/framework/easyblock.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ def test_make_module_req(self):
435435
# this is not a path that should be picked up
436436
os.mkdir(os.path.join(eb.installdir, 'CPATH'))
437437

438-
guess = eb.make_module_req()
438+
with eb.module_generator.start_module_creation():
439+
guess = eb.make_module_req()
439440

440441
if get_module_syntax() == 'Tcl':
441442
self.assertTrue(re.search(r"^prepend-path\s+CLASSPATH\s+\$root/bla.jar$", guess, re.M))
@@ -462,7 +463,8 @@ def test_make_module_req(self):
462463

463464
# check that bin is only added to PATH if there are files in there
464465
write_file(os.path.join(eb.installdir, 'bin', 'test'), 'test')
465-
guess = eb.make_module_req()
466+
with eb.module_generator.start_module_creation():
467+
guess = eb.make_module_req()
466468
if get_module_syntax() == 'Tcl':
467469
self.assertTrue(re.search(r"^prepend-path\s+PATH\s+\$root/bin$", guess, re.M))
468470
self.assertFalse(re.search(r"^prepend-path\s+PATH\s+\$root/sbin$", guess, re.M))
@@ -481,7 +483,8 @@ def test_make_module_req(self):
481483
self.assertFalse('prepend_path("CMAKE_LIBRARY_PATH", pathJoin(root, "lib64"))' in guess)
482484
# -- With files
483485
write_file(os.path.join(eb.installdir, 'lib64', 'libfoo.so'), 'test')
484-
guess = eb.make_module_req()
486+
with eb.module_generator.start_module_creation():
487+
guess = eb.make_module_req()
485488
if get_module_syntax() == 'Tcl':
486489
self.assertTrue(re.search(r"^prepend-path\s+CMAKE_LIBRARY_PATH\s+\$root/lib64$", guess, re.M))
487490
elif get_module_syntax() == 'Lua':
@@ -490,7 +493,8 @@ def test_make_module_req(self):
490493
write_file(os.path.join(eb.installdir, 'lib', 'libfoo.so'), 'test')
491494
shutil.rmtree(os.path.join(eb.installdir, 'lib64'))
492495
os.symlink('lib', os.path.join(eb.installdir, 'lib64'))
493-
guess = eb.make_module_req()
496+
with eb.module_generator.start_module_creation():
497+
guess = eb.make_module_req()
494498
if get_module_syntax() == 'Tcl':
495499
self.assertFalse(re.search(r"^prepend-path\s+CMAKE_LIBRARY_PATH\s+\$root/lib64$", guess, re.M))
496500
elif get_module_syntax() == 'Lua':
@@ -509,7 +513,8 @@ def test_make_module_req(self):
509513

510514
# check for behavior when a string value is used as dict value by make_module_req_guesses
511515
eb.make_module_req_guess = lambda: {'PATH': 'bin'}
512-
txt = eb.make_module_req()
516+
with eb.module_generator.start_module_creation():
517+
txt = eb.make_module_req()
513518
if get_module_syntax() == 'Tcl':
514519
self.assertTrue(re.match(r"^\nprepend-path\s+PATH\s+\$root/bin\n$", txt, re.M))
515520
elif get_module_syntax() == 'Lua':
@@ -520,7 +525,8 @@ def test_make_module_req(self):
520525
# check for correct behaviour if empty string is specified as one of the values
521526
# prepend-path statements should be included for both the 'bin' subdir and the install root
522527
eb.make_module_req_guess = lambda: {'PATH': ['bin', '']}
523-
txt = eb.make_module_req()
528+
with eb.module_generator.start_module_creation():
529+
txt = eb.make_module_req()
524530
if get_module_syntax() == 'Tcl':
525531
self.assertTrue(re.search(r"\nprepend-path\s+PATH\s+\$root/bin\n", txt, re.M))
526532
self.assertTrue(re.search(r"\nprepend-path\s+PATH\s+\$root\n", txt, re.M))
@@ -535,7 +541,8 @@ def test_make_module_req(self):
535541
for path in ['pathA', 'pathB', 'pathC']:
536542
os.mkdir(os.path.join(eb.installdir, 'lib', path))
537543
write_file(os.path.join(eb.installdir, 'lib', path, 'libfoo.so'), 'test')
538-
txt = eb.make_module_req()
544+
with eb.module_generator.start_module_creation():
545+
txt = eb.make_module_req()
539546
if get_module_syntax() == 'Tcl':
540547
self.assertTrue(re.search(r"\nprepend-path\s+LD_LIBRARY_PATH\s+\$root/lib/pathC\n" +
541548
r"prepend-path\s+LD_LIBRARY_PATH\s+\$root/lib/pathA\n" +
@@ -1152,7 +1159,7 @@ def test_make_module_step(self):
11521159
# purposely use a 'nasty' description, that includes (unbalanced) special chars: [, ], {, }
11531160
descr = "This {is a}} [fancy]] [[description]]. {{[[TEST}]"
11541161
modextravars = {'PI': '3.1415', 'FOO': 'bar'}
1155-
modextrapaths = {'PATH': 'pibin', 'CPATH': 'pi/include'}
1162+
modextrapaths = {'PATH': ('bin', 'pibin'), 'CPATH': 'pi/include'}
11561163
self.contents = '\n'.join([
11571164
'easyblock = "ConfigureMake"',
11581165
'name = "%s"' % name,
@@ -1177,6 +1184,10 @@ def test_make_module_step(self):
11771184
eb.make_builddir()
11781185
eb.prepare_step()
11791186

1187+
# Create a dummy file in bin to test if the duplicate entry of modextrapaths is ignored
1188+
os.makedirs(os.path.join(eb.installdir, 'bin'))
1189+
write_file(os.path.join(eb.installdir, 'bin', 'dummy_exe'), 'hello')
1190+
11801191
modpath = os.path.join(eb.make_module_step(), name, version)
11811192
if get_module_syntax() == 'Lua':
11821193
modpath += '.lua'
@@ -1210,14 +1221,20 @@ def test_make_module_step(self):
12101221
self.assertTrue(False, "Unknown module syntax: %s" % get_module_syntax())
12111222
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
12121223

1213-
for (key, val) in modextrapaths.items():
1214-
if get_module_syntax() == 'Tcl':
1215-
regex = re.compile(r'^prepend-path\s+%s\s+\$root/%s$' % (key, val), re.M)
1216-
elif get_module_syntax() == 'Lua':
1217-
regex = re.compile(r'^prepend_path\("%s", pathJoin\(root, "%s"\)\)$' % (key, val), re.M)
1218-
else:
1219-
self.assertTrue(False, "Unknown module syntax: %s" % get_module_syntax())
1220-
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
1224+
for (key, vals) in modextrapaths.items():
1225+
if isinstance(vals, string_type):
1226+
vals = [vals]
1227+
for val in vals:
1228+
if get_module_syntax() == 'Tcl':
1229+
regex = re.compile(r'^prepend-path\s+%s\s+\$root/%s$' % (key, val), re.M)
1230+
elif get_module_syntax() == 'Lua':
1231+
regex = re.compile(r'^prepend_path\("%s", pathJoin\(root, "%s"\)\)$' % (key, val), re.M)
1232+
else:
1233+
self.assertTrue(False, "Unknown module syntax: %s" % get_module_syntax())
1234+
self.assertTrue(regex.search(txt), "Pattern %s found in %s" % (regex.pattern, txt))
1235+
# Check for duplicates
1236+
num_prepends = len(regex.findall(txt))
1237+
self.assertEqual(num_prepends, 1, "Expected exactly 1 %s command in %s" % (regex.pattern, txt))
12211238

12221239
for (name, ver) in [('GCC', '6.4.0-2.28')]:
12231240
if get_module_syntax() == 'Tcl':

0 commit comments

Comments
 (0)