Skip to content

Commit 78fb8a4

Browse files
committed
Merge branch 'develop' into extract_file_chdir
2 parents 3a4bc98 + 4845565 commit 78fb8a4

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
@@ -2411,37 +2411,71 @@ def _sanity_check_step_common(self, custom_paths, custom_commands):
24112411
SANITY_CHECK_PATHS_DIRS: ("(non-empty) directory", lambda dp: os.path.isdir(dp) and os.listdir(dp)),
24122412
}
24132413

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

24462480
for i, command in enumerate(commands):
24472481
# set command to default. This allows for config files with
@@ -2477,9 +2511,17 @@ def _sanity_check_step_dry_run(self, custom_paths=None, custom_commands=None, **
24772511
"""
24782512
paths, path_keys_and_check, commands = self._sanity_check_step_common(custom_paths, custom_commands)
24792513

2480-
for key, (typ, _) in path_keys_and_check.items():
2514+
for key in [SANITY_CHECK_PATHS_FILES, SANITY_CHECK_PATHS_DIRS]:
2515+
(typ, _) = path_keys_and_check[key]
24812516
self.dry_run_msg("Sanity check paths - %s ['%s']", typ, key)
2482-
if paths[key]:
2517+
entries = paths[key]
2518+
if entries:
2519+
# some entries may be tuple values,
2520+
# we need to convert them to strings first so we can print them sorted
2521+
for idx, entry in enumerate(entries):
2522+
if isinstance(entry, tuple):
2523+
entries[idx] = ' or '.join(entry)
2524+
24832525
for path in sorted(paths[key]):
24842526
self.dry_run_msg(" * %s", str(path))
24852527
else:
@@ -2610,6 +2652,9 @@ def xs2str(xs):
26102652

26112653
# run sanity check commands
26122654
for command in commands:
2655+
2656+
trace_msg("running command '%s' ..." % command)
2657+
26132658
out, ec = run_cmd(command, simple=False, log_ok=False, log_all=False, trace=False)
26142659
if ec != 0:
26152660
fail_msg = "sanity check command %s exited with code %s (output: %s)" % (command, ec, out)
@@ -2618,7 +2663,7 @@ def xs2str(xs):
26182663
else:
26192664
self.log.info("sanity check command %s ran successfully! (output: %s)" % (command, out))
26202665

2621-
trace_msg("running command '%s': %s" % (command, ('FAILED', 'OK')[ec == 0]))
2666+
trace_msg("result for command '%s': %s" % (command, ('FAILED', 'OK')[ec == 0]))
26222667

26232668
# also run sanity check for extensions (unless we are an extension ourselves)
26242669
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
@@ -514,7 +513,13 @@ def pypi_source_urls(pkg_name):
514513
_log.debug("Failed to download %s to determine available PyPI URLs for %s", simple_url, pkg_name)
515514
res = []
516515
else:
517-
parsed_html = ElementTree.parse(urls_html)
516+
urls_txt = read_file(urls_html)
517+
518+
# ignore yanked releases (see https://pypi.org/help/#yanked)
519+
# see https://github.com/easybuilders/easybuild-framework/issues/3301
520+
urls_txt = re.sub(r'<a.*?data-yanked.*?</a>', '', urls_txt)
521+
522+
parsed_html = ElementTree.ElementTree(ElementTree.fromstring(urls_txt))
518523
if hasattr(parsed_html, 'iter'):
519524
res = [a.attrib['href'] for a in parsed_html.iter('a')]
520525
else:
@@ -777,6 +782,18 @@ def find_easyconfigs(path, ignore_dirs=None):
777782
return files
778783

779784

785+
def find_glob_pattern(glob_pattern, fail_on_no_match=True):
786+
"""Find unique file/dir matching glob_pattern (raises error if more than one match is found)"""
787+
if build_option('extended_dry_run'):
788+
return glob_pattern
789+
res = glob.glob(glob_pattern)
790+
if len(res) == 0 and not fail_on_no_match:
791+
return None
792+
if len(res) != 1:
793+
raise EasyBuildError("Was expecting exactly one match for '%s', found %d: %s", glob_pattern, len(res), res)
794+
return res[0]
795+
796+
780797
def search_file(paths, query, short=False, ignore_dirs=None, silent=False, filename_only=False, terse=False,
781798
case_sensitive=False):
782799
"""
@@ -1927,7 +1944,12 @@ def copy_file(path, target_path, force_in_dry_run=False):
19271944
_log.info("Copied contents of file %s to %s", path, target_path)
19281945
else:
19291946
mkdir(os.path.dirname(target_path), parents=True)
1930-
shutil.copy2(path, target_path)
1947+
if os.path.exists(path):
1948+
shutil.copy2(path, target_path)
1949+
elif os.path.islink(path):
1950+
# special care for copying broken symlinks
1951+
link_target = os.readlink(path)
1952+
symlink(link_target, target_path)
19311953
_log.info("%s copied to %s", path, target_path)
19321954
except (IOError, OSError, shutil.Error) as err:
19331955
raise EasyBuildError("Failed to copy file %s to %s: %s", path, target_path, err)
@@ -1962,16 +1984,13 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
19621984
:param path: the original directory path
19631985
:param target_path: path to copy the directory to
19641986
:param force_in_dry_run: force running the command during dry run
1965-
:param dirs_exist_ok: wrapper around shutil.copytree option, which was added in Python 3.8
1987+
:param dirs_exist_ok: boolean indicating whether it's OK if the target directory already exists
19661988
1967-
On Python >= 3.8 shutil.copytree is always used
1968-
On Python < 3.8 if 'dirs_exist_ok' is False - shutil.copytree is used
1969-
On Python < 3.8 if 'dirs_exist_ok' is True - distutils.dir_util.copy_tree is used
1989+
shutil.copytree is used if the target path does not exist yet;
1990+
if the target path already exists, the 'copy' function will be used to copy the contents of
1991+
the source path to the target path
19701992
1971-
Additional specified named arguments are passed down to shutil.copytree if used.
1972-
1973-
Because distutils.dir_util.copy_tree supports only 'symlinks' named argument,
1974-
using any other will raise EasyBuildError.
1993+
Additional specified named arguments are passed down to shutil.copytree/copy if used.
19751994
"""
19761995
if not force_in_dry_run and build_option('extended_dry_run'):
19771996
dry_run_msg("copied directory %s to %s" % (path, target_path))
@@ -1980,38 +1999,49 @@ def copy_dir(path, target_path, force_in_dry_run=False, dirs_exist_ok=False, **k
19801999
if not dirs_exist_ok and os.path.exists(target_path):
19812000
raise EasyBuildError("Target location %s to copy %s to already exists", target_path, path)
19822001

1983-
if sys.version_info >= (3, 8):
1984-
# on Python >= 3.8, shutil.copytree works fine, thanks to availability of dirs_exist_ok named argument
1985-
shutil.copytree(path, target_path, dirs_exist_ok=dirs_exist_ok, **kwargs)
2002+
# note: in Python >= 3.8 shutil.copytree works just fine thanks to the 'dirs_exist_ok' argument,
2003+
# but since we need to be more careful in earlier Python versions we use our own implementation
2004+
# in case the target directory exists and 'dirs_exist_ok' is enabled
2005+
if dirs_exist_ok and os.path.exists(target_path):
2006+
# if target directory already exists (and that's allowed via dirs_exist_ok),
2007+
# we need to be more careful, since shutil.copytree will fail (in Python < 3.8)
2008+
# if target directory already exists;
2009+
# so, recurse via 'copy' function to copy files/dirs in source path to target path
2010+
# (NOTE: don't use distutils.dir_util.copy_tree here, see
2011+
# https://github.com/easybuilders/easybuild-framework/issues/3306)
2012+
2013+
entries = os.listdir(path)
19862014

1987-
elif dirs_exist_ok:
1988-
# use distutils.dir_util.copy_tree with Python < 3.8 if dirs_exist_ok is enabled
2015+
# take into account 'ignore' function that is supported by shutil.copytree
2016+
# (but not by 'copy_file' function used by 'copy')
2017+
ignore = kwargs.get('ignore')
2018+
if ignore:
2019+
ignored_entries = ignore(path, entries)
2020+
entries = [x for x in entries if x not in ignored_entries]
19892021

1990-
# first get value for symlinks named argument (if any)
1991-
preserve_symlinks = kwargs.pop('symlinks', False)
2022+
# determine list of paths to copy
2023+
paths_to_copy = [os.path.join(path, x) for x in entries]
19922024

1993-
# check if there are other named arguments (there shouldn't be, only 'symlinks' is supported)
1994-
if kwargs:
1995-
raise EasyBuildError("Unknown named arguments passed to copy_dir with dirs_exist_ok=True: %s",
1996-
', '.join(sorted(kwargs.keys())))
1997-
distutils.dir_util.copy_tree(path, target_path, preserve_symlinks=preserve_symlinks)
2025+
copy(paths_to_copy, target_path,
2026+
force_in_dry_run=force_in_dry_run, dirs_exist_ok=dirs_exist_ok, **kwargs)
19982027

19992028
else:
2000-
# if dirs_exist_ok is not enabled, just use shutil.copytree
2029+
# if dirs_exist_ok is not enabled or target directory doesn't exist, just use shutil.copytree
20012030
shutil.copytree(path, target_path, **kwargs)
20022031

20032032
_log.info("%s copied to %s", path, target_path)
2004-
except (IOError, OSError) as err:
2033+
except (IOError, OSError, shutil.Error) as err:
20052034
raise EasyBuildError("Failed to copy directory %s to %s: %s", path, target_path, err)
20062035

20072036

2008-
def copy(paths, target_path, force_in_dry_run=False):
2037+
def copy(paths, target_path, force_in_dry_run=False, **kwargs):
20092038
"""
20102039
Copy single file/directory or list of files and directories to specified location
20112040
20122041
:param paths: path(s) to copy
20132042
:param target_path: target location
20142043
:param force_in_dry_run: force running the command during dry run
2044+
:param kwargs: additional named arguments to pass down to copy_dir
20152045
"""
20162046
if isinstance(paths, string_type):
20172047
paths = [paths]
@@ -2022,10 +2052,11 @@ def copy(paths, target_path, force_in_dry_run=False):
20222052
full_target_path = os.path.join(target_path, os.path.basename(path))
20232053
mkdir(os.path.dirname(full_target_path), parents=True)
20242054

2025-
if os.path.isfile(path):
2055+
# copy broken symlinks only if 'symlinks=True' is used
2056+
if os.path.isfile(path) or (os.path.islink(path) and kwargs.get('symlinks')):
20262057
copy_file(path, full_target_path, force_in_dry_run=force_in_dry_run)
20272058
elif os.path.isdir(path):
2028-
copy_dir(path, full_target_path, force_in_dry_run=force_in_dry_run)
2059+
copy_dir(path, full_target_path, force_in_dry_run=force_in_dry_run, **kwargs)
20292060
else:
20302061
raise EasyBuildError("Specified path to copy is not an existing file or directory: %s", path)
20312062

0 commit comments

Comments
 (0)