Skip to content

Commit

Permalink
[PkgConfigDeps] Tool requires (#11979)
Browse files Browse the repository at this point in the history
* Adding suffix code (from cmakedeps)

* Tests are OK

* Adding test

* wip

* Fixed tests

* Added test

* Added more tests

* Minor change

* minor changes

* Module function

* Backward compatible

* Better comment
  • Loading branch information
franramirez688 authored Aug 30, 2022
1 parent dd2965c commit 2d3f151
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 27 deletions.
92 changes: 75 additions & 17 deletions conan/tools/gnu/pkgconfigdeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,37 @@ def _get_component_aliases(dep, comp_name):
return comp_aliases or []


def _get_package_name(dep):
pkg_name = dep.cpp_info.get_property("pkg_config_name")
return pkg_name or _get_package_reference_name(dep)
def _get_package_name(dep, build_context_suffix=None):
pkg_name = dep.cpp_info.get_property("pkg_config_name") or _get_package_reference_name(dep)
suffix = _get_suffix(dep, build_context_suffix)
return f"{pkg_name}{suffix}"


def _get_component_name(dep, comp_name):
def _get_component_name(dep, comp_name, build_context_suffix=None):
if comp_name not in dep.cpp_info.components:
# foo::foo might be referencing the root cppinfo
if _get_package_reference_name(dep) == comp_name:
return _get_package_name(dep)
return _get_package_name(dep, build_context_suffix)
raise ConanException("Component '{name}::{cname}' not found in '{name}' "
"package requirement".format(name=_get_package_reference_name(dep),
cname=comp_name))
comp_name = dep.cpp_info.components[comp_name].get_property("pkg_config_name")
return comp_name
suffix = _get_suffix(dep, build_context_suffix)
return f"{comp_name}{suffix}" if comp_name else None


def _get_suffix(req, build_context_suffix=None):
"""
Get the package name suffix coming from PkgConfigDeps.build_context_suffix attribute, but only
for requirements declared as build requirement.
:param req: requirement ConanFile instance
:param build_context_suffix: `dict` with all the suffixes
:return: `str` with the suffix
"""
if not build_context_suffix or not req.is_build_context:
return ""
return build_context_suffix.get(req.ref.name, "")


def _get_formatted_dirs(folders, prefix_path_):
Expand Down Expand Up @@ -184,8 +200,9 @@ def shortened_content(self, info):

class PCGenerator:

def __init__(self, conanfile, dep):
def __init__(self, conanfile, dep, build_context_suffix=None):
self._conanfile = conanfile
self._build_context_suffix = build_context_suffix or {}
self._dep = dep
self._content_generator = PCContentGenerator(self._conanfile, self._dep)

Expand Down Expand Up @@ -223,9 +240,9 @@ def package_info(self):
req_conanfile = self._dep.dependencies.host[pkg_ref_name]
else: # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello
req_conanfile = self._dep
comp_name = _get_component_name(req_conanfile, comp_ref_name)
comp_name = _get_component_name(req_conanfile, comp_ref_name, self._build_context_suffix)
if not comp_name:
pkg_name = _get_package_name(req_conanfile)
pkg_name = _get_package_name(req_conanfile, self._build_context_suffix)
# Creating a component name with namespace, e.g., dep-comp1
comp_name = _get_name_with_namespace(pkg_name, comp_ref_name)
ret.append(comp_name)
Expand All @@ -239,16 +256,18 @@ def components_info(self):
:return: `list` of `_PCInfo` objects with all the components information
"""
pkg_name = _get_package_name(self._dep)
pkg_name = _get_package_name(self._dep, self._build_context_suffix)
components_info = []
# Loop through all the package's components
for comp_ref_name, cpp_info in self._dep.cpp_info.get_sorted_components().items():
# At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
comp_requires_names = self._get_cpp_info_requires_names(cpp_info)
comp_name = _get_component_name(self._dep, comp_ref_name)
comp_name = _get_component_name(self._dep, comp_ref_name, self._build_context_suffix)
if not comp_name:
comp_name = _get_name_with_namespace(pkg_name, comp_ref_name)
comp_description = "Conan component: %s-%s" % (pkg_name, comp_name)
comp_description = f"Conan component: {comp_name}"
else:
comp_description = f"Conan component: {pkg_name}-{comp_name}"
comp_aliases = _get_component_aliases(self._dep, comp_ref_name)
# Save each component information
components_info.append(_PCInfo(comp_name, comp_requires_names, comp_description,
Expand All @@ -262,14 +281,15 @@ def package_info(self):
:return: `_PCInfo` object with the package information
"""
pkg_name = _get_package_name(self._dep)
pkg_name = _get_package_name(self._dep, self._build_context_suffix)
# At first, let's check if we have defined some global requires, e.g., "other::cmp1"
requires = self._get_cpp_info_requires_names(self._dep.cpp_info)
# If we have found some component requires it would be enough
if not requires:
# If no requires were found, let's try to get all the direct dependencies,
# e.g., requires = "other_pkg/1.0"
requires = [_get_package_name(req) for req in self._dep.dependencies.direct_host.values()]
requires = [_get_package_name(req, self._build_context_suffix)
for req in self._dep.dependencies.direct_host.values()]
description = "Conan package: %s" % pkg_name
aliases = _get_package_aliases(self._dep)
cpp_info = self._dep.cpp_info
Expand Down Expand Up @@ -317,7 +337,7 @@ def _update_pc_files(info):
# Second, let's load the root package's PC file ONLY
# if it does not already exist in components one
# Issue related: https://github.com/conan-io/conan/issues/10341
pkg_name = _get_package_name(self._dep)
pkg_name = _get_package_name(self._dep, self._build_context_suffix)
if f"{pkg_name}.pc" not in pc_files:
package_info = _PCInfo(pkg_name, pkg_requires, f"Conan package: {pkg_name}", None,
_get_package_aliases(self._dep))
Expand All @@ -335,24 +355,62 @@ class PkgConfigDeps:

def __init__(self, conanfile):
self._conanfile = conanfile
# Activate the build *.pc files for the specified libraries
self.build_context_activated = []
# If specified, the files/requires/names for the build context will be renamed appending
# a suffix. It is necessary in case of same require and build_require and will cause an error
self.build_context_suffix = {}

def _validate_build_requires(self, host_req, build_req):
"""
Check if any package exists at host and build context at the same time, and
it doesn't have any suffix to avoid any name collisions
:param host_req: list of host requires
:param build_req: list of build requires
"""
activated_br = {r.ref.name for r in build_req.values()
if r.ref.name in self.build_context_activated}
common_names = {r.ref.name for r in host_req.values()}.intersection(activated_br)
without_suffixes = [common_name for common_name in common_names
if self.build_context_suffix.get(common_name) is None]
if without_suffixes:
raise ConanException(f"The packages {without_suffixes} exist both as 'require' and as"
f" 'build require'. You need to specify a suffix using the "
f"'build_context_suffix' attribute at the PkgConfigDeps generator.")

@property
def content(self):
"""Get all the *.pc files content"""
pc_files = {}
# Get all the dependencies
host_req = self._conanfile.dependencies.host
build_req = self._conanfile.dependencies.build # tool_requires
test_req = self._conanfile.dependencies.test

for require, dep in list(host_req.items()) + list(test_req.items()):
# Check if it exists both as require and as build require without a suffix
self._validate_build_requires(host_req, build_req)

for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()):
# Require is not used at the moment, but its information could be used,
# and will be used in Conan 2.0
pc_generator = PCGenerator(self._conanfile, dep)
# Filter the build_requires not activated with PkgConfigDeps.build_context_activated
if dep.is_build_context and dep.ref.name not in self.build_context_activated:
continue

pc_generator = PCGenerator(self._conanfile, dep, build_context_suffix=self.build_context_suffix)
pc_files.update(pc_generator.pc_files)
return pc_files

def generate(self):
"""Save all the *.pc files"""
# FIXME: Remove this in 2.0
if not hasattr(self._conanfile, "settings_build") and \
(self.build_context_activated or self.build_context_suffix):
raise ConanException("The 'build_context_activated' and 'build_context_build_modules' of"
" the PkgConfigDeps generator cannot be used without specifying"
" a build profile. e.g: -pr:b=default")

# Current directory is the generators_folder
generator_files = self.content
for generator_file, content in generator_files.items():
Expand Down
172 changes: 162 additions & 10 deletions conans/test/integration/toolchains/gnu/test_pkgconfigdeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import os
import textwrap

import pytest

from conans.test.assets.genconanfile import GenConanfile
from conans.test.utils.tools import TestClient
from conans.util.files import load
Expand Down Expand Up @@ -524,22 +522,28 @@ def test_pkgconfigdeps_with_test_requires():
Related issue: https://github.com/conan-io/conan/issues/11376
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
def package_info(self):
self.cpp_info.libs = ["lib%s"]
""")
with client.chdir("app"):
client.run("new app/1.0 -m cmake_lib")
# client.run("new cmake_lib -d name=app -d version=1.0")
client.run("create .")
client.save({"conanfile.py": conanfile % "app"})
# client.run("create . --name=app --version=1.0")
client.run("create . app/1.0@")
with client.chdir("test"):
client.run("new test/1.0 -m cmake_lib")
# client.run("new cmake_lib -d name=test -d version=1.0")
client.run("create .")
client.save({"conanfile.py": conanfile % "test"})
# client.run("create . --name=test --version=1.0")
client.run("create . test/1.0@")
# Create library having build and test requires
conanfile = textwrap.dedent(r'''
conanfile = textwrap.dedent("""
from conan import ConanFile
class HelloLib(ConanFile):
def build_requirements(self):
self.test_requires('app/1.0')
self.test_requires('test/1.0')
''')
""")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("install . -g PkgConfigDeps")
assert "Description: Conan package: test" in client.load("test.pc")
Expand Down Expand Up @@ -573,3 +577,151 @@ def package_info(self):
assert "Libs: -lmylib" in pc
assert 'includedir1=' in pc
assert 'Cflags: -I"${includedir1}"' in pc


def test_tool_requires():
"""
Testing if PC files are created for tool requires if build_context_activated/_suffix is used.
Issue related: https://github.com/conan-io/conan/issues/11710
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class PkgConfigConan(ConanFile):
def package_info(self):
self.cpp_info.libs = ["libtool"]
""")
client.save({"conanfile.py": conanfile})
client.run("create . tool/1.0@")

conanfile = textwrap.dedent("""
from conan import ConanFile
class PkgConfigConan(ConanFile):
def package_info(self):
self.cpp_info.set_property("pkg_config_name", "libother")
self.cpp_info.components["cmp1"].libs = ["other_cmp1"]
self.cpp_info.components["cmp1"].set_property("pkg_config_name", "component1")
self.cpp_info.components["cmp2"].libs = ["other_cmp2"]
self.cpp_info.components["cmp3"].requires.append("cmp1")
self.cpp_info.components["cmp3"].set_property("pkg_config_name", "component3")
""")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("create . other/1.0@")

conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.gnu import PkgConfigDeps
class PkgConfigConan(ConanFile):
name = "demo"
version = "1.0"
def build_requirements(self):
self.build_requires("tool/1.0")
self.build_requires("other/1.0")
def generate(self):
tc = PkgConfigDeps(self)
tc.build_context_activated = ["other", "tool"]
tc.build_context_suffix = {"tool": "_bt", "other": "_bo"}
tc.generate()
""")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("install . -pr:h default -pr:b default")
pc_files = [os.path.basename(i) for i in glob.glob(os.path.join(client.current_folder, '*.pc'))]
pc_files.sort()
# Let's check all the PC file names created just in case
assert pc_files == ['component1_bo.pc', 'component3_bo.pc',
'libother_bo-cmp2.pc', 'libother_bo.pc', 'tool_bt.pc']
pc_content = client.load("tool_bt.pc")
assert "Name: tool_bt" in pc_content
pc_content = client.load("libother_bo.pc")
assert "Name: libother_bo" in pc_content
assert "Requires: component1_bo libother_bo-cmp2 component3_bo" == get_requires_from_content(pc_content)
pc_content = client.load("component1_bo.pc")
assert "Name: component1_bo" in pc_content
pc_content = client.load("libother_bo-cmp2.pc")
assert "Name: libother_bo-cmp2" in pc_content
pc_content = client.load("component3_bo.pc")
assert "Name: component3_bo" in pc_content
assert "Requires: component1_bo" == get_requires_from_content(pc_content)


def test_tool_requires_not_created_if_no_activated():
"""
Testing if there are no PC files created in no context are activated
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class PkgConfigConan(ConanFile):
def package_info(self):
self.cpp_info.libs = ["libtool"]
""")
client.save({"conanfile.py": conanfile})
client.run("create . tool/1.0@")

conanfile = textwrap.dedent("""
from conan import ConanFile
class PkgConfigConan(ConanFile):
name = "demo"
version = "1.0"
generators = "PkgConfigDeps"
def build_requirements(self):
self.build_requires("tool/1.0")
""")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("install . -pr:h default -pr:b default")
pc_files = [os.path.basename(i) for i in glob.glob(os.path.join(client.current_folder, '*.pc'))]
pc_files.sort()
assert pc_files == []


def test_tool_requires_raise_exception_if_exist_both_require_and_build_one():
"""
Testing if same dependency exists in both require and build require (without suffix)
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class PkgConfigConan(ConanFile):
def package_info(self):
self.cpp_info.libs = ["libtool"]
""")
client.save({"conanfile.py": conanfile})
client.run("create . tool/1.0@")

conanfile = textwrap.dedent("""
from conan import ConanFile
from conan.tools.gnu import PkgConfigDeps
class PkgConfigConan(ConanFile):
name = "demo"
version = "1.0"
def requirements(self):
self.requires("tool/1.0")
def build_requirements(self):
self.build_requires("tool/1.0")
def generate(self):
tc = PkgConfigDeps(self)
tc.build_context_activated = ["tool"]
tc.generate()
""")
client.save({"conanfile.py": conanfile}, clean_first=True)
client.run("install . -pr:h default -pr:b default", assert_error=True)
assert "The packages ['tool'] exist both as 'require' and as 'build require'" in client.out

0 comments on commit 2d3f151

Please sign in to comment.