From ffef681377a2140040b4d65a991b9f75e56f9252 Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Sun, 2 Apr 2017 20:40:09 +0200 Subject: new directive: conflicts() (#3125) * Add conflicts() directive * openblas: added conflicts for intel@16 refs #3119 * added brief docs and unit tests --- lib/spack/docs/packaging_guide.rst | 22 +++++++++++ lib/spack/spack/directives.py | 27 +++++++++++++ lib/spack/spack/spec.py | 24 +++++++++++ lib/spack/spack/test/concretize.py | 27 ++++++++++++- .../packages/conflict-parent/package.py | 46 ++++++++++++++++++++++ .../builtin.mock/packages/conflict/package.py | 46 ++++++++++++++++++++++ .../repos/builtin/packages/openblas/package.py | 2 + 7 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 var/spack/repos/builtin.mock/packages/conflict-parent/package.py create mode 100644 var/spack/repos/builtin.mock/packages/conflict/package.py diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index 1a64c7db4a..18541179b2 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -1560,6 +1560,28 @@ Python's ``setup_dependent_environment`` method also sets up some other variables, creates a directory, and sets up the ``PYTHONPATH`` so that dependent packages can find their dependencies at build time. +.. _packaging_conflicts: + +--------- +Conflicts +--------- + +Sometimes packages have known bugs, or limitations, that would prevent them +to build e.g. against other dependencies or with certain compilers. Spack +makes it possible to express such constraints with the ``conflicts`` directive. + +Adding the following to a package: + +.. code-block:: python + + conflicts('%intel', when='@1.2') + +we express the fact that the current package *cannot be built* with the Intel +compiler when we are trying to install version "1.2". The ``when`` argument can +be omitted, in which case the conflict will always be active. +Conflicts are always evaluated after the concretization step has been performed, +and if any match is found a detailed error message is shown to the user. + .. _packaging_extensions: ---------- diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py index db280cb5fd..e2219d1f49 100644 --- a/lib/spack/spack/directives.py +++ b/lib/spack/spack/directives.py @@ -263,6 +263,33 @@ def _depends_on(pkg, spec, when=None, type=None): conditions[when_spec] = dep_spec +@directive('conflicts') +def conflicts(conflict_spec, when=None): + """Allows a package to define a conflict, i.e. a concretized configuration + that is known to be non-valid. + + For example a package that is known not to be buildable with intel + compilers can declare: + + conflicts('%intel') + + To express the same constraint only when the 'foo' variant is activated: + + conflicts('%intel', when='+foo') + + :param conflict_spec: constraint defining the known conflict + :param when: optional constraint that triggers the conflict + """ + def _execute(pkg): + # If when is not specified the conflict always holds + condition = pkg.name if when is None else when + when_spec = parse_anonymous_spec(condition, pkg.name) + + when_spec_list = pkg.conflicts.setdefault(conflict_spec, []) + when_spec_list.append(when_spec) + return _execute + + @directive(('dependencies', 'dependency_types')) def depends_on(spec, when=None, type=None): """Creates a dict of deps with specs defining when they apply. diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 095b04f837..b6089c7849 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -1707,6 +1707,18 @@ class Spec(object): # Mark everything in the spec as concrete, as well. self._mark_concrete() + # Now that the spec is concrete we should check if + # there are declared conflicts + matches = [] + for x in self.traverse(): + for conflict_spec, when_list in x.package.conflicts.items(): + if x.satisfies(conflict_spec): + for when_spec in when_list: + if x.satisfies(when_spec): + matches.append((x, conflict_spec, when_spec)) + if matches: + raise ConflictsInSpecError(self, matches) + def _mark_concrete(self, value=True): """Mark this spec and its dependencies as concrete. @@ -3336,3 +3348,15 @@ class RedundantSpecError(SpecError): "Attempting to add %s to spec %s which is already concrete." " This is likely the result of adding to a spec specified by hash." % (addition, spec)) + + +class ConflictsInSpecError(SpecError, RuntimeError): + def __init__(self, spec, matches): + message = 'Conflicts in concretized spec "{0}"\n'.format( + spec.short_spec + ) + long_message = 'List of matching conflicts:\n\n' + match_fmt = '{0}. "{1}" conflicts with "{2}" in spec "{3}"\n' + for idx, (s, c, w) in enumerate(matches): + long_message += match_fmt.format(idx + 1, c, w, s) + super(ConflictsInSpecError, self).__init__(message, long_message) diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index f4021a89ee..3b383584ce 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -26,7 +26,7 @@ import pytest import spack import spack.architecture from spack.concretize import find_spec -from spack.spec import Spec, CompilerSpec +from spack.spec import Spec, CompilerSpec, ConflictsInSpecError, SpecError from spack.version import ver @@ -82,6 +82,10 @@ def check_concretize(abstract_spec): 'mpileaks ^mpi', 'mpileaks ^mpi@:1.1', 'mpileaks ^mpi@2:', 'mpileaks ^mpi@2.1', 'mpileaks ^mpi@2.2', 'mpileaks ^mpi@2.2', 'mpileaks ^mpi@:1', 'mpileaks ^mpi@1.2:2' + # conflict not triggered + 'conflict', + 'conflict%clang~foo', + 'conflict-parent%gcc' ] ) def spec(request): @@ -89,6 +93,19 @@ def spec(request): return request.param +@pytest.fixture( + params=[ + 'conflict%clang', + 'conflict%clang+foo', + 'conflict-parent%clang', + 'conflict-parent@0.9^conflict~foo' + ] +) +def conflict_spec(request): + """Spec to be concretized""" + return request.param + + @pytest.mark.usefixtures('config', 'builtin_mock') class TestConcretize(object): def test_concretize(self, spec): @@ -372,3 +389,11 @@ class TestConcretize(object): s.concretize() assert s['mpileaks'].satisfies('%clang') assert s['dyninst'].satisfies('%gcc') + + def test_conflicts_in_spec(self, conflict_spec): + # Check that an exception is raised an caught by the appropriate + # exception types. + for exc_type in (ConflictsInSpecError, RuntimeError, SpecError): + s = Spec(conflict_spec) + with pytest.raises(exc_type): + s.concretize() diff --git a/var/spack/repos/builtin.mock/packages/conflict-parent/package.py b/var/spack/repos/builtin.mock/packages/conflict-parent/package.py new file mode 100644 index 0000000000..37805537a2 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/conflict-parent/package.py @@ -0,0 +1,46 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program 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 terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +from spack import * + + +class ConflictParent(Package): + homepage = 'https://github.com/tgamblin/callpath' + url = 'http://github.com/tgamblin/callpath-1.0.tar.gz' + + version(0.8, 'foobarbaz') + version(0.9, 'foobarbaz') + version(1.0, 'foobarbaz') + + depends_on('conflict') + + conflicts('^conflict~foo', when='@0.9') + + def install(self, spec, prefix): + configure("--prefix=%s" % prefix) + make() + make("install") + + def setup_environment(self, senv, renv): + renv.set('FOOBAR', self.name) diff --git a/var/spack/repos/builtin.mock/packages/conflict/package.py b/var/spack/repos/builtin.mock/packages/conflict/package.py new file mode 100644 index 0000000000..a6ba4b5c58 --- /dev/null +++ b/var/spack/repos/builtin.mock/packages/conflict/package.py @@ -0,0 +1,46 @@ +############################################################################## +# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/llnl/spack +# Please also see the LICENSE file for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program 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 terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +from spack import * + + +class Conflict(Package): + homepage = 'https://github.com/tgamblin/callpath' + url = 'http://github.com/tgamblin/callpath-1.0.tar.gz' + + version(0.8, 'foobarbaz') + version(0.9, 'foobarbaz') + version(1.0, 'foobarbaz') + + variant('foo', default=True, description='') + + conflicts('%clang', when='+foo') + + def install(self, spec, prefix): + configure("--prefix=%s" % prefix) + make() + make("install") + + def setup_environment(self, senv, renv): + renv.set('FOOBAR', self.name) diff --git a/var/spack/repos/builtin/packages/openblas/package.py b/var/spack/repos/builtin/packages/openblas/package.py index bfa5a16b42..6e16174563 100644 --- a/var/spack/repos/builtin/packages/openblas/package.py +++ b/var/spack/repos/builtin/packages/openblas/package.py @@ -59,6 +59,8 @@ class Openblas(MakefilePackage): parallel = False + conflicts('%intel@16', when='@0.2.15:0.2.19') + @run_before('edit') def check_compilers(self): # As of 06/2016 there is no mechanism to specify that packages which -- cgit v1.2.3-60-g2f50