Repository: conan-io/conan

Issue:
Feature: model intra-package dependencies
From https://github.com/conan-io/conan/issues/2382

This could help both to model system dependencies ``self.cpp_info.libs = ["mylib", "pthread"]``, and then some consumer can depend on pthread by other means, and the order mylib->pthread is not defined.

It can be generalized for your own libs deps "Mylib1"->"MyLib2" inside the same package.







Discussion:
Do you consider pthread an intra-package dependency? I'm just asking because a number of packages link against system libraries and to me, this doesn't really sound like an intra-package dependency. There are cases (like boost) which provides N libraries itself which may depend on each other in some way and then there are other cases where a package library depends on a system library
No, that is a system one. But current ``self.cpp_info.libs`` model does not support differentiating system and own libraries. We could check if it makes sense to add a ``self.cpp_info.system_libs``, but as the underlying issue is really that the relative order of libraries within a package is not defined, maybe just enabling to define this order will work both for intra-package dependencies and expressing dependencies from package libraries to system libraries as well.
so, for instance, if we have **ffmpeg** library which provides the following libraries: **libavcodec**, **libavformat**, **libavfilter**, etc. which might be used independently. so, it would be nice to define in **conanfile.py** something like this:
```
self.cpp_info.libavcodec.libs = ['avcodec']
self.cpp_info.libavformat.libs = ['avformat']
self.cpp_info.libavfilter.libs = ['avfilter']
```
and then, in consumer (e.g. CMake one), we can link to the particular libraries:
```
target_link_libraries(exe_using_avcodec PRIVATE CONAN_PKG::ffmpeg::libavcodec)
...
target_link_libraries(exe_using_avformat PRIVATE CONAN_PKG::ffmpeg::libavformat)
```
this is very conceptual, and actual syntax might be very different from my examples, but I guess you got the idea behind it.
same probably applies to lots of other multilib packages (like Qt, wxWidgets, OpenCV, GStreamer, etc.)
these libraries build as a single, and it's not feasible to split each of them into multiple conan files (as code will be the almost the same), like in boost case.
This feature should also integrate with frequently used build systems such as CMake. CMake has the information on intra-package dependencies (as well as other information such as public compile options etc.), and it would be very desirable if one wouldn't have to specify this redundantly in the conanfile.py
Automatically extracting information from CMake about this thing seems a huge challenge for me. Parsing and understanding the scripts files is almost impossible. Using the internal information files generated by cmake could be the way, but also would require a ton of work, and probably still be very fragile wrt cmake different versions, for example, so probably not worth the investment, at least in the first iteration. Having a model to model intra-package dependencies would already be a big and complex feature, so that level of automation won't be possible in the short term.
Yes, a CMake integration would of course build on top of that. Maybe it is a good idea to create a separate GitHub issue for that? It is quite a significant problem for us. 

Just to give some background on our use case:
We have packages that contain quite a lot of libraries (several packages contains 20-30 libraries, some even more), and we are losing valuable information, which is present in CMake. I am not too familiar with CMake on the other hand. Probably this can be solved on the CMake side in part by generating Find<package>.cmake files that contain the relevant information. At least when using CMake both on the supplier and consumer side, this might also work without modeling this within conan.
> Yes, a CMake integration would of course build on top of that. Maybe it is a good idea to create a separate GitHub issue for that? It is quite a significant problem for us.

Well, until this model has some progress, I think it is not very useful to have such an issue. As I said, it could be very complex and fragile, it is not likely that it will be implemented. So better wait and go step by step.

I think that if they are your own private packages, you might be good using ``find.cmake`` files for them, and then you might completely skip the ``package_info()`` method. No need to model intra-package dependencies at all, as they will be managed by cmake. The use cases are very different for your own private packages than when creating open-source packages for the community.
> The use cases are very different for your own private packages than when creating open-source packages for the community.

Well, I think our use cases are a subset of those that need to be supported by open source packages. However, what I sketched about cmake-cmake uses is not really optimal, since this requires the consumer to handle conan packages differently, depending on whether they were built with cmake (and exporting the cmake information) or not. I expect (and in some limited ways conan achieves that already) that a package manager allows to abstract from that, so that I do not need to care about the build systems used to build the packages.
I noticed that someone implemented a kind of CMake integration in #112
Feature needed for @vector-of-bool libman proposal at https://github.com/vector-of-bool/libman.

Related to https://github.com/conan-io/conan/issues/3931
@memsharded I came to this issue from #1323. I'm trying to define multiple `cpp_infos` like you reasoned about at the end of the thread.

Example:
```python
def package_info(self):
    self.cpp_info.libA.includedirs = ["libA/includes"]
    self.cpp_info.libB.includedirs = ["libB/includes"]
```

Currently (as of v1.11), the cmake generator will create variables for libA and libB, but will only create a single target. All the target property information is there for the cmake generator to make targets for libA and libB, but the generator isn't making targets for them. This portion of this issue is straight-forward. However, it is being held back by the the additional scope and complexity of intra-package dependencies.

Can we resurrect #1323 to implement the simple target generation? It should be a separate story. 


Hello,

I think it would be nice if this issue is linked to the getting started page for packaging. Anyone with multiple libs within the same package is going to stumble into this. 

Also, I agree with @sigiesec that it seems wasteful to not use the cmake information while building this order. (disclaimer: I don't know sufficient cmake to determine the complexity)
Hi @kenfred 

Could you please clarify what you mean with resurrecting #1323? That feature was already implemented, you can find CONAN_LIB::lib1, CONAN_LIB::lib2 targets for different libraries within a package in the ``conanbuildinfo.cmake``.


> Also, I agree with @sigiesec that it seems wasteful to not use the cmake information while building this order. (disclaimer: I don't know sufficient cmake to determine the complexity)

@skgbanga yes, it would be nice, but as commented above, parsing CMake to get this information is almost an impossible challenge, and would be a big source of failing corner cases, and a nightmare in maintenance, support and evolution. Furthermore, all of it would be only useful for CMake build system, but Conan has large usage with many other build systems, and we would need a feature that could be widely applicable to most of them. Defining a subgraph of library relations in a conanfile.py would take only a few minutes, it is mostly one time effort, and easily maintainable and understandable by consumers. It is not worth the effort.
> parsing CMake to get this information is almost an impossible challenge

I also don't think it is appropriate to parse the CMakeLists.txt files. However, this is also not necessary. CMake already has the option to export the information required by dependents using install(TARGETS), install(EXPORT): https://gitlab.kitware.com/cmake/community/wikis/doc/tutorials/Exporting-and-Importing-Targets#exporting-from-an-installation-tree

The current output of this is CMake code, but it has a specific structure, so it might be parseable already. Or CMake might be extended to output the same information in a well-defined way (JSON, e.g.).

If it were only about the dependencies, the Graphviz output using `cmake --graphviz=deps.dot` could already be used and parsed quite easily, but additional information is needed (all other library-level interface properties, such as compiler flags/definitions).

> we would need a feature that could be widely applicable to most of them

I don't see a contradiction here. Of course, the abstract model in conan must be independent from CMake. You have the option of specifiying the dependencies etc. manually, but you can also implement automatic extractors for specific build systems.

> ... and a nightmare in maintenance, support and evolution...
> Defining a subgraph of library relations ... is mostly one time effort, and easily maintainable

I assume you have a different complexity in mind. For our packages, it would be prohibitively complex to remodel the dependencies in the conanfile, apart from the fact that this introduces significant redundancies, which cannot be more than a workaround. 

IMO the reverse is the case: Not having such an automatic extraction/integration with the build system is a nightmare for maintenance, support and evolution of each package.
@memsharded 
I see that now! When I was looking to use the feature, I was expecting it to use `CONAN_PKG` namespace and not `CONAN_LIB`, so I missed it in the generated cmake file. Thanks for pointing it out.

> CMake might be extended to output the same information in a well-defined way (JSON, e.g.).

Such an extension would perfectly fit in with the file API added in cmake 3.14.0: https://cmake.org/cmake/help/v3.14/manual/cmake-file-api.7.html

Resolving patch:
diff --git a/conans/client/installer.py b/conans/client/installer.py
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -17,7 +17,7 @@
 from conans.client.tools.env import pythonpath
 from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
                            conanfile_exception_formatter)
-from conans.model.build_info import CppInfo
+from conans.model.build_info import CppInfo, DepCppInfo
 from conans.model.editable_layout import EditableLayout
 from conans.model.env_info import EnvInfo
 from conans.model.graph_info import GraphInfo
@@ -511,19 +511,19 @@ def _propagate_info(node, using_build_profile):
 
             if not using_build_profile:  # Do not touch anything
                 conan_file.deps_user_info[n.ref.name] = n.conanfile.user_info
-                conan_file.deps_cpp_info.update(n.conanfile.cpp_info, n.ref.name)
+                conan_file.deps_cpp_info.update(n.conanfile._conan_dep_cpp_info, n.ref.name)
                 conan_file.deps_env_info.update(n.conanfile.env_info, n.ref.name)
             else:
                 if n in transitive or n in br_host:
-                    conan_file.deps_cpp_info.update(n.conanfile.cpp_info, n.ref.name)
+                    conan_file.deps_cpp_info.update(n.conanfile._conan_dep_cpp_info, n.ref.name)
                 else:
                     env_info = EnvInfo()
                     env_info._values_ = n.conanfile.env_info._values_.copy()
                     # Add cpp_info.bin_paths/lib_paths to env_info (it is needed for runtime)
-                    env_info.DYLD_LIBRARY_PATH.extend(n.conanfile.cpp_info.lib_paths)
-                    env_info.DYLD_LIBRARY_PATH.extend(n.conanfile.cpp_info.framework_paths)
-                    env_info.LD_LIBRARY_PATH.extend(n.conanfile.cpp_info.lib_paths)
-                    env_info.PATH.extend(n.conanfile.cpp_info.bin_paths)
+                    env_info.DYLD_LIBRARY_PATH.extend(n.conanfile._conan_dep_cpp_info.lib_paths)
+                    env_info.DYLD_LIBRARY_PATH.extend(n.conanfile._conan_dep_cpp_info.framework_paths)
+                    env_info.LD_LIBRARY_PATH.extend(n.conanfile._conan_dep_cpp_info.lib_paths)
+                    env_info.PATH.extend(n.conanfile._conan_dep_cpp_info.bin_paths)
                     conan_file.deps_env_info.update(env_info, n.ref.name)
 
         # Update the info but filtering the package values that not apply to the subtree
@@ -555,5 +555,11 @@ def _call_package_info(self, conanfile, package_folder, ref):
                     self._hook_manager.execute("pre_package_info", conanfile=conanfile,
                                                reference=ref)
                     conanfile.package_info()
+                    if conanfile._conan_dep_cpp_info is None:
+                        try:
+                            conanfile.cpp_info._raise_if_mixing_components()
+                        except ConanException as e:
+                            raise ConanException("%s package_info(): %s" % (str(conanfile), e))
+                        conanfile._conan_dep_cpp_info = DepCppInfo(conanfile.cpp_info)
                     self._hook_manager.execute("post_package_info", conanfile=conanfile,
                                                reference=ref)
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,6 +1,7 @@
 import os
 from collections import OrderedDict
 
+from conans.errors import ConanException
 from conans.util.conan_v2_mode import conan_v2_behavior
 
 DEFAULT_INCLUDE = "include"
@@ -12,6 +13,18 @@
 DEFAULT_FRAMEWORK = "Frameworks"
 
 
+class DefaultOrderedDict(OrderedDict):
+
+    def __init__(self, factory):
+        self.factory = factory
+        super(DefaultOrderedDict, self).__init__()
+
+    def __getitem__(self, key):
+        if key not in self.keys():
+            super(DefaultOrderedDict, self).__setitem__(key, self.factory())
+        return super(DefaultOrderedDict, self).__getitem__(key)
+
+
 class _CppInfo(object):
     """ Object that stores all the necessary information to build in C/C++.
     It is intended to be system independent, translation to
@@ -124,6 +137,19 @@ def set_cppflags(self, value):
     cppflags = property(get_cppflags, set_cppflags)
 
 
+class Component(_CppInfo):
+
+    def __init__(self, rootpath):
+        super(Component, self).__init__()
+        self.rootpath = rootpath
+        self.includedirs.append(DEFAULT_INCLUDE)
+        self.libdirs.append(DEFAULT_LIB)
+        self.bindirs.append(DEFAULT_BIN)
+        self.resdirs.append(DEFAULT_RES)
+        self.builddirs.append(DEFAULT_BUILD)
+        self.frameworkdirs.append(DEFAULT_FRAMEWORK)
+
+
 class CppInfo(_CppInfo):
     """ Build Information declared to be used by the CONSUMERS of a
     conans. That means that consumers must use this flags and configs i order
@@ -139,12 +165,12 @@ def __init__(self, root_folder):
         self.resdirs.append(DEFAULT_RES)
         self.builddirs.append(DEFAULT_BUILD)
         self.frameworkdirs.append(DEFAULT_FRAMEWORK)
+        self.components = DefaultOrderedDict(lambda: Component(self.rootpath))
         # public_deps is needed to accumulate list of deps for cmake targets
         self.public_deps = []
         self.configs = {}
 
     def __getattr__(self, config):
-
         def _get_cpp_info():
             result = _CppInfo()
             result.rootpath = self.rootpath
@@ -159,6 +185,27 @@ def _get_cpp_info():
 
         return self.configs.setdefault(config, _get_cpp_info())
 
+    def _raise_if_mixing_components(self):
+        if (self.includedirs != [DEFAULT_INCLUDE] or
+                self.libdirs != [DEFAULT_LIB] or
+                self.bindirs != [DEFAULT_BIN] or
+                self.resdirs != [DEFAULT_RES] or
+                self.frameworkdirs != [DEFAULT_FRAMEWORK] or
+                self.libs or
+                self.system_libs or
+                self.frameworks or
+                self.defines or
+                self.cflags or
+                self.cxxflags or
+                self.sharedlinkflags or
+                self.exelinkflags or
+                self.build_modules) and self.components:
+            raise ConanException("self.cpp_info.components cannot be used with self.cpp_info "
+                                 "global values at the same time")
+        if self.configs and self.components:
+            raise ConanException("self.cpp_info.components cannot be used with self.cpp_info configs"
+                                 " (release/debug/...) at the same time")
+
 
 class _BaseDepsCppInfo(_CppInfo):
     def __init__(self):
@@ -179,6 +226,7 @@ def merge_lists(seq1, seq2):
         self.frameworkdirs = merge_lists(self.frameworkdirs, dep_cpp_info.framework_paths)
         self.libs = merge_lists(self.libs, dep_cpp_info.libs)
         self.frameworks = merge_lists(self.frameworks, dep_cpp_info.frameworks)
+        self.build_modules = merge_lists(self.build_modules, dep_cpp_info.build_modules_paths)
         self.rootpaths.append(dep_cpp_info.rootpath)
 
         # Note these are in reverse order
@@ -187,7 +235,6 @@ def merge_lists(seq1, seq2):
         self.cflags = merge_lists(dep_cpp_info.cflags, self.cflags)
         self.sharedlinkflags = merge_lists(dep_cpp_info.sharedlinkflags, self.sharedlinkflags)
         self.exelinkflags = merge_lists(dep_cpp_info.exelinkflags, self.exelinkflags)
-        self.build_modules = merge_lists(self.build_modules, dep_cpp_info.build_modules_paths)
 
         if not self.sysroot:
             self.sysroot = dep_cpp_info.sysroot
@@ -225,6 +272,126 @@ def framework_paths(self):
         return self.frameworkdirs
 
 
+class DepCppInfo(object):
+
+    def __init__(self, cpp_info):
+        self._cpp_info = cpp_info
+        self._libs = None
+        self._system_libs = None
+        self._frameworks = None
+        self._defines = None
+        self._cxxflags = None
+        self._cflags = None
+        self._sharedlinkflags = None
+        self._exelinkflags = None
+
+        self._include_paths = None
+        self._lib_paths = None
+        self._bin_paths = None
+        self._build_paths = None
+        self._res_paths = None
+        self._src_paths = None
+        self._framework_paths = None
+        self._build_module_paths = None
+
+    def __getattr__(self, item):
+        try:
+            attr = self._cpp_info.__getattribute__(item)
+        except AttributeError:  # item is not defined, get config (CppInfo)
+            attr = self._cpp_info.__getattr__(item)
+        return attr
+
+    @staticmethod
+    def _merge_lists(seq1, seq2):
+        return seq1 + [s for s in seq2 if s not in seq1]
+
+    def _aggregated_values(self, item):
+        values = getattr(self, "_%s" % item)
+        if values is not None:
+            return values
+        values = getattr(self._cpp_info, item)
+        if self._cpp_info.components:
+            for component in self._cpp_info.components.values():
+                values = self._merge_lists(values, getattr(component, item))
+        setattr(self, "_%s" % item, values)
+        return values
+
+    def _aggregated_paths(self, item):
+        paths = getattr(self, "_%s_paths" % item)
+        if paths is not None:
+            return paths
+        paths = getattr(self._cpp_info, "%s_paths" % item)
+        if self._cpp_info.components:
+            for component in self._cpp_info.components.values():
+                paths = self._merge_lists(paths, getattr(component, "%s_paths" % item))
+        setattr(self, "_%s_paths" % item, paths)
+        return paths
+
+    @property
+    def build_modules_paths(self):
+        return self._aggregated_paths("build_modules")
+
+    @property
+    def include_paths(self):
+        return self._aggregated_paths("include")
+
+    @property
+    def lib_paths(self):
+        return self._aggregated_paths("lib")
+
+    @property
+    def src_paths(self):
+        return self._aggregated_paths("src")
+
+    @property
+    def bin_paths(self):
+        return self._aggregated_paths("bin")
+
+    @property
+    def build_paths(self):
+        return self._aggregated_paths("build")
+
+    @property
+    def res_paths(self):
+        return self._aggregated_paths("res")
+
+    @property
+    def framework_paths(self):
+        return self._aggregated_paths("framework")
+
+    @property
+    def libs(self):
+        return self._aggregated_values("libs")
+
+    @property
+    def system_libs(self):
+        return self._aggregated_values("system_libs")
+
+    @property
+    def frameworks(self):
+        return self._aggregated_values("frameworks")
+
+    @property
+    def defines(self):
+        return self._aggregated_values("defines")
+
+    @property
+    def cxxflags(self):
+        return self._aggregated_values("cxxflags")
+
+    @property
+    def cflags(self):
+        return self._aggregated_values("cflags")
+
+    @property
+    def sharedlinkflags(self):
+        return self._aggregated_values("sharedlinkflags")
+
+    @property
+    def exelinkflags(self):
+        return self._aggregated_values("exelinkflags")
+
+
 class DepsCppInfo(_BaseDepsCppInfo):
     """ Build Information necessary to build a given conans. It contains the
     flags, directories and options if its dependencies. The conans CONANFILE
@@ -251,9 +418,9 @@ def deps(self):
     def __getitem__(self, item):
         return self._dependencies[item]
 
-    def update(self, dep_cpp_info, pkg_name):
-        assert isinstance(dep_cpp_info, CppInfo)
-        self._dependencies[pkg_name] = dep_cpp_info
-        super(DepsCppInfo, self).update(dep_cpp_info)
-        for config, cpp_info in dep_cpp_info.configs.items():
+    def update(self, cpp_info, pkg_name):
+        assert isinstance(cpp_info, (CppInfo, DepCppInfo))
+        self._dependencies[pkg_name] = cpp_info
+        super(DepsCppInfo, self).update(cpp_info)
+        for config, cpp_info in cpp_info.configs.items():
             self.configs.setdefault(config, _BaseDepsCppInfo()).update(cpp_info)
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -149,6 +149,7 @@ def initialize(self, settings, env):
 
         # needed variables to pack the project
         self.cpp_info = None  # Will be initialized at processing time
+        self._conan_dep_cpp_info = None  # Will be initialized at processing time
         self.deps_cpp_info = DepsCppInfo()
 
         # environment variables declared in the package_info


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,6 +1,7 @@
 import textwrap
 import unittest
 
+from conans.errors import ConanException
 from conans.paths import CONANFILE, CONANFILE_TXT
 from conans.test.utils.tools import TestClient
 
@@ -132,3 +133,110 @@ def build(self):
         self.assertIn("System deps: %s" % merged_system_libs, client.out)
         self.assertIn("intermediate system deps: %s" % intermediate_system_libs, client.out)
         self.assertIn("dep system deps: %s" % dep_system_libs, client.out)
+
+    def package_info_components_test(self):
+        dep = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+
+            class Dep(ConanFile):
+
+                def package_info(self):
+                    self.cpp_info.components["dep1"].libs.append("libdep1")
+                    self.cpp_info.components["dep1"].defines.append("definedep1")
+                    self.cpp_info.components["dep2"].libs.append("libdep2")
+                    os.mkdir(os.path.join(self.package_folder, "include"))
+                    os.mkdir(os.path.join(self.package_folder, "includedep2"))
+                    self.cpp_info.components["dep2"].includedirs.append("includedep2")
+                """)
+        intermediate = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+
+            class Intermediate(ConanFile):
+                requires = "dep/1.0@us/ch"
+
+                def package_info(self):
+                    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"))
+                    os.mkdir(os.path.join(self.package_folder, "includeint1"))
+                    self.cpp_info.components["int1"].includedirs.append("includeint1")
+                    self.cpp_info.components["int2"].libs.append("libint2")
+                    self.cpp_info.components["int2"].defines.append("defint2")
+                    os.mkdir(os.path.join(self.package_folder, "includeint2"))
+                    self.cpp_info.components["int2"].includedirs.append("includeint2")
+                """)
+        consumer = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+
+            class Consumer(ConanFile):
+                requires = "intermediate/1.0@us/ch"
+
+                def build(self):
+                    self.output.info("deps_cpp_info.libs: %s" % self.deps_cpp_info.libs)
+                    self.output.info("deps_cpp_info.defines: %s" % self.deps_cpp_info.defines)
+                    self.output.info("deps_cpp_info.include_paths: %s" %
+                        [os.path.basename(value) for value in self.deps_cpp_info.include_paths])
+                    for dep_key, dep_value in self.deps_cpp_info.dependencies:
+                        self.output.info("%s.libs: %s" % (dep_key, dep_value.libs))
+                        self.output.info("%s.defines: %s" % (dep_key, dep_value.defines))
+                        self.output.info("%s.include_paths: %s" % (dep_key,
+                                         [os.path.basename(value) for value in
+                                         dep_value.include_paths]))
+                """)
+
+        client = TestClient()
+        client.save({"conanfile_dep.py": dep,
+                     "conanfile_intermediate.py": intermediate,
+                     "conanfile_consumer.py": consumer})
+        client.run("export conanfile_dep.py dep/1.0@us/ch")
+        client.run("export conanfile_intermediate.py intermediate/1.0@us/ch")
+        client.run("create conanfile_consumer.py consumer/1.0@us/ch --build missing")
+
+        self.assertIn("deps_cpp_info.libs: ['libint1', 'libint2', 'libdep1', 'libdep2']", client.out)
+        self.assertIn("deps_cpp_info.defines: ['definedep1', 'defint1', 'defint2']", client.out)
+        self.assertIn("deps_cpp_info.include_paths: ['include', 'includeint1', 'includeint2', "
+                      "'include', 'includedep2']", client.out)
+
+        self.assertIn("intermediate.libs: ['libint1', 'libint2']",
+                      client.out)
+        self.assertIn("intermediate.defines: ['defint1', 'defint2']",
+                      client.out)
+        self.assertIn("intermediate.include_paths: ['include', 'includeint1', 'includeint2']",
+                      client.out)
+
+        self.assertIn("dep.libs: ['libdep1', 'libdep2']", client.out)
+        self.assertIn("dep.defines: ['definedep1']", client.out)
+        self.assertIn("dep.include_paths: ['include', 'includedep2']", client.out)
+
+    def package_info_raise_components_test(self):
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+
+            class Intermediate(ConanFile):
+
+                def package_info(self):
+                    self.cpp_info.defines.append("defint")
+                    self.cpp_info.components["int1"].libs.append("libint1")
+        """)
+        client = TestClient()
+        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(): self.cpp_info.components cannot be used "
+                      "with self.cpp_info global values at the same time", client.out)
+
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+
+            class Intermediate(ConanFile):
+
+                def package_info(self):
+                    self.cpp_info.release.defines.append("defint")
+                    self.cpp_info.components["int1"].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(): self.cpp_info.components cannot be used "
+                      "with self.cpp_info configs (release/debug/...) at the same time", client.out)
diff --git a/conans/test/unittests/model/build_info/components_test.py b/conans/test/unittests/model/build_info/components_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/unittests/model/build_info/components_test.py
@@ -0,0 +1,178 @@
+# coding=utf-8
+import os
+import unittest
+import warnings
+
+from conans.model.build_info import CppInfo, DepsCppInfo, DepCppInfo
+from conans.test.utils.test_files import temp_folder
+
+
+class CppInfoComponentsTest(unittest.TestCase):
+
+    def test_components_set(self):
+        cpp_info = CppInfo("root_folder")
+        cpp_info.components["liba"].name = "LIBA"
+        cpp_info.components["libb"].includedirs.append("includewhat")
+        cpp_info.components["libc"].libs.append("thelibc")
+        self.assertListEqual(list(cpp_info.components.keys()), ["liba", "libb", "libc"])
+        self.assertEqual(cpp_info.components["liba"].name, "LIBA")
+        self.assertListEqual(cpp_info.components["libb"].includedirs, ["include", "includewhat"])
+        self.assertListEqual(cpp_info.components["libc"].libs, ["thelibc"])
+
+    def test_no_components_inside_components(self):
+        cpp_info = CppInfo("root_folder")
+        cpp_info.components["liba"].name = "LIBA"
+        with self.assertRaises(AttributeError):
+            cpp_info.components["libb"].components
+
+    def test_deps_cpp_info_libs(self):
+        deps_cpp_info = DepsCppInfo()
+
+        dep1 = CppInfo("root")
+        dep1.components["liba"].libs.append("liba")
+        dep1.components["libb"].libs.append("libb")
+        deps_cpp_info.update(DepCppInfo(dep1), "dep1")
+
+        dep2 = CppInfo("root")
+        dep2.components["libc"].libs.append("libc")
+        dep2.components["libd"].libs.append("libd")
+        deps_cpp_info.update(DepCppInfo(dep2), "dep2")
+
+        dep3 = CppInfo("root")
+        dep3.libs.append("libdep3")
+        deps_cpp_info.update(DepCppInfo(dep3), "dep3")
+
+        self.assertListEqual(["liba", "libb"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libc", "libd"], deps_cpp_info["dep2"].libs)
+        self.assertListEqual(["libdep3"], deps_cpp_info["dep3"].libs)
+        self.assertListEqual(["liba", "libb", "libc", "libd", "libdep3"],
+                             deps_cpp_info.libs)
+
+    def test_deps_cpp_info_paths(self):
+        deps_cpp_info = DepsCppInfo()
+
+        folder1 = temp_folder()
+        dep1 = CppInfo(folder1)
+        os.mkdir(os.path.join(folder1, "include"))
+        os.mkdir(os.path.join(folder1, "includea"))
+        os.mkdir(os.path.join(folder1, "includeb"))
+        dep1.components["liba"].includedirs.append("includea")
+        dep1.components["libb"].includedirs.append("includeb")
+        deps_cpp_info.update(DepCppInfo(dep1), "dep1")
+
+        folder2 = temp_folder()
+        dep2 = CppInfo(folder2)
+        os.mkdir(os.path.join(folder2, "include"))
+        os.mkdir(os.path.join(folder2, "includec"))
+        os.mkdir(os.path.join(folder2, "included"))
+        dep2.components["libc"].includedirs.append("includec")
+        dep2.components["libd"].includedirs.append("included")
+        deps_cpp_info.update(DepCppInfo(dep2), "dep2")
+
+        self.assertListEqual([os.path.join(folder1, "include"), os.path.join(folder1, "includea"),
+                              os.path.join(folder1, "includeb")],
+                             deps_cpp_info["dep1"].include_paths)
+        self.assertListEqual([os.path.join(folder2, "include"), os.path.join(folder2, "includec"),
+                              os.path.join(folder2, "included")],
+                             deps_cpp_info["dep2"].include_paths)
+        self.assertListEqual([os.path.join(folder1, "include"), os.path.join(folder1, "includea"),
+                              os.path.join(folder1, "includeb"), os.path.join(folder2, "include"),
+                              os.path.join(folder2, "includec"), os.path.join(folder2, "included")],
+                             deps_cpp_info.include_paths)
+
+    def test_deps_cpp_info_libs_defines_flags(self):
+        deps_cpp_info = DepsCppInfo()
+
+        dep1 = CppInfo("root")
+        dep1.components["liba"].libs.append("liba")
+        dep1.components["liba"].defines.append("DEFINEA")
+        dep1.components["liba"].system_libs.append("sysa")
+        dep1.components["liba"].cxxflags.append("cxxflaga")
+        dep1.components["liba"].cflags.append("cflaga")
+        dep1.components["liba"].sharedlinkflags.append("slinka")
+        dep1.components["liba"].frameworks.append("frameworka")
+        dep1.components["liba"].exelinkflags.append("elinka")
+        dep1.components["libb"].libs.append("libb")
+        dep1.components["libb"].defines.append("DEFINEB")
+        dep1.components["libb"].system_libs.append("sysb")
+        dep1.components["libb"].cxxflags.append("cxxflagb")
+        dep1.components["libb"].cflags.append("cflagb")
+        dep1.components["libb"].sharedlinkflags.append("slinkb")
+        dep1.components["libb"].frameworks.append("frameworkb")
+        dep1.components["libb"].exelinkflags.append("elinkb")
+        deps_cpp_info.update(DepCppInfo(dep1), "dep1")
+
+        dep2 = CppInfo("root")
+        dep2.components["libc"].libs.append("libc")
+        dep2.components["libd"].libs.append("libd")
+        dep2.components["systemlib"].system_libs = ["systemlib"]
+        dep2.components["libc"].cxxflags = ["cxxflagc"]
+        dep2.components["libd"].cflags = ["cflagd"]
+        dep2.components["libc"].sharedlinkflags = ["slinkc"]
+        dep2.components["libd"].sharedlinkflags = ["slinkd"]
+        deps_cpp_info.update(DepCppInfo(dep2), "dep2")
+
+        self.assertListEqual(["liba", "libb"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libc", "libd"], deps_cpp_info["dep2"].libs)
+        self.assertListEqual(["liba", "libb", "libc", "libd"], deps_cpp_info.libs)
+
+        self.assertListEqual(["DEFINEA", "DEFINEB"], deps_cpp_info["dep1"].defines)
+        self.assertListEqual(["DEFINEA", "DEFINEB"], deps_cpp_info.defines)
+
+        self.assertListEqual(["sysa", "sysb"], deps_cpp_info["dep1"].system_libs)
+        self.assertListEqual(["systemlib"], deps_cpp_info["dep2"].system_libs)
+        self.assertListEqual(["sysa", "sysb", "systemlib"], deps_cpp_info.system_libs)
+
+        self.assertListEqual(["cxxflaga", "cxxflagb"], deps_cpp_info["dep1"].cxxflags)
+        self.assertListEqual(["cxxflagc"], deps_cpp_info["dep2"].cxxflags)
+        self.assertListEqual(["cxxflagc", "cxxflaga", "cxxflagb"], deps_cpp_info.cxxflags)
+
+        self.assertListEqual(["cflaga", "cflagb"], deps_cpp_info["dep1"].cflags)
+        self.assertListEqual(["cflagd"], deps_cpp_info["dep2"].cflags)
+        self.assertListEqual(["cflagd", "cflaga", "cflagb"], deps_cpp_info.cflags)
+
+        self.assertListEqual(["slinka", "slinkb"], deps_cpp_info["dep1"].sharedlinkflags)
+        self.assertListEqual(["slinkc", "slinkd"], deps_cpp_info["dep2"].sharedlinkflags)
+        self.assertListEqual(["slinkc", "slinkd", "slinka", "slinkb"],
+                             deps_cpp_info.sharedlinkflags)
+
+        self.assertListEqual(["frameworka", "frameworkb"], deps_cpp_info["dep1"].frameworks)
+        self.assertListEqual(["frameworka", "frameworkb"], deps_cpp_info.frameworks)
+
+        self.assertListEqual(["elinka", "elinkb"], deps_cpp_info["dep1"].exelinkflags)
+        self.assertListEqual([], deps_cpp_info["dep2"].exelinkflags)
+        self.assertListEqual(["elinka", "elinkb"], deps_cpp_info.exelinkflags)
+
+    def test_deps_cpp_info_libs_release_debug(self):
+        deps_cpp_info = DepsCppInfo()
+
+        dep1 = CppInfo("root")
+        dep1.components["liba"].libs.append("liba")
+        with self.assertRaises(AttributeError):
+            dep1.release.components["libb"].libs.append("libb")
+        with self.assertRaises(AttributeError):
+            dep1.debug.components["libb"].libs.append("libb_d")
+        deps_cpp_info.update(DepCppInfo(dep1), "dep1")
+
+        dep2 = CppInfo("root")
+        dep2.release.libs.append("libdep2")
+        dep2.debug.libs.append("libdep2_d")
+        with self.assertRaises(AttributeError):
+            dep2.components["libc"].release.libs.append("libc")
+        with self.assertRaises(AttributeError):
+            dep2.components["libc"].debug.libs.append("libc_d")
+        dep2.components["libc"].libs.append("libc")
+        dep2.components["libc"].libs.append("libc2")
+        deps_cpp_info.update(DepCppInfo(dep2), "dep2")
+
+        self.assertListEqual(["liba"], deps_cpp_info["dep1"].libs)
+        self.assertListEqual(["libc", "libc2"], deps_cpp_info["dep2"].libs)
+        self.assertListEqual(["liba", "libc", "libc2"], deps_cpp_info.libs)
+
+        self.assertListEqual([], deps_cpp_info["dep1"].release.libs)
+        self.assertListEqual(["libdep2"], deps_cpp_info["dep2"].release.libs)
+        self.assertListEqual(["libdep2"], deps_cpp_info.release.libs)
+
+        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)