From ef7f228931d55156e438187eabeb0c573efb26f4 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Wed, 29 Jun 2022 08:00:16 +0200 Subject: [PATCH] Aggregate transitive components information in the same package for XcodeDeps (#11507) * wip * wip * fix tests * update test * add test * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * minor changes * fix tests * review --- conan/tools/apple/xcodedeps.py | 88 ++++++++++++------ .../apple/test_xcodedeps_components.py | 2 - .../toolchains/apple/test_xcodedeps.py | 89 +++++++++++++++++++ 3 files changed, 151 insertions(+), 28 deletions(-) diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py index 24c3fc16adf..e972423d243 100644 --- a/conan/tools/apple/xcodedeps.py +++ b/conan/tools/apple/xcodedeps.py @@ -1,5 +1,6 @@ import os import textwrap +from collections import OrderedDict from jinja2 import Template @@ -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}} """) @@ -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) } @@ -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 @@ -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) + + _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) diff --git a/conans/test/functional/toolchains/apple/test_xcodedeps_components.py b/conans/test/functional/toolchains/apple/test_xcodedeps_components.py index 4ccd40f286d..d4898bff134 100644 --- a/conans/test/functional/toolchains/apple/test_xcodedeps_components.py +++ b/conans/test/functional/toolchains/apple/test_xcodedeps_components.py @@ -9,8 +9,6 @@ @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS") @pytest.mark.tool_cmake -@pytest.mark.tool_xcodebuild -@pytest.mark.tool_xcodegen def test_xcodedeps_components(): """ tcp/1.0 is a lib without components diff --git a/conans/test/integration/toolchains/apple/test_xcodedeps.py b/conans/test/integration/toolchains/apple/test_xcodedeps.py index 1a5ddb5f430..c60ea523ad8 100644 --- a/conans/test/integration/toolchains/apple/test_xcodedeps.py +++ b/conans/test/integration/toolchains/apple/test_xcodedeps.py @@ -1,5 +1,6 @@ import os import platform +import textwrap import pytest @@ -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