diff --git a/conans/client/generators/pkg_config.py b/conans/client/generators/pkg_config.py index 079d354d09e..fbd1726a32d 100644 --- a/conans/client/generators/pkg_config.py +++ b/conans/client/generators/pkg_config.py @@ -81,6 +81,10 @@ def _pc_file_content(self, name, cpp_info, requires_gennames): includedir_vars = varnames lines.extend(dir_lines) + pkg_config_custom_content = cpp_info.get_property("pkg_config_custom_content", self.name) + if pkg_config_custom_content: + lines.append(pkg_config_custom_content) + lines.append("") lines.append("Name: %s" % name) description = cpp_info.description or "Conan package: %s" % name diff --git a/conans/model/build_info.py b/conans/model/build_info.py index 88fc0cd3e90..8573223d09e 100644 --- a/conans/model/build_info.py +++ b/conans/model/build_info.py @@ -104,6 +104,7 @@ class _CppInfo(object): def __init__(self): self._name = None + self._generator_properties = {} self.names = {} self.system_libs = [] # Ordered list of system libraries self.includedirs = [] # Ordered list of include paths @@ -127,6 +128,7 @@ def __init__(self): self.sysroot = "" self.requires = [] self._build_modules_paths = None + self._build_modules = None self._include_paths = None self._lib_paths = None self._bin_paths = None @@ -154,7 +156,9 @@ def build_modules_paths(self): conan_v2_error("Use 'self.cpp_info.build_modules[\"\"] = " "{the_list}' instead".format(the_list=self.build_modules)) self.build_modules = BuildModulesDict.from_list(self.build_modules) - tmp = dict_to_abs_paths(BuildModulesDict(self.build_modules), self.rootpath) + # Invalidate necessary, get_build_modules used raise_incorrect_components_definition + self._build_modules = None + tmp = dict_to_abs_paths(BuildModulesDict(self.get_build_modules()), self.rootpath) self._build_modules_paths = tmp return self._build_modules_paths @@ -209,15 +213,58 @@ def name(self): def name(self, value): self._name = value + # TODO: Deprecate for 2.0. Only cmake and pkg_config generators should access this. + # Use get_property for 2.0 def get_name(self, generator): - return self.names.get(generator, self._name) - + property_name = None + if "cmake" in generator: + property_name = "cmake_target_name" + elif "pkg_config" in generator: + property_name = "pkg_config_name" + return self.get_property(property_name, generator) or self.names.get(generator, self._name) + + # TODO: Deprecate for 2.0. Only cmake generators should access this. Use get_property for 2.0 def get_filename(self, generator): - result = self.filenames.get(generator) + result = self.get_property("cmake_file_name", generator) or self.filenames.get(generator) if result: return result return self.get_name(generator) + # TODO: Deprecate for 2.0. Use get_property for 2.0 + def get_build_modules(self): + if self._build_modules is None: # Not cached yet + try: + default_build_modules_value = self._generator_properties[None]["cmake_build_modules"] + except KeyError: + ret_dict = {} + else: + ret_dict = {"cmake_find_package": default_build_modules_value, + "cmake_find_package_multi": default_build_modules_value, + "cmake": default_build_modules_value, + "cmake_multi": default_build_modules_value} + + for generator, values in self._generator_properties.items(): + if generator: + v = values.get("cmake_build_modules") + if v: + ret_dict[generator] = v + self._build_modules = ret_dict if ret_dict else self.build_modules + return self._build_modules + + def set_property(self, property_name, value, generator=None): + self._generator_properties.setdefault(generator, {})[property_name] = value + + def get_property(self, property_name, generator=None): + if generator: + try: + return self._generator_properties[generator][property_name] + except KeyError: + pass + try: + return self._generator_properties[None][property_name] + except KeyError: + pass + # Compatibility for 'cppflags' (old style property to allow decoration) def get_cppflags(self): conan_v2_error("'cpp_info.cppflags' is deprecated, use 'cxxflags' instead") @@ -354,7 +401,7 @@ def _raise_incorrect_components_definition(self, package_name, package_requires) self.cxxflags or self.sharedlinkflags or self.exelinkflags or - self.build_modules or + self.get_build_modules() or self.requires): raise ConanException("self.cpp_info.components cannot be used with self.cpp_info " "global values at the same time") @@ -437,7 +484,6 @@ def merge_lists(seq1, seq2): self.cflags = merge_lists(dep_cpp_info.cflags, self.cflags) self.sharedlinkflags = merge_lists(dep_cpp_info.sharedlinkflags, self.sharedlinkflags) self.exelinkflags = merge_lists(dep_cpp_info.exelinkflags, self.exelinkflags) - if not self.sysroot: self.sysroot = dep_cpp_info.sysroot diff --git a/conans/test/functional/generators/pkg_config_test.py b/conans/test/functional/generators/pkg_config_test.py index f24fe8167d5..c863f52cb2a 100644 --- a/conans/test/functional/generators/pkg_config_test.py +++ b/conans/test/functional/generators/pkg_config_test.py @@ -202,3 +202,58 @@ def test_empty_include(self): pc = client.load("pkg.pc") self.assertNotIn("libdir=${prefix}/lib", pc) self.assertNotIn("includedir=${prefix}/include", pc) + + def test_custom_content(self): + # https://github.com/conan-io/conan/issues/7661 + conanfile = textwrap.dedent(""" + from conans import ConanFile + from conans.tools import save + import os + import textwrap + + class PkgConfigConan(ConanFile): + def package(self): + save(os.path.join(self.package_folder, "include" ,"file"), "") + save(os.path.join(self.package_folder, "lib" ,"file"), "") + + def package_info(self): + custom_content = textwrap.dedent(\""" + datadir=${prefix}/share + schemasdir=${datadir}/mylib/schemas + bindir=${prefix}/bin + \""") + self.cpp_info.set_property("pkg_config_custom_content", custom_content) + self.cpp_info.includedirs = ["include"] + self.cpp_info.libdirs = ["lib"] + """) + client = TestClient() + client.save({"conanfile.py": conanfile}) + client.run("create . pkg/0.1@") + client.run("install pkg/0.1@ -g pkg_config") + + pc_content = client.load("pkg.pc") + self.assertIn("libdir=${prefix}/lib", pc_content) + self.assertIn("datadir=${prefix}/share", pc_content) + self.assertIn("schemasdir=${datadir}/mylib/schemas", pc_content) + self.assertIn("bindir=${prefix}/bin", pc_content) + self.assertIn("Name: pkg", pc_content) + + def test_custom_content_components(self): + conanfile = textwrap.dedent(""" + from conans import ConanFile + from conans.tools import save + import os + import textwrap + + class PkgConfigConan(ConanFile): + def package_info(self): + self.cpp_info.components["mycomponent"].set_property("pkg_config_custom_content", + "componentdir=${prefix}/mydir") + """) + client = TestClient() + client.save({"conanfile.py": conanfile}) + client.run("create . pkg/0.1@") + client.run("install pkg/0.1@ -g pkg_config") + + pc_content = client.load("mycomponent.pc") + self.assertIn("componentdir=${prefix}/mydir", pc_content) diff --git a/conans/test/integration/generators/cpp_info_set_generator_properties_test.py b/conans/test/integration/generators/cpp_info_set_generator_properties_test.py new file mode 100644 index 00000000000..55577696fd8 --- /dev/null +++ b/conans/test/integration/generators/cpp_info_set_generator_properties_test.py @@ -0,0 +1,262 @@ +import os +import textwrap + +import pytest + +from conans.test.assets.genconanfile import GenConanfile +from conans.test.utils.tools import TestClient + + +@pytest.fixture(scope="module") +def setup_client(): + client = TestClient() + custom_generator = textwrap.dedent(""" + from conans.model import Generator + from conans import ConanFile + from conans.model.conan_generator import GeneratorComponentsMixin + import os + + + class custom_generator(GeneratorComponentsMixin, Generator): + name = "custom_generator" + @property + def filename(self): + return "my-generator.txt" + + def _get_components_custom_names(self, cpp_info): + ret = [] + for comp_name, comp in self.sorted_components(cpp_info).items(): + comp_genname = comp.get_property("custom_name", generator=self.name) + ret.append("{}:{}".format(comp.name, comp_genname)) + return ret + + @property + def content(self): + info = [] + for pkg_name, cpp_info in self.deps_build_info.dependencies: + info.append("{}:{}".format(pkg_name, cpp_info.get_property("custom_name", self.name))) + info.extend(self._get_components_custom_names(cpp_info)) + return os.linesep.join(info) + """) + client.save({"custom_generator.py": custom_generator}) + client.run("config install custom_generator.py -tf generators") + + build_module = textwrap.dedent(""" + message("I am a build module") + """) + + another_build_module = textwrap.dedent(""" + message("I am another build module") + """) + + client.save({"consumer.py": GenConanfile("consumer", "1.0").with_requires("mypkg/1.0"). + with_generator("custom_generator").with_generator("cmake_find_package"). + with_generator("cmake_find_package_multi").with_generator("pkg_config"). + with_setting("build_type"), + "mypkg_bm.cmake": build_module, "mypkg_anootherbm.cmake": another_build_module}) + return client + + +def get_files_contents(client, filenames): + return [client.load(f) for f in filenames] + + +def test_same_results_components(setup_client): + client = setup_client + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile, CMake, tools + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "MyFileName") + self.cpp_info.components["mycomponent"].libs = ["mycomponent-lib"] + self.cpp_info.components["mycomponent"].set_property("cmake_target_name", "mycomponent-name") + self.cpp_info.components["mycomponent"].set_property("cmake_build_modules", [os.path.join("lib", "mypkg_bm.cmake")]) + self.cpp_info.components["mycomponent"].set_property("custom_name", "mycomponent-name", "custom_generator") + """) + + client.save({"mypkg.py": mypkg}) + client.run("export mypkg.py") + client.run("install consumer.py --build missing -s build_type=Release") + + my_generator = client.load("my-generator.txt") + assert "mycomponent:mycomponent-name" in my_generator + + files_to_compare = ["FindMyFileName.cmake", "MyFileNameConfig.cmake", "MyFileNameTargets.cmake", + "MyFileNameTarget-release.cmake", "MyFileNameConfigVersion.cmake", "mypkg.pc", + "mycomponent.pc"] + new_approach_contents = get_files_contents(client, files_to_compare) + + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + def package_info(self): + self.cpp_info.components["mycomponent"].libs = ["mycomponent-lib"] + self.cpp_info.filenames["cmake_find_package"] = "MyFileName" + self.cpp_info.filenames["cmake_find_package_multi"] = "MyFileName" + self.cpp_info.components["mycomponent"].names["cmake_find_package"] = "mycomponent-name" + self.cpp_info.components["mycomponent"].names["cmake_find_package_multi"] = "mycomponent-name" + self.cpp_info.components["mycomponent"].build_modules.append(os.path.join("lib", "mypkg_bm.cmake")) + """) + client.save({"mypkg.py": mypkg}) + client.run("export mypkg.py") + client.run("install consumer.py -s build_type=Release") + + old_approach_contents = get_files_contents(client, files_to_compare) + + assert new_approach_contents == old_approach_contents + + +def test_same_results_without_components(setup_client): + client = setup_client + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "MyFileName") + self.cpp_info.set_property("cmake_target_name", "mypkg-name") + self.cpp_info.set_property("cmake_build_modules",[os.path.join("lib", + "mypkg_bm.cmake")]) + self.cpp_info.set_property("custom_name", "mypkg-name", "custom_generator") + """) + + client.save({"mypkg.py": mypkg}) + client.run("export mypkg.py") + + client.run("install consumer.py --build missing -s build_type=Release") + + with open(os.path.join(client.current_folder, "my-generator.txt")) as custom_gen_file: + assert "mypkg:mypkg-name" in custom_gen_file.read() + + files_to_compare = ["FindMyFileName.cmake", "MyFileNameConfig.cmake", "MyFileNameTargets.cmake", + "MyFileNameTarget-release.cmake", "MyFileNameConfigVersion.cmake", "mypkg.pc"] + new_approach_contents = get_files_contents(client, files_to_compare) + + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + def package_info(self): + self.cpp_info.filenames["cmake_find_package"] = "MyFileName" + self.cpp_info.filenames["cmake_find_package_multi"] = "MyFileName" + self.cpp_info.names["cmake_find_package"] = "mypkg-name" + self.cpp_info.names["cmake_find_package_multi"] = "mypkg-name" + self.cpp_info.names["custom_generator"] = "mypkg-name" + self.cpp_info.build_modules.append(os.path.join("lib", "mypkg_bm.cmake")) + """) + client.save({"mypkg.py": mypkg}) + client.run("create mypkg.py") + client.run("install consumer.py -s build_type=Release") + + old_approach_contents = get_files_contents(client, files_to_compare) + + assert new_approach_contents == old_approach_contents + + +def test_same_results_specific_generators(setup_client): + client = setup_client + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake", "mypkg_anootherbm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + self.copy("mypkg_anootherbm.cmake", dst="lib") + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "MyFileName", "cmake_find_package") + self.cpp_info.set_property("cmake_file_name", "MyFileNameMulti", "cmake_find_package_multi") + self.cpp_info.set_property("cmake_target_name", "mypkg-name", "cmake_find_package") + self.cpp_info.set_property("cmake_target_name", "mypkg-name-multi", "cmake_find_package_multi") + self.cpp_info.set_property("cmake_build_modules",[os.path.join("lib", + "mypkg_bm.cmake")], "cmake_find_package") + self.cpp_info.set_property("cmake_build_modules",[os.path.join("lib", + "mypkg_anootherbm.cmake")], "cmake_find_package_multi") + """) + + client.save({"mypkg.py": mypkg}) + client.run("export mypkg.py") + + client.run("install consumer.py --build missing -s build_type=Release") + + files_to_compare = ["FindMyFileName.cmake", "MyFileNameMultiConfig.cmake", "MyFileNameMultiTargets.cmake", + "MyFileNameMultiTarget-release.cmake", "MyFileNameMultiConfigVersion.cmake"] + new_approach_contents = get_files_contents(client, files_to_compare) + + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + exports_sources = ["mypkg_bm.cmake", "mypkg_anootherbm.cmake"] + def package(self): + self.copy("mypkg_bm.cmake", dst="lib") + self.copy("mypkg_anootherbm.cmake", dst="lib") + def package_info(self): + self.cpp_info.filenames["cmake_find_package"] = "MyFileName" + self.cpp_info.filenames["cmake_find_package_multi"] = "MyFileNameMulti" + self.cpp_info.names["cmake_find_package"] = "mypkg-name" + self.cpp_info.names["cmake_find_package_multi"] = "mypkg-name-multi" + self.cpp_info.build_modules["cmake_find_package"].append(os.path.join("lib", "mypkg_bm.cmake")) + self.cpp_info.build_modules["cmake_find_package_multi"].append(os.path.join("lib", "mypkg_anootherbm.cmake")) + """) + client.save({"mypkg.py": mypkg}) + client.run("create mypkg.py") + client.run("install consumer.py -s build_type=Release") + + old_approach_contents = get_files_contents(client, files_to_compare) + + assert new_approach_contents == old_approach_contents + + +def test_pkg_config_names(setup_client): + client = setup_client + mypkg = textwrap.dedent(""" + import os + from conans import ConanFile + class MyPkg(ConanFile): + settings = "build_type" + name = "mypkg" + version = "1.0" + def package_info(self): + self.cpp_info.components["mycomponent"].libs = ["mycomponent-lib"] + self.cpp_info.components["mycomponent"].set_property("pkg_config_name", "mypkg-config-name") + """) + + client.save({"mypkg.py": mypkg}) + client.run("export mypkg.py") + client.run("install consumer.py --build missing") + + with open(os.path.join(client.current_folder, "mypkg-config-name.pc")) as gen_file: + assert "mypkg-config-name" in gen_file.read() diff --git a/conans/test/unittests/model/build_info/generic_properties_test.py b/conans/test/unittests/model/build_info/generic_properties_test.py new file mode 100644 index 00000000000..30ac0f3d5b9 --- /dev/null +++ b/conans/test/unittests/model/build_info/generic_properties_test.py @@ -0,0 +1,20 @@ +from conans.model.build_info import _CppInfo + + +def test_set_get_properties(): + cpp_info = _CppInfo() + + assert not cpp_info.get_property("my_property") + assert not cpp_info.get_property("my_property", "some_generator") + + cpp_info.set_property("my_property", "default_value") + assert cpp_info.get_property("my_property") == "default_value" + # can you do a get_property for just a family without generator? + assert cpp_info.get_property("my_property", generator="cmake_multi") == "default_value" + assert cpp_info.get_property("my_property", generator="pkg_config") == "default_value" + + cpp_info.set_property("my_property", "pkg_config_value", generator="pkg_config") + assert cpp_info.get_property("my_property", generator="pkg_config") == "pkg_config_value" + cpp_info.set_property("other_property", "other_pkg_config_value", generator="pkg_config") + assert not cpp_info.get_property("other_property") + assert cpp_info.get_property("other_property", generator="pkg_config") == "other_pkg_config_value"