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

new CmakeDeps transitive linking fixes #17459

Draft
wants to merge 12 commits into
base: develop2
Choose a base branch
from
72 changes: 51 additions & 21 deletions conan/internal/model/cpp_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def type(self):

@type.setter
def type(self, value):
self._type = value
self._type = PackageType(value) if value is not None else None

@property
def location(self):
Expand Down Expand Up @@ -538,6 +538,7 @@ def _find_matching(dirs, pattern):
static_location = None
shared_location = None
dll_location = None
deduced_type = None
# libname is exactly the pattern, e.g., ["mylib.a"] instead of ["mylib"]
_, ext = os.path.splitext(libname)
if ext in (".lib", ".a", ".dll", ".so", ".dylib"):
Expand All @@ -562,52 +563,78 @@ def _find_matching(dirs, pattern):
if shared_location:
out.warning(f"Lib {libname} has both static {static_location} and "
f"shared {shared_location} in the same package")
if pkg_type is PackageType.STATIC:
if self._type is PackageType.STATIC or pkg_type is PackageType.STATIC:
self._location = static_location
self._type = PackageType.STATIC
deduced_type = PackageType.STATIC
else:
self._location = shared_location
self._type = PackageType.SHARED
deduced_type = PackageType.SHARED
elif dll_location:
self._location = dll_location
self._link_location = static_location
self._type = PackageType.SHARED
deduced_type = PackageType.SHARED
else:
self._location = static_location
self._type = PackageType.STATIC
deduced_type = PackageType.STATIC
elif shared_location:
self._location = shared_location
self._type = PackageType.SHARED
deduced_type = PackageType.SHARED
elif dll_location:
# Only .dll but no link library
self._location = dll_location
self._type = PackageType.SHARED
deduced_type = PackageType.SHARED
if not self._location:
raise ConanException(f"{conanfile}: Cannot obtain 'location' for library '{libname}' "
f"in {libdirs}. You can specify 'cpp_info.location' directly "
f"or report in github.com/conan-io/conan/issues if you think it "
f"should have been deduced correctly.")
if self._type is not None and self._type != deduced_type:
ConanException(f"{conanfile}: Incorrect deduced type '{deduced_type}' for library"
f" '{libname}' that declared .type='{self._type}'")
self._type = deduced_type
if self._type != pkg_type:
out.warning(f"Lib {libname} deduced as '{self._type}, but 'package_type={pkg_type}'")

def deduce_locations(self, conanfile, component_name=""):
name = f'{conanfile} cpp_info.components["{component_name}"]' if component_name \
else f'{conanfile} cpp_info'
# executable
if self._exe: # exe is a new field, it should have the correct location
if self._type is None:
self._type = PackageType.APP
if self._type is not PackageType.APP:
raise ConanException(f"{name} incorrect .type {self._type} for .exe {self._exe}")
if self.libs:
raise ConanException(f"{name} has both .exe and .libs")
if not self.location:
raise ConanException(f"{name} has .exe and no .location")
return
if self._location or self._link_location:
if self._type is None or self._type is PackageType.HEADER:
raise ConanException("Incorrect cpp_info defining location without type or header")
if self._type is PackageType.APP:
# old school Conan application packages withoud defining an exe, not an error
return
if self._type not in [None, PackageType.SHARED, PackageType.STATIC, PackageType.APP]:

# libraries
if len(self.libs) > 1: # it could be 0, as the libs itself is not necessary
raise ConanException(f"{name} has more than 1 library in .libs: {self.libs}, "
"cannot deduce locations")
# fully defined by user in conanfile, nothing to do.
if self._location or self._link_location:
if self._type not in [PackageType.SHARED, PackageType.STATIC]:
raise ConanException(f"{name} location defined without defined library type")
return
num_libs = len(self.libs)
if num_libs == 0:

# possible header only, which allows also an empty header-only only for common flags
if len(self.libs) == 0:
if self._type is None:
self._type = PackageType.HEADER
return
elif num_libs > 1:
raise ConanException(
f"More than 1 library defined in cpp_info.libs, cannot deduce CPS ({num_libs} libraries found)")
else:
# If no location is defined, it's time to guess the location
self._auto_deduce_locations(conanfile, component_name=component_name)

# automatic location deduction from a single .lib=["lib"]
if self._type not in [None, PackageType.SHARED, PackageType.STATIC]:
raise ConanException(f"{name} has a library but .type {self._type} is not static/shared")

# If no location is defined, it's time to guess the location
self._auto_deduce_locations(conanfile, component_name=component_name)


class CppInfo:
Expand Down Expand Up @@ -779,6 +806,9 @@ def required_components(self):
return ret

def deduce_full_cpp_info(self, conanfile):
if conanfile.cpp_info.has_components and (conanfile.cpp_info.exe or conanfile.cpp_info.libs):
raise ConanException(f"{conanfile}: 'cpp_info' contains components and .exe or .libs")

result = CppInfo() # clone it

if self.libs and len(self.libs) > 1: # expand in multiple components
Expand All @@ -796,7 +826,7 @@ def deduce_full_cpp_info(self, conanfile):

common = self._package.clone()
common.libs = []
common.type = str(PackageType.HEADER) # the type of components is a string!
common.type = PackageType.HEADER # the type of components is a string!
common.requires = list(result.components.keys()) + (self.requires or [])
result.components["_common"] = common
else:
Expand Down
52 changes: 34 additions & 18 deletions conan/tools/cmake/cmakedeps2/target_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,21 @@ def filename(self):
return f"{f}-Targets{build}-{config}.cmake"

def _requires(self, info, components):
result = []
result = {}
requires = info.parsed_requires()
pkg_name = self._conanfile.ref.name
pkg_type = info.type
assert isinstance(pkg_type, PackageType), f"Pkg type {pkg_type} {type(pkg_type)}"
transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile)

if not requires and not components: # global cpp_info without components definition
# require the pkgname::pkgname base (user defined) or INTERFACE base target
targets = []
for d in transitive_reqs.values():
dep_target = self._cmakedeps.get_property("cmake_target_name", d)
targets.append(dep_target or f"{d.ref.name}::{d.ref.name}")
return targets
dep_target = dep_target or f"{d.ref.name}::{d.ref.name}"
link = not (pkg_type is PackageType.SHARED and d.package_type is PackageType.SHARED)
result[dep_target] = link
return result

for required_pkg, required_comp in requires:
if required_pkg is None: # Points to a component of same package
Expand All @@ -53,7 +57,9 @@ def _requires(self, info, components):
dep_target = self._cmakedeps.get_property("cmake_target_name", self._conanfile,
required_comp)
dep_target = dep_target or f"{pkg_name}::{required_comp}"
result.append(dep_target)
link = not (pkg_type is PackageType.SHARED and
dep_comp.package_type is PackageType.SHARED)
result[dep_target] = link
else: # Different package
try:
dep = transitive_reqs[required_pkg]
Expand All @@ -71,12 +77,15 @@ def _requires(self, info, components):
comp = required_comp
dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp)
dep_target = dep_target or f"{required_pkg}::{required_comp}"
result.append(dep_target)
link = not (pkg_type is PackageType.SHARED and
dep_comp.package_type is PackageType.SHARED)
result[dep_target] = link
return result

@property
def _context(self):
cpp_info = self._conanfile.cpp_info.deduce_full_cpp_info(self._conanfile)
assert isinstance(cpp_info.type, PackageType)
pkg_name = self._conanfile.ref.name
# fallback to consumer configuration if it doesn't have build_type
config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration)
Expand Down Expand Up @@ -148,10 +157,10 @@ def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var):

includedirs = ";".join(self._path(i, pkg_folder, pkg_folder_var)
for i in info.includedirs) if info.includedirs else ""
requires = ";".join(self._requires(info, components))
requires = self._requires(info, components)
assert isinstance(requires, dict)
defines = " ".join(info.defines)
# TODO: Missing escaping?
# TODO: Missing link language
# FIXME: Filter by lib traits!!!!!
if not self._require.headers: # If not depending on headers, paths and
includedirs = defines = None
Expand All @@ -169,7 +178,7 @@ def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var):
if info.frameworks:
ConanOutput(scope=str(self._conanfile)).warning("frameworks not supported yet in new CMakeDeps generator")

if info.libs:
if info.libs: # TODO: to change to location
if len(info.libs) != 1:
raise ConanException(f"New CMakeDeps only allows 1 lib per component:\n"
f"{self._conanfile}: {info.libs}")
Expand Down Expand Up @@ -201,24 +210,21 @@ def _add_root_lib_target(self, libs, pkg_name, cpp_info):
if libs and root_target_name not in libs:
# Add a generic interface target for the package depending on the others
if cpp_info.default_components is not None:
all_requires = []
all_requires = {}
for defaultc in cpp_info.default_components:
target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile,
defaultc)
comp_name = target_name or f"{pkg_name}::{defaultc}"
all_requires.append(comp_name)
all_requires = ";".join(all_requires)
all_requires[comp_name] = True # It is an interface, full link
else:
all_requires = ";".join(libs.keys())
all_requires = {k: True for k in libs.keys()}
libs[root_target_name] = {"type": "INTERFACE",
"requires": all_requires}

def _get_exes(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var):
exes = {}

if cpp_info.has_components:
assert not cpp_info.exe, "Package has components and exe"
assert not cpp_info.libs, "Package has components and libs"
for name, comp in cpp_info.components.items():
if comp.exe or comp.type is PackageType.APP:
target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile,
Expand All @@ -228,8 +234,6 @@ def _get_exes(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var):
exes[target] = exe_location
else:
if cpp_info.exe:
assert not cpp_info.libs, "Package has exe and libs"
assert cpp_info.location, "Package has exe and no location"
target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile)
target = target_name or f"{pkg_name}::{pkg_name}"
exe_location = self._path(cpp_info.location, pkg_folder, pkg_folder_var)
Expand Down Expand Up @@ -341,10 +345,22 @@ def _template(self):
set_target_properties({{lib}} PROPERTIES IMPORTED_IMPLIB_{{config}}
"{{lib_info["link_location"]}}")
{% endif %}

{% if lib_info.get("requires") %}
# Information of transitive dependencies
{% for require_target, link in lib_info["requires"].items() %}
# Requirement {{require_target}} => Full link: {{link}}

{% if link %}
set_target_properties({{lib}} PROPERTIES INTERFACE_LINK_LIBRARIES
"{{config_wrapper(config, lib_info["requires"])}}")
"{{config_wrapper(config, require_target)}}")
{% else %}
set_target_properties({{lib}} PROPERTIES IMPORTED_LINK_DEPENDENT_LIBRARIES_{{config}}
{{require_target}})
{% endif %}
{% endfor %}
{% endif %}

{% if lib_info.get("system_libs") %}
target_link_libraries({{lib}} INTERFACE {{lib_info["system_libs"]}})
{% endif %}
Expand Down
Loading