Repository: spack/spack

Issue:
Requirements ignored for a package providing multiple virtuals
### Steps to reproduce

Create an environment for `dla-future+scalapack` and require that all BLAS, LAPACK and SCALAPACK providers are provided by `intel-oneapi-mkl`.

```console
spack env create -d .
spack -e . add dla-future+scalapack
spack -e . config add 'packages:blas:require:intel-oneapi-mkl'
spack -e . config add 'packages:lapack:require:intel-oneapi-mkl'
spack -e . config add 'packages:scalapack:require:intel-oneapi-mkl'
spack -e . concretize
```

### Error message

The concretization shows

```console
[+]  tna32ut      ^intel-oneapi-mkl@2024.0.0%gcc@11.2.0~cluster+envmods~ilp64+shared build_system=generic mpi_family=none threads=none arch=cray-cnl7-haswell
[+]  ttde4dn          ^intel-tbb@2021.9.0%gcc@11.2.0~ipo+shared+tm build_system=cmake build_type=Release cxxstd=default generator=make patches=91755c6 arch=cray-cnl7-haswell
[+]  26ocg3c      ^lapackpp@2023.08.25%gcc@11.2.0~cuda~ipo~rocm+shared~sycl build_system=cmake build_type=Release generator=make arch=cray-cnl7-haswell
 -   iycxciq      ^netlib-scalapack@2.2.0%gcc@11.2.0~ipo~pic+shared build_system=cmake build_type=Release generator=make patches=072b006,1c9ce5f,244a9aa arch=cray-cnl7-haswell
```

which is kind of unexpected, since it break `require` clause silently.


### Additional notes
- I tried with another package and the right provider is selected
- Adding `conflicts` for `netlib-scalapack` in `dla-future`, make the concretizer selecting the correct provider (without any other change)

### Information on your system

* **Spack:** 0.22.0.dev0 (5f58a4c0792071ca4e681e43724fe7c66c2cefce)
* **Python:** 3.6.15
* **Platform:** cray-sles15-haswell
* **Concretizer:** clingo

### General information

- [X] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [X] I have searched the issues of this repo and believe this is not a duplicate
- [X] I have run the failing commands in debug mode and reported the output


Discussion:
Confirmed. Thanks for the reproducer.
The current facts that are emitted for the solver are:
![Screenshot from 2024-01-15 11-22-01](https://github.com/spack/spack/assets/4199709/ba28092c-8d6e-4d57-8a4f-6885c2905757)

These are probably too coarse when the node is also used as a provider of something else, since there is no connection between the `virtual_node` and the "imposed" node condition. We likely need to add something to connect the two.
This diff seems to work:
```diff
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 0f38331412..6881b51f27 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -1740,9 +1740,10 @@ def provider_requirements(self):
             rules = self._rules_from_requirements(
                 virtual_str, requirements, kind=RequirementKind.VIRTUAL
             )
-            self.emit_facts_from_requirement_rules(rules)
-            self.trigger_rules()
-            self.effect_rules()
+            if rules:
+                self.emit_facts_from_requirement_rules(rules)
+                self.trigger_rules()
+                self.effect_rules()
 
     def emit_facts_from_requirement_rules(self, rules: List[RequirementRule]):
         """Generate facts to enforce requirements.
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 21cdbbf848..3eef52022b 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -695,6 +695,18 @@ requirement_group_satisfied(node(ID, Package), X) :-
   activate_requirement(node(ID, Package), X),
   requirement_group(Package, X).
 
+% When we have a required provider, we need to ensure that the provider/2 facts respect
+% the requirement. This is particularly important for packages that could provide multiple
+% virtuals independently
+required_provider(Provider, Virtual)
+  :- requirement_group_member(ConditionID, Virtual, RequirementID),
+     condition_holds(ConditionID, _),
+     virtual(Virtual),
+     pkg_fact(Virtual, condition_effect(ConditionID, EffectID)),
+     imposed_constraint(EffectID, "node", Provider).
+
+:- provider(node(Y, Package), node(X, Virtual)), required_provider(Provider, Virtual), Package != Provider.
+
 % TODO: the following two choice rules allow the solver to add compiler
 % flags if their only source is from a requirement. This is overly-specific
 % and should use a more-generic approach like in https://github.com/spack/spack/pull/37180
```
I'll add a regression test and submit a PR.

Resolving patch:
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -1740,9 +1740,10 @@ def provider_requirements(self):
             rules = self._rules_from_requirements(
                 virtual_str, requirements, kind=RequirementKind.VIRTUAL
             )
-            self.emit_facts_from_requirement_rules(rules)
-            self.trigger_rules()
-            self.effect_rules()
+            if rules:
+                self.emit_facts_from_requirement_rules(rules)
+                self.trigger_rules()
+                self.effect_rules()
 
     def emit_facts_from_requirement_rules(self, rules: List[RequirementRule]):
         """Generate facts to enforce requirements.
diff --git a/var/spack/repos/builtin.mock/packages/dla-future/package.py b/var/spack/repos/builtin.mock/packages/dla-future/package.py
new file mode 100644
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/dla-future/package.py
@@ -0,0 +1,21 @@
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+from spack.package import *
+
+
+class DlaFuture(Package):
+    """A package that depends on 3 different virtuals, that might or might not be provided
+    by the same node.
+    """
+
+    homepage = "http://www.example.com"
+    url = "http://www.example.com/dla-1.0.tar.gz"
+
+    version("1.0", md5="0123456789abcdef0123456789abcdef")
+
+    depends_on("blas")
+    depends_on("lapack")
+    depends_on("scalapack")


Test patch:
diff --git a/lib/spack/spack/test/concretize_requirements.py b/lib/spack/spack/test/concretize_requirements.py
--- a/lib/spack/spack/test/concretize_requirements.py
+++ b/lib/spack/spack/test/concretize_requirements.py
@@ -1012,3 +1012,26 @@ def test_default_requirements_semantic_with_mv_variants(
 
     for constraint in not_expected:
         assert not s.satisfies(constraint), constraint
+
+
+@pytest.mark.regression("42084")
+def test_requiring_package_on_multiple_virtuals(concretize_scope, mock_packages):
+    update_packages_config(
+        """
+    packages:
+      all:
+        providers:
+          scalapack: [netlib-scalapack]
+      blas:
+        require: intel-parallel-studio
+      lapack:
+        require: intel-parallel-studio
+      scalapack:
+        require: intel-parallel-studio
+    """
+    )
+    s = Spec("dla-future").concretized()
+
+    assert s["blas"].name == "intel-parallel-studio"
+    assert s["lapack"].name == "intel-parallel-studio"
+    assert s["scalapack"].name == "intel-parallel-studio"