Repository: conan-io/conan

Issue:
Invalid formatting of libdir/includedir in pkg_config generator [bug]
Version: conan-1.2.3
OS: Linux

https://github.com/conan-io/conan/blob/ac25c8994878706bac23efd009ae74bc294c1add/conans/client/generators/pkg_config.py#L28-L36

`pkg_config` generator doesn't check if libdir or includedir are absolute paths which leads to the following (invalid) `.pc` being generated for `boost_math/1.66.0@bincrafters/stable`:

```
prefix=/home/user/.conan/data/boost_math/1.66.0/bincrafters/stable/package/937af278b1c9d86290dddaf1322dc99fb7d75862
libdir=${prefix}//home/user/.conan/data/boost_level8group/1.66.0/bincrafters/stable/package/afc149253b16bde7d64e527e0dbfa27b203330c8/math/lib
includedir=${prefix}//home/user/.conan/data/boost_level8group/1.66.0/bincrafters/stable/package/afc149253b16bde7d64e527e0dbfa27b203330c8/math/include
...
```


Discussion:
Hi @coffee-lord !

Thanks you so much fot the detailed report. Seems it is a bug with absolute paths as you said, mainly affecting recipes with `short_paths=True` like boost ones.

We will try to fix it for the next release.
Thanks!

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
@@ -1,3 +1,5 @@
+import os
+from conans.client.build.compiler_flags import rpath_flags
 from conans.model import Generator
 
 """
@@ -18,54 +20,82 @@
 """
 
 
-def concat_if_not_empty(groups):
-    return " ".join([param for group in groups for param in group if param and param.strip()])
-
-
-def single_pc_file_contents(name, cpp_info):
-    lines = ['prefix=%s' % cpp_info.rootpath.replace("\\", "/")]
-    libdir_vars = []
-    for i, libdir in enumerate(cpp_info.libdirs):
-        varname = "libdir" if i == 0 else "libdir%d" % (i + 2)
-        lines.append("%s=${prefix}/%s" % (varname, libdir))
-        libdir_vars.append(varname)
-    include_dir_vars = []
-    for i, includedir in enumerate(cpp_info.includedirs):
-        varname = "includedir" if i == 0 else "includedir%d" % (i + 2)
-        lines.append("%s=${prefix}/%s" % (varname, includedir))
-        include_dir_vars.append(varname)
-    lines.append("")
-    lines.append("Name: %s" % name)
-    description = cpp_info.description or "Conan package: %s" % name
-    lines.append("Description: %s" % description)
-    lines.append("Version: %s" % cpp_info.version)
-    libdirs_flags = ["-L${%s}" % name for name in libdir_vars]
-    libnames_flags = ["-l%s " % name for name in cpp_info.libs]
-    shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags
-    lines.append("Libs: %s" % concat_if_not_empty([libdirs_flags, libnames_flags, shared_flags]))
-    include_dirs_flags = ["-I${%s}" % name for name in include_dir_vars]
-
-    lines.append("Cflags: %s" % concat_if_not_empty(
-                                 [include_dirs_flags,
-                                  cpp_info.cppflags,
-                                  cpp_info.cflags,
-                                  ["-D%s" % d for d in cpp_info.defines]]))
-
-    if cpp_info.public_deps:
-        public_deps = " ".join(cpp_info.public_deps)
-        lines.append("Requires: %s" % public_deps)
-    return "\n".join(lines) + "\n"
-
-
 class PkgConfigGenerator(Generator):
 
     @property
     def filename(self):
         pass
 
+    @property
+    def compiler(self):
+        return self.conanfile.settings.get_safe("compiler")
+
     @property
     def content(self):
         ret = {}
         for depname, cpp_info in self.deps_build_info.dependencies:
-            ret["%s.pc" % depname] = single_pc_file_contents(depname, cpp_info)
+            ret["%s.pc" % depname] = self.single_pc_file_contents(depname, cpp_info)
         return ret
+
+    def single_pc_file_contents(self, name, cpp_info):
+        prefix_path = cpp_info.rootpath.replace("\\", "/")
+        lines = ['prefix=%s' % prefix_path]
+
+        libdir_vars = []
+        varname = "libdir"
+        dir_lines = _generate_dir_lines(prefix_path, varname, cpp_info.libdirs)
+        libdir_vars.append(varname)
+        lines.extend(dir_lines)
+
+        includedir_vars = []
+        varname = "includedir"
+        dir_lines = _generate_dir_lines(prefix_path, varname, cpp_info.includedirs)
+        includedir_vars.append(varname)
+        lines.extend(dir_lines)
+
+        lines.append("")
+        lines.append("Name: %s" % name)
+        description = cpp_info.description or "Conan package: %s" % name
+        lines.append("Description: %s" % description)
+        lines.append("Version: %s" % cpp_info.version)
+        libdirs_flags = ["-L${%s}" % name for name in libdir_vars]
+        libnames_flags = ["-l%s " % name for name in cpp_info.libs]
+        shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags
+        the_os = (self.conanfile.settings.get_safe("os_build") or
+                  self.conanfile.settings.get_safe("os"))
+        rpaths = rpath_flags(the_os, self.compiler, self._deps_build_info.lib_paths)
+        lines.append("Libs: %s" % _concat_if_not_empty([libdirs_flags,
+                                                        libnames_flags,
+                                                        shared_flags,
+                                                        rpaths]))
+        include_dirs_flags = ["-I${%s}" % name for name in includedir_vars]
+
+        lines.append("Cflags: %s" % _concat_if_not_empty(
+            [include_dirs_flags,
+             cpp_info.cppflags,
+             cpp_info.cflags,
+             ["-D%s" % d for d in cpp_info.defines]]))
+
+        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()])
+
+
+def _generate_dir_lines(prefix_path, varname, dirs):
+    lines = []
+    for i, directory in enumerate(dirs):
+        directory = os.path.normpath(directory).replace("\\", "/")
+        varname = varname if i == 0 else "%s%d" % (varname, (i + 2))
+        prefix = ""
+        if not os.path.isabs(directory):
+            prefix = "${prefix}/"
+        elif directory.startswith(prefix_path):
+            prefix = "${prefix}/"
+            directory = os.path.relpath(directory, prefix_path)
+        lines.append("%s=%s%s" % (varname, prefix, directory))
+    return lines


Test patch:
diff --git a/conans/test/generators/pkg_config_test.py b/conans/test/generators/pkg_config_test.py
--- a/conans/test/generators/pkg_config_test.py
+++ b/conans/test/generators/pkg_config_test.py
@@ -1,10 +1,13 @@
+import os
 import unittest
 
 from conans.client.generators.pkg_config import PkgConfigGenerator
 from conans.model.settings import Settings
 from conans.model.conan_file import ConanFile
 from conans.model.build_info import CppInfo
-from conans.model.ref import ConanFileReference
+from conans.model.ref import ConanFileReference, PackageReference
+from conans.test.utils.tools import TestClient
+from conans.util.files import load
 
 
 class PkgGeneratorTest(unittest.TestCase):
@@ -55,3 +58,99 @@ def variables_setup_test(self):
 Libs: -L${libdir}
 Cflags: -I${includedir} -Flag1=23 -DMYDEFINE1
 """)
+
+    def pkg_config_dirs_test(self):
+        # https://github.com/conan-io/conan/issues/2756
+        conanfile = """
+import os
+from conans import ConanFile
+
+class PkgConfigConan(ConanFile):
+    name = "MyLib"
+    version = "0.1"
+
+    def package_info(self):
+        self.cpp_info.includedirs = []
+        self.cpp_info.libdirs = []
+        self.cpp_info.bindirs = []
+        self.cpp_info.libs = []
+        libname = "mylib"
+        fake_dir = os.path.join("/", "my_absoulte_path", "fake")
+        include_dir = os.path.join(fake_dir, libname, "include")
+        lib_dir = os.path.join(fake_dir, libname, "lib")
+        lib_dir2 = os.path.join(self.package_folder, "lib2")
+        self.cpp_info.includedirs.append(include_dir)
+        self.cpp_info.libdirs.append(lib_dir)
+        self.cpp_info.libdirs.append(lib_dir2)
+"""
+        client = TestClient()
+        client.save({"conanfile.py": conanfile})
+        client.run("create . danimtb/testing")
+        client.run("install MyLib/0.1@danimtb/testing -g pkg_config")
+
+        pc_path = os.path.join(client.current_folder, "MyLib.pc")
+        self.assertTrue(os.path.exists(pc_path))
+        pc_content = load(pc_path)
+        self.assertEquals("\n".join(pc_content.splitlines()[1:]),
+                          """libdir=/my_absoulte_path/fake/mylib/lib
+libdir3=${prefix}/lib2
+includedir=/my_absoulte_path/fake/mylib/include
+
+Name: MyLib
+Description: Conan package: MyLib
+Version: 0.1
+Libs: -L${libdir}
+Cflags: -I${includedir}""")
+
+        def assert_is_abs(path):
+            self.assertTrue(os.path.isabs(path))
+
+        for line in pc_content.splitlines():
+            if line.startswith("includedir="):
+                assert_is_abs(line[len("includedir="):])
+                self.assertTrue(line.endswith("include"))
+            elif line.startswith("libdir="):
+                assert_is_abs(line[len("libdir="):])
+                self.assertTrue(line.endswith("lib"))
+            elif line.startswith("libdir3="):
+                self.assertIn("${prefix}/lib2", line)
+
+    def pkg_config_rpaths_test(self):
+        # rpath flags are only generated for gcc and clang
+        profile = """
+[settings]
+os=Linux
+compiler=gcc
+compiler.version=7
+compiler.libcxx=libstdc++
+"""
+        conanfile = """
+from conans import ConanFile
+
+class PkgConfigConan(ConanFile):
+    name = "MyLib"
+    version = "0.1"
+    settings = "os", "compiler"
+    exports = "mylib.so"
+    
+    def package(self):
+        self.copy("mylib.so", dst="lib")
+
+    def package_info(self):
+        self.cpp_info.libs = ["mylib"]
+"""
+        client = TestClient()
+        client.save({"conanfile.py": conanfile,
+                     "linux_gcc": profile,
+                     "mylib.so": "fake lib content"})
+        client.run("create . danimtb/testing -pr=linux_gcc")
+        client.run("install MyLib/0.1@danimtb/testing -g pkg_config -pr=linux_gcc")
+
+        pc_path = os.path.join(client.current_folder, "MyLib.pc")
+        self.assertTrue(os.path.exists(pc_path))
+        pc_content = load(pc_path)
+        conan_ref = ConanFileReference.loads("MyLib/0.1@danimtb/testing")
+        package_dir = client.paths.package(
+            PackageReference(conan_ref, "ea61221ce137c6bac82570628201a6e1ecb34adc"))
+        correct_path = os.path.join(package_dir, "lib").replace("\\", "/")
+        self.assertIn("-Wl,-rpath=\"%s\"" % correct_path, pc_content)