Feature: use git branches/tags as versions (#31200)
Building on #24639, this allows versions to be prefixed by `git.`. If a version begins `git.`, it is treated as a git ref, and handled as git commits are starting in the referenced PR. An exception is made for versions that are `git.develop`, `git.main`, `git.master`, `git.head`, or `git.trunk`. Those are assumed to be greater than all other versions, as those prefixed strings are in other contexts.
This commit is contained in:
parent
7dd2ca0207
commit
df44045fdb
9 changed files with 306 additions and 122 deletions
|
@ -16,7 +16,7 @@
|
||||||
import spack.util.crypto
|
import spack.util.crypto
|
||||||
from spack.package_base import preferred_version
|
from spack.package_base import preferred_version
|
||||||
from spack.util.naming import valid_fully_qualified_module_name
|
from spack.util.naming import valid_fully_qualified_module_name
|
||||||
from spack.version import Version, ver
|
from spack.version import VersionBase, ver
|
||||||
|
|
||||||
description = "checksum available versions of a package"
|
description = "checksum available versions of a package"
|
||||||
section = "packaging"
|
section = "packaging"
|
||||||
|
@ -65,7 +65,7 @@ def checksum(parser, args):
|
||||||
remote_versions = None
|
remote_versions = None
|
||||||
for version in versions:
|
for version in versions:
|
||||||
version = ver(version)
|
version = ver(version)
|
||||||
if not isinstance(version, Version):
|
if not isinstance(version, VersionBase):
|
||||||
tty.die("Cannot generate checksums for version lists or "
|
tty.die("Cannot generate checksums for version lists or "
|
||||||
"version ranges. Use unambiguous versions.")
|
"version ranges. Use unambiguous versions.")
|
||||||
url = pkg.find_valid_url_for_version(version)
|
url = pkg.find_valid_url_for_version(version)
|
||||||
|
|
|
@ -46,7 +46,7 @@ class OpenMpi(Package):
|
||||||
from spack.dependency import Dependency, canonical_deptype, default_deptype
|
from spack.dependency import Dependency, canonical_deptype, default_deptype
|
||||||
from spack.fetch_strategy import from_kwargs
|
from spack.fetch_strategy import from_kwargs
|
||||||
from spack.resource import Resource
|
from spack.resource import Resource
|
||||||
from spack.version import Version, VersionChecksumError
|
from spack.version import GitVersion, Version, VersionChecksumError, VersionLookupError
|
||||||
|
|
||||||
__all__ = ['DirectiveError', 'DirectiveMeta', 'version', 'conflicts', 'depends_on',
|
__all__ = ['DirectiveError', 'DirectiveMeta', 'version', 'conflicts', 'depends_on',
|
||||||
'extends', 'provides', 'patch', 'variant', 'resource']
|
'extends', 'provides', 'patch', 'variant', 'resource']
|
||||||
|
@ -330,7 +330,17 @@ def _execute_version(pkg):
|
||||||
kwargs['checksum'] = checksum
|
kwargs['checksum'] = checksum
|
||||||
|
|
||||||
# Store kwargs for the package to later with a fetch_strategy.
|
# Store kwargs for the package to later with a fetch_strategy.
|
||||||
pkg.versions[Version(ver)] = kwargs
|
version = Version(ver)
|
||||||
|
if isinstance(version, GitVersion):
|
||||||
|
if not hasattr(pkg, 'git') and 'git' not in kwargs:
|
||||||
|
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
|
return _execute_version
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1575,16 +1575,30 @@ def for_package_version(pkg, version):
|
||||||
|
|
||||||
check_pkg_attributes(pkg)
|
check_pkg_attributes(pkg)
|
||||||
|
|
||||||
if not isinstance(version, spack.version.Version):
|
if not isinstance(version, spack.version.VersionBase):
|
||||||
version = spack.version.Version(version)
|
version = spack.version.Version(version)
|
||||||
|
|
||||||
# if it's a commit, we must use a GitFetchStrategy
|
# if it's a commit, we must use a GitFetchStrategy
|
||||||
if version.is_commit and hasattr(pkg, "git"):
|
if isinstance(version, spack.version.GitVersion):
|
||||||
|
if not hasattr(pkg, "git"):
|
||||||
|
raise FetchError(
|
||||||
|
"Cannot fetch git version for %s. Package has no 'git' attribute" %
|
||||||
|
pkg.name
|
||||||
|
)
|
||||||
# Populate the version with comparisons to other commits
|
# Populate the version with comparisons to other commits
|
||||||
version.generate_commit_lookup(pkg.name)
|
version.generate_git_lookup(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
|
||||||
|
# handled slightly more conservatively for older versions of git.
|
||||||
|
# We call all non-commit refs tags in this context, at the cost of a slight
|
||||||
|
# performance hit for branches on older versions of git.
|
||||||
|
# Branches cannot be cached, so we tell the fetcher not to cache tags/branches
|
||||||
|
ref_type = 'commit' if version.is_commit else 'tag'
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'git': pkg.git,
|
'git': pkg.git,
|
||||||
'commit': str(version)
|
ref_type: version.ref,
|
||||||
|
'no_cache': True,
|
||||||
}
|
}
|
||||||
kwargs['submodules'] = getattr(pkg, 'submodules', False)
|
kwargs['submodules'] = getattr(pkg, 'submodules', False)
|
||||||
fetcher = GitFetchStrategy(**kwargs)
|
fetcher = GitFetchStrategy(**kwargs)
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
from spack.util.executable import ProcessError, which
|
from spack.util.executable import ProcessError, which
|
||||||
from spack.util.package_hash import package_hash
|
from spack.util.package_hash import package_hash
|
||||||
from spack.util.prefix import Prefix
|
from spack.util.prefix import Prefix
|
||||||
from spack.version import Version
|
from spack.version import GitVersion, Version, VersionBase
|
||||||
|
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
FLAG_HANDLER_RETURN_TYPE = Tuple[
|
FLAG_HANDLER_RETURN_TYPE = Tuple[
|
||||||
|
@ -1041,7 +1041,7 @@ def all_urls_for_version(self, version, custom_url_for_version=None):
|
||||||
return self._implement_all_urls_for_version(version, uf)
|
return self._implement_all_urls_for_version(version, uf)
|
||||||
|
|
||||||
def _implement_all_urls_for_version(self, version, custom_url_for_version=None):
|
def _implement_all_urls_for_version(self, version, custom_url_for_version=None):
|
||||||
if not isinstance(version, Version):
|
if not isinstance(version, VersionBase):
|
||||||
version = Version(version)
|
version = Version(version)
|
||||||
|
|
||||||
urls = []
|
urls = []
|
||||||
|
@ -1505,7 +1505,7 @@ def do_fetch(self, mirror_only=False):
|
||||||
checksum = spack.config.get('config:checksum')
|
checksum = spack.config.get('config:checksum')
|
||||||
fetch = self.stage.managed_by_spack
|
fetch = self.stage.managed_by_spack
|
||||||
if checksum and fetch and (self.version not in self.versions) \
|
if checksum and fetch and (self.version not in self.versions) \
|
||||||
and (not self.version.is_commit):
|
and (not isinstance(self.version, GitVersion)):
|
||||||
tty.warn("There is no checksum on file to fetch %s safely." %
|
tty.warn("There is no checksum on file to fetch %s safely." %
|
||||||
self.spec.cformat('{name}{@version}'))
|
self.spec.cformat('{name}{@version}'))
|
||||||
|
|
||||||
|
|
|
@ -1429,11 +1429,16 @@ def key_fn(item):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
known_versions = self.possible_versions[dep.name]
|
known_versions = self.possible_versions[dep.name]
|
||||||
if (not dep.version.is_commit and
|
if (not isinstance(dep.version, spack.version.GitVersion) and
|
||||||
any(v.satisfies(dep.version) for v in known_versions)):
|
any(v.satisfies(dep.version) for v in known_versions)):
|
||||||
# some version we know about satisfies this constraint, so we
|
# 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
|
# should use that one. e.g, if the user asks for qt@5 and we
|
||||||
# know about qt@5.5.
|
# 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
|
continue
|
||||||
|
|
||||||
# if there is a concrete version on the CLI *that we know nothing
|
# if there is a concrete version on the CLI *that we know nothing
|
||||||
|
@ -1678,7 +1683,7 @@ def define_virtual_constraints(self):
|
||||||
|
|
||||||
# extract all the real versions mentioned in version ranges
|
# extract all the real versions mentioned in version ranges
|
||||||
def versions_for(v):
|
def versions_for(v):
|
||||||
if isinstance(v, spack.version.Version):
|
if isinstance(v, spack.version.VersionBase):
|
||||||
return [v]
|
return [v]
|
||||||
elif isinstance(v, spack.version.VersionRange):
|
elif isinstance(v, spack.version.VersionRange):
|
||||||
result = [v.start] if v.start else []
|
result = [v.start] if v.start else []
|
||||||
|
@ -2187,8 +2192,8 @@ def build_specs(self, function_tuples):
|
||||||
# concretization process)
|
# concretization process)
|
||||||
for root in self._specs.values():
|
for root in self._specs.values():
|
||||||
for spec in root.traverse():
|
for spec in root.traverse():
|
||||||
if spec.version.is_commit:
|
if isinstance(spec.version, spack.version.GitVersion):
|
||||||
spec.version.generate_commit_lookup(spec.fullname)
|
spec.version.generate_git_lookup(spec.fullname)
|
||||||
|
|
||||||
return self._specs
|
return self._specs
|
||||||
|
|
||||||
|
|
|
@ -5154,9 +5154,9 @@ def do_parse(self):
|
||||||
# Note: VersionRange(x, x) is currently concrete, hence isinstance(...).
|
# Note: VersionRange(x, x) is currently concrete, hence isinstance(...).
|
||||||
if (
|
if (
|
||||||
spec.name and spec.versions.concrete and
|
spec.name and spec.versions.concrete and
|
||||||
isinstance(spec.version, vn.Version) and spec.version.is_commit
|
isinstance(spec.version, vn.GitVersion)
|
||||||
):
|
):
|
||||||
spec.version.generate_commit_lookup(spec.fullname)
|
spec.version.generate_git_lookup(spec.fullname)
|
||||||
|
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,14 @@
|
||||||
import spack.package_base
|
import spack.package_base
|
||||||
import spack.spec
|
import spack.spec
|
||||||
from spack.util.executable import which
|
from spack.util.executable import which
|
||||||
from spack.version import Version, VersionList, VersionRange, ver
|
from spack.version import (
|
||||||
|
GitVersion,
|
||||||
|
Version,
|
||||||
|
VersionBase,
|
||||||
|
VersionList,
|
||||||
|
VersionRange,
|
||||||
|
ver,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_ver_lt(a, b):
|
def assert_ver_lt(a, b):
|
||||||
|
@ -520,7 +527,7 @@ def test_repr_and_str():
|
||||||
|
|
||||||
def check_repr_and_str(vrs):
|
def check_repr_and_str(vrs):
|
||||||
a = Version(vrs)
|
a = Version(vrs)
|
||||||
assert repr(a) == "Version('" + vrs + "')"
|
assert repr(a) == "VersionBase('" + vrs + "')"
|
||||||
b = eval(repr(a))
|
b = eval(repr(a))
|
||||||
assert a == b
|
assert a == b
|
||||||
assert str(a) == vrs
|
assert str(a) == vrs
|
||||||
|
@ -544,19 +551,19 @@ def test_get_item():
|
||||||
assert isinstance(a[1], int)
|
assert isinstance(a[1], int)
|
||||||
# Test slicing
|
# Test slicing
|
||||||
b = a[0:2]
|
b = a[0:2]
|
||||||
assert isinstance(b, Version)
|
assert isinstance(b, VersionBase)
|
||||||
assert b == Version('0.1')
|
assert b == Version('0.1')
|
||||||
assert repr(b) == "Version('0.1')"
|
assert repr(b) == "VersionBase('0.1')"
|
||||||
assert str(b) == '0.1'
|
assert str(b) == '0.1'
|
||||||
b = a[0:3]
|
b = a[0:3]
|
||||||
assert isinstance(b, Version)
|
assert isinstance(b, VersionBase)
|
||||||
assert b == Version('0.1_2')
|
assert b == Version('0.1_2')
|
||||||
assert repr(b) == "Version('0.1_2')"
|
assert repr(b) == "VersionBase('0.1_2')"
|
||||||
assert str(b) == '0.1_2'
|
assert str(b) == '0.1_2'
|
||||||
b = a[1:]
|
b = a[1:]
|
||||||
assert isinstance(b, Version)
|
assert isinstance(b, VersionBase)
|
||||||
assert b == Version('1_2-3')
|
assert b == Version('1_2-3')
|
||||||
assert repr(b) == "Version('1_2-3')"
|
assert repr(b) == "VersionBase('1_2-3')"
|
||||||
assert str(b) == '1_2-3'
|
assert str(b) == '1_2-3'
|
||||||
# Raise TypeError on tuples
|
# Raise TypeError on tuples
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
@ -597,7 +604,7 @@ def test_versions_from_git(mock_git_version_info, monkeypatch, mock_packages):
|
||||||
spec = spack.spec.Spec('git-test-commit@%s' % commit)
|
spec = spack.spec.Spec('git-test-commit@%s' % commit)
|
||||||
version = spec.version
|
version = spec.version
|
||||||
comparator = [str(v) if not isinstance(v, int) else v
|
comparator = [str(v) if not isinstance(v, int) else v
|
||||||
for v in version._cmp(version.commit_lookup)]
|
for v in version._cmp(version.ref_lookup)]
|
||||||
|
|
||||||
with working_dir(repo_path):
|
with working_dir(repo_path):
|
||||||
which('git')('checkout', commit)
|
which('git')('checkout', commit)
|
||||||
|
@ -637,6 +644,43 @@ def test_git_hash_comparisons(
|
||||||
assert spec4.satisfies('@1.0:1.2')
|
assert spec4.satisfies('@1.0:1.2')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform == 'win32',
|
||||||
|
reason="Not supported on Windows (yet)")
|
||||||
|
def test_git_ref_comparisons(
|
||||||
|
mock_git_version_info, install_mockery, mock_packages, monkeypatch):
|
||||||
|
"""Check that hashes compare properly to versions
|
||||||
|
"""
|
||||||
|
repo_path, filename, commits = mock_git_version_info
|
||||||
|
monkeypatch.setattr(spack.package_base.PackageBase,
|
||||||
|
'git', 'file://%s' % repo_path,
|
||||||
|
raising=False)
|
||||||
|
|
||||||
|
# Spec based on tag v1.0
|
||||||
|
spec_tag = spack.spec.Spec('git-test-commit@git.v1.0')
|
||||||
|
spec_tag.concretize()
|
||||||
|
assert spec_tag.satisfies('@1.0')
|
||||||
|
assert not spec_tag.satisfies('@1.1:')
|
||||||
|
assert str(spec_tag.version) == 'git.v1.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'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('string,git', [
|
||||||
|
('1.2.9', False),
|
||||||
|
('gitmain', False),
|
||||||
|
('git.foo', True),
|
||||||
|
('git.abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd', True),
|
||||||
|
('abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd', True),
|
||||||
|
])
|
||||||
|
def test_version_git_vs_base(string, git):
|
||||||
|
assert isinstance(Version(string), GitVersion) == git
|
||||||
|
|
||||||
|
|
||||||
def test_version_range_nonempty():
|
def test_version_range_nonempty():
|
||||||
assert Version('1.2.9') in VersionRange('1.2.0', '1.2')
|
assert Version('1.2.9') in VersionRange('1.2.0', '1.2')
|
||||||
assert Version('1.1.1') in ver('1.0:1')
|
assert Version('1.1.1') in ver('1.0:1')
|
||||||
|
|
|
@ -67,11 +67,11 @@
|
||||||
def coerce_versions(a, b):
|
def coerce_versions(a, b):
|
||||||
"""
|
"""
|
||||||
Convert both a and b to the 'greatest' type between them, in this order:
|
Convert both a and b to the 'greatest' type between them, in this order:
|
||||||
Version < VersionRange < VersionList
|
VersionBase < GitVersion < VersionRange < VersionList
|
||||||
This is used to simplify comparison operations below so that we're always
|
This is used to simplify comparison operations below so that we're always
|
||||||
comparing things that are of the same type.
|
comparing things that are of the same type.
|
||||||
"""
|
"""
|
||||||
order = (Version, VersionRange, VersionList)
|
order = (VersionBase, GitVersion, VersionRange, VersionList)
|
||||||
ta, tb = type(a), type(b)
|
ta, tb = type(a), type(b)
|
||||||
|
|
||||||
def check_type(t):
|
def check_type(t):
|
||||||
|
@ -83,12 +83,16 @@ def check_type(t):
|
||||||
if ta == tb:
|
if ta == tb:
|
||||||
return (a, b)
|
return (a, b)
|
||||||
elif order.index(ta) > order.index(tb):
|
elif order.index(ta) > order.index(tb):
|
||||||
if ta == VersionRange:
|
if ta == GitVersion:
|
||||||
|
return (a, GitVersion(b))
|
||||||
|
elif ta == VersionRange:
|
||||||
return (a, VersionRange(b, b))
|
return (a, VersionRange(b, b))
|
||||||
else:
|
else:
|
||||||
return (a, VersionList([b]))
|
return (a, VersionList([b]))
|
||||||
else:
|
else:
|
||||||
if tb == VersionRange:
|
if tb == GitVersion:
|
||||||
|
return (GitVersion(a), b)
|
||||||
|
elif tb == VersionRange:
|
||||||
return (VersionRange(a, a), b)
|
return (VersionRange(a, a), b)
|
||||||
else:
|
else:
|
||||||
return (VersionList([a]), b)
|
return (VersionList([a]), b)
|
||||||
|
@ -165,15 +169,29 @@ def __gt__(self, other):
|
||||||
return not self.__lt__(other)
|
return not self.__lt__(other)
|
||||||
|
|
||||||
|
|
||||||
class Version(object):
|
def is_git_version(string):
|
||||||
|
if string.startswith('git.'):
|
||||||
|
return True
|
||||||
|
elif len(string) == 40 and COMMIT_VERSION.match(string):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def Version(string): # capitalized for backwards compatibility
|
||||||
|
if not isinstance(string, str):
|
||||||
|
string = str(string) # to handle VersionBase and GitVersion types
|
||||||
|
|
||||||
|
if is_git_version(string):
|
||||||
|
return GitVersion(string)
|
||||||
|
return VersionBase(string)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionBase(object):
|
||||||
"""Class to represent versions"""
|
"""Class to represent versions"""
|
||||||
__slots__ = [
|
__slots__ = [
|
||||||
"version",
|
"version",
|
||||||
"separators",
|
"separators",
|
||||||
"string",
|
"string",
|
||||||
"_commit_lookup",
|
|
||||||
"is_commit",
|
|
||||||
"commit_version",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, string):
|
def __init__(self, string):
|
||||||
|
@ -188,36 +206,12 @@ def __init__(self, string):
|
||||||
if string and not VALID_VERSION.match(string):
|
if string and not VALID_VERSION.match(string):
|
||||||
raise ValueError("Bad characters in version string: %s" % string)
|
raise ValueError("Bad characters in version string: %s" % string)
|
||||||
|
|
||||||
# An object that can lookup git commits to compare them to versions
|
|
||||||
self._commit_lookup = None
|
|
||||||
self.commit_version = None
|
|
||||||
segments = SEGMENT_REGEX.findall(string)
|
segments = SEGMENT_REGEX.findall(string)
|
||||||
self.version = tuple(
|
self.version = tuple(
|
||||||
int(m[0]) if m[0] else VersionStrComponent(m[1]) for m in segments
|
int(m[0]) if m[0] else VersionStrComponent(m[1]) for m in segments
|
||||||
)
|
)
|
||||||
self.separators = tuple(m[2] for m in segments)
|
self.separators = tuple(m[2] for m in segments)
|
||||||
|
|
||||||
self.is_commit = len(self.string) == 40 and COMMIT_VERSION.match(self.string)
|
|
||||||
|
|
||||||
def _cmp(self, other_lookups=None):
|
|
||||||
commit_lookup = self.commit_lookup or other_lookups
|
|
||||||
|
|
||||||
if self.is_commit and commit_lookup:
|
|
||||||
if self.commit_version is not None:
|
|
||||||
return self.commit_version
|
|
||||||
commit_info = commit_lookup.get(self.string)
|
|
||||||
if commit_info:
|
|
||||||
prev_version, distance = commit_info
|
|
||||||
|
|
||||||
# Extend previous version by empty component and distance
|
|
||||||
# If commit is exactly a known version, no distance suffix
|
|
||||||
prev_tuple = Version(prev_version).version if prev_version else ()
|
|
||||||
dist_suffix = (VersionStrComponent(''), distance) if distance else ()
|
|
||||||
self.commit_version = prev_tuple + dist_suffix
|
|
||||||
return self.commit_version
|
|
||||||
|
|
||||||
return self.version
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dotted(self):
|
def dotted(self):
|
||||||
"""The dotted representation of the version.
|
"""The dotted representation of the version.
|
||||||
|
@ -230,7 +224,7 @@ def dotted(self):
|
||||||
Returns:
|
Returns:
|
||||||
Version: The version with separator characters replaced by dots
|
Version: The version with separator characters replaced by dots
|
||||||
"""
|
"""
|
||||||
return Version(self.string.replace('-', '.').replace('_', '.'))
|
return type(self)(self.string.replace('-', '.').replace('_', '.'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def underscored(self):
|
def underscored(self):
|
||||||
|
@ -245,7 +239,7 @@ def underscored(self):
|
||||||
Version: The version with separator characters replaced by
|
Version: The version with separator characters replaced by
|
||||||
underscores
|
underscores
|
||||||
"""
|
"""
|
||||||
return Version(self.string.replace('.', '_').replace('-', '_'))
|
return type(self)(self.string.replace('.', '_').replace('-', '_'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dashed(self):
|
def dashed(self):
|
||||||
|
@ -259,7 +253,7 @@ def dashed(self):
|
||||||
Returns:
|
Returns:
|
||||||
Version: The version with separator characters replaced by dashes
|
Version: The version with separator characters replaced by dashes
|
||||||
"""
|
"""
|
||||||
return Version(self.string.replace('.', '-').replace('_', '-'))
|
return type(self)(self.string.replace('.', '-').replace('_', '-'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def joined(self):
|
def joined(self):
|
||||||
|
@ -273,7 +267,7 @@ def joined(self):
|
||||||
Returns:
|
Returns:
|
||||||
Version: The version with separator characters removed
|
Version: The version with separator characters removed
|
||||||
"""
|
"""
|
||||||
return Version(
|
return type(self)(
|
||||||
self.string.replace('.', '').replace('-', '').replace('_', ''))
|
self.string.replace('.', '').replace('-', '').replace('_', ''))
|
||||||
|
|
||||||
def up_to(self, index):
|
def up_to(self, index):
|
||||||
|
@ -323,13 +317,9 @@ def satisfies(self, other):
|
||||||
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
|
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
|
||||||
a suitable compiler.
|
a suitable compiler.
|
||||||
"""
|
"""
|
||||||
self_cmp = self._cmp(other.commit_lookup)
|
nself = len(self.version)
|
||||||
other_cmp = other._cmp(self.commit_lookup)
|
nother = len(other.version)
|
||||||
|
return nother <= nself and self.version[:nother] == other.version
|
||||||
# Do the final comparison
|
|
||||||
nself = len(self_cmp)
|
|
||||||
nother = len(other_cmp)
|
|
||||||
return nother <= nself and self_cmp[:nother] == other_cmp
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.version)
|
return iter(self.version)
|
||||||
|
@ -356,19 +346,19 @@ def __getitem__(self, idx):
|
||||||
string_arg = ''.join(string_arg)
|
string_arg = ''.join(string_arg)
|
||||||
return cls(string_arg)
|
return cls(string_arg)
|
||||||
else:
|
else:
|
||||||
return Version('')
|
return VersionBase('')
|
||||||
|
|
||||||
message = '{cls.__name__} indices must be integers'
|
message = '{cls.__name__} indices must be integers'
|
||||||
raise TypeError(message.format(cls=cls))
|
raise TypeError(message.format(cls=cls))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Version(' + repr(self.string) + ')'
|
return 'VersionBase(' + repr(self.string) + ')'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.string
|
return self.string
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __format__(self, format_spec):
|
||||||
return self.string.format(format_spec)
|
return str(self).format(format_spec)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def concrete(self):
|
def concrete(self):
|
||||||
|
@ -384,22 +374,16 @@ def __lt__(self, other):
|
||||||
if other is None:
|
if other is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If either is a commit and we haven't indexed yet, can't compare
|
|
||||||
if (other.is_commit or self.is_commit) and not (self.commit_lookup or
|
|
||||||
other.commit_lookup):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Use tuple comparison assisted by VersionStrComponent for performance
|
# Use tuple comparison assisted by VersionStrComponent for performance
|
||||||
return self._cmp(other.commit_lookup) < other._cmp(self.commit_lookup)
|
return self.version < other.version
|
||||||
|
|
||||||
@coerced
|
@coerced
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
|
||||||
# Cut out early if we don't have a version
|
# Cut out early if we don't have a version
|
||||||
if other is None or type(other) != Version:
|
if other is None or type(other) != VersionBase:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._cmp(other.commit_lookup) == other._cmp(self.commit_lookup)
|
return self.version == other.version
|
||||||
|
|
||||||
@coerced
|
@coerced
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
|
@ -425,24 +409,23 @@ def __contains__(self, other):
|
||||||
if other is None:
|
if other is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self_cmp = self._cmp(other.commit_lookup)
|
return other.version[:len(self.version)] == self.version
|
||||||
return other._cmp(self.commit_lookup)[:len(self_cmp)] == self_cmp
|
|
||||||
|
|
||||||
|
@coerced
|
||||||
def is_predecessor(self, other):
|
def is_predecessor(self, other):
|
||||||
"""True if the other version is the immediate predecessor of this one.
|
"""True if the other version is the immediate predecessor of this one.
|
||||||
That is, NO non-commit versions v exist such that:
|
That is, NO non-git versions v exist such that:
|
||||||
(self < v < other and v not in self).
|
(self < v < other and v not in self).
|
||||||
"""
|
"""
|
||||||
self_cmp = self._cmp(self.commit_lookup)
|
if self.version[:-1] != other.version[:-1]:
|
||||||
other_cmp = other._cmp(other.commit_lookup)
|
|
||||||
|
|
||||||
if self_cmp[:-1] != other_cmp[:-1]:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
sl = self_cmp[-1]
|
sl = self.version[-1]
|
||||||
ol = other_cmp[-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)
|
return type(sl) == int and type(ol) == int and (ol - sl == 1)
|
||||||
|
|
||||||
|
@coerced
|
||||||
def is_successor(self, other):
|
def is_successor(self, other):
|
||||||
return other.is_predecessor(self)
|
return other.is_predecessor(self)
|
||||||
|
|
||||||
|
@ -468,13 +451,135 @@ def intersection(self, other):
|
||||||
else:
|
else:
|
||||||
return VersionList()
|
return VersionList()
|
||||||
|
|
||||||
@property
|
|
||||||
def commit_lookup(self):
|
|
||||||
if self._commit_lookup:
|
|
||||||
self._commit_lookup.get(self.string)
|
|
||||||
return self._commit_lookup
|
|
||||||
|
|
||||||
def generate_commit_lookup(self, pkg_name):
|
class GitVersion(VersionBase):
|
||||||
|
"""Class to represent versions interpreted from git refs.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
git_prefix = string.startswith('git.')
|
||||||
|
self.ref = string[4:] if git_prefix else string
|
||||||
|
|
||||||
|
self.is_commit = 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)
|
||||||
|
|
||||||
|
# ensure git.<hash> and <hash> are treated the same by dropping 'git.'
|
||||||
|
canonical_string = self.ref if self.is_commit else string
|
||||||
|
super(GitVersion, self).__init__(canonical_string)
|
||||||
|
|
||||||
|
# An object that can lookup git refs to compare them to versions
|
||||||
|
self._ref_lookup = None
|
||||||
|
self.ref_version = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# If we've already looked this version up, return cached value
|
||||||
|
if self.ref_version is not None:
|
||||||
|
return self.ref_version
|
||||||
|
|
||||||
|
ref_lookup = self.ref_lookup or other_lookups
|
||||||
|
|
||||||
|
if self.is_ref and ref_lookup:
|
||||||
|
ref_info = ref_lookup.get(self.ref)
|
||||||
|
if ref_info:
|
||||||
|
prev_version, distance = ref_info
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
@coerced
|
||||||
|
def satisfies(self, other):
|
||||||
|
"""A Version 'satisfies' another if it is at least as specific and has
|
||||||
|
a common prefix. e.g., we want gcc@4.7.3 to satisfy a request for
|
||||||
|
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
|
||||||
|
a suitable compiler.
|
||||||
|
"""
|
||||||
|
self_cmp = self._cmp(other.ref_lookup)
|
||||||
|
other_cmp = other._cmp(self.ref_lookup)
|
||||||
|
|
||||||
|
# Do the final comparison
|
||||||
|
nself = len(self_cmp)
|
||||||
|
nother = len(other_cmp)
|
||||||
|
return nother <= nself and self_cmp[:nother] == other_cmp
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'GitVersion(' + repr(self.string) + ')'
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Use tuple comparison assisted by VersionStrComponent for performance
|
||||||
|
return self._cmp(other.ref_lookup) < other._cmp(self.ref_lookup)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
return self._cmp(other.ref_lookup) == other._cmp(self.ref_lookup)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(str(self))
|
||||||
|
|
||||||
|
@coerced
|
||||||
|
def __contains__(self, other):
|
||||||
|
if other is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self_cmp = self._cmp(other.ref_lookup)
|
||||||
|
return other._cmp(self.ref_lookup)[:len(self_cmp)] == self_cmp
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
if self_cmp[:-1] != other_cmp[:-1]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
sl = self_cmp[-1]
|
||||||
|
ol = other_cmp[-1]
|
||||||
|
return type(sl) == int and type(ol) == int and (ol - sl == 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ref_lookup(self):
|
||||||
|
if self._ref_lookup:
|
||||||
|
# Get operation ensures dict is populated
|
||||||
|
self._ref_lookup.get(self.ref)
|
||||||
|
return self._ref_lookup
|
||||||
|
|
||||||
|
def generate_git_lookup(self, pkg_name):
|
||||||
"""
|
"""
|
||||||
Use the git fetcher to look up a version for a commit.
|
Use the git fetcher to look up a version for a commit.
|
||||||
|
|
||||||
|
@ -492,11 +597,11 @@ def generate_commit_lookup(self, pkg_name):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sanity check we have a commit
|
# Sanity check we have a commit
|
||||||
if not self.is_commit:
|
if not self.is_ref:
|
||||||
tty.die("%s is not a commit." % self)
|
tty.die("%s is not a git version." % self)
|
||||||
|
|
||||||
# Generate a commit looker-upper
|
# Generate a commit looker-upper
|
||||||
self._commit_lookup = CommitLookup(pkg_name)
|
self._ref_lookup = CommitLookup(pkg_name)
|
||||||
|
|
||||||
|
|
||||||
class VersionRange(object):
|
class VersionRange(object):
|
||||||
|
@ -715,7 +820,7 @@ def __init__(self, vlist=None):
|
||||||
self.add(ver(v))
|
self.add(ver(v))
|
||||||
|
|
||||||
def add(self, version):
|
def add(self, version):
|
||||||
if type(version) in (Version, VersionRange):
|
if type(version) in (VersionBase, GitVersion, VersionRange):
|
||||||
# This normalizes single-value version ranges.
|
# This normalizes single-value version ranges.
|
||||||
if version.concrete:
|
if version.concrete:
|
||||||
version = version.concrete
|
version = version.concrete
|
||||||
|
@ -968,7 +1073,7 @@ def ver(obj):
|
||||||
return _string_to_version(obj)
|
return _string_to_version(obj)
|
||||||
elif isinstance(obj, (int, float)):
|
elif isinstance(obj, (int, float)):
|
||||||
return _string_to_version(str(obj))
|
return _string_to_version(str(obj))
|
||||||
elif type(obj) in (Version, VersionRange, VersionList):
|
elif type(obj) in (VersionBase, GitVersion, VersionRange, VersionList):
|
||||||
return obj
|
return obj
|
||||||
else:
|
else:
|
||||||
raise TypeError("ver() can't convert %s to version!" % type(obj))
|
raise TypeError("ver() can't convert %s to version!" % type(obj))
|
||||||
|
@ -990,8 +1095,8 @@ class CommitLookup(object):
|
||||||
"""An object for cached lookups of git commits
|
"""An object for cached lookups of git commits
|
||||||
|
|
||||||
CommitLookup objects delegate to the misc_cache for locking.
|
CommitLookup objects delegate to the misc_cache for locking.
|
||||||
CommitLookup objects may be attached to a Version object for which
|
CommitLookup objects may be attached to a GitVersion object for which
|
||||||
Version.is_commit returns True to allow for comparisons between git commits
|
Version.is_ref returns True to allow for comparisons between git refs
|
||||||
and versions as represented by tags in the git repository.
|
and versions as represented by tags in the git repository.
|
||||||
"""
|
"""
|
||||||
def __init__(self, pkg_name):
|
def __init__(self, pkg_name):
|
||||||
|
@ -1074,17 +1179,17 @@ def load_data(self):
|
||||||
with spack.caches.misc_cache.read_transaction(self.cache_key) as cache_file:
|
with spack.caches.misc_cache.read_transaction(self.cache_key) as cache_file:
|
||||||
self.data = sjson.load(cache_file)
|
self.data = sjson.load(cache_file)
|
||||||
|
|
||||||
def get(self, commit):
|
def get(self, ref):
|
||||||
if not self.data:
|
if not self.data:
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
if commit not in self.data:
|
if ref not in self.data:
|
||||||
self.data[commit] = self.lookup_commit(commit)
|
self.data[ref] = self.lookup_ref(ref)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return self.data[commit]
|
return self.data[ref]
|
||||||
|
|
||||||
def lookup_commit(self, commit):
|
def lookup_ref(self, ref):
|
||||||
"""Lookup the previous version and distance for a given commit.
|
"""Lookup the previous version and distance for a given commit.
|
||||||
|
|
||||||
We use git to compare the known versions from package to the git tags,
|
We use git to compare the known versions from package to the git tags,
|
||||||
|
@ -1111,13 +1216,19 @@ def lookup_commit(self, commit):
|
||||||
# remote instance, simply adding '-f' may not be sufficient
|
# remote instance, simply adding '-f' may not be sufficient
|
||||||
# (if commits are deleted on the remote, this command alone
|
# (if commits are deleted on the remote, this command alone
|
||||||
# won't properly update the local rev-list)
|
# won't properly update the local rev-list)
|
||||||
self.fetcher.git("fetch", '--tags')
|
self.fetcher.git("fetch", '--tags', output=os.devnull, error=os.devnull)
|
||||||
|
|
||||||
# Ensure commit is an object known to git
|
# Ensure ref is a commit object known to git
|
||||||
# Note the brackets are literals, the commit replaces the format string
|
# Note the brackets are literals, the ref replaces the format string
|
||||||
# This will raise a ProcessError if the commit does not exist
|
try:
|
||||||
# We may later design a custom error to re-raise
|
self.fetcher.git(
|
||||||
self.fetcher.git('cat-file', '-e', '%s^{commit}' % commit)
|
'cat-file', '-e', '%s^{commit}' % ref,
|
||||||
|
output=os.devnull, error=os.devnull
|
||||||
|
)
|
||||||
|
except spack.util.executable.ProcessError:
|
||||||
|
raise VersionLookupError(
|
||||||
|
"%s is not a valid git ref for %s" % (ref, self.pkg_name)
|
||||||
|
)
|
||||||
|
|
||||||
# List tags (refs) by date, so last reference of a tag is newest
|
# List tags (refs) by date, so last reference of a tag is newest
|
||||||
tag_info = self.fetcher.git(
|
tag_info = self.fetcher.git(
|
||||||
|
@ -1148,11 +1259,11 @@ def lookup_commit(self, commit):
|
||||||
ancestor_commits = []
|
ancestor_commits = []
|
||||||
for tag_commit in commit_to_version:
|
for tag_commit in commit_to_version:
|
||||||
self.fetcher.git(
|
self.fetcher.git(
|
||||||
'merge-base', '--is-ancestor', tag_commit, commit,
|
'merge-base', '--is-ancestor', tag_commit, ref,
|
||||||
ignore_errors=[1])
|
ignore_errors=[1])
|
||||||
if self.fetcher.git.returncode == 0:
|
if self.fetcher.git.returncode == 0:
|
||||||
distance = self.fetcher.git(
|
distance = self.fetcher.git(
|
||||||
'rev-list', '%s..%s' % (tag_commit, commit), '--count',
|
'rev-list', '%s..%s' % (tag_commit, ref), '--count',
|
||||||
output=str, error=str).strip()
|
output=str, error=str).strip()
|
||||||
ancestor_commits.append((tag_commit, int(distance)))
|
ancestor_commits.append((tag_commit, int(distance)))
|
||||||
|
|
||||||
|
@ -1164,14 +1275,14 @@ def lookup_commit(self, commit):
|
||||||
else:
|
else:
|
||||||
# Get list of all commits, this is in reverse order
|
# Get list of all commits, this is in reverse order
|
||||||
# We use this to get the first commit below
|
# We use this to get the first commit below
|
||||||
commit_info = self.fetcher.git("log", "--all", "--pretty=format:%H",
|
ref_info = self.fetcher.git("log", "--all", "--pretty=format:%H",
|
||||||
output=str)
|
output=str)
|
||||||
commits = [c for c in commit_info.split('\n') if c]
|
commits = [c for c in ref_info.split('\n') if c]
|
||||||
|
|
||||||
# No previous version and distance from first commit
|
# No previous version and distance from first commit
|
||||||
prev_version = None
|
prev_version = None
|
||||||
distance = int(self.fetcher.git(
|
distance = int(self.fetcher.git(
|
||||||
'rev-list', '%s..%s' % (commits[-1], commit), '--count',
|
'rev-list', '%s..%s' % (commits[-1], ref), '--count',
|
||||||
output=str, error=str
|
output=str, error=str
|
||||||
).strip())
|
).strip())
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Libcatalyst(CMakePackage):
|
||||||
maintainers = ['mathstuf']
|
maintainers = ['mathstuf']
|
||||||
|
|
||||||
# master as of 2021-05-12
|
# master as of 2021-05-12
|
||||||
version('8456ccd6015142b5a7705f79471361d4f5644fa7', sha256='5a01f12b271d9d9e9b89f31d45a5f4b8426904483639d38754893adfd3547bab')
|
version('2021-05-12', sha256='5a01f12b271d9d9e9b89f31d45a5f4b8426904483639d38754893adfd3547bab')
|
||||||
|
|
||||||
variant('mpi', default=False, description='Enable MPI support')
|
variant('mpi', default=False, description='Enable MPI support')
|
||||||
variant('python3', default=False, description='Enable Python3 support')
|
variant('python3', default=False, description='Enable Python3 support')
|
||||||
|
|
Loading…
Reference in a new issue