more-flexible user-specified dependency constraints (#8162)
* allow user to constrain dependencies that are added conditionally * remove check for not-visited deps from normalize, move it to concretize. The check now runs after the concretization loop completes (so an error is only reported if the user-mentioned spec doesnt appear anywhere in the dag) * remove separate full_spec_deps variable; rename spec_deps to all_spec_deps to clarify that it merges user-specified dependencies with derived dependencies * add unit test to confirm new functionality
This commit is contained in:
parent
55e42dca2c
commit
43114c2e06
@ -1833,13 +1833,26 @@ def concretize(self, tests=False):
|
|||||||
changed = True
|
changed = True
|
||||||
force = False
|
force = False
|
||||||
|
|
||||||
|
user_spec_deps = self.flat_dependencies(copy=False)
|
||||||
|
|
||||||
while changed:
|
while changed:
|
||||||
changes = (self.normalize(force, tests),
|
changes = (self.normalize(force, tests=tests,
|
||||||
|
user_spec_deps=user_spec_deps),
|
||||||
self._expand_virtual_packages(),
|
self._expand_virtual_packages(),
|
||||||
self._concretize_helper())
|
self._concretize_helper())
|
||||||
changed = any(changes)
|
changed = any(changes)
|
||||||
force = True
|
force = True
|
||||||
|
|
||||||
|
visited_user_specs = set()
|
||||||
|
for dep in self.traverse():
|
||||||
|
visited_user_specs.add(dep.name)
|
||||||
|
visited_user_specs.update(x.name for x in dep.package.provided)
|
||||||
|
|
||||||
|
extra = set(user_spec_deps.keys()).difference(visited_user_specs)
|
||||||
|
if extra:
|
||||||
|
raise InvalidDependencyError(
|
||||||
|
self.name + " does not depend on " + comma_or(extra))
|
||||||
|
|
||||||
for s in self.traverse():
|
for s in self.traverse():
|
||||||
# After concretizing, assign namespaces to anything left.
|
# After concretizing, assign namespaces to anything left.
|
||||||
# Note that this doesn't count as a "change". The repository
|
# Note that this doesn't count as a "change". The repository
|
||||||
@ -2190,7 +2203,7 @@ def _normalize_helper(self, visited, spec_deps, provider_index, tests):
|
|||||||
|
|
||||||
return any_change
|
return any_change
|
||||||
|
|
||||||
def normalize(self, force=False, tests=False):
|
def normalize(self, force=False, tests=False, user_spec_deps=None):
|
||||||
"""When specs are parsed, any dependencies specified are hanging off
|
"""When specs are parsed, any dependencies specified are hanging off
|
||||||
the root, and ONLY the ones that were explicitly provided are there.
|
the root, and ONLY the ones that were explicitly provided are there.
|
||||||
Normalization turns a partial flat spec into a DAG, where:
|
Normalization turns a partial flat spec into a DAG, where:
|
||||||
@ -2219,27 +2232,34 @@ def normalize(self, force=False, tests=False):
|
|||||||
|
|
||||||
# Ensure first that all packages & compilers in the DAG exist.
|
# Ensure first that all packages & compilers in the DAG exist.
|
||||||
self.validate_or_raise()
|
self.validate_or_raise()
|
||||||
# Get all the dependencies into one DependencyMap
|
# Clear the DAG and collect all dependencies in the DAG, which will be
|
||||||
spec_deps = self.flat_dependencies(copy=False)
|
# reapplied as constraints. All dependencies collected this way will
|
||||||
|
# have been created by a previous execution of 'normalize'.
|
||||||
|
# A dependency extracted here will only be reintegrated if it is
|
||||||
|
# discovered to apply according to _normalize_helper, so
|
||||||
|
# user-specified dependencies are recorded separately in case they
|
||||||
|
# refer to specs which take several normalization passes to
|
||||||
|
# materialize.
|
||||||
|
all_spec_deps = self.flat_dependencies(copy=False)
|
||||||
|
|
||||||
|
if user_spec_deps:
|
||||||
|
for name, spec in user_spec_deps.items():
|
||||||
|
if name not in all_spec_deps:
|
||||||
|
all_spec_deps[name] = spec
|
||||||
|
else:
|
||||||
|
all_spec_deps[name].constrain(spec)
|
||||||
|
|
||||||
# Initialize index of virtual dependency providers if
|
# Initialize index of virtual dependency providers if
|
||||||
# concretize didn't pass us one already
|
# concretize didn't pass us one already
|
||||||
provider_index = ProviderIndex(
|
provider_index = ProviderIndex(
|
||||||
[s for s in spec_deps.values()], restrict=True)
|
[s for s in all_spec_deps.values()], restrict=True)
|
||||||
|
|
||||||
# traverse the package DAG and fill out dependencies according
|
# traverse the package DAG and fill out dependencies according
|
||||||
# to package files & their 'when' specs
|
# to package files & their 'when' specs
|
||||||
visited = set()
|
visited = set()
|
||||||
|
|
||||||
any_change = self._normalize_helper(
|
any_change = self._normalize_helper(
|
||||||
visited, spec_deps, provider_index, tests)
|
visited, all_spec_deps, provider_index, tests)
|
||||||
|
|
||||||
# If there are deps specified but not visited, they're not
|
|
||||||
# actually deps of this package. Raise an error.
|
|
||||||
extra = set(spec_deps.keys()).difference(visited)
|
|
||||||
if extra:
|
|
||||||
raise InvalidDependencyError(
|
|
||||||
self.name + " does not depend on " + comma_or(extra))
|
|
||||||
|
|
||||||
# Mark the spec as normal once done.
|
# Mark the spec as normal once done.
|
||||||
self._normal = True
|
self._normal = True
|
||||||
|
@ -611,7 +611,11 @@ def __init__(self, name, dependencies, dependency_types, conditions=None,
|
|||||||
if not conditions or dep.name not in conditions:
|
if not conditions or dep.name not in conditions:
|
||||||
self.dependencies[dep.name] = {Spec(name): d}
|
self.dependencies[dep.name] = {Spec(name): d}
|
||||||
else:
|
else:
|
||||||
self.dependencies[dep.name] = {Spec(conditions[dep.name]): d}
|
dep_conditions = conditions[dep.name]
|
||||||
|
dep_conditions = dict(
|
||||||
|
(Spec(x), Dependency(self, Spec(y), type=dtype))
|
||||||
|
for x, y in dep_conditions.items())
|
||||||
|
self.dependencies[dep.name] = dep_conditions
|
||||||
|
|
||||||
if versions:
|
if versions:
|
||||||
self.versions = versions
|
self.versions = versions
|
||||||
|
@ -102,6 +102,44 @@ def test_test_deptype():
|
|||||||
assert ('z' not in spec)
|
assert ('z' not in spec)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('config')
|
||||||
|
def test_conditional_dep_with_user_constraints():
|
||||||
|
"""This sets up packages X->Y such that X depends on Y conditionally. It
|
||||||
|
then constructs a Spec with X but with no constraints on X, so that the
|
||||||
|
initial normalization pass cannot determine whether the constraints are
|
||||||
|
met to add the dependency; this checks whether a user-specified constraint
|
||||||
|
on Y is applied properly.
|
||||||
|
"""
|
||||||
|
default = ('build', 'link')
|
||||||
|
|
||||||
|
y = MockPackage('y', [], [])
|
||||||
|
x_on_y_conditions = {
|
||||||
|
y.name: {
|
||||||
|
'x@2:': 'y'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x = MockPackage('x', [y], [default], conditions=x_on_y_conditions)
|
||||||
|
|
||||||
|
mock_repo = MockPackageMultiRepo([x, y])
|
||||||
|
with spack.repo.swap(mock_repo):
|
||||||
|
spec = Spec('x ^y@2')
|
||||||
|
spec.concretize()
|
||||||
|
|
||||||
|
assert ('y@2' in spec)
|
||||||
|
|
||||||
|
with spack.repo.swap(mock_repo):
|
||||||
|
spec = Spec('x@1')
|
||||||
|
spec.concretize()
|
||||||
|
|
||||||
|
assert ('y' not in spec)
|
||||||
|
|
||||||
|
with spack.repo.swap(mock_repo):
|
||||||
|
spec = Spec('x')
|
||||||
|
spec.concretize()
|
||||||
|
|
||||||
|
assert ('y@3' in spec)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('mutable_mock_packages')
|
@pytest.mark.usefixtures('mutable_mock_packages')
|
||||||
class TestSpecDag(object):
|
class TestSpecDag(object):
|
||||||
|
|
||||||
@ -300,18 +338,19 @@ def test_unsatisfiable_architecture(self, set_dependency):
|
|||||||
with pytest.raises(spack.spec.UnsatisfiableArchitectureSpecError):
|
with pytest.raises(spack.spec.UnsatisfiableArchitectureSpecError):
|
||||||
spec.normalize()
|
spec.normalize()
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('config')
|
||||||
def test_invalid_dep(self):
|
def test_invalid_dep(self):
|
||||||
spec = Spec('libelf ^mpich')
|
spec = Spec('libelf ^mpich')
|
||||||
with pytest.raises(spack.spec.InvalidDependencyError):
|
with pytest.raises(spack.spec.InvalidDependencyError):
|
||||||
spec.normalize()
|
spec.concretize()
|
||||||
|
|
||||||
spec = Spec('libelf ^libdwarf')
|
spec = Spec('libelf ^libdwarf')
|
||||||
with pytest.raises(spack.spec.InvalidDependencyError):
|
with pytest.raises(spack.spec.InvalidDependencyError):
|
||||||
spec.normalize()
|
spec.concretize()
|
||||||
|
|
||||||
spec = Spec('mpich ^dyninst ^libelf')
|
spec = Spec('mpich ^dyninst ^libelf')
|
||||||
with pytest.raises(spack.spec.InvalidDependencyError):
|
with pytest.raises(spack.spec.InvalidDependencyError):
|
||||||
spec.normalize()
|
spec.concretize()
|
||||||
|
|
||||||
def test_equal(self):
|
def test_equal(self):
|
||||||
# Different spec structures to test for equality
|
# Different spec structures to test for equality
|
||||||
|
Loading…
Reference in New Issue
Block a user