Skip to content

Commit

Permalink
Add properties to MakeDeps generator (#16613)
Browse files Browse the repository at this point in the history
* 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}

* Add property feature for components

Furthermore handle case when _properties is None.

* Extracting property makefication

* Add some tests

* Review finding

- _makefy_properties() always returns a dict
- Simplification of define_multiple_variable_value jinja2 macro

* Remove unnecessary extra space

Co-authored-by: James <memsharded@gmail.com>

* 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.

* Display scope of warning

---------

Co-authored-by: James <memsharded@gmail.com>
  • Loading branch information
vajdaz and memsharded authored Jul 9, 2024
1 parent dd28339 commit 59bcd38
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 10 deletions.
60 changes: 50 additions & 10 deletions conan/tools/gnu/makedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 -%}
Expand Down Expand Up @@ -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
Expand All @@ -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:
"""
Expand All @@ -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)
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
12 changes: 12 additions & 0 deletions test/integration/toolchains/gnu/test_makedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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):
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
"""
Expand Down

0 comments on commit 59bcd38

Please sign in to comment.