Skip to content

Commit 274a696

Browse files
committed
Merge branch 'develop' into modenvvar-expand-paths
2 parents 8f0135a + 0d831da commit 274a696

File tree

22 files changed

+592
-206
lines changed

22 files changed

+592
-206
lines changed

easybuild/framework/easyblock.py

Lines changed: 159 additions & 93 deletions
Large diffs are not rendered by default.

easybuild/framework/easyconfig/default.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
'checksums': [[], "Checksums for sources and patches", BUILD],
9393
'configopts': ['', 'Extra options passed to configure (default already has --prefix)', BUILD],
9494
'cuda_compute_capabilities': [[], "List of CUDA compute capabilities to build with (if supported)", BUILD],
95+
'data_sources': [[], "List of source files for data", BUILD],
9596
'download_instructions': ['', "Specify steps to acquire necessary file, if obtaining it is difficult", BUILD],
9697
'easyblock': [None, "EasyBlock to use for building; if set to None, an easyblock is selected "
9798
"based on the software name", BUILD],
@@ -132,7 +133,7 @@
132133
'skip_mod_files_sanity_check': [False, "Skip the check for .mod files in a GCCcore level install", BUILD],
133134
'skipsteps': [[], "Skip these steps", BUILD],
134135
'source_urls': [[], "List of URLs for source files", BUILD],
135-
'sources': [[], "List of source files", BUILD],
136+
'sources': [[], "List of source files for software", BUILD],
136137
'stop': [None, 'Keyword to halt the build process after a certain step.', BUILD],
137138
'testopts': ['', 'Extra options for test.', BUILD],
138139
'tests': [[], ("List of test-scripts to run after install. A test script should return a "

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ def count_files(self):
835835

836836
# No need to resolve templates as we only need a count not the names
837837
with self.disable_templating():
838-
cnt = len(self['sources']) + len(self['patches'])
838+
cnt = sum(len(self[k]) for k in ['data_sources', 'sources', 'patches'])
839839
exts = self['exts_list']
840840

841841
for ext in exts:

easybuild/framework/easyconfig/format/format.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
['name', 'version', 'versionprefix', 'versionsuffix'],
6363
['homepage', 'description'],
6464
['toolchain', 'toolchainopts'],
65-
['source_urls', 'sources', 'patches', 'checksums'],
65+
['source_urls', 'sources', 'data_sources', 'patches', 'checksums'],
6666
DEPENDENCY_PARAMETERS + ['multi_deps'],
6767
['osdependencies'],
6868
['preconfigopts', 'configopts'],

easybuild/framework/extensioneasyblock.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -176,29 +176,22 @@ def sanity_check_step(self, exts_filter=None, custom_paths=None, custom_commands
176176
# make sure Extension sanity check step is run once, by using a single empty list of extra modules
177177
lists_of_extra_modules = [[]]
178178

179-
for extra_modules in lists_of_extra_modules:
180-
181-
fake_mod_data = None
182-
183-
# only load fake module + extra modules for stand-alone installations (not for extensions),
184-
# since for extension the necessary modules should already be loaded at this point;
185-
# take into account that module may already be loaded earlier in sanity check
186-
if not (self.sanity_check_module_loaded or self.is_extension or self.dry_run):
187-
# load fake module
188-
fake_mod_data = self.load_fake_module(purge=True, extra_modules=extra_modules)
189-
190-
if extra_modules:
191-
info_msg = "Running extension sanity check with extra modules: %s" % ', '.join(extra_modules)
192-
self.log.info(info_msg)
193-
trace_msg(info_msg)
194-
195-
# perform extension sanity check
179+
# only load fake module + extra modules for stand-alone installations (not for extensions),
180+
# since for extension the necessary modules should already be loaded at this point;
181+
# take into account that module may already be loaded earlier in sanity check
182+
if not (self.sanity_check_module_loaded or self.is_extension or self.dry_run):
183+
for extra_modules in lists_of_extra_modules:
184+
with self.fake_module_environment(extra_modules=extra_modules):
185+
if extra_modules:
186+
info_msg = f"Running extension sanity check with extra modules: {', '.join(extra_modules)}"
187+
self.log.info(info_msg)
188+
trace_msg(info_msg)
189+
# perform sanity check for stand-alone extension
190+
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
191+
else:
192+
# perform single sanity check for extension
196193
(sanity_check_ok, fail_msg) = Extension.sanity_check_step(self)
197194

198-
if fake_mod_data:
199-
# unload fake module and clean up
200-
self.clean_up_fake_module(fake_mod_data)
201-
202195
if custom_paths or custom_commands or not self.is_extension:
203196
super().sanity_check_step(custom_paths=custom_paths,
204197
custom_commands=custom_commands,

easybuild/main.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,30 @@ def find_easyconfigs_by_specs(build_specs, robot_path, try_to_generate, testing=
111111
return [(ec_file, generated)]
112112

113113

114-
def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
114+
def summary(ecs_with_res):
115+
"""
116+
Compose summary of the build:
117+
* [SUCCESS] for a successful build
118+
* [FAILED] for a failed build
119+
* [SKIPPED] for a build that didn’t run
120+
121+
:param ecs_with_res: list of tuples (ec, ec_res), ec is an EasyConfig object, and ec_res is a dict of the result
122+
"""
123+
summary_fmt = " * {} {}"
124+
success_map = {
125+
True: f'{"[SUCCESS]":<9}',
126+
False: f'{"[FAILED]":<9}',
127+
None: f'{"[SKIPPED]":<9}',
128+
}
129+
lines = ["Summary:"]
130+
lines.extend([
131+
summary_fmt.format(success_map[ec_res.get('success', False)], ec['full_mod_name'])
132+
for ec, ec_res in ecs_with_res
133+
])
134+
return '\n'.join(lines)
135+
136+
137+
def build_and_install_software(ecs, init_session_state, exit_on_failure=True, testing=False):
115138
"""
116139
Build and install software for all provided parsed easyconfig files.
117140
@@ -126,7 +149,7 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
126149

127150
start_progress_bar(STATUS_BAR, size=len(ecs))
128151

129-
res = []
152+
ecs_with_res = []
130153
ec_results = []
131154
failed_cnt = 0
132155

@@ -171,14 +194,17 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
171194
write_file(test_report_fp, test_report_txt['full'])
172195
adjust_permissions(parent_dir, stat.S_IWUSR, add=False, recursive=False)
173196

197+
ecs_with_res.append((ec, ec_res))
198+
174199
if not ec_res['success'] and exit_on_failure:
200+
ecs_in_res = [res[0] for res in ecs_with_res]
201+
ecs_without_res = [(ec, {'success': None}) for ec in ecs if ec not in ecs_in_res]
202+
print_msg(summary(ecs_with_res + ecs_without_res), log=_log, silent=testing)
175203
error = ec_res['err']
176204
if isinstance(error, EasyBuildError):
177205
error = EasyBuildError(test_msg, exit_code=error.exit_code)
178206
raise error
179207

180-
res.append((ec, ec_res))
181-
182208
if failed_cnt:
183209
# if installations failed: indicate th
184210
status_label = ' (%s): ' % colorize('%s failed!' % failed_cnt, COLOR_RED)
@@ -192,7 +218,7 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
192218

193219
stop_progress_bar(STATUS_BAR)
194220

195-
return res
221+
return ecs_with_res
196222

197223

198224
def run_contrib_style_checks(ecs, check_contrib, check_style):
@@ -563,11 +589,11 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
563589

564590
with rich_live_cm():
565591
run_hook(PRE_PREF + BUILD_AND_INSTALL_LOOP, hooks, args=[ordered_ecs])
566-
ecs_with_res = build_and_install_software(ordered_ecs, init_session_state,
567-
exit_on_failure=exit_on_failure)
592+
ecs_with_res = build_and_install_software(
593+
ordered_ecs, init_session_state, exit_on_failure=exit_on_failure, testing=testing)
568594
run_hook(POST_PREF + BUILD_AND_INSTALL_LOOP, hooks, args=[ecs_with_res])
569595
else:
570-
ecs_with_res = [(ec, {}) for ec in ordered_ecs]
596+
ecs_with_res = [(ec, {'success': None}) for ec in ordered_ecs]
571597

572598
correct_builds_cnt = len([ec_res for (_, ec_res) in ecs_with_res if ec_res.get('success', False)])
573599
overall_success = correct_builds_cnt == len(ordered_ecs)
@@ -585,6 +611,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
585611
print_msg(test_report_msg)
586612

587613
print_msg(success_msg, log=_log, silent=testing)
614+
if ecs_with_res:
615+
print_msg(summary(ecs_with_res), log=_log, silent=testing)
588616

589617
# cleanup and spec files
590618
for ec in easyconfigs:

easybuild/scripts/rpath_wrapper_template.sh.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function log {
4242

4343
# command name
4444
CMD=`basename $0`
45+
TOPDIR=`dirname $0`
4546

4647
log "found CMD: $CMD | original command: %(orig_cmd)s | orig args: '$(echo \"$@\")'"
4748

easybuild/tools/config.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@
7171

7272
EMPTY_LIST = 'empty_list'
7373

74+
DATA = 'data'
75+
MODULES = 'modules'
76+
SOFTWARE = 'software'
77+
7478
PKG_TOOL_FPM = 'fpm'
7579
PKG_TYPE_RPM = 'rpm'
7680

@@ -92,6 +96,8 @@
9296
DEFAULT_CONT_TYPE = CONT_TYPE_SINGULARITY
9397

9498
DEFAULT_BRANCH = 'develop'
99+
DEFAULT_DOWNLOAD_INITIAL_WAIT_TIME = 10
100+
DEFAULT_DOWNLOAD_MAX_ATTEMPTS = 6
95101
DEFAULT_DOWNLOAD_TIMEOUT = 10
96102
DEFAULT_ENV_FOR_SHEBANG = '/usr/bin/env'
97103
DEFAULT_ENVVAR_USERS_MODULES = 'HOME'
@@ -112,8 +118,10 @@
112118
'packagepath': 'packages',
113119
'repositorypath': 'ebfiles_repo',
114120
'sourcepath': 'sources',
115-
'subdir_modules': 'modules',
116-
'subdir_software': 'software',
121+
'sourcepath_data': 'sources',
122+
'subdir_data': DATA,
123+
'subdir_modules': MODULES,
124+
'subdir_software': SOFTWARE,
117125
}
118126
DEFAULT_PKG_RELEASE = '1'
119127
DEFAULT_PKG_TOOL = PKG_TOOL_FPM
@@ -478,6 +486,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
478486
('chem', "Chemistry, Computational Chemistry and Quantum Chemistry"),
479487
('compiler', "Compilers"),
480488
('data', "Data management & processing tools"),
489+
('dataset', "Datasets"),
481490
('debugger', "Debuggers"),
482491
('devel', "Development tools"),
483492
('geo', "Earth Sciences"),
@@ -512,6 +521,7 @@ class ConfigurationVariables(BaseConfigurationVariables):
512521
'failed_install_build_dirs_path',
513522
'failed_install_logs_path',
514523
'installpath',
524+
'installpath_data',
515525
'installpath_modules',
516526
'installpath_software',
517527
'job_backend',
@@ -526,6 +536,8 @@ class ConfigurationVariables(BaseConfigurationVariables):
526536
'repository',
527537
'repositorypath',
528538
'sourcepath',
539+
'sourcepath_data',
540+
'subdir_data',
529541
'subdir_modules',
530542
'subdir_software',
531543
'tmp_logdir',
@@ -569,16 +581,20 @@ def init(options, config_options_dict):
569581
"""
570582
tmpdict = copy.deepcopy(config_options_dict)
571583

572-
# make sure source path is a list
573-
sourcepath = tmpdict['sourcepath']
574-
if isinstance(sourcepath, str):
575-
tmpdict['sourcepath'] = sourcepath.split(':')
576-
_log.debug("Converted source path ('%s') to a list of paths: %s" % (sourcepath, tmpdict['sourcepath']))
577-
elif not isinstance(sourcepath, (tuple, list)):
578-
raise EasyBuildError(
579-
"Value for sourcepath has invalid type (%s): %s", type(sourcepath), sourcepath,
580-
exit_code=EasyBuildExit.OPTION_ERROR
581-
)
584+
if tmpdict['sourcepath_data'] is None:
585+
tmpdict['sourcepath_data'] = tmpdict['sourcepath'][:]
586+
587+
for srcpath in ['sourcepath', 'sourcepath_data']:
588+
# make sure source path is a list
589+
sourcepath = tmpdict[srcpath]
590+
if isinstance(sourcepath, str):
591+
tmpdict[srcpath] = sourcepath.split(':')
592+
_log.debug("Converted source path ('%s') to a list of paths: %s" % (sourcepath, tmpdict[srcpath]))
593+
elif not isinstance(sourcepath, (tuple, list)):
594+
raise EasyBuildError(
595+
"Value for %s has invalid type (%s): %s", srcpath, type(sourcepath), sourcepath,
596+
exit_code=EasyBuildExit.OPTION_ERROR
597+
)
582598

583599
# initialize configuration variables (any future calls to ConfigurationVariables() will yield the same instance
584600
variables = ConfigurationVariables(tmpdict, ignore_unknown_keys=True)
@@ -704,11 +720,18 @@ def build_path():
704720

705721
def source_paths():
706722
"""
707-
Return the list of source paths
723+
Return the list of source paths for software
708724
"""
709725
return ConfigurationVariables()['sourcepath']
710726

711727

728+
def source_paths_data():
729+
"""
730+
Return the list of source paths for data
731+
"""
732+
return ConfigurationVariables()['sourcepath_data']
733+
734+
712735
def source_path():
713736
"""NO LONGER SUPPORTED: use source_paths instead"""
714737
_log.nosupport("source_path() is replaced by source_paths()", '2.0')
@@ -717,15 +740,16 @@ def source_path():
717740
def install_path(typ=None):
718741
"""
719742
Returns the install path
720-
- subdir 'software' for actual installation (default)
743+
- subdir 'software' for actual software installation (default)
721744
- subdir 'modules' for environment modules (typ='mod')
745+
- subdir 'data' for data installation (typ='data')
722746
"""
723747
if typ is None:
724-
typ = 'software'
748+
typ = SOFTWARE
725749
elif typ == 'mod':
726-
typ = 'modules'
750+
typ = MODULES
727751

728-
known_types = ['modules', 'software']
752+
known_types = [MODULES, SOFTWARE, DATA]
729753
if typ not in known_types:
730754
raise EasyBuildError(
731755
"Unknown type specified in install_path(): %s (known: %s)", typ, ', '.join(known_types),

easybuild/tools/filetools.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
# import build_log must stay, to use of EasyBuildLog
7070
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, CWD_NOTFOUND_ERROR
7171
from easybuild.tools.build_log import dry_run_msg, print_msg, print_warning
72+
from easybuild.tools.config import DEFAULT_DOWNLOAD_INITIAL_WAIT_TIME, DEFAULT_DOWNLOAD_MAX_ATTEMPTS
7273
from easybuild.tools.config import ERROR, GENERIC_EASYBLOCK_PKG, IGNORE, WARN, build_option, install_path
7374
from easybuild.tools.output import PROGRESS_BAR_DOWNLOAD_ONE, start_progress_bar, stop_progress_bar, update_progress_bar
7475
from easybuild.tools.hooks import load_source
@@ -777,8 +778,23 @@ def det_file_size(http_header):
777778
return res
778779

779780

780-
def download_file(filename, url, path, forced=False, trace=True):
781-
"""Download a file from the given URL, to the specified path."""
781+
def download_file(filename, url, path, forced=False, trace=True, max_attempts=None, initial_wait_time=None):
782+
"""
783+
Download a file from the given URL, to the specified path.
784+
785+
:param filename: name of file to download
786+
:param url: URL of file to download
787+
:param path: path to download file to
788+
:param forced: boolean to indicate whether force should be used to write the file
789+
:param trace: boolean to indicate whether trace output should be printed
790+
:param max_attempts: max. number of attempts to download file from specified URL
791+
:param initial_wait_time: wait time (in seconds) after first attempt (doubled at each attempt)
792+
"""
793+
794+
if max_attempts is None:
795+
max_attempts = DEFAULT_DOWNLOAD_MAX_ATTEMPTS
796+
if initial_wait_time is None:
797+
initial_wait_time = DEFAULT_DOWNLOAD_INITIAL_WAIT_TIME
782798

783799
insecure = build_option('insecure_download')
784800

@@ -802,7 +818,6 @@ def download_file(filename, url, path, forced=False, trace=True):
802818

803819
# try downloading, three times max.
804820
downloaded = False
805-
max_attempts = 3
806821
attempt_cnt = 0
807822

808823
# use custom HTTP header
@@ -823,6 +838,9 @@ def download_file(filename, url, path, forced=False, trace=True):
823838
used_urllib = std_urllib
824839
switch_to_requests = False
825840

841+
wait = False
842+
wait_time = initial_wait_time
843+
826844
while not downloaded and attempt_cnt < max_attempts:
827845
attempt_cnt += 1
828846
try:
@@ -861,6 +879,9 @@ def download_file(filename, url, path, forced=False, trace=True):
861879
status_code = err.code
862880
if status_code == 403 and attempt_cnt == 1:
863881
switch_to_requests = True
882+
elif status_code == 429: # too many requests
883+
_log.warning(f"Downloading of {url} failed with HTTP status code 429 (Too many requests)")
884+
wait = True
864885
elif 400 <= status_code <= 499:
865886
_log.warning("URL %s was not found (HTTP response code %s), not trying again" % (url, status_code))
866887
break
@@ -887,6 +908,12 @@ def download_file(filename, url, path, forced=False, trace=True):
887908
_log.info("Downloading using requests package instead of urllib2")
888909
used_urllib = requests
889910

911+
if wait:
912+
_log.info(f"Waiting for {wait_time} seconds before trying download of {url} again...")
913+
time.sleep(wait_time)
914+
# exponential backoff
915+
wait_time *= 2
916+
890917
if downloaded:
891918
_log.info("Successful download of file %s from url %s to path %s" % (filename, url, path))
892919
if trace:

0 commit comments

Comments
 (0)