diff --git a/conans/client/installer.py b/conans/client/installer.py index 2e0cb09529d..ffcaf9d3b42 100644 --- 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 index f8b274905f6..1900a20ab9e 100644 --- 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") diff --git a/conans/test/integration/package_info_test.py b/conans/test/integration/package_info_test.py index 325045da94f..411152f43d2 100644 --- 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 index fd0cd0b1ff5..40ed0ce617a 100644 --- 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 index b3e75b5933e..98e2c205877 100644 --- 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 index 5d5f112e830..308982fb725 100644 --- 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)))