summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/solver/asp.py319
-rw-r--r--lib/spack/spack/solver/concretize.lp60
-rw-r--r--lib/spack/spack/solver/core.py14
-rw-r--r--lib/spack/spack/solver/display.lp1
-rw-r--r--lib/spack/spack/spec.py94
-rw-r--r--lib/spack/spack/test/concretize.py15
-rw-r--r--lib/spack/spack/test/flag_mixing.py333
-rw-r--r--lib/spack/spack/test/spec_semantics.py70
8 files changed, 732 insertions, 174 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index b8d9eea037..2d053721b1 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -1022,6 +1022,102 @@ ConditionIdFunctionPair = Tuple[int, List[AspFunction]]
ConditionSpecCache = Dict[str, Dict[ConditionSpecKey, ConditionIdFunctionPair]]
+class ConstraintOrigin(enum.Enum):
+ """Generates identifiers that can be pased into the solver attached
+ to constraints, and then later retrieved to determine the origin of
+ those constraints when ``SpecBuilder`` creates Specs from the solve
+ result.
+ """
+
+ DEPENDS_ON = 1
+ REQUIRE = 2
+
+ @staticmethod
+ def _SUFFIXES() -> Dict["ConstraintOrigin", str]:
+ return {ConstraintOrigin.DEPENDS_ON: "_dep", ConstraintOrigin.REQUIRE: "_req"}
+
+ @staticmethod
+ def append_type_suffix(pkg_id: str, kind: "ConstraintOrigin") -> str:
+ """Given a package identifier and a constraint kind, generate a string ID."""
+ suffix = ConstraintOrigin._SUFFIXES()[kind]
+ return f"{pkg_id}{suffix}"
+
+ @staticmethod
+ def strip_type_suffix(source: str) -> Tuple[int, Optional[str]]:
+ """Take a combined package/type ID generated by
+ ``append_type_suffix``, and extract the package ID and
+ an associated weight.
+ """
+ if not source:
+ return -1, None
+ for kind, suffix in ConstraintOrigin._SUFFIXES().items():
+ if source.endswith(suffix):
+ return kind.value, source[: -len(suffix)]
+ return -1, source
+
+
+class SourceContext:
+ """Tracks context in which a Spec's clause-set is generated (i.e.
+ with ``SpackSolverSetup.spec_clauses``).
+
+ Facts generated for the spec may include this context.
+ """
+
+ def __init__(self):
+ # This can be "literal" for constraints that come from a user
+ # spec (e.g. from the command line); it can be the output of
+ # `ConstraintOrigin.append_type_suffix`; the default is "none"
+ # (which means it isn't important to keep track of the source
+ # in that case).
+ self.source = "none"
+
+
+class ConditionIdContext(SourceContext):
+ """Derived from a ``ConditionContext``: for clause-sets generated by
+ imposed/required specs, stores an associated transform.
+
+ This is primarily used for tracking whether we are generating clauses
+ in the context of a required spec, or for an imposed spec.
+
+ Is not a subclass of ``ConditionContext`` because it exists in a
+ lower-level context with less information.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.transform = None
+
+
+class ConditionContext(SourceContext):
+ """Tracks context in which a condition (i.e. ``SpackSolverSetup.condition``)
+ is generated (e.g. for a `depends_on`).
+
+ This may modify the required/imposed specs generated as relevant
+ for the context.
+ """
+
+ def __init__(self):
+ super().__init__()
+ # transformation applied to facts from the required spec. Defaults
+ # to leave facts as they are.
+ self.transform_required = None
+ # transformation applied to facts from the imposed spec. Defaults
+ # to removing "node" and "virtual_node" facts.
+ self.transform_imposed = None
+
+ def requirement_context(self) -> ConditionIdContext:
+ ctxt = ConditionIdContext()
+ ctxt.source = self.source
+ ctxt.transform = self.transform_required
+ return ctxt
+
+ def impose_context(self) -> ConditionIdContext:
+ ctxt = ConditionIdContext()
+ ctxt.source = self.source
+ ctxt.transform = self.transform_imposed
+ return ctxt
+
+
class SpackSolverSetup:
"""Class to set up and run a Spack concretization solve."""
@@ -1197,8 +1293,9 @@ class SpackSolverSetup:
if compiler.compiler_obj is not None:
c = compiler.compiler_obj
for flag_type, flags in c.flags.items():
+ flag_group = " ".join(flags)
for flag in flags:
- self.gen.fact(fn.compiler_flag(compiler_id, flag_type, flag))
+ self.gen.fact(fn.compiler_flag(compiler_id, flag_type, flag, flag_group))
if compiler.available:
self.gen.fact(fn.compiler_available(compiler_id))
@@ -1375,7 +1472,7 @@ class SpackSolverSetup:
named_cond: spack.spec.Spec,
cache: ConditionSpecCache,
body: bool,
- transform: Optional[TransformFunction] = None,
+ context: ConditionIdContext,
) -> int:
"""Get the id for one half of a condition (either a trigger or an imposed constraint).
@@ -1389,15 +1486,15 @@ class SpackSolverSetup:
"""
pkg_cache = cache[named_cond.name]
- named_cond_key = (str(named_cond), transform)
+ named_cond_key = (str(named_cond), context.transform)
result = pkg_cache.get(named_cond_key)
if result:
return result[0]
cond_id = next(self._id_counter)
- requirements = self.spec_clauses(named_cond, body=body)
- if transform:
- requirements = transform(named_cond, requirements)
+ requirements = self.spec_clauses(named_cond, body=body, context=context)
+ if context.transform:
+ requirements = context.transform(named_cond, requirements)
pkg_cache[named_cond_key] = (cond_id, requirements)
return cond_id
@@ -1408,8 +1505,7 @@ class SpackSolverSetup:
imposed_spec: Optional[spack.spec.Spec] = None,
name: Optional[str] = None,
msg: Optional[str] = None,
- transform_required: Optional[TransformFunction] = None,
- transform_imposed: Optional[TransformFunction] = remove_node,
+ context: Optional[ConditionContext] = None,
):
"""Generate facts for a dependency or virtual provider condition.
@@ -1418,10 +1514,8 @@ class SpackSolverSetup:
imposed_spec: the constraints that are imposed when this condition is triggered
name: name for `required_spec` (required if required_spec is anonymous, ignored if not)
msg: description of the condition
- transform_required: transformation applied to facts from the required spec. Defaults
- to leave facts as they are.
- transform_imposed: transformation applied to facts from the imposed spec. Defaults
- to removing "node" and "virtual_node" facts.
+ 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
"""
@@ -1429,14 +1523,19 @@ class SpackSolverSetup:
if not name:
raise ValueError(f"Must provide a name for anonymous condition: '{required_spec}'")
+ if not context:
+ context = ConditionContext()
+ context.transform_imposed = remove_node
+
with spec_with_name(required_spec, 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, transform=transform_required
+ 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))
@@ -1446,8 +1545,9 @@ class SpackSolverSetup:
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, transform=transform_imposed
+ 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))
@@ -1455,8 +1555,8 @@ class SpackSolverSetup:
return condition_id
- def impose(self, condition_id, imposed_spec, node=True, name=None, body=False):
- imposed_constraints = self.spec_clauses(imposed_spec, body=body, required_from=name)
+ def impose(self, condition_id, imposed_spec, node=True, body=False):
+ imposed_constraints = self.spec_clauses(imposed_spec, body=body)
for pred in imposed_constraints:
# imposed "node"-like conditions are no-ops
if not node and pred.args[0] in ("node", "virtual_node"):
@@ -1528,14 +1628,14 @@ class SpackSolverSetup:
if t & depflag
]
- self.condition(
- cond,
- dep.spec,
- name=pkg.name,
- msg=msg,
- transform_required=track_dependencies,
- transform_imposed=dependency_holds,
+ context = ConditionContext()
+ context.source = ConstraintOrigin.append_type_suffix(
+ pkg.name, ConstraintOrigin.DEPENDS_ON
)
+ context.transform_required = track_dependencies
+ context.transform_imposed = dependency_holds
+
+ self.condition(cond, dep.spec, name=pkg.name, msg=msg, context=context)
self.gen.newline()
@@ -1613,17 +1713,21 @@ class SpackSolverSetup:
when_spec = spack.spec.Spec(pkg_name)
try:
- # With virtual we want to emit "node" and "virtual_node" in imposed specs
- transform: Optional[TransformFunction] = remove_node
- if virtual:
- transform = None
+ context = ConditionContext()
+ context.source = ConstraintOrigin.append_type_suffix(
+ pkg_name, ConstraintOrigin.REQUIRE
+ )
+ if not virtual:
+ context.transform_imposed = remove_node
+ # else: for virtuals we want to emit "node" and
+ # "virtual_node" in imposed specs
member_id = self.condition(
required_spec=when_spec,
imposed_spec=spec,
name=pkg_name,
- transform_imposed=transform,
msg=f"{input_spec} is a requirement for package {pkg_name}",
+ context=context,
)
except Exception as e:
# Do not raise if the rule comes from the 'all' subsection, since usability
@@ -1722,7 +1826,9 @@ class SpackSolverSetup:
]
try:
- self.condition(spec, spec, msg=msg, transform_imposed=external_imposition)
+ context = ConditionContext()
+ context.transform_imposed = external_imposition
+ self.condition(spec, spec, msg=msg, context=context)
except (spack.error.SpecError, RuntimeError) as e:
warnings.warn(f"while setting up external spec {spec}: {e}")
continue
@@ -1797,6 +1903,7 @@ class SpackSolverSetup:
expand_hashes: bool = False,
concrete_build_deps=False,
required_from: Optional[str] = None,
+ context: Optional[SourceContext] = None,
) -> List[AspFunction]:
"""Wrap a call to `_spec_clauses()` into a try/except block with better error handling.
@@ -1812,6 +1919,7 @@ class SpackSolverSetup:
transitive=transitive,
expand_hashes=expand_hashes,
concrete_build_deps=concrete_build_deps,
+ context=context,
)
except RuntimeError as exc:
msg = str(exc)
@@ -1828,6 +1936,7 @@ class SpackSolverSetup:
transitive: bool = True,
expand_hashes: bool = False,
concrete_build_deps: bool = False,
+ context: Optional[SourceContext] = None,
) -> List[AspFunction]:
"""Return a list of clauses for a spec mandates are true.
@@ -1839,6 +1948,8 @@ class SpackSolverSetup:
expand_hashes: if True, descend into hashes of concrete specs (default False)
concrete_build_deps: if False, do not include pure build deps of concrete specs
(as they have no effect on runtime constraints)
+ context: tracks what constraint this clause set is generated for (e.g. a
+ `depends_on` constraint in a package.py file)
Normally, if called with ``transitive=True``, ``spec_clauses()`` just generates
hashes for the dependency requirements of concrete specs. If ``expand_hashes``
@@ -1925,13 +2036,19 @@ class SpackSolverSetup:
self.compiler_version_constraints.add(spec.compiler)
# compiler flags
+ source = context.source if context else "none"
for flag_type, flags in spec.compiler_flags.items():
+ flag_group = " ".join(flags)
for flag in flags:
- clauses.append(f.node_flag(spec.name, flag_type, flag))
+ clauses.append(
+ f.node_flag(spec.name, fn.node_flag(flag_type, flag, flag_group, source))
+ )
if not spec.concrete and flag.propagate is True:
clauses.append(
f.propagate(
- spec.name, fn.node_flag(flag_type, flag), fn.edge_types("link", "run")
+ spec.name,
+ fn.node_flag(flag_type, flag, flag_group, source),
+ fn.edge_types("link", "run"),
)
)
@@ -2013,6 +2130,7 @@ class SpackSolverSetup:
body=body,
expand_hashes=expand_hashes,
concrete_build_deps=concrete_build_deps,
+ context=context,
)
)
@@ -2648,7 +2766,9 @@ class SpackSolverSetup:
effect_id, requirements = cache[imposed_spec_key]
else:
effect_id = next(self._id_counter)
- requirements = self.spec_clauses(spec)
+ context = SourceContext()
+ context.source = "literal"
+ requirements = self.spec_clauses(spec, context=context)
root_name = spec.name
for clause in requirements:
clause_name = clause.args[0]
@@ -3405,11 +3525,10 @@ class SpecBuilder:
self._specs[node].compiler = spack.spec.CompilerSpec(compiler)
self._specs[node].compiler.versions = vn.VersionList([vn.Version(version)])
- def node_flag(self, node, flag_type, flag):
- self._specs[node].compiler_flags.add_flag(flag_type, flag, False)
-
- def node_flag_source(self, node, flag_type, source):
- self._flag_sources[(node, flag_type)].add(source)
+ def node_flag(self, node, node_flag):
+ self._specs[node].compiler_flags.add_flag(
+ node_flag.flag_type, node_flag.flag, False, node_flag.flag_group, node_flag.source
+ )
def external_spec_selected(self, node, idx):
"""This means that the external spec and index idx has been selected for this package."""
@@ -3450,15 +3569,23 @@ class SpecBuilder:
dependencies[0].update_virtuals((virtual,))
def reorder_flags(self):
- """Order compiler flags on specs in predefined order.
-
- We order flags so that any node's flags will take priority over
- those of its dependents. That is, the deepest node in the DAG's
- flags will appear last on the compile line, in the order they
- were specified.
+ """For each spec, determine the order of compiler flags applied to it.
The solver determines which flags are on nodes; this routine
- imposes order afterwards.
+ imposes order afterwards. The order is:
+
+ 1. Flags applied in compiler definitions should come first
+ 2. Flags applied by dependents are ordered topologically (with a
+ dependency on `traverse` to resolve the partial order into a
+ stable total order)
+ 3. Flags from requirements are then applied (requirements always
+ come from the package and never a parent)
+ 4. Command-line flags should come last
+
+ Additionally, for each source (requirements, compiler, command line, and
+ dependents), flags from that source should retain their order and grouping:
+ e.g. for `y cflags="-z -a"` "-z" and "-a" should never have any intervening
+ flags inserted, and should always appear in that order.
"""
# reverse compilers so we get highest priority compilers that share a spec
compilers = dict(
@@ -3473,40 +3600,78 @@ class SpecBuilder:
flagmap_from_compiler = compilers[spec.compiler].flags
for flag_type in spec.compiler_flags.valid_compiler_flags():
- from_compiler = flagmap_from_compiler.get(flag_type, [])
- from_sources = []
-
- # order is determined by the DAG. A spec's flags come after any of its ancestors
- # on the compile line
node = SpecBuilder.make_node(pkg=spec.name)
- source_key = (node, flag_type)
- if source_key in self._flag_sources:
- order = [
- SpecBuilder.make_node(pkg=s.name)
- for s in spec.traverse(order="post", direction="parents")
- ]
- sorted_sources = sorted(
- self._flag_sources[source_key], key=lambda s: order.index(s)
+
+ ordered_flags = []
+
+ # 1. Put compiler flags first
+ from_compiler = tuple(flagmap_from_compiler.get(flag_type, []))
+ extend_flag_list(ordered_flags, from_compiler)
+
+ # 2. Add all sources (the compiler is one of them, so skip any
+ # flag group that matches it exactly)
+ flag_groups = set()
+ for flag in self._specs[node].compiler_flags.get(flag_type, []):
+ flag_groups.add(
+ spack.spec.CompilerFlag(
+ flag.flag_group,
+ propagate=flag.propagate,
+ flag_group=flag.flag_group,
+ source=flag.source,
+ )
)
- # add flags from each source, lowest to highest precedence
- for node in sorted_sources:
- all_src_flags = list()
- per_pkg_sources = [self._specs[node]]
- if node.pkg in cmd_specs:
- per_pkg_sources.append(cmd_specs[node.pkg])
- for source in per_pkg_sources:
- all_src_flags.extend(source.compiler_flags.get(flag_type, []))
- extend_flag_list(from_sources, all_src_flags)
-
- # compiler flags from compilers config are lowest precedence
- ordered_compiler_flags = list(llnl.util.lang.dedupe(from_compiler + from_sources))
- compiler_flags = spec.compiler_flags.get(flag_type, [])
+ # For flags that are applied by dependents, put flags from parents
+ # before children; we depend on the stability of traverse() to
+ # achieve a stable flag order for flags introduced in this manner.
+ topo_order = list(s.name for s in spec.traverse(order="post", direction="parents"))
+ lex_order = list(sorted(flag_groups))
+
+ def _order_index(flag_group):
+ source = flag_group.source
+ # Note: if 'require: ^dependency cflags=...' is ever possible,
+ # this will topologically sort for require as well
+ type_index, pkg_source = ConstraintOrigin.strip_type_suffix(source)
+ if pkg_source in topo_order:
+ major_index = topo_order.index(pkg_source)
+ # If for x->y, x has multiple depends_on declarations that
+ # are activated, and each adds cflags to y, we fall back on
+ # alphabetical ordering to maintain a total order
+ minor_index = lex_order.index(flag_group)
+ else:
+ major_index = len(topo_order) + lex_order.index(flag_group)
+ minor_index = 0
+ return (type_index, major_index, minor_index)
- msg = f"{set(compiler_flags)} does not equal {set(ordered_compiler_flags)}"
- assert set(compiler_flags) == set(ordered_compiler_flags), msg
+ prioritized_groups = sorted(flag_groups, key=lambda x: _order_index(x))
- spec.compiler_flags.update({flag_type: ordered_compiler_flags})
+ for grp in prioritized_groups:
+ grp_flags = tuple(
+ x for (x, y) in spack.compiler.tokenize_flags(grp.flag_group)
+ )
+ if grp_flags == from_compiler:
+ continue
+ as_compiler_flags = list(
+ spack.spec.CompilerFlag(
+ x,
+ propagate=grp.propagate,
+ flag_group=grp.flag_group,
+ source=grp.source,
+ )
+ for x in grp_flags
+ )
+ extend_flag_list(ordered_flags, as_compiler_flags)
+
+ # 3. Now put cmd-line flags last
+ if node.pkg in cmd_specs:
+ cmd_flags = cmd_specs[node.pkg].compiler_flags.get(flag_type, [])
+ extend_flag_list(ordered_flags, cmd_flags)
+
+ compiler_flags = spec.compiler_flags.get(flag_type, [])
+ msg = "%s does not equal %s" % (set(compiler_flags), set(ordered_flags))
+ assert set(compiler_flags) == set(ordered_flags), msg
+
+ spec.compiler_flags.update({flag_type: ordered_flags})
def deprecated(self, node: NodeArgument, version: str) -> None:
tty.warn(f'using "{node.pkg}@{version}" which is a deprecated version')
@@ -3570,10 +3735,9 @@ class SpecBuilder:
continue
# if we've already gotten a concrete spec for this pkg,
- # do not bother calling actions on it except for node_flag_source,
- # since node_flag_source is tracking information not in the spec itself
+ # do not bother calling actions on it
spec = self._specs.get(args[0])
- if spec and spec.concrete and name != "node_flag_source":
+ if spec and spec.concrete:
continue
action(*args)
@@ -3633,7 +3797,8 @@ def _develop_specs_from_env(spec, env):
assert spec.variants["dev_path"].value == path, error_msg
else:
spec.variants.setdefault("dev_path", spack.variant.SingleValuedVariant("dev_path", path))
- spec.constrain(dev_info["spec"])
+
+ assert spec.satisfies(dev_info["spec"])
def _is_reusable(spec: spack.spec.Spec, packages, local: bool) -> bool:
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 342620238f..dab05adaa3 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -43,7 +43,7 @@
internal_error("Only nodes can have node_compiler_version").
:- attr("variant_value", PackageNode, _, _), not attr("node", PackageNode),
internal_error("variant_value true for a non-node").
-:- attr("node_flag", PackageNode, _, _), not attr("node", PackageNode),
+:- attr("node_flag", PackageNode, _), not attr("node", PackageNode),
internal_error("node_flag assigned for non-node").
:- attr("external_spec_selected", PackageNode, _), not attr("node", PackageNode),
internal_error("external_spec_selected for non-node").
@@ -51,10 +51,6 @@
internal_error("non-node depends on something").
:- attr("depends_on", _, ChildNode, _), not attr("node", ChildNode),
internal_error("something depends_on a non-node").
-:- attr("node_flag_source", Node, _, _), not attr("node", Node),
- internal_error("node_flag_source assigned for a non-node").
-:- attr("node_flag_source", _, _, SourceNode), not attr("node", SourceNode),
- internal_error("node_flag_source assigned with a non-node source").
:- attr("virtual_node", VirtualNode), not provider(_, VirtualNode),
internal_error("virtual node with no provider").
:- provider(_, VirtualNode), not attr("virtual_node", VirtualNode),
@@ -152,7 +148,6 @@ unification_set(SetID, VirtualNode)
% TODO: literals, at the moment, can only influence the "root" unification set. This needs to be extended later.
% Node attributes that have multiple node arguments (usually, only the first argument is a node)
-multiple_nodes_attribute("node_flag_source").
multiple_nodes_attribute("depends_on").
multiple_nodes_attribute("virtual_on_edge").
multiple_nodes_attribute("provider_set").
@@ -390,7 +385,6 @@ trigger_condition_holds(ID, RequestorNode) :-
attr(Name, node(X, A1), A2, A3) : condition_requirement(ID, Name, A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name);
attr(Name, node(X, A1), A2, A3, A4) : condition_requirement(ID, Name, A1, A2, A3, A4), condition_nodes(ID, PackageNode, node(X, A1));
% Special cases
- attr("node_flag_source", node(X, A1), A2, node(Y, A3)) : condition_requirement(ID, "node_flag_source", A1, A2, A3), condition_nodes(ID, PackageNode, node(X, A1)), condition_nodes(ID, PackageNode, node(Y, A3));
not cannot_hold(ID, PackageNode).
condition_holds(ConditionID, node(X, Package))
@@ -438,13 +432,6 @@ attr(Name, node(X, A1), A2) :- impose(ID, PackageNode), imposed_constrai
attr(Name, node(X, A1), A2, A3) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3), imposed_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name).
attr(Name, node(X, A1), A2, A3, A4) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3, A4), imposed_nodes(ID, PackageNode, node(X, A1)).
-% For node flag sources we need to look at the condition_set of the source, since it is the dependent
-% of the package on which I want to impose the constraint
-attr("node_flag_source", node(X, A1), A2, node(Y, A3))
- :- impose(ID, node(X, A1)),
- imposed_constraint(ID, "node_flag_source", A1, A2, A3),
- condition_set(node(Y, A3), node(X, A1)).
-
% Provider set is relevant only for literals, since it's the only place where `^[virtuals=foo] bar`
% might appear in the HEAD of a rule
attr("provider_set", node(min_dupe_id, Provider), node(min_dupe_id, Virtual))
@@ -485,8 +472,8 @@ virtual_condition_holds(node(Y, A2), Virtual)
% we cannot have additional flag values when we are working with concrete specs
:- attr("node", node(ID, Package)),
attr("hash", node(ID, Package), Hash),
- attr("node_flag", node(ID, Package), FlagType, Flag),
- not imposed_constraint(Hash, "node_flag", Package, FlagType, Flag),
+ attr("node_flag", node(ID, Package), node_flag(FlagType, Flag, _, _)),
+ not imposed_constraint(Hash, "node_flag", Package, node_flag(FlagType, Flag, _, _)),
internal_error("imposed hash without imposing all flag values").
#defined condition/2.
@@ -787,22 +774,15 @@ required_provider(Provider, Virtual)
:- provider(node(Y, Package), node(X, Virtual)), required_provider(Provider, Virtual), Package != Provider.
-% TODO: the following two choice rules allow the solver to add compiler
+% TODO: the following choice rule allows the solver to add compiler
% flags if their only source is from a requirement. This is overly-specific
% and should use a more-generic approach like in https://github.com/spack/spack/pull/37180
-{ attr("node_flag", node(ID, Package), FlagType, FlagValue) } :-
+{ attr("node_flag", node(ID, Package), NodeFlag) } :-
requirement_group_member(ConditionID, Package, RequirementID),
activate_requirement(node(ID, Package), RequirementID),
pkg_fact(Package, condition_effect(ConditionID, EffectID)),
- imposed_constraint(EffectID, "node_flag_set", Package, FlagType, FlagValue).
-
-{ attr("node_flag_source", node(NodeID1, Package1), FlagType, node(NodeID2, Package2)) } :-
- requirement_group_member(ConditionID, Package1, RequirementID),
- activate_requirement(node(NodeID1, Package1), RequirementID),
- pkg_fact(Package1, condition_effect(ConditionID, EffectID)),
- imposed_constraint(EffectID, "node_flag_source", Package1, FlagType, Package2),
- imposed_nodes(EffectID, node(NodeID2, Package2), node(NodeID1, Package1)).
+ imposed_constraint(EffectID, "node_flag_set", Package, NodeFlag).
requirement_weight(node(ID, Package), Group, W) :-
W = #min {
@@ -1048,23 +1028,22 @@ variant_is_propagated(PackageNode, Variant) :-
% 1. The same flag type is not set on this node
% 2. This node has the same compiler as the propagation source
-propagated_flag(node(PackageID, Package), node_flag(FlagType, Flag), SourceNode) :-
- propagate(node(PackageID, Package), node_flag(FlagType, Flag), _),
- not attr("node_flag_set", node(PackageID, Package), FlagType, _),
+propagated_flag(node(PackageID, Package), node_flag(FlagType, Flag, FlagGroup, Source), SourceNode) :-
+ propagate(node(PackageID, Package), node_flag(FlagType, Flag, FlagGroup, Source), _),
+ not attr("node_flag_set", node(PackageID, Package), node_flag(FlagType, _, _, "literal")),
% Same compiler as propagation source
node_compiler(node(PackageID, Package), CompilerID),
node_compiler(SourceNode, CompilerID),
- attr("propagate", SourceNode, node_flag(FlagType, Flag), _),
+ attr("propagate", SourceNode, node_flag(FlagType, Flag, FlagGroup, Source), _),
node(PackageID, Package) != SourceNode,
not runtime(Package).
-attr("node_flag", PackageNode, FlagType, Flag) :- propagated_flag(PackageNode, node_flag(FlagType, Flag), _).
-attr("node_flag_source", PackageNode, FlagType, SourceNode) :- propagated_flag(PackageNode, node_flag(FlagType, _), SourceNode).
+attr("node_flag", PackageNode, NodeFlag) :- propagated_flag(PackageNode, NodeFlag, _).
% Cannot propagate the same flag from two distinct sources
error(100, "{0} and {1} cannot both propagate compiler flags '{2}' to {3}", Source1, Source2, Package, FlagType) :-
- propagated_flag(node(ID, Package), node_flag(FlagType, _), node(_, Source1)),
- propagated_flag(node(ID, Package), node_flag(FlagType, _), node(_, Source2)),
+ propagated_flag(node(ID, Package), node_flag(FlagType, _, _, _), node(_, Source1)),
+ propagated_flag(node(ID, Package), node_flag(FlagType, _, _, _), node(_, Source2)),
Source1 < Source2.
%----
@@ -1351,23 +1330,18 @@ error(100, "Compiler {1}@{2} requested for {0} cannot be found. Set install_miss
% Compiler flags
%-----------------------------------------------------------------------------
-% remember where flags came from
-attr("node_flag_source", PackageNode, FlagType, PackageNode) :- attr("node_flag_set", PackageNode, FlagType, _).
-attr("node_flag_source", PackageNode, FlagType, PackageNode) :- attr("node_flag", PackageNode, FlagType, _), attr("hash", PackageNode, _).
-
% compiler flags from compilers.yaml are put on nodes if compiler matches
-attr("node_flag", PackageNode, FlagType, Flag)
- :- compiler_flag(CompilerID, FlagType, Flag),
+attr("node_flag", PackageNode, node_flag(FlagType, Flag, FlagGroup, CompilerID))
+ :- compiler_flag(CompilerID, FlagType, Flag, FlagGroup),
node_compiler(PackageNode, CompilerID),
flag_type(FlagType),
compiler_id(CompilerID),
compiler_name(CompilerID, CompilerName),
compiler_version(CompilerID, Version).
-% Flag set to something
-attr("node_flag", PackageNode, FlagType, Flag) :- attr("node_flag_set", PackageNode, FlagType, Flag).
+attr("node_flag", PackageNode, NodeFlag) :- attr("node_flag_set", PackageNode, NodeFlag).
-#defined compiler_flag/3.
+#defined compiler_flag/4.
%-----------------------------------------------------------------------------
diff --git a/lib/spack/spack/solver/core.py b/lib/spack/spack/solver/core.py
index 896631290c..2530981a21 100644
--- a/lib/spack/spack/solver/core.py
+++ b/lib/spack/spack/solver/core.py
@@ -230,6 +230,13 @@ class NodeArgument(NamedTuple):
pkg: str
+class NodeFlag(NamedTuple):
+ flag_type: str
+ flag: str
+ flag_group: str
+ source: str
+
+
def intermediate_repr(sym):
"""Returns an intermediate representation of clingo models for Spack's spec builder.
@@ -248,6 +255,13 @@ def intermediate_repr(sym):
return NodeArgument(
id=intermediate_repr(sym.arguments[0]), pkg=intermediate_repr(sym.arguments[1])
)
+ elif sym.name == "node_flag":
+ return NodeFlag(
+ flag_type=intermediate_repr(sym.arguments[0]),
+ flag=intermediate_repr(sym.arguments[1]),
+ flag_group=intermediate_repr(sym.arguments[2]),
+ source=intermediate_repr(sym.arguments[3]),
+ )
except RuntimeError:
# This happens when using clingo w/ CFFI and trying to access ".name" for symbols
# that are not functions
diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp
index 358a1628aa..fb3b2c41df 100644
--- a/lib/spack/spack/solver/display.lp
+++ b/lib/spack/spack/solver/display.lp
@@ -13,6 +13,7 @@
#show attr/2.
#show attr/3.
#show attr/4.
+#show attr/5.
% names of optimization criteria
#show opt_criterion/2.
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 700f3a74de..0c869febb1 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -781,17 +781,49 @@ class CompilerFlag(str):
propagate (bool): if ``True`` the flag value will
be passed to the package's dependencies. If
``False`` it will not
+ flag_group (str): if this flag was introduced along
+ with several flags via a single source, then
+ this will store all such flags
+ source (str): identifies the type of constraint that
+ introduced this flag (e.g. if a package has
+ ``depends_on(... cflags=-g)``, then the ``source``
+ for "-g" would indicate ``depends_on``.
"""
def __new__(cls, value, **kwargs):
obj = str.__new__(cls, value)
obj.propagate = kwargs.pop("propagate", False)
+ obj.flag_group = kwargs.pop("flag_group", value)
+ obj.source = kwargs.pop("source", None)
return obj
_valid_compiler_flags = ["cflags", "cxxflags", "fflags", "ldflags", "ldlibs", "cppflags"]
+def _shared_subset_pair_iterate(container1, container2):
+ """
+ [0, a, c, d, f]
+ [a, d, e, f]
+
+ yields [(a, a), (d, d), (f, f)]
+
+ no repeated elements
+ """
+ a_idx, b_idx = 0, 0
+ max_a, max_b = len(container1), len(container2)
+ while a_idx < max_a and b_idx < max_b:
+ if container1[a_idx] == container2[b_idx]:
+ yield (container1[a_idx], container2[b_idx])
+ a_idx += 1
+ b_idx += 1
+ else:
+ while container1[a_idx] < container2[b_idx]:
+ a_idx += 1
+ while container1[a_idx] > container2[b_idx]:
+ b_idx += 1
+
+
class FlagMap(lang.HashableMap):
__slots__ = ("spec",)
@@ -800,23 +832,9 @@ class FlagMap(lang.HashableMap):
self.spec = spec
def satisfies(self, other):
- return all(f in self and self[f] == other[f] for f in other)
+ return all(f in self and set(self[f]) >= set(other[f]) for f in other)
def intersects(self, other):
- common_types = set(self) & set(other)
- for flag_type in common_types:
- if not self[flag_type] or not other[flag_type]:
- # At least one of the two is empty
- continue
-
- if self[flag_type] != other[flag_type]:
- return False
-
- if not all(
- f1.propagate == f2.propagate for f1, f2 in zip(self[flag_type], other[flag_type])
- ):
- # At least one propagation flag didn't match
- return False
return True
def constrain(self, other):
@@ -824,28 +842,28 @@ class FlagMap(lang.HashableMap):
Return whether the spec changed.
"""
- if other.spec and other.spec._concrete:
- for k in self:
- if k not in other:
- raise UnsatisfiableCompilerFlagSpecError(self[k], "<absent>")
-
changed = False
- for k in other:
- if k in self and not set(self[k]) <= set(other[k]):
- raise UnsatisfiableCompilerFlagSpecError(
- " ".join(f for f in self[k]), " ".join(f for f in other[k])
- )
- elif k not in self:
- self[k] = other[k]
+ for flag_type in other:
+ if flag_type not in self:
+ self[flag_type] = other[flag_type]
changed = True
+ else:
+ extra_other = set(other[flag_type]) - set(self[flag_type])
+ if extra_other:
+ self[flag_type] = list(self[flag_type]) + list(
+ x for x in other[flag_type] if x in extra_other
+ )
+ changed = True
+
+ # Next, if any flags in other propagate, we force them to propagate in our case
+ shared = list(sorted(set(other[flag_type]) - extra_other))
+ for x, y in _shared_subset_pair_iterate(shared, sorted(self[flag_type])):
+ if x.propagate:
+ y.propagate = True
+
+ # TODO: what happens if flag groups with a partial (but not complete)
+ # intersection specify different behaviors for flag propagation?
- # Check that the propagation values match
- if self[k] == other[k]:
- for i in range(len(other[k])):
- if self[k][i].propagate != other[k][i].propagate:
- raise UnsatisfiableCompilerFlagSpecError(
- self[k][i].propagate, other[k][i].propagate
- )
return changed
@staticmethod
@@ -858,7 +876,7 @@ class FlagMap(lang.HashableMap):
clone[name] = compiler_flag
return clone
- def add_flag(self, flag_type, value, propagation):
+ def add_flag(self, flag_type, value, propagation, flag_group=None, source=None):
"""Stores the flag's value in CompilerFlag and adds it
to the FlagMap
@@ -869,7 +887,8 @@ class FlagMap(lang.HashableMap):
propagation (bool): if ``True`` the flag value will be passed to
the packages' dependencies. If``False`` it will not be passed
"""
- flag = CompilerFlag(value, propagate=propagation)
+ flag_group = flag_group or value
+ flag = CompilerFlag(value, propagate=propagation, flag_group=flag_group, source=source)
if flag_type not in self:
self[flag_type] = [flag]
@@ -1665,8 +1684,9 @@ class Spec:
elif name in valid_flags:
assert self.compiler_flags is not None
flags_and_propagation = spack.compiler.tokenize_flags(value, propagate)
+ flag_group = " ".join(x for (x, y) in flags_and_propagation)
for flag, propagation in flags_and_propagation:
- self.compiler_flags.add_flag(name, flag, propagation)
+ self.compiler_flags.add_flag(name, flag, propagation, flag_group)
else:
# FIXME:
# All other flags represent variants. 'foo=true' and 'foo=false'
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
index bbddf7ff41..786528f30f 100644
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -401,14 +401,6 @@ class TestConcretize:
s.compiler_flags[x] == ["-O0", "-g"] for x in ("cflags", "cxxflags", "fflags")
)
- @pytest.mark.xfail(reason="Broken, needs to be fixed")
- def test_compiler_flags_from_compiler_and_dependent(self):
- client = Spec("cmake-client %clang@12.2.0 platform=test os=fe target=fe cflags==-g")
- client.concretize()
- cmake = client["cmake"]
- for spec in [client, cmake]:
- assert spec.compiler_flags["cflags"] == ["-O3", "-g"]
-
def test_compiler_flags_differ_identical_compilers(self, mutable_config, clang12_with_flags):
mutable_config.set("compilers", [clang12_with_flags])
# Correct arch to use test compiler that has flags
@@ -442,6 +434,13 @@ class TestConcretize:
["hypre cflags='-g'", "^openblas cflags='-O3'"],
["^openblas cflags='-g'"],
),
+ # Setting propagation on parent and dependency -> the
+ # dependency propagation flags override
+ (
+ "hypre cflags=='-g' ^openblas cflags=='-O3'",
+ ["hypre cflags='-g'", "^openblas cflags='-O3'"],
+ ["^openblas cflags='-g'"],
+ ),
# Propagation doesn't go across build dependencies
(
"cmake-client cflags=='-O2 -g'",
diff --git a/lib/spack/spack/test/flag_mixing.py b/lib/spack/spack/test/flag_mixing.py
new file mode 100644
index 0000000000..f1c2cf7def
--- /dev/null
+++ b/lib/spack/spack/test/flag_mixing.py
@@ -0,0 +1,333 @@
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import pytest
+
+import spack.build_systems.generic
+import spack.config
+import spack.environment as ev
+import spack.error
+import spack.package_base
+import spack.repo
+import spack.util.spack_yaml as syaml
+import spack.version
+from spack.spec import Spec
+from spack.test.conftest import create_test_repo
+
+"""
+These tests include the following package DAGs:
+
+Firstly, w, x, y where w and x apply cflags to y.
+
+w
+|\
+x |
+|/
+y
+
+Secondly, v, y which where v does not apply cflags to y - this is for testing
+mixing with compiler flag propagation in the absence of compiler flags applied
+by dependents.
+
+v
+|
+y
+
+Finally, a diamond dag to check that the topological order is resolved into
+a total order:
+
+t
+|\
+u x
+|/
+y
+"""
+
+_pkgx = (
+ "x",
+ """\
+class X(Package):
+ version("1.1")
+ version("1.0")
+
+ variant("activatemultiflag", default=False)
+ depends_on('y cflags="-d1"', when="~activatemultiflag")
+ depends_on('y cflags="-d1 -d2"', when="+activatemultiflag")
+""",
+)
+
+
+_pkgy = (
+ "y",
+ """\
+class Y(Package):
+ version("2.1")
+ version("2.0")
+""",
+)
+
+
+_pkgw = (
+ "w",
+ """\
+class W(Package):
+ version("3.1")
+ version("3.0")
+
+ variant("moveflaglater", default=False)
+
+ depends_on('x +activatemultiflag')
+ depends_on('y cflags="-d0"', when="~moveflaglater")
+ depends_on('y cflags="-d3"', when="+moveflaglater")
+""",
+)
+
+
+_pkgv = (
+ "v",
+ """\
+class V(Package):
+ version("4.1")
+ version("4.0")
+
+ depends_on("y")
+""",
+)
+
+
+_pkgt = (
+ "t",
+ """\
+class T(Package):
+ version("5.0")
+
+ depends_on("u")
+ depends_on("x+activatemultiflag")
+ depends_on("y cflags='-c1 -c2'")
+""",
+)
+
+
+_pkgu = (
+ "u",
+ """\
+class U(Package):
+ version("6.0")
+
+ depends_on("y cflags='-e1 -e2'")
+""",
+)
+
+
+@pytest.fixture
+def _create_test_repo(tmpdir, mutable_config):
+ yield create_test_repo(tmpdir, [_pkgt, _pkgu, _pkgv, _pkgw, _pkgx, _pkgy])
+
+
+@pytest.fixture
+def test_repo(_create_test_repo, monkeypatch, mock_stage):
+ with spack.repo.use_repositories(_create_test_repo) as mock_repo_path:
+ yield mock_repo_path
+
+
+def update_concretize_scope(conf_str, section):
+ conf = syaml.load_config(conf_str)
+ spack.config.set(section, conf[section], scope="concretize")
+
+
+def test_mix_spec_and_requirements(concretize_scope, test_repo):
+ conf_str = """\
+packages:
+ y:
+ require: cflags="-c"
+"""
+ update_concretize_scope(conf_str, "packages")
+
+ s1 = Spec('y cflags="-a"').concretized()
+ assert s1.satisfies('cflags="-a -c"')
+
+
+def test_mix_spec_and_dependent(concretize_scope, test_repo):
+ s1 = Spec('x ^y cflags="-a"').concretized()
+ assert s1["y"].satisfies('cflags="-a -d1"')
+
+
+def _compiler_cfg_one_entry_with_cflags(cflags):
+ return f"""\
+compilers::
+- compiler:
+ spec: gcc@12.100.100
+ paths:
+ cc: /usr/bin/fake-gcc
+ cxx: /usr/bin/fake-g++
+ f77: null
+ fc: null
+ flags:
+ cflags: {cflags}
+ operating_system: debian6
+ modules: []
+"""
+
+
+def test_mix_spec_and_compiler_cfg(concretize_scope, test_repo):
+ conf_str = _compiler_cfg_one_entry_with_cflags("-Wall")
+ update_concretize_scope(conf_str, "compilers")
+
+ s1 = Spec('y %gcc@12.100.100 cflags="-O2"').concretized()
+ assert s1.satisfies('cflags="-Wall -O2"')
+
+
+@pytest.mark.parametrize(
+ "cmd_flags,req_flags,cmp_flags,dflags,expected_order",
+ [
+ ("-a -b", "-c", None, False, "-c -a -b"),
+ ("-x7 -x4", "-x5 -x6", None, False, "-x5 -x6 -x7 -x4"),
+ ("-x7 -x4", "-x5 -x6", "-x3 -x8", False, "-x3 -x8 -x5 -x6 -x7 -x4"),
+ ("-x7 -x4", "-x5 -x6", "-x3 -x8", True, "-x3 -x8 -d1 -d2 -x5 -x6 -x7 -x4"),
+ ("-x7 -x4", None, "-x3 -x8", False, "-x3 -x8 -x7 -x4"),
+ ("-x7 -x4", None, "-x3 -x8", True, "-x3 -x8 -d1 -d2 -x7 -x4"),
+ # The remaining test cover cases of intersection
+ ("-a -b", "-a -c", None, False, "-c -a -b"),
+ ("-a -b", None, "-a -c", False, "-c -a -b"),
+ ("-a -b", "-a -c", "-a -d", False, "-d -c -a -b"),
+ ("-a -d2 -d1", "-d2 -c", "-d1 -b", True, "-b -c -a -d2 -d1"),
+ ("-a", "-d0 -d2 -c", "-d1 -b", True, "-b -d1 -d0 -d2 -c -a"),
+ ],
+)
+def test_flag_order_and_grouping(
+ concretize_scope, test_repo, cmd_flags, req_flags, cmp_flags, dflags, expected_order
+):
+ """Check consistent flag ordering and grouping on a package "y"
+ with flags introduced from a variety of sources.
+
+ The ordering rules are explained in ``asp.SpecBuilder.reorder_flags``.
+ """
+ if req_flags:
+ conf_str = f"""\
+packages:
+ y:
+ require: cflags="{req_flags}"
+"""
+ update_concretize_scope(conf_str, "packages")
+
+ if cmp_flags:
+ conf_str = _compiler_cfg_one_entry_with_cflags(cmp_flags)
+ update_concretize_scope(conf_str, "compilers")
+
+ compiler_spec = ""
+ if cmp_flags:
+ compiler_spec = "%gcc@12.100.100"
+
+ if dflags:
+ spec_str = f"x+activatemultiflag {compiler_spec} ^y"
+ expected_dflags = "-d1 -d2"
+ else:
+ spec_str = f"y {compiler_spec}"
+ expected_dflags = None
+
+ if cmd_flags:
+ spec_str += f' cflags="{cmd_flags}"'
+
+ root_spec = Spec(spec_str).concretized()
+ spec = root_spec["y"]
+ satisfy_flags = " ".join(x for x in [cmd_flags, req_flags, cmp_flags, expected_dflags] if x)
+ assert spec.satisfies(f'cflags="{satisfy_flags}"')
+ assert spec.compiler_flags["cflags"] == expected_order.split()
+
+
+def test_two_dependents_flag_mixing(concretize_scope, test_repo):
+ root_spec1 = Spec("w~moveflaglater").concretized()
+ spec1 = root_spec1["y"]
+ assert spec1.compiler_flags["cflags"] == "-d0 -d1 -d2".split()
+
+ root_spec2 = Spec("w+moveflaglater").concretized()
+ spec2 = root_spec2["y"]
+ assert spec2.compiler_flags["cflags"] == "-d3 -d1 -d2".split()
+
+
+def test_propagate_and_compiler_cfg(concretize_scope, test_repo):
+ conf_str = _compiler_cfg_one_entry_with_cflags("-f2")
+ update_concretize_scope(conf_str, "compilers")
+
+ root_spec = Spec("v %gcc@12.100.100 cflags=='-f1'").concretized()
+ assert root_spec["y"].satisfies("cflags='-f1 -f2'")
+
+
+# Note: setting flags on a dependency overrides propagation, which
+# is tested in test/concretize.py:test_compiler_flag_propagation
+
+
+def test_propagate_and_pkg_dep(concretize_scope, test_repo):
+ root_spec1 = Spec("x ~activatemultiflag cflags=='-f1'").concretized()
+ assert root_spec1["y"].satisfies("cflags='-f1 -d1'")
+
+
+def test_propagate_and_require(concretize_scope, test_repo):
+ conf_str = """\
+packages:
+ y:
+ require: cflags="-f2"
+"""
+ update_concretize_scope(conf_str, "packages")
+
+ root_spec1 = Spec("v cflags=='-f1'").concretized()
+ assert root_spec1["y"].satisfies("cflags='-f1 -f2'")
+
+ # Next, check that a requirement does not "undo" a request for
+ # propagation from the command-line spec
+ conf_str = """\
+packages:
+ v:
+ require: cflags="-f1"
+"""
+ update_concretize_scope(conf_str, "packages")
+
+ root_spec2 = Spec("v cflags=='-f1'").concretized()
+ assert root_spec2["y"].satisfies("cflags='-f1'")
+
+ # Note: requirements cannot enforce propagation: any attempt to do
+ # so will generate a concretization error; this likely relates to
+ # the note about #37180 in concretize.lp
+
+
+def test_dev_mix_flags(tmp_path, concretize_scope, mutable_mock_env_path, test_repo):
+ src_dir = tmp_path / "x-src"
+
+ env_content = f"""\
+spack:
+ specs:
+ - y %gcc@12.100.100 cflags=='-fsanitize=address'
+ develop:
+ y:
+ spec: y cflags=='-fsanitize=address'
+ path: {src_dir}
+"""
+
+ conf_str = _compiler_cfg_one_entry_with_cflags("-f1")
+ update_concretize_scope(conf_str, "compilers")
+
+ manifest_file = tmp_path / ev.manifest_name
+ manifest_file.write_text(env_content)
+ e = ev.create("test", manifest_file)
+ with e:
+ e.concretize()
+ e.write()
+
+ (result,) = list(j for i, j in e.concretized_specs() if j.name == "y")
+
+ assert result["y"].satisfies("cflags='-fsanitize=address -f1'")
+
+
+def test_diamond_dep_flag_mixing(concretize_scope, test_repo):
+ """A diamond where each dependent applies flags to the bottom
+ dependency. The goal is to ensure that the flag ordering is
+ (a) topological and (b) repeatable for elements not subject to
+ this partial ordering (i.e. the flags for the left and right
+ nodes of the diamond always appear in the same order).
+ `Spec.traverse` is responsible for handling both of these needs.
+ """
+ root_spec1 = Spec("t").concretized()
+ spec1 = root_spec1["y"]
+ assert spec1.satisfies('cflags="-c1 -c2 -d1 -d2 -e1 -e2"')
+ assert spec1.compiler_flags["cflags"] == "-c1 -c2 -e1 -e2 -d1 -d2".split()
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index e11e663338..cd0f930acd 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -245,6 +245,65 @@ class TestSpecSemantics:
assert c1 == c2
assert c1 == expected
+ @pytest.mark.parametrize(
+ "lhs,rhs,expected_lhs,expected_rhs,propagated_lhs,propagated_rhs",
+ [
+ (
+ 'mpich cppflags="-O3"',
+ 'mpich cppflags="-O2"',
+ 'mpich cppflags="-O3 -O2"',
+ 'mpich cppflags="-O2 -O3"',
+ [],
+ [],
+ ),
+ (
+ 'mpich cflags="-O3 -g"',
+ 'mpich cflags=="-O3"',
+ 'mpich cflags="-O3 -g"',
+ 'mpich cflags=="-O3 -g"',
+ [("cflags", "-O3")],
+ [("cflags", "-O3")],
+ ),
+ ],
+ )
+ def test_constrain_compiler_flags(
+ self, lhs, rhs, expected_lhs, expected_rhs, propagated_lhs, propagated_rhs
+ ):
+ """Constraining is asymmetric for compiler flags. Also note that
+ Spec equality does not account for flag propagation, so the checks
+ here are manual.
+ """
+ lhs, rhs, expected_lhs, expected_rhs = (
+ Spec(lhs),
+ Spec(rhs),
+ Spec(expected_lhs),
+ Spec(expected_rhs),
+ )
+
+ assert lhs.intersects(rhs)
+ assert rhs.intersects(lhs)
+
+ c1, c2 = lhs.copy(), rhs.copy()
+ c1.constrain(rhs)
+ c2.constrain(lhs)
+
+ assert c1 == expected_lhs
+ assert c2 == expected_rhs
+ for x in [c1, c2]:
+ assert x.satisfies(lhs)
+ assert x.satisfies(rhs)
+
+ def _propagated_flags(_spec):
+ result = set()
+ for flagtype in _spec.compiler_flags:
+ for flag in _spec.compiler_flags[flagtype]:
+ if flag.propagate:
+ result.add((flagtype, flag))
+ return result
+
+ assert set(propagated_lhs) <= _propagated_flags(c1)
+ assert set(propagated_rhs) <= _propagated_flags(c2)
+
def test_constrain_specs_by_hash(self, default_mock_concretization, database):
"""Test that Specs specified only by their hashes can constrain eachother."""
mpich_dag_hash = "/" + database.query_one("mpich").dag_hash()
@@ -311,14 +370,11 @@ class TestSpecSemantics:
("mpich~~foo", "mpich++foo"),
("mpich++foo", "mpich~~foo"),
("mpich foo==True", "mpich foo==False"),
- ('mpich cppflags="-O3"', 'mpich cppflags="-O2"'),
- ('mpich cppflags="-O3"', 'mpich cppflags=="-O3"'),
("libelf@0:2.0", "libelf@2.1:3"),
("libelf@0:2.5%gcc@4.8:4.9", "libelf@2.1:3%gcc@4.5:4.7"),
("libelf+debug", "libelf~debug"),
("libelf+debug~foo", "libelf+debug+foo"),
("libelf debug=True", "libelf debug=False"),
- ('libelf cppflags="-O3"', 'libelf cppflags="-O2"'),
("libelf platform=test target=be os=be", "libelf target=fe os=fe"),
("namespace=builtin.mock", "namespace=builtin"),
],
@@ -347,10 +403,6 @@ class TestSpecSemantics:
("mpich", "mpich++foo"),
("mpich", "mpich~~foo"),
("mpich", "mpich foo==1"),
- # Flags semantics is currently different from other variant
- ("mpich", 'mpich cflags="-O3"'),
- ("mpich cflags=-O3", 'mpich cflags="-O3 -Ofast"'),
- ("mpich cflags=-O2", 'mpich cflags="-O3"'),
("multivalue-variant foo=bar", "multivalue-variant +foo"),
("multivalue-variant foo=bar", "multivalue-variant ~foo"),
("multivalue-variant fee=bar", "multivalue-variant fee=baz"),
@@ -1451,8 +1503,8 @@ def test_abstract_contains_semantic(lhs, rhs, expected, mock_packages):
(CompilerSpec, "gcc@5", "gcc@5-tag", (True, False, True)),
# Flags (flags are a map, so for convenience we initialize a full Spec)
# Note: the semantic is that of sv variants, not mv variants
- (Spec, "cppflags=-foo", "cppflags=-bar", (False, False, False)),
- (Spec, "cppflags='-bar -foo'", "cppflags=-bar", (False, False, False)),
+ (Spec, "cppflags=-foo", "cppflags=-bar", (True, False, False)),
+ (Spec, "cppflags='-bar -foo'", "cppflags=-bar", (True, True, False)),
(Spec, "cppflags=-foo", "cppflags=-foo", (True, True, True)),
(Spec, "cppflags=-foo", "cflags=-foo", (True, False, False)),
# Versions