Skip to content

Commit 6f598eb

Browse files
authored
Merge pull request #3845 from Flamefire/alternative_sanity_check_paths
Add option to make sanity_check_paths arch dependent
2 parents 89fc160 + 8f21e20 commit 6f598eb

File tree

6 files changed

+165
-39
lines changed

6 files changed

+165
-39
lines changed

easybuild/framework/easyblock.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
from easybuild.tools.py2vs3 import extract_method_name, string_type
101101
from easybuild.tools.repository.repository import init_repository
102102
from easybuild.tools.systemtools import check_linked_shared_libs, det_parallelism, get_linked_libs_raw
103-
from easybuild.tools.systemtools import get_shared_lib_ext, use_group
103+
from easybuild.tools.systemtools import get_shared_lib_ext, pick_system_specific_value, use_group
104104
from easybuild.tools.utilities import INDENT_4SPACES, get_class_for, nub, quote_str
105105
from easybuild.tools.utilities import remove_unwanted_chars, time2str, trace_msg
106106
from easybuild.tools.version import this_is_easybuild, VERBOSE_VERSION, VERSION
@@ -3327,6 +3327,15 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
33273327
error_msg += "values should be lists (at least one non-empty)."
33283328
raise EasyBuildError(error_msg % ', '.join("'%s'" % k for k in known_keys))
33293329

3330+
# Resolve arch specific entries
3331+
for values in paths.values():
3332+
new_values = []
3333+
for value in values:
3334+
value = pick_system_specific_value('sanity_check_paths', value, allow_none=True)
3335+
if value is not None:
3336+
new_values.append(value)
3337+
values[:] = new_values
3338+
33303339
# if enhance_sanity_check is not enabled, only sanity_check_commands specified in the easyconfig file are used,
33313340
# the ones provided by the easyblock (via custom_commands) are ignored
33323341
if ec_commands and not enhance_sanity_check:

easybuild/framework/easyconfig/types.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,42 @@ def to_list_of_strings_and_tuples_and_dicts(spec):
377377
return str_tup_list
378378

379379

380+
def to_sanity_check_paths_entry(spec):
381+
"""
382+
Convert a 'list of lists and strings' to a 'list of tuples and strings' while allowing dicts of lists or strings
383+
384+
Example:
385+
['foo', ['bar', 'baz'], {'f42': ['a', 'b']}]
386+
to
387+
['foo', ('bar', 'baz'), {'f42': ('a', 'b')}]
388+
"""
389+
result = []
390+
391+
if not isinstance(spec, (list, tuple)):
392+
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
393+
394+
for elem in spec:
395+
if isinstance(elem, (string_type, tuple)):
396+
result.append(elem)
397+
elif isinstance(elem, list):
398+
result.append(tuple(elem))
399+
elif isinstance(elem, dict):
400+
for key, value in elem.items():
401+
if not isinstance(key, string_type):
402+
raise EasyBuildError("Expected keys to be of type string, got %s (%s)", key, type(key))
403+
elif isinstance(value, list):
404+
elem[key] = tuple(value)
405+
elif not isinstance(value, (string_type, tuple)):
406+
raise EasyBuildError("Expected elements to be of type string, tuple or list, got %s (%s)",
407+
value, type(value))
408+
result.append(elem)
409+
else:
410+
raise EasyBuildError("Expected elements to be of type string, tuple/list or dict, got %s (%s)",
411+
elem, type(elem))
412+
413+
return result
414+
415+
380416
def to_sanity_check_paths_dict(spec):
381417
"""
382418
Convert a sanity_check_paths dict as received by yaml (a dict with list values that contain either lists or strings)
@@ -391,7 +427,7 @@ def to_sanity_check_paths_dict(spec):
391427

392428
sanity_check_dict = {}
393429
for key in spec:
394-
sanity_check_dict[key] = to_list_of_strings_and_tuples(spec[key])
430+
sanity_check_dict[key] = to_sanity_check_paths_entry(spec[key])
395431
return sanity_check_dict
396432

397433

@@ -554,11 +590,18 @@ def ensure_iterable_license_specs(specs):
554590
'key_types': [str],
555591
}
556592
))
593+
STRING_OR_TUPLE_DICT = (dict, as_hashable(
594+
{
595+
'elem_types': [str],
596+
'key_types': [str, TUPLE_OF_STRINGS],
597+
}
598+
))
557599
STRING_OR_TUPLE_OR_DICT_LIST = (list, as_hashable({'elem_types': [str, TUPLE_OF_STRINGS, STRING_DICT]}))
600+
SANITY_CHECK_PATHS_ENTRY = (list, as_hashable({'elem_types': [str, TUPLE_OF_STRINGS, STRING_OR_TUPLE_DICT]}))
558601
SANITY_CHECK_PATHS_DICT = (dict, as_hashable({
559602
'elem_types': {
560-
SANITY_CHECK_PATHS_FILES: [STRING_OR_TUPLE_LIST],
561-
SANITY_CHECK_PATHS_DIRS: [STRING_OR_TUPLE_LIST],
603+
SANITY_CHECK_PATHS_FILES: [SANITY_CHECK_PATHS_ENTRY],
604+
SANITY_CHECK_PATHS_DIRS: [SANITY_CHECK_PATHS_ENTRY],
562605
},
563606
'opt_keys': [],
564607
'req_keys': [SANITY_CHECK_PATHS_FILES, SANITY_CHECK_PATHS_DIRS],
@@ -573,8 +616,8 @@ def ensure_iterable_license_specs(specs):
573616
CHECKSUMS = (list, as_hashable({'elem_types': [str, tuple, STRING_DICT, CHECKSUM_LIST]}))
574617

575618
CHECKABLE_TYPES = [CHECKSUM_LIST, CHECKSUMS, DEPENDENCIES, DEPENDENCY_DICT, LIST_OF_STRINGS,
576-
SANITY_CHECK_PATHS_DICT, STRING_DICT, STRING_OR_TUPLE_LIST, STRING_OR_TUPLE_OR_DICT_LIST,
577-
TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
619+
SANITY_CHECK_PATHS_DICT, SANITY_CHECK_PATHS_ENTRY, STRING_DICT, STRING_OR_TUPLE_LIST,
620+
STRING_OR_TUPLE_DICT, STRING_OR_TUPLE_OR_DICT_LIST, TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
578621

579622
# easy types, that can be verified with isinstance
580623
EASY_TYPES = [string_type, bool, dict, int, list, str, tuple]

easybuild/tools/systemtools.py

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,48 +1257,63 @@ def check_python_version():
12571257
return (python_maj_ver, python_min_ver)
12581258

12591259

1260+
def pick_system_specific_value(description, options_or_value, allow_none=False):
1261+
"""Pick an entry for the current system when the input has multiple options
1262+
1263+
:param description: Descriptive string about the value to be retrieved. Used for logging.
1264+
:param options_or_value: Either a dictionary with options to choose from or a value of any other type
1265+
:param allow_none: When True and no matching arch key was found, return None instead of an error
1266+
1267+
:return options_or_value when it is not a dictionary or the matching entry (if existing)
1268+
"""
1269+
result = options_or_value
1270+
if isinstance(options_or_value, dict):
1271+
if not options_or_value:
1272+
raise EasyBuildError("Found empty dict as %s!", description)
1273+
other_keys = [x for x in options_or_value.keys() if not x.startswith(ARCH_KEY_PREFIX)]
1274+
if other_keys:
1275+
other_keys = ','.join(sorted(other_keys))
1276+
raise EasyBuildError("Unexpected keys in %s: %s (only '%s' keys are supported)",
1277+
description, other_keys, ARCH_KEY_PREFIX)
1278+
host_arch_key = ARCH_KEY_PREFIX + get_cpu_architecture()
1279+
star_arch_key = ARCH_KEY_PREFIX + '*'
1280+
# check for specific 'arch=' key first
1281+
try:
1282+
result = options_or_value[host_arch_key]
1283+
_log.info("Selected %s from %s for %s (using key %s)",
1284+
result, options_or_value, description, host_arch_key)
1285+
except KeyError:
1286+
# fall back to 'arch=*'
1287+
try:
1288+
result = options_or_value[star_arch_key]
1289+
_log.info("Selected %s from %s for %s (using fallback key %s)",
1290+
result, options_or_value, description, star_arch_key)
1291+
except KeyError:
1292+
if allow_none:
1293+
result = None
1294+
else:
1295+
raise EasyBuildError("No matches for %s in %s (looking for %s)",
1296+
description, options_or_value, host_arch_key)
1297+
return result
1298+
1299+
12601300
def pick_dep_version(dep_version):
12611301
"""
12621302
Pick the correct dependency version to use for this system.
12631303
Input can either be:
12641304
* a string value (or None)
12651305
* a dict with options to choose from
12661306
1267-
Return value is the version to use.
1307+
Return value is the version to use or False to skip this dependency.
12681308
"""
1269-
if isinstance(dep_version, string_type):
1270-
_log.debug("Version is already a string ('%s'), OK", dep_version)
1271-
result = dep_version
1272-
1273-
elif dep_version is None:
1309+
if dep_version is None:
12741310
_log.debug("Version is None, OK")
12751311
result = None
1276-
1277-
elif isinstance(dep_version, dict):
1278-
arch_keys = [x for x in dep_version.keys() if x.startswith(ARCH_KEY_PREFIX)]
1279-
other_keys = [x for x in dep_version.keys() if x not in arch_keys]
1280-
if other_keys:
1281-
other_keys = ','.join(sorted(other_keys))
1282-
raise EasyBuildError("Unexpected keys in version: %s (only 'arch=' keys are supported)", other_keys)
1283-
if arch_keys:
1284-
host_arch_key = ARCH_KEY_PREFIX + get_cpu_architecture()
1285-
star_arch_key = ARCH_KEY_PREFIX + '*'
1286-
# check for specific 'arch=' key first
1287-
if host_arch_key in dep_version:
1288-
result = dep_version[host_arch_key]
1289-
_log.info("Version selected from %s using key %s: %s", dep_version, host_arch_key, result)
1290-
# fall back to 'arch=*'
1291-
elif star_arch_key in dep_version:
1292-
result = dep_version[star_arch_key]
1293-
_log.info("Version selected for %s using fallback key %s: %s", dep_version, star_arch_key, result)
1294-
else:
1295-
raise EasyBuildError("No matches for version in %s (looking for %s)", dep_version, host_arch_key)
1296-
else:
1297-
raise EasyBuildError("Found empty dict as version!")
1298-
12991312
else:
1300-
typ = type(dep_version)
1301-
raise EasyBuildError("Unknown value type for version: %s (%s), should be string value", typ, dep_version)
1313+
result = pick_system_specific_value("version", dep_version)
1314+
if not isinstance(result, string_type) and result is not False:
1315+
typ = type(dep_version)
1316+
raise EasyBuildError("Unknown value type for version: %s (%s), should be string value", typ, dep_version)
13021317

13031318
return result
13041319

test/framework/easyblock.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, init_config
3939
from unittest import TextTestRunner
4040

41+
import easybuild.tools.systemtools as st
4142
from easybuild.framework.easyblock import EasyBlock, get_easyblock_instance
4243
from easybuild.framework.easyconfig import CUSTOM
4344
from easybuild.framework.easyconfig.easyconfig import EasyConfig
@@ -2744,6 +2745,32 @@ def test_avail_easyblocks(self):
27442745
self.assertEqual(hpl['class'], 'EB_HPL')
27452746
self.assertTrue(hpl['loc'].endswith('sandbox/easybuild/easyblocks/h/hpl.py'))
27462747

2748+
def test_arch_specific_sanity_check(self):
2749+
"""Tests that the correct version is chosen for this architecture"""
2750+
2751+
my_arch = st.get_cpu_architecture()
2752+
2753+
self.contents = '\n'.join([
2754+
'easyblock = "ConfigureMake"',
2755+
'name = "test"',
2756+
'version = "0.2"',
2757+
'homepage = "https://example.com"',
2758+
'description = "test"',
2759+
'toolchain = SYSTEM',
2760+
'sanity_check_paths = {',
2761+
" 'files': [{'arch=%s': 'correct.a'}, 'default.a']," % my_arch,
2762+
" 'dirs': [{'arch=%s': ('correct', 'alternative')}, {'arch=no-arch': 'not-used'}]," % my_arch,
2763+
'}',
2764+
])
2765+
self.writeEC()
2766+
ec = EasyConfig(self.eb_file)
2767+
eb = EasyBlock(ec)
2768+
paths, _, _ = eb._sanity_check_step_common(None, None)
2769+
2770+
self.assertEqual(set(paths.keys()), set(('files', 'dirs')))
2771+
self.assertEqual(paths['files'], ['correct.a', 'default.a'])
2772+
self.assertEqual(paths['dirs'], [('correct', 'alternative')])
2773+
27472774
def test_sanity_check_paths_verification(self):
27482775
"""Test verification of sanity_check_paths w.r.t. keys & values."""
27492776

test/framework/systemtools.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from easybuild.tools.systemtools import get_cpu_family, get_cpu_features, get_cpu_model, get_cpu_speed, get_cpu_vendor
5252
from easybuild.tools.systemtools import get_gcc_version, get_glibc_version, get_os_type, get_os_name, get_os_version
5353
from easybuild.tools.systemtools import get_platform_name, get_shared_lib_ext, get_system_info, get_total_memory
54-
from easybuild.tools.systemtools import find_library_path, locate_solib, pick_dep_version
54+
from easybuild.tools.systemtools import find_library_path, locate_solib, pick_dep_version, pick_system_specific_value
5555

5656

5757
PROC_CPUINFO_TXT = None
@@ -933,6 +933,38 @@ def test_pick_dep_version(self):
933933
error_pattern = r"Unknown value type for version: .* \(1.23\), should be string value"
934934
self.assertErrorRegex(EasyBuildError, error_pattern, pick_dep_version, 1.23)
935935

936+
def test_pick_system_specific_value(self):
937+
"""Test pick_system_specific_value function."""
938+
939+
self.assertEqual(pick_system_specific_value('test-desc', None), None)
940+
self.assertEqual(pick_system_specific_value('test-desc', '1.2.3'), '1.2.3')
941+
self.assertEqual(pick_system_specific_value('test-desc', (42, 'foobar')), (42, 'foobar'))
942+
943+
option_dict = {
944+
'arch=x86_64': '1.2.3-amd64',
945+
'arch=POWER': '1.2.3-ppc64le',
946+
'arch=*': '1.2.3-other',
947+
}
948+
949+
st.get_cpu_architecture = lambda: X86_64
950+
self.assertEqual(pick_system_specific_value('test-desc', option_dict), '1.2.3-amd64')
951+
952+
st.get_cpu_architecture = lambda: POWER
953+
self.assertEqual(pick_system_specific_value('test-desc', option_dict), '1.2.3-ppc64le')
954+
955+
st.get_cpu_architecture = lambda: "NON_EXISTING_ARCH"
956+
self.assertEqual(pick_system_specific_value('test-desc', option_dict), '1.2.3-other')
957+
958+
error_pattern = "Found empty dict as test-desc"
959+
self.assertErrorRegex(EasyBuildError, error_pattern, pick_system_specific_value, 'test-desc', {})
960+
961+
error_pattern = r"Unexpected keys in test-desc: foo \(only 'arch=' keys are supported\)"
962+
self.assertErrorRegex(EasyBuildError, error_pattern, pick_system_specific_value, 'test-desc',
963+
{'foo': '1'})
964+
error_pattern = r"Unexpected keys in test-desc: foo \(only 'arch=' keys are supported\)"
965+
self.assertErrorRegex(EasyBuildError, error_pattern, pick_system_specific_value, 'test-desc',
966+
{'foo': '1', 'arch=POWER': '2'})
967+
936968
def test_check_os_dependency(self):
937969
"""Test check_os_dependency."""
938970

test/framework/type_checking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ def test_to_sanity_check_paths_dict(self):
701701
self.assertErrorRegex(EasyBuildError, "Expected value to be a dict", to_sanity_check_paths_dict, [])
702702
error_msg = "Expected value to be a list"
703703
self.assertErrorRegex(EasyBuildError, error_msg, to_sanity_check_paths_dict, {'files': 'foo', 'dirs': []})
704-
error_msg = "Expected elements to be of type string, tuple or list"
704+
error_msg = "Expected elements to be of type string, tuple/list or dict"
705705
self.assertErrorRegex(EasyBuildError, error_msg, to_sanity_check_paths_dict, {'files': [], 'dirs': [1]})
706706

707707
def test_to_checksums(self):

0 commit comments

Comments
 (0)