Repository: conan-io/conan
Pull request #17640 (2025-01-27 12:12:57)

[bugfix] `conf_build` for `cli` and `conanfile.txt` contexts 
Changelog: Bugfix: `conf_build` does not exist for `cli` and `conanfile.txt` contexts.
Docs: omit
Closes: https://github.com/conan-io/conan/issues/17601

Note: it only affects `AutotoolsToolchain` and `GnuToolchain`.



Patch:
diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py
index 7b6fd14fe..30fe82cc2 100644
--- a/conan/api/conan_api.py
+++ b/conan/api/conan_api.py
@@ -25,6 +25,11 @@ from conan.internal.model.version_range import validate_conan_version
 
 
 class ConanAPI:
+    """
+    This is the main object to interact with the Conan API. It provides all the subapis to work with
+    recipes, packages, remotes, etc., which are exposed as attributes of this class, and should
+    not be created directly.
+    """
     def __init__(self, cache_folder=None):
 
         version = sys.version_info
@@ -61,6 +66,9 @@ class ConanAPI:
         _check_conan_version(self)
 
     def reinit(self):
+        """
+        Reinitialize the Conan API. This is useful when the configuration changes.
+        """
         self.config.reinit()
         self.remotes.reinit()
         self.local.reinit()
diff --git a/conan/api/model/remote.py b/conan/api/model/remote.py
index 0f6120bd2..a66a28fb9 100644
--- a/conan/api/model/remote.py
+++ b/conan/api/model/remote.py
@@ -2,7 +2,10 @@ LOCAL_RECIPES_INDEX = "local-recipes-index"
 
 
 class Remote:
-
+    """
+    The ``Remote`` class represents a remote registry of packages. It's a read-only opaque object that
+    should not be created directly, but obtained from the relevant ``RemotesAPI`` subapi methods.
+    """
     def __init__(self, name, url, verify_ssl=True, disabled=False, allowed_packages=None,
                  remote_type=None):
         self.name = name  # Read only, is the key
@@ -11,7 +14,7 @@ class Remote:
         self.disabled = disabled
         self.allowed_packages = allowed_packages
         self.remote_type = remote_type
-        self.caching = {}
+        self._caching = {}
 
     def __eq__(self, other):
         if other is None:
@@ -33,5 +36,4 @@ class Remote:
         return str(self)
 
     def invalidate_cache(self):
-        # TODO: make it private
-        self.caching = {}
+        self._caching = {}
diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py
index ebc17c4d9..1bb55664a 100644
--- a/conan/api/subapi/remotes.py
+++ b/conan/api/subapi/remotes.py
@@ -40,12 +40,12 @@ class RemotesAPI:
 
     def list(self, pattern=None, only_enabled=True):
         """
-        Obtain a list of ``Remote`` objects matching the pattern.
+        Obtain a list of :ref:`Remote <conan.api.model.Remote>` objects matching the pattern.
 
         :param pattern: ``None``, single ``str`` or list of ``str``. If it is ``None``,
           all remotes will be returned (equivalent to ``pattern="*"``).
         :param only_enabled: boolean, by default return only enabled remotes
-        :return: A list of ``Remote`` objects
+        :return: A list of :ref:`Remote <conan.api.model.Remote>` objects
 
         """
         remotes = _load(self._remotes_file)
@@ -61,7 +61,7 @@ class RemotesAPI:
 
         :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without
           wildcards like "*" and no remote is found matching that exact name, it will raise an error.
-        :return: the list of disabled ``Remote`` objects  (even if they were already disabled)
+        :return: the list of disabled :ref:`Remote <conan.api.model.Remote>` objects  (even if they were already disabled)
         """
         remotes = _load(self._remotes_file)
         disabled = _filter(remotes, pattern, only_enabled=False)
@@ -79,7 +79,7 @@ class RemotesAPI:
 
         :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without
           wildcards like "*" and no remote is found matching that exact name, it will raise an error.
-        :return: the list of enabled ``Remote`` objects (even if they were already enabled)
+        :return: the list of enabled :ref:`Remote <conan.api.model.Remote>` objects (even if they were already enabled)
         """
         remotes = _load(self._remotes_file)
         enabled = _filter(remotes, pattern, only_enabled=False)
@@ -93,10 +93,10 @@ class RemotesAPI:
 
     def get(self, remote_name):
         """
-        Obtain a ``Remote`` object
+        Obtain a :ref:`Remote <conan.api.model.Remote>` object
 
         :param remote_name: the exact name of the remote to be returned
-        :return: the ``Remote`` object, or raise an Exception if the remote does not exist.
+        :return: the :ref:`Remote <conan.api.model.Remote>` object, or raise an Exception if the remote does not exist.
         """
         remotes = _load(self._remotes_file)
         try:
@@ -106,11 +106,11 @@ class RemotesAPI:
 
     def add(self, remote: Remote, force=False, index=None):
         """
-        Add a new ``Remote`` object to the existing ones
+        Add a new :ref:`Remote <conan.api.model.Remote>` object to the existing ones
 
 
-        :param remote: a ``Remote`` object to be added
-        :param force: do not fail if the remote already exist (but default it failes)
+        :param remote: a :ref:`Remote <conan.api.model.Remote>` object to be added
+        :param force: do not fail if the remote already exist (but default it fails)
         :param index: if not defined, the new remote will be last one. Pass an integer to insert
           the remote in that position instead of the last one
         """
@@ -143,7 +143,7 @@ class RemotesAPI:
 
         :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without
           wildcards like "*" and no remote is found matching that exact name, it will raise an error.
-        :return: The list of removed ``Remote`` objects
+        :return: The list of removed :ref:`Remote <conan.api.model.Remote>` objects
         """
         remotes = _load(self._remotes_file)
         removed = _filter(remotes, pattern, only_enabled=False)
@@ -220,7 +220,7 @@ class RemotesAPI:
         """
         Perform user authentication against the given remote with the provided username and password
 
-        :param remote: a ``Remote`` object
+        :param remote: a :ref:`Remote <conan.api.model.Remote>` object
         :param username: the user login as ``str``
         :param password: password ``str``
         """
@@ -229,9 +229,9 @@ class RemotesAPI:
 
     def user_logout(self, remote: Remote):
         """
-        Logout from the given ``Remote``
+        Logout from the given :ref:`Remote <conan.api.model.Remote>`
 
-        :param remote: The ``Remote`` object to logout
+        :param remote: The :ref:`Remote <conan.api.model.Remote>` object to logout
         """
         localdb = LocalDB(self._home_folder)
         # The localdb only stores url + username + token, not remote name, so use URL as key
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index e2daf568f..fd5421cc8 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -257,7 +257,7 @@ class AutotoolsToolchain:
         env.append("LDFLAGS", self.ldflags)
         env.prepend_path("PKG_CONFIG_PATH", self._conanfile.generators_folder)
         # Issue related: https://github.com/conan-io/conan/issues/15486
-        if self._is_cross_building and self._conanfile.conf_build:
+        if self._is_cross_building and getattr(self._conanfile, 'conf_build', False):
             compilers_build_mapping = (
                 self._conanfile.conf_build.get("tools.build:compiler_executables", default={},
                                                check_type=dict)
diff --git a/conan/tools/gnu/gnutoolchain.py b/conan/tools/gnu/gnutoolchain.py
index 1b22240e8..a23f4bf0b 100644
--- a/conan/tools/gnu/gnutoolchain.py
+++ b/conan/tools/gnu/gnutoolchain.py
@@ -186,7 +186,7 @@ class GnuToolchain:
                     compiler = unix_path(self._conanfile, compiler)
                     ret[env_var] = compiler  # User/tools ones have precedence
         # Issue related: https://github.com/conan-io/conan/issues/15486
-        if self._is_cross_building and self._conanfile.conf_build:
+        if self._is_cross_building and getattr(self._conanfile, 'conf_build', False):
             compilers_build_mapping = (
                 self._conanfile.conf_build.get("tools.build:compiler_executables", default={},
                                                check_type=dict)
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index ae1c860de..4f5fff28e 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -187,7 +187,7 @@ class RemoteManager:
             raise
 
     def search_recipes(self, remote, pattern):
-        cached_method = remote.caching.setdefault("search_recipes", {})
+        cached_method = remote._caching.setdefault("search_recipes", {})
         try:
             return cached_method[pattern]
         except KeyError:
@@ -241,7 +241,7 @@ class RemoteManager:
             if options:
                 headers['Conan-PkgID-Options'] = ';'.join(options)
 
-        cached_method = remote.caching.setdefault("get_latest_package_reference", {})
+        cached_method = remote._caching.setdefault("get_latest_package_reference", {})
         try:
             return cached_method[pref]
         except KeyError:


Test patch:
diff --git a/test/integration/toolchains/gnu/test_autotoolstoolchain.py b/test/integration/toolchains/gnu/test_autotoolstoolchain.py
index e7fdbfc05..c15908f98 100644
--- a/test/integration/toolchains/gnu/test_autotoolstoolchain.py
+++ b/test/integration/toolchains/gnu/test_autotoolstoolchain.py
@@ -341,3 +341,36 @@ def test_toolchain_crossbuild_to_android():
         "conanfile.py": consumer
     })
     client.run("create . -pr:h host -pr:b build")
+
+
+def test_conf_build_does_not_exist():
+    host = textwrap.dedent("""
+    [settings]
+    arch=x86_64
+    build_type=Release
+    compiler=gcc
+    compiler.cppstd=gnu17
+    compiler.libcxx=libstdc++11
+    compiler.version=13
+    os=Linux
+    [conf]
+    tools.build:compiler_executables={'c': 'x86_64-linux-gnu-gcc', 'cpp': 'x86_64-linux-gnu-g++'}
+    """)
+    build = textwrap.dedent("""
+    [settings]
+    arch=armv8
+    build_type=Release
+    compiler=gcc
+    compiler.cppstd=gnu17
+    compiler.libcxx=libstdc++11
+    compiler.version=13
+    os=Linux
+    """)
+    c = TestClient()
+    c.save({
+        "conanfile.py": GenConanfile("pkg", "0.1"),
+        "host": host,
+        "build": build
+    })
+    c.run("export .")
+    c.run("install --requires=pkg/0.1 --build=pkg/0.1 -g AutotoolsToolchain -pr:h host -pr:b build")
diff --git a/test/integration/toolchains/gnu/test_gnutoolchain.py b/test/integration/toolchains/gnu/test_gnutoolchain.py
index 0982ef010..a5fe7f3f6 100644
--- a/test/integration/toolchains/gnu/test_gnutoolchain.py
+++ b/test/integration/toolchains/gnu/test_gnutoolchain.py
@@ -430,3 +430,36 @@ def test_msvc_profile_defaults():
         "consumer/conanfile.py": consumer
     })
     client.run("create consumer -pr:a profile --build=missing")
+
+
+def test_conf_build_does_not_exist():
+    host = textwrap.dedent("""
+    [settings]
+    arch=x86_64
+    build_type=Release
+    compiler=gcc
+    compiler.cppstd=gnu17
+    compiler.libcxx=libstdc++11
+    compiler.version=13
+    os=Linux
+    [conf]
+    tools.build:compiler_executables={'c': 'x86_64-linux-gnu-gcc', 'cpp': 'x86_64-linux-gnu-g++'}
+    """)
+    build = textwrap.dedent("""
+    [settings]
+    arch=armv8
+    build_type=Release
+    compiler=gcc
+    compiler.cppstd=gnu17
+    compiler.libcxx=libstdc++11
+    compiler.version=13
+    os=Linux
+    """)
+    c = TestClient()
+    c.save({
+        "conanfile.py": GenConanfile("pkg", "0.1"),
+        "host": host,
+        "build": build
+    })
+    c.run("export .")
+    c.run("install --requires=pkg/0.1 --build=pkg/0.1 -g GnuToolchain -pr:h host -pr:b build")