Repository: conan-io/conan
Pull request #18411 (2025-06-04 10:58:09)

better remote caching
Changelog: Omit
Docs: Omit

Derived from ``conan workspace create`` PR: https://github.com/conan-io/conan/pull/18390, while checking the viability of the approach


Patch:
diff --git a/conan/internal/api/detect/detect_api.py b/conan/internal/api/detect/detect_api.py
index 0f96ebcb2..4b19c3615 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
@@ -628,9 +628,13 @@ def default_compiler_version(compiler, version):
     if compiler == "clang" and major >= 8:
         output.info("clang>=8, using the major as version")
         return major
-    elif compiler == "gcc" and major >= 5:
-        output.info("gcc>=5, using the major as version")
-        return major
+    elif compiler == "gcc":
+        if major >= 5:
+            output.info("gcc>=5, using the major as version")
+            return major
+        else:
+            output.info("gcc<5, using the major.minor as version")
+            return Version(f"{major}.{minor}")
     elif compiler == "apple-clang" and major >= 13:
         output.info("apple-clang>=13, using the major as version")
         return major
diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py
index d87f3d849..b5bfb0e2d 100644
--- a/conan/internal/rest/remote_manager.py
+++ b/conan/internal/rest/remote_manager.py
@@ -38,11 +38,13 @@ class RemoteManager:
     def upload_recipe(self, ref, files_to_upload, remote):
         assert isinstance(ref, RecipeReference)
         assert ref.revision, "upload_recipe requires RREV"
+        remote.invalidate_cache()
         self._call_remote(remote, "upload_recipe", ref, files_to_upload)
 
     def upload_package(self, pref, files_to_upload, remote):
         assert pref.ref.revision, "upload_package requires RREV"
         assert pref.revision, "upload_package requires PREV"
+        remote.invalidate_cache()
         self._call_remote(remote, "upload_package", pref, files_to_upload)
 
     def get_recipe(self, ref, remote, metadata=None):
@@ -204,12 +206,15 @@ class RemoteManager:
         return packages
 
     def remove_recipe(self, ref, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_recipe", ref)
 
     def remove_packages(self, prefs, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_packages", prefs)
 
     def remove_all_packages(self, ref, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_all_packages", ref)
 
     def authenticate(self, remote, name, password):
@@ -243,10 +248,19 @@ class RemoteManager:
 
         cached_method = remote._caching.setdefault("get_latest_package_reference", {})
         try:
-            return cached_method[pref]
+            result = cached_method[pref]
         except KeyError:
-            result = self._call_remote(remote, "get_latest_package_reference", pref, headers=headers)
-            cached_method[pref] = result
+            try:
+                result = self._call_remote(remote, "get_latest_package_reference", pref,
+                                           headers=headers)
+                cached_method[pref] = result
+                return result
+            except Exception as e:
+                cached_method[pref] = e
+                raise e
+        else:
+            if isinstance(result, Exception):
+                raise result
             return result
 
     def get_recipe_revision_reference(self, ref, remote) -> bool:
diff --git a/conan/internal/rest/rest_client_local_recipe_index.py b/conan/internal/rest/rest_client_local_recipe_index.py
index 33082ca13..ebc2e8c18 100644
--- a/conan/internal/rest/rest_client_local_recipe_index.py
+++ b/conan/internal/rest/rest_client_local_recipe_index.py
@@ -210,6 +210,8 @@ class _LocalRecipesIndexLayout:
             pattern = f"{pattern_ref.name}/{pattern_ref.version}"
         except:
             # pattern = pattern
+            pattern = f"{name_pattern}/*"
+            original_pattern = pattern
             pass
 
         loader = ConanFileLoader(None)
diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py
index 3572174e9..c0509548e 100644
--- a/conan/tools/cmake/cmakedeps2/target_configuration.py
+++ b/conan/tools/cmake/cmakedeps2/target_configuration.py
@@ -400,7 +400,8 @@ class TargetConfigurationTemplate2:
         {% endif %}
 
         {% if lib_info.get("system_libs") %}
-        target_link_libraries({{lib}} INTERFACE {{lib_info["system_libs"]}})
+        set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES
+                     {{config_wrapper(config, lib_info["system_libs"])}})
         {% endif %}
         {% if lib_info.get("frameworks") %}
         set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES
diff --git a/conan/tools/cmake/cmakedeps2/targets.py b/conan/tools/cmake/cmakedeps2/targets.py
index e02d2926d..fba8092de 100644
--- a/conan/tools/cmake/cmakedeps2/targets.py
+++ b/conan/tools/cmake/cmakedeps2/targets.py
@@ -32,6 +32,7 @@ class TargetsTemplate2:
     @property
     def _template(self):
         return textwrap.dedent("""\
+            include_guard()
             message(STATUS "Conan: Configuring Targets for {{ ref }}")
 
             # Load information for each installed configuration.
diff --git a/conan/tools/google/bazeldeps.py b/conan/tools/google/bazeldeps.py
index f2202b417..ad77246f3 100644
--- a/conan/tools/google/bazeldeps.py
+++ b/conan/tools/google/bazeldeps.py
@@ -1,121 +1,28 @@
 import os
 import re
 import textwrap
-from collections import namedtuple, defaultdict
 
 from jinja2 import Template, StrictUndefined
 
-from conan.errors import ConanException
 from conan.internal import check_duplicated_generator
 from conan.internal.model.dependencies import get_transitive_requires
 from conan.internal.model.pkg_type import PackageType
 from conan.internal.util.files import save
 
-_BazelTargetInfo = namedtuple("DepInfo", ['repository_name', 'name', 'ref_name', 'requires', 'cpp_info'])
 
-
-def _get_name_with_namespace(namespace, name):
-    """
-    Build a name with a namespace, e.g., openssl-crypto
-    """
-    return f"{namespace}-{name}"
-
-
-def _get_package_reference_name(dep):
-    """
-    Get the reference name for the given package
-    """
-    return dep.ref.name
-
-
-def _get_repository_name(dep, is_build_require=False):
-    pkg_name = dep.cpp_info.get_property("bazel_repository_name") or _get_package_reference_name(dep)
-    return f"build-{pkg_name}" if is_build_require else pkg_name
-
-
-def _get_target_name(dep):
-    pkg_name = dep.cpp_info.get_property("bazel_target_name") or _get_package_reference_name(dep)
-    return pkg_name
-
-
-def _get_component_name(dep, comp_ref_name):
-    pkg_name = _get_target_name(dep)
-    if comp_ref_name not in dep.cpp_info.components:
-        # foo::foo might be referencing the root cppinfo
-        if _get_package_reference_name(dep) == comp_ref_name:
-            return pkg_name
-        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
-                             "package requirement".format(name=_get_package_reference_name(dep),
-                                                          cname=comp_ref_name))
-    comp_name = dep.cpp_info.components[comp_ref_name].get_property("bazel_target_name")
-    # If user did not set bazel_target_name, let's create a component name
-    # with a namespace, e.g., dep-comp1
-    return comp_name or _get_name_with_namespace(pkg_name, comp_ref_name)
-
-
-# FIXME: This function should be a common one to be used by PkgConfigDeps, CMakeDeps?, etc.
-def _get_requirements(conanfile, build_context_activated):
-    """
-    Simply save the activated requirements (host + build + test), and the deactivated ones
-    """
-    # All the requirements
-    host_req = conanfile.dependencies.host
-    build_req = conanfile.dependencies.direct_build  # tool_requires
-    test_req = conanfile.dependencies.test
-
-    for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
-        # Require is not used at the moment, but its information could be used,
-        # and will be used in Conan 2.0
-        # Filter the build_requires not activated with self.build_context_activated
-        if require.build and dep.ref.name not in build_context_activated:
-            continue
-        yield require, dep
-
-
-def _get_headers(cpp_info, package_folder_path):
-    return ['"{}/**"'.format(_relativize_path(path, package_folder_path))
-            for path in cpp_info.includedirs]
-
-
-def _get_includes(cpp_info, package_folder_path):
-    return ['"{}"'.format(_relativize_path(path, package_folder_path))
-            for path in cpp_info.includedirs]
-
-
-def _get_defines(cpp_info):
-    return ['"{}"'.format(define.replace('"', '\\' * 3 + '"'))
-            for define in cpp_info.defines]
-
-
-def _get_linkopts(cpp_info, os_build):
-    link_opt = '/DEFAULTLIB:{}' if os_build == "Windows" else '-l{}'
-    system_libs = [link_opt.format(lib) for lib in cpp_info.system_libs]
-    shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags
-    return [f'"{flag}"' for flag in (system_libs + shared_flags)]
-
-
-def _get_copts(cpp_info):
-    # FIXME: long discussions between copts (-Iflag) vs includes in Bazel. Not sure yet
-    # includedirsflags = ['"-I{}"'.format(_relativize_path(d, package_folder_path))
-    #                     for d in cpp_info.includedirs]
-    cxxflags = [var.replace('"', '\\"') for var in cpp_info.cxxflags]
-    cflags = [var.replace('"', '\\"') for var in cpp_info.cflags]
-    return [f'"{flag}"' for flag in (cxxflags + cflags)]
-
-
-def _relativize_path(path, pattern):
+def _relativize_path(path, start_folder):
     """
-    Returns a relative path with regard to pattern given.
+    Returns a relative path with regard to the given folder.
 
-    :param path: absolute or relative path
-    :param pattern: either a piece of path or a pattern to match the leading part of the path
+    :param path: absolute or relative path.
+    :param start_folder: folder to start relative to.
     :return: Unix-like path relative if matches to the given pattern.
              Otherwise, it returns the original path.
     """
-    if not path or not pattern:
+    if not path or not start_folder:
         return path
     path_ = path.replace("\\", "/").replace("/./", "/")
-    pattern_ = pattern.replace("\\", "/").replace("/./", "/")
+    pattern_ = start_folder.replace("\\", "/").replace("/./", "/")
     match = re.match(pattern_, path_)
     if match:
         matching = match[0]
@@ -125,157 +32,26 @@ def _relativize_path(path, pattern):
     return path
 
 
-class _BazelDependenciesBZLGenerator:
-    """
-    Bazel 6.0 needs to know all the dependencies for its current project. So, the only way
-    to do that is to tell the WORKSPACE file how to load all the Conan ones. This is the goal
-    of the function created by this class, the ``load_conan_dependencies`` one.
-
-    More information:
-        * https://bazel.build/reference/be/workspace#new_local_repository
-
-    Bazel >= 7.1 needs to know all the dependencies as well, but provided via the MODULE.bazel file.
-    Therefor we provide a static repository rule to load the dependencies. This rule is used by a
-    module extension, passing the package path and the BUILD file path to the repository rule.
-    """
-
-    repository_filename = "dependencies.bzl"
-    modules_filename = "conan_deps_module_extension.bzl"
-    repository_rules_filename = "conan_deps_repo_rules.bzl"
-    repository_template = textwrap.dedent("""\
-        # This Bazel module should be loaded by your WORKSPACE file.
-        # Add these lines to your WORKSPACE one (assuming that you're using the "bazel_layout"):
-        # load("@//conan:dependencies.bzl", "load_conan_dependencies")
-        # load_conan_dependencies()
-
-        def load_conan_dependencies():
-        {% for repository_name, pkg_folder, pkg_build_file_path in dependencies %}
-            native.new_local_repository(
-                name="{{repository_name}}",
-                path="{{pkg_folder}}",
-                build_file="{{pkg_build_file_path}}",
-            )
-        {% endfor %}
-        """)
-    module_template = textwrap.dedent("""\
-        # This module provides a repo for each requires-dependency in your conanfile.
-        # It's generated by the BazelDeps, and should be used in your Module.bazel file.
-        load(":conan_deps_repo_rules.bzl", "conan_dependency_repo")
-
-        def _load_dependenies_impl(mctx):
-        {% for repository_name, pkg_folder, pkg_build_file_path in dependencies %}
-            conan_dependency_repo(
-                name = "{{repository_name}}",
-                package_path = "{{pkg_folder}}",
-                build_file_path = "{{pkg_build_file_path}}",
-            )
-        {% endfor %}
-
-            return mctx.extension_metadata(
-                # It will only warn you if any direct
-                # dependency is not imported by the 'use_repo' or even it is imported
-                # but not created. Notice that root_module_direct_dev_deps can not be None as we
-                # are giving 'all' value to root_module_direct_deps.
-                # Fix the 'use_repo' calls by running 'bazel mod tidy'
-                root_module_direct_deps = 'all',
-                root_module_direct_dev_deps = [],
-
-                # Prevent writing function content to lockfiles:
-                # - https://bazel.build/rules/lib/builtins/module_ctx#extension_metadata
-                # Important for remote build. Actually it's not reproducible, as local paths will
-                # be different on different machines. But we assume that conan works correctly here.
-                # IMPORTANT: Not compatible with bazel < 7.1
-                reproducible = True,
-            )
-
-        conan_extension = module_extension(
-            implementation = _load_dependenies_impl,
-            os_dependent = True,
-            arch_dependent = True,
-        )
-        """)
-    repository_rules_content = textwrap.dedent("""\
-        # This bazel repository rule is used to load Conan dependencies into the Bazel workspace.
-        # It's used by a generated module file that provides information about the conan packages.
-        # Each conan package is loaded into a bazel repository rule, with having the name of the
-        # package. The whole method is based on symlinks to not copy the whole package into the
-        # Bazel workspace, which is expensive.
-        def _conan_dependency_repo(rctx):
-            package_path = rctx.workspace_root.get_child(rctx.attr.package_path)
-
-            child_packages = package_path.readdir()
-            for child in child_packages:
-                rctx.symlink(child, child.basename)
-
-            rctx.symlink(rctx.attr.build_file_path, "BUILD.bazel")
-
-        conan_dependency_repo = repository_rule(
-            implementation = _conan_dependency_repo,
-            attrs = {
-                "package_path": attr.string(
-                    mandatory = True,
-                    doc = "The path to the Conan package in conan cache.",
-                ),
-                "build_file_path": attr.string(
-                    mandatory = True,
-                    doc = "The path to the BUILD file.",
-                ),
-            },
-        )
-        """)
-
-    def __init__(self, conanfile, dependencies):
-        self._conanfile = conanfile
-        self._dependencies = dependencies
-
-    def _generate_6x_compatible(self):
-        repository_template = Template(self.repository_template, trim_blocks=True,
-                                       lstrip_blocks=True,
-                                       undefined=StrictUndefined)
-        content = repository_template.render(dependencies=self._dependencies)
-        # dependencies.bzl file (Bazel 6.x compatible)
-        save(self.repository_filename, content)
-
-    def generate(self):
-        # Keeping available Bazel 6.x, but it'll likely be dropped soon
-        self._generate_6x_compatible()
-        # Bazel 7.x files
-        module_template = Template(self.module_template, trim_blocks=True, lstrip_blocks=True,
-                                   undefined=StrictUndefined)
-        content = module_template.render(dependencies=self._dependencies)
-        save(self.modules_filename, content)
-        save(self.repository_rules_filename, self.repository_rules_content)
-        save("BUILD.bazel", "# This is an empty BUILD file.")
-
-
-class _LibInfo:
-    def __init__(self, lib_name, cpp_info_, package_folder_path):
-        self.name = lib_name
-        self.is_shared = cpp_info_.type == PackageType.SHARED
-        self.lib_path = _relativize_path(cpp_info_.location, package_folder_path)
-        self.import_lib_path = _relativize_path(cpp_info_.link_location, package_folder_path)
-
-
-class _BazelBUILDGenerator:
+class _BazelDepBuildGenerator:
     """
     This class creates the BUILD.bazel for each dependency where it's declared all the
     necessary information to load the libraries
     """
     # If both files exist, BUILD.bazel takes precedence over BUILD
     # https://bazel.build/concepts/build-files
-    filename = "BUILD.bazel"
-    template = textwrap.dedent("""\
+    dep_build_filename = "BUILD.bazel"
+    dep_build_template = textwrap.dedent("""\
     {% macro cc_import_macro(libs) %}
     {% for lib_info in libs %}
     cc_import(
-        name = "{{ lib_info.name }}_precompiled",
-        {% if lib_info.is_shared %}
-        shared_library = "{{ lib_info.lib_path }}",
+        name = "{{ lib_info['name'] }}_precompiled",
+        {% if lib_info['is_shared'] %}
+        shared_library = "{{ lib_info['lib_path'] }}",
         {% else %}
-        static_library = "{{ lib_info.lib_path }}",
+        static_library = "{{ lib_info['lib_path'] }}",
         {% endif %}
-        {% if lib_info.import_lib_path %}
-        interface_library = "{{ lib_info.import_lib_path }}",
+        {% if lib_info['import_lib_path'] %}
+        interface_library = "{{ lib_info['import_lib_path'] }}",
         {% endif %}
     )
     {% endfor %}
@@ -319,13 +95,13 @@ class _BazelBUILDGenerator:
         ],
         {% endif %}
         visibility = ["//visibility:public"],
-        {% if obj["libs"] or obj["dependencies"] or obj["component_names"] %}
+        {% if obj["libs"] or obj["dependencies"] or obj.get("component_names", []) %}
         deps = [
             # do not sort
             {% for lib in obj["libs"] %}
             ":{{ lib.name }}_precompiled",
             {% endfor %}
-            {% for name in obj["component_names"] %}
+            {% for name in obj.get("component_names", []) %}
             ":{{ name }}",
             {% endfor %}
             {% for dep in obj["dependencies"] %}
@@ -366,30 +142,30 @@ class _BazelBUILDGenerator:
     {{ filegroup_bindirs_macro(root) }}
     """)
 
-    def __init__(self, conanfile, dep, root_package_info, components_info):
+    def __init__(self, conanfile, dep, require):
         self._conanfile = conanfile
         self._dep = dep
-        self._root_package_info = root_package_info
-        self._components_info = components_info
+        self._is_build_require = require.build
+        self._transitive_reqs = get_transitive_requires(self._conanfile, dep)
 
     @property
-    def build_file_pah(self):
+    def _build_file_path(self):
         """
         Returns the absolute path to the BUILD file created by Conan
         """
-        folder = os.path.join(self._root_package_info.repository_name, self.filename)
+        folder = os.path.join(self._get_repository_name(self._dep), self.dep_build_filename)
         return folder.replace("\\", "/")
 
     @property
-    def absolute_build_file_pah(self):
+    def _absolute_build_file_path(self):
         """
         Returns the absolute path to the BUILD file created by Conan
         """
-        folder = os.path.join(self._conanfile.generators_folder, self.build_file_pah)
+        folder = os.path.join(self._conanfile.generators_folder, self._build_file_path)
         return folder.replace("\\", "/")
 
     @property
-    def package_folder(self):
+    def _package_folder(self):
         """
         Returns the package folder path
         """
@@ -398,105 +174,55 @@ class _BazelBUILDGenerator:
             else self._dep.package_folder
         return root_folder.replace("\\", "/")
 
-    @property
-    def repository_name(self):
-        """
-        Wrapper to get the final name used for the root dependency cc_library declaration
-        """
-        return self._root_package_info.repository_name
-
-    def get_full_libs_info(self):
-        full_libs_info = defaultdict(list)
-        cpp_info = self._dep.cpp_info
-        deduced_cpp_info = cpp_info.deduce_full_cpp_info(self._dep)
-        package_folder_path = self.package_folder
-        # Root
-        if len(cpp_info.libs) > 1:
-            for lib_name in cpp_info.libs:
-                virtual_component = deduced_cpp_info.components.pop(f"_{lib_name}")  # removing it!
-                full_libs_info["root"].append(
-                    _LibInfo(lib_name, virtual_component, package_folder_path)
-                )
-        elif cpp_info.libs:
-            full_libs_info["root"].append(
-                _LibInfo(cpp_info.libs[0], deduced_cpp_info, package_folder_path)
-            )
-        # components
-        for cmp_name, cmp_cpp_info in deduced_cpp_info.components.items():
-            if cmp_name == "_common":  # FIXME: placeholder?
-                continue
-            if len(cmp_cpp_info.libs) > 1:
-                raise ConanException(f"BazelDeps only allows 1 lib per component:\n"
-                                     f"{self._dep}: {cmp_cpp_info.libs}")
-            elif cmp_cpp_info.libs:
-                # Bazel needs to relativize each path
-                full_libs_info[_get_component_name(self._dep, cmp_name)].append(
-                    _LibInfo(cmp_cpp_info.libs[0], cmp_cpp_info, package_folder_path)
-                )
-        return full_libs_info
-
-    def _get_context(self):
-        def fill_info(info, libs_info):
-            ret = {
-                "name": info.name,  # package name and components name
-                "libs": {},
-                "headers": "",
-                "includes": "",
-                "defines": "",
-                "linkopts": "",
-                "copts": "",
-                "dependencies": info.requires,
-                "component_names": []  # filled only by the root
-            }
-            if info.cpp_info is not None:
-                cpp_info = info.cpp_info
-                headers = _get_headers(cpp_info, package_folder_path)
-                includes = _get_includes(cpp_info, package_folder_path)
-                copts = _get_copts(cpp_info)
-                defines = _get_defines(cpp_info)
-                os_build = self._dep.settings_build.get_safe("os")
-                linkopts = _get_linkopts(cpp_info, os_build)
-                bindirs = [_relativize_path(bindir, package_folder_path)
-                           for bindir in cpp_info.bindirs]
-                ret.update({
-                    "libs": libs_info,
-                    "bindirs": bindirs,
-                    "headers": headers,
-                    "includes": includes,
-                    "defines": defines,
-                    "linkopts": linkopts,
-                    "copts": copts
-                })
-            return ret
-
-        package_folder_path = self.package_folder
-        context = dict()
-        full_libs_info = self.get_full_libs_info()
-        context["root"] = fill_info(self._root_package_info, full_libs_info.get("root", []))
-        context["components"] = []
-        for component in self._components_info:
-            component_context = fill_info(component, full_libs_info.get(component.name, []))
-            context["components"].append(component_context)
-            context["root"]["component_names"].append(component_context["name"])
-
-        return context
-
-    def generate(self):
-        context = self._get_context()
-        template = Template(self.template, trim_blocks=True, lstrip_blocks=True,
-                            undefined=StrictUndefined)
-        content = template.render(context)
-        save(self.build_file_pah, content)
-
-
-class _InfoGenerator:
-
-    def __init__(self, conanfile, dep, require):
-        self._conanfile = conanfile
-        self._dep = dep
-        self._req = require
-        self._is_build_require = require.build
-        self._transitive_reqs = get_transitive_requires(self._conanfile, dep)
+    def _get_repository_name(self, dep):
+        pkg_name = dep.cpp_info.get_property("bazel_repository_name") or dep.ref.name
+        return f"build-{pkg_name}" if self._is_build_require else pkg_name
+
+    def _get_target_name(self, dep):
+        pkg_name = dep.cpp_info.get_property("bazel_target_name") or dep.ref.name
+        return pkg_name
+
+    def _get_component_name(self, dep, comp_ref_name):
+        pkg_name = self._get_target_name(dep)
+        if comp_ref_name not in dep.cpp_info.components:
+            # foo::foo might be referencing the root cppinfo
+            if dep.ref.name == comp_ref_name:
+                return pkg_name
+            return f"{pkg_name}-{comp_ref_name}"
+        comp_name = dep.cpp_info.components[comp_ref_name].get_property("bazel_target_name")
+        # If user did not set bazel_target_name, let's create a component name
+        # with a namespace, e.g., dep-comp1
+        return comp_name or f"{pkg_name}-{comp_ref_name}"
+
+    def _get_headers(self, cpp_info):
+        return ['"{}/**"'.format(_relativize_path(path, self._package_folder))
+                for path in cpp_info.includedirs]
+
+    def _get_bindirs(self, cpp_info):
+        return [_relativize_path(bindir, self._package_folder) for bindir in cpp_info.bindirs]
+
+    def _get_includes(self, cpp_info):
+        return ['"{}"'.format(_relativize_path(path, self._package_folder))
+                for path in cpp_info.includedirs]
+
+    def _get_defines(self, cpp_info):
+        return ['"{}"'.format(define.replace('"', '\\' * 3 + '"'))
+                for define in cpp_info.defines]
+
+    def _get_linkopts(self, cpp_info):
+        os_build = self._dep.settings_build.get_safe("os")
+        link_opt = '/DEFAULTLIB:{}' if os_build == "Windows" else '-l{}'
+        system_libs = [link_opt.format(lib) for lib in cpp_info.system_libs]
+        shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags
+        return [f'"{flag}"' for flag in (system_libs + shared_flags)]
+
+    def _get_copts(self, cpp_info):
+        # FIXME: long discussions between copts (-Iflag) vs includes in Bazel. Not sure yet
+        # includedirsflags = ['"-I{}"'.format(_relativize_path(d, package_folder_path))
+        #                     for d in cpp_info.includedirs]
+        cxxflags = [var.replace('"', '\\"') for var in cpp_info.cxxflags]
+        cflags = [var.replace('"', '\\"') for var in cpp_info.cflags]
+        return [f'"{flag}"' for flag in (cxxflags + cflags)]
 
     def _get_component_requirement_names(self, cpp_info):
         """
@@ -518,7 +244,7 @@ class _InfoGenerator:
                 self.cpp_info.components["cmp"].requires = ["other::cmp1"]
         ```
         """
-        dep_ref_name = _get_package_reference_name(self._dep)
+        dep_ref_name = self._dep.ref.name
         ret = []
         for req in cpp_info.requires:
             pkg_ref_name, comp_ref_name = req.split("::") if "::" in req else (dep_ref_name, req)
@@ -528,47 +254,71 @@ class _InfoGenerator:
                 try:
                     req_conanfile = self._transitive_reqs[pkg_ref_name]
                     # Requirements declared in another dependency BUILD file
-                    prefix = f"@{_get_repository_name(req_conanfile, is_build_require=self._is_build_require)}//:"
+                    prefix = f"@{self._get_repository_name(req_conanfile)}//:"
                 except KeyError:
                     continue  # If the dependency is not in the transitive, might be skipped
             else:  # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello
                 req_conanfile = self._dep
-            comp_name = _get_component_name(req_conanfile, comp_ref_name)
+            comp_name = self._get_component_name(req_conanfile, comp_ref_name)
             dep_name = f"{prefix}{comp_name}"
             if dep_name not in ret:
                 ret.append(dep_name)
         return ret
 
-    @property
-    def components_info(self):
-        """
-        Get the whole package and its components information like their own requires, names and even
-        the cpp_info for each component.
+    def _get_lib_info(self, cpp_info, deduced_cpp_info, component_name=None):
 
-        :return: `list` of `_BazelTargetInfo` objects with all the components information
-        """
-        if not self._dep.cpp_info.has_components:
-            return []
-        components_info = []
-        # Loop through all the package's components
-        for comp_ref_name, cpp_info in self._dep.cpp_info.get_sorted_components().items():
-            # At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
-            comp_requires_names = self._get_component_requirement_names(cpp_info)
-            comp_name = _get_component_name(self._dep, comp_ref_name)
-            # Save each component information
-            components_info.append(_BazelTargetInfo(None, comp_name, comp_ref_name,
-                                                    comp_requires_names, cpp_info))
-        return components_info
+        def _lib_info(lib_name, virtual_cpp_info):
+            return {
+                "name": lib_name,
+                "is_shared": virtual_cpp_info.type == PackageType.SHARED,
+                "lib_path": _relativize_path(virtual_cpp_info.location, self._package_folder),
+                "import_lib_path": _relativize_path(virtual_cpp_info.link_location, self._package_folder)
+            }
 
-    @property
-    def root_package_info(self):
+        libs = cpp_info.libs
+        libs_info = []
+        if libs:
+            if len(libs) > 1:
+                for lib_name in libs:
+                    name = f'_{component_name}_{lib_name}' if component_name else f'_{lib_name}'
+                    virtual_cpp_info = deduced_cpp_info.components[name]
+                    libs_info.append(_lib_info(lib_name, virtual_cpp_info))
+            else:
+                lib_name = libs[0]
+                virtual_cpp_info = deduced_cpp_info.components[component_name] if component_name else deduced_cpp_info
+                libs_info.append(_lib_info(lib_name, virtual_cpp_info))
+        return libs_info
+
+    def _get_build_file_context(self):
         """
         Get the whole package information
 
         :return: `_BazelTargetInfo` object with the package information
         """
-        repository_name = _get_repository_name(self._dep, is_build_require=self._is_build_require)
-        pkg_name = _get_target_name(self._dep)
+        build_content = {"components": [], "root": {}}
+        component_names = []
+        deduced_cpp_info = self._dep.cpp_info.deduce_full_cpp_info(self._dep)
+
+        if self._dep.cpp_info.has_components:
+            # Loop through all the package's components
+            for comp_ref_name, cmp_cpp_info in self._dep.cpp_info.get_sorted_components().items():
+                # At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
+                comp_requires_names = self._get_component_requirement_names(cmp_cpp_info)
+                comp_name = self._get_component_name(self._dep, comp_ref_name)
+                component_names.append(comp_name)
+                build_content["components"].append({
+                    "name": comp_name,
+                    "libs": self._get_lib_info(cmp_cpp_info, deduced_cpp_info, component_name=comp_ref_name),
+                    "bindirs": self._get_bindirs(cmp_cpp_info),
+                    "headers": self._get_headers(cmp_cpp_info),
+                    "includes": self._get_includes(cmp_cpp_info),
+                    "defines": self._get_defines(cmp_cpp_info),
+                    "linkopts": self._get_linkopts(cmp_cpp_info),
+                    "copts": self._get_copts(cmp_cpp_info),
+                    "dependencies": comp_requires_names,
+                })
+
+        pkg_name = self._get_target_name(self._dep)
         # At first, let's check if we have defined some global requires, e.g., "other::cmp1"
         requires = self._get_component_requirement_names(self._dep.cpp_info)
         # If we have found some component requires it would be enough
@@ -576,12 +326,159 @@ class _InfoGenerator:
             # If no requires were found, let's try to get all the direct dependencies,
             # e.g., requires = "other_pkg/1.0"
             requires = [
-                f"@{_get_repository_name(req, is_build_require=self._is_build_require)}//:{_get_target_name(req)}"
+                f"@{self._get_repository_name(req)}//:{self._get_target_name(req)}"
                 for req in self._transitive_reqs.values()
             ]
         cpp_info = self._dep.cpp_info
-        return _BazelTargetInfo(repository_name, pkg_name, _get_package_reference_name(self._dep),
-                                requires, cpp_info)
+        build_content["root"] = {
+            "name": pkg_name,
+            "libs": self._get_lib_info(cpp_info, deduced_cpp_info),
+            "bindirs": self._get_bindirs(cpp_info),
+            "headers": self._get_headers(cpp_info),
+            "includes": self._get_includes(cpp_info),
+            "defines": self._get_defines(cpp_info),
+            "linkopts": self._get_linkopts(cpp_info),
+            "copts": self._get_copts(cpp_info),
+            "dependencies": requires,
+            "component_names": component_names
+        }
+        return build_content
+
+    @property
+    def dep_context(self):
+        """
+        Return the dependency context to fill later the conan_deps_module_extension.bzl and so on.
+        """
+        return {
+            'repository_name': self._get_repository_name(self._dep),
+            'package_folder': self._package_folder,
+            'package_build_file_path': self._absolute_build_file_path,
+        }
+
+    def items(self):
+        template = Template(self.dep_build_template, trim_blocks=True, lstrip_blocks=True,
+                            undefined=StrictUndefined)
+        content = template.render(self._get_build_file_context())
+        return {self._build_file_path: content}.items()
+
+
+class _BazelPathsGenerator:
+    """
+    Bazel 6.0 needs to know all the dependencies for its current project. So, the only way
+    to do that is to tell the WORKSPACE file how to load all the Conan ones. This is the goal
+    of the function created by this class, the ``load_conan_dependencies`` one.
+
+    More information:
+        * https://bazel.build/reference/be/workspace#new_local_repository
+
+    Bazel >= 7.1 needs to know all the dependencies as well, but provided via the MODULE.bazel file.
+    Therefor we provide a static repository rule to load the dependencies. This rule is used by a
+    module extension, passing the package path and the BUILD file path to the repository rule.
+    """
+    repository_filename = "dependencies.bzl"
+    modules_filename = "conan_deps_module_extension.bzl"
+    repository_rules_filename = "conan_deps_repo_rules.bzl"
+    repository_template = textwrap.dedent("""\
+        # This Bazel module should be loaded by your WORKSPACE file.
+        # Add these lines to your WORKSPACE one (assuming that you're using the "bazel_layout"):
+        # load("@//conan:dependencies.bzl", "load_conan_dependencies")
+        # load_conan_dependencies()
+
+        def load_conan_dependencies():
+        {% for dep_info in dependencies %}
+            native.new_local_repository(
+                name="{{dep_info['repository_name']}}",
+                path="{{dep_info['package_folder']}}",
+                build_file="{{dep_info['package_build_file_path']}}",
+            )
+        {% endfor %}
+        """)
+    module_template = textwrap.dedent("""\
+        # This module provides a repo for each requires-dependency in your conanfile.
+        # It's generated by the BazelDeps, and should be used in your Module.bazel file.
+        load(":conan_deps_repo_rules.bzl", "conan_dependency_repo")
+
+        def _load_dependencies_impl(mctx):
+        {% for dep_info in dependencies %}
+            conan_dependency_repo(
+                name = "{{dep_info['repository_name']}}",
+                package_path = "{{dep_info['package_folder']}}",
+                build_file_path = "{{dep_info['package_build_file_path']}}",
+            )
+        {% endfor %}
+
+            return mctx.extension_metadata(
+                # It will only warn you if any direct
+                # dependency is not imported by the 'use_repo' or even it is imported
+                # but not created. Notice that root_module_direct_dev_deps can not be None as we
+                # are giving 'all' value to root_module_direct_deps.
+                # Fix the 'use_repo' calls by running 'bazel mod tidy'
+                root_module_direct_deps = 'all',
+                root_module_direct_dev_deps = [],
+
+                # Prevent writing function content to lockfiles:
+                # - https://bazel.build/rules/lib/builtins/module_ctx#extension_metadata
+                # Important for remote build. Actually it's not reproducible, as local paths will
+                # be different on different machines. But we assume that conan works correctly here.
+                # IMPORTANT: Not compatible with bazel < 7.1
+                reproducible = True,
+            )
+
+        conan_extension = module_extension(
+            implementation = _load_dependencies_impl,
+            os_dependent = True,
+            arch_dependent = True,
+        )
+        """)
+    repository_rules_content = textwrap.dedent("""\
+        # This bazel repository rule is used to load Conan dependencies into the Bazel workspace.
+        # It's used by a generated module file that provides information about the conan packages.
+        # Each conan package is loaded into a bazel repository rule, with having the name of the
+        # package. The whole method is based on symlinks to not copy the whole package into the
+        # Bazel workspace, which is expensive.
+        def _conan_dependency_repo(rctx):
+            package_path = rctx.workspace_root.get_child(rctx.attr.package_path)
+
+            child_packages = package_path.readdir()
+            for child in child_packages:
+                rctx.symlink(child, child.basename)
+
+            rctx.symlink(rctx.attr.build_file_path, "BUILD.bazel")
+
+        conan_dependency_repo = repository_rule(
+            implementation = _conan_dependency_repo,
+            attrs = {
+                "package_path": attr.string(
+                    mandatory = True,
+                    doc = "The path to the Conan package in conan cache.",
+                ),
+                "build_file_path": attr.string(
+                    mandatory = True,
+                    doc = "The path to the BUILD file.",
+                ),
+            },
+        )
+        """)
+
+    @classmethod
+    def items(cls, dependencies_context):
+        if not dependencies_context:
+            return {}
+        # Bazel 6.x, but it'll likely be dropped soon
+        repository_template = Template(cls.repository_template, trim_blocks=True,
+                                       lstrip_blocks=True,
+                                       undefined=StrictUndefined)
+        content_6x = repository_template.render(dependencies=dependencies_context)
+        # Bazel 7.x files
+        module_template = Template(cls.module_template, trim_blocks=True, lstrip_blocks=True,
+                                   undefined=StrictUndefined)
+        content = module_template.render(dependencies=dependencies_context)
+        return {
+            cls.repository_filename: content_6x,  # bazel 6.x compatible
+            cls.modules_filename: content,
+            cls.repository_rules_filename: cls.repository_rules_content,
+            "BUILD.bazel": "# This is an empty BUILD file."  # Bazel needs this file in each subfolder
+        }.items()
 
 
 class BazelDeps:
@@ -594,6 +491,24 @@ class BazelDeps:
         #: Activates the build context for the specified Conan package names.
         self.build_context_activated = []
 
+    def _get_requirements(self, build_context_activated):
+        """
+        Simply save the activated requirements (host + build + test), and the deactivated ones
+        """
+        # All the requirements
+        host_req = self._conanfile.dependencies.host
+        build_req = self._conanfile.dependencies.direct_build  # tool_requires
+        test_req = self._conanfile.dependencies.test
+
+        for require, dep in list(host_req.items()) + list(build_req.items()) + list(
+            test_req.items()):
+            # Require is not used at the moment, but its information could be used,
+            # and will be used in Conan 2.0
+            # Filter the build_requires not activated with self.build_context_activated
+            if require.build and dep.ref.name not in build_context_activated:
+                continue
+            yield require, dep
+
     def generate(self):
         """
         Generates all the targets <DEP>/BUILD.bazel files, a dependencies.bzl (for bazel<7), a
@@ -620,25 +535,13 @@ class BazelDeps:
             use_repo(load_conan_dependencies, "dep-1", "dep-2", ...)
         """
         check_duplicated_generator(self, self._conanfile)
-        requirements = _get_requirements(self._conanfile, self.build_context_activated)
-        deps_info = []
-        for require, dep in requirements:
+        dependencies_context = []
+        for require, dep in self._get_requirements(self.build_context_activated):
             # Bazel info generator
-            info_generator = _InfoGenerator(self._conanfile, dep, require)
-            root_package_info = info_generator.root_package_info
-            components_info = info_generator.components_info
-            # Generating single BUILD files per dependency
-            bazel_generator = _BazelBUILDGenerator(self._conanfile, dep,
-                                                   root_package_info, components_info)
-            bazel_generator.generate()
-            # Saving pieces of information from each BUILD file
-            deps_info.append((
-                bazel_generator.repository_name,  # Bazel repository name == @repository_name
-                bazel_generator.package_folder,  # path to the Conan dependency folder
-                bazel_generator.absolute_build_file_pah  # path to the BUILD.bazel file created
-            ))
-        if deps_info:
-            # dependencies.bzl has all the information about where to look for the dependencies
-            bazel_dependencies_module_generator = _BazelDependenciesBZLGenerator(self._conanfile,
-                                                                                 deps_info)
-            bazel_dependencies_module_generator.generate()
+            dep_build_generator = _BazelDepBuildGenerator(self._conanfile, dep, require)
+            dependencies_context.append(dep_build_generator.dep_context)
+            for name, content in dep_build_generator.items():
+                save(name, content)
+
+        for name, content in _BazelPathsGenerator.items(dependencies_context):
+            save(name, content)


Test patch:
diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py b/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py
index 87124f69d..3bee9360f 100644
--- a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py
+++ b/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py
@@ -1515,3 +1515,51 @@ class TestCppInfoChecks:
         args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}"
         c.run(f"install --requires=dep/0.1 {args}", assert_error=True)
         assert "dep/0.1 cpp_info incorrect .type shared-library for .exe myexe" in c.out
+
+
+
+def test_multiple_find_package_subfolder():
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+    class TestPackage(ConanFile):
+        name = "matrix"
+        version = "1.0"
+        def package_info(self):
+            self.cpp_info.system_libs = ["mysystemlib"]
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .")
+
+    cmake = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(app NONE)
+
+        find_package(matrix CONFIG REQUIRED)
+        add_subdirectory(subdir)
+        """)
+    subcmake = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(subdir NONE)
+
+        find_package(matrix CONFIG REQUIRED)
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import CMake
+        class Pkg(ConanFile):
+            requires = "matrix/1.0"
+            generators = "CMakeToolchain", "CMakeDeps"
+            settings = "os", "compiler", "build_type", "arch"
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+        """)
+    c.save({"conanfile.py": conanfile,
+            "CMakeLists.txt": cmake,
+            "subdir/CMakeLists.txt": subcmake}, clean_first=True)
+
+    c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}")
+    assert "find_package(matrix)" in c.out
+    assert "target_link_libraries(... matrix::matrix)" in c.out
+    assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out
diff --git a/test/integration/command/list/list_test.py b/test/integration/command/list/list_test.py
index fddcacf42..5d04ed535 100644
--- a/test/integration/command/list/list_test.py
+++ b/test/integration/command/list/list_test.py
@@ -10,10 +10,14 @@ import pytest
 
 from conan.internal.errors import ConanConnectionError
 from conan.errors import ConanException
+from conan.test.assets.cmake import gen_cmakelists
 from conan.test.assets.genconanfile import GenConanfile
-from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID
+from conan.test.assets.sources import gen_function_h, gen_function_cpp
+from conan.test.utils.file_server import TestFileServer
+from conan.test.utils.test_files import temp_folder
+from conan.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID, zipdir
 from conan.test.utils.env import environment_update
-from conan.internal.util.files import save
+from conan.internal.util.files import save, save_files, sha256sum
 
 
 class TestParamErrors:
@@ -947,3 +951,31 @@ def test_overlapping_versions():
     tc.run("list * -c -f=json", redirect_stdout="list.json")
     results = json.loads(tc.load("list.json"))
     assert len(results["Local Cache"]) == 2
+
+def test_list_local_recipe_index():
+    c = TestClient(light=True)
+    c.run(f"new local_recipes_index -d name=pkg -d version=0.1 -d url='https://conan-fake-url.com' ")
+    c.run(f"remote add local '{c.current_folder}'")
+
+    c.run("list '*' -r=local")
+    assert "Found 1 pkg/version recipes matching * in local" in c.out
+    c.run("list '*/*' -r=local")
+    assert "Found 1 pkg/version recipes matching */* in local" in c.out
+    c.run("list '*/0.1' -r=local")
+    assert "Found 1 pkg/version recipes matching */0.1 in local" in c.out
+    c.run("list 'pkg/*' -r=local")
+    assert "Found 1 pkg/version recipes matching pkg/* in local" in c.out
+    c.run("list 'pkg/0.*' -r=local")
+    assert "Found 1 pkg/version recipes matching pkg/0.* in local" in c.out
+    c.run("list 'pkg' -r=local")
+    assert "Found 1 pkg/version recipes matching pkg in local" in c.out
+    c.run("list 'pkg/0.1' -r=local")
+    assert "Found 1 pkg/version recipes matching pkg/0.1 in local" in c.out
+    c.run("list 'foo' -r=local")
+    assert "ERROR: Recipe 'foo' not found" in c.out
+    c.run("list '/#@&%%&@#/' -r=local")
+    assert "ERROR: Recipe '/' not found" in c.out
+    c.run("list 'pkg/0.1@a' -r=local")
+    assert "ERROR: Recipe 'pkg/0.1@a' not found" in c.out
+    c.run("list 'pkg%0.1#a@b/c' -r=local")
+    assert "ERROR: Recipe 'pkg%0.1' not found" in c.out
diff --git a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
index 30228b704..6639dc44f 100644
--- a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
+++ b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
@@ -234,7 +234,8 @@ def test_system_wrappers():
           f"-c tools.cmake.cmakedeps:new={new_value}")
     cmake = c.load("lib-Targets-release.cmake")
     assert "add_library(lib::lib INTERFACE IMPORTED)" in cmake
-    assert "target_link_libraries(lib::lib INTERFACE my_system_cool_lib)" in cmake
+    assert "set_property(TARGET lib::lib APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \
+           '             $<$<CONFIG:RELEASE>:my_system_cool_lib>)' in cmake
 
 
 def test_autolink_pragma():
diff --git a/test/integration/toolchains/google/test_bazeldeps.py b/test/integration/toolchains/google/test_bazeldeps.py
index 843174fa8..d243b34cd 100644
--- a/test/integration/toolchains/google/test_bazeldeps.py
+++ b/test/integration/toolchains/google/test_bazeldeps.py
@@ -644,7 +644,7 @@ def test_with_editable_layout():
         # Bazel 7.x
         content = client.load("conan_deps_module_extension.bzl")
         assert textwrap.dedent(f"""\
-        def _load_dependenies_impl(mctx):
+        def _load_dependencies_impl(mctx):
             conan_dependency_repo(
                 name = "dep",
                 package_path = "{recipes_folder}/dep",
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", [