#1149705 dh-fortran: FC_DEFAULT=gfortran resolves to hardcoded gfortran-16 instead of active default gfortran

#1149705#5
Date:
2026-10-02 13:48:41 UTC
From:
To:
Dear Maintainer,

In dhfortran/compilers.py, default_compilers["gfortran"] is hardcoded to "gfortran-16":

  default_compilers = {
      "gfortran": "gfortran-16",
      ...
  }

When debhelper packages or environment settings define FC_DEFAULT=gfortran (e.g. debian/rules or build harnesses), get_fc_default() calls canon("gfortran"):

  def canon(f):
      return default_compilers[f] if f in default_compilers else f

  if "FC_DEFAULT" in os.environ:
      return canon(os.environ["FC_DEFAULT"])

Because "gfortran-16" is not yet available and the system default is gfortran-15 (or whatever gfortran package is installed), get_fc_default() returns "gfortran-16". Subsequent calls fail because which("gfortran-16") returns None (for example in fortran-assert with "Compiler gfortran-16 not on PATH").

To fix this:
1. Dynamically detect the active default compiler from which("gfortran") and resolve symlinks (stripping multiarch / DEB_BUILD_GNU_TYPE prefixes so it matches the compiler dictionary key).
2. Fall back to gfortran-15 if available, else gfortran-16.
3. In canon(f), resolve symlinks when f is in default_compilers so canon("gfortran") maps to the target compiler flavor instead of an uninstalled version.
4. In get_f77(), check which("gfortran") before raising an error.

This issue is also tracked in Ubuntu at:
https://bugs.launchpad.net/ubuntu/+source/dh-fortran/+bug/2169224

The patch below resolves this behavior:
--- a/dhfortran/compilers.py
+++ b/dhfortran/compilers.py
@@ -20,9 +20,23 @@
     check_output(["dpkg-architecture", "-qDEB_HOST_MULTIARCH"]).strip().decode("utf-8")
 )

+def _detect_default_compiler(cmd: str, fallback: str) -> str:
+    p = which(cmd)
+    if p is not None:
+        name = Path(p).resolve().name
+        for m in (
+            multiarch,
+            check_output(["dpkg-architecture", "-qDEB_BUILD_GNU_TYPE"]).strip().decode("utf-8"),
+        ):
+            if name.startswith(m + "-"):
+                name = name[len(m) + 1 :]
+        return name
+    return fallback
+
+
 # To be updated as gfortran, flang etc are updated  for new versions
 default_compilers = {
-    "gfortran": "gfortran-16",
+    "gfortran": _detect_default_compiler("gfortran", "gfortran-15" if which("gfortran-15") is not None else "gfortran-16"),
     "flang": "flang-22",
     "flangext": "flang-to-external-fc-18",
 }
@@ -441,7 +455,7 @@
         f = f77 if f77 else "/etc/alternatives/f77"
     fullpath = which(f)
     if fullpath is None:
-        fullpath = which(default_compilers['gfortran'])
+        fullpath = which("gfortran") or which(default_compilers['gfortran'])
     if fullpath is None:
         if f is None:
             f='gfortran' # for better error message
@@ -454,7 +468,22 @@
     """Return the default Fortran compiler"""

     def canon(f):
-        return default_compilers[f] if f in default_compilers else f
+        if f in default_compilers:
+            p = which(f)
+            if p is not None:
+                p_name = Path(p).resolve().name
+                for m in (
+                    multiarch,
+                    check_output(["dpkg-architecture", "-qDEB_BUILD_GNU_TYPE"]).strip().decode("utf-8"),
+                ):
+                    if p_name.startswith(m + "-"):
+                        p_name = p_name[len(m) + 1 :]
+                for c in compilers:
+                    if p_name in compilers[c]["exe"]:
+                        return c
+                return p_name
+            return default_compilers[f]
+        return f
     if "FC_DEFAULT" in os.environ:
         return canon(os.environ["FC_DEFAULT"])
     if fc is not None:
--- a/dhfortran/tests/compilers.py
+++ b/dhfortran/tests/compilers.py
@@ -26,6 +26,22 @@
         1, "get_fc_flavor_arch('/bin/garbage') should fail"


+
+def test_get_fc_default_env():
+    import os
+    orig = os.environ.get("FC_DEFAULT")
+    try:
+        os.environ["FC_DEFAULT"] = "gfortran"
+        cp.get_fc_default.cache_clear()
+        assert cp.get_fc_default() == cp.default_compilers["gfortran"]
+        assert cp.which(cp.get_fc_default()) is not None
+    finally:
+        if orig is not None:
+            os.environ["FC_DEFAULT"] = orig
+        else:
+            os.environ.pop("FC_DEFAULT", None)
+        cp.get_fc_default.cache_clear()
+
 def test_get_fc_default():
     gf = cp.default_compilers["gfortran"]
     assert cp.get_fc_default() == gf

#1149705#10
Date:
2026-10-05 18:01:31 UTC
From:
To:
Control: block 1146496 by 1149705

Hi,

This is a follow-up to bug #1146496 and links with bug #1149705.

The fix for resolving 'gfortran' via its symlink in get_fc_flavor_arch() works, but get_fc_default() checks os.environ["FC_DEFAULT"] first and calls canon(), which still maps "gfortran" to hardcoded default_compilers["gfortran"] = "gfortran-16". Packages setting FC_DEFAULT=gfortran (like fortran-assert) still hit this issue.

Attached is an updated patch against dh-fortran 0.85 that:
1. Resolves symlinks in canon() and default_compilers["gfortran"] via _detect_default_compiler("gfortran", "gfortran-16").
2. Keeps the generic upstream fallback "gfortran-16" if unversioned gfortran is absent.
3. Adds a test in dhfortran/tests/compilers.py for FC_DEFAULT=gfortran.

Please consider applying this together with the fix for #1146496.

Thanks,
Viktor Pashaiev <w.paszajew@gmail.com>