diff options
Diffstat (limited to 'lib')
69 files changed, 2001 insertions, 1812 deletions
diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst index cf1a9ec673..af6d2dab91 100644 --- a/lib/spack/docs/basic_usage.rst +++ b/lib/spack/docs/basic_usage.rst @@ -1103,16 +1103,31 @@ Below are more details about the specifiers that you can add to specs. Version specifier ^^^^^^^^^^^^^^^^^ -A version specifier comes somewhere after a package name and starts -with ``@``. It can be a single version, e.g. ``@1.0``, ``@3``, or -``@1.2a7``. Or, it can be a range of versions, such as ``@1.0:1.5`` -(all versions between ``1.0`` and ``1.5``, inclusive). Version ranges -can be open, e.g. ``:3`` means any version up to and including ``3``. -This would include ``3.4`` and ``3.4.2``. ``4.2:`` means any version -above and including ``4.2``. Finally, a version specifier can be a -set of arbitrary versions, such as ``@1.0,1.5,1.7`` (``1.0``, ``1.5``, -or ``1.7``). When you supply such a specifier to ``spack install``, -it constrains the set of versions that Spack will install. +A version specifier ``pkg@<specifier>`` comes after a package name +and starts with ``@``. It can be something abstract that matches +multiple known versions, or a specific version. During concretization, +Spack will pick the optimal version within the spec's constraints +according to policies set for the particular Spack installation. + +The version specifier can be *a specific version*, such as ``@=1.0.0`` or +``@=1.2a7``. Or, it can be *a range of versions*, such as ``@1.0:1.5``. +Version ranges are inclusive, so this example includes both ``1.0`` +and any ``1.5.x`` version. Version ranges can be unbounded, e.g. ``@:3`` +means any version up to and including ``3``. This would include ``3.4`` +and ``3.4.2``. Similarly, ``@4.2:`` means any version above and including +``4.2``. As a short-hand, ``@3`` is equivalent to the range ``@3:3`` and +includes any version with major version ``3``. + +Notice that you can distinguish between the specific version ``@=3.2`` and +the range ``@3.2``. This is useful for packages that follow a versioning +scheme that omits the zero patch version number: ``3.2``, ``3.2.1``, +``3.2.2``, etc. In general it is preferable to use the range syntax +``@3.2``, since ranges also match versions with one-off suffixes, such as +``3.2-custom``. + +A version specifier can also be a list of ranges and specific versions, +separated by commas. For example, ``@1.0:1.5,=1.7.1`` matches any version +in the range ``1.0:1.5`` and the specific version ``1.7.1``. For packages with a ``git`` attribute, ``git`` references may be specified instead of a numerical version i.e. branches, tags @@ -1121,36 +1136,35 @@ reference provided. Acceptable syntaxes for this are: .. code-block:: sh + # commit hashes + foo@abcdef1234abcdef1234abcdef1234abcdef1234 # 40 character hashes are automatically treated as git commits + foo@git.abcdef1234abcdef1234abcdef1234abcdef1234 + # branches and tags foo@git.develop # use the develop branch foo@git.0.19 # use the 0.19 tag - # commit hashes - foo@abcdef1234abcdef1234abcdef1234abcdef1234 # 40 character hashes are automatically treated as git commits - foo@git.abcdef1234abcdef1234abcdef1234abcdef1234 +Spack always needs to associate a Spack version with the git reference, +which is used for version comparison. This Spack version is heuristically +taken from the closest valid git tag among ancestors of the git ref. + +Once a Spack version is associated with a git ref, it always printed with +the git ref. For example, if the commit ``@git.abcdefg`` is tagged +``0.19``, then the spec will be shown as ``@git.abcdefg=0.19``. -Spack versions from git reference either have an associated version supplied by the user, -or infer a relationship to known versions from the structure of the git repository. If an -associated version is supplied by the user, Spack treats the git version as equivalent to that -version for all version comparisons in the package logic (e.g. ``depends_on('foo', when='@1.5')``). +If the git ref is not exactly a tag, then the distance to the nearest tag +is also part of the resolved version. ``@git.abcdefg=0.19.git.8`` means +that the commit is 8 commits away from the ``0.19`` tag. -The associated version can be assigned with ``[git ref]=[version]`` syntax, with the caveat that the specified version is known to Spack from either the package definition, or in the configuration preferences (i.e. ``packages.yaml``). +In cases where Spack cannot resolve a sensible version from a git ref, +users can specify the Spack version to use for the git ref. This is done +by appending ``=`` and the Spack version to the git ref. For example: .. code-block:: sh foo@git.my_ref=3.2 # use the my_ref tag or branch, but treat it as version 3.2 for version comparisons foo@git.abcdef1234abcdef1234abcdef1234abcdef1234=develop # use the given commit, but treat it as develop for version comparisons -If an associated version is not supplied then the tags in the git repo are used to determine -the most recent previous version known to Spack. Details about how versions are compared -and how Spack determines if one version is less than another are discussed in the developer guide. - -If the version spec is not provided, then Spack will choose one -according to policies set for the particular spack installation. If -the spec is ambiguous, i.e. it could match multiple versions, Spack -will choose a version within the spec's constraints according to -policies set for the particular Spack installation. - Details about how versions are compared and how Spack determines if one version is less than another are discussed in the developer guide. diff --git a/lib/spack/docs/conf.py b/lib/spack/docs/conf.py index 2b04fa642f..dafe318275 100644 --- a/lib/spack/docs/conf.py +++ b/lib/spack/docs/conf.py @@ -215,7 +215,7 @@ nitpick_ignore = [ ("py:class", "spack.repo._PrependFileLoader"), ("py:class", "spack.build_systems._checks.BaseBuilder"), # Spack classes that intersphinx is unable to resolve - ("py:class", "spack.version.VersionBase"), + ("py:class", "spack.version.StandardVersion"), ("py:class", "spack.spec.DependencySpec"), ] diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index b6ab8f7230..1a90b8d417 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -851,16 +851,16 @@ Version comparison ^^^^^^^^^^^^^^^^^^ Most Spack versions are numeric, a tuple of integers; for example, -``apex@0.1``, ``ferret@6.96`` or ``py-netcdf@1.2.3.1``. Spack knows -how to compare and sort numeric versions. +``0.1``, ``6.96`` or ``1.2.3.1``. Spack knows how to compare and sort +numeric versions. Some Spack versions involve slight extensions of numeric syntax; for -example, ``py-sphinx-rtd-theme@0.1.10a0``. In this case, numbers are +example, ``py-sphinx-rtd-theme@=0.1.10a0``. In this case, numbers are always considered to be "newer" than letters. This is for consistency with `RPM <https://bugzilla.redhat.com/show_bug.cgi?id=50977>`_. Spack versions may also be arbitrary non-numeric strings, for example -``@develop``, ``@master``, ``@local``. +``develop``, ``master``, ``local``. The order on versions is defined as follows. A version string is split into a list of components based on delimiters such as ``.``, ``-`` etc. @@ -918,6 +918,32 @@ use: will be used. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Ranges versus specific versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When specifying versions in Spack using the ``pkg@<specifier>`` syntax, +you can use either ranges or specific versions. It is generally +recommended to use ranges instead of specific versions when packaging +to avoid overly constraining dependencies, patches, and conflicts. + +For example, ``depends_on("python@3")`` denotes a range of versions, +allowing Spack to pick any ``3.x.y`` version for Python, while +``depends_on("python@=3.10.1")`` restricts it to a specific version. + +Specific ``@=`` versions should only be used in exceptional cases, such +as when the package has a versioning scheme that omits the zero in the +first patch release: ``3.1``, ``3.1.1``, ``3.1.2``. In this example, +the specifier ``@=3.1`` is the correct way to select only the ``3.1`` +version, whereas ``@3.1`` would match all those versions. + +Ranges are preferred even if they would only match a single version +defined in the package. This is because users can define custom versions +in ``packages.yaml`` that typically include a custom suffix. For example, +if the package defines the version ``1.2.3``, the specifier ``@1.2.3`` +will also match a user-defined version ``1.2.3-custom``. + + .. _cmd-spack-checksum: ^^^^^^^^^^^^^^^^^^ @@ -2388,21 +2414,29 @@ requires Python 2, you can similarly leave out the lower bound: Notice that we didn't use ``@:3``. Version ranges are *inclusive*, so ``@:3`` means "up to and including any 3.x version". -What if a package can only be built with Python 2.7? You might be -inclined to use: +You can also simply write .. code-block:: python depends_on("python@2.7") -However, this would be wrong. Spack assumes that all version constraints -are exact, so it would try to install Python not at ``2.7.18``, but -exactly at ``2.7``, which is a non-existent version. The correct way to -specify this would be: +to tell Spack that the package needs Python 2.7.x. This is equivalent to +``@2.7:2.7``. + +In very rare cases, you may need to specify an exact version, for example +if you need to distinguish between ``3.2`` and ``3.2.1``: + +.. code-block:: python + + depends_on("pkg@=3.2") + +But in general, you should try to use version ranges as much as possible, +so that custom suffixes are included too. The above example can be +rewritten in terms of ranges as follows: .. code-block:: python - depends_on("python@2.7.0:2.7") + depends_on("pkg@3.2:3.2.0") A spec can contain a version list of ranges and individual versions separated by commas. For example, if you need Boost 1.59.0 or newer, diff --git a/lib/spack/spack/binary_distribution.py b/lib/spack/spack/binary_distribution.py index e974eb2237..ca69695f11 100644 --- a/lib/spack/spack/binary_distribution.py +++ b/lib/spack/spack/binary_distribution.py @@ -776,11 +776,10 @@ def tarball_directory_name(spec): Return name of the tarball directory according to the convention <os>-<architecture>/<compiler>/<package>-<version>/ """ - return "%s/%s/%s-%s" % ( - spec.architecture, - str(spec.compiler).replace("@", "-"), - spec.name, - spec.version, + return os.path.join( + str(spec.architecture), + f"{spec.compiler.name}-{spec.compiler.version}", + f"{spec.name}-{spec.version}", ) @@ -789,13 +788,9 @@ def tarball_name(spec, ext): Return the name of the tarfile according to the convention <os>-<architecture>-<package>-<dag_hash><ext> """ - return "%s-%s-%s-%s-%s%s" % ( - spec.architecture, - str(spec.compiler).replace("@", "-"), - spec.name, - spec.version, - spec.dag_hash(), - ext, + return ( + f"{spec.architecture}-{spec.compiler.name}-{spec.compiler.version}-" + f"{spec.name}-{spec.version}-{spec.dag_hash()}{ext}" ) diff --git a/lib/spack/spack/build_systems/intel.py b/lib/spack/spack/build_systems/intel.py index 771761119a..dbb8313acf 100644 --- a/lib/spack/spack/build_systems/intel.py +++ b/lib/spack/spack/build_systems/intel.py @@ -142,7 +142,7 @@ class IntelPackage(Package): # The Intel libraries are provided without requiring a license as of # version 2017.2. Trying to specify one anyway will fail. See: # https://software.intel.com/en-us/articles/free-ipsxe-tools-and-libraries - return self._has_compilers or self.version < ver("2017.2") + return self._has_compilers or self.version < Version("2017.2") #: Comment symbol used in the license.lic file license_comment = "#" @@ -341,7 +341,7 @@ class IntelPackage(Package): v_year = year break - return ver("%s.%s" % (v_year, v_tail)) + return Version("%s.%s" % (v_year, v_tail)) # --------------------------------------------------------------------- # Directory handling common to all Intel components @@ -764,9 +764,9 @@ class IntelPackage(Package): elif matches: # TODO: Confirm that this covers clang (needed on Linux only) gcc_version = Version(matches.groups()[1]) - if gcc_version >= ver("4.7"): + if gcc_version >= Version("4.7"): abi = "gcc4.7" - elif gcc_version >= ver("4.4"): + elif gcc_version >= Version("4.4"): abi = "gcc4.4" else: abi = "gcc4.1" # unlikely, one hopes. @@ -1019,7 +1019,7 @@ class IntelPackage(Package): # Intel MPI since 2019 depends on libfabric which is not in the # lib directory but in a directory of its own which should be # included in the rpath - if self.version_yearlike >= ver("2019"): + if self.version_yearlike >= Version("2019"): d = ancestor(self.component_lib_dir("mpi")) if "+external-libfabric" in self.spec: result += self.spec["libfabric"].libs diff --git a/lib/spack/spack/build_systems/python.py b/lib/spack/spack/build_systems/python.py index 4641f85172..8df7144999 100644 --- a/lib/spack/spack/build_systems/python.py +++ b/lib/spack/spack/build_systems/python.py @@ -290,7 +290,7 @@ class PythonPackage(PythonExtension): python_external_config = spack.config.get("packages:python:externals", []) python_externals_configured = [ - spack.spec.Spec(item["spec"]) + spack.spec.parse_with_version_concrete(item["spec"]) for item in python_external_config if item["prefix"] == self.spec.external_path ] diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index 2dae43d41b..9ca3ae4c6d 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -231,7 +231,7 @@ def parse_specs(args, **kwargs): msg += "\n\n" msg += unquoted_flags.report() - raise spack.error.SpackError(msg) + raise spack.error.SpackError(msg) from e def matching_spec_from_env(spec): diff --git a/lib/spack/spack/cmd/checksum.py b/lib/spack/spack/cmd/checksum.py index a80aaa2eec..efb27a410c 100644 --- a/lib/spack/spack/cmd/checksum.py +++ b/lib/spack/spack/cmd/checksum.py @@ -19,7 +19,7 @@ import spack.util.crypto from spack.package_base import deprecated_version, preferred_version from spack.util.editor import editor from spack.util.naming import valid_fully_qualified_module_name -from spack.version import VersionBase, ver +from spack.version import Version description = "checksum available versions of a package" section = "packaging" @@ -83,9 +83,10 @@ def checksum(parser, args): pkg = pkg_cls(spack.spec.Spec(args.package)) url_dict = {} - versions = args.versions - if (not versions) and args.preferred: + if not args.versions and args.preferred: versions = [preferred_version(pkg)] + else: + versions = [Version(v) for v in args.versions] if versions: remote_versions = None @@ -93,12 +94,6 @@ def checksum(parser, args): if deprecated_version(pkg, version): tty.warn("Version {0} is deprecated".format(version)) - version = ver(version) - if not isinstance(version, VersionBase): - tty.die( - "Cannot generate checksums for version lists or " - "version ranges. Use unambiguous versions." - ) url = pkg.find_valid_url_for_version(version) if url is not None: url_dict[version] = url diff --git a/lib/spack/spack/cmd/dev_build.py b/lib/spack/spack/cmd/dev_build.py index 4ba0033873..c837b58c62 100644 --- a/lib/spack/spack/cmd/dev_build.py +++ b/lib/spack/spack/cmd/dev_build.py @@ -107,7 +107,7 @@ def dev_build(self, args): " Use `spack create` to create a new package", ) - if not spec.versions.concrete: + if not spec.versions.concrete_range_as_version: tty.die( "spack dev-build spec must have a single, concrete version. " "Did you forget a package version number?" diff --git a/lib/spack/spack/cmd/develop.py b/lib/spack/spack/cmd/develop.py index 15354bdb0c..4746f9c96e 100644 --- a/lib/spack/spack/cmd/develop.py +++ b/lib/spack/spack/cmd/develop.py @@ -9,7 +9,9 @@ import llnl.util.tty as tty import spack.cmd import spack.cmd.common.arguments as arguments +import spack.spec import spack.util.path +import spack.version from spack.error import SpackError description = "add a spec to an environment's dev-build information" @@ -61,7 +63,9 @@ def develop(parser, args): tty.msg(msg) continue - spec = spack.spec.Spec(entry["spec"]) + # Both old syntax `spack develop pkg@x` and new syntax `spack develop pkg@=x` + # are currently supported. + spec = spack.spec.parse_with_version_concrete(entry["spec"]) pkg_cls = spack.repo.path.get_pkg_class(spec.name) pkg_cls(spec).stage.steal_source(abspath) @@ -75,9 +79,12 @@ def develop(parser, args): raise SpackError("spack develop requires at most one named spec") spec = specs[0] - if not spec.versions.concrete: + version = spec.versions.concrete_range_as_version + if not version: raise SpackError("Packages to develop must have a concrete version") + spec.versions = spack.version.VersionList([version]) + # default path is relative path to spec.name path = args.path or spec.name abspath = spack.util.path.canonicalize_path(path, default_wd=env.path) diff --git a/lib/spack/spack/cmd/solve.py b/lib/spack/spack/cmd/solve.py index 3752c16f41..fbb4d358ef 100644 --- a/lib/spack/spack/cmd/solve.py +++ b/lib/spack/spack/cmd/solve.py @@ -141,7 +141,7 @@ def _process_result(result, show, required_format, kwargs): def solve(parser, args): # these are the same options as `spack spec` name_fmt = "{namespace}.{name}" if args.namespaces else "{name}" - fmt = "{@version}{%compiler}{compiler_flags}{variants}{arch=architecture}" + fmt = "{@versions}{%compiler}{compiler_flags}{variants}{arch=architecture}" install_status_fn = spack.spec.Spec.install_status kwargs = { "cover": args.cover, diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py index b31c7a93b7..fc3ff05437 100644 --- a/lib/spack/spack/cmd/spec.py +++ b/lib/spack/spack/cmd/spec.py @@ -81,7 +81,7 @@ for further documentation regarding the spec syntax, see: def spec(parser, args): name_fmt = "{namespace}.{name}" if args.namespaces else "{name}" - fmt = "{@version}{%compiler}{compiler_flags}{variants}{arch=architecture}" + fmt = "{@versions}{%compiler}{compiler_flags}{variants}{arch=architecture}" install_status_fn = spack.spec.Spec.install_status tree_kwargs = { "cover": args.cover, diff --git a/lib/spack/spack/compilers/__init__.py b/lib/spack/spack/compilers/__init__.py index b5dc6c4a8f..aedaaaa501 100644 --- a/lib/spack/spack/compilers/__init__.py +++ b/lib/spack/spack/compilers/__init__.py @@ -24,6 +24,7 @@ import spack.error import spack.paths import spack.platforms import spack.spec +import spack.version from spack.util.environment import get_path from spack.util.naming import mod_to_class @@ -69,7 +70,7 @@ def pkg_spec_for_compiler(cspec): break else: spec_str = str(cspec) - return spack.spec.Spec(spec_str) + return spack.spec.parse_with_version_concrete(spec_str) def _auto_compiler_spec(function): @@ -213,7 +214,7 @@ def all_compilers_config(scope=None, init_config=True): def all_compiler_specs(scope=None, init_config=True): # Return compiler specs from the merged config. return [ - spack.spec.CompilerSpec(s["compiler"]["spec"]) + spack.spec.parse_with_version_concrete(s["compiler"]["spec"], compiler=True) for s in all_compilers_config(scope, init_config) ] @@ -384,7 +385,7 @@ class CacheReference(object): def compiler_from_dict(items): - cspec = spack.spec.CompilerSpec(items["spec"]) + cspec = spack.spec.parse_with_version_concrete(items["spec"], compiler=True) os = items.get("operating_system", None) target = items.get("target", None) @@ -453,7 +454,10 @@ def get_compilers(config, cspec=None, arch_spec=None): for items in config: items = items["compiler"] - if cspec and items["spec"] != str(cspec): + + # NOTE: in principle this should be equality not satisfies, but config can still + # be written in old format gcc@10.1.0 instead of gcc@=10.1.0. + if cspec and not cspec.satisfies(items["spec"]): continue # If an arch spec is given, confirm that this compiler diff --git a/lib/spack/spack/compilers/aocc.py b/lib/spack/spack/compilers/aocc.py index 4597d1a2f5..51f7b02e2b 100644 --- a/lib/spack/spack/compilers/aocc.py +++ b/lib/spack/spack/compilers/aocc.py @@ -143,5 +143,5 @@ class Aocc(Compiler): def _handle_default_flag_addtions(self): # This is a known issue for AOCC 3.0 see: # https://developer.amd.com/wp-content/resources/AOCC-3.0-Install-Guide.pdf - if self.real_version == ver("3.0.0"): + if self.real_version.satisfies(ver("3.0.0")): return "-Wno-unused-command-line-argument " "-mllvm -eliminate-similar-expr=false" diff --git a/lib/spack/spack/compilers/apple_clang.py b/lib/spack/spack/compilers/apple_clang.py index 8a39b6427a..cb3c5d2646 100644 --- a/lib/spack/spack/compilers/apple_clang.py +++ b/lib/spack/spack/compilers/apple_clang.py @@ -13,7 +13,7 @@ from llnl.util.symlink import symlink import spack.compiler import spack.compilers.clang import spack.util.executable -import spack.version +from spack.version import Version class AppleClang(spack.compilers.clang.Clang): @@ -41,7 +41,7 @@ class AppleClang(spack.compilers.clang.Clang): @property def cxx11_flag(self): # Spack's AppleClang detection only valid from Xcode >= 4.6 - if self.real_version < spack.version.ver("4.0"): + if self.real_version < Version("4.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++11 standard", "cxx11_flag", "Xcode < 4.0" ) @@ -49,38 +49,38 @@ class AppleClang(spack.compilers.clang.Clang): @property def cxx14_flag(self): - if self.real_version < spack.version.ver("5.1"): + if self.real_version < Version("5.1"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++14 standard", "cxx14_flag", "Xcode < 5.1" ) - elif self.real_version < spack.version.ver("6.1"): + elif self.real_version < Version("6.1"): return "-std=c++1y" return "-std=c++14" @property def cxx17_flag(self): - if self.real_version < spack.version.ver("6.1"): + if self.real_version < Version("6.1"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++17 standard", "cxx17_flag", "Xcode < 6.1" ) - elif self.real_version < spack.version.ver("10.0"): + elif self.real_version < Version("10.0"): return "-std=c++1z" return "-std=c++17" @property def cxx20_flag(self): - if self.real_version < spack.version.ver("10.0"): + if self.real_version < Version("10.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++20 standard", "cxx20_flag", "Xcode < 10.0" ) - elif self.real_version < spack.version.ver("13.0"): + elif self.real_version < Version("13.0"): return "-std=c++2a" return "-std=c++20" @property def cxx23_flag(self): - if self.real_version < spack.version.ver("13.0"): + if self.real_version < Version("13.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++23 standard", "cxx23_flag", "Xcode < 13.0" ) @@ -90,7 +90,7 @@ class AppleClang(spack.compilers.clang.Clang): @property def c99_flag(self): - if self.real_version < spack.version.ver("4.0"): + if self.real_version < Version("4.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C99 standard", "c99_flag", "< 4.0" ) @@ -98,7 +98,7 @@ class AppleClang(spack.compilers.clang.Clang): @property def c11_flag(self): - if self.real_version < spack.version.ver("4.0"): + if self.real_version < Version("4.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C11 standard", "c11_flag", "< 4.0" ) @@ -106,7 +106,7 @@ class AppleClang(spack.compilers.clang.Clang): @property def c17_flag(self): - if self.real_version < spack.version.ver("11.0"): + if self.real_version < Version("11.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C17 standard", "c17_flag", "< 11.0" ) @@ -114,7 +114,7 @@ class AppleClang(spack.compilers.clang.Clang): @property def c23_flag(self): - if self.real_version < spack.version.ver("11.0.3"): + if self.real_version < Version("11.0.3"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C23 standard", "c23_flag", "< 11.0.3" ) diff --git a/lib/spack/spack/compilers/cce.py b/lib/spack/spack/compilers/cce.py index cd9d9f43f1..b2840a8229 100644 --- a/lib/spack/spack/compilers/cce.py +++ b/lib/spack/spack/compilers/cce.py @@ -5,7 +5,7 @@ import os from spack.compiler import Compiler, UnsupportedCompilerFlag -from spack.version import ver +from spack.version import Version class Cce(Compiler): @@ -58,7 +58,7 @@ class Cce(Compiler): @property def is_clang_based(self): version = self._real_version or self.version - return version >= ver("9.0") and "classic" not in str(version) + return version >= Version("9.0") and "classic" not in str(version) version_argument = "--version" version_regex = r"[Cc]ray (?:clang|C :|C\+\+ :|Fortran :) [Vv]ersion.*?(\d+(\.\d+)+)" @@ -98,9 +98,9 @@ class Cce(Compiler): def c99_flag(self): if self.is_clang_based: return "-std=c99" - elif self.real_version >= ver("8.4"): + elif self.real_version >= Version("8.4"): return "-h std=c99,noconform,gnu" - elif self.real_version >= ver("8.1"): + elif self.real_version >= Version("8.1"): return "-h c99,noconform,gnu" raise UnsupportedCompilerFlag(self, "the C99 standard", "c99_flag", "< 8.1") @@ -108,7 +108,7 @@ class Cce(Compiler): def c11_flag(self): if self.is_clang_based: return "-std=c11" - elif self.real_version >= ver("8.5"): + elif self.real_version >= Version("8.5"): return "-h std=c11,noconform,gnu" raise UnsupportedCompilerFlag(self, "the C11 standard", "c11_flag", "< 8.5") diff --git a/lib/spack/spack/compilers/clang.py b/lib/spack/spack/compilers/clang.py index 53535256ae..a9356227de 100644 --- a/lib/spack/spack/compilers/clang.py +++ b/lib/spack/spack/compilers/clang.py @@ -10,7 +10,7 @@ import sys import llnl.util.lang from spack.compiler import Compiler, UnsupportedCompilerFlag -from spack.version import ver +from spack.version import Version #: compiler symlink mappings for mixed f77 compilers f77_mapping = [ @@ -100,24 +100,24 @@ class Clang(Compiler): @property def cxx11_flag(self): - if self.real_version < ver("3.3"): + if self.real_version < Version("3.3"): raise UnsupportedCompilerFlag(self, "the C++11 standard", "cxx11_flag", "< 3.3") return "-std=c++11" @property def cxx14_flag(self): - if self.real_version < ver("3.4"): + if self.real_version < Version("3.4"): raise UnsupportedCompilerFlag(self, "the C++14 standard", "cxx14_flag", "< 3.5") - elif self.real_version < ver("3.5"): + elif self.real_version < Version("3.5"): return "-std=c++1y" return "-std=c++14" @property def cxx17_flag(self): - if self.real_version < ver("3.5"): + if self.real_version < Version("3.5"): raise UnsupportedCompilerFlag(self, "the C++17 standard", "cxx17_flag", "< 3.5") - elif self.real_version < ver("5.0"): + elif self.real_version < Version("5.0"): return "-std=c++1z" return "-std=c++17" @@ -128,21 +128,21 @@ class Clang(Compiler): @property def c11_flag(self): - if self.real_version < ver("3.0"): + if self.real_version < Version("3.0"): raise UnsupportedCompilerFlag(self, "the C11 standard", "c11_flag", "< 3.0") - if self.real_version < ver("3.1"): + if self.real_version < Version("3.1"): return "-std=c1x" return "-std=c11" @property def c17_flag(self): - if self.real_version < ver("6.0"): + if self.real_version < Version("6.0"): raise UnsupportedCompilerFlag(self, "the C17 standard", "c17_flag", "< 6.0") return "-std=c17" @property def c23_flag(self): - if self.real_version < ver("9.0"): + if self.real_version < Version("9.0"): raise UnsupportedCompilerFlag(self, "the C23 standard", "c23_flag", "< 9.0") return "-std=c2x" diff --git a/lib/spack/spack/compilers/gcc.py b/lib/spack/spack/compilers/gcc.py index 8a099417f4..ae8d5aa97e 100644 --- a/lib/spack/spack/compilers/gcc.py +++ b/lib/spack/spack/compilers/gcc.py @@ -11,7 +11,7 @@ from llnl.util.filesystem import ancestor import spack.compiler import spack.compilers.apple_clang as apple_clang import spack.util.executable -from spack.version import ver +from spack.version import Version class Gcc(spack.compiler.Compiler): @@ -61,47 +61,47 @@ class Gcc(spack.compiler.Compiler): @property def cxx98_flag(self): - if self.real_version < ver("6.0"): + if self.real_version < Version("6.0"): return "" else: return "-std=c++98" @property def cxx11_flag(self): - if self.real_version < ver("4.3"): + if self.real_version < Version("4.3"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++11 standard", "cxx11_flag", " < 4.3" ) - elif self.real_version < ver("4.7"): + elif self.real_version < Version("4.7"): return "-std=c++0x" else: return "-std=c++11" @property def cxx14_flag(self): - if self.real_version < ver("4.8"): + if self.real_version < Version("4.8"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++14 standard", "cxx14_flag", "< 4.8" ) - elif self.real_version < ver("4.9"): + elif self.real_version < Version("4.9"): return "-std=c++1y" else: return "-std=c++14" @property def cxx17_flag(self): - if self.real_version < ver("5.0"): + if self.real_version < Version("5.0"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C++17 standard", "cxx17_flag", "< 5.0" ) - elif self.real_version < ver("6.0"): + elif self.real_version < Version("6.0"): return "-std=c++1z" else: return "-std=c++17" @property def c99_flag(self): - if self.real_version < ver("4.5"): + if self.real_version < Version("4.5"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C99 standard", "c99_flag", "< 4.5" ) @@ -109,7 +109,7 @@ class Gcc(spack.compiler.Compiler): @property def c11_flag(self): - if self.real_version < ver("4.7"): + if self.real_version < Version("4.7"): raise spack.compiler.UnsupportedCompilerFlag( self, "the C11 standard", "c11_flag", "< 4.7" ) @@ -157,7 +157,7 @@ class Gcc(spack.compiler.Compiler): return "unknown" version = super(Gcc, cls).default_version(cc) - if ver(version) >= ver("7"): + if Version(version) >= Version("7"): output = spack.compiler.get_compiler_version_output(cc, "-dumpfullversion") version = cls.extract_version_from_output(output) return version @@ -187,7 +187,7 @@ class Gcc(spack.compiler.Compiler): output = spack.compiler.get_compiler_version_output(fc, "-dumpversion") match = re.search(r"(?:GNU Fortran \(GCC\) )?([\d.]+)", output) version = match.group(match.lastindex) if match else "unknown" - if ver(version) >= ver("7"): + if Version(version) >= Version("7"): output = spack.compiler.get_compiler_version_output(fc, "-dumpfullversion") version = cls.extract_version_from_output(output) return version diff --git a/lib/spack/spack/compilers/intel.py b/lib/spack/spack/compilers/intel.py index 4ecd3c9ef4..4ec2960525 100644 --- a/lib/spack/spack/compilers/intel.py +++ b/lib/spack/spack/compilers/intel.py @@ -7,7 +7,7 @@ import os import sys from spack.compiler import Compiler, UnsupportedCompilerFlag -from spack.version import ver +from spack.version import Version class Intel(Compiler): @@ -60,17 +60,17 @@ class Intel(Compiler): @property def openmp_flag(self): - if self.real_version < ver("16.0"): + if self.real_version < Version("16.0"): return "-openmp" else: return "-qopenmp" @property def cxx11_flag(self): - if self.real_version < ver("11.1"): + if self.real_version < Version("11.1"): raise UnsupportedCompilerFlag(self, "the C++11 standard", "cxx11_flag", "< 11.1") - elif self.real_version < ver("13"): + elif self.real_version < Version("13"): return "-std=c++0x" else: return "-std=c++11" @@ -78,23 +78,23 @@ class Intel(Compiler): @property def cxx14_flag(self): # Adapted from CMake's Intel-CXX rules. - if self.real_version < ver("15"): + if self.real_version < Version("15"): raise UnsupportedCompilerFlag(self, "the C++14 standard", "cxx14_flag", "< 15") - elif self.real_version < ver("15.0.2"): + elif self.real_version < Version("15.0.2"): return "-std=c++1y" else: return "-std=c++14" @property def c99_flag(self): - if self.real_version < ver("12"): + if self.real_version < Version("12"): raise UnsupportedCompilerFlag(self, "the C99 standard", "c99_flag", "< 12") else: return "-std=c99" @property def c11_flag(self): - if self.real_version < ver("16"): + if self.real_version < Version("16"): raise UnsupportedCompilerFlag(self, "the C11 standard", "c11_flag", "< 16") else: return "-std=c1x" diff --git a/lib/spack/spack/compilers/pgi.py b/lib/spack/spack/compilers/pgi.py index 85e450d4b1..77a0db7860 100644 --- a/lib/spack/spack/compilers/pgi.py +++ b/lib/spack/spack/compilers/pgi.py @@ -6,7 +6,7 @@ import os from spack.compiler import Compiler, UnsupportedCompilerFlag -from spack.version import ver +from spack.version import Version class Pgi(Compiler): @@ -77,13 +77,13 @@ class Pgi(Compiler): @property def c99_flag(self): - if self.real_version >= ver("12.10"): + if self.real_version >= Version("12.10"): return "-c99" raise UnsupportedCompilerFlag(self, "the C99 standard", "c99_flag", "< 12.10") @property def c11_flag(self): - if self.real_version >= ver("15.3"): + if self.real_version >= Version("15.3"): return "-c11" raise UnsupportedCompilerFlag(self, "the C11 standard", "c11_flag", "< 15.3") diff --git a/lib/spack/spack/compilers/xl.py b/lib/spack/spack/compilers/xl.py index 8d70838904..db7241cb0f 100644 --- a/lib/spack/spack/compilers/xl.py +++ b/lib/spack/spack/compilers/xl.py @@ -6,7 +6,7 @@ import os from spack.compiler import Compiler, UnsupportedCompilerFlag -from spack.version import ver +from spack.version import Version class Xl(Compiler): @@ -51,24 +51,24 @@ class Xl(Compiler): @property def cxx11_flag(self): - if self.real_version < ver("13.1"): + if self.real_version < Version("13.1"): raise UnsupportedCompilerFlag(self, "the C++11 standard", "cxx11_flag", "< 13.1") else: return "-qlanglvl=extended0x" @property def c99_flag(self): - if self.real_version >= ver("13.1.1"): + if self.real_version >= Version("13.1.1"): return "-std=gnu99" - if self.real_version >= ver("10.1"): + if self.real_version >= Version("10.1"): return "-qlanglvl=extc99" raise UnsupportedCompilerFlag(self, "the C99 standard", "c99_flag", "< 10.1") @property def c11_flag(self): - if self.real_version >= ver("13.1.2"): + if self.real_version >= Version("13.1.2"): return "-std=gnu11" - if self.real_version >= ver("12.1"): + if self.real_version >= Version("12.1"): return "-qlanglvl=extc1x" raise UnsupportedCompilerFlag(self, "the C11 standard", "c11_flag", "< 12.1") @@ -76,7 +76,7 @@ class Xl(Compiler): def cxx14_flag(self): # .real_version does not have the "y.z" component of "w.x.y.z", which # is required to distinguish whether support is available - if self.version >= ver("16.1.1.8"): + if self.version >= Version("16.1.1.8"): return "-std=c++14" raise UnsupportedCompilerFlag(self, "the C++14 standard", "cxx14_flag", "< 16.1.1.8") diff --git a/lib/spack/spack/concretize.py b/lib/spack/spack/concretize.py index 3452e42738..74eb6ea05d 100644 --- a/lib/spack/spack/concretize.py +++ b/lib/spack/spack/concretize.py @@ -41,7 +41,7 @@ import spack.util.path import spack.variant as vt from spack.config import config from spack.package_prefs import PackagePrefs, is_spec_buildable, spec_externals -from spack.version import Version, VersionList, VersionRange, ver +from spack.version import ClosedOpenRange, VersionList, ver #: impements rudimentary logic for ABI compatibility _abi: Union[spack.abi.ABI, llnl.util.lang.Singleton] = llnl.util.lang.Singleton( @@ -219,7 +219,7 @@ class Concretizer(object): # Respect order listed in packages.yaml -yaml_prefs(v), # The preferred=True flag (packages or packages.yaml or both?) - pkg_versions.get(Version(v)).get("preferred", False), + pkg_versions.get(v).get("preferred", False), # ------- Regular case: use latest non-develop version by default. # Avoid @develop version, which would otherwise be the "largest" # in straight version comparisons @@ -246,11 +246,12 @@ class Concretizer(object): raise NoValidVersionError(spec) else: last = spec.versions[-1] - if isinstance(last, VersionRange): - if last.end: - spec.versions = ver([last.end]) + if isinstance(last, ClosedOpenRange): + range_as_version = VersionList([last]).concrete_range_as_version + if range_as_version: + spec.versions = ver([range_as_version]) else: - spec.versions = ver([last.start]) + raise NoValidVersionError(spec) else: spec.versions = ver([last]) diff --git a/lib/spack/spack/cray_manifest.py b/lib/spack/spack/cray_manifest.py index 6f686aaf57..4fdbc095e5 100644 --- a/lib/spack/spack/cray_manifest.py +++ b/lib/spack/spack/cray_manifest.py @@ -11,7 +11,11 @@ import jsonschema.exceptions import llnl.util.tty as tty import spack.cmd +import spack.error import spack.hash_types as hash_types +import spack.platforms +import spack.repo +import spack.spec from spack.schema.cray_manifest import schema as manifest_schema #: Cray systems can store a Spack-compatible description of system @@ -74,13 +78,13 @@ def spec_from_entry(entry): compiler_str = "" if "compiler" in entry: - compiler_format = "%{name}@{version}" + compiler_format = "%{name}@={version}" compiler_str = compiler_format.format( name=translated_compiler_name(entry["compiler"]["name"]), version=entry["compiler"]["version"], ) - spec_format = "{name}@{version} {compiler} {arch}" + spec_format = "{name}@={version} {compiler} {arch}" spec_str = spec_format.format( name=entry["name"], version=entry["version"], compiler=compiler_str, arch=arch_str ) diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py index af321323c9..f75e540a70 100644 --- a/lib/spack/spack/database.py +++ b/lib/spack/spack/database.py @@ -46,10 +46,10 @@ import spack.spec import spack.store import spack.util.lock as lk import spack.util.spack_json as sjson +import spack.version as vn from spack.directory_layout import DirectoryLayoutError, InconsistentInstallDirectoryError from spack.error import SpackError from spack.util.crypto import bit_length -from spack.version import Version # TODO: Provide an API automatically retyring a build after detecting and # TODO: clearing a failure. @@ -60,7 +60,7 @@ _db_dirname = ".spack-db" # DB version. This is stuck in the DB file to track changes in format. # Increment by one when the database format changes. # Versions before 5 were not integers. -_db_version = Version("6") +_db_version = vn.Version("6") # For any version combinations here, skip reindex when upgrading. # Reindexing can take considerable time and is not always necessary. @@ -70,8 +70,8 @@ _skip_reindex = [ # only difference is that v5 can contain "deprecated_for" # fields. So, skip the reindex for this transition. The new # version is saved to disk the first time the DB is written. - (Version("0.9.3"), Version("5")), - (Version("5"), Version("6")), + (vn.Version("0.9.3"), vn.Version("5")), + (vn.Version("5"), vn.Version("6")), ] # Default timeout for spack database locks in seconds or None (no timeout). @@ -105,7 +105,7 @@ default_install_record_fields = [ def reader(version): - reader_cls = {Version("5"): spack.spec.SpecfileV1, Version("6"): spack.spec.SpecfileV3} + reader_cls = {vn.Version("5"): spack.spec.SpecfileV1, vn.Version("6"): spack.spec.SpecfileV3} return reader_cls[version] @@ -694,8 +694,7 @@ class Database(object): spec_dict[hash.name] = hash_key # Build spec from dict first. - spec = spec_reader.from_node_dict(spec_dict) - return spec + return spec_reader.from_node_dict(spec_dict) def db_for_spec_hash(self, hash_key): with self.read_transaction(): @@ -798,7 +797,7 @@ class Database(object): installs = db["installs"] # TODO: better version checking semantics. - version = Version(db["version"]) + version = vn.Version(db["version"]) spec_reader = reader(version) if version > _db_version: raise InvalidDatabaseVersionError(_db_version, version) @@ -816,9 +815,11 @@ class Database(object): ) def invalid_record(hash_key, error): - msg = "Invalid record in Spack database: " "hash: %s, cause: %s: %s" - msg %= (hash_key, type(error).__name__, str(error)) - raise CorruptDatabaseError(msg, self._index_path) + return CorruptDatabaseError( + f"Invalid record in Spack database: hash: {hash_key}, cause: " + f"{type(error).__name__}: {error}", + self._index_path, + ) # Build up the database in three passes: # @@ -846,7 +847,7 @@ class Database(object): if not spec.external and "installed" in rec and rec["installed"]: installed_prefixes.add(rec["path"]) except Exception as e: - invalid_record(hash_key, e) + raise invalid_record(hash_key, e) from e # Pass 2: Assign dependencies once all specs are created. for hash_key in data: @@ -855,7 +856,7 @@ class Database(object): except MissingDependenciesError: raise except Exception as e: - invalid_record(hash_key, e) + raise invalid_record(hash_key, e) from e # Pass 3: Mark all specs concrete. Specs representing real # installations must be explicitly marked. diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py index 341b8b2c1f..27075d47b4 100644 --- a/lib/spack/spack/directives.py +++ b/lib/spack/spack/directives.py @@ -32,7 +32,7 @@ import collections.abc import functools import os.path import re -from typing import List, Optional, Set +from typing import List, Optional, Set, Union import llnl.util.lang import llnl.util.tty.color @@ -45,7 +45,13 @@ import spack.variant from spack.dependency import Dependency, canonical_deptype, default_deptype from spack.fetch_strategy import from_kwargs from spack.resource import Resource -from spack.version import GitVersion, Version, VersionChecksumError, VersionLookupError +from spack.version import ( + GitVersion, + Version, + VersionChecksumError, + VersionError, + VersionLookupError, +) __all__ = [ "DirectiveError", @@ -318,7 +324,7 @@ directive = DirectiveMeta.directive @directive("versions") def version( - ver: str, + ver: Union[str, int], # this positional argument is deprecated, use sha256=... instead checksum: Optional[str] = None, *, @@ -362,64 +368,72 @@ def version( The (keyword) arguments are turned into a valid fetch strategy for code packages later. See ``spack.fetch_strategy.for_package_version()``. """ + kwargs = { + key: value + for key, value in ( + ("sha256", sha256), + ("sha384", sha384), + ("sha512", sha512), + ("preferred", preferred), + ("deprecated", deprecated), + ("expand", expand), + ("url", url), + ("extension", extension), + ("no_cache", no_cache), + ("fetch_options", fetch_options), + ("git", git), + ("svn", svn), + ("hg", hg), + ("cvs", cvs), + ("get_full_repo", get_full_repo), + ("branch", branch), + ("submodules", submodules), + ("submodules_delete", submodules_delete), + ("commit", commit), + ("tag", tag), + ("revision", revision), + ("date", date), + ("md5", md5), + ("sha1", sha1), + ("sha224", sha224), + ("checksum", checksum), + ) + if value is not None + } + return lambda pkg: _execute_version(pkg, ver, **kwargs) - def _execute_version(pkg): - if ( - any((sha256, sha384, sha512, md5, sha1, sha224, checksum)) - and hasattr(pkg, "has_code") - and not pkg.has_code - ): - raise VersionChecksumError( - "{0}: Checksums not allowed in no-code packages " - "(see '{1}' version).".format(pkg.name, ver) - ) - kwargs = { - key: value - for key, value in ( - ("sha256", sha256), - ("sha384", sha384), - ("sha512", sha512), - ("preferred", preferred), - ("deprecated", deprecated), - ("expand", expand), - ("url", url), - ("extension", extension), - ("no_cache", no_cache), - ("fetch_options", fetch_options), - ("git", git), - ("svn", svn), - ("hg", hg), - ("cvs", cvs), - ("get_full_repo", get_full_repo), - ("branch", branch), - ("submodules", submodules), - ("submodules_delete", submodules_delete), - ("commit", commit), - ("tag", tag), - ("revision", revision), - ("date", date), - ("md5", md5), - ("sha1", sha1), - ("sha224", sha224), - ("checksum", checksum), - ) - if value is not None - } - - # Store kwargs for the package to later with a fetch_strategy. - version = Version(ver) - if isinstance(version, GitVersion): - if git is None and not hasattr(pkg, "git"): - msg = "Spack version directives cannot include git hashes fetched from" - msg += " URLs. Error in package '%s'\n" % pkg.name - msg += " version('%s', " % version.string - msg += ", ".join("%s='%s'" % (argname, value) for argname, value in kwargs.items()) - msg += ")" - raise VersionLookupError(msg) - pkg.versions[version] = kwargs - - return _execute_version +def _execute_version(pkg, ver, **kwargs): + if ( + any( + s in kwargs + for s in ("sha256", "sha384", "sha512", "md5", "sha1", "sha224", "checksum") + ) + and hasattr(pkg, "has_code") + and not pkg.has_code + ): + raise VersionChecksumError( + "{0}: Checksums not allowed in no-code packages " + "(see '{1}' version).".format(pkg.name, ver) + ) + + if not isinstance(ver, (int, str)): + raise VersionError( + f"{pkg.name}: declared version '{ver!r}' in package should be a string or int." + ) + + # Declared versions are concrete + version = Version(ver) + + if isinstance(version, GitVersion) and not hasattr(pkg, "git") and "git" not in kwargs: + args = ", ".join(f"{argname}='{value}'" for argname, value in kwargs.items()) + raise VersionLookupError( + f"{pkg.name}: spack version directives cannot include git hashes fetched from URLs.\n" + f" version('{ver}', {args})" + ) + + # Store kwargs for the package to later with a fetch_strategy. + pkg.versions[version] = kwargs def _depends_on(pkg, spec, when=None, type=default_deptype, patches=None): diff --git a/lib/spack/spack/environment/environment.py b/lib/spack/spack/environment/environment.py index b3d4176c04..7774810102 100644 --- a/lib/spack/spack/environment/environment.py +++ b/lib/spack/spack/environment/environment.py @@ -49,6 +49,7 @@ import spack.util.path import spack.util.spack_json as sjson import spack.util.spack_yaml as syaml import spack.util.url +import spack.version from spack.filesystem_view import SimpleFilesystemView, inverse_view_func_parser, view_func_parser from spack.installer import PackageInstaller from spack.spec import Spec @@ -774,7 +775,7 @@ class Environment: self.views: Dict[str, ViewDescriptor] = {} #: Specs from "spack.yaml" - self.spec_lists = {user_speclist_name: SpecList()} + self.spec_lists: Dict[str, SpecList] = {user_speclist_name: SpecList()} #: Dev-build specs from "spack.yaml" self.dev_specs: Dict[str, Any] = {} #: User specs from the last concretization @@ -863,7 +864,7 @@ class Environment: self.dev_specs = copy.deepcopy(configuration.get("develop", {})) for name, entry in self.dev_specs.items(): # spec must include a concrete version - assert Spec(entry["spec"]).version.concrete + assert Spec(entry["spec"]).versions.concrete_range_as_version # default path is the spec name if "path" not in entry: self.dev_specs[name]["path"] = name @@ -1139,21 +1140,21 @@ class Environment: def change_existing_spec( self, - change_spec, - list_name=user_speclist_name, - match_spec=None, + change_spec: Spec, + list_name: str = user_speclist_name, + match_spec: Optional[Spec] = None, allow_changing_multiple_specs=False, ): """ Find the spec identified by `match_spec` and change it to `change_spec`. Arguments: - change_spec (spack.spec.Spec): defines the spec properties that + change_spec: defines the spec properties that need to be changed. This will not change attributes of the matched spec unless they conflict with `change_spec`. - list_name (str): identifies the spec list in the environment that + list_name: identifies the spec list in the environment that should be modified - match_spec (spack.spec.Spec): if set, this identifies the spec + match_spec: if set, this identifies the spec that should be changed. If not set, it is assumed we are looking for a spec with the same name as `change_spec`. """ @@ -1252,15 +1253,15 @@ class Environment: del self.concretized_order[i] del self.specs_by_hash[dag_hash] - def develop(self, spec, path, clone=False): + def develop(self, spec: Spec, path: str, clone: bool = False) -> bool: """Add dev-build info for package Args: - spec (spack.spec.Spec): Set constraints on development specs. Must include a + spec: Set constraints on development specs. Must include a concrete version. - path (str): Path to find code for developer builds. Relative + path: Path to find code for developer builds. Relative paths will be resolved relative to the environment. - clone (bool): Clone the package code to the path. + clone: Clone the package code to the path. If clone is False Spack will assume the code is already present at ``path``. diff --git a/lib/spack/spack/fetch_strategy.py b/lib/spack/spack/fetch_strategy.py index 07d6a99966..d5530db524 100644 --- a/lib/spack/spack/fetch_strategy.py +++ b/lib/spack/spack/fetch_strategy.py @@ -874,12 +874,12 @@ class GitFetchStrategy(VCSFetchStrategy): # If we want a particular branch ask for it. if branch: args.extend(["--branch", branch]) - elif tag and self.git_version >= spack.version.ver("1.8.5.2"): + elif tag and self.git_version >= spack.version.Version("1.8.5.2"): args.extend(["--branch", tag]) # Try to be efficient if we're using a new enough git. # This checks out only one branch's history - if self.git_version >= spack.version.ver("1.7.10"): + if self.git_version >= spack.version.Version("1.7.10"): if self.get_full_repo: args.append("--no-single-branch") else: @@ -890,7 +890,7 @@ class GitFetchStrategy(VCSFetchStrategy): # tree, if the in-use git and protocol permit it. if ( (not self.get_full_repo) - and self.git_version >= spack.version.ver("1.7.1") + and self.git_version >= spack.version.Version("1.7.1") and self.protocol_supports_shallow_clone() ): args.extend(["--depth", "1"]) @@ -907,7 +907,7 @@ class GitFetchStrategy(VCSFetchStrategy): # For tags, be conservative and check them out AFTER # cloning. Later git versions can do this with clone # --branch, but older ones fail. - if tag and self.git_version < spack.version.ver("1.8.5.2"): + if tag and self.git_version < spack.version.Version("1.8.5.2"): # pull --tags returns a "special" error code of 1 in # older versions that we have to ignore. # see: https://github.com/git/git/commit/19d122b @@ -1516,7 +1516,7 @@ def for_package_version(pkg, version=None): assert not pkg.spec.concrete, "concrete specs should not pass the 'version=' argument" # Specs are initialized with the universe range, if no version information is given, # so here we make sure we always match the version passed as argument - if not isinstance(version, spack.version.VersionBase): + if not isinstance(version, spack.version.StandardVersion): version = spack.version.Version(version) version_list = spack.version.VersionList() @@ -1529,10 +1529,10 @@ def for_package_version(pkg, version=None): if isinstance(version, spack.version.GitVersion): if not hasattr(pkg, "git"): raise web_util.FetchError( - "Cannot fetch git version for %s. Package has no 'git' attribute" % pkg.name + f"Cannot fetch git version for {pkg.name}. Package has no 'git' attribute" ) # Populate the version with comparisons to other commits - version.generate_git_lookup(pkg.name) + version.attach_git_lookup_from_package(pkg.name) # For GitVersion, we have no way to determine whether a ref is a branch or tag # Fortunately, we handle branches and tags identically, except tags are @@ -1545,15 +1545,11 @@ def for_package_version(pkg, version=None): kwargs["submodules"] = getattr(pkg, "submodules", False) - # if we have a ref_version already, and it is a version from the package - # we can use that version's submodule specifications - if pkg.version.ref_version: - ref_version = spack.version.Version(pkg.version.ref_version[0]) - ref_version_attributes = pkg.versions.get(ref_version) - if ref_version_attributes: - kwargs["submodules"] = ref_version_attributes.get( - "submodules", kwargs["submodules"] - ) + # if the ref_version is a known version from the package, use that version's + # submodule specifications + ref_version_attributes = pkg.versions.get(pkg.version.ref_version) + if ref_version_attributes: + kwargs["submodules"] = ref_version_attributes.get("submodules", kwargs["submodules"]) fetcher = GitFetchStrategy(**kwargs) return fetcher diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py index 1c56a08980..40018ee609 100644 --- a/lib/spack/spack/installer.py +++ b/lib/spack/spack/installer.py @@ -52,6 +52,7 @@ import spack.mirror import spack.package_base import spack.package_prefs as prefs import spack.repo +import spack.spec import spack.store import spack.util.executable import spack.util.path @@ -628,9 +629,7 @@ def package_id(pkg): derived """ if not pkg.spec.concrete: - raise ValueError( - "Cannot provide a unique, readable id when " "the spec is not concretized." - ) + raise ValueError("Cannot provide a unique, readable id when the spec is not concretized.") return "{0}-{1}-{2}".format(pkg.name, pkg.version, pkg.spec.dag_hash()) @@ -908,7 +907,6 @@ class PackageInstaller(object): """ install_args = task.request.install_args keep_prefix = install_args.get("keep_prefix") - keep_stage = install_args.get("keep_stage") restage = install_args.get("restage") # Make sure the package is ready to be locally installed. @@ -941,9 +939,9 @@ class PackageInstaller(object): else: tty.debug("{0} is partially installed".format(task.pkg_id)) - # Destroy the stage for a locally installed, non-DIYStage, package - if restage and task.pkg.stage.managed_by_spack: - task.pkg.stage.destroy() + # Destroy the stage for a locally installed, non-DIYStage, package + if restage and task.pkg.stage.managed_by_spack: + task.pkg.stage.destroy() if installed_in_db and ( rec.spec.dag_hash() not in task.request.overwrite @@ -955,12 +953,6 @@ class PackageInstaller(object): if task.explicit: spack.store.db.update_explicit(task.pkg.spec, True) - # In case the stage directory has already been created, this - # check ensures it is removed after we checked that the spec is - # installed. - if not keep_stage: - task.pkg.stage.destroy() - def _cleanup_all_tasks(self): """Cleanup all build tasks to include releasing their locks.""" for pkg_id in self.locks: diff --git a/lib/spack/spack/package_base.py b/lib/spack/spack/package_base.py index 98bf4d9c23..2da5380153 100644 --- a/lib/spack/spack/package_base.py +++ b/lib/spack/spack/package_base.py @@ -62,7 +62,7 @@ from spack.util.executable import ProcessError, which from spack.util.package_hash import package_hash from spack.util.prefix import Prefix from spack.util.web import FetchError -from spack.version import GitVersion, Version, VersionBase +from spack.version import GitVersion, StandardVersion, Version FLAG_HANDLER_RETURN_TYPE = Tuple[ Optional[Iterable[str]], Optional[Iterable[str]], Optional[Iterable[str]] @@ -97,9 +97,9 @@ def deprecated_version(pkg, version): Arguments: pkg (PackageBase): The package whose version is to be checked. - version (str or spack.version.VersionBase): The version being checked + version (str or spack.version.StandardVersion): The version being checked """ - if not isinstance(version, VersionBase): + if not isinstance(version, StandardVersion): version = Version(version) for k, v in pkg.versions.items(): @@ -120,7 +120,7 @@ def preferred_version(pkg): # as preferred in the package, then on the fact that the # version is not develop, then lexicographically key_fn = lambda v: (pkg.versions[v].get("preferred", False), not v.isdevelop(), v) - return sorted(pkg.versions, key=key_fn).pop() + return max(pkg.versions, key=key_fn) class WindowsRPath(object): @@ -928,7 +928,7 @@ class PackageBase(WindowsRPath, PackageViewMixin, metaclass=PackageMeta): return self._implement_all_urls_for_version(version, uf) def _implement_all_urls_for_version(self, version, custom_url_for_version=None): - if not isinstance(version, VersionBase): + if not isinstance(version, StandardVersion): version = Version(version) urls = [] diff --git a/lib/spack/spack/package_prefs.py b/lib/spack/spack/package_prefs.py index c0bc52b239..a30c9c7bfd 100644 --- a/lib/spack/spack/package_prefs.py +++ b/lib/spack/spack/package_prefs.py @@ -9,9 +9,9 @@ import spack.error import spack.repo from spack.config import ConfigError from spack.util.path import canonicalize_path -from spack.version import VersionList +from spack.version import Version -_lesser_spec_types = {"compiler": spack.spec.CompilerSpec, "version": VersionList} +_lesser_spec_types = {"compiler": spack.spec.CompilerSpec, "version": Version} def _spec_type(component): diff --git a/lib/spack/spack/parser.py b/lib/spack/spack/parser.py index c721cdde98..9ab3bc7c28 100644 --- a/lib/spack/spack/parser.py +++ b/lib/spack/spack/parser.py @@ -95,7 +95,7 @@ else: VALUE = r"([a-zA-Z_0-9\-+\*.,:=\~\/\\]+)" QUOTED_VALUE = r"[\"']+([a-zA-Z_0-9\-+\*.,:=\~\/\\\s]+)[\"']+" -VERSION = r"([a-zA-Z0-9_][a-zA-Z_0-9\-\.]*\b)" +VERSION = r"=?([a-zA-Z0-9_][a-zA-Z_0-9\-\.]*\b)" VERSION_RANGE = rf"({VERSION}\s*:\s*{VERSION}(?!\s*=)|:\s*{VERSION}(?!\s*=)|{VERSION}\s*:|:)" VERSION_LIST = rf"({VERSION_RANGE}|{VERSION})(\s*[,]\s*({VERSION_RANGE}|{VERSION}))*" @@ -361,19 +361,10 @@ class SpecNodeParser: raise spack.spec.MultipleVersionError( f"{initial_spec} cannot have multiple versions" ) - - version_list = spack.version.VersionList() - version_list.add(spack.version.from_string(self.ctx.current_token.value[1:])) - initial_spec.versions = version_list - - # Add a git lookup method for GitVersions - if ( - initial_spec.name - and initial_spec.versions.concrete - and isinstance(initial_spec.version, spack.version.GitVersion) - ): - initial_spec.version.generate_git_lookup(initial_spec.fullname) - + initial_spec.versions = spack.version.VersionList( + [spack.version.from_string(self.ctx.current_token.value[1:])] + ) + initial_spec.attach_git_version_lookup() self.has_version = True elif self.ctx.accept(TokenType.BOOL_VARIANT): self.hash_not_parsed_or_raise(initial_spec, self.ctx.current_token.value) diff --git a/lib/spack/spack/schema/packages.py b/lib/spack/spack/schema/packages.py index cb05d57857..698787a914 100644 --- a/lib/spack/spack/schema/packages.py +++ b/lib/spack/spack/schema/packages.py @@ -53,7 +53,8 @@ properties = { "version": { "type": "array", "default": [], - # version strings + # version strings (type should be string, number is still possible + # but deprecated. this is to avoid issues with e.g. 3.10 -> 3.1) "items": {"anyOf": [{"type": "string"}, {"type": "number"}]}, }, "target": { @@ -131,3 +132,16 @@ schema = { "additionalProperties": False, "properties": properties, } + + +def update(data): + changed = False + for key in data: + version = data[key].get("version") + if not version or all(isinstance(v, str) for v in version): + continue + + data[key]["version"] = [str(v) for v in version] + changed = True + + return changed diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index 7c5d409ca0..ae3fc64e33 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -43,10 +43,11 @@ import spack.platforms import spack.repo import spack.spec import spack.store +import spack.traverse import spack.util.path import spack.util.timer import spack.variant -import spack.version +import spack.version as vn # these are from clingo.ast and bootstrapped later ASTType = None @@ -893,7 +894,7 @@ class SpackSolverSetup(object): most_to_least_preferred = [] for _, group in itertools.groupby(partially_sorted_versions, key=key_fn): most_to_least_preferred.extend( - list(sorted(group, reverse=True, key=lambda x: spack.version.ver(x.version))) + list(sorted(group, reverse=True, key=lambda x: vn.ver(x.version))) ) for weight, declared_version in enumerate(most_to_least_preferred): @@ -920,7 +921,7 @@ class SpackSolverSetup(object): if spec.concrete: return [fn.attr("version", spec.name, spec.version)] - if spec.versions == spack.version.ver(":"): + if spec.versions == vn.any_version: return [] # record all version constraints for later @@ -1286,6 +1287,7 @@ class SpackSolverSetup(object): spec = spack.spec.Spec(spec_str) if not spec.name: spec.name = pkg_name + spec.attach_git_version_lookup() when_spec = spec if virtual: when_spec = spack.spec.Spec(pkg_name) @@ -1330,7 +1332,7 @@ class SpackSolverSetup(object): # Read a list of all the specs for this package externals = data.get("externals", []) - external_specs = [spack.spec.Spec(x["spec"]) for x in externals] + external_specs = [spack.spec.parse_with_version_concrete(x["spec"]) for x in externals] # Order the external versions to prefer more recent versions # even if specs in packages.yaml are not ordered that way @@ -1636,48 +1638,27 @@ class SpackSolverSetup(object): version_preferences = packages_yaml.get(pkg_name, {}).get("version", []) for idx, v in enumerate(version_preferences): # v can be a string so force it into an actual version for comparisons - ver = spack.version.Version(v) + ver = vn.Version(v) self.declared_versions[pkg_name].append( DeclaredVersion(version=ver, idx=idx, origin=version_provenance.packages_yaml) ) + self.possible_versions[pkg_name].add(ver) def add_concrete_versions_from_specs(self, specs, origin): """Add concrete versions to possible versions from lists of CLI/dev specs.""" - for spec in specs: - for dep in spec.traverse(): - if not dep.versions.concrete: - continue + for s in spack.traverse.traverse_nodes(specs): + # If there is a concrete version on the CLI *that we know nothing + # about*, add it to the known versions. Use idx=0, which is the + # best possible, so they're guaranteed to be used preferentially. + version = s.versions.concrete - known_versions = self.possible_versions[dep.name] - if not isinstance(dep.version, spack.version.GitVersion) and any( - v.satisfies(dep.version) for v in known_versions - ): - # some version we know about satisfies this constraint, so we - # should use that one. e.g, if the user asks for qt@5 and we - # know about qt@5.5. This ensures we don't add under-specified - # versions to the solver - # - # For git versions, we know the version is already fully specified - # so we don't have to worry about whether it's an under-specified - # version - continue + if version is None or any(v == version for v in self.possible_versions[s.name]): + continue - # if there is a concrete version on the CLI *that we know nothing - # about*, add it to the known versions. Use idx=0, which is the - # best possible, so they're guaranteed to be used preferentially. - self.declared_versions[dep.name].append( - DeclaredVersion(version=dep.version, idx=0, origin=origin) - ) - self.possible_versions[dep.name].add(dep.version) - if ( - isinstance(dep.version, spack.version.GitVersion) - and dep.version.user_supplied_reference - ): - defined_version = spack.version.Version(dep.version.ref_version_str) - self.declared_versions[dep.name].append( - DeclaredVersion(version=defined_version, idx=1, origin=origin) - ) - self.possible_versions[dep.name].add(defined_version) + self.declared_versions[s.name].append( + DeclaredVersion(version=version, idx=0, origin=origin) + ) + self.possible_versions[s.name].add(version) def _supported_targets(self, compiler_name, compiler_version, targets): """Get a list of which targets are supported by the compiler. @@ -1872,28 +1853,26 @@ class SpackSolverSetup(object): # add compiler specs from the input line to possibilities if we # don't require compilers to exist. strict = spack.concretize.Concretizer().check_for_compiler_existence - for spec in specs: - for s in spec.traverse(): - # we don't need to validate compilers for already-built specs - if s.concrete: - continue + for s in spack.traverse.traverse_nodes(specs): + # we don't need to validate compilers for already-built specs + if s.concrete or not s.compiler: + continue - if not s.compiler or not s.compiler.concrete: - continue + version = s.compiler.versions.concrete - if strict and s.compiler not in cspecs: - if not s.concrete: - raise spack.concretize.UnavailableCompilerVersionError(s.compiler) - # Allow unknown compilers to exist if the associated spec - # is already built - else: - compiler_cls = spack.compilers.class_for_compiler_name(s.compiler.name) - compilers.append( - compiler_cls( - s.compiler, operating_system=None, target=None, paths=[None] * 4 - ) - ) - self.gen.fact(fn.allow_compiler(s.compiler.name, s.compiler.version)) + if not version or any(c.satisfies(s.compiler) for c in cspecs): + continue + + # Error when a compiler is not found and strict mode is enabled + if strict: + raise spack.concretize.UnavailableCompilerVersionError(s.compiler) + + # Make up a compiler matching the input spec. This is for bootstrapping. + compiler_cls = spack.compilers.class_for_compiler_name(s.compiler.name) + compilers.append( + compiler_cls(s.compiler, operating_system=None, target=None, paths=[None] * 4) + ) + self.gen.fact(fn.allow_compiler(s.compiler.name, version)) return list( sorted( @@ -1906,25 +1885,9 @@ class SpackSolverSetup(object): def define_version_constraints(self): """Define what version_satisfies(...) means in ASP logic.""" for pkg_name, versions in sorted(self.version_constraints): - # version must be *one* of the ones the spec allows. - # Also, "possible versions" contain only concrete versions, so satisfies is appropriate - allowed_versions = [ - v for v in sorted(self.possible_versions[pkg_name]) if v.satisfies(versions) - ] - - # This is needed to account for a variable number of - # numbers e.g. if both 1.0 and 1.0.2 are possible versions - exact_match = [ - v - for v in allowed_versions - if v == versions and not isinstance(v, spack.version.GitVersion) - ] - if exact_match: - allowed_versions = exact_match - # generate facts for each package constraint and the version # that satisfies it - for v in allowed_versions: + for v in sorted(v for v in self.possible_versions[pkg_name] if v.satisfies(versions)): self.gen.fact(fn.version_satisfies(pkg_name, versions, v)) self.gen.newline() @@ -1943,13 +1906,11 @@ class SpackSolverSetup(object): # extract all the real versions mentioned in version ranges def versions_for(v): - if isinstance(v, spack.version.VersionBase): + if isinstance(v, vn.StandardVersion): return [v] - elif isinstance(v, spack.version.VersionRange): - result = [v.start] if v.start else [] - result += [v.end] if v.end else [] - return result - elif isinstance(v, spack.version.VersionList): + elif isinstance(v, vn.ClosedOpenRange): + return [v.lo, vn.prev_version(v.hi)] + elif isinstance(v, vn.VersionList): return sum((versions_for(e) for e in v), []) else: raise TypeError("expected version type, found: %s" % type(v)) @@ -2237,14 +2198,9 @@ def _specs_from_requires(pkg_name, section): spec.name = pkg_name extracted_specs.append(spec) - version_specs = [] - for spec in extracted_specs: - try: - spec.version - version_specs.append(spec) - except spack.error.SpecError: - pass - + version_specs = [x for x in extracted_specs if x.versions.concrete] + for spec in version_specs: + spec.attach_git_version_lookup() return version_specs @@ -2320,11 +2276,11 @@ class SpecBuilder(object): self._specs[pkg].update_variant_validate(name, value) def version(self, pkg, version): - self._specs[pkg].versions = spack.version.ver([version]) + self._specs[pkg].versions = vn.VersionList([vn.Version(version)]) def node_compiler_version(self, pkg, compiler, version): self._specs[pkg].compiler = spack.spec.CompilerSpec(compiler) - self._specs[pkg].compiler.versions = spack.version.VersionList([version]) + self._specs[pkg].compiler.versions = vn.VersionList([vn.Version(version)]) def node_flag_compiler_default(self, pkg): self._flag_compiler_defaults.add(pkg) @@ -2525,8 +2481,8 @@ class SpecBuilder(object): # concretization process) for root in self._specs.values(): for spec in root.traverse(): - if isinstance(spec.version, spack.version.GitVersion): - spec.version.generate_git_lookup(spec.fullname) + if isinstance(spec.version, vn.GitVersion): + spec.version.attach_git_lookup_from_package(spec.fullname) return self._specs diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 87f07e88eb..43f73ab991 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -55,7 +55,7 @@ import itertools import os import re import warnings -from typing import Tuple +from typing import Tuple, Union import llnl.util.filesystem as fs import llnl.util.lang as lang @@ -145,12 +145,8 @@ color_formats = { #: ``color_formats.keys()``. _separators = "[\\%s]" % "\\".join(color_formats.keys()) -#: Versionlist constant so we don't have to build a list -#: every time we call str() -_any_version = vn.VersionList([":"]) - -default_format = "{name}{@version}" -default_format += "{%compiler.name}{@compiler.version}{compiler_flags}" +default_format = "{name}{@versions}" +default_format += "{%compiler.name}{@compiler.versions}{compiler_flags}" default_format += "{variants}{arch=architecture}" #: Regular expression to pull spec contents out of clearsigned signature @@ -581,20 +577,18 @@ class CompilerSpec(object): elif nargs == 2: name, version = args self.name = name - self.versions = vn.VersionList() - versions = vn.ver(version) - self.versions.add(versions) + self.versions = vn.VersionList([vn.ver(version)]) else: raise TypeError("__init__ takes 1 or 2 arguments. (%d given)" % nargs) def _add_versions(self, version_list): # If it already has a non-trivial version list, this is an error - if self.versions and self.versions != vn.VersionList(":"): + if self.versions and self.versions != vn.any_version: # Note: This may be impossible to reach by the current parser # Keeping it in case the implementation changes. raise MultipleVersionError( - "A spec cannot contain multiple version signifiers." " Use a version list instead." + "A spec cannot contain multiple version signifiers. Use a version list instead." ) self.versions = vn.VersionList() for version in version_list: @@ -677,9 +671,8 @@ class CompilerSpec(object): def __str__(self): out = self.name - if self.versions and self.versions != _any_version: - vlist = ",".join(str(v) for v in self.versions) - out += "@%s" % vlist + if self.versions and self.versions != vn.any_version: + out += f"@{self.versions}" return out def __repr__(self): @@ -1477,7 +1470,7 @@ class Spec(object): def _add_versions(self, version_list): """Called by the parser to add an allowable version.""" # If it already has a non-trivial version list, this is an error - if self.versions and self.versions != vn.VersionList(":"): + if self.versions and self.versions != vn.any_version: raise MultipleVersionError( "A spec cannot contain multiple version signifiers." " Use a version list instead." ) @@ -2108,7 +2101,7 @@ class Spec(object): # (and the user spec) have dependencies new_spec = init_spec.copy() package_cls = spack.repo.path.get_pkg_class(new_spec.name) - if change_spec.versions and not change_spec.versions == spack.version.ver(":"): + if change_spec.versions and not change_spec.versions == vn.any_version: new_spec.versions = change_spec.versions for variant, value in change_spec.variants.items(): if variant in package_cls.variants: @@ -2289,12 +2282,17 @@ class Spec(object): """ # Legacy specfile format if isinstance(data["spec"], list): - return SpecfileV1.load(data) + spec = SpecfileV1.load(data) + elif int(data["spec"]["_meta"]["version"]) == 2: + spec = SpecfileV2.load(data) + else: + spec = SpecfileV3.load(data) + + # Any git version should + for s in spec.traverse(): + s.attach_git_version_lookup() - specfile_version = int(data["spec"]["_meta"]["version"]) - if specfile_version == 2: - return SpecfileV2.load(data) - return SpecfileV3.load(data) + return spec @staticmethod def from_yaml(stream): @@ -2823,6 +2821,26 @@ class Spec(object): return self._normal = value self._concrete = value + self._validate_version() + + def _validate_version(self): + # Specs that were concretized with just a git sha as version, without associated + # Spack version, get their Spack version mapped to develop. This should only apply + # when reading specs concretized with Spack 0.19 or earlier. Currently Spack always + # ensures that GitVersion specs have an associated Spack version. + v = self.versions.concrete + if not isinstance(v, vn.GitVersion): + return + + try: + v.ref_version + except vn.VersionLookupError: + before = self.cformat("{name}{@version}{/hash:7}") + v._ref_version = vn.StandardVersion.from_string("develop") + tty.debug( + f"the git sha of {before} could not be resolved to spack version; " + f"it has been replaced by {self.cformat('{name}{@version}{/hash:7}')}." + ) def _mark_concrete(self, value=True): """Mark this spec and its dependencies as concrete. @@ -4166,9 +4184,13 @@ class Spec(object): if part == "arch": part = "architecture" elif part == "version": - # Version requires concrete spec, versions does not - # when concrete, they print the same thing - part = "versions" + # version (singular) requires a concrete versions list. Avoid + # pedantic errors by using versions (plural) when not concrete. + # These two are not entirely equivalent for pkg@=1.2.3: + # - version prints '1.2.3' + # - versions prints '=1.2.3' + if not current.versions.concrete: + part = "versions" try: current = getattr(current, part) except AttributeError: @@ -4177,7 +4199,7 @@ class Spec(object): m += "Spec %s has no attribute %s" % (parent, part) raise SpecFormatStringError(m) if isinstance(current, vn.VersionList): - if current == _any_version: + if current == vn.any_version: # We don't print empty version lists return @@ -4195,7 +4217,7 @@ class Spec(object): col = "=" elif "compiler" in parts or "compiler_flags" in parts: col = "%" - elif "version" in parts: + elif "version" in parts or "versions" in parts: col = "@" # Finally, write the output @@ -4539,6 +4561,23 @@ class Spec(object): def __reduce__(self): return Spec.from_dict, (self.to_dict(hash=ht.process_hash),) + def attach_git_version_lookup(self): + # Add a git lookup method for GitVersions + if not self.name: + return + for v in self.versions: + if isinstance(v, vn.GitVersion) and v._ref_version is None: + v.attach_git_lookup_from_package(self.fullname) + + +def parse_with_version_concrete(string: str, compiler: bool = False): + """Same as Spec(string), but interprets @x as @=x""" + s: Union[CompilerSpec, Spec] = CompilerSpec(string) if compiler else Spec(string) + interpreted_version = s.versions.concrete_range_as_version + if interpreted_version: + s.versions = vn.VersionList([interpreted_version]) + return s + def merge_abstract_anonymous_specs(*abstract_specs: Spec): """Merge the abstracts specs passed as input and return the result. @@ -4580,6 +4619,7 @@ class SpecfileReaderBase: if "version" in node or "versions" in node: spec.versions = vn.VersionList.from_dict(node) + spec.attach_git_version_lookup() if "arch" in node: spec.architecture = ArchSpec.from_dict(node) @@ -4614,7 +4654,8 @@ class SpecfileReaderBase: ) # specs read in are concrete unless marked abstract - spec._concrete = node.get("concrete", True) + if node.get("concrete", True): + spec._mark_root_concrete() if "patches" in node: patches = node["patches"] diff --git a/lib/spack/spack/spec_list.py b/lib/spack/spack/spec_list.py index 9efa8cbb50..be70d6d522 100644 --- a/lib/spack/spack/spec_list.py +++ b/lib/spack/spack/spec_list.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) import itertools +from typing import List import spack.variant from spack.error import SpackError @@ -59,7 +60,7 @@ class SpecList(object): return self._constraints @property - def specs(self): + def specs(self) -> List[Spec]: if self._specs is None: specs = [] # This could be slightly faster done directly from yaml_list, @@ -167,6 +168,9 @@ class SpecList(object): def __getitem__(self, key): return self.specs[key] + def __iter__(self): + return iter(self.specs) + def _expand_matrix_constraints(matrix_config): # recurse so we can handle nested matrices diff --git a/lib/spack/spack/test/architecture.py b/lib/spack/spack/test/architecture.py index 7aba886375..f00d66ad32 100644 --- a/lib/spack/spack/test/architecture.py +++ b/lib/spack/spack/test/architecture.py @@ -142,24 +142,24 @@ def test_optimization_flags(compiler_spec, target_name, expected_flags, config): @pytest.mark.parametrize( "compiler,real_version,target_str,expected_flags", [ - (spack.spec.CompilerSpec("gcc@9.2.0"), None, "haswell", "-march=haswell -mtune=haswell"), + (spack.spec.CompilerSpec("gcc@=9.2.0"), None, "haswell", "-march=haswell -mtune=haswell"), # Check that custom string versions are accepted ( - spack.spec.CompilerSpec("gcc@10foo"), + spack.spec.CompilerSpec("gcc@=10foo"), "9.2.0", "icelake", "-march=icelake-client -mtune=icelake-client", ), # Check that we run version detection (4.4.0 doesn't support icelake) ( - spack.spec.CompilerSpec("gcc@4.4.0-special"), + spack.spec.CompilerSpec("gcc@=4.4.0-special"), "9.2.0", "icelake", "-march=icelake-client -mtune=icelake-client", ), # Check that the special case for Apple's clang is treated correctly # i.e. it won't try to detect the version again - (spack.spec.CompilerSpec("apple-clang@9.1.0"), None, "x86_64", "-march=x86-64"), + (spack.spec.CompilerSpec("apple-clang@=9.1.0"), None, "x86_64", "-march=x86-64"), ], ) def test_optimization_flags_with_custom_versions( diff --git a/lib/spack/spack/test/cmd/ci.py b/lib/spack/spack/test/cmd/ci.py index c32b627b2d..b0588bd1dd 100644 --- a/lib/spack/spack/test/cmd/ci.py +++ b/lib/spack/spack/test/cmd/ci.py @@ -281,7 +281,7 @@ spack: - bootstrap: - gcc@3.0 specs: - - dyninst%gcc@3.0 + - dyninst%gcc@=3.0 mirrors: some-mirror: https://my.fake.mirror ci: @@ -341,7 +341,7 @@ spack: - bootstrap: - gcc@3.0 specs: - - dyninst%gcc@3.0 + - dyninst%gcc@=3.0 mirrors: some-mirror: https://my.fake.mirror ci: @@ -1527,7 +1527,7 @@ def test_ci_generate_with_workarounds( """\ spack: specs: - - callpath%gcc@9.5 + - callpath%gcc@=9.5 mirrors: some-mirror: https://my.fake.mirror ci: @@ -1644,7 +1644,7 @@ def test_ci_generate_bootstrap_prune_dag( mirror_url = "file://{0}".format(mirror_dir.strpath) # Install a compiler, because we want to put it in a buildcache - install_cmd("gcc@12.2.0%gcc@10.2.1") + install_cmd("gcc@=12.2.0%gcc@10.2.1") # Put installed compiler in the buildcache buildcache_cmd("push", "-u", "-a", "-f", "-d", mirror_dir.strpath, "gcc@12.2.0%gcc@10.2.1") @@ -1654,12 +1654,12 @@ def test_ci_generate_bootstrap_prune_dag( monkeypatch.setattr(spack.concretize.Concretizer, "check_for_compiler_existence", False) spack.config.set("config:install_missing_compilers", True) - assert CompilerSpec("gcc@12.2.0") not in compilers.all_compiler_specs() + assert CompilerSpec("gcc@=12.2.0") not in compilers.all_compiler_specs() # Configure the mirror where we put that buildcache w/ the compiler mirror_cmd("add", "test-mirror", mirror_url) - install_cmd("--no-check-signature", "b%gcc@12.2.0") + install_cmd("--no-check-signature", "b%gcc@=12.2.0") # Put spec built with installed compiler in the buildcache buildcache_cmd("push", "-u", "-a", "-f", "-d", mirror_dir.strpath, "b%gcc@12.2.0") @@ -1674,7 +1674,7 @@ def test_ci_generate_bootstrap_prune_dag( spack: definitions: - bootstrap: - - gcc@12.2.0%gcc@10.2.1 + - gcc@=12.2.0%gcc@10.2.1 specs: - b%gcc@12.2.0 mirrors: diff --git a/lib/spack/spack/test/cmd/config.py b/lib/spack/spack/test/cmd/config.py index 39cd9fcb7d..c410fcfc76 100644 --- a/lib/spack/spack/test/cmd/config.py +++ b/lib/spack/spack/test/cmd/config.py @@ -632,13 +632,13 @@ def test_config_prefer_upstream( # Make sure only the non-default variants are set. assert packages["boost"] == { - "compiler": ["gcc@10.2.1"], + "compiler": ["gcc@=10.2.1"], "variants": "+debug +graph", "version": ["1.63.0"], } - assert packages["dependency-install"] == {"compiler": ["gcc@10.2.1"], "version": ["2.0"]} + assert packages["dependency-install"] == {"compiler": ["gcc@=10.2.1"], "version": ["2.0"]} # Ensure that neither variant gets listed for hdf5, since they conflict - assert packages["hdf5"] == {"compiler": ["gcc@10.2.1"], "version": ["2.3"]} + assert packages["hdf5"] == {"compiler": ["gcc@=10.2.1"], "version": ["2.3"]} # Make sure a message about the conflicting hdf5's was given. assert "- hdf5" in output diff --git a/lib/spack/spack/test/cmd/dev_build.py b/lib/spack/spack/test/cmd/dev_build.py index 010daa1176..c1aef58740 100644 --- a/lib/spack/spack/test/cmd/dev_build.py +++ b/lib/spack/spack/test/cmd/dev_build.py @@ -265,7 +265,7 @@ def test_dev_build_multiple( # without the environment, the user would need to set dev_path for both the # root and dependency if they wanted a dev build for both. leaf_dir = tmpdir.mkdir("leaf") - leaf_spec = spack.spec.Spec("dev-build-test-install@1.0.0") + leaf_spec = spack.spec.Spec("dev-build-test-install@=1.0.0") # non-existing version leaf_pkg_cls = spack.repo.path.get_pkg_class(leaf_spec.name) with leaf_dir.as_cwd(): with open(leaf_pkg_cls.filename, "w") as f: @@ -293,7 +293,7 @@ spack: develop: dev-build-test-install: path: %s - spec: dev-build-test-install@1.0.0 + spec: dev-build-test-install@=1.0.0 dev-build-test-dependent: spec: dev-build-test-dependent@0.0.0 path: %s diff --git a/lib/spack/spack/test/cmd/develop.py b/lib/spack/spack/test/cmd/develop.py index bea3aa3b8a..1f77bbfc63 100644 --- a/lib/spack/spack/test/cmd/develop.py +++ b/lib/spack/spack/test/cmd/develop.py @@ -48,19 +48,19 @@ class TestDevelop(object): # develop checks that the path exists fs.mkdirp(os.path.join(e.path, "mpich")) develop("--no-clone", "mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0")) + self.check_develop(e, spack.spec.Spec("mpich@=1.0")) def test_develop_no_clone(self, tmpdir): env("create", "test") with ev.read("test") as e: develop("--no-clone", "-p", str(tmpdir), "mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0"), str(tmpdir)) + self.check_develop(e, spack.spec.Spec("mpich@=1.0"), str(tmpdir)) def test_develop(self): env("create", "test") with ev.read("test") as e: develop("mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0")) + self.check_develop(e, spack.spec.Spec("mpich@=1.0")) def test_develop_no_args(self): env("create", "test") @@ -71,20 +71,20 @@ class TestDevelop(object): # test develop with no args develop() - self.check_develop(e, spack.spec.Spec("mpich@1.0")) + self.check_develop(e, spack.spec.Spec("mpich@=1.0")) def test_develop_twice(self): env("create", "test") with ev.read("test") as e: develop("mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0")) + self.check_develop(e, spack.spec.Spec("mpich@=1.0")) develop("mpich@1.0") # disk representation isn't updated unless we write # second develop command doesn't change it, so we don't write # but we check disk representation e.write() - self.check_develop(e, spack.spec.Spec("mpich@1.0")) + self.check_develop(e, spack.spec.Spec("mpich@=1.0")) assert len(e.dev_specs) == 1 def test_develop_update_path(self, tmpdir): @@ -92,7 +92,7 @@ class TestDevelop(object): with ev.read("test") as e: develop("mpich@1.0") develop("-p", str(tmpdir), "mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0"), str(tmpdir)) + self.check_develop(e, spack.spec.Spec("mpich@=1.0"), str(tmpdir)) assert len(e.dev_specs) == 1 def test_develop_update_spec(self): @@ -100,7 +100,7 @@ class TestDevelop(object): with ev.read("test") as e: develop("mpich@1.0") develop("mpich@2.0") - self.check_develop(e, spack.spec.Spec("mpich@2.0")) + self.check_develop(e, spack.spec.Spec("mpich@=2.0")) assert len(e.dev_specs) == 1 def test_develop_canonicalize_path(self, monkeypatch, config): @@ -115,7 +115,7 @@ class TestDevelop(object): monkeypatch.setattr(spack.stage.Stage, "steal_source", check_path) develop("-p", path, "mpich@1.0") - self.check_develop(e, spack.spec.Spec("mpich@1.0"), path) + self.check_develop(e, spack.spec.Spec("mpich@=1.0"), path) # Check modifications actually worked assert spack.spec.Spec("mpich@1.0").concretized().satisfies("dev_path=%s" % abspath) @@ -142,7 +142,7 @@ class TestDevelop(object): os.rmdir(abspath) develop() - self.check_develop(e, spack.spec.Spec("mpich@1.0"), path) + self.check_develop(e, spack.spec.Spec("mpich@=1.0"), path) # Check modifications actually worked assert spack.spec.Spec("mpich@1.0").concretized().satisfies("dev_path=%s" % abspath) diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py index 0fd5a34dd5..93ce112215 100644 --- a/lib/spack/spack/test/cmd/env.py +++ b/lib/spack/spack/test/cmd/env.py @@ -691,7 +691,7 @@ spack: - mpileaks packages: mpileaks: - version: [2.2] + version: ["2.2"] """ ) with e: @@ -741,7 +741,7 @@ spack: """\ packages: libelf: - version: [0.8.10] + version: ["0.8.10"] """ ) @@ -751,7 +751,7 @@ spack: """\ packages: mpileaks: - version: [2.2] + version: ["2.2"] """ ) @@ -770,7 +770,7 @@ def packages_file(tmpdir): raw_yaml = """ packages: mpileaks: - version: [2.2] + version: ["2.2"] """ filename = tmpdir.ensure("testconfig", "packages.yaml") filename.write(raw_yaml) @@ -829,7 +829,7 @@ def test_env_with_included_config_file_url(tmpdir, mutable_empty_config, package assert len(scopes) == 1 cfg = spack.config.get("packages") - assert cfg["mpileaks"]["version"] == [2.2] + assert cfg["mpileaks"]["version"] == ["2.2"] def test_env_with_included_config_missing_file(tmpdir, mutable_empty_config): @@ -895,7 +895,7 @@ def test_env_config_precedence(environment_from_manifest): spack: packages: libelf: - version: [0.8.12] + version: ["0.8.12"] include: - ./included-config.yaml specs: @@ -907,9 +907,9 @@ spack: """\ packages: mpileaks: - version: [2.2] + version: ["2.2"] libelf: - version: [0.8.11] + version: ["0.8.11"] """ ) @@ -940,7 +940,7 @@ spack: """\ packages: libelf: - version: [0.8.10] # this should override libelf version below + version: ["0.8.10"] # this should override libelf version below """ ) @@ -949,9 +949,9 @@ packages: """\ packages: mpileaks: - version: [2.2] + version: ["2.2"] libelf: - version: [0.8.12] + version: ["0.8.12"] """ ) @@ -2647,11 +2647,11 @@ def test_custom_version_concretize_together(tmpdir): e.unify = True # Concretize a first time using 'mpich' as the MPI provider - e.add("hdf5@myversion") + e.add("hdf5@=myversion") e.add("mpich") e.concretize() - assert any("hdf5@myversion" in spec for _, spec in e.concretized_specs()) + assert any(spec.satisfies("hdf5@myversion") for _, spec in e.concretized_specs()) def test_modules_relative_to_views(environment_from_manifest, install_mockery, mock_fetch): @@ -2751,7 +2751,7 @@ def test_query_develop_specs(): with ev.read("test") as e: e.add("mpich") e.add("mpileaks") - e.develop(Spec("mpich@1"), "here", clone=False) + e.develop(Spec("mpich@=1"), "here", clone=False) assert e.is_develop(Spec("mpich")) assert not e.is_develop(Spec("mpileaks")) diff --git a/lib/spack/spack/test/cmd/fetch.py b/lib/spack/spack/test/cmd/fetch.py index cf4ec911b9..19c5d33585 100644 --- a/lib/spack/spack/test/cmd/fetch.py +++ b/lib/spack/spack/test/cmd/fetch.py @@ -30,7 +30,7 @@ def test_fetch_single_spec(tmpdir, mock_archive, mock_stage, mock_fetch, install @pytest.mark.disable_clean_stage_check def test_fetch_multiple_specs(tmpdir, mock_archive, mock_stage, mock_fetch, install_mockery): - SpackCommand("fetch")("mpileaks", "gcc@10.2.0", "python") + SpackCommand("fetch")("mpileaks", "gcc@3.0", "python") def test_fetch_no_argument(): diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py index ed1da4b740..70e73b6412 100644 --- a/lib/spack/spack/test/cmd/install.py +++ b/lib/spack/spack/test/cmd/install.py @@ -265,10 +265,7 @@ def test_install_commit(mock_git_version_info, install_mockery, mock_packages, m ) # Use the earliest commit in the respository - commit = commits[-1] - spec = spack.spec.Spec("git-test-commit@%s" % commit) - spec.concretize() - print(spec) + spec = Spec(f"git-test-commit@{commits[-1]}").concretized() spec.package.do_install() # Ensure first commit file contents were written @@ -942,10 +939,10 @@ def test_compiler_bootstrap( ): monkeypatch.setattr(spack.concretize.Concretizer, "check_for_compiler_existence", False) spack.config.set("config:install_missing_compilers", True) - assert CompilerSpec("gcc@12.0") not in compilers.all_compiler_specs() + assert CompilerSpec("gcc@=12.0") not in compilers.all_compiler_specs() # Test succeeds if it does not raise an error - install("a%gcc@12.0") + install("a%gcc@=12.0") def test_compiler_bootstrap_from_binary_mirror( @@ -966,7 +963,7 @@ def test_compiler_bootstrap_from_binary_mirror( mirror_url = "file://{0}".format(mirror_dir.strpath) # Install a compiler, because we want to put it in a buildcache - install("gcc@10.2.0") + install("gcc@=10.2.0") # Put installed compiler in the buildcache buildcache("push", "-u", "-a", "-f", "-d", mirror_dir.strpath, "gcc@10.2.0") @@ -976,7 +973,7 @@ def test_compiler_bootstrap_from_binary_mirror( monkeypatch.setattr(spack.concretize.Concretizer, "check_for_compiler_existence", False) spack.config.set("config:install_missing_compilers", True) - assert CompilerSpec("gcc@10.2.0") not in compilers.all_compiler_specs() + assert CompilerSpec("gcc@=10.2.0") not in compilers.all_compiler_specs() # Configure the mirror where we put that buildcache w/ the compiler mirror("add", "test-mirror", mirror_url) @@ -984,7 +981,7 @@ def test_compiler_bootstrap_from_binary_mirror( # Now make sure that when the compiler is installed from binary mirror, # it also gets configured as a compiler. Test succeeds if it does not # raise an error - install("--no-check-signature", "--cache-only", "--only", "dependencies", "b%gcc@10.2.0") + install("--no-check-signature", "--cache-only", "--only", "dependencies", "b%gcc@=10.2.0") install("--no-cache", "--only", "package", "b%gcc@10.2.0") @@ -1000,11 +997,11 @@ def test_compiler_bootstrap_already_installed( monkeypatch.setattr(spack.concretize.Concretizer, "check_for_compiler_existence", False) spack.config.set("config:install_missing_compilers", True) - assert CompilerSpec("gcc@12.0") not in compilers.all_compiler_specs() + assert CompilerSpec("gcc@=12.0") not in compilers.all_compiler_specs() # Test succeeds if it does not raise an error - install("gcc@12.0") - install("a%gcc@12.0") + install("gcc@=12.0") + install("a%gcc@=12.0") def test_install_fails_no_args(tmpdir): diff --git a/lib/spack/spack/test/cmd/spec.py b/lib/spack/spack/test/cmd/spec.py index 6ced00c0bc..0e454bf794 100644 --- a/lib/spack/spack/test/cmd/spec.py +++ b/lib/spack/spack/test/cmd/spec.py @@ -24,12 +24,12 @@ spec = SpackCommand("spec") def test_spec(): output = spec("mpileaks") - assert "mpileaks@2.3" in output - assert "callpath@1.0" in output - assert "dyninst@8.2" in output - assert "libdwarf@20130729" in output - assert "libelf@0.8.1" in output - assert "mpich@3.0.4" in output + assert "mpileaks@=2.3" in output + assert "callpath@=1.0" in output + assert "dyninst@=8.2" in output + assert "libdwarf@=20130729" in output + assert "libelf@=0.8.1" in output + assert "mpich@=3.0.4" in output def test_spec_concretizer_args(mutable_config, mutable_database): @@ -196,12 +196,12 @@ def test_env_aware_spec(mutable_mock_env_path): with env: output = spec() - assert "mpileaks@2.3" in output - assert "callpath@1.0" in output - assert "dyninst@8.2" in output - assert "libdwarf@20130729" in output - assert "libelf@0.8.1" in output - assert "mpich@3.0.4" in output + assert "mpileaks@=2.3" in output + assert "callpath@=1.0" in output + assert "dyninst@=8.2" in output + assert "libdwarf@=20130729" in output + assert "libelf@=0.8.1" in output + assert "mpich@=3.0.4" in output @pytest.mark.parametrize( diff --git a/lib/spack/spack/test/cmd/stage.py b/lib/spack/spack/test/cmd/stage.py index 27e6abc1c7..9fff89afb4 100644 --- a/lib/spack/spack/test/cmd/stage.py +++ b/lib/spack/spack/test/cmd/stage.py @@ -89,7 +89,7 @@ def test_stage_with_env_inside_env(mutable_mock_env_path, monkeypatch): monkeypatch.setattr(spack.package_base.PackageBase, "do_stage", fake_stage) e = ev.create("test") - e.add("mpileaks@100.100") + e.add("mpileaks@=100.100") e.concretize() with e: @@ -101,7 +101,7 @@ def test_stage_full_env(mutable_mock_env_path, monkeypatch): """Verify that stage filters specs in environment.""" e = ev.create("test") - e.add("mpileaks@100.100") + e.add("mpileaks@=100.100") e.concretize() # list all the package names that should be staged diff --git a/lib/spack/spack/test/compilers/basics.py b/lib/spack/spack/test/compilers/basics.py index edd2f3acc9..66c5fe451f 100644 --- a/lib/spack/spack/test/compilers/basics.py +++ b/lib/spack/spack/test/compilers/basics.py @@ -58,8 +58,7 @@ def test_multiple_conflicting_compiler_definitions(mutable_config): mutable_config.update_config("compilers", compiler_config) arch_spec = spack.spec.ArchSpec(("test", "test", "test")) - cspec = compiler_config[0]["compiler"]["spec"] - cmp = compilers.compiler_for_spec(cspec, arch_spec) + cmp = compilers.compiler_for_spec("clang@=0.0.0", arch_spec) assert cmp.f77 == "f77" @@ -78,7 +77,7 @@ def test_get_compiler_duplicates(config): def test_all_compilers(config): all_compilers = compilers.all_compilers() - filtered = [x for x in all_compilers if str(x.spec) == "clang@3.3"] + filtered = [x for x in all_compilers if str(x.spec) == "clang@=3.3"] filtered = [x for x in filtered if x.operating_system == "SuSE11"] assert len(filtered) == 1 @@ -525,135 +524,135 @@ def test_gcc_flags(): def test_intel_flags(): - supported_flag_test("openmp_flag", "-openmp", "intel@15.0") - supported_flag_test("openmp_flag", "-qopenmp", "intel@16.0") - unsupported_flag_test("cxx11_flag", "intel@11.0") - supported_flag_test("cxx11_flag", "-std=c++0x", "intel@12.0") - supported_flag_test("cxx11_flag", "-std=c++11", "intel@13") - unsupported_flag_test("cxx14_flag", "intel@14.0") - supported_flag_test("cxx14_flag", "-std=c++1y", "intel@15.0") - supported_flag_test("cxx14_flag", "-std=c++14", "intel@15.0.2") - unsupported_flag_test("c99_flag", "intel@11.0") - supported_flag_test("c99_flag", "-std=c99", "intel@12.0") - unsupported_flag_test("c11_flag", "intel@15.0") - supported_flag_test("c11_flag", "-std=c1x", "intel@16.0") - supported_flag_test("cc_pic_flag", "-fPIC", "intel@1.0") - supported_flag_test("cxx_pic_flag", "-fPIC", "intel@1.0") - supported_flag_test("f77_pic_flag", "-fPIC", "intel@1.0") - supported_flag_test("fc_pic_flag", "-fPIC", "intel@1.0") - supported_flag_test("stdcxx_libs", ("-cxxlib",), "intel@1.0") - supported_flag_test("debug_flags", ["-debug", "-g", "-g0", "-g1", "-g2", "-g3"], "intel@1.0") + supported_flag_test("openmp_flag", "-openmp", "intel@=15.0") + supported_flag_test("openmp_flag", "-qopenmp", "intel@=16.0") + unsupported_flag_test("cxx11_flag", "intel@=11.0") + supported_flag_test("cxx11_flag", "-std=c++0x", "intel@=12.0") + supported_flag_test("cxx11_flag", "-std=c++11", "intel@=13") + unsupported_flag_test("cxx14_flag", "intel@=14.0") + supported_flag_test("cxx14_flag", "-std=c++1y", "intel@=15.0") + supported_flag_test("cxx14_flag", "-std=c++14", "intel@=15.0.2") + unsupported_flag_test("c99_flag", "intel@=11.0") + supported_flag_test("c99_flag", "-std=c99", "intel@=12.0") + unsupported_flag_test("c11_flag", "intel@=15.0") + supported_flag_test("c11_flag", "-std=c1x", "intel@=16.0") + supported_flag_test("cc_pic_flag", "-fPIC", "intel@=1.0") + supported_flag_test("cxx_pic_flag", "-fPIC", "intel@=1.0") + supported_flag_test("f77_pic_flag", "-fPIC", "intel@=1.0") + supported_flag_test("fc_pic_flag", "-fPIC", "intel@=1.0") + supported_flag_test("stdcxx_libs", ("-cxxlib",), "intel@=1.0") + supported_flag_test("debug_flags", ["-debug", "-g", "-g0", "-g1", "-g2", "-g3"], "intel@=1.0") supported_flag_test( - "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Os"], "intel@1.0" + "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Os"], "intel@=1.0" ) def test_oneapi_flags(): - supported_flag_test("openmp_flag", "-fiopenmp", "oneapi@2020.8.0.0827") - supported_flag_test("cxx11_flag", "-std=c++11", "oneapi@2020.8.0.0827") - supported_flag_test("cxx14_flag", "-std=c++14", "oneapi@2020.8.0.0827") - supported_flag_test("c99_flag", "-std=c99", "oneapi@2020.8.0.0827") - supported_flag_test("c11_flag", "-std=c1x", "oneapi@2020.8.0.0827") - supported_flag_test("cc_pic_flag", "-fPIC", "oneapi@2020.8.0.0827") - supported_flag_test("cxx_pic_flag", "-fPIC", "oneapi@2020.8.0.0827") - supported_flag_test("f77_pic_flag", "-fPIC", "oneapi@2020.8.0.0827") - supported_flag_test("fc_pic_flag", "-fPIC", "oneapi@2020.8.0.0827") - supported_flag_test("stdcxx_libs", ("-cxxlib",), "oneapi@2020.8.0.0827") + supported_flag_test("openmp_flag", "-fiopenmp", "oneapi@=2020.8.0.0827") + supported_flag_test("cxx11_flag", "-std=c++11", "oneapi@=2020.8.0.0827") + supported_flag_test("cxx14_flag", "-std=c++14", "oneapi@=2020.8.0.0827") + supported_flag_test("c99_flag", "-std=c99", "oneapi@=2020.8.0.0827") + supported_flag_test("c11_flag", "-std=c1x", "oneapi@=2020.8.0.0827") + supported_flag_test("cc_pic_flag", "-fPIC", "oneapi@=2020.8.0.0827") + supported_flag_test("cxx_pic_flag", "-fPIC", "oneapi@=2020.8.0.0827") + supported_flag_test("f77_pic_flag", "-fPIC", "oneapi@=2020.8.0.0827") + supported_flag_test("fc_pic_flag", "-fPIC", "oneapi@=2020.8.0.0827") + supported_flag_test("stdcxx_libs", ("-cxxlib",), "oneapi@=2020.8.0.0827") supported_flag_test( - "debug_flags", ["-debug", "-g", "-g0", "-g1", "-g2", "-g3"], "oneapi@2020.8.0.0827" + "debug_flags", ["-debug", "-g", "-g0", "-g1", "-g2", "-g3"], "oneapi@=2020.8.0.0827" ) supported_flag_test( - "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Os"], "oneapi@2020.8.0.0827" + "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Os"], "oneapi@=2020.8.0.0827" ) def test_nag_flags(): - supported_flag_test("openmp_flag", "-openmp", "nag@1.0") - supported_flag_test("cxx11_flag", "-std=c++11", "nag@1.0") - supported_flag_test("cc_pic_flag", "-fPIC", "nag@1.0") - supported_flag_test("cxx_pic_flag", "-fPIC", "nag@1.0") - supported_flag_test("f77_pic_flag", "-PIC", "nag@1.0") - supported_flag_test("fc_pic_flag", "-PIC", "nag@1.0") - supported_flag_test("cc_rpath_arg", "-Wl,-rpath,", "nag@1.0") - supported_flag_test("cxx_rpath_arg", "-Wl,-rpath,", "nag@1.0") - supported_flag_test("f77_rpath_arg", "-Wl,-Wl,,-rpath,,", "nag@1.0") - supported_flag_test("fc_rpath_arg", "-Wl,-Wl,,-rpath,,", "nag@1.0") - supported_flag_test("linker_arg", "-Wl,-Wl,,", "nag@1.0") - supported_flag_test("debug_flags", ["-g", "-gline", "-g90"], "nag@1.0") - supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "nag@1.0") + supported_flag_test("openmp_flag", "-openmp", "nag@=1.0") + supported_flag_test("cxx11_flag", "-std=c++11", "nag@=1.0") + supported_flag_test("cc_pic_flag", "-fPIC", "nag@=1.0") + supported_flag_test("cxx_pic_flag", "-fPIC", "nag@=1.0") + supported_flag_test("f77_pic_flag", "-PIC", "nag@=1.0") + supported_flag_test("fc_pic_flag", "-PIC", "nag@=1.0") + supported_flag_test("cc_rpath_arg", "-Wl,-rpath,", "nag@=1.0") + supported_flag_test("cxx_rpath_arg", "-Wl,-rpath,", "nag@=1.0") + supported_flag_test("f77_rpath_arg", "-Wl,-Wl,,-rpath,,", "nag@=1.0") + supported_flag_test("fc_rpath_arg", "-Wl,-Wl,,-rpath,,", "nag@=1.0") + supported_flag_test("linker_arg", "-Wl,-Wl,,", "nag@=1.0") + supported_flag_test("debug_flags", ["-g", "-gline", "-g90"], "nag@=1.0") + supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "nag@=1.0") def test_nvhpc_flags(): - supported_flag_test("openmp_flag", "-mp", "nvhpc@20.9") - supported_flag_test("cxx11_flag", "--c++11", "nvhpc@20.9") - supported_flag_test("cxx14_flag", "--c++14", "nvhpc@20.9") - supported_flag_test("cxx17_flag", "--c++17", "nvhpc@20.9") - supported_flag_test("c99_flag", "-c99", "nvhpc@20.9") - supported_flag_test("c11_flag", "-c11", "nvhpc@20.9") - supported_flag_test("cc_pic_flag", "-fpic", "nvhpc@20.9") - supported_flag_test("cxx_pic_flag", "-fpic", "nvhpc@20.9") - supported_flag_test("f77_pic_flag", "-fpic", "nvhpc@20.9") - supported_flag_test("fc_pic_flag", "-fpic", "nvhpc@20.9") - supported_flag_test("debug_flags", ["-g", "-gopt"], "nvhpc@20.9") - supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "nvhpc@20.9") - supported_flag_test("stdcxx_libs", ("-c++libs",), "nvhpc@20.9") + supported_flag_test("openmp_flag", "-mp", "nvhpc@=20.9") + supported_flag_test("cxx11_flag", "--c++11", "nvhpc@=20.9") + supported_flag_test("cxx14_flag", "--c++14", "nvhpc@=20.9") + supported_flag_test("cxx17_flag", "--c++17", "nvhpc@=20.9") + supported_flag_test("c99_flag", "-c99", "nvhpc@=20.9") + supported_flag_test("c11_flag", "-c11", "nvhpc@=20.9") + supported_flag_test("cc_pic_flag", "-fpic", "nvhpc@=20.9") + supported_flag_test("cxx_pic_flag", "-fpic", "nvhpc@=20.9") + supported_flag_test("f77_pic_flag", "-fpic", "nvhpc@=20.9") + supported_flag_test("fc_pic_flag", "-fpic", "nvhpc@=20.9") + supported_flag_test("debug_flags", ["-g", "-gopt"], "nvhpc@=20.9") + supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "nvhpc@=20.9") + supported_flag_test("stdcxx_libs", ("-c++libs",), "nvhpc@=20.9") def test_pgi_flags(): - supported_flag_test("openmp_flag", "-mp", "pgi@1.0") - supported_flag_test("cxx11_flag", "-std=c++11", "pgi@1.0") - unsupported_flag_test("c99_flag", "pgi@12.9") - supported_flag_test("c99_flag", "-c99", "pgi@12.10") - unsupported_flag_test("c11_flag", "pgi@15.2") - supported_flag_test("c11_flag", "-c11", "pgi@15.3") - supported_flag_test("cc_pic_flag", "-fpic", "pgi@1.0") - supported_flag_test("cxx_pic_flag", "-fpic", "pgi@1.0") - supported_flag_test("f77_pic_flag", "-fpic", "pgi@1.0") - supported_flag_test("fc_pic_flag", "-fpic", "pgi@1.0") - supported_flag_test("stdcxx_libs", ("-pgc++libs",), "pgi@1.0") - supported_flag_test("debug_flags", ["-g", "-gopt"], "pgi@1.0") - supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "pgi@1.0") + supported_flag_test("openmp_flag", "-mp", "pgi@=1.0") + supported_flag_test("cxx11_flag", "-std=c++11", "pgi@=1.0") + unsupported_flag_test("c99_flag", "pgi@=12.9") + supported_flag_test("c99_flag", "-c99", "pgi@=12.10") + unsupported_flag_test("c11_flag", "pgi@=15.2") + supported_flag_test("c11_flag", "-c11", "pgi@=15.3") + supported_flag_test("cc_pic_flag", "-fpic", "pgi@=1.0") + supported_flag_test("cxx_pic_flag", "-fpic", "pgi@=1.0") + supported_flag_test("f77_pic_flag", "-fpic", "pgi@=1.0") + supported_flag_test("fc_pic_flag", "-fpic", "pgi@=1.0") + supported_flag_test("stdcxx_libs", ("-pgc++libs",), "pgi@=1.0") + supported_flag_test("debug_flags", ["-g", "-gopt"], "pgi@=1.0") + supported_flag_test("opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4"], "pgi@=1.0") def test_xl_flags(): - supported_flag_test("openmp_flag", "-qsmp=omp", "xl@1.0") - unsupported_flag_test("cxx11_flag", "xl@13.0") - supported_flag_test("cxx11_flag", "-qlanglvl=extended0x", "xl@13.1") - unsupported_flag_test("c99_flag", "xl@10.0") - supported_flag_test("c99_flag", "-qlanglvl=extc99", "xl@10.1") - supported_flag_test("c99_flag", "-std=gnu99", "xl@13.1.1") - unsupported_flag_test("c11_flag", "xl@12.0") - supported_flag_test("c11_flag", "-qlanglvl=extc1x", "xl@12.1") - supported_flag_test("c11_flag", "-std=gnu11", "xl@13.1.2") - supported_flag_test("cc_pic_flag", "-qpic", "xl@1.0") - supported_flag_test("cxx_pic_flag", "-qpic", "xl@1.0") - supported_flag_test("f77_pic_flag", "-qpic", "xl@1.0") - supported_flag_test("fc_pic_flag", "-qpic", "xl@1.0") - supported_flag_test("fflags", "-qzerosize", "xl@1.0") - supported_flag_test("debug_flags", ["-g", "-g0", "-g1", "-g2", "-g8", "-g9"], "xl@1.0") + supported_flag_test("openmp_flag", "-qsmp=omp", "xl@=1.0") + unsupported_flag_test("cxx11_flag", "xl@=13.0") + supported_flag_test("cxx11_flag", "-qlanglvl=extended0x", "xl@=13.1") + unsupported_flag_test("c99_flag", "xl@=10.0") + supported_flag_test("c99_flag", "-qlanglvl=extc99", "xl@=10.1") + supported_flag_test("c99_flag", "-std=gnu99", "xl@=13.1.1") + unsupported_flag_test("c11_flag", "xl@=12.0") + supported_flag_test("c11_flag", "-qlanglvl=extc1x", "xl@=12.1") + supported_flag_test("c11_flag", "-std=gnu11", "xl@=13.1.2") + supported_flag_test("cc_pic_flag", "-qpic", "xl@=1.0") + supported_flag_test("cxx_pic_flag", "-qpic", "xl@=1.0") + supported_flag_test("f77_pic_flag", "-qpic", "xl@=1.0") + supported_flag_test("fc_pic_flag", "-qpic", "xl@=1.0") + supported_flag_test("fflags", "-qzerosize", "xl@=1.0") + supported_flag_test("debug_flags", ["-g", "-g0", "-g1", "-g2", "-g8", "-g9"], "xl@=1.0") supported_flag_test( - "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4", "-O5", "-Ofast"], "xl@1.0" + "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4", "-O5", "-Ofast"], "xl@=1.0" ) def test_xl_r_flags(): - supported_flag_test("openmp_flag", "-qsmp=omp", "xl_r@1.0") - unsupported_flag_test("cxx11_flag", "xl_r@13.0") - supported_flag_test("cxx11_flag", "-qlanglvl=extended0x", "xl_r@13.1") - unsupported_flag_test("c99_flag", "xl_r@10.0") - supported_flag_test("c99_flag", "-qlanglvl=extc99", "xl_r@10.1") - supported_flag_test("c99_flag", "-std=gnu99", "xl_r@13.1.1") - unsupported_flag_test("c11_flag", "xl_r@12.0") - supported_flag_test("c11_flag", "-qlanglvl=extc1x", "xl_r@12.1") - supported_flag_test("c11_flag", "-std=gnu11", "xl_r@13.1.2") - supported_flag_test("cc_pic_flag", "-qpic", "xl_r@1.0") - supported_flag_test("cxx_pic_flag", "-qpic", "xl_r@1.0") - supported_flag_test("f77_pic_flag", "-qpic", "xl_r@1.0") - supported_flag_test("fc_pic_flag", "-qpic", "xl_r@1.0") - supported_flag_test("fflags", "-qzerosize", "xl_r@1.0") - supported_flag_test("debug_flags", ["-g", "-g0", "-g1", "-g2", "-g8", "-g9"], "xl@1.0") + supported_flag_test("openmp_flag", "-qsmp=omp", "xl_r@=1.0") + unsupported_flag_test("cxx11_flag", "xl_r@=13.0") + supported_flag_test("cxx11_flag", "-qlanglvl=extended0x", "xl_r@=13.1") + unsupported_flag_test("c99_flag", "xl_r@=10.0") + supported_flag_test("c99_flag", "-qlanglvl=extc99", "xl_r@=10.1") + supported_flag_test("c99_flag", "-std=gnu99", "xl_r@=13.1.1") + unsupported_flag_test("c11_flag", "xl_r@=12.0") + supported_flag_test("c11_flag", "-qlanglvl=extc1x", "xl_r@=12.1") + supported_flag_test("c11_flag", "-std=gnu11", "xl_r@=13.1.2") + supported_flag_test("cc_pic_flag", "-qpic", "xl_r@=1.0") + supported_flag_test("cxx_pic_flag", "-qpic", "xl_r@=1.0") + supported_flag_test("f77_pic_flag", "-qpic", "xl_r@=1.0") + supported_flag_test("fc_pic_flag", "-qpic", "xl_r@=1.0") + supported_flag_test("fflags", "-qzerosize", "xl_r@=1.0") + supported_flag_test("debug_flags", ["-g", "-g0", "-g1", "-g2", "-g8", "-g9"], "xl@=1.0") supported_flag_test( - "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4", "-O5", "-Ofast"], "xl@1.0" + "opt_flags", ["-O", "-O0", "-O1", "-O2", "-O3", "-O4", "-O5", "-Ofast"], "xl@=1.0" ) @@ -662,8 +661,8 @@ def test_xl_r_flags(): [("gcc@4.7.2", False), ("clang@3.3", False), ("clang@8.0.0", True)], ) def test_detecting_mixed_toolchains(compiler_spec, expected_result, config): - compiler = spack.compilers.compilers_for_spec(compiler_spec).pop() - assert spack.compilers.is_mixed_toolchain(compiler) is expected_result + compiler = compilers.compilers_for_spec(compiler_spec).pop() + assert compilers.is_mixed_toolchain(compiler) is expected_result @pytest.mark.regression("14798,13733") @@ -692,7 +691,7 @@ def test_raising_if_compiler_target_is_over_specific(config): with spack.config.override("compilers", compilers): cfg = spack.compilers.get_compiler_config() with pytest.raises(ValueError): - spack.compilers.get_compilers(cfg, "gcc@9.0.1", arch_spec) + spack.compilers.get_compilers(cfg, spack.spec.CompilerSpec("gcc@9.0.1"), arch_spec) @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") @@ -844,7 +843,7 @@ def test_apple_clang_setup_environment(mock_executable, monkeypatch): apple_clang_cls = spack.compilers.class_for_compiler_name("apple-clang") compiler = apple_clang_cls( - spack.spec.CompilerSpec("apple-clang@11.0.0"), + spack.spec.CompilerSpec("apple-clang@=11.0.0"), "catalina", "x86_64", ["/usr/bin/clang", "/usr/bin/clang++", None, None], diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index 8671e5288f..8ef9b558b1 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -15,6 +15,7 @@ import llnl.util.lang import spack.compilers import spack.concretize +import spack.config import spack.detection import spack.error import spack.hash_types as ht @@ -22,7 +23,7 @@ import spack.platforms import spack.repo import spack.variant as vt from spack.concretize import find_spec -from spack.spec import Spec +from spack.spec import CompilerSpec, Spec from spack.version import ver @@ -148,10 +149,10 @@ class Root(Package): homepage = "http://www.example.com" url = "http://www.example.com/root-1.0.tar.gz" - version(1.0, sha256='abcde') - depends_on('changing') + version("1.0", sha256="abcde") + depends_on("changing") - conflicts('changing~foo') + conflicts("changing~foo") """ packages_dir.join("root", "package.py").write(root_pkg_str, ensure=True) @@ -162,17 +163,17 @@ class Changing(Package): {% if not delete_version %} - version(1.0, sha256='abcde') + version("1.0", sha256="abcde") {% endif %} - version(0.9, sha256='abcde') + version("0.9", sha256="abcde") {% if not delete_variant %} - variant('fee', default=True, description='nope') + variant("fee", default=True, description="nope") {% endif %} - variant('foo', default=True, description='nope') + variant("foo", default=True, description="nope") {% if add_variant %} - variant('fum', default=True, description='nope') - variant('fum2', default=True, description='nope') + variant("fum", default=True, description="nope") + variant("fum2", default=True, description="nope") {% endif %} """ @@ -228,7 +229,7 @@ class TestConcretize(object): check_concretize(spec) def test_concretize_mention_build_dep(self): - spec = check_concretize("cmake-client ^cmake@3.21.3") + spec = check_concretize("cmake-client ^cmake@=3.21.3") # Check parent's perspective of child to_dependencies = spec.edges_to_dependencies(name="cmake") @@ -243,9 +244,9 @@ class TestConcretize(object): def test_concretize_preferred_version(self): spec = check_concretize("python") - assert spec.versions == ver("2.7.11") + assert spec.version == ver("=2.7.11") spec = check_concretize("python@3.5.1") - assert spec.versions == ver("3.5.1") + assert spec.version == ver("=3.5.1") def test_concretize_with_restricted_virtual(self): check_concretize("mpileaks ^mpich2") @@ -280,10 +281,10 @@ class TestConcretize(object): def test_concretize_enable_disable_compiler_existence_check(self): with spack.concretize.enable_compiler_existence_check(): with pytest.raises(spack.concretize.UnavailableCompilerVersionError): - check_concretize("dttop %gcc@100.100") + check_concretize("dttop %gcc@=100.100") with spack.concretize.disable_compiler_existence_check(): - spec = check_concretize("dttop %gcc@100.100") + spec = check_concretize("dttop %gcc@=100.100") assert spec.satisfies("%gcc@100.100") assert spec["dtlink3"].satisfies("%gcc@100.100") @@ -335,7 +336,7 @@ class TestConcretize(object): spec = Spec("a %clang@12.2.0 platform=test os=fe target=fe") # Get the compiler that matches the spec ( - compiler = spack.compilers.compiler_for_spec("clang@12.2.0", spec.architecture) + compiler = spack.compilers.compiler_for_spec("clang@=12.2.0", spec.architecture) # Clear cache for compiler config since it has its own cache mechanism outside of config spack.compilers._cache_config_file = [] @@ -479,7 +480,7 @@ class TestConcretize(object): def test_no_matching_compiler_specs(self, mock_low_high_config): # only relevant when not building compilers as needed with spack.concretize.enable_compiler_existence_check(): - s = Spec("a %gcc@0.0.0") + s = Spec("a %gcc@=0.0.0") with pytest.raises(spack.concretize.UnavailableCompilerVersionError): s.concretize() @@ -748,10 +749,10 @@ class TestConcretize(object): @pytest.mark.parametrize( "spec, best_achievable", [ - ("mpileaks%gcc@4.4.7 ^dyninst@10.2.1 target=x86_64:", "core2"), - ("mpileaks%gcc@4.8 target=x86_64:", "haswell"), - ("mpileaks%gcc@5.3.0 target=x86_64:", "broadwell"), - ("mpileaks%apple-clang@5.1.0 target=x86_64:", "x86_64"), + ("mpileaks%gcc@=4.4.7 ^dyninst@=10.2.1 target=x86_64:", "core2"), + ("mpileaks%gcc@=4.8 target=x86_64:", "haswell"), + ("mpileaks%gcc@=5.3.0 target=x86_64:", "broadwell"), + ("mpileaks%apple-clang@=5.1.0 target=x86_64:", "x86_64"), ], ) @pytest.mark.regression("13361", "20537") @@ -764,18 +765,15 @@ class TestConcretize(object): s = Spec(spec).concretized() assert str(s.architecture.target) == str(expected) - @pytest.mark.regression("8735,14730") def test_compiler_version_matches_any_entry_in_compilers_yaml(self): - # Ensure that a concrete compiler with different compiler version - # doesn't match (here it's 10.2 vs. 10.2.1) - with pytest.raises(spack.concretize.UnavailableCompilerVersionError): - s = Spec("mpileaks %gcc@10.2") - s.concretize() + # The behavior here has changed since #8735 / #14730. Now %gcc@10.2 is an abstract + # compiler spec, and it should first find a matching compiler gcc@=10.2.1 + assert Spec("mpileaks %gcc@10.2").concretized().compiler == CompilerSpec("gcc@=10.2.1") + assert Spec("mpileaks %gcc@10.2:").concretized().compiler == CompilerSpec("gcc@=10.2.1") - # An abstract compiler with a version list could resolve to 4.5.0 - s = Spec("mpileaks %gcc@10.2:") - s.concretize() - assert str(s.compiler.version) == "10.2.1" + # This compiler does not exist + with pytest.raises(spack.concretize.UnavailableCompilerVersionError): + Spec("mpileaks %gcc@=10.2").concretized() def test_concretize_anonymous(self): with pytest.raises(spack.error.SpackError): @@ -1158,7 +1156,7 @@ class TestConcretize(object): else "d0df7988457ec999c148a4a2af25ce831bfaad13954ba18a4446374cb0aef55e" ) localpatch = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - spec = spack.spec.Spec("conditionally-patch-dependency+jasper") + spec = Spec("conditionally-patch-dependency+jasper") spec.concretize() assert (uuidpatch, localpatch) == spec["libelf"].variants["patches"].value @@ -1411,14 +1409,14 @@ class TestConcretize(object): # a transitive dependency with a multi-valued variant, that old # version was preferred because of the order of our optimization # criteria. - s = spack.spec.Spec("root").concretized() + s = Spec("root").concretized() assert s["gmt"].satisfies("@2.0") @pytest.mark.regression("24205") def test_provider_must_meet_requirements(self): # A package can be a provider of a virtual only if the underlying # requirements are met. - s = spack.spec.Spec("unsat-virtual-dependency") + s = Spec("unsat-virtual-dependency") with pytest.raises((RuntimeError, spack.error.UnsatisfiableSpecError)): s.concretize() @@ -1432,7 +1430,7 @@ class TestConcretize(object): # root@1.0 <- middle@1.0 <- leaf@1.0 # # and "blas" is pulled in only by newer versions of "leaf" - s = spack.spec.Spec("root-adds-virtual").concretized() + s = Spec("root-adds-virtual").concretized() assert s["leaf-adds-virtual"].satisfies("@2.0") assert "blas" in s @@ -1440,12 +1438,12 @@ class TestConcretize(object): def test_versions_in_virtual_dependencies(self): # Ensure that a package that needs a given version of a virtual # package doesn't end up using a later implementation - s = spack.spec.Spec("hpcviewer@2019.02").concretized() + s = Spec("hpcviewer@2019.02").concretized() assert s["java"].satisfies("virtual-with-versions@1.8.0") @pytest.mark.regression("26866") def test_non_default_provider_of_multiple_virtuals(self): - s = spack.spec.Spec("many-virtual-consumer ^low-priority-provider").concretized() + s = Spec("many-virtual-consumer ^low-priority-provider").concretized() assert s["mpi"].name == "low-priority-provider" assert s["lapack"].name == "low-priority-provider" @@ -1471,7 +1469,7 @@ class TestConcretize(object): # like additional constraints being added to concrete specs in # the answer set produced by clingo. with spack.config.override("concretizer:reuse", True): - s = spack.spec.Spec(spec_str).concretized() + s = Spec(spec_str).concretized() assert s.installed is expect_installed assert s.satisfies(spec_str) @@ -1485,12 +1483,12 @@ class TestConcretize(object): # to have +allow-gcc set to be concretized with %gcc and clingo is not allowed # to change the default ~allow-gcc with pytest.raises(spack.error.SpackError): - spack.spec.Spec("sticky-variant %gcc").concretized() + Spec("sticky-variant %gcc").concretized() - s = spack.spec.Spec("sticky-variant+allow-gcc %gcc").concretized() + s = Spec("sticky-variant+allow-gcc %gcc").concretized() assert s.satisfies("%gcc") and s.satisfies("+allow-gcc") - s = spack.spec.Spec("sticky-variant %clang").concretized() + s = Spec("sticky-variant %clang").concretized() assert s.satisfies("%clang") and s.satisfies("~allow-gcc") def test_do_not_invent_new_concrete_versions_unless_necessary(self): @@ -1499,10 +1497,10 @@ class TestConcretize(object): # ensure we select a known satisfying version rather than creating # a new '2.7' version. - assert ver("2.7.11") == Spec("python@2.7").concretized().version + assert ver("=2.7.11") == Spec("python@2.7").concretized().version # Here there is no known satisfying version - use the one on the spec. - assert ver("2.7.21") == Spec("python@2.7.21").concretized().version + assert ver("=2.7.21") == Spec("python@=2.7.21").concretized().version @pytest.mark.parametrize( "spec_str,valid", @@ -1663,7 +1661,7 @@ class TestConcretize(object): if spack.config.get("config:concretizer") == "original": pytest.skip("Original concretizer cannot concretize in rounds") - specs = [spack.spec.Spec(s) for s in specs] + specs = [Spec(s) for s in specs] solver = spack.solver.asp.Solver() solver.reuse = False concrete_specs = set() @@ -1710,7 +1708,7 @@ class TestConcretize(object): if spack.config.get("config:concretizer") == "original": pytest.skip("Original concretizer cannot concretize in rounds") - specs = [spack.spec.Spec(s) for s in specs] + specs = [Spec(s) for s in specs] solver = spack.solver.asp.Solver() solver.reuse = False concrete_specs = {} @@ -1731,9 +1729,9 @@ class TestConcretize(object): reusable_specs = [] for s in ["mpileaks ^mpich", "zmpi"]: - reusable_specs.extend(spack.spec.Spec(s).concretized().traverse(root=True)) + reusable_specs.extend(Spec(s).concretized().traverse(root=True)) - root_specs = [spack.spec.Spec("mpileaks"), spack.spec.Spec("zmpi")] + root_specs = [Spec("mpileaks"), Spec("zmpi")] import spack.solver.asp @@ -1755,8 +1753,8 @@ class TestConcretize(object): if spack.config.get("config:concretizer") == "original": pytest.skip("Original concretizer cannot reuse") - reusable_specs = [spack.spec.Spec("non-existing-conditional-dep@1.0").concretized()] - root_spec = spack.spec.Spec("non-existing-conditional-dep@2.0") + reusable_specs = [Spec("non-existing-conditional-dep@1.0").concretized()] + root_spec = Spec("non-existing-conditional-dep@2.0") with spack.config.override("concretizer:reuse", True): solver = spack.solver.asp.Solver() @@ -1774,10 +1772,8 @@ class TestConcretize(object): if spack.config.get("config:concretizer") == "original": pytest.skip("Original concretizer cannot reuse") - reusable_specs = [ - spack.spec.Spec(spec_str).concretized() for spec_str in ("b@0.9", "b@1.0") - ] - root_spec = spack.spec.Spec("a foobar=bar") + reusable_specs = [Spec(spec_str).concretized() for spec_str in ("b@0.9", "b@1.0")] + root_spec = Spec("a foobar=bar") with spack.config.override("concretizer:reuse", True): solver = spack.solver.asp.Solver() @@ -1811,7 +1807,7 @@ class TestConcretize(object): if spack.config.get("config:concretizer") == "original": pytest.skip("Original concretizer cannot reuse") - root_spec = spack.spec.Spec("b") + root_spec = Spec("b") s = root_spec.concretized() wrong_compiler, wrong_os = s.copy(), s.copy() wrong_compiler.compiler = spack.spec.CompilerSpec("gcc@12.1.0") @@ -1926,7 +1922,7 @@ class TestConcretize(object): # Add a conflict to "mpich" that match an already installed "mpich~debug" pkg_cls = spack.repo.path.get_pkg_class("mpich") - monkeypatch.setitem(pkg_cls.conflicts, "~debug", [(spack.spec.Spec(), None)]) + monkeypatch.setitem(pkg_cls.conflicts, "~debug", [(Spec(), None)]) # If we concretize with --fresh the conflict is taken into account with spack.config.override("concretizer:reuse", False): @@ -1998,7 +1994,7 @@ class TestConcretize(object): assert "python" in spec["py-extension1"] assert spec["python"].prefix == fake_path - # The spec is not equal to spack.spec.Spec("python@configured") because it gets a + # The spec is not equal to Spec("python@configured") because it gets a # namespace and an external prefix before marking concrete assert spec["python"].satisfies(python_spec) @@ -2029,7 +2025,7 @@ class TestConcretize(object): assert "python" in spec["py-extension1"] assert spec["python"].prefix == fake_path - # The spec is not equal to spack.spec.Spec("python@configured") because it gets a + # The spec is not equal to Spec("python@configured") because it gets a # namespace and an external prefix before marking concrete assert spec["python"].satisfies(python) @@ -2037,7 +2033,7 @@ class TestConcretize(object): """Test that python extensions have access to a python dependency when python isn't otherwise in the DAG""" - python_spec = spack.spec.Spec("python@detected") + python_spec = Spec("python@=detected") prefix = os.path.sep + "fake" def find_fake_python(classes, path_hints): @@ -2068,7 +2064,7 @@ class TestConcretize(object): } spack.config.set("packages", external_conf) - abstract_specs = [spack.spec.Spec(s) for s in ["py-extension1", "python"]] + abstract_specs = [Spec(s) for s in ["py-extension1", "python"]] specs = spack.concretize.concretize_specs_together(*abstract_specs) assert specs[0]["python"] == specs[1]["python"] @@ -2085,7 +2081,7 @@ class TestConcretize(object): """Check that the implementation of "result.specs" is correct in cases where we know a concretization exists. """ - specs = [spack.spec.Spec(s) for s in specs] + specs = [Spec(s) for s in specs] solver = spack.solver.asp.Solver() setup = spack.solver.asp.SpackSolverSetup() result, _, _ = solver.driver.solve(setup, specs, reuse=[]) @@ -2127,8 +2123,8 @@ class TestConcretize(object): }, ] spack.config.set("compilers", compiler_configuration) - s = spack.spec.Spec("a %gcc@:11").concretized() - assert s.compiler.version == ver("11.1.0"), s + s = Spec("a %gcc@:11").concretized() + assert s.compiler.version == ver("=11.1.0"), s @pytest.mark.regression("36339") @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows") @@ -2148,8 +2144,8 @@ class TestConcretize(object): } ] spack.config.set("compilers", compiler_configuration) - s = spack.spec.Spec("a %gcc@foo").concretized() - assert s.compiler.version == ver("foo") + s = Spec("a %gcc@foo").concretized() + assert s.compiler.version == ver("=foo") @pytest.mark.regression("36628") def test_concretization_with_compilers_supporting_target_any(self): diff --git a/lib/spack/spack/test/concretize_preferences.py b/lib/spack/spack/test/concretize_preferences.py index 2a5ccd1f96..0a8d1c2ce6 100644 --- a/lib/spack/spack/test/concretize_preferences.py +++ b/lib/spack/spack/test/concretize_preferences.py @@ -13,7 +13,7 @@ import spack.package_prefs import spack.repo import spack.util.spack_yaml as syaml from spack.config import ConfigError -from spack.spec import Spec +from spack.spec import CompilerSpec, Spec from spack.version import Version @@ -109,10 +109,13 @@ class TestConcretizePreferences(object): ) def test_preferred_compilers(self, compiler_str, spec_str): """Test preferred compilers are applied correctly""" - spec = spack.spec.Spec(spec_str) + spec = Spec(spec_str) update_packages(spec.name, "compiler", [compiler_str]) spec.concretize() - assert spec.compiler == spack.spec.CompilerSpec(compiler_str) + # note: lhs has concrete compiler version, rhs still abstract. + # Could be made more strict by checking for equality with `gcc@=4.5.0` + # etc. + assert spec.compiler.satisfies(CompilerSpec(compiler_str)) def test_preferred_target(self, mutable_mock_repo): """Test preferred targets are applied correctly""" diff --git a/lib/spack/spack/test/concretize_requirements.py b/lib/spack/spack/test/concretize_requirements.py index ecb33c6527..ee022b5f77 100644 --- a/lib/spack/spack/test/concretize_requirements.py +++ b/lib/spack/spack/test/concretize_requirements.py @@ -9,8 +9,10 @@ import pytest import spack.build_systems.generic import spack.config +import spack.package_base import spack.repo import spack.util.spack_yaml as syaml +import spack.version from spack.solver.asp import UnsatisfiableSpecError from spack.spec import Spec from spack.util.url import path_to_file_url @@ -27,14 +29,14 @@ _pkgx = ( "x", """\ class X(Package): - version('1.1') - version('1.0') - version('0.9') + version("1.1") + version("1.0") + version("0.9") - variant('shared', default=True, - description='Build shared libraries') + variant("shared", default=True, + description="Build shared libraries") - depends_on('y') + depends_on("y") """, ) @@ -43,12 +45,12 @@ _pkgy = ( "y", """\ class Y(Package): - version('2.5') - version('2.4') - version('2.3', deprecated=True) + version("2.5") + version("2.4") + version("2.3", deprecated=True) - variant('shared', default=True, - description='Build shared libraries') + variant("shared", default=True, + description="Build shared libraries") """, ) @@ -57,8 +59,8 @@ _pkgv = ( "v", """\ class V(Package): - version('2.1') - version('2.0') + version("2.1") + version("2.0") """, ) @@ -150,27 +152,50 @@ def test_git_user_supplied_reference_satisfaction( spack.package_base.PackageBase, "git", path_to_file_url(repo_path), raising=False ) - specs = ["v@{commit0}=2.2", "v@{commit0}", "v@2.2", "v@{commit0}=2.3"] + hash_eq_ver = Spec(f"v@{commits[0]}=2.2") + hash_eq_ver_copy = Spec(f"v@{commits[0]}=2.2") + just_hash = Spec(f"v@{commits[0]}") + just_ver = Spec("v@=2.2") + hash_eq_other_ver = Spec(f"v@{commits[0]}=2.3") - format_info = {"commit0": commits[0]} + assert not hash_eq_ver == just_hash + assert not hash_eq_ver.satisfies(just_hash) + assert not hash_eq_ver.intersects(just_hash) - hash_eq_ver, just_hash, just_ver, hash_eq_other_ver = [ - Spec(x.format(**format_info)) for x in specs - ] - - assert hash_eq_ver.satisfies(just_hash) - assert not just_hash.satisfies(hash_eq_ver) - assert hash_eq_ver.satisfies(just_ver) + # Git versions and literal versions are distinct versions, like + # pkg@10.1.0 and pkg@10.1.0-suffix are distinct versions. + assert not hash_eq_ver.satisfies(just_ver) assert not just_ver.satisfies(hash_eq_ver) + assert not hash_eq_ver.intersects(just_ver) + assert hash_eq_ver != just_ver + assert just_ver != hash_eq_ver + assert not hash_eq_ver == just_ver + assert not just_ver == hash_eq_ver + + # When a different version is associated, they're not equal assert not hash_eq_ver.satisfies(hash_eq_other_ver) assert not hash_eq_other_ver.satisfies(hash_eq_ver) + assert not hash_eq_ver.intersects(hash_eq_other_ver) + assert not hash_eq_other_ver.intersects(hash_eq_ver) + assert hash_eq_ver != hash_eq_other_ver + assert hash_eq_other_ver != hash_eq_ver + assert not hash_eq_ver == hash_eq_other_ver + assert not hash_eq_other_ver == hash_eq_ver + + # These should be equal + assert hash_eq_ver == hash_eq_ver_copy + assert not hash_eq_ver != hash_eq_ver_copy + assert hash_eq_ver.satisfies(hash_eq_ver_copy) + assert hash_eq_ver_copy.satisfies(hash_eq_ver) + assert hash_eq_ver.intersects(hash_eq_ver_copy) + assert hash_eq_ver_copy.intersects(hash_eq_ver) def test_requirement_adds_new_version( concretize_scope, test_repo, mock_git_version_info, monkeypatch ): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") repo_path, filename, commits = mock_git_version_info monkeypatch.setattr( @@ -189,7 +214,6 @@ packages: s1 = Spec("v").concretized() assert s1.satisfies("@2.2") - assert s1.satisfies("@{0}".format(a_commit_hash)) # Make sure the git commit info is retained assert isinstance(s1.version, spack.version.GitVersion) assert s1.version.ref == a_commit_hash @@ -199,7 +223,7 @@ def test_requirement_adds_git_hash_version( concretize_scope, test_repo, mock_git_version_info, monkeypatch ): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") repo_path, filename, commits = mock_git_version_info monkeypatch.setattr( @@ -207,46 +231,39 @@ def test_requirement_adds_git_hash_version( ) a_commit_hash = commits[0] - conf_str = """\ + conf_str = f"""\ packages: v: - require: "@{0}" -""".format( - a_commit_hash - ) + require: "@{a_commit_hash}" +""" update_packages_config(conf_str) s1 = Spec("v").concretized() - assert s1.satisfies("@{0}".format(a_commit_hash)) + assert isinstance(s1.version, spack.version.GitVersion) + assert s1.satisfies(f"v@{a_commit_hash}") def test_requirement_adds_multiple_new_versions( concretize_scope, test_repo, mock_git_version_info, monkeypatch ): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") repo_path, filename, commits = mock_git_version_info monkeypatch.setattr( spack.package_base.PackageBase, "git", path_to_file_url(repo_path), raising=False ) - conf_str = """\ + conf_str = f"""\ packages: v: require: - - one_of: ["@{0}=2.2", "@{1}=2.3"] -""".format( - commits[0], commits[1] - ) + - one_of: ["@{commits[0]}=2.2", "@{commits[1]}=2.3"] +""" update_packages_config(conf_str) - s1 = Spec("v").concretized() - assert s1.satisfies("@2.2") - - s2 = Spec("v@{0}".format(commits[1])).concretized() - assert s2.satisfies("@{0}".format(commits[1])) - assert s2.satisfies("@2.3") + assert Spec("v").concretized().satisfies(f"@{commits[0]}=2.2") + assert Spec("v@2.3").concretized().satisfies(f"v@{commits[1]}=2.3") # TODO: this belongs in the concretize_preferences test module but uses @@ -255,35 +272,27 @@ def test_preference_adds_new_version( concretize_scope, test_repo, mock_git_version_info, monkeypatch ): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") repo_path, filename, commits = mock_git_version_info monkeypatch.setattr( spack.package_base.PackageBase, "git", path_to_file_url(repo_path), raising=False ) - conf_str = """\ + conf_str = f"""\ packages: v: - version: ["{0}=2.2", "{1}=2.3"] -""".format( - commits[0], commits[1] - ) + version: ["{commits[0]}=2.2", "{commits[1]}=2.3"] +""" update_packages_config(conf_str) - s1 = Spec("v").concretized() - assert s1.satisfies("@2.2") - assert s1.satisfies("@{0}".format(commits[0])) - - s2 = Spec("v@2.3").concretized() - # Note: this check will fail: the command-line spec version is preferred - # assert s2.satisfies("@{0}".format(commits[1])) - assert s2.satisfies("@2.3") + assert Spec("v").concretized().satisfies(f"@{commits[0]}=2.2") + assert Spec("v@2.3").concretized().satisfies(f"@{commits[1]}=2.3") - s3 = Spec("v@{0}".format(commits[1])).concretized() - assert s3.satisfies("@{0}".format(commits[1])) - # Note: this check will fail: the command-line spec version is preferred - # assert s3.satisfies("@2.3") + # When installing by hash, a lookup is triggered, so it's not mapped to =2.3. + s3 = Spec(f"v@{commits[1]}").concretized() + assert s3.satisfies(f"v@{commits[1]}") + assert not s3.satisfies("@2.3") def test_requirement_is_successfully_applied(concretize_scope, test_repo): @@ -291,7 +300,7 @@ def test_requirement_is_successfully_applied(concretize_scope, test_repo): concretization succeeds and the requirement spec is applied. """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") s1 = Spec("x").concretized() # Without any requirements/preferences, the later version is preferred @@ -313,7 +322,7 @@ def test_multiple_packages_requirements_are_respected(concretize_scope, test_rep succeeds and both requirements are respected. """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -333,7 +342,7 @@ def test_oneof(concretize_scope, test_repo): the specs in the group (but not all have to be satisfied). """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -353,7 +362,7 @@ def test_one_package_multiple_oneof_groups(concretize_scope, test_repo): applied. """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -377,7 +386,7 @@ def test_requirements_for_package_that_is_not_needed(concretize_scope, test_repo the requirements are used for the requested spec). """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") # Note that the exact contents aren't important since this isn't # intended to be used, but the important thing is that a number of @@ -403,7 +412,7 @@ def test_oneof_ordering(concretize_scope, test_repo): later versions). """ if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -422,7 +431,7 @@ packages: def test_reuse_oneof(concretize_scope, create_test_repo, mutable_database, fake_installs): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -445,7 +454,7 @@ packages: def test_requirements_are_higher_priority_than_deprecation(concretize_scope, test_repo): """Test that users can override a deprecated version with a requirement.""" if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") # @2.3 is a deprecated versions. Ensure that any_of picks both constraints, # since they are possible @@ -466,7 +475,7 @@ packages: def test_default_requirements_with_all(spec_str, requirement_str, concretize_scope, test_repo): """Test that default requirements are applied to all packages.""" if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: @@ -494,7 +503,7 @@ def test_default_and_package_specific_requirements( ): """Test that specific package requirements override default package requirements.""" if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") generic_req, specific_req = requirements generic_exp, specific_exp = expectations conf_str = """\ @@ -517,7 +526,7 @@ packages: @pytest.mark.parametrize("mpi_requirement", ["mpich", "mpich2", "zmpi"]) def test_requirements_on_virtual(mpi_requirement, concretize_scope, mock_packages): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: mpi: @@ -540,7 +549,7 @@ def test_requirements_on_virtual_and_on_package( mpi_requirement, specific_requirement, concretize_scope, mock_packages ): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: mpi: @@ -560,7 +569,7 @@ packages: def test_incompatible_virtual_requirements_raise(concretize_scope, mock_packages): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: mpi: @@ -575,7 +584,7 @@ def test_incompatible_virtual_requirements_raise(concretize_scope, mock_packages def test_non_existing_variants_under_all(concretize_scope, mock_packages): if spack.config.get("config:concretizer") == "original": - pytest.skip("Original concretizer does not support configuration" " requirements") + pytest.skip("Original concretizer does not support configuration requirements") conf_str = """\ packages: all: diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index 1badc88019..cf67786136 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -123,7 +123,7 @@ def mock_git_version_info(git, tmpdir, override_git_repos_cache_path): o second commit (v1.0) o first commit - The repo consists of a single file, in which the Version._cmp representation + The repo consists of a single file, in which the GitVersion._ref_version representation of each commit is expressed as a string. Important attributes of the repo for test coverage are: multiple branches, @@ -175,7 +175,7 @@ def mock_git_version_info(git, tmpdir, override_git_repos_cache_path): # Add two commits and a tag on 1.x branch git("checkout", "-b", "1.x") - write_file(filename, "[1, 0, '', 1]") + write_file(filename, "[1, 0, 'git', 1]") commit("first 1.x commit") commits.append(latest_commit()) @@ -186,7 +186,7 @@ def mock_git_version_info(git, tmpdir, override_git_repos_cache_path): # Add two commits and a tag on main branch git("checkout", main) - write_file(filename, "[1, 0, '', 1]") + write_file(filename, "[1, 0, 'git', 1]") commit("third main commit") commits.append(latest_commit()) write_file(filename, "[2, 0]") @@ -196,7 +196,7 @@ def mock_git_version_info(git, tmpdir, override_git_repos_cache_path): # Add two more commits on 1.x branch to ensure we aren't cheating by using time git("checkout", "1.x") - write_file(filename, "[1, 1, '', 1]") + write_file(filename, "[1, 1, 'git', 1]") commit("third 1.x commit") commits.append(latest_commit()) write_file(filename, "[1, 2]") diff --git a/lib/spack/spack/test/cray_manifest.py b/lib/spack/spack/test/cray_manifest.py index 4ffff0b926..ba35f4083e 100644 --- a/lib/spack/spack/test/cray_manifest.py +++ b/lib/spack/spack/test/cray_manifest.py @@ -15,7 +15,12 @@ import os import pytest import spack +import spack.cmd +import spack.compilers +import spack.config import spack.cray_manifest as cray_manifest +import spack.spec +import spack.store from spack.cray_manifest import compiler_from_entry, entries_to_specs example_x_json_str = """\ @@ -348,7 +353,7 @@ def test_read_cray_manifest_twice_no_compiler_duplicates( ): if spack.config.get("config:concretizer") == "clingo": pytest.skip( - "The ASP-based concretizer is currently picky about " " OS matching and will fail." + "The ASP-based concretizer is currently picky about OS matching and will fail." ) with tmpdir.as_cwd(): @@ -362,7 +367,7 @@ def test_read_cray_manifest_twice_no_compiler_duplicates( compilers = spack.compilers.all_compilers() filtered = list( - c for c in compilers if c.spec == spack.spec.CompilerSpec("gcc@10.2.0.cray") + c for c in compilers if c.spec == spack.spec.CompilerSpec("gcc@=10.2.0.cray") ) assert len(filtered) == 1 diff --git a/lib/spack/spack/test/cvs_fetch.py b/lib/spack/spack/test/cvs_fetch.py index bf96a8e40d..4cf3105799 100644 --- a/lib/spack/spack/test/cvs_fetch.py +++ b/lib/spack/spack/test/cvs_fetch.py @@ -13,7 +13,7 @@ from spack.fetch_strategy import CvsFetchStrategy from spack.spec import Spec from spack.stage import Stage from spack.util.executable import which -from spack.version import ver +from spack.version import Version pytestmark = pytest.mark.skipif(not which("cvs"), reason="requires CVS to be installed") @@ -39,7 +39,7 @@ def test_fetch(type_of_test, mock_cvs_repository, config, mutable_mock_repo): # Construct the package under test spec = Spec("cvs-test").concretized() - spec.package.versions[ver("cvs")] = test.args + spec.package.versions[Version("cvs")] = test.args # Enter the stage directory and check some properties with spec.package.stage: diff --git a/lib/spack/spack/test/directives.py b/lib/spack/spack/test/directives.py index 40bbd08d8b..bbb5789e21 100644 --- a/lib/spack/spack/test/directives.py +++ b/lib/spack/spack/test/directives.py @@ -2,11 +2,14 @@ # Spack Project Developers. See the top-level COPYRIGHT file for details. # # SPDX-License-Identifier: (Apache-2.0 OR MIT) +from collections import namedtuple + import pytest import spack.directives import spack.repo import spack.spec +import spack.version def test_false_directives_do_not_exist(mock_packages): @@ -84,3 +87,20 @@ def test_error_on_anonymous_dependency(config, mock_packages): def test_maintainer_directive(config, mock_packages, package_name, expected_maintainers): pkg_cls = spack.repo.path.get_pkg_class(package_name) assert pkg_cls.maintainers == expected_maintainers + + +def test_version_type_validation(): + # A version should be a string or an int, not a float, because it leads to subtle issues + # such as 3.10 being interpreted as 3.1. + + package = namedtuple("package", ["name"]) + + msg = r"python: declared version '.+' in package should be a string or int\." + + # Pass a float + with pytest.raises(spack.version.VersionError, match=msg): + spack.directives._execute_version(package(name="python"), 3.10) + + # Try passing a bogus type; it's just that we want a nice error message + with pytest.raises(spack.version.VersionError, match=msg): + spack.directives._execute_version(package(name="python"), {}) diff --git a/lib/spack/spack/test/git_fetch.py b/lib/spack/spack/test/git_fetch.py index bf647da55e..a31648e224 100644 --- a/lib/spack/spack/test/git_fetch.py +++ b/lib/spack/spack/test/git_fetch.py @@ -16,7 +16,7 @@ import spack.repo from spack.fetch_strategy import GitFetchStrategy from spack.spec import Spec from spack.stage import Stage -from spack.version import ver +from spack.version import Version _mock_transport_error = "Mock HTTP transport error" @@ -36,7 +36,7 @@ def git_version(git, request, monkeypatch): # Don't patch; run with the real git_version method. yield real_git_version else: - test_git_version = ver(request.param) + test_git_version = Version(request.param) if test_git_version > real_git_version: pytest.skip("Can't test clone logic for newer version of git.") @@ -61,7 +61,7 @@ def mock_bad_git(monkeypatch): # Patch the fetch strategy to think it's using a git version that # will error out when git is called. monkeypatch.setattr(GitFetchStrategy, "git", bad_git) - monkeypatch.setattr(GitFetchStrategy, "git_version", ver("1.7.1")) + monkeypatch.setattr(GitFetchStrategy, "git_version", Version("1.7.1")) yield @@ -107,7 +107,7 @@ def test_fetch( # Construct the package under test s = default_mock_concretization("git-test") - monkeypatch.setitem(s.package.versions, ver("git"), t.args) + monkeypatch.setitem(s.package.versions, Version("git"), t.args) # Enter the stage directory and check some properties with s.package.stage: @@ -154,7 +154,7 @@ def test_fetch_pkg_attr_submodule_init( # Construct the package under test s = default_mock_concretization("git-test") - monkeypatch.setitem(s.package.versions, ver("git"), t.args) + monkeypatch.setitem(s.package.versions, Version("git"), t.args) s.package.do_stage() collected_fnames = set() @@ -180,7 +180,7 @@ def test_adhoc_version_submodules( t = mock_git_repository.checks["tag"] # Construct the package under test pkg_class = spack.repo.path.get_pkg_class("git-test") - monkeypatch.setitem(pkg_class.versions, ver("git"), t.args) + monkeypatch.setitem(pkg_class.versions, Version("git"), t.args) monkeypatch.setattr(pkg_class, "git", "file://%s" % mock_git_repository.path, raising=False) spec = Spec("git-test@{0}".format(mock_git_repository.unversioned_commit)) @@ -203,7 +203,7 @@ def test_debug_fetch( # Construct the package under test s = default_mock_concretization("git-test") - monkeypatch.setitem(s.package.versions, ver("git"), t.args) + monkeypatch.setitem(s.package.versions, Version("git"), t.args) # Fetch then ensure source path exists with s.package.stage: @@ -243,7 +243,7 @@ def test_get_full_repo( ): """Ensure that we can clone a full repository.""" - if git_version < ver("1.7.1"): + if git_version < Version("1.7.1"): pytest.skip("Not testing get_full_repo for older git {0}".format(git_version)) secure = True @@ -254,7 +254,7 @@ def test_get_full_repo( s = default_mock_concretization("git-test") args = copy.copy(t.args) args["get_full_repo"] = get_full_repo - monkeypatch.setitem(s.package.versions, ver("git"), args) + monkeypatch.setitem(s.package.versions, Version("git"), args) with s.package.stage: with spack.config.override("config:verify_ssl", secure): @@ -299,7 +299,7 @@ def test_gitsubmodule( s = default_mock_concretization("git-test") args = copy.copy(t.args) args["submodules"] = submodules - monkeypatch.setitem(s.package.versions, ver("git"), args) + monkeypatch.setitem(s.package.versions, Version("git"), args) s.package.do_stage() with working_dir(s.package.stage.source_path): for submodule_count in range(2): @@ -332,7 +332,7 @@ def test_gitsubmodules_callable( s = default_mock_concretization("git-test") args = copy.copy(t.args) args["submodules"] = submodules_callback - monkeypatch.setitem(s.package.versions, ver("git"), args) + monkeypatch.setitem(s.package.versions, Version("git"), args) s.package.do_stage() with working_dir(s.package.stage.source_path): file_path = os.path.join(s.package.stage.source_path, "third_party/submodule0/r0_file_0") @@ -356,7 +356,7 @@ def test_gitsubmodules_delete( args = copy.copy(t.args) args["submodules"] = True args["submodules_delete"] = ["third_party/submodule0", "third_party/submodule1"] - monkeypatch.setitem(s.package.versions, ver("git"), args) + monkeypatch.setitem(s.package.versions, Version("git"), args) s.package.do_stage() with working_dir(s.package.stage.source_path): file_path = os.path.join(s.package.stage.source_path, "third_party/submodule0") diff --git a/lib/spack/spack/test/hg_fetch.py b/lib/spack/spack/test/hg_fetch.py index e2f7603c09..3939d460e9 100644 --- a/lib/spack/spack/test/hg_fetch.py +++ b/lib/spack/spack/test/hg_fetch.py @@ -16,7 +16,7 @@ from spack.fetch_strategy import HgFetchStrategy from spack.spec import Spec from spack.stage import Stage from spack.util.executable import which -from spack.version import ver +from spack.version import Version # Test functionality covered is supported on Windows, but currently failing # and expected to be fixed @@ -44,7 +44,7 @@ def test_fetch(type_of_test, secure, mock_hg_repository, config, mutable_mock_re # Construct the package under test s = Spec("hg-test").concretized() - monkeypatch.setitem(s.package.versions, ver("hg"), t.args) + monkeypatch.setitem(s.package.versions, Version("hg"), t.args) # Enter the stage directory and check some properties with s.package.stage: diff --git a/lib/spack/spack/test/installer.py b/lib/spack/spack/test/installer.py index 4e92802b72..91f02efbbc 100644 --- a/lib/spack/spack/test/installer.py +++ b/lib/spack/spack/test/installer.py @@ -17,12 +17,15 @@ import llnl.util.tty as tty import spack.binary_distribution import spack.compilers +import spack.concretize +import spack.config import spack.installer as inst import spack.package_prefs as prefs import spack.repo import spack.spec import spack.store import spack.util.lock as lk +import spack.version def _mock_repo(root, namespace): @@ -528,10 +531,12 @@ def test_bootstrapping_compilers_with_different_names_from_spec( ): with spack.config.override("config:install_missing_compilers", True): with spack.concretize.disable_compiler_existence_check(): - spec = spack.spec.Spec("trivial-install-test-package%oneapi@22.2.0").concretized() + spec = spack.spec.Spec("trivial-install-test-package%oneapi@=22.2.0").concretized() spec.package.do_install() - assert spack.spec.CompilerSpec("oneapi@22.2.0") in spack.compilers.all_compiler_specs() + assert ( + spack.spec.CompilerSpec("oneapi@=22.2.0") in spack.compilers.all_compiler_specs() + ) def test_dump_packages_deps_ok(install_mockery, tmpdir, mock_packages): diff --git a/lib/spack/spack/test/mirror.py b/lib/spack/spack/test/mirror.py index 80a6b8690c..2da1032d45 100644 --- a/lib/spack/spack/test/mirror.py +++ b/lib/spack/spack/test/mirror.py @@ -197,7 +197,7 @@ def test_invalid_json_mirror_collection(invalid_json, error_message): def test_mirror_archive_paths_no_version(mock_packages, config, mock_archive): - spec = Spec("trivial-install-test-package@nonexistingversion").concretized() + spec = Spec("trivial-install-test-package@=nonexistingversion").concretized() fetcher = spack.fetch_strategy.URLFetchStrategy(mock_archive.url) spack.mirror.mirror_archive_paths(fetcher, "per-package-ref", spec) @@ -281,8 +281,8 @@ def test_mirror_cache_symlinks(tmpdir): @pytest.mark.parametrize( "specs,expected_specs", [ - (["a"], ["a@1.0", "a@2.0"]), - (["a", "brillig"], ["a@1.0", "a@2.0", "brillig@1.0.0", "brillig@2.0.0"]), + (["a"], ["a@=1.0", "a@=2.0"]), + (["a", "brillig"], ["a@=1.0", "a@=2.0", "brillig@=1.0.0", "brillig@=2.0.0"]), ], ) def test_get_all_versions(specs, expected_specs): diff --git a/lib/spack/spack/test/modules/lmod.py b/lib/spack/spack/test/modules/lmod.py index 26370d1a6e..9dd37bb05f 100644 --- a/lib/spack/spack/test/modules/lmod.py +++ b/lib/spack/spack/test/modules/lmod.py @@ -24,7 +24,7 @@ writer_cls = spack.modules.lmod.LmodModulefileWriter pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -@pytest.fixture(params=["clang@12.0.0", "gcc@10.2.1"]) +@pytest.fixture(params=["clang@=12.0.0", "gcc@=10.2.1"]) def compiler(request): return request.param @@ -61,10 +61,10 @@ class TestLmod(object): # is transformed to r"Core" if the compiler is listed among core # compilers # Check that specs listed as core_specs are transformed to "Core" - if compiler == "clang@3.3" or spec_string == "mpich@3.0.1": + if compiler == "clang@=3.3" or spec_string == "mpich@3.0.1": assert "Core" in layout.available_path_parts else: - assert compiler.replace("@", "/") in layout.available_path_parts + assert compiler.replace("@=", "/") in layout.available_path_parts # Check that the provider part instead has always an hash even if # hash has been disallowed in the configuration file diff --git a/lib/spack/spack/test/multimethod.py b/lib/spack/spack/test/multimethod.py index 4df8eeafc2..8d4cec1253 100644 --- a/lib/spack/spack/test/multimethod.py +++ b/lib/spack/spack/test/multimethod.py @@ -54,8 +54,8 @@ def test_no_version_match(pkg_name): ("^mpich2@1.2", "mpi_version", 2), ("^mpich@1.0", "mpi_version", 1), # Undefined mpi versions - ("^mpich@0.4", "mpi_version", 1), - ("^mpich@1.4", "mpi_version", 1), + ("^mpich@=0.4", "mpi_version", 1), + ("^mpich@=1.4", "mpi_version", 1), # Constraints on compilers with a default ("%gcc", "has_a_default", "gcc"), ("%clang", "has_a_default", "clang"), @@ -107,11 +107,11 @@ def test_target_match(pkg_name): ("multimethod@2.0", "inherited_and_overridden", "base@2.0"), # Diamond-like inheritance (even though the MRO linearize everything) ("multimethod-diamond@1.0", "diamond_inheritance", "base_package"), - ("multimethod-base@1.0", "diamond_inheritance", "base_package"), + ("multimethod-base@=1.0", "diamond_inheritance", "base_package"), ("multimethod-diamond@2.0", "diamond_inheritance", "first_parent"), ("multimethod-inheritor@2.0", "diamond_inheritance", "first_parent"), - ("multimethod-diamond@3.0", "diamond_inheritance", "second_parent"), - ("multimethod-diamond-parent@3.0", "diamond_inheritance", "second_parent"), + ("multimethod-diamond@=3.0", "diamond_inheritance", "second_parent"), + ("multimethod-diamond-parent@=3.0", "diamond_inheritance", "second_parent"), ("multimethod-diamond@4.0", "diamond_inheritance", "subclass"), ], ) diff --git a/lib/spack/spack/test/patch.py b/lib/spack/spack/test/patch.py index 396cff12df..1be3f44b18 100644 --- a/lib/spack/spack/test/patch.py +++ b/lib/spack/spack/test/patch.py @@ -148,7 +148,7 @@ def test_patch_mixed_versions_subset_constraint(mock_packages, config): spec1.concretize() assert biz_sha256 in spec1.variants["patches"].value - spec2 = Spec("patch@1.0") + spec2 = Spec("patch@=1.0") spec2.concretize() assert biz_sha256 not in spec2.variants["patches"].value diff --git a/lib/spack/spack/test/spec_dag.py b/lib/spack/spack/test/spec_dag.py index 5c437da6ba..177deba6c2 100644 --- a/lib/spack/spack/test/spec_dag.py +++ b/lib/spack/spack/test/spec_dag.py @@ -986,8 +986,8 @@ def test_synthetic_construction_of_split_dependencies_from_same_package(mock_pac # To demonstrate that a spec can now hold two direct # dependencies from the same package root = Spec("b").concretized() - link_run_spec = Spec("c@1.0").concretized() - build_spec = Spec("c@2.0").concretized() + link_run_spec = Spec("c@=1.0").concretized() + build_spec = Spec("c@=2.0").concretized() root.add_dependency_edge(link_run_spec, deptypes="link") root.add_dependency_edge(link_run_spec, deptypes="run") @@ -1014,8 +1014,8 @@ def test_synthetic_construction_bootstrapping(mock_packages, config): # | build # b@1.0 # - root = Spec("b@2.0").concretized() - bootstrap = Spec("b@1.0").concretized() + root = Spec("b@=2.0").concretized() + bootstrap = Spec("b@=1.0").concretized() root.add_dependency_edge(bootstrap, deptypes="build") @@ -1032,8 +1032,8 @@ def test_addition_of_different_deptypes_in_multiple_calls(mock_packages, config) # b@1.0 # # with three calls and check we always have a single edge - root = Spec("b@2.0").concretized() - bootstrap = Spec("b@1.0").concretized() + root = Spec("b@=2.0").concretized() + bootstrap = Spec("b@=1.0").concretized() for current_deptype in ("build", "link", "run"): root.add_dependency_edge(bootstrap, deptypes=current_deptype) @@ -1059,9 +1059,9 @@ def test_addition_of_different_deptypes_in_multiple_calls(mock_packages, config) def test_adding_same_deptype_with_the_same_name_raises( mock_packages, config, c1_deptypes, c2_deptypes ): - p = Spec("b@2.0").concretized() - c1 = Spec("b@1.0").concretized() - c2 = Spec("b@2.0").concretized() + p = Spec("b@=2.0").concretized() + c1 = Spec("b@=1.0").concretized() + c2 = Spec("b@=2.0").concretized() p.add_dependency_edge(c1, deptypes=c1_deptypes) with pytest.raises(spack.error.SpackError): diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py index d1766eb8fc..bd755df84e 100644 --- a/lib/spack/spack/test/spec_semantics.py +++ b/lib/spack/spack/test/spec_semantics.py @@ -628,16 +628,16 @@ class TestSpecSemantics(object): # component: subcomponent of spec from which to get property package_segments = [ ("{NAME}", "", "name", lambda spec: spec), - ("{VERSION}", "", "versions", lambda spec: spec), + ("{VERSION}", "", "version", lambda spec: spec), ("{compiler}", "", "compiler", lambda spec: spec), ("{compiler_flags}", "", "compiler_flags", lambda spec: spec), ("{variants}", "", "variants", lambda spec: spec), ("{architecture}", "", "architecture", lambda spec: spec), - ("{@VERSIONS}", "@", "version", lambda spec: spec), + ("{@VERSIONS}", "@", "versions", lambda spec: spec), ("{%compiler}", "%", "compiler", lambda spec: spec), ("{arch=architecture}", "arch=", "architecture", lambda spec: spec), ("{compiler.name}", "", "name", lambda spec: spec.compiler), - ("{compiler.version}", "", "versions", lambda spec: spec.compiler), + ("{compiler.version}", "", "version", lambda spec: spec.compiler), ("{%compiler.name}", "%", "name", lambda spec: spec.compiler), ("{@compiler.version}", "@", "version", lambda spec: spec.compiler), ("{architecture.platform}", "", "platform", lambda spec: spec.architecture), diff --git a/lib/spack/spack/test/spec_syntax.py b/lib/spack/spack/test/spec_syntax.py index 87d1c7b8e0..ffce60b104 100644 --- a/lib/spack/spack/test/spec_syntax.py +++ b/lib/spack/spack/test/spec_syntax.py @@ -662,7 +662,13 @@ def test_dep_spec_by_hash(database): ).next_spec() assert "zmpi" in mpileaks_hash_zmpi assert mpileaks_hash_zmpi["zmpi"] == zmpi - assert mpileaks_hash_zmpi.compiler == mpileaks_zmpi.compiler + + # notice: the round-trip str -> Spec loses specificity when + # since %gcc@=x gets printed as %gcc@x. So stick to satisfies + # here, unless/until we want to differentiate between ranges + # and specific versions in the future. + # assert mpileaks_hash_zmpi.compiler == mpileaks_zmpi.compiler + assert mpileaks_zmpi.compiler.satisfies(mpileaks_hash_zmpi.compiler) mpileaks_hash_fake_and_zmpi = SpecParser( f"mpileaks ^/{fake.dag_hash()[:4]} ^ /{zmpi.dag_hash()[:5]}" diff --git a/lib/spack/spack/test/svn_fetch.py b/lib/spack/spack/test/svn_fetch.py index 58b4bbbe35..523b4b012c 100644 --- a/lib/spack/spack/test/svn_fetch.py +++ b/lib/spack/spack/test/svn_fetch.py @@ -16,7 +16,7 @@ from spack.fetch_strategy import SvnFetchStrategy from spack.spec import Spec from spack.stage import Stage from spack.util.executable import which -from spack.version import ver +from spack.version import Version pytestmark = [ pytest.mark.skipif( @@ -44,7 +44,7 @@ def test_fetch(type_of_test, secure, mock_svn_repository, config, mutable_mock_r # Construct the package under test s = Spec("svn-test").concretized() - monkeypatch.setitem(s.package.versions, ver("svn"), t.args) + monkeypatch.setitem(s.package.versions, Version("svn"), t.args) # Enter the stage directory and check some properties with s.package.stage: diff --git a/lib/spack/spack/test/url_fetch.py b/lib/spack/spack/test/url_fetch.py index 5842c6855d..81ae9d7471 100644 --- a/lib/spack/spack/test/url_fetch.py +++ b/lib/spack/spack/test/url_fetch.py @@ -21,7 +21,6 @@ import spack.util.web as web_util from spack.spec import Spec from spack.stage import Stage from spack.util.executable import which -from spack.version import ver @pytest.fixture(params=list(crypto.hashes.keys())) @@ -153,7 +152,10 @@ def test_fetch( # Get a spec and tweak the test package with new checksum params s = default_mock_concretization("url-test") s.package.url = mock_archive.url - s.package.versions[ver("test")] = {checksum_type: checksum, "url": s.package.url} + s.package.versions[spack.version.Version("test")] = { + checksum_type: checksum, + "url": s.package.url, + } # Enter the stage directory and check some properties with s.package.stage: @@ -175,13 +177,13 @@ def test_fetch( @pytest.mark.parametrize( "spec,url,digest", [ - ("url-list-test @0.0.0", "foo-0.0.0.tar.gz", "00000000000000000000000000000000"), - ("url-list-test @1.0.0", "foo-1.0.0.tar.gz", "00000000000000000000000000000100"), - ("url-list-test @3.0", "foo-3.0.tar.gz", "00000000000000000000000000000030"), - ("url-list-test @4.5", "foo-4.5.tar.gz", "00000000000000000000000000000450"), - ("url-list-test @2.0.0b2", "foo-2.0.0b2.tar.gz", "000000000000000000000000000200b2"), - ("url-list-test @3.0a1", "foo-3.0a1.tar.gz", "000000000000000000000000000030a1"), - ("url-list-test @4.5-rc5", "foo-4.5-rc5.tar.gz", "000000000000000000000000000045c5"), + ("url-list-test @=0.0.0", "foo-0.0.0.tar.gz", "00000000000000000000000000000000"), + ("url-list-test @=1.0.0", "foo-1.0.0.tar.gz", "00000000000000000000000000000100"), + ("url-list-test @=3.0", "foo-3.0.tar.gz", "00000000000000000000000000000030"), + ("url-list-test @=4.5", "foo-4.5.tar.gz", "00000000000000000000000000000450"), + ("url-list-test @=2.0.0b2", "foo-2.0.0b2.tar.gz", "000000000000000000000000000200b2"), + ("url-list-test @=3.0a1", "foo-3.0a1.tar.gz", "000000000000000000000000000030a1"), + ("url-list-test @=4.5-rc5", "foo-4.5-rc5.tar.gz", "000000000000000000000000000045c5"), ], ) @pytest.mark.parametrize("_fetch_method", ["curl", "urllib"]) @@ -209,7 +211,7 @@ def test_from_list_url(mock_packages, config, spec, url, digest, _fetch_method): [ # This version is in the web data path (test/data/web/4.html), but not in the # url-list-test package. We expect Spack to generate a URL with the new version. - ("4.5.0", "foo-4.5.0.tar.gz", None), + ("=4.5.0", "foo-4.5.0.tar.gz", None), # This version is in web data path and not in the package file, BUT the 2.0.0b2 # version in the package file satisfies 2.0.0, so Spack will use the known version. # TODO: this is *probably* not what the user wants, but it's here as an example diff --git a/lib/spack/spack/test/util/package_hash.py b/lib/spack/spack/test/util/package_hash.py index 331e2c4989..9ab0ab0c47 100644 --- a/lib/spack/spack/test/util/package_hash.py +++ b/lib/spack/spack/test/util/package_hash.py @@ -40,29 +40,26 @@ def compare_hash_sans_name(eq, spec1, spec2): content2 = content2.replace(pkg_cls2.__name__, "TestPackage") hash2 = pkg_cls2(spec2).content_hash(content=content2) - if eq: - assert hash1 == hash2 - else: - assert hash1 != hash2 + assert (hash1 == hash2) == eq def test_hash(mock_packages, config): - ph.package_hash(Spec("hash-test1@1.2")) + ph.package_hash(Spec("hash-test1@=1.2")) def test_different_variants(mock_packages, config): - spec1 = Spec("hash-test1@1.2 +variantx") - spec2 = Spec("hash-test1@1.2 +varianty") + spec1 = Spec("hash-test1@=1.2 +variantx") + spec2 = Spec("hash-test1@=1.2 +varianty") assert ph.package_hash(spec1) == ph.package_hash(spec2) def test_all_same_but_name(mock_packages, config): - spec1 = Spec("hash-test1@1.2") - spec2 = Spec("hash-test2@1.2") + spec1 = Spec("hash-test1@=1.2") + spec2 = Spec("hash-test2@=1.2") compare_sans_name(True, spec1, spec2) - spec1 = Spec("hash-test1@1.2 +varianty") - spec2 = Spec("hash-test2@1.2 +varianty") + spec1 = Spec("hash-test1@=1.2 +varianty") + spec2 = Spec("hash-test2@=1.2 +varianty") compare_sans_name(True, spec1, spec2) @@ -70,26 +67,26 @@ def test_all_same_but_archive_hash(mock_packages, config): """ Archive hash is not intended to be reflected in Package hash. """ - spec1 = Spec("hash-test1@1.3") - spec2 = Spec("hash-test2@1.3") + spec1 = Spec("hash-test1@=1.3") + spec2 = Spec("hash-test2@=1.3") compare_sans_name(True, spec1, spec2) def test_all_same_but_patch_contents(mock_packages, config): - spec1 = Spec("hash-test1@1.1") - spec2 = Spec("hash-test2@1.1") + spec1 = Spec("hash-test1@=1.1") + spec2 = Spec("hash-test2@=1.1") compare_sans_name(True, spec1, spec2) def test_all_same_but_patches_to_apply(mock_packages, config): - spec1 = Spec("hash-test1@1.4") - spec2 = Spec("hash-test2@1.4") + spec1 = Spec("hash-test1@=1.4") + spec2 = Spec("hash-test2@=1.4") compare_sans_name(True, spec1, spec2) def test_all_same_but_install(mock_packages, config): - spec1 = Spec("hash-test1@1.5") - spec2 = Spec("hash-test2@1.5") + spec1 = Spec("hash-test1@=1.5") + spec2 = Spec("hash-test2@=1.5") compare_sans_name(False, spec1, spec2) @@ -102,14 +99,14 @@ def test_content_hash_all_same_but_patch_contents(mock_packages, config): def test_content_hash_not_concretized(mock_packages, config): """Check that Package.content_hash() works on abstract specs.""" # these are different due to the package hash - spec1 = Spec("hash-test1@1.1") - spec2 = Spec("hash-test2@1.3") + spec1 = Spec("hash-test1@=1.1") + spec2 = Spec("hash-test2@=1.3") compare_hash_sans_name(False, spec1, spec2) # at v1.1 these are actually the same package when @when's are removed # and the name isn't considered - spec1 = Spec("hash-test1@1.1") - spec2 = Spec("hash-test2@1.1") + spec1 = Spec("hash-test1@=1.1") + spec2 = Spec("hash-test2@=1.1") compare_hash_sans_name(True, spec1, spec2) # these end up being different b/c we can't eliminate much of the package.py diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py index d10ade63ca..8b58bf14ee 100644 --- a/lib/spack/spack/test/versions.py +++ b/lib/spack/spack/test/versions.py @@ -16,7 +16,16 @@ from llnl.util.filesystem import working_dir import spack.package_base import spack.spec -from spack.version import GitVersion, Version, VersionBase, VersionList, VersionRange, ver +from spack.version import ( + GitVersion, + StandardVersion, + Version, + VersionList, + VersionLookupError, + VersionRange, + is_git_version, + ver, +) def assert_ver_lt(a, b): @@ -98,160 +107,160 @@ def check_union(expected, a, b): def test_string_prefix(): - assert_ver_eq("xsdk-0.2.0", "xsdk-0.2.0") - assert_ver_lt("xsdk-0.2.0", "xsdk-0.3") - assert_ver_gt("xsdk-0.3", "xsdk-0.2.0") + assert_ver_eq("=xsdk-0.2.0", "=xsdk-0.2.0") + assert_ver_lt("=xsdk-0.2.0", "=xsdk-0.3") + assert_ver_gt("=xsdk-0.3", "=xsdk-0.2.0") def test_two_segments(): - assert_ver_eq("1.0", "1.0") - assert_ver_lt("1.0", "2.0") - assert_ver_gt("2.0", "1.0") + assert_ver_eq("=1.0", "=1.0") + assert_ver_lt("=1.0", "=2.0") + assert_ver_gt("=2.0", "=1.0") def test_develop(): - assert_ver_eq("develop", "develop") - assert_ver_eq("develop.local", "develop.local") - assert_ver_lt("1.0", "develop") - assert_ver_gt("develop", "1.0") - assert_ver_eq("1.develop", "1.develop") - assert_ver_lt("1.1", "1.develop") - assert_ver_gt("1.develop", "1.0") - assert_ver_gt("0.5.develop", "0.5") - assert_ver_lt("0.5", "0.5.develop") - assert_ver_lt("1.develop", "2.1") - assert_ver_gt("2.1", "1.develop") - assert_ver_lt("1.develop.1", "1.develop.2") - assert_ver_gt("1.develop.2", "1.develop.1") - assert_ver_lt("develop.1", "develop.2") - assert_ver_gt("develop.2", "develop.1") + assert_ver_eq("=develop", "=develop") + assert_ver_eq("=develop.local", "=develop.local") + assert_ver_lt("=1.0", "=develop") + assert_ver_gt("=develop", "=1.0") + assert_ver_eq("=1.develop", "=1.develop") + assert_ver_lt("=1.1", "=1.develop") + assert_ver_gt("=1.develop", "=1.0") + assert_ver_gt("=0.5.develop", "=0.5") + assert_ver_lt("=0.5", "=0.5.develop") + assert_ver_lt("=1.develop", "=2.1") + assert_ver_gt("=2.1", "=1.develop") + assert_ver_lt("=1.develop.1", "=1.develop.2") + assert_ver_gt("=1.develop.2", "=1.develop.1") + assert_ver_lt("=develop.1", "=develop.2") + assert_ver_gt("=develop.2", "=develop.1") # other +infinity versions - assert_ver_gt("master", "9.0") - assert_ver_gt("head", "9.0") - assert_ver_gt("trunk", "9.0") - assert_ver_gt("develop", "9.0") + assert_ver_gt("=master", "=9.0") + assert_ver_gt("=head", "=9.0") + assert_ver_gt("=trunk", "=9.0") + assert_ver_gt("=develop", "=9.0") # hierarchical develop-like versions - assert_ver_gt("develop", "master") - assert_ver_gt("master", "head") - assert_ver_gt("head", "trunk") - assert_ver_gt("9.0", "system") + assert_ver_gt("=develop", "=master") + assert_ver_gt("=master", "=head") + assert_ver_gt("=head", "=trunk") + assert_ver_gt("=9.0", "=system") # not develop - assert_ver_lt("mydevelopmentnightmare", "1.1") - assert_ver_lt("1.mydevelopmentnightmare", "1.1") - assert_ver_gt("1.1", "1.mydevelopmentnightmare") + assert_ver_lt("=mydevelopmentnightmare", "=1.1") + assert_ver_lt("=1.mydevelopmentnightmare", "=1.1") + assert_ver_gt("=1.1", "=1.mydevelopmentnightmare") def test_isdevelop(): - assert ver("develop").isdevelop() - assert ver("develop.1").isdevelop() - assert ver("develop.local").isdevelop() - assert ver("master").isdevelop() - assert ver("head").isdevelop() - assert ver("trunk").isdevelop() - assert ver("1.develop").isdevelop() - assert ver("1.develop.2").isdevelop() - assert not ver("1.1").isdevelop() - assert not ver("1.mydevelopmentnightmare.3").isdevelop() - assert not ver("mydevelopmentnightmare.3").isdevelop() + assert ver("=develop").isdevelop() + assert ver("=develop.1").isdevelop() + assert ver("=develop.local").isdevelop() + assert ver("=master").isdevelop() + assert ver("=head").isdevelop() + assert ver("=trunk").isdevelop() + assert ver("=1.develop").isdevelop() + assert ver("=1.develop.2").isdevelop() + assert not ver("=1.1").isdevelop() + assert not ver("=1.mydevelopmentnightmare.3").isdevelop() + assert not ver("=mydevelopmentnightmare.3").isdevelop() def test_three_segments(): - assert_ver_eq("2.0.1", "2.0.1") - assert_ver_lt("2.0", "2.0.1") - assert_ver_gt("2.0.1", "2.0") + assert_ver_eq("=2.0.1", "=2.0.1") + assert_ver_lt("=2.0", "=2.0.1") + assert_ver_gt("=2.0.1", "=2.0") def test_alpha(): # TODO: not sure whether I like this. 2.0.1a is *usually* # TODO: less than 2.0.1, but special-casing it makes version # TODO: comparison complicated. See version.py - assert_ver_eq("2.0.1a", "2.0.1a") - assert_ver_gt("2.0.1a", "2.0.1") - assert_ver_lt("2.0.1", "2.0.1a") + assert_ver_eq("=2.0.1a", "=2.0.1a") + assert_ver_gt("=2.0.1a", "=2.0.1") + assert_ver_lt("=2.0.1", "=2.0.1a") def test_patch(): - assert_ver_eq("5.5p1", "5.5p1") - assert_ver_lt("5.5p1", "5.5p2") - assert_ver_gt("5.5p2", "5.5p1") - assert_ver_eq("5.5p10", "5.5p10") - assert_ver_lt("5.5p1", "5.5p10") - assert_ver_gt("5.5p10", "5.5p1") + assert_ver_eq("=5.5p1", "=5.5p1") + assert_ver_lt("=5.5p1", "=5.5p2") + assert_ver_gt("=5.5p2", "=5.5p1") + assert_ver_eq("=5.5p10", "=5.5p10") + assert_ver_lt("=5.5p1", "=5.5p10") + assert_ver_gt("=5.5p10", "=5.5p1") def test_num_alpha_with_no_separator(): - assert_ver_lt("10xyz", "10.1xyz") - assert_ver_gt("10.1xyz", "10xyz") - assert_ver_eq("xyz10", "xyz10") - assert_ver_lt("xyz10", "xyz10.1") - assert_ver_gt("xyz10.1", "xyz10") + assert_ver_lt("=10xyz", "=10.1xyz") + assert_ver_gt("=10.1xyz", "=10xyz") + assert_ver_eq("=xyz10", "=xyz10") + assert_ver_lt("=xyz10", "=xyz10.1") + assert_ver_gt("=xyz10.1", "=xyz10") def test_alpha_with_dots(): - assert_ver_eq("xyz.4", "xyz.4") - assert_ver_lt("xyz.4", "8") - assert_ver_gt("8", "xyz.4") - assert_ver_lt("xyz.4", "2") - assert_ver_gt("2", "xyz.4") + assert_ver_eq("=xyz.4", "=xyz.4") + assert_ver_lt("=xyz.4", "=8") + assert_ver_gt("=8", "=xyz.4") + assert_ver_lt("=xyz.4", "=2") + assert_ver_gt("=2", "=xyz.4") def test_nums_and_patch(): - assert_ver_lt("5.5p2", "5.6p1") - assert_ver_gt("5.6p1", "5.5p2") - assert_ver_lt("5.6p1", "6.5p1") - assert_ver_gt("6.5p1", "5.6p1") + assert_ver_lt("=5.5p2", "=5.6p1") + assert_ver_gt("=5.6p1", "=5.5p2") + assert_ver_lt("=5.6p1", "=6.5p1") + assert_ver_gt("=6.5p1", "=5.6p1") def test_rc_versions(): - assert_ver_gt("6.0.rc1", "6.0") - assert_ver_lt("6.0", "6.0.rc1") + assert_ver_gt("=6.0.rc1", "=6.0") + assert_ver_lt("=6.0", "=6.0.rc1") def test_alpha_beta(): - assert_ver_gt("10b2", "10a1") - assert_ver_lt("10a2", "10b2") + assert_ver_gt("=10b2", "=10a1") + assert_ver_lt("=10a2", "=10b2") def test_double_alpha(): - assert_ver_eq("1.0aa", "1.0aa") - assert_ver_lt("1.0a", "1.0aa") - assert_ver_gt("1.0aa", "1.0a") + assert_ver_eq("=1.0aa", "=1.0aa") + assert_ver_lt("=1.0a", "=1.0aa") + assert_ver_gt("=1.0aa", "=1.0a") def test_padded_numbers(): - assert_ver_eq("10.0001", "10.0001") - assert_ver_eq("10.0001", "10.1") - assert_ver_eq("10.1", "10.0001") - assert_ver_lt("10.0001", "10.0039") - assert_ver_gt("10.0039", "10.0001") + assert_ver_eq("=10.0001", "=10.0001") + assert_ver_eq("=10.0001", "=10.1") + assert_ver_eq("=10.1", "=10.0001") + assert_ver_lt("=10.0001", "=10.0039") + assert_ver_gt("=10.0039", "=10.0001") def test_close_numbers(): - assert_ver_lt("4.999.9", "5.0") - assert_ver_gt("5.0", "4.999.9") + assert_ver_lt("=4.999.9", "=5.0") + assert_ver_gt("=5.0", "=4.999.9") def test_date_stamps(): - assert_ver_eq("20101121", "20101121") - assert_ver_lt("20101121", "20101122") - assert_ver_gt("20101122", "20101121") + assert_ver_eq("=20101121", "=20101121") + assert_ver_lt("=20101121", "=20101122") + assert_ver_gt("=20101122", "=20101121") def test_underscores(): - assert_ver_eq("2_0", "2_0") - assert_ver_eq("2.0", "2_0") - assert_ver_eq("2_0", "2.0") - assert_ver_eq("2-0", "2_0") - assert_ver_eq("2_0", "2-0") + assert_ver_eq("=2_0", "=2_0") + assert_ver_eq("=2.0", "=2_0") + assert_ver_eq("=2_0", "=2.0") + assert_ver_eq("=2-0", "=2_0") + assert_ver_eq("=2_0", "=2-0") def test_rpm_oddities(): - assert_ver_eq("1b.fc17", "1b.fc17") - assert_ver_lt("1b.fc17", "1.fc17") - assert_ver_gt("1.fc17", "1b.fc17") - assert_ver_eq("1g.fc17", "1g.fc17") - assert_ver_gt("1g.fc17", "1.fc17") - assert_ver_lt("1.fc17", "1g.fc17") + assert_ver_eq("=1b.fc17", "=1b.fc17") + assert_ver_lt("=1b.fc17", "=1.fc17") + assert_ver_gt("=1.fc17", "=1b.fc17") + assert_ver_eq("=1g.fc17", "=1g.fc17") + assert_ver_gt("=1g.fc17", "=1.fc17") + assert_ver_lt("=1.fc17", "=1g.fc17") # Stuff below here is not taken from RPM's tests and is @@ -267,24 +276,24 @@ def test_version_ranges(): def test_contains(): - assert_in("1.3", "1.2:1.4") - assert_in("1.2.5", "1.2:1.4") - assert_in("1.3.5", "1.2:1.4") - assert_in("1.3.5-7", "1.2:1.4") - assert_not_in("1.1", "1.2:1.4") - assert_not_in("1.5", "1.2:1.4") - assert_not_in("1.5", "1.5.1:1.6") - assert_not_in("1.5", "1.5.1:") - - assert_in("1.4.2", "1.2:1.4") - assert_not_in("1.4.2", "1.2:1.4.0") - - assert_in("1.2.8", "1.2.7:1.4") + assert_in("=1.3", "1.2:1.4") + assert_in("=1.2.5", "1.2:1.4") + assert_in("=1.3.5", "1.2:1.4") + assert_in("=1.3.5-7", "1.2:1.4") + assert_not_in("=1.1", "1.2:1.4") + assert_not_in("=1.5", "1.2:1.4") + assert_not_in("=1.5", "1.5.1:1.6") + assert_not_in("=1.5", "1.5.1:") + + assert_in("=1.4.2", "1.2:1.4") + assert_not_in("=1.4.2", "1.2:1.4.0") + + assert_in("=1.2.8", "1.2.7:1.4") assert_in("1.2.7:1.4", ":") - assert_not_in("1.2.5", "1.2.7:1.4") + assert_not_in("=1.2.5", "1.2.7:1.4") - assert_in("1.4.1", "1.2.7:1.4") - assert_not_in("1.4.1", "1.2.7:1.4.0") + assert_in("=1.4.1", "1.2.7:1.4") + assert_not_in("=1.4.1", "1.2.7:1.4.0") def test_in_list(): @@ -370,6 +379,8 @@ def test_intersection(): check_intersection(["2.5:2.7"], ["1.1:2.7"], ["2.5:3.0", "1.0"]) check_intersection(["0:1"], [":"], ["0:1"]) + check_intersection(["=ref=1.0", "=1.1"], ["=ref=1.0", "1.1"], ["1:1.0", "=1.1"]) + def test_intersect_with_containment(): check_intersection("1.6.5", "1.6.5", ":1.6") @@ -397,6 +408,8 @@ def test_union_with_containment(): # Tests successor/predecessor case. check_union("1:4", "1:2", "3:4") + check_union(["1:1.0", "1.1"], ["=ref=1.0", "1.1"], ["1:1.0", "=1.1"]) + def test_basic_version_satisfaction(): assert_satisfies("4.7.3", "4.7.3") @@ -522,7 +535,7 @@ def test_up_to(): def test_repr_and_str(): def check_repr_and_str(vrs): a = Version(vrs) - assert repr(a) == "VersionBase('" + vrs + "')" + assert repr(a) == f'Version("{vrs}")' b = eval(repr(a)) assert a == b assert str(a) == vrs @@ -546,19 +559,19 @@ def test_get_item(): assert isinstance(a[1], int) # Test slicing b = a[0:2] - assert isinstance(b, VersionBase) + assert isinstance(b, StandardVersion) assert b == Version("0.1") - assert repr(b) == "VersionBase('0.1')" + assert repr(b) == 'Version("0.1")' assert str(b) == "0.1" b = a[0:3] - assert isinstance(b, VersionBase) + assert isinstance(b, StandardVersion) assert b == Version("0.1_2") - assert repr(b) == "VersionBase('0.1_2')" + assert repr(b) == 'Version("0.1_2")' assert str(b) == "0.1_2" b = a[1:] - assert isinstance(b, VersionBase) + assert isinstance(b, StandardVersion) assert b == Version("1_2-3") - assert repr(b) == "VersionBase('1_2-3')" + assert repr(b) == 'Version("1_2-3")' assert str(b) == "1_2-3" # Raise TypeError on tuples with pytest.raises(TypeError): @@ -566,12 +579,12 @@ def test_get_item(): def test_list_highest(): - vl = VersionList(["master", "1.2.3", "develop", "3.4.5", "foobar"]) + vl = VersionList(["=master", "=1.2.3", "=develop", "=3.4.5", "=foobar"]) assert vl.highest() == Version("develop") assert vl.lowest() == Version("foobar") assert vl.highest_numeric() == Version("3.4.5") - vl2 = VersionList(["master", "develop"]) + vl2 = VersionList(["=master", "=develop"]) assert vl2.highest_numeric() is None assert vl2.preferred() == Version("develop") assert vl2.lowest() == Version("master") @@ -593,10 +606,8 @@ def test_versions_from_git(git, mock_git_version_info, monkeypatch, mock_package for commit in commits: spec = spack.spec.Spec("git-test-commit@%s" % commit) - version = spec.version - comparator = [ - str(v) if not isinstance(v, int) else v for v in version._cmp(version.ref_lookup) - ] + version: GitVersion = spec.version + comparator = [str(v) if not isinstance(v, int) else v for v in version.ref_version] with working_dir(repo_path): git("checkout", commit) @@ -655,14 +666,14 @@ def test_git_ref_comparisons(mock_git_version_info, install_mockery, mock_packag spec_tag.concretize() assert spec_tag.satisfies("@1.0") assert not spec_tag.satisfies("@1.1:") - assert str(spec_tag.version) == "git.v1.0" + assert str(spec_tag.version) == "git.v1.0=1.0" # Spec based on branch 1.x spec_branch = spack.spec.Spec("git-test-commit@git.1.x") spec_branch.concretize() assert spec_branch.satisfies("@1.2") assert spec_branch.satisfies("@1.1:1.3") - assert str(spec_branch.version) == "git.1.x" + assert str(spec_branch.version) == "git.1.x=1.2" @pytest.mark.parametrize( @@ -676,6 +687,7 @@ def test_git_ref_comparisons(mock_git_version_info, install_mockery, mock_packag ], ) def test_version_git_vs_base(string, git): + assert is_git_version(string) == git assert isinstance(Version(string), GitVersion) == git @@ -713,21 +725,9 @@ def test_version_range_satisfies_means_nonempty_intersection(): assert not y.satisfies(x) -@pytest.mark.regression("26482") -def test_version_list_with_range_included_in_concrete_version_interpreted_as_range(): - # Note: this test only tests whether we can construct a version list of a range - # and a version, where the range is contained in the version when it is interpreted - # as a range. That is: Version('3.1') interpreted as VersionRange('3.1', '3.1'). - # Cleary it *shouldn't* be interpreted that way, but that is how Spack currently - # behaves, and this test only ensures that creating a VersionList of this type - # does not throw like reported in the linked Github issue. - VersionList([Version("3.1"), VersionRange("3.1.1", "3.1.2")]) - - -@pytest.mark.xfail def test_version_list_with_range_and_concrete_version_is_not_concrete(): - v = VersionList([Version("3.1"), VersionRange("3.1.1", "3.1.2")]) - assert v.concrete + v = VersionList([Version("3.1"), VersionRange(Version("3.1.1"), Version("3.1.2"))]) + assert not v.concrete @pytest.mark.parametrize( @@ -744,15 +744,14 @@ def test_git_ref_can_be_assigned_a_version(vstring, eq_vstring, is_commit): v = Version(vstring) v_equivalent = Version(eq_vstring) assert v.is_commit == is_commit - assert v.is_ref assert not v._ref_lookup - assert v_equivalent.version == v.ref_version + assert v_equivalent == v.ref_version @pytest.mark.parametrize( "lhs_str,rhs_str,expected", [ - # VersionBase + # StandardVersion ("4.7.3", "4.7.3", (True, True, True)), ("4.7.3", "4.7", (True, True, False)), ("4.7.3", "4", (True, True, False)), @@ -808,3 +807,170 @@ def test_git_versions_without_explicit_reference( for test_str, expected in tested_intersects: assert spec.intersects(test_str) is expected, test_str + + +def test_total_order_versions_and_ranges(): + # The set of version ranges and individual versions are comparable, which is used in + # VersionList. The comparsion across types is based on default version comparsion + # of StandardVersion, GitVersion.ref_version, and ClosedOpenRange.lo. + + # StandardVersion / GitVersion (at equal ref version) + assert_ver_lt("=1.2", "git.ref=1.2") + assert_ver_gt("git.ref=1.2", "=1.2") + + # StandardVersion / GitVersion (at different ref versions) + assert_ver_lt("git.ref=1.2", "=1.3") + assert_ver_gt("=1.3", "git.ref=1.2") + assert_ver_lt("=1.2", "git.ref=1.3") + assert_ver_gt("git.ref=1.3", "=1.2") + + # GitVersion / ClosedOpenRange (at equal ref/lo version) + assert_ver_lt("git.ref=1.2", "1.2") + assert_ver_gt("1.2", "git.ref=1.2") + + # GitVersion / ClosedOpenRange (at different ref/lo version) + assert_ver_lt("git.ref=1.2", "1.3") + assert_ver_gt("1.3", "git.ref=1.2") + assert_ver_lt("1.2", "git.ref=1.3") + assert_ver_gt("git.ref=1.3", "1.2") + + # StandardVersion / ClosedOpenRange (at equal lo version) + assert_ver_lt("=1.2", "1.2") + assert_ver_gt("1.2", "=1.2") + + # StandardVersion / ClosedOpenRange (at different lo version) + assert_ver_lt("=1.2", "1.3") + assert_ver_gt("1.3", "=1.2") + assert_ver_lt("1.2", "=1.3") + assert_ver_gt("=1.3", "1.2") + + +def test_git_version_accessors(): + """Test whether iteration, indexing, slicing, dotted, dashed, and underscored works for + GitVersion.""" + v = GitVersion("my_branch=1.2-3") + assert [x for x in v] == [1, 2, 3] + assert v[0] == 1 + assert v[1] == 2 + assert v[2] == 3 + assert v[0:2] == Version("1.2") + assert v[0:10] == Version("1.2.3") + assert str(v.dotted) == "1.2.3" + assert str(v.dashed) == "1-2-3" + assert str(v.underscored) == "1_2_3" + assert v.up_to(1) == Version("1") + assert v.up_to(2) == Version("1.2") + assert len(v) == 3 + assert not v.isdevelop() + assert GitVersion("my_branch=develop").isdevelop() + + +def test_boolness_of_versions(): + # We do implement __len__, but at the end of the day versions are used as elements in + # the first place, not as lists of version components. So VersionList(...).concrete + # should be truthy even when there are no version components. + assert bool(Version("1.2")) + assert bool(Version("1.2").up_to(0)) + + # bool(GitVersion) shouldn't trigger a ref lookup. + assert bool(GitVersion("a" * 40)) + + +def test_version_list_normalization(): + # Git versions and ordinary versions can live together in a VersionList + assert len(VersionList(["=1.2", "ref=1.2"])) == 2 + + # But when a range is added, the only disjoint bit is the range. + assert VersionList(["=1.2", "ref=1.2", "ref=1.3", "1.2:1.3"]) == VersionList(["1.2:1.3"]) + + # Also test normalization when using ver. + assert ver("=1.0,ref=1.0,1.0:2.0") == ver(["1.0:2.0"]) + assert ver("=1.0,1.0:2.0,ref=1.0") == ver(["1.0:2.0"]) + assert ver("1.0:2.0,=1.0,ref=1.0") == ver(["1.0:2.0"]) + + +@pytest.mark.parametrize("version", ["=1.2", "git.ref=1.2", "1.2"]) +def test_version_comparison_with_list_fails(version): + vlist = VersionList(["=1.3"]) + + with pytest.raises(TypeError): + version < vlist + + with pytest.raises(TypeError): + vlist < version + + with pytest.raises(TypeError): + version <= vlist + + with pytest.raises(TypeError): + vlist <= version + + with pytest.raises(TypeError): + version >= vlist + + with pytest.raises(TypeError): + vlist >= version + + with pytest.raises(TypeError): + version > vlist + + with pytest.raises(TypeError): + vlist > version + + +def test_inclusion_upperbound(): + is_specific = spack.spec.Spec("x@=1.2") + is_range = spack.spec.Spec("x@1.2") + upperbound = spack.spec.Spec("x@:1.2.0") + + # The exact version is included in the range + assert is_specific.satisfies(upperbound) + + # But the range 1.2:1.2 is not, since it includes for example 1.2.1 + assert not is_range.satisfies(upperbound) + + # They do intersect of course. + assert is_specific.intersects(upperbound) and is_range.intersects(upperbound) + + +@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") +def test_git_version_repo_attached_after_serialization( + mock_git_version_info, mock_packages, monkeypatch +): + """Test that a GitVersion instance can be serialized and deserialized + without losing its repository reference. + """ + repo_path, _, commits = mock_git_version_info + monkeypatch.setattr( + spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False + ) + spec = spack.spec.Spec(f"git-test-commit@{commits[-2]}").concretized() + + # Before serialization, the repo is attached + assert spec.satisfies("@1.0") + + # After serialization, the repo is still attached + assert spack.spec.Spec.from_dict(spec.to_dict()).satisfies("@1.0") + + +@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") +def test_resolved_git_version_is_shown_in_str(mock_git_version_info, mock_packages, monkeypatch): + """Test that a GitVersion from a commit without a user supplied version is printed + as <hash>=<version>, and not just <hash>.""" + repo_path, _, commits = mock_git_version_info + monkeypatch.setattr( + spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False + ) + commit = commits[-3] + spec = spack.spec.Spec(f"git-test-commit@{commit}").concretized() + + assert spec.version.satisfies(ver("1.0")) + assert str(spec.version) == f"{commit}=1.0-git.1" + + +def test_unresolvable_git_versions_error(mock_packages): + """Test that VersionLookupError is raised when a git prop is not set on a package.""" + with pytest.raises(VersionLookupError): + # The package exists, but does not have a git property set. When dereferencing + # the version, we should get VersionLookupError, not a generic AttributeError. + spack.spec.Spec(f"git-test-commit@{'a' * 40}").version.ref_version diff --git a/lib/spack/spack/test/web.py b/lib/spack/spack/test/web.py index 7f6c12a5e2..adebe23b97 100644 --- a/lib/spack/spack/test/web.py +++ b/lib/spack/spack/test/web.py @@ -16,7 +16,7 @@ import spack.paths import spack.util.s3 import spack.util.url as url_util import spack.util.web -from spack.version import ver +from spack.version import Version def _create_url(relative_url): @@ -102,47 +102,47 @@ def test_spider_no_response(monkeypatch): @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_versions_of_archive_0(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=0) - assert ver("0.0.0") in versions + assert Version("0.0.0") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_versions_of_archive_1(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=1) - assert ver("0.0.0") in versions - assert ver("1.0.0") in versions + assert Version("0.0.0") in versions + assert Version("1.0.0") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_versions_of_archive_2(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=2) - assert ver("0.0.0") in versions - assert ver("1.0.0") in versions - assert ver("2.0.0") in versions + assert Version("0.0.0") in versions + assert Version("1.0.0") in versions + assert Version("2.0.0") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_exotic_versions_of_archive_2(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=2) # up for grabs to make this better. - assert ver("2.0.0b2") in versions + assert Version("2.0.0b2") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_versions_of_archive_3(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=3) - assert ver("0.0.0") in versions - assert ver("1.0.0") in versions - assert ver("2.0.0") in versions - assert ver("3.0") in versions - assert ver("4.5") in versions + assert Version("0.0.0") in versions + assert Version("1.0.0") in versions + assert Version("2.0.0") in versions + assert Version("3.0") in versions + assert Version("4.5") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") def test_find_exotic_versions_of_archive_3(): versions = spack.util.web.find_versions_of_archive(root_tarball, root, list_depth=3) - assert ver("2.0.0b2") in versions - assert ver("3.0a1") in versions - assert ver("4.5-rc5") in versions + assert Version("2.0.0b2") in versions + assert Version("3.0a1") in versions + assert Version("4.5-rc5") in versions @pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)") @@ -150,7 +150,7 @@ def test_find_versions_of_archive_with_fragment(): versions = spack.util.web.find_versions_of_archive( root_tarball, root_with_fragment, list_depth=0 ) - assert ver("5.0.0") in versions + assert Version("5.0.0") in versions def test_get_header(): diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py index d744e223c2..b8452ab61d 100644 --- a/lib/spack/spack/version.py +++ b/lib/spack/spack/version.py @@ -6,31 +6,20 @@ """ This module implements Version and version-ish objects. These are: -Version - A single version of a package. -VersionRange - A range of versions of a package. -VersionList - A list of Versions and VersionRanges. - -All of these types support the following operations, which can -be called on any of the types:: - - __eq__, __ne__, __lt__, __gt__, __ge__, __le__, __hash__ - __contains__ - satisfies - overlaps - union - intersection - concrete +StandardVersion: A single version of a package. +ClosedOpenRange: A range of versions of a package. +VersionList: A ordered list of Version and VersionRange elements. + +The set of Version and ClosedOpenRange is totally ordered wiht < +defined as Version(x) < VersionRange(Version(y), Version(x)) +if Version(x) <= Version(y). """ import numbers import os import re from bisect import bisect_left -from functools import wraps +from typing import Dict, List, Optional, Tuple, Union -import llnl.util.tty as tty from llnl.util.filesystem import mkdirp, working_dir import spack.caches @@ -38,10 +27,9 @@ import spack.error import spack.paths import spack.util.executable import spack.util.spack_json as sjson +import spack.util.url from spack.util.spack_yaml import syaml_dict -__all__ = ["Version", "VersionRange", "VersionList", "ver"] - # Valid version characters VALID_VERSION = re.compile(r"^[A-Za-z0-9_.-][=A-Za-z0-9_.-]*$") @@ -59,199 +47,240 @@ SEMVER_REGEX = re.compile( ) # Infinity-like versions. The order in the list implies the comparison rules -infinity_versions = ["develop", "main", "master", "head", "trunk", "stable"] +infinity_versions = ["stable", "trunk", "head", "master", "main", "develop"] iv_min_len = min(len(s) for s in infinity_versions) -def coerce_versions(a, b): - """ - Convert both a and b to the 'greatest' type between them, in this order: - VersionBase < GitVersion < VersionRange < VersionList - This is used to simplify comparison operations below so that we're always - comparing things that are of the same type. - """ - order = (VersionBase, GitVersion, VersionRange, VersionList) - ta, tb = type(a), type(b) - - def check_type(t): - if t not in order: - raise TypeError("coerce_versions cannot be called on %s" % t) - - check_type(ta) - check_type(tb) - - if ta == tb: - return (a, b) - elif order.index(ta) > order.index(tb): - if ta == GitVersion: - return (a, GitVersion(b)) - elif ta == VersionRange: - return (a, VersionRange(b, b)) - else: - return (a, VersionList([b])) - else: - if tb == GitVersion: - return (GitVersion(a), b) - elif tb == VersionRange: - return (VersionRange(a, a), b) - else: - return (VersionList([a]), b) - - -def coerced(method): - """Decorator that ensures that argument types of a method are coerced.""" - - @wraps(method) - def coercing_method(a, b, *args, **kwargs): - if type(a) == type(b) or a is None or b is None: - return method(a, b, *args, **kwargs) - else: - ca, cb = coerce_versions(a, b) - return getattr(ca, method.__name__)(cb, *args, **kwargs) - - return coercing_method - - class VersionStrComponent(object): - # NOTE: this is intentionally not a UserString, the abc instanceof - # check is slow enough to eliminate all gains - __slots__ = ["inf_ver", "data"] + __slots__ = ["data"] + + def __init__(self, data): + # int for infinity index, str for literal. + self.data: Union[int, str] = data - def __init__(self, string): - self.inf_ver = None - self.data = string + @staticmethod + def from_string(string): if len(string) >= iv_min_len: try: - self.inf_ver = infinity_versions.index(string) + string = infinity_versions.index(string) except ValueError: pass + return VersionStrComponent(string) + def __hash__(self): return hash(self.data) def __str__(self): - return self.data + return ( + ("infinity" if self.data >= len(infinity_versions) else infinity_versions[self.data]) + if isinstance(self.data, int) + else self.data + ) - def __repr__(self): - return f"VersionStrComponent('{self.data}')" + def __repr__(self) -> str: + return f'VersionStrComponent("{self}")' def __eq__(self, other): - if isinstance(other, VersionStrComponent): - return self.data == other.data - return self.data == other + return isinstance(other, VersionStrComponent) and self.data == other.data def __lt__(self, other): - if isinstance(other, VersionStrComponent): - if self.inf_ver is not None: - if other.inf_ver is not None: - return self.inf_ver > other.inf_ver - return False - if other.inf_ver is not None: - return True - - return self.data < other.data + lhs_inf = isinstance(self.data, int) + if isinstance(other, int): + return not lhs_inf + rhs_inf = isinstance(other.data, int) + return (not lhs_inf and rhs_inf) if lhs_inf ^ rhs_inf else self.data < other.data - if self.inf_ver is not None: - return False + def __le__(self, other): + return self < other or self == other - # Numbers are always "newer" than letters. - # This is for consistency with RPM. See patch - # #60884 (and details) from bugzilla #50977 in - # the RPM project at rpm.org. Or look at - # rpmvercmp.c if you want to see how this is - # implemented there. + def __gt__(self, other): + lhs_inf = isinstance(self.data, int) if isinstance(other, int): - return True + return lhs_inf + rhs_inf = isinstance(other.data, int) + return (lhs_inf and not rhs_inf) if lhs_inf ^ rhs_inf else self.data > other.data + + def __ge__(self, other): + return self > other or self == other - if isinstance(other, str): - return self < VersionStrComponent(other) - # If we get here, it's an unsupported comparison - raise ValueError("VersionStrComponent can only be compared with itself, " "int and str") +def parse_string_components(string: str) -> Tuple[tuple, tuple]: + string = string.strip() - def __gt__(self, other): - return not self.__lt__(other) + if string and not VALID_VERSION.match(string): + raise ValueError("Bad characters in version string: %s" % string) + segments = SEGMENT_REGEX.findall(string) + version = tuple(int(m[0]) if m[0] else VersionStrComponent.from_string(m[1]) for m in segments) + separators = tuple(m[2] for m in segments) + return version, separators -def is_git_version(string): - if string.startswith("git."): - return True - elif len(string) == 40 and COMMIT_VERSION.match(string): + +class ConcreteVersion: + pass + + +class StandardVersion(ConcreteVersion): + """Class to represent versions""" + + __slots__ = ["version", "string", "separators"] + + def __init__(self, string: Optional[str], version: tuple, separators: tuple): + self.string = string + self.version = version + self.separators = separators + + @staticmethod + def from_string(string: str): + return StandardVersion(string, *parse_string_components(string)) + + @staticmethod + def typemin(): + return StandardVersion("", (), ()) + + @staticmethod + def typemax(): + return StandardVersion("infinity", (VersionStrComponent(len(infinity_versions)),), ()) + + def __bool__(self): return True - elif "=" in string: + + def __eq__(self, other): + if isinstance(other, StandardVersion): + return self.version == other.version + return False + + def __ne__(self, other): + if isinstance(other, StandardVersion): + return self.version != other.version return True - return False + def __lt__(self, other): + if isinstance(other, StandardVersion): + return self.version < other.version + if isinstance(other, ClosedOpenRange): + # Use <= here so that Version(x) < ClosedOpenRange(Version(x), ...). + return self <= other.lo + return NotImplemented -def Version(string): # capitalized for backwards compatibility - if not isinstance(string, str): - string = str(string) # to handle VersionBase and GitVersion types + def __le__(self, other): + if isinstance(other, StandardVersion): + return self.version <= other.version + if isinstance(other, ClosedOpenRange): + # Versions are never equal to ranges, so follow < logic. + return self <= other.lo + return NotImplemented - if is_git_version(string): - return GitVersion(string) - return VersionBase(string) + def __ge__(self, other): + if isinstance(other, StandardVersion): + return self.version >= other.version + if isinstance(other, ClosedOpenRange): + # Versions are never equal to ranges, so follow > logic. + return self > other.lo + return NotImplemented + def __gt__(self, other): + if isinstance(other, StandardVersion): + return self.version > other.version + if isinstance(other, ClosedOpenRange): + return self > other.lo + return NotImplemented -class VersionBase(object): - """Class to represent versions + def __iter__(self): + return iter(self.version) - Versions are compared by converting to a tuple and comparing - lexicographically. + def __len__(self): + return len(self.version) + + def __getitem__(self, idx): + cls = type(self) - The most common Versions are alpha-numeric, and are parsed from strings - such as ``2.3.0`` or ``1.2-5``. These Versions are represented by - the tuples ``(2, 3, 0)`` and ``(1, 2, 5)`` respectively. + if isinstance(idx, numbers.Integral): + return self.version[idx] - Versions are split on ``.``, ``-``, and - ``_`` characters, as well as any point at which they switch from - numeric to alphabetical or vice-versa. For example, the version - ``0.1.3a`` is represented by the tuple ``(0, 1, 3, 'a') and the - version ``a-5b`` is represented by the tuple ``('a', 5, 'b')``. + elif isinstance(idx, slice): + string_arg = [] + + pairs = zip(self.version[idx], self.separators[idx]) + for token, sep in pairs: + string_arg.append(str(token)) + string_arg.append(str(sep)) - Spack versions may also be arbitrary non-numeric strings or git - commit SHAs. The following are the full rules for comparing - versions. + if string_arg: + string_arg.pop() # We don't need the last separator + string_arg = "".join(string_arg) + return cls.from_string(string_arg) + else: + return StandardVersion.from_string("") - 1. If the version represents a git reference (i.e. commit, tag, branch), see GitVersions. + message = "{cls.__name__} indices must be integers" + raise TypeError(message.format(cls=cls)) - 2. The version is split into fields based on the delimiters ``.``, - ``-``, and ``_``, as well as alphabetic-numeric boundaries. + def __str__(self): + return ( + self.string + if isinstance(self.string, str) + else ".".join((str(c) for c in self.version)) + ) - 3. The following develop-like strings are greater (newer) than all - numbers and are ordered as ``develop > main > master > head > - trunk``. + def __repr__(self) -> str: + # Print indirect repr through Version(...) + return f'Version("{str(self)}")' - 4. All other non-numeric versions are less than numeric versions, - and are sorted alphabetically. + def __hash__(self): + return hash(self.version) - These rules can be summarized as follows: + def __contains__(rhs, lhs): + # We should probably get rid of `x in y` for versions, since + # versions still have a dual interpretation as singleton sets + # or elements. x in y should be: is the lhs-element in the + # rhs-set. Instead this function also does subset checks. + if isinstance(lhs, (StandardVersion, ClosedOpenRange, VersionList)): + return lhs.satisfies(rhs) + raise ValueError(lhs) + + def intersects(self, other: Union["StandardVersion", "GitVersion", "ClosedOpenRange"]) -> bool: + if isinstance(other, StandardVersion): + return self == other + return other.intersects(self) + + def overlaps(self, other) -> bool: + return self.intersects(other) - ``develop > main > master > head > trunk > 999 > 0 > 'zzz' > 'a' > - ''`` + def satisfies( + self, other: Union["ClosedOpenRange", "StandardVersion", "GitVersion", "VersionList"] + ) -> bool: + if isinstance(other, GitVersion): + return False - """ + if isinstance(other, StandardVersion): + return self == other - __slots__ = ["version", "separators", "string"] + if isinstance(other, ClosedOpenRange): + return other.intersects(self) - def __init__(self, string: str) -> None: - if not isinstance(string, str): - string = str(string) + if isinstance(other, VersionList): + return other.intersects(self) - # preserve the original string, but trimmed. - string = string.strip() - self.string = string + return NotImplemented - if string and not VALID_VERSION.match(string): - raise ValueError("Bad characters in version string: %s" % string) + def union(self, other: Union["ClosedOpenRange", "StandardVersion"]): + if isinstance(other, StandardVersion): + return self if self == other else VersionList([self, other]) + return other.union(self) - self.separators, self.version = self._generate_separators_and_components(string) + def intersection(self, other: Union["ClosedOpenRange", "StandardVersion"]): + if isinstance(other, StandardVersion): + return self if self == other else VersionList() + return other.intersection(self) - def _generate_separators_and_components(self, string): - segments = SEGMENT_REGEX.findall(string) - components = tuple(int(m[0]) if m[0] else VersionStrComponent(m[1]) for m in segments) - separators = tuple(m[2] for m in segments) - return separators, components + def isdevelop(self): + """Triggers on the special case of the `@develop-like` version.""" + return any( + isinstance(p, VersionStrComponent) and isinstance(p.data, int) for p in self.version + ) @property def dotted(self): @@ -265,7 +294,7 @@ class VersionBase(object): Returns: Version: The version with separator characters replaced by dots """ - return type(self)(self.string.replace("-", ".").replace("_", ".")) + return type(self).from_string(self.string.replace("-", ".").replace("_", ".")) @property def underscored(self): @@ -280,7 +309,7 @@ class VersionBase(object): Version: The version with separator characters replaced by underscores """ - return type(self)(self.string.replace(".", "_").replace("-", "_")) + return type(self).from_string(self.string.replace(".", "_").replace("-", "_")) @property def dashed(self): @@ -294,7 +323,7 @@ class VersionBase(object): Returns: Version: The version with separator characters replaced by dashes """ - return type(self)(self.string.replace(".", "-").replace("_", "-")) + return type(self).from_string(self.string.replace(".", "-").replace("_", "-")) @property def joined(self): @@ -308,7 +337,9 @@ class VersionBase(object): Returns: Version: The version with separator characters removed """ - return type(self)(self.string.replace(".", "").replace("-", "").replace("_", "")) + return type(self).from_string( + self.string.replace(".", "").replace("-", "").replace("_", "") + ) def up_to(self, index): """The version up to the specified component. @@ -335,178 +366,8 @@ class VersionBase(object): """ return self[:index] - def lowest(self): - return self - def highest(self): - return self - - def isdevelop(self): - """Triggers on the special case of the `@develop-like` version.""" - for inf in infinity_versions: - for v in self.version: - if v == inf: - return True - - return False - - @coerced - def intersects(self, other: "VersionBase") -> bool: - """Return True if self intersects with other, False otherwise. - - Two versions intersect if one can be constrained by the other. For instance - @4.7 and @4.7.3 intersect (the intersection being @4.7.3). - - Arg: - other: version to be checked for intersection - """ - n = min(len(self.version), len(other.version)) - return self.version[:n] == other.version[:n] - - @coerced - def satisfies(self, other: "VersionBase") -> bool: - """Return True if self is at least as specific and share a common prefix with other. - - For instance, @4.7.3 satisfies @4.7 but not vice-versa. - - Arg: - other: version to be checked for intersection - """ - nself = len(self.version) - nother = len(other.version) - return nother <= nself and self.version[:nother] == other.version - - def __iter__(self): - return iter(self.version) - - def __len__(self): - return len(self.version) - - def __getitem__(self, idx): - cls = type(self) - - if isinstance(idx, numbers.Integral): - return self.version[idx] - - elif isinstance(idx, slice): - string_arg = [] - - pairs = zip(self.version[idx], self.separators[idx]) - for token, sep in pairs: - string_arg.append(str(token)) - string_arg.append(str(sep)) - - if string_arg: - string_arg.pop() # We don't need the last separator - string_arg = "".join(string_arg) - return cls(string_arg) - else: - return VersionBase("") - - message = "{cls.__name__} indices must be integers" - raise TypeError(message.format(cls=cls)) - - def __repr__(self): - return "VersionBase(" + repr(self.string) + ")" - - def __str__(self): - return self.string - - def __format__(self, format_spec): - return str(self).format(format_spec) - - @property - def concrete(self): - return self - - @coerced - def __lt__(self, other): - """Version comparison is designed for consistency with the way RPM - does things. If you need more complicated versions in installed - packages, you should override your package's version string to - express it more sensibly. - """ - if other is None: - return False - - # Use tuple comparison assisted by VersionStrComponent for performance - return self.version < other.version - - @coerced - def __eq__(self, other): - # Cut out early if we don't have a version - if other is None or type(other) != VersionBase: - return False - - return self.version == other.version - - @coerced - def __ne__(self, other): - return not (self == other) - - @coerced - def __le__(self, other): - return self == other or self < other - - @coerced - def __ge__(self, other): - return not (self < other) - - @coerced - def __gt__(self, other): - return not (self == other) and not (self < other) - - def __hash__(self): - return hash(self.version) - - @coerced - def __contains__(self, other): - if other is None: - return False - - return other.version[: len(self.version)] == self.version - - @coerced - def is_predecessor(self, other): - """True if the other version is the immediate predecessor of this one. - That is, NO non-git versions v exist such that: - (self < v < other and v not in self). - """ - if self.version[:-1] != other.version[:-1]: - return False - - sl = self.version[-1] - ol = other.version[-1] - # TODO: extend this to consecutive letters, z/0, and infinity versions - return type(sl) == int and type(ol) == int and (ol - sl == 1) - - @coerced - def is_successor(self, other): - return other.is_predecessor(self) - - def overlaps(self, other): - return self.intersects(other) - - @coerced - def union(self, other): - if self == other or other in self: - return self - elif self in other: - return other - else: - return VersionList([self, other]) - - @coerced - def intersection(self, other): - if self in other: # also covers `self == other` - return self - elif other in self: - return other - else: - return VersionList() - - -class GitVersion(VersionBase): +class GitVersion(ConcreteVersion): """Class to represent versions interpreted from git refs. There are two distinct categories of git versions: @@ -514,208 +375,196 @@ class GitVersion(VersionBase): 1) GitVersions instantiated with an associated reference version (e.g. 'git.foo=1.2') 2) GitVersions requiring commit lookups - Git ref versions that are not paired with a known version - are handled separately from all other version comparisons. - When Spack identifies a git ref version, it associates a - ``CommitLookup`` object with the version. This object - handles caching of information from the git repo. When executing - comparisons with a git ref version, Spack queries the - ``CommitLookup`` for the most recent version previous to this - git ref, as well as the distance between them expressed as a number - of commits. If the previous version is ``X.Y.Z`` and the distance - is ``D``, the git commit version is represented by the tuple ``(X, - Y, Z, '', D)``. The component ``''`` cannot be parsed as part of - any valid version, but is a valid component. This allows a git - ref version to be less than (older than) every Version newer - than its previous version, but still newer than its previous + Git ref versions that are not paired with a known version are handled separately from + all other version comparisons. When Spack identifies a git ref version, it associates a + ``CommitLookup`` object with the version. This object handles caching of information + from the git repo. When executing comparisons with a git ref version, Spack queries the + ``CommitLookup`` for the most recent version previous to this git ref, as well as the + distance between them expressed as a number of commits. If the previous version is + ``X.Y.Z`` and the distance is ``D``, the git commit version is represented by the + tuple ``(X, Y, Z, '', D)``. The component ``''`` cannot be parsed as part of any valid + version, but is a valid component. This allows a git ref version to be less than (older + than) every Version newer than its previous version, but still newer than its previous version. - To find the previous version from a git ref version, Spack - queries the git repo for its tags. Any tag that matches a version - known to Spack is associated with that version, as is any tag that - is a known version prepended with the character ``v`` (i.e., a tag - ``v1.0`` is associated with the known version - ``1.0``). Additionally, any tag that represents a semver version - (X.Y.Z with X, Y, Z all integers) is associated with the version - it represents, even if that version is not known to Spack. Each - tag is then queried in git to see whether it is an ancestor of the - git ref in question, and if so the distance between the two. The - previous version is the version that is an ancestor with the least - distance from the git ref in question. - - This procedure can be circumvented if the user supplies a known version - to associate with the GitVersion (e.g. ``[hash]=develop``). If the user - prescribes the version then there is no need to do a lookup - and the standard version comparison operations are sufficient. - - Non-git versions may be coerced to GitVersion for comparison, but no Spec will ever - have a GitVersion that is not actually referencing a version from git.""" - - def __init__(self, string): - if not isinstance(string, str): - string = str(string) # In case we got a VersionBase or GitVersion object - - # An object that can lookup git refs to compare them to versions - self.user_supplied_reference = False - self._ref_lookup = None - self.ref_version = None + To find the previous version from a git ref version, Spack queries the git repo for its + tags. Any tag that matches a version known to Spack is associated with that version, as + is any tag that is a known version prepended with the character ``v`` (i.e., a tag + ``v1.0`` is associated with the known version ``1.0``). Additionally, any tag that + represents a semver version (X.Y.Z with X, Y, Z all integers) is associated with the + version it represents, even if that version is not known to Spack. Each tag is then + queried in git to see whether it is an ancestor of the git ref in question, and if so + the distance between the two. The previous version is the version that is an ancestor + with the least distance from the git ref in question. + + This procedure can be circumvented if the user supplies a known version to associate + with the GitVersion (e.g. ``[hash]=develop``). If the user prescribes the version then + there is no need to do a lookup and the standard version comparison operations are + sufficient. + """ - git_prefix = string.startswith("git.") - pruned_string = string[4:] if git_prefix else string + __slots__ = ["ref", "has_git_prefix", "is_commit", "_ref_lookup", "_ref_version"] - if "=" in pruned_string: - self.ref, self.ref_version_str = pruned_string.split("=") - _, self.ref_version = self._generate_separators_and_components(self.ref_version_str) - self.user_supplied_reference = True - else: - self.ref = pruned_string - - self.is_commit = bool(len(self.ref) == 40 and COMMIT_VERSION.match(self.ref)) - self.is_ref = git_prefix # is_ref False only for comparing to VersionBase - self.is_ref |= bool(self.is_commit) + def __init__(self, string: str): + # An object that can lookup git refs to compare them to versions + self._ref_lookup: Optional[CommitLookup] = None - # ensure git.<hash> and <hash> are treated the same by dropping 'git.' - # unless we are assigning a version with = - canonical_string = self.ref if (self.is_commit and not self.ref_version) else string - super(GitVersion, self).__init__(canonical_string) + # This is the effective version. + self._ref_version: Optional[StandardVersion] - def _cmp(self, other_lookups=None): - # No need to rely on git comparisons for develop-like refs - if len(self.version) == 2 and self.isdevelop(): - return self.version + self.has_git_prefix = string.startswith("git.") - # If we've already looked this version up, return cached value - if self.ref_version is not None: - return self.ref_version + # Drop `git.` prefix + normalized_string = string[4:] if self.has_git_prefix else string - ref_lookup = self.ref_lookup or other_lookups + if "=" in normalized_string: + # Store the git reference, and parse the user provided version. + self.ref, spack_version = normalized_string.split("=") + self._ref_version = StandardVersion( + spack_version, *parse_string_components(spack_version) + ) + else: + # The ref_version is lazily attached after parsing, since we don't know what + # package it applies to here. + self._ref_version = None + self.ref = normalized_string - if self.is_ref and ref_lookup: - ref_info = ref_lookup.get(self.ref) - if ref_info: - prev_version, distance = ref_info + # Used by fetcher + self.is_commit: bool = len(self.ref) == 40 and bool(COMMIT_VERSION.match(self.ref)) - if prev_version is None: - prev_version = "0" + @property + def ref_version(self) -> StandardVersion: + # Return cached version if we have it + if self._ref_version is not None: + return self._ref_version + + if self.ref_lookup is None: + raise VersionLookupError( + f"git ref '{self.ref}' cannot be looked up: " + "call attach_git_lookup_from_package first" + ) - # Extend previous version by empty component and distance - # If commit is exactly a known version, no distance suffix - prev_tuple = VersionBase(prev_version).version if prev_version else () - dist_suffix = (VersionStrComponent(""), distance) if distance else () - self.ref_version = prev_tuple + dist_suffix - return self.ref_version + version_string, distance = self.ref_lookup.get(self.ref) + version_string = version_string or "0" - return self.version + # Add a -git.<distance> suffix when we're not exactly on a tag + if distance > 0: + version_string += f"-git.{distance}" + self._ref_version = StandardVersion( + version_string, *parse_string_components(version_string) + ) + return self._ref_version - @coerced def intersects(self, other): - # If they are both references, they must match exactly - if self.is_ref and other.is_ref: - return self.version == other.version - - # Otherwise the ref_version of the reference must intersect with the version of the other - v1 = self.ref_version if self.is_ref else self.version - v2 = other.ref_version if other.is_ref else other.version - n = min(len(v1), len(v2)) - return v1[:n] == v2[:n] - - @coerced - def satisfies(self, other): - # In the case of two GitVersions we require the ref_versions - # to satisfy one another and the versions to be an exact match. - - self_cmp = self._cmp(other.ref_lookup) - other_cmp = other._cmp(self.ref_lookup) - - if self.is_ref and other.is_ref: - if self.ref != other.ref: - return False - elif self.user_supplied_reference and other.user_supplied_reference: - return self.ref_version == other.ref_version - elif other.user_supplied_reference: - return False - else: - # In this case, 'other' does not supply a version equivalence - # with "=" and the commit strings are equal. 'self' may specify - # a version equivalence, but that is extra info and will - # satisfy no matter what it is. - return True - elif other.is_ref: - # if other is a ref then satisfaction requires an exact version match - # i.e. the GitRef must match this.version for satisfaction - # this creates an asymmetric comparison: - # - 'foo@main'.satisfies('foo@git.hash=main') == False - # - 'foo@git.hash=main'.satisfies('foo@main') == True - version_match = self.version == other.version - elif self.is_ref: - # other is not a ref then it is a version base and we need to compare - # this.ref - version_match = self.ref_version == other.version - else: - # neither is a git ref. We shouldn't ever be here, but if we are this variable - # is not meaningful and defaults to true - version_match = True + # For concrete things intersects = satisfies = equality + if isinstance(other, GitVersion): + return self == other + if isinstance(other, StandardVersion): + return False + if isinstance(other, ClosedOpenRange): + return self.ref_version.intersects(other) + if isinstance(other, VersionList): + return any(self.intersects(rhs) for rhs in other) + raise ValueError(f"Unexpected type {type(other)}") - # Do the final comparison - nself = len(self_cmp) - nother = len(other_cmp) - return nother <= nself and self_cmp[:nother] == other_cmp and version_match + def intersection(self, other): + if isinstance(other, ConcreteVersion): + return self if self == other else VersionList() + return other.intersection(self) - def __repr__(self): - return "GitVersion(" + repr(self.string) + ")" + def overlaps(self, other) -> bool: + return self.intersects(other) - @coerced - def __lt__(self, other): - """Version comparison is designed for consistency with the way RPM - does things. If you need more complicated versions in installed - packages, you should override your package's version string to - express it more sensibly. - """ - if other is None: + def satisfies( + self, other: Union["GitVersion", StandardVersion, "ClosedOpenRange", "VersionList"] + ): + # Concrete versions mean we have to do an equality check + if isinstance(other, GitVersion): + return self == other + if isinstance(other, StandardVersion): return False + if isinstance(other, ClosedOpenRange): + return self.ref_version.satisfies(other) + if isinstance(other, VersionList): + return any(self.satisfies(rhs) for rhs in other) + raise ValueError(f"Unexpected type {type(other)}") - # If we haven't indexed yet, can't compare - # If we called this, we know at least one is a git ref - if not (self.ref_lookup or other.ref_lookup): - return False + def __str__(self): + s = f"git.{self.ref}" if self.has_git_prefix else self.ref + # Note: the solver actually depends on str(...) to produce the effective version. + # So when a lookup is attached, we require the resolved version to be printed. + # But for standalone git versions that don't have a repo attached, it would still + # be nice if we could print @<hash>. + try: + s += f"={self.ref_version}" + except VersionLookupError: + pass + return s - # Use tuple comparison assisted by VersionStrComponent for performance - return self._cmp(other.ref_lookup) < other._cmp(self.ref_lookup) + def __repr__(self): + return f'GitVersion("{self}")' + + def __bool__(self): + return True - @coerced def __eq__(self, other): - # Cut out early if we don't have a git version - if other is None or type(other) != GitVersion: - return False + # GitVersion cannot be equal to StandardVersion, otherwise == is not transitive + return ( + isinstance(other, GitVersion) + and self.ref == other.ref + and self.ref_version == other.ref_version + ) - return self._cmp(other.ref_lookup) == other._cmp(self.ref_lookup) + def __ne__(self, other): + return not self == other - def __hash__(self): - return hash(str(self)) + def __lt__(self, other): + if isinstance(other, GitVersion): + return (self.ref_version, self.ref) < (other.ref_version, other.ref) + if isinstance(other, StandardVersion): + # GitVersion at equal ref version is larger than StandardVersion + return self.ref_version < other + if isinstance(other, ClosedOpenRange): + return self.ref_version < other + raise ValueError(f"Unexpected type {type(other)}") - @coerced - def __contains__(self, other): - if other is None: - return False + def __le__(self, other): + if isinstance(other, GitVersion): + return (self.ref_version, self.ref) <= (other.ref_version, other.ref) + if isinstance(other, StandardVersion): + # Note: GitVersion hash=1.2.3 > StandardVersion 1.2.3, so use < comparsion. + return self.ref_version < other + if isinstance(other, ClosedOpenRange): + # Equality is not a thing + return self.ref_version < other + raise ValueError(f"Unexpected type {type(other)}") - self_cmp = self._cmp(other.ref_lookup) - return other._cmp(self.ref_lookup)[: len(self_cmp)] == self_cmp + def __ge__(self, other): + if isinstance(other, GitVersion): + return (self.ref_version, self.ref) >= (other.ref_version, other.ref) + if isinstance(other, StandardVersion): + # Note: GitVersion hash=1.2.3 > StandardVersion 1.2.3, so use >= here. + return self.ref_version >= other + if isinstance(other, ClosedOpenRange): + return self.ref_version > other + raise ValueError(f"Unexpected type {type(other)}") - @coerced - def is_predecessor(self, other): - """True if the other version is the immediate predecessor of this one. - That is, NO non-commit versions v exist such that: - (self < v < other and v not in self). - """ - self_cmp = self._cmp(self.ref_lookup) - other_cmp = other._cmp(other.ref_lookup) + def __gt__(self, other): + if isinstance(other, GitVersion): + return (self.ref_version, self.ref) > (other.ref_version, other.ref) + if isinstance(other, StandardVersion): + # Note: GitVersion hash=1.2.3 > StandardVersion 1.2.3, so use >= here. + return self.ref_version >= other + if isinstance(other, ClosedOpenRange): + return self.ref_version > other + raise ValueError(f"Unexpected type {type(other)}") - if self_cmp[:-1] != other_cmp[:-1]: - return False + def __hash__(self): + # hashing should not cause version lookup + return hash(self.ref) - sl = self_cmp[-1] - ol = other_cmp[-1] - return type(sl) == int and type(ol) == int and (ol - sl == 1) + def __contains__(self, other): + raise Exception("Not implemented yet") @property def ref_lookup(self): @@ -724,7 +573,7 @@ class GitVersion(VersionBase): self._ref_lookup.get(self.ref) return self._ref_lookup - def generate_git_lookup(self, pkg_name): + def attach_git_lookup_from_package(self, pkg_name): """ Use the git fetcher to look up a version for a commit. @@ -735,248 +584,177 @@ class GitVersion(VersionBase): context of the version, we cannot continue. This implementation is alongside the GitFetcher because eventually the git repos cache will be one and the same with the source cache. - - Args: - fetcher: the fetcher to use. - versions: the known versions of the package """ + self._ref_lookup = self._ref_lookup or CommitLookup(pkg_name) - # Sanity check we have a commit - if not self.is_ref: - tty.die("%s is not a git version." % self) + def __iter__(self): + return self.ref_version.__iter__() - # don't need a lookup if we already have a version assigned - if self.ref_version: - return + def __len__(self): + return self.ref_version.__len__() - # Generate a commit looker-upper - self._ref_lookup = CommitLookup(pkg_name) + def __getitem__(self, idx): + return self.ref_version.__getitem__(idx) + def isdevelop(self): + return self.ref_version.isdevelop() -class VersionRange(object): - def __init__(self, start, end): - if isinstance(start, str): - start = Version(start) - if isinstance(end, str): - end = Version(end) + @property + def dotted(self) -> StandardVersion: + return self.ref_version.dotted - self.start = start - self.end = end + @property + def underscored(self) -> StandardVersion: + return self.ref_version.underscored - # Unbounded ranges are not empty - if not start or not end: - return + @property + def dashed(self) -> StandardVersion: + return self.ref_version.dashed - # Do not allow empty ranges. We have to be careful about lexicographical - # ordering of versions here: 1.2 < 1.2.3 lexicographically, but 1.2.3:1.2 - # means the range [1.2.3, 1.3), which is non-empty. - min_len = min(len(start), len(end)) - if end.up_to(min_len) < start.up_to(min_len): - raise ValueError(f"Invalid Version range: {self}") + @property + def joined(self) -> StandardVersion: + return self.ref_version.joined - def lowest(self): - return self.start + def up_to(self, index) -> StandardVersion: + return self.ref_version.up_to(index) - def highest(self): - return self.end - @coerced - def __lt__(self, other): - """Sort VersionRanges lexicographically so that they are ordered first - by start and then by end. None denotes an open range, so None in - the start position is less than everything except None, and None in - the end position is greater than everything but None. - """ - if other is None: - return False +class ClosedOpenRange: + def __init__(self, lo: StandardVersion, hi: StandardVersion): + if hi < lo: + raise ValueError(f"{lo}:{hi} is an empty range") + self.lo: StandardVersion = lo + self.hi: StandardVersion = hi + + @classmethod + def from_version_range(cls, lo: StandardVersion, hi: StandardVersion): + """Construct ClosedOpenRange from lo:hi range.""" + return ClosedOpenRange(lo, next_version(hi)) + + def __str__(self): + # This simplifies 3.1:<3.2 to 3.1:3.1 to 3.1 + # 3:3 -> 3 + hi_prev = prev_version(self.hi) + if self.lo != StandardVersion.typemin() and self.lo == hi_prev: + return str(self.lo) + lhs = "" if self.lo == StandardVersion.typemin() else str(self.lo) + rhs = "" if hi_prev == StandardVersion.typemax() else str(hi_prev) + return f"{lhs}:{rhs}" + + def __repr__(self): + return str(self) - s, o = self, other - if s.start != o.start: - return s.start is None or (o.start is not None and s.start < o.start) - return s.end != o.end and o.end is None or (s.end is not None and s.end < o.end) + def __hash__(self): + # prev_version for backward compat. + return hash((self.lo, prev_version(self.hi))) - @coerced def __eq__(self, other): - return ( - other is not None - and type(other) == VersionRange - and self.start == other.start - and self.end == other.end - ) + if isinstance(other, StandardVersion): + return False + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) == (other.lo, other.hi) + return NotImplemented - @coerced def __ne__(self, other): - return not (self == other) + if isinstance(other, StandardVersion): + return True + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) != (other.lo, other.hi) + return NotImplemented + + def __lt__(self, other): + if isinstance(other, StandardVersion): + return other > self + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) < (other.lo, other.hi) + return NotImplemented - @coerced def __le__(self, other): - return self == other or self < other + if isinstance(other, StandardVersion): + return other >= self + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) <= (other.lo, other.hi) + return NotImplemented - @coerced def __ge__(self, other): - return not (self < other) + if isinstance(other, StandardVersion): + return other <= self + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) >= (other.lo, other.hi) + return NotImplemented - @coerced def __gt__(self, other): - return not (self == other) and not (self < other) - - @property - def concrete(self): - return self.start if self.start == self.end else None - - @coerced - def __contains__(self, other): - if other is None: + if isinstance(other, StandardVersion): + return other < self + if isinstance(other, ClosedOpenRange): + return (self.lo, self.hi) > (other.lo, other.hi) + return NotImplemented + + def __contains__(rhs, lhs): + if isinstance(lhs, (ConcreteVersion, ClosedOpenRange, VersionList)): + return lhs.satisfies(rhs) + raise ValueError(f"Unexpected type {type(lhs)}") + + def intersects(self, other: Union[ConcreteVersion, "ClosedOpenRange", "VersionList"]): + if isinstance(other, StandardVersion): + return self.lo <= other < self.hi + if isinstance(other, GitVersion): + return self.lo <= other.ref_version < self.hi + if isinstance(other, ClosedOpenRange): + return (self.lo < other.hi) and (other.lo < self.hi) + if isinstance(other, VersionList): + return any(self.intersects(rhs) for rhs in other) + raise ValueError(f"Unexpected type {type(other)}") + + def satisfies(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]): + if isinstance(other, ConcreteVersion): return False + if isinstance(other, ClosedOpenRange): + return not (self.lo < other.lo or other.hi < self.hi) + if isinstance(other, VersionList): + return any(self.satisfies(rhs) for rhs in other) + raise ValueError(other) - in_lower = ( - self.start == other.start - or self.start is None - or ( - other.start is not None and (self.start < other.start or other.start in self.start) - ) - ) - if not in_lower: - return False - - in_upper = ( - self.end == other.end - or self.end is None - or (other.end is not None and (self.end > other.end or other.end in self.end)) - ) - return in_upper - - def intersects(self, other) -> bool: - """Return two if two version ranges overlap with each other, False otherwise. - - This is a commutative operation. - - Examples: - - 1:3 satisfies 2:4, as their intersection is 2:3. - - 1:2 does not satisfy 3:4, as their intersection is empty. - - 4.5:4.7 satisfies 4.7.2:4.8, as their intersection is 4.7.2:4.7 - - Args: - other: version range to be checked for intersection - """ - return self.overlaps(other) + def overlaps(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]) -> bool: + return self.intersects(other) - @coerced - def satisfies(self, other): - """A version range satisfies another if it is a subset of the other. + def union(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]): + if isinstance(other, StandardVersion): + return self if self.lo <= other < self.hi else VersionList([self, other]) - Examples: - - 1:2 does not satisfy 3:4, as their intersection is empty. - - 1:3 does not satisfy 2:4, as they overlap but neither is a subset of the other - - 1:3 satisfies 1:4. - """ - return self.intersection(other) == self + if isinstance(other, GitVersion): + return self if self.lo <= other.ref_version < self.hi else VersionList([self, other]) - @coerced - def overlaps(self, other): - return ( - self.start is None - or other.end is None - or self.start <= other.end - or other.end in self.start - or self.start in other.end - ) and ( - other.start is None - or self.end is None - or other.start <= self.end - or other.start in self.end - or self.end in other.start - ) - - @coerced - def union(self, other): - if not self.overlaps(other): - if ( - self.end is not None - and other.start is not None - and self.end.is_predecessor(other.start) - ): - return VersionRange(self.start, other.end) - - if ( - other.end is not None - and self.start is not None - and other.end.is_predecessor(self.start) - ): - return VersionRange(other.start, self.end) + if isinstance(other, ClosedOpenRange): + # Notice <= cause we want union(1:2, 3:4) = 1:4. + if self.lo <= other.hi and other.lo <= self.hi: + return ClosedOpenRange(min(self.lo, other.lo), max(self.hi, other.hi)) return VersionList([self, other]) - # if we're here, then we know the ranges overlap. - if self.start is None or other.start is None: - start = None - else: - start = self.start - # TODO: See note in intersection() about < and in discrepancy. - if self.start in other.start or other.start < self.start: - start = other.start + if isinstance(other, VersionList): + v = other.copy() + v.add(self) + return v - if self.end is None or other.end is None: - end = None - else: - end = self.end - # TODO: See note in intersection() about < and in discrepancy. - if other.end not in self.end: - if end in other.end or other.end > self.end: - end = other.end + raise ValueError(f"Unexpected type {type(other)}") - return VersionRange(start, end) + def intersection(self, other: Union["ClosedOpenRange", ConcreteVersion]): + # range - version -> singleton or nothing. + if isinstance(other, ConcreteVersion): + return other if self.intersects(other) else VersionList() - @coerced - def intersection(self, other): - if not self.overlaps(other): - return VersionList() + # range - range -> range or nothing. + max_lo = max(self.lo, other.lo) + min_hi = min(self.hi, other.hi) + return ClosedOpenRange(max_lo, min_hi) if max_lo < min_hi else VersionList() - if self.start is None: - start = other.start - else: - start = self.start - if other.start is not None: - if other.start > start or other.start in start: - start = other.start - if self.end is None: - end = other.end - else: - end = self.end - # TODO: does this make sense? - # This is tricky: - # 1.6.5 in 1.6 = True (1.6.5 is more specific) - # 1.6 < 1.6.5 = True (lexicographic) - # Should 1.6 NOT be less than 1.6.5? Hmm. - # Here we test (not end in other.end) first to avoid paradox. - if other.end is not None and end not in other.end: - if other.end < end or other.end in end: - end = other.end - - return VersionRange(start, end) - - def __hash__(self): - return hash((self.start, self.end)) - - def __repr__(self): - return self.__str__() - - def __str__(self): - out = "" - if self.start: - out += str(self.start) - out += ":" - if self.end: - out += str(self.end) - return out - - -class VersionList(object): - """Sorted, non-redundant list of Versions and VersionRanges.""" +class VersionList: + """Sorted, non-redundant list of Version and ClosedOpenRange elements.""" def __init__(self, vlist=None): - self.versions = [] + self.versions: List[StandardVersion, GitVersion, ClosedOpenRange] = [] if vlist is not None: if isinstance(vlist, str): vlist = from_string(vlist) @@ -988,95 +766,112 @@ class VersionList(object): for v in vlist: self.add(ver(v)) - def add(self, version): - if type(version) in (VersionBase, GitVersion, VersionRange): - # This normalizes single-value version ranges. - if version.concrete: - version = version.concrete + def add(self, item): + if isinstance(item, ConcreteVersion): + i = bisect_left(self, item) + # Only insert when prev and next are not intersected. + if (i == 0 or not item.intersects(self[i - 1])) and ( + i == len(self) or not item.intersects(self[i]) + ): + self.versions.insert(i, item) - i = bisect_left(self, version) + elif isinstance(item, ClosedOpenRange): + i = bisect_left(self, item) - while i - 1 >= 0 and version.overlaps(self[i - 1]): - version = version.union(self[i - 1]) + # Note: can span multiple concrete versions to the left, + # For instance insert 1.2: into [1.2, hash=1.2, 1.3] + # would bisect to i = 1. + while i > 0 and item.intersects(self[i - 1]): + item = item.union(self[i - 1]) del self.versions[i - 1] i -= 1 - while i < len(self) and version.overlaps(self[i]): - version = version.union(self[i]) + while i < len(self) and item.intersects(self[i]): + item = item.union(self[i]) del self.versions[i] - self.versions.insert(i, version) + self.versions.insert(i, item) - elif type(version) == VersionList: - for v in version: + elif type(item) == VersionList: + for v in item: self.add(v) else: - raise TypeError("Can't add %s to VersionList" % type(version)) + raise TypeError("Can't add %s to VersionList" % type(item)) @property - def concrete(self): - if len(self) == 1: - return self[0].concrete - else: + def concrete(self) -> Optional[ConcreteVersion]: + return self[0] if len(self) == 1 and isinstance(self[0], ConcreteVersion) else None + + @property + def concrete_range_as_version(self) -> Optional[ConcreteVersion]: + """Like concrete, but collapses VersionRange(x, x) to Version(x). + This is just for compatibility with old Spack.""" + if len(self) != 1: return None + v = self[0] + if isinstance(v, ConcreteVersion): + return v + if isinstance(v, ClosedOpenRange) and next_version(v.lo) == v.hi: + return v.lo + return None def copy(self): return VersionList(self) - def lowest(self): + def lowest(self) -> Optional[StandardVersion]: """Get the lowest version in the list.""" - if not self: - return None - else: - return self[0].lowest() + return None if not self else self[0] - def highest(self): + def highest(self) -> Optional[StandardVersion]: """Get the highest version in the list.""" - if not self: - return None - else: - return self[-1].highest() + return None if not self else self[-1] - def highest_numeric(self): + def highest_numeric(self) -> Optional[StandardVersion]: """Get the highest numeric version in the list.""" numeric_versions = list(filter(lambda v: str(v) not in infinity_versions, self.versions)) - if not any(numeric_versions): - return None - else: - return numeric_versions[-1].highest() + return None if not any(numeric_versions) else numeric_versions[-1] - def preferred(self): + def preferred(self) -> Optional[StandardVersion]: """Get the preferred (latest) version in the list.""" - latest = self.highest_numeric() - if latest is None: - latest = self.highest() - return latest - - @coerced - def overlaps(self, other): - if not other or not self: - return False + return self.highest_numeric() or self.highest() - s = o = 0 - while s < len(self) and o < len(other): - if self[s].overlaps(other[o]): - return True - elif self[s] < other[o]: - s += 1 - else: - o += 1 - return False + def satisfies(self, other) -> bool: + # This exploits the fact that version lists are "reduced" and normalized, so we can + # never have a list like [1:3, 2:4] since that would be normalized to [1:4] + if isinstance(other, VersionList): + return all(any(lhs.satisfies(rhs) for rhs in other) for lhs in self) + + if isinstance(other, (ConcreteVersion, ClosedOpenRange)): + return all(lhs.satisfies(other) for lhs in self) + + raise ValueError(f"Unsupported type {type(other)}") def intersects(self, other): - return self.overlaps(other) + if isinstance(other, VersionList): + s = o = 0 + while s < len(self) and o < len(other): + if self[s].intersects(other[o]): + return True + elif self[s] < other[o]: + s += 1 + else: + o += 1 + return False + + if isinstance(other, (ClosedOpenRange, StandardVersion)): + return any(v.intersects(other) for v in self) + + raise ValueError(f"Unsupported type {type(other)}") + + def overlaps(self, other) -> bool: + return self.intersects(other) def to_dict(self): """Generate human-readable dict for YAML.""" if self.concrete: return syaml_dict([("version", str(self[0]))]) - else: - return syaml_dict([("versions", [str(v) for v in self])]) + return syaml_dict([("versions", [str(v) for v in self])]) @staticmethod def from_dict(dictionary): @@ -1084,38 +879,30 @@ class VersionList(object): if "versions" in dictionary: return VersionList(dictionary["versions"]) elif "version" in dictionary: - return VersionList([dictionary["version"]]) - else: - raise ValueError("Dict must have 'version' or 'versions' in it.") + return VersionList([Version(dictionary["version"])]) + raise ValueError("Dict must have 'version' or 'versions' in it.") - @coerced - def satisfies(self, other) -> bool: - # This exploits the fact that version lists are "reduced" and normalized, so we can - # never have a list like [1:3, 2:4] since that would be normalized to [1:4] - return all(any(lhs.satisfies(rhs) for rhs in other) for lhs in self) - - @coerced - def update(self, other): + def update(self, other: "VersionList"): for v in other.versions: self.add(v) - @coerced - def union(self, other): + def union(self, other: "VersionList"): result = self.copy() result.update(other) return result - @coerced - def intersection(self, other): - # TODO: make this faster. This is O(n^2). + def intersection(self, other: "VersionList") -> "VersionList": result = VersionList() - for s in self: - for o in other: - result.add(s.intersection(o)) + for lhs, rhs in ((self, other), (other, self)): + for x in lhs: + i = bisect_left(rhs.versions, x) + if i > 0: + result.add(rhs[i - 1].intersection(x)) + if i < len(rhs): + result.add(rhs[i].intersection(x)) return result - @coerced - def intersect(self, other): + def intersect(self, other) -> bool: """Intersect this spec's list with other. Return True if the spec changed as a result; False otherwise @@ -1125,20 +912,15 @@ class VersionList(object): self.versions = isection.versions return changed - @coerced def __contains__(self, other): - if len(self) == 0: - return False - - for version in other: + if isinstance(other, (ClosedOpenRange, StandardVersion)): i = bisect_left(self, other) - if i == 0: - if version not in self[0]: - return False - elif all(version not in v for v in self[i - 1 :]): - return False + return (i > 0 and other in self[i - 1]) or (i < len(self) and other in self[i]) - return True + if isinstance(other, VersionList): + return all(item in self for item in other) + + return False def __getitem__(self, index): return self.versions[index] @@ -1155,60 +937,196 @@ class VersionList(object): def __bool__(self): return bool(self.versions) - @coerced def __eq__(self, other): - return other is not None and self.versions == other.versions + if isinstance(other, VersionList): + return self.versions == other.versions + return False - @coerced def __ne__(self, other): - return not (self == other) + if isinstance(other, VersionList): + return self.versions != other.versions + return False - @coerced def __lt__(self, other): - return other is not None and self.versions < other.versions + if isinstance(other, VersionList): + return self.versions < other.versions + return NotImplemented - @coerced def __le__(self, other): - return self == other or self < other + if isinstance(other, VersionList): + return self.versions <= other.versions + return NotImplemented - @coerced def __ge__(self, other): - return not (self < other) + if isinstance(other, VersionList): + return self.versions >= other.versions + return NotImplemented - @coerced def __gt__(self, other): - return not (self == other) and not (self < other) + if isinstance(other, VersionList): + return self.versions > other.versions + return NotImplemented def __hash__(self): return hash(tuple(self.versions)) def __str__(self): - return ",".join(str(v) for v in self.versions) + return ",".join( + f"={v}" if isinstance(v, StandardVersion) else str(v) for v in self.versions + ) def __repr__(self): return str(self.versions) -def from_string(string): - """Converts a string to a Version, VersionList, or VersionRange. - This is private. Client code should use ver(). +def next_str(s: str) -> str: + """Produce the next string of A-Z and a-z characters""" + return ( + (s + "A") + if (len(s) == 0 or s[-1] == "z") + else s[:-1] + ("a" if s[-1] == "Z" else chr(ord(s[-1]) + 1)) + ) + + +def prev_str(s: str) -> str: + """Produce the previous string of A-Z and a-z characters""" + return ( + s[:-1] + if (len(s) == 0 or s[-1] == "A") + else s[:-1] + ("Z" if s[-1] == "a" else chr(ord(s[-1]) - 1)) + ) + + +def next_version_str_component(v: VersionStrComponent) -> VersionStrComponent: + """ + Produce the next VersionStrComponent, where + masteq -> mastes + master -> main + """ + # First deal with the infinity case. + data = v.data + if isinstance(data, int): + return VersionStrComponent(data + 1) + + # Find the next non-infinity string. + while True: + data = next_str(data) + if data not in infinity_versions: + break + + return VersionStrComponent(data) + + +def prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent: """ + Produce the previous VersionStrComponent, where + mastes -> masteq + master -> head + """ + # First deal with the infinity case. Allow underflows + data = v.data + if isinstance(data, int): + return VersionStrComponent(data - 1) + + # Find the next string. + while True: + data = prev_str(data) + if data not in infinity_versions: + break + + return VersionStrComponent(data) + + +def next_version(v: StandardVersion) -> StandardVersion: + if len(v.version) == 0: + nxt = VersionStrComponent("A") + elif isinstance(v.version[-1], VersionStrComponent): + nxt = next_version_str_component(v.version[-1]) + else: + nxt = v.version[-1] + 1 + + # Construct a string-version for printing + string_components = [] + for part, sep in zip(v.version[:-1], v.separators): + string_components.append(str(part)) + string_components.append(str(sep)) + string_components.append(str(nxt)) + + return StandardVersion("".join(string_components), v.version[:-1] + (nxt,), v.separators) + + +def prev_version(v: StandardVersion) -> StandardVersion: + if len(v.version) == 0: + return v + elif isinstance(v.version[-1], VersionStrComponent): + prev = prev_version_str_component(v.version[-1]) + else: + prev = v.version[-1] - 1 + + # Construct a string-version for printing + string_components = [] + for part, sep in zip(v.version[:-1], v.separators): + string_components.append(str(part)) + string_components.append(str(sep)) + string_components.append(str(prev)) + + return StandardVersion("".join(string_components), v.version[:-1] + (prev,), v.separators) + + +def is_git_version(string: str) -> bool: + return ( + string.startswith("git.") + or len(string) == 40 + and bool(COMMIT_VERSION.match(string)) + or "=" in string[1:] + ) + + +def Version(string: Union[str, int]) -> Union[GitVersion, StandardVersion]: + if not isinstance(string, (str, int)): + raise ValueError(f"Cannot construct a version from {type(string)}") + string = str(string) + if is_git_version(string): + return GitVersion(string) + return StandardVersion.from_string(str(string)) + + +def VersionRange(lo: Union[str, StandardVersion], hi: Union[str, StandardVersion]): + lo = lo if isinstance(lo, StandardVersion) else StandardVersion.from_string(lo) + hi = hi if isinstance(hi, StandardVersion) else StandardVersion.from_string(hi) + return ClosedOpenRange.from_version_range(lo, hi) + + +def from_string(string) -> Union[VersionList, ClosedOpenRange, StandardVersion, GitVersion]: + """Converts a string to a version object. This is private. Client code should use ver().""" string = string.replace(" ", "") + # VersionList if "," in string: - return VersionList(string.split(",")) + return VersionList(list(map(from_string, string.split(",")))) + # ClosedOpenRange elif ":" in string: s, e = string.split(":") - start = Version(s) if s else None - end = Version(e) if e else None - return VersionRange(start, end) + lo = StandardVersion.typemin() if s == "" else StandardVersion.from_string(s) + hi = StandardVersion.typemax() if e == "" else StandardVersion.from_string(e) + return VersionRange(lo, hi) + + # StandardVersion + elif string.startswith("="): + # @=1.2.3 is an exact version + return Version(string[1:]) + + elif is_git_version(string): + return GitVersion(string) else: - return Version(string) + # @1.2.3 is short for 1.2.3:1.2.3 + v = StandardVersion.from_string(string) + return VersionRange(v, v) -def ver(obj): +def ver(obj) -> Union[VersionList, ClosedOpenRange, StandardVersion, GitVersion]: """Parses a Version, VersionRange, or VersionList from a string or list of strings. """ @@ -1218,12 +1136,16 @@ def ver(obj): return from_string(obj) elif isinstance(obj, (int, float)): return from_string(str(obj)) - elif type(obj) in (VersionBase, GitVersion, VersionRange, VersionList): + elif isinstance(obj, (StandardVersion, GitVersion, ClosedOpenRange, VersionList)): return obj else: raise TypeError("ver() can't convert %s to version!" % type(obj)) +#: This version contains all possible versions. +any_version: VersionList = VersionList([":"]) + + class VersionError(spack.error.SpackError): """This is raised when something is wrong with a version.""" @@ -1239,16 +1161,15 @@ class VersionLookupError(VersionError): class CommitLookup(object): """An object for cached lookups of git commits - CommitLookup objects delegate to the misc_cache for locking. - CommitLookup objects may be attached to a GitVersion object for which - Version.is_ref returns True to allow for comparisons between git refs - and versions as represented by tags in the git repository. + CommitLookup objects delegate to the misc_cache for locking. CommitLookup objects may + be attached to a GitVersion to allow for comparisons between git refs and versions as + represented by tags in the git repository. """ def __init__(self, pkg_name): self.pkg_name = pkg_name - self.data = {} + self.data: Dict[str, Tuple[Optional[str], int]] = {} self._pkg = None self._fetcher = None @@ -1281,7 +1202,14 @@ class CommitLookup(object): @property def pkg(self): if not self._pkg: - self._pkg = spack.repo.path.get_pkg_class(self.pkg_name) + import spack.repo # break cycle + + try: + pkg = spack.repo.path.get_pkg_class(self.pkg_name) + pkg.git + except (spack.repo.RepoError, AttributeError) as e: + raise VersionLookupError(f"Couldn't get the git repo for {self.pkg_name}") from e + self._pkg = pkg return self._pkg @property @@ -1297,10 +1225,7 @@ class CommitLookup(object): @property def repository_uri(self): - """ - Identifier for git repos used within the repo and metadata caches. - - """ + """Identifier for git repos used within the repo and metadata caches.""" try: components = [ str(c).lstrip("/") for c in spack.util.url.parse_git_url(self.pkg.git) if c @@ -1311,21 +1236,17 @@ class CommitLookup(object): return os.path.abspath(self.pkg.git) def save(self): - """ - Save the data to file - """ + """Save the data to file""" with spack.caches.misc_cache.write_transaction(self.cache_key) as (old, new): sjson.dump(self.data, new) def load_data(self): - """ - Load data if the path already exists. - """ + """Load data if the path already exists.""" if os.path.isfile(self.cache_path): with spack.caches.misc_cache.read_transaction(self.cache_key) as cache_file: self.data = sjson.load(cache_file) - def get(self, ref): + def get(self, ref) -> Tuple[Optional[str], int]: if not self.data: self.load_data() @@ -1335,7 +1256,7 @@ class CommitLookup(object): return self.data[ref] - def lookup_ref(self, ref): + def lookup_ref(self, ref) -> Tuple[Optional[str], int]: """Lookup the previous version and distance for a given commit. We use git to compare the known versions from package to the git tags, @@ -1413,10 +1334,9 @@ class CommitLookup(object): ).strip() ancestor_commits.append((tag_commit, int(distance))) - # Get nearest ancestor that is a known version - ancestor_commits.sort(key=lambda x: x[1]) if ancestor_commits: - prev_version_commit, distance = ancestor_commits[0] + # Get nearest ancestor that is a known version + prev_version_commit, distance = min(ancestor_commits, key=lambda x: x[1]) prev_version = commit_to_version[prev_version_commit] else: # Get list of all commits, this is in reverse order |