From b38de545b58e85358560190cd5c5ad69b0973455 Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Mon, 1 Jul 2024 19:18:08 +0200 Subject: [PATCH 1/8] Add properties to MakeDeps generator If the receipt defines custom properties in the package_info() method then these properties should appear as make variables, too. The pattern of the make variable is CONAN_PROPERTY_{dependency_name}_{property_name} --- conan/tools/gnu/makedeps.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index c3d0017b988..27595c2f3f3 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -131,6 +131,14 @@ def _jinja_format_list_values() -> str: {%- endif -%} {%- endmacro %} + {%- macro define_multiple_variable_value(var, values) -%} + {%- if values is not none -%} + {% for property_name, value in values.items() %} + {{ var }}_{{ property_name }} = {{ value }} + {% endfor %} + {%- endif -%} + {%- endmacro %} + {%- macro define_variable_value(var, values) -%} {%- if values is not none -%} {%- if values|length > 0 -%} @@ -397,6 +405,7 @@ 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): @@ -420,6 +429,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(name): value for name, value in self._dep.cpp_info._properties.items()}, } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) From 408b04938114599a0f304b83b0a112af324fd7d2 Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Mon, 1 Jul 2024 22:38:15 +0200 Subject: [PATCH 2/8] Add property feature for components Furthermore handle case when _properties is None. --- conan/tools/gnu/makedeps.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index 27595c2f3f3..6d2cbdff815 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -340,6 +340,7 @@ 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): @@ -364,7 +365,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(name): value for name, value in self._dep.cpp_info.components[self._name]._properties.items()} if self._dep.cpp_info.components[self._name]._properties else None, } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) @@ -429,7 +431,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(name): value for name, value in self._dep.cpp_info._properties.items()}, + "properties": {_makefy(name): value for name, value in self._dep.cpp_info._properties.items()} if self._dep.cpp_info._properties else None, } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) From 3b11a76015f9ff057488b33d4189cce2274f04c4 Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Mon, 1 Jul 2024 22:56:58 +0200 Subject: [PATCH 3/8] Extracting property makefication --- conan/tools/gnu/makedeps.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index 6d2cbdff815..8b9c9a407d4 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -30,6 +30,7 @@ import textwrap from jinja2 import Template, StrictUndefined +from typing import Optional from conan.internal import check_duplicated_generator from conan.tools.files import save @@ -67,6 +68,15 @@ def _makefy(name: str) -> str: return re.sub(r'[^0-9A-Z_]', '_', name.upper()) +def _makefy_properties(properties: dict) -> Optional[dict]: + """ + Convert property dictionary keys to Make-variable-friendly syntax + :param properties: The property dictionary to be converted + :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 None + + def _conan_prefix_flag(variable: str) -> str: """ Return a global flag to be used as prefix to any value in the makefile @@ -366,7 +376,7 @@ def content(self) -> str: "name": _makefy(self._name), "cpp_info_dirs": self._dirs, "cpp_info_flags": self._flags, - "properties": {_makefy(name): value for name, value in self._dep.cpp_info.components[self._name]._properties.items()} if self._dep.cpp_info.components[self._name]._properties else None, + "properties": _makefy_properties(self._dep.cpp_info.components[self._name]._properties), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) @@ -431,7 +441,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(name): value for name, value in self._dep.cpp_info._properties.items()} if self._dep.cpp_info._properties else None, + "properties": _makefy_properties(self._dep.cpp_info._properties), } template = Template(_jinja_format_list_values() + self.template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined) From 6b30e4ba8b12d4daaba3877db49fb11be0ef3ce8 Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Thu, 4 Jul 2024 21:37:11 +0200 Subject: [PATCH 4/8] Add some tests --- test/integration/toolchains/gnu/test_makedeps.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/toolchains/gnu/test_makedeps.py b/test/integration/toolchains/gnu/test_makedeps.py index 948174280b1..17240f3e225 100644 --- a/test/integration/toolchains/gnu/test_makedeps.py +++ b/test/integration/toolchains/gnu/test_makedeps.py @@ -30,6 +30,7 @@ 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") """) client = TestClient() client.save({"conanfile.py": conanfile}) @@ -44,6 +45,7 @@ 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 lines = makefile_content.splitlines() for line_no, line in enumerate(lines): @@ -90,6 +92,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 +200,7 @@ 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") """) client.save({"conanfile.py": conanfile}, clean_first=True) @@ -236,6 +240,8 @@ 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 + def test_make_with_public_deps_and_component_requires_second(): """ From 3015fc80f60a7b1969bc1e9f562456f8dde69c2a Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Fri, 5 Jul 2024 18:56:32 +0200 Subject: [PATCH 5/8] Review finding - _makefy_properties() always returns a dict - Simplification of define_multiple_variable_value jinja2 macro --- conan/tools/gnu/makedeps.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index 8b9c9a407d4..4831cd4ac6c 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -68,13 +68,13 @@ def _makefy(name: str) -> str: return re.sub(r'[^0-9A-Z_]', '_', name.upper()) -def _makefy_properties(properties: dict) -> Optional[dict]: +def _makefy_properties(properties: Optional[dict]) -> dict: """ Convert property dictionary keys to Make-variable-friendly syntax - :param properties: The property dictionary to be converted + :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 None + return {_makefy(name): value for name, value in properties.items()} if properties else {} def _conan_prefix_flag(variable: str) -> str: @@ -142,11 +142,9 @@ def _jinja_format_list_values() -> str: {%- endmacro %} {%- macro define_multiple_variable_value(var, values) -%} - {%- if values is not none -%} {% for property_name, value in values.items() %} {{ var }}_{{ property_name }} = {{ value }} {% endfor %} - {%- endif -%} {%- endmacro %} {%- macro define_variable_value(var, values) -%} From 8486fce4d985b756c2b02728c5f00a995d302d59 Mon Sep 17 00:00:00 2001 From: vajdaz Date: Mon, 8 Jul 2024 22:21:14 +0200 Subject: [PATCH 6/8] Remove unnecessary extra space Co-authored-by: James --- conan/tools/gnu/makedeps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index 4831cd4ac6c..66b32e8aafa 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -74,7 +74,7 @@ def _makefy_properties(properties: Optional[dict]) -> dict: :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 {} + return {_makefy(name): value for name, value in properties.items()} if properties else {} def _conan_prefix_flag(variable: str) -> str: From 489717fcf2da060920cbdbc29bd5a65a5e10f2fa Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Tue, 9 Jul 2024 19:24:11 +0200 Subject: [PATCH 7/8] Skip properties with newlines It would break the makefile if we would create property values with newlines. Therefore we output a warning message and skip such properties. --- conan/tools/gnu/makedeps.py | 39 ++++++++++++++----- .../toolchains/gnu/test_makedeps.py | 6 +++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index 66b32e8aafa..bb8651c3cb2 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -76,6 +76,21 @@ def _makefy_properties(properties: Optional[dict]) -> dict: """ 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: """ @@ -351,7 +366,7 @@ class DepComponentContentGenerator: {{- 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 @@ -362,6 +377,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: """ @@ -374,7 +390,7 @@ def content(self) -> str: "name": _makefy(self._name), "cpp_info_dirs": self._dirs, "cpp_info_flags": self._flags, - "properties": _makefy_properties(self._dep.cpp_info.components[self._name]._properties), + "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) @@ -418,13 +434,14 @@ class DepContentGenerator: {{- 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: """ @@ -439,7 +456,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(self._dep.cpp_info._properties), + "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) @@ -451,7 +468,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 @@ -464,6 +481,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: """ @@ -520,7 +538,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 @@ -530,10 +548,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: @@ -605,11 +624,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 @@ -651,7 +670,7 @@ def generate(self) -> None: if require.build: continue - dep_gen = DepGenerator(dep, require) + dep_gen = DepGenerator(dep, require, self._conanfile.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 17240f3e225..31fd5d3a6fd 100644 --- a/test/integration/toolchains/gnu/test_makedeps.py +++ b/test/integration/toolchains/gnu/test_makedeps.py @@ -31,6 +31,7 @@ def package_info(self): 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}) @@ -46,6 +47,8 @@ def package_info(self): 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): @@ -201,6 +204,7 @@ 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) @@ -241,6 +245,8 @@ def package_info(self): 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(): From aeaa4030b4bde7ecefa854e74c5d5abef3c5480c Mon Sep 17 00:00:00 2001 From: Zoltan Vajda Date: Tue, 9 Jul 2024 21:27:02 +0200 Subject: [PATCH 8/8] Display scope of warning --- conan/tools/gnu/makedeps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conan/tools/gnu/makedeps.py b/conan/tools/gnu/makedeps.py index bb8651c3cb2..d02cc353404 100644 --- a/conan/tools/gnu/makedeps.py +++ b/conan/tools/gnu/makedeps.py @@ -32,6 +32,7 @@ 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 @@ -669,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, self._conanfile.output) + 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()