From fc866ae0fe960abf723d5f89c2ae6c6baaa3ce5e Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Wed, 25 Jan 2017 16:57:01 +0100 Subject: build systems: simpler, clearer decorators: run_after, run_before (#2860) * PackageMeta: `run_before` is an alias of `precondition`, `run_after` an alias of `sanity_check` * PackageMeta: removed `precondition` and `sanity_check` * PackageMeta: decorators are now free-standing * package: modified/added docstrings. Fixed the semantics of `on_package_attributes`. * package: added unit test assertion as side effects of install * build_systems: factored build-time test running into base class * r: updated decorators in package.py * docs: updated decorator names --- lib/spack/docs/packaging_guide.rst | 8 +- lib/spack/spack/__init__.py | 16 ++- lib/spack/spack/build_systems/autotools.py | 24 ++--- lib/spack/spack/build_systems/cmake.py | 22 +--- lib/spack/spack/build_systems/makefile.py | 4 +- lib/spack/spack/build_systems/python.py | 4 +- lib/spack/spack/build_systems/r.py | 4 +- lib/spack/spack/package.py | 158 +++++++++++++++++------------ 8 files changed, 126 insertions(+), 114 deletions(-) (limited to 'lib') diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index f3927a0709..9b08a7d498 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -2775,17 +2775,17 @@ any base class listed in :ref:`installation_procedure`. .. code-block:: python - @MakefilePackage.sanity_check('build') - @MakefilePackage.on_package_attributes(run_tests=True) + @run_after('build') + @on_package_attributes(run_tests=True) def check_build(self): # Custom implementation goes here pass -The first decorator ``MakefilePackage.sanity_check('build')`` schedules this +The first decorator ``run_after('build')`` schedules this function to be invoked after the ``build`` phase has been executed, while the second one makes the invocation conditional on the fact that ``self.run_tests == True``. It is also possible to schedule a function to be invoked *before* a given phase -using the ``MakefilePackage.precondition`` decorator. +using the ``run_before`` decorator. .. note:: diff --git a/lib/spack/spack/__init__.py b/lib/spack/spack/__init__.py index 0b1934112e..6a28fbb2b0 100644 --- a/lib/spack/spack/__init__.py +++ b/lib/spack/spack/__init__.py @@ -156,14 +156,24 @@ dirty = _config.get('dirty', False) #----------------------------------------------------------------------------- __all__ = [] -from spack.package import Package +from spack.package import Package, run_before, run_after, on_package_attributes from spack.build_systems.makefile import MakefilePackage from spack.build_systems.autotools import AutotoolsPackage from spack.build_systems.cmake import CMakePackage from spack.build_systems.python import PythonPackage from spack.build_systems.r import RPackage -__all__ += ['Package', 'CMakePackage', 'AutotoolsPackage', 'MakefilePackage', - 'PythonPackage', 'RPackage'] + +__all__ += [ + 'run_before', + 'run_after', + 'on_package_attributes', + 'Package', + 'CMakePackage', + 'AutotoolsPackage', + 'MakefilePackage', + 'PythonPackage', + 'RPackage' +] from spack.version import Version, ver __all__ += ['Version', 'ver'] diff --git a/lib/spack/spack/build_systems/autotools.py b/lib/spack/spack/build_systems/autotools.py index 37c780b360..d08ea02428 100644 --- a/lib/spack/spack/build_systems/autotools.py +++ b/lib/spack/spack/build_systems/autotools.py @@ -30,9 +30,8 @@ import shutil from subprocess import PIPE from subprocess import check_call -import llnl.util.tty as tty from llnl.util.filesystem import working_dir -from spack.package import PackageBase +from spack.package import PackageBase, run_after class AutotoolsPackage(PackageBase): @@ -80,6 +79,8 @@ class AutotoolsPackage(PackageBase): #: phase install_targets = ['install'] + build_time_test_callbacks = ['check'] + def _do_patch_config_guess(self): """Some packages ship with an older config.guess and need to have this updated when installed on a newer architecture.""" @@ -168,7 +169,7 @@ class AutotoolsPackage(PackageBase): """Not needed usually, configure should be already there""" pass - @PackageBase.sanity_check('autoreconf') + @run_after('autoreconf') def is_configure_or_die(self): """Checks the presence of a `configure` file after the :py:meth:`.autoreconf` phase. @@ -211,20 +212,7 @@ class AutotoolsPackage(PackageBase): with working_dir(self.build_directory()): inspect.getmodule(self).make(*self.install_targets) - @PackageBase.sanity_check('build') - @PackageBase.on_package_attributes(run_tests=True) - def _run_default_function(self): - """This function is run after build if ``self.run_tests == True`` - - It will search for a method named :py:meth:`.check` and run it. A - sensible default is provided in the base class. - """ - try: - fn = getattr(self, 'check') - tty.msg('Trying default sanity checks [check]') - fn() - except AttributeError: - tty.msg('Skipping default sanity checks [method `check` not implemented]') # NOQA: ignore=E501 + run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Makefile for targets ``test`` and ``check`` @@ -235,4 +223,4 @@ class AutotoolsPackage(PackageBase): self._if_make_target_execute('check') # Check that self.prefix is there after installation - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/spack/build_systems/cmake.py b/lib/spack/spack/build_systems/cmake.py index a5e23e54f4..823ef502c4 100644 --- a/lib/spack/spack/build_systems/cmake.py +++ b/lib/spack/spack/build_systems/cmake.py @@ -26,11 +26,10 @@ import inspect import platform -import llnl.util.tty as tty import spack.build_environment from llnl.util.filesystem import working_dir, join_path from spack.directives import depends_on -from spack.package import PackageBase +from spack.package import PackageBase, run_after class CMakePackage(PackageBase): @@ -72,6 +71,8 @@ class CMakePackage(PackageBase): build_targets = [] install_targets = ['install'] + build_time_test_callbacks = ['check'] + depends_on('cmake', type='build') def build_type(self): @@ -155,20 +156,7 @@ class CMakePackage(PackageBase): with working_dir(self.build_directory()): inspect.getmodule(self).make(*self.install_targets) - @PackageBase.sanity_check('build') - @PackageBase.on_package_attributes(run_tests=True) - def _run_default_function(self): - """This function is run after build if ``self.run_tests == True`` - - It will search for a method named ``check`` and run it. A sensible - default is provided in the base class. - """ - try: - fn = getattr(self, 'check') - tty.msg('Trying default build sanity checks [check]') - fn() - except AttributeError: - tty.msg('Skipping default build sanity checks [method `check` not implemented]') # NOQA: ignore=E501 + run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the CMake-generated Makefile for the target ``test`` @@ -178,4 +166,4 @@ class CMakePackage(PackageBase): self._if_make_target_execute('test') # Check that self.prefix is there after installation - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/spack/build_systems/makefile.py b/lib/spack/spack/build_systems/makefile.py index e8fa86264b..f07bcd62ab 100644 --- a/lib/spack/spack/build_systems/makefile.py +++ b/lib/spack/spack/build_systems/makefile.py @@ -27,7 +27,7 @@ import inspect import llnl.util.tty as tty from llnl.util.filesystem import working_dir -from spack.package import PackageBase +from spack.package import PackageBase, run_after class MakefilePackage(PackageBase): @@ -100,4 +100,4 @@ class MakefilePackage(PackageBase): inspect.getmodule(self).make(*self.install_targets) # Check that self.prefix is there after installation - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/spack/build_systems/python.py b/lib/spack/spack/build_systems/python.py index d21c291ae6..5e7c1c356d 100644 --- a/lib/spack/spack/build_systems/python.py +++ b/lib/spack/spack/build_systems/python.py @@ -26,7 +26,7 @@ import inspect from spack.directives import extends -from spack.package import PackageBase +from spack.package import PackageBase, run_after from llnl.util.filesystem import working_dir @@ -306,4 +306,4 @@ class PythonPackage(PackageBase): return [] # Check that self.prefix is there after installation - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/spack/build_systems/r.py b/lib/spack/spack/build_systems/r.py index a4f7359ec8..cde3dc9fdd 100644 --- a/lib/spack/spack/build_systems/r.py +++ b/lib/spack/spack/build_systems/r.py @@ -26,7 +26,7 @@ import inspect from spack.directives import extends -from spack.package import PackageBase +from spack.package import PackageBase, run_after class RPackage(PackageBase): @@ -55,4 +55,4 @@ class RPackage(PackageBase): self.stage.source_path) # Check that self.prefix is there after installation - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 24ff82fa38..0841ddcd61 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -78,13 +78,14 @@ class InstallPhase(object): search for execution. The method is retrieved at __get__ time, so that it can be overridden by subclasses of whatever class declared the phases. - It also provides hooks to execute prerequisite and sanity checks. + It also provides hooks to execute arbitrary callbacks before and after + the phase. """ def __init__(self, name): self.name = name - self.preconditions = [] - self.sanity_checks = [] + self.run_before = [] + self.run_after = [] def __get__(self, instance, owner): # The caller is a class that is trying to customize @@ -101,14 +102,13 @@ class InstallPhase(object): self._on_phase_start(instance) # Execute phase pre-conditions, # and give them the chance to fail - for check in self.preconditions: - # Do something sensible at some point - check(instance) + for callback in self.run_before: + callback(instance) phase(spec, prefix) # Execute phase sanity_checks, # and give them the chance to fail - for check in self.sanity_checks: - check(instance) + for callback in self.run_after: + callback(instance) # Check instance attributes at the end of a phase self._on_phase_exit(instance) return phase_wrapper @@ -129,8 +129,8 @@ class InstallPhase(object): # This bug-fix was not back-ported in Python 2.6 # http://bugs.python.org/issue1515 other = InstallPhase(self.name) - other.preconditions.extend(self.preconditions) - other.sanity_checks.extend(self.sanity_checks) + other.run_before.extend(self.run_before) + other.run_after.extend(self.run_after) return other @@ -142,22 +142,23 @@ class PackageMeta(spack.directives.DirectiveMetaMixin): """ phase_fmt = '_InstallPhase_{0}' - _InstallPhase_sanity_checks = {} - _InstallPhase_preconditions = {} + _InstallPhase_run_before = {} + _InstallPhase_run_after = {} + + def __new__(mcs, name, bases, attr_dict): - def __new__(meta, name, bases, attr_dict): - # Check if phases is in attr dict, then set - # install phases wrappers if 'phases' in attr_dict: + # Turn the strings in 'phases' into InstallPhase instances + # and add them as private attributes _InstallPhase_phases = [PackageMeta.phase_fmt.format(x) for x in attr_dict['phases']] # NOQA: ignore=E501 for phase_name, callback_name in zip(_InstallPhase_phases, attr_dict['phases']): # NOQA: ignore=E501 attr_dict[phase_name] = InstallPhase(callback_name) attr_dict['_InstallPhase_phases'] = _InstallPhase_phases - def _append_checks(check_name): + def _flush_callbacks(check_name): # Name of the attribute I am going to check it exists attr_name = PackageMeta.phase_fmt.format(check_name) - checks = getattr(meta, attr_name) + checks = getattr(mcs, attr_name) if checks: for phase_name, funcs in checks.items(): try: @@ -180,57 +181,61 @@ class PackageMeta(spack.directives.DirectiveMetaMixin): PackageMeta.phase_fmt.format(phase_name)] getattr(phase, check_name).extend(funcs) # Clear the attribute for the next class - setattr(meta, attr_name, {}) - - @classmethod - def _register_checks(cls, check_type, *args): - def _register_sanity_checks(func): - attr_name = PackageMeta.phase_fmt.format(check_type) - check_list = getattr(meta, attr_name) - for item in args: - checks = check_list.setdefault(item, []) - checks.append(func) - setattr(meta, attr_name, check_list) - return func - return _register_sanity_checks - - @staticmethod - def on_package_attributes(**attrs): - def _execute_under_condition(func): - @functools.wraps(func) - def _wrapper(instance): - # If all the attributes have the value we require, then - # execute - if all([getattr(instance, key, None) == value for key, value in attrs.items()]): # NOQA: ignore=E501 - func(instance) - return _wrapper - return _execute_under_condition - - @classmethod - def precondition(cls, *args): - return cls._register_checks('preconditions', *args) - - @classmethod - def sanity_check(cls, *args): - return cls._register_checks('sanity_checks', *args) - - if all([not hasattr(x, '_register_checks') for x in bases]): - attr_dict['_register_checks'] = _register_checks - - if all([not hasattr(x, 'sanity_check') for x in bases]): - attr_dict['sanity_check'] = sanity_check - - if all([not hasattr(x, 'precondition') for x in bases]): - attr_dict['precondition'] = precondition - - if all([not hasattr(x, 'on_package_attributes') for x in bases]): - attr_dict['on_package_attributes'] = on_package_attributes + setattr(mcs, attr_name, {}) # Preconditions - _append_checks('preconditions') + _flush_callbacks('run_before') # Sanity checks - _append_checks('sanity_checks') - return super(PackageMeta, meta).__new__(meta, name, bases, attr_dict) + _flush_callbacks('run_after') + return super(PackageMeta, mcs).__new__(mcs, name, bases, attr_dict) + + @staticmethod + def register_callback(check_type, *phases): + def _decorator(func): + attr_name = PackageMeta.phase_fmt.format(check_type) + check_list = getattr(PackageMeta, attr_name) + for item in phases: + checks = check_list.setdefault(item, []) + checks.append(func) + setattr(PackageMeta, attr_name, check_list) + return func + return _decorator + + +def run_before(*phases): + """Registers a method of a package to be run before a given phase""" + return PackageMeta.register_callback('run_before', *phases) + + +def run_after(*phases): + """Registers a method of a package to be run after a given phase""" + return PackageMeta.register_callback('run_after', *phases) + + +def on_package_attributes(**attr_dict): + """Executes the decorated method only if at the moment of calling + the instance has attributes that are equal to certain values. + + :param dict attr_dict: dictionary mapping attribute names to their + required values + """ + def _execute_under_condition(func): + + @functools.wraps(func) + def _wrapper(instance, *args, **kwargs): + # If all the attributes have the value we require, then execute + has_all_attributes = all( + [hasattr(instance, key) for key in attr_dict] + ) + if has_all_attributes: + has_the_right_values = all( + [getattr(instance, key) == value for key, value in attr_dict.items()] # NOQA: ignore=E501 + ) + if has_the_right_values: + func(instance, *args, **kwargs) + return _wrapper + + return _execute_under_condition class PackageBase(object): @@ -1704,6 +1709,27 @@ class PackageBase(object): """ return " ".join("-Wl,-rpath,%s" % p for p in self.rpath) + build_time_test_callbacks = None + + @on_package_attributes(run_tests=True) + def _run_default_build_time_test_callbacks(self): + """Tries to call all the methods that are listed in the attribute + ``build_time_test_callbacks`` if ``self.run_tests is True``. + + If ``build_time_test_callbacks is None`` returns immediately. + """ + if self.build_time_test_callbacks is None: + return + + for name in self.build_time_test_callbacks: + try: + fn = getattr(self, name) + tty.msg('RUN-TESTS: build-time tests [{0}]'.format(name)) + fn() + except AttributeError: + msg = 'RUN-TESTS: method not implemented [{0}]' + tty.warn(msg.format(name)) + class Package(PackageBase): """General purpose class with a single ``install`` @@ -1716,7 +1742,7 @@ class Package(PackageBase): build_system_class = 'Package' # This will be used as a registration decorator in user # packages, if need be - PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix) + run_after('install')(PackageBase.sanity_check_prefix) def install_dependency_symlinks(pkg, spec, prefix): -- cgit v1.2.3-60-g2f50