Skip to content

Commit c6fb00f

Browse files
committed
Merge branch '5.0.x' of github.com:easybuilders/easybuild-framework into 5.0.x
2 parents 537beea + 7666088 commit c6fb00f

File tree

7 files changed

+93
-30
lines changed

7 files changed

+93
-30
lines changed

easybuild/framework/easyblock.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -613,19 +613,16 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
613613
template_values = copy.deepcopy(self.cfg.template_values)
614614
template_values.update(template_constant_dict(ext_src))
615615

616-
# resolve templates in extension options
617-
ext_options = resolve_template(ext_options, template_values)
618-
619-
source_urls = ext_options.get('source_urls', [])
616+
source_urls = resolve_template(ext_options.get('source_urls', []), template_values)
620617
checksums = ext_options.get('checksums', [])
621618

622-
download_instructions = ext_options.get('download_instructions')
619+
download_instructions = resolve_template(ext_options.get('download_instructions'), template_values)
623620

624621
if ext_options.get('nosource', None):
625622
self.log.debug("No sources for extension %s, as indicated by 'nosource'", ext_name)
626623

627624
elif ext_options.get('sources', None):
628-
sources = ext_options['sources']
625+
sources = resolve_template(ext_options['sources'], template_values)
629626

630627
# only a single source file is supported for extensions currently,
631628
# see https://github.com/easybuilders/easybuild-framework/issues/3463
@@ -662,17 +659,18 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
662659
})
663660

664661
else:
665-
# use default template for name of source file if none is specified
666-
default_source_tmpl = resolve_template('%(name)s-%(version)s.tar.gz', template_values)
667662

668663
# if no sources are specified via 'sources', fall back to 'source_tmpl'
669664
src_fn = ext_options.get('source_tmpl')
670665
if src_fn is None:
671-
src_fn = default_source_tmpl
666+
# use default template for name of source file if none is specified
667+
src_fn = '%(name)s-%(version)s.tar.gz'
672668
elif not isinstance(src_fn, str):
673669
error_msg = "source_tmpl value must be a string! (found value of type '%s'): %s"
674670
raise EasyBuildError(error_msg, type(src_fn).__name__, src_fn)
675671

672+
src_fn = resolve_template(src_fn, template_values)
673+
676674
if fetch_files:
677675
src_path = self.obtain_file(src_fn, extension=True, urls=source_urls,
678676
force_download=force_download,
@@ -707,7 +705,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
707705
)
708706

709707
# locate extension patches (if any), and verify checksums
710-
ext_patches = ext_options.get('patches', [])
708+
ext_patches = resolve_template(ext_options.get('patches', []), template_values)
711709
if fetch_files:
712710
ext_patches = self.fetch_patches(patch_specs=ext_patches, extension=True)
713711
else:
@@ -2991,7 +2989,7 @@ def extensions_step(self, fetch=False, install=True):
29912989

29922990
fake_mod_data = self.load_fake_module(purge=True, extra_modules=build_dep_mods)
29932991

2994-
start_progress_bar(PROGRESS_BAR_EXTENSIONS, len(self.cfg['exts_list']))
2992+
start_progress_bar(PROGRESS_BAR_EXTENSIONS, len(self.cfg.get_ref('exts_list')))
29952993

29962994
self.prepare_for_extensions()
29972995

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -765,9 +765,13 @@ def count_files(self):
765765
"""
766766
Determine number of files (sources + patches) required for this easyconfig.
767767
"""
768-
cnt = len(self['sources']) + len(self['patches'])
769768

770-
for ext in self['exts_list']:
769+
# No need to resolve templates as we only need a count not the names
770+
with self.disable_templating():
771+
cnt = len(self['sources']) + len(self['patches'])
772+
exts = self['exts_list']
773+
774+
for ext in exts:
771775
if isinstance(ext, tuple) and len(ext) >= 3:
772776
ext_opts = ext[2]
773777
# check for 'sources' first, since that's also considered first by EasyBlock.collect_exts_file_info
@@ -1653,8 +1657,9 @@ def _finalize_dependencies(self):
16531657
filter_deps_specs = self.parse_filter_deps()
16541658

16551659
for key in DEPENDENCY_PARAMETERS:
1656-
# loop over a *copy* of dependency dicts (with resolved templates);
1657-
deps = self[key]
1660+
# loop over a *copy* of dependency dicts with resolved templates,
1661+
# although some templates may not resolve yet (e.g. those relying on dependencies like %(pyver)s)
1662+
deps = resolve_template(self.get_ref(key), self.template_values, expect_resolved=False)
16581663

16591664
# to update the original dep dict, we need to get a reference with templating disabled...
16601665
deps_ref = self.get_ref(key)
@@ -1830,11 +1835,14 @@ def get(self, key, default=None, resolve=True):
18301835
# see also https://docs.python.org/2/reference/datamodel.html#object.__eq__
18311836
def __eq__(self, ec):
18321837
"""Is this EasyConfig instance equivalent to the provided one?"""
1833-
return self.asdict() == ec.asdict()
1838+
# Compare raw values to check that templates used are the same
1839+
with self.disable_templating():
1840+
with ec.disable_templating():
1841+
return self.asdict() == ec.asdict()
18341842

18351843
def __ne__(self, ec):
18361844
"""Is this EasyConfig instance equivalent to the provided one?"""
1837-
return self.asdict() != ec.asdict()
1845+
return not self == ec
18381846

18391847
def __hash__(self):
18401848
"""Return hash value for a hashable representation of this EasyConfig instance."""
@@ -1847,8 +1855,9 @@ def make_hashable(val):
18471855
return val
18481856

18491857
lst = []
1850-
for (key, val) in sorted(self.asdict().items()):
1851-
lst.append((key, make_hashable(val)))
1858+
with self.disable_templating():
1859+
for (key, val) in sorted(self.asdict().items()):
1860+
lst.append((key, make_hashable(val)))
18521861

18531862
# a list is not hashable, but a tuple is
18541863
return hash(tuple(lst))
@@ -1861,7 +1870,10 @@ def asdict(self):
18611870
for key, tup in self._config.items():
18621871
value = tup[0]
18631872
if self.enable_templating:
1864-
value = self.resolve_template(value)
1873+
if not self.template_values:
1874+
self.generate_template_values()
1875+
# Not all values can be resolved, e.g. %(installdir)s
1876+
value = resolve_template(value, self.template_values, expect_resolved=False)
18651877
res[key] = value
18661878
return res
18671879

@@ -2009,10 +2021,11 @@ def get_module_path(name, generic=None, decode=True):
20092021
return '.'.join(modpath + [module_name])
20102022

20112023

2012-
def resolve_template(value, tmpl_dict):
2024+
def resolve_template(value, tmpl_dict, expect_resolved=True):
20132025
"""Given a value, try to susbstitute the templated strings with actual values.
20142026
- value: some python object (supported are string, tuple/list, dict or some mix thereof)
20152027
- tmpl_dict: template dictionary
2028+
- expect_resolved: Expects that all templates get resolved
20162029
"""
20172030
if isinstance(value, str):
20182031
# simple escaping, making all '%foo', '%%foo', '%%%foo' post-templates values available,
@@ -2065,7 +2078,14 @@ def resolve_template(value, tmpl_dict):
20652078
_log.deprecated(f"Easyconfig template '{old_tmpl}' is deprecated, use '{new_tmpl}' instead",
20662079
ver)
20672080
except KeyError:
2068-
_log.warning(f"Unable to resolve template value {value} with dict {tmpl_dict}")
2081+
if expect_resolved:
2082+
msg = (f'Failed to resolve all templates in "{value}" using template dictionary: {tmpl_dict}. '
2083+
'This might cause failures or unexpected behavior, '
2084+
'check for correct escaping if this is intended!')
2085+
if build_option('allow_unresolved_templates'):
2086+
print_warning(msg)
2087+
else:
2088+
raise EasyBuildError(msg)
20692089
value = raw_value # Undo "%"-escaping
20702090

20712091
for key in tmpl_dict:
@@ -2086,11 +2106,12 @@ def resolve_template(value, tmpl_dict):
20862106
# self._config['x']['y'] = z
20872107
# it can not be intercepted with __setitem__ because the set is done at a deeper level
20882108
if isinstance(value, list):
2089-
value = [resolve_template(val, tmpl_dict) for val in value]
2109+
value = [resolve_template(val, tmpl_dict, expect_resolved) for val in value]
20902110
elif isinstance(value, tuple):
2091-
value = tuple(resolve_template(list(value), tmpl_dict))
2111+
value = tuple(resolve_template(list(value), tmpl_dict, expect_resolved))
20922112
elif isinstance(value, dict):
2093-
value = {resolve_template(k, tmpl_dict): resolve_template(v, tmpl_dict) for k, v in value.items()}
2113+
value = {resolve_template(k, tmpl_dict, expect_resolved): resolve_template(v, tmpl_dict, expect_resolved)
2114+
for k, v in value.items()}
20942115

20952116
return value
20962117

easybuild/framework/extension.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ def __init__(self, mself, ext, extra_params=None):
128128
self.src = resolve_template(self.ext.get('src', []), self.cfg.template_values)
129129
self.src_extract_cmd = self.ext.get('extract_cmd', None)
130130
self.patches = resolve_template(self.ext.get('patches', []), self.cfg.template_values)
131-
self.options = resolve_template(copy.deepcopy(self.ext.get('options', {})), self.cfg.template_values)
131+
# Some options may not be resolvable yet
132+
self.options = resolve_template(copy.deepcopy(self.ext.get('options', {})),
133+
self.cfg.template_values,
134+
expect_resolved=False)
132135

133136
if extra_params:
134137
self.cfg.extend_params(extra_params, overwrite=False)

easybuild/tools/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
280280
False: [
281281
'add_system_to_minimal_toolchains',
282282
'allow_modules_tool_mismatch',
283+
'allow_unresolved_templates',
283284
'backup_patched_files',
284285
'consider_archived_easyconfigs',
285286
'container_build_image',

easybuild/tools/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ def override_options(self):
363363
None, 'store_true', False),
364364
'allow-loaded-modules': ("List of software names for which to allow loaded modules in initial environment",
365365
'strlist', 'store', DEFAULT_ALLOW_LOADED_MODULES),
366+
'allow-unresolved-templates': ("Don't error out when templates such as %(name)s in EasyConfigs "
367+
"could not be resolved", None, 'store_true', False),
366368
'allow-modules-tool-mismatch': ("Allow mismatch of modules tool and definition of 'module' function",
367369
None, 'store_true', False),
368370
'allow-use-as-root-and-accept-consequences': ("Allow using of EasyBuild as root (NOT RECOMMENDED!)",

test/framework/easyblock.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1225,7 +1225,7 @@ def test_extension_source_tmpl(self):
12251225
eb = EasyBlock(EasyConfig(self.eb_file))
12261226

12271227
error_pattern = r"source_tmpl value must be a string! "
1228-
error_pattern += r"\(found value of type 'list'\): \['bar-0\.0\.tar\.gz'\]"
1228+
error_pattern += r"\(found value of type 'list'\): \['%\(name\)s-%\(version\)s.tar.gz'\]"
12291229
self.assertErrorRegex(EasyBuildError, error_pattern, eb.fetch_step)
12301230

12311231
self.contents = self.contents.replace("'source_tmpl': [SOURCE_TAR_GZ]", "'source_tmpl': SOURCE_TAR_GZ")

test/framework/easyconfig.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,8 +1934,9 @@ def test_alternative_easyconfig_parameters(self):
19341934
self.assertEqual(ec['env_mod_class'], expected)
19351935

19361936
expected = ['echo TOY > %(installdir)s/README']
1937-
self.assertEqual(ec['postinstallcmds'], expected)
1938-
self.assertEqual(ec['post_install_cmds'], expected)
1937+
with ec.disable_templating():
1938+
self.assertEqual(ec['postinstallcmds'], expected)
1939+
self.assertEqual(ec['post_install_cmds'], expected)
19391940

19401941
# test setting of easyconfig parameter with original & alternative name
19411942
ec['moduleclass'] = 'test1'
@@ -3888,7 +3889,7 @@ def test_resolve_template(self):
38883889

38893890
# On unknown values the value is returned unchanged
38903891
for value in ('%(invalid)s', '%(name)s %(invalid)s', '%%%(invalid)s', '% %(invalid)s', '%s %(invalid)s'):
3891-
self.assertEqual(resolve_template(value, tmpl_dict), value)
3892+
self.assertEqual(resolve_template(value, tmpl_dict, expect_resolved=False), value)
38923893

38933894
def test_det_subtoolchain_version(self):
38943895
"""Test det_subtoolchain_version function"""
@@ -5170,6 +5171,43 @@ def test_easyconfigs_caches(self):
51705171
regex = re.compile(r"libtoy/0\.0 is already installed", re.M)
51715172
self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout))
51725173

5174+
def test_templates(self):
5175+
"""
5176+
Test use of template values like %(version)s
5177+
"""
5178+
test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
5179+
toy_ec = os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb')
5180+
5181+
test_ec_txt = read_file(toy_ec)
5182+
test_ec_txt += '\ndescription = "name: %(name)s, version: %(version)s"'
5183+
5184+
test_ec = os.path.join(self.test_prefix, 'test.eb')
5185+
write_file(test_ec, test_ec_txt)
5186+
ec = EasyConfig(test_ec)
5187+
5188+
# get_ref provides access to non-templated raw value
5189+
self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s")
5190+
self.assertEqual(ec['description'], "name: toy, version: 0.0")
5191+
5192+
# error when using wrong template value or using template value that can not be resolved yet too early
5193+
test_ec_txt += '\ndescription = "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s"'
5194+
write_file(test_ec, test_ec_txt)
5195+
ec = EasyConfig(test_ec)
5196+
5197+
self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
5198+
error_pattern = r"Failed to resolve all templates in.* %\(pyshortver\)s.* using template dictionary:"
5199+
self.assertErrorRegex(EasyBuildError, error_pattern, ec.__getitem__, 'description')
5200+
5201+
# EasyBuild can be configured to allow unresolved templates
5202+
update_build_option('allow_unresolved_templates', True)
5203+
self.assertEqual(ec.get_ref('description'), "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
5204+
with self.mocked_stdout_stderr() as (stdout, stderr):
5205+
self.assertEqual(ec['description'], "name: %(name)s, version: %(version)s, pyshortver: %(pyshortver)s")
5206+
5207+
self.assertFalse(stdout.getvalue())
5208+
regex = re.compile(r"WARNING: Failed to resolve all templates.* %\(pyshortver\)s", re.M)
5209+
self.assertRegex(stderr.getvalue(), regex)
5210+
51735211

51745212
def suite():
51755213
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)