Repository: spack/spack

Issue:
"spack buildcache list" prints the same spec multiple times
When using more than one constraint to list specs in a binary mirror, `spack buildcache list` shows the same spec multiple times base on the number of constraints it satisfies.


### Steps to reproduce the issue
Create a binary mirror with at least one package, for instance:
```console
$ spack buildcache list
==> Finding buildcaches in /home/culpo/.mirror/build_cache
==> Finding buildcaches in /home/culpo/production/spack/binary-mirror/build_cache
==> Finding buildcaches in /home/culpo/production/spack/mirror/build_cache
-- linux-ubuntu18.04-broadwell / gcc@9.0.1 ----------------------
zlib@1.2.11

-- linux-ubuntu18.04-x86_64 / gcc@9.0.1 -------------------------
zlib@1.2.11
```
Then try to use more than on matching constraint:
```console
$ spack buildcache list -l zlib @1.2.11 
==> Finding buildcaches in /home/culpo/.mirror/build_cache
==> Finding buildcaches in /home/culpo/production/spack/binary-mirror/build_cache
==> Finding buildcaches in /home/culpo/production/spack/mirror/build_cache
-- linux-ubuntu18.04-broadwell / gcc@9.0.1 ----------------------
os3djb5 zlib@1.2.11  os3djb5 zlib@1.2.11

-- linux-ubuntu18.04-x86_64 / gcc@9.0.1 -------------------------
tn4qvs7 zlib@1.2.11  tn4qvs7 zlib@1.2.11
```
As the output above shows the same spec is displayed twice.

### Error Message

No error message, just wrong output displayed to `tty`.

### Information on your system

Not relevant.


Discussion:
PR incoming

Resolving patch:
diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py
--- a/lib/spack/spack/cmd/buildcache.py
+++ b/lib/spack/spack/cmd/buildcache.py
@@ -416,11 +416,9 @@ def listspecs(args):
     """list binary packages available from mirrors"""
     specs = bindist.get_specs(args.force)
     if args.packages:
-        pkgs = set(args.packages)
-        specs = [s for s in specs for p in pkgs if s.satisfies(p)]
-        display_specs(specs, args, all_headers=True)
-    else:
-        display_specs(specs, args, all_headers=True)
+        constraints = set(args.packages)
+        specs = [s for s in specs if any(s.satisfies(c) for c in constraints)]
+    display_specs(specs, args, all_headers=True)
 
 
 def getkeys(args):


Test patch:
diff --git a/lib/spack/spack/test/cmd/buildcache.py b/lib/spack/spack/test/cmd/buildcache.py
--- a/lib/spack/spack/test/cmd/buildcache.py
+++ b/lib/spack/spack/test/cmd/buildcache.py
@@ -8,11 +8,19 @@
 import pytest
 
 import spack.main
-
+import spack.binary_distribution
 
 buildcache = spack.main.SpackCommand('buildcache')
 
 
+@pytest.fixture()
+def mock_get_specs(database, monkeypatch):
+    specs = database.query_local()
+    monkeypatch.setattr(
+        spack.binary_distribution, 'get_specs', lambda x: specs
+    )
+
+
 @pytest.mark.skipif(
     platform.system().lower() != 'linux',
     reason='implementation for MacOS still missing'
@@ -20,3 +28,16 @@
 @pytest.mark.db
 def test_buildcache_preview_just_runs(database):
     buildcache('preview', 'mpileaks')
+
+
+@pytest.mark.skipif(
+    platform.system().lower() != 'linux',
+    reason='implementation for MacOS still missing'
+)
+@pytest.mark.db
+@pytest.mark.regression('13757')
+def test_buildcache_list_duplicates(mock_get_specs, capsys):
+    with capsys.disabled():
+        output = buildcache('list', 'mpileaks', '@2.3')
+
+    assert output.count('mpileaks') == 3