Repository: conan-io/conan

Issue:
[feature] Components: implement internal dependencies and link order resolution
Coming from #5090 

Use ``deps`` or ``require`` to indicate the list of components that a component depends on:

```
self.cpp_info.components["comp1"].deps = ["comp2", "comp3"]
```

or prepare to model components depending on other components of a different library:

```
self.cpp_info.components["liba"].requires = ["::comp2", "other_lib::comp3"]
```


Discussion:
"other_lib::comp3" is c++, why not just a simple dot? `other_lib.comp3`
> "other_lib::comp3" is c++, why not just a simple dot? `other_lib.comp3`

We need a symbol forbidden as a package name, the dot is already used by `approvaltests.cpp`. Current regex: `"^[a-zA-Z0-9_][a-zA-Z0-9_\+\.-]{%s,%s}$"`

I think `::` is a good one, it resembles the CMake targets notation.
I'll mention it here (not sure if it something for the generators too/only), but we should think about library loops  https://github.com/conan-io/conan/issues/6530

Resolving patch:
diff --git a/conans/client/installer.py b/conans/client/installer.py
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -562,7 +562,8 @@ def _call_package_info(self, conanfile, package_folder, ref):
                     conanfile.package_info()
                     if conanfile._conan_dep_cpp_info is None:
                         try:
-                            conanfile.cpp_info._raise_if_mixing_components()
+                            conanfile.cpp_info._raise_incorrect_components_definition(
+                                conanfile.name, conanfile.requires)
                         except ConanException as e:
                             raise ConanException("%s package_info(): %s" % (str(conanfile), e))
                         conanfile._conan_dep_cpp_info = DepCppInfo(conanfile.cpp_info)
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -1,5 +1,6 @@
 import os
 from collections import OrderedDict
+from copy import copy
 
 from conans.errors import ConanException
 from conans.util.conan_v2_mode import conan_v2_behavior
@@ -12,6 +13,8 @@
 DEFAULT_BUILD = ""
 DEFAULT_FRAMEWORK = "Frameworks"
 
+COMPONENT_SCOPE = "::"
+
 
 class DefaultOrderedDict(OrderedDict):
 
@@ -24,6 +27,12 @@ def __getitem__(self, key):
             super(DefaultOrderedDict, self).__setitem__(key, self.factory())
         return super(DefaultOrderedDict, self).__getitem__(key)
 
+    def __copy__(self):
+        the_copy = DefaultOrderedDict(self.factory)
+        for key, value in super(DefaultOrderedDict, self).items():
+            the_copy[key] = value
+        return the_copy
+
 
 class _CppInfo(object):
     """ Object that stores all the necessary information to build in C/C++.
@@ -148,6 +157,7 @@ def __init__(self, rootpath):
         self.resdirs.append(DEFAULT_RES)
         self.builddirs.append(DEFAULT_BUILD)
         self.frameworkdirs.append(DEFAULT_FRAMEWORK)
+        self.requires = []
 
 
 class CppInfo(_CppInfo):
@@ -185,11 +195,13 @@ def _get_cpp_info():
 
         return self.configs.setdefault(config, _get_cpp_info())
 
-    def _raise_if_mixing_components(self):
+    def _raise_incorrect_components_definition(self, package_name, package_requires):
+        # Raise if mixing components
         if (self.includedirs != [DEFAULT_INCLUDE] or
                 self.libdirs != [DEFAULT_LIB] or
                 self.bindirs != [DEFAULT_BIN] or
                 self.resdirs != [DEFAULT_RES] or
+                self.builddirs != [DEFAULT_BUILD] or
                 self.frameworkdirs != [DEFAULT_FRAMEWORK] or
                 self.libs or
                 self.system_libs or
@@ -206,6 +218,31 @@ def _raise_if_mixing_components(self):
             raise ConanException("self.cpp_info.components cannot be used with self.cpp_info configs"
                                  " (release/debug/...) at the same time")
 
+        # Raise on component name
+        for comp_name, comp in self.components.items():
+            if comp_name == package_name:
+                raise ConanException("Component name cannot be the same as the package name: '%s'"
+                                     % comp_name)
+
+        if self.components:
+            comp_requires = set()
+            for comp_name, comp in self.components.items():
+                for comp_require in comp.requires:
+                    if COMPONENT_SCOPE in comp_require:
+                        comp_requires.add(
+                            comp_require[:comp_require.find(COMPONENT_SCOPE)])
+            pkg_requires = [require.ref.name for require in package_requires.values()]
+            # Raise on components requires without package requires
+            for pkg_require in pkg_requires:
+                if pkg_require not in comp_requires:
+                    raise ConanException("Package require '%s' not used in components requires"
+                                         % pkg_require)
+            # Raise on components requires requiring inexistent package requires
+            for comp_require in comp_requires:
+                if comp_require not in pkg_requires:
+                    raise ConanException("Package require '%s' declared in components requires "
+                                         "but not defined as a recipe requirement" % comp_require)
+
 
 class _BaseDepsCppInfo(_CppInfo):
     def __init__(self):
@@ -293,6 +330,8 @@ def __init__(self, cpp_info):
         self._src_paths = None
         self._framework_paths = None
         self._build_module_paths = None
+        self._sorted_components = None
+        self._check_component_requires()
 
     def __getattr__(self, item):
         try:
@@ -311,7 +350,7 @@ def _aggregated_values(self, item):
             return values
         values = getattr(self._cpp_info, item)
         if self._cpp_info.components:
-            for component in self._cpp_info.components.values():
+            for component in self._get_sorted_components().values():
                 values = self._merge_lists(values, getattr(component, item))
         setattr(self, "_%s" % item, values)
         return values
@@ -322,11 +361,54 @@ def _aggregated_paths(self, item):
             return paths
         paths = getattr(self._cpp_info, "%s_paths" % item)
         if self._cpp_info.components:
-            for component in self._cpp_info.components.values():
+            for component in self._get_sorted_components().values():
                 paths = self._merge_lists(paths, getattr(component, "%s_paths" % item))
         setattr(self, "_%s_paths" % item, paths)
         return paths
 
+    @staticmethod
+    def _filter_component_requires(requires):
+        return [r for r in requires if COMPONENT_SCOPE not in r]
+
+    def _check_component_requires(self):
+        for comp_name, comp in self._cpp_info.components.items():
+            if not all([require in self._cpp_info.components for require in
+                        self._filter_component_requires(comp.requires)]):
+                raise ConanException("Component '%s' declares a missing dependency" % comp_name)
+            bad_requires = [r for r in comp.requires if r.startswith(COMPONENT_SCOPE)]
+            if bad_requires:
+                msg = "Leading character '%s' not allowed in %s requires: %s. Omit it to require " \
+                      "components inside the same package." \
+                      % (COMPONENT_SCOPE, comp_name, bad_requires)
+                raise ConanException(msg)
+
+    def _get_sorted_components(self):
+        """
+        Sort Components from most dependent one first to the less dependent one last
+        :return: List of sorted components
+        """
+        if not self._sorted_components:
+            if any([[require for require in self._filter_component_requires(comp.requires)]
+                    for comp in self._cpp_info.components.values()]):
+                ordered = OrderedDict()
+                components = copy(self._cpp_info.components)
+                while len(ordered) != len(self._cpp_info.components):
+                    # Search next element to be processed
+                    for comp_name, comp in components.items():
+                        # Check if component is not required and can be added to ordered
+                        if comp_name not in [require for dep in components.values() for require in
+                                             self._filter_component_requires(dep.requires)]:
+                            ordered[comp_name] = comp
+                            del components[comp_name]
+                            break
+                    else:
+                        raise ConanException("There is a dependency loop in "
+                                             "'self.cpp_info.components' requires")
+                self._sorted_components = ordered
+            else:  # If components do not have requirements, keep them in the same order
+                self._sorted_components = self._cpp_info.components
+        return self._sorted_components
+
     @property
     def build_modules_paths(self):
         return self._aggregated_paths("build_modules")


Test patch:
diff --git a/conans/test/integration/package_info_test.py b/conans/test/integration/package_info_test.py
--- a/conans/test/integration/package_info_test.py
+++ b/conans/test/integration/package_info_test.py
@@ -1,9 +1,12 @@
+import os
 import textwrap
 import unittest
 
 from conans.errors import ConanException
+from conans.model.ref import ConanFileReference, PackageReference
 from conans.paths import CONANFILE, CONANFILE_TXT
-from conans.test.utils.tools import TestClient
+from conans.test.utils.genconanfile import GenConanfile
+from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient
 
 
 class TestPackageInfo(unittest.TestCase):
@@ -152,6 +155,7 @@ class Intermediate(ConanFile):
                 requires = "dep/1.0@us/ch"
 
                 def package_info(self):
+                    self.cpp_info.components["int1"].requires = ["dep::dep"]  # To avoid exception
                     self.cpp_info.components["int1"].libs.append("libint1")
                     self.cpp_info.components["int1"].defines.append("defint1")
                     os.mkdir(os.path.join(self.package_folder, "include"))
@@ -210,7 +214,7 @@ def package_info_raise_components_test(self):
         conanfile = textwrap.dedent("""
             from conans import ConanFile
 
-            class Intermediate(ConanFile):
+            class MyConan(ConanFile):
 
                 def package_info(self):
                     self.cpp_info.defines.append("defint")
@@ -225,7 +229,7 @@ def package_info(self):
         conanfile = textwrap.dedent("""
             from conans import ConanFile
 
-            class Intermediate(ConanFile):
+            class MyConan(ConanFile):
 
                 def package_info(self):
                     self.cpp_info.release.defines.append("defint")
@@ -235,3 +239,236 @@ def package_info(self):
         client.run("create conanfile.py dep/1.0@us/ch", assert_error=True)
         self.assertIn("dep/1.0@us/ch package_info(): self.cpp_info.components cannot be used "
                       "with self.cpp_info configs (release/debug/...) at the same time", client.out)
+
+        conanfile = textwrap.dedent("""
+                    from conans import ConanFile
+
+                    class MyConan(ConanFile):
+
+                        def package_info(self):
+                            self.cpp_info.components["dep"].libs.append("libint1")
+                """)
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py dep/1.0@us/ch", assert_error=True)
+        self.assertIn("dep/1.0@us/ch package_info(): Component name cannot be the same as the "
+                      "package name: 'dep'", client.out)
+
+    def package_info_components_complete_test(self):
+        dep = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+            class Dep(ConanFile):
+                exports_sources = "*"
+                def package(self):
+                    self.copy("*")
+                def package_info(self):
+                    self.cpp_info.name = "Galaxy"
+                    self.cpp_info.components["Starlight"].includedirs = [os.path.join("galaxy", "starlight")]
+                    self.cpp_info.components["Starlight"].libs = ["libstarlight"]
+                    self.cpp_info.components["Planet"].includedirs = [os.path.join("galaxy", "planet")]
+                    self.cpp_info.components["Planet"].libs = ["libplanet"]
+                    self.cpp_info.components["Planet"].requires = ["Starlight"]
+                    self.cpp_info.components["Launcher"].system_libs = ["ground"]
+                    self.cpp_info.components["ISS"].includedirs = [os.path.join("galaxy", "iss")]
+                    self.cpp_info.components["ISS"].libs = ["libiss"]
+                    self.cpp_info.components["ISS"].libdirs = ["iss_libs"]
+                    self.cpp_info.components["ISS"].system_libs = ["solar", "magnetism"]
+                    self.cpp_info.components["ISS"].requires = ["Starlight", "Launcher"]
+        """)
+        consumer = textwrap.dedent("""
+        from conans import ConanFile
+        class Consumer(ConanFile):
+            requires = "dep/1.0@us/ch"
+            def build(self):
+                # Global values
+                self.output.info("GLOBAL Include paths: %s" % self.deps_cpp_info.include_paths)
+                self.output.info("GLOBAL Library paths: %s" % self.deps_cpp_info.lib_paths)
+                self.output.info("GLOBAL Binary paths: %s" % self.deps_cpp_info.bin_paths)
+                self.output.info("GLOBAL Libs: %s" % self.deps_cpp_info.libs)
+                self.output.info("GLOBAL Exes: %s" % self.deps_cpp_info.exes)
+                self.output.info("GLOBAL System libs: %s" % self.deps_cpp_info.system_libs)
+                # Deps values
+                for dep_key, dep_value in self.deps_cpp_info.dependencies:
+                    self.output.info("DEPS name: %s" % dep_value.name)
+                    self.output.info("DEPS Include paths: %s" % dep_value.include_paths)
+                    self.output.info("DEPS Library paths: %s" % dep_value.lib_paths)
+                    self.output.info("DEPS Binary paths: %s" % dep_value.bin_paths)
+                    self.output.info("DEPS Libs: %s" % dep_value.libs)
+                    self.output.info("DEPS System libs: %s" % dep_value.system_libs)
+                # Components values
+                for dep_key, dep_value in self.deps_cpp_info.dependencies:
+                    for comp_name, comp_value in dep_value.components.items():
+                        self.output.info("COMP %s Include paths: %s" % (comp_name,
+                        comp_value.include_paths))
+                        self.output.info("COMP %s Library paths: %s" % (comp_name, comp_value.lib_paths))
+                        self.output.info("COMP %s Binary paths: %s" % (comp_name, comp_value.bin_paths))
+                        self.output.info("COMP %s Libs: %s" % (comp_name, comp_value.libs))
+                        self.output.info("COMP %s Requires: %s" % (comp_name, comp_value.requires))
+                        self.output.info("COMP %s System libs: %s" % (comp_name, comp_value.system_libs))
+        """)
+
+        client = TestClient()
+        client.save({"conanfile_dep.py": dep, "conanfile_consumer.py": consumer,
+                     "galaxy/starlight/starlight.h": "",
+                     "lib/libstarlight": "",
+                     "galaxy/planet/planet.h": "",
+                     "lib/libplanet": "",
+                     "galaxy/iss/iss.h": "",
+                     "iss_libs/libiss": "",
+                     "bin/exelauncher": ""})
+        dep_ref = ConanFileReference("dep", "1.0", "us", "ch")
+        dep_pref = PackageReference(dep_ref, NO_SETTINGS_PACKAGE_ID)
+        client.run("create conanfile_dep.py dep/1.0@us/ch")
+        client.run("create conanfile_consumer.py consumer/1.0@us/ch")
+        package_folder = client.cache.package_layout(dep_ref).package(dep_pref)
+
+        expected_comp_starlight_include_paths = [os.path.join(package_folder, "galaxy", "starlight")]
+        expected_comp_planet_include_paths = [os.path.join(package_folder, "galaxy", "planet")]
+        expected_comp_launcher_include_paths = []
+        expected_comp_iss_include_paths = [os.path.join(package_folder, "galaxy", "iss")]
+        expected_comp_starlight_library_paths = [os.path.join(package_folder, "lib")]
+        expected_comp_launcher_library_paths = [os.path.join(package_folder, "lib")]
+        expected_comp_planet_library_paths = [os.path.join(package_folder, "lib")]
+        expected_comp_iss_library_paths = [os.path.join(package_folder, "iss_libs")]
+        expected_comp_starlight_binary_paths = [os.path.join(package_folder, "bin")]
+        expected_comp_launcher_binary_paths = [os.path.join(package_folder, "bin")]
+        expected_comp_planet_binary_paths = [os.path.join(package_folder, "bin")]
+        expected_comp_iss_binary_paths = [os.path.join(package_folder, "bin")]
+
+        expected_global_include_paths = expected_comp_planet_include_paths + \
+            expected_comp_iss_include_paths + expected_comp_starlight_include_paths
+        expected_global_library_paths = expected_comp_starlight_library_paths + \
+            expected_comp_iss_library_paths
+        expected_global_binary_paths = expected_comp_starlight_binary_paths
+
+        self.assertIn("GLOBAL Include paths: %s" % expected_global_include_paths, client.out)
+        self.assertIn("GLOBAL Library paths: %s" % expected_global_library_paths, client.out)
+        self.assertIn("GLOBAL Binary paths: %s" % expected_global_binary_paths, client.out)
+        self.assertIn("GLOBAL Libs: ['libplanet', 'libiss', 'libstarlight']", client.out)
+        self.assertIn("GLOBAL System libs: ['solar', 'magnetism', 'ground']", client.out)
+
+        self.assertIn("DEPS name: Galaxy", client.out)
+        self.assertIn("DEPS Include paths: %s" % expected_global_include_paths, client.out)
+        self.assertIn("DEPS Library paths: %s" % expected_global_library_paths, client.out)
+        self.assertIn("DEPS Binary paths: %s" % expected_global_binary_paths, client.out)
+        self.assertIn("DEPS Libs: ['libplanet', 'libiss', 'libstarlight']", client.out)
+        self.assertIn("DEPS System libs: ['solar', 'magnetism', 'ground']", client.out)
+
+        self.assertIn("COMP Starlight Include paths: %s" % expected_comp_starlight_include_paths,
+                      client.out)
+        self.assertIn("COMP Planet Include paths: %s" % expected_comp_planet_include_paths,
+                      client.out)
+        self.assertIn("COMP Launcher Include paths: %s" % expected_comp_launcher_include_paths,
+                      client.out)
+        self.assertIn("COMP ISS Include paths: %s" % expected_comp_iss_include_paths, client.out)
+        self.assertIn("COMP Starlight Library paths: %s" % expected_comp_starlight_library_paths,
+                      client.out)
+        self.assertIn("COMP Planet Library paths: %s" % expected_comp_planet_library_paths,
+                      client.out)
+        self.assertIn("COMP Launcher Library paths: %s" % expected_comp_launcher_library_paths,
+                      client.out)
+        self.assertIn("COMP ISS Library paths: %s" % expected_comp_iss_library_paths, client.out)
+        self.assertIn("COMP Starlight Binary paths: %s" % expected_comp_iss_binary_paths, client.out)
+        self.assertIn("COMP Planet Binary paths: %s" % expected_comp_planet_binary_paths, client.out)
+        self.assertIn("COMP Launcher Binary paths: %s" % expected_comp_launcher_binary_paths,
+                      client.out)
+        self.assertIn("COMP ISS Binary paths: %s" % expected_comp_iss_binary_paths, client.out)
+        self.assertIn("COMP Starlight Libs: ['libstarlight']", client.out)
+        self.assertIn("COMP Planet Libs: ['libplanet']", client.out)
+        self.assertIn("COMP Launcher Libs: []", client.out)
+        self.assertIn("COMP ISS Libs: ['libiss']", client.out)
+        self.assertIn("COMP Starlight System libs: []", client.out)
+        self.assertIn("COMP Planet System libs: []", client.out)
+        self.assertIn("COMP Launcher System libs: ['ground']", client.out)
+        self.assertIn("COMP ISS System libs: ['solar', 'magnetism']", client.out)
+        self.assertIn("COMP Starlight Requires: []", client.out)
+        self.assertIn("COMP Launcher Requires: []", client.out)
+        self.assertIn("COMP Planet Requires: ['Starlight']", client.out)
+        self.assertIn("COMP ISS Requires: ['Starlight', 'Launcher']", client.out)
+
+    def package_requires_in_components_requires_test(self):
+        client = TestClient()
+        client.save({"conanfile1.py": GenConanfile("dep1", "0.1"),
+                     "conanfile2.py": GenConanfile("dep2", "0.1")})
+        client.run("create conanfile1.py")
+        client.run("create conanfile2.py")
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": []}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep1' not used in "
+                      "components requires", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep1::dep1"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep2' not used in components "
+                      "requires", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep1::dep1"]},
+                                                        "kkk": {"requires": ["kk"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep2' not used in components "
+                      "requires", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": []},
+                                                        "kkk": {"requires": ["kk"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep1' not used in components "
+                      "requires", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep2::comp"]},
+                                                        "kkk": {"requires": ["dep3::dep3"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep1' not used in components "
+                      "requires", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep2::comp"]},
+                                                        "kkk": {"requires": ["dep3::dep3"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep3' declared in components "
+                      "requires but not defined as a recipe requirement", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep2::comp"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py", assert_error=True)
+        self.assertIn("consumer/0.1 package_info(): Package require 'dep2' declared in components "
+                      "requires but not defined as a recipe requirement", client.out)
+
+        conanfile = GenConanfile("consumer", "0.1") \
+            .with_requirement_plain("dep1/0.1") \
+            .with_requirement_plain("dep2/0.1") \
+            .with_package_info(cpp_info={"components": {"kk": {"requires": ["dep2::comp"]},
+                                                        "kkk": {"requires": ["dep1::dep1"]}}},
+                               env_info={})
+        client.save({"conanfile.py": conanfile})
+        client.run("create conanfile.py")  # Correct usage
diff --git a/conans/test/unittests/model/build_info/components_test.py b/conans/test/unittests/model/build_info/components_test.py
--- a/conans/test/unittests/model/build_info/components_test.py
+++ b/conans/test/unittests/model/build_info/components_test.py
@@ -3,8 +3,12 @@
 import unittest
 import warnings
 
+import six
+
+from conans.errors import ConanException
 from conans.model.build_info import CppInfo, DepsCppInfo, DepCppInfo
 from conans.test.utils.test_files import temp_folder
+from conans.util.files import save
 
 
 class CppInfoComponentsTest(unittest.TestCase):
@@ -176,3 +180,241 @@ def test_deps_cpp_info_libs_release_debug(self):
         self.assertListEqual([], deps_cpp_info["dep1"].debug.libs)
         self.assertListEqual(["libdep2_d"], deps_cpp_info["dep2"].debug.libs)
         self.assertListEqual(["libdep2_d"], deps_cpp_info.debug.libs)
+
+    def cpp_info_link_order_test(self):
+
+        def _assert_link_order(sorted_libs):
+            """
+            Assert that dependent libs of a component are always found later in the list
+            """
+            assert sorted_libs, "'sorted_libs' is empty"
+            for num, lib in enumerate(sorted_libs):
+                component_name = lib[-1]
+                for dep in info.components[component_name].requires:
+                    for lib in info.components[dep].libs:
+                        self.assertIn(lib, sorted_libs[num:])
+
+        info = CppInfo("")
+        info.components["6"].libs = ["lib6"]
+        info.components["6"].requires = ["4", "5"]
+        info.components["5"].libs = ["lib5"]
+        info.components["5"].requires = ["2"]
+        info.components["4"].libs = ["lib4"]
+        info.components["4"].requires = ["1"]
+        info.components["3"].libs = ["lib3"]
+        info.components["3"].requires = ["1"]
+        info.components["1"].libs = ["lib1"]
+        info.components["1"].requires = ["2"]
+        info.components["2"].libs = ["lib2"]
+        info.components["2"].requires = []
+        dep_cpp_info = DepCppInfo(info)
+        _assert_link_order(dep_cpp_info.libs)
+        self.assertEqual(["lib6", "lib5", "lib4", "lib3", "lib1", "lib2"], dep_cpp_info.libs)
+        deps_cpp_info = DepsCppInfo()
+        deps_cpp_info.update(dep_cpp_info, "dep1")
+        self.assertEqual(["lib6", "lib5", "lib4", "lib3", "lib1", "lib2"],
+                         deps_cpp_info.libs)
+
+        info = CppInfo("")
+        info.components["K"].libs = ["libK"]
+        info.components["K"].requires = ["G", "H"]
+        info.components["J"].libs = ["libJ"]
+        info.components["J"].requires = ["F"]
+        info.components["G"].libs = ["libG"]
+        info.components["G"].requires = ["F"]
+        info.components["H"].libs = ["libH"]
+        info.components["H"].requires = ["F", "E"]
+        info.components["L"].libs = ["libL"]
+        info.components["L"].requires = ["I"]
+        info.components["F"].libs = ["libF"]
+        info.components["F"].requires = ["C", "D"]
+        info.components["I"].libs = ["libI"]
+        info.components["I"].requires = ["E"]
+        info.components["C"].libs = ["libC"]
+        info.components["C"].requires = ["A"]
+        info.components["D"].libs = ["libD"]
+        info.components["D"].requires = ["A"]
+        info.components["E"].libs = ["libE"]
+        info.components["E"].requires = ["A", "B"]
+        info.components["A"].libs = ["libA"]
+        info.components["A"].requires = []
+        info.components["B"].libs = ["libB"]
+        info.components["B"].requires = []
+        dep_cpp_info = DepCppInfo(info)
+        _assert_link_order(dep_cpp_info.libs)
+        self.assertEqual(["libK", "libJ", "libG", "libH", "libL", "libF", "libI", "libC", "libD",
+                          "libE", "libA", "libB"], dep_cpp_info.libs)
+        deps_cpp_info.update(dep_cpp_info, "dep2")
+        self.assertEqual(["lib6", "lib5", "lib4", "lib3", "lib1", "lib2","libK", "libJ", "libG",
+                          "libH", "libL", "libF", "libI", "libC", "libD", "libE", "libA", "libB"],
+                         deps_cpp_info.libs)
+
+    def cppinfo_inexistent_component_dep_test(self):
+        info = CppInfo(None)
+        info.components["LIB1"].requires = ["LIB2"]
+        with six.assertRaisesRegex(self, ConanException, "Component 'LIB1' "
+                                                         "declares a missing dependency"):
+            DepCppInfo(info).libs
+        info.components["LIB1"].requires = ["::LIB2"]
+        with six.assertRaisesRegex(self, ConanException, "Leading character '::' not allowed in "
+                                                         "LIB1 requires"):
+            DepCppInfo(info).libs
+
+    def cpp_info_components_requires_loop_test(self):
+        info = CppInfo("")
+        info.components["LIB1"].requires = ["LIB1"]
+        msg = "There is a dependency loop in 'self.cpp_info.components' requires"
+        with six.assertRaisesRegex(self, ConanException, msg):
+            DepCppInfo(info).libs
+        info = CppInfo("")
+        info.components["LIB1"].requires = ["LIB2"]
+        info.components["LIB2"].requires = ["LIB1", "LIB2"]
+        with six.assertRaisesRegex(self, ConanException, msg):
+            DepCppInfo(info).build_paths
+        info = CppInfo("")
+        info.components["LIB1"].requires = ["LIB2"]
+        info.components["LIB2"].requires = ["LIB3"]
+        info.components["LIB3"].requires = ["LIB1"]
+        with six.assertRaisesRegex(self, ConanException, msg):
+            DepCppInfo(info).defines
+
+    def components_libs_order_test(self):
+        info = CppInfo("")
+        info.components["liba"].libs = ["liba"]
+        info.components["libb"].libs = ["libb"]
+        dep_cpp_info = DepCppInfo(info)
+        self.assertListEqual(["liba", "libb"], dep_cpp_info.libs)
+        deps_cpp_info = DepsCppInfo()
+        deps_cpp_info.update(dep_cpp_info, "dep1")
+        self.assertListEqual(["liba", "libb"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["liba", "libb"], deps_cpp_info.libs)
+
+        info = CppInfo("")
+        info.components["liba"].libs = ["liba"]
+        info.components["libb"].libs = ["libb"]
+        dep_cpp_info = DepCppInfo(info)
+        info2 = CppInfo("")
+        info2.components["libc"].libs = ["libc"]
+        dep_cpp_info2 = DepCppInfo(info2)
+        deps_cpp_info = DepsCppInfo()
+        # Update in reverse order
+        deps_cpp_info.update(dep_cpp_info2, "dep2")
+        deps_cpp_info.update(dep_cpp_info, "dep1")
+        self.assertListEqual(["liba", "libb"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libc"], deps_cpp_info["dep2"].libs)
+        self.assertListEqual(["libc", "liba", "libb"], deps_cpp_info.libs)
+
+        info = CppInfo("")
+        info.components["liba"].libs = ["liba"]
+        info.components["libb"].libs = ["libb"]
+        info.components["libb"].requires = ["liba"]
+        dep_cpp_info = DepCppInfo(info)
+        self.assertListEqual(["libb", "liba"], dep_cpp_info.libs)
+        deps_cpp_info = DepsCppInfo()
+        deps_cpp_info.update(dep_cpp_info, "dep1")
+        self.assertListEqual(["libb", "liba"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libb", "liba"], deps_cpp_info.libs)
+
+        info = CppInfo("")
+        info.components["liba"].libs = ["liba"]
+        info.components["libb"].libs = ["libb"]
+        info.components["libb"].requires = ["liba"]
+        dep_cpp_info = DepCppInfo(info)
+        info2 = CppInfo("")
+        info2.components["libc"].libs = ["libc"]
+        dep_cpp_info2 = DepCppInfo(info2)
+        deps_cpp_info = DepsCppInfo()
+        # Update in reverse order
+        deps_cpp_info.update(dep_cpp_info2, "dep2")
+        deps_cpp_info.update(dep_cpp_info, "dep1")
+        self.assertListEqual(["libb", "liba"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libc"], deps_cpp_info["dep2"].libs)
+        self.assertListEqual(["libc", "libb", "liba"], deps_cpp_info.libs)
+
+    def cppinfo_components_dirs_test(self):
+        folder = temp_folder()
+        info = CppInfo(folder)
+        info.name = "OpenSSL"
+        info.components["OpenSSL"].includedirs = ["include"]
+        info.components["OpenSSL"].libdirs = ["lib"]
+        info.components["OpenSSL"].builddirs = ["build"]
+        info.components["OpenSSL"].bindirs = ["bin"]
+        info.components["OpenSSL"].resdirs = ["res"]
+        info.components["Crypto"].includedirs = ["headers"]
+        info.components["Crypto"].libdirs = ["libraries"]
+        info.components["Crypto"].builddirs = ["build_scripts"]
+        info.components["Crypto"].bindirs = ["binaries"]
+        info.components["Crypto"].resdirs = ["resources"]
+        self.assertEqual(["include"], info.components["OpenSSL"].includedirs)
+        self.assertEqual(["lib"], info.components["OpenSSL"].libdirs)
+        self.assertEqual(["build"], info.components["OpenSSL"].builddirs)
+        self.assertEqual(["bin"], info.components["OpenSSL"].bindirs)
+        self.assertEqual(["res"], info.components["OpenSSL"].resdirs)
+        self.assertEqual(["headers"], info.components["Crypto"].includedirs)
+        self.assertEqual(["libraries"], info.components["Crypto"].libdirs)
+        self.assertEqual(["build_scripts"], info.components["Crypto"].builddirs)
+        self.assertEqual(["binaries"], info.components["Crypto"].bindirs)
+        self.assertEqual(["resources"], info.components["Crypto"].resdirs)
+
+        info.components["Crypto"].includedirs = ["different_include"]
+        info.components["Crypto"].libdirs = ["different_lib"]
+        info.components["Crypto"].builddirs = ["different_build"]
+        info.components["Crypto"].bindirs = ["different_bin"]
+        info.components["Crypto"].resdirs = ["different_res"]
+        self.assertEqual(["different_include"], info.components["Crypto"].includedirs)
+        self.assertEqual(["different_lib"], info.components["Crypto"].libdirs)
+        self.assertEqual(["different_build"], info.components["Crypto"].builddirs)
+        self.assertEqual(["different_bin"], info.components["Crypto"].bindirs)
+        self.assertEqual(["different_res"], info.components["Crypto"].resdirs)
+
+        info.components["Crypto"].includedirs.extend(["another_include"])
+        info.components["Crypto"].includedirs.append("another_other_include")
+        info.components["Crypto"].libdirs.extend(["another_lib"])
+        info.components["Crypto"].libdirs.append("another_other_lib")
+        info.components["Crypto"].builddirs.extend(["another_build"])
+        info.components["Crypto"].builddirs.append("another_other_build")
+        info.components["Crypto"].bindirs.extend(["another_bin"])
+        info.components["Crypto"].bindirs.append("another_other_bin")
+        info.components["Crypto"].resdirs.extend(["another_res"])
+        info.components["Crypto"].resdirs.append("another_other_res")
+        self.assertEqual(["different_include", "another_include", "another_other_include"],
+                         info.components["Crypto"].includedirs)
+        self.assertEqual(["different_lib", "another_lib", "another_other_lib"],
+                         info.components["Crypto"].libdirs)
+        self.assertEqual(["different_build", "another_build", "another_other_build"],
+                         info.components["Crypto"].builddirs)
+        self.assertEqual(["different_bin", "another_bin", "another_other_bin"],
+                         info.components["Crypto"].bindirs)
+        self.assertEqual(["different_res", "another_res", "another_other_res"],
+                         info.components["Crypto"].resdirs)
+
+    def component_default_dirs_deps_cpp_info_test(self):
+        folder = temp_folder()
+        info = CppInfo(folder)
+        info.components["Component"]
+        info.components["Component"].filter_empty = False  # For testing purposes
+        dep_info = DepCppInfo(info)
+        deps_cpp_info = DepsCppInfo()
+        deps_cpp_info.update(dep_info, "my_lib")
+        self.assertListEqual([os.path.join(folder, "include")], deps_cpp_info.includedirs)
+        self.assertListEqual([], deps_cpp_info.srcdirs)
+        self.assertListEqual([os.path.join(folder, "lib")], deps_cpp_info.libdirs)
+        self.assertListEqual([os.path.join(folder, "bin")], deps_cpp_info.bindirs)
+        self.assertListEqual([os.path.join(folder, "")], deps_cpp_info.builddirs)
+        self.assertListEqual([os.path.join(folder, "res")], deps_cpp_info.resdirs)
+        self.assertListEqual([os.path.join(folder, "Frameworks")], deps_cpp_info.frameworkdirs)
+
+    def deps_cpp_info_components_test(self):
+        folder = temp_folder()
+        info = CppInfo(folder)
+        # Create file so path is not cleared
+        save(os.path.join(folder, "include", "my_file.h"), "")
+        info.components["Component"].libs = ["libcomp"]
+        dep_info = DepCppInfo(info)
+        deps_cpp_info = DepsCppInfo()
+        deps_cpp_info.update(dep_info, "my_lib")
+        self.assertListEqual(["libcomp"], deps_cpp_info.libs)
+        self.assertListEqual(["libcomp"], deps_cpp_info["my_lib"].components["Component"].libs)
+        self.assertListEqual([os.path.join(folder, "include")], deps_cpp_info.include_paths)
+        self.assertListEqual([os.path.join(folder, "include")],
+                             deps_cpp_info["my_lib"].components["Component"].include_paths)
diff --git a/conans/test/unittests/model/build_info_test.py b/conans/test/unittests/model/build_info_test.py
--- a/conans/test/unittests/model/build_info_test.py
+++ b/conans/test/unittests/model/build_info_test.py
@@ -209,3 +209,25 @@ def cpp_info_build_modules_test(self):
                              deps_cpp_info["myname"].build_modules_paths)
         self.assertListEqual([os.path.join(folder, "mod-release.cmake")],
                              deps_cpp_info["myname"].debug.build_modules_paths)
+
+    def cppinfo_public_interface_test(self):
+        folder = temp_folder()
+        info = CppInfo(folder)
+        self.assertEqual([], info.libs)
+        self.assertEqual([], info.system_libs)
+        self.assertEqual(["include"], info.includedirs)
+        self.assertEqual([], info.srcdirs)
+        self.assertEqual(["res"], info.resdirs)
+        self.assertEqual([""], info.builddirs)
+        self.assertEqual(["bin"], info.bindirs)
+        self.assertEqual(["lib"], info.libdirs)
+        self.assertEqual(folder, info.rootpath)
+        self.assertEqual([], info.defines)
+        self.assertIsNone(info.name)
+        self.assertEqual("", info.sysroot)
+        self.assertEqual([], info.cflags)
+        self.assertEqual({}, info.configs)
+        self.assertEqual([], info.cxxflags)
+        self.assertEqual([], info.exelinkflags)
+        self.assertEqual([], info.public_deps)
+        self.assertEqual([], info.sharedlinkflags)
diff --git a/conans/test/utils/genconanfile.py b/conans/test/utils/genconanfile.py
--- a/conans/test/utils/genconanfile.py
+++ b/conans/test/utils/genconanfile.py
@@ -254,7 +254,13 @@ def _package_info_method(self):
         lines = []
         if "cpp_info" in self._package_info:
             for k, v in self._package_info["cpp_info"].items():
-                lines.append('        self.cpp_info.{} = {}'.format(k, str(v)))
+                if k == "components":
+                    for comp_name, comp in v.items():
+                        for comp_attr_name, comp_attr_value in comp.items():
+                            lines.append('        self.cpp_info.components["{}"].{} = {}'.format(
+                                comp_name, comp_attr_name, str(comp_attr_value)))
+                else:
+                    lines.append('        self.cpp_info.{} = {}'.format(k, str(v)))
         if "env_info" in self._package_info:
             for k, v in self._package_info["env_info"].items():
                 lines.append('        self.env_info.{} = {}'.format(k, str(v)))