Signed-off-by: Gregory Becker <becker33@llnl.gov>
This commit is contained in:
Gregory Becker 2025-04-16 18:15:14 -07:00
parent f8538a1b1c
commit 02513eae7e
No known key found for this signature in database
GPG Key ID: 2362541F6D14ED84
3 changed files with 135 additions and 58 deletions

View File

@ -1877,6 +1877,73 @@ def _get_condition_id(
return cond_id return cond_id
def condition_clauses(
self,
required_spec: spack.spec.Spec,
imposed_spec: Optional[spack.spec.Spec] = None,
*,
required_name: Optional[str] = None,
imposed_name: Optional[str] = None,
msg: Optional[str] = None,
context: Optional[ConditionContext] = None,
):
"""Generate facts for a dependency or virtual provider condition.
Arguments:
required_spec: the constraints that triggers this condition
imposed_spec: the constraints that are imposed when this condition is triggered
required_name: name for ``required_spec``
(required if required_spec is anonymous, ignored if not)
imposed_name: name for ``imposed_spec``
(required if imposed_spec is anonymous, ignored if not)
msg: description of the condition
context: if provided, indicates how to modify the clause-sets for the required/imposed
specs based on the type of constraint they are generated for (e.g. `depends_on`)
Returns:
int: id of the condition created by this function
"""
clauses = []
required_name = required_spec.name or required_name
if not required_name:
raise ValueError(f"Must provide a name for anonymous condition: '{required_spec}'")
if not context:
context = ConditionContext()
context.transform_imposed = remove_facts("node", "virtual_node")
if imposed_spec:
imposed_name = imposed_spec.name or imposed_name
if not imposed_name:
raise ValueError(f"Must provide a name for imposed constraint: '{imposed_spec}'")
with named_spec(required_spec, required_name), named_spec(imposed_spec, imposed_name):
# Check if we can emit the requirements before updating the condition ID counter.
# In this way, if a condition can't be emitted but the exception is handled in the
# caller, we won't emit partial facts.
condition_id = next(self._id_counter)
requirement_context = context.requirement_context()
trigger_id = self._get_condition_id(
required_spec, cache=self._trigger_cache, body=True, context=requirement_context
)
clauses.append(fn.pkg_fact(required_spec.name, fn.condition(condition_id)))
clauses.append(fn.condition_reason(condition_id, msg))
clauses.append(
fn.pkg_fact(required_spec.name, fn.condition_trigger(condition_id, trigger_id))
)
if not imposed_spec:
return clauses, condition_id
impose_context = context.impose_context()
effect_id = self._get_condition_id(
imposed_spec, cache=self._effect_cache, body=False, context=impose_context
)
clauses.append(
fn.pkg_fact(required_spec.name, fn.condition_effect(condition_id, effect_id))
)
return clauses, condition_id
def condition( def condition(
self, self,
required_spec: spack.spec.Spec, required_spec: spack.spec.Spec,
@ -1902,46 +1969,18 @@ def condition(
Returns: Returns:
int: id of the condition created by this function int: id of the condition created by this function
""" """
required_name = required_spec.name or required_name clauses, condition_id = self.condition_clauses(
if not required_name: required_spec=required_spec,
raise ValueError(f"Must provide a name for anonymous condition: '{required_spec}'") imposed_spec=imposed_spec,
required_name=required_name,
imposed_name=imposed_name,
msg=msg,
context=context,
)
for clause in clauses:
self.gen.fact(clause)
if not context: return condition_id
context = ConditionContext()
context.transform_imposed = remove_facts("node", "virtual_node")
if imposed_spec:
imposed_name = imposed_spec.name or imposed_name
if not imposed_name:
raise ValueError(f"Must provide a name for imposed constraint: '{imposed_spec}'")
with named_spec(required_spec, required_name), named_spec(imposed_spec, imposed_name):
# Check if we can emit the requirements before updating the condition ID counter.
# In this way, if a condition can't be emitted but the exception is handled in the
# caller, we won't emit partial facts.
condition_id = next(self._id_counter)
requirement_context = context.requirement_context()
trigger_id = self._get_condition_id(
required_spec, cache=self._trigger_cache, body=True, context=requirement_context
)
self.gen.fact(fn.pkg_fact(required_spec.name, fn.condition(condition_id)))
self.gen.fact(fn.condition_reason(condition_id, msg))
self.gen.fact(
fn.pkg_fact(required_spec.name, fn.condition_trigger(condition_id, trigger_id))
)
if not imposed_spec:
return condition_id
impose_context = context.impose_context()
effect_id = self._get_condition_id(
imposed_spec, cache=self._effect_cache, body=False, context=impose_context
)
self.gen.fact(
fn.pkg_fact(required_spec.name, fn.condition_effect(condition_id, effect_id))
)
return condition_id
def impose(self, condition_id, imposed_spec, node=True, body=False): def impose(self, condition_id, imposed_spec, node=True, body=False):
imposed_constraints = self.spec_clauses(imposed_spec, body=body) imposed_constraints = self.spec_clauses(imposed_spec, body=body)
@ -2635,13 +2674,22 @@ def _spec_clauses(
# if it's concrete, then the hashes above take care of dependency # if it's concrete, then the hashes above take care of dependency
# constraints, but expand the hashes if asked for. # constraints, but expand the hashes if asked for.
if not spec.concrete or expand_hashes: if not spec.concrete or expand_hashes:
dependency_clauses = self._spec_clauses( if dspec.when != spack.spec.Spec():
dep, # Are the non-attr things issued here a problem?
body=body, dependency_clauses, _ = self.condition_clauses(
expand_hashes=expand_hashes, required_spec=dspec.when,
concrete_build_deps=concrete_build_deps, imposed_spec=dep,
context=context, required_name=dspec.when.name or spec.name,
) msg=f"{spec.name} depends conditionally on {dep.name}",
)
else:
dependency_clauses = self._spec_clauses(
dep,
body=body,
expand_hashes=expand_hashes,
concrete_build_deps=concrete_build_deps,
context=context,
)
if dspec.depflag == dt.BUILD: if dspec.depflag == dt.BUILD:
clauses.append(fn.attr("depends_on", spec.name, dep.name, "build")) clauses.append(fn.attr("depends_on", spec.name, dep.name, "build"))
if body is False: if body is False:

View File

@ -720,7 +720,7 @@ class DependencySpec:
virtuals: virtual packages provided from child to parent node. virtuals: virtual packages provided from child to parent node.
""" """
__slots__ = "parent", "spec", "depflag", "virtuals", "direct" __slots__ = "parent", "spec", "depflag", "virtuals", "direct", "when"
def __init__( def __init__(
self, self,
@ -730,12 +730,14 @@ def __init__(
depflag: dt.DepFlag, depflag: dt.DepFlag,
virtuals: Tuple[str, ...], virtuals: Tuple[str, ...],
direct: bool = False, direct: bool = False,
when: Optional["Spec"] = None,
): ):
self.parent = parent self.parent = parent
self.spec = spec self.spec = spec
self.depflag = depflag self.depflag = depflag
self.virtuals = tuple(sorted(set(virtuals))) self.virtuals = tuple(sorted(set(virtuals)))
self.direct = direct self.direct = direct
self.when = when or Spec()
def update_deptypes(self, depflag: dt.DepFlag) -> bool: def update_deptypes(self, depflag: dt.DepFlag) -> bool:
"""Update the current dependency types""" """Update the current dependency types"""
@ -766,6 +768,7 @@ def copy(self) -> "DependencySpec":
depflag=self.depflag, depflag=self.depflag,
virtuals=self.virtuals, virtuals=self.virtuals,
direct=self.direct, direct=self.direct,
when=self.when,
) )
def _cmp_iter(self): def _cmp_iter(self):
@ -777,10 +780,13 @@ def _cmp_iter(self):
def __str__(self) -> str: def __str__(self) -> str:
parent = self.parent.name if self.parent else None parent = self.parent.name if self.parent else None
child = self.spec.name if self.spec else None child = self.spec.name if self.spec else None
return f"{parent} {self.depflag}[virtuals={','.join(self.virtuals)}] --> {child}" virtuals_string = f"virtuals={','.join(self.virtuals)}" if self.virtuals else ""
when_string = f"when={self.when}" if self.when != Spec() else ""
edge_attrs = filter((virtuals_string, when_string), lambda x: bool(x))
return f"{parent} {self.depflag}[{' '.join(edge_attrs)}] --> {child}"
def flip(self) -> "DependencySpec": def flip(self) -> "DependencySpec":
"""Flip the dependency, and drop virtual information""" """Flip the dependency, and drop virtual and conditional information"""
return DependencySpec( return DependencySpec(
parent=self.spec, spec=self.parent, depflag=self.depflag, virtuals=() parent=self.spec, spec=self.parent, depflag=self.depflag, virtuals=()
) )
@ -1752,7 +1758,13 @@ def _set_architecture(self, **kwargs):
setattr(self.architecture, new_attr, new_value) setattr(self.architecture, new_attr, new_value)
def _add_dependency( def _add_dependency(
self, spec: "Spec", *, depflag: dt.DepFlag, virtuals: Tuple[str, ...], direct: bool = False self,
spec: "Spec",
*,
depflag: dt.DepFlag,
virtuals: Tuple[str, ...],
direct: bool = False,
when: Optional["Spec"] = None,
): ):
"""Called by the parser to add another spec as a dependency. """Called by the parser to add another spec as a dependency.
@ -1760,20 +1772,25 @@ def _add_dependency(
depflag: dependency type for this edge depflag: dependency type for this edge
virtuals: virtuals on this edge virtuals: virtuals on this edge
direct: if True denotes a direct dependency (associated with the % sigil) direct: if True denotes a direct dependency (associated with the % sigil)
when: if non-None, condition under which dependency holds
""" """
if spec.name not in self._dependencies or not spec.name: if spec.name not in self._dependencies or not spec.name:
self.add_dependency_edge(spec, depflag=depflag, virtuals=virtuals, direct=direct) self.add_dependency_edge(
spec, depflag=depflag, virtuals=virtuals, direct=direct, when=when
)
return return
# Keep the intersection of constraints when a dependency is added multiple times with # Keep the intersection of constraints when a dependency is added multiple times with
# the same deptype. Add a new dependency if it is added with a compatible deptype # the same deptype. Add a new dependency if it is added with a compatible deptype
# (for example, a build-only dependency is compatible with a link-only dependenyc). # (for example, a build-only dependency is compatible with a link-only dependency).
# The only restrictions, currently, are that we cannot add edges with overlapping # The only restrictions, currently, are that we cannot add edges with overlapping
# dependency types and we cannot add multiple edges that have link/run dependency types. # dependency types and we cannot add multiple edges that have link/run dependency types.
# See ``spack.deptypes.compatible``. # See ``spack.deptypes.compatible``.
orig = self._dependencies[spec.name] orig = self._dependencies[spec.name]
try: try:
dspec = next(dspec for dspec in orig if depflag == dspec.depflag) dspec = next(
dspec for dspec in orig if depflag == dspec.depflag and when == dspec.when
)
except StopIteration: except StopIteration:
# Error if we have overlapping or incompatible deptypes # Error if we have overlapping or incompatible deptypes
if any(not dt.compatible(dspec.depflag, depflag) for dspec in orig): if any(not dt.compatible(dspec.depflag, depflag) for dspec in orig):
@ -1785,7 +1802,9 @@ def _add_dependency(
f"\t'{str(self)}' cannot depend on '{required_dep_str}'" f"\t'{str(self)}' cannot depend on '{required_dep_str}'"
) )
self.add_dependency_edge(spec, depflag=depflag, virtuals=virtuals, direct=direct) self.add_dependency_edge(
spec, depflag=depflag, virtuals=virtuals, direct=direct, when=when
)
return return
try: try:
@ -1803,6 +1822,7 @@ def add_dependency_edge(
depflag: dt.DepFlag, depflag: dt.DepFlag,
virtuals: Tuple[str, ...], virtuals: Tuple[str, ...],
direct: bool = False, direct: bool = False,
when: Optional["Spec"] = None,
): ):
"""Add a dependency edge to this spec. """Add a dependency edge to this spec.
@ -1811,6 +1831,7 @@ def add_dependency_edge(
deptypes: dependency types for this edge deptypes: dependency types for this edge
virtuals: virtuals provided by this edge virtuals: virtuals provided by this edge
direct: if True denotes a direct dependency direct: if True denotes a direct dependency
when: if non-None, condition under which dependency holds
""" """
# Check if we need to update edges that are already present # Check if we need to update edges that are already present
selected = self._dependencies.select(child=dependency_spec.name) selected = self._dependencies.select(child=dependency_spec.name)
@ -1818,6 +1839,9 @@ def add_dependency_edge(
has_errors, details = False, [] has_errors, details = False, []
msg = f"cannot update the edge from {edge.parent.name} to {edge.spec.name}" msg = f"cannot update the edge from {edge.parent.name} to {edge.spec.name}"
if edge.when != when:
continue
# If the dependency is to an existing spec, we can update dependency # If the dependency is to an existing spec, we can update dependency
# types. If it is to a new object, check deptype compatibility. # types. If it is to a new object, check deptype compatibility.
if id(edge.spec) != id(dependency_spec) and not dt.compatible(edge.depflag, depflag): if id(edge.spec) != id(dependency_spec) and not dt.compatible(edge.depflag, depflag):
@ -1841,7 +1865,7 @@ def add_dependency_edge(
raise spack.error.SpecError(msg, "\n".join(details)) raise spack.error.SpecError(msg, "\n".join(details))
for edge in selected: for edge in selected:
if id(dependency_spec) == id(edge.spec): if id(dependency_spec) == id(edge.spec) and edge.when == when:
# If we are here, it means the edge object was previously added to # If we are here, it means the edge object was previously added to
# both the parent and the child. When we update this object they'll # both the parent and the child. When we update this object they'll
# both see the deptype modification. # both see the deptype modification.
@ -1850,7 +1874,7 @@ def add_dependency_edge(
return return
edge = DependencySpec( edge = DependencySpec(
self, dependency_spec, depflag=depflag, virtuals=virtuals, direct=direct self, dependency_spec, depflag=depflag, virtuals=virtuals, direct=direct, when=when
) )
self._dependencies.add(edge) self._dependencies.add(edge)
dependency_spec._dependents.add(edge) dependency_spec._dependents.add(edge)

View File

@ -511,10 +511,10 @@ def parse(self):
name = name[:-1] name = name[:-1]
value = value.strip("'\" ").split(",") value = value.strip("'\" ").split(",")
attributes[name] = value attributes[name] = value
if name not in ("deptypes", "virtuals"): if name not in ("deptypes", "virtuals", "when"):
msg = ( msg = (
"the only edge attributes that are currently accepted " "the only edge attributes that are currently accepted "
'are "deptypes" and "virtuals"' 'are "deptypes", "virtuals", and "when"'
) )
raise SpecParsingError(msg, self.ctx.current_token, self.literal_str) raise SpecParsingError(msg, self.ctx.current_token, self.literal_str)
# TODO: Add code to accept bool variants here as soon as use variants are implemented # TODO: Add code to accept bool variants here as soon as use variants are implemented
@ -528,6 +528,11 @@ def parse(self):
if "deptypes" in attributes: if "deptypes" in attributes:
deptype_string = attributes.pop("deptypes") deptype_string = attributes.pop("deptypes")
attributes["depflag"] = spack.deptypes.canonicalize(deptype_string) attributes["depflag"] = spack.deptypes.canonicalize(deptype_string)
# Turn "when" into a spec
if "when" in attributes:
attributes["when"] = spack.spec.Spec(attributes["when"][0])
return attributes return attributes