Repository: conan-io/conan
Pull request #16919 (2024-09-01 23:13:35)

Prepare build compatibles refactor
Changelog: Omit
Docs: Omit

This should be a pure refactor.
In preparation for https://github.com/conan-io/conan/pull/16871

- Move from ``root_node.cant_build`` => ``root_node.conanfile.info.cant_build``, so the ``cant_build`` information belongs per ``info``, so multiple "compatible" packages can be analyzed if they can be built or not.
- Split the ``def _process_compatible_packages(self, node, remotes, update):`` into ``_get_compatibles`` and ``_compatible_found()`` so they can be reused


Patch:
diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py
index 5b943f16b..3bd34b2ef 100644
--- a/conan/api/subapi/config.py
+++ b/conan/api/subapi/config.py
@@ -34,7 +34,7 @@ class ConfigAPI:
     def install(self, path_or_url, verify_ssl, config_type=None, args=None,
                 source_folder=None, target_folder=None):
         # TODO: We probably want to split this into git-folder-http cases?
-        from conans.client.conf.config_installer import configuration_install
+        from conan.internal.api.config.config_installer import configuration_install
         app = ConanApp(self.conan_api)
         configuration_install(app, path_or_url, verify_ssl, config_type=config_type, args=args,
                               source_folder=source_folder, target_folder=target_folder)
@@ -89,7 +89,7 @@ class ConfigAPI:
                                        "skipping configuration install")
                     return pkg.pref  # Already installed, we can skip repeating the install
 
-        from conans.client.conf.config_installer import configuration_install
+        from conan.internal.api.config.config_installer import configuration_install
         configuration_install(app, uri=pkg.conanfile.package_folder, verify_ssl=False,
                               config_type="dir", ignore=["conaninfo.txt", "conanmanifest.txt"])
         # We save the current package full reference in the file for future
diff --git a/conan/api/subapi/export.py b/conan/api/subapi/export.py
index ab8d410cd..4eeb0201b 100644
--- a/conan/api/subapi/export.py
+++ b/conan/api/subapi/export.py
@@ -1,6 +1,6 @@
 from conan.api.output import ConanOutput
 from conan.internal.conan_app import ConanApp
-from conans.client.cmd.export import cmd_export
+from conan.internal.api.export import cmd_export
 from conans.client.conanfile.package import run_package_method
 from conans.client.graph.graph import BINARY_BUILD, RECIPE_INCACHE
 from conans.model.package_ref import PkgReference
diff --git a/conan/api/subapi/install.py b/conan/api/subapi/install.py
index 26bc0e202..72867f65d 100644
--- a/conan/api/subapi/install.py
+++ b/conan/api/subapi/install.py
@@ -64,8 +64,8 @@ class InstallAPI:
             msg = "{}: Invalid ID: {}: {}".format(conanfile, binary, reason)
             raise ConanInvalidConfiguration(msg)
 
-        if root_node.cant_build and root_node.should_build:
-            binary, reason = "Cannot build for this configuration", root_node.cant_build
+        if conanfile.info is not None and conanfile.info.cant_build and root_node.should_build:
+            binary, reason = "Cannot build for this configuration", conanfile.info.cant_build
             msg = "{}: {}: {}".format(conanfile, binary, reason)
             raise ConanInvalidConfiguration(msg)
 
diff --git a/conan/api/subapi/profiles.py b/conan/api/subapi/profiles.py
index 62d50fd65..d3849525a 100644
--- a/conan/api/subapi/profiles.py
+++ b/conan/api/subapi/profiles.py
@@ -144,7 +144,7 @@ class ProfilesAPI:
         :return: an automatically detected Profile, with a "best guess" of the system settings
         """
         profile = Profile()
-        from conans.client.conf.detect import detect_defaults_settings
+        from conan.internal.api.profile.detect import detect_defaults_settings
         settings = detect_defaults_settings()
         for name, value in settings:
             profile.settings[name] = value
diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py
index 6113c5463..cc151a761 100644
--- a/conan/api/subapi/upload.py
+++ b/conan/api/subapi/upload.py
@@ -5,7 +5,7 @@ from multiprocessing.pool import ThreadPool
 from conan.api.output import ConanOutput
 from conan.internal.conan_app import ConanApp
 from conan.internal.upload_metadata import gather_metadata
-from conans.client.cmd.uploader import PackagePreparator, UploadExecutor, UploadUpstreamChecker
+from conan.internal.api.uploader import PackagePreparator, UploadExecutor, UploadUpstreamChecker
 from conans.client.pkg_sign import PkgSignaturesPlugin
 from conans.client.rest.file_uploader import FileUploader
 from conans.errors import ConanException, AuthenticationException, ForbiddenException
diff --git a/conans/client/cmd/__init__.py b/conan/internal/api/config/__init__.py
similarity index 100%
rename from conans/client/cmd/__init__.py
rename to conan/internal/api/config/__init__.py
diff --git a/conans/client/conf/config_installer.py b/conan/internal/api/config/config_installer.py
similarity index 100%
rename from conans/client/conf/config_installer.py
rename to conan/internal/api/config/config_installer.py
diff --git a/conans/client/cmd/export.py b/conan/internal/api/export.py
similarity index 100%
rename from conans/client/cmd/export.py
rename to conan/internal/api/export.py
diff --git a/conans/client/conf/detect.py b/conan/internal/api/profile/detect.py
similarity index 100%
rename from conans/client/conf/detect.py
rename to conan/internal/api/profile/detect.py
diff --git a/conans/client/cmd/uploader.py b/conan/internal/api/uploader.py
similarity index 100%
rename from conans/client/cmd/uploader.py
rename to conan/internal/api/uploader.py
diff --git a/conans/client/graph/compute_pid.py b/conans/client/graph/compute_pid.py
index cbfff7099..2ed1d6a5f 100644
--- a/conans/client/graph/compute_pid.py
+++ b/conans/client/graph/compute_pid.py
@@ -65,7 +65,7 @@ def compute_package_id(node, new_config, config_version):
                     conanfile.validate_build()
                 except ConanInvalidConfiguration as e:
                     # This 'cant_build' will be ignored if we don't have to build the node.
-                    node.cant_build = str(e)
+                    conanfile.info.cant_build = str(e)
 
     run_validate_package_id(conanfile)
 
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index 74a7f0273..284a05b20 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -64,7 +64,6 @@ class Node(object):
         self.dependencies = []  # Ordered Edges
         self.dependants = []  # Edges
         self.error = None
-        self.cant_build = False  # It will set to a str with a reason if the validate_build() fails
         self.should_build = False  # If the --build or policy wants to build this binary
         self.build_allowed = False
         self.is_conf = False
@@ -238,7 +237,7 @@ class Node(object):
         result["build_id"] = build_id(self.conanfile)
         result["binary"] = self.binary
         # TODO: This doesn't match the model, check it
-        result["invalid_build"] = self.cant_build
+        result["invalid_build"] = getattr(getattr(self.conanfile, "info", None), "cant_build", False)
         result["info_invalid"] = getattr(getattr(self.conanfile, "info", None), "invalid", None)
         # Adding the conanfile information: settings, options, etc
         result.update(self.conanfile.serialize())
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 8fdfd4f23..f10991628 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -44,7 +44,7 @@ class GraphBinariesAnalyzer(object):
         if build_mode.forced(conanfile, ref, with_deps_to_build):
             node.should_build = True
             conanfile.output.info('Forced build from source')
-            node.binary = BINARY_BUILD if not node.cant_build else BINARY_INVALID
+            node.binary = BINARY_BUILD if not conanfile.info.cant_build else BINARY_INVALID
             node.prev = None
             return True
 
@@ -114,9 +114,8 @@ class GraphBinariesAnalyzer(object):
             return True
         self._evaluated[pref] = [node]
 
-    def _process_compatible_packages(self, node, remotes, update):
+    def _get_compatible_packages(self, node):
         conanfile = node.conanfile
-        original_binary = node.binary
         original_package_id = node.package_id
 
         compatibles = self._compatibility.compatibles(conanfile)
@@ -124,26 +123,29 @@ class GraphBinariesAnalyzer(object):
         if existing:  # Skip the check if same package_id
             conanfile.output.debug(f"Compatible package ID {original_package_id} equal to "
                                    "the default package ID: Skipping it.")
+        return compatibles
 
-        if not compatibles:
-            return
+    @staticmethod
+    def _compatible_found(conanfile, pkg_id, compatible_pkg):
+        diff = conanfile.info.dump_diff(compatible_pkg)
+        conanfile.output.success(f"Found compatible package '{pkg_id}': {diff}")
+        # So they are available in package_info() method
+        conanfile.info = compatible_pkg  # Redefine current
+
+        # TODO: Improve this interface
+        # The package_id method might have modified the settings to erase information,
+        # ensure we allow those new values
+        conanfile.settings = conanfile.settings.copy_conaninfo_settings()
+        conanfile.settings.update_values(compatible_pkg.settings.values_list)
+        # Trick to allow mutating the options (they were freeze=True)
+        conanfile.options = conanfile.options.copy_conaninfo_options()
+        conanfile.options.update_options(compatible_pkg.options)
+
+    def _find_existing_compatible_binaries(self, node, compatibles, remotes, update):
+        conanfile = node.conanfile
+        original_binary = node.binary
+        original_package_id = node.package_id
 
-        def _compatible_found(pkg_id, compatible_pkg):
-            diff = conanfile.info.dump_diff(compatible_pkg)
-            conanfile.output.success(f"Found compatible package '{pkg_id}': {diff}")
-            # So they are available in package_info() method
-            conanfile.info = compatible_pkg  # Redefine current
-
-            # TODO: Improve this interface
-            # The package_id method might have modified the settings to erase information,
-            # ensure we allow those new values
-            conanfile.settings = conanfile.settings.copy_conaninfo_settings()
-            conanfile.settings.update_values(compatible_pkg.settings.values_list)
-            # Trick to allow mutating the options (they were freeze=True)
-            conanfile.options = conanfile.options.copy_conaninfo_options()
-            conanfile.options.update_options(compatible_pkg.options)
-
-        conanfile.output.info(f"Main binary package '{original_package_id}' missing")
         conanfile.output.info(f"Checking {len(compatibles)} compatible configurations")
         for package_id, compatible_package in compatibles.items():
             if should_update_reference(node.ref, update):
@@ -153,7 +155,7 @@ class GraphBinariesAnalyzer(object):
             node.binary = None  # Invalidate it
             self._process_compatible_node(node, remotes, update)  # TODO: what if BINARY_BUILD
             if node.binary in (BINARY_CACHE, BINARY_UPDATE, BINARY_DOWNLOAD):
-                _compatible_found(package_id, compatible_package)
+                self._compatible_found(conanfile, package_id, compatible_package)
                 return
         if not should_update_reference(conanfile.ref, update):
             conanfile.output.info(f"Compatible configurations not found in cache, checking servers")
@@ -164,8 +166,9 @@ class GraphBinariesAnalyzer(object):
                 node.binary = None  # Invalidate it
                 self._evaluate_download(node, remotes, update)
                 if node.binary == BINARY_DOWNLOAD:
-                    _compatible_found(package_id, compatible_package)
+                    self._compatible_found(conanfile, package_id, compatible_package)
                     return
+
         # If no compatible is found, restore original state
         node.binary = original_binary
         node._package_id = original_package_id
@@ -176,14 +179,17 @@ class GraphBinariesAnalyzer(object):
         assert node.prev is None, "Node.prev should be None"
 
         self._process_node(node, build_mode, remotes, update)
+        original_package_id = node.package_id
         if node.binary == BINARY_MISSING \
                 and not build_mode.should_build_missing(node.conanfile) and not node.should_build:
-            self._process_compatible_packages(node, remotes, update)
+            compatibles = self._get_compatible_packages(node)
+            node.conanfile.output.info(f"Main binary package '{original_package_id}' missing")
+            self._find_existing_compatible_binaries(node, compatibles, remotes, update)
 
         if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
             node.should_build = True
             node.build_allowed = True
-            node.binary = BINARY_BUILD if not node.cant_build else BINARY_INVALID
+            node.binary = BINARY_BUILD if not node.conanfile.info.cant_build else BINARY_INVALID
 
         if node.binary == BINARY_BUILD:
             conanfile = node.conanfile
diff --git a/conans/client/graph/install_graph.py b/conans/client/graph/install_graph.py
index dbba4d845..82e6e1700 100644
--- a/conans/client/graph/install_graph.py
+++ b/conans/client/graph/install_graph.py
@@ -498,8 +498,8 @@ class InstallGraph:
         msg = ["There are invalid packages:"]
         for package in invalid:
             node = package.nodes[0]
-            if node.cant_build and node.should_build:
-                binary, reason = "Cannot build for this configuration", node.cant_build
+            if node.conanfile.info.cant_build and node.should_build:
+                binary, reason = "Cannot build for this configuration", node.conanfile.info.cant_build
             else:
                 binary, reason = "Invalid", node.conanfile.info.invalid
             msg.append("{}: {}: {}".format(node.conanfile, binary, reason))
diff --git a/conans/client/rest_client_local_recipe_index.py b/conans/client/rest_client_local_recipe_index.py
index e806fe3fe..39de35508 100644
--- a/conans/client/rest_client_local_recipe_index.py
+++ b/conans/client/rest_client_local_recipe_index.py
@@ -9,7 +9,7 @@ import yaml
 from conan.api.model import LOCAL_RECIPES_INDEX
 from conan.api.output import ConanOutput
 from conan.internal.cache.home_paths import HomePaths
-from conans.client.cmd.export import cmd_export
+from conan.internal.api.export import cmd_export
 from conans.client.loader import ConanFileLoader
 from conans.errors import ConanException, PackageNotFoundException, RecipeNotFoundException, \
     ConanReferenceDoesNotExistInDB, NotFoundException
diff --git a/conans/model/info.py b/conans/model/info.py
index 7968a6cbf..d6ede1166 100644
--- a/conans/model/info.py
+++ b/conans/model/info.py
@@ -328,6 +328,7 @@ class ConanInfo:
     def __init__(self, settings=None, options=None, reqs_info=None, build_requires_info=None,
                  python_requires=None, conf=None, config_version=None):
         self.invalid = None
+        self.cant_build = False  # It will set to a str with a reason if the validate_build() fails
         self.settings = settings
         self.settings_target = None  # needs to be explicitly defined by recipe package_id()
         self.options = options


Test patch:
diff --git a/test/functional/command/config_install_test.py b/test/functional/command/config_install_test.py
index 5e5d668c5..ad16aae8d 100644
--- a/test/functional/command/config_install_test.py
+++ b/test/functional/command/config_install_test.py
@@ -8,7 +8,7 @@ import pytest
 from mock import patch
 
 from conan.api.model import Remote
-from conans.client.conf.config_installer import _hide_password
+from conan.internal.api.config.config_installer import _hide_password
 from conans.client.downloaders.file_downloader import FileDownloader
 from conan.internal.paths import DEFAULT_CONAN_HOME
 from conan.test.assets.genconanfile import GenConanfile
diff --git a/test/functional/command/profile_test.py b/test/functional/command/profile_test.py
index f8362416d..8e67b3390 100644
--- a/test/functional/command/profile_test.py
+++ b/test/functional/command/profile_test.py
@@ -5,7 +5,7 @@ import unittest
 
 import pytest
 
-from conans.client.conf.detect import detect_defaults_settings
+from conan.internal.api.profile.detect import detect_defaults_settings
 from conan.test.utils.mocks import RedirectedTestOutput
 from conan.test.utils.tools import TestClient, redirect_output
 from conan.test.utils.env import environment_update
diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py
index 1e344ae45..a37008435 100644
--- a/test/integration/command/upload/upload_test.py
+++ b/test/integration/command/upload/upload_test.py
@@ -116,7 +116,7 @@ class UploadTest(unittest.TestCase):
 
         def gzopen_patched(name, mode="r", fileobj=None, **kwargs):
             raise ConanException("Error gzopen %s" % name)
-        with patch('conans.client.cmd.uploader.gzopen_without_timestamps', new=gzopen_patched):
+        with patch('conan.internal.api.uploader.gzopen_without_timestamps', new=gzopen_patched):
             client.run("upload * --confirm -r default --only-recipe",
                        assert_error=True)
             self.assertIn("Error gzopen conan_sources.tgz", client.out)
@@ -145,7 +145,7 @@ class UploadTest(unittest.TestCase):
             if name == PACKAGE_TGZ_NAME:
                 raise ConanException("Error gzopen %s" % name)
             return gzopen_without_timestamps(name, mode, fileobj, **kwargs)
-        with patch('conans.client.cmd.uploader.gzopen_without_timestamps', new=gzopen_patched):
+        with patch('conan.internal.api.uploader.gzopen_without_timestamps', new=gzopen_patched):
             client.run("upload * --confirm -r default", assert_error=True)
             self.assertIn("Error gzopen conan_package.tgz", client.out)
 
diff --git a/test/unittests/client/conf/config_installer/test_install_folder.py b/test/unittests/client/conf/config_installer/test_install_folder.py
index f2eecc1f3..1f72d626e 100644
--- a/test/unittests/client/conf/config_installer/test_install_folder.py
+++ b/test/unittests/client/conf/config_installer/test_install_folder.py
@@ -1,7 +1,7 @@
 import unittest
 import os
 
-from conans.client.conf.config_installer import tmp_config_install_folder
+from conan.internal.api.config.config_installer import tmp_config_install_folder
 from conan.test.utils.test_files import temp_folder
 
 
diff --git a/test/unittests/client/remote_manager_test.py b/test/unittests/client/remote_manager_test.py
index e0438bcd8..871cf6097 100644
--- a/test/unittests/client/remote_manager_test.py
+++ b/test/unittests/client/remote_manager_test.py
@@ -1,7 +1,7 @@
 import os
 import unittest
 
-from conans.client.cmd.uploader import compress_files
+from conan.internal.api.uploader import compress_files
 from conan.internal.paths import PACKAGE_TGZ_NAME
 from conan.test.utils.test_files import temp_folder
 from conans.util.files import save
diff --git a/test/unittests/util/detect_test.py b/test/unittests/util/detect_test.py
index c0e3c8b7e..0309715e8 100644
--- a/test/unittests/util/detect_test.py
+++ b/test/unittests/util/detect_test.py
@@ -6,7 +6,7 @@ import pytest
 from parameterized import parameterized
 
 from conan.internal.api.detect_api import _cc_compiler
-from conans.client.conf.detect import detect_defaults_settings
+from conan.internal.api.profile.detect import detect_defaults_settings
 from conans.model.version import Version
 from conan.test.utils.mocks import RedirectedTestOutput
 from conan.test.utils.tools import redirect_output