flags/variants: Add ++/~~/== syntax for propagation to dependencies

Currently, compiler flags and variants are inconsistent: compiler flags set for a
package are inherited by its dependencies, while variants are not. We should have these
be consistent by allowing for inheritance to be enabled or disabled for both variants
and compiler flags.

- [x] Make new (spec language) operators
- [x] Apply operators to variants and compiler flags
- [x] Conflicts currently result in an unsatisfiable spec
      (i.e., you can't propagate two conflicting values)

What I propose is using two of the currently used sigils to symbolized that the variant
or compiler flag will be inherited:

Example syntax:
- `package ++variant`
      enabled variant that will be propagated to dependencies
- `package +variant`
      enabled variant that will NOT be propagated to dependencies
- `package ~~variant`
      disabled variant that will be propagated to dependencies
- `package ~variant`
      disabled variant that will NOT be propagated to dependencies
- `package cflags==True`
      `cflags` will be propagated to dependencies
- `package cflags=True`
      `cflags` will NOT be propagated to dependencies

Syntax for string-valued variants is similar to compiler flags.
This commit is contained in:
Kayla Butler 2022-03-28 14:18:00 -07:00 committed by Todd Gamblin
parent ae99829af4
commit bc209c470d
16 changed files with 434 additions and 117 deletions

View File

@ -998,11 +998,15 @@ More formally, a spec consists of the following pieces:
* ``%`` Optional compiler specifier, with an optional compiler version
(``gcc`` or ``gcc@4.7.3``)
* ``+`` or ``-`` or ``~`` Optional variant specifiers (``+debug``,
``-qt``, or ``~qt``) for boolean variants
``-qt``, or ``~qt``) for boolean variants. Use ``++`` or ``--`` or
``~~`` to propagate variants through the dependencies (``++debug``,
``--qt``, or ``~~qt``).
* ``name=<value>`` Optional variant specifiers that are not restricted to
boolean variants
boolean variants. Use ``name==<value>`` to propagate variant through the
dependencies.
* ``name=<value>`` Optional compiler flag specifiers. Valid flag names are
``cflags``, ``cxxflags``, ``fflags``, ``cppflags``, ``ldflags``, and ``ldlibs``.
Use ``name==<value>`` to propagate compiler flags through the dependencies.
* ``target=<value> os=<value>`` Optional architecture specifier
(``target=haswell os=CNL10``)
* ``^`` Dependency specs (``^callpath@1.1``)
@ -1226,6 +1230,23 @@ variants using the backwards compatibility syntax and uses only ``~``
for disabled boolean variants. The ``-`` and spaces on the command
line are provided for convenience and legibility.
Spack allows variants to propagate their value to the package's
dependency by using ``++``, ``--``, and ``~~`` for boolean variants.
For example, for a ``debug`` variant:
.. code-block:: sh
mpileaks ++debug # enabled debug will be propagated to dependencies
mpileaks +debug # only mpileaks will have debug enabled
To propagate the value of non-boolean variants Spack uses ``name==value``.
For example, for the ``stackstart`` variant:
.. code-block:: sh
mpileaks stackstart=4 # variant will be propagated to dependencies
mpileaks stackstart==4 # only mpileaks will have this variant value
^^^^^^^^^^^^^^
Compiler Flags
^^^^^^^^^^^^^^
@ -1233,10 +1254,15 @@ Compiler Flags
Compiler flags are specified using the same syntax as non-boolean variants,
but fulfill a different purpose. While the function of a variant is set by
the package, compiler flags are used by the compiler wrappers to inject
flags into the compile line of the build. Additionally, compiler flags are
inherited by dependencies. ``spack install libdwarf cppflags="-g"`` will
install both libdwarf and libelf with the ``-g`` flag injected into their
compile line.
flags into the compile line of the build. Additionally, compiler flags can
be inherited by dependencies by using ``==``.
``spack install libdwarf cppflags=="-g"`` will install both libdwarf and
libelf with the ``-g`` flag injected into their compile line.
.. note::
versions of spack prior to 0.19.0 will propagate compiler flags using
the ``=`` syntax.
Notice that the value of the compiler flags must be quoted if it
contains any spaces. Any of ``cppflags=-O3``, ``cppflags="-O3"``,

View File

@ -234,7 +234,8 @@ def parse_specs(args, **kwargs):
msg = e.message
if e.long_message:
msg += e.long_message
if unquoted_flags:
# Unquoted flags will be read as a variant or hash
if unquoted_flags and ("variant" in msg or "hash" in msg):
msg += "\n\n"
msg += unquoted_flags.report()

View File

@ -91,6 +91,6 @@ def deactivate(parser, args):
)
if not args.force and not spec.package.is_activated(view):
tty.die("Package %s is not activated." % specs[0].short_spec)
tty.die("Package %s is not activated." % spec.short_spec)
spec.package.do_deactivate(view, force=args.force)

View File

@ -76,7 +76,7 @@ def extensions(parser, args):
spec = cmd.disambiguate_spec(spec[0], env)
if not spec.package.extendable:
tty.die("%s is not an extendable package." % spec[0].name)
tty.die("%s is not an extendable package." % spec.name)
if not spec.package.extendable:
tty.die("%s does not have extensions." % spec.short_spec)

View File

@ -56,25 +56,25 @@ def get_compiler_version_output(compiler_path, *args, **kwargs):
return _get_compiler_version_output(compiler_path, *args, **kwargs)
def tokenize_flags(flags_str):
def tokenize_flags(flags_values, propagate=False):
"""Given a compiler flag specification as a string, this returns a list
where the entries are the flags. For compiler options which set values
using the syntax "-flag value", this function groups flags and their
values together. Any token not preceded by a "-" is considered the
value of a prior flag."""
tokens = flags_str.split()
tokens = flags_values.split()
if not tokens:
return []
flag = tokens[0]
flags = []
flags_with_propagation = []
for token in tokens[1:]:
if not token.startswith("-"):
flag += " " + token
else:
flags.append(flag)
flags_with_propagation.append((flag, propagate))
flag = token
flags.append(flag)
return flags
flags_with_propagation.append((flag, propagate))
return flags_with_propagation
#: regex for parsing linker lines
@ -311,11 +311,13 @@ def __init__(
# Unfortunately have to make sure these params are accepted
# in the same order they are returned by sorted(flags)
# in compilers/__init__.py
self.flags = {}
for flag in spack.spec.FlagMap.valid_compiler_flags():
self.flags = spack.spec.FlagMap(self.spec)
for flag in self.flags.valid_compiler_flags():
value = kwargs.get(flag, None)
if value is not None:
self.flags[flag] = tokenize_flags(value)
values_with_propagation = tokenize_flags(value, False)
for value, propagation in values_with_propagation:
self.flags.add_flag(flag, value, propagation)
# caching value for compiler reported version
# used for version checks for API, e.g. C++11 flag

View File

@ -175,9 +175,10 @@ def update(self, spec):
pkg_provided = self.repository.get_pkg_class(spec.name).provided
for provided_spec, provider_specs in six.iteritems(pkg_provided):
for provider_spec in provider_specs:
for provider_spec_readonly in provider_specs:
# TODO: fix this comment.
# We want satisfaction other than flags
provider_spec = provider_spec_readonly.copy()
provider_spec.compiler_flags = spec.compiler_flags.copy()
if spec.satisfies(provider_spec, deps=False):

View File

@ -302,18 +302,6 @@ def extend_flag_list(flag_list, new_flags):
flag_list.append(flag)
def check_same_flags(flag_dict_1, flag_dict_2):
"""Return True if flag dicts contain the same flags regardless of order."""
types = set(flag_dict_1.keys()).union(set(flag_dict_2.keys()))
for t in types:
values1 = set(flag_dict_1.get(t, []))
values2 = set(flag_dict_2.get(t, []))
error_msg = "Internal Error: A mismatch in flags has occurred:"
error_msg += "\n\tvalues1: {v1}\n\tvalues2: {v2}".format(v1=values1, v2=values2)
error_msg += "\n Please report this as an issue to the spack maintainers"
assert values1 == values2, error_msg
def check_packages_exist(specs):
"""Ensure all packages mentioned in specs exist."""
repo = spack.repo.path
@ -723,6 +711,7 @@ def stringify(x):
if output.timers:
timer.write_tty()
print()
if output.stats:
print("Statistics:")
pprint.pprint(self.control.statistics)
@ -1359,6 +1348,8 @@ class Head(object):
node_compiler = fn.node_compiler_set
node_compiler_version = fn.node_compiler_version_set
node_flag = fn.node_flag_set
node_flag_propagate = fn.node_flag_propagate
variant_propagate = fn.variant_propagate
class Body(object):
node = fn.node
@ -1370,6 +1361,8 @@ class Body(object):
node_compiler = fn.node_compiler
node_compiler_version = fn.node_compiler_version
node_flag = fn.node_flag
node_flag_propagate = fn.node_flag_propagate
variant_propagate = fn.variant_propagate
f = Body if body else Head
@ -1417,6 +1410,9 @@ class Body(object):
clauses.append(f.variant_value(spec.name, vname, value))
if variant.propagate:
clauses.append(f.variant_propagate(spec.name, vname))
# Tell the concretizer that this is a possible value for the
# variant, to account for things like int/str values where we
# can't enumerate the valid values
@ -1443,6 +1439,8 @@ class Body(object):
for flag_type, flags in spec.compiler_flags.items():
for flag in flags:
clauses.append(f.node_flag(spec.name, flag_type, flag))
if not spec.concrete and flag.propagate is True:
clauses.append(f.node_flag_propagate(spec.name, flag_type))
# dependencies
if spec.concrete:
@ -2076,13 +2074,15 @@ def variant_value(self, pkg, name, value):
# FIXME: is there a way not to special case 'dev_path' everywhere?
if name == "dev_path":
self._specs[pkg].variants.setdefault(
name, spack.variant.SingleValuedVariant(name, value)
name,
spack.variant.SingleValuedVariant(name, value)
)
return
if name == "patches":
self._specs[pkg].variants.setdefault(
name, spack.variant.MultiValuedVariant(name, value)
name,
spack.variant.MultiValuedVariant(name, value)
)
return
@ -2101,10 +2101,10 @@ def node_flag_compiler_default(self, pkg):
self._flag_compiler_defaults.add(pkg)
def node_flag(self, pkg, flag_type, flag):
self._specs[pkg].compiler_flags.setdefault(flag_type, []).append(flag)
self._specs[pkg].compiler_flags.add_flag(flag_type, flag, False)
def node_flag_source(self, pkg, source):
self._flag_sources[pkg].add(source)
def node_flag_source(self, pkg, flag_type, source):
self._flag_sources[(pkg, flag_type)].add(source)
def no_flags(self, pkg, flag_type):
self._specs[pkg].compiler_flags[flag_type] = []
@ -2151,15 +2151,24 @@ def reorder_flags(self):
for pkg in self._flag_compiler_defaults:
spec = self._specs[pkg]
compiler_flags = compilers[spec.compiler].flags
check_same_flags(spec.compiler_flags, compiler_flags)
spec.compiler_flags.update(compiler_flags)
for key in spec.compiler_flags:
spec_compiler_flags_set = set(spec.compiler_flags.get(key, []))
compiler_flags_set = set(compiler_flags.get(key, []))
assert spec_compiler_flags_set == compiler_flags_set, "%s does not equal %s" % (
spec_compiler_flags_set,
compiler_flags_set,
)
spec.compiler_flags[key] = compiler_flags.get(key, [])
# index of all specs (and deps) from the command line by name
cmd_specs = dict((s.name, s) for spec in self._command_line_specs for s in spec.traverse())
# iterate through specs with specified flags
for pkg, sources in self._flag_sources.items():
for key, sources in self._flag_sources.items():
pkg, flag_type = key
spec = self._specs[pkg]
compiler_flags = spec.compiler_flags.get(flag_type, [])
# order is determined by the DAG. A spec's flags come after
# any from its ancestors on the compile line.
@ -2169,14 +2178,16 @@ def reorder_flags(self):
sorted_sources = sorted(sources, key=lambda s: order.index(s))
# add flags from each source, lowest to highest precedence
flags = collections.defaultdict(lambda: [])
flags = []
for source_name in sorted_sources:
source = cmd_specs[source_name]
for name, flag_list in source.compiler_flags.items():
extend_flag_list(flags[name], flag_list)
extend_flag_list(flags, source.compiler_flags.get(flag_type, []))
check_same_flags(spec.compiler_flags, flags)
spec.compiler_flags.update(flags)
assert set(compiler_flags) == set(flags), "%s does not equal %s" % (
set(compiler_flags),
set(flags),
)
spec.compiler_flags.update({flag_type: source.compiler_flags[flag_type]})
def deprecated(self, pkg, version):
msg = 'using "{0}@{1}" which is a deprecated version'
@ -2187,12 +2198,14 @@ def sort_fn(function_tuple):
name = function_tuple[0]
if name == "error":
priority = function_tuple[1][0]
return (-4, priority)
return (-5, priority)
elif name == "hash":
return (-3, 0)
return (-4, 0)
elif name == "node":
return (-2, 0)
return (-3, 0)
elif name == "node_compiler":
return (-2, 0)
elif name == "node_flag":
return (-1, 0)
else:
return (0, 0)

View File

@ -400,6 +400,7 @@ node_target(Package, Target) :- attr("node_target", Package, Target).
node_target_satisfies(Package, Target) :- attr("node_target_satisfies", Package, Target).
variant_value(Package, Variant, Value) :- attr("variant_value", Package, Variant, Value).
variant_set(Package, Variant, Value) :- attr("variant_set", Package, Variant, Value).
variant_propagate(Package, Variant) :- attr("variant_propagate", Package, Variant).
node_flag(Package, FlagType, Flag) :- attr("node_flag", Package, FlagType, Flag).
node_compiler(Package, Compiler) :- attr("node_compiler", Package, Compiler).
depends_on(Package, Dependency, Type) :- attr("depends_on", Package, Dependency, Type).
@ -407,6 +408,8 @@ node_compiler_version(Package, Compiler, Version)
:- attr("node_compiler_version", Package, Compiler, Version).
node_compiler_version_satisfies(Package, Compiler, Version)
:- attr("node_compiler_version_satisfies", Package, Compiler, Version).
node_flag_propagate(Package, FlagType)
:- attr("node_flag_propagate", Package, FlagType).
attr("node", Package) :- node(Package).
attr("virtual_node", Virtual) :- virtual_node(Virtual).
@ -419,6 +422,7 @@ attr("node_target", Package, Target) :- node_target(Package, Target).
attr("node_target_satisfies", Package, Target) :- node_target_satisfies(Package, Target).
attr("variant_value", Package, Variant, Value) :- variant_value(Package, Variant, Value).
attr("variant_set", Package, Variant, Value) :- variant_set(Package, Variant, Value).
attr("variant_propagate", Package, Variant) :- variant_propagate(Package, Variant).
attr("node_flag", Package, FlagType, Flag) :- node_flag(Package, FlagType, Flag).
attr("node_compiler", Package, Compiler) :- node_compiler(Package, Compiler).
attr("depends_on", Package, Dependency, Type) :- depends_on(Package, Dependency, Type).
@ -426,6 +430,8 @@ attr("node_compiler_version", Package, Compiler, Version)
:- node_compiler_version(Package, Compiler, Version).
attr("node_compiler_version_satisfies", Package, Compiler, Version)
:- node_compiler_version_satisfies(Package, Compiler, Version).
attr("node_flag_propagate", Package, FlagType)
:- node_flag_propagate(Package, FlagType).
% do not warn if generated program contains none of these.
#defined depends_on/3.
@ -559,6 +565,24 @@ error(2, "Cannot satisfy requirement group for package '{0}'", Package) :-
variant(Package, Variant) :- variant_condition(ID, Package, Variant),
condition_holds(ID).
% propagate the variant
variant_value(Descendant, Variant, Value) :-
node(Package), path(Package, Descendant),
variant(Package, Variant),
variant(Descendant, Variant),
variant_value(Package, Variant, Value),
variant_propagate(Package, Variant),
not variant_set(Descendant, Variant),
variant_possible_value(Descendant, Variant, Value).
error(2, "{0} and dependency {1} cannot both propagate variant '{2}'", Package1, Package2, Variant) :-
Package1 != Package2,
variant_propagate(Package1, Variant),
variant_propagate(Package2, Variant),
path(Package1, Descendent),
path(Package2, Descendent),
build(Package).
% a variant cannot be set if it is not a variant on the package
error(2, "Cannot set variant '{0}' for package '{1}' because the variant condition cannot be satisfied for the given spec", Variant, Package)
:- variant_set(Package, Variant),
@ -703,6 +727,7 @@ variant_single_value(Package, "dev_path")
% warnings like 'info: atom does not occur in any rule head'.
#defined variant/2.
#defined variant_sticky/2.
#defined variant_propagate/2.
#defined variant_set/3.
#defined variant_condition/3.
#defined variant_single_value/2.
@ -1005,29 +1030,37 @@ compiler_weight(Package, 100)
%-----------------------------------------------------------------------------
% Compiler flags
%-----------------------------------------------------------------------------
% propagate flags when compilers match
inherit_flags(Package, Dependency)
:- depends_on(Package, Dependency),
% propagate flags when compiler match
can_inherit_flags(Package, Dependency, FlagType)
:- path(Package, Dependency),
node_compiler(Package, Compiler),
node_compiler(Dependency, Compiler),
not node_flag_set(Dependency, FlagType, _),
compiler(Compiler), flag_type(FlagType).
node_flag_inherited(Dependency, FlagType, Flag)
:- node_flag_set(Package, FlagType, Flag), inherit_flags(Package, Dependency).
node_flag_inherited(Dependency, FlagType, Flag)
:- node_flag_inherited(Package, FlagType, Flag),
inherit_flags(Package, Dependency).
:- node_flag_set(Package, FlagType, Flag), can_inherit_flags(Package, Dependency, FlagType),
node_flag_propagate(Package, FlagType).
% Insure propagation
:- node_flag_inherited(Package, FlagType, Flag),
can_inherit_flags(Package, Dependency, FlagType),
node_flag_propagate(Package, FlagType).
% node with flags set to anythingg is "set"
node_flag_set(Package) :- node_flag_set(Package, _, _).
error(0, "{0} and dependency {1} cannot both propagate compiler flags '{2}'", Package, Dependency, FlagType) :-
node(Dependency),
node_flag_propagate(Package, FlagType),
node_flag_propagate(Dependency, FlagType),
path(Package, Dependency).
% remember where flags came from
node_flag_source(Package, Package) :- node_flag_set(Package).
node_flag_source(Dependency, Q)
:- node_flag_source(Package, Q), inherit_flags(Package, Dependency).
node_flag_source(Package, FlagType, Package) :- node_flag_set(Package, FlagType, _).
node_flag_source(Dependency, FlagType, Q)
:- node_flag_source(Package, FlagType, Q), node_flag_inherited(Dependency, FlagType, _),
node_flag_propagate(Package, FlagType).
% compiler flags from compilers.yaml are put on nodes if compiler matches
node_flag(Package, FlagType, Flag)
:- not node_flag_set(Package),
:- not node_flag_set(Package, FlagType, _),
compiler_version_flag(Compiler, Version, FlagType, Flag),
node_compiler_version(Package, Compiler, Version),
flag_type(FlagType),
@ -1035,7 +1068,7 @@ node_flag(Package, FlagType, Flag)
compiler_version(Compiler, Version).
node_flag_compiler_default(Package)
:- not node_flag_set(Package),
:- not node_flag_set(Package, FlagType, _),
compiler_version_flag(Compiler, Version, FlagType, Flag),
node_compiler_version(Package, Compiler, Version),
flag_type(FlagType),
@ -1054,6 +1087,7 @@ no_flags(Package, FlagType)
#defined compiler_version_flag/4.
#defined node_flag/3.
#defined node_flag_set/3.
#defined node_flag_propagate/2.
%-----------------------------------------------------------------------------

View File

@ -23,7 +23,7 @@
#show node_compiler_version/3.
#show node_flag/3.
#show node_flag_compiler_default/1.
#show node_flag_source/2.
#show node_flag_source/3.
#show no_flags/2.
#show external_spec_selected/2.
#show version_equivalent/3.

View File

@ -34,6 +34,8 @@
built in debug mode for your package to work, you can require it by
adding +debug to the openmpi spec when you depend on it. If you do
NOT want the debug option to be enabled, then replace this with -debug.
If you would like for the variant to be propagated through all your
package's dependencies use "++" for enabling and "--" or "~~" for disabling.
4. The name of the compiler to build with.
@ -51,8 +53,10 @@
spec-list = { spec [ dep-list ] }
dep_list = { ^ spec }
spec = id [ options ]
options = { @version-list | +variant | -variant | ~variant |
%compiler | arch=architecture | [ flag ]=value}
options = { @version-list | ++variant | +variant |
--variant | -variant | ~~variant | ~variant |
variant=value | variant==value | %compiler |
arch=architecture | [ flag ]==value | [ flag ]=value}
flag = { cflags | cxxflags | fcflags | fflags | cppflags |
ldflags | ldlibs }
variant = id
@ -124,6 +128,7 @@
"SpecParser",
"parse",
"SpecParseError",
"ArchitecturePropagationError",
"DuplicateDependencyError",
"DuplicateCompilerSpecError",
"UnsupportedCompilerError",
@ -148,15 +153,16 @@
is_windows = sys.platform == "win32"
#: Valid pattern for an identifier in Spack
identifier_re = r"\w[\w-]*"
compiler_color = "@g" #: color for highlighting compilers
version_color = "@c" #: color for highlighting versions
architecture_color = "@m" #: color for highlighting architectures
enabled_variant_color = "@B" #: color for highlighting enabled variants
disabled_variant_color = "@r" #: color for highlighting disabled varaints
dependency_color = "@." #: color for highlighting dependencies
hash_color = "@K" #: color for highlighting package hashes
compiler_color = "@g" #: color for highlighting compilers
version_color = "@c" #: color for highlighting versions
architecture_color ="@m" #: color for highlighting architectures
enabled_variant_color = "@B" #: color for highlighting enabled variants
disabled_variant_color = "r" #: color for highlighting disabled varaints
dependency_color = "@." #: color for highlighting dependencies
hash_color = "@K" #: color for highlighting package hashes
#: This map determines the coloring of specs when using color output.
#: We make the fields different colors to enhance readability.
@ -737,6 +743,22 @@ def flip(self):
return DependencySpec(parent=self.spec, spec=self.parent, deptypes=self.deptypes)
class CompilerFlag(str):
"""Will store a flag value and it's propagation value
Args:
value (str): the flag's value
propagate (bool): if ``True`` the flag value will
be passed to the package's dependencies. If
``False`` it will not
"""
def __new__(cls, value, **kwargs):
obj = str.__new__(cls, value)
obj.propagate = kwargs.pop("propagate", False)
return obj
_valid_compiler_flags = ["cflags", "cxxflags", "fflags", "ldflags", "ldlibs", "cppflags"]
@ -752,9 +774,20 @@ def satisfies(self, other, strict=False):
if strict or (self.spec and self.spec._concrete):
return all(f in self and set(self[f]) == set(other[f]) for f in other)
else:
return all(
if not all(
set(self[f]) == set(other[f]) for f in other if (other[f] != [] and f in self)
)
):
return False
# Check that the propagation values match
for flag_type in other:
if not all(
other[flag_type][i].propagate == self[flag_type][i].propagate
for i in range(len(other[flag_type]))
if flag_type in self
):
return False
return True
def constrain(self, other):
"""Add all flags in other that aren't in self to self.
@ -775,6 +808,14 @@ def constrain(self, other):
elif k not in self:
self[k] = other[k]
changed = True
# 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
@ -782,11 +823,41 @@ def valid_compiler_flags():
return _valid_compiler_flags
def copy(self):
clone = FlagMap(None)
for name, value in self.items():
clone[name] = value
clone = FlagMap(self.spec)
for name, compiler_flag in self.items():
clone[name] = compiler_flag
return clone
def add_flag(self, flag_type, value, propagation):
"""Stores the flag's value in CompilerFlag and adds it
to the FlagMap
Args:
flag_type (str): the type of flag
value (str): the flag's value that will be added to the flag_type's
corresponding list
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)
if flag_type not in self:
self[flag_type] = [flag]
else:
self[flag_type].append(flag)
def yaml_entry(self, flag_type):
"""Returns the flag type and a list of the flag values since the
propagation values aren't needed when writing to yaml
Args:
flag_type (str): the type of flag to get values from
Returns the flag_type and a list of the corresponding flags in
string format
"""
return flag_type, [str(flag) for flag in self[flag_type]]
def _cmp_iter(self):
for k, v in sorted(self.items()):
yield k
@ -803,7 +874,11 @@ def __str__(self):
return (
cond_symbol
+ " ".join(
str(key) + '="' + " ".join(str(f) for f in self[key]) + '"' for key in sorted_keys
key
+ ('=="' if True in [f.propagate for f in self[key]] else '="')
+ " ".join(self[key])
+ '"'
for key in sorted_keys
)
+ cond_symbol
)
@ -1405,10 +1480,26 @@ def _add_versions(self, version_list):
for version in version_list:
self.versions.add(version)
def _add_flag(self, name, value):
def _add_flag(self, name, value, propagate):
"""Called by the parser to add a known flag.
Known flags currently include "arch"
"""
# If the == syntax is used to propagate the spec architecture
# This is an error
architecture_names = [
"arch",
"architecture",
"platform",
"os",
"operating_system",
"target",
]
if propagate and name in architecture_names:
raise ArchitecturePropagationError(
"Unable to propagate the architecture failed." " Use a '=' instead."
)
valid_flags = FlagMap.valid_compiler_flags()
if name == "arch" or name == "architecture":
parts = tuple(value.split("-"))
@ -1422,16 +1513,18 @@ def _add_flag(self, name, value):
self._set_architecture(target=value)
elif name in valid_flags:
assert self.compiler_flags is not None
self.compiler_flags[name] = spack.compiler.tokenize_flags(value)
flags_and_propagation = spack.compiler.tokenize_flags(value, propagate)
for flag, propagation in flags_and_propagation:
self.compiler_flags.add_flag(name, flag, propagation)
else:
# FIXME:
# All other flags represent variants. 'foo=true' and 'foo=false'
# map to '+foo' and '~foo' respectively. As such they need a
# BoolValuedVariant instance.
if str(value).upper() == "TRUE" or str(value).upper() == "FALSE":
self.variants[name] = vt.BoolValuedVariant(name, value)
self.variants[name] = vt.BoolValuedVariant(name, value, propagate)
else:
self.variants[name] = vt.AbstractVariant(name, value)
self.variants[name] = vt.AbstractVariant(name, value, propagate)
def _set_architecture(self, **kwargs):
"""Called by the parser to set the architecture."""
@ -1783,7 +1876,14 @@ def to_node_dict(self, hash=ht.dag_hash):
params = syaml.syaml_dict(sorted(v.yaml_entry() for _, v in self.variants.items()))
params.update(sorted(self.compiler_flags.items()))
# Only need the string compiler flag for yaml file
params.update(
sorted(
self.compiler_flags.yaml_entry(flag_type)
for flag_type in self.compiler_flags.keys()
)
)
if params:
d["parameters"] = params
@ -2008,11 +2108,13 @@ def from_node_dict(node):
spec.compiler = None
if "parameters" in node:
for name, value in node["parameters"].items():
for name, values in node["parameters"].items():
if name in _valid_compiler_flags:
spec.compiler_flags[name] = value
spec.compiler_flags[name] = []
for val in values:
spec.compiler_flags.add_flag(name, val, False)
else:
spec.variants[name] = vt.MultiValuedVariant.from_node_dict(name, value)
spec.variants[name] = vt.MultiValuedVariant.from_node_dict(name, values)
elif "variants" in node:
for name, value in node["variants"].items():
spec.variants[name] = vt.MultiValuedVariant.from_node_dict(name, value)
@ -4122,7 +4224,7 @@ def write_attribute(spec, attribute, color):
except AttributeError:
parent = ".".join(parts[:idx])
m = "Attempted to format attribute %s." % attribute
m += "Spec.%s has no attribute %s" % (parent, part)
m += "Spec %s has no attribute %s" % (parent, part)
raise SpecFormatStringError(m)
if isinstance(current, vn.VersionList):
if current == _any_version:
@ -4859,7 +4961,7 @@ def __missing__(self, key):
#: These are possible token types in the spec grammar.
HASH, DEP, VER, COLON, COMMA, ON, OFF, PCT, EQ, ID, VAL, FILE = range(12)
HASH, DEP, VER, COLON, COMMA, ON, D_ON, OFF, D_OFF, PCT, EQ, D_EQ, ID, VAL, FILE = range(15)
#: Regex for fully qualified spec names. (e.g., builtin.hdf5)
spec_id_re = r"\w[\w.-]*"
@ -4882,15 +4984,19 @@ def __init__(self):
(
r"\@([\w.\-]*\s*)*(\s*\=\s*\w[\w.\-]*)?",
lambda scanner, val: self.token(VER, val),
),
(r"\:", lambda scanner, val: self.token(COLON, val)),
(r"\,", lambda scanner, val: self.token(COMMA, val)),
(r"\^", lambda scanner, val: self.token(DEP, val)),
(r"\+", lambda scanner, val: self.token(ON, val)),
(r"\-", lambda scanner, val: self.token(OFF, val)),
(r"\~", lambda scanner, val: self.token(OFF, val)),
(r"\%", lambda scanner, val: self.token(PCT, val)),
(r"\=", lambda scanner, val: self.token(EQ, val)),
),
(r"\:", lambda scanner, val: self.token(COLON, val)),
(r"\,", lambda scanner, val: self.token(COMMA, val)),
(r"\^", lambda scanner, val: self.token(DEP, val)),
(r"\+\+", lambda scanner, val: self.token(D_ON, val)),
(r"\+", lambda scanner, val: self.token(ON, val)),
(r"\-\-", lambda scanner, val: self.token(D_OFF, val)),
(r"\-", lambda scanner, val: self.token(OFF, val)),
(r"\~\~", lambda scanner, val: self.token(D_OFF, val)),
(r"\~", lambda scanner, val: self.token(OFF, val)),
(r"\%", lambda scanner, val: self.token(PCT, val)),
(r"\=\=", lambda scanner, val: self.token(D_EQ, val)),
(r"\=", lambda scanner, val: self.token(EQ, val)),
# Filenames match before identifiers, so no initial filename
# component is parsed as a spec (e.g., in subdir/spec.yaml/json)
(filename_reg, lambda scanner, v: self.token(FILE, v)),
@ -4901,7 +5007,7 @@ def __init__(self):
(spec_id_re, lambda scanner, val: self.token(ID, val)),
(r"\s+", lambda scanner, val: None),
],
[EQ],
[D_EQ, EQ],
[
(r"[\S].*", lambda scanner, val: self.token(VAL, val)),
(r"\s+", lambda scanner, val: None),
@ -4946,7 +5052,7 @@ def do_parse(self):
if self.accept(ID):
self.previous = self.token
if self.accept(EQ):
if self.accept(EQ) or self.accept(D_EQ):
# We're parsing an anonymous spec beginning with a
# key-value pair.
if not specs:
@ -5023,10 +5129,13 @@ def do_parse(self):
else:
# If the next token can be part of a valid anonymous spec,
# create the anonymous spec
if self.next.type in (VER, ON, OFF, PCT):
# Raise an error if the previous spec is already concrete
if specs and specs[-1].concrete:
raise RedundantSpecError(specs[-1], "compiler, version, " "or variant")
if self.next.type in (VER, ON, D_ON, OFF, D_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))
else:
self.unexpected_token()
@ -5135,22 +5244,36 @@ def spec(self, name):
vlist = self.version_list()
spec._add_versions(vlist)
elif self.accept(D_ON):
name = self.variant()
spec.variants[name] = vt.BoolValuedVariant(name, True, propagate=True)
elif self.accept(ON):
name = self.variant()
spec.variants[name] = vt.BoolValuedVariant(name, True)
spec.variants[name] = vt.BoolValuedVariant(name, True, propagate=False)
elif self.accept(D_OFF):
name = self.variant()
spec.variants[name] = vt.BoolValuedVariant(name, False, propagate=True)
elif self.accept(OFF):
name = self.variant()
spec.variants[name] = vt.BoolValuedVariant(name, False)
spec.variants[name] = vt.BoolValuedVariant(name, False, propagate=False)
elif self.accept(PCT):
spec._set_compiler(self.compiler())
elif self.accept(ID):
self.previous = self.token
if self.accept(EQ):
if self.accept(D_EQ):
# We're adding a key-value pair to the spec
self.expect(VAL)
spec._add_flag(self.previous.value, self.token.value)
spec._add_flag(self.previous.value, self.token.value, propagate=True)
self.previous = None
elif self.accept(EQ):
# We're adding a key-value pair to the spec
self.expect(VAL)
spec._add_flag(self.previous.value, self.token.value, propagate=False)
self.previous = None
else:
# We've found the start of a new spec. Go back to do_parse
@ -5313,6 +5436,12 @@ def long_message(self):
)
class ArchitecturePropagationError(spack.error.SpecError):
"""Raised when the double equal symbols are used to assign
the spec's architecture.
"""
class DuplicateDependencyError(spack.error.SpecError):
"""Raised when the same dependency occurs in a spec twice."""

View File

@ -46,21 +46,27 @@ def test_negative_integers_not_allowed_for_parallel_jobs(job_parser):
@pytest.mark.parametrize(
"specs,cflags,negated_variants",
"specs,cflags,propagation,negated_variants",
[
(['coreutils cflags="-O3 -g"'], ["-O3", "-g"], []),
(["coreutils", "cflags=-O3 -g"], ["-O3"], ["g"]),
(["coreutils", "cflags=-O3", "-g"], ["-O3"], ["g"]),
(['coreutils cflags="-O3 -g"'], ["-O3", "-g"], [False, False], []),
(['coreutils cflags=="-O3 -g"'], ["-O3", "-g"], [True, True], []),
(["coreutils", "cflags=-O3 -g"], ["-O3"], [False], ["g"]),
(["coreutils", "cflags==-O3 -g"], ["-O3"], [True], ["g"]),
(["coreutils", "cflags=-O3", "-g"], ["-O3"], [False], ["g"]),
],
)
@pytest.mark.regression("12951")
def test_parse_spec_flags_with_spaces(specs, cflags, negated_variants):
def test_parse_spec_flags_with_spaces(specs, cflags, propagation, negated_variants):
spec_list = spack.cmd.parse_specs(specs)
assert len(spec_list) == 1
s = spec_list.pop()
assert s.compiler_flags["cflags"] == cflags
compiler_flags = [flag for flag in s.compiler_flags["cflags"]]
flag_propagation = [flag.propagate for flag in s.compiler_flags["cflags"]]
assert compiler_flags == cflags
assert flag_propagation == propagation
assert list(s.variants.keys()) == negated_variants
for v in negated_variants:
assert "~{0}".format(v) in s

View File

@ -2,6 +2,7 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os
import posixpath
import sys
@ -81,6 +82,7 @@ def check_concretize(abstract_spec):
"mpich",
# compiler flags
'mpich cppflags="-O3"',
'mpich cppflags=="-O3"',
# with virtual
"mpileaks ^mpi",
"mpileaks ^mpi@:1.1",
@ -323,6 +325,33 @@ def test_different_compilers_get_different_flags(self):
assert set(client.compiler_flags["fflags"]) == set(["-O0", "-g"])
assert not set(cmake.compiler_flags["fflags"])
def test_concretize_compiler_flag_propagate(self):
spec = Spec("hypre cflags=='-g' ^openblas")
spec.concretize()
assert spec.satisfies("^openblas cflags='-g'")
@pytest.mark.skipif(
os.environ.get("SPACK_TEST_SOLVER") == "original" or sys.platform == "win32",
reason="Optional compiler propagation isn't deprecated for original concretizer",
)
def test_concretize_compiler_flag_does_not_propagate(self):
spec = Spec("hypre cflags='-g' ^openblas")
spec.concretize()
assert not spec.satisfies("^openblas cflags='-g'")
@pytest.mark.skipif(
os.environ.get("SPACK_TEST_SOLVER") == "original" or sys.platform == "win32",
reason="Optional compiler propagation isn't deprecated for original concretizer",
)
def test_concretize_propagate_compiler_flag_not_passed_to_dependent(self):
spec = Spec("hypre cflags=='-g' ^openblas cflags='-O3'")
spec.concretize()
assert set(spec.compiler_flags["cflags"]) == set(["-g"])
assert spec.satisfies("^openblas cflags='-O3'")
def test_architecture_inheritance(self):
"""test_architecture_inheritance is likely to fail with an
UnavailableCompilerVersionError if the architecture is concretized
@ -401,6 +430,24 @@ def test_concretize_two_virtuals_with_dual_provider_and_a_conflict(self):
with pytest.raises(spack.error.SpackError):
s.concretize()
@pytest.mark.skipif(
os.environ.get("SPACK_TEST_SOLVER") == "original" or sys.platform == "win32",
reason="Optional compiler propagation isn't deprecated for original concretizer",
)
def test_concretize_propagate_disabled_variant(self):
"""Test a package variant value was passed from its parent."""
spec = Spec("hypre~~shared ^openblas")
spec.concretize()
assert spec.satisfies("^openblas~shared")
def test_concretize_propagated_variant_is_not_passed_to_dependent(self):
"""Test a package variant value was passed from its parent."""
spec = Spec("hypre~~shared ^openblas+shared")
spec.concretize()
assert spec.satisfies("^openblas+shared")
@pytest.mark.skipif(sys.platform == "win32", reason="No Compiler for Arch on Win")
def test_no_matching_compiler_specs(self, mock_low_high_config):
# only relevant when not building compilers as needed

View File

@ -247,16 +247,23 @@ def test_satisfies_virtual_dependency_versions(self):
def test_satisfies_matching_variant(self):
check_satisfies("mpich+foo", "mpich+foo")
check_satisfies("mpich++foo", "mpich++foo")
check_satisfies("mpich~foo", "mpich~foo")
check_satisfies("mpich~~foo", "mpich~~foo")
check_satisfies("mpich foo=1", "mpich foo=1")
check_satisfies("mpich foo==1", "mpich foo==1")
# confirm that synonymous syntax works correctly
check_satisfies("mpich+foo", "mpich foo=True")
check_satisfies("mpich++foo", "mpich foo=True")
check_satisfies("mpich foo=true", "mpich+foo")
check_satisfies("mpich foo==true", "mpich++foo")
check_satisfies("mpich~foo", "mpich foo=FALSE")
check_satisfies("mpich~~foo", "mpich foo=FALSE")
check_satisfies("mpich foo=False", "mpich~foo")
check_satisfies("mpich foo==False", "mpich~foo")
check_satisfies("mpich foo=*", "mpich~foo")
check_satisfies("mpich +foo", "mpich foo=*")
check_satisfies("mpich+foo", "mpich foo=*")
def test_satisfies_multi_value_variant(self):
# Check quoting
@ -295,6 +302,7 @@ def test_satisfies_single_valued_variant(self):
# Assert that an autospec generated from a literal
# gives the right result for a single valued variant
assert "foobar=bar" in a
assert "foobar==bar" in a
assert "foobar=baz" not in a
assert "foobar=fee" not in a
@ -415,21 +423,32 @@ def test_unsatisfiable_variants(self):
check_satisfies("mpich", "mpich+foo", False)
check_satisfies("mpich", "mpich~foo", False)
check_satisfies("mpich", "mpich foo=1", False)
check_satisfies("mpich", "mpich++foo", False)
check_satisfies("mpich", "mpich~~foo", False)
check_satisfies("mpich", "mpich foo==1", False)
# 'mpich' is concrete:
check_unsatisfiable("mpich", "mpich+foo", True)
check_unsatisfiable("mpich", "mpich~foo", True)
check_unsatisfiable("mpich", "mpich foo=1", True)
check_unsatisfiable("mpich", "mpich++foo", True)
check_unsatisfiable("mpich", "mpich~~foo", True)
check_unsatisfiable("mpich", "mpich foo==1", True)
def test_unsatisfiable_variant_mismatch(self):
# No matchi in specs
check_unsatisfiable("mpich~foo", "mpich+foo")
check_unsatisfiable("mpich+foo", "mpich~foo")
check_unsatisfiable("mpich foo=True", "mpich foo=False")
check_unsatisfiable("mpich~~foo", "mpich++foo")
check_unsatisfiable("mpich++foo", "mpich~~foo")
check_unsatisfiable("mpich foo==True", "mpich foo==False")
def test_satisfies_matching_compiler_flag(self):
check_satisfies('mpich cppflags="-O3"', 'mpich cppflags="-O3"')
check_satisfies('mpich cppflags="-O3 -Wall"', 'mpich cppflags="-O3 -Wall"')
check_satisfies('mpich cppflags=="-O3"', 'mpich cppflags=="-O3"')
check_satisfies('mpich cppflags=="-O3 -Wall"', 'mpich cppflags=="-O3 -Wall"')
def test_satisfies_unconstrained_compiler_flag(self):
# only asked for mpich, no constraints. Any will do.
@ -453,8 +472,9 @@ def test_copy_satisfies_transitive(self):
assert copy[s.name].satisfies(s)
def test_unsatisfiable_compiler_flag_mismatch(self):
# No matchi in specs
# No match in specs
check_unsatisfiable('mpich cppflags="-O3"', 'mpich cppflags="-O2"')
check_unsatisfiable('mpich cppflags="-O3"', 'mpich cppflags=="-O3"')
def test_satisfies_virtual(self):
# Don't use check_satisfies: it checks constrain() too, and
@ -554,6 +574,12 @@ def test_constrain_variants(self):
check_constrain("libelf+debug~foo", "libelf+debug", "libelf~foo")
check_constrain("libelf+debug~foo", "libelf+debug", "libelf+debug~foo")
check_constrain("libelf++debug++foo", "libelf++debug", "libelf+debug+foo")
check_constrain("libelf debug==2 foo==1", "libelf debug==2", "libelf foo=1")
check_constrain("libelf debug==2 foo==1", "libelf debug==2", "libelf debug=2 foo=1")
check_constrain("libelf++debug~~foo", "libelf++debug", "libelf++debug~foo")
def test_constrain_multi_value_variant(self):
check_constrain(
'multivalue-variant foo="bar,baz"',
@ -582,6 +608,17 @@ def test_constrain_compiler_flags(self):
'libelf cflags="-O3" cppflags="-Wall"',
)
check_constrain(
'libelf cflags="-O3" cppflags=="-Wall"',
'libelf cppflags=="-Wall"',
'libelf cflags="-O3"',
)
check_constrain(
'libelf cflags=="-O3" cppflags=="-Wall"',
'libelf cflags=="-O3"',
'libelf cflags=="-O3" cppflags=="-Wall"',
)
def test_constrain_architecture(self):
check_constrain(
"libelf target=default_target os=default_os",
@ -620,6 +657,7 @@ def test_constrain_changed(self):
check_constrain_changed("libelf", "~debug")
check_constrain_changed("libelf", "debug=2")
check_constrain_changed("libelf", 'cppflags="-O3"')
check_constrain_changed("libelf", 'cppflags=="-O3"')
platform = spack.platforms.host()
check_constrain_changed("libelf", "target=" + platform.target("default_target").name)
@ -636,6 +674,7 @@ def test_constrain_not_changed(self):
check_constrain_not_changed("libelf debug=2", "debug=2")
check_constrain_not_changed("libelf debug=2", "debug=*")
check_constrain_not_changed('libelf cppflags="-O3"', 'cppflags="-O3"')
check_constrain_not_changed('libelf cppflags=="-O3"', 'cppflags=="-O3"')
platform = spack.platforms.host()
default_target = platform.target("default_target").name
@ -791,7 +830,7 @@ def test_spec_formatting_escapes(self):
spec.format(fmt_str)
def test_spec_deprecated_formatting(self):
spec = Spec("libelf cflags=-O2")
spec = Spec("libelf cflags==-O2")
spec.concretize()
# Since the default is the full spec see if the string rep of

View File

@ -245,8 +245,9 @@ class AbstractVariant(object):
values.
"""
def __init__(self, name, value):
def __init__(self, name, value, propagate=False):
self.name = name
self.propagate = propagate
# Stores 'value' after a bit of massaging
# done by the property setter
@ -334,7 +335,7 @@ def copy(self):
>>> assert a == b
>>> assert a is not b
"""
return type(self)(self.name, self._original_value)
return type(self)(self.name, self._original_value, self.propagate)
@implicit_variant_conversion
def satisfies(self, other):
@ -401,6 +402,8 @@ def __repr__(self):
return "{0.__name__}({1}, {2})".format(cls, repr(self.name), repr(self._original_value))
def __str__(self):
if self.propagate:
return "{0}=={1}".format(self.name, ",".join(str(x) for x in self.value))
return "{0}={1}".format(self.name, ",".join(str(x) for x in self.value))
@ -444,6 +447,9 @@ def __str__(self):
values_str = ",".join(x[:7] for x in self.value)
else:
values_str = ",".join(str(x) for x in self.value)
if self.propagate:
return "{0}=={1}".format(self.name, values_str)
return "{0}={1}".format(self.name, values_str)
@ -460,6 +466,8 @@ def _value_setter(self, value):
self._value = str(self._value[0])
def __str__(self):
if self.propagate:
return "{0}=={1}".format(self.name, self.value)
return "{0}={1}".format(self.name, self.value)
@implicit_variant_conversion
@ -523,6 +531,8 @@ def __contains__(self, item):
return item is self.value
def __str__(self):
if self.propagate:
return "{0}{1}".format("++" if self.value else "~~", self.name)
return "{0}{1}".format("+" if self.value else "~", self.name)

View File

@ -2,6 +2,7 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import sys
from spack.package import *
@ -17,3 +18,9 @@ class Hypre(Package):
depends_on("lapack")
depends_on("blas")
variant(
"shared",
default=(sys.platform != "darwin"),
description="Build shared library (disables static library)",
)

View File

@ -17,6 +17,8 @@ class Openblas(Package):
version("0.2.14", "b1190f3d3471685f17cfd1ec1d252ac9")
version("0.2.13", "b1190f3d3471685f17cfd1ec1d252ac9")
variant("shared", default=True, description="Build shared libraries")
# See #20019 for this conflict
conflicts("%gcc@:4.4", when="@0.2.14:")