solver: allow prefer and conflict on virtuals in packages config (#45017)

This commit is contained in:
Massimiliano Culpo 2025-03-19 09:53:05 +01:00 committed by GitHub
parent ec34e88d79
commit 5a04e84097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 126 additions and 17 deletions

View File

@ -2234,8 +2234,8 @@ def emit_facts_from_requirement_rules(self, rules: List[RequirementRule]):
spec.attach_git_version_lookup()
when_spec = spec
if virtual:
when_spec = spack.spec.Spec(pkg_name)
if virtual and spec.name != pkg_name:
when_spec = spack.spec.Spec(f"^[virtuals={pkg_name}] {spec.name}")
try:
context = ConditionContext()

View File

@ -597,6 +597,13 @@ attr("virtual_on_edge", PackageNode, ProviderNode, Virtual)
attr("virtual_on_incoming_edges", ProviderNode, Virtual)
:- attr("virtual_on_edge", _, ProviderNode, Virtual).
% This is needed to allow requirement on virtuals,
% when a virtual root is requested
attr("virtual_on_incoming_edges", ProviderNode, Virtual)
:- attr("virtual_root", node(min_dupe_id, Virtual)),
attr("root", ProviderNode),
provider(ProviderNode, node(min_dupe_id, Virtual)).
% dependencies on virtuals also imply that the virtual is a virtual node
1 { attr("virtual_node", node(0..X-1, Virtual)) : max_dupes(Virtual, X) }
:- node_depends_on_virtual(PackageNode, Virtual).

View File

@ -66,18 +66,29 @@ def rules_from_package_py(self, pkg: spack.package_base.PackageBase) -> List[Req
return rules
def rules_from_virtual(self, virtual_str: str) -> List[RequirementRule]:
requirements = self.config.get("packages", {}).get(virtual_str, {}).get("require", [])
return self._rules_from_requirements(
virtual_str, requirements, kind=RequirementKind.VIRTUAL
)
kind, requests = self._raw_yaml_data(virtual_str, section="require", virtual=True)
result = self._rules_from_requirements(virtual_str, requests, kind=kind)
kind, requests = self._raw_yaml_data(virtual_str, section="prefer", virtual=True)
result.extend(self._rules_from_preferences(virtual_str, preferences=requests, kind=kind))
kind, requests = self._raw_yaml_data(virtual_str, section="conflict", virtual=True)
result.extend(self._rules_from_conflicts(virtual_str, conflicts=requests, kind=kind))
return result
def rules_from_require(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
kind, requirements = self._raw_yaml_data(pkg, section="require")
kind, requirements = self._raw_yaml_data(pkg.name, section="require")
return self._rules_from_requirements(pkg.name, requirements, kind=kind)
def rules_from_prefer(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
kind, preferences = self._raw_yaml_data(pkg.name, section="prefer")
return self._rules_from_preferences(pkg.name, preferences=preferences, kind=kind)
def _rules_from_preferences(
self, pkg_name: str, *, preferences, kind: RequirementKind
) -> List[RequirementRule]:
result = []
kind, preferences = self._raw_yaml_data(pkg, section="prefer")
for item in preferences:
spec, condition, message = self._parse_prefer_conflict_item(item)
result.append(
@ -86,7 +97,7 @@ def rules_from_prefer(self, pkg: spack.package_base.PackageBase) -> List[Require
# require:
# - any_of: [spec_str, "@:"]
RequirementRule(
pkg_name=pkg.name,
pkg_name=pkg_name,
policy="any_of",
requirements=[spec, spack.spec.Spec("@:")],
kind=kind,
@ -97,8 +108,13 @@ def rules_from_prefer(self, pkg: spack.package_base.PackageBase) -> List[Require
return result
def rules_from_conflict(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
kind, conflicts = self._raw_yaml_data(pkg.name, section="conflict")
return self._rules_from_conflicts(pkg.name, conflicts=conflicts, kind=kind)
def _rules_from_conflicts(
self, pkg_name: str, *, conflicts, kind: RequirementKind
) -> List[RequirementRule]:
result = []
kind, conflicts = self._raw_yaml_data(pkg, section="conflict")
for item in conflicts:
spec, condition, message = self._parse_prefer_conflict_item(item)
result.append(
@ -107,7 +123,7 @@ def rules_from_conflict(self, pkg: spack.package_base.PackageBase) -> List[Requi
# require:
# - one_of: [spec_str, "@:"]
RequirementRule(
pkg_name=pkg.name,
pkg_name=pkg_name,
policy="one_of",
requirements=[spec, spack.spec.Spec("@:")],
kind=kind,
@ -129,10 +145,14 @@ def _parse_prefer_conflict_item(self, item):
message = item.get("message")
return spec, condition, message
def _raw_yaml_data(self, pkg: spack.package_base.PackageBase, *, section: str):
def _raw_yaml_data(self, pkg_name: str, *, section: str, virtual: bool = False):
config = self.config.get("packages")
data = config.get(pkg.name, {}).get(section, [])
data = config.get(pkg_name, {}).get(section, [])
kind = RequirementKind.PACKAGE
if virtual:
return RequirementKind.VIRTUAL, data
if not data:
data = config.get("all", {}).get(section, [])
kind = RequirementKind.DEFAULT

View File

@ -377,11 +377,14 @@ def test_require_cflags(concretize_scope, mock_packages):
"""
update_packages_config(conf_str)
spec_mpich2 = spack.concretize.concretize_one("mpich2")
assert spec_mpich2.satisfies("cflags=-g")
mpich2 = spack.concretize.concretize_one("mpich2")
assert mpich2.satisfies("cflags=-g")
spec_mpi = spack.concretize.concretize_one("mpi")
assert spec_mpi.satisfies("mpich cflags=-O1")
mpileaks = spack.concretize.concretize_one("mpileaks")
assert mpileaks["mpi"].satisfies("mpich cflags=-O1")
mpi = spack.concretize.concretize_one("mpi")
assert mpi.satisfies("mpich cflags=-O1")
def test_requirements_for_package_that_is_not_needed(concretize_scope, test_repo):
@ -982,6 +985,52 @@ def test_requiring_package_on_multiple_virtuals(concretize_scope, mock_packages)
["%clang"],
["%gcc"],
),
# Test using preferences on virtuals
(
"""
packages:
all:
providers:
mpi: [mpich]
mpi:
prefer:
- zmpi
""",
"mpileaks",
["^[virtuals=mpi] zmpi"],
["^[virtuals=mpi] mpich"],
),
(
"""
packages:
all:
providers:
mpi: [mpich]
mpi:
prefer:
- zmpi
""",
"mpileaks ^[virtuals=mpi] mpich",
["^[virtuals=mpi] mpich"],
["^[virtuals=mpi] zmpi"],
),
# Tests that strong preferences can be overridden by requirements
(
"""
packages:
all:
providers:
mpi: [zmpi]
mpi:
require:
- mpich
prefer:
- zmpi
""",
"mpileaks",
["^[virtuals=mpi] mpich"],
["^[virtuals=mpi] zmpi"],
),
],
)
def test_strong_preferences_packages_yaml(
@ -1032,6 +1081,16 @@ def test_strong_preferences_packages_yaml(
""",
"multivalue-variant@=2.3 %clang",
),
# Test using conflict on virtual
(
"""
packages:
mpi:
conflict:
- mpich
""",
"mpileaks ^[virtuals=mpi] mpich",
),
],
)
def test_conflict_packages_yaml(packages_yaml, spec_str, concretize_scope, mock_packages):
@ -1168,3 +1227,26 @@ def test_anonymous_spec_cannot_be_used_in_virtual_requirements(
update_packages_config(packages_yaml)
with pytest.raises(spack.error.SpackError, match=err_match):
spack.concretize.concretize_one("mpileaks")
def test_virtual_requirement_respects_any_of(concretize_scope, mock_packages):
"""Tests that "any of" requirements can be used with virtuals"""
conf_str = """\
packages:
mpi:
require:
- any_of: ["mpich2", "mpich"]
"""
update_packages_config(conf_str)
s = spack.concretize.concretize_one("mpileaks")
assert s.satisfies("^[virtuals=mpi] mpich2")
s = spack.concretize.concretize_one("mpileaks ^mpich2")
assert s.satisfies("^[virtuals=mpi] mpich2")
s = spack.concretize.concretize_one("mpileaks ^mpich")
assert s.satisfies("^[virtuals=mpi] mpich")
with pytest.raises(spack.error.SpackError):
spack.concretize.concretize_one("mpileaks ^[virtuals=mpi] zmpi")