Repository: conan-io/conan

Issue:
[feature] Add properties to MakeDeps generator
### What is your suggestion?

I would like to access custom properties from GNU Make files generated by the MakeDeps generator. If the receipt defines custom properties in the `package_info()` method then those properties should appear as make variables, too. The pattern of the make variable would be
```
CONAN_PROPERTY_{dependency_name}_{property_name}
```
where both, `dependency_name` and `property_name` would be uppercase. Dots in `property_name` would be replaced by underscores.

Here is my first take: https://github.com/conan-io/conan/commit/19daca6aaebb3a4d4242e77686d1c2105ef04216

I am aware that in this implementation has the assumption that custom properties are always strings. Is there such a constraint. or can properties contain data of any type?

My questions:
- Would such kind of a merge request have any chance to be merged? (Of course with docs and tests etc.) 
- Any ideas how to cope with non-string property values if they are allowed?
- Where should such a feature be documented?
- What kind of tests would make sense here?

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide


Discussion:
On second thought I would drop the replacement of dots with underscores. Dots are valid charactes in GNU Make variable names.
Hi @vajdaz 

Thanks for your suggestion

What would be exactly the use case? What kind of information are you propagating from dependencies to the consumers via properties?

The ``properties`` are intended to be mapped to somehow "native" things in the consumer build system. When a property like ``cmake_target_name`` is defined is because the CMake build system understand that thing.

I think this could be a valid request and probably can be moved forward, as it would be very low risk

> On second thought I would drop the replacement of dots with underscores. Dots are valid charactes in GNU Make variable names.

If there are other Make systems that this generator could be valid beyond GNU Make, then it might be worth using underscores.
Hi @memsharded ,

I have packages that contain more than one library files which have circular dependencies (let's imagine `liba.a` and `libb.a`). When you link such libraries, you must put them between appropriate linker options that make the linker iterate over them several times when the symbols are resolved. So instead of having the linking options
```
-L/my/lib/path -la -lb
```
you will have to use following linking options
```
-L/my/lib/path -Wl,--start-group -la -lb -Wl,--end-group
```
I signal this situation by means of a custom property. Currently I have a custom makefile generator that creates a makefile similar to what MakeDeps creates but additionally it creates variables for the custom properties. My build system integration receives the info about circular dependencies via this mechanism and can prepend/append the mentioned linker options.

In my current implementation I have a property for the linker options that should be prepended and appended. So in the receipt I have
```
    def package_info(self):
        # has circular dependencies
        self.cpp_info.set_property("ldflags_prepend", "-Wl,--start-group")
        self.cpp_info.set_property("ldflags_append", "-Wl,--end-group")
```
My generator creates the make variables `CONAN_PROPERTY_LDFLAGS_PREPEND_MYLIBNAME` and `CONAN_PROPERTY_LDFLAGS_APPEND_MYLIBNAME`. In the build system integration I use these variables to create the linker command (together with the well known other make variables, like `CONAN_LIB_DIRS_`, `CONAN_LIBS_`, etc.).

I am also aware that this is not very portable. I could abstract the linker options by having an abstract property
```
    def package_info(self):
        self.cpp_info.set_property("has_circular_dependencies", "True")
```
and let the build system decide what to do with this information.

But this details do not matter. The point is, I want to access custom property values set in the receipt in my GNU Make based build system to control linking behavior in special cases.

> I am also aware that this is not very portable. I could abstract the linker options by having an abstract property

Indeed, this is something that we have wanted to improve for a long time, improving the ``cpp_info`` model or adding new "built-in" properties that other build systems as CMake coudl also use.

If you have some other cases besides this one, then it might be possible to move this feature forward now, otherwise we are trying to improve and address this model in https://github.com/conan-io/conan/pull/15866 (very early stage) so maybe it would be better to wait until the definition of a built-in way for link groups, if you have your own generator at the moment and is good by now, then it wouldn't be very painful?
My comment on the lack of portabiliy of my example implementation for my current use case refers only to my the implementation itslef. The feautre (being able to access properties by make variables) would be a generic feature in my opinion.

I don't really get the connection between #15866 and this feature. I wouldn't see such a property (like "this package has cicrular dependencies") as a built-in property, because t is too platform specific. Solaris linker have no problems linking static libs with circular dependencies. How Microsoft linker behave, I don't know.

Currently I  have no other use cases.

About the painfulness of not having this feature. MakeDeps is much better than my generator. I can also copy MakeDeps, rename it and patch it. Then I have a nice generator with my feature. But you know how it is. Conan will change, then my "fork" will break, I will have to patch it again... Everything would be so simple if this nice little feature would flow back upstream, and I could use the built-in MakeDeps without having an own patched copy. But I also can understand your wariness. New features, new problems... You decide. I would also invest some time to complete this feature if you would give me some directions (for example, I discovered `_makefiy()` and adjusted the code to use it to transform the property name to a make variable name segment).
> I don't really get the connection between https://github.com/conan-io/conan/pull/15866 and this feature. I wouldn't see such a property (like "this package has cicrular dependencies") as a built-in property, because t is too platform specific. Solaris linker have no problems linking static libs with circular dependencies. How Microsoft linker behave, I don't know.

The fact that libraries have circular dependencies isn't really platform specific, the libraries have that "topology". It is the linkers of different platforms that might have issues or not with that, and might need extra inputs or not. But defining that some libraries form a cycle can be part of the ``cpp_info`` model, as it is intrinsic to those libraries.


Ok, I think it would be no risk to have MakeDeps translate properties to CONAN_PROPERTY_ variables, if you want to propose a PR, we will try to help to move it forward.



Patch:
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()
 


Test patch:
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():
     """