Repository: conan-io/conan
Pull request: Bugfix libcxx detection when using CC/CXX
Changelog: Bugfix: Fix libcxx detection when using `CC/CXX` env vars.
Docs: https://github.com/conan-io/docs/pull/3509

Closes: https://github.com/conan-io/conan/issues/15399




Problem statement:
[bug] profile detect does not try to use the compiler provided
### Environment details

* Operating System+version: Ubuntu 22.04.3 LTS
* Compiler+version: Ubuntu clang version 17.0.6
* Conan version: Conan version 2.0.16
* Python version: Python 3.10.12


### Steps to reproduce

`CC=clang-17 CXX=clang++-17 conan profile detect`
I would expect it to use clang++-17 when detecting libcxx

### Logs

```
detect_api: CC and CXX: clang-17, clang++-17 
detect_api: Found clang 17.0
detect_api: clang>=8, using the major as version
detect_api: WARN: compiler.libcxx check error: /bin/sh: 1: clang++: not found
...
```

Hints from issue discussion:
Hi @Ignition 

Thanks for your report.
Yes, it seems that there could be a bug there, and the detection should be using CC/CXX defined compiler.
@RubenRBS I am having a look how this could be improved:

- Breaking the ``detect_api`` interface, and make "detect_compiler" return: ``compiler, version, compiler_exe = detect_compiler()``, so we can pass the ``compiler_exe`` as optional argument to ``detect_libcxx`` and others
- Make it optional to not break with ``detect_compiler(return_compiler_exe=True)``
- Extract and re-use the logic that checks CC/CXX env-vars and call it again inside the ``detect_libcxx`` call. Not super robust, but doable

Commit message: Prettier error when jinja2 rendering fails

Patch to review:
diff --git a/conan/internal/api/detect_api.py b/conan/internal/api/detect_api.py
index 0f6ad396313..a38215d6dc8 100644
--- a/conan/internal/api/detect_api.py
+++ b/conan/internal/api/detect_api.py
@@ -122,7 +122,7 @@ def _get_e2k_architecture():
     }.get(platform.processor())
 
 
-def detect_libcxx(compiler, version):
+def detect_libcxx(compiler, version, compiler_exe=None):
     assert isinstance(version, Version)
 
     def _detect_gcc_libcxx(version_, executable):
@@ -166,7 +166,7 @@ def _detect_gcc_libcxx(version_, executable):
     if compiler == "apple-clang":
         return "libc++"
     elif compiler == "gcc":
-        libcxx = _detect_gcc_libcxx(version, "g++")
+        libcxx = _detect_gcc_libcxx(version, compiler_exe or "g++")
         return libcxx
     elif compiler == "cc":
         if platform.system() == "SunOS":
@@ -179,7 +179,7 @@ def _detect_gcc_libcxx(version_, executable):
         elif platform.system() == "Windows":
             return  # by default windows will assume LLVM/Clang with VS backend
         else:  # Linux
-            libcxx = _detect_gcc_libcxx(version, "clang++")
+            libcxx = _detect_gcc_libcxx(version, compiler_exe or "clang++")
             return libcxx
     elif compiler == "sun-cc":
         return "libCstd"
@@ -243,17 +243,17 @@ def detect_cppstd(compiler, compiler_version):
     return cppstd
 
 
-def detect_compiler():
-    """
-    find the default compiler on the build machine
-    search order and priority:
-    1. CC and CXX environment variables are always top priority
-    2. Visual Studio detection (Windows only) via vswhere or registry or environment variables
-    3. Apple Clang (Mac only)
-    4. cc executable
-    5. gcc executable
-    6. clang executable
+def detect_default_compiler():
     """
+        find the default compiler on the build machine
+        search order and priority:
+        1. CC and CXX environment variables are always top priority
+        2. Visual Studio detection (Windows only) via vswhere or registry or environment variables
+        3. Apple Clang (Mac only)
+        4. cc executable
+        5. gcc executable
+        6. clang executable
+        """
     output = ConanOutput(scope="detect_api")
     cc = os.environ.get("CC", "")
     cxx = os.environ.get("CXX", "")
@@ -263,11 +263,11 @@ def detect_compiler():
         if "clang" in command.lower():
             return _clang_compiler(command)
         if "gcc" in command or "g++" in command or "c++" in command:
-            gcc, gcc_version = _gcc_compiler(command)
+            gcc, gcc_version, compiler_exe = _gcc_compiler(command)
             if platform.system() == "Darwin" and gcc is None:
                 output.error("%s detected as a frontend using apple-clang. "
                              "Compiler not supported" % command)
-            return gcc, gcc_version
+            return gcc, gcc_version, compiler_exe
         if platform.system() == "SunOS" and command.lower() == "cc":
             return _sun_cc_compiler(command)
         if (platform.system() == "Windows" and command.rstrip('"').endswith(("cl", "cl.exe"))
@@ -276,28 +276,28 @@ def detect_compiler():
 
         # I am not able to find its version
         output.error("Not able to automatically detect '%s' version" % command)
-        return None, None
+        return None, None, None
 
     if platform.system() == "Windows":
         version = _detect_vs_ide_version()
         version = {"17": "193", "16": "192", "15": "191"}.get(str(version))  # Map to compiler
         if version:
-            return 'msvc', Version(version)
+            return 'msvc', Version(version), None
 
     if platform.system() == "SunOS":
-        sun_cc, sun_cc_version = _sun_cc_compiler()
+        sun_cc, sun_cc_version, compiler_exe = _sun_cc_compiler()
         if sun_cc:
-            return sun_cc, sun_cc_version
+            return sun_cc, sun_cc_version, compiler_exe
 
     if platform.system() in ["Darwin", "FreeBSD"]:
-        clang, clang_version = _clang_compiler()  # prioritize clang
+        clang, clang_version, compiler_exe = _clang_compiler()  # prioritize clang
         if clang:
-            return clang, clang_version
+            return clang, clang_version, compiler_exe
         return
     else:
-        gcc, gcc_version = _gcc_compiler()
+        gcc, gcc_version, compiler_exe = _gcc_compiler()
         if gcc:
-            return gcc, gcc_version
+            return gcc, gcc_version, compiler_exe
         return _clang_compiler()
 
 
@@ -326,11 +326,11 @@ def _gcc_compiler(compiler_exe="gcc"):
             _, out = detect_runner("%s --version" % compiler_exe)
             out = out.lower()
             if "clang" in out:
-                return None, None
+                return None, None, None
 
         ret, out = detect_runner('%s -dumpversion' % compiler_exe)
         if ret != 0:
-            return None, None
+            return None, None, None
         compiler = "gcc"
         installed_version = re.search(r"([0-9]+(\.[0-9])?)", out).group()
         # Since GCC 7.1, -dumpversion return the major version number
@@ -338,9 +338,15 @@ def _gcc_compiler(compiler_exe="gcc"):
         # number ("7.1.1").
         if installed_version:
             ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version))
-            return compiler, Version(installed_version)
+            return compiler, Version(installed_version), compiler_exe
     except (Exception,):  # to disable broad-except
-        return None, None
+        return None, None, None
+
+
+def detect_compiler():
+    ConanOutput(scope="detect_api").warning("detect_compiler() is deprecated, use detect_default_compiler()")
+    compiler, version, _ = detect_default_compiler()
+    return compiler, version
 
 
 def _sun_cc_compiler(compiler_exe="cc"):
@@ -354,28 +360,28 @@ def _sun_cc_compiler(compiler_exe="cc"):
             installed_version = re.search(r"([0-9]+\.[0-9]+)", out).group()
         if installed_version:
             ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version))
-            return compiler, Version(installed_version)
+            return compiler, Version(installed_version), compiler_exe
     except (Exception,):  # to disable broad-except
-        return None, None
+        return None, None, None
 
 
 def _clang_compiler(compiler_exe="clang"):
     try:
         ret, out = detect_runner('%s --version' % compiler_exe)
         if ret != 0:
-            return None, None
+            return None, None, None
         if "Apple" in out:
             compiler = "apple-clang"
         elif "clang version" in out:
             compiler = "clang"
         else:
-            return None, None
+            return None, None, None
         installed_version = re.search(r"([0-9]+\.[0-9])", out).group()
         if installed_version:
             ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version))
-            return compiler, Version(installed_version)
+            return compiler, Version(installed_version), compiler_exe
     except (Exception,):  # to disable broad-except
-        return None, None
+        return None, None, None
 
 
 def _msvc_cl_compiler(compiler_exe="cl"):
@@ -386,20 +392,20 @@ def _msvc_cl_compiler(compiler_exe="cl"):
         compiler_exe = compiler_exe.strip('"')
         ret, out = detect_runner(f'"{compiler_exe}" /?')
         if ret != 0:
-            return None, None
+            return None, None, None
         first_line = out.splitlines()[0]
         if "Microsoft" not in first_line:
-            return None, None
+            return None, None, None
         compiler = "msvc"
         version_regex = re.search(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.([0-9]+)\.?([0-9]+)?",
                                   first_line)
         if not version_regex:
-            return None, None
+            return None, None, None
         # 19.36.32535 -> 193
         version = f"{version_regex.group('major')}{version_regex.group('minor')[0]}"
-        return compiler, Version(version)
+        return compiler, Version(version), compiler_exe
     except (Exception,):  # to disable broad-except
-        return None, None
+        return None, None, None
 
 
 def default_compiler_version(compiler, version):
diff --git a/conans/client/conf/detect.py b/conans/client/conf/detect.py
index de2422836ad..9048a82be2f 100644
--- a/conans/client/conf/detect.py
+++ b/conans/client/conf/detect.py
@@ -1,6 +1,6 @@
 from conan.api.output import ConanOutput
 from conan.internal.api.detect_api import detect_os, detect_arch, default_msvc_runtime, \
-    detect_libcxx, detect_cppstd, detect_compiler, default_compiler_version
+    detect_libcxx, detect_cppstd, detect_default_compiler, default_compiler_version
 
 
 def detect_defaults_settings():
@@ -14,7 +14,7 @@ def detect_defaults_settings():
     arch = detect_arch()
     if arch:
         result.append(("arch", arch))
-    compiler, version = detect_compiler()
+    compiler, version, compiler_exe = detect_default_compiler()
     if not compiler:
         result.append(("build_type", "Release"))
         ConanOutput().warning("No compiler was detected (one may not be needed)")
@@ -28,7 +28,7 @@ def detect_defaults_settings():
         result.append(("compiler.runtime", runtime))
     if runtime_version:
         result.append(("compiler.runtime_version", runtime_version))
-    libcxx = detect_libcxx(compiler, version)
+    libcxx = detect_libcxx(compiler, version, compiler_exe)
     if libcxx:
         result.append(("compiler.libcxx", libcxx))
     cppstd = detect_cppstd(compiler, version)
diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py
index 54b6d3d0140..f1247c01bfa 100644
--- a/conans/client/profile_loader.py
+++ b/conans/client/profile_loader.py
@@ -126,8 +126,14 @@ def _load_profile(self, profile_name, cwd):
                    "profile_name": file_path,
                    "conan_version": conan_version,
                    "detect_api": detect_api}
+
         rtemplate = Environment(loader=FileSystemLoader(base_path)).from_string(text)
-        text = rtemplate.render(context)
+
+        try:
+            text = rtemplate.render(context)
+        except:
+            raise ConanException("Error while rendering the profile template file. "
+                                 "Check your Jinja2 syntax")
 
         try:
             return self._recurse_load_profile(text, profile_path)
diff --git a/conans/test/functional/test_profile_detect_api.py b/conans/test/functional/test_profile_detect_api.py
index 3c2b732389f..1601e97ac77 100644
--- a/conans/test/functional/test_profile_detect_api.py
+++ b/conans/test/functional/test_profile_detect_api.py
@@ -11,12 +11,13 @@ def test_profile_detect_compiler(self):
 
         client = TestClient()
         tpl1 = textwrap.dedent("""
-            {% set compiler, version = detect_api.detect_compiler() %}
+            {% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %}
             {% set runtime, _ = detect_api.default_msvc_runtime(compiler) %}
             [settings]
             compiler={{compiler}}
             compiler.version={{detect_api.default_compiler_version(compiler, version)}}
             compiler.runtime={{runtime}}
+            compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
             compiler.cppstd={{detect_api.default_cppstd(compiler, version)}}
 
             [conf]
diff --git a/conans/test/unittests/client/conf/detect/test_gcc_compiler.py b/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
index 39f3a6aa65b..e8874383270 100644
--- a/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
+++ b/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
@@ -13,6 +13,7 @@ class GCCCompilerTestCase(unittest.TestCase):
     def test_detect_gcc_10(self, version):
         with mock.patch("platform.system", return_value="Linux"):
             with mock.patch("conan.internal.api.detect_api.detect_runner", return_value=(0, version)):
-                compiler, installed_version = _gcc_compiler()
+                compiler, installed_version, compiler_exe = _gcc_compiler()
         self.assertEqual(compiler, 'gcc')
         self.assertEqual(installed_version, version)
+        self.assertEqual(compiler_exe, 'gcc')
diff --git a/conans/test/unittests/util/detect_test.py b/conans/test/unittests/util/detect_test.py
index c9c03baf8e8..a9c1c9a2d4f 100644
--- a/conans/test/unittests/util/detect_test.py
+++ b/conans/test/unittests/util/detect_test.py
@@ -50,7 +50,7 @@ def test_detect_arch(self, machine, expected_arch):
             self.assertEqual(expected_arch, result['arch'])
 
     @mock.patch("conan.internal.api.detect_api._clang_compiler",
-                return_value=("clang", Version("9")))
+                return_value=("clang", Version("9"), "clang"))
     def test_detect_clang_gcc_toolchain(self, _):
         output = RedirectedTestOutput()
         with redirect_output(output):


Code review comments:
diff_hunk: @@ -326,21 +326,27 @@ def _gcc_compiler(compiler_exe="gcc"):
             _, out = detect_runner("%s --version" % compiler_exe)
             out = out.lower()
             if "clang" in out:
-                return None, None
+                return None, None, None
 
         ret, out = detect_runner('%s -dumpversion' % compiler_exe)
         if ret != 0:
-            return None, None
+            return None, None, None
         compiler = "gcc"
         installed_version = re.search(r"([0-9]+(\.[0-9])?)", out).group()
         # Since GCC 7.1, -dumpversion return the major version number
         # only ("7"). We must use -dumpfullversion to get the full version
         # number ("7.1.1").
         if installed_version:
             ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, installed_version))
-            return compiler, Version(installed_version)
+            return compiler, Version(installed_version), compiler_exe
     except (Exception,):  # to disable broad-except
-        return None, None
+        return None, None, None
+
+
+def detect_compiler():
+    ConanOutput(scope="detect_api").warning("detect_compiler() is deprecated, use detect_default_compiler()")
path: conan/internal/api/detect_api.py
text: @user1:
I think the `warn_tag` is missing
```suggestion
    ConanOutput(scope="detect_api").warning("detect_compiler() is deprecated, use detect_default_compiler()", warn_tag="deprecated")
```

@author:
Damn, really nice catch