Repository: spack/spack

Issue:
`spack debug report` states no builtin repository available for local repositories
### Steps to reproduce

```console
$ spack debug report
* **Spack:** 1.0.0.dev0 (a40b98368c75dc1af94b48799f14e7bee57fca68)
* **Builtin repo:** not available
* **Python:** 3.13.3
* **Platform:** darwin-sequoia-m1
```

### Error message

Reporting that `builtin` is `not available` may be confusing if someone is using a locally configured `builtin` repository.

### Information on your system

* **Spack:** 1.0.0.dev0 (a40b98368c75dc1af94b48799f14e7bee57fca68)
* **Builtin repo:** not available
* **Python:** 3.13.3
* **Platform:** darwin-sequoia-m1

### 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
~- [ ] I have run the failing commands in debug mode and reported the output~

Discussion:

Solution patch:
diff --git a/.github/workflows/requirements/style/requirements.txt b/.github/workflows/requirements/style/requirements.txt
index 2a3b9048d1..aec235b815 100644
--- a/.github/workflows/requirements/style/requirements.txt
+++ b/.github/workflows/requirements/style/requirements.txt
@@ -2,7 +2,7 @@ black==25.1.0
 clingo==5.8.0
 flake8==7.2.0
 isort==6.0.1
-mypy==1.15.0
+mypy==1.16.1
 types-six==1.17.0.20250515
 vermin==1.6.0
 pylint==3.3.7
diff --git a/lib/spack/docs/requirements.txt b/lib/spack/docs/requirements.txt
index f3893d4e0c..c716c19bb8 100644
--- a/lib/spack/docs/requirements.txt
+++ b/lib/spack/docs/requirements.txt
@@ -11,4 +11,4 @@ pytest==8.3.5
 isort==6.0.1
 black==25.1.0
 flake8==7.2.0
-mypy==1.11.1
+mypy==1.16.1
diff --git a/lib/spack/spack/binary_distribution.py b/lib/spack/spack/binary_distribution.py
index 217435a1d0..206fce33e0 100644
--- a/lib/spack/spack/binary_distribution.py
+++ b/lib/spack/spack/binary_distribution.py
@@ -19,6 +19,7 @@
 import sys
 import tarfile
 import tempfile
+import textwrap
 import time
 import urllib.error
 import urllib.parse
@@ -266,6 +267,26 @@ def _associate_built_specs_with_mirror(self, cache_key, url_and_version: MirrorU
                         # recreate index if it is missing
                         cache_entry = self._local_index_cache[str(url_and_version)]
                         self._fetch_and_cache_index(url_and_version, cache_entry)
+
+                    if os.path.getsize(cache_file_path) == 0:
+                        lines = textwrap.wrap(
+                            f"Buildcache index for the v{layout_version} mirror "
+                            f"at '{mirror_url}' is empty. "
+                            "If the mirror layout is deprecated and you cannot migrate "
+                            "it to the new format, consider removing it using:",
+                            width=72,
+                            subsequent_indent="  ",
+                        )
+                        lines.extend(
+                            [
+                                "    'spack mirror list'",
+                                "    'spack mirror remove <name>'",
+                                "  with the <name> for the mirror url shown in the list.",
+                            ]
+                        )
+                        tty.warn("\n".join(lines))
+                        return
+
                     db._read_from_file(cache_file_path)
             except spack_db.InvalidDatabaseVersionError as e:
                 tty.warn(
@@ -676,12 +697,24 @@ def buildcache_relative_index_url(layout_version: int = CURRENT_BUILD_CACHE_LAYO
 
 @llnl.util.lang.memoized
 def warn_v2_layout(mirror_url: str, action: str) -> bool:
-    tty.warn(
-        f"{action} from a v2 binary mirror layout, located at \n"
-        f"    {mirror_url} is deprecated. Support for this will be \n"
-        "    removed in a future version of spack. Please consider running `spack \n"
-        "    buildcache migrate' or rebuilding the specs in this mirror."
+    lines = textwrap.wrap(
+        f"{action} from a v2 binary mirror layout, located at "
+        f"{mirror_url} is deprecated. Support for this will be "
+        "removed in a future version of spack. "
+        "If you manage the buildcache please consider running:",
+        width=72,
+        subsequent_indent="  ",
+    )
+    lines.extend(
+        [
+            "    'spack buildcache migrate'",
+            "  or rebuilding the specs in this mirror. Otherwise, consider running:",
+            "    'spack mirror list'",
+            "    'spack mirror remove <name>'",
+            "  with the <name> for the mirror url shown in the list.",
+        ]
     )
+    tty.warn("\n".join(lines))
     return True
 
 
diff --git a/lib/spack/spack/bootstrap/config.py b/lib/spack/spack/bootstrap/config.py
index eed77066df..f1e8080e5e 100644
--- a/lib/spack/spack/bootstrap/config.py
+++ b/lib/spack/spack/bootstrap/config.py
@@ -117,8 +117,11 @@ def _read_and_sanitize_configuration() -> Dict[str, Any]:
     # to have it in the configuration).
     config_yaml = spack.config.get("config")
     config_yaml.pop("install_tree", None)
-    user_configuration = {"bootstrap": spack.config.get("bootstrap"), "config": config_yaml}
-    return user_configuration
+    return {
+        "bootstrap": spack.config.get("bootstrap"),
+        "config": config_yaml,
+        "repos": spack.config.get("repos"),
+    }
 
 
 def _bootstrap_config_scopes() -> Sequence["spack.config.ConfigScope"]:
@@ -155,10 +158,11 @@ def _ensure_bootstrap_configuration() -> Generator:
     ), spack.store.use_store(
         bootstrap_store_path, extra_data={"padded_length": 0}
     ):
+        spack.config.set("bootstrap", user_configuration["bootstrap"])
+        spack.config.set("config", user_configuration["config"])
+        spack.config.set("repos", user_configuration["repos"])
         # We may need to compile code from sources, so ensure we
         # have compilers for the current platform
         _add_compilers_if_missing()
-        spack.config.set("bootstrap", user_configuration["bootstrap"])
-        spack.config.set("config", user_configuration["config"])
         with spack.modules.disable_modules(), spack_python_interpreter():
             yield
diff --git a/lib/spack/spack/ci/__init__.py b/lib/spack/spack/ci/__init__.py
index 7f786094b4..5f6598fc06 100644
--- a/lib/spack/spack/ci/__init__.py
+++ b/lib/spack/spack/ci/__init__.py
@@ -268,7 +268,7 @@ def rebuild_filter(s: spack.spec.Spec) -> RebuildDecision:
 
 def _format_pruning_message(spec: spack.spec.Spec, prune: bool, reasons: List[str]) -> str:
     reason_msg = ", ".join(reasons)
-    spec_fmt = "{name}{@version}{/hash:7}{%compiler}"
+    spec_fmt = "{name}{@version}{/hash:7}{compilers}"
 
     if not prune:
         status = colorize("@*g{[x]}  ")
@@ -520,9 +520,7 @@ def generate_pipeline(env: ev.Environment, args) -> None:
     # Use all unpruned specs to populate the build group for this set
     cdash_config = cfg.get("cdash")
     if options.cdash_handler and options.cdash_handler.auth_token:
-        options.cdash_handler.populate_buildgroup(
-            [options.cdash_handler.build_name(s) for s in pipeline_specs]
-        )
+        options.cdash_handler.create_buildgroup()
     elif cdash_config:
         # warn only if there was actually a CDash configuration.
         tty.warn("Unable to populate buildgroup without CDash credentials")
diff --git a/lib/spack/spack/ci/common.py b/lib/spack/spack/ci/common.py
index ecef706ad1..b91317fdf5 100644
--- a/lib/spack/spack/ci/common.py
+++ b/lib/spack/spack/ci/common.py
@@ -284,56 +284,33 @@ def copy_test_results(self, source, dest):
         reports = fs.join_path(source, "*_Test*.xml")
         copy_files_to_artifacts(reports, dest)
 
-    def create_buildgroup(self, headers, url, group_name, group_type):
-        data = {"newbuildgroup": group_name, "project": self.project, "type": group_type}
-
-        enc_data = json.dumps(data).encode("utf-8")
-
-        request = Request(url, data=enc_data, headers=headers)
-
-        try:
-            response_text = _urlopen(request, timeout=SPACK_CDASH_TIMEOUT).read()
-        except OSError as e:
-            tty.warn(f"Failed to create CDash buildgroup: {e}")
-            return None
-
-        try:
-            response_json = json.loads(response_text)
-            return response_json["id"]
-        except (json.JSONDecodeError, KeyError) as e:
-            tty.warn(f"Failed to parse CDash response: {e}")
-            return None
-
-    def populate_buildgroup(self, job_names):
-        url = f"{self.url}/api/v1/buildgroup.php"
-
+    def create_buildgroup(self):
+        """Create the CDash buildgroup if it does not already exist."""
         headers = {
             "Authorization": f"Bearer {self.auth_token}",
             "Content-Type": "application/json",
         }
-
-        parent_group_id = self.create_buildgroup(headers, url, self.build_group, "Daily")
-        group_id = self.create_buildgroup(headers, url, f"Latest {self.build_group}", "Latest")
-
-        if not parent_group_id or not group_id:
-            tty.warn(f"Failed to create or retrieve buildgroups for {self.build_group}")
-            return
-
-        data = {
-            "dynamiclist": [
-                {"match": name, "parentgroupid": parent_group_id, "site": self.site}
-                for name in job_names
-            ]
-        }
-
+        data = {"newbuildgroup": self.build_group, "project": self.project, "type": "Daily"}
         enc_data = json.dumps(data).encode("utf-8")
+        request = Request(f"{self.url}/api/v1/buildgroup.php", data=enc_data, headers=headers)
 
-        request = Request(url, data=enc_data, headers=headers, method="PUT")
+        response_text = None
+        group_id = None
 
         try:
-            _urlopen(request, timeout=SPACK_CDASH_TIMEOUT)
+            response_text = _urlopen(request, timeout=SPACK_CDASH_TIMEOUT).read()
         except OSError as e:
-            tty.warn(f"Failed to populate CDash buildgroup: {e}")
+            tty.warn(f"Failed to create CDash buildgroup: {e}")
+
+        if response_text:
+            try:
+                response_json = json.loads(response_text)
+                group_id = response_json["id"]
+            except (json.JSONDecodeError, KeyError) as e:
+                tty.warn(f"Failed to parse CDash response: {e}")
+
+        if not group_id:
+            tty.warn(f"Failed to create or retrieve buildgroup for {self.build_group}")
 
     def report_skipped(self, spec: spack.spec.Spec, report_dir: str, reason: Optional[str]):
         """Explicitly report skipping testing of a spec (e.g., it's CI
diff --git a/lib/spack/spack/cmd/debug.py b/lib/spack/spack/cmd/debug.py
index 4ea59d1df5..d7e711631d 100644
--- a/lib/spack/spack/cmd/debug.py
+++ b/lib/spack/spack/cmd/debug.py
@@ -25,19 +25,32 @@ def setup_parser(subparser: argparse.ArgumentParser) -> None:
     sp.add_parser("report", help="print information useful for bug reports")
 
 
-def _get_builtin_repo_commit() -> Optional[str]:
+def _format_repo_info(source, commit):
+    if source.endswith(".git"):
+        return f"{source[:-4]}/commit/{commit}"
+
+    return f"{source} ({commit[:7]})"
+
+
+def _get_builtin_repo_info() -> Optional[str]:
     """Get the builtin package repository git commit sha."""
     # Get builtin from config
     descriptors = spack.repo.RepoDescriptors.from_config(
         spack.repo.package_repository_lock(), spack.config.CONFIG
     )
-
     if "builtin" not in descriptors:
         return None
 
     builtin = descriptors["builtin"]
 
-    if not isinstance(builtin, spack.repo.RemoteRepoDescriptor) or not builtin.fetched():
+    source = None
+    if isinstance(builtin, spack.repo.RemoteRepoDescriptor) and builtin.fetched():
+        destination = builtin.destination
+        source = builtin.repository
+    elif isinstance(builtin, spack.repo.LocalRepoDescriptor):
+        destination = builtin.path
+        source = builtin.path
+    else:
         return None  # no git info
 
     git = spack.util.git.git(required=False)
@@ -45,19 +58,23 @@ def _get_builtin_repo_commit() -> Optional[str]:
         return None
 
     rev = git(
-        "-C",
-        builtin.destination,
-        "rev-parse",
-        "HEAD",
-        output=str,
-        error=os.devnull,
-        fail_on_error=False,
+        "-C", destination, "rev-parse", "HEAD", output=str, error=os.devnull, fail_on_error=False
     )
     if git.returncode != 0:
         return None
 
     match = re.match(r"[a-f\d]{7,}$", rev)
-    return match.group(0) if match else None
+    return _format_repo_info(source, match.group(0)) if match else None
+
+
+def _get_spack_repo_info() -> str:
+    """Get the spack package repository git info."""
+    commit = spack.get_spack_commit()
+    if not commit:
+        return spack.spack_version
+
+    repo_info = _format_repo_info("https://github.com/spack/spack.git", commit)
+    return f"{spack.spack_version} ({repo_info})"
 
 
 def report(args):
@@ -65,8 +82,8 @@ def report(args):
     host_os = host_platform.default_operating_system()
     host_target = host_platform.default_target()
     architecture = spack.spec.ArchSpec((str(host_platform), str(host_os), str(host_target)))
-    print("* **Spack:**", spack.get_version())
-    print("* **Builtin repo:**", _get_builtin_repo_commit() or "not available")
+    print("* **Spack:**", _get_spack_repo_info())
+    print("* **Builtin repo:**", _get_builtin_repo_info() or "not available")
     print("* **Python:**", platform.python_version())
     print("* **Platform:**", architecture)
 
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index dc66b21e67..ef9c7bdce4 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -2160,9 +2160,13 @@ def _format_dependencies(
         """
         include = include or (lambda dep: True)
         parts = []
-        direct, transitive = lang.stable_partition(
-            self.edges_to_dependencies(), predicate_fn=lambda x: x.direct
-        )
+        if self.concrete:
+            direct = self.edges_to_dependencies()
+            transitive: List[DependencySpec] = []
+        else:
+            direct, transitive = lang.stable_partition(
+                self.edges_to_dependencies(), predicate_fn=lambda x: x.direct
+            )
 
         # helper for direct and transitive loops below
         def format_edge(edge, sigil, dep_spec=None):
@@ -2195,6 +2199,11 @@ def format_edge(edge, sigil, dep_spec=None):
             finally:
                 edge.spec.name = old_name
 
+        if self.concrete:
+            # Concrete specs should go no further, as the complexity
+            # below is O(paths)
+            return " ".join(parts).strip()
+
         # transitive dependencies (with any direct dependencies)
         for edge in sorted(transitive, key=lambda x: x.spec.name):
             if not include(edge):
@@ -2228,6 +2237,8 @@ def compilers(self):
     @property
     def long_spec(self):
         """Returns a string of the spec with the dependencies completely enumerated."""
+        if self.concrete:
+            return self.tree(format=DISPLAY_FORMAT)
         return f"{self.format()} {self._format_dependencies()}".strip()
 
     @property
diff --git a/lib/spack/spack/util/path.py b/lib/spack/spack/util/path.py
index f5c6bc1833..6199fca849 100644
--- a/lib/spack/spack/util/path.py
+++ b/lib/spack/spack/util/path.py
@@ -275,7 +275,7 @@ def canonicalize_path(path: str, default_wd: Optional[str] = None) -> str:
     if url.scheme:
         if url.scheme != "file":
             # Have a remote URL so simply return it with substitutions
-            return os.path.normpath(path)
+            return path
 
         # Drop the URL scheme from the local path
         path = url_path


Test patch:
diff --git a/lib/spack/spack/test/cmd/buildcache.py b/lib/spack/spack/test/cmd/buildcache.py
index a5e669821f..4339aeeb2f 100644
--- a/lib/spack/spack/test/cmd/buildcache.py
+++ b/lib/spack/spack/test/cmd/buildcache.py
@@ -611,7 +611,8 @@ def test_install_v2_layout(
     assert "Extracting libdwarf" in output
     assert "libdwarf: Successfully installed" in output
     assert "Installing a spec from a v2 binary mirror layout" in output
-    assert "is deprecated" in output
+    assert "Fetching an index from a v2 binary mirror layout" in output
+    assert "deprecated" in output
 
 
 def test_basic_migrate_unsigned(capsys, v2_buildcache_layout, mutable_config):
diff --git a/lib/spack/spack/test/cmd/ci.py b/lib/spack/spack/test/cmd/ci.py
index 69cfc4a27f..2a46dd39c6 100644
--- a/lib/spack/spack/test/cmd/ci.py
+++ b/lib/spack/spack/test/cmd/ci.py
@@ -237,13 +237,19 @@ def test_ci_generate_with_cdash_token(ci_generate_test, tmp_path, mock_binary_in
     project: Not used
     site: Nothing
 """
+
+    def _urlopen(*args, **kwargs):
+        return MockHTTPResponse.with_json(200, "OK", headers={}, body={})
+
+    monkeypatch.setattr(ci.common, "_urlopen", _urlopen)
+
     spack_yaml, original_file, output = ci_generate_test(spack_yaml_content)
     yaml_contents = syaml.load(original_file.read_text())
 
     # That fake token should have resulted in being unable to
     # register build group with cdash, but the workload should
     # still have been generated.
-    assert "Failed to create or retrieve buildgroups" in output
+    assert "Failed to create or retrieve buildgroup" in output
     expected_keys = ["rebuild-index", "stages", "variables", "workflow"]
     assert all([key in yaml_contents.keys() for key in expected_keys])
 
diff --git a/lib/spack/spack/test/cmd/debug.py b/lib/spack/spack/test/cmd/debug.py
index 8c11e50881..389adb2bb0 100644
--- a/lib/spack/spack/test/cmd/debug.py
+++ b/lib/spack/spack/test/cmd/debug.py
@@ -5,9 +5,12 @@
 import platform
 
 import spack
+import spack.cmd.debug
 import spack.platforms
+import spack.repo
 import spack.spec
 from spack.main import SpackCommand
+from spack.test.conftest import _return_none
 
 debug = SpackCommand("debug")
 
@@ -19,6 +22,57 @@ def test_report():
     host_target = host_platform.default_target()
     architecture = spack.spec.ArchSpec((str(host_platform), str(host_os), str(host_target)))
 
-    assert spack.get_version() in out
+    assert spack.spack_version in out
+    assert spack.get_spack_commit() in out
     assert platform.python_version() in out
     assert str(architecture) in out
+
+
+def test_get_builtin_repo_info_local_repo(mock_git_version_info, monkeypatch):
+    """Confirm local git repo descriptor returns expected path."""
+    path = mock_git_version_info[0]
+
+    def _from_config(*args, **kwargs):
+        return {"builtin": spack.repo.LocalRepoDescriptor("builtin", path)}
+
+    monkeypatch.setattr(spack.repo.RepoDescriptors, "from_config", _from_config)
+    assert path in spack.cmd.debug._get_builtin_repo_info()
+
+
+def test_get_builtin_repo_info_unsupported_type(mock_git_version_info, monkeypatch):
+    """Confirm None is return if the 'builtin' repo descriptor's type is unsupported."""
+
+    def _from_config(*args, **kwargs):
+        path = mock_git_version_info[0]
+        return {"builtin": path}
+
+    monkeypatch.setattr(spack.repo.RepoDescriptors, "from_config", _from_config)
+    assert spack.cmd.debug._get_builtin_repo_info() is None
+
+
+def test_get_builtin_repo_info_no_builtin(monkeypatch):
+    """Confirm None is return if there is no 'builtin' repo descriptor."""
+
+    def _from_config(*args, **kwargs):
+        return {"local": "/assumes/no/descriptor/needed"}
+
+    monkeypatch.setattr(spack.repo.RepoDescriptors, "from_config", _from_config)
+    assert spack.cmd.debug._get_builtin_repo_info() is None
+
+
+def test_get_builtin_repo_info_bad_destination(mock_git_version_info, monkeypatch):
+    """Confirm git failure of a repository returns None."""
+
+    def _from_config(*args, **kwargs):
+        path = mock_git_version_info[0]
+        return {"builtin": spack.repo.LocalRepoDescriptor("builtin", f"{path}/missing")}
+
+    monkeypatch.setattr(spack.repo.RepoDescriptors, "from_config", _from_config)
+    assert spack.cmd.debug._get_builtin_repo_info() is None
+
+
+def test_get_spack_repo_info_no_commit(monkeypatch):
+    """Confirm the version is returned if there is no spack commit."""
+
+    monkeypatch.setattr(spack, "get_spack_commit", _return_none)
+    assert spack.cmd.debug._get_spack_repo_info() == spack.spack_version
diff --git a/lib/spack/spack/test/concretization/requirements.py b/lib/spack/spack/test/concretization/requirements.py
index 92a773c474..e193c07fb6 100644
--- a/lib/spack/spack/test/concretization/requirements.py
+++ b/lib/spack/spack/test/concretization/requirements.py
@@ -1332,3 +1332,15 @@ def test_requirements_conditional_deps(
 
     assert requirements.satisfies(required_spec)
     assert (requirements == no_requirements) == req_is_noop  # show the reqs change concretization
+
+
+@pytest.mark.regression("50898")
+def test_preferring_compilers_can_be_overridden(mutable_config, mock_packages):
+    """Tests that we can override preferences for languages, without triggering an error."""
+    mutable_config.set("packages:c", {"prefer": ["llvm"]})
+
+    s = spack.spec.Spec("pkg-a %gcc ^pkg-b %llvm")
+    concrete = spack.concretize.concretize_one(s)
+
+    assert concrete.satisfies("%c=gcc")
+    assert concrete["pkg-b"].satisfies("%c=llvm")