Repository: conan-io/conan
Pull request #17036 (2024-09-23 08:32:23)

defining CMAKE_SYSTEM_XXX even if toolchain is defined, but protected
Changelog: Fix: Let ``CMakeToolchain`` defining ``CMAKE_SYSTEM_XXX`` even if ``user_toolchain`` is defined, but protected in case the toolchain really defines them.
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/16807





Patch:
diff --git a/conan/cli/commands/remote.py b/conan/cli/commands/remote.py
index 8cd437110..b1737fe06 100644
--- a/conan/cli/commands/remote.py
+++ b/conan/cli/commands/remote.py
@@ -196,14 +196,19 @@ def remote_login(conan_api, parser, subparser, *args):
         raise ConanException("There are no remotes matching the '{}' pattern".format(args.remote))
 
     creds = RemoteCredentials(conan_api.cache_folder, conan_api.config.global_conf)
-    user, password = creds.auth(args.remote, args.username, args.password)
-    if args.username is not None and args.username != user:
-        raise ConanException(f"User '{args.username}' doesn't match user '{user}' in "
-                             f"credentials.json or environment variables")
 
     ret = OrderedDict()
     for r in remotes:
         previous_info = conan_api.remotes.user_info(r)
+
+        if args.username is not None and args.password is not None:
+            user, password = args.username, args.password
+        else:
+            user, password = creds.auth(r, args.username)
+            if args.username is not None and args.username != user:
+                raise ConanException(f"User '{args.username}' doesn't match user '{user}' in "
+                                     f"credentials.json or environment variables")
+
         conan_api.remotes.user_login(r, user, password)
         info = conan_api.remotes.user_info(r)
         ret[r.name] = {"previous_info": previous_info, "info": info}
diff --git a/conan/internal/cache/home_paths.py b/conan/internal/cache/home_paths.py
index ba2402ded..30e79002b 100644
--- a/conan/internal/cache/home_paths.py
+++ b/conan/internal/cache/home_paths.py
@@ -55,6 +55,14 @@ class HomePaths:
     def profile_plugin_path(self):
         return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "profile.py")
 
+    @property
+    def auth_remote_plugin_path(self):
+        return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "auth_remote.py")
+
+    @property
+    def auth_source_plugin_path(self):
+        return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "auth_source.py")
+
     @property
     def sign_plugin_path(self):
         return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "sign", "sign.py")
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index a493f8c44..7ce0761ec 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -890,13 +890,19 @@ class GenericSystemBlock(Block):
         {% endif %}
         {% if cmake_system_name %}
         # Cross building
+        if(NOT DEFINED CMAKE_SYSTEM_NAME) # It might have been defined by a user toolchain
         set(CMAKE_SYSTEM_NAME {{ cmake_system_name }})
+        endif()
         {% endif %}
         {% if cmake_system_version %}
+        if(NOT DEFINED CMAKE_SYSTEM_VERSION) # It might have been defined by a user toolchain
         set(CMAKE_SYSTEM_VERSION {{ cmake_system_version }})
+        endif()
         {% endif %}
         {% if cmake_system_processor %}
+        if(NOT DEFINED CMAKE_SYSTEM_PROCESSOR) # It might have been defined by a user toolchain
         set(CMAKE_SYSTEM_PROCESSOR {{ cmake_system_processor }})
+        endif()
         {% endif %}
 
         {% if generator_platform and not winsdk_version %}
@@ -1042,15 +1048,13 @@ class GenericSystemBlock(Block):
         return version_mapping.get(os_name, {}).get(str(os_version))
 
     def _get_cross_build(self):
-        user_toolchain = self._conanfile.conf.get("tools.cmake.cmaketoolchain:user_toolchain")
-
         system_name = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_name")
         system_version = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_version")
         system_processor = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_processor")
 
         # try to detect automatically
-        if not user_toolchain and not is_universal_arch(self._conanfile.settings.get_safe("arch"),
-                                                        self._conanfile.settings.possible_values().get("arch")):
+        if not is_universal_arch(self._conanfile.settings.get_safe("arch"),
+                                 self._conanfile.settings.possible_values().get("arch")):
             os_host = self._conanfile.settings.get_safe("os")
             os_host_version = self._conanfile.settings.get_safe("os.version")
             arch_host = self._conanfile.settings.get_safe("arch")
@@ -1074,7 +1078,6 @@ class GenericSystemBlock(Block):
                         _system_processor = arch_host
                     _system_version = os_host_version
 
-
                 if system_name is not None and system_version is None:
                     system_version = _system_version
                 if system_name is not None and system_processor is None:
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index cf1ab8dde..35b5c9ddc 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -71,7 +71,7 @@ class ConanApiAuthManager(object):
         credentials are stored in localdb and rest method is called"""
         for _ in range(LOGIN_RETRIES):
             creds = RemoteCredentials(self._cache_folder, self._global_conf)
-            input_user, input_password = creds.auth(remote.name)
+            input_user, input_password = creds.auth(remote)
             try:
                 self._authenticate(remote, input_user, input_password)
             except AuthenticationException:
diff --git a/conans/client/rest/conan_requester.py b/conans/client/rest/conan_requester.py
index 4903ac181..73974a79c 100644
--- a/conans/client/rest/conan_requester.py
+++ b/conans/client/rest/conan_requester.py
@@ -9,8 +9,11 @@ import urllib3
 from jinja2 import Template
 from requests.adapters import HTTPAdapter
 
+from conan.internal.cache.home_paths import HomePaths
+
 from conans import __version__ as client_version
-from conans.errors import ConanException
+from conans.client.loader import load_python_file
+from conans.errors import ConanException, scoped_traceback
 
 # Capture SSL warnings as pointed out here:
 # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
@@ -30,8 +33,11 @@ class _SourceURLCredentials:
     """
     def __init__(self, cache_folder):
         self._urls = {}
+        self._auth_source_plugin = None
         if not cache_folder:
             return
+        auth_source_plugin_path = HomePaths(cache_folder).auth_source_plugin_path
+        self._auth_source_plugin = _load_auth_source_plugin(auth_source_plugin_path)
         creds_path = os.path.join(cache_folder, "source_credentials.json")
         if not os.path.exists(creds_path):
             return
@@ -61,6 +67,22 @@ class _SourceURLCredentials:
             raise ConanException(f"Error loading 'source_credentials.json' {creds_path}: {repr(e)}")
 
     def add_auth(self, url, kwargs):
+        # First, try to use "auth_source_plugin"
+        if self._auth_source_plugin:
+            try:
+                c = self._auth_source_plugin(url)
+            except Exception as e:
+                msg = f"Error while processing 'auth_source_remote.py' plugin"
+                msg = scoped_traceback(msg, e, scope="/extensions/plugins")
+                raise ConanException(msg)
+            if c:
+                if c.get("token"):
+                    kwargs["headers"]["Authorization"] = f"Bearer {c.get('token')}"
+                if c.get("user") and c.get("password"):
+                    kwargs["auth"] = (c.get("user"), c.get("password"))
+                return
+
+        # Then, try to find the credentials in "_urls"
         for u, creds in self._urls.items():
             if url.startswith(u):
                 token = creds.get("token")
@@ -183,3 +205,9 @@ class ConanRequester:
             if popped:
                 os.environ.clear()
                 os.environ.update(old_env)
+
+
+def _load_auth_source_plugin(auth_source_plugin_path):
+    if os.path.exists(auth_source_plugin_path):
+        mod, _ = load_python_file(auth_source_plugin_path)
+        return getattr(mod, "auth_source_plugin", None)
diff --git a/conans/client/rest/remote_credentials.py b/conans/client/rest/remote_credentials.py
index 33dbe638d..701af5776 100644
--- a/conans/client/rest/remote_credentials.py
+++ b/conans/client/rest/remote_credentials.py
@@ -4,9 +4,12 @@ import platform
 
 from jinja2 import Template
 
+from conan.internal.cache.home_paths import HomePaths
+
+from conans.client.loader import load_python_file
 from conan.api.output import ConanOutput
 from conans.client.userio import UserInput
-from conans.errors import ConanException
+from conans.errors import ConanException, scoped_traceback
 from conans.util.files import load
 
 
@@ -14,6 +17,8 @@ class RemoteCredentials:
     def __init__(self, cache_folder, global_conf):
         self._global_conf = global_conf
         self._urls = {}
+        auth_plugin_path = HomePaths(cache_folder).auth_remote_plugin_path
+        self._auth_remote_plugin = _load_auth_remote_plugin(auth_plugin_path)
         creds_path = os.path.join(cache_folder, "credentials.json")
         if not os.path.exists(creds_path):
             return
@@ -28,12 +33,20 @@ class RemoteCredentials:
         except Exception as e:
             raise ConanException(f"Error loading 'credentials.json' {creds_path}: {repr(e)}")
 
-    def auth(self, remote, user=None, password=None):
-        if user is not None and password is not None:
-            return user, password
+    def auth(self, remote, user=None):
+        # First get the auth_remote_plugin
+        if self._auth_remote_plugin is not None:
+            try:
+                plugin_user, plugin_password = self._auth_remote_plugin(remote, user=user)
+            except Exception as e:
+                msg = f"Error while processing 'auth_remote.py' plugin"
+                msg = scoped_traceback(msg, e, scope="/extensions/plugins")
+                raise ConanException(msg)
+            if plugin_user and plugin_password:
+                return plugin_user, plugin_password
 
-        # First prioritize the cache "credentials.json" file
-        creds = self._urls.get(remote)
+        # Then prioritize the cache "credentials.json" file
+        creds = self._urls.get(remote.name)
         if creds is not None:
             try:
                 return creds["user"], creds["password"]
@@ -41,7 +54,7 @@ class RemoteCredentials:
                 raise ConanException(f"Authentication error, wrong credentials.json: {e}")
 
         # Then, check environment definition
-        env_user, env_passwd = self._get_env(remote, user)
+        env_user, env_passwd = self._get_env(remote.name, user)
         if env_passwd is not None:
             if env_user is None:
                 raise ConanException("Found password in env-var, but not defined user")
@@ -49,7 +62,7 @@ class RemoteCredentials:
 
         # If not found, then interactive prompt
         ui = UserInput(self._global_conf.get("core:non_interactive", check_type=bool))
-        input_user, input_password = ui.request_login(remote, user)
+        input_user, input_password = ui.request_login(remote.name, user)
         return input_user, input_password
 
     @staticmethod
@@ -66,3 +79,9 @@ class RemoteCredentials:
         if passwd:
             ConanOutput().info("Got password '******' from environment")
         return user, passwd
+
+
+def _load_auth_remote_plugin(auth_remote_plugin_path):
+    if os.path.exists(auth_remote_plugin_path):
+        mod, _ = load_python_file(auth_remote_plugin_path)
+        return getattr(mod, "auth_remote_plugin", None)


Test patch:
diff --git a/test/integration/configuration/conf/test_auth_source_plugin.py b/test/integration/configuration/conf/test_auth_source_plugin.py
new file mode 100644
index 000000000..c87116a63
--- /dev/null
+++ b/test/integration/configuration/conf/test_auth_source_plugin.py
@@ -0,0 +1,82 @@
+import json
+import os
+import textwrap
+
+import pytest
+
+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
+
+
+class TestAuthSourcePlugin:
+    @pytest.fixture
+    def setup_test_client(self):
+        self.client = TestClient(default_server_user=True, light=True)
+        self.file_server = TestFileServer()
+        self.client.servers["file_server"] = self.file_server
+        self.download_cache_folder = temp_folder()
+        self.client.save_home({"global.conf": "tools.files.download:retry=0"})
+
+        save(os.path.join(self.file_server.store, "myfile.txt"), "Bye, world!")
+
+        conanfile = textwrap.dedent(f"""
+                           from conan import ConanFile
+                           from conan.tools.files import download
+                           class Pkg2(ConanFile):
+                               name = "pkg"
+                               version = "1.0"
+                               def source(self):
+                                   download(self, "{self.file_server.fake_url}/basic-auth/myfile.txt", "myfile.txt")
+                           """)
+        self.client.save({"conanfile.py": conanfile})
+
+        return self.client, self.file_server.fake_url
+
+    """ Test when the plugin fails, we want a clear message and a helpful trace
+    """
+    def test_error_source_plugin(self, setup_test_client):
+        c, url = setup_test_client
+        auth_plugin = textwrap.dedent("""\
+            def auth_source_plugin(url):
+                raise Exception("Test Error")
+            """)
+        save(os.path.join(c.cache.plugins_path, "auth_source.py"), auth_plugin)
+        c.run("source conanfile.py", assert_error=True)
+        assert "Test Error" in c.out
+
+    """ Test when the plugin give a correct and wrong password, we want a message about the success
+        or fail in login
+    """
+    @pytest.mark.parametrize("password", ["password", "bad-password"])
+    def test_auth_source_plugin_direct_credentials(self, password, setup_test_client):
+        should_fail = password == "bad-password"
+        c, url = setup_test_client
+        auth_plugin = textwrap.dedent(f"""\
+            def auth_source_plugin(url):
+                return { json.dumps({'user': 'user', 'password': password}) }
+            """)
+        save(os.path.join(c.cache.plugins_path, "auth_source.py"), auth_plugin)
+        c.run("source conanfile.py", assert_error=should_fail)
+        if should_fail:
+            assert "AuthenticationException" in c.out
+        else:
+            assert os.path.exists(os.path.join(c.current_folder, "myfile.txt"))
+
+    """ Test when the plugin do not give any user or password, we want the code to continue with
+        the rest of the input methods
+    """
+    def test_auth_source_plugin_fallback(self, setup_test_client):
+        c, url = setup_test_client
+        auth_plugin = textwrap.dedent("""\
+                def auth_source_plugin(url):
+                    return None
+                """)
+        save(os.path.join(c.cache.plugins_path, "auth_source.py"), auth_plugin)
+        source_credentials = json.dumps({"credentials": [{"url": url, "token": "password"}]})
+        save(os.path.join(c.cache_folder, "source_credentials.json"), source_credentials)
+        c.run("source conanfile.py")
+        # As the auth plugin is not returning any password the code is falling back to the rest of
+        # the input methods in this case provided by source_credentials.json.
+        assert os.path.exists(os.path.join(c.current_folder, "myfile.txt"))
diff --git a/test/integration/configuration/test_auth_remote_plugin.py b/test/integration/configuration/test_auth_remote_plugin.py
new file mode 100644
index 000000000..ab62507cc
--- /dev/null
+++ b/test/integration/configuration/test_auth_remote_plugin.py
@@ -0,0 +1,59 @@
+import os
+import textwrap
+
+import pytest
+
+from conan.test.utils.tools import TestClient
+from conans.util.files import save
+
+
+class TestAuthRemotePlugin:
+    """ Test when the plugin fails, we want a clear message and a helpful trace
+    """
+    def test_error_auth_remote_plugin(self):
+        c = TestClient(default_server_user=True)
+        auth_plugin = textwrap.dedent("""\
+            def auth_remote_plugin(remote, user=None):
+                raise Exception("Test Error")
+            """)
+        save(os.path.join(c.cache.plugins_path, "auth_remote.py"), auth_plugin)
+        c.run("remote logout default")
+        c.run("remote login default", assert_error=True)
+        assert "Error while processing 'auth_remote.py' plugin" in c.out
+        assert "ERROR: Error while processing 'auth_remote.py' plugin, line " in c.out
+
+    """ Test when the plugin give a correct and wrong password, we want a message about the success
+        or fail in login
+    """
+    @pytest.mark.parametrize("password", ["password", "bad-password"])
+    def test_auth_remote_plugin_direct_credentials(self, password):
+        should_fail = password == "bad-password"
+        c = TestClient(default_server_user=True)
+        auth_plugin = textwrap.dedent(f"""\
+            def auth_remote_plugin(remote, user=None):
+                return "admin", "{password}"
+            """)
+        save(os.path.join(c.cache.plugins_path, "auth_remote.py"), auth_plugin)
+        c.run("remote logout default")
+        c.run("remote login default", assert_error=should_fail)
+        if should_fail:
+            assert "ERROR: Wrong user or password. [Remote: default]" in c.out
+        else:
+            assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin' (authenticated)" in c.out
+
+
+    """ Test when the plugin do not give any user or password, we want the code to continue with
+        the rest of the input methods
+    """
+    def test_auth_remote_plugin_fallback(self):
+        c = TestClient(default_server_user=True)
+        auth_plugin = textwrap.dedent("""\
+                def auth_remote_plugin(remote, user=None):
+                    return None, None
+                """)
+        save(os.path.join(c.cache.plugins_path, "auth_remote.py"), auth_plugin)
+        c.run("remote logout default")
+        c.run("remote login default")
+        # As the auth plugin is not returning any password the code is falling back to the rest of
+        # the input methods in this case the stdin provided by TestClient.
+        assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin' (authenticated)" in c.out
diff --git a/test/integration/toolchains/cmake/test_cmaketoolchain.py b/test/integration/toolchains/cmake/test_cmaketoolchain.py
index a1ed3da24..7af778df4 100644
--- a/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -119,8 +119,16 @@ def test_cross_build_user_toolchain():
     client.run("install . --profile:build=windows --profile:host=rpi")
     toolchain = client.load("conan_toolchain.cmake")
 
-    assert "CMAKE_SYSTEM_NAME " not in toolchain
-    assert "CMAKE_SYSTEM_PROCESSOR" not in toolchain
+    # Fixed in https://github.com/conan-io/conan/issues/16807
+    expected = textwrap.dedent("""\
+        # Cross building
+        if(NOT DEFINED CMAKE_SYSTEM_NAME) # It might have been defined by a user toolchain
+        set(CMAKE_SYSTEM_NAME Linux)
+        endif()
+        if(NOT DEFINED CMAKE_SYSTEM_PROCESSOR) # It might have been defined by a user toolchain
+        set(CMAKE_SYSTEM_PROCESSOR aarch64)
+        endif()""")
+    assert expected in toolchain
 
 
 def test_cross_build_user_toolchain_confs():