Repository: conan-io/conan
Pull request #18410 (2025-06-04 10:57:18)

Allow multi-digit version minors for detected compilers
Changelog: Fix: Allow minors greater than 9 in detect api
Docs: Omit

Closes https://github.com/conan-io/conan/issues/18409


Patch:
diff --git a/conan/api/subapi/local.py b/conan/api/subapi/local.py
index 11c6d5c1f..d8b2ef386 100644
--- a/conan/api/subapi/local.py
+++ b/conan/api/subapi/local.py
@@ -82,9 +82,7 @@ class LocalAPI:
             with conanfile_exception_formatter(conanfile, "layout"):
                 conanfile.layout()
 
-        folder = conanfile.recipe_folder if conanfile.folders.root is None else \
-            os.path.normpath(os.path.join(conanfile.recipe_folder, conanfile.folders.root))
-
+        folder = conanfile.recipe_folder
         conanfile.folders.set_base_source(folder)
         conanfile.folders.set_base_export_sources(folder)
         conanfile.folders.set_base_recipe_metadata(os.path.join(folder, "metadata"))
diff --git a/conan/internal/api/detect/detect_api.py b/conan/internal/api/detect/detect_api.py
index 0f96ebcb2..fb29cd615 100644
--- a/conan/internal/api/detect/detect_api.py
+++ b/conan/internal/api/detect/detect_api.py
@@ -488,13 +488,13 @@ def _cc_compiler(compiler_exe="cc"):
         compiler = "clang" if "clang" in out else "gcc"
         # clang and gcc have version after a space, first try to find that to skip extra numbers
         # that might appear in the first line of the output before the version
-        installed_version = re.search(r" ([0-9]+(\.[0-9])+)", out)
+        installed_version = re.search(r" ([0-9]+(\.[0-9]+)+)", out)
         # Try only major but with spaces next
-        installed_version = installed_version or re.search(r" ([0-9]+(\.[0-9])?)", out)
+        installed_version = installed_version or re.search(r" ([0-9]+(\.[0-9]+)?)", out)
         # Fallback to the first number we find optionally followed by other version fields
-        installed_version = installed_version or re.search(r"([0-9]+(\.[0-9])?)", out)
-        if installed_version and installed_version.group():
-            installed_version = installed_version.group()
+        installed_version = installed_version or re.search(r"([0-9]+(\.[0-9]+)?)", out)
+        if installed_version and installed_version.group(1):
+            installed_version = installed_version.group(1)
             ConanOutput(scope="detect_api").info("Found cc=%s-%s" % (compiler, installed_version))
             return compiler, Version(installed_version), compiler_exe
     except (Exception,):  # to disable broad-except
@@ -515,7 +515,7 @@ def detect_gcc_compiler(compiler_exe="gcc"):
         if ret != 0:
             return None, None, None
         compiler = "gcc"
-        installed_version = re.search(r"([0-9]+(\.[0-9])?)", out).group()
+        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), compiler_exe


Test patch:
diff --git a/test/integration/command/source_test.py b/test/integration/command/source_test.py
index 92fbc708e..89cd5372f 100644
--- a/test/integration/command/source_test.py
+++ b/test/integration/command/source_test.py
@@ -142,39 +142,6 @@ class ConanLib(ConanFile):
         self.assertIn("conanfile.py: Running source!", client.out)
         self.assertEqual("Hello World", client.load("file1.txt"))
 
-    def test_local_source_layout_root(self):
-        # Ensure that if a root folder is specified Conan source method
-        # uses it. This is especially important when running with
-        # no_copy_source where both the build()  and source() methods
-        # need to reference source_folder (so the value must be the
-        # same in both methods)
-        conanfile = textwrap.dedent('''
-            from conan import ConanFile
-            from conan.tools.files import save, load
-
-            class ConanLib(ConanFile):
-                
-                def layout(self):
-                    self.folders.root = ".."
-                    self.folders.source = "src"
-                
-                def source(self):
-                    self.output.info(f"In the source() method the Source folder is: {self.source_folder}")
-                    save(self, "source_file.c", "conent")
-
-                def build(self):
-                    self.output.info(f"In the build() method the Source folder is: {self.source_folder}")
-                    load(self, f"{self.source_folder}/source_file.c")
-            ''')
-        client = TestClient(light=True)
-        client.save({CONANFILE: conanfile})
-        
-        client.run("source .")
-        # The build method will fail if source() and build() don't 
-        # both use the correct source_folder
-        client.run("build .")
-        assert "conanfile.py: Calling build()" in client.out  # doesn't fail
-
     def test_retrieve_exports_sources(self):
         # For Conan 2.0 if we install a package from a remote and we want to upload to other
         # remote we need to download the sources, as we consider revisions immutable, let's
diff --git a/test/unittests/client/conf/detect/test_gcc_compiler.py b/test/unittests/client/conf/detect/test_gcc_compiler.py
index b7059c600..ae961c6c0 100644
--- a/test/unittests/client/conf/detect/test_gcc_compiler.py
+++ b/test/unittests/client/conf/detect/test_gcc_compiler.py
@@ -1,19 +1,14 @@
-import unittest
 
 import mock
-from parameterized import parameterized
-
+import pytest
 
 from conan.internal.api.detect.detect_api import detect_gcc_compiler
 
-
-class GCCCompilerTestCase(unittest.TestCase):
-
-    @parameterized.expand([("10",), ("4.2",), ('7', )])
-    def test_detect_gcc_10(self, version):
-        with mock.patch("platform.system", return_value="Linux"):
-            with mock.patch("conan.internal.api.detect.detect_api.detect_runner", return_value=(0, version)):
-                compiler, installed_version, compiler_exe = detect_gcc_compiler()
-        self.assertEqual(compiler, 'gcc')
-        self.assertEqual(installed_version, version)
-        self.assertEqual(compiler_exe, 'gcc')
+@pytest.mark.parametrize("version", ["10", "4.2", '7', "8.12"])
+def test_detect_gcc_10(version):
+    with mock.patch("platform.system", return_value="Linux"):
+        with mock.patch("conan.internal.api.detect.detect_api.detect_runner", return_value=(0, version)):
+            compiler, installed_version, compiler_exe = detect_gcc_compiler()
+    assert compiler == 'gcc'
+    assert installed_version == version
+    assert compiler_exe == 'gcc'
diff --git a/test/unittests/util/detect_test.py b/test/unittests/util/detect_test.py
index 51df4cfe9..84f7cf3e2 100644
--- a/test/unittests/util/detect_test.py
+++ b/test/unittests/util/detect_test.py
@@ -65,6 +65,9 @@ class DetectTest(unittest.TestCase):
 @pytest.mark.parametrize("version_return,expected_version", [
     ["cc.exe (Rev3, Built by MSYS2 project) 14.1.0", "14.1.0"],
     ["g++ (Conan-Build-gcc--binutils-2.42) 14.1.0", "14.1.0"],
+    ["gcc.exe (x86_64-posix-seh-rev0, Built by MinGW-Builds project) 15.1.0", "15.1.0"],
+    ["gcc.exe (x86_64-posix-seh-rev0, Built by MinGW-Builds project) 15.15.0", "15.15.0"],
+    ["gcc (GCC) 4.8.1", "4.8.1"],
     ["clang version 18.1.0rc (https://github.com/llvm/llvm-project.git 461274b81d8641eab64d494accddc81d7db8a09e)", "18.1.0"],
     ["cc.exe (Rev3, Built by MSYS2 project) 14.0", "14.0"],
     ["clang version 18 (https://github.com/llvm/llvm-project.git 461274b81d8641eab64d494accddc81d7db8a09e)", "18"],
@@ -74,6 +77,8 @@ class DetectTest(unittest.TestCase):
 def test_detect_cc_versionings(detect_runner_mock, version_return, expected_version):
     detect_runner_mock.return_value = 0, version_return
     compiler, installed_version, compiler_exe = _cc_compiler()
+    # Using private _value attribute to compare the str that reaches it, so no space issues arise
+    assert installed_version._value == Version(expected_version)._value
     assert installed_version == Version(expected_version)
 
 @pytest.mark.parametrize("function,version_return,expected_version", [