Repository: conan-io/conan
Pull request #17594 (2025-01-17 20:22:59)

improvements deduce locations incubating cmakedeps
Changelog: Omit
Docs: Omit

Some internal refactors to deduce_locations:
- Using cpp_info.type always as PackageType not str
- Better condition checking
- Using better the ``cpp.type`` if defined and checking it


Patch:
diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py
index fe6d0f079..af66f87e6 100644
--- a/conan/internal/model/cpp_info.py
+++ b/conan/internal/model/cpp_info.py
@@ -285,7 +285,7 @@ class _Component:
 
     @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 @@ class _Component:
     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 @@ class _Component:
         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 @@ class _Component:
                 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 @@ class _Component:
             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 @@ class CppInfo:
         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 @@ class CppInfo:
             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 f6ef07c95..0cb24590d 100644
--- a/conan/tools/cmake/cmakedeps2/target_configuration.py
+++ b/conan/tools/cmake/cmakedeps2/target_configuration.py
@@ -217,8 +217,6 @@ class TargetConfigurationTemplate2:
         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 @@ class TargetConfigurationTemplate2:
                     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)


Test patch:
diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py
index 8ad1b74f1..4ee67b740 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 @@ class TestCMakeComponents:
         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