diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index c3d0017b988..d02cc353404 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -30,7 +30,9 @@ import textwrap from jinja2 import Template, StrictUndefined +from typing import Optional +from conan.api.output import ConanOutput from conan.internal import check_duplicated_generator from conan.tools.files import save @@ -67,6 +69,30 @@ def _makefy(name: str) -> str: return re.sub(r'[^0-9A-Z_]', '_', name.upper()) +def _makefy_properties(properties: Optional[dict]) -> dict: + """ + Convert property dictionary keys to Make-variable-friendly syntax + :param properties: The property dictionary to be converted (None is also accepted) + :return: Modified property dictionary with keys not including bad characters that are not parsed correctly + """ + return {_makefy(name): value for name, value in properties.items()} if properties else {} + +def _check_property_value(name, value, output): + if "\n" in value: + output.warning(f"Skipping propery '{name}' because it contains newline") + return False + else: + return True + +def _filter_properties(properties: Optional[dict], output) -> dict: + """ + Filter out properties whose values contain newlines, because they would break the generated makefile + :param properties: A property dictionary (None is also accepted) + :return: A property dictionary without the properties containing newlines + """ + return {name: value for name, value in properties.items() if _check_property_value(name, value, output)} if properties else {} + + def _conan_prefix_flag(variable: str) -> str: """ Return a global flag to be used as prefix to any value in the makefile @@ -131,6 +157,12 @@ def _jinja_format_list_values() -> str: {%- endif -%} {%- endmacro %} + {%- macro define_multiple_variable_value(var, values) -%} + {% for property_name, value in values.items() %} + {{ var }}_{{ property_name }} = {{ value }} + {% endfor %} + {%- endmacro %} + {%- macro define_variable_value(var, values) -%} {%- if values is not none -%} {%- if values|length > 0 -%} @@ -332,9 +364,10 @@ class DepComponentContentGenerator: {{- define_variable_value_safe("CONAN_FRAMEWORKS_{}_{}".format(dep_name, name), cpp_info_flags, 'frameworks') -}} {{- define_variable_value_safe("CONAN_REQUIRES_{}_{}".format(dep_name, name), cpp_info_flags, 'requires') -}} {{- define_variable_value_safe("CONAN_SYSTEM_LIBS_{}_{}".format(dep_name, name), cpp_info_flags, 'system_libs') -}} + {{- define_multiple_variable_value("CONAN_PROPERTY_{}_{}".format(dep_name, name), properties) -}} """) - def __init__(self, dependency, component_name: str, dirs: dict, flags: dict): + def __init__(self, dependency, component_name: str, dirs: dict, flags: dict, output): """ :param dependency: The dependency object that owns the component :param component_name: component raw name e.g. poco::poco_json @@ -345,6 +378,7 @@ def __init__(self, dependency, component_name: str, dirs: dict, flags: dict): self._name = component_name self._dirs = dirs or {} self._flags = flags or {} + self._output = output def content(self) -> str: """ @@ -356,7 +390,8 @@ def content(self) -> str: "dep_name": _makefy(self._dep.ref.name), "name": _makefy(self._name), "cpp_info_dirs": self._dirs, - "cpp_info_flags": self._flags + "cpp_info_flags": self._flags, + "properties": _makefy_properties(_filter_properties(self._dep.cpp_info.components[self._name]._properties, self._output)), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) @@ -397,15 +432,17 @@ class DepContentGenerator: {{- define_variable_value_safe("CONAN_REQUIRES_{}".format(name), cpp_info_flags, 'requires') -}} {{- define_variable_value_safe("CONAN_SYSTEM_LIBS_{}".format(name), cpp_info_flags, 'system_libs') -}} {{- define_variable_value("CONAN_COMPONENTS_{}".format(name), components) -}} + {{- define_multiple_variable_value("CONAN_PROPERTY_{}".format(name), properties) -}} """) - def __init__(self, dependency, require, root: str, sysroot, dirs: dict, flags: dict): + def __init__(self, dependency, require, root: str, sysroot, dirs: dict, flags: dict, output): self._dep = dependency self._req = require self._root = root self._sysroot = sysroot self._dirs = dirs or {} self._flags = flags or {} + self._output = output def content(self) -> str: """ @@ -420,6 +457,7 @@ def content(self) -> str: "components": list(self._dep.cpp_info.get_sorted_components().keys()), "cpp_info_dirs": self._dirs, "cpp_info_flags": self._flags, + "properties": _makefy_properties(_filter_properties(self._dep.cpp_info._properties, self._output)), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) @@ -431,7 +469,7 @@ class DepComponentGenerator: Generates Makefile content for a dependency component """ - def __init__(self, dependency, makeinfo: MakeInfo, component_name: str, component, root: str): + def __init__(self, dependency, makeinfo: MakeInfo, component_name: str, component, root: str, output): """ :param dependency: The dependency object that owns the component :param makeinfo: Makeinfo to store component variables @@ -444,6 +482,7 @@ def __init__(self, dependency, makeinfo: MakeInfo, component_name: str, componen self._comp = component self._root = root self._makeinfo = makeinfo + self._output = output def _get_component_dirs(self) -> dict: """ @@ -500,7 +539,7 @@ def generate(self) -> str: """ dirs = self._get_component_dirs() flags = self._get_component_flags() - comp_content_gen = DepComponentContentGenerator(self._dep, self._name, dirs, flags) + comp_content_gen = DepComponentContentGenerator(self._dep, self._name, dirs, flags, self._output) comp_content = comp_content_gen.content() return comp_content @@ -510,10 +549,11 @@ class DepGenerator: Process a dependency cpp_info variables and generate its Makefile content """ - def __init__(self, dependency, require): + def __init__(self, dependency, require, output): self._dep = dependency self._req = require self._info = MakeInfo(self._dep.ref.name, [], []) + self._output = output @property def makeinfo(self) -> MakeInfo: @@ -585,11 +625,11 @@ def generate(self) -> str: sysroot = self._get_sysroot(root) dirs = self._get_dependency_dirs(root, self._dep) flags = self._get_dependency_flags(self._dep) - dep_content_gen = DepContentGenerator(self._dep, self._req, root, sysroot, dirs, flags) + dep_content_gen = DepContentGenerator(self._dep, self._req, root, sysroot, dirs, flags, self._output) content = dep_content_gen.content() for comp_name, comp in self._dep.cpp_info.get_sorted_components().items(): - component_gen = DepComponentGenerator(self._dep, self._info, comp_name, comp, root) + component_gen = DepComponentGenerator(self._dep, self._info, comp_name, comp, root, self._output) content += component_gen.generate() return content @@ -630,8 +670,8 @@ def generate(self) -> None: # Require is not used at the moment, but its information could be used, and will be used in Conan 2.0 if require.build: continue - - dep_gen = DepGenerator(dep, require) + output = ConanOutput(scope=f"{self._conanfile} MakeDeps: {dep}:") + dep_gen = DepGenerator(dep, require, output) make_infos.append(dep_gen.makeinfo) deps_buffer += dep_gen.generate() diff --git a/test/integration/toolchains/gnu/test_makedeps.py b/test/integration/toolchains/gnu/test_makedeps.py index 948174280b1..31fd5d3a6fd 100644 --- a/test/integration/toolchains/gnu/test_makedeps.py +++ b/test/integration/toolchains/gnu/test_makedeps.py @@ -30,6 +30,8 @@ def package_info(self): lib_dir2 = os.path.join(self.package_folder, "lib2") self.cpp_info.includedirs = [include_dir] self.cpp_info.libdirs = [lib_dir, lib_dir2] + self.cpp_info.set_property("my_prop", "my prop value") + self.cpp_info.set_property("my_prop_with_newline", "my\\nprop") """) client = TestClient() client.save({"conanfile.py": conanfile}) @@ -44,6 +46,9 @@ def package_info(self): assert f'CONAN_LIB_DIRS_MYLIB = \\\n\t$(CONAN_LIB_DIR_FLAG){prefix}/my_absoulte_path/fake/mylib/lib \\\n\t$(CONAN_LIB_DIR_FLAG)$(CONAN_ROOT_MYLIB)/lib2' in makefile_content assert f'CONAN_INCLUDE_DIRS_MYLIB = $(CONAN_INCLUDE_DIR_FLAG){prefix}/my_absoulte_path/fake/mylib/include' in makefile_content assert 'CONAN_BIN_DIRS_MYLIB = $(CONAN_BIN_DIR_FLAG)$(CONAN_ROOT_MYLIB)/bin' in makefile_content + assert 'CONAN_PROPERTY_MYLIB_MY_PROP = my prop value' in makefile_content + assert 'CONAN_PROPERTY_MYLIB_MY_PROP_WITH_NEWLINE' not in makefile_content + assert "WARN: Skipping propery 'my_prop_with_newline' because it contains newline" in client.stderr lines = makefile_content.splitlines() for line_no, line in enumerate(lines): @@ -90,6 +95,7 @@ def package_info(self): assert 'CONAN_BIN_DIRS' not in makefile_content assert 'CONAN_LIBS' not in makefile_content assert 'CONAN_FRAMEWORK_DIRS' not in makefile_content + assert 'CONAN_PROPERTY' not in makefile_content def test_libs_and_system_libs(): @@ -197,6 +203,8 @@ class TestMakeDepsConan(ConanFile): def package_info(self): self.cpp_info.components["mycomponent"].requires.append("lib::cmp1") self.cpp_info.components["myfirstcomp"].requires.append("mycomponent") + self.cpp_info.components["myfirstcomp"].set_property("my_prop", "my prop value") + self.cpp_info.components["myfirstcomp"].set_property("my_prop_with_newline", "my\\nprop") """) client.save({"conanfile.py": conanfile}, clean_first=True) @@ -236,6 +244,10 @@ def package_info(self): assert 'CONAN_REQUIRES = $(CONAN_REQUIRES_SECOND)\n' in makefile_content assert 'CONAN_LIBS = $(CONAN_LIBS_LIB)\n' in makefile_content + assert 'CONAN_PROPERTY_SECOND_MYFIRSTCOMP_MY_PROP = my prop value\n' in makefile_content + assert 'CONAN_PROPERTY_SECOND_MYFIRSTCOMP_MY_PROP_WITH_NEWLINE' not in makefile_content + assert "WARN: Skipping propery 'my_prop_with_newline' because it contains newline" in client2.stderr + def test_make_with_public_deps_and_component_requires_second(): """