Skip to content

Commit 6e02e34

Browse files
authored
Merge pull request #4868 from lexming/fake-module-per-extension
Update fake module for each extension installed
2 parents 76e9033 + c840ea9 commit 6e02e34

File tree

6 files changed

+149
-99
lines changed

6 files changed

+149
-99
lines changed

easybuild/framework/easyblock.py

Lines changed: 69 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import time
5757
import traceback
5858
from concurrent.futures import ThreadPoolExecutor
59+
from contextlib import contextmanager
5960
from datetime import datetime
6061
from string import ascii_letters
6162
from textwrap import indent
@@ -1268,9 +1269,6 @@ def make_devel_module(self, create_in_builddir=False):
12681269

12691270
self.log.info("Making devel module...")
12701271

1271-
# load fake module
1272-
fake_mod_data = self.load_fake_module(purge=True)
1273-
12741272
header = self.module_generator.MODULE_SHEBANG
12751273
if header:
12761274
header += '\n'
@@ -1314,9 +1312,6 @@ def make_devel_module(self, create_in_builddir=False):
13141312
txt = ''.join([header] + load_lines + env_lines)
13151313
write_file(filename, txt)
13161314

1317-
# cleanup: unload fake module, remove fake module dir
1318-
self.clean_up_fake_module(fake_mod_data)
1319-
13201315
def make_module_deppaths(self):
13211316
"""
13221317
Add specific 'module use' actions to module file, in order to find
@@ -1646,7 +1641,7 @@ def make_module_group_check(self):
16461641

16471642
return txt
16481643

1649-
def make_module_req(self, fake=False):
1644+
def make_module_req(self):
16501645
"""
16511646
Generate the environment-variables required to run the module.
16521647
"""
@@ -1687,11 +1682,10 @@ def make_module_req(self, fake=False):
16871682
mod_lines.append(self.module_generator.comment(note))
16881683

16891684
for env_var, search_paths in env_var_requirements.items():
1690-
if self.dry_run or fake:
1685+
if self.dry_run:
16911686
# Don't expand globs or do any filtering for dry run
16921687
mod_req_paths = search_paths
1693-
if self.dry_run:
1694-
self.dry_run_msg(f" ${env_var}:{', '.join(mod_req_paths)}")
1688+
self.dry_run_msg(f" ${env_var}:{', '.join(mod_req_paths)}")
16951689
else:
16961690
mod_req_paths = [
16971691
expanded_path for unexpanded_path in search_paths
@@ -1888,7 +1882,6 @@ def load_fake_module(self, purge=False, extra_modules=None, verbose=False):
18881882
# load fake module
18891883
self.modules_tool.prepend_module_path(os.path.join(fake_mod_path, self.mod_subdir), priority=10000)
18901884
self.load_module(purge=purge, extra_modules=extra_modules, verbose=verbose)
1891-
18921885
return (fake_mod_path, env)
18931886

18941887
def clean_up_fake_module(self, fake_mod_data):
@@ -1972,10 +1965,11 @@ def skip_extensions(self):
19721965
if not exts_filter or len(exts_filter) == 0:
19731966
raise EasyBuildError("Skipping of extensions, but no exts_filter set in easyconfig")
19741967

1975-
if build_option('parallel_extensions_install'):
1976-
self.skip_extensions_parallel(exts_filter)
1977-
else:
1978-
self.skip_extensions_sequential(exts_filter)
1968+
with self.fake_module_environment():
1969+
if build_option('parallel_extensions_install'):
1970+
self.skip_extensions_parallel(exts_filter)
1971+
else:
1972+
self.skip_extensions_sequential(exts_filter)
19791973

19801974
def skip_extensions_sequential(self, exts_filter):
19811975
"""
@@ -2103,31 +2097,27 @@ def install_extensions_sequential(self, install=True):
21032097
msg = "\n* installing extension %s %s using '%s' easyblock\n" % tup
21042098
self.dry_run_msg(msg)
21052099

2106-
self.log.debug("List of loaded modules: %s", self.modules_tool.list())
2107-
2108-
# prepare toolchain build environment, but only when not doing a dry run
2109-
# since in that case the build environment is the same as for the parent
21102100
if self.dry_run:
21112101
self.dry_run_msg("defining build environment based on toolchain (options) and dependencies...")
2112-
else:
2113-
# don't reload modules for toolchain, there is no need since they will be loaded already;
2114-
# the (fake) module for the parent software gets loaded before installing extensions
2115-
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2116-
rpath_filter_dirs=self.rpath_filter_dirs,
2117-
rpath_include_dirs=self.rpath_include_dirs,
2118-
rpath_wrappers_dir=self.rpath_wrappers_dir)
21192102

21202103
# actual installation of the extension
2121-
if install:
2122-
try:
2123-
ext.install_extension_substep("pre_install_extension")
2124-
with self.module_generator.start_module_creation():
2125-
txt = ext.install_extension_substep("install_extension")
2126-
if txt:
2127-
self.module_extra_extensions += txt
2128-
ext.install_extension_substep("post_install_extension")
2129-
finally:
2130-
if not self.dry_run:
2104+
if install and not self.dry_run:
2105+
with self.fake_module_environment(with_build_deps=True):
2106+
self.log.debug("List of loaded modules: %s", self.modules_tool.list())
2107+
# don't reload modules for toolchain, there is no need
2108+
# since they will be loaded already by the fake module
2109+
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2110+
rpath_filter_dirs=self.rpath_filter_dirs,
2111+
rpath_include_dirs=self.rpath_include_dirs,
2112+
rpath_wrappers_dir=self.rpath_wrappers_dir)
2113+
try:
2114+
ext.install_extension_substep("pre_install_extension")
2115+
with self.module_generator.start_module_creation():
2116+
txt = ext.install_extension_substep("install_extension")
2117+
if txt:
2118+
self.module_extra_extensions += txt
2119+
ext.install_extension_substep("post_install_extension")
2120+
finally:
21312121
ext_duration = datetime.now() - start_time
21322122
if ext_duration.total_seconds() >= 1:
21332123
print_msg("\t... (took %s)", time2str(ext_duration), log=self.log, silent=self.silent)
@@ -2274,17 +2264,18 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
22742264
tup = (ext.name, ext.version or '')
22752265
print_msg("starting installation of extension %s %s..." % tup, silent=self.silent, log=self.log)
22762266

2277-
# don't reload modules for toolchain, there is no need since they will be loaded already;
2278-
# the (fake) module for the parent software gets loaded before installing extensions
2279-
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2280-
rpath_filter_dirs=self.rpath_filter_dirs,
2281-
rpath_include_dirs=self.rpath_include_dirs,
2282-
rpath_wrappers_dir=self.rpath_wrappers_dir)
2283-
if install:
2284-
ext.install_extension_substep("pre_install_extension")
2285-
ext.async_cmd_task = ext.install_extension_substep("install_extension_async", thread_pool)
2286-
running_exts.append(ext)
2287-
self.log.info(f"Started installation of extension {ext.name} in the background...")
2267+
if install and not self.dry_run:
2268+
with self.fake_module_environment(with_build_deps=True):
2269+
# don't reload modules for toolchain, there is no
2270+
# need since they will be loaded by the fake module
2271+
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2272+
rpath_filter_dirs=self.rpath_filter_dirs,
2273+
rpath_include_dirs=self.rpath_include_dirs,
2274+
rpath_wrappers_dir=self.rpath_wrappers_dir)
2275+
ext.install_extension_substep("pre_install_extension")
2276+
ext.async_cmd_task = ext.install_extension_substep("install_extension_async", thread_pool)
2277+
running_exts.append(ext)
2278+
self.log.info(f"Started installation of extension {ext.name} in the background...")
22882279
update_exts_progress_bar_helper(running_exts, 0)
22892280

22902281
# print progress info after every iteration (unless that info is already shown via progress bar)
@@ -2308,6 +2299,25 @@ def start_dir(self):
23082299
"""Start directory in build directory"""
23092300
return self.cfg['start_dir']
23102301

2302+
@contextmanager
2303+
def fake_module_environment(self, extra_modules=None, with_build_deps=False):
2304+
"""
2305+
Load/Unload fake module
2306+
"""
2307+
fake_mod_data = None
2308+
2309+
if with_build_deps:
2310+
# load modules for build dependencies as extra modules
2311+
extra_modules = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]
2312+
2313+
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules)
2314+
2315+
yield
2316+
2317+
# cleanup (unload fake module, remove fake module dir)
2318+
if fake_mod_data:
2319+
self.clean_up_fake_module(fake_mod_data)
2320+
23112321
def guess_start_dir(self):
23122322
"""
23132323
Return the directory where to start the whole configure/make/make install cycle from
@@ -3130,14 +3140,9 @@ def extensions_step(self, fetch=False, install=True):
31303140
self.log.debug("No extensions in exts_list")
31313141
return
31323142

3133-
# load fake module
3134-
fake_mod_data = None
3135-
if install and not self.dry_run:
3136-
3137-
# load modules for build dependencies as extra modules
3138-
build_dep_mods = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)]
3139-
3140-
fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)
3143+
# we really need a default class
3144+
if not self.cfg['exts_defaultclass'] and install:
3145+
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)
31413146

31423147
start_progress_bar(PROGRESS_BAR_EXTENSIONS, len(self.cfg.get_ref('exts_list')))
31433148

@@ -3153,22 +3158,13 @@ def extensions_step(self, fetch=False, install=True):
31533158
if install:
31543159
self.log.info("Installing extensions")
31553160

3156-
# we really need a default class
3157-
if not self.cfg['exts_defaultclass'] and fake_mod_data:
3158-
self.clean_up_fake_module(fake_mod_data)
3159-
raise EasyBuildError("ERROR: No default extension class set for %s", self.name)
3160-
31613161
self.init_ext_instances()
31623162

31633163
if self.skip:
31643164
self.skip_extensions()
31653165

31663166
self.install_all_extensions(install=install)
31673167

3168-
# cleanup (unload fake module, remove fake module dir)
3169-
if fake_mod_data:
3170-
self.clean_up_fake_module(fake_mod_data)
3171-
31723168
stop_progress_bar(PROGRESS_BAR_EXTENSIONS, visible=False)
31733169

31743170
def package_step(self):
@@ -4078,7 +4074,7 @@ def make_module_step(self, fake=False):
40784074
txt += self.make_module_deppaths()
40794075
txt += self.make_module_dep()
40804076
txt += self.make_module_extend_modpath()
4081-
txt += self.make_module_req(fake=fake)
4077+
txt += self.make_module_req()
40824078
txt += self.make_module_extra()
40834079
txt += self.make_module_footer()
40844080

@@ -4122,13 +4118,14 @@ def make_module_step(self, fake=False):
41224118
self.module_generator.create_symlinks(mod_symlink_paths, fake=fake)
41234119

41244120
if ActiveMNS().mns.det_make_devel_module() and not fake and build_option('generate_devel_module'):
4125-
try:
4126-
self.make_devel_module()
4127-
except EasyBuildError as error:
4128-
if build_option('module_only') or self.cfg['module_only']:
4129-
self.log.info("Using --module-only so can recover from error: %s", error)
4130-
else:
4131-
raise error
4121+
with self.fake_module_environment():
4122+
try:
4123+
self.make_devel_module()
4124+
except EasyBuildError as error:
4125+
if build_option('module_only') or self.cfg['module_only']:
4126+
self.log.info("Using --module-only so can recover from error: %s", error)
4127+
else:
4128+
raise error
41324129
else:
41334130
self.log.info("Skipping devel module...")
41344131

easybuild/framework/extensioneasyblock.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -176,29 +176,22 @@ def sanity_check_step(self, exts_filter=None, custom_paths=None, custom_commands
176176
# make sure Extension sanity check step is run once, by using a single empty list of extra modules
177177
lists_of_extra_modules = [[]]
178178

179-
for extra_modules in lists_of_extra_modules:
180-
181-
fake_mod_data = None
182-
183-
# only load fake module + extra modules for stand-alone installations (not for extensions),
184-
# since for extension the necessary modules should already be loaded at this point;
185-
# take into account that module may already be loaded earlier in sanity check
186-
if not (self.sanity_check_module_loaded or self.is_extension or self.dry_run):
187-
# load fake module
188-
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules)
189-
190-
if extra_modules:
191-
info_msg = "Running extension sanity check with extra modules: %s" % ', '.join(extra_modules)
192-
self.log.info(info_msg)
193-
trace_msg(info_msg)
194-
195-
# perform extension sanity check
179+
# only load fake module + extra modules for stand-alone installations (not for extensions),
180+
# since for extension the necessary modules should already be loaded at this point;
181+
# take into account that module may already be loaded earlier in sanity check
182+
if not (self.sanity_check_module_loaded or self.is_extension or self.dry_run):
183+
for extra_modules in lists_of_extra_modules:
184+
with self.fake_module_environment(extra_modules=extra_modules):
185+
if extra_modules:
186+
info_msg = f"Running extension sanity check with extra modules: {', '.join(extra_modules)}"
187+
self.log.info(info_msg)
188+
trace_msg(info_msg)
189+
# perform sanity check for stand-alone extension
190+
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
191+
else:
192+
# perform single sanity check for extension
196193
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
197194

198-
if fake_mod_data:
199-
# unload fake module and clean up
200-
self.clean_up_fake_module(fake_mod_data)
201-
202195
if custom_paths or custom_commands or not self.is_extension:
203196
super().sanity_check_step(custom_paths=custom_paths,
204197
custom_commands=custom_commands,

test/framework/easyblock.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,6 @@ def test_extensions_step_deprecations(self):
13241324
'description = "test easyconfig"',
13251325
'toolchain = SYSTEM',
13261326
'exts_defaultclass = "DummyExtension"',
1327-
'exts_list = ["ext1"]',
13281327
'exts_list = [',
13291328
' "dummy_ext",',
13301329
' ("custom_ext", "0.0", {"easyblock": "CustomDummyExtension"}),',
@@ -1416,7 +1415,7 @@ def test_extension_source_tmpl(self):
14161415
"toolchain = SYSTEM",
14171416
"exts_list = [",
14181417
" ('bar', '0.0', {",
1419-
" 'source_tmpl': [SOURCE_TAR_GZ],",
1418+
" 'source_tmpl': [SOURCE_TAR_GZ],",
14201419
" }),",
14211420
"]",
14221421
])
@@ -1490,6 +1489,46 @@ def test_skip_extensions_step(self):
14901489
eb.close_log()
14911490
os.remove(eb.logfile)
14921491

1492+
def test_extension_fake_modules(self):
1493+
"""
1494+
Test that extensions relying on installation files from previous extensions work
1495+
Search paths of fake module should update for each extension and resolve any globs
1496+
"""
1497+
self.contents = cleandoc("""
1498+
easyblock = 'ConfigureMake'
1499+
name = 'toy'
1500+
version = '0.0'
1501+
homepage = 'https://example.com'
1502+
description = 'test'
1503+
toolchain = SYSTEM
1504+
exts_list = [
1505+
('bar', '0.0', {
1506+
'postinstallcmds': [
1507+
'mkdir -p %(installdir)s/custom_bin',
1508+
'touch %(installdir)s/custom_bin/bar.sh',
1509+
'chmod +x %(installdir)s/custom_bin/bar.sh',
1510+
],
1511+
}),
1512+
('barbar', '0.0', {
1513+
'postinstallcmds': ['bar.sh'],
1514+
}),
1515+
]
1516+
exts_defaultclass = "DummyExtension"
1517+
modextrapaths = {'PATH': 'custom*'}
1518+
""")
1519+
self.writeEC()
1520+
eb = EasyBlock(EasyConfig(self.eb_file))
1521+
eb.builddir = config.build_path()
1522+
eb.installdir = config.install_path()
1523+
1524+
self.mock_stdout(True)
1525+
eb.extensions_step(fetch=True)
1526+
stdout = self.get_stdout()
1527+
self.mock_stdout(False)
1528+
1529+
pattern = r">> running shell command:\n\s+bar.sh(\n\s+\[.*\]){3}\n\s+>> command completed: exit 0"
1530+
self.assertTrue(re.search(pattern, stdout, re.M))
1531+
14931532
def test_make_module_step(self):
14941533
"""Test the make_module_step"""
14951534

@@ -2489,6 +2528,7 @@ def test_extensions_sanity_check(self):
24892528
exts_list = toy_ec['exts_list']
24902529
exts_list[-1][2]['exts_filter'] = ("thisshouldfail", '')
24912530
toy_ec['exts_list'] = exts_list
2531+
toy_ec['exts_defaultclass'] = 'DummyExtension'
24922532

24932533
eb = EB_toy(toy_ec)
24942534
eb.silent = True
@@ -2503,6 +2543,7 @@ def test_extensions_sanity_check(self):
25032543
# sanity check commands are checked after checking sanity check paths, so this should work
25042544
toy_ec = EasyConfig(toy_ec_fn)
25052545
toy_ec.update('sanity_check_commands', [("%(installdir)s/bin/toy && rm %(installdir)s/bin/toy", '')])
2546+
toy_ec['exts_defaultclass'] = 'DummyExtension'
25062547
eb = EB_toy(toy_ec)
25072548
eb.silent = True
25082549
with self.mocked_stdout_stderr():

test/framework/easyconfigs/test_ecs/t/toy/toy-0.0-gompi-2018a-test.eb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ local_bar_buildopts = " && gcc bar.c -o anotherbar && "
2727
# used to check whether $TOY_LIBS_PATH is defined even when 'lib' subdirectory doesn't exist yet
2828
local_bar_buildopts += 'echo "TOY_EXAMPLES=$TOY_EXAMPLES" > %(installdir)s/toy_libs_path.txt'
2929

30+
exts_defaultclass = 'DummyExtension'
3031
exts_list = [
3132
'ulimit', # extension that is part of "standard library"
3233
('bar', '0.0', {
@@ -59,7 +60,7 @@ sanity_check_paths = {
5960
'dirs': [],
6061
}
6162

62-
modextrapaths = {'TOY_EXAMPLES': 'examples'}
63+
modextravars = {'TOY_EXAMPLES': 'examples'}
6364

6465
postinstallcmds = ["echo TOY > %(installdir)s/README"]
6566

0 commit comments

Comments
 (0)