Fixes for parsing specs with hashes (#2889)
- Allows hashes to be specified after other parts of the spec - Does not allow other parts of the spec to be specified after the hash - The hash must either end input or be followed by another separate spec - The next spec cannot be an anonymous spec (it must start with a package name or a hash) See #2769 (after it was merged) for further discussion of this interface addition. That discussion resulted in these requirements: ``` python # 1 spec /abc123 # 1 spec python /abc123 # 1 spec /456789 # 1 spec python /abc123 /456789 # 2 specs python /456789 /abc123 # 2 specs /abc123 /456789 # 2 specs /456789 /abc123 # 2 specs /456789 /abc123 python # 3 specs ``` assuming `abc123` and `456789` are both hashes of different python specs.
This commit is contained in:
parent
a4f594a68d
commit
8ae380fb71
@ -152,7 +152,9 @@
|
|||||||
'UnsatisfiableArchitectureSpecError',
|
'UnsatisfiableArchitectureSpecError',
|
||||||
'UnsatisfiableProviderSpecError',
|
'UnsatisfiableProviderSpecError',
|
||||||
'UnsatisfiableDependencySpecError',
|
'UnsatisfiableDependencySpecError',
|
||||||
'AmbiguousHashError']
|
'AmbiguousHashError',
|
||||||
|
'InvalidHashError',
|
||||||
|
'RedundantSpecError']
|
||||||
|
|
||||||
# Valid pattern for an identifier in Spack
|
# Valid pattern for an identifier in Spack
|
||||||
identifier_re = r'\w[\w-]*'
|
identifier_re = r'\w[\w-]*'
|
||||||
@ -1993,7 +1995,7 @@ def _autospec(self, spec_like):
|
|||||||
except SpecError:
|
except SpecError:
|
||||||
return parse_anonymous_spec(spec_like, self.name)
|
return parse_anonymous_spec(spec_like, self.name)
|
||||||
|
|
||||||
def satisfies(self, other, deps=True, strict=False):
|
def satisfies(self, other, deps=True, strict=False, strict_deps=False):
|
||||||
"""Determine if this spec satisfies all constraints of another.
|
"""Determine if this spec satisfies all constraints of another.
|
||||||
|
|
||||||
There are two senses for satisfies:
|
There are two senses for satisfies:
|
||||||
@ -2067,7 +2069,8 @@ def satisfies(self, other, deps=True, strict=False):
|
|||||||
# If we need to descend into dependencies, do it, otherwise we're done.
|
# If we need to descend into dependencies, do it, otherwise we're done.
|
||||||
if deps:
|
if deps:
|
||||||
deps_strict = strict
|
deps_strict = strict
|
||||||
if not (self.name and other.name):
|
if self.concrete and not other.name:
|
||||||
|
# We're dealing with existing specs
|
||||||
deps_strict = True
|
deps_strict = True
|
||||||
return self.satisfies_dependencies(other, strict=deps_strict)
|
return self.satisfies_dependencies(other, strict=deps_strict)
|
||||||
else:
|
else:
|
||||||
@ -2083,9 +2086,10 @@ def satisfies_dependencies(self, other, strict=False):
|
|||||||
if other._dependencies and not self._dependencies:
|
if other._dependencies and not self._dependencies:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
alldeps = set(d.name for d in self.traverse(root=False))
|
selfdeps = self.traverse(root=False)
|
||||||
if not all(dep.name in alldeps
|
otherdeps = other.traverse(root=False)
|
||||||
for dep in other.traverse(root=False)):
|
if not all(any(d.satisfies(dep) for d in selfdeps)
|
||||||
|
for dep in otherdeps):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif not self._dependencies or not other._dependencies:
|
elif not self._dependencies or not other._dependencies:
|
||||||
@ -2697,30 +2701,28 @@ def do_parse(self):
|
|||||||
specs = []
|
specs = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while self.next or self.previous:
|
while self.next:
|
||||||
# TODO: clean this parsing up a bit
|
# TODO: clean this parsing up a bit
|
||||||
if self.previous:
|
if self.accept(ID):
|
||||||
# We picked up the name of this spec while finishing the
|
|
||||||
# previous spec
|
|
||||||
specs.append(self.spec(self.previous.value))
|
|
||||||
self.previous = None
|
|
||||||
elif self.accept(ID):
|
|
||||||
self.previous = self.token
|
self.previous = self.token
|
||||||
if self.accept(EQ):
|
if self.accept(EQ):
|
||||||
# We're either parsing an anonymous spec beginning
|
# We're parsing an anonymous spec beginning with a
|
||||||
# with a key-value pair or adding a key-value pair
|
# key-value pair.
|
||||||
# to the last spec
|
|
||||||
if not specs:
|
if not specs:
|
||||||
specs.append(self.spec(None))
|
specs.append(self.spec(None))
|
||||||
self.expect(VAL)
|
self.expect(VAL)
|
||||||
|
# Raise an error if the previous spec is already
|
||||||
|
# concrete (assigned by hash)
|
||||||
|
if specs[-1]._hash:
|
||||||
|
raise RedundantSpecError(specs[-1],
|
||||||
|
'key-value pair')
|
||||||
specs[-1]._add_flag(
|
specs[-1]._add_flag(
|
||||||
self.previous.value, self.token.value)
|
self.previous.value, self.token.value)
|
||||||
self.previous = None
|
self.previous = None
|
||||||
else:
|
else:
|
||||||
# We're parsing a new spec by name
|
# We're parsing a new spec by name
|
||||||
value = self.previous.value
|
|
||||||
self.previous = None
|
self.previous = None
|
||||||
specs.append(self.spec(value))
|
specs.append(self.spec(self.token.value))
|
||||||
elif self.accept(HASH):
|
elif self.accept(HASH):
|
||||||
# We're finding a spec by hash
|
# We're finding a spec by hash
|
||||||
specs.append(self.spec_by_hash())
|
specs.append(self.spec_by_hash())
|
||||||
@ -2728,19 +2730,24 @@ def do_parse(self):
|
|||||||
elif self.accept(DEP):
|
elif self.accept(DEP):
|
||||||
if not specs:
|
if not specs:
|
||||||
# We're parsing an anonymous spec beginning with a
|
# We're parsing an anonymous spec beginning with a
|
||||||
# dependency
|
# dependency. Push the token to recover after creating
|
||||||
self.previous = self.token
|
# anonymous spec
|
||||||
|
self.push_tokens([self.token])
|
||||||
specs.append(self.spec(None))
|
specs.append(self.spec(None))
|
||||||
self.previous = None
|
else:
|
||||||
if self.accept(HASH):
|
if self.accept(HASH):
|
||||||
# We're finding a dependency by hash for an anonymous
|
# We're finding a dependency by hash for an
|
||||||
# spec
|
# anonymous spec
|
||||||
dep = self.spec_by_hash()
|
dep = self.spec_by_hash()
|
||||||
else:
|
else:
|
||||||
# We're adding a dependency to the last spec
|
# We're adding a dependency to the last spec
|
||||||
self.expect(ID)
|
self.expect(ID)
|
||||||
dep = self.spec(self.token.value)
|
dep = self.spec(self.token.value)
|
||||||
|
|
||||||
|
# Raise an error if the previous spec is already
|
||||||
|
# concrete (assigned by hash)
|
||||||
|
if specs[-1]._hash:
|
||||||
|
raise RedundantSpecError(specs[-1], 'dependency')
|
||||||
# command line deps get empty deptypes now.
|
# command line deps get empty deptypes now.
|
||||||
# Real deptypes are assigned later per packages.
|
# Real deptypes are assigned later per packages.
|
||||||
specs[-1]._add_dependency(dep, ())
|
specs[-1]._add_dependency(dep, ())
|
||||||
@ -2749,6 +2756,12 @@ def do_parse(self):
|
|||||||
# If the next token can be part of a valid anonymous spec,
|
# If the next token can be part of a valid anonymous spec,
|
||||||
# create the anonymous spec
|
# create the anonymous spec
|
||||||
if self.next.type in (AT, ON, OFF, PCT):
|
if self.next.type in (AT, ON, OFF, PCT):
|
||||||
|
# Raise an error if the previous spec is already
|
||||||
|
# concrete (assigned by hash)
|
||||||
|
if specs and specs[-1]._hash:
|
||||||
|
raise RedundantSpecError(specs[-1],
|
||||||
|
'compiler, version, '
|
||||||
|
'or variant')
|
||||||
specs.append(self.spec(None))
|
specs.append(self.spec(None))
|
||||||
else:
|
else:
|
||||||
self.unexpected_token()
|
self.unexpected_token()
|
||||||
@ -2783,13 +2796,13 @@ def spec_by_hash(self):
|
|||||||
|
|
||||||
if len(matches) != 1:
|
if len(matches) != 1:
|
||||||
raise AmbiguousHashError(
|
raise AmbiguousHashError(
|
||||||
"Multiple packages specify hash %s." % self.token.value,
|
"Multiple packages specify hash beginning %s."
|
||||||
*matches)
|
% self.token.value, *matches)
|
||||||
|
|
||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
def spec(self, name):
|
def spec(self, name):
|
||||||
"""Parse a spec out of the input. If a spec is supplied, then initialize
|
"""Parse a spec out of the input. If a spec is supplied, initialize
|
||||||
and return it instead of creating a new one."""
|
and return it instead of creating a new one."""
|
||||||
if name:
|
if name:
|
||||||
spec_namespace, dot, spec_name = name.rpartition('.')
|
spec_namespace, dot, spec_name = name.rpartition('.')
|
||||||
@ -2823,16 +2836,6 @@ def spec(self, name):
|
|||||||
# unspecified or not.
|
# unspecified or not.
|
||||||
added_version = False
|
added_version = False
|
||||||
|
|
||||||
if self.previous and self.previous.value == DEP:
|
|
||||||
if self.accept(HASH):
|
|
||||||
spec.add_dependency(self.spec_by_hash())
|
|
||||||
else:
|
|
||||||
self.expect(ID)
|
|
||||||
if self.accept(EQ):
|
|
||||||
raise SpecParseError(spack.parse.ParseError(
|
|
||||||
"", "", "Expected dependency received anonymous spec"))
|
|
||||||
spec.add_dependency(self.spec(self.token.value))
|
|
||||||
|
|
||||||
while self.next:
|
while self.next:
|
||||||
if self.accept(AT):
|
if self.accept(AT):
|
||||||
vlist = self.version_list()
|
vlist = self.version_list()
|
||||||
@ -2858,13 +2861,25 @@ def spec(self, name):
|
|||||||
self.previous = None
|
self.previous = None
|
||||||
else:
|
else:
|
||||||
# We've found the start of a new spec. Go back to do_parse
|
# We've found the start of a new spec. Go back to do_parse
|
||||||
|
# and read this token again.
|
||||||
|
self.push_tokens([self.token])
|
||||||
|
self.previous = None
|
||||||
break
|
break
|
||||||
|
|
||||||
|
elif self.accept(HASH):
|
||||||
|
# Get spec by hash and confirm it matches what we already have
|
||||||
|
hash_spec = self.spec_by_hash()
|
||||||
|
if hash_spec.satisfies(spec):
|
||||||
|
spec = hash_spec
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise InvalidHashError(spec, hash_spec.dag_hash())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
# If there was no version in the spec, consier it an open range
|
# If there was no version in the spec, consier it an open range
|
||||||
if not added_version:
|
if not added_version and not spec._hash:
|
||||||
spec.versions = VersionList(':')
|
spec.versions = VersionList(':')
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
@ -3139,3 +3154,18 @@ def __init__(self, msg, *specs):
|
|||||||
super(AmbiguousHashError, self).__init__(msg)
|
super(AmbiguousHashError, self).__init__(msg)
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
print(' ', spec.format('$.$@$%@+$+$=$#'))
|
print(' ', spec.format('$.$@$%@+$+$=$#'))
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidHashError(SpecError):
|
||||||
|
def __init__(self, spec, hash):
|
||||||
|
super(InvalidHashError, self).__init__(
|
||||||
|
"The spec specified by %s does not match provided spec %s"
|
||||||
|
% (hash, spec))
|
||||||
|
|
||||||
|
|
||||||
|
class RedundantSpecError(SpecError):
|
||||||
|
def __init__(self, spec, addition):
|
||||||
|
super(RedundantSpecError, self).__init__(
|
||||||
|
"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))
|
||||||
|
@ -132,6 +132,13 @@ def test_package_names(self):
|
|||||||
self.check_parse("mvapich_foo")
|
self.check_parse("mvapich_foo")
|
||||||
self.check_parse("_mvapich_foo")
|
self.check_parse("_mvapich_foo")
|
||||||
|
|
||||||
|
def test_anonymous_specs(self):
|
||||||
|
self.check_parse("%intel")
|
||||||
|
self.check_parse("@2.7")
|
||||||
|
self.check_parse("^zlib")
|
||||||
|
self.check_parse("+foo")
|
||||||
|
self.check_parse("arch=test-None-None", "platform=test")
|
||||||
|
|
||||||
def test_simple_dependence(self):
|
def test_simple_dependence(self):
|
||||||
self.check_parse("openmpi^hwloc")
|
self.check_parse("openmpi^hwloc")
|
||||||
self.check_parse("openmpi^hwloc^libunwind")
|
self.check_parse("openmpi^hwloc^libunwind")
|
||||||
@ -218,6 +225,115 @@ def test_parse_errors(self):
|
|||||||
errors = ['x@@1.2', 'x ^y@@1.2', 'x@1.2::', 'x::']
|
errors = ['x@@1.2', 'x ^y@@1.2', 'x@1.2::', 'x::']
|
||||||
self._check_raises(SpecParseError, errors)
|
self._check_raises(SpecParseError, errors)
|
||||||
|
|
||||||
|
def test_spec_by_hash(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is still the shape we expect
|
||||||
|
assert len(specs) > 3
|
||||||
|
|
||||||
|
self.check_parse(str(specs[0]), '/' + hashes[0])
|
||||||
|
self.check_parse(str(specs[1]), '/ ' + hashes[1][:5])
|
||||||
|
self.check_parse(str(specs[2]), specs[2].name + '/' + hashes[2])
|
||||||
|
self.check_parse(str(specs[3]),
|
||||||
|
specs[3].name + '@' + str(specs[3].version) +
|
||||||
|
' /' + hashes[3][:6])
|
||||||
|
|
||||||
|
def test_dep_spec_by_hash(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is still the shape we expect
|
||||||
|
assert len(specs) > 10
|
||||||
|
assert specs[4].name in specs[10]
|
||||||
|
assert specs[-1].name in specs[10]
|
||||||
|
|
||||||
|
spec1 = sp.Spec(specs[10].name + '^/' + hashes[4])
|
||||||
|
assert specs[4].name in spec1 and spec1[specs[4].name] == specs[4]
|
||||||
|
spec2 = sp.Spec(specs[10].name + '%' + str(specs[10].compiler) +
|
||||||
|
' ^ / ' + hashes[-1])
|
||||||
|
assert (specs[-1].name in spec2 and
|
||||||
|
spec2[specs[-1].name] == specs[-1] and
|
||||||
|
spec2.compiler == specs[10].compiler)
|
||||||
|
spec3 = sp.Spec(specs[10].name + '^/' + hashes[4][:4] +
|
||||||
|
'^ / ' + hashes[-1][:5])
|
||||||
|
assert (specs[-1].name in spec3 and
|
||||||
|
spec3[specs[-1].name] == specs[-1] and
|
||||||
|
specs[4].name in spec3 and spec3[specs[4].name] == specs[4])
|
||||||
|
|
||||||
|
def test_multiple_specs_with_hash(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
assert len(specs) > 3
|
||||||
|
|
||||||
|
output = sp.parse(specs[0].name + '/' + hashes[0] + '/' + hashes[1])
|
||||||
|
assert len(output) == 2
|
||||||
|
output = sp.parse('/' + hashes[0] + '/' + hashes[1])
|
||||||
|
assert len(output) == 2
|
||||||
|
output = sp.parse('/' + hashes[0] + '/' + hashes[1] +
|
||||||
|
' ' + specs[2].name)
|
||||||
|
assert len(output) == 3
|
||||||
|
output = sp.parse('/' + hashes[0] +
|
||||||
|
' ' + specs[1].name + ' ' + specs[2].name)
|
||||||
|
assert len(output) == 3
|
||||||
|
output = sp.parse('/' + hashes[0] + ' ' +
|
||||||
|
specs[1].name + ' / ' + hashes[1])
|
||||||
|
assert len(output) == 2
|
||||||
|
|
||||||
|
def test_ambiguous_hash(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is as expected
|
||||||
|
assert hashes[1][:1] == hashes[2][:1] == 'b'
|
||||||
|
|
||||||
|
ambiguous_hashes = ['/b',
|
||||||
|
specs[1].name + '/b',
|
||||||
|
specs[0].name + '^/b',
|
||||||
|
specs[0].name + '^' + specs[1].name + '/b']
|
||||||
|
self._check_raises(AmbiguousHashError, ambiguous_hashes)
|
||||||
|
|
||||||
|
def test_invalid_hash(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is as expected
|
||||||
|
assert (hashes[0] != hashes[3] and
|
||||||
|
hashes[1] != hashes[4] and len(specs) > 4)
|
||||||
|
|
||||||
|
inputs = [specs[0].name + '/' + hashes[3],
|
||||||
|
specs[1].name + '^' + specs[4].name + '/' + hashes[0],
|
||||||
|
specs[1].name + '^' + specs[4].name + '/' + hashes[1]]
|
||||||
|
self._check_raises(InvalidHashError, inputs)
|
||||||
|
|
||||||
|
def test_nonexistent_hash(self, database):
|
||||||
|
# This test uses database to make sure we don't accidentally access
|
||||||
|
# real installs, however unlikely
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is as expected
|
||||||
|
assert 'abc123' not in [h[:6] for h in hashes]
|
||||||
|
|
||||||
|
nonexistant_hashes = ['/abc123',
|
||||||
|
specs[0].name + '/abc123']
|
||||||
|
self._check_raises(SystemExit, nonexistant_hashes)
|
||||||
|
|
||||||
|
def test_redundant_spec(self, database):
|
||||||
|
specs = database.mock.db.query()
|
||||||
|
hashes = [s._hash for s in specs] # Preserves order of elements
|
||||||
|
|
||||||
|
# Make sure the database is as expected
|
||||||
|
assert len(specs) > 3
|
||||||
|
|
||||||
|
redundant_specs = ['/' + hashes[0] + '%' + str(specs[0].compiler),
|
||||||
|
specs[1].name + '/' + hashes[1] +
|
||||||
|
'@' + str(specs[1].version),
|
||||||
|
specs[2].name + '/' + hashes[2] + '^ libelf',
|
||||||
|
'/' + hashes[3] + ' cflags="-O3 -fPIC"']
|
||||||
|
self._check_raises(RedundantSpecError, redundant_specs)
|
||||||
|
|
||||||
def test_duplicate_variant(self):
|
def test_duplicate_variant(self):
|
||||||
duplicates = [
|
duplicates = [
|
||||||
'x@1.2+debug+debug',
|
'x@1.2+debug+debug',
|
||||||
|
Loading…
Reference in New Issue
Block a user