Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate transitive components information in the same package for XcodeDeps #11507

Merged
merged 20 commits into from Jun 29, 2022
88 changes: 62 additions & 26 deletions conan/tools/apple/xcodedeps.py
@@ -1,5 +1,6 @@
import os
import textwrap
from collections import OrderedDict

from jinja2 import Template

Expand Down Expand Up @@ -60,14 +61,14 @@ class XcodeDeps(object):
general_name = "conandeps.xcconfig"

_conf_xconfig = textwrap.dedent("""\
// Compiler options for {{pkg_name}}::{{pkg_name}}
// Compiler options for {{pkg_name}}::{{comp_name}}
HEADER_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{include_dirs}}
GCC_PREPROCESSOR_DEFINITIONS_{{pkg_name}}_{{comp_name}}{{condition}} = {{definitions}}
OTHER_CFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{c_compiler_flags}}
OTHER_CPLUSPLUSFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{cxx_compiler_flags}}
FRAMEWORK_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{frameworkdirs}}

// Link options for {{name}}
// Link options for {{pkg_name}}::{{comp_name}}
LIBRARY_SEARCH_PATHS_{{pkg_name}}_{{comp_name}}{{condition}} = {{lib_dirs}}
OTHER_LDFLAGS_{{pkg_name}}_{{comp_name}}{{condition}} = {{linker_flags}} {{libs}} {{system_libs}} {{frameworks}}
""")
Expand Down Expand Up @@ -121,25 +122,28 @@ def generate(self):
for generator_file, content in generator_files.items():
save(generator_file, content)

def _conf_xconfig_file(self, pkg_name, comp_name, cpp_info):
def _conf_xconfig_file(self, pkg_name, comp_name, transitive_cpp_infos):
"""
content for conan_poco_x86_release.xcconfig, containing the activation
"""
def _merged_vars(name):
merged = [var for cpp_info in transitive_cpp_infos for var in getattr(cpp_info, name)]
return list(OrderedDict.fromkeys(merged).keys())

fields = {
'pkg_name': pkg_name,
'comp_name': comp_name,
'include_dirs': " ".join('"{}"'.format(p) for p in cpp_info.includedirs),
'lib_dirs': " ".join('"{}"'.format(p) for p in cpp_info.libdirs),
'libs': " ".join("-l{}".format(lib) for lib in cpp_info.libs),
'system_libs': " ".join("-l{}".format(sys_lib) for sys_lib in cpp_info.system_libs),
'frameworksdirs': " ".join('"{}"'.format(p) for p in cpp_info.frameworkdirs),
'frameworks': " ".join("-framework {}".format(framework) for framework in cpp_info.frameworks),
'definitions': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in cpp_info.defines),
'c_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in cpp_info.cflags),
'cxx_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in cpp_info.cxxflags),
'linker_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in cpp_info.sharedlinkflags),
'exe_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in cpp_info.exelinkflags),
'include_dirs': " ".join('"{}"'.format(p) for p in _merged_vars("includedirs")),
'lib_dirs': " ".join('"{}"'.format(p) for p in _merged_vars("libdirs")),
'libs': " ".join("-l{}".format(lib) for lib in _merged_vars("libs")),
'system_libs': " ".join("-l{}".format(sys_lib) for sys_lib in _merged_vars("system_libs")),
'frameworksdirs': " ".join('"{}"'.format(p) for p in _merged_vars("frameworkdirs")),
'frameworks': " ".join("-framework {}".format(framework) for framework in _merged_vars("frameworks")),
'definitions': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("defines")),
'c_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("cflags")),
'cxx_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("cxxflags")),
'linker_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("sharedlinkflags")),
'exe_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("exelinkflags")),
'condition': _xcconfig_conditional(self._conanfile.settings)
}

Expand Down Expand Up @@ -193,17 +197,17 @@ def _global_xconfig_content(self):
GLOBAL_XCCONFIG_TEMPLATE,
[self.general_name])

def get_content_for_component(self, pkg_name, component_name, cpp_info, reqs):
def get_content_for_component(self, pkg_name, component_name, transitive_internal, transitive_external):
result = {}

conf_name = _xcconfig_settings_filename(self._conanfile.settings)

props_name = "conan_{}_{}{}.xcconfig".format(pkg_name, component_name, conf_name)
result[props_name] = self._conf_xconfig_file(pkg_name, component_name, cpp_info)
result[props_name] = self._conf_xconfig_file(pkg_name, component_name, transitive_internal)

# The entry point for each package
file_dep_name = "conan_{}_{}.xcconfig".format(pkg_name, component_name)
dep_content = self._dep_xconfig_file(pkg_name, component_name, file_dep_name, props_name, reqs)
dep_content = self._dep_xconfig_file(pkg_name, component_name, file_dep_name, props_name, transitive_external)

result[file_dep_name] = dep_content
return result
Expand All @@ -213,28 +217,60 @@ def _content(self):

# Generate the config files for each component with name conan_pkgname_compname.xcconfig
# If a package has no components the name is conan_pkgname_pkgname.xcconfig
# Then all components are included in the conan_pkgname.xcconfig file
# All components are included in the conan_pkgname.xcconfig file
host_req = self._conanfile.dependencies.host
test_req = self._conanfile.dependencies.test
for dep in list(host_req.values()) + list(test_req.values()):
all_deps = list(host_req.values()) + list(test_req.values())
for dep in all_deps:

dep_name = _format_name(dep.ref.name)

include_components_names = []
if dep.cpp_info.has_components:
for comp_name, comp_cpp_info in dep.cpp_info.get_sorted_components().items():
component_deps = []
for req in comp_cpp_info.requires:
req_pkg, req_cmp = req.split("::") if "::" in req else (dep_name, req)
component_deps.append((req_pkg, req_cmp))

component_content = self.get_content_for_component(dep_name, comp_name, comp_cpp_info, component_deps)
sorted_components = dep.cpp_info.get_sorted_components().items()
for comp_name, comp_cpp_info in sorted_components:

# returns: ("list of cpp infos from required components in same package",
# "list of names from required components from other packages")
def _get_component_requires(component):
requires_external = [(req.split("::")[0], req.split("::")[1]) for req in
component.requires if "::" in req]
requires_internal = [dep.cpp_info.components.get(req) for req in
component.requires if "::" not in req]
return requires_internal, requires_external

# these are the transitive dependencies between components of the same package
transitive_internal = []
# these are the transitive dependencies to components from other packages
transitive_external = []

# return the internal cpp_infos and external components names
def _transitive_components(component):
requires_internal, requires_external = _get_component_requires(component)
transitive_internal.append(component)
transitive_internal.extend(requires_internal)
transitive_external.extend(requires_external)
for require in requires_internal:
_transitive_components(require)
czoido marked this conversation as resolved.
Show resolved Hide resolved
return transitive_internal, transitive_external
transitive_internal, transitive_external = _transitive_components(comp_cpp_info)

# remove duplicates
transitive_internal = list(OrderedDict.fromkeys(transitive_internal).keys())
transitive_external = list(OrderedDict.fromkeys(transitive_external).keys())

component_content = self.get_content_for_component(dep_name, comp_name,
transitive_internal,
transitive_external)
include_components_names.append((dep_name, comp_name))
result.update(component_content)
else:
public_deps = [(_format_name(d.ref.name),) * 2 for r, d in
dep.dependencies.direct_host.items() if r.visible]
required_components = dep.cpp_info.required_components if dep.cpp_info.required_components else public_deps
root_content = self.get_content_for_component(dep_name, dep_name, dep.cpp_info, required_components)
root_content = self.get_content_for_component(dep_name, dep_name, [dep.cpp_info],
required_components)
include_components_names.append((dep_name, dep_name))
result.update(root_content)

Expand Down
Expand Up @@ -8,9 +8,6 @@


@pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS")
@pytest.mark.tool_cmake
czoido marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.tool_xcodebuild
@pytest.mark.tool_xcodegen
def test_xcodedeps_components():
"""
tcp/1.0 is a lib without components
Expand Down
89 changes: 89 additions & 0 deletions conans/test/integration/toolchains/apple/test_xcodedeps.py
@@ -1,5 +1,6 @@
import os
import platform
import textwrap

import pytest

Expand Down Expand Up @@ -85,3 +86,91 @@ def test_generator_files():
assert '#include "conandeps.xcconfig"' in conan_config

check_contents(client, ["hello", "goodbye"], build_type, "x86_64", "macosx", "12.1")


@pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS")
def test_xcodedeps_aggregate_components():
client = TestClient()

conanfile_py = textwrap.dedent("""
from conan import ConanFile
class LibConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
def package_info(self):
self.cpp_info.includedirs = ["liba_include"]
""")

client.save({"conanfile.py": conanfile_py})

client.run("create . liba/1.0@")

r""""
1 a
/ \ /
2 3
\ /
4 5 6
| | /
\ / /
7
"""

conanfile_py = textwrap.dedent("""
from conan import ConanFile
class LibConan(ConanFile):
settings = "os", "compiler", "build_type", "arch"
requires = "liba/1.0"
def package_info(self):
self.cpp_info.components["libb_comp1"].includedirs = ["libb_comp1"]
self.cpp_info.components["libb_comp1"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp2"].includedirs = ["libb_comp2"]
self.cpp_info.components["libb_comp2"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp2"].requires = ["libb_comp1"]
self.cpp_info.components["libb_comp3"].includedirs = ["libb_comp3"]
self.cpp_info.components["libb_comp3"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp3"].requires = ["libb_comp1", "liba::liba"]
self.cpp_info.components["libb_comp4"].includedirs = ["libb_comp4"]
self.cpp_info.components["libb_comp4"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp4"].requires = ["libb_comp2", "libb_comp3"]
self.cpp_info.components["libb_comp5"].includedirs = ["libb_comp5"]
self.cpp_info.components["libb_comp5"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp6"].includedirs = ["libb_comp6"]
self.cpp_info.components["libb_comp6"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp7"].includedirs = ["libb_comp7"]
self.cpp_info.components["libb_comp7"].libdirs = ["mylibdir"]
self.cpp_info.components["libb_comp7"].requires = ["libb_comp4", "libb_comp5", "libb_comp6"]
""")

client.save({"conanfile.py": conanfile_py})

client.run("create . libb/1.0@")

client.run("install libb/1.0@ -g XcodeDeps")

lib_entry = client.load("conan_libb.xcconfig")

for index in range(1, 8):
assert f"conan_libb_libb_comp{index}.xcconfig" in lib_entry

component7_entry = client.load("conan_libb_libb_comp7.xcconfig")
assert '#include "conan_liba_liba.xcconfig"' in component7_entry

component7_vars = client.load("conan_libb_libb_comp7_release_x86_64.xcconfig")

# all of the transitive required components and the component itself are added
for index in range(1, 8):
assert f"libb_comp{index}" in component7_vars

assert "mylibdir" in component7_vars

component4_vars = client.load("conan_libb_libb_comp4_release_x86_64.xcconfig")

# all of the transitive required components and the component itself are added
for index in range(1, 5):
assert f"libb_comp{index}" in component4_vars

for index in range(5, 8):
assert f"libb_comp{index}" not in component4_vars

# folders are aggregated
assert "mylibdir" in component4_vars