Skip to content

Commit

Permalink
improvements deduce locations incubating cmakedeps (#17594)
Browse files Browse the repository at this point in the history
  • Loading branch information
memsharded authored Jan 21, 2025
1 parent 9064871 commit eb95f5b
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 27 deletions.
76 changes: 53 additions & 23 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 @@ -487,7 +487,7 @@ def relocate(el):
def parsed_requires(self):
return [r.split("::", 1) if "::" in r else (None, r) for r in self.requires]

def _auto_deduce_locations(self, conanfile, component_name):
def _auto_deduce_locations(self, conanfile, library_name):

def _lib_match_by_glob(dir_, filename):
# Run a glob.glob function to find the file given by the filename
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 @@ -549,7 +550,7 @@ def _find_matching(dirs, pattern):
dll_location = _find_matching(bindirs, libname)
else:
lib_sanitized = re.escape(libname)
component_sanitized = re.escape(component_name)
component_sanitized = re.escape(library_name)
regex_static = re.compile(rf"(?:lib)?{lib_sanitized}(?:[._-].+)?\.(?:a|lib)")
regex_shared = re.compile(rf"(?:lib)?{lib_sanitized}(?:[._-].+)?\.(?:so|dylib)")
regex_dll = re.compile(rf".*(?:{lib_sanitized}|{component_sanitized}).*\.dll")
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, library_name=component_name or conanfile.ref.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 @@ -804,7 +834,7 @@ def deduce_full_cpp_info(self, conanfile):
result.default_components = self.default_components
result.components = {k: v.clone() for k, v in self.components.items()}

result._package.deduce_locations(conanfile, component_name=conanfile.ref.name)
result._package.deduce_locations(conanfile)
for comp_name, comp in result.components.items():
comp.deduce_locations(conanfile, component_name=comp_name)

Expand Down
4 changes: 0 additions & 4 deletions conan/tools/cmake/cmakedeps2/target_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,6 @@ 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 +226,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
52 changes: 52 additions & 0 deletions test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,3 +1303,55 @@ def package_info(self):
c.run(f"install --requires=dep/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}")
cmake = c.load("dep-config.cmake")
assert 'set(dep_PACKAGE_PROVIDED_COMPONENTS MyCompC1 MyC2 c3)' in cmake


class TestCppInfoChecks:
def test_check_exe_libs(self):
c = TestClient()
dep = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
name = "dep"
version = "0.1"
def package_info(self):
self.cpp_info.libs = ["mylib"]
self.cpp_info.exe = "myexe"
""")
c.save({"conanfile.py": dep})
c.run("create .")
args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}"
c.run(f"install --requires=dep/0.1 {args}", assert_error=True)
assert "Error in generator 'CMakeDeps': dep/0.1 " 'cpp_info has both .exe and .libs' in c.out

def test_exe_no_location(self):
c = TestClient()
dep = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
name = "dep"
version = "0.1"
def package_info(self):
self.cpp_info.exe = "myexe"
""")
c.save({"conanfile.py": dep})
c.run("create .")
args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}"
c.run(f"install --requires=dep/0.1 {args}", assert_error=True)
assert "Error in generator 'CMakeDeps': dep/0.1 cpp_info has .exe and no .location" in c.out

def test_check_exe_wrong_type(self):
c = TestClient()
dep = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
name = "dep"
version = "0.1"
def package_info(self):
self.cpp_info.type = "shared-library"
self.cpp_info.exe = "myexe"
""")
c.save({"conanfile.py": dep})
c.run("create .")
args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}"
c.run(f"install --requires=dep/0.1 {args}", assert_error=True)
assert "dep/0.1 cpp_info incorrect .type shared-library for .exe myexe" in c.out

0 comments on commit eb95f5b

Please sign in to comment.