Skip to content

Commit

Permalink
Feature/refactor pkgconfigdeps (#9806)
Browse files Browse the repository at this point in the history
* Using templates as content instead of python lists

* Simplifying all the class structure

* Fixed template bugs

* Removed breakpoint()

* Added docstings and comments

* Making pc classes private

* Better variable name

* Fixed problem with wrong dirs order

* Changed static-structure OO

* Naming changes

* Renamed methods and attributes

* Adding more logic to templates

* Always creating the main pc file

* Safer method to print the flags

* Avoiding trailing whitespaces
  • Loading branch information
franramirez688 committed Oct 27, 2021
1 parent 704b395 commit bc64a47
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 158 deletions.
324 changes: 182 additions & 142 deletions conan/tools/gnu/pkgconfigdeps.py
Expand Up @@ -17,26 +17,25 @@
Requires.private: gthread-2.0 >= 2.40
"""
import os
import textwrap

from jinja2 import Template, StrictUndefined

from conan.tools.gnu.gnudeps_flags import GnuDepsFlags
from conans.errors import ConanException
from conans.util.files import save


def _concat_if_not_empty(groups):
return " ".join([param for group in groups for param in group if param and param.strip()])


def get_target_namespace(req):
def get_package_name(req):
ret = req.cpp_info.get_property("pkg_config_name", "PkgConfigDeps")
return ret or req.ref.name


def get_component_alias(req, comp_name):
def get_component_name(req, comp_name):
if comp_name not in req.cpp_info.components:
# foo::foo might be referencing the root cppinfo
if req.ref.name == comp_name:
return get_target_namespace(req)
return get_package_name(req)
raise ConanException("Component '{name}::{cname}' not found in '{name}' "
"package requirement".format(name=req.ref.name, cname=comp_name))
ret = req.cpp_info.components[comp_name].get_property("pkg_config_name", "PkgConfigDeps")
Expand All @@ -49,156 +48,197 @@ def __init__(self, conanfile):
self._conanfile = conanfile

@staticmethod
def _get_composed_require_name(pkg_name, comp_name):
def _get_pc_name(pkg_name, comp_name):
"""Build a composed name for all the components and its package root name"""
return "%s-%s" % (pkg_name, comp_name)

def _get_require_comp_name(self, dep, req):
# FIXME: this str() is only needed for python2.7 (unicode values). Remove it for Conan 2.0
pkg_name = str(dep.ref.name)
pkg, comp_name = req.split("::") if "::" in req else (pkg_name, req)
# FIXME: it could allow defining requires to not direct dependencies
req = self._conanfile.dependencies.host[pkg]
cmp_name = get_component_alias(req, comp_name)
return self._get_composed_require_name(pkg, cmp_name)
def _get_requires_names(self, name, cpp_info):
"""
Get all the pkg-config valid names from the requires ones given a dependency and
a CppInfo object.
def _get_components(self, dep):
Note: CppInfo could be coming from one Component object instead of the dependency
"""
ret = []
for comp_name, comp in dep.cpp_info.get_sorted_components().items():
comp_genname = get_component_alias(dep, comp_name)
comp_requires_gennames = []
for require in comp.requires:
comp_requires_gennames.append(self._get_require_comp_name(dep, require))
ret.append((comp_genname, comp, comp_requires_gennames))
for req in cpp_info.requires:
pkg_name, comp_name = req.split("::") if "::" in req else (name, req)
# FIXME: it could allow defining requires to not direct dependencies
req_conanfile = self._conanfile.dependencies.host[pkg_name]
comp_alias_name = get_component_name(req_conanfile, comp_name)
ret.append(self._get_pc_name(pkg_name, comp_alias_name))
return ret

def _get_public_require_deps(self, dep):
public_comp_deps = []

for require in dep.cpp_info.requires:
if "::" in require: # Points to a component of a different package
pkg, cmp_name = require.split("::")
req = dep.dependencies.direct_host[pkg]
public_comp_deps.append(
self._get_composed_require_name(pkg, get_component_alias(req, cmp_name))
)
else: # Points to a component of same package
public_comp_deps.append(get_component_alias(dep, require))
return public_comp_deps
def get_components_files_and_content(self, dep):
"""Get all the *.pc files content for the dependency and each of its components"""
pc_files = {}
pkg_name = get_package_name(dep)
comp_names = []
pc_gen = _PCFilesTemplate(self._conanfile, dep)
# Loop through all the package's components
for comp_name, comp_cpp_info in dep.cpp_info.get_sorted_components().items():
comp_name = get_component_name(dep, comp_name)
comp_names.append(comp_name)
# FIXME: this str(dep.ref.name) is only needed for python2.7 (unicode values).
# Remove it for Conan 2.0
comp_requires_names = self._get_requires_names(str(dep.ref.name), comp_cpp_info)
# Get the *.pc file content for each component
pkg_comp_name = self._get_pc_name(pkg_name, comp_name)
pc_files.update(pc_gen.get_pc_filename_and_content(comp_requires_names,
name=pkg_comp_name,
cpp_info=comp_cpp_info))
# Let's create a *.pc file for the main package
pkg_requires = [self._get_pc_name(pkg_name, i) for i in comp_names]
pc_files.update(pc_gen.get_wrapper_pc_filename_and_content(pkg_requires))
return pc_files

@property
def content(self):
ret = {}
"""Get all the *.pc files content"""
pc_files = {}
host_req = self._conanfile.dependencies.host
for require, dep in host_req.items():
pkg_genname = get_target_namespace(dep)

if dep.cpp_info.has_components:
components = self._get_components(dep)
# Adding one *.pc file per component, e.g., pkg-comp1.pc
for comp_genname, comp_cpp_info, comp_requires_gennames in components:
pkg_comp_genname = self._get_composed_require_name(pkg_genname, comp_genname)
ret["%s.pc" % pkg_comp_genname] = self._pc_file_content(
pkg_comp_genname, comp_cpp_info,
comp_requires_gennames,
dep.package_folder, dep.ref.version)
# Adding the pkg *.pc file (including its components as requires if any)
comp_gennames = [comp_genname for comp_genname, _, _ in components]
if pkg_genname not in comp_gennames:
pkg_requires = (self._get_composed_require_name(pkg_genname, i)
for i in comp_gennames)
ret["%s.pc" % pkg_genname] = self._global_pc_file_contents(pkg_genname,
dep,
pkg_requires)
else:
ret["%s.pc" % pkg_genname] = self._pc_file_content(pkg_genname, dep.cpp_info,
self._get_public_require_deps(dep),
dep.package_folder,
dep.ref.version)
return ret

def _pc_file_content(self, name, cpp_info, requires_gennames, package_folder, version):
prefix_path = package_folder.replace("\\", "/")
lines = ['prefix=%s' % prefix_path]

gnudeps_flags = GnuDepsFlags(self._conanfile, cpp_info)

libdir_vars = []
dir_lines, varnames = self._generate_dir_lines(prefix_path, "libdir", cpp_info.libdirs)
if dir_lines:
libdir_vars = varnames
lines.extend(dir_lines)

includedir_vars = []
dir_lines, varnames = self._generate_dir_lines(prefix_path, "includedir",
cpp_info.includedirs)
if dir_lines:
includedir_vars = varnames
lines.extend(dir_lines)

pkg_config_custom_content = cpp_info.get_property("pkg_config_custom_content",
"PkgConfigDeps")
if pkg_config_custom_content:
lines.append(pkg_config_custom_content)

lines.append("")
lines.append("Name: %s" % name)
description = self._conanfile.description or "Conan package: %s" % name
lines.append("Description: %s" % description)
lines.append("Version: %s" % version)
libdirs_flags = ['-L"${%s}"' % name for name in libdir_vars]
lib_paths = ["${%s}" % libdir for libdir in libdir_vars]
libnames_flags = ["-l%s " % name for name in (cpp_info.libs + cpp_info.system_libs)]
shared_flags = cpp_info.sharedlinkflags + cpp_info.exelinkflags

lines.append("Libs: %s" % _concat_if_not_empty([libdirs_flags,
libnames_flags,
shared_flags,
gnudeps_flags._rpath_flags(lib_paths),
gnudeps_flags.frameworks,
gnudeps_flags.framework_paths]))
include_dirs_flags = ['-I"${%s}"' % name for name in includedir_vars]

lines.append("Cflags: %s" % _concat_if_not_empty(
[include_dirs_flags,
cpp_info.cxxflags,
cpp_info.cflags,
["-D%s" % d for d in cpp_info.defines]]))

if requires_gennames:
public_deps = " ".join(requires_gennames)
lines.append("Requires: %s" % public_deps)
return "\n".join(lines) + "\n"

def _global_pc_file_contents(self, name, dep, comp_gennames):
lines = ["Name: %s" % name]
description = self._conanfile.description or "Conan package: %s" % name
lines.append("Description: %s" % description)
lines.append("Version: %s" % dep.ref.version)

if comp_gennames:
public_deps = " ".join(comp_gennames)
lines.append("Requires: %s" % public_deps)
return "\n".join(lines) + "\n"

@staticmethod
def _generate_dir_lines(prefix_path, varname, dirs):
lines = []
varnames = []
for i, directory in enumerate(dirs):
directory = os.path.normpath(directory).replace("\\", "/")
name = varname if i == 0 else "%s%d" % (varname, (i + 1))
prefix = ""
if not os.path.isabs(directory):
prefix = "${prefix}/"
elif directory.startswith(prefix_path):
prefix = "${prefix}/"
directory = os.path.relpath(directory, prefix_path).replace("\\", "/")
lines.append("%s=%s%s" % (name, prefix, directory))
varnames.append(name)
return lines, varnames
pc_files.update(self.get_components_files_and_content(dep))
else: # Content for package without components
pc_gen = _PCFilesTemplate(self._conanfile, dep)
# FIXME: this str(dep.ref.name) is only needed for python2.7 (unicode values).
# Remove it for Conan 2.0
requires = self._get_requires_names(str(dep.ref.name), dep.cpp_info)
pc_files.update(pc_gen.get_pc_filename_and_content(requires))
return pc_files

def generate(self):
"""Save all the *.pc files"""
# Current directory is the generators_folder
generator_files = self.content
for generator_file, content in generator_files.items():
save(generator_file, content)


class _PCFilesTemplate(object):

def __init__(self, conanfile, dep):
self._conanfile = conanfile
self._dep = dep
self._name = get_package_name(dep)

pc_file_template = textwrap.dedent("""\
{%- macro get_libs(libdirs, cpp_info, gnudeps_flags) -%}
{%- for _ in libdirs -%}
{{ '-L"${libdir%s}"' % loop.index + " " }}
{%- endfor -%}
{%- for sys_lib in (cpp_info.libs + cpp_info.system_libs) -%}
{{ "-l%s" % sys_lib + " " }}
{%- endfor -%}
{%- for shared_flag in (cpp_info.sharedlinkflags + cpp_info.exelinkflags) -%}
{{ shared_flag + " " }}
{%- endfor -%}
{%- for _ in libdirs -%}
{%- set flag = gnudeps_flags._rpath_flags(["${libdir%s}" % loop.index]) -%}
{%- if flag|length -%}
{{ flag[0] + " " }}
{%- endif -%}
{%- endfor -%}
{%- for framework in (gnudeps_flags.frameworks + gnudeps_flags.framework_paths) -%}
{{ framework + " " }}
{%- endfor -%}
{%- endmacro -%}
{%- macro get_cflags(includedirs, cpp_info) -%}
{%- for _ in includedirs -%}
{{ '-I"${includedir%s}"' % loop.index + " " }}
{%- endfor -%}
{%- for cxxflags in cpp_info.cxxflags -%}
{{ cxxflags + " " }}
{%- endfor -%}
{%- for cflags in cpp_info.cflags-%}
{{ cflags + " " }}
{%- endfor -%}
{%- for define in cpp_info.defines-%}
{{ "-D%s" % define + " " }}
{%- endfor -%}
{%- endmacro -%}
prefix={{ prefix_path }}
{% for path in libdirs %}
{{ "libdir{}={}".format(loop.index, path) }}
{% endfor %}
{% for path in includedirs %}
{{ "includedir%d=%s" % (loop.index, path) }}
{% endfor %}
{% if pkg_config_custom_content %}
# Custom PC content
{{ pkg_config_custom_content }}
{% endif %}
Name: {{ name }}
Description: {{ description }}
Version: {{ version }}
Libs: {{ get_libs(libdirs, cpp_info, gnudeps_flags) }}
Cflags: {{ get_cflags(includedirs, cpp_info) }}
{% if requires|length %}
Requires: {{ requires|join(' ') }}
{% endif %}
""")

wrapper_pc_file_template = textwrap.dedent("""\
Name: {{ name }}
Description: {{ description }}
Version: {{ version }}
{% if requires|length %}
Requires: {{ requires|join(' ') }}
{% endif %}
""")

def get_pc_filename_and_content(self, requires, name=None, cpp_info=None):

def get_formatted_dirs(folders, prefix_path_):
ret = []
for i, directory in enumerate(folders):
directory = os.path.normpath(directory).replace("\\", "/")
prefix = ""
if not os.path.isabs(directory):
prefix = "${prefix}/"
elif directory.startswith(prefix_path_):
prefix = "${prefix}/"
directory = os.path.relpath(directory, prefix_path_).replace("\\", "/")
ret.append("%s%s" % (prefix, directory))
return ret

dep_name = name or self._name
package_folder = self._dep.package_folder
version = self._dep.ref.version
cpp_info = cpp_info or self._dep.cpp_info

prefix_path = package_folder.replace("\\", "/")
libdirs = get_formatted_dirs(cpp_info.libdirs, prefix_path)
includedirs = get_formatted_dirs(cpp_info.includedirs, prefix_path)

context = {
"prefix_path": prefix_path,
"libdirs": libdirs,
"includedirs": includedirs,
"pkg_config_custom_content": cpp_info.get_property("pkg_config_custom_content", "PkgConfigDeps"),
"name": dep_name,
"description": self._conanfile.description or "Conan package: %s" % dep_name,
"version": version,
"requires": requires,
"cpp_info": cpp_info,
"gnudeps_flags": GnuDepsFlags(self._conanfile, cpp_info)
}
template = Template(self.pc_file_template, trim_blocks=True, lstrip_blocks=True,
undefined=StrictUndefined)
return {dep_name + ".pc": template.render(context)}

def get_wrapper_pc_filename_and_content(self, requires, name=None):
dep_name = name or self._name
context = {
"name": dep_name,
"description": self._conanfile.description or "Conan package: %s" % dep_name,
"version": self._dep.ref.version,
"requires": requires
}
template = Template(self.wrapper_pc_file_template, trim_blocks=True, lstrip_blocks=True,
undefined=StrictUndefined)
return {dep_name + ".pc": template.render(context)}

0 comments on commit bc64a47

Please sign in to comment.