diff --git a/lib/spack/spack/multimethod.py b/lib/spack/spack/multimethod.py index 8d91e4f86d..974401e1aa 100644 --- a/lib/spack/spack/multimethod.py +++ b/lib/spack/spack/multimethod.py @@ -117,7 +117,7 @@ def __call__(self, package_self, *args, **kwargs): or if there is none, then raise a NoSuchMethodError. """ for spec, method in self.method_list: - if spec.satisfies(package_self.spec): + if package_self.spec.satisfies(spec): return method(package_self, *args, **kwargs) if self.default: diff --git a/lib/spack/spack/packages.py b/lib/spack/spack/packages.py index 36f3d4286a..5a31f1fbb9 100644 --- a/lib/spack/spack/packages.py +++ b/lib/spack/spack/packages.py @@ -77,6 +77,7 @@ def get(self, spec): @_autospec def get_installed(self, spec): + """Get all the installed specs that satisfy the provided spec constraint.""" return [s for s in self.installed_package_specs() if s.satisfies(spec)] diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index f0244695bc..35a17621b6 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -214,17 +214,17 @@ def _autospec(self, compiler_spec_like): def satisfies(self, other): - # TODO: This should not just look for overlapping versions. - # TODO: e.g., 4.7.3 should satisfy a requirement for 4.7. other = self._autospec(other) return (self.name == other.name and - self.versions.overlaps(other.versions)) + self.versions.satisfies(other.versions)) def constrain(self, other): other = self._autospec(other) - if not self.satisfies(other): - raise UnsatisfiableCompilerSpecError(self, other) + + # ensure that other will actually constrain this spec. + if not other.satisfies(self): + raise UnsatisfiableCompilerSpecError(other, self) self.versions.intersect(other.versions) @@ -866,8 +866,8 @@ def _constrain_dependencies(self, other): # TODO: might want more detail than this, e.g. specific deps # in violation. if this becomes a priority get rid of this # check and be more specici about what's wrong. - if not self.satisfies_dependencies(other): - raise UnsatisfiableDependencySpecError(self, other) + if not other.satisfies_dependencies(self): + raise UnsatisfiableDependencySpecError(other, self) # Handle common first-order constraints directly for name in self.common_dependencies(other): diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py index 37fd28a8e7..e272274a4f 100644 --- a/lib/spack/spack/test/versions.py +++ b/lib/spack/spack/test/versions.py @@ -83,6 +83,14 @@ def assert_no_overlap(self, v1, v2): self.assertFalse(ver(v1).overlaps(ver(v2))) + def assert_satisfies(self, v1, v2): + self.assertTrue(ver(v1).satisfies(ver(v2))) + + + def assert_does_not_satisfy(self, v1, v2): + self.assertFalse(ver(v1).satisfies(ver(v2))) + + def check_intersection(self, expected, a, b): self.assertEqual(ver(expected), ver(a).intersection(ver(b))) @@ -301,3 +309,40 @@ def test_intersection(self): self.check_intersection(['2.5:2.7'], ['1.1:2.7'], ['2.5:3.0','1.0']) self.check_intersection(['0:1'], [':'], ['0:1']) + + + def test_satisfaction(self): + self.assert_satisfies('4.7.3', '4.7.3') + + self.assert_satisfies('4.7.3', '4.7') + self.assert_satisfies('4.7.3b2', '4.7') + self.assert_satisfies('4.7b6', '4.7') + + self.assert_satisfies('4.7.3', '4') + self.assert_satisfies('4.7.3b2', '4') + self.assert_satisfies('4.7b6', '4') + + self.assert_does_not_satisfy('4.8.0', '4.9') + self.assert_does_not_satisfy('4.8', '4.9') + self.assert_does_not_satisfy('4', '4.9') + + self.assert_satisfies('4.7b6', '4.3:4.7') + self.assert_satisfies('4.3.0', '4.3:4.7') + self.assert_satisfies('4.3.2', '4.3:4.7') + + self.assert_does_not_satisfy('4.8.0', '4.3:4.7') + self.assert_does_not_satisfy('4.3', '4.4:4.7') + + self.assert_satisfies('4.7b6', '4.3:4.7') + self.assert_does_not_satisfy('4.8.0', '4.3:4.7') + + self.assert_satisfies('4.7', '4.3, 4.6, 4.7') + self.assert_satisfies('4.7.3', '4.3, 4.6, 4.7') + self.assert_satisfies('4.6.5', '4.3, 4.6, 4.7') + self.assert_satisfies('4.6.5.2', '4.3, 4.6, 4.7') + + self.assert_does_not_satisfy('4', '4.3, 4.6, 4.7') + self.assert_does_not_satisfy('4.8.0', '4.2, 4.3:4.7') + + self.assert_satisfies('4.8.0', '4.2, 4.3:4.8') + self.assert_satisfies('4.8.2', '4.2, 4.3:4.8') diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py index 0b5125fdf0..ce94303a9c 100644 --- a/lib/spack/spack/version.py +++ b/lib/spack/spack/version.py @@ -143,6 +143,18 @@ def highest(self): return self + @coerced + def satisfies(self, other): + """A Version 'satisfies' another if it is at least as specific and has a + common prefix. e.g., we want gcc@4.7.3 to satisfy a request for + gcc@4.7 so that when a user asks to build with gcc@4.7, we can find + a suitable compiler. + """ + nself = len(self.version) + nother = len(other.version) + return nother <= nself and self.version[:nother] == other.version + + def wildcard(self): """Create a regex that will match variants of this version string.""" def a_or_n(seg): @@ -326,6 +338,37 @@ def __contains__(self, other): none_high.le(other.end, self.end)) + @coerced + def satisfies(self, other): + """A VersionRange satisfies another if some version in this range + would satisfy some version in the other range. To do this it must + either: + a) Overlap with the other range + b) The start of this range satisfies the end of the other range. + + This is essentially the same as overlaps(), but overlaps assumes + that its arguments are specific. That is, 4.7 is interpreted as + 4.7.0.0.0.0... . This funciton assumes that 4.7 woudl be satisfied + by 4.7.3.5, etc. + + Rationale: + If a user asks for gcc@4.5:4.7, and a package is only compatible with + gcc@4.7.3:4.8, then that package should be able to build under the + constraints. Just using overlaps() would not work here. + + Note that we don't need to check whether the end of this range + would satisfy the start of the other range, because overlaps() + already covers that case. + + Note further that overlaps() is a symmetric operation, while + satisfies() is not. + """ + return (self.overlaps(other) or + # if either self.start or other.end are None, then this can't + # satisfy, or overlaps() would've taken care of it. + self.start and other.end and self.start.satisfies(other.end)) + + @coerced def overlaps(self, other): return (other in self or self in other or @@ -444,11 +487,6 @@ def highest(self): return self[-1].highest() - def satisfies(self, other): - """Synonym for overlaps.""" - return self.overlaps(other) - - @coerced def overlaps(self, other): if not other or not self: @@ -465,6 +503,27 @@ def overlaps(self, other): return False + @coerced + def satisfies(self, other): + """A VersionList satisfies another if some version in the list would + would satisfy some version in the other list. This uses essentially + the same algorithm as overlaps() does for VersionList, but it calls + satisfies() on member Versions and VersionRanges. + """ + if not other or not self: + return False + + s = o = 0 + while s < len(self) and o < len(other): + if self[s].satisfies(other[o]): + return True + elif self[s] < other[o]: + s += 1 + else: + o += 1 + return False + + @coerced def update(self, other): for v in other.versions: