diff --git a/conans/client/recorder/action_recorder.py b/conans/client/recorder/action_recorder.py index b5e81d89534..f612d6bc843 100644 --- a/conans/client/recorder/action_recorder.py +++ b/conans/client/recorder/action_recorder.py @@ -24,18 +24,28 @@ def _cpp_info_to_dict(cpp_info): doc = {} - for it, value in vars(cpp_info).items(): - if it.startswith("_") or not value: + items = list(vars(cpp_info).items()) + if not cpp_info._components: + items = items + [("libs", cpp_info.libs), ("exes", cpp_info.exes), + ("system_deps", cpp_info.system_deps)] + for key, value in items: + if key.startswith("_components") and value: + doc["components"] = {} + for comp_key, comp_value in value.items(): + doc["components"][comp_key] = comp_value.as_dict() + continue + + if key.startswith("_") or not value: continue - if it == "configs": + if key == "configs": configs_data = {} for cfg_name, cfg_cpp_info in value.items(): configs_data[cfg_name] = _cpp_info_to_dict(cfg_cpp_info) doc["configs"] = configs_data continue - doc[it] = value + doc[key] = value return doc diff --git a/conans/model/build_info.py b/conans/model/build_info.py index 5d31a09bf66..0b22d271ebc 100644 --- a/conans/model/build_info.py +++ b/conans/model/build_info.py @@ -3,11 +3,14 @@ import deprecation +from conans.errors import ConanException + DEFAULT_INCLUDE = "include" DEFAULT_LIB = "lib" DEFAULT_BIN = "bin" DEFAULT_RES = "res" DEFAULT_SHARE = "share" +DEFAULT_BUILD = "" class _CppInfo(object): @@ -16,6 +19,8 @@ class _CppInfo(object): specific systems will be produced from this info """ def __init__(self): + self.name = None + self._system_deps = [] self.includedirs = [] # Ordered list of include paths self.srcdirs = [] # Ordered list of source paths self.libdirs = [] # Directories to find libraries @@ -23,7 +28,8 @@ def __init__(self): self.bindirs = [] # Directories to find executables and shared libs self.builddirs = [] self.rootpaths = [] - self.libs = [] # The libs to link against + self._libs = [] # The libs to link against + self._exes = [] self.defines = [] # preprocessor definitions self.cflags = [] # pure C flags self.cxxflags = [] # C++ compilation flags @@ -41,49 +47,158 @@ def __init__(self): self.description = None # Description of the conan package # When package is editable, filter_empty=False, so empty dirs are maintained self.filter_empty = True + self._components = OrderedDict() + + @property + def _sorted_components(self): + ordered = OrderedDict() + while len(ordered) != len(self._components): + # Search for next element to be processed + for comp_name, comp in self._components.items(): + if comp_name in ordered: + continue + # check if all the deps are declared + if not all([dep in self._components for dep in comp.deps]): + raise ConanException("Component '%s' declares a missing dependency" % comp.name) + # check if all the deps are already added to ordered + if all([dep in ordered for dep in comp.deps]): + break + else: + raise ConanException("There is a dependency loop in the components declared in " + "'self.cpp_info'") + + ordered[comp_name] = comp + return ordered.values() + + @property + def libs(self): + if self._components: + result = [] + for component in self._sorted_components: + for sys_dep in component.system_deps: + if sys_dep and sys_dep not in result: + result.append(sys_dep) + if component.lib: + result.append(component.lib) + return result + else: + return self._libs + + @libs.setter + def libs(self, libs): + assert isinstance(libs, list), "'libs' attribute should be a list of strings" + if self._components: + raise ConanException("Setting first level libs is not supported when Components are " + "already in use") + self._libs = libs + + @property + def exes(self): + if self._components: + return [component.exe for component in self._sorted_components if component.exe] + else: + return self._exes + + @exes.setter + def exes(self, exes): + assert isinstance(exes, list), "'exes' attribute should be a list of strings" + if self._components: + raise ConanException("Setting first level exes is not supported when Components are " + "already in use") + self._exes = exes + + @property + def system_deps(self): + if self._components: + result = [] + for component in self._sorted_components: + if component.system_deps: + for system_dep in component.system_deps: + if system_dep and system_dep not in result: + result.append(system_dep) + return result + else: + return self._system_deps + + @system_deps.setter + def system_deps(self, system_deps): + assert isinstance(system_deps, list), "'system_deps' attribute should be a list of strings" + if self._components: + raise ConanException("Setting first level system_deps is not supported when Components " + "are already in use") + self._system_deps = system_deps + + def __getitem__(self, key): + """ + This is called when the user accesses to a component: self.cpp_info["whatever"] + """ + if self._libs: + raise ConanException("Usage of Components with '.libs' values is not allowed") + if key not in self._components.keys(): + self._components[key] = Component(self, key) + return self._components[key] def _filter_paths(self, paths): - abs_paths = [os.path.join(self.rootpath, p) - if not os.path.isabs(p) else p for p in paths] + abs_paths = [os.path.join(self.rootpath, p) for p in paths] if self.filter_empty: return [p for p in abs_paths if os.path.isdir(p)] else: return abs_paths + def _get_paths(self, path_name): + """ + Get the absolute paths either composing the lists from components or from the global + variables. Also filter the values checking if the folders exist or not and avoid repeated + values. + :param path_name: name of the path variable to get (include_paths, res_paths...) + :return: List of absolute paths + """ + result = [] + + if self._components: + for dep_value in self._sorted_components: + abs_paths = self._filter_paths(getattr(dep_value, "%s_paths" % path_name)) + for path in abs_paths: + if path not in result: + result.append(path) + else: + result = self._filter_paths(getattr(self, "%sdirs" % path_name)) + return result + @property def include_paths(self): if self._include_paths is None: - self._include_paths = self._filter_paths(self.includedirs) + self._include_paths = self._get_paths("include") return self._include_paths @property def lib_paths(self): if self._lib_paths is None: - self._lib_paths = self._filter_paths(self.libdirs) + self._lib_paths = self._get_paths("lib") return self._lib_paths @property def src_paths(self): if self._src_paths is None: - self._src_paths = self._filter_paths(self.srcdirs) + self._src_paths = self._get_paths("src") return self._src_paths @property def bin_paths(self): if self._bin_paths is None: - self._bin_paths = self._filter_paths(self.bindirs) + self._bin_paths = self._get_paths("bin") return self._bin_paths @property def build_paths(self): if self._build_paths is None: - self._build_paths = self._filter_paths(self.builddirs) + self._build_paths = self._get_paths("build") return self._build_paths @property def res_paths(self): if self._res_paths is None: - self._res_paths = self._filter_paths(self.resdirs) + self._res_paths = self._get_paths("res") return self._res_paths # Compatibility for 'cppflags' (old style property to allow decoration) @@ -107,32 +222,148 @@ class CppInfo(_CppInfo): def __init__(self, root_folder): super(CppInfo, self).__init__() self.rootpath = root_folder # the full path of the package in which the conans is found - self.includedirs.append(DEFAULT_INCLUDE) - self.libdirs.append(DEFAULT_LIB) - self.bindirs.append(DEFAULT_BIN) - self.resdirs.append(DEFAULT_RES) - self.builddirs.append("") + self._default_dirs_values = { + "includedirs": [DEFAULT_INCLUDE], + "libdirs": [DEFAULT_LIB], + "bindirs": [DEFAULT_BIN], + "resdirs": [DEFAULT_RES], + "builddirs": [DEFAULT_BUILD], + "srcdirs": [] + } + self.includedirs.extend(self._default_dirs_values["includedirs"]) + self.libdirs.extend(self._default_dirs_values["libdirs"]) + self.bindirs.extend(self._default_dirs_values["bindirs"]) + self.resdirs.extend(self._default_dirs_values["resdirs"]) + self.builddirs.extend(self._default_dirs_values["builddirs"]) # public_deps is needed to accumulate list of deps for cmake targets self.public_deps = [] self.configs = {} + def _check_and_clear_dirs_values(self): + for dir_name in self._default_dirs_values: + dirs_value = getattr(self, dir_name) + if dirs_value is not None and dirs_value != self._default_dirs_values[dir_name]: + msg_template = "Using Components and global '{}' values ('{}') is not supported" + raise ConanException(msg_template.format(dir_name, dirs_value)) + else: + self.__dict__[dir_name] = None + + def __getitem__(self, key): + if self._libs or self._exes: + raise ConanException("Usage of Components with '.libs' or '.exes' values is not allowed") + self._check_and_clear_dirs_values() + if key not in self._components: + self._components[key] = Component(key, self.rootpath) + return self._components[key] + + @property + def components(self): + return self._components + def __getattr__(self, config): def _get_cpp_info(): result = _CppInfo() result.rootpath = self.rootpath result.sysroot = self.sysroot - result.includedirs.append(DEFAULT_INCLUDE) - result.libdirs.append(DEFAULT_LIB) - result.bindirs.append(DEFAULT_BIN) - result.resdirs.append(DEFAULT_RES) - result.builddirs.append("") + result.includedirs.extend(self._default_dirs_values["includedirs"]) + result.libdirs.extend(self._default_dirs_values["libdirs"]) + result.bindirs.extend(self._default_dirs_values["bindirs"]) + result.resdirs.extend(self._default_dirs_values["resdirs"]) + result.builddirs.extend(self._default_dirs_values["builddirs"]) return result return self.configs.setdefault(config, _get_cpp_info()) +class Component(object): + + def __init__(self, name, root_folder): + self._rootpath = root_folder + self.name = name + self.deps = [] + self._lib = None + self._exe = None + self.system_deps = [] + self.includedirs = [DEFAULT_INCLUDE] + self.libdirs = [DEFAULT_LIB] + self.resdirs = [DEFAULT_RES] + self.bindirs = [DEFAULT_BIN] + self.builddirs = [DEFAULT_BUILD] + self.srcdirs = [] + self.defines = [] + self.cflags = [] + self.cxxflags = [] + self.sharedlinkflags = [] + self.exelinkflags = [] + self._filter_empty = True + + def as_dict(self): + result = {} + for key, value in vars(self).items(): + if key.startswith("_"): + continue + result[key] = value + result["lib"] = self.lib + result["exe"] = self.exe + return result + + def _filter_paths(self, paths): + abs_paths = [os.path.join(self._rootpath, p) for p in paths] + if self._filter_empty: + return [p for p in abs_paths if os.path.isdir(p)] + else: + return abs_paths + + @property + def lib(self): + return self._lib + + @lib.setter + def lib(self, name): + assert isinstance(name, str), "'lib' attribute should be a string" + if self._exe: + raise ConanException("'.exe' is already set for this Component") + self._lib = name + + @property + def exe(self): + return self._exe + + @exe.setter + def exe(self, name): + assert isinstance(name, str), "'exe' attribute should be a string" + if self._lib: + raise ConanException("'.lib' is already set for this Component") + self._exe = name + + @property + def include_paths(self): + return self._filter_paths(self.includedirs) + + @property + def lib_paths(self): + return self._filter_paths(self.libdirs) + + @property + def bin_paths(self): + return self._filter_paths(self.bindirs) + + @property + def build_paths(self): + return self._filter_paths(self.builddirs) + + @property + def res_paths(self): + return self._filter_paths(self.resdirs) + + @property + def src_paths(self): + return self._filter_paths(self.srcdirs) + + class _BaseDepsCppInfo(_CppInfo): + def __init__(self): super(_BaseDepsCppInfo, self).__init__() @@ -148,6 +379,7 @@ def merge_lists(seq1, seq2): self.resdirs = merge_lists(self.resdirs, dep_cpp_info.res_paths) self.builddirs = merge_lists(self.builddirs, dep_cpp_info.build_paths) self.libs = merge_lists(self.libs, dep_cpp_info.libs) + self.exes = merge_lists(self.exes, dep_cpp_info.exes) self.rootpaths.append(dep_cpp_info.rootpath) # Note these are in reverse order @@ -156,6 +388,7 @@ 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.system_deps = merge_lists(dep_cpp_info.system_deps, self.system_deps) if not self.sysroot: self.sysroot = dep_cpp_info.sysroot @@ -222,3 +455,4 @@ def update_deps_cpp_info(self, dep_cpp_info): assert isinstance(dep_cpp_info, DepsCppInfo) for pkg_name, cpp_info in dep_cpp_info.dependencies: self.update(cpp_info, pkg_name) + diff --git a/conans/test/functional/command/json_output_test.py b/conans/test/functional/command/json_output_test.py index ef6c72d5c0c..b9a84271112 100644 --- a/conans/test/functional/command/json_output_test.py +++ b/conans/test/functional/command/json_output_test.py @@ -218,3 +218,40 @@ def package_info(self): for dupe in dupe_nodes: self.assertEqual(cpp_info[dupe], cpp_info_debug[dupe]) self.assertEqual(cpp_info[dupe], cpp_info_release[dupe]) + + def test_json_create_cpp_info_components(self): + conanfile = textwrap.dedent(""" + from conans import ConanFile + + class Lib(ConanFile): + def package_info(self): + self.cpp_info.name = "Boost" + self.cpp_info["whatever"].lib = "libwhaterver" + self.cpp_info["whatever"].system_deps = ["sysdep1", "sysdep2"] + self.cpp_info["whenever"].lib = "libwhenever" + self.cpp_info["whenever"].deps = ["whatever"] + self.cpp_info["however"].exe = "exehowever" + self.cpp_info["however"].system_deps = ["pthread"] + self.cpp_info["however"].deps = ["whenever"] + """) + self.client.save({'conanfile.py': conanfile}) + self.client.run("create . name/version@user/channel --json=myfile.json") + my_json = load(os.path.join(self.client.current_folder, "myfile.json")) + my_json = json.loads(my_json) + + # Nodes with cpp_info + cpp_info = my_json["installed"][0]["packages"][0]["cpp_info"] + + self.assertEqual("Boost", cpp_info["name"]) + self.assertFalse("includedirs" in cpp_info) + self.assertFalse("includedirs" in cpp_info) + self.assertFalse("includedirs" in cpp_info) + self.assertFalse("includedirs" in cpp_info) + self.assertFalse("libs" in cpp_info) + self.assertFalse("exes" in cpp_info) + self.assertFalse("system_deps" in cpp_info) + self.assertEqual("libwhaterver", cpp_info["components"]["whatever"]["lib"]) + self.assertEqual("libwhenever", cpp_info["components"]["whenever"]["lib"]) + self.assertEqual("exehowever", cpp_info["components"]["however"]["exe"]) + self.assertEqual(["pthread"], cpp_info["components"]["however"]["system_deps"]) + self.assertEqual(["sysdep1", "sysdep2"], cpp_info["components"]["whatever"]["system_deps"]) diff --git a/conans/test/integration/package_info_test.py b/conans/test/integration/package_info_test.py index dad8b6ad4f0..da5a6959116 100644 --- a/conans/test/integration/package_info_test.py +++ b/conans/test/integration/package_info_test.py @@ -1,7 +1,10 @@ +import os +import textwrap import unittest +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.tools import TestClient, NO_SETTINGS_PACKAGE_ID class TestPackageInfo(unittest.TestCase): @@ -19,7 +22,7 @@ class HelloConan(ConanFile): options = {"switch": ["1", "0"]} default_options = "switch=0" %s - + def build(self): self.output.warn("Env var MYVAR={0}.".format(os.getenv("MYVAR", ""))) @@ -28,8 +31,8 @@ def package_info(self): self.env_info.MYVAR = "foo" else: self.env_info.MYVAR = "bar" - ''' + for index in range(4): requires = "requires = 'Lib%s/1.0@conan/stable'" % index if index > 0 else "" conanfile = conanfile_tmp % ("Lib%s" % (index + 1), requires) @@ -46,3 +49,249 @@ def package_info(self): client.run("install . -o *:switch=0 --build Lib3") self.assertIn("Lib3/1.0@conan/stable: WARN: Env var MYVAR=foo", client.out) + + def package_info_components_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 = "Boost" + self.cpp_info["Accumulators"].includedirs = [os.path.join("boost", "accumulators")] + self.cpp_info["Accumulators"].lib = "libaccumulators" + self.cpp_info["Containers"].includedirs = [os.path.join("boost", "containers")] + self.cpp_info["Containers"].lib = "libcontainers" + self.cpp_info["Containers"].deps = ["Accumulators"] + self.cpp_info["SuperContainers"].includedirs = [os.path.join("boost", + "supercontainers")] + self.cpp_info["SuperContainers"].lib = "libsupercontainers" + self.cpp_info["SuperContainers"].deps = ["Containers"] + """) + consumer = textwrap.dedent(""" + from conans import ConanFile + + class Consumer(ConanFile): + requires = "dep/1.0@us/ch" + + def build(self): + acc_includes = self.deps_cpp_info["dep"]["Accumulators"].include_paths + con_include = self.deps_cpp_info["dep"]["Containers"].include_paths + sup_include = self.deps_cpp_info["dep"]["SuperContainers"].include_paths + self.output.info("Name: %s" % self.deps_cpp_info["dep"].name) + self.output.info("Accumulators: %s" % acc_includes) + self.output.info("Containers: %s" % con_include) + self.output.info("SuperContainers: %s" % sup_include) + self.output.info("LIBS: %s" % self.deps_cpp_info["dep"].libs) + """) + + client = TestClient() + client.save({"conanfile_dep.py": dep, "conanfile_consumer.py": consumer, + "boost/boost.h": "", + "boost/accumulators/accumulators.h": "", + "boost/containers/containers.h": "", + "boost/supercontainers/supercontainers.h": ""}) + 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) + accumulators_expected = [os.path.join(package_folder, "boost", "accumulators")] + containers_expected = [os.path.join(package_folder, "boost", "containers")] + supercontainers_expected = [os.path.join(package_folder, "boost", "supercontainers")] + self.assertIn("Name: Boost", client.out) + self.assertIn("LIBS: ['libaccumulators', 'libcontainers', 'libsupercontainers']", client.out) + self.assertIn("Accumulators: %s" % accumulators_expected, client.out) + self.assertIn("Containers: %s" % containers_expected, client.out) + self.assertIn("SuperContainers: %s" % supercontainers_expected, client.out) + + def package_info_wrong_cpp_info_test(self): + conanfile = textwrap.dedent(""" + import os + from conans import ConanFile + + class Dep(ConanFile): + + def package_info(self): + self.cpp_info.name = "Boost" + self.cpp_info["Accumulators"].includedirs = [os.path.join("boost", "accumulators")] + self.cpp_info.libs = ["hello"] + """) + + client = TestClient() + client.save({"conanfile.py": conanfile}) + client.run("export . name/1.0@us/ch") # Does NOT fail on export + client.run("create . name/1.0@us/ch", assert_error=True) + self.assertIn("Setting first level libs is not supported when Components are already in use", + 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["Starlight"].includedirs = [os.path.join("galaxy", "starlight")] + self.cpp_info["Starlight"].lib = "libstarlight" + + self.cpp_info["Planet"].includedirs = [os.path.join("galaxy", "planet")] + self.cpp_info["Planet"].lib = "libplanet" + self.cpp_info["Planet"].deps = ["Starlight"] + + self.cpp_info["Launcher"].exe = "exelauncher" + self.cpp_info["Launcher"].system_deps = ["ground"] + + self.cpp_info["ISS"].includedirs = [os.path.join("galaxy", "iss")] + self.cpp_info["ISS"].lib = "libiss" + self.cpp_info["ISS"].libdirs = ["iss_libs"] + self.cpp_info["ISS"].system_deps = ["solar", "magnetism"] + self.cpp_info["ISS"].deps = ["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 deps: %s" % self.deps_cpp_info.system_deps) + # Deps values + for dep_key, dep_value in self.deps_cpp_info.dependencies: + 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 Exes: %s" % dep_value.exes) + self.output.info("DEPS System deps: %s" % dep_value.system_deps) + # 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 Lib: %s" % (comp_name, comp_value.lib)) + self.output.info("COMP %s Exe: %s" % (comp_name, comp_value.exe)) + self.output.info("COMP %s Deps: %s" % (comp_name, comp_value.deps)) + self.output.info("COMP %s System deps: %s" % (comp_name, comp_value.system_deps)) + """) + + 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_comp_starlight_lib = "libstarlight" + expected_comp_planet_lib = "libplanet" + expected_comp_launcher_lib = None + expected_comp_iss_lib = "libiss" + expected_comp_starlight_system_deps = [] + expected_comp_planet_system_deps = [] + expected_comp_launcher_system_deps = ["ground"] + expected_comp_iss_system_deps = ["solar", "magnetism"] + expected_comp_starlight_exe = "" + expected_comp_planet_exe = "" + expected_comp_launcher_exe = "exelauncher" + expected_comp_iss_exe = "" + + expected_global_include_paths = expected_comp_starlight_include_paths + \ + expected_comp_planet_include_paths + expected_comp_iss_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 + expected_global_libs = expected_comp_starlight_system_deps + [expected_comp_starlight_lib] + expected_global_libs.extend(expected_comp_planet_system_deps) + expected_global_libs.append(expected_comp_planet_lib) + expected_global_libs.extend(expected_comp_launcher_system_deps) + expected_global_libs.extend(expected_comp_iss_system_deps) + expected_global_libs.append(expected_comp_iss_lib) + expected_global_exes = [expected_comp_launcher_exe] + expected_global_system_deps = expected_comp_launcher_system_deps + expected_comp_iss_system_deps + + 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: %s" % expected_global_libs, client.out) + self.assertIn("GLOBAL Exes: %s" % expected_global_exes, client.out) + self.assertIn("GLOBAL System deps: %s" % expected_global_system_deps, 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: %s" % expected_global_libs, client.out) + self.assertIn("DEPS Exes: %s" % expected_global_exes, client.out) + self.assertIn("DEPS System deps: %s" % expected_global_system_deps, 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 Lib: %s" % expected_comp_starlight_lib, client.out) + self.assertIn("COMP Planet Lib: %s" % expected_comp_planet_lib, client.out) + self.assertIn("COMP Launcher Lib: %s" % expected_comp_launcher_lib, client.out) + self.assertIn("COMP ISS Lib: %s" % expected_comp_iss_lib, client.out) + self.assertIn("COMP Starlight Exe: %s" % expected_comp_starlight_exe, client.out) + self.assertIn("COMP Planet Exe: %s" % expected_comp_planet_exe, client.out) + self.assertIn("COMP Launcher Exe: %s" % expected_comp_launcher_exe, client.out) + self.assertIn("COMP ISS Exe: %s" % expected_comp_iss_exe, client.out) + self.assertIn("COMP Starlight System deps: %s" % expected_comp_starlight_system_deps, + client.out) + self.assertIn("COMP Planet System deps: %s" % expected_comp_planet_system_deps, client.out) + self.assertIn("COMP Launcher System deps: %s" % expected_comp_launcher_system_deps, + client.out) + self.assertIn("COMP ISS System deps: %s" % expected_comp_iss_system_deps, client.out) diff --git a/conans/test/unittests/model/build_info_test.py b/conans/test/unittests/model/build_info_test.py index 631aac1a03d..bfe26c21380 100644 --- a/conans/test/unittests/model/build_info_test.py +++ b/conans/test/unittests/model/build_info_test.py @@ -1,9 +1,11 @@ import os +import six import unittest from collections import defaultdict, namedtuple from conans.client.generators import TXTGenerator -from conans.model.build_info import CppInfo, DepsCppInfo +from conans.errors import ConanException +from conans.model.build_info import CppInfo, DepsCppInfo, Component from conans.model.env_info import DepsEnvInfo, EnvInfo from conans.model.user_info import DepsUserInfo from conans.test.utils.test_files import temp_folder @@ -173,3 +175,226 @@ def cpp_info_test(self): self.assertEqual(info.lib_paths, [os.path.join(folder, "lib"), abs_lib]) self.assertEqual(info.bin_paths, [abs_bin, os.path.join(folder, "local_bindir")]) + + def basic_components_test(self): + cpp_info = CppInfo(None) + component = cpp_info["my_component"] + self.assertEqual(component.name, "my_component") + component.lib = "libhola" + self.assertEqual(component.lib, "libhola") + with six.assertRaisesRegex(self, ConanException, "'.lib' is already set for this Component"): + component.exe = "hola.exe" + + component = cpp_info["my_other_component"] + component.exe = "hola.exe" + self.assertEqual(component.lib, None) + with six.assertRaisesRegex(self, ConanException, "'.exe' is already set for this Component"): + component.lib = "libhola" + + def cpp_info_libs_components_fail_test(self): + """ + Usage of .libs is not allowed in cpp_info when using Components + """ + info = CppInfo(None) + info.name = "Greetings" + self.assertEqual(info.name, "Greetings") + info.libs = ["libgreet"] + with six.assertRaisesRegex(self, ConanException, "Usage of Components with '.libs' or " + "'.exes' values is not allowed"): + info["hola"].exe = "hola.exe" + + info.libs = [] + info["greet"].exe = "libgreet" + with six.assertRaisesRegex(self, ConanException, "Setting first level libs is not supported " + "when Components are already in use"): + info.libs = ["libgreet"] + + def cpp_info_system_deps_test(self): + """ + System deps are composed in '.libs' attribute even if there are no '.lib' in the component. + Also make sure None values are discarded. + Same for '.system_deps' making sure that mixing Components and global use is not supported. + """ + info = CppInfo(None) + info["LIB1"].system_deps = ["sys1", "sys11"] + info["LIB1"].deps = ["LIB2"] + info["LIB2"].system_deps = ["sys2"] + info["LIB2"].deps = ["LIB3"] + info["LIB3"].system_deps = ["sys3", "sys2"] + self.assertEqual(['sys3', 'sys2', 'sys1', 'sys11'], info.libs) + self.assertEqual(['sys3', 'sys2', 'sys1', 'sys11'], info.system_deps) + info["LIB3"].system_deps = [None, "sys2"] + self.assertEqual(['sys2', 'sys1', 'sys11'], info.libs) + self.assertEqual(['sys2', 'sys1', 'sys11'], info.system_deps) + + with six.assertRaisesRegex(self, ConanException, "Setting first level system_deps is not " + "supported when Components are already in " + "use"): + info.system_deps = ["random_system"] + + def cpp_info_libs_system_deps_order_test(self): + """ + Check the order of libs and system_deps and discard repeated values + """ + info = CppInfo(None) + info["LIB1"].lib = "lib1" + info["LIB1"].system_deps = ["sys1", "sys11"] + info["LIB1"].deps = ["LIB2"] + info["LIB2"].lib = "lib2" + info["LIB2"].system_deps = ["sys2"] + info["LIB2"].deps = ["LIB3"] + info["LIB3"].lib = "lib3" + info["LIB3"].system_deps = ["sys3", "sys2"] + self.assertEqual(['sys3', 'sys2', 'lib3', 'lib2', 'sys1', 'sys11', 'lib1'], info.libs) + self.assertEqual(['sys3', 'sys2', 'sys1', 'sys11'], info.system_deps) + + def cpp_info_link_order_test(self): + + def _assert_link_order(sorted_libs): + assert sorted_libs, "'sorted_libs' is empty" + for num, lib in enumerate(sorted_libs): + component_name = lib[-1] + for dep in info[component_name].deps: + self.assertIn(info[dep].lib, sorted_libs[:num]) + + info = CppInfo(None) + info["F"].lib = "libF" + info["F"].deps = ["D", "E"] + info["E"].lib = "libE" + info["E"].deps = ["B"] + info["D"].lib = "libD" + info["D"].deps = ["A"] + info["C"].lib = "libC" + info["C"].deps = ["A"] + info["A"].lib = "libA" + info["A"].deps = ["B"] + info["B"].lib = "libB" + info["B"].deps = [] + _assert_link_order(info.libs) + + info = CppInfo(None) + info["K"].lib = "libK" + info["K"].deps = ["G", "H"] + info["J"].lib = "libJ" + info["J"].deps = ["F"] + info["G"].lib = "libG" + info["G"].deps = ["F"] + info["H"].lib = "libH" + info["H"].deps = ["F", "E"] + info["L"].lib = "libL" + info["L"].deps = ["I"] + info["F"].lib = "libF" + info["F"].deps = ["C", "D"] + info["I"].lib = "libI" + info["I"].deps = ["E"] + info["C"].lib = "libC" + info["C"].deps = ["A"] + info["D"].lib = "libD" + info["D"].deps = ["A"] + info["E"].lib = "libE" + info["E"].deps = ["A", "B"] + info["A"].lib = "libA" + info["A"].deps = [] + info["B"].lib = "libB" + info["B"].deps = [] + _assert_link_order(info.libs) + + def cppinfo_inexistent_component_dep_test(self): + info = CppInfo(None) + info["LIB1"].lib = "lib1" + info["LIB1"].deps = ["LIB2"] + with six.assertRaisesRegex(self, ConanException, "Component 'LIB1' " + "declares a missing dependency"): + info.libs + + def cpp_info_components_dep_loop_test(self): + info = CppInfo(None) + info["LIB1"].lib = "lib1" + info["LIB1"].deps = ["LIB1"] + msg = "There is a dependency loop in the components declared in 'self.cpp_info'" + with six.assertRaisesRegex(self, ConanException, msg): + info.libs + info = CppInfo(None) + info["LIB1"].lib = "lib1" + info["LIB1"].deps = ["LIB2"] + info["LIB2"].lib = "lib2" + info["LIB2"].deps = ["LIB1", "LIB2"] + with six.assertRaisesRegex(self, ConanException, msg): + info.libs + info = CppInfo(None) + info["LIB1"].lib = "lib1" + info["LIB1"].deps = ["LIB2"] + info["LIB2"].lib = "lib2" + info["LIB2"].deps = ["LIB3"] + info["LIB3"].lib = "lib3" + info["LIB3"].deps = ["LIB1"] + with six.assertRaisesRegex(self, ConanException, msg): + info.libs + + def cppinfo_dirs_test(self): + folder = temp_folder() + info = CppInfo(folder) + info.name = "OpenSSL" + info["OpenSSL"].includedirs = ["include"] + info["OpenSSL"].libdirs = ["lib"] + info["OpenSSL"].builddirs = ["build"] + info["OpenSSL"].bindirs = ["bin"] + info["OpenSSL"].resdirs = ["res"] + info["Crypto"].includedirs = ["headers"] + info["Crypto"].libdirs = ["libraries"] + info["Crypto"].builddirs = ["build_scripts"] + info["Crypto"].bindirs = ["binaries"] + info["Crypto"].resdirs = ["resources"] + self.assertEqual(["include"], info["OpenSSL"].includedirs) + self.assertEqual(["lib"], info["OpenSSL"].libdirs) + self.assertEqual(["build"], info["OpenSSL"].builddirs) + self.assertEqual(["bin"], info["OpenSSL"].bindirs) + self.assertEqual(["res"], info["OpenSSL"].resdirs) + self.assertEqual(["headers"], info["Crypto"].includedirs) + self.assertEqual(["libraries"], info["Crypto"].libdirs) + self.assertEqual(["build_scripts"], info["Crypto"].builddirs) + self.assertEqual(["binaries"], info["Crypto"].bindirs) + self.assertEqual(["resources"], info["Crypto"].resdirs) + + info["Crypto"].includedirs = ["different_include"] + info["Crypto"].libdirs = ["different_lib"] + info["Crypto"].builddirs = ["different_build"] + info["Crypto"].bindirs = ["different_bin"] + info["Crypto"].resdirs = ["different_res"] + self.assertEqual(["different_include"], info["Crypto"].includedirs) + self.assertEqual(["different_lib"], info["Crypto"].libdirs) + self.assertEqual(["different_build"], info["Crypto"].builddirs) + self.assertEqual(["different_bin"], info["Crypto"].bindirs) + self.assertEqual(["different_res"], info["Crypto"].resdirs) + + info["Crypto"].includedirs.extend(["another_include"]) + info["Crypto"].includedirs.append("another_other_include") + info["Crypto"].libdirs.extend(["another_lib"]) + info["Crypto"].libdirs.append("another_other_lib") + info["Crypto"].builddirs.extend(["another_build"]) + info["Crypto"].builddirs.append("another_other_build") + info["Crypto"].bindirs.extend(["another_bin"]) + info["Crypto"].bindirs.append("another_other_bin") + info["Crypto"].resdirs.extend(["another_res"]) + info["Crypto"].resdirs.append("another_other_res") + self.assertEqual(["different_include", "another_include", "another_other_include"], + info["Crypto"].includedirs) + self.assertEqual(["different_lib", "another_lib", "another_other_lib"], + info["Crypto"].libdirs) + self.assertEqual(["different_build", "another_build", "another_other_build"], + info["Crypto"].builddirs) + self.assertEqual(["different_bin", "another_bin", "another_other_bin"], + info["Crypto"].bindirs) + self.assertEqual(["different_res", "another_res", "another_other_res"], + info["Crypto"].resdirs) + + def cppinfo_exes_test(self): + info = CppInfo(None) + info.name = "OpenSSL" + info["Exe1"].exe = "the_exe1" + info["Exe2"].exe = "the_exe2" + self.assertEqual(["the_exe1", "the_exe2"], info.exes) + + with six.assertRaisesRegex(self, ConanException, "Setting first level exes is not supported " + "when Components are already in use"): + info.exes = ["another_exe"]