Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CppInfo Components internal requirements #6871

Merged
merged 16 commits into from Apr 29, 2020
2 changes: 2 additions & 0 deletions conans/client/installer.py
Expand Up @@ -563,6 +563,8 @@ def _call_package_info(self, conanfile, package_folder, ref):
if conanfile._conan_dep_cpp_info is None:
try:
conanfile.cpp_info._raise_if_mixing_components()
conanfile.cpp_info._raise_if_not_scoped_requires_in_components()
danimtb marked this conversation as resolved.
Show resolved Hide resolved
conanfile.cpp_info._raise_components_name(conanfile.name)
except ConanException as e:
raise ConanException("%s package_info(): %s" % (str(conanfile), e))
conanfile._conan_dep_cpp_info = DepCppInfo(conanfile.cpp_info)
Expand Down
73 changes: 71 additions & 2 deletions 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
Expand All @@ -12,6 +13,8 @@
DEFAULT_BUILD = ""
DEFAULT_FRAMEWORK = "Frameworks"

COMPONENT_SCOPE = "::"


class DefaultOrderedDict(OrderedDict):

Expand All @@ -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++.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -190,6 +200,7 @@ def _raise_if_mixing_components(self):
self.libdirs != [DEFAULT_LIB] or
self.bindirs != [DEFAULT_BIN] or
self.resdirs != [DEFAULT_RES] or
self.builddirs != [DEFAULT_BUILD] or
jgsogo marked this conversation as resolved.
Show resolved Hide resolved
self.frameworkdirs != [DEFAULT_FRAMEWORK] or
self.libs or
self.system_libs or
Expand All @@ -206,6 +217,21 @@ 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")

def _raise_if_not_scoped_requires_in_components(self):
for comp_name, comp in self.components.items():
for require in comp.requires:
if COMPONENT_SCOPE not in require:
msg = "Character '%s' not found in one or more of the " \
"self.cpp_info.components['%s'].requires: %s"\
% (COMPONENT_SCOPE, comp_name, comp.requires)
raise ConanException(msg)

def _raise_components_name(self, name):
for comp_name in self.components:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also as a oneliner: if any(comp_name == name for comp_name in self.components)

if comp_name == name:
raise ConanException("Component name cannot be the same as the package name: '%s'"
% comp_name)


class _BaseDepsCppInfo(_CppInfo):
def __init__(self):
Expand Down Expand Up @@ -293,6 +319,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:
Expand All @@ -311,7 +339,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
Expand All @@ -322,11 +350,52 @@ 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):
filtered_requires = []
danimtb marked this conversation as resolved.
Show resolved Hide resolved
for require in requires:
if require.startswith(COMPONENT_SCOPE):
filtered_requires.append(require[len(COMPONENT_SCOPE):])
return filtered_requires

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)

def _get_sorted_components(self):
memsharded marked this conversation as resolved.
Show resolved Hide resolved
"""
Sort Components from most dependent one first to the less dependent one last
:return: List of sorted components
"""
if not self._sorted_components:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When lazy/caching, this is better checked against None if self._sorted_components is None, to avoid firing again computations when result is an empty dict, list or set.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably way simpler and faster to sort in the opposite order: check that the current components requires are None or all in ordered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented that logic before and it was a mess calculating the link order later. I'd like to keep it as is for now and improve the speed in the future if needed

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")
Expand Down
154 changes: 151 additions & 3 deletions conans/test/integration/package_info_test.py
@@ -1,9 +1,11 @@
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.tools import NO_SETTINGS_PACKAGE_ID, TestClient


class TestPackageInfo(unittest.TestCase):
Expand Down Expand Up @@ -210,7 +212,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")
Expand All @@ -225,7 +227,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")
Expand All @@ -235,3 +237,149 @@ 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")]
danimtb marked this conversation as resolved.
Show resolved Hide resolved
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)
danimtb marked this conversation as resolved.
Show resolved Hide resolved
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)