Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add (experimental) support for specifying easyconfig files via "easystack" file #3479

Merged
merged 82 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
78b482a
building eb commands added to eb framework
deniskristak Oct 22, 2020
f0a7c61
format changes
deniskristak Oct 22, 2020
3180d78
format changes
deniskristak Oct 22, 2020
9ac800b
beautified
deniskristak Oct 22, 2020
d78923b
pep8 repairs & class reorganisation
deniskristak Oct 22, 2020
2e5f2ab
format changes
deniskristak Oct 22, 2020
1cd2b3a
input validation
deniskristak Oct 26, 2020
de8049b
passed easyconfigs as arguments inside of main
deniskristak Oct 27, 2020
28e7fde
added test skeleton
deniskristak Nov 2, 2020
cfe38af
test
deniskristak Nov 2, 2020
b0ddf19
format tweaks
deniskristak Nov 2, 2020
7075c31
added Wrong structure test
deniskristak Nov 2, 2020
e4230c4
small stuff
deniskristak Nov 2, 2020
9d13d1d
test success - WIP
deniskristak Nov 2, 2020
364699d
WIP success test
deniskristak Nov 2, 2020
9e240c1
wip: testing success
deniskristak Nov 2, 2020
e62a4ea
specsfile pouziva dobre ec
deniskristak Nov 3, 2020
00ebd3c
test
deniskristak Nov 3, 2020
eb8dce2
test
Nov 3, 2020
1de3e4a
test
deniskristak Nov 3, 2020
e6f7452
test
deniskristak Nov 3, 2020
f659426
test
Nov 3, 2020
24b53eb
Merge pull request #1 from deniskristak/feature/specsfile-private
deniskristak Nov 3, 2020
58c5d7d
cosmetic changes
deniskristak Nov 3, 2020
4f83aac
Merge pull request #2 from deniskristak/feature/specsfile-private
deniskristak Nov 3, 2020
d61d078
format changes
deniskristak Nov 3, 2020
8435b5f
cosmetic changes
Nov 3, 2020
c6ad30e
test
Nov 4, 2020
feec790
test
deniskristak Nov 4, 2020
2a8dcf2
test
deniskristak Nov 4, 2020
fd01052
test
Nov 4, 2020
be5bf5c
Merge pull request #3 from deniskristak/feature/specsfile-devel
deniskristak Nov 4, 2020
36117c5
rename to easystack
deniskristak Nov 5, 2020
840c7bf
Merge branch 'feature/specsfile-devel' of https://github.com/deniskri…
Nov 5, 2020
64879f5
test
deniskristak Nov 5, 2020
0543275
test
Nov 5, 2020
82bedcb
Merge pull request #4 from deniskristak/feature/specsfile-devel
deniskristak Nov 5, 2020
2ed5b0d
tests
deniskristak Nov 5, 2020
768a512
tests
deniskristak Nov 5, 2020
b7e3ac5
test
deniskristak Nov 5, 2020
dfc2d91
added tests, cleaned up
Nov 5, 2020
49a7535
Merge pull request #5 from deniskristak/easystack-devel
deniskristak Nov 5, 2020
c7a88fb
test
Nov 5, 2020
db9a127
tests
Nov 5, 2020
2a8485d
tests
Nov 5, 2020
da2b46b
experimental set
Nov 5, 2020
41e5ce9
tests
Nov 5, 2020
3aff446
tests
Nov 5, 2020
f78c411
tests
Nov 5, 2020
92dd35e
tests
Nov 6, 2020
f9081e4
tests
Nov 6, 2020
9ec4eb5
tests
Nov 6, 2020
9747f16
Delete test_result.log
deniskristak Nov 6, 2020
844523d
tests
Nov 6, 2020
ee685d6
Merge branch 'feature/build-from-specsfile' of https://github.com/den…
Nov 6, 2020
45d0a22
tests
Nov 6, 2020
42c0953
tests
Nov 6, 2020
f29da8e
tests
Nov 6, 2020
8ca3e3e
tests
Nov 6, 2020
311b2c3
tests
Nov 6, 2020
add91c8
tests
Nov 9, 2020
97343a1
tests
Nov 9, 2020
e3b3c82
more adequate naming
Nov 11, 2020
79739ae
changes based on the latest review
Nov 13, 2020
05e2d86
changes based on comments
Nov 19, 2020
7e1c984
tests
Nov 19, 2020
8d036e4
tests
Nov 19, 2020
0073e4d
tests
Nov 19, 2020
2b9fa56
tests
Nov 19, 2020
6e7497e
tests
Nov 19, 2020
a73787a
tests
Nov 19, 2020
913695f
tests
Nov 19, 2020
b5a5ab6
tests
Nov 19, 2020
823925f
tests
Nov 20, 2020
ccf8c4d
tests
Nov 20, 2020
390e7ab
tests
Nov 20, 2020
7c24a46
tests
Nov 20, 2020
f60a0c6
changes after review
deniskristak Nov 30, 2020
de1a4c0
linting
deniskristak Nov 30, 2020
e6c5d9c
make only_if_module_is_available produce slightly better error message
boegel Nov 30, 2020
1cd9131
only run EasyStackParser.parse if PyYAML is installed
boegel Nov 30, 2020
f584f53
tweak error messages produces for easystack files + reformat how Soft…
boegel Nov 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions easybuild/framework/easystack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Copyright 2020-2020 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# https://github.com/easybuilders/easybuild
#
# EasyBuild is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation v2.
#
# EasyBuild is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>.
#
"""
Support for easybuild-ing from multiple easyconfigs based on
information obtained from provided file (easystack) with build specifications.

:author: Denis Kristak (Inuits)
:author: Pavel Grochal (Inuits)
"""

from easybuild.base import fancylogger
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.filetools import read_file
boegel marked this conversation as resolved.
Show resolved Hide resolved
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
from easybuild.tools.utilities import only_if_module_is_available
try:
import yaml
except ImportError:
pass
_log = fancylogger.getLogger('easystack', fname=False)


class EasyStack(object):
"""One class instance per easystack. General options + list of all SoftwareSpecs instances"""

def __init__(self):
self.easybuild_version = None
self.robot = False
self.software_list = []

def compose_ec_filenames(self):
"""Returns a list of all easyconfig names"""
ec_filenames = []
for sw in self.software_list:
full_ec_version = det_full_ec_version({
'toolchain': {'name': sw.toolchain_name, 'version': sw.toolchain_version},
'version': sw.version,
'versionsuffix': sw.versionsuffix,
})
ec_filename = '%s-%s.eb' % (sw.name, full_ec_version)
ec_filenames.append(ec_filename)
return ec_filenames

# flags applicable to all sw (i.e. robot)
def get_general_options(self):
"""Returns general options (flags applicable to all sw (i.e. --robot))"""
general_options = {}
# TODO add support for general_options
# general_options['robot'] = self.robot
# general_options['easybuild_version'] = self.easybuild_version
return general_options


class SoftwareSpecs(object):
"""Contains information about every software that should be installed"""

def __init__(self, name, version, versionsuffix, toolchain_version, toolchain_name):
self.name = name
self.version = version
self.toolchain_version = toolchain_version
self.toolchain_name = toolchain_name
self.versionsuffix = versionsuffix


class EasyStackParser(object):
"""Parser for easystack files (in YAML syntax)."""

@only_if_module_is_available('yaml', pkgname='PyYAML')
@staticmethod
def parse(filepath):
"""Parses YAML file and assigns obtained values to SW config instances as well as general config instance"""
yaml_txt = read_file(filepath)
easystack_raw = yaml.safe_load(yaml_txt)
easystack = EasyStack()

try:
deniskristak marked this conversation as resolved.
Show resolved Hide resolved
software = easystack_raw["software"]
except KeyError:
wrong_structure_file = "Not a valid EasyStack YAML file: no 'software' key found"
raise EasyBuildError(wrong_structure_file)

# assign software-specific easystack attributes
for name in software:
# ensure we have a string value (YAML parser returns type = dict
# if levels under the current attribute are present)
name = str(name)
boegel marked this conversation as resolved.
Show resolved Hide resolved
try:
toolchains = software[name]['toolchains']
except KeyError:
raise EasyBuildError("Toolchains for software '%s' are not defined in %s", name, filepath)
for toolchain in toolchains:
toolchain = str(toolchain)
toolchain_parts = toolchain.split('-', 1)
if len(toolchain_parts) == 2:
toolchain_name, toolchain_version = toolchain_parts
elif len(toolchain_parts) == 1:
toolchain_name, toolchain_version = toolchain, ''
else:
raise EasyBuildError("Incorrect toolchain specification for '%s' in %s, too many parts: %s",
name, filepath, toolchain_parts)

try:
# if version string containts asterisk or labels, raise error (asterisks not supported)
versions = toolchains[toolchain]['versions']
except TypeError as err:
wrong_structure_err = "An error occurred when interpreting "
wrong_structure_err += "the data for software %s: %s" % (name, err)
raise EasyBuildError(wrong_structure_err)
if '*' in str(versions):
asterisk_err = "EasyStack specifications of '%s' in %s contain asterisk. "
asterisk_err += "Wildcard feature is not supported yet."
raise EasyBuildError(asterisk_err, name, filepath)

# yaml versions can be in different formats in yaml file
# firstly, check if versions in yaml file are read as a dictionary.
# Example of yaml structure:
# ========================================================================
# versions:
# 2.25:
# 2.23:
# versionsuffix: '-R-4.0.0'
# ========================================================================
if isinstance(versions, dict):
for version in versions:
if versions[version] is not None:
version_spec = versions[version]
if 'versionsuffix' in version_spec:
versionsuffix = str(version_spec['versionsuffix'])
else:
versionsuffix = ''
if 'exclude-labels' in str(version_spec) or 'include-labels' in str(version_spec):
lab_err = "EasyStack specifications of '%s' in %s "
lab_err += "contain labels. Labels aren't supported yet."
raise EasyBuildError(lab_err, name, filepath)
else:
versionsuffix = ''

specs = {
'name': name,
'toolchain_name': toolchain_name,
'toolchain_version': toolchain_version,
'version': version,
'versionsuffix': versionsuffix,
}
sw = SoftwareSpecs(**specs)

# append newly created class instance to the list in instance of EasyStack class
easystack.software_list.append(sw)
continue

# is format read as a list of versions?
# ========================================================================
# versions:
# [2.24, 2.51]
# ========================================================================
elif isinstance(versions, list):
versions_list = versions

# format = multiple lines without ':' (read as a string)?
# ========================================================================
# versions:
# 2.24
# 2.51
# ========================================================================
elif isinstance(versions, str):
versions_list = str(versions).split()

# format read as float (containing one version only)?
# ========================================================================
# versions:
# 2.24
# ========================================================================
elif isinstance(versions, float):
versions_list = [str(versions)]

# if no version is a dictionary, versionsuffix isn't specified
versionsuffix = ''

for version in versions_list:
sw = SoftwareSpecs(
name=name, version=version, versionsuffix=versionsuffix,
toolchain_name=toolchain_name, toolchain_version=toolchain_version)
# append newly created class instance to the list in instance of EasyStack class
easystack.software_list.append(sw)

# assign general easystack attributes
easystack.easybuild_version = easystack_raw.get('easybuild_version', None)
easystack.robot = easystack_raw.get('robot', False)

return easystack


def parse_easystack(filepath):
"""Parses through easystack file, returns what EC are to be installed together with their options."""
log_msg = "Support for easybuild-ing from multiple easyconfigs based on "
log_msg += "information obtained from provided file (easystack) with build specifications."
_log.experimental(log_msg)
_log.info("Building from easystack: '%s'" % filepath)

# class instance which contains all info about planned build
easystack = EasyStackParser.parse(filepath)

easyconfig_names = easystack.compose_ec_filenames()

general_options = easystack.get_general_options()

_log.debug("EasyStack parsed. Proceeding to install these Easyconfigs: \n'%s'" % "',\n'".join(easyconfig_names))
if len(general_options) != 0:
_log.debug("General options for installation are: \n%s" % str(general_options))
else:
_log.debug("No general options were specified in easystack")

return easyconfig_names, general_options
8 changes: 8 additions & 0 deletions easybuild/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

from easybuild.framework.easyblock import build_and_install_one, inject_checksums
from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
from easybuild.framework.easystack import parse_easystack
from easybuild.framework.easyconfig.easyconfig import clean_up_easyconfigs
from easybuild.framework.easyconfig.easyconfig import fix_deprecated_easyconfigs, verify_easyconfig_filename
from easybuild.framework.easyconfig.style import cmdline_easyconfigs_style_check
Expand Down Expand Up @@ -223,6 +224,13 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
last_log = find_last_log(logfile) or '(none)'
print_msg(last_log, log=_log, prefix=False)

# if easystack is provided with the command, commands with arguments from it will be executed
if options.easystack:
# TODO add general_options (i.e. robot) to build options
orig_paths, general_options = parse_easystack(options.easystack)
if general_options:
raise EasyBuildError("Specifying general configuration options in easystack file is not supported yet.")

# check whether packaging is supported when it's being used
if options.package:
check_pkg_support()
Expand Down
2 changes: 2 additions & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,8 @@ def informative_options(self):
'show-full-config': ("Show current EasyBuild configuration (all settings)", None, 'store_true', False),
'show-system-info': ("Show system information relevant to EasyBuild", None, 'store_true', False),
'terse': ("Terse output (machine-readable)", None, 'store_true', False),
'easystack': ("Path to easystack file in YAML format, specifying details of a software stack",
None, 'store', None),
})

self.log.debug("informative_options: descr %s opts %s" % (descr, opts))
Expand Down
2 changes: 1 addition & 1 deletion easybuild/tools/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def wrap(orig):
pass

if imported is None:
raise ImportError("None of the specified modules %s is available" % ', '.join(modnames))
raise ImportError("None of the specified modules (%s) is available" % ', '.join(modnames))
else:
return orig

Expand Down
6 changes: 6 additions & 0 deletions test/framework/easystacks/test_easystack_asterisk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
software:
binutils:
toolchains:
GCCcore-4.9.3:
versions:
"2.11.*"
13 changes: 13 additions & 0 deletions test/framework/easystacks/test_easystack_basic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
software:
binutils:
toolchains:
GCCcore-4.9.3:
versions:
2.25:
2.26:
toy:
toolchains:
gompi-2018a:
versions:
0.0:
versionsuffix: '-test'
7 changes: 7 additions & 0 deletions test/framework/easystacks/test_easystack_labels.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
software:
binutils:
toolchains:
GCCcore-4.9.3:
versions:
3.11:
exclude-labels: arch:aarch64
6 changes: 6 additions & 0 deletions test/framework/easystacks/test_easystack_wrong_structure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
software:
Bioconductor:
toolchains:
# foss-2020a:
versions:
3.11
2 changes: 1 addition & 1 deletion test/framework/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def bar():
def bar2():
pass

err_pat = "ImportError: None of the specified modules nosuchmodule, anothernosuchmodule is available"
err_pat = r"ImportError: None of the specified modules \(nosuchmodule, anothernosuchmodule\) is available"
self.assertErrorRegex(EasyBuildError, err_pat, bar2)

class Foo():
Expand Down
Loading