Skip to content

Commit 5675133

Browse files
committed
Merge branch 'develop' into lock_cleanup
2 parents 482975a + 4845565 commit 5675133

File tree

14 files changed

+833
-205
lines changed

14 files changed

+833
-205
lines changed

easybuild/framework/easyblock.py

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,37 +2409,71 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
24092409
SANITY_CHECK_PATHS_DIRS: ("(non-empty) directory", lambda dp: os.path.isdir(dp) and os.listdir(dp)),
24102410
}
24112411

2412-
# prepare sanity check paths
2413-
paths = self.cfg['sanity_check_paths']
2414-
if not paths:
2412+
enhance_sanity_check = self.cfg['enhance_sanity_check']
2413+
ec_commands = self.cfg['sanity_check_commands']
2414+
ec_paths = self.cfg['sanity_check_paths']
2415+
2416+
# if enhance_sanity_check is not enabled, only sanity_check_paths specified in the easyconfig file are used,
2417+
# the ones provided by the easyblock (via custom_paths) are ignored
2418+
if ec_paths and not enhance_sanity_check:
2419+
paths = ec_paths
2420+
self.log.info("Using (only) sanity check paths specified by easyconfig file: %s", paths)
2421+
else:
2422+
# if no sanity_check_paths are specified in easyconfig,
2423+
# we fall back to the ones provided by the easyblock via custom_paths
24152424
if custom_paths:
24162425
paths = custom_paths
2417-
self.log.info("Using customized sanity check paths: %s" % paths)
2426+
self.log.info("Using customized sanity check paths: %s", paths)
2427+
# if custom_paths is empty, we fall back to a generic set of paths:
2428+
# non-empty bin/ + /lib or /lib64 directories
24182429
else:
24192430
paths = {}
24202431
for key in path_keys_and_check:
24212432
paths.setdefault(key, [])
24222433
paths.update({SANITY_CHECK_PATHS_DIRS: ['bin', ('lib', 'lib64')]})
2423-
self.log.info("Using default sanity check paths: %s" % paths)
2434+
self.log.info("Using default sanity check paths: %s", paths)
2435+
2436+
# if enhance_sanity_check is enabled *and* sanity_check_paths are specified in the easyconfig,
2437+
# those paths are used to enhance the paths provided by the easyblock
2438+
if enhance_sanity_check and ec_paths:
2439+
for key in ec_paths:
2440+
val = ec_paths[key]
2441+
if isinstance(val, list):
2442+
paths[key] = paths.get(key, []) + val
2443+
else:
2444+
error_pattern = "Incorrect value type in sanity_check_paths, should be a list: "
2445+
error_pattern += "%s (type: %s)" % (val, type(val))
2446+
raise EasyBuildError(error_pattern)
2447+
self.log.info("Enhanced sanity check paths after taking into account easyconfig file: %s", paths)
2448+
2449+
sorted_keys = sorted(paths.keys())
2450+
known_keys = sorted(path_keys_and_check.keys())
2451+
2452+
# verify sanity_check_paths value: only known keys, correct value types, at least one non-empty value
2453+
only_list_values = all(isinstance(x, list) for x in paths.values())
2454+
only_empty_lists = all(not x for x in paths.values())
2455+
if sorted_keys != known_keys or not only_list_values or only_empty_lists:
2456+
error_msg = "Incorrect format for sanity_check_paths: should (only) have %s keys, "
2457+
error_msg += "values should be lists (at least one non-empty)."
2458+
raise EasyBuildError(error_msg % ', '.join("'%s'" % k for k in known_keys))
2459+
2460+
# if enhance_sanity_check is not enabled, only sanity_check_commands specified in the easyconfig file are used,
2461+
# the ones provided by the easyblock (via custom_commands) are ignored
2462+
if ec_commands and not enhance_sanity_check:
2463+
commands = ec_commands
2464+
self.log.info("Using (only) sanity check commands specified by easyconfig file: %s", commands)
24242465
else:
2425-
self.log.info("Using specified sanity check paths: %s" % paths)
2426-
2427-
ks = sorted(paths.keys())
2428-
valnottypes = [not isinstance(x, list) for x in paths.values()]
2429-
lenvals = [len(x) for x in paths.values()]
2430-
req_keys = sorted(path_keys_and_check.keys())
2431-
if not ks == req_keys or sum(valnottypes) > 0 or sum(lenvals) == 0:
2432-
raise EasyBuildError("Incorrect format for sanity_check_paths (should (only) have %s keys, "
2433-
"values should be lists (at least one non-empty)).", ','.join(req_keys))
2434-
2435-
commands = self.cfg['sanity_check_commands']
2436-
if not commands:
24372466
if custom_commands:
24382467
commands = custom_commands
2439-
self.log.info("Using customised sanity check commands: %s" % commands)
2468+
self.log.info("Using customised sanity check commands: %s", commands)
24402469
else:
24412470
commands = []
2442-
self.log.info("Using specified sanity check commands: %s" % commands)
2471+
2472+
# if enhance_sanity_check is enabled, the sanity_check_commands specified in the easyconfig file
2473+
# are combined with those provided by the easyblock via custom_commands
2474+
if enhance_sanity_check and ec_commands:
2475+
commands = commands + ec_commands
2476+
self.log.info("Enhanced sanity check commands after taking into account easyconfig file: %s", commands)
24432477

24442478
for i, command in enumerate(commands):
24452479
# set command to default. This allows for config files with
@@ -2475,9 +2509,17 @@ def _sanity_check_step_dry_run(self, custom_paths=None, custom_commands=None, **
24752509
"""
24762510
paths, path_keys_and_check, commands = self._sanity_check_step_common(custom_paths, custom_commands)
24772511

2478-
for key, (typ, _) in path_keys_and_check.items():
2512+
for key in [SANITY_CHECK_PATHS_FILES, SANITY_CHECK_PATHS_DIRS]:
2513+
(typ, _) = path_keys_and_check[key]
24792514
self.dry_run_msg("Sanity check paths - %s ['%s']", typ, key)
2480-
if paths[key]:
2515+
entries = paths[key]
2516+
if entries:
2517+
# some entries may be tuple values,
2518+
# we need to convert them to strings first so we can print them sorted
2519+
for idx, entry in enumerate(entries):
2520+
if isinstance(entry, tuple):
2521+
entries[idx] = ' or '.join(entry)
2522+
24812523
for path in sorted(paths[key]):
24822524
self.dry_run_msg(" * %s", str(path))
24832525
else:
@@ -2608,6 +2650,9 @@ def xs2str(xs):
26082650

26092651
# run sanity check commands
26102652
for command in commands:
2653+
2654+
trace_msg("running command '%s' ..." % command)
2655+
26112656
out, ec = run_cmd(command, simple=False, log_ok=False, log_all=False, trace=False)
26122657
if ec != 0:
26132658
fail_msg = "sanity check command %s exited with code %s (output: %s)" % (command, ec, out)
@@ -2616,7 +2661,7 @@ def xs2str(xs):
26162661
else:
26172662
self.log.info("sanity check command %s ran successfully! (output: %s)" % (command, out))
26182663

2619-
trace_msg("running command '%s': %s" % (command, ('FAILED', 'OK')[ec == 0]))
2664+
trace_msg("result for command '%s': %s" % (command, ('FAILED', 'OK')[ec == 0]))
26202665

26212666
# also run sanity check for extensions (unless we are an extension ourselves)
26222667
if not extension:

easybuild/framework/easyconfig/default.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@
9090
'easyblock': [None, "EasyBlock to use for building; if set to None, an easyblock is selected "
9191
"based on the software name", BUILD],
9292
'easybuild_version': [None, "EasyBuild-version this spec-file was written for", BUILD],
93+
'enhance_sanity_check': [False, "Indicate that additional sanity check commands & paths should enhance "
94+
"the existin sanity check, not replace it", BUILD],
9395
'fix_perl_shebang_for': [None, "List of files for which Perl shebang should be fixed "
9496
"to '#!/usr/bin/env perl' (glob patterns supported)", BUILD],
9597
'fix_python_shebang_for': [None, "List of files for which Python shebang should be fixed "

easybuild/framework/easyconfig/format/one.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ class FormatOneZero(EasyConfigFormatConfigObj):
9797
PYHEADER_MANDATORY = ['version', 'name', 'toolchain', 'homepage', 'description']
9898
PYHEADER_BLACKLIST = []
9999

100+
def __init__(self, *args, **kwargs):
101+
"""FormatOneZero constructor."""
102+
super(FormatOneZero, self).__init__(*args, **kwargs)
103+
104+
self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)
105+
self.strict_sanity_check_paths_keys = True
106+
100107
def validate(self):
101108
"""Format validation"""
102109
# minimal checks
@@ -168,11 +175,14 @@ def _reformat_line(self, param_name, param_val, outer=False, addlen=0):
168175
for item_key in ordered_item_keys:
169176
if item_key in param_val:
170177
item_val = param_val[item_key]
178+
item_comments = self._get_item_comments(param_name, item_val)
179+
elif param_name == 'sanity_check_paths' and not self.strict_sanity_check_paths_keys:
180+
item_val = []
181+
item_comments = {}
182+
self.log.info("Using default value for '%s' in sanity_check_paths: %s", item_key, item_val)
171183
else:
172184
raise EasyBuildError("Missing mandatory key '%s' in %s.", item_key, param_name)
173185

174-
item_comments = self._get_item_comments(param_name, item_val)
175-
176186
inline_comment = item_comments.get('inline', '')
177187
item_tmpl_dict = {'inline_comment': inline_comment}
178188

@@ -317,6 +327,10 @@ def dump(self, ecfg, default_values, templ_const, templ_val, toolchain_hierarchy
317327
:param templ_val: known template values
318328
:param toolchain_hierarchy: hierarchy of toolchains for easyconfig
319329
"""
330+
# figoure out whether we should be strict about the format of sanity_check_paths;
331+
# if enhance_sanity_check is set, then both files/dirs keys are not strictly required...
332+
self.strict_sanity_check_paths_keys = not ecfg['enhance_sanity_check']
333+
320334
# include header comments first
321335
dump = self.comments['header'][:]
322336

easybuild/tools/filetools.py

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
"""
4141
import datetime
4242
import difflib
43-
import distutils.dir_util
4443
import fileinput
4544
import glob
4645
import hashlib
@@ -499,7 +498,13 @@ def pypi_source_urls(pkg_name):
499498
_log.debug("Failed to download %s to determine available PyPI URLs for %s", simple_url, pkg_name)
500499
res = []
501500
else:
502-
parsed_html = ElementTree.parse(urls_html)
501+
urls_txt = read_file(urls_html)
502+
503+
# ignore yanked releases (see https://pypi.org/help/#yanked)
504+
# see https://github.com/easybuilders/easybuild-framework/issues/3301
505+
urls_txt = re.sub(r'<a.*?data-yanked.*?</a>', '', urls_txt)
506+
507+
parsed_html = ElementTree.ElementTree(ElementTree.fromstring(urls_txt))
503508
if hasattr(parsed_html, 'iter'):
504509
res = [a.attrib['href'] for a in parsed_html.iter('a')]
505510
else:
@@ -762,6 +767,18 @@ def find_easyconfigs(path, ignore_dirs=None):
762767
return files
763768

764769

770+
def find_glob_pattern(glob_pattern, fail_on_no_match=True):
771+
"""Find unique file/dir matching glob_pattern (raises error if more than one match is found)"""
772+
if build_option('extended_dry_run'):
773+
return glob_pattern
774+
res = glob.glob(glob_pattern)
775+
if len(res) == 0 and not fail_on_no_match:
776+
return None
777+
if len(res) != 1:
778+
raise EasyBuildError("Was expecting exactly one match for '%s', found %d: %s", glob_pattern, len(res), res)
779+
return res[0]
780+
781+
765782
def search_file(paths, query, short=False, ignore_dirs=None, silent=False, filename_only=False, terse=False,
766783
case_sensitive=False):
767784
"""
@@ -2008,7 +2025,12 @@ def copy_file(path, target_path, force_in_dry_run=False):
20082025
_log.info("Copied contents of file %s to %s", path, target_path)
20092026
else:
20102027
mkdir(os.path.dirname(target_path), parents=True)
2011-
shutil.copy2(path, target_path)
2028+
if os.path.exists(path):
2029+
shutil.copy2(path, target_path)
2030+
elif os.path.islink(path):
2031+
# special care for copying broken symlinks
2032+
link_target = os.readlink(path)
2033+
symlink(link_target, target_path)
20122034
_log.info("%s copied to %s", path, target_path)
20132035
except (IOError, OSError, shutil.Error) as err:
20142036
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
@@ -2043,16 +2065,13 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
20432065
:param path: the original directory path
20442066
:param target_path: path to copy the directory to
20452067
:param force_in_dry_run: force running the command during dry run
2046-
:param dirs_exist_ok: wrapper around shutil.copytree option, which was added in Python 3.8
2068+
:param dirs_exist_ok: boolean indicating whether it's OK if the target directory already exists
20472069
2048-
On Python >= 3.8 shutil.copytree is always used
2049-
On Python < 3.8 if 'dirs_exist_ok' is False - shutil.copytree is used
2050-
On Python < 3.8 if 'dirs_exist_ok' is True - distutils.dir_util.copy_tree is used
2070+
shutil.copytree is used if the target path does not exist yet;
2071+
if the target path already exists, the 'copy' function will be used to copy the contents of
2072+
the source path to the target path
20512073
2052-
Additional specified named arguments are passed down to shutil.copytree if used.
2053-
2054-
Because distutils.dir_util.copy_tree supports only 'symlinks' named argument,
2055-
using any other will raise EasyBuildError.
2074+
Additional specified named arguments are passed down to shutil.copytree/copy if used.
20562075
"""
20572076
if not force_in_dry_run and build_option('extended_dry_run'):
20582077
dry_run_msg("copied directory %s to %s" % (path, target_path))
@@ -2061,38 +2080,49 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
20612080
if not dirs_exist_ok and os.path.exists(target_path):
20622081
raise EasyBuildError("Target location %s to copy %s to already exists", target_path, path)
20632082

2064-
if sys.version_info >= (3, 8):
2065-
# on Python >= 3.8, shutil.copytree works fine, thanks to availability of dirs_exist_ok named argument
2066-
shutil.copytree(path, target_path, dirs_exist_ok=dirs_exist_ok, **kwargs)
2083+
# note: in Python >= 3.8 shutil.copytree works just fine thanks to the 'dirs_exist_ok' argument,
2084+
# but since we need to be more careful in earlier Python versions we use our own implementation
2085+
# in case the target directory exists and 'dirs_exist_ok' is enabled
2086+
if dirs_exist_ok and os.path.exists(target_path):
2087+
# if target directory already exists (and that's allowed via dirs_exist_ok),
2088+
# we need to be more careful, since shutil.copytree will fail (in Python < 3.8)
2089+
# if target directory already exists;
2090+
# so, recurse via 'copy' function to copy files/dirs in source path to target path
2091+
# (NOTE: don't use distutils.dir_util.copy_tree here, see
2092+
# https://github.com/easybuilders/easybuild-framework/issues/3306)
2093+
2094+
entries = os.listdir(path)
20672095

2068-
elif dirs_exist_ok:
2069-
# use distutils.dir_util.copy_tree with Python < 3.8 if dirs_exist_ok is enabled
2096+
# take into account 'ignore' function that is supported by shutil.copytree
2097+
# (but not by 'copy_file' function used by 'copy')
2098+
ignore = kwargs.get('ignore')
2099+
if ignore:
2100+
ignored_entries = ignore(path, entries)
2101+
entries = [x for x in entries if x not in ignored_entries]
20702102

2071-
# first get value for symlinks named argument (if any)
2072-
preserve_symlinks = kwargs.pop('symlinks', False)
2103+
# determine list of paths to copy
2104+
paths_to_copy = [os.path.join(path, x) for x in entries]
20732105

2074-
# check if there are other named arguments (there shouldn't be, only 'symlinks' is supported)
2075-
if kwargs:
2076-
raise EasyBuildError("Unknown named arguments passed to copy_dir with dirs_exist_ok=True: %s",
2077-
', '.join(sorted(kwargs.keys())))
2078-
distutils.dir_util.copy_tree(path, target_path, preserve_symlinks=preserve_symlinks)
2106+
copy(paths_to_copy, target_path,
2107+
force_in_dry_run=force_in_dry_run, dirs_exist_ok=dirs_exist_ok, **kwargs)
20792108

20802109
else:
2081-
# if dirs_exist_ok is not enabled, just use shutil.copytree
2110+
# if dirs_exist_ok is not enabled or target directory doesn't exist, just use shutil.copytree
20822111
shutil.copytree(path, target_path, **kwargs)
20832112

20842113
_log.info("%s copied to %s", path, target_path)
2085-
except (IOError, OSError) as err:
2114+
except (IOError, OSError, shutil.Error) as err:
20862115
raise EasyBuildError("Failed to copy directory %s to %s: %s", path, target_path, err)
20872116

20882117

2089-
def copy(paths, target_path, force_in_dry_run=False):
2118+
def copy(paths, target_path, force_in_dry_run=False, **kwargs):
20902119
"""
20912120
Copy single file/directory or list of files and directories to specified location
20922121
20932122
:param paths: path(s) to copy
20942123
:param target_path: target location
20952124
:param force_in_dry_run: force running the command during dry run
2125+
:param kwargs: additional named arguments to pass down to copy_dir
20962126
"""
20972127
if isinstance(paths, string_type):
20982128
paths = [paths]
@@ -2103,10 +2133,11 @@ def copy(paths, target_path, force_in_dry_run=False):
21032133
full_target_path = os.path.join(target_path, os.path.basename(path))
21042134
mkdir(os.path.dirname(full_target_path), parents=True)
21052135

2106-
if os.path.isfile(path):
2136+
# copy broken symlinks only if 'symlinks=True' is used
2137+
if os.path.isfile(path) or (os.path.islink(path) and kwargs.get('symlinks')):
21072138
copy_file(path, full_target_path, force_in_dry_run=force_in_dry_run)
21082139
elif os.path.isdir(path):
2109-
copy_dir(path, full_target_path, force_in_dry_run=force_in_dry_run)
2140+
copy_dir(path, full_target_path, force_in_dry_run=force_in_dry_run, **kwargs)
21102141
else:
21112142
raise EasyBuildError("Specified path to copy is not an existing file or directory: %s", path)
21122143

0 commit comments

Comments
 (0)