Repository: conan-io/conan
Pull request #16443 (2024-06-08 21:33:13)

explore replace_requires references adjustements
Changelog: Feature: Allow ``[replace_requires]`` to replace the package name and ``self.dependencies`` still works with the old name.
Docs: Omit

Close https://github.com/conan-io/conan/issues/16399

This is still an idea, need more research and there are pending things to fix like ``self.cpp_info.requires`` references


Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 839672431..dd62c1956 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -170,6 +170,7 @@ class CacheAPI:
             the_tar = tarfile.open(fileobj=file_handler)
             fileobj = the_tar.extractfile("pkglist.json")
             pkglist = fileobj.read()
+            the_tar.extraction_filter = (lambda member, _: member)  # fully_trusted (Py 3.14)
             the_tar.extractall(path=cache_folder)
             the_tar.close()
 
diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py
index 28de3d94c..cdb86f783 100644
--- a/conan/internal/conan_app.py
+++ b/conan/internal/conan_app.py
@@ -11,7 +11,6 @@ from conans.client.loader import ConanFileLoader, load_python_file
 from conans.client.remote_manager import RemoteManager
 from conans.client.rest.auth_manager import ConanApiAuthManager
 from conans.client.rest.conan_requester import ConanRequester
-from conans.client.rest.rest_client import RestApiClientFactory
 from conan.internal.api.remotes.localdb import LocalDB
 
 
@@ -52,11 +51,9 @@ class ConanApp:
         # Wraps an http_requester to inject proxies, certs, etc
         self.requester = ConanRequester(global_conf, cache_folder)
         # To handle remote connections
-        rest_client_factory = RestApiClientFactory(self.requester, global_conf)
         # Wraps RestApiClient to add authentication support (same interface)
         self.localdb = LocalDB(cache_folder)
-        auth_manager = ConanApiAuthManager(rest_client_factory, cache_folder, self.localdb,
-                                           global_conf)
+        auth_manager = ConanApiAuthManager(self.requester, cache_folder, self.localdb, global_conf)
         # Handle remote connections
         self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder)
 
diff --git a/conan/tools/cmake/cmakedeps/cmakedeps.py b/conan/tools/cmake/cmakedeps/cmakedeps.py
index f4b49bd1e..e37f270cc 100644
--- a/conan/tools/cmake/cmakedeps/cmakedeps.py
+++ b/conan/tools/cmake/cmakedeps/cmakedeps.py
@@ -155,7 +155,11 @@ class CMakeDeps(object):
             dep_name) in self.build_context_activated and dep.context == "build" else ""
         dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}"
         try:
-            return self._properties[f"{dep_comp}{build_suffix}"][prop]
+            value = self._properties[f"{dep_comp}{build_suffix}"][prop]
+            if check_type is not None and not isinstance(value, check_type):
+                raise ConanException(
+                    f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found')
+            return value
         except KeyError:
             return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \
                 else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type)
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index 7ce0761ec..74aa1c2f0 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -700,6 +700,7 @@ class UserToolchain(Block):
         # Include one or more CMake user toolchain from tools.cmake.cmaketoolchain:user_toolchain
 
         {% for user_toolchain in paths %}
+        message(STATUS "Conan toolchain: Including user_toolchain: {{user_toolchain}}")
         include("{{user_toolchain}}")
         {% endfor %}
         """)
diff --git a/conan/tools/files/files.py b/conan/tools/files/files.py
index c69dd26fb..bbb96bf90 100644
--- a/conan/tools/files/files.py
+++ b/conan/tools/files/files.py
@@ -92,7 +92,7 @@ def rm(conanfile, pattern, folder, recursive=False, excludes=None):
 
 def get(conanfile, url, md5=None, sha1=None, sha256=None, destination=".", filename="",
         keep_permissions=False, pattern=None, verify=True, retry=None, retry_wait=None,
-        auth=None, headers=None, strip_root=False):
+        auth=None, headers=None, strip_root=False, extract_filter=None):
     """
     High level download and decompressing of a tgz, zip or other compressed format file.
     Just a high level wrapper for download, unzip, and remove the temporary zip file once unzipped.
@@ -115,6 +115,7 @@ def get(conanfile, url, md5=None, sha1=None, sha256=None, destination=".", filen
     :param auth:  forwarded to ``tools.file.download()``.
     :param headers:  forwarded to ``tools.file.download()``.
     :param strip_root: forwarded to ``tools.file.unzip()``.
+    :param extract_filter: forwarded to ``tools.file.unzip()``.
     """
 
     if not filename:  # deduce filename from the URL
@@ -128,7 +129,7 @@ def get(conanfile, url, md5=None, sha1=None, sha256=None, destination=".", filen
              retry=retry, retry_wait=retry_wait, auth=auth, headers=headers,
              md5=md5, sha1=sha1, sha256=sha256)
     unzip(conanfile, filename, destination=destination, keep_permissions=keep_permissions,
-          pattern=pattern, strip_root=strip_root)
+          pattern=pattern, strip_root=strip_root, extract_filter=extract_filter)
     os.unlink(filename)
 
 
@@ -262,7 +263,7 @@ def chdir(conanfile, newdir):
 
 
 def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=None,
-          strip_root=False):
+          strip_root=False, extract_filter=None):
     """
     Extract different compressed formats
 
@@ -277,14 +278,18 @@ def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=
            This should be a Unix shell-style wildcard, see fnmatch documentation for more details.
     :param strip_root: (Optional, Defaulted to False) If True, and all the unzipped contents are
            in a single folder it will flat the folder moving all the contents to the parent folder.
+    :param extract_filter: (Optional, defaulted to None). When extracting a tar file,
+           use the tar extracting filters define by Python in
+           https://docs.python.org/3/library/tarfile.html
     """
 
     output = conanfile.output
+    extract_filter = conanfile.conf.get("tools.files.unzip:filter") or extract_filter
     output.info(f"Unzipping {filename} to {destination}")
     if (filename.endswith(".tar.gz") or filename.endswith(".tgz") or
             filename.endswith(".tbz2") or filename.endswith(".tar.bz2") or
             filename.endswith(".tar")):
-        return untargz(filename, destination, pattern, strip_root)
+        return untargz(filename, destination, pattern, strip_root, extract_filter)
     if filename.endswith(".gz"):
         target_name = filename[:-3] if destination == "." else destination
         target_dir = os.path.dirname(target_name)
@@ -295,7 +300,7 @@ def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=
                 shutil.copyfileobj(fin, fout)
         return
     if filename.endswith(".tar.xz") or filename.endswith(".txz"):
-        return untargz(filename, destination, pattern, strip_root)
+        return untargz(filename, destination, pattern, strip_root, extract_filter)
 
     import zipfile
     full_path = os.path.normpath(os.path.join(os.getcwd(), destination))
@@ -363,10 +368,12 @@ def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=
         output.writeln("")
 
 
-def untargz(filename, destination=".", pattern=None, strip_root=False):
+def untargz(filename, destination=".", pattern=None, strip_root=False, extract_filter=None):
     # NOT EXPOSED at `conan.tools.files` but used in tests
     import tarfile
     with tarfile.TarFile.open(filename, 'r:*') as tarredgzippedFile:
+        f = getattr(tarfile, f"{extract_filter}_filter", None) if extract_filter else None
+        tarredgzippedFile.extraction_filter = f or (lambda member_, _: member_)
         if not pattern and not strip_root:
             tarredgzippedFile.extractall(destination)
         else:
diff --git a/conan/tools/gnu/pkgconfigdeps.py b/conan/tools/gnu/pkgconfigdeps.py
index 3cd688b15..561085bd0 100644
--- a/conan/tools/gnu/pkgconfigdeps.py
+++ b/conan/tools/gnu/pkgconfigdeps.py
@@ -347,7 +347,11 @@ class _PCGenerator:
         dep_name = dep.ref.name
         dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}"
         try:
-            return self._properties[f"{dep_comp}{self._suffix}"][prop]
+            value = self._properties[f"{dep_comp}{self._suffix}"][prop]
+            if check_type is not None and not isinstance(value, check_type):
+                raise ConanException(
+                    f'The expected type for {prop} is "{check_type.__name__}", but "{type(value).__name__}" was found')
+            return value
         except KeyError:
             return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \
                 else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type)
diff --git a/conan/tools/google/bazeldeps.py b/conan/tools/google/bazeldeps.py
index 4cb213e88..916e13f42 100644
--- a/conan/tools/google/bazeldeps.py
+++ b/conan/tools/google/bazeldeps.py
@@ -10,8 +10,8 @@ from conan.internal import check_duplicated_generator
 from conans.model.dependencies import get_transitive_requires
 from conans.util.files import save
 
-_BazelTargetInfo = namedtuple("DepInfo", ['repository_name', 'name', 'requires', 'cpp_info'])
-_LibInfo = namedtuple("LibInfo", ['name', 'is_shared', 'lib_path', 'interface_lib_path'])
+_BazelTargetInfo = namedtuple("DepInfo", ['repository_name', 'name', 'ref_name', 'requires', 'cpp_info'])
+_LibInfo = namedtuple("LibInfo", ['name', 'is_shared', 'lib_path', 'import_lib_path'])
 
 
 def _get_name_with_namespace(namespace, name):
@@ -72,18 +72,19 @@ def _get_requirements(conanfile, build_context_activated):
         yield require, dep
 
 
-def _get_libs(dep, cpp_info=None) -> list:
+def _get_libs(dep, cpp_info=None, reference_name=None) -> list:
     """
     Get the static/shared library paths
 
     :param dep: normally a <ConanFileInterface obj>
     :param cpp_info: <CppInfo obj> of the component.
+    :param reference_name: <str> Package/Component's reference name. ``None`` by default.
     :return: list of tuples per static/shared library ->
-             [(lib_name, is_shared, library_path, interface_library_path)]
+             [(name, is_shared, lib_path, import_lib_path)]
              Note: ``library_path`` could be both static and shared ones in case of UNIX systems.
                     Windows would have:
-                        * shared: library_path as DLL, and interface_library_path as LIB
-                        * static: library_path as LIB, and interface_library_path as None
+                        * shared: library_path as DLL, and import_library_path as LIB
+                        * static: library_path as LIB, and import_library_path as None
     """
     def _is_shared():
         """
@@ -93,22 +94,52 @@ def _get_libs(dep, cpp_info=None) -> list:
         return {"shared-library": True,
                 "static-library": False}.get(str(dep.package_type), default_value)
 
-    def _save_lib_path(lib_, lib_path_):
+    def _save_lib_path(file_name, file_path):
         """Add each lib with its full library path"""
-        formatted_path = lib_path_.replace("\\", "/")
-        _, ext_ = os.path.splitext(formatted_path)
-        if is_shared and ext_ == ".lib":  # Windows interface library
-            interface_lib_paths[lib_] = formatted_path
-        else:
-            lib_paths[lib_] = formatted_path
+        name, ext = file_name.split('.', maxsplit=1)  # ext could be .if.lib
+        formatted_path = file_path.replace("\\", "/")
+        lib_name = None
+        # Users may not name their libraries in a conventional way. For example, directly
+        # use the basename of the lib file as lib name, e.g., cpp_info.libs = ["liblib1.a"]
+        # Issue related: https://github.com/conan-io/conan/issues/11331
+        if file_name in libs:  # let's ensure that it has any extension
+            lib_name = file_name
+        elif name in libs:
+            lib_name = name
+        elif name.startswith("lib"):
+            short_name = name[3:]  # libpkg -> pkg
+            if short_name in libs:
+                lib_name = short_name
+        # FIXME: CPS will be in charge of defining correctly this part. At the moment, we can
+        #        only guess the name of the DLL binary as it does not follow any Conan pattern.
+        if ext == "dll" or ext.endswith(".dll"):
+            if lib_name:
+                shared_windows_libs[lib_name] = formatted_path
+            elif total_libs_number == 1 and libs[0] not in shared_windows_libs:
+                shared_windows_libs[libs[0]] = formatted_path
+            else:  # let's cross the fingers... This is the last chance.
+                for lib in libs:
+                    if ref_name in name and ref_name in lib and lib not in shared_windows_libs:
+                        shared_windows_libs[lib] = formatted_path
+                        break
+        elif lib_name is not None:
+            lib_paths[lib_name] = formatted_path
 
     cpp_info = cpp_info or dep.cpp_info
+    libs = cpp_info.libs[:]  # copying the values
+    if not libs:  # no libraries declared
+        return []
     is_shared = _is_shared()
     libdirs = cpp_info.libdirs
     bindirs = cpp_info.bindirs if is_shared else []  # just want to get shared libraries
-    libs = cpp_info.libs[:]  # copying the values
+    ref_name = reference_name or dep.ref.name
+    if hasattr(cpp_info, "aggregated_components"):
+        # Global cpp_info
+        total_libs_number = len(cpp_info.aggregated_components().libs)
+    else:
+        total_libs_number = len(libs)
     lib_paths = {}
-    interface_lib_paths = {}
+    shared_windows_libs = {}
     for libdir in set(libdirs + bindirs):
         if not os.path.exists(libdir):
             continue
@@ -117,35 +148,22 @@ def _get_libs(dep, cpp_info=None) -> list:
             full_path = os.path.join(libdir, f)
             if not os.path.isfile(full_path):  # Make sure that directories are excluded
                 continue
-            name, ext = os.path.splitext(f)
-            # Users may not name their libraries in a conventional way. For example, directly
-            # use the basename of the lib file as lib name, e.g., cpp_info.libs = ["liblib1.a"]
-            # Issue related: https://github.com/conan-io/conan/issues/11331
-            if ext and f in libs:  # let's ensure that it has any extension
+            _, ext = os.path.splitext(f)
+            if is_shared and ext in (".so", ".dylib", ".lib", ".dll"):
+                _save_lib_path(f, full_path)
+            elif not is_shared and ext in (".a", ".lib"):
                 _save_lib_path(f, full_path)
-                continue
-            if name not in libs and name.startswith("lib"):
-                name = name[3:]  # libpkg -> pkg
-            # FIXME: Should it read a conf variable to know unexpected extensions?
-            if (is_shared and ext in (".so", ".dylib", ".lib", ".dll")) or \
-               (not is_shared and ext in (".a", ".lib")):
-                if name in libs:
-                    _save_lib_path(name, full_path)
-                    continue
-                else:  # last chance: some cases the name could be pkg.if instead of pkg
-                    name = name.split(".", maxsplit=1)[0]
-                    if name in libs:
-                        _save_lib_path(name, full_path)
 
     libraries = []
-    for lib, lib_path in lib_paths.items():
-        interface_lib_path = None
-        if lib_path.endswith(".dll"):
-            if lib not in interface_lib_paths:
+    for name, lib_path in lib_paths.items():
+        import_lib_path = None
+        if is_shared and os.path.splitext(lib_path)[1] == ".lib":
+            if name not in shared_windows_libs:
                 raise ConanException(f"Windows needs a .lib for link-time and .dll for runtime."
                                      f" Only found {lib_path}")
-            interface_lib_path = interface_lib_paths.pop(lib)
-        libraries.append((lib, is_shared, lib_path, interface_lib_path))
+            import_lib_path = lib_path  # .lib
+            lib_path = shared_windows_libs.pop(name)  # .dll
+        libraries.append((name, is_shared, lib_path, import_lib_path))
     # TODO: Would we want to manage the cases where DLLs are provided by the system?
     return libraries
 
@@ -345,8 +363,8 @@ class _BazelBUILDGenerator:
         {% else %}
         static_library = "{{ lib_info.lib_path }}",
         {% endif %}
-        {% if lib_info.interface_lib_path %}
-        interface_library = "{{ lib_info.interface_lib_path }}",
+        {% if lib_info.import_lib_path %}
+        interface_library = "{{ lib_info.import_lib_path }}",
         {% endif %}
     )
     {% endfor %}
@@ -498,12 +516,12 @@ class _BazelBUILDGenerator:
                 libs_info = []
                 bindirs = [_relativize_path(bindir, package_folder_path)
                            for bindir in cpp_info.bindirs]
-                for (lib, is_shared, lib_path, interface_lib_path) in libs:
+                for (lib, is_shared, lib_path, import_lib_path) in libs:
                     # Bazel needs to relativize each path
                     libs_info.append(
                         _LibInfo(lib, is_shared,
                                  _relativize_path(lib_path, package_folder_path),
-                                 _relativize_path(interface_lib_path, package_folder_path))
+                                 _relativize_path(import_lib_path, package_folder_path))
                     )
                 ret.update({
                     "libs": libs_info,
@@ -599,7 +617,8 @@ class _InfoGenerator:
             comp_requires_names = self._get_cpp_info_requires_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_requires_names, cpp_info))
+            components_info.append(_BazelTargetInfo(None, comp_name, comp_ref_name,
+                                                    comp_requires_names, cpp_info))
         return components_info
 
     @property
@@ -622,7 +641,8 @@ class _InfoGenerator:
                 for req in self._transitive_reqs.values()
             ]
         cpp_info = self._dep.cpp_info
-        return _BazelTargetInfo(repository_name, pkg_name, requires, cpp_info)
+        return _BazelTargetInfo(repository_name, pkg_name, _get_package_reference_name(self._dep),
+                                requires, cpp_info)
 
 
 class BazelDeps:
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index 284a05b20..d79cf16ba 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -67,6 +67,7 @@ class Node(object):
         self.should_build = False  # If the --build or policy wants to build this binary
         self.build_allowed = False
         self.is_conf = False
+        self.replaced_requires = {}  # To track the replaced requires for self.dependencies[old-ref]
 
     def __lt__(self, other):
         """
@@ -135,7 +136,7 @@ class Node(object):
             if require.build and (self.context == CONTEXT_HOST or  # switch context
                                   require.ref.version != self.ref.version):  # or different version
                 pass
-            elif require.visible is False and require.ref.version != self.ref.version:
+            elif require.visible is False: #  and require.ref.version != self.ref.version:
                 # Experimental, to support repackaging of openssl previous versions FIPS plugins
                 pass  # An invisible require doesn't conflict with itself
             else:
@@ -177,15 +178,17 @@ class Node(object):
         source_node = dependant.src
         return source_node.check_downstream_exists(down_require) or result
 
-    def check_loops(self, new_node):
+    def check_loops(self, new_node, count=0):
         if self.ref == new_node.ref and self.context == new_node.context:
-            return self
+            if count >= 1:
+                return self
+            count += 1
         if not self.dependants:
             return
         assert len(self.dependants) == 1
         dependant = self.dependants[0]
         source_node = dependant.src
-        return source_node.check_loops(new_node)
+        return source_node.check_loops(new_node, count)
 
     @property
     def package_id(self):
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 41793dee9..a442fa4a1 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -312,6 +312,7 @@ class DepsGraphBuilder(object):
                 node.conanfile.requires.reindex(require, alternative_ref.name)
             require.ref.name = alternative_ref.name
             graph.replaced_requires[original_require] = repr(require.ref)
+            node.replaced_requires[original_require] = require
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index 35b5c9ddc..8320bb86c 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -16,18 +16,20 @@ from uuid import getnode as get_mac
 
 from conan.api.output import ConanOutput
 from conans.client.rest.remote_credentials import RemoteCredentials
+from conans.client.rest.rest_client import RestApiClient
 from conans.errors import AuthenticationException, ConanException, ForbiddenException
 
 LOGIN_RETRIES = 3
 
 
-class ConanApiAuthManager(object):
+class ConanApiAuthManager:
 
-    def __init__(self, rest_client_factory, cache_folder, localdb, global_conf):
-        self._rest_client_factory = rest_client_factory
+    def __init__(self, requester, cache_folder, localdb, global_conf):
+        self._requester = requester
         self._localdb = localdb
         self._global_conf = global_conf
         self._cache_folder = cache_folder
+        self._cached_capabilities = {}  # common to all RestApiClient
 
     def call_rest_api_method(self, remote, method_name, *args, **kwargs):
         """Handles AuthenticationException and request user to input a user and a password"""
@@ -89,7 +91,8 @@ class ConanApiAuthManager(object):
         username, token, refresh_token = self._localdb.get_login(remote.url)
         custom_headers = {'X-Client-Anonymous-Id': self._get_mac_digest(),
                           'X-Client-Id': str(username or "")}
-        return self._rest_client_factory.new(remote, token, refresh_token, custom_headers)
+        return RestApiClient(remote, token, refresh_token, custom_headers, self._requester,
+                             self._global_conf, self._cached_capabilities)
 
     def _clear_user_tokens_in_db(self, user, remote):
         try:
diff --git a/conans/client/rest/file_uploader.py b/conans/client/rest/file_uploader.py
index 025f44595..1eadc2e6e 100644
--- a/conans/client/rest/file_uploader.py
+++ b/conans/client/rest/file_uploader.py
@@ -1,6 +1,5 @@
 import os
 import time
-from copy import copy
 
 from conan.api.output import ConanOutput, TimedOutput
 from conans.client.rest import response_to_str
@@ -12,7 +11,6 @@ from conans.util.files import sha1sum
 class FileUploader(object):
 
     def __init__(self, requester, verify, config, source_credentials=None):
-        self._output = ConanOutput()
         self._requester = requester
         self._config = config
         self._verify_ssl = verify
@@ -54,15 +52,14 @@ class FileUploader(object):
         return bool(response.ok)
 
     def upload(self, url, abs_path, auth=None, dedup=False, retry=None, retry_wait=None,
-               headers=None, ref=None):
+               ref=None):
         retry = retry if retry is not None else self._config.get("core.upload:retry", default=1,
                                                                  check_type=int)
         retry_wait = retry_wait if retry_wait is not None else \
             self._config.get("core.upload:retry_wait", default=5, check_type=int)
 
         # Send always the header with the Sha1
-        headers = copy(headers) or {}
-        headers["X-Checksum-Sha1"] = sha1sum(abs_path)
+        headers = {"X-Checksum-Sha1": sha1sum(abs_path)}
         if dedup:
             response = self._dedup(url, headers, auth)
             if response:
@@ -78,9 +75,8 @@ class FileUploader(object):
                 if counter == retry:
                     raise
                 else:
-                    if self._output:
-                        self._output.warning(exc, warn_tag="network")
-                        self._output.info("Waiting %d seconds to retry..." % retry_wait)
+                    ConanOutput().warning(exc, warn_tag="network")
+                    ConanOutput().info("Waiting %d seconds to retry..." % retry_wait)
                     time.sleep(retry_wait)
 
     def _upload_file(self, url, abs_path, headers, auth, ref):
diff --git a/conans/client/rest/rest_client.py b/conans/client/rest/rest_client.py
index 00e8e9b4f..76eade345 100644
--- a/conans/client/rest/rest_client.py
+++ b/conans/client/rest/rest_client.py
@@ -3,21 +3,7 @@ from conans.client.rest.rest_client_v2 import RestV2Methods
 from conans.errors import AuthenticationException, ConanException
 
 
-class RestApiClientFactory(object):
-
-    def __init__(self, requester, config):
-        self._requester = requester
-        self._config = config
-        self._cached_capabilities = {}
-
-    def new(self, remote, token, refresh_token, custom_headers):
-        tmp = RestApiClient(remote, token, refresh_token, custom_headers,
-                            self._requester, self._config,
-                            self._cached_capabilities)
-        return tmp
-
-
-class RestApiClient(object):
+class RestApiClient:
     """
         Rest Api Client for handle remote.
     """
@@ -114,9 +100,6 @@ class RestApiClient(object):
     def remove_packages(self, prefs):
         return self._get_api().remove_packages(prefs)
 
-    def server_capabilities(self):
-        return self._get_api().server_capabilities()
-
     def get_recipe_revisions_references(self, ref):
         return self._get_api().get_recipe_revisions_references(ref)
 
diff --git a/conans/client/rest/rest_client_common.py b/conans/client/rest/rest_client_common.py
deleted file mode 100644
index 9a3f76243..000000000
--- a/conans/client/rest/rest_client_common.py
+++ /dev/null
@@ -1,224 +0,0 @@
-import json
-
-from requests.auth import AuthBase, HTTPBasicAuth
-
-from conans.client.rest import response_to_str
-from conans.errors import (EXCEPTION_CODE_MAPPING, ConanException,
-                           AuthenticationException, RecipeNotFoundException,
-                           PackageNotFoundException)
-from conans.model.recipe_ref import RecipeReference
-
-
-class JWTAuth(AuthBase):
-    """Attaches JWT Authentication to the given Request object."""
-
-    def __init__(self, token):
-        self.token = token
-
-    def __call__(self, request):
-        if self.token:
-            request.headers['Authorization'] = "Bearer %s" % str(self.token)
-        return request
-
-
-def get_exception_from_error(error_code):
-    tmp = {v: k for k, v in EXCEPTION_CODE_MAPPING.items()  # All except NotFound
-           if k not in (RecipeNotFoundException, PackageNotFoundException)}
-    if error_code in tmp:
-        # logger.debug("REST ERROR: %s" % str(tmp[error_code]))
-        return tmp[error_code]
-    else:
-        base_error = int(str(error_code)[0] + "00")
-        # logger.debug("REST ERROR: %s" % str(base_error))
-        try:
-            return tmp[base_error]
-        except KeyError:
-            return None
-
-
-class RestCommonMethods(object):
-
-    def __init__(self, remote_url, token, custom_headers, requester, config, verify_ssl):
-        self.token = token
-        self.remote_url = remote_url
-        self.custom_headers = custom_headers
-        self.requester = requester
-        self._config = config
-        self.verify_ssl = verify_ssl
-
-    @property
-    def auth(self):
-        return JWTAuth(self.token)
-
-    @staticmethod
-    def _check_error_response(ret):
-        if ret.status_code == 401:
-            raise AuthenticationException("Wrong user or password")
-        # Cannot check content-type=text/html, conan server is doing it wrong
-        if not ret.ok or "html>" in str(ret.content):
-            raise ConanException("%s\n\nInvalid server response, check remote URL and "
-                                 "try again" % str(ret.content))
-
-    def authenticate(self, user, password):
-        """Sends user + password to get:
-          - A plain response with a regular token (not supported refresh in the remote) and None
-        """
-        auth = HTTPBasicAuth(user, password)
-        url = self.router.common_authenticate()
-        # logger.debug("REST: Authenticate to get access_token: %s" % url)
-        ret = self.requester.get(url, auth=auth, headers=self.custom_headers,
-                                 verify=self.verify_ssl)
-
-        self._check_error_response(ret)
-        return ret.content.decode()
-
-    def authenticate_oauth(self, user, password):
-        """Sends user + password to get:
-            - A json with an access_token and a refresh token (if supported in the remote)
-                    Artifactory >= 6.13.X
-        """
-        url = self.router.oauth_authenticate()
-        auth = HTTPBasicAuth(user, password)
-        headers = {}
-        headers.update(self.custom_headers)
-        headers["Content-type"] = "application/x-www-form-urlencoded"
-        # logger.debug("REST: Authenticating with OAUTH: %s" % url)
-        ret = self.requester.post(url, auth=auth, headers=headers, verify=self.verify_ssl)
-        self._check_error_response(ret)
-
-        data = ret.json()
-        access_token = data["access_token"]
-        refresh_token = data["refresh_token"]
-        # logger.debug("REST: Obtained refresh and access tokens")
-        return access_token, refresh_token
-
-    def refresh_token(self, token, refresh_token):
-        """Sends access_token and the refresh_token to get a pair of
-        access_token and refresh token
-
-        Artifactory >= 6.13.X
-        """
-        url = self.router.oauth_authenticate()
-        # logger.debug("REST: Refreshing Token: %s" % url)
-        headers = {}
-        headers.update(self.custom_headers)
-        headers["Content-type"] = "application/x-www-form-urlencoded"
-        payload = {'access_token': token, 'refresh_token': refresh_token,
-                   'grant_type': 'refresh_token'}
-        ret = self.requester.post(url, headers=headers, verify=self.verify_ssl, data=payload)
-        self._check_error_response(ret)
-
-        data = ret.json()
-        if "access_token" not in data:
-            # logger.debug("REST: unexpected data from server: {}".format(data))
-            raise ConanException("Error refreshing the token")
-
-        new_access_token = data["access_token"]
-        new_refresh_token = data["refresh_token"]
-        # logger.debug("REST: Obtained new refresh and access tokens")
-        return new_access_token, new_refresh_token
-
-    def check_credentials(self):
-        """If token is not valid will raise AuthenticationException.
-        User will be asked for new user/pass"""
-        url = self.router.common_check_credentials()
-        # logger.debug("REST: Check credentials: %s" % url)
-        ret = self.requester.get(url, auth=self.auth, headers=self.custom_headers,
-                                 verify=self.verify_ssl)
-        if ret.status_code != 200:
-            ret.charset = "utf-8"  # To be able to access ret.text (ret.content are bytes)
-            text = ret.text if ret.status_code != 404 else "404 Not found"
-            raise get_exception_from_error(ret.status_code)(text)
-        return ret.content.decode()
-
-    def server_capabilities(self, user=None, password=None):
-        """Get information about the server: status, version, type and capabilities"""
-        url = self.router.ping()
-        # logger.debug("REST: ping: %s" % url)
-        if user and password:
-            # This can happen in "conan remote login" cmd. Instead of empty token, use HttpBasic
-            auth = HTTPBasicAuth(user, password)
-        else:
-            auth = self.auth
-        ret = self.requester.get(url, auth=auth, headers=self.custom_headers, verify=self.verify_ssl)
-
-        server_capabilities = ret.headers.get('X-Conan-Server-Capabilities')
-        if not server_capabilities and not ret.ok:
-            # Old Artifactory might return 401/403 without capabilities, we don't want
-            # to cache them #5687, so raise the exception and force authentication
-            raise get_exception_from_error(ret.status_code)(response_to_str(ret))
-        if server_capabilities is None:
-            # Some servers returning 200-ok, even if not valid repo
-            raise ConanException(f"Remote {self.remote_url} doesn't seem like a valid Conan remote")
-
-        return [cap.strip() for cap in server_capabilities.split(",") if cap]
-
-    def get_json(self, url, data=None, headers=None):
-        req_headers = self.custom_headers.copy()
-        req_headers.update(headers or {})
-        if data:  # POST request
-            req_headers.update({'Content-type': 'application/json',
-                                'Accept': 'application/json'})
-            # logger.debug("REST: post: %s" % url)
-            response = self.requester.post(url, auth=self.auth, headers=req_headers,
-                                           verify=self.verify_ssl,
-                                           stream=True,
-                                           data=json.dumps(data))
-        else:
-            # logger.debug("REST: get: %s" % url)
-            response = self.requester.get(url, auth=self.auth, headers=req_headers,
-                                          verify=self.verify_ssl,
-                                          stream=True)
-
-        if response.status_code != 200:  # Error message is text
-            response.charset = "utf-8"  # To be able to access ret.text (ret.content are bytes)
-            raise get_exception_from_error(response.status_code)(response_to_str(response))
-
-        content = response.content.decode()
-        content_type = response.headers.get("Content-Type")
-        if content_type != 'application/json' and content_type != 'application/json; charset=utf-8':
-            raise ConanException("%s\n\nResponse from remote is not json, but '%s'"
-                                 % (content, content_type))
-
-        try:  # This can fail, if some proxy returns 200 and an html message
-            result = json.loads(content)
-        except Exception:
-            raise ConanException("Remote responded with broken json: %s" % content)
-        if not isinstance(result, dict):
-            raise ConanException("Unexpected server response %s" % result)
-        return result
-
-    def upload_recipe(self, ref, files_to_upload):
-        if files_to_upload:
-            self._upload_recipe(ref, files_to_upload)
-
-    def upload_package(self, pref, files_to_upload):
-        self._upload_package(pref, files_to_upload)
-
-    def search(self, pattern=None, ignorecase=True):
-        """
-        the_files: dict with relative_path: content
-        """
-        url = self.router.search(pattern, ignorecase)
-        response = self.get_json(url)["results"]
-        # We need to filter the "_/_" user and channel from Artifactory
-        ret = []
-        for reference in response:
-            try:
-                ref = RecipeReference.loads(reference)
-            except TypeError as te:
-                raise ConanException("Unexpected response from server.\n"
-                                     "URL: `{}`\n"
-                                     "Expected an iterable, but got {}.".format(url, type(response)))
-            if ref.user == "_":
-                ref.user = None
-            if ref.channel == "_":
-                ref.channel = None
-            ret.append(ref)
-        return ret
-
-    def search_packages(self, ref):
-        """Client is filtering by the query"""
-        url = self.router.search_packages(ref)
-        package_infos = self.get_json(url)
-        return package_infos
diff --git a/conans/client/rest/rest_client_v2.py b/conans/client/rest/rest_client_v2.py
index 0002e7f05..21d27634a 100644
--- a/conans/client/rest/rest_client_v2.py
+++ b/conans/client/rest/rest_client_v2.py
@@ -1,36 +1,251 @@
 import copy
 import fnmatch
+import json
 import os
 
+from requests.auth import AuthBase, HTTPBasicAuth
+
 from conan.api.output import ConanOutput
 
 from conans.client.downloaders.caching_file_downloader import ConanInternalCacheDownloader
+from conans.client.rest import response_to_str
 from conans.client.rest.client_routes import ClientV2Router
 from conans.client.rest.file_uploader import FileUploader
-from conans.client.rest.rest_client_common import RestCommonMethods, get_exception_from_error
 from conans.errors import ConanException, NotFoundException, PackageNotFoundException, \
-    RecipeNotFoundException, AuthenticationException, ForbiddenException
+    RecipeNotFoundException, AuthenticationException, ForbiddenException, EXCEPTION_CODE_MAPPING
 from conans.model.package_ref import PkgReference
 from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME
+from conans.model.recipe_ref import RecipeReference
 from conans.util.dates import from_iso8601_to_timestamp
 from conans.util.thread import ExceptionThread
 
 
-class RestV2Methods(RestCommonMethods):
+class JWTAuth(AuthBase):
+    """Attaches JWT Authentication to the given Request object."""
+
+    def __init__(self, token):
+        self.token = token
+
+    def __call__(self, request):
+        if self.token:
+            request.headers['Authorization'] = "Bearer %s" % str(self.token)
+        return request
+
+
+def get_exception_from_error(error_code):
+    tmp = {v: k for k, v in EXCEPTION_CODE_MAPPING.items()  # All except NotFound
+           if k not in (RecipeNotFoundException, PackageNotFoundException)}
+    if error_code in tmp:
+        # logger.debug("REST ERROR: %s" % str(tmp[error_code]))
+        return tmp[error_code]
+    else:
+        base_error = int(str(error_code)[0] + "00")
+        # logger.debug("REST ERROR: %s" % str(base_error))
+        try:
+            return tmp[base_error]
+        except KeyError:
+            return None
+
+
+class RestV2Methods:
 
     def __init__(self, remote_url, token, custom_headers, requester, config, verify_ssl,
                  checksum_deploy=False):
-
-        super(RestV2Methods, self).__init__(remote_url, token, custom_headers, requester,
-                                            config, verify_ssl)
+        self.token = token
+        self.remote_url = remote_url
+        self.custom_headers = custom_headers
+        self.requester = requester
+        self._config = config
+        self.verify_ssl = verify_ssl
         self._checksum_deploy = checksum_deploy
 
+    @property
+    def auth(self):
+        return JWTAuth(self.token)
+
+    @staticmethod
+    def _check_error_response(ret):
+        if ret.status_code == 401:
+            raise AuthenticationException("Wrong user or password")
+        # Cannot check content-type=text/html, conan server is doing it wrong
+        if not ret.ok or "html>" in str(ret.content):
+            raise ConanException("%s\n\nInvalid server response, check remote URL and "
+                                 "try again" % str(ret.content))
+
+    def authenticate(self, user, password):
+        """Sends user + password to get:
+          - A plain response with a regular token (not supported refresh in the remote) and None
+        """
+        auth = HTTPBasicAuth(user, password)
+        url = self.router.common_authenticate()
+        # logger.debug("REST: Authenticate to get access_token: %s" % url)
+        ret = self.requester.get(url, auth=auth, headers=self.custom_headers,
+                                 verify=self.verify_ssl)
+
+        self._check_error_response(ret)
+        return ret.content.decode()
+
+    def authenticate_oauth(self, user, password):
+        """Sends user + password to get:
+            - A json with an access_token and a refresh token (if supported in the remote)
+                    Artifactory >= 6.13.X
+        """
+        url = self.router.oauth_authenticate()
+        auth = HTTPBasicAuth(user, password)
+        headers = {}
+        headers.update(self.custom_headers)
+        headers["Content-type"] = "application/x-www-form-urlencoded"
+        # logger.debug("REST: Authenticating with OAUTH: %s" % url)
+        ret = self.requester.post(url, auth=auth, headers=headers, verify=self.verify_ssl)
+        self._check_error_response(ret)
+
+        data = ret.json()
+        access_token = data["access_token"]
+        refresh_token = data["refresh_token"]
+        # logger.debug("REST: Obtained refresh and access tokens")
+        return access_token, refresh_token
+
+    def refresh_token(self, token, refresh_token):
+        """Sends access_token and the refresh_token to get a pair of
+        access_token and refresh token
+
+        Artifactory >= 6.13.X
+        """
+        url = self.router.oauth_authenticate()
+        # logger.debug("REST: Refreshing Token: %s" % url)
+        headers = {}
+        headers.update(self.custom_headers)
+        headers["Content-type"] = "application/x-www-form-urlencoded"
+        payload = {'access_token': token, 'refresh_token': refresh_token,
+                   'grant_type': 'refresh_token'}
+        ret = self.requester.post(url, headers=headers, verify=self.verify_ssl, data=payload)
+        self._check_error_response(ret)
+
+        data = ret.json()
+        if "access_token" not in data:
+            # logger.debug("REST: unexpected data from server: {}".format(data))
+            raise ConanException("Error refreshing the token")
+
+        new_access_token = data["access_token"]
+        new_refresh_token = data["refresh_token"]
+        # logger.debug("REST: Obtained new refresh and access tokens")
+        return new_access_token, new_refresh_token
+
+    def check_credentials(self):
+        """If token is not valid will raise AuthenticationException.
+        User will be asked for new user/pass"""
+        url = self.router.common_check_credentials()
+        # logger.debug("REST: Check credentials: %s" % url)
+        ret = self.requester.get(url, auth=self.auth, headers=self.custom_headers,
+                                 verify=self.verify_ssl)
+        if ret.status_code != 200:
+            ret.charset = "utf-8"  # To be able to access ret.text (ret.content are bytes)
+            text = ret.text if ret.status_code != 404 else "404 Not found"
+            raise get_exception_from_error(ret.status_code)(text)
+        return ret.content.decode()
+
+    def server_capabilities(self, user=None, password=None):
+        """Get information about the server: status, version, type and capabilities"""
+        url = self.router.ping()
+        # logger.debug("REST: ping: %s" % url)
+        if user and password:
+            # This can happen in "conan remote login" cmd. Instead of empty token, use HttpBasic
+            auth = HTTPBasicAuth(user, password)
+        else:
+            auth = self.auth
+        ret = self.requester.get(url, auth=auth, headers=self.custom_headers, verify=self.verify_ssl)
+
+        server_capabilities = ret.headers.get('X-Conan-Server-Capabilities')
+        if not server_capabilities and not ret.ok:
+            # Old Artifactory might return 401/403 without capabilities, we don't want
+            # to cache them #5687, so raise the exception and force authentication
+            raise get_exception_from_error(ret.status_code)(response_to_str(ret))
+        if server_capabilities is None:
+            # Some servers returning 200-ok, even if not valid repo
+            raise ConanException(f"Remote {self.remote_url} doesn't seem like a valid Conan remote")
+
+        return [cap.strip() for cap in server_capabilities.split(",") if cap]
+
+    def _get_json(self, url, data=None, headers=None):
+        req_headers = self.custom_headers.copy()
+        req_headers.update(headers or {})
+        if data:  # POST request
+            req_headers.update({'Content-type': 'application/json',
+                                'Accept': 'application/json'})
+            # logger.debug("REST: post: %s" % url)
+            response = self.requester.post(url, auth=self.auth, headers=req_headers,
+                                           verify=self.verify_ssl,
+                                           stream=True,
+                                           data=json.dumps(data))
+        else:
+            # logger.debug("REST: get: %s" % url)
+            response = self.requester.get(url, auth=self.auth, headers=req_headers,
+                                          verify=self.verify_ssl,
+                                          stream=True)
+
+        if response.status_code != 200:  # Error message is text
+            response.charset = "utf-8"  # To be able to access ret.text (ret.content are bytes)
+            raise get_exception_from_error(response.status_code)(response_to_str(response))
+
+        content = response.content.decode()
+        content_type = response.headers.get("Content-Type")
+        if content_type != 'application/json' and content_type != 'application/json; charset=utf-8':
+            raise ConanException("%s\n\nResponse from remote is not json, but '%s'"
+                                 % (content, content_type))
+
+        try:  # This can fail, if some proxy returns 200 and an html message
+            result = json.loads(content)
+        except Exception:
+            raise ConanException("Remote responded with broken json: %s" % content)
+        if not isinstance(result, dict):
+            raise ConanException("Unexpected server response %s" % result)
+        return result
+
+    def upload_recipe(self, ref, files_to_upload):
+        if files_to_upload:
+            urls = {fn: self.router.recipe_file(ref, fn)
+                    for fn in files_to_upload}
+            self._upload_files(files_to_upload, urls, str(ref))
+
+    def upload_package(self, pref, files_to_upload):
+        urls = {fn: self.router.package_file(pref, fn)
+                for fn in files_to_upload}
+        self._upload_files(files_to_upload, urls, str(pref))
+
+    def search(self, pattern=None, ignorecase=True):
+        """
+        the_files: dict with relative_path: content
+        """
+        url = self.router.search(pattern, ignorecase)
+        response = self._get_json(url)["results"]
+        # We need to filter the "_/_" user and channel from Artifactory
+        ret = []
+        for reference in response:
+            try:
+                ref = RecipeReference.loads(reference)
+            except TypeError:
+                raise ConanException("Unexpected response from server.\n"
+                                     "URL: `{}`\n"
+                                     "Expected an iterable, but got {}.".format(url, type(response)))
+            if ref.user == "_":
+                ref.user = None
+            if ref.channel == "_":
+                ref.channel = None
+            ret.append(ref)
+        return ret
+
+    def search_packages(self, ref):
+        """Client is filtering by the query"""
+        url = self.router.search_packages(ref)
+        package_infos = self._get_json(url)
+        return package_infos
+
     @property
     def router(self):
         return ClientV2Router(self.remote_url.rstrip("/"))
 
     def _get_file_list_json(self, url):
-        data = self.get_json(url)
+        data = self._get_json(url)
         # Discarding (.keys()) still empty metadata for files
         # and make sure the paths like metadata/sign/signature are normalized to /
         data["files"] = list(d.replace("\\", "/") for d in data["files"].keys())
@@ -119,35 +334,22 @@ class RestV2Methods(RestCommonMethods):
                     ret.append(tmp)
         return sorted(ret)
 
-    def _upload_recipe(self, ref, files_to_upload):
-        # Direct upload the recipe
-        urls = {fn: self.router.recipe_file(ref, fn)
-                for fn in files_to_upload}
-        self._upload_files(files_to_upload, urls, str(ref))
-
-    def _upload_package(self, pref, files_to_upload):
-        urls = {fn: self.router.package_file(pref, fn)
-                for fn in files_to_upload}
-        self._upload_files(files_to_upload, urls, str(pref))
-
     def _upload_files(self, files, urls, ref):
         failed = []
         uploader = FileUploader(self.requester, self.verify_ssl, self._config)
         # conan_package.tgz and conan_export.tgz are uploaded first to avoid uploading conaninfo.txt
         # or conanamanifest.txt with missing files due to a network failure
-        output = ConanOutput()
         for filename in sorted(files):
             # As the filenames are sorted, the last one is always "conanmanifest.txt"
             resource_url = urls[filename]
             try:
-                headers = {}
                 uploader.upload(resource_url, files[filename], auth=self.auth,
-                                dedup=self._checksum_deploy,
-                                headers=headers, ref=ref)
+                                dedup=self._checksum_deploy, ref=ref)
             except (AuthenticationException, ForbiddenException):
                 raise
             except Exception as exc:
-                output.error(f"\nError uploading file: {filename}, '{exc}'", error_type="exception")
+                ConanOutput().error(f"\nError uploading file: {filename}, '{exc}'",
+                                    error_type="exception")
                 failed.append(filename)
 
         if failed:
@@ -195,7 +397,7 @@ class RestV2Methods(RestCommonMethods):
             # Double check if it is a 404 because there are no packages
             try:
                 package_search_url = self.router.search_packages(ref)
-                if not self.get_json(package_search_url):
+                if not self._get_json(package_search_url):
                     return
             except Exception as e:
                 pass
@@ -272,7 +474,7 @@ class RestV2Methods(RestCommonMethods):
 
     def get_recipe_revisions_references(self, ref):
         url = self.router.recipe_revisions(ref)
-        tmp = self.get_json(url)["revisions"]
+        tmp = self._get_json(url)["revisions"]
         remote_refs = []
         for item in tmp:
             _tmp = copy.copy(ref)
@@ -286,7 +488,7 @@ class RestV2Methods(RestCommonMethods):
 
     def get_latest_recipe_reference(self, ref):
         url = self.router.recipe_latest(ref)
-        data = self.get_json(url)
+        data = self._get_json(url)
         remote_ref = copy.copy(ref)
         remote_ref.revision = data.get("revision")
         remote_ref.timestamp = from_iso8601_to_timestamp(data.get("time"))
@@ -294,7 +496,7 @@ class RestV2Methods(RestCommonMethods):
 
     def get_package_revisions_references(self, pref, headers=None):
         url = self.router.package_revisions(pref)
-        tmp = self.get_json(url, headers=headers)["revisions"]
+        tmp = self._get_json(url, headers=headers)["revisions"]
         remote_prefs = [PkgReference(pref.ref, pref.package_id, item.get("revision"),
                         from_iso8601_to_timestamp(item.get("time"))) for item in tmp]
 
@@ -307,7 +509,7 @@ class RestV2Methods(RestCommonMethods):
 
     def get_latest_package_reference(self, pref: PkgReference, headers):
         url = self.router.package_latest(pref)
-        data = self.get_json(url, headers=headers)
+        data = self._get_json(url, headers=headers)
         remote_pref = copy.copy(pref)
         remote_pref.revision = data.get("revision")
         remote_pref.timestamp = from_iso8601_to_timestamp(data.get("time"))
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index 4aa0aee2d..b392c7f55 100644
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -564,8 +564,10 @@ class CppInfo:
         if not external:
             return
         # Only direct host (not test) dependencies can define required components
-        direct_dependencies = [d.ref.name for d in conanfile.requires.values()
-                               if not d.build and not d.is_test and d.visible and not d.override]
+        # We use conanfile.dependencies to use the already replaced ones by "replace_requires"
+        # So consumers can keep their ``self.cpp_info.requires = ["pkg_name::comp"]``
+        direct_dependencies = [r.ref.name for r, d in conanfile.dependencies.items() if r.direct
+                               and not r.build and not r.is_test and r.visible and not r.override]
 
         for e in external:
             if e not in direct_dependencies:
diff --git a/conans/model/conf.py b/conans/model/conf.py
index f8e1924e5..bd5da94e7 100644
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -85,6 +85,7 @@ BUILT_IN_CONFS = {
     "tools.files.download:retry": "Number of retries in case of failure when downloading",
     "tools.files.download:retry_wait": "Seconds to wait between download attempts",
     "tools.files.download:verify": "If set, overrides recipes on whether to perform SSL verification for their downloaded files. Only recommended to be set while testing",
+    "tools.files.unzip:filter": "Define tar extraction filter: 'fully_trusted', 'tar', 'data'",
     "tools.graph:vendor": "(Experimental) If 'build', enables the computation of dependencies of vendoring packages to build them",
     "tools.graph:skip_binaries": "Allow the graph to skip binaries not needed in the current configuration (True by default)",
     "tools.gnu:make_program": "Indicate path to make program",
diff --git a/conans/model/dependencies.py b/conans/model/dependencies.py
index 0f8640fc8..a2734b5e8 100644
--- a/conans/model/dependencies.py
+++ b/conans/model/dependencies.py
@@ -91,6 +91,12 @@ class ConanFileDependencies(UserRequirementsDict):
     def from_node(node):
         d = OrderedDict((require, ConanFileInterface(transitive.node.conanfile))
                         for require, transitive in node.transitive_deps.items())
+        for old_req, new_req in node.replaced_requires.items():
+            existing = d.pop(new_req, None)
+            if existing is not None:
+                added_req = new_req.copy_requirement()
+                added_req.ref = RecipeReference.loads(old_req)
+                d[added_req] = existing
         return ConanFileDependencies(d)
 
     def filter(self, require_filter, remove_system=True):
diff --git a/conans/util/files.py b/conans/util/files.py
index 65471f457..9a42d0bb9 100644
--- a/conans/util/files.py
+++ b/conans/util/files.py
@@ -296,6 +296,7 @@ def tar_extract(fileobj, destination_dir):
     # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to
     # "could not change modification time", with time=0
     # the_tar.errorlevel = 2  # raise exception if any error
+    the_tar.extraction_filter = (lambda member, path: member)  # fully_trusted, avoid Py3.14 break
     the_tar.extractall(path=destination_dir)
     the_tar.close()
 


Test patch:
diff --git a/test/integration/build_requires/test_install_test_build_require.py b/test/integration/build_requires/test_install_test_build_require.py
index 148dfd2a3..8892dc032 100644
--- a/test/integration/build_requires/test_install_test_build_require.py
+++ b/test/integration/build_requires/test_install_test_build_require.py
@@ -222,3 +222,37 @@ def test_bootstrap_other_architecture():
     assert package1["depends"] == []
     assert package2["package_id"] == linux_pkg_id
     assert package2["depends"] == [win_pkg_id]
+
+
+def test_bootstrap_cc():
+    # https://github.com/conan-io/conan/issues/16758
+    cc = textwrap.dedent("""
+        from conan import ConanFile
+
+        class CcConanfile(ConanFile):
+            name = "cc"
+            version = "1.0"
+            options = {"bootstrap": [True, False]}
+            default_options = {"bootstrap": True}
+
+            def build_requirements(self):
+                if self.options.bootstrap:
+                    self.tool_requires("cc/1.0", options={"*:bootstrap": False})
+        """)
+    foo = GenConanfile("foo", "1.0").with_tool_requirement("cc/1.0")
+    c = TestClient()
+    c.save({"cc/conanfile.py": cc,
+            "foo/conanfile.py": foo})
+    c.run("create cc --build-require")
+    # Both are build-requires
+    c.assert_listed_binary({"cc/1.0": ("826727aac60b5956d1df5121a3921f26a6984f15", "Build")},
+                           build=True)
+    c.assert_listed_binary({"cc/1.0": ("96ae1d965ce6f2e2256ac0cfc3a35dcd4860c389", "Build")},
+                           build=True)
+    # This works fine, being able to build the 2 different binaries both bootstrapped and not
+    c.run("list cc/1.0:*")
+    assert "bootstrap: False" in c.out
+    assert "bootstrap: True" in c.out
+    c.run("create foo")
+    c.assert_listed_binary({"cc/1.0": ("96ae1d965ce6f2e2256ac0cfc3a35dcd4860c389", "Cache")},
+                           build=True)
diff --git a/test/integration/configuration/conf/test_conf.py b/test/integration/configuration/conf/test_conf.py
index d8329e09e..52e2bf858 100644
--- a/test/integration/configuration/conf/test_conf.py
+++ b/test/integration/configuration/conf/test_conf.py
@@ -414,3 +414,30 @@ def test_especial_strings_fail():
     assert "user.mycompany:myother: fnmatch" in c.out
     assert "user.mycompany:myfunct: re.search" in c.out
     assert "user.mycompany:mydict: {1: 're', 2: 'fnmatch'}" in c.out
+
+
+def test_conf_test_package():
+    c = TestClient()
+    dep = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            name = "dep"
+            version = "0.1"
+            def generate(self):
+                self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}')
+        """)
+    pkg = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            def requirements(self):
+                self.requires(self.tested_reference_str)
+            def generate(self):
+                self.output.info(f'user.myteam:myconf: {self.conf.get("user.myteam:myconf")}')
+            def test(self):
+                pass
+        """)
+    c.save({"conanfile.py": dep,
+            "test_package/conanfile.py": pkg})
+    c.run("create . -c &:user.myteam:myconf=myvalue")
+    assert "dep/0.1: user.myteam:myconf: myvalue" in c.out
+    assert "dep/0.1 (test package): user.myteam:myconf: myvalue" in c.out
diff --git a/test/integration/graph/core/test_build_requires.py b/test/integration/graph/core/test_build_requires.py
index 7e396044b..a3945956a 100644
--- a/test/integration/graph/core/test_build_requires.py
+++ b/test/integration/graph/core/test_build_requires.py
@@ -883,12 +883,15 @@ class TestLoops(GraphManagerTest):
         assert type(deps_graph.error) == GraphLoopError
 
         # Build requires always apply to the consumer
-        self.assertEqual(2, len(deps_graph.nodes))
+        self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
         tool = app.dependencies[0].dst
+        tool2 = tool.dependencies[0].dst
+        tool3 = tool2.dependencies[0].dst
 
         self._check_node(app, "app/0.1", deps=[tool], dependents=[])
-        self._check_node(tool, "cmake/0.1#123", deps=[], dependents=[app])
+        self._check_node(tool, "cmake/0.1#123", deps=[tool2], dependents=[app])
+        self._check_node(tool2, "cmake/0.1#123", deps=[tool3], dependents=[tool])
 
     def test_indirect_loop_error(self):
         # app -(br)-> gtest/0.1 -(br)-> cmake/0.1 -(br)->gtest/0.1 ....
@@ -902,14 +905,16 @@ class TestLoops(GraphManagerTest):
         assert type(deps_graph.error) == GraphLoopError
 
         # Build requires always apply to the consumer
-        self.assertEqual(4, len(deps_graph.nodes))
+        self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
         cmake = app.dependencies[0].dst
         gtest = cmake.dependencies[0].dst
         cmake2 = gtest.dependencies[0].dst
+        gtest2 = cmake2.dependencies[0].dst
+        cmake3 = gtest2.dependencies[0].dst
 
         assert deps_graph.error.ancestor == cmake
-        assert deps_graph.error.node == cmake2
+        assert deps_graph.error.node == cmake3
 
     def test_infinite_recursion_test(self):
         """Ensure that direct conflicts in a node are properly reported and Conan does not loop"""
diff --git a/test/integration/graph/test_replace_requires.py b/test/integration/graph/test_replace_requires.py
index 31edd82f2..909854d0b 100644
--- a/test/integration/graph/test_replace_requires.py
+++ b/test/integration/graph/test_replace_requires.py
@@ -1,4 +1,5 @@
 import json
+import textwrap
 
 import pytest
 
@@ -157,3 +158,120 @@ def test_replace_requires_test_requires():
     c.run("create gtest")
     c.run("install app -pr=profile")
     assert "gtest/0.1: gtest/0.2" in c.out  # The replacement happens
+
+
+def test_replace_requires_consumer_references():
+    c = TestClient()
+    # IMPORTANT: The replacement package must be target-compatible
+    zlib_ng = textwrap.dedent("""
+        from conan import ConanFile
+        class ZlibNG(ConanFile):
+            name = "zlib-ng"
+            version = "0.1"
+            def package_info(self):
+                self.cpp_info.set_property("cmake_file_name", "ZLIB")
+                self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB")
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class App(ConanFile):
+            name = "app"
+            version = "0.1"
+            settings = "build_type"
+            requires = "zlib/0.1"
+            generators = "CMakeDeps"
+
+            def generate(self):
+                self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!")
+            def build(self):
+                self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!")
+            def package_info(self):
+                self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!")
+                self.cpp_info.requires = ["zlib::zlib"]
+        """)
+    c.save({"zlibng/conanfile.py": zlib_ng,
+            "app/conanfile.py": conanfile,
+            "profile": "[replace_requires]\nzlib/0.1: zlib-ng/0.1"})
+    c.run("create zlibng")
+    c.run("build app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib-ng!" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB build: zlib-ng!" in c.out
+    # Check generated CMake code. If the targets are NOT compatible, then the replacement
+    # Cannot happen
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
+    cmake = c.load("app/ZLIBTargets.cmake")
+    assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake
+    c.run("create app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "app/0.1: DEP ZLIB generate: zlib-ng!" in c.out
+    assert "app/0.1: DEP ZLIB build: zlib-ng!" in c.out
+
+
+def test_replace_requires_consumer_components_options():
+    c = TestClient()
+    # IMPORTANT: The replacement package must be target-compatible
+    zlib_ng = textwrap.dedent("""
+        from conan import ConanFile
+        class ZlibNG(ConanFile):
+            name = "zlib-ng"
+            version = "0.1"
+            options = {"compat": [False, True]}
+            default_options = {"compat": False}
+            def package_info(self):
+                self.cpp_info.set_property("cmake_file_name", "ZLIB")
+                self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB")
+                if self.options.compat:
+                    self.cpp_info.components["myzlib"].set_property("cmake_target_name",
+                                                                    "ZLIB::zmylib")
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class App(ConanFile):
+            name = "app"
+            version = "0.1"
+            settings = "build_type"
+            requires = "zlib/0.1"
+            generators = "CMakeDeps"
+
+            def generate(self):
+                self.output.info(f"DEP ZLIB generate: {self.dependencies['zlib'].ref.name}!")
+            def build(self):
+                self.output.info(f"DEP ZLIB build: {self.dependencies['zlib'].ref.name}!")
+            def package_info(self):
+                self.output.info(f"DEP ZLIB package_info: {self.dependencies['zlib'].ref.name}!")
+                self.cpp_info.requires = ["zlib::myzlib"]
+        """)
+    profile = textwrap.dedent("""
+        [options]
+        zlib-ng/*:compat=True
+
+        [replace_requires]
+        zlib/0.1: zlib-ng/0.1
+        """)
+    c.save({"zlibng/conanfile.py": zlib_ng,
+            "app/conanfile.py": conanfile,
+            "profile": profile})
+
+    c.run("create zlibng -o *:compat=True")
+    c.run("build app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB generate: zlib-ng!" in c.out
+    assert "conanfile.py (app/0.1): DEP ZLIB build: zlib-ng!" in c.out
+    # Check generated CMake code. If the targets are NOT compatible, then the replacement
+    # Cannot happen
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
+    cmake = c.load("app/ZLIBTargets.cmake")
+    assert "add_library(ZLIB::ZLIB INTERFACE IMPORTED)" in cmake
+    cmake = c.load("app/ZLIB-Target-none.cmake")
+    assert "set_property(TARGET ZLIB::ZLIB APPEND PROPERTY INTERFACE_LINK_LIBRARIES ZLIB::zmylib)" \
+           in cmake
+
+    c.run("create app -pr=profile")
+    assert "zlib/0.1: zlib-ng/0.1" in c.out
+    assert "app/0.1: DEP ZLIB generate: zlib-ng!" in c.out
+    assert "app/0.1: DEP ZLIB build: zlib-ng!" in c.out
+    assert "find_package(ZLIB)" in c.out
+    assert "target_link_libraries(... ZLIB::ZLIB)" in c.out
diff --git a/test/integration/options/test_configure_options.py b/test/integration/options/test_configure_options.py
index 1aff0e4d9..0cbf34743 100644
--- a/test/integration/options/test_configure_options.py
+++ b/test/integration/options/test_configure_options.py
@@ -123,3 +123,29 @@ class ConfigureOptionsTest(unittest.TestCase):
         client.save({"conanfile.py": conanfile})
         client.run(f"create . --name=pkg --version=0.1")
         self.assertIn("Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created", client.out)
+
+
+def test_config_options_override_behaviour():
+    tc = TestClient(light=True)
+    tc.save({"conanfile.py": textwrap.dedent("""
+    from conan import ConanFile
+
+    class Pkg(ConanFile):
+        name = "pkg"
+        version = "1.0"
+
+        options = {"foo": [1, 2, 3]}
+        default_options = {"foo": 1}
+
+        def config_options(self):
+            self.options.foo = 2
+
+        def build(self):
+            self.output.info(f"Foo: {self.options.foo}")
+    """)})
+
+    tc.run("create .")
+    assert "Foo: 2" in tc.out
+
+    tc.run("create . -o=&:foo=3")
+    assert "Foo: 3" in tc.out
diff --git a/test/integration/remote/rest_api_test.py b/test/integration/remote/rest_api_test.py
index 7710d1faa..5566bbbfb 100644
--- a/test/integration/remote/rest_api_test.py
+++ b/test/integration/remote/rest_api_test.py
@@ -1,14 +1,12 @@
 import os
 import unittest
 
-import pytest
 from mock import Mock
 
-from conans import REVISIONS
 from conans.client.remote_manager import Remote
 from conans.client.rest.auth_manager import ConanApiAuthManager
 from conans.client.rest.conan_requester import ConanRequester
-from conans.client.rest.rest_client import RestApiClientFactory
+from conans.client.rest.rest_client import RestApiClient
 from conans.model.conf import ConfDefinition
 from conan.test.utils.env import environment_update
 from conans.client.userio import UserInput
@@ -24,7 +22,6 @@ from conan.test.utils.tools import get_free_port
 from conans.util.files import md5, save
 
 
-@pytest.mark.rest_api
 class RestApiTest(unittest.TestCase):
     """Open a real server (sockets) to test rest_api function."""
 
@@ -42,8 +39,6 @@ class RestApiTest(unittest.TestCase):
                 save(filename, "")
                 config = ConfDefinition()
                 requester = ConanRequester(config)
-                client_factory = RestApiClientFactory(requester=requester,
-                                                      config=config)
                 localdb = LocalDBMock()
 
                 mocked_user_input = UserInput(non_interactive=False)
@@ -51,13 +46,13 @@ class RestApiTest(unittest.TestCase):
                 mocked_user_input.get_password = Mock(return_value="private_pass")
 
                 # FIXME: Missing mock
-                cls.auth_manager = ConanApiAuthManager(client_factory, temp_folder(), localdb, config)
+                cls.auth_manager = ConanApiAuthManager(requester, temp_folder(), localdb, config)
                 cls.remote = Remote("myremote", "http://127.0.0.1:%s" % str(cls.server.port), True,
                                     True)
                 cls.auth_manager._authenticate(cls.remote, user="private_user",
                                                password="private_pass")
-                cls.api = client_factory.new(cls.remote, localdb.access_token, localdb.refresh_token,
-                                             {})
+                cls.api = RestApiClient(cls.remote, localdb.access_token, localdb.refresh_token,
+                                        {}, requester, config, {})
 
     @classmethod
     def tearDownClass(cls):
@@ -66,10 +61,6 @@ class RestApiTest(unittest.TestCase):
     def tearDown(self):
         RestApiTest.server.clean()
 
-    def test_server_capabilities(self):
-        capabilities = self.api.server_capabilities()
-        self.assertEqual(capabilities, ["ImCool", "TooCool", REVISIONS])
-
     def test_get_conan(self):
         # Upload a conans
         ref = RecipeReference.loads("conan1/1.0.0@private_user/testing#myreciperev")
diff --git a/test/integration/remote/token_refresh_test.py b/test/integration/remote/token_refresh_test.py
index 8bae5ab59..b1ef3ce03 100644
--- a/test/integration/remote/token_refresh_test.py
+++ b/test/integration/remote/token_refresh_test.py
@@ -4,7 +4,6 @@ import mock
 
 from conan.api.model import Remote
 from conans.client.rest.auth_manager import ConanApiAuthManager
-from conans.client.rest.rest_client import RestApiClientFactory
 from conans.model.conf import ConfDefinition
 from conans.model.recipe_ref import RecipeReference
 from conan.test.utils.mocks import LocalDBMock
@@ -78,9 +77,8 @@ class TestTokenRefresh(unittest.TestCase):
     def setUp(self):
         requester = RequesterWithTokenMock()
         config = ConfDefinition()
-        self.rest_client_factory = RestApiClientFactory(requester, config=config)
         self.localdb = LocalDBMock()
-        self.auth_manager = ConanApiAuthManager(self.rest_client_factory, temp_folder(), self.localdb, config)
+        self.auth_manager = ConanApiAuthManager(requester, temp_folder(), self.localdb, config)
         self.remote = Remote("myremote", "myurl", True, True)
         self.ref = RecipeReference.loads("lib/1.0@conan/stable#myreciperev")
 
diff --git a/test/integration/test_source_download_password.py b/test/integration/test_source_download_password.py
index 8778ad028..3a116e826 100644
--- a/test/integration/test_source_download_password.py
+++ b/test/integration/test_source_download_password.py
@@ -1,10 +1,17 @@
 import json
 import os
+import platform
+import sys
 import textwrap
+from shutil import copy
 from unittest import mock
 
+import pytest
+
+from conan.internal.api.uploader import compress_files
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.file_server import TestFileServer
+from conan.test.utils.test_files import temp_folder
 from conan.test.utils.tools import TestClient
 from conans.util.files import save
 
@@ -77,3 +84,39 @@ def test_source_credentials_only_download():
         c.run("upload * -c -r=default")
         c.run("remove * -c")
         c.run("download pkg/0.1 -r=default")
+
+
+@pytest.mark.skipif(sys.version_info.minor < 12 or platform.system() == "Windows",
+                    reason="Extraction filters only Python 3.12, using symlinks (not Windows)")
+def test_blocked_malicius_tgz():
+    folder = temp_folder()
+    f = os.path.join(folder, "myfile.txt")
+    save(f, "The contents")
+    s = os.path.join(folder, "mylink.txt")
+    os.symlink(f, s)
+    tgz_path = compress_files({f: f, s: s}, "myfiles.tgz", dest_dir=folder)
+    os.remove(f)
+
+    conan_file = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.files import get
+        class Pkg(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            def source(self):
+                get(self, "http://fake_url/myfiles.tgz")
+            """)
+    client = TestClient()
+    client.save({"conanfile.py": conan_file})
+
+    with mock.patch("conan.tools.files.files.download") as mock_download:
+        def download_zip(*args, **kwargs):  # noqa
+            copy(tgz_path, os.getcwd())
+        mock_download.side_effect = download_zip
+        client.run("create . -c tools.files.unzip:filter=data", assert_error=True)
+        assert "AbsoluteLinkError" in client.out
+        client.save({"conanfile.py": conan_file.format("extract_filter='fully_trusted'")})
+        client.run("create . ")  # Doesn't fail now
+        # user conf has precedence
+        client.save({"conanfile.py": conan_file.format("extract_filter='data'")})
+        client.run("create . -c tools.files.unzip:filter=fully_trusted")  # Doesn't fail now
diff --git a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
index b125b2e1d..93c63ce62 100644
--- a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -819,3 +819,27 @@ def test_component_name_same_package():
     # FIXME: Depending on itself, this doesn't look good
     assert 'set_property(TARGET dep::dep APPEND PROPERTY ' \
            'INTERFACE_LINK_LIBRARIES dep::dep)' in cmake_target
+
+
+def test_cmakedeps_set_get_property_checktype():
+    c = TestClient()
+    app = textwrap.dedent("""\
+        from conan import ConanFile
+        from conan.tools.cmake import CMakeDeps
+        class Pkg(ConanFile):
+            name = "app"
+            version = "0.1"
+            settings = "build_type"
+            requires = "dep/0.1"
+            def generate(self):
+                deps = CMakeDeps(self)
+                deps.set_property("dep", "foo", 1)
+                deps.get_property("foo", self.dependencies["dep"], check_type=list)
+                deps.generate()
+            """)
+
+    c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"),
+            "app/conanfile.py": app})
+    c.run("create dep")
+    c.run("create app", assert_error=True)
+    assert 'The expected type for foo is "list", but "int" was found' in c.out
diff --git a/test/integration/toolchains/google/test_bazeldeps.py b/test/integration/toolchains/google/test_bazeldeps.py
index d4b99059a..62c4a45a2 100644
--- a/test/integration/toolchains/google/test_bazeldeps.py
+++ b/test/integration/toolchains/google/test_bazeldeps.py
@@ -4,6 +4,7 @@ import textwrap
 
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
+from conan.tools.files import load
 
 
 def test_bazel():
@@ -983,3 +984,132 @@ class TestBazelGenerationBuildContext:
         # Now make sure we can actually build with build!=host context
         c.run("install app -s:h build_type=Debug --build=missing")
         assert "Install finished successfully" in c.out  # the asserts in build() didn't fail
+
+
+
+def test_shared_windows_find_libraries():
+    """
+    Testing the ``_get_libs`` mechanism in Windows, the shared libraries and their
+    import ones are correctly found.
+
+    Note: simulating dependencies with openssl, libcurl, and zlib packages:
+
+    zlib:
+        - (zlib package) bin/zlib1.dll AND lib/zdll.lib
+    libcurl:
+        - (curl component) bin/libcurl.dll AND lib/libcurl_imp.lib
+    openssl:
+        - (crypto component) bin/libcrypto-3-x64.dll AND lib/libcrypto.lib
+        - (ssl component) bin/libssl-3-x64.dll AND lib/libssl.lib
+
+
+    Issue: https://github.com/conan-io/conan/issues/16691
+    """
+    c = TestClient()
+    zlib = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import save
+        class Example(ConanFile):
+            name = "zlib"
+            version = "1.0"
+            options = {"shared": [True, False]}
+            default_options = {"shared": False}
+            def package(self):
+                bindirs = os.path.join(self.package_folder, "bin")
+                libdirs = os.path.join(self.package_folder, "lib")
+                save(self, os.path.join(bindirs, "zlib1.dll"), "")
+                save(self, os.path.join(libdirs, "zdll.lib"), "")
+            def package_info(self):
+                self.cpp_info.libs = ["zdll"]
+            """)
+    openssl = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import save
+        class Pkg(ConanFile):
+            name = "openssl"
+            version = "1.0"
+            options = {"shared": [True, False]}
+            default_options = {"shared": False}
+            def package(self):
+                bindirs = os.path.join(self.package_folder, "bin")
+                libdirs = os.path.join(self.package_folder, "lib")
+                save(self, os.path.join(bindirs, "libcrypto-3-x64.dll"), "")
+                save(self, os.path.join(bindirs, "libssl-3-x64.dll"), "")
+                save(self, os.path.join(libdirs, "libcrypto.lib"), "")
+                save(self, os.path.join(libdirs, "libssl.lib"), "")
+            def package_info(self):
+                self.cpp_info.components["crypto"].libs = ["libcrypto"]
+                self.cpp_info.components["ssl"].libs = ["libssl"]
+        """)
+    libcurl = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import save
+        class Example(ConanFile):
+            name = "libcurl"
+            version = "1.0"
+            options = {"shared": [True, False]}
+            default_options = {"shared": False}
+            def package(self):
+                bindirs = os.path.join(self.package_folder, "bin")
+                libdirs = os.path.join(self.package_folder, "lib")
+                save(self, os.path.join(bindirs, "libcurl.dll"), "")
+                save(self, os.path.join(libdirs, "libcurl_imp.lib"), "")
+            def package_info(self):
+                self.cpp_info.components["curl"].libs = ["libcurl_imp"]
+            """)
+    consumer = textwrap.dedent("""
+        [requires]
+        zlib/1.0
+        openssl/1.0
+        libcurl/1.0
+        [options]
+        *:shared=True
+    """)
+    c.save({"conanfile.txt": consumer,
+            "zlib/conanfile.py": zlib,
+            "openssl/conanfile.py": openssl,
+            "libcurl/conanfile.py": libcurl})
+    c.run("export-pkg zlib -o:a shared=True")
+    c.run("export-pkg openssl -o:a shared=True")
+    c.run("export-pkg libcurl -o:a shared=True")
+    c.run("install . -g BazelDeps")
+    libcurl_bazel_build = load(None, os.path.join(c.current_folder, "libcurl", "BUILD.bazel"))
+    zlib_bazel_build = load(None, os.path.join(c.current_folder, "zlib", "BUILD.bazel"))
+    openssl_bazel_build = load(None, os.path.join(c.current_folder, "openssl", "BUILD.bazel"))
+    libcurl_expected = textwrap.dedent("""\
+    # Components precompiled libs
+    cc_import(
+        name = "libcurl_imp_precompiled",
+        shared_library = "bin/libcurl.dll",
+        interface_library = "lib/libcurl_imp.lib",
+    )
+    """)
+    openssl_expected = textwrap.dedent("""\
+    # Components precompiled libs
+    cc_import(
+        name = "libcrypto_precompiled",
+        shared_library = "bin/libcrypto-3-x64.dll",
+        interface_library = "lib/libcrypto.lib",
+    )
+
+    cc_import(
+        name = "libssl_precompiled",
+        shared_library = "bin/libcrypto-3-x64.dll",
+        interface_library = "lib/libssl.lib",
+    )
+    """)
+    zlib_expected = textwrap.dedent("""\
+    # Components precompiled libs
+    # Root package precompiled libs
+    cc_import(
+        name = "zdll_precompiled",
+        shared_library = "bin/zlib1.dll",
+        interface_library = "lib/zdll.lib",
+    )
+    """)
+    assert libcurl_expected in libcurl_bazel_build
+    assert zlib_expected in zlib_bazel_build
+    assert openssl_expected in openssl_bazel_build
diff --git a/test/unittests/tools/cmake/test_cmaketoolchain.py b/test/unittests/tools/cmake/test_cmaketoolchain.py
index 25ffb1104..09ba6d0fd 100644
--- a/test/unittests/tools/cmake/test_cmaketoolchain.py
+++ b/test/unittests/tools/cmake/test_cmaketoolchain.py
@@ -32,6 +32,7 @@ def conanfile():
     c.folders.set_base_generators("/some/abs/path")  # non-existing to not relativize
     c._conan_node = Mock()
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -229,6 +230,8 @@ def conanfile_apple():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    # FIXME: Repeated fix pattern
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -258,6 +261,7 @@ def conanfile_msvc():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -332,6 +336,7 @@ def test_older_msvc_toolset():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v110"' in content
@@ -360,6 +365,7 @@ def test_older_msvc_toolset_update():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v142,version=14.29"' in content
@@ -385,6 +391,7 @@ def test_msvc_xp_toolsets():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     toolchain = CMakeToolchain(c)
     content = toolchain.content
     assert 'CMAKE_GENERATOR_TOOLSET "v110_xp"' in content
@@ -411,6 +418,7 @@ def conanfile_linux():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -442,6 +450,7 @@ def conanfile_linux_shared():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -482,6 +491,7 @@ def conanfile_windows_fpic():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -513,6 +523,7 @@ def conanfile_linux_fpic():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
     return c
 
 
@@ -547,6 +558,7 @@ def test_libcxx_abi_flag():
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
 
     toolchain = CMakeToolchain(c)
     content = toolchain.content
@@ -597,6 +609,7 @@ def test_apple_cmake_osx_sysroot(os, os_sdk, arch, expected_sdk):
     c._conan_node = Mock()
     c._conan_node.dependencies = []
     c._conan_node.transitive_deps = {}
+    c._conan_node.replaced_requires = {}
 
     toolchain = CMakeToolchain(c)
     content = toolchain.content
@@ -680,6 +693,7 @@ class TestCrossBuild:
         c._conan_node = Mock()
         c._conan_node.dependencies = []
         c._conan_node.transitive_deps = {}
+        c._conan_node.replaced_requires = {}
         return c
 
     def test_cmake_system_name(self, conanfile_cross):
diff --git a/test/unittests/tools/google/test_bazel.py b/test/unittests/tools/google/test_bazel.py
index dae3611bf..5c5aa5836 100644
--- a/test/unittests/tools/google/test_bazel.py
+++ b/test/unittests/tools/google/test_bazel.py
@@ -4,12 +4,11 @@ from unittest.mock import MagicMock
 
 import pytest
 
+from conan.test.utils.mocks import ConanFileMock
+from conan.test.utils.test_files import temp_folder
 from conan.tools.files import save
 from conan.tools.google import Bazel
 from conan.tools.google.bazeldeps import _relativize_path, _get_libs
-from conans.model.options import Options
-from conan.test.utils.mocks import ConanFileMock
-from conan.test.utils.test_files import temp_folder
 
 
 @pytest.fixture(scope="module")
@@ -28,10 +27,12 @@ def cpp_info():
     save(ConanFileMock(), os.path.join(libdirs, "mylibwin2.if.lib"), "")
     save(ConanFileMock(), os.path.join(libdirs, "libmylib.so"), "")
     save(ConanFileMock(), os.path.join(libdirs, "subfolder", "libmylib.a"), "")  # recursive
-    cpp_info_mock = MagicMock(_base_folder=None, libdirs=None, bindirs=None, libs=None)
+    cpp_info_mock = MagicMock(_base_folder=None, libdirs=[], bindirs=[], libs=[],
+                              aggregated_components=MagicMock())
     cpp_info_mock._base_folder = folder.replace("\\", "/")
     cpp_info_mock.libdirs = [libdirs]
     cpp_info_mock.bindirs = [bindirs]
+    cpp_info_mock.aggregated_components.return_value = cpp_info_mock
     return cpp_info_mock
 
 
@@ -82,15 +83,11 @@ def test_bazeldeps_relativize_path(path, pattern, expected):
     # Win + shared
     (["mylibwin"], True, [('mylibwin', True, '{base_folder}/bin/mylibwin.dll', '{base_folder}/lib/mylibwin.lib')]),
     # Win + shared (interface with another ext)
-    (["mylibwin2"], True,
-     [('mylibwin2', True, '{base_folder}/bin/mylibwin2.dll', '{base_folder}/lib/mylibwin2.if.lib')]),
-    # Win + Mac + shared
-    (["mylibwin", "mylibmac"], True, [('mylibmac', True, '{base_folder}/bin/mylibmac.dylib', None),
-                                      ('mylibwin', True, '{base_folder}/bin/mylibwin.dll',
-                                       '{base_folder}/lib/mylibwin.lib')]),
-    # Linux + Mac + static
-    (["myliblin", "mylibmac"], False, [('mylibmac', False, '{base_folder}/lib/mylibmac.a', None),
-                                       ('myliblin', False, '{base_folder}/lib/myliblin.a', None)]),
+    (["mylibwin2"], True, [('mylibwin2', True, '{base_folder}/bin/mylibwin2.dll', '{base_folder}/lib/mylibwin2.if.lib')]),
+    # Mac + shared
+    (["mylibmac"], True, [('mylibmac', True, '{base_folder}/bin/mylibmac.dylib', None)]),
+    # Mac + static
+    (["mylibmac"], False, [('mylibmac', False, '{base_folder}/lib/mylibmac.a', None)]),
     # mylib + shared (saved as libmylib.so) -> removing the leading "lib" if it matches
     (["mylib"], True, [('mylib', True, '{base_folder}/lib/libmylib.so', None)]),
     # mylib + static (saved in a subfolder subfolder/libmylib.a) -> non-recursive at this moment
@@ -113,8 +110,10 @@ def test_bazeldeps_get_libs(cpp_info, libs, is_shared, expected):
         if interface_lib_path:
             interface_lib_path = interface_lib_path.format(base_folder=cpp_info._base_folder)
         ret.append((lib, is_shared, lib_path, interface_lib_path))
-    found_libs = _get_libs(ConanFileMock(options=Options(options_values={"shared": is_shared})),
-                           cpp_info)
+    dep = MagicMock()
+    dep.options.get_safe.return_value = is_shared
+    dep.ref.name = "my_pkg"
+    found_libs = _get_libs(dep, cpp_info)
     found_libs.sort()
     ret.sort()
     assert found_libs == ret