Skip to content

Commit e0102ea

Browse files
authored
Merge pull request #2852 from boegel/exts_templates
correctly resolve template values used for extensions
2 parents 6fe04bc + ad17ced commit e0102ea

File tree

3 files changed

+119
-15
lines changed

3 files changed

+119
-15
lines changed

easybuild/framework/easyblock.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
5959
from easybuild.framework.easyconfig.easyconfig import ITERATE_OPTIONS, EasyConfig, ActiveMNS, get_easyblock_class
6060
from easybuild.framework.easyconfig.easyconfig import get_module_path, letter_dir_for, resolve_template
61+
from easybuild.framework.easyconfig.templates import template_constant_dict
6162
from easybuild.framework.easyconfig.format.format import INDENT_4SPACES
6263
from easybuild.framework.easyconfig.parser import fetch_parameters_from_easyconfig
6364
from easybuild.framework.easyconfig.style import MAX_LINE_LENGTH
@@ -482,8 +483,6 @@ def fetch_extension_sources(self, skip_checksums=False):
482483
# since it may use template values like %(name)s & %(version)s
483484
ext_options = copy.deepcopy(self.cfg.get_ref('exts_default_options'))
484485

485-
def_src_tmpl = "%(name)s-%(version)s.tar.gz"
486-
487486
if len(ext) == 3:
488487
if isinstance(ext_options, dict):
489488
ext_options.update(ext[2])
@@ -498,17 +497,24 @@ def fetch_extension_sources(self, skip_checksums=False):
498497
'options': ext_options,
499498
}
500499

500+
# construct dictionary with template values;
501+
# inherited from parent, except for name/version templates which are specific to this extension
502+
template_values = copy.deepcopy(self.cfg.template_values)
503+
template_values.update(template_constant_dict(ext_src))
504+
505+
# resolve templates in extension options
506+
ext_options = resolve_template(ext_options, template_values)
507+
501508
checksums = ext_options.get('checksums', [])
502509

503-
if ext_options.get('source_tmpl', None):
504-
fn = resolve_template(ext_options['source_tmpl'], ext_src)
505-
else:
506-
fn = resolve_template(def_src_tmpl, ext_src)
510+
# use default template for name of source file if none is specified
511+
default_source_tmpl = resolve_template('%(name)s-%(version)s.tar.gz', template_values)
512+
fn = ext_options.get('source_tmpl', default_source_tmpl)
507513

508514
if ext_options.get('nosource', None):
509515
exts_sources.append(ext_src)
510516
else:
511-
source_urls = [resolve_template(url, ext_src) for url in ext_options.get('source_urls', [])]
517+
source_urls = ext_options.get('source_urls', [])
512518
force_download = build_option('force_download') in [FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_SOURCES]
513519
src_fn = self.obtain_file(fn, extension=True, urls=source_urls, force_download=force_download)
514520

easybuild/framework/extension.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
import copy
3737
import os
3838

39+
from easybuild.framework.easyconfig.easyconfig import resolve_template
40+
from easybuild.framework.easyconfig.templates import template_constant_dict
3941
from easybuild.tools.build_log import EasyBuildError
40-
from easybuild.tools.config import build_option, build_path
4142
from easybuild.tools.filetools import change_dir
4243
from easybuild.tools.run import run_cmd
4344

@@ -60,17 +61,22 @@ def __init__(self, mself, ext, extra_params=None):
6061
self.ext = copy.deepcopy(ext)
6162
self.dry_run = self.master.dry_run
6263

63-
if not 'name' in self.ext:
64+
if 'name' not in self.ext:
6465
raise EasyBuildError("'name' is missing in supplied class instance 'ext'.")
6566

67+
name, version = self.ext['name'], self.ext.get('version', None)
68+
6669
# parent sanity check paths/commands are not relevant for extension
6770
self.cfg['sanity_check_commands'] = []
6871
self.cfg['sanity_check_paths'] = []
6972

73+
# construct dict with template values that can be used
74+
self.cfg.template_values.update(template_constant_dict({'name': name, 'version': version}))
75+
7076
# list of source/patch files: we use an empty list as default value like in EasyBlock
71-
self.src = self.ext.get('src', [])
72-
self.patches = self.ext.get('patches', [])
73-
self.options = copy.deepcopy(self.ext.get('options', {}))
77+
self.src = resolve_template(self.ext.get('src', []), self.cfg.template_values)
78+
self.patches = resolve_template(self.ext.get('patches', []), self.cfg.template_values)
79+
self.options = resolve_template(copy.deepcopy(self.ext.get('options', {})), self.cfg.template_values)
7480

7581
if extra_params:
7682
self.cfg.extend_params(extra_params, overwrite=False)
@@ -81,12 +87,12 @@ def __init__(self, mself, ext, extra_params=None):
8187
# this allows to specify custom easyconfig parameters on a per-extension basis
8288
for key in self.options:
8389
if key in self.cfg:
84-
self.cfg[key] = self.options[key]
90+
self.cfg[key] = resolve_template(self.options[key], self.cfg.template_values)
8591
self.log.debug("Customising known easyconfig parameter '%s' for extension %s/%s: %s",
86-
key, self.ext['name'], self.ext['version'], self.cfg[key])
92+
key, name, version, self.cfg[key])
8793
else:
8894
self.log.debug("Skipping unknown custom easyconfig parameter '%s' for extension %s/%s: %s",
89-
key, self.ext['name'], self.ext['version'], self.options[key])
95+
key, name, version, self.options[key])
9096

9197
self.sanity_check_fail_msgs = []
9298

test/framework/easyconfig.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,73 @@ def test_exts_list(self):
390390
regex = re.compile('EBEXTSLISTPI.*ext1-1.0,ext2-2.0')
391391
self.assertTrue(regex.search(modtxt), "Pattern '%s' found in: %s" % (regex.pattern, modtxt))
392392

393+
def test_extensions_templates(self):
394+
"""Test whether templates used in exts_list are resolved properly."""
395+
396+
# put dummy source file in place to avoid download fail
397+
toy_tar_gz = os.path.join(self.test_sourcepath, 'toy', 'toy-0.0.tar.gz')
398+
copy_file(toy_tar_gz, os.path.join(self.test_prefix, 'toy-0.0-py3-test.tar.gz'))
399+
toy_patch_fn = 'toy-0.0_fix-silly-typo-in-printf-statement.patch'
400+
toy_patch = os.path.join(self.test_sourcepath, 'toy', toy_patch_fn)
401+
copy_file(toy_patch, self.test_prefix)
402+
403+
os.environ['EASYBUILD_SOURCEPATH'] = self.test_prefix
404+
init_config(build_options={'silent': True})
405+
406+
self.contents = '\n'.join([
407+
'easyblock = "ConfigureMake"',
408+
'name = "pi"',
409+
'version = "3.14"',
410+
'versionsuffix = "-test"',
411+
'homepage = "http://example.com"',
412+
'description = "test easyconfig"',
413+
'toolchain = {"name": "dummy", "version": ""}',
414+
'dependencies = [("Python", "3.6.6")]',
415+
'exts_defaultclass = "EB_Toy"',
416+
# bogus, but useful to check whether this get resolved
417+
'exts_default_options = {"source_urls": [PYPI_SOURCE]}',
418+
'exts_list = [',
419+
' ("toy", "0.0", {',
420+
# %(name)s and %(version_major_minor)s should be resolved using name/version of extension (not parent)
421+
# %(pymajver)s should get resolved because Python is listed as a (runtime) dep
422+
# %(versionsuffix)s should get resolved with value of parent
423+
' "source_tmpl": "%(name)s-%(version_major_minor)s-py%(pymajver)s%(versionsuffix)s.tar.gz",',
424+
' "patches": ["%(name)s-%(version)s_fix-silly-typo-in-printf-statement.patch"],',
425+
# use hacky prebuildopts that is picked up by 'EB_Toy' easyblock, to check whether templates are resolved
426+
' "prebuildopts": "gcc -O2 %(name)s.c -o toy-%(version)s && mv toy-%(version)s toy #",',
427+
' }),',
428+
']',
429+
])
430+
self.prep()
431+
ec = EasyConfig(self.eb_file)
432+
eb = EasyBlock(ec)
433+
eb.fetch_step()
434+
435+
# run extensions step to install 'toy' extension
436+
eb.extensions_step()
437+
438+
# check whether template values were resolved correctly in Extension instances that were created/used
439+
toy_ext = eb.ext_instances[0]
440+
self.assertEqual(os.path.basename(toy_ext.src), 'toy-0.0-py3-test.tar.gz')
441+
self.assertEqual(toy_ext.patches, [os.path.join(self.test_prefix, toy_patch_fn)])
442+
expected = {
443+
'patches': ['toy-0.0_fix-silly-typo-in-printf-statement.patch'],
444+
'prebuildopts': 'gcc -O2 toy.c -o toy-0.0 && mv toy-0.0 toy #',
445+
'source_tmpl': 'toy-0.0-py3-test.tar.gz',
446+
'source_urls': ['https://pypi.python.org/packages/source/t/toy'],
447+
}
448+
self.assertEqual(toy_ext.options, expected)
449+
450+
# also .cfg of Extension instance was updated correctly
451+
self.assertEqual(toy_ext.cfg['source_urls'], ['https://pypi.python.org/packages/source/t/toy'])
452+
self.assertEqual(toy_ext.cfg['patches'], [toy_patch_fn])
453+
self.assertEqual(toy_ext.cfg['prebuildopts'], "gcc -O2 toy.c -o toy-0.0 && mv toy-0.0 toy #")
454+
455+
# check whether files expected to be installed for 'toy' extension are in place
456+
pi_installdir = os.path.join(self.test_installpath, 'software', 'pi', '3.14-test')
457+
self.assertTrue(os.path.exists(os.path.join(pi_installdir, 'bin', 'toy')))
458+
self.assertTrue(os.path.exists(os.path.join(pi_installdir, 'lib', 'libtoy.a')))
459+
393460
def test_suggestions(self):
394461
""" If a typo is present, suggestions should be provided (if possible) """
395462
self.contents = '\n'.join([
@@ -2163,6 +2230,31 @@ def test_template_constant_dict(self):
21632230

21642231
self.assertEqual(res, expected)
21652232

2233+
# also check result of template_constant_dict when dict representing extension is passed
2234+
ext_dict = {
2235+
'name': 'foo',
2236+
'version': '1.2.3',
2237+
'options': {
2238+
'source_urls': ['https://example.com'],
2239+
'source_tmpl': '%(name)s-%(version)s.tar.gz',
2240+
},
2241+
}
2242+
res = template_constant_dict(ext_dict)
2243+
2244+
self.assertTrue('arch' in res)
2245+
arch = res.pop('arch')
2246+
self.assertTrue(arch_regex.match(arch), "'%s' matches with pattern '%s'" % (arch, arch_regex.pattern))
2247+
2248+
expected = {
2249+
'name': 'foo',
2250+
'nameletter': 'f',
2251+
'version': '1.2.3',
2252+
'version_major': '1',
2253+
'version_major_minor': '1.2',
2254+
'version_minor': '2'
2255+
}
2256+
self.assertEqual(res, expected)
2257+
21662258
def test_parse_deps_templates(self):
21672259
"""Test whether handling of templates defined by dependencies is done correctly."""
21682260
test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')

0 commit comments

Comments
 (0)