Skip to content

Commit 81e59c3

Browse files
committed
Add option to make sanity_check_paths arch dependent
1 parent a8c0cad commit 81e59c3

File tree

5 files changed

+165
-36
lines changed

5 files changed

+165
-36
lines changed

easybuild/framework/easyblock.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
from easybuild.tools.package.utilities import package
9797
from easybuild.tools.py2vs3 import extract_method_name, string_type
9898
from easybuild.tools.repository.repository import init_repository
99-
from easybuild.tools.systemtools import check_linked_shared_libs, det_parallelism, get_shared_lib_ext, use_group
99+
from easybuild.tools.systemtools import check_linked_shared_libs, det_parallelism, get_shared_lib_ext
100+
from easybuild.tools.systemtools import pick_system_specific_value, use_group
100101
from easybuild.tools.utilities import INDENT_4SPACES, get_class_for, nub, quote_str
101102
from easybuild.tools.utilities import remove_unwanted_chars, time2str, trace_msg
102103
from easybuild.tools.version import this_is_easybuild, VERBOSE_VERSION, VERSION
@@ -3227,6 +3228,15 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
32273228
error_msg += "values should be lists (at least one non-empty)."
32283229
raise EasyBuildError(error_msg % ', '.join("'%s'" % k for k in known_keys))
32293230

3231+
# Resolve arch specific entries
3232+
for values in paths.values():
3233+
new_values = []
3234+
for value in values:
3235+
value = pick_system_specific_value('sanity_check_paths', value, allow_none=True)
3236+
if value is not None:
3237+
new_values.append(value)
3238+
values[:] = new_values
3239+
32303240
# if enhance_sanity_check is not enabled, only sanity_check_commands specified in the easyconfig file are used,
32313241
# the ones provided by the easyblock (via custom_commands) are ignored
32323242
if ec_commands and not enhance_sanity_check:

easybuild/framework/easyconfig/types.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,43 @@ def to_list_of_strings_and_tuples(spec):
349349
return str_tup_list
350350

351351

352+
def to_sanity_check_paths_entry(spec):
353+
"""
354+
Convert a 'list of lists and strings' to a 'list of tuples and strings' while allowing dicts of lists or strings
355+
356+
Example:
357+
['foo', ['bar', 'baz'], {'f42': ['a', 'b']}]
358+
to
359+
['foo', ('bar', 'baz'), {'f42': ('a', 'b')}]
360+
"""
361+
result = []
362+
363+
if not isinstance(spec, (list, tuple)):
364+
raise EasyBuildError("Expected value to be a list, found %s (%s)", spec, type(spec))
365+
366+
for elem in spec:
367+
if isinstance(elem, (string_type, tuple)):
368+
result.append(elem)
369+
elif isinstance(elem, list):
370+
result.append(tuple(elem))
371+
elif isinstance(elem, dict):
372+
result.append({})
373+
for key, value in elem.items():
374+
if not isinstance(key, string_type):
375+
raise EasyBuildError("Expected keys to be of type string, got %s (%s)", key, type(key))
376+
if isinstance(value, (string_type, tuple)):
377+
result[-1][key] = value
378+
elif isinstance(value, list):
379+
result[-1][key] = tuple(value)
380+
else:
381+
raise EasyBuildError("Expected elements to be of type string, tuple or list, got %s (%s)",
382+
value, type(value))
383+
else:
384+
raise EasyBuildError("Expected elements to be of type string, tuple or list, got %s (%s)", elem, type(elem))
385+
386+
return result
387+
388+
352389
def to_sanity_check_paths_dict(spec):
353390
"""
354391
Convert a sanity_check_paths dict as received by yaml (a dict with list values that contain either lists or strings)
@@ -363,7 +400,7 @@ def to_sanity_check_paths_dict(spec):
363400

364401
sanity_check_dict = {}
365402
for key in spec:
366-
sanity_check_dict[key] = to_list_of_strings_and_tuples(spec[key])
403+
sanity_check_dict[key] = to_sanity_check_paths_entry(spec[key])
367404
return sanity_check_dict
368405

369406

@@ -526,10 +563,17 @@ def ensure_iterable_license_specs(specs):
526563
'key_types': [str],
527564
}
528565
))
566+
STRING_OR_TUPLE_DICT = (dict, as_hashable(
567+
{
568+
'elem_types': [str],
569+
'key_types': [str, TUPLE_OF_STRINGS],
570+
}
571+
))
572+
SANITY_CHECK_PATHS_ENTRY = (list, as_hashable({'elem_types': [str, TUPLE_OF_STRINGS, STRING_OR_TUPLE_DICT]}))
529573
SANITY_CHECK_PATHS_DICT = (dict, as_hashable({
530574
'elem_types': {
531-
SANITY_CHECK_PATHS_FILES: [STRING_OR_TUPLE_LIST],
532-
SANITY_CHECK_PATHS_DIRS: [STRING_OR_TUPLE_LIST],
575+
SANITY_CHECK_PATHS_FILES: [SANITY_CHECK_PATHS_ENTRY],
576+
SANITY_CHECK_PATHS_DIRS: [SANITY_CHECK_PATHS_ENTRY],
533577
},
534578
'opt_keys': [],
535579
'req_keys': [SANITY_CHECK_PATHS_FILES, SANITY_CHECK_PATHS_DIRS],
@@ -544,7 +588,8 @@ def ensure_iterable_license_specs(specs):
544588
CHECKSUMS = (list, as_hashable({'elem_types': [str, tuple, STRING_DICT, CHECKSUM_LIST]}))
545589

546590
CHECKABLE_TYPES = [CHECKSUM_LIST, CHECKSUMS, DEPENDENCIES, DEPENDENCY_DICT, LIST_OF_STRINGS,
547-
SANITY_CHECK_PATHS_DICT, STRING_DICT, STRING_OR_TUPLE_LIST, TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
591+
SANITY_CHECK_PATHS_DICT, SANITY_CHECK_PATHS_ENTRY, STRING_DICT, STRING_OR_TUPLE_LIST,
592+
TOOLCHAIN_DICT, TUPLE_OF_STRINGS]
548593

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

easybuild/tools/systemtools.py

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,46 @@ def check_python_version():
12151215
return (python_maj_ver, python_min_ver)
12161216

12171217

1218+
def pick_system_specific_value(description, options_or_value, allow_none=False):
1219+
"""Pick an entry for the current system when the input has multiple options
1220+
1221+
:param description: Descriptive string about the value to be retrieved. Used for logging.
1222+
:param options_or_value: Either a dictionary with options to choose from or a value of any other type
1223+
:param allow_none: When True and no matching arch key was found, return None instead of an error
1224+
1225+
:return options_or_value when it is not a dictionary or the matching entry (if existing)
1226+
"""
1227+
result = options_or_value
1228+
if isinstance(options_or_value, dict):
1229+
if not options_or_value:
1230+
raise EasyBuildError("Found empty dict as %s!", description)
1231+
other_keys = [x for x in options_or_value.keys() if not x.startswith(ARCH_KEY_PREFIX)]
1232+
if other_keys:
1233+
other_keys = ','.join(sorted(other_keys))
1234+
raise EasyBuildError("Unexpected keys in %s: %s (only '%s' keys are supported)",
1235+
description, other_keys, ARCH_KEY_PREFIX)
1236+
host_arch_key = ARCH_KEY_PREFIX + get_cpu_architecture()
1237+
star_arch_key = ARCH_KEY_PREFIX + '*'
1238+
# check for specific 'arch=' key first
1239+
try:
1240+
result = options_or_value[host_arch_key]
1241+
_log.info("Selected %s from %s for %s (using key %s)",
1242+
result, options_or_value, description, host_arch_key)
1243+
except KeyError:
1244+
# fall back to 'arch=*'
1245+
try:
1246+
result = options_or_value[star_arch_key]
1247+
_log.info("Selected %s from %s for %s (using fallback key %s)",
1248+
result, options_or_value, description, star_arch_key)
1249+
except KeyError:
1250+
if allow_none:
1251+
result = None
1252+
else:
1253+
raise EasyBuildError("No matches for %s in %s (looking for %s)",
1254+
description, options_or_value, host_arch_key)
1255+
return result
1256+
1257+
12181258
def pick_dep_version(dep_version):
12191259
"""
12201260
Pick the correct dependency version to use for this system.
@@ -1224,39 +1264,14 @@ def pick_dep_version(dep_version):
12241264
12251265
Return value is the version to use.
12261266
"""
1227-
if isinstance(dep_version, string_type):
1228-
_log.debug("Version is already a string ('%s'), OK", dep_version)
1229-
result = dep_version
1230-
1231-
elif dep_version is None:
1267+
if dep_version is None:
12321268
_log.debug("Version is None, OK")
12331269
result = None
1234-
1235-
elif isinstance(dep_version, dict):
1236-
arch_keys = [x for x in dep_version.keys() if x.startswith(ARCH_KEY_PREFIX)]
1237-
other_keys = [x for x in dep_version.keys() if x not in arch_keys]
1238-
if other_keys:
1239-
other_keys = ','.join(sorted(other_keys))
1240-
raise EasyBuildError("Unexpected keys in version: %s (only 'arch=' keys are supported)", other_keys)
1241-
if arch_keys:
1242-
host_arch_key = ARCH_KEY_PREFIX + get_cpu_architecture()
1243-
star_arch_key = ARCH_KEY_PREFIX + '*'
1244-
# check for specific 'arch=' key first
1245-
if host_arch_key in dep_version:
1246-
result = dep_version[host_arch_key]
1247-
_log.info("Version selected from %s using key %s: %s", dep_version, host_arch_key, result)
1248-
# fall back to 'arch=*'
1249-
elif star_arch_key in dep_version:
1250-
result = dep_version[star_arch_key]
1251-
_log.info("Version selected for %s using fallback key %s: %s", dep_version, star_arch_key, result)
1252-
else:
1253-
raise EasyBuildError("No matches for version in %s (looking for %s)", dep_version, host_arch_key)
1254-
else:
1255-
raise EasyBuildError("Found empty dict as version!")
1256-
12571270
else:
1258-
typ = type(dep_version)
1259-
raise EasyBuildError("Unknown value type for version: %s (%s), should be string value", typ, dep_version)
1271+
result = pick_system_specific_value("version", dep_version)
1272+
if not isinstance(result, string_type):
1273+
typ = type(dep_version)
1274+
raise EasyBuildError("Unknown value type for version: %s (%s), should be string value", typ, dep_version)
12601275

12611276
return result
12621277

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
@@ -2543,6 +2544,32 @@ def test_avail_easyblocks(self):
25432544
self.assertEqual(hpl['class'], 'EB_HPL')
25442545
self.assertTrue(hpl['loc'].endswith('sandbox/easybuild/easyblocks/h/hpl.py'))
25452546

2547+
def test_arch_specific_sanity_check(self):
2548+
"""Tests that the correct version is chosen for this architecture"""
2549+
2550+
my_arch = st.get_cpu_architecture()
2551+
2552+
self.contents = '\n'.join([
2553+
'easyblock = "ConfigureMake"',
2554+
'name = "test"',
2555+
'version = "0.2"',
2556+
'homepage = "https://example.com"',
2557+
'description = "test"',
2558+
'toolchain = SYSTEM',
2559+
'sanity_check_paths = {',
2560+
" 'files': [{'arch=%s': 'correct.a'}, 'default.a']," % my_arch,
2561+
" 'dirs': [{'arch=%s': ('correct', 'alternative')}, {'arch=no-arch': 'not-used'}]," % my_arch,
2562+
'}',
2563+
])
2564+
self.writeEC()
2565+
ec = EasyConfig(self.eb_file)
2566+
eb = EasyBlock(ec)
2567+
paths, _, _ = eb._sanity_check_step_common(None, None)
2568+
2569+
self.assertEqual(set(paths.keys()), set(('files', 'dirs')))
2570+
self.assertEqual(paths['files'], ['correct.a', 'default.a'])
2571+
self.assertEqual(paths['dirs'], [('correct', 'alternative')])
2572+
25462573
def test_sanity_check_paths_verification(self):
25472574
"""Test verification of sanity_check_paths w.r.t. keys & values."""
25482575

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

0 commit comments

Comments
 (0)