Repository: conan-io/conan

Issue:
[feature] Add components to 'pkg_config'  generator
This PR (https://github.com/conan-io/conan-center-index/pull/1782) is adding components to OpenSSL and overriding the name of those components for `pkg_config` generator:

```python
        self.cpp_info.components["crypto"].names["cmake_find_package"] = "Crypto"
        self.cpp_info.components["crypto"].names["cmake_find_package_multi"] = "Crypto"
        self.cpp_info.components["crypto"].names['pkg_config'] = 'libcrypto'
        self.cpp_info.components["ssl"].names["cmake_find_package"] = "SSL"
        self.cpp_info.components["ssl"].names["cmake_find_package_multi"] = "SSL"
        self.cpp_info.components["ssl"].names['pkg_config'] = 'libssl'
```

How do components work with `pkg_config`?


Discussion:
IMO they should generate one pkg-config file per component
Taken from https://github.com/conan-io/conan-center-index/pull/1782#discussion_r451820928

```
./lib/pkgconfig/openssl.pc
./lib/pkgconfig/libssl.pc
./lib/pkgconfig/libcrypto.pc
```

https://fedora.pkgs.org/30/fedora-x86_64/openssl-devel-1.1.1b-3.fc30.x86_64.rpm.html

Resolving patch:
diff --git a/conans/client/generators/pkg_config.py b/conans/client/generators/pkg_config.py
--- a/conans/client/generators/pkg_config.py
+++ b/conans/client/generators/pkg_config.py
@@ -2,7 +2,9 @@
 
 from conans.client.build.compiler_flags import rpath_flags, format_frameworks, format_framework_paths
 from conans.client.tools.oss import get_build_os_arch
+from conans.errors import ConanException
 from conans.model import Generator
+from conans.model.build_info import COMPONENT_SCOPE
 
 """
 PC FILE EXAMPLE:
@@ -33,15 +35,60 @@ def filename(self):
     def compiler(self):
         return self.conanfile.settings.get_safe("compiler")
 
+    @classmethod
+    def _get_name(cls, obj):
+        get_name = getattr(obj, "get_name")
+        return get_name(cls.name)
+
+    def _get_components(self, pkg_name, cpp_info):
+        generator_components = []
+        for comp_name, comp in self.sorted_components(cpp_info).items():
+            comp_genname = self._get_name(cpp_info.components[comp_name])
+            comp.public_deps = self._get_component_requires(pkg_name, comp)
+            generator_components.append((comp_genname, comp))
+        generator_components.reverse()  # From the less dependent to most one
+        return generator_components
+
+    def _get_component_requires(self, pkg_name, comp):
+        comp_requires_gennames = []
+        for require in comp.requires:
+            if COMPONENT_SCOPE in require:
+                comp_require_pkg_name, comp_require_comp_name = require.split(COMPONENT_SCOPE)
+                comp_require_pkg = self.deps_build_info[comp_require_pkg_name]
+                comp_require_pkg_genname = self._get_name(comp_require_pkg)
+                if comp_require_comp_name == comp_require_pkg_name:
+                    comp_require_comp_genname = comp_require_pkg_genname
+                elif comp_require_comp_name in self.deps_build_info[comp_require_pkg_name].components:
+                    comp_require_comp = comp_require_pkg.components[comp_require_comp_name]
+                    comp_require_comp_genname = self._get_name(comp_require_comp)
+                else:
+                    raise ConanException("Component '%s' not found in '%s' package requirement"
+                                         % (require, comp_require_pkg_name))
+            else:
+                comp_require_comp = self.deps_build_info[pkg_name].components[require]
+                comp_require_comp_genname = self._get_name(comp_require_comp)
+            comp_requires_gennames.append(comp_require_comp_genname)
+        return comp_requires_gennames
+
     @property
     def content(self):
         ret = {}
         for depname, cpp_info in self.deps_build_info.dependencies:
-            name = cpp_info.get_name(PkgConfigGenerator.name)
-            ret["%s.pc" % name] = self.single_pc_file_contents(name, cpp_info)
+            pkg_genname = cpp_info.get_name(PkgConfigGenerator.name)
+            if not cpp_info.components:
+                ret["%s.pc" % pkg_genname] = self.single_pc_file_contents(pkg_genname, cpp_info)
+            else:
+                components = self._get_components(depname, cpp_info)
+                for comp_genname, comp in components:
+                    ret["%s.pc" % comp_genname] = self.single_pc_file_contents(
+                        "%s-%s" % (pkg_genname, comp_genname), comp, is_component=True)
+                comp_gennames = [comp_genname for comp_genname, _ in components]
+                if pkg_genname not in comp_gennames:
+                    cpp_info.public_deps = comp_gennames
+                    ret["%s.pc" % pkg_genname] = self.global_pc_file_contents(pkg_genname, cpp_info)
         return ret
 
-    def single_pc_file_contents(self, name, cpp_info):
+    def single_pc_file_contents(self, name, cpp_info, is_component=False):
         prefix_path = cpp_info.rootpath.replace("\\", "/")
         lines = ['prefix=%s' % prefix_path]
 
@@ -89,14 +136,29 @@ def single_pc_file_contents(self, name, cpp_info):
              ["-D%s" % d for d in cpp_info.defines]]))
 
         if cpp_info.public_deps:
-            pkg_config_names = []
-            for public_dep in cpp_info.public_deps:
-                name = self.deps_build_info[public_dep].get_name(PkgConfigGenerator.name)
-                pkg_config_names.append(name)
+            if is_component:
+                pkg_config_names = cpp_info.public_deps
+            else:
+                pkg_config_names = []
+                for public_dep in cpp_info.public_deps:
+                    name = self.deps_build_info[public_dep].get_name(PkgConfigGenerator.name)
+                    pkg_config_names.append(name)
             public_deps = " ".join(pkg_config_names)
             lines.append("Requires: %s" % public_deps)
         return "\n".join(lines) + "\n"
 
+    @staticmethod
+    def global_pc_file_contents(name, cpp_info):
+        lines = ["Name: %s" % name]
+        description = cpp_info.description or "Conan package: %s" % name
+        lines.append("Description: %s" % description)
+        lines.append("Version: %s" % cpp_info.version)
+
+        if cpp_info.public_deps:
+            public_deps = " ".join(cpp_info.public_deps)
+            lines.append("Requires: %s" % public_deps)
+        return "\n".join(lines) + "\n"
+
 
 def _concat_if_not_empty(groups):
     return " ".join([param for group in groups for param in group if param and param.strip()])


Test patch:
diff --git a/conans/test/functional/generators/components/pkg_config_test.py b/conans/test/functional/generators/components/pkg_config_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/functional/generators/components/pkg_config_test.py
@@ -0,0 +1,225 @@
+import os
+import platform
+import textwrap
+import unittest
+
+from nose.plugins.attrib import attr
+
+from conans.client.tools import which, PkgConfig, environment_append
+from conans.model.ref import ConanFileReference
+from conans.test.utils.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+class PkgConfigGeneratorWithComponentsTest(unittest.TestCase):
+
+    @staticmethod
+    def _create_greetings(client, custom_names=False, components=True):
+        conanfile_greetings = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class GreetingsConan(ConanFile):
+                name = "greetings"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+
+                def package_info(self):
+                %s
+            """)
+        if components:
+            info = textwrap.dedent("""
+                        self.cpp_info.components["hello"].libs = ["hello"]
+                        self.cpp_info.components["bye"].libs = ["bye"]
+                        """)
+            if custom_names:
+                info += textwrap.dedent("""
+                        self.cpp_info.names["pkg_config"] = "Greetings"
+                        self.cpp_info.components["hello"].names["pkg_config"] = "Hello"
+                        self.cpp_info.components["bye"].names["pkg_config"] = "Bye"
+                        """)
+        else:
+            info = textwrap.dedent("""
+                        self.cpp_info.libs = ["hello", "bye"]
+                        """)
+        wrapper = textwrap.TextWrapper(width=81, initial_indent="   ", subsequent_indent="        ")
+        conanfile_greetings = conanfile_greetings % wrapper.fill(info)
+        client.save({"conanfile.py": conanfile_greetings})
+        client.run("create .")
+
+    @staticmethod
+    def _create_world(client, conanfile=None):
+        _conanfile_world = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class WorldConan(ConanFile):
+                name = "world"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+                requires = "greetings/0.0.1"
+
+                def package_info(self):
+                    self.cpp_info.components["helloworld"].requires = ["greetings::hello"]
+                    self.cpp_info.components["helloworld"].libs = ["helloworld"]
+                    self.cpp_info.components["worldall"].requires = ["helloworld",
+                                                                     "greetings::greetings"]
+                    self.cpp_info.components["worldall"].libs = ["worldall"]
+            """)
+        client.save({"conanfile.py": conanfile or _conanfile_world})
+        client.run("create .")
+
+    @staticmethod
+    def _get_libs_from_pkg_config(library, folder):
+        with environment_append({"PKG_CONFIG_PATH": folder}):
+            pconfig = PkgConfig(library)
+            libs = pconfig.libs_only_l
+        return libs
+
+    def basic_test(self):
+        client = TestClient()
+        self._create_greetings(client)
+        self._create_world(client)
+        client.run("install world/0.0.1@ -g pkg_config")
+        self.assertNotIn("Requires:", client.load(os.path.join(client.current_folder, "hello.pc")))
+        self.assertNotIn("Requires:", client.load(os.path.join(client.current_folder, "bye.pc")))
+        self.assertIn("Requires: bye hello",
+                      client.load(os.path.join(client.current_folder, "greetings.pc")))
+        libs = self._get_libs_from_pkg_config("greetings", client.current_folder)
+        self.assertListEqual(["-lbye", "-lhello"], libs)
+
+    def pkg_config_general_test(self):
+        client = TestClient()
+        self._create_greetings(client, custom_names=True)
+
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class WorldConan(ConanFile):
+                name = "world"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+                requires = "greetings/0.0.1"
+
+                def package_info(self):
+                    self.cpp_info.names["pkg_config"] = "World"
+                    self.cpp_info.components["helloworld"].names["pkg_config"] = "Helloworld"
+                    self.cpp_info.components["helloworld"].requires = ["greetings::hello"]
+                    self.cpp_info.components["helloworld"].libs = ["Helloworld"]
+                    self.cpp_info.components["worldall"].names["pkg_config"] = "Worldall"
+                    self.cpp_info.components["worldall"].requires = ["greetings::bye", "helloworld"]
+                    self.cpp_info.components["worldall"].libs = ["Worldall"]
+        """)
+        self._create_world(client, conanfile=conanfile)
+        client.run("install world/0.0.1@ -g pkg_config")
+        libs = self._get_libs_from_pkg_config("Worldall", client.current_folder)
+        self.assertListEqual(["-lWorldall", "-lbye", "-lHelloworld", "-lhello"], libs)
+        libs = self._get_libs_from_pkg_config("Helloworld", client.current_folder)
+        self.assertListEqual(["-lHelloworld", "-lhello"], libs)
+        libs = self._get_libs_from_pkg_config("World", client.current_folder)
+        self.assertListEqual(["-lWorldall", "-lbye", "-lHelloworld", "-lhello"], libs)
+
+    def pkg_config_components_test(self):
+        client = TestClient()
+        self._create_greetings(client)
+        conanfile2 = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class WorldConan(ConanFile):
+                name = "world"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+                requires = "greetings/0.0.1"
+
+                def package_info(self):
+                    self.cpp_info.components["helloworld"].requires = ["greetings::hello"]
+                    self.cpp_info.components["helloworld"].libs = ["helloworld"]
+                    self.cpp_info.components["worldall"].requires = ["helloworld", "greetings::bye"]
+                    self.cpp_info.components["worldall"].libs = ["worldall"]
+        """)
+        self._create_world(client, conanfile=conanfile2)
+        client.run("install world/0.0.1@ -g pkg_config")
+        libs = self._get_libs_from_pkg_config("helloworld", client.current_folder)
+        self.assertListEqual(["-lhelloworld", "-lhello"], libs)
+        libs = self._get_libs_from_pkg_config("worldall", client.current_folder)
+        self.assertListEqual(["-lworldall", "-lhelloworld", "-lhello", "-lbye"], libs)
+        world_pc = client.load(os.path.join(client.current_folder, "world.pc"))
+        self.assertIn("Requires: helloworld worldall", world_pc)
+        libs = self._get_libs_from_pkg_config("world", client.current_folder)
+        self.assertListEqual(["-lworldall", "-lhelloworld", "-lhello", "-lbye"], libs)
+
+    def recipe_with_components_requiring_recipe_without_components_test(self):
+        client = TestClient()
+        self._create_greetings(client, components=False)
+
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class WorldConan(ConanFile):
+                name = "world"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+                requires = "greetings/0.0.1"
+
+                def package_info(self):
+                    self.cpp_info.components["helloworld"].requires = ["greetings::greetings"]
+                    self.cpp_info.components["helloworld"].libs = ["helloworld"]
+                    self.cpp_info.components["worldall"].requires = ["helloworld",
+                                                                     "greetings::greetings"]
+                    self.cpp_info.components["worldall"].libs = ["worldall"]
+            """)
+        self._create_world(client, conanfile=conanfile)
+        client.run("install world/0.0.1@ -g pkg_config")
+        self.assertFalse(os.path.isfile(os.path.join(client.current_folder, "hello.pc")))
+        self.assertFalse(os.path.isfile(os.path.join(client.current_folder, "bye.pc")))
+        greetings_pc = client.load(os.path.join(client.current_folder, "greetings.pc"))
+        self.assertNotIn("Requires:", greetings_pc)
+        libs = self._get_libs_from_pkg_config("greetings", client.current_folder)
+        self.assertListEqual(["-lhello", "-lbye"], libs)
+        libs = self._get_libs_from_pkg_config("world", client.current_folder)
+        self.assertListEqual(["-lworldall", "-lhelloworld", "-lhello", "-lbye"], libs)
+        libs = self._get_libs_from_pkg_config("helloworld", client.current_folder)
+        self.assertListEqual(["-lhelloworld", "-lhello", "-lbye"], libs)
+        libs = self._get_libs_from_pkg_config("worldall", client.current_folder)
+        self.assertListEqual(["-lworldall", "-lhelloworld", "-lhello", "-lbye"], libs)
+
+    def same_names_test(self):
+        client = TestClient()
+        conanfile_greetings = textwrap.dedent("""
+            from conans import ConanFile, CMake
+
+            class HelloConan(ConanFile):
+                name = "hello"
+                version = "0.0.1"
+                settings = "os", "compiler", "build_type", "arch"
+
+                def package_info(self):
+                    self.cpp_info.components["global"].name = "hello"
+                    self.cpp_info.components["global"].libs = ["hello"]
+            """)
+        client.save({"conanfile.py": conanfile_greetings})
+        client.run("create .")
+        client.run("install hello/0.0.1@ -g pkg_config")
+        self.assertNotIn("Requires:", client.load(os.path.join(client.current_folder, "hello.pc")))
+
+    def component_not_found_same_name_as_pkg_require_test(self):
+        zlib = GenConanfile("zlib", "0.1").with_setting("build_type")\
+            .with_generator("pkg_config")
+        mypkg = GenConanfile("mypkg", "0.1").with_setting("build_type")\
+            .with_generator("pkg_config")
+        final = GenConanfile("final", "0.1").with_setting("build_type")\
+            .with_generator("pkg_config")\
+            .with_require(ConanFileReference("zlib", "0.1", None, None))\
+            .with_require(ConanFileReference("mypkg", "0.1", None, None))\
+            .with_package_info(cpp_info={"components": {"cmp": {"requires": ["mypkg::zlib",
+                                                                             "zlib::zlib"]}}},
+                               env_info={})
+        consumer = GenConanfile("consumer", "0.1").with_setting("build_type")\
+            .with_generator("pkg_config")\
+            .with_requirement(ConanFileReference("final", "0.1", None, None))
+        client = TestClient()
+        client.save({"zlib.py": zlib, "mypkg.py": mypkg, "final.py": final, "consumer.py": consumer})
+        client.run("create zlib.py")
+        client.run("create mypkg.py")
+        client.run("create final.py")
+        client.run("install consumer.py", assert_error=True)
+        self.assertIn("Component 'mypkg::zlib' not found in 'mypkg' package requirement", client.out)
+