Skip to content

Commit 54655d3

Browse files
authored
Merge pull request #4021 from boegel/easystack_easyconfigs
add support for easystack file that contains easyconfig filenames + implement parsing of configuration options
2 parents 329a95c + 2ba2124 commit 54655d3

File tree

6 files changed

+200
-24
lines changed

6 files changed

+200
-24
lines changed

easybuild/framework/easystack.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
pass
4242
_log = fancylogger.getLogger('easystack', fname=False)
4343

44+
EASYSTACK_DOC_URL = 'https://docs.easybuild.io/en/latest/Easystack-files.html'
45+
4446

4547
def check_value(value, context):
4648
"""
@@ -68,10 +70,16 @@ def __init__(self):
6870
self.easybuild_version = None
6971
self.robot = False
7072
self.software_list = []
73+
self.easyconfigs = [] # A list of easyconfig names. May or may not include .eb extension
74+
# A dict where keys are easyconfig names, values are dictionary of options that should be
75+
# applied for that easyconfig
76+
self.ec_opts = {}
7177

7278
def compose_ec_filenames(self):
7379
"""Returns a list of all easyconfig names"""
7480
ec_filenames = []
81+
82+
# entries specified via 'software' top-level key
7583
for sw in self.software_list:
7684
full_ec_version = det_full_ec_version({
7785
'toolchain': {'name': sw.toolchain_name, 'version': sw.toolchain_version},
@@ -80,6 +88,10 @@ def compose_ec_filenames(self):
8088
})
8189
ec_filename = '%s-%s.eb' % (sw.name, full_ec_version)
8290
ec_filenames.append(ec_filename)
91+
92+
# entries specified via 'easyconfigs' top-level key
93+
for ec in self.easyconfigs:
94+
ec_filenames.append(ec)
8395
return ec_filenames
8496

8597
# flags applicable to all sw (i.e. robot)
@@ -108,21 +120,89 @@ class EasyStackParser(object):
108120

109121
@staticmethod
110122
def parse(filepath):
111-
"""Parses YAML file and assigns obtained values to SW config instances as well as general config instance"""
123+
"""
124+
Parses YAML file and assigns obtained values to SW config instances as well as general config instance"""
112125
yaml_txt = read_file(filepath)
113126

114127
try:
115128
easystack_raw = yaml.safe_load(yaml_txt)
116129
except yaml.YAMLError as err:
117130
raise EasyBuildError("Failed to parse %s: %s" % (filepath, err))
118131

132+
easystack_data = None
133+
top_keys = ('easyconfigs', 'software')
134+
keys_found = []
135+
for key in top_keys:
136+
if key in easystack_raw:
137+
keys_found.append(key)
138+
# For now, we don't support mixing multiple top_keys, so check that only one was defined
139+
if len(keys_found) > 1:
140+
keys_string = ', '.join(keys_found)
141+
msg = "Specifying multiple top level keys (%s) " % keys_string
142+
msg += "in one EasyStack file is currently not supported"
143+
msg += ", see %s for documentation." % EASYSTACK_DOC_URL
144+
raise EasyBuildError(msg)
145+
elif len(keys_found) == 0:
146+
msg = "Not a valid EasyStack YAML file: no 'easyconfigs' or 'software' top-level key found"
147+
msg += ", see %s for documentation." % EASYSTACK_DOC_URL
148+
raise EasyBuildError(msg)
149+
else:
150+
key = keys_found[0]
151+
easystack_data = easystack_raw[key]
152+
153+
parse_method_name = 'parse_by_' + key
154+
parse_method = getattr(EasyStackParser, 'parse_by_%s' % key, None)
155+
if parse_method is None:
156+
raise EasyBuildError("Easystack parse method '%s' not found!", parse_method_name)
157+
158+
# assign general easystack attributes
159+
easybuild_version = easystack_raw.get('easybuild_version', None)
160+
robot = easystack_raw.get('robot', False)
161+
162+
return parse_method(filepath, easystack_data, easybuild_version=easybuild_version, robot=robot)
163+
164+
@staticmethod
165+
def parse_by_easyconfigs(filepath, easyconfigs, easybuild_version=None, robot=False):
166+
"""
167+
Parse easystack file with 'easyconfigs' as top-level key.
168+
"""
169+
119170
easystack = EasyStack()
120171

121-
try:
122-
software = easystack_raw["software"]
123-
except KeyError:
124-
wrong_structure_file = "Not a valid EasyStack YAML file: no 'software' key found"
125-
raise EasyBuildError(wrong_structure_file)
172+
for easyconfig in easyconfigs:
173+
if isinstance(easyconfig, str):
174+
if not easyconfig.endswith('.eb'):
175+
easyconfig = easyconfig + '.eb'
176+
easystack.easyconfigs.append(easyconfig)
177+
elif isinstance(easyconfig, dict):
178+
if len(easyconfig) == 1:
179+
# Get single key from dictionary 'easyconfig'
180+
easyconf_name = list(easyconfig.keys())[0]
181+
# Add easyconfig name to the list
182+
if not easyconf_name.endswith('.eb'):
183+
easyconf_name_with_eb = easyconf_name + '.eb'
184+
else:
185+
easyconf_name_with_eb = easyconf_name
186+
easystack.easyconfigs.append(easyconf_name_with_eb)
187+
# Add options to the ec_opts dict
188+
if 'options' in easyconfig[easyconf_name].keys():
189+
easystack.ec_opts[easyconf_name_with_eb] = easyconfig[easyconf_name]['options']
190+
else:
191+
dict_keys = ', '.join(easyconfig.keys())
192+
msg = "Failed to parse easystack file: expected a dictionary with one key (the EasyConfig name), "
193+
msg += "instead found keys: %s" % dict_keys
194+
msg += ", see %s for documentation." % EASYSTACK_DOC_URL
195+
raise EasyBuildError(msg)
196+
197+
return easystack
198+
199+
@staticmethod
200+
def parse_by_software(filepath, software, easybuild_version=None, robot=False):
201+
"""
202+
Parse easystack file with 'software' as top-level key.
203+
"""
204+
205+
easystack = EasyStack()
126206

127207
# assign software-specific easystack attributes
128208
for name in software:
@@ -224,8 +304,8 @@ def parse(filepath):
224304
easystack.software_list.append(sw)
225305

226306
# assign general easystack attributes
227-
easystack.easybuild_version = easystack_raw.get('easybuild_version', None)
228-
easystack.robot = easystack_raw.get('robot', False)
307+
easystack.easybuild_version = easybuild_version
308+
easystack.robot = robot
229309

230310
return easystack
231311

@@ -243,12 +323,16 @@ def parse_easystack(filepath):
243323

244324
easyconfig_names = easystack.compose_ec_filenames()
245325

246-
general_options = easystack.get_general_options()
326+
# Disabled general options for now. We weren't using them, and first want support for EasyConfig-specific options.
327+
# Then, we need a method to resolve conflicts (specific options should win)
328+
# general_options = easystack.get_general_options()
247329

248330
_log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: %s" % ', '.join(sorted(easyconfig_names)))
249-
if len(general_options) != 0:
250-
_log.debug("General options for installation are: \n%s" % str(general_options))
251-
else:
252-
_log.debug("No general options were specified in easystack")
253-
254-
return easyconfig_names, general_options
331+
_log.debug("Using EasyConfig specific options based on the following dict:")
332+
_log.debug(easystack.ec_opts)
333+
# if len(general_options) != 0:
334+
# _log.debug("General options for installation are: \n%s" % str(general_options))
335+
# else:
336+
# _log.debug("No general options were specified in easystack")
337+
338+
return easyconfig_names, easystack.ec_opts

easybuild/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
# IMPORTANT this has to be the first easybuild import as it customises the logging
4545
# expect missing log output when this not the case!
46-
from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, stop_logging
46+
from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, print_warning, stop_logging
4747

4848
from easybuild.framework.easyblock import build_and_install_one, inject_checksums
4949
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
@@ -55,7 +55,6 @@
5555
from easybuild.framework.easyconfig.tools import det_easyconfig_paths, dump_env_script, get_paths_for
5656
from easybuild.framework.easyconfig.tools import parse_easyconfigs, review_pr, run_contrib_checks, skip_available
5757
from easybuild.framework.easyconfig.tweak import obtain_ec_for, tweak
58-
from easybuild.tools.build_log import print_warning
5958
from easybuild.tools.config import find_last_log, get_repository, get_repositorypath, build_option
6059
from easybuild.tools.containers.common import containerize
6160
from easybuild.tools.docs import list_software
@@ -266,7 +265,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
266265
# TODO add general_options (i.e. robot) to build options
267266
orig_paths, general_options = parse_easystack(options.easystack)
268267
if general_options:
269-
raise EasyBuildError("Specifying general configuration options in easystack file is not supported yet.")
268+
print_warning("Specifying options in easystack files is not supported yet. They are parsed, but ignored.")
270269

271270
# check whether packaging is supported when it's being used
272271
if options.package:

test/framework/easystack.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,70 @@ def tearDown(self):
5656
easybuild.tools.build_log.EXPERIMENTAL = self.orig_experimental
5757
super(EasyStackTest, self).tearDown()
5858

59+
def test_easystack_basic(self):
60+
"""Test for basic easystack file."""
61+
topdir = os.path.dirname(os.path.abspath(__file__))
62+
test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_basic.yaml')
63+
64+
ec_fns, opts = parse_easystack(test_easystack)
65+
expected = [
66+
'binutils-2.25-GCCcore-4.9.3.eb',
67+
'binutils-2.26-GCCcore-4.9.3.eb',
68+
'foss-2018a.eb',
69+
'toy-0.0-gompi-2018a-test.eb',
70+
]
71+
self.assertEqual(sorted(ec_fns), sorted(expected))
72+
self.assertEqual(opts, {})
73+
74+
def test_easystack_easyconfigs(self):
75+
"""Test for easystack file using 'easyconfigs' key."""
76+
topdir = os.path.dirname(os.path.abspath(__file__))
77+
test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs.yaml')
78+
79+
ec_fns, opts = parse_easystack(test_easystack)
80+
expected = [
81+
'binutils-2.25-GCCcore-4.9.3.eb',
82+
'binutils-2.26-GCCcore-4.9.3.eb',
83+
'foss-2018a.eb',
84+
'toy-0.0-gompi-2018a-test.eb',
85+
]
86+
self.assertEqual(sorted(ec_fns), sorted(expected))
87+
self.assertEqual(opts, {})
88+
89+
def test_easystack_easyconfigs_with_eb_ext(self):
90+
"""Test for easystack file using 'easyconfigs' key, where eb extension is included in the easystack file"""
91+
topdir = os.path.dirname(os.path.abspath(__file__))
92+
test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_with_eb_ext.yaml')
93+
94+
ec_fns, opts = parse_easystack(test_easystack)
95+
expected = [
96+
'binutils-2.25-GCCcore-4.9.3.eb',
97+
'binutils-2.26-GCCcore-4.9.3.eb',
98+
'foss-2018a.eb',
99+
'toy-0.0-gompi-2018a-test.eb',
100+
]
101+
self.assertEqual(sorted(ec_fns), sorted(expected))
102+
self.assertEqual(opts, {})
103+
104+
def test_easystack_easyconfig_opts(self):
105+
"""Test an easystack file using the 'easyconfigs' key, with additonal options for some easyconfigs"""
106+
topdir = os.path.dirname(os.path.abspath(__file__))
107+
test_easystack = os.path.join(topdir, 'easystacks', 'test_easystack_easyconfigs_opts.yaml')
108+
109+
ec_fns, opts = parse_easystack(test_easystack)
110+
expected = [
111+
'binutils-2.25-GCCcore-4.9.3.eb',
112+
'binutils-2.26-GCCcore-4.9.3.eb',
113+
'foss-2018a.eb',
114+
'toy-0.0-gompi-2018a-test.eb',
115+
]
116+
expected_opts = {
117+
'binutils-2.25-GCCcore-4.9.3.eb': {'debug': True, 'from-pr': 12345},
118+
'foss-2018a.eb': {'enforce-checksums': True, 'robot': True},
119+
}
120+
self.assertEqual(sorted(ec_fns), sorted(expected))
121+
self.assertEqual(opts, expected_opts)
122+
59123
def test_parse_fail(self):
60124
"""Test for clean error when easystack file fails to parse."""
61125
test_yml = os.path.join(self.test_prefix, 'test.yml')
@@ -120,15 +184,17 @@ def test_easystack_versions(self):
120184
versions = ('1.2.3', '1.2.30', '2021a', '1.2.3')
121185
for version in versions:
122186
write_file(test_easystack, tmpl_easystack_txt + ' ' + version)
123-
ec_fns, _ = parse_easystack(test_easystack)
187+
ec_fns, opts = parse_easystack(test_easystack)
124188
self.assertEqual(ec_fns, ['foo-%s.eb' % version])
189+
self.assertEqual(opts, {})
125190

126191
# multiple versions as a list
127192
test_easystack_txt = tmpl_easystack_txt + " [1.2.3, 3.2.1]"
128193
write_file(test_easystack, test_easystack_txt)
129-
ec_fns, _ = parse_easystack(test_easystack)
194+
ec_fns, opts = parse_easystack(test_easystack)
130195
expected = ['foo-1.2.3.eb', 'foo-3.2.1.eb']
131196
self.assertEqual(sorted(ec_fns), sorted(expected))
197+
self.assertEqual(opts, {})
132198

133199
# multiple versions listed with more info
134200
test_easystack_txt = '\n'.join([
@@ -139,9 +205,10 @@ def test_easystack_versions(self):
139205
" versionsuffix: -foo",
140206
])
141207
write_file(test_easystack, test_easystack_txt)
142-
ec_fns, _ = parse_easystack(test_easystack)
208+
ec_fns, opts = parse_easystack(test_easystack)
143209
expected = ['foo-1.2.3.eb', 'foo-2021a.eb', 'foo-3.2.1-foo.eb']
144210
self.assertEqual(sorted(ec_fns), sorted(expected))
211+
self.assertEqual(opts, {})
145212

146213
# versions that get interpreted by YAML as float or int, single quotes required
147214
for version in ('1.2', '123', '3.50', '100', '2.44_01'):
@@ -152,8 +219,9 @@ def test_easystack_versions(self):
152219

153220
# all is fine when wrapping the value in single quotes
154221
write_file(test_easystack, tmpl_easystack_txt + " '" + version + "'")
155-
ec_fns, _ = parse_easystack(test_easystack)
222+
ec_fns, opts = parse_easystack(test_easystack)
156223
self.assertEqual(ec_fns, ['foo-%s.eb' % version])
224+
self.assertEqual(opts, {})
157225

158226
# one rotten apple in the basket is enough
159227
test_easystack_txt = tmpl_easystack_txt + " [1.2.3, %s, 3.2.1]" % version
@@ -179,9 +247,10 @@ def test_easystack_versions(self):
179247
" versionsuffix: -foo",
180248
])
181249
write_file(test_easystack, test_easystack_txt)
182-
ec_fns, _ = parse_easystack(test_easystack)
250+
ec_fns, opts = parse_easystack(test_easystack)
183251
expected = ['foo-1.2.3.eb', 'foo-%s.eb' % version, 'foo-3.2.1-foo.eb']
184252
self.assertEqual(sorted(ec_fns), sorted(expected))
253+
self.assertEqual(opts, {})
185254

186255
# also check toolchain version that could be interpreted as a non-string value...
187256
test_easystack_txt = '\n'.join([
@@ -192,9 +261,10 @@ def test_easystack_versions(self):
192261
" versions: [1.2.3, '2.3']",
193262
])
194263
write_file(test_easystack, test_easystack_txt)
195-
ec_fns, _ = parse_easystack(test_easystack)
264+
ec_fns, opts = parse_easystack(test_easystack)
196265
expected = ['test-1.2.3-intel-2021.03.eb', 'test-2.3-intel-2021.03.eb']
197266
self.assertEqual(sorted(ec_fns), sorted(expected))
267+
self.assertEqual(opts, {})
198268

199269

200270
def suite():
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
easyconfigs:
2+
- binutils-2.25-GCCcore-4.9.3
3+
- binutils-2.26-GCCcore-4.9.3
4+
- foss-2018a
5+
- toy-0.0-gompi-2018a-test
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
easyconfigs:
2+
- binutils-2.25-GCCcore-4.9.3:
3+
options: {
4+
'debug': True,
5+
'from-pr': 12345,
6+
}
7+
- binutils-2.26-GCCcore-4.9.3
8+
- foss-2018a:
9+
options: {
10+
'enforce-checksums': True,
11+
'robot': True,
12+
}
13+
- toy-0.0-gompi-2018a-test
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
easyconfigs:
2+
- binutils-2.25-GCCcore-4.9.3.eb
3+
- binutils-2.26-GCCcore-4.9.3.eb
4+
- foss-2018a.eb
5+
- toy-0.0-gompi-2018a-test.eb

0 commit comments

Comments
 (0)