Skip to content

Commit 08a56e9

Browse files
authored
Merge pull request #3749 from ComputeCanada/checksums_external
add support for checksums specified in external `checksums.json` file
2 parents 1ea5d57 + 86a6e31 commit 08a56e9

File tree

9 files changed

+290
-23
lines changed

9 files changed

+290
-23
lines changed

easybuild/framework/easyblock.py

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import copy
4343
import glob
4444
import inspect
45+
import json
4546
import os
4647
import re
4748
import stat
@@ -67,7 +68,7 @@
6768
from easybuild.tools.build_details import get_build_stats
6869
from easybuild.tools.build_log import EasyBuildError, dry_run_msg, dry_run_warning, dry_run_set_dirs
6970
from easybuild.tools.build_log import print_error, print_msg, print_warning
70-
from easybuild.tools.config import DEFAULT_ENVVAR_USERS_MODULES
71+
from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES
7172
from easybuild.tools.config import FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES
7273
from easybuild.tools.config import build_option, build_path, get_log_filename, get_repository, get_repositorypath
7374
from easybuild.tools.config import install_path, log_path, package_path, source_paths
@@ -156,6 +157,7 @@ def __init__(self, ec):
156157
self.patches = []
157158
self.src = []
158159
self.checksums = []
160+
self.json_checksums = None
159161

160162
# build/install directories
161163
self.builddir = None
@@ -347,23 +349,55 @@ def get_checksum_for(self, checksums, filename=None, index=None):
347349
Obtain checksum for given filename.
348350
349351
:param checksums: a list or tuple of checksums (or None)
350-
:param filename: name of the file to obtain checksum for (Deprecated)
352+
:param filename: name of the file to obtain checksum for
351353
:param index: index of file in list
352354
"""
353-
# Filename has never been used; flag it as deprecated
354-
if filename:
355-
self.log.deprecated("Filename argument to get_checksum_for() is deprecated", '5.0')
355+
checksum = None
356+
357+
# sometimes, filename are specified as a dict
358+
if isinstance(filename, dict):
359+
filename = filename['filename']
356360

357361
# if checksums are provided as a dict, lookup by source filename as key
358-
if isinstance(checksums, (list, tuple)):
362+
if isinstance(checksums, dict):
363+
if filename is not None and filename in checksums:
364+
checksum = checksums[filename]
365+
else:
366+
checksum = None
367+
elif isinstance(checksums, (list, tuple)):
359368
if index is not None and index < len(checksums) and (index >= 0 or abs(index) <= len(checksums)):
360-
return checksums[index]
369+
checksum = checksums[index]
361370
else:
362-
return None
371+
checksum = None
363372
elif checksums is None:
364-
return None
373+
checksum = None
374+
else:
375+
raise EasyBuildError("Invalid type for checksums (%s), should be dict, list, tuple or None.",
376+
type(checksums))
377+
378+
if checksum is None or build_option("checksum_priority") == CHECKSUM_PRIORITY_JSON:
379+
json_checksums = self.get_checksums_from_json()
380+
return json_checksums.get(filename, None)
365381
else:
366-
raise EasyBuildError("Invalid type for checksums (%s), should be list, tuple or None.", type(checksums))
382+
return checksum
383+
384+
def get_checksums_from_json(self, always_read=False):
385+
"""
386+
Get checksums for this software that are provided in a checksums.json file
387+
388+
:param: always_read: always read the checksums.json file, even if it has been read before
389+
"""
390+
if always_read or self.json_checksums is None:
391+
try:
392+
path = self.obtain_file("checksums.json", no_download=True)
393+
self.log.info("Loading checksums from file %s", path)
394+
json_txt = read_file(path)
395+
self.json_checksums = json.loads(json_txt)
396+
# if the file can't be found, return an empty dict
397+
except EasyBuildError:
398+
self.json_checksums = {}
399+
400+
return self.json_checksums
367401

368402
def fetch_source(self, source, checksum=None, extension=False, download_instructions=None):
369403
"""
@@ -445,7 +479,8 @@ def fetch_sources(self, sources=None, checksums=None):
445479
if source is None:
446480
raise EasyBuildError("Empty source in sources list at index %d", index)
447481

448-
src_spec = self.fetch_source(source, self.get_checksum_for(checksums=checksums, index=index))
482+
checksum = self.get_checksum_for(checksums=checksums, filename=source, index=index)
483+
src_spec = self.fetch_source(source, checksum=checksum)
449484
if src_spec:
450485
self.src.append(src_spec)
451486
else:
@@ -477,7 +512,7 @@ def fetch_patches(self, patch_specs=None, extension=False, checksums=None):
477512
if path:
478513
self.log.debug('File %s found for patch %s', path, patch_spec)
479514
patch_info['path'] = path
480-
patch_info['checksum'] = self.get_checksum_for(checksums, index=index)
515+
patch_info['checksum'] = self.get_checksum_for(checksums, filename=patch_info['name'], index=index)
481516

482517
if extension:
483518
patches.append(patch_info)
@@ -638,7 +673,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
638673

639674
# verify checksum (if provided)
640675
self.log.debug('Verifying checksums for extension source...')
641-
fn_checksum = self.get_checksum_for(checksums, index=0)
676+
fn_checksum = self.get_checksum_for(checksums, filename=src_fn, index=0)
642677
if verify_checksum(src_path, fn_checksum):
643678
self.log.info('Checksum for extension source %s verified', src_fn)
644679
elif build_option('ignore_checksums'):
@@ -672,7 +707,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
672707
patch = patch['path']
673708
patch_fn = os.path.basename(patch)
674709

675-
checksum = self.get_checksum_for(checksums[1:], index=idx)
710+
checksum = self.get_checksum_for(checksums, filename=patch_fn, index=idx+1)
676711
if verify_checksum(patch, checksum):
677712
self.log.info('Checksum for extension patch %s verified', patch_fn)
678713
elif build_option('ignore_checksums'):
@@ -694,7 +729,7 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True):
694729
return exts_sources
695730

696731
def obtain_file(self, filename, extension=False, urls=None, download_filename=None, force_download=False,
697-
git_config=None, download_instructions=None, alt_location=None):
732+
git_config=None, no_download=False, download_instructions=None, alt_location=None):
698733
"""
699734
Locate the file with the given name
700735
- searches in different subdirectories of source path
@@ -705,6 +740,7 @@ def obtain_file(self, filename, extension=False, urls=None, download_filename=No
705740
:param download_filename: filename with which the file should be downloaded, and then renamed to <filename>
706741
:param force_download: always try to download file, even if it's already available in source path
707742
:param git_config: dictionary to define how to download a git repository
743+
:param no_download: do not try to download the file
708744
:param download_instructions: instructions to manually add source (used for complex cases)
709745
:param alt_location: alternative location to use instead of self.name
710746
"""
@@ -818,6 +854,13 @@ def obtain_file(self, filename, extension=False, urls=None, download_filename=No
818854
if self.dry_run:
819855
self.dry_run_msg(" * %s found at %s", filename, foundfile)
820856
return foundfile
857+
elif no_download:
858+
if self.dry_run:
859+
self.dry_run_msg(" * %s (MISSING)", filename)
860+
return filename
861+
else:
862+
raise EasyBuildError("Couldn't find file %s anywhere, and downloading it is disabled... "
863+
"Paths attempted (in order): %s ", filename, ', '.join(failedpaths))
821864
elif git_config:
822865
return get_source_tarball_from_git(filename, targetdir, git_config)
823866
else:
@@ -2280,7 +2323,7 @@ def fetch_step(self, skip_checksums=False):
22802323

22812324
# fetch patches
22822325
if self.cfg['patches'] + self.cfg['postinstallpatches']:
2283-
if isinstance(self.cfg['checksums'], (list, tuple)):
2326+
if self.cfg['checksums'] and isinstance(self.cfg['checksums'], (list, tuple)):
22842327
# if checksums are provided as a list, first entries are assumed to be for sources
22852328
patches_checksums = self.cfg['checksums'][len(self.cfg['sources']):]
22862329
else:
@@ -2367,6 +2410,20 @@ def check_checksums_for(self, ent, sub='', source_cnt=None):
23672410
patches = ent.get('patches', [])
23682411
checksums = ent.get('checksums', [])
23692412

2413+
if not checksums:
2414+
checksums_from_json = self.get_checksums_from_json()
2415+
# recreate a list of checksums. If each filename is found, the generated list of checksums should match
2416+
# what is expected in list format
2417+
for fn in sources + patches:
2418+
# if the filename is a tuple, the actual source file name is the first element
2419+
if isinstance(fn, tuple):
2420+
fn = fn[0]
2421+
# if the filename is a dict, the actual source file name is the "filename" element
2422+
if isinstance(fn, dict):
2423+
fn = fn["filename"]
2424+
if fn in checksums_from_json.keys():
2425+
checksums += [checksums_from_json[fn]]
2426+
23702427
if source_cnt is None:
23712428
source_cnt = len(sources)
23722429
patch_cnt, checksum_cnt = len(patches), len(checksums)
@@ -4406,6 +4463,67 @@ class StopException(Exception):
44064463
pass
44074464

44084465

4466+
def inject_checksums_to_json(ecs, checksum_type):
4467+
"""
4468+
Inject checksums of given type in corresponding json files
4469+
4470+
:param ecs: list of EasyConfig instances to calculate checksums and inject them into checksums.json
4471+
:param checksum_type: type of checksum to use
4472+
"""
4473+
for ec in ecs:
4474+
ec_fn = os.path.basename(ec['spec'])
4475+
ec_dir = os.path.dirname(ec['spec'])
4476+
print_msg("injecting %s checksums for %s in checksums.json" % (checksum_type, ec['spec']), log=_log)
4477+
4478+
# get easyblock instance and make sure all sources/patches are available by running fetch_step
4479+
print_msg("fetching sources & patches for %s..." % ec_fn, log=_log)
4480+
app = get_easyblock_instance(ec)
4481+
app.update_config_template_run_step()
4482+
app.fetch_step(skip_checksums=True)
4483+
4484+
# compute & inject checksums for sources/patches
4485+
print_msg("computing %s checksums for sources & patches for %s..." % (checksum_type, ec_fn), log=_log)
4486+
checksums = {}
4487+
for entry in app.src + app.patches:
4488+
checksum = compute_checksum(entry['path'], checksum_type)
4489+
print_msg("* %s: %s" % (os.path.basename(entry['path']), checksum), log=_log)
4490+
checksums[os.path.basename(entry['path'])] = checksum
4491+
4492+
# compute & inject checksums for extension sources/patches
4493+
if app.exts:
4494+
print_msg("computing %s checksums for extensions for %s..." % (checksum_type, ec_fn), log=_log)
4495+
4496+
for ext in app.exts:
4497+
# compute checksums for extension sources & patches
4498+
if 'src' in ext:
4499+
src_fn = os.path.basename(ext['src'])
4500+
checksum = compute_checksum(ext['src'], checksum_type)
4501+
print_msg(" * %s: %s" % (src_fn, checksum), log=_log)
4502+
checksums[src_fn] = checksum
4503+
for ext_patch in ext.get('patches', []):
4504+
patch_fn = os.path.basename(ext_patch['path'])
4505+
checksum = compute_checksum(ext_patch['path'], checksum_type)
4506+
print_msg(" * %s: %s" % (patch_fn, checksum), log=_log)
4507+
checksums[patch_fn] = checksum
4508+
4509+
# actually inject new checksums or overwrite existing ones (if --force)
4510+
existing_checksums = app.get_checksums_from_json(always_read=True)
4511+
for filename in checksums:
4512+
if filename not in existing_checksums:
4513+
existing_checksums[filename] = checksums[filename]
4514+
# don't do anything if the checksum already exist and is the same
4515+
elif checksums[filename] != existing_checksums[filename]:
4516+
if build_option('force'):
4517+
print_warning("Found existing checksums for %s, overwriting them (due to --force)..." % ec_fn)
4518+
existing_checksums[filename] = checksums[filename]
4519+
else:
4520+
raise EasyBuildError("Found existing checksum for %s, use --force to overwrite them" % filename)
4521+
4522+
# actually write the checksums
4523+
with open(os.path.join(ec_dir, 'checksums.json'), 'w') as outfile:
4524+
json.dump(existing_checksums, outfile, indent=2, sort_keys=True)
4525+
4526+
44094527
def inject_checksums(ecs, checksum_type):
44104528
"""
44114529
Inject checksums of given type in specified easyconfig files

easybuild/main.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
:author: Toon Willems (Ghent University)
3535
:author: Ward Poelmans (Ghent University)
3636
:author: Fotis Georgatos (Uni.Lu, NTUA)
37+
:author: Maxime Boissonneault (Compute Canada)
3738
"""
3839
import copy
3940
import os
@@ -45,7 +46,7 @@
4546
# expect missing log output when this not the case!
4647
from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, print_warning, stop_logging
4748

48-
from easybuild.framework.easyblock import build_and_install_one, inject_checksums
49+
from easybuild.framework.easyblock import build_and_install_one, inject_checksums, inject_checksums_to_json
4950
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
5051
from easybuild.framework.easystack import parse_easystack
5152
from easybuild.framework.easyconfig.easyconfig import clean_up_easyconfigs
@@ -425,7 +426,8 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
425426
sys.exit(31) # exit -> 3x1t -> 31
426427

427428
# read easyconfig files
428-
easyconfigs, generated_ecs = parse_easyconfigs(paths, validate=not options.inject_checksums)
429+
validate = not options.inject_checksums and not options.inject_checksums_to_json
430+
easyconfigs, generated_ecs = parse_easyconfigs(paths, validate=validate)
429431

430432
# handle --check-contrib & --check-style options
431433
if run_contrib_style_checks([ec['ec'] for ec in easyconfigs], options.check_contrib, options.check_style):
@@ -453,6 +455,7 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
453455

454456
keep_available_modules = forced or dry_run_mode or options.extended_dry_run or pr_options or options.copy_ec
455457
keep_available_modules = keep_available_modules or options.inject_checksums or options.sanity_check_only
458+
keep_available_modules = keep_available_modules or options.inject_checksums_to_json
456459

457460
# skip modules that are already installed unless forced, or unless an option is used that warrants not skipping
458461
if not keep_available_modules:
@@ -538,8 +541,12 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
538541
with rich_live_cm():
539542
inject_checksums(ordered_ecs, options.inject_checksums)
540543

544+
elif options.inject_checksums_to_json:
545+
inject_checksums_to_json(ordered_ecs, options.inject_checksums_to_json)
546+
541547
# cleanup and exit after dry run, searching easyconfigs or submitting regression test
542548
stop_options = [options.check_conflicts, dry_run_mode, options.dump_env_script, options.inject_checksums]
549+
stop_options += [options.inject_checksums_to_json]
543550
if any(no_ec_opts) or any(stop_options):
544551
clean_exit(logfile, eb_tmpdir, testing)
545552

easybuild/tools/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
:author: Ward Poelmans (Ghent University)
3535
:author: Damian Alvarez (Forschungszentrum Juelich GmbH)
3636
:author: Andy Georges (Ghent University)
37+
:author: Maxime Boissonneault (Compute Canada)
3738
"""
3839
import copy
3940
import glob
@@ -126,6 +127,11 @@
126127
FORCE_DOWNLOAD_CHOICES = [FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES]
127128
DEFAULT_FORCE_DOWNLOAD = FORCE_DOWNLOAD_SOURCES
128129

130+
CHECKSUM_PRIORITY_JSON = "json"
131+
CHECKSUM_PRIORITY_EASYCONFIG = "easyconfig"
132+
CHECKSUM_PRIORITY_CHOICES = [CHECKSUM_PRIORITY_JSON, CHECKSUM_PRIORITY_EASYCONFIG]
133+
DEFAULT_CHECKSUM_PRIORITY = CHECKSUM_PRIORITY_EASYCONFIG
134+
129135
# package name for generic easyblocks
130136
GENERIC_EASYBLOCK_PKG = 'generic'
131137

@@ -180,6 +186,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
180186
'accept_eula_for',
181187
'aggregate_regtest',
182188
'backup_modules',
189+
'checksum_priority',
183190
'container_config',
184191
'container_image_format',
185192
'container_image_name',

0 commit comments

Comments
 (0)