From eb95f5b9472f8d75888d0ffcecba32e17a882423 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Jan 2025 09:11:55 +0100 Subject: [PATCH] improvements deduce locations incubating cmakedeps (#17594) --- conan/internal/model/cpp_info.py | 76 +++++++++++++------ .../cmake/cmakedeps2/target_configuration.py | 4 - .../cmake/cmakedeps/test_cmakedeps_new.py | 52 +++++++++++++ 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index fe6d0f07986..af66f87e6c6 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -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): @@ -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 @@ -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"): @@ -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") @@ -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: @@ -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 @@ -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) diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py index f6ef07c956a..0cb24590de9 100644 --- a/conan/tools/cmake/cmakedeps2/target_configuration.py +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -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, @@ -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) diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py index 8ad1b74f10c..4ee67b740ec 100644 --- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py @@ -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