SPACK-56: fix Variant concretization.

- Variant concretization is tricky:

  - During concretization, a spec without variants (e.g., mpich) means
    "don't care".  So, Spec('mpich').satisfies('mpich+debug') is true
    because it *could* still be built that way.

  - After concretization, a spec without a particular variant means
    "don't know", as that wasn't part of the spec, so the opposite
    relationship is true.  Assume 'spec' is already installed:

      spec.satisfies('mpich+debug')

    this is false beacuse the `debug` variant didn't exist when spec
    was built, so we can't satisfy the explicit request for +debug.
This commit is contained in:
Todd Gamblin 2015-04-27 00:10:40 -07:00
parent 3b1898b8e4
commit 535c1fac87
5 changed files with 107 additions and 23 deletions

View File

@ -101,6 +101,16 @@ def concretize_architecture(self, spec):
spec.architecture = spack.architecture.sys_type() spec.architecture = spack.architecture.sys_type()
def concretize_variants(self, spec):
"""If the spec already has variants filled in, return. Otherwise, add
the default variants from the package specification.
"""
for name, variant in spec.package.variants.items():
if name not in spec.variants:
spec.variants[name] = spack.spec.VariantSpec(
name, variant.default)
def concretize_compiler(self, spec): def concretize_compiler(self, spec):
"""If the spec already has a compiler, we're done. If not, then take """If the spec already has a compiler, we're done. If not, then take
the compiler used for the nearest ancestor with a compiler the compiler used for the nearest ancestor with a compiler

View File

@ -270,7 +270,7 @@ def __repr__(self):
@key_ordering @key_ordering
class Variant(object): class VariantSpec(object):
"""Variants are named, build-time options for a package. Names depend """Variants are named, build-time options for a package. Names depend
on the particular package being built, and each named variant can on the particular package being built, and each named variant can
be enabled or disabled. be enabled or disabled.
@ -285,7 +285,7 @@ def _cmp_key(self):
def copy(self): def copy(self):
return Variant(self.name, self.enabled) return VariantSpec(self.name, self.enabled)
def __str__(self): def __str__(self):
@ -294,9 +294,27 @@ def __str__(self):
class VariantMap(HashableMap): class VariantMap(HashableMap):
def satisfies(self, other): def satisfies(self, other, self_is_concrete):
return all(self[key].enabled == other[key].enabled if self_is_concrete:
for key in other if key in self) return all(k in self and self[k].enabled == other[k].enabled
for k in other)
else:
return all(self[k].enabled == other[k].enabled
for k in other if k in self)
def constrain(self, other, other_is_concrete):
if other_is_concrete:
for k in self:
if k not in other:
raise UnsatisfiableVariantSpecError(self[k], '<absent>')
for k in other:
if k in self:
if self[k].enabled != other[k].enabled:
raise UnsatisfiableVariantSpecError(self[k], other[k])
else:
self[k] = other[k].copy()
def __str__(self): def __str__(self):
@ -375,7 +393,7 @@ def _add_variant(self, name, enabled):
"""Called by the parser to add a variant.""" """Called by the parser to add a variant."""
if name in self.variants: raise DuplicateVariantError( if name in self.variants: raise DuplicateVariantError(
"Cannot specify variant '%s' twice" % name) "Cannot specify variant '%s' twice" % name)
self.variants[name] = Variant(name, enabled) self.variants[name] = VariantSpec(name, enabled)
def _set_compiler(self, compiler): def _set_compiler(self, compiler):
@ -607,6 +625,7 @@ def _concretize_helper(self, presets=None, visited=None):
spack.concretizer.concretize_architecture(self) spack.concretizer.concretize_architecture(self)
spack.concretizer.concretize_compiler(self) spack.concretizer.concretize_compiler(self)
spack.concretizer.concretize_version(self) spack.concretizer.concretize_version(self)
spack.concretizer.concretize_variants(self)
presets[self.name] = self presets[self.name] = self
visited.add(self.name) visited.add(self.name)
@ -789,8 +808,7 @@ def _normalize_helper(self, visited, spec_deps, provider_index):
else: else:
required = index.providers_for(vspec.name) required = index.providers_for(vspec.name)
if required: if required:
raise UnsatisfiableProviderSpecError( raise UnsatisfiableProviderSpecError(required[0], pkg_dep)
required[0], pkg_dep)
provider_index.update(pkg_dep) provider_index.update(pkg_dep)
if name not in spec_deps: if name not in spec_deps:
@ -929,7 +947,7 @@ def constrain(self, other, **kwargs):
self.compiler = other.compiler self.compiler = other.compiler
self.versions.intersect(other.versions) self.versions.intersect(other.versions)
self.variants.update(other.variants) self.variants.constrain(other.variants, other._concrete)
self.architecture = self.architecture or other.architecture self.architecture = self.architecture or other.architecture
if constrain_deps: if constrain_deps:
@ -998,11 +1016,13 @@ def satisfies(self, other, **kwargs):
# All these attrs have satisfies criteria of their own, # All these attrs have satisfies criteria of their own,
# but can be None to indicate no constraints. # but can be None to indicate no constraints.
for s, o in ((self.versions, other.versions), for s, o in ((self.versions, other.versions),
(self.variants, other.variants),
(self.compiler, other.compiler)): (self.compiler, other.compiler)):
if s and o and not s.satisfies(o): if s and o and not s.satisfies(o):
return False return False
if not self.variants.satisfies(other.variants, self._concrete):
return False
# Architecture satisfaction is currently just string equality. # Architecture satisfaction is currently just string equality.
# Can be None for unconstrained, though. # Can be None for unconstrained, though.
if (self.architecture and other.architecture and if (self.architecture and other.architecture and

View File

@ -242,12 +242,6 @@ def test_unsatisfiable_compiler_version(self):
self.assertRaises(spack.spec.UnsatisfiableCompilerSpecError, spec.normalize) self.assertRaises(spack.spec.UnsatisfiableCompilerSpecError, spec.normalize)
def test_unsatisfiable_variant(self):
set_pkg_dep('mpileaks', 'mpich+debug')
spec = Spec('mpileaks ^mpich~debug ^callpath ^dyninst ^libelf ^libdwarf')
self.assertRaises(spack.spec.UnsatisfiableVariantSpecError, spec.normalize)
def test_unsatisfiable_architecture(self): def test_unsatisfiable_architecture(self):
set_pkg_dep('mpileaks', 'mpich=bgqos_0') set_pkg_dep('mpileaks', 'mpich=bgqos_0')
spec = Spec('mpileaks ^mpich=sles_10_ppc64 ^callpath ^dyninst ^libelf ^libdwarf') spec = Spec('mpileaks ^mpich=sles_10_ppc64 ^callpath ^dyninst ^libelf ^libdwarf')

View File

@ -33,8 +33,8 @@ class SpecSematicsTest(MockPackagesTest):
# ================================================================================ # ================================================================================
# Utility functions to set everything up. # Utility functions to set everything up.
# ================================================================================ # ================================================================================
def check_satisfies(self, spec, anon_spec): def check_satisfies(self, spec, anon_spec, concrete=False):
left = Spec(spec) left = Spec(spec, concrete=concrete)
right = parse_anonymous_spec(anon_spec, left.name) right = parse_anonymous_spec(anon_spec, left.name)
# Satisfies is one-directional. # Satisfies is one-directional.
@ -46,8 +46,8 @@ def check_satisfies(self, spec, anon_spec):
right.copy().constrain(left) right.copy().constrain(left)
def check_unsatisfiable(self, spec, anon_spec): def check_unsatisfiable(self, spec, anon_spec, concrete=False):
left = Spec(spec) left = Spec(spec, concrete=concrete)
right = parse_anonymous_spec(anon_spec, left.name) right = parse_anonymous_spec(anon_spec, left.name)
self.assertFalse(left.satisfies(right)) self.assertFalse(left.satisfies(right))
@ -150,9 +150,33 @@ def test_satisfies_virtual_dependency_versions(self):
self.check_unsatisfiable('mpileaks^mpi@3:', '^mpich@1.0') self.check_unsatisfiable('mpileaks^mpi@3:', '^mpich@1.0')
def test_satisfies_variant(self): def test_satisfies_matching_variant(self):
self.check_satisfies('foo %gcc@4.7.3', '%gcc@4.7') self.check_satisfies('mpich+foo', 'mpich+foo')
self.check_unsatisfiable('foo %gcc@4.7', '%gcc@4.7.3') self.check_satisfies('mpich~foo', 'mpich~foo')
def test_satisfies_unconstrained_variant(self):
# only asked for mpich, no constraints. Either will do.
self.check_satisfies('mpich+foo', 'mpich')
self.check_satisfies('mpich~foo', 'mpich')
def test_unsatisfiable_variants(self):
# This case is different depending on whether the specs are concrete.
# 'mpich' is not concrete:
self.check_satisfies('mpich', 'mpich+foo', False)
self.check_satisfies('mpich', 'mpich~foo', False)
# 'mpich' is concrete:
self.check_unsatisfiable('mpich', 'mpich+foo', True)
self.check_unsatisfiable('mpich', 'mpich~foo', True)
def test_unsatisfiable_variant_mismatch(self):
# No matchi in specs
self.check_unsatisfiable('mpich~foo', 'mpich+foo')
self.check_unsatisfiable('mpich+foo', 'mpich~foo')

View File

@ -0,0 +1,36 @@
##############################################################################
# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
# Produced at the Lawrence Livermore National Laboratory.
#
# This file is part of Spack.
# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
# LLNL-CODE-647188
#
# For details, see https://scalability-llnl.github.io/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 General Public License (as published by
# the Free Software Foundation) version 2.1 dated 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 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
##############################################################################
"""Variant is a class describing flags on builds, or "variants".
Could be generalized later to describe aribitrary parameters, but
currently variants are just flags.
"""
class Variant(object):
"""Represents a variant on a build. Can be either on or off."""
def __init__(self, default, description):
self.default = bool(default)
self.description = str(description)