From bc209c470d07621ba9af277dab71d048d11f2844 Mon Sep 17 00:00:00 2001 From: Kayla Butler Date: Mon, 28 Mar 2022 14:18:00 -0700 Subject: 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. --- lib/spack/docs/basic_usage.rst | 38 +++- lib/spack/spack/cmd/__init__.py | 3 +- lib/spack/spack/cmd/deactivate.py | 2 +- lib/spack/spack/cmd/extensions.py | 2 +- lib/spack/spack/compiler.py | 20 +- lib/spack/spack/provider_index.py | 3 +- lib/spack/spack/solver/asp.py | 69 ++++--- lib/spack/spack/solver/concretize.lp | 64 ++++-- lib/spack/spack/solver/display.lp | 2 +- lib/spack/spack/spec.py | 217 ++++++++++++++++----- lib/spack/spack/test/cmd/common/arguments.py | 18 +- lib/spack/spack/test/concretize.py | 47 +++++ lib/spack/spack/test/spec_semantics.py | 45 ++++- lib/spack/spack/variant.py | 14 +- .../repos/builtin.mock/packages/hypre/package.py | 7 + .../builtin.mock/packages/openblas/package.py | 2 + 16 files changed, 435 insertions(+), 118 deletions(-) diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst index 95c37dd8bf..73895449b0 100644 --- a/lib/spack/docs/basic_usage.rst +++ b/lib/spack/docs/basic_usage.rst @@ -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=`` Optional variant specifiers that are not restricted to - boolean variants + boolean variants. Use ``name==`` to propagate variant through the + dependencies. * ``name=`` Optional compiler flag specifiers. Valid flag names are ``cflags``, ``cxxflags``, ``fflags``, ``cppflags``, ``ldflags``, and ``ldlibs``. + Use ``name==`` to propagate compiler flags through the dependencies. * ``target= os=`` 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"``, diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index e829166d39..ee1297e51c 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -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() diff --git a/lib/spack/spack/cmd/deactivate.py b/lib/spack/spack/cmd/deactivate.py index d68341037f..2fa18fc2b1 100644 --- a/lib/spack/spack/cmd/deactivate.py +++ b/lib/spack/spack/cmd/deactivate.py @@ -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) diff --git a/lib/spack/spack/cmd/extensions.py b/lib/spack/spack/cmd/extensions.py index 50f41529b2..fa79f8df2d 100644 --- a/lib/spack/spack/cmd/extensions.py +++ b/lib/spack/spack/cmd/extensions.py @@ -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) diff --git a/lib/spack/spack/compiler.py b/lib/spack/spack/compiler.py index 2e86309699..37bc250de8 100644 --- a/lib/spack/spack/compiler.py +++ b/lib/spack/spack/compiler.py @@ -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 @@ class Compiler(object): # 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 diff --git a/lib/spack/spack/provider_index.py b/lib/spack/spack/provider_index.py index 2da7d05e94..254492fc21 100644 --- a/lib/spack/spack/provider_index.py +++ b/lib/spack/spack/provider_index.py @@ -175,9 +175,10 @@ class ProviderIndex(_IndexBase): 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): diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index af55b757be..fffa58f337 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -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 @@ class PyclingoDriver(object): if output.timers: timer.write_tty() print() + if output.stats: print("Statistics:") pprint.pprint(self.control.statistics) @@ -1359,6 +1348,8 @@ class SpackSolverSetup(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 SpackSolverSetup(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 SpackSolverSetup(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 SpackSolverSetup(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 @@ class SpecBuilder(object): # 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 @@ class SpecBuilder(object): 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 @@ class SpecBuilder(object): 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 @@ class SpecBuilder(object): 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 @@ class SpecBuilder(object): 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) diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp index 8bef7f2c1e..a176803074 100644 --- a/lib/spack/spack/solver/concretize.lp +++ b/lib/spack/spack/solver/concretize.lp @@ -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 with flags set to anythingg is "set" -node_flag_set(Package) :- node_flag_set(Package, _, _). + :- 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). + +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. %----------------------------------------------------------------------------- diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp index e8f5eeba16..4ba8e9e2cb 100644 --- a/lib/spack/spack/solver/display.lp +++ b/lib/spack/spack/solver/display.lp @@ -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. diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index bf0acf58d8..194726baa9 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -34,6 +34,8 @@ line is a spec for a particular installation of the mpileaks package. 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 @@ Here is the EBNF grammar for a spec:: 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 @@ __all__ = [ "SpecParser", "parse", "SpecParseError", + "ArchitecturePropagationError", "DuplicateDependencyError", "DuplicateCompilerSpecError", "UnsupportedCompilerError", @@ -148,15 +153,16 @@ __all__ = [ 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 @@ class DependencySpec(object): 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 @@ class FlagMap(lang.HashableMap): 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 @@ class FlagMap(lang.HashableMap): 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 @@ class FlagMap(lang.HashableMap): 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 @@ class FlagMap(lang.HashableMap): 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 @@ class Spec(object): 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 @@ class Spec(object): 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 @@ class Spec(object): 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 @@ class Spec(object): 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 @@ class Spec(object): 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 @@ class LazySpecCache(collections.defaultdict): #: 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 @@ class SpecLexer(spack.parse.Lexer): ( 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 @@ class SpecLexer(spack.parse.Lexer): (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 @@ class SpecParser(spack.parse.Parser): 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 @@ class SpecParser(spack.parse.Parser): 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 @@ class SpecParser(spack.parse.Parser): 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 @@ class SpecParseError(spack.error.SpecError): ) +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.""" diff --git a/lib/spack/spack/test/cmd/common/arguments.py b/lib/spack/spack/test/cmd/common/arguments.py index 772cc0297e..d56ad59c85 100644 --- a/lib/spack/spack/test/cmd/common/arguments.py +++ b/lib/spack/spack/test/cmd/common/arguments.py @@ -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 diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index bf0110f3e6..43f47259bf 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -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 @@ class TestConcretize(object): 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 @@ class TestConcretize(object): 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 diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py index 711e901416..04aa82797c 100644 --- a/lib/spack/spack/test/spec_semantics.py +++ b/lib/spack/spack/test/spec_semantics.py @@ -247,16 +247,23 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): # 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 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): '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 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): 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 @@ class TestSpecSematics(object): 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 diff --git a/lib/spack/spack/variant.py b/lib/spack/spack/variant.py index 92e512219b..cfb43cfc70 100644 --- a/lib/spack/spack/variant.py +++ b/lib/spack/spack/variant.py @@ -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 @@ class AbstractVariant(object): >>> 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 @@ class AbstractVariant(object): 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 @@ class MultiValuedVariant(AbstractVariant): 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 @@ class SingleValuedVariant(AbstractVariant): 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 @@ class BoolValuedVariant(SingleValuedVariant): 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) diff --git a/var/spack/repos/builtin.mock/packages/hypre/package.py b/var/spack/repos/builtin.mock/packages/hypre/package.py index fe077f067d..7182cf628a 100644 --- a/var/spack/repos/builtin.mock/packages/hypre/package.py +++ b/var/spack/repos/builtin.mock/packages/hypre/package.py @@ -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)", + ) diff --git a/var/spack/repos/builtin.mock/packages/openblas/package.py b/var/spack/repos/builtin.mock/packages/openblas/package.py index 1e948da8f2..8c2f26de57 100644 --- a/var/spack/repos/builtin.mock/packages/openblas/package.py +++ b/var/spack/repos/builtin.mock/packages/openblas/package.py @@ -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:") -- cgit v1.2.3-70-g09d2