X Tutup
Skip to content

Commit 2aec5b6

Browse files
authored
Git commit versions bugfix: Environments and Concretization (spack#29717)
Spack added support in spack#24639 for ad-hoc Git-commit-hash-based versions: A user can install a package x@hash, where X is a package that stores its source code in a Git repository, and the hash refers to a commit in that repository which is not recorded as an explicit version in the package.py file for X. A couple issues were found relating to this: * If an environment defines an alternative package repo (i.e. with repos.yaml), and spack.yaml contains user Specs with ad-hoc Git-commit-hash-based versions for packages in that repo, then as part of retrieving the data needed for version comparisons it will attempt to retrieve the package before the environment's configuration is instantiated. * The bookkeeping information added to compare ad-hoc git versions was being stripped from Specs during concretization (such that user Specs which succeeded before concretizing would then fail after) This addresses the issues: * The first issue is resolved by deferring access to the associated Package until the versions are actually compared to one another. * The second issue is resolved by ensuring that the Git bookkeeping information is explicitly applied to Specs after they are concretized. This also: * Resolves an ambiguity in the mock_git_version_info fixture used to create a tree of Git commits and provide a list where each index maps to a known commit. * Isolates the cache used for Git repositories in tests using the mock_git_version_info fixture * Adds a TODO which points out that if the remote Git repository overwrites tags, that Spack will then fail when using ad-hoc Git-commit-hash-based versions
1 parent 17e2fb0 commit 2aec5b6

File tree

8 files changed

+129
-32
lines changed

8 files changed

+129
-32
lines changed

lib/spack/spack/fetch_strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1595,7 +1595,7 @@ def for_package_version(pkg, version):
15951595
# if it's a commit, we must use a GitFetchStrategy
15961596
if version.is_commit and hasattr(pkg, "git"):
15971597
# Populate the version with comparisons to other commits
1598-
version.generate_commit_lookup(pkg)
1598+
version.generate_commit_lookup(pkg.name)
15991599
fetcher = GitFetchStrategy(git=pkg.git, commit=str(version))
16001600
return fetcher
16011601

lib/spack/spack/solver/asp.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,6 +2050,14 @@ def build_specs(self, function_tuples):
20502050
for s in self._specs.values():
20512051
spack.spec.Spec.ensure_no_deprecated(s)
20522052

2053+
# Add git version lookup info to concrete Specs (this is generated for
2054+
# abstract specs as well but the Versions may be replaced during the
2055+
# concretization process)
2056+
for root in self._specs.values():
2057+
for spec in root.traverse():
2058+
if spec.version.is_commit:
2059+
spec.version.generate_commit_lookup(spec.fullname)
2060+
20532061
return self._specs
20542062

20552063

lib/spack/spack/spec.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5047,9 +5047,7 @@ def do_parse(self):
50475047
spec.name and spec.versions.concrete and
50485048
isinstance(spec.version, vn.Version) and spec.version.is_commit
50495049
):
5050-
pkg = spec.package
5051-
if hasattr(pkg, 'git'):
5052-
spec.version.generate_commit_lookup(pkg)
5050+
spec.version.generate_commit_lookup(spec.fullname)
50535051

50545052
return specs
50555053

lib/spack/spack/test/cmd/install.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def test_install_commit(
263263
'git', 'file://%s' % repo_path,
264264
raising=False)
265265

266+
# Use the earliest commit in the respository
266267
commit = commits[-1]
267268
spec = spack.spec.Spec('git-test-commit@%s' % commit)
268269
spec.concretize()

lib/spack/spack/test/conftest.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,17 @@ def write_file(filename, contents):
7575

7676

7777
@pytest.fixture
78-
def mock_git_version_info(tmpdir, scope="function"):
78+
def override_git_repos_cache_path(tmpdir):
79+
saved = spack.paths.user_repos_cache_path
80+
tmp_path = tmpdir.mkdir('git-repo-cache-path-for-tests')
81+
spack.paths.user_repos_cache_path = str(tmp_path)
82+
yield
83+
spack.paths.user_repos_cache_path = saved
84+
85+
86+
@pytest.fixture
87+
def mock_git_version_info(tmpdir, override_git_repos_cache_path,
88+
scope="function"):
7989
"""Create a mock git repo with known structure
8090
8191
The structure of commits in this repo is as follows::
@@ -116,48 +126,59 @@ def commit(message):
116126
git('config', 'user.name', 'Spack')
117127
git('config', 'user.email', 'spack@spack.io')
118128

129+
commits = []
130+
131+
def latest_commit():
132+
return git('rev-list', '-n1', 'HEAD', output=str, error=str).strip()
133+
119134
# Add two commits on main branch
120135
write_file(filename, '[]')
121136
git('add', filename)
122137
commit('first commit')
138+
commits.append(latest_commit())
123139

124140
# Get name of default branch (differs by git version)
125141
main = git('rev-parse', '--abbrev-ref', 'HEAD', output=str, error=str).strip()
126142

127143
# Tag second commit as v1.0
128144
write_file(filename, "[1, 0]")
129145
commit('second commit')
146+
commits.append(latest_commit())
130147
git('tag', 'v1.0')
131148

132149
# Add two commits and a tag on 1.x branch
133150
git('checkout', '-b', '1.x')
134151
write_file(filename, "[1, 0, '', 1]")
135152
commit('first 1.x commit')
153+
commits.append(latest_commit())
136154

137155
write_file(filename, "[1, 1]")
138156
commit('second 1.x commit')
157+
commits.append(latest_commit())
139158
git('tag', 'v1.1')
140159

141160
# Add two commits and a tag on main branch
142161
git('checkout', main)
143162
write_file(filename, "[1, 0, '', 1]")
144163
commit('third main commit')
164+
commits.append(latest_commit())
145165
write_file(filename, "[2, 0]")
146166
commit('fourth main commit')
167+
commits.append(latest_commit())
147168
git('tag', 'v2.0')
148169

149170
# Add two more commits on 1.x branch to ensure we aren't cheating by using time
150171
git('checkout', '1.x')
151172
write_file(filename, "[1, 1, '', 1]")
152173
commit('third 1.x commit')
174+
commits.append(latest_commit())
153175
write_file(filename, "[1, 2]")
154176
commit('fourth 1.x commit')
177+
commits.append(latest_commit())
155178
git('tag', '1.2') # test robust parsing to different syntax, no v
156179

157-
# Get the commits in topo order
158-
log = git('log', '--all', '--pretty=format:%H', '--topo-order',
159-
output=str, error=str)
160-
commits = [c for c in log.split('\n') if c]
180+
# The commits are ordered with the last commit first in the list
181+
commits = list(reversed(commits))
161182

162183
# Return the git directory to install, the filename used, and the commits
163184
yield repo_path, filename, commits

lib/spack/spack/test/versions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,36 @@ def test_versions_from_git(mock_git_version_info, monkeypatch, mock_packages):
607607
assert str(comparator) == expected
608608

609609

610+
@pytest.mark.skipif(sys.platform == 'win32',
611+
reason="Not supported on Windows (yet)")
612+
def test_git_hash_comparisons(
613+
mock_git_version_info, install_mockery, mock_packages, monkeypatch):
614+
"""Check that hashes compare properly to versions
615+
"""
616+
repo_path, filename, commits = mock_git_version_info
617+
monkeypatch.setattr(spack.package.PackageBase,
618+
'git', 'file://%s' % repo_path,
619+
raising=False)
620+
621+
# Spec based on earliest commit
622+
spec0 = spack.spec.Spec('git-test-commit@%s' % commits[-1])
623+
spec0.concretize()
624+
assert spec0.satisfies('@:0')
625+
assert not spec0.satisfies('@1.0')
626+
627+
# Spec based on second commit (same as version 1.0)
628+
spec1 = spack.spec.Spec('git-test-commit@%s' % commits[-2])
629+
spec1.concretize()
630+
assert spec1.satisfies('@1.0')
631+
assert not spec1.satisfies('@1.1:')
632+
633+
# Spec based on 4th commit (in timestamp order)
634+
spec4 = spack.spec.Spec('git-test-commit@%s' % commits[-4])
635+
spec4.concretize()
636+
assert spec4.satisfies('@1.1')
637+
assert spec4.satisfies('@1.0:1.2')
638+
639+
610640
def test_version_range_nonempty():
611641
assert Version('1.2.9') in VersionRange('1.2.0', '1.2')
612642
assert Version('1.1.1') in ver('1.0:1')

lib/spack/spack/version.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class Version(object):
171171
"version",
172172
"separators",
173173
"string",
174-
"commit_lookup",
174+
"_commit_lookup",
175175
"is_commit",
176176
"commit_version",
177177
]
@@ -188,7 +188,7 @@ def __init__(self, string):
188188
raise ValueError("Bad characters in version string: %s" % string)
189189

190190
# An object that can lookup git commits to compare them to versions
191-
self.commit_lookup = None
191+
self._commit_lookup = None
192192
self.commit_version = None
193193
segments = SEGMENT_REGEX.findall(string)
194194
self.version = tuple(
@@ -467,7 +467,13 @@ def intersection(self, other):
467467
else:
468468
return VersionList()
469469

470-
def generate_commit_lookup(self, pkg):
470+
@property
471+
def commit_lookup(self):
472+
if self._commit_lookup:
473+
self._commit_lookup.get(self.string)
474+
return self._commit_lookup
475+
476+
def generate_commit_lookup(self, pkg_name):
471477
"""
472478
Use the git fetcher to look up a version for a commit.
473479
@@ -483,17 +489,13 @@ def generate_commit_lookup(self, pkg):
483489
fetcher: the fetcher to use.
484490
versions: the known versions of the package
485491
"""
486-
if self.commit_lookup:
487-
return
488492

489493
# Sanity check we have a commit
490494
if not self.is_commit:
491495
tty.die("%s is not a commit." % self)
492496

493497
# Generate a commit looker-upper
494-
self.commit_lookup = CommitLookup(pkg)
495-
self.commit_lookup.get(self.string)
496-
self.commit_lookup.save()
498+
self._commit_lookup = CommitLookup(pkg_name)
497499

498500

499501
class VersionRange(object):
@@ -991,24 +993,55 @@ class CommitLookup(object):
991993
Version.is_commit returns True to allow for comparisons between git commits
992994
and versions as represented by tags in the git repository.
993995
"""
994-
def __init__(self, pkg):
995-
self.pkg = pkg
996-
997-
# We require the full git repository history
998-
import spack.fetch_strategy # break cycle
999-
fetcher = spack.fetch_strategy.GitFetchStrategy(git=pkg.git)
1000-
fetcher.get_full_repo = True
1001-
self.fetcher = fetcher
996+
def __init__(self, pkg_name):
997+
self.pkg_name = pkg_name
1002998

1003999
self.data = {}
10041000

1005-
# Cache data in misc_cache
1006-
key_base = 'git_metadata'
1007-
if not self.repository_uri.startswith('/'):
1008-
key_base += '/'
1009-
self.cache_key = key_base + self.repository_uri
1010-
spack.caches.misc_cache.init_entry(self.cache_key)
1011-
self.cache_path = spack.caches.misc_cache.cache_path(self.cache_key)
1001+
self._pkg = None
1002+
self._fetcher = None
1003+
self._cache_key = None
1004+
self._cache_path = None
1005+
1006+
# The following properties are used as part of a lazy reference scheme
1007+
# to avoid querying the package repository until it is necessary (and
1008+
# in particular to wait until after the configuration has been
1009+
# assembled)
1010+
@property
1011+
def cache_key(self):
1012+
if not self._cache_key:
1013+
key_base = 'git_metadata'
1014+
if not self.repository_uri.startswith('/'):
1015+
key_base += '/'
1016+
self._cache_key = key_base + self.repository_uri
1017+
1018+
# Cache data in misc_cache
1019+
# If this is the first lazy access, initialize the cache as well
1020+
spack.caches.misc_cache.init_entry(self.cache_key)
1021+
return self._cache_key
1022+
1023+
@property
1024+
def cache_path(self):
1025+
if not self._cache_path:
1026+
self._cache_path = spack.caches.misc_cache.cache_path(
1027+
self.cache_key)
1028+
return self._cache_path
1029+
1030+
@property
1031+
def pkg(self):
1032+
if not self._pkg:
1033+
self._pkg = spack.repo.get(self.pkg_name)
1034+
return self._pkg
1035+
1036+
@property
1037+
def fetcher(self):
1038+
if not self._fetcher:
1039+
# We require the full git repository history
1040+
import spack.fetch_strategy # break cycle
1041+
fetcher = spack.fetch_strategy.GitFetchStrategy(git=self.pkg.git)
1042+
fetcher.get_full_repo = True
1043+
self._fetcher = fetcher
1044+
return self._fetcher
10121045

10131046
@property
10141047
def repository_uri(self):
@@ -1073,6 +1106,10 @@ def lookup_commit(self, commit):
10731106

10741107
# Lookup commit info
10751108
with working_dir(dest):
1109+
# TODO: we need to update the local tags if they changed on the
1110+
# remote instance, simply adding '-f' may not be sufficient
1111+
# (if commits are deleted on the remote, this command alone
1112+
# won't properly update the local rev-list)
10761113
self.fetcher.git("fetch", '--tags')
10771114

10781115
# Ensure commit is an object known to git

var/spack/repos/builtin.mock/packages/git-test-commit/package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class GitTestCommit(Package):
1717
version('2.0', tag='v2.0')
1818

1919
def install(self, spec, prefix):
20+
# It is assumed for the test which installs this package, that it will
21+
# be using the earliest commit, which is contained in the range @:0
2022
assert spec.satisfies('@:0')
2123
mkdir(prefix.bin)
2224

0 commit comments

Comments
 (0)
X Tutup