Repository: spack/spack

Issue:
Cannot filter external specs using `reuse:from`
### Steps to reproduce

Originally reported by @pranav-sivaraman on Slack.

The following environment fails to concretize:
```yaml
spack:
  specs:
  - flap
  view: true
  concretizer:
    unify: true
    reuse:
      from:
      - type: external
        include:
        - "cmake"
  packages:
    cmake:
      externals:
      - spec: cmake@3.20.4
        prefix: /usr
```

### Error message

With the environment above active:
```console
$ spack solve
==> Error: Spack concretizer internal error. Please submit a bug report and include the command, environment if applicable and the following error message.
    flap is unsatisfiable
```

### Information on your system

* **Spack:** 0.22.0.dev0 (05c1e7ecc22b873c3e534aef45774c88521fe7b3)
* **Python:** 3.11.5
* **Platform:** linux-ubuntu20.04-icelake
* **Concretizer:** clingo

The error is due to a logical merge conflict between the reuse PR and the `libc` PR. `libc` providers are not implicitly included in the reuse filter, but they are implicitly added to the external configuration.

### 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:
This diff fixes the issue, and makes the external selected by default even if deprecated:
```diff
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 0d84c53970..3c8f385a29 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -1649,11 +1649,13 @@ def external_packages(self):
         if isinstance(reuse_yaml, typing.Mapping):
             default_include = reuse_yaml.get("include", [])
             default_exclude = reuse_yaml.get("exclude", [])
+            libc_externals = list(all_libcs())
             for source in reuse_yaml.get("from", []):
                 if source["type"] != "external":
                     continue
 
-                include = source.get("include", default_include)
+                # Since libcs are implicit externals, we need to implicitly include them
+                include = source.get("include", default_include) + libc_externals
                 exclude = source.get("exclude", default_exclude)
                 spec_filters.append(
                     SpecFilter(
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 85cc697a2d..60165338c6 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -1424,6 +1424,7 @@ opt_criterion(73, "deprecated versions used").
 #minimize{
     1@73+Priority,PackageNode
     : attr("deprecated", PackageNode, _),
+      not external(PackageNode),
       build_priority(PackageNode, Priority)
 }.
```


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
@@ -1649,11 +1649,15 @@ def external_packages(self):
         if isinstance(reuse_yaml, typing.Mapping):
             default_include = reuse_yaml.get("include", [])
             default_exclude = reuse_yaml.get("exclude", [])
+            libc_externals = list(all_libcs())
             for source in reuse_yaml.get("from", []):
                 if source["type"] != "external":
                     continue
 
                 include = source.get("include", default_include)
+                if include:
+                    # Since libcs are implicit externals, we need to implicitly include them
+                    include = include + libc_externals
                 exclude = source.get("exclude", default_exclude)
                 spec_filters.append(
                     SpecFilter(
diff --git a/var/spack/repos/builtin.mock/packages/deprecated-client/package.py b/var/spack/repos/builtin.mock/packages/deprecated-client/package.py
new file mode 100644
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/deprecated-client/package.py
@@ -0,0 +1,16 @@
+# 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 DeprecatedClient(Package):
+    """A package depending on another which has deprecated versions."""
+
+    homepage = "http://www.example.com"
+    url = "http://www.example.com/c-1.0.tar.gz"
+
+    version("1.1.0", sha256="abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+
+    depends_on("deprecated-versions")


Test patch:
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -2464,6 +2464,7 @@ def test_spec_with_build_dep_from_json(self, tmp_path):
         assert s["dttop"].dag_hash() == build_dep.dag_hash()
 
     @pytest.mark.regression("44040")
+    @pytest.mark.only_clingo("Use case not supported by the original concretizer")
     def test_exclude_specs_from_reuse(self, monkeypatch):
         """Tests that we can exclude a spec from reuse when concretizing, and that the spec
         is not added back to the solve as a dependency of another reusable spec.
@@ -2503,6 +2504,48 @@ def test_exclude_specs_from_reuse(self, monkeypatch):
         for dep in result["dyninst"].traverse(root=False):
             assert dep.dag_hash() == reused[dep.name].dag_hash()
 
+    @pytest.mark.regression("44091")
+    @pytest.mark.parametrize(
+        "included_externals",
+        [
+            ["deprecated-versions"],
+            # Try the empty list, to ensure that in that case everything will be included
+            # since filtering should happen only when the list is non-empty
+            [],
+        ],
+    )
+    @pytest.mark.only_clingo("Use case not supported by the original concretizer")
+    def test_include_specs_from_externals_and_libcs(
+        self, included_externals, mutable_config, tmp_path
+    ):
+        """Tests that when we include specs from externals, we always include libcs."""
+        mutable_config.set(
+            "packages",
+            {
+                "deprecated-versions": {
+                    "externals": [{"spec": "deprecated-versions@1.1.0", "prefix": str(tmp_path)}]
+                }
+            },
+        )
+        request_str = "deprecated-client"
+
+        # When using the external the version is selected even if deprecated
+        with spack.config.override(
+            "concretizer:reuse", {"from": [{"type": "external", "include": included_externals}]}
+        ):
+            result = Spec(request_str).concretized()
+
+        assert result["deprecated-versions"].satisfies("@1.1.0")
+
+        # When excluding it, we pick the non-deprecated version
+        with spack.config.override(
+            "concretizer:reuse",
+            {"from": [{"type": "external", "exclude": ["deprecated-versions"]}]},
+        ):
+            result = Spec(request_str).concretized()
+
+        assert result["deprecated-versions"].satisfies("@1.0.0")
+
 
 @pytest.fixture()
 def duplicates_test_repository():