Skip to content

Commit 3d597e2

Browse files
authored
Bugfix: package requirements with git commits (#35057)
* Specs that define 'new' versions in the require: section need to generate associated facts to indicate that those versions are valid. * add test to verify success with unknown versions.
1 parent 739a67e commit 3d597e2

4 files changed

Lines changed: 215 additions & 11 deletions

File tree

lib/spack/spack/solver/asp.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,30 @@ def getter(node):
115115
"VersionProvenance", version_origin_fields
116116
)(**{name: i for i, name in enumerate(version_origin_fields)})
117117

118-
#: Named tuple to contain information on declared versions
119-
DeclaredVersion = collections.namedtuple("DeclaredVersion", ["version", "idx", "origin"])
118+
119+
class DeclaredVersion(object):
120+
def __init__(self, version: spack.version.VersionBase, idx, origin):
121+
if not isinstance(version, spack.version.VersionBase):
122+
raise ValueError("Unexpected type for declared version: {0}".format(type(version)))
123+
self.version = version
124+
self.idx = idx
125+
self.origin = origin
126+
127+
def __eq__(self, other):
128+
if not isinstance(other, DeclaredVersion):
129+
return False
130+
is_git = lambda v: isinstance(v, spack.version.GitVersion)
131+
if is_git(self.version) != is_git(other.version):
132+
# GitVersion(hash=x) and Version(y) compare equal if x == y, but
133+
# we do not want that to be true for DeclaredVersion, which is
134+
# tracking how much information is provided: in that sense, the
135+
# GitVersion provides more information and is therefore not equal.
136+
return False
137+
return (self.version, self.idx, self.origin) == (other.version, other.idx, other.origin)
138+
139+
def __hash__(self):
140+
return hash((self.version, self.idx, self.origin))
141+
120142

121143
# Below numbers are used to map names of criteria to the order
122144
# they appear in the solution. See concretize.lp
@@ -1651,6 +1673,15 @@ def add_concrete_versions_from_specs(self, specs, origin):
16511673
DeclaredVersion(version=dep.version, idx=0, origin=origin)
16521674
)
16531675
self.possible_versions[dep.name].add(dep.version)
1676+
if (
1677+
isinstance(dep.version, spack.version.GitVersion)
1678+
and dep.version.user_supplied_reference
1679+
):
1680+
defined_version = spack.version.Version(dep.version.ref_version_str)
1681+
self.declared_versions[dep.name].append(
1682+
DeclaredVersion(version=defined_version, idx=0, origin=origin)
1683+
)
1684+
self.possible_versions[dep.name].add(defined_version)
16541685

16551686
def _supported_targets(self, compiler_name, compiler_version, targets):
16561687
"""Get a list of which targets are supported by the compiler.
@@ -1889,7 +1920,11 @@ def define_version_constraints(self):
18891920

18901921
# This is needed to account for a variable number of
18911922
# numbers e.g. if both 1.0 and 1.0.2 are possible versions
1892-
exact_match = [v for v in allowed_versions if v == versions]
1923+
exact_match = [
1924+
v
1925+
for v in allowed_versions
1926+
if v == versions and not isinstance(v, spack.version.GitVersion)
1927+
]
18931928
if exact_match:
18941929
allowed_versions = exact_match
18951930

@@ -2091,6 +2126,9 @@ def setup(self, driver, specs, reuse=None):
20912126
self.add_concrete_versions_from_specs(specs, version_provenance.spec)
20922127
self.add_concrete_versions_from_specs(dev_specs, version_provenance.dev_spec)
20932128

2129+
req_version_specs = _get_versioned_specs_from_pkg_requirements()
2130+
self.add_concrete_versions_from_specs(req_version_specs, version_provenance.packages_yaml)
2131+
20942132
self.gen.h1("Concrete input spec definitions")
20952133
self.define_concrete_input_specs(specs, possible)
20962134

@@ -2165,6 +2203,52 @@ def literal_specs(self, specs):
21652203
self.gen.fact(fn.concretize_everything())
21662204

21672205

2206+
def _get_versioned_specs_from_pkg_requirements():
2207+
"""If package requirements mention versions that are not mentioned
2208+
elsewhere, then we need to collect those to mark them as possible
2209+
versions.
2210+
"""
2211+
req_version_specs = list()
2212+
config = spack.config.get("packages")
2213+
for pkg_name, d in config.items():
2214+
if pkg_name == "all":
2215+
continue
2216+
if "require" in d:
2217+
req_version_specs.extend(_specs_from_requires(pkg_name, d["require"]))
2218+
return req_version_specs
2219+
2220+
2221+
def _specs_from_requires(pkg_name, section):
2222+
if isinstance(section, str):
2223+
spec = spack.spec.Spec(section)
2224+
if not spec.name:
2225+
spec.name = pkg_name
2226+
extracted_specs = [spec]
2227+
else:
2228+
spec_strs = []
2229+
# Each of these will be one_of or any_of
2230+
for spec_group in section:
2231+
(x,) = spec_group.values()
2232+
spec_strs.extend(x)
2233+
2234+
extracted_specs = []
2235+
for spec_str in spec_strs:
2236+
spec = spack.spec.Spec(spec_str)
2237+
if not spec.name:
2238+
spec.name = pkg_name
2239+
extracted_specs.append(spec)
2240+
2241+
version_specs = []
2242+
for spec in extracted_specs:
2243+
try:
2244+
spec.version
2245+
version_specs.append(spec)
2246+
except spack.error.SpecError:
2247+
pass
2248+
2249+
return version_specs
2250+
2251+
21682252
class SpecBuilder(object):
21692253
"""Class with actions to rebuild a spec from ASP results."""
21702254

lib/spack/spack/test/concretize.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import spack.repo
2323
import spack.variant as vt
2424
from spack.concretize import find_spec
25-
from spack.solver.asp import UnsatisfiableSpecError
2625
from spack.spec import Spec
2726
from spack.version import ver
2827

@@ -1845,16 +1844,13 @@ def test_git_ref_version_is_equivalent_to_specified_version(self, git_ref):
18451844
assert s.satisfies("@0.1:")
18461845

18471846
@pytest.mark.parametrize("git_ref", ("a" * 40, "0.2.15", "fbranch"))
1848-
def test_git_ref_version_errors_if_unknown_version(self, git_ref):
1847+
def test_git_ref_version_succeeds_with_unknown_version(self, git_ref):
18491848
if spack.config.get("config:concretizer") == "original":
18501849
pytest.skip("Original concretizer cannot account for git hashes")
18511850
# main is not defined in the package.py for this file
18521851
s = Spec("develop-branch-version@git.%s=main" % git_ref)
1853-
with pytest.raises(
1854-
UnsatisfiableSpecError,
1855-
match="The reference version 'main' for package 'develop-branch-version'",
1856-
):
1857-
s.concretized()
1852+
s.concretize()
1853+
assert s.satisfies("develop-branch-version@main")
18581854

18591855
@pytest.mark.regression("31484")
18601856
def test_installed_externals_are_reused(self, mutable_database, repo_with_changing_recipe):

lib/spack/spack/test/concretize_requirements.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,114 @@ def test_requirement_isnt_optional(concretize_scope, test_repo):
140140
Spec("x@1.1").concretize()
141141

142142

143+
def test_git_user_supplied_reference_satisfaction(
144+
concretize_scope, test_repo, mock_git_version_info, monkeypatch
145+
):
146+
repo_path, filename, commits = mock_git_version_info
147+
148+
monkeypatch.setattr(
149+
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
150+
)
151+
152+
specs = ["v@{commit0}=2.2", "v@{commit0}", "v@2.2", "v@{commit0}=2.3"]
153+
154+
format_info = {"commit0": commits[0]}
155+
156+
hash_eq_ver, just_hash, just_ver, hash_eq_other_ver = [
157+
Spec(x.format(**format_info)) for x in specs
158+
]
159+
160+
assert hash_eq_ver.satisfies(just_hash)
161+
assert not just_hash.satisfies(hash_eq_ver)
162+
assert hash_eq_ver.satisfies(just_ver)
163+
assert not just_ver.satisfies(hash_eq_ver)
164+
assert not hash_eq_ver.satisfies(hash_eq_other_ver)
165+
assert not hash_eq_other_ver.satisfies(hash_eq_ver)
166+
167+
168+
def test_requirement_adds_new_version(
169+
concretize_scope, test_repo, mock_git_version_info, monkeypatch
170+
):
171+
if spack.config.get("config:concretizer") == "original":
172+
pytest.skip("Original concretizer does not support configuration" " requirements")
173+
174+
repo_path, filename, commits = mock_git_version_info
175+
monkeypatch.setattr(
176+
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
177+
)
178+
179+
a_commit_hash = commits[0]
180+
conf_str = """\
181+
packages:
182+
v:
183+
require: "@{0}=2.2"
184+
""".format(
185+
a_commit_hash
186+
)
187+
update_packages_config(conf_str)
188+
189+
s1 = Spec("v").concretized()
190+
assert s1.satisfies("@2.2")
191+
assert s1.satisfies("@{0}".format(a_commit_hash))
192+
# Make sure the git commit info is retained
193+
assert isinstance(s1.version, spack.version.GitVersion)
194+
assert s1.version.ref == a_commit_hash
195+
196+
197+
def test_requirement_adds_git_hash_version(
198+
concretize_scope, test_repo, mock_git_version_info, monkeypatch
199+
):
200+
if spack.config.get("config:concretizer") == "original":
201+
pytest.skip("Original concretizer does not support configuration" " requirements")
202+
203+
repo_path, filename, commits = mock_git_version_info
204+
monkeypatch.setattr(
205+
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
206+
)
207+
208+
a_commit_hash = commits[0]
209+
conf_str = """\
210+
packages:
211+
v:
212+
require: "@{0}"
213+
""".format(
214+
a_commit_hash
215+
)
216+
update_packages_config(conf_str)
217+
218+
s1 = Spec("v").concretized()
219+
assert s1.satisfies("@{0}".format(a_commit_hash))
220+
221+
222+
def test_requirement_adds_multiple_new_versions(
223+
concretize_scope, test_repo, mock_git_version_info, monkeypatch
224+
):
225+
if spack.config.get("config:concretizer") == "original":
226+
pytest.skip("Original concretizer does not support configuration" " requirements")
227+
228+
repo_path, filename, commits = mock_git_version_info
229+
monkeypatch.setattr(
230+
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
231+
)
232+
233+
conf_str = """\
234+
packages:
235+
v:
236+
require:
237+
- one_of: ["@{0}=2.2", "@{1}=2.3"]
238+
""".format(
239+
commits[0], commits[1]
240+
)
241+
update_packages_config(conf_str)
242+
243+
s1 = Spec("v").concretized()
244+
assert s1.satisfies("@2.2")
245+
246+
s2 = Spec("v@{0}".format(commits[1])).concretized()
247+
assert s2.satisfies("@{0}".format(commits[1]))
248+
assert s2.satisfies("@2.3")
249+
250+
143251
def test_requirement_is_successfully_applied(concretize_scope, test_repo):
144252
"""If a simple requirement can be satisfied, make sure the
145253
concretization succeeds and the requirement spec is applied.

lib/spack/spack/version.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,9 @@ def intersection(self, other):
506506
return VersionList()
507507

508508

509+
_version_debug = False
510+
511+
509512
class GitVersion(VersionBase):
510513
"""Class to represent versions interpreted from git refs.
511514
@@ -627,7 +630,20 @@ def satisfies(self, other):
627630
self_cmp = self._cmp(other.ref_lookup)
628631
other_cmp = other._cmp(self.ref_lookup)
629632

630-
if other.is_ref:
633+
if self.is_ref and other.is_ref:
634+
if self.ref != other.ref:
635+
return False
636+
elif self.user_supplied_reference and other.user_supplied_reference:
637+
return self.ref_version == other.ref_version
638+
elif other.user_supplied_reference:
639+
return False
640+
else:
641+
# In this case, 'other' does not supply a version equivalence
642+
# with "=" and the commit strings are equal. 'self' may specify
643+
# a version equivalence, but that is extra info and will
644+
# satisfy no matter what it is.
645+
return True
646+
elif other.is_ref:
631647
# if other is a ref then satisfaction requires an exact version match
632648
# i.e. the GitRef must match this.version for satisfaction
633649
# this creates an asymmetric comparison:

0 commit comments

Comments
 (0)