new directive: conflicts() (#3125)

* Add conflicts(<spec>) directive
* openblas: added conflicts for intel@16 refs #3119
* added brief docs and unit tests
This commit is contained in:
Massimiliano Culpo 2017-04-02 20:40:09 +02:00 committed by Todd Gamblin
parent 0e785e0500
commit ffef681377
7 changed files with 193 additions and 1 deletions

View File

@ -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:
----------

View File

@ -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.

View File

@ -1707,6 +1707,18 @@ def concretize(self):
# 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 @@ def __init__(self, spec, addition):
"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)

View File

@ -26,7 +26,7 @@
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 @@ def test_compiler_child(self):
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()

View File

@ -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)

View File

@ -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)

View File

@ -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